downtown demo scene: paid Downtown City MegaKit block (texture-crushed to 512), kerbside quaternius vehicles, dusk fog lighting

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-30 11:34:44 +10:00
parent 2a5449cc24
commit f32747d2ce
13 changed files with 78 additions and 50 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/city/Cop.glb Normal file

Binary file not shown.

BIN
assets/city/NormalCar1.glb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/city/Taxi.glb Normal file

Binary file not shown.

View File

@ -4,7 +4,7 @@ import { GLTFLoader } from '../vendor/GLTFLoader.js';
import { OrbitControls } from '../vendor/OrbitControls.js'; import { OrbitControls } from '../vendor/OrbitControls.js';
import { RetroScreen, retroState, retrofyObject, patchRetroMaterial } from './retro.js'; import { RetroScreen, retroState, retrofyObject, patchRetroMaterial } from './retro.js';
const FOG_COLOR = 0x2a2036; const FOG_COLOR = 0x8d84a8;
const renderer = new THREE.WebGLRenderer({ antialias: false }); const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setPixelRatio(1); renderer.setPixelRatio(1);
@ -12,70 +12,98 @@ document.getElementById('view').appendChild(renderer.domElement);
const scene = new THREE.Scene(); const scene = new THREE.Scene();
scene.background = new THREE.Color(FOG_COLOR); scene.background = new THREE.Color(FOG_COLOR);
scene.fog = new THREE.Fog(FOG_COLOR, 6, 26); scene.fog = new THREE.Fog(FOG_COLOR, 14, 55);
const camera = new THREE.PerspectiveCamera(60, 4 / 3, 0.1, 100); const camera = new THREE.PerspectiveCamera(60, 4 / 3, 0.1, 100);
camera.position.set(3.2, 2.2, 4.2); camera.position.set(6.5, 3.2, 9.5);
const controls = new OrbitControls(camera, renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0); controls.target.set(0, 1, 0);
controls.enableDamping = true; controls.enableDamping = true;
scene.add(new THREE.HemisphereLight(0xcabfe8, 0x4a3c5e, 1.6)); scene.add(new THREE.HemisphereLight(0xe8e2f5, 0x8a7a95, 2.4));
const sun = new THREE.DirectionalLight(0xffeedd, 2.2); const sun = new THREE.DirectionalLight(0xfff2dd, 3.2);
sun.position.set(4, 6, 3); sun.position.set(4, 6, 3);
scene.add(sun); scene.add(sun);
const screen = new RetroScreen(renderer); const screen = new RetroScreen(renderer);
// --- procedural retro textures (canvas, nearest) ------------------------------ // --- downtown block (paid Downtown City MegaKit, texture-crushed) --------------
function canvasTexture(draw, size = 64) { const cityLoader = new GLTFLoader();
const c = document.createElement('canvas'); function loadPiece(name) {
c.width = c.height = size; return new Promise((res, rej) =>
draw(c.getContext('2d'), size); cityLoader.load('assets/city/' + name + '.glb', (g) => {
const t = new THREE.CanvasTexture(c); retrofyObject(g.scene);
t.magFilter = THREE.NearestFilter; res(g.scene);
t.minFilter = THREE.NearestFilter; }, undefined, rej));
t.generateMipmaps = false; }
t.wrapS = t.wrapT = THREE.RepeatWrapping; function sizeOf(obj) {
return t; const b = new THREE.Box3().setFromObject(obj);
return { size: b.getSize(new THREE.Vector3()), min: b.min, max: b.max };
}
function put(proto, x, z, ry = 0) {
const c = proto.clone(true);
c.position.set(x, 0, z);
c.rotation.y = ry;
scene.add(c);
return c;
} }
const checkerTex = canvasTexture((g, s) => {
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) {
g.fillStyle = (x + y) % 2 ? '#4a3f66' : '#5d5080';
g.fillRect(x * s / 8, y * s / 8, s / 8, s / 8);
}
});
checkerTex.repeat.set(12, 12);
const brickTex = canvasTexture((g, s) => {
g.fillStyle = '#6e5a4e'; g.fillRect(0, 0, s, s);
g.fillStyle = '#8a7263';
for (let y = 0; y < 4; y++) for (let x = 0; x < 4; x++)
g.fillRect(x * 16 + (y % 2 ? 8 : 0) + 1, y * 16 + 1, 14, 14);
});
const ground = new THREE.Mesh( // dark asphalt base plane so there are no holes at the fog line
new THREE.PlaneGeometry(40, 40), const base = new THREE.Mesh(
patchRetroMaterial(new THREE.MeshLambertMaterial({ map: checkerTex })) new THREE.PlaneGeometry(120, 120),
patchRetroMaterial(new THREE.MeshLambertMaterial({ color: 0x2b2b30 }))
); );
ground.rotation.x = -Math.PI / 2; base.rotation.x = -Math.PI / 2;
scene.add(ground); base.position.y = -0.03;
scene.add(base);
// scatter some chunky geometry so the wobble has something to chew on (async () => {
const brickMat = patchRetroMaterial(new THREE.MeshLambertMaterial({ map: brickTex })); try {
const shapes = []; const [street, walk, bSmall, bMed, bLarge, bollard, planter, taxi, car, cop] =
for (let i = 0; i < 14; i++) { await Promise.all([
const kind = i % 3; loadPiece('Street_2Lane'), loadPiece('Sidewalk_Straight_3m'),
const geo = kind === 0 ? new THREE.BoxGeometry(1.4, 1 + (i % 4), 1.4) loadPiece('Building_Small_1'), loadPiece('Building_Medium_2_001'),
: kind === 1 ? new THREE.ConeGeometry(0.9, 1.8, 6) loadPiece('Building_Large_2'), loadPiece('Prop_Bollard'),
: new THREE.CylinderGeometry(0.6, 0.6, 1.4, 8); loadPiece('Prop_Planter_Single'), loadPiece('Taxi'),
const m = new THREE.Mesh(geo, brickMat); loadPiece('NormalCar1'), loadPiece('Cop'),
const a = (i / 14) * Math.PI * 2; ]);
const r = 6 + (i % 3) * 3; const st = sizeOf(street);
m.position.set(Math.cos(a) * r, geo.parameters.height ? geo.parameters.height / 2 : 0.7, Math.sin(a) * r); const stepX = st.size.x || 3; // tile the road along X
m.rotation.y = a; const roadHalf = (st.size.z || 9) / 2;
scene.add(m); for (let i = -5; i <= 5; i++) put(street, i * stepX, 0);
shapes.push(m); const wk = sizeOf(walk);
} const wkStep = wk.size.x || 3;
for (let i = -10; i <= 10; i++) {
put(walk, i * wkStep, roadHalf + wk.size.z / 2);
put(walk, i * wkStep, -(roadHalf + wk.size.z / 2), Math.PI);
}
// buildings lining both sides, fronts to the road
const row = roadHalf + wk.size.z + 0.2;
const north = [bSmall, bMed, bLarge, bSmall];
let x = -22;
for (const b of north) {
const bs = sizeOf(b);
put(b, x + bs.size.x / 2, row + bs.size.z / 2, Math.PI);
x += bs.size.x + 1.2;
}
const south = [bLarge, bSmall, bMed];
x = -18;
for (const b of south) {
const bs = sizeOf(b);
put(b, x + bs.size.x / 2, -(row + bs.size.z / 2));
x += bs.size.x + 1.5;
}
// kerbside cars + props
put(taxi, -4, roadHalf - 1.2, Math.PI);
put(car, 5, roadHalf - 1.2, Math.PI);
put(cop, 10, -(roadHalf - 1.2));
for (let i = -3; i <= 3; i++) put(bollard, i * 6 + 1.5, roadHalf + 0.6);
put(planter, -8, roadHalf + 1.4);
put(planter, 8, roadHalf + 1.4);
} catch (e) {
console.error('city load failed', e);
}
})();
// --- character ---------------------------------------------------------------- // --- character ----------------------------------------------------------------
let mixer = null; let mixer = null;