|
import * as THREE from 'three' |
|
import { Bone, Object3D } from 'three' |
|
import * as SkeletonUtils from 'three/examples/jsm/utils/SkeletonUtils' |
|
import type { TupleToUnion } from 'type-fest' |
|
import { |
|
FindObjectItem, |
|
GetLocalPosition, |
|
GetWorldPosition, |
|
} from './utils/three-utils' |
|
import { CCDIKSolver } from './utils/CCDIKSolver' |
|
import { |
|
BoneThickness, |
|
ConnectColor, |
|
ToHexColor, |
|
GetColorOfLinkByName, |
|
OpenposeKeypoints, |
|
OpenposeyKeypointsConst, |
|
PartIndexMappingOfBlazePoseModel, |
|
PartIndexMappingOfPoseModel, |
|
} from './defines' |
|
import { |
|
ExtremitiesMapping, |
|
FootObject, |
|
HandObject, |
|
IsMatchBonePrefix, |
|
} from './models' |
|
|
|
function GetPresetColorOfJoint(name: string) { |
|
const index = OpenposeKeypoints.indexOf(name) |
|
return index !== -1 ? ToHexColor(ConnectColor[index]) : 0x0 |
|
} |
|
|
|
function CreateLink( |
|
startObject: THREE.Object3D, |
|
startName: string, |
|
endName: string |
|
) { |
|
const presetColor = GetColorOfLinkByName(startName, endName) |
|
const material = new THREE.MeshBasicMaterial({ |
|
color: presetColor ?? 0x0, |
|
opacity: 0.6, |
|
transparent: true, |
|
}) |
|
const mesh = new THREE.Mesh( |
|
new THREE.SphereGeometry(BoneThickness), |
|
material |
|
) |
|
mesh.name = startName + '_link_' + endName |
|
startObject.add(mesh) |
|
return mesh |
|
} |
|
|
|
function UpdateJointSphere(obj: Object3D, thickness = BoneThickness) { |
|
const name = obj.name + '_joint_sphere' |
|
obj.getObjectByName(name)?.scale.setScalar(thickness) |
|
} |
|
|
|
function UpdateLink4( |
|
startObject: Object3D, |
|
endObject: Object3D, |
|
startName: string, |
|
endName: string, |
|
thickness = BoneThickness, |
|
create = false |
|
) { |
|
const startPosition = new THREE.Vector3(0, 0, 0) |
|
const endPostion = endObject.position |
|
const distance = startPosition.distanceTo(endPostion) |
|
|
|
const origin = startPosition.clone().add(endPostion).multiplyScalar(0.5) |
|
|
|
|
|
|
|
const v = endPostion.clone().sub(startPosition) |
|
const unit = new THREE.Vector3(1, 0, 0) |
|
const axis = unit.clone().cross(v) |
|
const angle = unit.clone().angleTo(v) |
|
const mesh = create |
|
? CreateLink(startObject, startName, endName) |
|
: startObject.getObjectByName(startName + '_link_' + endName)! |
|
mesh.scale.copy( |
|
new THREE.Vector3( |
|
distance / 2, |
|
thickness / BoneThickness, |
|
thickness / BoneThickness |
|
) |
|
) |
|
mesh.position.copy(origin) |
|
mesh.setRotationFromAxisAngle(axis.normalize(), angle) |
|
|
|
UpdateJointSphere(startObject, thickness) |
|
UpdateJointSphere(endObject, thickness) |
|
} |
|
|
|
function UpdateLink2( |
|
startObject: Object3D, |
|
endObject: Object3D, |
|
thickness = BoneThickness, |
|
create = false |
|
) { |
|
UpdateLink4( |
|
startObject, |
|
endObject, |
|
startObject.name, |
|
endObject.name, |
|
thickness, |
|
create |
|
) |
|
} |
|
|
|
function CreateSphere(name: string, thickness: number, color: number) { |
|
const sphere = new THREE.Mesh( |
|
new THREE.SphereGeometry(thickness), |
|
new THREE.MeshBasicMaterial({ color: color }) |
|
) |
|
sphere.name = name |
|
return sphere |
|
} |
|
|
|
function CreateGroup(name: string, x = 0, y = 0, z = 0) { |
|
const object = new THREE.Group() |
|
object.name = name |
|
object.translateX(x) |
|
object.translateY(y) |
|
object.translateZ(z) |
|
return object |
|
} |
|
|
|
function CreateJoint( |
|
name: string, |
|
x = 0, |
|
y = 0, |
|
z = 0, |
|
thickness: number = BoneThickness |
|
) { |
|
return CreateGroup(name, x, y, z).add( |
|
CreateSphere( |
|
name + '_joint_sphere', |
|
thickness, |
|
GetPresetColorOfJoint(name) |
|
) |
|
) |
|
} |
|
|
|
let templateBody: THREE.Group | null = null |
|
|
|
export function CloneBody() { |
|
if (templateBody) { |
|
return SkeletonUtils.clone(templateBody) |
|
} |
|
return null |
|
} |
|
|
|
const HandScale = 2.2 |
|
const FootScale = 0.25 |
|
|
|
function CreateHand(name: 'right_hand' | 'left_hand') { |
|
if (!HandObject || !FootObject) { |
|
throw new Error('Failed to create body') |
|
} |
|
|
|
if (name === 'right_hand') { |
|
const right_hand = SkeletonUtils.clone(HandObject) |
|
|
|
right_hand.name = 'right_hand' |
|
right_hand.translateX(-0.4) |
|
right_hand.translateY(3) |
|
right_hand.rotateY(Math.PI) |
|
right_hand.rotateZ(-Math.PI / 2) |
|
|
|
right_hand.scale.multiplyScalar(HandScale) |
|
|
|
right_hand.traverse((o) => { |
|
if (IsBone(o.name)) { |
|
o.name = o.name + '_R' |
|
} |
|
}) |
|
return right_hand |
|
} else { |
|
const left_hand = SkeletonUtils.clone(HandObject) |
|
|
|
left_hand.name = 'left_hand' |
|
left_hand.scale.x = -1 |
|
left_hand.translateX(0.4) |
|
left_hand.translateY(3) |
|
left_hand.rotateY(Math.PI) |
|
left_hand.rotateZ(Math.PI / 2) |
|
left_hand.scale.multiplyScalar(HandScale) |
|
|
|
left_hand.traverse((o) => { |
|
if (IsBone(o.name)) { |
|
o.name = o.name + '_L' |
|
} |
|
}) |
|
return left_hand |
|
} |
|
} |
|
|
|
function CreateFoot(name: 'right_foot' | 'left_foot') { |
|
if (!HandObject || !FootObject) { |
|
throw new Error('Failed to create body') |
|
} |
|
|
|
if (name === 'right_foot') { |
|
const right_foot = SkeletonUtils.clone(FootObject) |
|
|
|
right_foot.name = 'right_foot' |
|
right_foot.scale.setX(-1) |
|
right_foot.scale.multiplyScalar(FootScale) |
|
|
|
right_foot.traverse((o) => { |
|
if (IsBone(o.name)) { |
|
o.name = o.name + '_R' |
|
} |
|
}) |
|
|
|
return right_foot |
|
} else { |
|
const left_foot = SkeletonUtils.clone(FootObject) |
|
left_foot.name = 'left_foot' |
|
left_foot.scale.multiplyScalar(FootScale) |
|
|
|
left_foot.traverse((o) => { |
|
if (IsBone(o.name)) { |
|
o.name = o.name + '_L' |
|
} |
|
}) |
|
return left_foot |
|
} |
|
} |
|
|
|
export function CreateTemplateBody() { |
|
if (!HandObject || !FootObject) { |
|
throw new Error('Failed to create body') |
|
} |
|
const width = 34 |
|
const height = 46 |
|
|
|
const torso = CreateGroup('torso', 0, 115, 0).add( |
|
CreateSphere('center', BoneThickness, 0x888888), |
|
CreateGroup('five', 0, height / 2, 0).add( |
|
CreateJoint('neck').add( |
|
CreateJoint('nose', 0, 20, 14).add( |
|
CreateJoint('right_eye', -3, 3, -3).add( |
|
CreateJoint('right_ear', -4, -3, -8) |
|
), |
|
CreateJoint('left_eye', 3, 3, -3).add( |
|
CreateJoint('left_ear', 4, -3, -8) |
|
) |
|
) |
|
), |
|
CreateGroup('left_shoulder_inner').add( |
|
CreateJoint('right_shoulder', -width / 2, 0, 0).add( |
|
CreateJoint('right_elbow', 0, -25, 0).add( |
|
CreateJoint('right_wrist', 0, -25, 0).add( |
|
CreateHand('right_hand') |
|
) |
|
) |
|
) |
|
), |
|
CreateGroup('right_shoulder_inner').add( |
|
CreateJoint('left_shoulder', width / 2, 0, 0).add( |
|
CreateJoint('left_elbow', 0, -25, 0).add( |
|
CreateJoint('left_wrist', 0, -25, 0).add( |
|
CreateHand('left_hand') |
|
) |
|
) |
|
) |
|
), |
|
CreateGroup('right_hip_inner').add( |
|
CreateJoint('right_hip', -width / 2 + 7, -height, 0).add( |
|
CreateJoint('right_knee', 0, -40, 0).add( |
|
CreateJoint('right_ankle', 0, -36, 0).add( |
|
CreateFoot('right_foot') |
|
) |
|
) |
|
) |
|
), |
|
CreateGroup('left_hip_inner').add( |
|
CreateJoint('left_hip', width / 2 - 7, -height, 0).add( |
|
CreateJoint('left_knee', 0, -40, 0).add( |
|
CreateJoint('left_ankle', 0, -36, 0).add( |
|
CreateFoot('left_foot') |
|
) |
|
) |
|
) |
|
) |
|
) |
|
) |
|
|
|
new BodyControlor(torso).Create() |
|
templateBody = torso |
|
} |
|
|
|
function CreateIKTarget(body: Object3D, effector: Object3D, name: string) { |
|
const target = new THREE.Mesh( |
|
new THREE.BoxGeometry( |
|
BoneThickness * 5, |
|
BoneThickness * 5, |
|
BoneThickness * 5 |
|
), |
|
new THREE.MeshBasicMaterial({ |
|
color: 0x0088ff, |
|
transparent: true, |
|
opacity: 0.5, |
|
}) |
|
) |
|
|
|
const effector_pos = GetWorldPosition(effector) |
|
target.position.copy(GetLocalPosition(body, effector_pos)) |
|
target.name = name |
|
|
|
return target |
|
} |
|
|
|
export function GetExtremityMesh(o: Object3D) { |
|
if (!(o.name in ExtremitiesMapping)) { |
|
return null |
|
} |
|
return FindObjectItem<THREE.SkinnedMesh>( |
|
o, |
|
ExtremitiesMapping[o.name].meshName |
|
) |
|
} |
|
|
|
export function IsVirtualPoint(name: string) { |
|
return [ |
|
'right_shoulder_inner', |
|
'left_shoulder_inner', |
|
'right_hip_inner', |
|
'left_hip_inner', |
|
'five', |
|
].includes(name) |
|
} |
|
|
|
export function IsNeedSaveObject(name: string) { |
|
if (OpenposeKeypoints.includes(name)) return true |
|
if (name === 'right_hand' || name === 'left_hand') return true |
|
if (name === 'right_foot' || name === 'left_foot') return true |
|
if (IsVirtualPoint(name)) return true |
|
if (IsBone(name)) return true |
|
if (name.includes('_joint_sphere')) return true |
|
if (name.includes('_link_')) return true |
|
return false |
|
} |
|
|
|
export function IsBone(name: string) { |
|
return IsMatchBonePrefix(name) |
|
} |
|
|
|
const pickableObjectNames: string[] = [ |
|
'torso', |
|
'nose', |
|
'neck', |
|
'right_shoulder', |
|
'left_shoulder', |
|
'right_elbow', |
|
'left_elbow', |
|
'right_hip', |
|
'left_hip', |
|
'right_knee', |
|
'left_knee', |
|
|
|
'right_shoulder_inner', |
|
'left_shoulder_inner', |
|
'right_hip_inner', |
|
'left_hip_inner', |
|
'left_wrist_target', |
|
'right_wrist_target', |
|
'left_ankle_target', |
|
'right_ankle_target', |
|
] |
|
|
|
export function IsPickable(name: string, isFreeMode = false) { |
|
if (isFreeMode && OpenposeKeypoints.includes(name)) return true |
|
if (pickableObjectNames.includes(name)) return true |
|
if (IsBone(name)) return true |
|
return false |
|
} |
|
|
|
export function IsTranslate(name: string, isFreeMode = false) { |
|
if (isFreeMode) |
|
return ( |
|
[ |
|
'right_shoulder_inner', |
|
'left_shoulder_inner', |
|
'right_hip_inner', |
|
'left_hip_inner', |
|
'neck', |
|
].includes(name) == false |
|
) |
|
|
|
if (name.endsWith('_target')) return true |
|
return false |
|
} |
|
|
|
export function IsTarget(name: string) { |
|
if (name.endsWith('_target')) return true |
|
return false |
|
} |
|
|
|
export function IsHand(name: string) { |
|
return ['left_hand', 'right_hand'].includes(name) |
|
} |
|
|
|
export function IsFoot(name: string) { |
|
return ['left_foot', 'right_foot'].includes(name) |
|
} |
|
|
|
export function IsMask(name: string) { |
|
return ['foot_mask', 'hand_mask'].includes(name) |
|
} |
|
|
|
export function IsSkeleton(name: string) { |
|
if (name == 'torso') return true |
|
if (OpenposeKeypoints.includes(name)) return true |
|
if (IsVirtualPoint(name)) return true |
|
if (name.includes('_joint_sphere')) return true |
|
if (name.includes('_link_')) return true |
|
return false |
|
} |
|
|
|
export function IsExtremities(name: string) { |
|
return ['left_hand', 'right_hand', 'left_foot', 'right_foot'].includes(name) |
|
} |
|
|
|
const ControlablePart = [ |
|
...OpenposeyKeypointsConst, |
|
'left_shoulder_inner', |
|
'right_shoulder_inner', |
|
'left_hip_inner', |
|
'right_hip_inner', |
|
'five', |
|
'right_hand', |
|
'left_hand', |
|
'left_foot', |
|
'right_foot', |
|
'torso', |
|
|
|
'left_wrist_target', |
|
'right_wrist_target', |
|
'left_ankle_target', |
|
'right_ankle_target', |
|
] as const |
|
|
|
type ControlPartName = TupleToUnion<typeof ControlablePart> |
|
|
|
export interface BodyData { |
|
position?: ReturnType<THREE.Vector3['toArray']> |
|
rotation?: ReturnType<THREE.Euler['toArray']> |
|
scale?: ReturnType<THREE.Vector3['toArray']> |
|
|
|
child: Record< |
|
string, |
|
{ |
|
position?: ReturnType<THREE.Vector3['toArray']> |
|
rotation?: ReturnType<THREE.Euler['toArray']> |
|
scale?: ReturnType<THREE.Vector3['toArray']> |
|
} |
|
> |
|
} |
|
|
|
export interface HandData { |
|
child: Record< |
|
string, |
|
{ |
|
position?: ReturnType<THREE.Vector3['toArray']> |
|
rotation?: ReturnType<THREE.Euler['toArray']> |
|
scale?: ReturnType<THREE.Vector3['toArray']> |
|
} |
|
> |
|
} |
|
|
|
export class BodyControlor { |
|
body: Object3D |
|
part: Record<ControlPartName, Object3D> = {} as any |
|
constructor(o: Object3D) { |
|
this.body = o |
|
this.body.traverse((o) => { |
|
if (ControlablePart.includes(o.name as ControlPartName)) { |
|
this.part[o.name as ControlPartName] = o |
|
} |
|
}) |
|
|
|
this.part['left_shoulder_inner'] = this.getObjectByName( |
|
'left_shoulder_inner' |
|
) |
|
this.part['right_shoulder_inner'] = this.getObjectByName( |
|
'right_shoulder_inner' |
|
) |
|
this.part['left_hip_inner'] = this.getObjectByName('left_hip_inner') |
|
this.part['right_hip_inner'] = this.getObjectByName('right_hip_inner') |
|
this.part['five'] = this.getObjectByName('five') |
|
this.part['right_hand'] = this.getObjectByName('right_hand') |
|
this.part['left_hand'] = this.getObjectByName('left_hand') |
|
this.part['right_foot'] = this.getObjectByName('right_foot') |
|
this.part['left_foot'] = this.getObjectByName('left_foot') |
|
this.part['torso'] = this.body |
|
} |
|
|
|
getObjectByName(name: string) { |
|
const part = this.body.getObjectByName(name) |
|
|
|
if (!part) throw new Error(`Not found part: ${name}`) |
|
|
|
return part |
|
} |
|
getWorldPosition(o: Object3D) { |
|
const pos = new THREE.Vector3() |
|
o.getWorldPosition(pos) |
|
return pos |
|
} |
|
|
|
UpdateLink( |
|
name: ControlPartName, |
|
thickness = this.BoneThickness, |
|
create = false |
|
) { |
|
if ( |
|
[ |
|
'left_hip', |
|
'right_hip', |
|
'right_shoulder', |
|
'left_shoulder', |
|
].includes(name) |
|
) |
|
UpdateLink4( |
|
this.part[`${name}_inner` as ControlPartName], |
|
this.part[name], |
|
'neck', |
|
name, |
|
thickness, |
|
create |
|
) |
|
else if (name !== 'neck' && OpenposeKeypoints.includes(name)) { |
|
UpdateLink2( |
|
this.part[name].parent!, |
|
this.part[name], |
|
thickness, |
|
create |
|
) |
|
} |
|
} |
|
|
|
get HeadSize() { |
|
const size = this.getWorldPosition(this.part['right_ear']).distanceTo( |
|
this.getWorldPosition(this.part['left_ear']) |
|
) |
|
return size |
|
} |
|
|
|
set HeadSize(value: number) { |
|
const scale = value / this.HeadSize |
|
|
|
const earLength = this.part['left_ear'].position.length() * scale |
|
const eyeLength = this.part['left_eye'].position.length() * scale |
|
|
|
this.part['left_ear'].position.normalize().multiplyScalar(earLength) |
|
this.part['right_ear'].position.normalize().multiplyScalar(earLength) |
|
this.part['left_eye'].position.normalize().multiplyScalar(eyeLength) |
|
this.part['right_eye'].position.normalize().multiplyScalar(eyeLength) |
|
|
|
this.UpdateLink('left_eye') |
|
this.UpdateLink('right_eye') |
|
this.UpdateLink('left_ear') |
|
this.UpdateLink('right_ear') |
|
} |
|
get NoseToNeck() { |
|
return this.part['nose'].position.length() |
|
} |
|
set NoseToNeck(value: number) { |
|
this.part['nose'].position.normalize().multiplyScalar(value) |
|
this.UpdateLink('nose') |
|
} |
|
get ShoulderToHip() { |
|
return this.getDistanceOf( |
|
this.getWorldPosition(this.part['five']), |
|
this.getMidpoint( |
|
this.getWorldPosition(this.part['left_hip']), |
|
this.getWorldPosition(this.part['right_hip']) |
|
) |
|
) |
|
} |
|
set ShoulderToHip(value: number) { |
|
const origin = this.ShoulderToHip |
|
|
|
this.part['five'].position.normalize().multiplyScalar(value / 2) |
|
this.part['left_hip'].position.multiplyScalar(value / origin) |
|
this.part['right_hip'].position.multiplyScalar(value / origin) |
|
|
|
this.UpdateLink('left_hip') |
|
this.UpdateLink('right_hip') |
|
} |
|
get ShoulderWidth() { |
|
return this.part['left_shoulder'].position.distanceTo( |
|
this.part['right_shoulder'].position |
|
) |
|
} |
|
set ShoulderWidth(width: number) { |
|
const right_shoulder = this.part['right_shoulder'] |
|
right_shoulder.position.x = -width / 2 |
|
const left_shoulder = this.part['left_shoulder'] |
|
left_shoulder.position.x = width / 2 |
|
|
|
this.UpdateLink('right_shoulder') |
|
this.UpdateLink('left_shoulder') |
|
} |
|
|
|
get UpperArm() { |
|
return this.part['left_elbow'].position.length() |
|
} |
|
set UpperArm(length: number) { |
|
this.part['left_elbow'].position.normalize().multiplyScalar(length) |
|
this.part['right_elbow'].position.normalize().multiplyScalar(length) |
|
this.UpdateLink('left_elbow') |
|
this.UpdateLink('right_elbow') |
|
} |
|
get Forearm() { |
|
return this.part['left_wrist'].position.length() |
|
} |
|
set Forearm(length: number) { |
|
this.part['left_wrist'].position.normalize().multiplyScalar(length) |
|
this.part['right_wrist'].position.normalize().multiplyScalar(length) |
|
|
|
this.UpdateLink('left_wrist') |
|
this.UpdateLink('right_wrist') |
|
} |
|
|
|
get ArmLength() { |
|
return this.UpperArm + this.Forearm |
|
} |
|
set ArmLength(length: number) { |
|
const origin = this.ArmLength |
|
this.UpperArm = (length * this.UpperArm) / origin |
|
this.Forearm = (length * this.Forearm) / origin |
|
} |
|
|
|
get Thigh() { |
|
return this.part['left_knee'].position.length() |
|
} |
|
set Thigh(length: number) { |
|
this.part['left_knee'].position.normalize().multiplyScalar(length) |
|
this.part['right_knee'].position.normalize().multiplyScalar(length) |
|
|
|
this.UpdateLink('left_knee') |
|
this.UpdateLink('right_knee') |
|
} |
|
|
|
get HandSize() { |
|
return Math.abs(this.part['left_hand'].scale.x) / HandScale |
|
} |
|
set HandSize(size: number) { |
|
const origin = this.HandSize |
|
this.part['left_hand'].scale |
|
.divideScalar(origin * HandScale) |
|
.multiplyScalar(size * HandScale) |
|
this.part['right_hand'].scale |
|
.divideScalar(origin * HandScale) |
|
.multiplyScalar(size * HandScale) |
|
} |
|
|
|
get Hips() { |
|
return this.getDistanceOf( |
|
this.getWorldPosition(this.part['left_hip']), |
|
this.getWorldPosition(this.part['right_hip']) |
|
) |
|
} |
|
set Hips(width: number) { |
|
const left = this.getWorldPosition(this.part['left_hip']) |
|
const right = this.getWorldPosition(this.part['right_hip']) |
|
|
|
const mid = this.getMidpoint(left, right) |
|
|
|
|
|
|
|
const newLeft = left |
|
.sub(mid) |
|
.normalize() |
|
.multiplyScalar(width / 2.0) |
|
.add(mid) |
|
const newRight = right |
|
.sub(mid) |
|
.normalize() |
|
.multiplyScalar(width / 2.0) |
|
.add(mid) |
|
|
|
this.setPositionFromWorld(this.part['left_hip'], newLeft) |
|
this.setPositionFromWorld(this.part['right_hip'], newRight) |
|
|
|
this.UpdateLink('left_hip') |
|
this.UpdateLink('right_hip') |
|
} |
|
get LowerLeg() { |
|
return this.part['left_ankle'].position.length() |
|
} |
|
set LowerLeg(length: number) { |
|
this.part['left_ankle'].position.normalize().multiplyScalar(length) |
|
this.part['right_ankle'].position.normalize().multiplyScalar(length) |
|
|
|
this.UpdateLink('left_ankle') |
|
this.UpdateLink('right_ankle') |
|
} |
|
|
|
get LegLength() { |
|
return this.Thigh + this.LowerLeg |
|
} |
|
set LegLength(length: number) { |
|
const origin = this.LegLength |
|
this.Thigh = (length * this.Thigh) / origin |
|
this.LowerLeg = (length * this.LowerLeg) / origin |
|
} |
|
|
|
get FootSize() { |
|
return Math.abs(this.part['left_foot'].scale.x) / FootScale |
|
} |
|
set FootSize(size: number) { |
|
const origin = this.FootSize |
|
this.part['left_foot'].scale |
|
.divideScalar(origin * FootScale) |
|
.multiplyScalar(size * FootScale) |
|
this.part['right_foot'].scale |
|
.divideScalar(origin * FootScale) |
|
.multiplyScalar(size * FootScale) |
|
} |
|
getLocalPosition(obj: Object3D, postion: THREE.Vector3) { |
|
return obj.worldToLocal(postion.clone()) |
|
} |
|
|
|
setPositionFromWorld(obj: Object3D, postion: THREE.Vector3) { |
|
return obj.position.copy(this.getLocalPosition(obj.parent!, postion)) |
|
} |
|
|
|
getDirectionVectorByParentOf( |
|
name: ControlPartName, |
|
from: THREE.Vector3, |
|
to: THREE.Vector3 |
|
) { |
|
const parent = this.part[name].parent! |
|
const localFrom = this.getLocalPosition(parent, from) |
|
const localTo = this.getLocalPosition(parent, to) |
|
|
|
return localTo.clone().sub(localFrom).normalize() |
|
} |
|
|
|
rotateTo(name: ControlPartName, dir: THREE.Vector3) { |
|
const obj = this.part[name] |
|
const unit = obj.position.clone().normalize() |
|
const axis = unit.clone().cross(dir) |
|
const angle = unit.clone().angleTo(dir) |
|
obj.parent?.rotateOnAxis(axis.normalize(), angle) |
|
} |
|
|
|
rotateTo3(name: ControlPartName, from: THREE.Vector3, to: THREE.Vector3) { |
|
this.rotateTo(name, this.getDirectionVectorByParentOf(name, from, to)) |
|
} |
|
|
|
setPositionByDistance( |
|
name: ControlPartName, |
|
from: THREE.Vector3, |
|
to: THREE.Vector3 |
|
) { |
|
const dis = this.getDistanceOf(from, to) |
|
this.part[name].position.normalize().multiplyScalar(dis) |
|
} |
|
|
|
setDirectionVector(name: ControlPartName, v: THREE.Vector3) { |
|
const len = this.part[name].position.length() |
|
this.part[name].position.copy(v).multiplyScalar(len) |
|
this.UpdateLink(name) |
|
} |
|
|
|
getDistanceOf(from: THREE.Vector3, to: THREE.Vector3) { |
|
return from.distanceTo(to) |
|
} |
|
|
|
getMidpoint(from: THREE.Vector3, to: THREE.Vector3) { |
|
return from.clone().add(to).multiplyScalar(0.5) |
|
} |
|
ResetPose() { |
|
templateBody?.traverse((o) => { |
|
if (o.name in this.part) { |
|
const name = o.name as ControlPartName |
|
|
|
if (name == 'torso') this.part[name].position.setY(o.position.y) |
|
else this.part[name].position.copy(o.position) |
|
|
|
this.part[name].rotation.copy(o.rotation) |
|
this.part[name].scale.copy(o.scale) |
|
this.UpdateLink(name) |
|
} |
|
}) |
|
} |
|
|
|
SetBlazePose(rawData: [number, number, number][]) { |
|
this.ResetPose() |
|
|
|
const data = Object.fromEntries( |
|
Object.entries(PartIndexMappingOfBlazePoseModel).map( |
|
([name, index]) => { |
|
return [ |
|
name, |
|
new THREE.Vector3().fromArray( |
|
rawData[index] ?? [0, 0, 0] |
|
), |
|
] |
|
} |
|
) |
|
) as Record< |
|
keyof typeof PartIndexMappingOfBlazePoseModel, |
|
THREE.Vector3 |
|
> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const map: [ |
|
ControlPartName, |
|
[ |
|
THREE.Vector3 | keyof typeof PartIndexMappingOfBlazePoseModel, |
|
THREE.Vector3 | keyof typeof PartIndexMappingOfBlazePoseModel |
|
] |
|
][] = [ |
|
[ |
|
'five', |
|
[ |
|
this.getMidpoint( |
|
this.getMidpoint(data['left_hip'], data['right_hip']), |
|
this.getMidpoint( |
|
data['left_shoulder'], |
|
data['right_shoulder'] |
|
) |
|
), |
|
this.getMidpoint( |
|
data['left_shoulder'], |
|
data['right_shoulder'] |
|
), |
|
], |
|
], |
|
|
|
[ |
|
'left_shoulder', |
|
[ |
|
this.getMidpoint( |
|
data['left_shoulder'], |
|
data['right_shoulder'] |
|
), |
|
'left_shoulder', |
|
], |
|
], |
|
['left_elbow', ['left_shoulder', 'left_elbow']], |
|
['left_wrist', ['left_elbow', 'left_wrist']], |
|
[ |
|
'left_hip', |
|
[ |
|
this.getMidpoint( |
|
data['left_shoulder'], |
|
data['right_shoulder'] |
|
), |
|
'left_hip', |
|
], |
|
], |
|
['left_knee', ['left_hip', 'left_knee']], |
|
['left_ankle', ['left_knee', 'left_ankle']], |
|
|
|
[ |
|
'right_shoulder', |
|
[ |
|
this.getMidpoint( |
|
data['left_shoulder'], |
|
data['right_shoulder'] |
|
), |
|
'right_shoulder', |
|
], |
|
], |
|
['right_elbow', ['right_shoulder', 'right_elbow']], |
|
['right_wrist', ['right_elbow', 'right_wrist']], |
|
|
|
[ |
|
'right_hip', |
|
[ |
|
this.getMidpoint( |
|
data['left_shoulder'], |
|
data['right_shoulder'] |
|
), |
|
'right_hip', |
|
], |
|
], |
|
['right_knee', ['right_hip', 'right_knee']], |
|
['right_ankle', ['right_knee', 'right_ankle']], |
|
|
|
[ |
|
'nose', |
|
[ |
|
this.getMidpoint( |
|
data['left_shoulder'], |
|
data['right_shoulder'] |
|
), |
|
'nose', |
|
], |
|
], |
|
['left_eye', ['nose', 'left_eye']], |
|
['right_eye', ['nose', 'right_eye']], |
|
['left_ear', ['left_eye', 'left_ear']], |
|
['right_ear', ['right_eye', 'right_ear']], |
|
] |
|
|
|
for (const [name, [from, to]] of map) { |
|
this.rotateTo3( |
|
name, |
|
from instanceof THREE.Vector3 ? from : data[from], |
|
to instanceof THREE.Vector3 ? to : data[to] |
|
) |
|
|
|
this.setPositionByDistance( |
|
name, |
|
from instanceof THREE.Vector3 ? from : data[from], |
|
to instanceof THREE.Vector3 ? to : data[to] |
|
) |
|
|
|
this.UpdateLink(name) |
|
} |
|
this.Update() |
|
} |
|
SetPose(rawData: [number, number, number][]) { |
|
this.ResetPose() |
|
|
|
const data = Object.fromEntries( |
|
Object.entries(PartIndexMappingOfPoseModel).map(([name, index]) => { |
|
return [ |
|
name, |
|
new THREE.Vector3().fromArray(rawData[index] ?? [0, 0, 0]), |
|
] |
|
}) |
|
) as Record<keyof typeof PartIndexMappingOfPoseModel, THREE.Vector3> |
|
|
|
this.part['torso'].position.setY( |
|
this.getMidpoint(data['Hips'], data['Chest']).y |
|
) |
|
this.Hips = this.getDistanceOf(data['Hips'], data['UpLeg_L']) * 2 |
|
this.Thigh = this.getDistanceOf(data['UpLeg_L'], data['Leg_L']) |
|
this.LowerLeg = this.getDistanceOf(data['Leg_L'], data['Foot_L']) |
|
this.UpperArm = this.getDistanceOf(data['Arm_L'], data['ForeArm_L']) |
|
this.Forearm = this.getDistanceOf(data['ForeArm_L'], data['Hand_L']) |
|
this.ShoulderWidth = |
|
2 * |
|
(this.getDistanceOf(data['Shoulder_L'], data['Arm_L']) + |
|
this.getDistanceOf(data['Chest'], data['Shoulder_L']) / |
|
Math.SQRT2) |
|
|
|
const map: [ |
|
ControlPartName, |
|
[ |
|
keyof typeof PartIndexMappingOfPoseModel, |
|
keyof typeof PartIndexMappingOfPoseModel |
|
] |
|
][] = [ |
|
['five', ['Hips', 'Chest']], |
|
['left_elbow', ['Arm_L', 'ForeArm_L']], |
|
['left_wrist', ['ForeArm_L', 'Hand_L']], |
|
['left_knee', ['UpLeg_L', 'Leg_L']], |
|
['left_ankle', ['Leg_L', 'Foot_L']], |
|
['right_elbow', ['Arm_R', 'ForeArm_R']], |
|
['right_wrist', ['ForeArm_R', 'Hand_R']], |
|
['right_knee', ['UpLeg_R', 'Leg_R']], |
|
['right_ankle', ['Leg_R', 'Foot_R']], |
|
] |
|
|
|
for (const [name, [from, to]] of map) |
|
this.rotateTo( |
|
name, |
|
this.getDirectionVectorByParentOf(name, data[from], data[to]) |
|
) |
|
this.Update() |
|
} |
|
|
|
GetHandData(hand: 'left_hand' | 'right_hand'): HandData { |
|
const o = this.part[hand] |
|
const result: HandData = { |
|
child: {}, |
|
} |
|
o.traverse((child) => { |
|
if (child.name && IsBone(child.name)) { |
|
if (child.name in result.child) |
|
console.log('Duplicate name', child.name, child) |
|
const data: Pick<BodyData, 'position' | 'rotation' | 'scale'> = |
|
{} |
|
|
|
if ( |
|
this.getDistanceOf( |
|
child.position, |
|
new THREE.Vector3(0, 0, 0) |
|
) != 0 |
|
) { |
|
data.position = child.position.toArray() |
|
} |
|
|
|
if ( |
|
this.getDistanceOf( |
|
child.scale, |
|
new THREE.Vector3(1, 1, 1) |
|
) != 0 |
|
) { |
|
data.scale = child.scale.toArray() |
|
} |
|
|
|
if ( |
|
child.rotation.x !== 0 || |
|
child.rotation.y !== 0 || |
|
child.rotation.z !== 0 |
|
) { |
|
data.rotation = child.rotation.toArray() |
|
} |
|
if (data) result.child[child.name] = data |
|
} |
|
}) |
|
|
|
return result |
|
} |
|
|
|
RestoreHand(hand: 'left_hand' | 'right_hand', data: HandData) { |
|
data.child = Object.fromEntries( |
|
Object.entries(data.child).map(([k, v]) => { |
|
if (hand == 'left_hand') return [k.replace('_R', '_L'), v] |
|
if (hand == 'right_hand') return [k.replace('_L', '_R'), v] |
|
return [k, v] |
|
}) |
|
) |
|
this.part[hand]?.traverse((o) => { |
|
if (o.name && o.name in data.child) { |
|
const child = data.child[o.name] |
|
if (child.position) o.position.fromArray(child.position) |
|
if (child.rotation) o.rotation.fromArray(child.rotation as any) |
|
if (child.scale) o.scale.fromArray(child.scale) |
|
} |
|
}) |
|
} |
|
|
|
GetBodyData(): BodyData { |
|
const o = this.part['torso'] |
|
const result: BodyData = { |
|
position: o.position.toArray(), |
|
rotation: o.rotation.toArray(), |
|
scale: o.scale.toArray(), |
|
child: {}, |
|
} |
|
o.traverse((child) => { |
|
if (child.name && IsNeedSaveObject(child.name)) { |
|
if (child.name in result.child) |
|
console.log('Duplicate name', child.name, child) |
|
const data: Pick<BodyData, 'position' | 'rotation' | 'scale'> = |
|
{} |
|
|
|
if ( |
|
this.getDistanceOf( |
|
child.position, |
|
new THREE.Vector3(0, 0, 0) |
|
) != 0 |
|
) { |
|
data.position = child.position.toArray() |
|
} |
|
|
|
if ( |
|
this.getDistanceOf( |
|
child.scale, |
|
new THREE.Vector3(1, 1, 1) |
|
) != 0 |
|
) { |
|
data.scale = child.scale.toArray() |
|
} |
|
|
|
if ( |
|
child.rotation.x !== 0 || |
|
child.rotation.y !== 0 || |
|
child.rotation.z !== 0 |
|
) { |
|
data.rotation = child.rotation.toArray() |
|
} |
|
if (data) result.child[child.name] = data |
|
} |
|
}) |
|
|
|
return result |
|
} |
|
|
|
RestoreBody(data: BodyData) { |
|
const body = this.part['torso'] |
|
|
|
body?.traverse((o) => { |
|
if (o.name && o.name in data.child) { |
|
const child = data.child[o.name] |
|
if (child.position) o.position.fromArray(child.position) |
|
if (child.rotation) o.rotation.fromArray(child.rotation as any) |
|
if (child.scale) o.scale.fromArray(child.scale) |
|
} |
|
}) |
|
if (data.position) body.position.fromArray(data.position) |
|
if (data.rotation) body.rotation.fromArray(data.rotation as any) |
|
if (data.scale) body.scale.fromArray(data.scale) |
|
} |
|
|
|
UpdateBones(thickness = this.BoneThickness) { |
|
this.part['torso'].traverse((o) => { |
|
if (o.name in this.part) { |
|
const name = o.name as ControlPartName |
|
this.UpdateLink(name, thickness) |
|
} |
|
}) |
|
} |
|
|
|
CreateBones(thickness = this.BoneThickness) { |
|
this.part['torso'].traverse((o) => { |
|
if (o.name in this.part) { |
|
const name = o.name as ControlPartName |
|
this.UpdateLink(name, thickness, true) |
|
} |
|
}) |
|
} |
|
|
|
Create() { |
|
this.CreateBones() |
|
|
|
this.part['torso'].add( |
|
CreateIKTarget( |
|
this.part['torso'], |
|
this.part['left_wrist'], |
|
'left_wrist_target' |
|
) |
|
) |
|
this.part['torso'].add( |
|
CreateIKTarget( |
|
this.part['torso'], |
|
this.part['right_wrist'], |
|
'right_wrist_target' |
|
) |
|
) |
|
this.part['torso'].add( |
|
CreateIKTarget( |
|
this.part['torso'], |
|
this.part['left_ankle'], |
|
'left_ankle_target' |
|
) |
|
) |
|
this.part['torso'].add( |
|
CreateIKTarget( |
|
this.part['torso'], |
|
this.part['right_ankle'], |
|
'right_ankle_target' |
|
) |
|
) |
|
} |
|
|
|
Get18keyPointsData(): Array<[number, number, number]> { |
|
return OpenposeKeypoints.map((name) => { |
|
if (name in this.part) { |
|
return this.getWorldPosition( |
|
this.part[name as ControlPartName] |
|
).toArray() |
|
} else { |
|
return [0, 0, 0] |
|
} |
|
}) |
|
} |
|
|
|
get BoneThickness() { |
|
return Math.abs( |
|
this.part['neck'].getObjectByName('neck_joint_sphere')?.scale.x ?? |
|
BoneThickness |
|
) |
|
} |
|
|
|
set BoneThickness(thickness: number) { |
|
this.UpdateBones(thickness) |
|
} |
|
|
|
GetIKSolver() { |
|
return new CCDIKSolver([ |
|
{ |
|
target: this.part['left_wrist_target'], |
|
effector: this.part['left_wrist'], |
|
links: [ |
|
{ |
|
index: this.part['left_elbow'], |
|
enabled: true, |
|
}, |
|
{ |
|
index: this.part['left_shoulder'], |
|
enabled: true, |
|
}, |
|
], |
|
iteration: 10, |
|
minAngle: 0.0, |
|
maxAngle: 1.0, |
|
}, |
|
{ |
|
target: this.part['right_wrist_target'], |
|
effector: this.part['right_wrist'], |
|
links: [ |
|
{ |
|
index: this.part['right_elbow'], |
|
enabled: true, |
|
}, |
|
{ |
|
index: this.part['right_shoulder'], |
|
enabled: true, |
|
}, |
|
], |
|
iteration: 10, |
|
minAngle: 0.0, |
|
maxAngle: 1.0, |
|
}, |
|
{ |
|
target: this.part['left_ankle_target'], |
|
effector: this.part['left_ankle'], |
|
links: [ |
|
{ |
|
index: this.part['left_knee'], |
|
enabled: true, |
|
}, |
|
{ |
|
index: this.part['left_hip'], |
|
enabled: true, |
|
}, |
|
], |
|
iteration: 10, |
|
minAngle: 0.0, |
|
maxAngle: 1.0, |
|
}, |
|
{ |
|
target: this.part['right_ankle_target'], |
|
effector: this.part['right_ankle'], |
|
links: [ |
|
{ |
|
index: this.part['right_knee'], |
|
enabled: true, |
|
}, |
|
{ |
|
index: this.part['right_hip'], |
|
enabled: true, |
|
}, |
|
], |
|
iteration: 10, |
|
minAngle: 0.0, |
|
maxAngle: 1.0, |
|
}, |
|
]) |
|
} |
|
|
|
ResetTargetPosition( |
|
effectorName: ControlPartName, |
|
targetName: ControlPartName |
|
) { |
|
const body = this.part['torso'] |
|
const effector = this.part[effectorName] |
|
const target = this.part[targetName] |
|
|
|
const effector_pos = GetWorldPosition(effector) |
|
target.position.copy(this.getLocalPosition(body, effector_pos)) |
|
} |
|
|
|
ResetAllTargetsPosition() { |
|
this.ResetTargetPosition('left_wrist', 'left_wrist_target') |
|
this.ResetTargetPosition('right_wrist', 'right_wrist_target') |
|
this.ResetTargetPosition('left_ankle', 'left_ankle_target') |
|
this.ResetTargetPosition('right_ankle', 'right_ankle_target') |
|
} |
|
|
|
Update() { |
|
this.ResetAllTargetsPosition() |
|
this.UpdateBones() |
|
this.part['torso'].updateMatrixWorld(true) |
|
} |
|
} |
|
|