Skip to main content

TwoBoneIK

Last Updated Time: 09/08/2023

Overview

Use TwoBoneIK to control two bones to complete the specified action through Inverse Kinematic, which is used to realize the interaction between the character's actions and the environment.

Using Nodes

Right-click the blank space of an Animation Tree to open the Node selection panel, then expand the Animation Post-processing category and select Add TwoBoneIK.

image-20230717160135209

After adding the node successfully:

image-20220909135108666

The pose that needs to be solved by TwoBoneIK can be connected to the input pin of the TwoBoneIK node.

image-20230215144733760

Properties

After selecting a node, you can see properties of the node in the Property Window.

image-20220909160146456

PropertyDescription
Tip BoneThe marked IK tip bone.
Callback StringExecute the callback string. When the callback string returns false, the node function will not be executed.
Target Point NameMark the target point position of the bone.
Joint Point NameThe joint point position.
Alpha NameSpecify the name of the animation's float parameter, and use the parameter value as the blend weight.
Target Rotate NameSpecify the animation parameter name (string or vector type), and use the parameter value as the rotation of the target point. The string type needs to separate the rotation components with commas.
Rotate From TargetWhether to rotate based on the target point.
Take Rotation From ParentWhether to rotate based on the parent bone.
Allow StretchWhether the bone allows stretching.
Stretch StartStretch start ratio.
Stretch MaxMaximum stretch ratio.
Allow RotateMaximum rotation angle of the target bone.

Example

The example implements IK for the character’s left and right feet in static state, allowing the character’s feet to stick close to the ground on different slopes.

When the capsule is on a slope, it will be in a suspended state. By adding TwoBoneIK nodes to the character’s Animation Tree and using the Animation Tree callback function, the character's feet can be kept close to the ground when it is on a slope. In the picture below, the character is standing at the same position. With TwoBoneIK nodes, the feet can be kept close to the ground by controlling the direction of knee bending and by bending the knees to an unfixed degree based on the height of the feet from the ground.


With TwoBoneIK Nodes

Without TwoBoneIK Nodes
This example adds IK to the character's feet based on the example of [*Animation State Machine*](/Character-and-Skinned-Animation/Examples/Logic-Related/Animation-State-Machine/#example), and the corresponding functions need to be added during steps Creating Animation Trees and Binding Scripts.

Animation Trees

Add two TwoBoneIK nodes to an Animation Tree for the IK of the left foot and right foot, and connect the nodes together as shown below.

image-20230718112854348

In the Animation Parameter window, click the image-20230718113319832 button, then select String and Float to create 4 String parameters footlpos, footljpos, footrpos, footrjpos, and 1 Float parameter foot_ik_weight for the properties Target Point Name, Joint Point Name and Alpha Name of the 2 nodes respectively.

image-20230718113423510

After creating the parameters:

image-20230718131713373

Select the two TwoBoneIK nodes separately and set the node properties in the Property Window.

image-20230718132110788

image-20230718132033940

Click the Save button after editing the Animation Tree.

image-20230718171408236

Creating the Terrain

Click Create Game Object -> Visual -> Terrain Object in the Component Editor.

image-20230718172400854

Set the properties of the created terrain in the pop-up Initial Edit window and click OK to complete the creation.

image-20230718172743708

Create a slope. Click Windows -> Edit Mode -> Terrain Mode, then select Height Edit [Alt+1] in the opened Terrain Editor.

image-20230718174330051

Then in the Level panel, left-click the ground to raise the ground.

EditTerrain

Binding Scripts

Bind two scripts robot_character.lua and robot_character_animation.lua to the character, just like the binding scripts in the Animation State Machine example.

Call animtree_callback_LRootIK and animtree_callback_RRootIK via the Animation Tree callback function.

  1. First, when the terrain object does not exist in the scene,or the character's speed is over 1, return directly without creating IK.
  2. Get the offsets of the left foot and the right foot from the ground respectively.

Get the world space coordinates and model space coordinates of the foot bones, and the Terrain Object gets the height of the ground under feet by the XZ coordinates in world space. World space height minus model space height minus ground height is the desired offset for the foot bones.

local obj_pos_lx, obj_pos_ly, obj_pos_lz = role:GetNodeObjectPosition("Bip01 L Foot")
local position_lx, position_ly, position_lz = role:GetNodeWorldPosition("Bip01 L Foot")
local lgroundheight = terrain:GetGroundHeight(position_lx,position_lz)
local left_offset = position_ly - obj_pos_ly - lgroundheight

