Browse Source

Lua script bindings and CPP sample

TheComet 8 years ago
parent
commit
8fc0aa2866

+ 120 - 45
Source/Samples/45_InverseKinematics/InverseKinematics.cpp

@@ -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();
 }

+ 20 - 6
Source/Samples/45_InverseKinematics/InverseKinematics.h

@@ -30,6 +30,7 @@ namespace Urho3D
 class AnimationController;
 class Node;
 class IKEffector;
+class IKSolver;
 class Scene;
 class RibbonTrail;
 
@@ -49,13 +50,22 @@ public:
     virtual void Start();
 
 protected:
-    /// Animation controller of the ninja.
-    SharedPtr<Urho3D::AnimationController> jackAnimController_;
-    /// Inverse kinematic effectors
+    /// Animation controller of Jack.
+    SharedPtr<Urho3D::AnimationController> jackAnimCtrl_;
+    /// Inverse kinematic effectors and solver
     SharedPtr<Urho3D::IKEffector> leftEffector_;
     SharedPtr<Urho3D::IKEffector> rightEffector_;
-    /// Sum of timestep.
-    float timeStepSum_;
+    SharedPtr<Urho3D::IKSolver> solver_;
+    /// Need references to these nodes to calculate foot angles and offsets
+    SharedPtr<Urho3D::Node> leftFoot_;
+    SharedPtr<Urho3D::Node> rightFoot_;
+    SharedPtr<Urho3D::Node> jackNode_;
+    /// So we can rotate the floor
+    SharedPtr<Urho3D::Node> floorNode_;
+    float floorPitch_;
+    float floorRoll_;
+    /// Whether or not to draw debug geometry
+    bool drawDebug_;
 
 private:
     /// Construct the scene content.
@@ -65,11 +75,15 @@ private:
     /// Set up a viewport for displaying the scene.
     void SetupViewport();
     /// Read input and moves the camera.
-    void MoveCamera(float timeStep);
+    void UpdateCameraAndFloor(float timeStep);
     /// Subscribe to application-wide logic update events.
     void SubscribeToEvents();
     /// Handle the logic update event.
     void HandleUpdate(StringHash eventType, VariantMap& eventData);
+    /// Draw debug geometry
+    void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData);
+    /// Process IK logic
+    void HandleSceneDrawableUpdateFinished(StringHash eventType, VariantMap& eventData);
 
     SharedPtr<Node> cameraRotateNode_;
 };

+ 11 - 1
Source/Urho3D/Core/Context.cpp

@@ -42,11 +42,19 @@ namespace Urho3D
 
 // Keeps track of how many times SDL was initialised so we know when to call SDL_Quit().
 static int sdlInitCounter = 0;
-// Keeps track of how many times IK was initialised
+
 #ifdef URHO3D_IK
+// Keeps track of how many times IK was initialised
 static int ikInitCounter = 0;
+
+// Reroute all messages from the ik library to the Urho3D log
+static void HandleIKLog(const char* msg)
+{
+    URHO3D_LOGINFOF("[IK] %s", msg);
+}
 #endif
 
+
 void EventReceiverGroup::BeginSendEvent()
 {
     ++inSend_;
@@ -287,6 +295,7 @@ void Context::RequireIK()
         URHO3D_LOGDEBUG("Initialising Inverse Kinematics library");
         ik_memory_init();
         ik_log_init(IK_LOG_NONE);
+        ik_log_register_listener(HandleIKLog);
     }
 }
 
@@ -297,6 +306,7 @@ void Context::ReleaseIK()
     if (ikInitCounter == 0)
     {
         URHO3D_LOGDEBUG("De-initialising Inverse Kinematics library");
+        ik_log_unregister_listener(HandleIKLog);
         ik_log_deinit();
         ik_memory_deinit();
     }

+ 0 - 8
Source/Urho3D/IK/IKSolver.cpp

@@ -39,17 +39,12 @@
 #include <ik/solver.h>
 #include <ik/node.h>
 #include <ik/effector.h>
