|
|
@@ -28,11 +28,15 @@
|
|
|
#include <Urho3D/Graphics/Graphics.h>
|
|
|
#include <Urho3D/Graphics/Material.h>
|
|
|
#include <Urho3D/Graphics/Octree.h>
|
|
|
-#include <Urho3D/Graphics/Renderer.h>
|
|
|
+#include <Urho3D/Graphics/DebugRenderer.h>
|
|
|
#include <Urho3D/Graphics/RibbonTrail.h>
|
|
|
#include <Urho3D/IK/IKEffector.h>
|
|
|
#include <Urho3D/IK/IKSolver.h>
|
|
|
#include <Urho3D/Input/Input.h>
|
|
|
+#include <Urho3D/Math/Matrix2.h>
|
|
|
+#include <Urho3D/Physics/PhysicsWorld.h>
|
|
|
+#include <Urho3D/Physics/CollisionShape.h>
|
|
|
+#include <Urho3D/Physics/RigidBody.h>
|
|
|
#include <Urho3D/Resource/ResourceCache.h>
|
|
|
#include <Urho3D/Scene/Scene.h>
|
|
|
#include <Urho3D/UI/Font.h>
|
|
|
@@ -47,8 +51,7 @@
|
|
|
URHO3D_DEFINE_APPLICATION_MAIN(InverseKinematics)
|
|
|
|
|
|
InverseKinematics::InverseKinematics(Context* context) :
|
|
|
- Sample(context),
|
|
|
- timeStepSum_(0.0f)
|
|
|
+ Sample(context)
|
|
|
{
|
|
|
}
|
|
|
|
|
|
@@ -83,14 +86,21 @@ void InverseKinematics::CreateScene()
|
|
|
|
|
|
// Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000)
|
|
|
scene_->CreateComponent<Octree>();
|
|
|
+ scene_->CreateComponent<DebugRenderer>();
|
|
|
+ scene_->CreateComponent<PhysicsWorld>();
|
|
|
|
|
|
// Create scene node & StaticModel component for showing a static plane
|
|
|
- Node* planeNode = scene_->CreateChild("Plane");
|
|
|
- planeNode->SetScale(Vector3(100.0f, 1.0f, 100.0f));
|
|
|
- StaticModel* planeObject = planeNode->CreateComponent<StaticModel>();
|
|
|
+ floorNode_ = scene_->CreateChild("Plane");
|
|
|
+ floorNode_->SetScale(Vector3(50.0f, 1.0f, 50.0f));
|
|
|
+ StaticModel* planeObject = floorNode_->CreateComponent<StaticModel>();
|
|
|
planeObject->SetModel(cache->GetResource<Model>("Models/Plane.mdl"));
|
|
|
planeObject->SetMaterial(cache->GetResource<Material>("Materials/StoneTiled.xml"));
|
|
|
|
|
|
+ // Set up collision, we need to raycast to determine foot height
|
|
|
+ floorNode_->CreateComponent<RigidBody>();
|
|
|
+ CollisionShape* col = floorNode_->CreateComponent<CollisionShape>();
|
|
|
+ col->SetBox(Vector3(1, 0, 1));
|
|
|
+
|
|
|
// Create a directional light to the world.
|
|
|
Node* lightNode = scene_->CreateChild("DirectionalLight");
|
|
|
lightNode->SetDirection(Vector3(0.6f, -1.0f, 0.8f)); // The direction vector does not need to be normalized
|
|
|
@@ -101,57 +111,56 @@ void InverseKinematics::CreateScene()
|
|
|
// Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
|
|
|
light->SetShadowCascade(CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f));
|
|
|
|
|
|
- // Load Jack model and walking animation.
|
|
|
- Node* jackNode = scene_->CreateChild("Jack");
|
|
|
- jackNode->SetPosition(Vector3(5.0f, 0.0f, 0.0f));
|
|
|
- jackNode->SetRotation(Quaternion(0.0f, 180.0f, 0.0f));
|
|
|
- AnimatedModel* ninja = jackNode->CreateComponent<AnimatedModel>();
|
|
|
- ninja->SetModel(cache->GetResource<Model>("Models/Jack.mdl"));
|
|
|
- ninja->SetMaterial(cache->GetResource<Material>("Materials/Jack.xml"));
|
|
|
- ninja->SetCastShadows(true);
|
|
|
+ // Load Jack model
|
|
|
+ jackNode_ = scene_->CreateChild("Jack");
|
|
|
+ jackNode_->SetRotation(Quaternion(0.0f, 270.0f, 0.0f));
|
|
|
+ AnimatedModel* jack = jackNode_->CreateComponent<AnimatedModel>();
|
|
|
+ jack->SetModel(cache->GetResource<Model>("Models/Jack.mdl"));
|
|
|
+ jack->SetMaterial(cache->GetResource<Material>("Materials/Jack.xml"));
|
|
|
+ jack->SetCastShadows(true);
|
|
|
+
|
|
|
+ // Create animation controller and play walk animation
|
|
|
+ jackAnimCtrl_ = jackNode_->CreateComponent<AnimationController>();
|
|
|
+ jackAnimCtrl_->PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.0f);
|
|
|
|
|
|
// We need to attach two inverse kinematic effectors to Jack's feet to
|
|
|
// control the grounding.
|
|
|
- Node* leftToe = jackNode->GetChild("Bip01_L_Toe0", true);
|
|
|
- Node* rightToe = jackNode->GetChild("Bip01_R_Toe0", true);
|
|
|
- leftEffector_ = leftToe->CreateComponent<IKEffector>();
|
|
|
- rightEffector_ = rightToe->CreateComponent<IKEffector>();
|
|
|
- // Control 3 segments up to the hips
|
|
|
- leftEffector_->SetChainLength(3);
|
|
|
- rightEffector_->SetChainLength(3);
|
|
|
+ leftFoot_ = jackNode_->GetChild("Bip01_L_Foot", true);
|
|
|
+ rightFoot_ = jackNode_->GetChild("Bip01_R_Foot", true);
|
|
|
+ leftEffector_ = leftFoot_->CreateComponent<IKEffector>();
|
|
|
+ rightEffector_ = rightFoot_->CreateComponent<IKEffector>();
|
|
|
+ // Control 2 segments up to the hips
|
|
|
+ leftEffector_->SetChainLength(2);
|
|
|
+ rightEffector_->SetChainLength(2);
|
|
|
|
|
|
// For the effectors to work, an IKSolver needs to be attached to one of
|
|
|
// the parent nodes. Typically, you want to place the solver as close as
|
|
|
// possible to the effectors for optimal performance. Since in this case
|
|
|
// we're solving the legs only, we can place the solver at the spine.
|
|
|
- Node* spine = jackNode->GetChild("Bip01_Spine", true);
|
|
|
- IKSolver* solver = spine->CreateComponent<IKSolver>();
|
|
|
-
|
|
|
- // We want to solve for target rotations so the feet can be properly
|
|
|
- // rotated to match the incline. Sometimes, if the feet are small, this
|
|
|
- // isn't necessary. It does have a performance impact on the solver.
|
|
|
- solver->EnableTargetRotation(true);
|
|
|
-
|
|
|
- // Set the effectors so only the feet bones are affected by the target
|
|
|
- // rotation. This is accomplished by setting the rotation weight to 1 and
|
|
|
- // then decay by a factor of 0 so the next bone has weight 0.
|
|
|
- leftEffector_->SetRotationWeight(1.0);
|
|
|
- leftEffector_->SetRotationDecay(0.0);
|
|
|
- rightEffector_->SetRotationWeight(1.0);
|
|
|
- rightEffector_->SetRotationDecay(0.0);
|
|
|
+ Node* spine = jackNode_->GetChild("Bip01_Spine", true);
|
|
|
+ solver_ = spine->CreateComponent<IKSolver>();
|
|
|
|
|
|
- // Create animation controller and play walk animation
|
|
|
- jackAnimController_ = jackNode->CreateComponent<AnimationController>();
|
|
|
- jackAnimController_->PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.0f);
|
|
|
+ // Disable auto-solving, which means we need to call Solve() manually
|
|
|
+ solver_->EnableAutoSolve(false);
|
|
|
+
|
|
|
+ // When this is enabled, the solver will use the current positions of the
|
|
|
+ // nodes in the skeleton as its basis every frame. If you disable this, then
|
|
|
+ // the solver will store the initial positions of the nodes once and always
|
|
|
+ // use those positions for calculating solutions.
|
|
|
+ // With animated characters you generally want to continuously update the
|
|
|
+ // initial positions.
|
|
|
+ solver_->EnableUpdatePose(true);
|
|
|
|
|
|
// Create the camera.
|
|
|
- cameraRotateNode_ = scene_->CreateChild("CameraAttach");
|
|
|
+ cameraRotateNode_ = scene_->CreateChild("CameraRotate");
|
|
|
cameraNode_ = cameraRotateNode_->CreateChild("Camera");
|
|
|
cameraNode_->CreateComponent<Camera>();
|
|
|
|
|
|
// Set an initial position for the camera scene node above the plane
|
|
|
- cameraNode_->SetPosition(Vector3(0.0f, 0.0f, -14.0f));
|
|
|
- pitch_ = 5;
|
|
|
+ cameraNode_->SetPosition(Vector3(0, 0, -4));
|
|
|
+ cameraRotateNode_->SetPosition(Vector3(0, 0.4, 0));
|
|
|
+ pitch_ = 20;
|
|
|
+ yaw_ = 50;
|
|
|
}
|
|
|
|
|
|
void InverseKinematics::CreateInstructions()
|
|
|
@@ -161,7 +170,7 @@ void InverseKinematics::CreateInstructions()
|
|
|
|
|
|
// Construct new Text object, set string to display and font to use
|
|
|
Text* instructionText = ui->GetRoot()->CreateChild<Text>();
|
|
|
- instructionText->SetText("Use WASD keys and mouse/touch to move");
|
|
|
+ instructionText->SetText("Left-Click and drag to look around\nRight-Click and drag to change incline\nPress space to reset floor\nPress D to draw debug geometry");
|
|
|
instructionText->SetFont(cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 15);
|
|
|
|
|
|
// Position the text relative to the screen center
|
|
|
@@ -181,7 +190,7 @@ void InverseKinematics::SetupViewport()
|
|
|
renderer->SetViewport(0, viewport);
|
|
|
}
|
|
|
|
|
|
-void InverseKinematics::MoveCamera(float timeStep)
|
|
|
+void InverseKinematics::UpdateCameraAndFloor(float timeStep)
|
|
|
{
|
|
|
// Do not move if the UI has a focused element (the console)
|
|
|
if (GetSubsystem<UI>()->GetFocusElement())
|
|
|
@@ -203,14 +212,40 @@ void InverseKinematics::MoveCamera(float timeStep)
|
|
|
pitch_ = Clamp(pitch_, -90.0f, 90.0f);
|
|
|
}
|
|
|
|
|
|
+ if (input->GetMouseButtonDown(MOUSEB_RIGHT))
|
|
|
+ {
|
|
|
+ IntVector2 mouseMoveInt = input->GetMouseMove();
|
|
|
+ Vector2 mouseMove = Matrix2(
|
|
|
+ -Cos(yaw_), Sin(yaw_),
|
|
|
+ Sin(yaw_), Cos(yaw_)
|
|
|
+ ) * Vector2(mouseMoveInt.y_, -mouseMoveInt.x_);
|
|
|
+ floorPitch_ += MOUSE_SENSITIVITY * mouseMove.x_;
|
|
|
+ floorPitch_ = Clamp(floorPitch_, -90.0f, 90.0f);
|
|
|
+ floorRoll_ += MOUSE_SENSITIVITY * mouseMove.y_;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (input->GetKeyPress(KEY_SPACE))
|
|
|
+ {
|
|
|
+ floorPitch_ = 0;
|
|
|
+ floorRoll_ = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (input->GetKeyPress(KEY_D))
|
|
|
+ {
|
|
|
+ drawDebug_ = !drawDebug_;
|
|
|
+ }
|
|
|
+
|
|
|
// Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
|
|
|
cameraRotateNode_->SetRotation(Quaternion(pitch_, yaw_, 0.0f));
|
|
|
+ floorNode_->SetRotation(Quaternion(floorPitch_, 0, floorRoll_));
|
|
|
}
|
|
|
|
|
|
void InverseKinematics::SubscribeToEvents()
|
|
|
{
|
|
|
// Subscribe HandleUpdate() function for processing update events
|
|
|
SubscribeToEvent(E_UPDATE, URHO3D_HANDLER(InverseKinematics, HandleUpdate));
|
|
|
+ SubscribeToEvent(E_POSTRENDERUPDATE, URHO3D_HANDLER(InverseKinematics, HandlePostRenderUpdate));
|
|
|
+ SubscribeToEvent(E_SCENEDRAWABLEUPDATEFINISHED, URHO3D_HANDLER(InverseKinematics, HandleSceneDrawableUpdateFinished));
|
|
|
}
|
|
|
|
|
|
void InverseKinematics::HandleUpdate(StringHash eventType, VariantMap& eventData)
|
|
|
@@ -221,5 +256,45 @@ void InverseKinematics::HandleUpdate(StringHash eventType, VariantMap& eventData
|
|
|
float timeStep = eventData[P_TIMESTEP].GetFloat();
|
|
|
|
|
|
// Move the camera, scale movement with time step
|
|
|
- MoveCamera(timeStep);
|
|
|
+ UpdateCameraAndFloor(timeStep);
|
|
|
+}
|
|
|
+
|
|
|
+void InverseKinematics::HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
|
|
|
+{
|
|
|
+ if (drawDebug_)
|
|
|
+ solver_->DrawDebugGeometry(false);
|
|
|
+}
|
|
|
+
|
|
|
+void InverseKinematics::HandleSceneDrawableUpdateFinished(StringHash eventType, VariantMap& eventData)
|
|
|
+{
|
|
|
+ PhysicsWorld* phyWorld = scene_->GetComponent<PhysicsWorld>();
|
|
|
+ Vector3 leftFootPosition = leftFoot_->GetWorldPosition();
|
|
|
+ Vector3 rightFootPosition = rightFoot_->GetWorldPosition();
|
|
|
+
|
|
|
+ // Cast ray down to get the normal of the underlying surface
|
|
|
+ PhysicsRaycastResult result;
|
|
|
+ phyWorld->RaycastSingle(result, Ray(leftFootPosition + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2);
|
|
|
+ if (result.body_)
|
|
|
+ {
|
|
|
+ // Cast again, but this time along the normal. Set the target position
|
|
|
+ // to the ray intersection
|
|
|
+ phyWorld->RaycastSingle(result, Ray(leftFootPosition + result.normal_, -result.normal_), 2);
|
|
|
+ // The foot node has an offset relative to the root node
|
|
|
+ float footOffset = jackNode_->GetWorldPosition().y_ + leftFoot_->GetWorldPosition().y_;
|
|
|
+ leftEffector_->SetTargetPosition(result.position_ + result.normal_ * footOffset);
|
|
|
+ // Rotate foot according to normal
|
|
|
+ leftFoot_->Rotate(Quaternion(Vector3(0, 1, 0), result.normal_), TS_WORLD);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Same deal with the right foot
|
|
|
+ phyWorld->RaycastSingle(result, Ray(rightFootPosition + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2);
|
|
|
+ if (result.body_)
|
|
|
+ {
|
|
|
+ phyWorld->RaycastSingle(result, Ray(rightFootPosition + result.normal_, -result.normal_), 2);
|
|
|
+ float footOffset = jackNode_->GetWorldPosition().y_ + rightFoot_->GetWorldPosition().y_;
|
|
|
+ rightEffector_->SetTargetPosition(result.position_ + result.normal_ * footOffset);
|
|
|
+ rightFoot_->Rotate(Quaternion(Vector3(0, 1, 0), result.normal_), TS_WORLD);
|
|
|
+ }
|
|
|
+
|
|
|
+ solver_->Solve();
|
|
|
}
|