Urho3D/bin/Data/LuaScripts/45_InverseKinematics.lua

237 lines
9.4 KiB
Lua

-- Ribbon trail demo.
-- This sample demonstrates how to adjust the position of animated feet so they match the ground's angle using IK.
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)
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")
-- Two-bone solver is more efficient and more stable than FABRIK (but only
-- works for two bones, obviously).
solver_.algorithm = IKSolver.TWO_BONE
-- Disable auto-solving, which means we can call Solve() manually.
solver_.AUTO_SOLVE = false
-- Only enable this so the debug draw shows us the pose before solving.
-- This should NOT be enabled for any other reason (it does nothing and is
-- a waste of performance).
solver_.UPDATE_ORIGINAL_POSE = true
-- Create the camera.
cameraRotateNode_ = scene_:CreateChild("CameraRotate")
cameraNode = cameraRotateNode_:CreateChild("Camera")
cameraNode:CreateComponent("Camera")
-- Set an initial position for the camera scene node above the plane
cameraNode.position = Vector3(0.0, 0.0, -4.0)
cameraRotateNode_.position = Vector3(0.0, 0.4, 0.0)
pitch = 20.0
yaw = 50.0
end
function CreateInstructions()
-- Construct new Text object, set string to display and font to use
local instructionText = ui.root:CreateChild("Text")
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
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
-- 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
if input:GetMouseButtonDown(MOUSEB_LEFT) then
local mouseMove = input.mouseMove
yaw = yaw +MOUSE_SENSITIVITY * mouseMove.x
pitch = pitch + MOUSE_SENSITIVITY * mouseMove.y
pitch = Clamp(pitch, -90.0, 90.0)
end
if input:GetMouseButtonDown(MOUSEB_RIGHT) then
local mouseMoveInt = input.mouseMove
local mouseMove = Vector2()
mouseMove.x = -Cos(yaw) * mouseMoveInt.y - Sin(yaw) * mouseMoveInt.x
mouseMove.y = Sin(yaw) * mouseMoveInt.y - Cos(yaw) * mouseMoveInt.x
floorPitch_ = floorPitch_ + MOUSE_SENSITIVITY * mouseMove.x
floorPitch_ = Clamp(floorPitch_, -90.0, 90.0)
floorRoll_ = floorRoll_ + MOUSE_SENSITIVITY * mouseMove.y
end
if input:GetKeyPress(KEY_SPACE) then
floorPitch_ = 0.0
floorRoll_ = 0.0
end
if input:GetKeyPress(KEY_D) then
drawDebug_ = not drawDebug_
end
-- Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
cameraRotateNode_.rotation = Quaternion(pitch, yaw, 0.0)
floorNode_.rotation = Quaternion(floorPitch_, 0.0, floorRoll_)
end
function SubscribeToEvents()
-- Subscribe HandleUpdate() function for processing update events
SubscribeToEvent("Update", "HandleUpdate")
SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate")
SubscribeToEvent("SceneDrawableUpdateFinished", "HandleSceneDrawableUpdateFinished")
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
function HandlePostRenderUpdate(eventType, eventData)
if drawDebug_ then
solver_:DrawDebugGeometry(false)
end
end
function HandleSceneDrawableUpdateFinished(eventType, eventData)
local physicsWorld = scene_:GetComponent("PhysicsWorld")
local leftFootPosition = leftFoot_.worldPosition
local rightFootPosition = rightFoot_.worldPosition
-- Cast ray down to get the normal of the underlying surface
local result = physicsWorld:RaycastSingle(Ray(leftFootPosition + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2)
if result.body then
-- Cast again, but this time along the normal. Set the target position
-- to the ray intersection
local oppositeNormal = result.normal * -1
result = physicsWorld:RaycastSingle(Ray(leftFootPosition + result.normal, oppositeNormal), 2)
-- The foot node has an offset relative to the root node
footOffset = leftFoot_.worldPosition.y - jackNode_.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)
end
-- Same deal with the right foot
result = physicsWorld:RaycastSingle(Ray(rightFootPosition + Vector3(0, 1, 0), Vector3(0, -1, 0)), 2)
if result.body then
local oppositeNormal = result.normal * -1
result = physicsWorld:RaycastSingle(Ray(rightFootPosition + result.normal, oppositeNormal), 2)
footOffset = rightFoot_.worldPosition.y - jackNode_.worldPosition.y
rightEffector_.targetPosition = result.position + result.normal * footOffset
rightFoot_:Rotate(Quaternion(Vector3(0, 1, 0), result.normal), TS_WORLD)
end
solver_:Solve()
end