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 { RetroScreen, retroState, retrofyObject, patchRetroMaterial } from './retro.js';
const FOG_COLOR = 0x2a2036;
const FOG_COLOR = 0x8d84a8;
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setPixelRatio(1);
@ -12,70 +12,98 @@ document.getElementById('view').appendChild(renderer.domElement);
const scene = new THREE.Scene();
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);
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);
controls.target.set(0, 1, 0);
controls.enableDamping = true;
scene.add(new THREE.HemisphereLight(0xcabfe8, 0x4a3c5e, 1.6));
const sun = new THREE.DirectionalLight(0xffeedd, 2.2);
scene.add(new THREE.HemisphereLight(0xe8e2f5, 0x8a7a95, 2.4));
const sun = new THREE.DirectionalLight(0xfff2dd, 3.2);
sun.position.set(4, 6, 3);
scene.add(sun);
const screen = new RetroScreen(renderer);
// --- procedural retro textures (canvas, nearest) ------------------------------
function canvasTexture(draw, size = 64) {
const c = document.createElement('canvas');
c.width = c.height = size;
draw(c.getContext('2d'), size);
const t = new THREE.CanvasTexture(c);
t.magFilter = THREE.NearestFilter;
t.minFilter = THREE.NearestFilter;
t.generateMipmaps = false;
t.wrapS = t.wrapT = THREE.RepeatWrapping;
return t;
// --- downtown block (paid Downtown City MegaKit, texture-crushed) --------------
const cityLoader = new GLTFLoader();
function loadPiece(name) {
return new Promise((res, rej) =>
cityLoader.load('assets/city/' + name + '.glb', (g) => {
retrofyObject(g.scene);
res(g.scene);
}, undefined, rej));
}
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);
function sizeOf(obj) {
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;
}
});
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(
new THREE.PlaneGeometry(40, 40),
patchRetroMaterial(new THREE.MeshLambertMaterial({ map: checkerTex }))
// dark asphalt base plane so there are no holes at the fog line
const base = new THREE.Mesh(
new THREE.PlaneGeometry(120, 120),
patchRetroMaterial(new THREE.MeshLambertMaterial({ color: 0x2b2b30 }))
);
ground.rotation.x = -Math.PI / 2;
scene.add(ground);
base.rotation.x = -Math.PI / 2;
base.position.y = -0.03;
scene.add(base);
// scatter some chunky geometry so the wobble has something to chew on
const brickMat = patchRetroMaterial(new THREE.MeshLambertMaterial({ map: brickTex }));
const shapes = [];
for (let i = 0; i < 14; i++) {
const kind = i % 3;
const geo = kind === 0 ? new THREE.BoxGeometry(1.4, 1 + (i % 4), 1.4)
: kind === 1 ? new THREE.ConeGeometry(0.9, 1.8, 6)
: new THREE.CylinderGeometry(0.6, 0.6, 1.4, 8);
const m = new THREE.Mesh(geo, brickMat);
const a = (i / 14) * Math.PI * 2;
const r = 6 + (i % 3) * 3;
m.position.set(Math.cos(a) * r, geo.parameters.height ? geo.parameters.height / 2 : 0.7, Math.sin(a) * r);
m.rotation.y = a;
scene.add(m);
shapes.push(m);
(async () => {
try {
const [street, walk, bSmall, bMed, bLarge, bollard, planter, taxi, car, cop] =
await Promise.all([
loadPiece('Street_2Lane'), loadPiece('Sidewalk_Straight_3m'),
loadPiece('Building_Small_1'), loadPiece('Building_Medium_2_001'),
loadPiece('Building_Large_2'), loadPiece('Prop_Bollard'),
loadPiece('Prop_Planter_Single'), loadPiece('Taxi'),
loadPiece('NormalCar1'), loadPiece('Cop'),
]);
const st = sizeOf(street);
const stepX = st.size.x || 3; // tile the road along X
const roadHalf = (st.size.z || 9) / 2;
for (let i = -5; i <= 5; i++) put(street, i * stepX, 0);
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 ----------------------------------------------------------------
let mixer = null;