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.
After adding the node successfully:
The pose that needs to be solved by TwoBoneIK can be connected to the input pin of the TwoBoneIK node.
Properties
After selecting a node, you can see properties of the node in the Property Window.
Property | Description |
---|---|
Tip Bone | The marked IK tip bone. |
Callback String | Execute the callback string. When the callback string returns false, the node function will not be executed. |
Target Point Name | Mark the target point position of the bone. |
Joint Point Name | The joint point position. |
Alpha Name | Specify the name of the animation's float parameter, and use the parameter value as the blend weight. |
Target Rotate Name | Specify 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 Target | Whether to rotate based on the target point. |
Take Rotation From Parent | Whether to rotate based on the parent bone. |
Allow Stretch | Whether the bone allows stretching. |
Stretch Start | Stretch start ratio. |
Stretch Max | Maximum stretch ratio. |
Allow Rotate | Maximum 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 |
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.
In the Animation Parameter window, click the 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.
After creating the parameters:
Select the two TwoBoneIK nodes separately and set the node properties in the Property Window.
Click the Save button after editing the Animation Tree.
Creating the Terrain
Click Create Game Object -> Visual -> Terrain Object in the Component Editor.
Set the properties of the created terrain in the pop-up Initial Edit window and click OK to complete the creation.
Create a slope. Click Windows -> Edit Mode -> Terrain Mode, then select Height Edit [Alt+1] in the opened Terrain Editor.
Then in the Level panel, left-click the ground to raise the ground.
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.
- First, when the terrain object does not exist in the scene,or the character's speed is over 1, return directly without creating IK.
- 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.