-#include <ik/log.h>
 
 namespace Urho3D
 {
 
 extern const char* IK_CATEGORY;
 
-static void HandleIKLog(const char* msg)
-{
-    URHO3D_LOGINFOF("[IK] %s", msg);
-}
 static bool ChildrenHaveEffector(const Node* node)
 {
     if (node->HasComponent<IKEffector>())
@@ -89,7 +84,6 @@ IKSolver::~IKSolver()
     for(PODVector<IKEffector*>::ConstIterator it = effectorList_.Begin(); it != effectorList_.End(); ++it)
         (*it)->SetIKEffector(NULL);
 
-    ik_log_unregister_listener(HandleIKLog);
     ik_solver_destroy(solver_);
     context_->ReleaseIK();
 }
@@ -136,8 +130,6 @@ void IKSolver::SetAlgorithm(IKSolver::Algorithm algorithm)
     }
 
     solver_->flags = SOLVER_CALCULATE_FINAL_ROTATIONS;
-
-    ik_log_register_listener(HandleIKLog);
 }
 
 // ----------------------------------------------------------------------------

+ 9 - 1
Source/Urho3D/LuaScript/pkgs/IK/IKConstraint.pkg

@@ -1,5 +1,13 @@
-#include "Scene/Component.h"
+$#include "IK/IKConstraint.h"
 
 class IKConstraint : public Component
 {
+    float GetStiffness() const;
+    void SetStiffness(float stiffness);
+
+    float GetStretchiness() const;
+    void SetStretchiness(float stretchiness);
+
+    const Vector2& GetLengthConstraints() const;
+    void SetLengthConstraints(const Vector2& lengthConstraints);
 };

+ 16 - 8
Source/Urho3D/LuaScript/pkgs/IK/IKEffector.pkg

@@ -1,31 +1,39 @@
-#include "Scene/Component.h"
-#include "Scene/Scene.h"
+$#include "IK/IKEffector.h"
 
 class IKEffector : public Component
 {
     Node* GetTargetNode() const;
     void SetTargetNode(Node* targetNode);
+
     const String& GetTargetName() const;
     void SetTargetName(const String& nodeName);
+
     const Vector3& GetTargetPosition() const;
     void SetTargetPosition(const Vector3& targetPosition);
+
     const Quaternion& GetTargetRotation() const;
     void SetTargetRotation(const Quaternion& targetRotation);
+
     Vector3 GetTargetRotationEuler() const;
     void SetTargetRotationEuler(const Vector3& targetRotation);
+
     unsigned GetChainLength() const;
     void SetChainLength(unsigned chainLength);
+
     float GetWeight() const;
     void SetWeight(float weight);
+
     float GetRotationWeight() const;
     void SetRotationWeight(float weight);
+
     float GetRotationDecay() const;
     void SetRotationDecay(float decay);
-    bool DoWeightedNlerp() const;
-    void SetWeightedNlerp(bool enable);
-    bool DoInheritParentRotation() const;
-    void SetInheritParentRotation(bool enable);
+
+    bool WeightedNlerpEnabled() const;
+    void EnableWeightedNlerp(bool enable);
+
+    bool InheritParentRotationEnabled() const;
+    void EnableInheritParentRotation(bool enable);
+
     void UpdateTargetNodePosition();
 };
-
-} // namespace Urho3D

+ 8 - 3
Source/Urho3D/LuaScript/pkgs/IK/IKSolver.pkg

@@ -1,5 +1,4 @@
-#include "IK/IKEffector.h"
-#include "Scene/Component.h"
+$#include "IK/IKSolver.h"
 
 class IKSolver : public Component
 {
@@ -14,18 +13,24 @@ class IKSolver : public Component
     void SetMaximumIterations(unsigned iterations);
     float GetTolerance() const;
     void SetTolerance(float tolerance);
+
     bool BoneRotationsEnabled() const;
     void EnableBoneRotations(bool enable);
+
     bool TargetRotationEnabled() const;
     void EnableTargetRotation(bool enable);
+
     bool ContinuousSolvingEnabled() const;
     void EnableContinuousSolving(bool enable);
+
     bool UpdatePoseEnabled() const;
     void EnableUpdatePose(bool enable);
 
+    bool AutoSolveEnabled() const;
+    void EnableAutoSolve(bool enable);
+
     void Solve();
     void ResetToInitialPose();
     void UpdateInitialPose();
 };
 