After the desired offsets of the feet are calculated, use the maximum value to move the skeletal mesh component to move the character downward, so that the foot highest from the ground sticks close to the ground. 0.1 is the interpolation coefficient to prevent jumps when the character moves downward.

local y_offset = math.max(left_offset, right_offset) - 0.02
if y_offset > 0 then
pawn.MeshComponent.PositionY = pawn.MeshComponent.PositionY - y_offset * 0.1
end

Recalculate the desired displacement of the foot bones and set the foot target position (footpos) parameter and weight (foot_ik_weight) of the Animation Tree.

The blend weight is inversely proportional to the character's speed. When the character's speed is 1, the blend weight is 0, and when the speed is 0, the blend weight is 1. Same for the other foot but without calculating the maximum desired displacement and moving the skeletal mesh component.

local strnewpos = nx_string(position_lx) .. "," .. nx_string(lgroundheight + foot_rel_height) .. "," .. nx_string(position_lz)
role:SetAnimTreeValue(action_index, "footlpos", strnewpos)
role:SetAnimTreeValue(action_index, "foot_ik_weight", 1 - get_speed(pawn))

Since the mesh component was moved, the relative position of the mesh component needs to be restored when the character is moved again.

In the callback of the Blend Space node of the Animation Tree, set the character's custom property bool_foot_ik to false.

function animtree_callback_bs_x_direction(pawn, role)
pawn.bool_foot_ik = false
return pawn.direction
end

Set bool_foot_ik to true in the callback of the IK node.

role:SetAnimTreeValue(action_index, "footlpos", strnewpos)
role:SetAnimTreeValue(action_index, "foot_ik_weight", 1 - get_speed(pawn))
pawn.bool_foot_ik = true

Record the relative displacement of the mesh component in the on_begin_play function.

owner.meshcomp_init_relative_posy = owner.MeshComponent.RelativePositionY

Determine and restore the relative displacement of the mesh component in the tick function.

if nx_find_custom(owner, "bool_foot_ik") and owner.bool_foot_ik == false then
owner.MeshComponent.RelativePositionY = owner.meshcomp_init_relative_posy
end

The following are reference scripts:

robot_character.lua

--script template

local IE_Pressed = 0 --Key pressed
local IE_Released = 1 --Key released
local IE_Repeat = 2
local IE_DoubleClick = 3
local IE_Axis = 4
local IE_MAX = 5

local CAMERA_DISTANCE_CHANGE_SPEED = 0.5
local CAMERA_DISTANCE_MIN = -1
local CAMERA_DISTANCE_MAX = 50

--Get the actor
function character_get_actor(character)
if nx_is_valid(character) then
local mesh_component = character.MeshComponent

if nx_is_valid(mesh_component) then
local vis_base = mesh_component.VisBase

if nx_is_valid(vis_base) and nx_is_kind(vis_base, "Actor") then
return vis_base
end
end
end

return nx_null()
end

