64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
import { SkinnedMesh, Vector3, type Object3D } from "three";
|
|
import type { AvatarAttachmentPoint } from "./characterManifest";
|
|
|
|
export interface AvatarAttachmentTransform {
|
|
readonly target: Object3D;
|
|
readonly position: Vector3;
|
|
readonly scale: Vector3;
|
|
}
|
|
|
|
export interface AvatarAttachmentOptions {
|
|
/** The manifest point is the authored origin of its referenced attachment joint. */
|
|
readonly useTargetOrigin?: boolean;
|
|
}
|
|
|
|
const safeScaleRatio = (rootScale: number, targetScale: number): number => (
|
|
Number.isFinite(rootScale)
|
|
&& Number.isFinite(targetScale)
|
|
&& Math.abs(targetScale) > 1e-6
|
|
? rootScale / targetScale
|
|
: 1
|
|
);
|
|
|
|
function attachmentScale(root: Object3D, target: Object3D): Vector3 {
|
|
const rootScale = root.getWorldScale(new Vector3());
|
|
const targetScale = target.getWorldScale(new Vector3());
|
|
return new Vector3(
|
|
safeScaleRatio(rootScale.x, targetScale.x),
|
|
safeScaleRatio(rootScale.y, targetScale.y),
|
|
safeScaleRatio(rootScale.z, targetScale.z),
|
|
);
|
|
}
|
|
|
|
export function findAvatarSkinBone(root: Object3D, boneIndex: number): Object3D | null {
|
|
let bone: Object3D | null = null;
|
|
root.traverse((node) => {
|
|
if (bone) return;
|
|
const skinnedMesh = node as SkinnedMesh;
|
|
if (!skinnedMesh.isSkinnedMesh) return;
|
|
bone = skinnedMesh.skeleton.bones[boneIndex] ?? null;
|
|
});
|
|
return bone;
|
|
}
|
|
|
|
/** Converts a manifest point from avatar-local space into its target bone. */
|
|
export function resolveAvatarAttachmentPoint(
|
|
root: Object3D,
|
|
point: AvatarAttachmentPoint | undefined,
|
|
fallbackTarget: Object3D | null,
|
|
options: AvatarAttachmentOptions = {},
|
|
): AvatarAttachmentTransform | null {
|
|
const skinBone = point ? findAvatarSkinBone(root, point.bone) : null;
|
|
const target = skinBone ?? fallbackTarget;
|
|
if (!target) return null;
|
|
root.updateWorldMatrix(true, true);
|
|
const scale = attachmentScale(root, target);
|
|
if (!point || (skinBone && options.useTargetOrigin)) {
|
|
return { target, position: new Vector3(), scale };
|
|
}
|
|
|
|
const position = root.localToWorld(new Vector3(...point.position));
|
|
target.worldToLocal(position);
|
|
return { target, position, scale };
|
|
}
|