Skip to main content

TwoBoneIK

最近更新时间:2023-09-08

概述

使用TwoBoneIK通过反向运动学(Inverse Kinematic)控制两根骨骼来完成指定动作,用于实现角色动作与环境的交互。

节点使用

在动画树空白处右键打开节点选择面板,展开动画后处理(Animation Post-processing)分类并从中选取添加TwoBoneIK(Add TwoBoneIK)

image-20230717160135209

成功添加节点:

image-20220909135108666

可以将需要TwoBoneIK解算的姿势连至其输入引脚。

image-20230215144733760

属性

选中节点后,在属性窗口(Property Window)中可看到该节点的各项属性。

image-20220909160146456

属性说明
末端骨骼(Tip Bone)标记的IK末梢骨骼。
回调字符串(Callback String)执行回调字符串,当回调字符串返回false时,不会执行该节点功能。
目标点名称(Target Point Name)标记骨骼的目标点位置。
链接点名称(Joint Point Name)链接点位置。
混合名称(Alpha Name)指定动画浮点参数名称,用该参数值作为混合权重。
目标旋转名称(Target Rotate Name)指定动画参数名称(字符串或者向量类型),用该参数值作为目标点的旋转,其中字符串类型需要用逗号将旋转分量隔开。
旋转是否根据目标(Rotate From Target)是否根据目标点旋转。
使用父空间的旋转(Take Rotation From Parent)是否根据父骨骼旋转。
允许拉伸(Allow Stretch)骨骼是否允许拉伸。
拉伸起始比率(Stretch Start)拉伸起始比率。
拉伸最大比率(Stretch Max)拉伸最大比率。
允许旋转(Allow Rotate)目标骨骼旋转最大角度。

示例

示例实现了角色左右脚在静止状态下的IK,可使角色在不同坡度的地面上,脚部与地面保持贴合。

胶囊体在斜坡上的时候会处于悬空状态,在该角色的动画树中添加TwoBoneIK节点,然后通过动画树回调函数,可以使得角色在斜坡时双脚都贴合地面。下图中角色在站在同一位置上,使用TwoBoneIK节点,可通过控制膝盖弯曲方向,并根据足部与地面的高度,膝盖不定程度的弯曲来使足部与地面保持贴合。


使用TwoBoneIK节点

未使用TwoBoneIK节点

本示例是在动画状态机示例的基础上增加为角色足部添加IK,在创建动画树与绑定脚本步骤中需增加对应的功能。

动画树

在动画树中添加两个TwoBoneIK节点用于左右脚的IK,并按下图将节点链接到一起。

image-20230718112854348

动画参数(Animation Parameter)窗口中,点击 image-20230718113319832 按钮,选择字符串(String)浮点(Float),创建四个字符串型参数footlpos、footljpos、footrpos、footrjpos和一个浮点型参数foot_ik_weight,分别用于两个节点的目标点名称(Target Point Name)链接点名称(Joint Point Name)属性和混合名称(Alpha Name)

image-20230718113423510

参数创建完成:

image-20230718131713373

分别选中两个TwoBoneIK节点,在属性窗口(Property Window)中设置节点属性。

image-20230718132110788

image-20230718132033940

编辑完动画树后,点击保存(Save)按钮。

image-20230718171408236

创建地形

在组件编辑器中依次点击创建游戏对象(Create Game Object) -> 可视(Visual) -> 地形对象(Terrain Object)

image-20230718172400854

在弹出的初始编辑(Initial Edit)窗口中设置所创建的地形属性,然后点击确定(OK)完成创建。

image-20230718172743708

创建斜坡。依次点击菜单栏窗口(Windows) -> 编辑模式(Edit Mode) -> 地形模式(Terrain Mode)。在打开的地形编辑器(Terrain Editor)面板中选中高度编辑[Alt+1](Height Edit [Atl+1]

image-20230718174330051

然后在关卡(Level)面板中左击地面抬高地面。

EditTerrain

绑定脚本

与动画状态机示例中的绑定脚本一样,为角色绑定两个脚本robot_character.lua和robot_character_animation.lua。

通过动画树回调函数调用animtree_callback_LRootIK和animtree_callback_RRootIK。

  1. 首先判断当场景地形对象不存在或者角色速度大于1时,直接返回不做IK。
  2. 分别获得左脚和右脚距离地面的偏移。

获得脚骨骼的世界空间坐标和模型空间坐标,地形对象通过世界空间的XZ坐标得到脚下地面的高度。世界空间高度减去模型空间高度减去地面高度得到脚骨骼期望的偏移。

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

将两脚的期望偏移算出后取最大值,用来移动骨架网格组件,使角色向下移动,使距离地面最高的脚贴住地面。

系数0.1为插值系数,防止在角色向下位移时的出现跳变。

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

重新计算脚骨骼期望位移,设置动画树脚目标位置(footpos)参数和权重(foot_ik_weight)。

其中混合权重和角色速度成反比,当角色速度为1时为0,速度为0时为1。另一只脚同理,但是不需要计算最大期望位移和移动骨架网格组件。

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))

因为移动了网格组件,在角色再次移动时,需要使网格组件相对位置复原。

在动画树的混合空间(Blend Space)节点的回调中,设置角色的自定义属性bool_foot_ik为false。

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

在IK节点的回调中,设置bool_foot_ik为true。

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

在on_begin_play函数中记录网格组件的相对位移。

owner.meshcomp_init_relative_posy = owner.MeshComponent.RelativePositionY

在tick函数中判断并复原网格组件的相对位移。

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

以下为参考脚本:

robot_character.lua

--script template

local IE_Pressed = 0 --按键按下
local IE_Released = 1 --按键抬起
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

--获取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

--向前移动回调
function on_moveforward(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--获得控制器Y轴角度
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--获得向前方向
local x, y, z = nx_function("ext_angle_get_forward_vector", 0.0, yaw, 0.0)

--增加移动量

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

return 1
end

--向右移动回调
function on_moveright(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--获得控制器Y轴角度
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--获得向右方向
local x, y, z = nx_function("ext_angle_get_right_vector", 0.0, yaw, 0.0)

--增加移动量
owner:AddMovementInput(x, y, z, axis_value)
end
return 1
end

--左右旋转(绕Y轴旋转)回调
function on_turn(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

owner:AddControllerYawInput(axis_value)

return 1
end

--上下旋转(绕X轴旋转)回调
function on_lookup(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

owner:AddControllerPitchInput(-axis_value)

return 1
end

--跳跃键按下回调
function on_jump_pressed(owner)
if nx_find_custom(owner, "JumpAgainLock") and owner.JumpAgainLock then
return 1
end

--角色跳跃
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

--资源管理器:全局变量

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 = ""

--资源类型定义
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"

--动画资源类型
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"

--获取visual_put文件
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

最终结果

点击运行后可查看效果,角色走到斜坡上后,脚底部始终与地面贴合。

Preview