-} // namespace Urho3D

+ 6 - 0
Source/Urho3D/LuaScript/pkgs/IKLuaAPI.pkg

@@ -0,0 +1,6 @@
+$pfile "IK/IKSolver.pkg"
+$pfile "IK/IKConstraint.pkg"
+$pfile "IK/IKEffector.pkg"
+
+$using namespace Urho3D;
+$#pragma warning(disable:4800)

+ 188 - 0
bin/Data/LuaScripts/45_InverseKinematics.lua

@@ -0,0 +1,188 @@
+-- Ribbon trail demo.
+-- This sample demonstrates how to use both trail types of RibbonTrail component.
+
+require "LuaScripts/Utilities/Sample"
+
+local jackAnimCtrl_
+local cameraRotateNode_
+local floorNode_
+local leftFoot_
+local rightFoot_
+local leftEffector_
+local rightEffector_
+local solver_
+local floorPitch_ = 0.0
+local floorRoll_ = 0.0
+local drawDebug_ = false
+
+function Start()
+    cache.autoReloadResources = true
+
+    -- Execute the common startup for samples
+    SampleStart()
+
+    -- Create the scene content
+    CreateScene()
+
+    -- Create the UI content
+    CreateInstructions()
+
+    -- Setup the viewport for displaying the scene
+    SetupViewport()
+
+    -- Set the mouse mode to use in the sample
+    SampleInitMouseMode(MM_RELATIVE)
+
+    -- Hook up to the frame update events
+    SubscribeToEvents()
+end
+
+function CreateScene()
+    scene_ = Scene()
+
+    -- 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
+    floorNode_ = scene_:CreateChild("Plane")
+    floorNode_.scale = Vector3(50.0, 1.0, 50.0)
+    local planeObject = floorNode_:CreateComponent("StaticModel")
+    planeObject.model = cache:GetResource("Model", "Models/Plane.mdl")
+    planeObject.material = cache:GetResource("Material", "Materials/StoneTiled.xml")
+
+    -- Set up collision, we need to raycast to determine foot height
+    floorNode_:CreateComponent("RigidBody")
+    local col = floorNode_:CreateComponent("CollisionShape")
+    col:SetBox(Vector3(1, 0, 1))
+
+    -- Create a directional light to the world.
+    local lightNode = scene_:CreateChild("DirectionalLight")
+    lightNode.direction = Vector3(0.6, -1.0, 0.8) -- The direction vector does not need to be normalized
+    local light = lightNode:CreateComponent("Light")
+    light.lightType = LIGHT_DIRECTIONAL
+    light.castShadows = true
+    light.shadowBias = BiasParameters(0.00005, 0.5)
+    -- Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
+    light.shadowCascade = CascadeParameters(10.0, 50.0, 200.0, 0.0, 0.8)
+
+    -- Load Jack animated model
+    jackNode_ = scene_:CreateChild("Jack")
+    jackNode_.rotation = Quaternion(0.0, 270.0, 0.0)
+    jack = jackNode_:CreateComponent("AnimatedModel")
+    jack.model = cache:GetResource("Model", "Models/Jack.mdl")
+    jack.material = cache:GetResource("Material", "Materials/Jack.xml")
+    jack.castShadows = true
+
+    -- Create animation controller and play walk animation
+    jackAnimCtrl_ = jackNode_:CreateComponent("AnimationController")
+    jackAnimCtrl_:PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.0)
+
+    -- We need to attach two inverse kinematic effectors to Jack's feet to
+    -- control the grounding.
+    leftFoot_  = jackNode_:GetChild("Bip01_L_Foot", true);
+    rightFoot_ = jackNode_:GetChild("Bip01_R_Foot", true);
+
+    -- NOTE: Crashes here for some reason
+    leftEffector_  = leftFoot_:CreateComponent("IKEffector")
+
+    --rightEffector_ = rightFoot_:CreateComponent("IKEffector")
+    -- Control 2 segments up to the hips
+    --leftEffector_.chainLength = 2;
+    --rightEffector_.chainLength = 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.
+    --local spine = jackNode_:GetChild("Bip01_Spine", true);
+    --solver_ = spine:CreateComponent("IKSolver");
+
+    -- Disable auto-solving, which means we can call Solve() manually.
+    --solver_.autoSolve = 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_.updatePose = true;
+
+    -- Create the camera.
+    cameraNode = scene_:CreateChild("Camera")
+    cameraNode:CreateComponent("Camera")
+
+    -- Set an initial position for the camera scene node above the plane
+    cameraNode.position = Vector3(0.0, 2.0, -14.0)
+end
+
+function CreateInstructions()
+    -- Construct new Text object, set string to display and font to use
+    local instructionText = ui.root:CreateChild("Text")
+    instructionText:SetText("Use WASD keys and mouse to move")
+    instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
+
+    -- Position the text relative to the screen center
+    instructionText.horizontalAlignment = HA_CENTER
+    instructionText.verticalAlignment = VA_CENTER
+    instructionText:SetPosition(0, ui.root.height / 4)
+end
+
+function SetupViewport()
+    -- Set up a viewport to the Renderer subsystem so that the 3D scene can be seen. We need to define the scene and the camera
+    -- at minimum. Additionally we could configure the viewport screen size and the rendering path (eg. forward / deferred) to
+    -- use, but now we just use full screen and default render path configured in the engine command line options
+    local viewport = Viewport:new(scene_, cameraNode:GetComponent("Camera"))
+    renderer:SetViewport(0, viewport)
+end
+
+function UpdateCameraAndFloor(timeStep)
+    -- Do not move if the UI has a focused element (the console)
+    if ui.focusElement ~= nil then
+        return
+    end
+
+    -- Movement speed as world units per second
+    local MOVE_SPEED = 20.0
+    -- Mouse sensitivity as degrees per pixel
+    local MOUSE_SENSITIVITY = 0.1
+
+    -- Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees
+    local mouseMove = input.mouseMove
+    yaw = yaw +MOUSE_SENSITIVITY * mouseMove.x
+    pitch = pitch + MOUSE_SENSITIVITY * mouseMove.y
+    pitch = Clamp(pitch, -90.0, 90.0)
+
+    -- Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0)
+
+    -- Read WASD keys and move the camera scene node to the corresponding direction if they are pressed
+    -- Use the Translate() function (default local space) to move relative to the node's orientation.
+    if input:GetKeyDown(KEY_W) then
+        cameraNode:Translate(Vector3(0.0, 0.0, 1.0) * MOVE_SPEED * timeStep)
+    end
+    if input:GetKeyDown(KEY_S) then
+        cameraNode:Translate(Vector3(0.0, 0.0, -1.0) * MOVE_SPEED * timeStep)
+    end
+    if input:GetKeyDown(KEY_A) then
+        cameraNode:Translate(Vector3(-1.0, 0.0, 0.0) * MOVE_SPEED * timeStep)
+    end
+    if input:GetKeyDown(KEY_D) then
+        cameraNode:Translate(Vector3(1.0, 0.0, 0.0) * MOVE_SPEED * timeStep)
+    end
+end
+
+function SubscribeToEvents()
+    -- Subscribe HandleUpdate() function for processing update events
+    SubscribeToEvent("Update", "HandleUpdate")
+end
+
+function HandleUpdate(eventType, eventData)
+    -- Take the frame time step, which is stored as a float
+    local timeStep = eventData["TimeStep"]:GetFloat()
+
+    -- Move the camera, scale movement with time step
+    UpdateCameraAndFloor(timeStep)
+end

