new rpg mode
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { createPortal } from "@react-three/fiber";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
CHARACTER_PART_CATALOG,
|
||||
characterAppearancePartIds,
|
||||
type CharacterAppearanceV1,
|
||||
type CharacterPartId,
|
||||
} from "../game/characterAppearance";
|
||||
import type { MemberId } from "../game/types";
|
||||
import { useGameGLTF } from "./GameAssetProvider";
|
||||
|
||||
interface BoundCharacterPart {
|
||||
group: THREE.Group;
|
||||
skeletons: THREE.Skeleton[];
|
||||
}
|
||||
|
||||
function rigBonesByName(rigScene: THREE.Object3D) {
|
||||
const bones = new Map<string, THREE.Bone>();
|
||||
rigScene.traverse((object) => {
|
||||
if (object instanceof THREE.Bone) bones.set(object.name, object);
|
||||
});
|
||||
return bones;
|
||||
}
|
||||
|
||||
function createBoundCharacterPart(
|
||||
sourceScene: THREE.Object3D,
|
||||
rigScene: THREE.Object3D,
|
||||
partId: CharacterPartId,
|
||||
): BoundCharacterPart {
|
||||
const definition = CHARACTER_PART_CATALOG[partId];
|
||||
const rigBones = rigBonesByName(rigScene);
|
||||
const group = new THREE.Group();
|
||||
group.name = `character-part:${partId}`;
|
||||
const skeletons: THREE.Skeleton[] = [];
|
||||
|
||||
sourceScene.updateMatrixWorld(true);
|
||||
for (const nodeName of definition.nodeNames) {
|
||||
const sourceNode = sourceScene.getObjectByName(nodeName);
|
||||
if (!sourceNode) throw new Error(`Character part ${partId} is missing node ${nodeName}.`);
|
||||
|
||||
const partNode = sourceNode.clone(true);
|
||||
partNode.matrix.copy(sourceNode.matrixWorld);
|
||||
partNode.matrix.decompose(partNode.position, partNode.quaternion, partNode.scale);
|
||||
partNode.traverse((object) => {
|
||||
if (!(object instanceof THREE.SkinnedMesh)) return;
|
||||
const mappedBones = object.skeleton.bones.map((sourceBone) => {
|
||||
const rigBone = rigBones.get(sourceBone.name);
|
||||
if (!rigBone) throw new Error(`Character part ${partId} cannot resolve rig bone ${sourceBone.name}.`);
|
||||
return rigBone;
|
||||
});
|
||||
const skeleton = new THREE.Skeleton(
|
||||
mappedBones,
|
||||
object.skeleton.boneInverses.map((inverse) => inverse.clone()),
|
||||
);
|
||||
const bindMatrix = object.bindMatrix.clone();
|
||||
object.bind(skeleton, bindMatrix);
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
object.frustumCulled = false;
|
||||
skeletons.push(skeleton);
|
||||
});
|
||||
group.add(partNode);
|
||||
}
|
||||
|
||||
return { group, skeletons };
|
||||
}
|
||||
|
||||
function ModularCharacterPart({
|
||||
actorScene,
|
||||
modelUrls,
|
||||
partId,
|
||||
}: {
|
||||
actorScene: THREE.Object3D;
|
||||
modelUrls: Record<MemberId, string>;
|
||||
partId: CharacterPartId;
|
||||
}) {
|
||||
const definition = CHARACTER_PART_CATALOG[partId];
|
||||
const gltf = useGameGLTF(modelUrls[definition.sourceMemberId]);
|
||||
const boundPart = useMemo(
|
||||
() => createBoundCharacterPart(gltf.scene, actorScene, partId),
|
||||
[actorScene, gltf.scene, partId],
|
||||
);
|
||||
|
||||
useEffect(() => () => {
|
||||
for (const skeleton of boundPart.skeletons) skeleton.dispose();
|
||||
}, [boundPart]);
|
||||
|
||||
const rigRoot = actorScene.getObjectByName("Rig_Medium") ?? actorScene;
|
||||
return createPortal(<primitive object={boundPart.group} />, rigRoot);
|
||||
}
|
||||
|
||||
export function ModularCharacterBody({
|
||||
actorScene,
|
||||
appearance,
|
||||
modelUrls,
|
||||
}: {
|
||||
actorScene: THREE.Object3D;
|
||||
appearance: CharacterAppearanceV1;
|
||||
modelUrls: Record<MemberId, string>;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{characterAppearancePartIds(appearance).map((partId) => (
|
||||
<ModularCharacterPart
|
||||
key={`${partId}:${appearance.version}`}
|
||||
actorScene={actorScene}
|
||||
modelUrls={modelUrls}
|
||||
partId={partId}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user