Districts that look like places, and buildings worth steering by
Every building was the same grey box between four and twelve metres tall. From the air the whole world was one texture; from the driver's seat every junction looked like the junction before it, and "learn the map" was not something the map supported. The world is now cut into districts, and a district agrees with itself: its buildings share a palette, a rough height and a silhouette and vary only slightly around them. That agreement is the point - it is what makes crossing into the next one a visible event, and it is the only reason the exceptions read at all. Because the occasional building ignores its district completely: much taller, lighter, a different shape. Those are the things you actually navigate by. They clear every ordinary roofline on the map, since a landmark you cannot see from outside its own district is no use in the only place you need it. Colour rides on the instance rather than the material, so a district costs no extra draw calls. Colliders stay boxes whatever the silhouette says - both the physics and the line-of-sight grid already assume a building is solid to its own footprint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+37
-18
@@ -138,29 +138,48 @@ export function createScene(model: WorldModel): SceneView {
|
||||
}
|
||||
|
||||
// --- Obstacles ---
|
||||
// Buildings never move, so they go into one instanced draw call. With a few
|
||||
// hundred of them, a mesh each was a real cost in a frame budget that also has
|
||||
// to fit a shadow pass.
|
||||
const blockMat = new THREE.MeshStandardMaterial({ color: 0x767c82, roughness: 0.9 });
|
||||
// Buildings never move, so they go into instanced draw calls: one per
|
||||
// silhouette. With a few hundred of them, a mesh each was a real cost in a
|
||||
// frame budget that also has to fit a shadow pass.
|
||||
//
|
||||
// Colour is per instance rather than per material, which is what lets a
|
||||
// district agree with itself without needing a material per district.
|
||||
const blockMat = new THREE.MeshStandardMaterial({ roughness: 0.9 });
|
||||
const crateMat = new THREE.MeshStandardMaterial({ color: 0xa9773f, roughness: 0.8 });
|
||||
const boxGeo = new THREE.BoxGeometry(1, 1, 1);
|
||||
// Unit-sized like the box, so one scale vector drives every shape. Both are
|
||||
// centred on the origin and one metre tall, so `height / 2` places them all.
|
||||
const roundGeo = new THREE.CylinderGeometry(0.5, 0.5, 1, 12);
|
||||
const spireGeo = new THREE.ConeGeometry(0.72, 1, 4).rotateY(Math.PI / 4);
|
||||
|
||||
const blocks = model.obstacles.filter((o) => o.kind === 'block');
|
||||
const blockMesh = new THREE.InstancedMesh(boxGeo, blockMat, Math.max(1, blocks.length));
|
||||
blockMesh.castShadow = true;
|
||||
blockMesh.receiveShadow = true;
|
||||
const transform = new THREE.Matrix4();
|
||||
const quaternion = new THREE.Quaternion();
|
||||
blocks.forEach((o, i) => {
|
||||
transform.compose(
|
||||
new THREE.Vector3(o.x, o.height / 2, o.z),
|
||||
quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), o.yaw),
|
||||
new THREE.Vector3(o.width, o.height, o.depth),
|
||||
);
|
||||
blockMesh.setMatrixAt(i, transform);
|
||||
});
|
||||
blockMesh.instanceMatrix.needsUpdate = true;
|
||||
scene.add(blockMesh);
|
||||
const instanceColour = new THREE.Color();
|
||||
const blocks = model.obstacles.filter((o) => o.kind === 'block');
|
||||
|
||||
for (const [shape, geometry] of [
|
||||
['box', boxGeo],
|
||||
['round', roundGeo],
|
||||
['spire', spireGeo],
|
||||
] as const) {
|
||||
const of = blocks.filter((o) => o.shape === shape);
|
||||
if (of.length === 0) continue;
|
||||
const mesh = new THREE.InstancedMesh(geometry, blockMat, of.length);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
of.forEach((o, i) => {
|
||||
transform.compose(
|
||||
new THREE.Vector3(o.x, o.height / 2, o.z),
|
||||
quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), o.yaw),
|
||||
new THREE.Vector3(o.width, o.height, o.depth),
|
||||
);
|
||||
mesh.setMatrixAt(i, transform);
|
||||
mesh.setColorAt(i, instanceColour.setHSL(o.hue, o.saturation, o.lightness));
|
||||
});
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
|
||||
scene.add(mesh);
|
||||
}
|
||||
|
||||
// Crates can be shoved around, so they stay individual meshes that get synced.
|
||||
const crates = model.obstacles
|
||||
|
||||
Reference in New Issue
Block a user