+ 21 - 19
bin/Data/Scripts/45_InverseKinematics.as

@@ -50,7 +50,7 @@ void CreateScene()
 
     // Create scene node & StaticModel component for showing a static plane
     floorNode_ = scene_.CreateChild("Plane");
-    floorNode_.scale = Vector3(100.0f, 1.0f, 100.0f);
+    floorNode_.scale = Vector3(50.0f, 1.0f, 50.0f);
     StaticModel@ planeObject = floorNode_.CreateComponent("StaticModel");
     planeObject.model = cache.GetResource("Model", "Models/Plane.mdl");
     planeObject.material = cache.GetResource("Material", "Materials/StoneTiled.xml");
@@ -71,7 +71,7 @@ void CreateScene()
     light.shadowCascade = CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f);
 
     // Load Jack animated model
-    jackNode_ = scene_.CreateChild("Ninja");
+    jackNode_ = scene_.CreateChild("Jack");
     jackNode_.rotation = Quaternion(0.0f, 270.0f, 0.0f);
     AnimatedModel@ jack = jackNode_.CreateComponent("AnimatedModel");
     jack.model = cache.GetResource("Model", "Models/Jack.mdl");
@@ -103,9 +103,9 @@ void CreateScene()
     solver_.autoSolve = false;
 
     // When this is enabled, the solver will use the current positions of the
