California gets roads, traffic, and a car to follow
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import * as THREE from "three";
|
||||
|
||||
export interface ActorRigBase {
|
||||
/** Floor-centred root. Every actor faces -Z at yaw zero. */
|
||||
root: THREE.Group;
|
||||
/** True only when the builder created the material set. */
|
||||
readonly ownsMaterials: boolean;
|
||||
}
|
||||
|
||||
export function actorMesh(
|
||||
name: string,
|
||||
geometry: THREE.BufferGeometry,
|
||||
material: THREE.Material,
|
||||
options: {
|
||||
position?: readonly [number, number, number];
|
||||
rotation?: readonly [number, number, number];
|
||||
scale?: readonly [number, number, number];
|
||||
receiveShadow?: boolean;
|
||||
} = {},
|
||||
): THREE.Mesh {
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.name = name;
|
||||
if (options.position) mesh.position.set(...options.position);
|
||||
if (options.rotation) mesh.rotation.set(...options.rotation);
|
||||
if (options.scale) mesh.scale.set(...options.scale);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = options.receiveShadow ?? true;
|
||||
return mesh;
|
||||
}
|
||||
|
||||
export function namedGroup(name: string, position?: readonly [number, number, number]): THREE.Group {
|
||||
const group = new THREE.Group();
|
||||
group.name = name;
|
||||
if (position) group.position.set(...position);
|
||||
return group;
|
||||
}
|
||||
|
||||
export function requireGroup(root: THREE.Object3D, name: string): THREE.Group {
|
||||
const object = root.getObjectByName(name);
|
||||
if (!(object instanceof THREE.Group)) throw new Error(`actor: missing joint "${name}"`);
|
||||
return object;
|
||||
}
|
||||
|
||||
/** Release only resources owned by a prototype. Clones share those resources. */
|
||||
export function disposeActor(
|
||||
rig: ActorRigBase,
|
||||
options: { disposeMaterials?: boolean } = {},
|
||||
): void {
|
||||
const geometries = new Set<THREE.BufferGeometry>();
|
||||
const materials = new Set<THREE.Material>();
|
||||
rig.root.traverse((object) => {
|
||||
if (!(object instanceof THREE.Mesh)) return;
|
||||
geometries.add(object.geometry);
|
||||
if (Array.isArray(object.material)) {
|
||||
for (const material of object.material) materials.add(material);
|
||||
} else materials.add(object.material);
|
||||
});
|
||||
for (const geometry of geometries) geometry.dispose();
|
||||
if (options.disposeMaterials ?? rig.ownsMaterials) {
|
||||
for (const material of materials) material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export function actorMeshes(root: THREE.Object3D): THREE.Mesh[] {
|
||||
const meshes: THREE.Mesh[] = [];
|
||||
root.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) meshes.push(object);
|
||||
});
|
||||
return meshes;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/** A low-cost anonymous Tera crow, ready to perch or flap in flight. */
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
actorMesh,
|
||||
disposeActor,
|
||||
namedGroup,
|
||||
requireGroup,
|
||||
type ActorRigBase,
|
||||
} from "./common.ts";
|
||||
|
||||
export const CROW_METRICS = { bodyLength: 0.42, perchedHeight: 0.34, wingspan: 0.84 } as const;
|
||||
|
||||
export interface CrowMaterials {
|
||||
feather: THREE.Material;
|
||||
sheen: THREE.Material;
|
||||
beak: THREE.Material;
|
||||
eye: THREE.Material;
|
||||
foot: THREE.Material;
|
||||
}
|
||||
|
||||
export interface CrowBuildOptions {
|
||||
materials?: CrowMaterials;
|
||||
featherColor?: THREE.ColorRepresentation;
|
||||
sheenColor?: THREE.ColorRepresentation;
|
||||
}
|
||||
|
||||
export interface CrowJoints {
|
||||
body: THREE.Group;
|
||||
head: THREE.Group;
|
||||
wingLeft: THREE.Group;
|
||||
wingRight: THREE.Group;
|
||||
tail: THREE.Group;
|
||||
}
|
||||
|
||||
export interface CrowRig extends ActorRigBase {
|
||||
joints: CrowJoints;
|
||||
}
|
||||
|
||||
export function createCrowMaterials(options: CrowBuildOptions = {}): CrowMaterials {
|
||||
return {
|
||||
feather: new THREE.MeshStandardMaterial({ name: "crow.feather", color: options.featherColor ?? 0x111519, roughness: 0.62, metalness: 0.12 }),
|
||||
sheen: new THREE.MeshStandardMaterial({ name: "crow.sheen", color: options.sheenColor ?? 0x1f2c36, roughness: 0.4, metalness: 0.28 }),
|
||||
beak: new THREE.MeshStandardMaterial({ name: "crow.beak", color: 0x202327, roughness: 0.78 }),
|
||||
eye: new THREE.MeshBasicMaterial({ name: "crow.eye", color: 0xd4b168, toneMapped: false }),
|
||||
foot: new THREE.MeshStandardMaterial({ name: "crow.foot", color: 0x24272a, roughness: 0.9 }),
|
||||
};
|
||||
}
|
||||
|
||||
function wingGeometry(side: -1 | 1): THREE.BufferGeometry {
|
||||
const s = side;
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute([
|
||||
0, 0, 0.04,
|
||||
s * 0.32, -0.015, 0.1,
|
||||
s * 0.4, -0.03, 0.2,
|
||||
s * 0.18, -0.015, -0.14,
|
||||
0, 0, -0.16,
|
||||
], 3),
|
||||
);
|
||||
geometry.setIndex([0, 1, 3, 1, 2, 3, 0, 3, 4]);
|
||||
geometry.computeVertexNormals();
|
||||
geometry.name = `crow.wing.${side < 0 ? "left" : "right"}`;
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function resolveCrow(root: THREE.Group, ownsMaterials: boolean): CrowRig {
|
||||
return {
|
||||
root,
|
||||
ownsMaterials,
|
||||
joints: {
|
||||
body: requireGroup(root, "crow.body"),
|
||||
head: requireGroup(root, "crow.head"),
|
||||
wingLeft: requireGroup(root, "crow.wing.left"),
|
||||
wingRight: requireGroup(root, "crow.wing.right"),
|
||||
tail: requireGroup(root, "crow.tail"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCrow(options: CrowBuildOptions = {}): CrowRig {
|
||||
const m = options.materials ?? createCrowMaterials(options);
|
||||
const root = namedGroup("crow");
|
||||
root.userData.kind = "actor";
|
||||
root.userData.actorType = "anonymous-crow";
|
||||
root.userData.forwardAxis = "-Z";
|
||||
const body = namedGroup("crow.body", [0, 0.2, 0]);
|
||||
root.add(body);
|
||||
body.add(
|
||||
actorMesh("crow.torso", new THREE.SphereGeometry(0.13, 12, 9), m.feather, {
|
||||
rotation: [-0.16, 0, 0],
|
||||
scale: [0.82, 1.08, 1.34],
|
||||
}),
|
||||
actorMesh("crow.breast", new THREE.SphereGeometry(0.105, 10, 8), m.sheen, {
|
||||
position: [0, 0.012, -0.105],
|
||||
scale: [0.72, 1, 0.58],
|
||||
}),
|
||||
);
|
||||
const head = namedGroup("crow.head", [0, 0.145, -0.105]);
|
||||
body.add(head);
|
||||
head.add(
|
||||
actorMesh("crow.skull", new THREE.SphereGeometry(0.093, 12, 9), m.feather, { scale: [0.92, 1, 0.95] }),
|
||||
actorMesh("crow.beak", new THREE.ConeGeometry(0.055, 0.18, 6), m.beak, {
|
||||
position: [0, -0.018, -0.145],
|
||||
rotation: [-Math.PI / 2, 0, 0],
|
||||
scale: [0.72, 1, 0.68],
|
||||
}),
|
||||
);
|
||||
for (const side of [-1, 1] as const) {
|
||||
const word = side < 0 ? "left" : "right";
|
||||
head.add(
|
||||
actorMesh(`crow.eye.${word}`, new THREE.SphereGeometry(0.011, 7, 5), m.eye, {
|
||||
position: [side * 0.068, 0.02, -0.06],
|
||||
}),
|
||||
);
|
||||
const wing = namedGroup(`crow.wing.${word}`, [side * 0.075, 0.035, 0]);
|
||||
body.add(wing);
|
||||
wing.add(actorMesh(`crow.wing-mesh.${word}`, wingGeometry(side), m.feather, { receiveShadow: false }));
|
||||
}
|
||||
const tail = namedGroup("crow.tail", [0, -0.01, 0.13]);
|
||||
body.add(tail);
|
||||
for (const side of [-1, 0, 1] as const) {
|
||||
tail.add(
|
||||
actorMesh(`crow.tail-feather.${side}`, new THREE.ConeGeometry(0.045, 0.25, 4), m.feather, {
|
||||
position: [side * 0.04, -0.015, 0.12],
|
||||
rotation: [Math.PI / 2, 0, 0],
|
||||
scale: [0.72, 1, 0.35],
|
||||
}),
|
||||
);
|
||||
}
|
||||
for (const side of [-1, 1] as const) {
|
||||
body.add(
|
||||
actorMesh(`crow.foot.${side < 0 ? "left" : "right"}`, new THREE.CylinderGeometry(0.012, 0.009, 0.14, 6), m.foot, {
|
||||
position: [side * 0.048, -0.14, -0.012],
|
||||
}),
|
||||
);
|
||||
}
|
||||
return resolveCrow(root, !options.materials);
|
||||
}
|
||||
|
||||
export function cloneCrow(source: CrowRig): CrowRig {
|
||||
return resolveCrow(source.root.clone(true), false);
|
||||
}
|
||||
|
||||
/** Pose a flap. `amount=0` folds the wings; `amount=1` is a broad flight stroke. */
|
||||
export function poseCrowFlight(rig: CrowRig, phase: number, amount = 1): void {
|
||||
const strength = THREE.MathUtils.clamp(amount, 0, 1);
|
||||
const stroke = Math.sin(phase) * 0.72 * strength;
|
||||
const spread = 0.2 + strength * 0.74;
|
||||
rig.joints.wingLeft.rotation.z = spread + stroke;
|
||||
rig.joints.wingRight.rotation.z = -spread - stroke;
|
||||
rig.joints.wingLeft.rotation.x = -0.12 * strength;
|
||||
rig.joints.wingRight.rotation.x = -0.12 * strength;
|
||||
rig.joints.body.rotation.x = 0.08 * Math.cos(phase) * strength;
|
||||
rig.joints.tail.rotation.x = -0.12 * Math.cos(phase) * strength;
|
||||
}
|
||||
|
||||
export function disposeCrow(rig: CrowRig, options: { disposeMaterials?: boolean } = {}): void {
|
||||
disposeActor(rig, options);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/** A compact, friendly anonymous office dog with a poseable head, legs and tail. */
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
actorMesh,
|
||||
disposeActor,
|
||||
namedGroup,
|
||||
requireGroup,
|
||||
type ActorRigBase,
|
||||
} from "./common.ts";
|
||||
|
||||
export const DOG_METRICS = { length: 0.76, shoulderHeight: 0.46, height: 0.74 } as const;
|
||||
|
||||
export interface DogMaterials {
|
||||
coat: THREE.Material;
|
||||
markings: THREE.Material;
|
||||
nose: THREE.Material;
|
||||
collar: THREE.Material;
|
||||
}
|
||||
|
||||
export interface DogBuildOptions {
|
||||
materials?: DogMaterials;
|
||||
coatColor?: THREE.ColorRepresentation;
|
||||
markingsColor?: THREE.ColorRepresentation;
|
||||
collarColor?: THREE.ColorRepresentation;
|
||||
}
|
||||
|
||||
export interface DogJoints {
|
||||
body: THREE.Group;
|
||||
head: THREE.Group;
|
||||
earLeft: THREE.Group;
|
||||
earRight: THREE.Group;
|
||||
tail: THREE.Group;
|
||||
legFrontLeft: THREE.Group;
|
||||
legFrontRight: THREE.Group;
|
||||
legRearLeft: THREE.Group;
|
||||
legRearRight: THREE.Group;
|
||||
}
|
||||
|
||||
export interface DogRig extends ActorRigBase {
|
||||
joints: DogJoints;
|
||||
}
|
||||
|
||||
export function createDogMaterials(options: DogBuildOptions = {}): DogMaterials {
|
||||
return {
|
||||
coat: new THREE.MeshStandardMaterial({ name: "dog.coat", color: options.coatColor ?? 0x9a673d, roughness: 0.92 }),
|
||||
markings: new THREE.MeshStandardMaterial({ name: "dog.markings", color: options.markingsColor ?? 0xe4c9a2, roughness: 0.95 }),
|
||||
nose: new THREE.MeshStandardMaterial({ name: "dog.nose", color: 0x151719, roughness: 0.72 }),
|
||||
collar: new THREE.MeshStandardMaterial({ name: "dog.collar", color: options.collarColor ?? 0x2d92a7, roughness: 0.55 }),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveDog(root: THREE.Group, ownsMaterials: boolean): DogRig {
|
||||
return {
|
||||
root,
|
||||
ownsMaterials,
|
||||
joints: {
|
||||
body: requireGroup(root, "dog.body"),
|
||||
head: requireGroup(root, "dog.head"),
|
||||
earLeft: requireGroup(root, "dog.ear.left"),
|
||||
earRight: requireGroup(root, "dog.ear.right"),
|
||||
tail: requireGroup(root, "dog.tail"),
|
||||
legFrontLeft: requireGroup(root, "dog.leg.front.left"),
|
||||
legFrontRight: requireGroup(root, "dog.leg.front.right"),
|
||||
legRearLeft: requireGroup(root, "dog.leg.rear.left"),
|
||||
legRearRight: requireGroup(root, "dog.leg.rear.right"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDog(options: DogBuildOptions = {}): DogRig {
|
||||
const m = options.materials ?? createDogMaterials(options);
|
||||
const root = namedGroup("dog");
|
||||
root.userData.kind = "actor";
|
||||
root.userData.actorType = "anonymous-dog";
|
||||
root.userData.forwardAxis = "-Z";
|
||||
const body = namedGroup("dog.body", [0, 0.39, 0.04]);
|
||||
root.add(body);
|
||||
body.add(
|
||||
actorMesh("dog.torso", new THREE.CapsuleGeometry(0.18, 0.34, 5, 10), m.coat, {
|
||||
rotation: [Math.PI / 2, 0, 0],
|
||||
scale: [0.82, 1, 0.92],
|
||||
}),
|
||||
actorMesh("dog.chest", new THREE.SphereGeometry(0.185, 12, 9), m.markings, {
|
||||
position: [0, 0.01, -0.17],
|
||||
scale: [0.72, 1, 0.56],
|
||||
}),
|
||||
);
|
||||
|
||||
const head = namedGroup("dog.head", [0, 0.16, -0.32]);
|
||||
body.add(head);
|
||||
head.add(
|
||||
actorMesh("dog.skull", new THREE.SphereGeometry(0.16, 12, 10), m.coat, { scale: [0.82, 0.92, 0.88] }),
|
||||
actorMesh("dog.muzzle", new THREE.SphereGeometry(0.105, 12, 8), m.markings, {
|
||||
position: [0, -0.045, -0.13],
|
||||
scale: [0.8, 0.62, 1],
|
||||
}),
|
||||
actorMesh("dog.nose", new THREE.SphereGeometry(0.045, 10, 7), m.nose, {
|
||||
position: [0, -0.035, -0.224],
|
||||
scale: [1.15, 0.72, 0.7],
|
||||
}),
|
||||
actorMesh("dog.collar", new THREE.TorusGeometry(0.118, 0.016, 6, 18), m.collar, {
|
||||
position: [0, -0.11, 0.1],
|
||||
rotation: [Math.PI / 2, 0, 0],
|
||||
scale: [1, 0.82, 1],
|
||||
}),
|
||||
);
|
||||
for (const side of [-1, 1] as const) {
|
||||
const word = side < 0 ? "left" : "right";
|
||||
const ear = namedGroup(`dog.ear.${word}`, [side * 0.1, 0.105, -0.015]);
|
||||
head.add(ear);
|
||||
ear.add(
|
||||
actorMesh(`dog.ear-flap.${word}`, new THREE.ConeGeometry(0.075, 0.19, 5), m.coat, {
|
||||
position: [side * 0.015, -0.065, 0.02],
|
||||
rotation: [0.18, 0, side * 0.28],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (const z of [-0.2, 0.22] as const) {
|
||||
for (const side of [-1, 1] as const) {
|
||||
const fore = z < 0 ? "front" : "rear";
|
||||
const word = side < 0 ? "left" : "right";
|
||||
const leg = namedGroup(`dog.leg.${fore}.${word}`, [side * 0.125, -0.1, z]);
|
||||
body.add(leg);
|
||||
leg.add(
|
||||
actorMesh(`dog.leg-mesh.${fore}.${word}`, new THREE.CapsuleGeometry(0.046, 0.19, 3, 7), m.coat, {
|
||||
position: [0, -0.135, 0],
|
||||
}),
|
||||
actorMesh(`dog.paw.${fore}.${word}`, new THREE.SphereGeometry(0.06, 8, 6), m.markings, {
|
||||
position: [0, -0.29, -0.022],
|
||||
scale: [0.84, 0.48, 1.18],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const tail = namedGroup("dog.tail", [0, 0.03, 0.31]);
|
||||
body.add(tail);
|
||||
tail.rotation.x = 0.68;
|
||||
tail.add(
|
||||
actorMesh("dog.tail-mesh", new THREE.CapsuleGeometry(0.04, 0.25, 4, 8), m.coat, {
|
||||
position: [0, 0.15, 0],
|
||||
}),
|
||||
);
|
||||
return resolveDog(root, !options.materials);
|
||||
}
|
||||
|
||||
export function cloneDog(source: DogRig): DogRig {
|
||||
return resolveDog(source.root.clone(true), false);
|
||||
}
|
||||
|
||||
export function poseDogWalk(rig: DogRig, phase: number, amount = 0.55): void {
|
||||
const swing = Math.sin(phase) * THREE.MathUtils.clamp(amount, 0, 0.8);
|
||||
rig.joints.legFrontLeft.rotation.x = swing;
|
||||
rig.joints.legRearRight.rotation.x = swing;
|
||||
rig.joints.legFrontRight.rotation.x = -swing;
|
||||
rig.joints.legRearLeft.rotation.x = -swing;
|
||||
rig.joints.body.position.y = 0.39 + Math.abs(Math.cos(phase)) * Math.abs(swing) * 0.018;
|
||||
}
|
||||
|
||||
export function poseDogAttention(rig: DogRig, lookYaw: number, tailPhase: number): void {
|
||||
rig.joints.head.rotation.y = THREE.MathUtils.clamp(lookYaw, -0.9, 0.9);
|
||||
rig.joints.head.rotation.x = -0.08;
|
||||
rig.joints.earLeft.rotation.z = 0.08;
|
||||
rig.joints.earRight.rotation.z = -0.08;
|
||||
rig.joints.tail.rotation.z = Math.sin(tailPhase) * 0.72;
|
||||
}
|
||||
|
||||
export function disposeDog(rig: DogRig, options: { disposeMaterials?: boolean } = {}): void {
|
||||
disposeActor(rig, options);
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* A friendly, original humanoid avatar built entirely from Three.js primitives.
|
||||
*
|
||||
* Dimensions are metres, the origin is between the soles, and -Z is forward.
|
||||
* The face is a slightly curved dark display with an optional caller-owned
|
||||
* texture. A deployment can build one prototype per appearance/webcam stream,
|
||||
* then cheaply clone that prototype for repeated views: clones share immutable
|
||||
* geometry and materials while all animation joints remain independent.
|
||||
*/
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
actorMesh,
|
||||
disposeActor,
|
||||
namedGroup,
|
||||
requireGroup,
|
||||
type ActorRigBase,
|
||||
} from "./common.ts";
|
||||
|
||||
export const HUMANOID_METRICS = {
|
||||
height: 1.76,
|
||||
shoulderWidth: 0.46,
|
||||
hipY: 0.94,
|
||||
eyeY: 1.65,
|
||||
} as const;
|
||||
|
||||
export interface HumanoidMaterials {
|
||||
skin: THREE.Material;
|
||||
outfit: THREE.Material;
|
||||
accent: THREE.Material;
|
||||
sole: THREE.Material;
|
||||
hair: THREE.Material;
|
||||
face: THREE.Material;
|
||||
}
|
||||
|
||||
export interface HumanoidBuildOptions {
|
||||
materials?: HumanoidMaterials;
|
||||
skinTone?: THREE.ColorRepresentation;
|
||||
outfitColor?: THREE.ColorRepresentation;
|
||||
accentColor?: THREE.ColorRepresentation;
|
||||
hairColor?: THREE.ColorRepresentation;
|
||||
/** Caller-owned and never disposed by this asset. */
|
||||
faceTexture?: THREE.Texture;
|
||||
bodyShape?: "slim" | "average" | "broad";
|
||||
}
|
||||
|
||||
export interface HumanoidJoints {
|
||||
pelvis: THREE.Group;
|
||||
torso: THREE.Group;
|
||||
head: THREE.Group;
|
||||
shoulderLeft: THREE.Group;
|
||||
shoulderRight: THREE.Group;
|
||||
elbowLeft: THREE.Group;
|
||||
elbowRight: THREE.Group;
|
||||
hipLeft: THREE.Group;
|
||||
hipRight: THREE.Group;
|
||||
kneeLeft: THREE.Group;
|
||||
kneeRight: THREE.Group;
|
||||
}
|
||||
|
||||
export interface HumanoidRig extends ActorRigBase {
|
||||
joints: HumanoidJoints;
|
||||
/** Front-facing surface for a webcam or generated profile face. */
|
||||
face: THREE.Mesh;
|
||||
}
|
||||
|
||||
export interface HumanoidPose {
|
||||
walkPhase?: number;
|
||||
stride?: number;
|
||||
armSwing?: number;
|
||||
headYaw?: number;
|
||||
headPitch?: number;
|
||||
}
|
||||
|
||||
export function createHumanoidMaterials(options: HumanoidBuildOptions = {}): HumanoidMaterials {
|
||||
return {
|
||||
skin: new THREE.MeshStandardMaterial({
|
||||
name: "humanoid.skin",
|
||||
color: options.skinTone ?? 0x9b6246,
|
||||
roughness: 0.68,
|
||||
}),
|
||||
outfit: new THREE.MeshStandardMaterial({
|
||||
name: "humanoid.outfit",
|
||||
color: options.outfitColor ?? 0x26364b,
|
||||
roughness: 0.72,
|
||||
}),
|
||||
accent: new THREE.MeshStandardMaterial({
|
||||
name: "humanoid.accent",
|
||||
color: options.accentColor ?? 0x4fa9c8,
|
||||
roughness: 0.55,
|
||||
}),
|
||||
sole: new THREE.MeshStandardMaterial({
|
||||
name: "humanoid.sole",
|
||||
color: 0x181b1e,
|
||||
roughness: 0.9,
|
||||
}),
|
||||
hair: new THREE.MeshStandardMaterial({
|
||||
name: "humanoid.hair",
|
||||
color: options.hairColor ?? 0x241b18,
|
||||
roughness: 0.86,
|
||||
}),
|
||||
face: new THREE.MeshBasicMaterial({
|
||||
name: "humanoid.face",
|
||||
color: options.faceTexture ? 0xffffff : 0x18242b,
|
||||
map: options.faceTexture ?? null,
|
||||
toneMapped: false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function limb(
|
||||
joint: THREE.Group,
|
||||
name: string,
|
||||
material: THREE.Material,
|
||||
radius: number,
|
||||
length: number,
|
||||
): void {
|
||||
joint.add(
|
||||
actorMesh(name, new THREE.CapsuleGeometry(radius, Math.max(0.01, length - radius * 2), 4, 8), material, {
|
||||
position: [0, -length / 2, 0],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveHumanoid(root: THREE.Group, ownsMaterials: boolean): HumanoidRig {
|
||||
const face = root.getObjectByName("humanoid.face");
|
||||
if (!(face instanceof THREE.Mesh)) throw new Error("humanoid: missing face surface");
|
||||
return {
|
||||
root,
|
||||
ownsMaterials,
|
||||
face,
|
||||
joints: {
|
||||
pelvis: requireGroup(root, "humanoid.pelvis"),
|
||||
torso: requireGroup(root, "humanoid.torso"),
|
||||
head: requireGroup(root, "humanoid.head"),
|
||||
shoulderLeft: requireGroup(root, "humanoid.shoulder.left"),
|
||||
shoulderRight: requireGroup(root, "humanoid.shoulder.right"),
|
||||
elbowLeft: requireGroup(root, "humanoid.elbow.left"),
|
||||
elbowRight: requireGroup(root, "humanoid.elbow.right"),
|
||||
hipLeft: requireGroup(root, "humanoid.hip.left"),
|
||||
hipRight: requireGroup(root, "humanoid.hip.right"),
|
||||
kneeLeft: requireGroup(root, "humanoid.knee.left"),
|
||||
kneeRight: requireGroup(root, "humanoid.knee.right"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildHumanoid(options: HumanoidBuildOptions = {}): HumanoidRig {
|
||||
const materials = options.materials ?? createHumanoidMaterials(options);
|
||||
const shape = options.bodyShape ?? "average";
|
||||
const widthScale = shape === "slim" ? 0.86 : shape === "broad" ? 1.14 : 1;
|
||||
const root = namedGroup("humanoid");
|
||||
root.userData.kind = "actor";
|
||||
root.userData.actorType = "humanoid";
|
||||
root.userData.forwardAxis = "-Z";
|
||||
|
||||
const pelvis = namedGroup("humanoid.pelvis", [0, HUMANOID_METRICS.hipY, 0]);
|
||||
root.add(pelvis);
|
||||
pelvis.add(
|
||||
actorMesh("humanoid.pelvis.shell", new THREE.CapsuleGeometry(0.12, 0.12, 4, 8), materials.outfit, {
|
||||
scale: [1.25 * widthScale, 0.75, 0.9],
|
||||
position: [0, 0.02, 0],
|
||||
}),
|
||||
);
|
||||
|
||||
const torso = namedGroup("humanoid.torso", [0, 0.11, 0]);
|
||||
pelvis.add(torso);
|
||||
torso.add(
|
||||
actorMesh("humanoid.chest", new THREE.CapsuleGeometry(0.18, 0.25, 5, 10), materials.outfit, {
|
||||
position: [0, 0.24, 0],
|
||||
scale: [1.08 * widthScale, 1, 0.72],
|
||||
}),
|
||||
actorMesh("humanoid.chest.accent", new THREE.BoxGeometry(0.2 * widthScale, 0.065, 0.018), materials.accent, {
|
||||
position: [0, 0.32, -0.134],
|
||||
}),
|
||||
);
|
||||
|
||||
const head = namedGroup("humanoid.head", [0, 0.58, 0]);
|
||||
torso.add(head);
|
||||
head.add(
|
||||
actorMesh("humanoid.head.shell", new THREE.SphereGeometry(0.12, 16, 12), materials.skin, {
|
||||
position: [0, 0.1, 0],
|
||||
scale: [0.86, 1.05, 0.86],
|
||||
}),
|
||||
actorMesh("humanoid.hair", new THREE.SphereGeometry(0.122, 14, 8, 0, Math.PI * 2, 0, Math.PI * 0.47), materials.hair, {
|
||||
position: [0, 0.115, 0.004],
|
||||
scale: [0.88, 1.06, 0.88],
|
||||
}),
|
||||
);
|
||||
const face = actorMesh("humanoid.face", new THREE.PlaneGeometry(0.125, 0.105, 2, 2), materials.face, {
|
||||
position: [0, 0.09, -0.105],
|
||||
rotation: [0, Math.PI, 0],
|
||||
receiveShadow: false,
|
||||
});
|
||||
head.add(face);
|
||||
|
||||
const shoulderY = 0.47;
|
||||
for (const side of [-1, 1] as const) {
|
||||
const word = side < 0 ? "left" : "right";
|
||||
const shoulder = namedGroup(`humanoid.shoulder.${word}`, [side * 0.23 * widthScale, shoulderY, 0]);
|
||||
torso.add(shoulder);
|
||||
limb(shoulder, `humanoid.upper-arm.${word}`, materials.outfit, 0.066, 0.3);
|
||||
const elbow = namedGroup(`humanoid.elbow.${word}`, [0, -0.3, 0]);
|
||||
shoulder.add(elbow);
|
||||
limb(elbow, `humanoid.forearm.${word}`, materials.skin, 0.055, 0.27);
|
||||
elbow.add(
|
||||
actorMesh(`humanoid.hand.${word}`, new THREE.SphereGeometry(0.065, 10, 8), materials.skin, {
|
||||
position: [0, -0.295, -0.012],
|
||||
scale: [0.72, 1.08, 0.55],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (const side of [-1, 1] as const) {
|
||||
const word = side < 0 ? "left" : "right";
|
||||
const hip = namedGroup(`humanoid.hip.${word}`, [side * 0.105 * widthScale, -0.02, 0]);
|
||||
pelvis.add(hip);
|
||||
limb(hip, `humanoid.thigh.${word}`, materials.outfit, 0.085, 0.43);
|
||||
const knee = namedGroup(`humanoid.knee.${word}`, [0, -0.43, 0]);
|
||||
hip.add(knee);
|
||||
limb(knee, `humanoid.shin.${word}`, materials.outfit, 0.072, 0.4);
|
||||
knee.add(
|
||||
actorMesh(`humanoid.shoe.${word}`, new THREE.BoxGeometry(0.15, 0.09, 0.25), materials.sole, {
|
||||
position: [0, -0.405, -0.055],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return resolveHumanoid(root, !options.materials);
|
||||
}
|
||||
|
||||
/** Clone the hierarchy while sharing geometry and materials. */
|
||||
export function cloneHumanoid(source: HumanoidRig): HumanoidRig {
|
||||
return resolveHumanoid(source.root.clone(true), false);
|
||||
}
|
||||
|
||||
export function poseHumanoid(rig: HumanoidRig, pose: HumanoidPose = {}): void {
|
||||
const phase = pose.walkPhase ?? 0;
|
||||
const stride = THREE.MathUtils.clamp(pose.stride ?? 0, 0, 0.75);
|
||||
const arm = THREE.MathUtils.clamp(pose.armSwing ?? stride * 0.85, 0, 0.7);
|
||||
const swing = Math.sin(phase);
|
||||
const bendLeft = Math.max(0, -swing) * stride * 0.7;
|
||||
const bendRight = Math.max(0, swing) * stride * 0.7;
|
||||
rig.joints.hipLeft.rotation.x = swing * stride;
|
||||
rig.joints.hipRight.rotation.x = -swing * stride;
|
||||
rig.joints.kneeLeft.rotation.x = -bendLeft;
|
||||
rig.joints.kneeRight.rotation.x = -bendRight;
|
||||
rig.joints.shoulderLeft.rotation.x = -swing * arm;
|
||||
rig.joints.shoulderRight.rotation.x = swing * arm;
|
||||
rig.joints.elbowLeft.rotation.x = 0.1 + Math.max(0, swing) * 0.18;
|
||||
rig.joints.elbowRight.rotation.x = 0.1 + Math.max(0, -swing) * 0.18;
|
||||
rig.joints.pelvis.position.y = HUMANOID_METRICS.hipY + Math.abs(Math.cos(phase)) * stride * 0.018;
|
||||
rig.joints.head.rotation.y = THREE.MathUtils.clamp(pose.headYaw ?? 0, -0.9, 0.9);
|
||||
rig.joints.head.rotation.x = THREE.MathUtils.clamp(pose.headPitch ?? 0, -0.45, 0.45);
|
||||
}
|
||||
|
||||
export function disposeHumanoid(
|
||||
rig: HumanoidRig,
|
||||
options: { disposeMaterials?: boolean } = {},
|
||||
): void {
|
||||
disposeActor(rig, options);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
export {
|
||||
HUMANOID_METRICS,
|
||||
buildHumanoid,
|
||||
cloneHumanoid,
|
||||
createHumanoidMaterials,
|
||||
disposeHumanoid,
|
||||
poseHumanoid,
|
||||
type HumanoidBuildOptions,
|
||||
type HumanoidJoints,
|
||||
type HumanoidMaterials,
|
||||
type HumanoidPose,
|
||||
type HumanoidRig,
|
||||
} from "./humanoid.ts";
|
||||
|
||||
export {
|
||||
DOG_METRICS,
|
||||
buildDog,
|
||||
cloneDog,
|
||||
createDogMaterials,
|
||||
disposeDog,
|
||||
poseDogAttention,
|
||||
poseDogWalk,
|
||||
type DogBuildOptions,
|
||||
type DogJoints,
|
||||
type DogMaterials,
|
||||
type DogRig,
|
||||
} from "./dog.ts";
|
||||
|
||||
export {
|
||||
CROW_METRICS,
|
||||
buildCrow,
|
||||
cloneCrow,
|
||||
createCrowMaterials,
|
||||
disposeCrow,
|
||||
poseCrowFlight,
|
||||
type CrowBuildOptions,
|
||||
type CrowJoints,
|
||||
type CrowMaterials,
|
||||
type CrowRig,
|
||||
} from "./crow.ts";
|
||||
Reference in New Issue
Block a user