Epic 5 Phase 0 — actors have bodies; the stage has depth
ActorGenerator is the cast with bodies: generateActor{mpl} takes
{summary,rng,archetype,personality,identity} and returns a serialisable
ActorSpec — same audio-tilts-centre / seed-picks-within rule as
Personality/Identity, forked rng so adding an actor never shifts later
decisions. Five archetypes (monolith/swarm/walker/vehicle/structure),
per-track actor set on look.actors, HUD helper included. Stage C will
grow as a library on this without infra changes.
Mesh twin of Identity.form: actors/meshes.js builds BufferGeometry from
the same assembly (cast SDF → Shape → ExtrudeGeometry, box/capsule/
torus/sphere primitives, symmetry folding radial/mirror/stack). Shared
with the shader impostor path — one character, two projectors.
Renderer depth targets: createDepthTarget / createTarget{depthTexture}
for WebGL DepthTexture plumbing.
Compositor shared rig: one PerspectiveCamera + DepthTexture so a ground
mesh can occlude a subject mesh from another layer. 4/scale dolly,
Personality.camera drift/sway/spin, framing shift — matches particles.js
and shader epilogue behaviour. ModelLayer (kind:model) with
build/update(actorSpec) and sharedCamera injection; createLayer dispatches
on model. Shader contract gains MODEL_PREAMBLE.
LookGenerator now derives actors before scenes; ArcDriver._actorFor +
_layerFor wires ActorSpec into ModelLayer; schema validates kind:model
and actor archetype; lint determinism gate covers actors/.
Gate: lint 107 files clean, 70 shader literals, 68 scenes green; vite
build 294 modules; ActorGenerator determinism + mesh smoke tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -317,7 +317,90 @@ export class SceneLayer extends Layer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A 3D model layer — the mesh twin of the shader impostor.
|
||||
*
|
||||
* Like SceneLayer it owns a THREE.Scene and receives build/update hooks, but
|
||||
* its geometry comes from the song's ActorSpec (the mesh assembly), not from
|
||||
* a hand-written point cloud. Determinism rule is the same: no integration,
|
||||
* only analytic f(time, index, seed). See processors/meshes.js and
|
||||
* src/actors/ActorGenerator.js.
|
||||
*
|
||||
* The camera is borrowed from Compositor.sharedCamera when one exists, so
|
||||
* multiple ModelLayers share one perspective and one depth buffer — that is
|
||||
* what makes a ground mesh occlude a subject mesh from another layer.
|
||||
* Falls back to its own camera when no shared rig is present (tests, solo
|
||||
* preview), so existing SceneLayer behaviour is unchanged.
|
||||
*/
|
||||
export class ModelLayer extends Layer {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.scene = new THREE.Scene();
|
||||
this.camera = new THREE.PerspectiveCamera(60, 16 / 9, 0.1, 200);
|
||||
this.camera.position.set(0, 0, 5);
|
||||
this.actorSpec = options.actorSpec || null;
|
||||
// Shared rig injected by Compositor at render time when available.
|
||||
this.sharedCamera = null;
|
||||
this.instance = this.module.build({
|
||||
scene: this.scene,
|
||||
camera: this.camera,
|
||||
seed: this.seed,
|
||||
params: this.baseParams,
|
||||
actorSpec: this.actorSpec,
|
||||
THREE,
|
||||
});
|
||||
}
|
||||
|
||||
/** Allow the look to swap the actor without rebuilding the layer. */
|
||||
setActor(actorSpec) {
|
||||
this.actorSpec = actorSpec || null;
|
||||
return this;
|
||||
}
|
||||
|
||||
render(renderer, target, ctx) {
|
||||
const { timeline, features } = ctx;
|
||||
const w = target ? target.width : renderer.width;
|
||||
const h = target ? target.height : renderer.height;
|
||||
// Use the compositor's shared camera when it has been injected; it is
|
||||
// updated centrally from ArcDriver's framing/gaze so every model layer
|
||||
// shares one perspective and one depth, and a ground in one layer can
|
||||
// occlude a subject in another.
|
||||
const cam = this.sharedCamera || this.camera;
|
||||
if (cam.aspect !== w / h) {
|
||||
cam.aspect = w / h;
|
||||
cam.updateProjectionMatrix();
|
||||
}
|
||||
const resolved = this.resolveParams(features);
|
||||
this.module.update({
|
||||
instance: this.instance,
|
||||
scene: this.scene,
|
||||
camera: cam,
|
||||
timeline,
|
||||
features: features || {},
|
||||
params: resolved,
|
||||
palette: this.palette,
|
||||
personality: this.personality,
|
||||
framing: this.framing,
|
||||
opacity: this.opacity,
|
||||
actorSpec: this.actorSpec,
|
||||
THREE,
|
||||
});
|
||||
renderer.renderScene(this.scene, cam, target, true);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.scene.traverse((obj) => {
|
||||
if (obj.geometry) obj.geometry.dispose();
|
||||
if (obj.material) {
|
||||
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
||||
mats.forEach((m) => m.dispose());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createLayer(module, options) {
|
||||
if (module.kind === 'model') return new ModelLayer({ module, ...options });
|
||||
if (module.kind === 'layer3d') return new SceneLayer({ module, ...options });
|
||||
return new ShaderLayer({ module, ...options });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user