--Moving forward callback
function on_moveforward(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--Get controller angle Y
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--Get forward vector
local x, y, z = nx_function("ext_angle_get_forward_vector", 0.0, yaw, 0.0)

--Add movement input

owner:AddMovementInput(x, y, z, axis_value)
end

return 1
end

--Moving right callback
function on_moveright(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--Get controller angle Y
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--Get right vector
local x, y, z = nx_function("ext_angle_get_right_vector", 0.0, yaw, 0.0)

--Add movement input
owner:AddMovementInput(x, y, z, axis_value)
end
return 1
end

--Left and right rotation (rotation around the Y-axis) callback
function on_turn(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

owner:AddControllerYawInput(axis_value)

return 1
end

--Up and down rotation (rotation around X-axis) callback
function on_lookup(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

owner:AddControllerPitchInput(-axis_value)

return 1
end

--Jump key pressed callback
function on_jump_pressed(owner)
if nx_find_custom(owner, "JumpAgainLock") and owner.JumpAgainLock then
return 1
end

--Character jumps
owner:Jump()

return 1
end


function input_comp_init_bind(component)
local owner = component.GameObjectOwner

if nx_is_valid(owner) then
local input_comp = owner.InputComponent

if nx_is_valid(input_comp) then
nx_bind_script(owner, nx_current())

input_comp:AddAxisBinding("MoveForward", true, false, true, "InputAxisEvent_MoveForward")
input_comp:AddAxisBinding("MoveRight", true, false, true, "InputAxisEvent_MoveRight")
input_comp:AddAxisBinding("Turn", true, false, true, "InputAxisEvent_Turn")
input_comp:AddAxisBinding("LookUp", true, false, true, "InputAxisEvent_LookUp")

input_comp:AddCombinationBinding("Jump", IE_Pressed, true, false, true, "InputActionEvent_Jump_Pressed")

nx_callback(owner, "InputAxisEvent_MoveForward", "on_moveforward")
nx_callback(owner, "InputAxisEvent_MoveRight", "on_moveright")
nx_callback(owner, "InputAxisEvent_Turn", "on_turn")
nx_callback(owner, "InputAxisEvent_LookUp", "on_lookup")

nx_callback(owner, "InputActionEvent_Jump_Pressed", "on_jump_pressed")
end
end
end

--use this for initialization
function on_begin_play(component)
input_comp_init_bind(component)
nx_callback(component, "on_tick", "tick")
end

robot_character_animation.lua

--script template

--------------------------
--local utility function--
--------------------------

require("public_attr")
require("scene_utils")

local radian = 57.2958
local foot_rel_height = 0.14

local function get_terrain(pawn)
if nx_is_valid(pawn) then
local lscene = pawn.Scene

if nx_is_valid(lscene) then
return scene_get_engine_terrain(lscene)
end
end

return nx_null()
end

local function get_movement_component(pawn)
if nx_is_valid(pawn) then
return pawn:GetMovementComponentID()
end

return nx_null()
end

local function get_speed(pawn)
if nx_is_valid(pawn) then
local movement = get_movement_component(pawn)

if nx_is_valid(movement) then
local speed = nx_function("ext_vector_length", pawn.VelocityX, pawn.VelocityY, pawn.VelocityZ)
return speed
end
end

return 0.0
end

local function is_jumping(pawn)
if nx_is_valid(pawn) then
local movement = get_movement_component(pawn)

if nx_is_valid(movement) then
if movement:IsFalling() then
return true
end
end
end

return false
end

local function is_flying(pawn)
if nx_is_valid(pawn) then
local movement = get_movement_component(pawn)

if nx_is_valid(movement) then
if movement:IsFlying() then
return true
end
end
end

return false
end

function transition_callback_jump_start(pawn, role, action_index, machine_index, prev_state, next_state)
if is_jumping(pawn) then
return true
else
return false
end
end

function transition_callback_jump_end(pawn, role, action_index, machine_index, prev_state, next_state)
if not is_jumping(pawn) then
return true
else
return false
end
end

transition_event_callback =
{
["jump_start"] = transition_callback_jump_start,
["jump_end"] = transition_callback_jump_end,
}

function on_transition_event(mesh_comp, action_index, machine_index, prev_state, next_state, callback_name)

local pawn = mesh_comp.GameObjectOwner
local role = mesh_comp.VisBase

if not nx_is_valid(pawn) or not nx_is_valid(role) then
return false
end

local callback = transition_event_callback[callback_name]
if callback then
return callback(pawn, role, action_index, machine_index, prev_state, next_state)
end

return false
end

function state_machine_state_callback_EnterState(pawn, role, action_index, machine_index, state_index)
-- do something
nx_log("state machine state enter jump")
end

function state_machine_state_callback_LeaveState(pawn, role, action_index, machine_index, state_index)
-- do something
nx_log("state machine state leave jump")
end

function state_machine_state_callback_FullyBlendState(pawn, role, action_index, machine_index, state_index)
-- do something
nx_log("state machine state fully blend jump")
end

state_machine_state_event_callback =
{
["start_state"] = state_machine_state_callback_EnterState,
["end_state"] = state_machine_state_callback_LeaveState,
["fully_blend_state"] = state_machine_state_callback_FullyBlendState,
}

function on_state_machine_state_event(mesh_comp, action_index, machine_index, state_index, callback_name)

local pawn = mesh_comp.GameObjectOwner
local role = mesh_comp.VisBase

if not nx_is_valid(pawn) or not nx_is_valid(role) then
return
end

local callback = state_machine_state_event_callback[callback_name]
if callback then
callback(pawn, role, action_index, machine_index, state_index)
end
end

function animtree_callback_bs_x_direction(pawn, role)
pawn.bool_foot_ik = false

if nx_find_custom(pawn, "direction") then
return pawn.direction
end

return 0
end

function animtree_callback_bs_y_speed(pawn, role)
return get_speed(pawn)
end

local function get_anim_tree_index(role)
if role:GetBlendActionCount() > 0 then
local action_name = role:GetBlendActionName(0)
local action_type = role:GetActionType(action_name)
if action_type == AT_ANIMTREE then
return role:GetActionIndex(action_name)
end
end

return -1
end

function animtree_callback_LRootIK(pawn, role)
local terrain = get_terrain(pawn)
if not nx_is_valid(terrain) or is_jumping(pawn) or get_speed(pawn) > 1 or
nx_string(pawn.CharacterMovement.MovementMode) == "MOVEMENT_MODE::Flying" then
return false
end

--calculate left foot offset
local obj_pos_lx, obj_pos_ly, obj_pos_lz = role:GetNodeObjectPosition("Bip01 L Foot")
local position_lx, position_ly, position_lz = role:GetNodeWorldPosition("Bip01 L Foot")
local lgroundheight = terrain:GetGroundHeight(position_lx,position_lz)
local left_offset = position_ly - obj_pos_ly - lgroundheight

--calculate right foot offset
local obj_pos_rx, obj_pos_ry, obj_pos_rz = role:GetNodeObjectPosition("Bip01 R Foot")
local position_rx, position_ry, position_rz = role:GetNodeWorldPosition("Bip01 R Foot")
local rgroundheight = terrain:GetGroundHeight(position_rx,position_rz)
local right_offset = position_ry - obj_pos_ry - rgroundheight

--move mesh component
local y_offset = math.max(left_offset, right_offset) - 0.02
if y_offset > 0 then
pawn.MeshComponent.PositionY = pawn.MeshComponent.PositionY - y_offset * 0.1 --interpolation y_offset
end

local strnewpos = nx_string(position_lx) .. "," .. nx_string(lgroundheight + foot_rel_height) .. "," .. nx_string(position_lz)
local action_index = get_anim_tree_index(role)
if action_index >= 0 then
role:SetAnimTreeValue(action_index, "footlpos", strnewpos)
role:SetAnimTreeValue(action_index, "foot_ik_weight", 1 - get_speed(pawn))
pawn.bool_foot_ik = true
return true
end

return false
end

--Animation tree right foot IK node callback
function animtree_callback_RRootIK(pawn, role)
local terrain = get_terrain(pawn)
if not nx_is_valid(terrain) or is_jumping(pawn) or get_speed(pawn) > 1 or
nx_string(pawn.CharacterMovement.MovementMode) == "MOVEMENT_MODE::Flying" then
return false
end

local position_x, position_y, position_z = role:GetNodeWorldPosition("Bip01 R Foot")
local groundheight = terrain:GetGroundHeight(position_x, position_z)
local strnewpos = nx_string(position_x) .. "," .. nx_string(groundheight + foot_rel_height) .. "," .. nx_string(position_z)
local action_index = get_anim_tree_index(role)

if action_index >= 0 then
role:SetAnimTreeValue(action_index, "footrpos", strnewpos)
role:SetAnimTreeValue(action_index, "foot_ik_weight", 1 - get_speed(pawn))
return true
end

return false
end

animtree_event_callback =
{
["bs_x_direction"] = animtree_callback_bs_x_direction,
["bs_y_speed"] = animtree_callback_bs_y_speed,
["LRootIK"] = animtree_callback_LRootIK,
["RRootIK"] = animtree_callback_RRootIK,
}

function on_animtree_event(mesh_comp, action_index, callback_name)

local pawn = mesh_comp.GameObjectOwner
local role = mesh_comp.VisBase

if not nx_is_valid(pawn) or not nx_is_valid(role) then
return
end

local callback = animtree_event_callback[callback_name]
if callback then
return callback(pawn, role)
end

return false
end


function anim_callee_init(mesh_comp)
nx_callback(mesh_comp, "on_transition_event", "on_transition_event")
nx_callback(mesh_comp, "on_animtree_event", "on_animtree_event")
nx_callback(mesh_comp, "on_action_event", "on_action_event")
nx_callback(mesh_comp, "on_state_machine_state_event", "on_state_machine_state_event")
nx_callback(mesh_comp, "on_state_action_event_begin", "on_state_action_event_begin")
nx_callback(mesh_comp, "on_state_action_event_update", "on_state_action_event_update")
nx_callback(mesh_comp, "on_state_action_event_end", "on_state_action_event_end")
end

--use this for initialization
function on_begin_play(component)
local owner = component.GameObjectOwner

if nx_is_valid(owner) then
local mesh_comp = owner.MeshComponent

if nx_is_valid(mesh_comp) then

owner.meshcomp_init_relative_posy = owner.MeshComponent.RelativePositionY
mesh_comp.ActorCalleeType = "CComponentActorCalleeScript"
nx_bind_script(mesh_comp, nx_current(), "anim_callee_init")
end
end
nx_callback(component, "on_tick", "tick")
end

function tick(component, delta_time)
local owner = component.GameObjectOwner

local move_comp = get_movement_component(owner)
local speedx = move_comp.VelocityX
local speedz = move_comp.VelocityZ
local rate = speedx / speedz
local tan = math.atan(rate)
local velocity_angle = radian * tan

if speedx == 0 and speedz == 0 then
velocity_angle = 0
elseif speedx > 0 and speedz == 0 then
velocity_angle = 90
elseif speedx == 0 and speedz < 0 then
velocity_angle = 180
elseif speedx < 0 and speedz == 0 then
velocity_angle = 270
elseif speedx > 0 and speedz < 0 then
velocity_angle = 180 - math.abs(velocity_angle)
elseif speedx <= 0 and speedz < 0 then
velocity_angle = velocity_angle + 180
elseif speedx < 0 and speedz > 0 then
velocity_angle = 360 - math.abs(velocity_angle)
end

local cap_comp = owner.CapsuleComponent
local foward_angle = cap_comp.AngleY * radian
local relative_angle = velocity_angle - foward_angle

if relative_angle > 180 then
relative_angle = relative_angle - 360
elseif relative_angle < -180 then
relative_angle = relative_angle + 360
end
owner.direction = relative_angle

if nx_find_custom(owner, "bool_foot_ik") and owner.bool_foot_ik == false then
owner.MeshComponent.RelativePositionY = owner.meshcomp_init_relative_posy
end
end

public_attr.lua

--Explorer: Global Variables

MODEL_PATH = "mdl\\"
MODEL_PATH_DX9 = ""
ACTOR_PATH = "ini\\actor\\"
ACTOR_PATH_DX9 = "ini\\actor\\"
LIGHT_PATH = "ini\\light\\"
LIGHT_PATH_DX9 = "ini\\light\\"
EFFECT_PATH = "ini\\effect\\"
PARTICLE_PATH = "ini\\particle\\"
SOUND_PATH = "snd\\"
REVERB_PATH = "reverb\\"
TRIGGER_PATH = "ini\\trigger\\"
PROBE_PATH = "ini\\light_probe\\"
VOLUME_FOG_PATH = "ini\\volume_fog\\"
MATERIAL_PATH = ""

--Resource Type Definitions
TYPE_MODEL = "model"
TYPE_ACTOR = "actor"
TYPE_LIGHT = "light"
TYPE_EFFECT = "effect"
TYPE_PARTICLE = "particle"
TYPE_SOUND = "sound"
TYPE_REVERB = "reverb"
TYPE_TRIGGER = "trigger"
TYPE_PROBE = "light_probe"
TYPE_VOLUME_FOG = "volume_fog"
TYPE_DECAL = "decal"
TYPE_GROUP = "group"
TYPE_RIPPLE = "ripple"
TYPE_SNOW = "snow"
TYPE_RAIN = "rainlayer"
TYPE_UI3D = "ui3d"

--Animation Resource Types
AT_UNKNOWN = 0
AT_SKELETON = 1
AT_SKELETON_AS_ANIMSEQUENCE = 2
AT_ANIMSEQUENCE = 3
AT_MONTAGE = 4
AT_BLENDSPACE = 5
AT_BLENDSPACE1D = 6
AT_AIMOFFSETBLENDSPACE = 7
AT_ANIMTREE = 8

TYPE_LIST = {
"model",
"actor",
"light",
"effect",
"particle",
"decal",
"sound",
"reverb",
"trigger",
"light_probe",
"volume_fog",
"ripple",
"snow",
"rainlayer",
}

FORM_TREE_BROWSER = "common_form\\form_tree_browser"
SEARCH_PATH = "ini\\common_form\\form_tree_browser\\"
SEARCH_CONFIG = SEARCH_PATH .. "form_put_visual.ini"
SEARCH_CONFIG_TEMP = "cache\\common_form\\form_tree_browser\\form_put_visual_temp.ini"
GROUP_CONFIG = "ter\\visual_group.ini"

--Get the visual_put file
function get_search_file()
local ini = nx_create("IniDocument")

ini.FileName = nx_resource_path() .. SEARCH_CONFIG

if not ini:LoadFromFile() then
nx_destroy(ini)
return 0
end

local file = ini:ReadString("SEARCH_CONFIG", "search_file", "form_put_visual.ini")

nx_destroy(ini)

return nx_resource_path() .. SEARCH_PATH .. file, file
end

scene_utils.lua

--scene utils script

function scene_get_engine_terrain(lscene)
local level = lscene.PersistentLevel

if nx_is_valid(level) then
local lterrain = level:GetGameObjectByType("fx_component_terrain.LTerrain")

if nx_is_valid(lterrain) then
return lterrain.EngineTerrain
end
end

return nx_null()
end

Final Result

Click the PIE button to see the effect. When the character walks up the slope, the soles of its feet are always close to the ground.

Preview