-    // nodes in the skeleton as its basis. If you disable this, then the solver
-    // will store the initial positions of the nodes once and always use those
-    // positions.
+    // 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_.updatePose = true;
@@ -218,31 +218,33 @@ void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
 
 void HandleSceneDrawableUpdateFinished(StringHash eventType, VariantMap& eventData)
 {
-    Vector3 leftTarget = leftFoot_.worldPosition;
-    Vector3 rightTarget = rightFoot_.worldPosition;
+    Vector3 leftFootPosition = leftFoot_.worldPosition;
+    Vector3 rightFootPosition = rightFoot_.worldPosition;
 
-    PhysicsRaycastResult result = scene_.physicsWorld.RaycastSingle(Ray(leftTarget + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2);
+    // Cast ray down to get the normal of the underlying surface
+    PhysicsRaycastResult result = scene_.physicsWorld.RaycastSingle(Ray(leftFootPosition + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2);
     if (result.body !is null)
     {
-        result = scene_.physicsWorld.RaycastSingle(Ray(leftTarget + result.normal, -result.normal), 2);
-        leftTarget = result.position;
+        // Cast again, but this time along the normal. Set the target position
+        // to the ray intersection
+        result = scene_.physicsWorld.RaycastSingle(Ray(leftFootPosition + result.normal, -result.normal), 2);
+        // The foot node has an offset relative to the root node
+        float footOffset = jackNode_.worldPosition.y + leftFoot_.worldPosition.y;
+        leftEffector_.targetPosition = result.position + result.normal * footOffset;
+        // Rotate foot according to normal
         leftFoot_.Rotate(Quaternion(Vector3(0, 1, 0), result.normal), TS_WORLD);
     }
 
-    result = scene_.physicsWorld.RaycastSingle(Ray(rightTarget + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2);
+    // Same deal with the right foot
+    result = scene_.physicsWorld.RaycastSingle(Ray(rightFootPosition + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2);
     if (result.body !is null)
     {
-        result = scene_.physicsWorld.RaycastSingle(Ray(rightTarget + result.normal, -result.normal), 2);
-        rightTarget = result.position;
+        result = scene_.physicsWorld.RaycastSingle(Ray(rightFootPosition + result.normal, -result.normal), 2);
+        float footOffset = jackNode_.worldPosition.y + rightFoot_.worldPosition.y;
+        rightEffector_.targetPosition = result.position + result.normal * footOffset;
         rightFoot_.Rotate(Quaternion(Vector3(0, 1, 0), result.normal), TS_WORLD);
     }
 
-    Vector3 leftFootOffset = Vector3(0, jackNode_.worldPosition.y + leftFoot_.worldPosition.y, 0);
-    Vector3 rightFootOffset = Vector3(0, jackNode_.worldPosition.y + rightFoot_.worldPosition.y, 0);
-
-    leftEffector_.targetPosition = leftTarget + leftFootOffset;
-    rightEffector_.targetPosition = rightTarget + rightFootOffset;
-
     solver_.Solve();
 }