new help section explaining classes and their mechanics
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { createPortal } from "@react-three/fiber";
|
||||
import { useTexture } from "@react-three/drei";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
CHARACTER_PART_CATALOG,
|
||||
characterAppearancePartIds,
|
||||
type CharacterAppearanceV1,
|
||||
type CharacterMaterialVariant,
|
||||
type CharacterPartId,
|
||||
} from "../game/characterAppearance";
|
||||
import type { MemberId } from "../game/types";
|
||||
@@ -12,9 +14,14 @@ import { useGameGLTF } from "./GameAssetProvider";
|
||||
|
||||
interface BoundCharacterPart {
|
||||
group: THREE.Group;
|
||||
materials: THREE.Material[];
|
||||
skeletons: THREE.Skeleton[];
|
||||
}
|
||||
|
||||
const CHARACTER_MATERIAL_VARIANT_URLS: Record<CharacterMaterialVariant, string> = {
|
||||
"priest-vestments": new URL("../assets/game/textures/claudecraft/chars/players/priest-vestments.webp", import.meta.url).href,
|
||||
};
|
||||
|
||||
function rigBonesByName(rigScene: THREE.Object3D) {
|
||||
const bones = new Map<string, THREE.Bone>();
|
||||
rigScene.traverse((object) => {
|
||||
@@ -27,11 +34,13 @@ function createBoundCharacterPart(
|
||||
sourceScene: THREE.Object3D,
|
||||
rigScene: THREE.Object3D,
|
||||
partId: CharacterPartId,
|
||||
materialTexture?: THREE.Texture,
|
||||
): BoundCharacterPart {
|
||||
const definition = CHARACTER_PART_CATALOG[partId];
|
||||
const rigBones = rigBonesByName(rigScene);
|
||||
const group = new THREE.Group();
|
||||
group.name = `character-part:${partId}`;
|
||||
const materialClones = new Map<THREE.Material, THREE.Material>();
|
||||
const skeletons: THREE.Skeleton[] = [];
|
||||
|
||||
sourceScene.updateMatrixWorld(true);
|
||||
@@ -44,6 +53,20 @@ function createBoundCharacterPart(
|
||||
partNode.matrix.decompose(partNode.position, partNode.quaternion, partNode.scale);
|
||||
partNode.traverse((object) => {
|
||||
if (!(object instanceof THREE.SkinnedMesh)) return;
|
||||
if (materialTexture) {
|
||||
const cloneMaterial = (source: THREE.Material) => {
|
||||
const existing = materialClones.get(source);
|
||||
if (existing) return existing;
|
||||
const clone = source.clone();
|
||||
if ("map" in clone) clone.map = materialTexture;
|
||||
clone.needsUpdate = true;
|
||||
materialClones.set(source, clone);
|
||||
return clone;
|
||||
};
|
||||
object.material = Array.isArray(object.material)
|
||||
? object.material.map(cloneMaterial)
|
||||
: cloneMaterial(object.material);
|
||||
}
|
||||
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}.`);
|
||||
@@ -63,7 +86,56 @@ function createBoundCharacterPart(
|
||||
group.add(partNode);
|
||||
}
|
||||
|
||||
return { group, skeletons };
|
||||
return { group, materials: [...materialClones.values()], skeletons };
|
||||
}
|
||||
|
||||
function BoundCharacterPart({
|
||||
actorScene,
|
||||
materialTexture,
|
||||
modelUrls,
|
||||
partId,
|
||||
}: {
|
||||
actorScene: THREE.Object3D;
|
||||
materialTexture?: THREE.Texture;
|
||||
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, materialTexture),
|
||||
[actorScene, gltf.scene, materialTexture, partId],
|
||||
);
|
||||
|
||||
useEffect(() => () => {
|
||||
for (const skeleton of boundPart.skeletons) skeleton.dispose();
|
||||
for (const material of boundPart.materials) material.dispose();
|
||||
}, [boundPart]);
|
||||
|
||||
const rigRoot = actorScene.getObjectByName("Rig_Medium") ?? actorScene;
|
||||
return createPortal(<primitive object={boundPart.group} />, rigRoot);
|
||||
}
|
||||
|
||||
function MaterialVariantCharacterPart({
|
||||
actorScene,
|
||||
materialVariant,
|
||||
modelUrls,
|
||||
partId,
|
||||
}: {
|
||||
actorScene: THREE.Object3D;
|
||||
materialVariant: CharacterMaterialVariant;
|
||||
modelUrls: Record<MemberId, string>;
|
||||
partId: CharacterPartId;
|
||||
}) {
|
||||
const sourceTexture = useTexture(CHARACTER_MATERIAL_VARIANT_URLS[materialVariant]);
|
||||
const materialTexture = useMemo(() => {
|
||||
sourceTexture.flipY = false;
|
||||
sourceTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
sourceTexture.magFilter = THREE.NearestFilter;
|
||||
sourceTexture.needsUpdate = true;
|
||||
return sourceTexture;
|
||||
}, [sourceTexture]);
|
||||
return <BoundCharacterPart actorScene={actorScene} materialTexture={materialTexture} modelUrls={modelUrls} partId={partId} />;
|
||||
}
|
||||
|
||||
function ModularCharacterPart({
|
||||
@@ -76,18 +148,10 @@ function ModularCharacterPart({
|
||||
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);
|
||||
const materialVariant = "materialVariant" in definition ? definition.materialVariant : undefined;
|
||||
return materialVariant
|
||||
? <MaterialVariantCharacterPart actorScene={actorScene} materialVariant={materialVariant} modelUrls={modelUrls} partId={partId} />
|
||||
: <BoundCharacterPart actorScene={actorScene} modelUrls={modelUrls} partId={partId} />;
|
||||
}
|
||||
|
||||
export function ModularCharacterBody({
|
||||
|
||||
Reference in New Issue
Block a user