import * as THREE from "three"; function canvasTex(size, draw, repeatX = 1, repeatY = 1) { const c = document.createElement("canvas"); c.width = c.height = size; draw(c.getContext("2d"), size); const t = new THREE.CanvasTexture(c); t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(repeatX, repeatY); t.colorSpace = THREE.SRGBColorSpace; t.anisotropy = 8; return t; } function rand(seed) { let s = seed; return () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; }; } /* asphalt shingle courses with staggered tabs */ function drawShingles(g, S, base, seed = 7, weather = 0) { const r = rand(seed); g.fillStyle = base; g.fillRect(0, 0, S, S); const course = S / 10; for (let row = 0; row < 10; row++) { const y = row * course; const off = (row % 2) * (course * 1.1); for (let x = -1; x < 8; x++) { const w = course * 2.2; const px = x * w + off; const shade = 0.82 + r() * 0.3 - weather * r() * 0.25; g.fillStyle = `rgba(0,0,0,${Math.max(0, 1 - shade) * 0.9})`; g.fillRect(px, y, w - 1.5, course - 1.5); if (r() < 0.12 * (1 + weather * 3)) { g.fillStyle = `rgba(255,255,255,${0.04 + r() * 0.05})`; g.fillRect(px, y, w - 1.5, course - 1.5); } } g.fillStyle = "rgba(0,0,0,.5)"; g.fillRect(0, y + course - 1.5, S, 1.5); } } /* hail damage overlay: bruise blotches + torn patches */ function drawStormShingles(g, S) { drawShingles(g, S, "#4c4a45", 11, 1); const r = rand(41); for (let i = 0; i < 7; i++) { const x = r() * S, y = r() * S, rad = 18 + r() * 46; const gr = g.createRadialGradient(x, y, 2, x, y, rad); gr.addColorStop(0, "rgba(140,60,20,.5)"); gr.addColorStop(0.6, "rgba(120,50,25,.22)"); gr.addColorStop(1, "rgba(0,0,0,0)"); g.fillStyle = gr; g.fillRect(x - rad, y - rad, rad * 2, rad * 2); } for (let i = 0; i < 3; i++) { const x = r() * S * 0.8, y = r() * S * 0.8; g.fillStyle = "rgba(15,14,12,.85)"; g.fillRect(x, y, 26 + r() * 30, 12 + r() * 10); } } function drawClapboard(g, S, base, shadow) { const board = S / 12; for (let i = 0; i < 12; i++) { const y = i * board; const gr = g.createLinearGradient(0, y, 0, y + board); gr.addColorStop(0, base); gr.addColorStop(0.85, base); gr.addColorStop(1, shadow); g.fillStyle = gr; g.fillRect(0, y, S, board); } const r = rand(5); g.fillStyle = "rgba(0,0,0,.05)"; for (let i = 0; i < 40; i++) g.fillRect(r() * S, r() * S, 1.5, 4 + r() * 8); } function drawBrick(g, S) { g.fillStyle = "#8a8078"; g.fillRect(0, 0, S, S); const bh = S / 10, bw = S / 4; const r = rand(23); for (let row = 0; row < 10; row++) { const off = (row % 2) * bw * 0.5; for (let col = -1; col < 5; col++) { const tone = 96 + r() * 46; g.fillStyle = `rgb(${tone + 20},${tone * 0.62},${tone * 0.5})`; g.fillRect(col * bw + off + 2, row * bh + 2, bw - 4, bh - 4); } } } function drawConcrete(g, S, tone = 168) { g.fillStyle = `rgb(${tone},${tone},${tone - 4})`; g.fillRect(0, 0, S, S); const r = rand(3); for (let i = 0; i < 900; i++) { const v = r(); g.fillStyle = v > 0.5 ? "rgba(255,255,255,.05)" : "rgba(0,0,0,.06)"; g.fillRect(r() * S, r() * S, 1 + r() * 2, 1 + r() * 2); } } function drawAsphalt(g, S) { g.fillStyle = "#292b2a"; g.fillRect(0, 0, S, S); const r = rand(17); for (let i = 0; i < 1200; i++) { const v = r(); g.fillStyle = v > 0.55 ? "rgba(255,255,255,.05)" : "rgba(0,0,0,.12)"; g.fillRect(r() * S, r() * S, 1 + r() * 2, 1 + r() * 2); } } /* grass detail and the radial alpha falloff baked into ONE texture (repeat 1x1), because standard materials share the map's UV transform across texture slots */ function drawGrassDisc(g, S) { g.fillStyle = "#242d1b"; g.fillRect(0, 0, S, S); const r = rand(9); for (let i = 0; i < 26000; i++) { const v = r(); const c = v > 0.66 ? "rgba(58,74,40,.5)" : v > 0.33 ? "rgba(34,44,26,.55)" : "rgba(20,26,16,.5)"; g.fillStyle = c; g.fillRect(r() * S, r() * S, 1 + r() * 2.4, 1 + r() * 2); } /* mow lines */ g.fillStyle = "rgba(255,255,255,.016)"; for (let i = 0; i < 14; i++) g.fillRect(0, i * (S / 14), S, S / 28); /* carve the radial fade into the alpha channel */ g.globalCompositeOperation = "destination-in"; const gr = g.createRadialGradient(S / 2, S / 2, S * 0.18, S / 2, S / 2, S * 0.5); gr.addColorStop(0, "rgba(0,0,0,1)"); gr.addColorStop(0.5, "rgba(0,0,0,1)"); gr.addColorStop(0.78, "rgba(0,0,0,.45)"); gr.addColorStop(0.98, "rgba(0,0,0,0)"); g.fillStyle = gr; g.fillRect(0, 0, S, S); g.globalCompositeOperation = "source-over"; } /* street strip: asphalt with lane dashes, alpha-faded at both ends */ export function drawRoad(g, S) { drawAsphalt(g, S); g.fillStyle = "rgba(210,200,160,.5)"; for (let i = 0; i < 9; i++) g.fillRect(i * (S / 9) + S / 36, S * 0.485, S / 18, S * 0.03); g.fillStyle = "rgba(220,220,220,.28)"; g.fillRect(0, S * 0.06, S, S * 0.016); g.fillRect(0, S * 0.924, S, S * 0.016); g.globalCompositeOperation = "destination-in"; const gr = g.createLinearGradient(0, 0, S, 0); gr.addColorStop(0, "rgba(0,0,0,0)"); gr.addColorStop(0.18, "rgba(0,0,0,1)"); gr.addColorStop(0.82, "rgba(0,0,0,1)"); gr.addColorStop(1, "rgba(0,0,0,0)"); g.fillStyle = gr; g.fillRect(0, 0, S, S); g.globalCompositeOperation = "source-over"; } /* soft dark contact blob that grounds whatever stands on it */ export function drawContact(g, S) { const gr = g.createRadialGradient(S / 2, S / 2, S * 0.05, S / 2, S / 2, S * 0.5); gr.addColorStop(0, "rgba(0,0,0,.62)"); gr.addColorStop(0.55, "rgba(0,0,0,.34)"); gr.addColorStop(1, "rgba(0,0,0,0)"); g.fillStyle = gr; g.fillRect(0, 0, S, S); } function drawGaragePanels(g, S) { g.fillStyle = "#d8d6d0"; g.fillRect(0, 0, S, S); const ph = S / 4; for (let i = 0; i < 4; i++) { const y = i * ph; const gr = g.createLinearGradient(0, y, 0, y + ph); gr.addColorStop(0, "rgba(0,0,0,.22)"); gr.addColorStop(0.14, "rgba(255,255,255,.16)"); gr.addColorStop(0.85, "rgba(0,0,0,.05)"); gr.addColorStop(1, "rgba(0,0,0,.34)"); g.fillStyle = gr; g.fillRect(0, y, S, ph); } } /* soft elliptical glow for the LiDAR sheet: fades before every plane edge */ function drawScanBand(g, S) { const gr = g.createRadialGradient(S / 2, S / 2, S * 0.05, S / 2, S / 2, S * 0.5); gr.addColorStop(0, "rgba(255,214,90,.85)"); gr.addColorStop(0.45, "rgba(255,184,0,.34)"); gr.addColorStop(1, "rgba(255,184,0,0)"); g.fillStyle = gr; g.fillRect(0, 0, S, S); } export function makeMaterials() { const std = (o) => new THREE.MeshStandardMaterial(o); const shingleTex = canvasTex(256, (g, s) => drawShingles(g, s, "#2b2e32", 7), 6, 3); const shingleWarmTex = canvasTex(256, (g, s) => drawShingles(g, s, "#352e26", 19), 6, 3); const stormTex = canvasTex(512, (g, s) => drawStormShingles(g, s), 3, 1.6); return { shingle: std({ map: shingleTex, roughness: 0.92, metalness: 0.02 }), shingleWarm: std({ map: shingleWarmTex, roughness: 0.92, metalness: 0.02 }), shingleStorm: std({ map: stormTex, roughness: 0.95, metalness: 0.02 }), sidingWhite: std({ map: canvasTex(256, (g, s) => drawClapboard(g, s, "#c9c6bd", "#8f8d85"), 3, 3), roughness: 0.85 }), sidingBeige: std({ map: canvasTex(256, (g, s) => drawClapboard(g, s, "#b3a58c", "#7d7360"), 3, 3), roughness: 0.85 }), sidingGray: std({ map: canvasTex(256, (g, s) => drawClapboard(g, s, "#9aa0a2", "#6a6f71"), 3, 3), roughness: 0.85 }), trim: std({ color: 0xe8e6df, roughness: 0.6 }), fascia: std({ color: 0xdad7cf, roughness: 0.65 }), doorRed: std({ color: 0x7e2f26, roughness: 0.5 }), doorBlue: std({ color: 0x2c3f4d, roughness: 0.5 }), glass: std({ color: 0x0e1a22, roughness: 0.08, metalness: 0.85, envMapIntensity: 1.4 }), garage: std({ map: canvasTex(256, drawGaragePanels, 1, 1), roughness: 0.6 }), brick: std({ map: canvasTex(256, drawBrick, 1.4, 2.4), roughness: 0.95 }), concrete: std({ map: canvasTex(256, (g, s) => drawConcrete(g, s), 2, 2), roughness: 0.95 }), asphalt: std({ map: canvasTex(256, drawAsphalt, 2, 3), roughness: 0.98 }), grass: std({ map: canvasTex(2048, drawGrassDisc), transparent: true, roughness: 1, depthWrite: false, }), soil: std({ color: 0x2b2822, roughness: 1 }), trunk: std({ color: 0x3c2f21, roughness: 1 }), foliage: std({ color: 0x33431f, roughness: 1, flatShading: true }), foliage2: std({ color: 0x415229, roughness: 1, flatShading: true }), contact: new THREE.MeshBasicMaterial({ map: canvasTex(512, drawContact), transparent: true, depthWrite: false, }), road: std({ map: canvasTex(512, drawRoad), transparent: true, roughness: 0.98, depthWrite: false }), tarp: std({ color: 0x27506e, roughness: 0.55, side: THREE.DoubleSide }), porchRoof: std({ color: 0x3a3d40, roughness: 0.7, metalness: 0.25 }), hlAmber: new THREE.MeshBasicMaterial({ color: 0xffb800, transparent: true, opacity: 0.95 }), hlFace: new THREE.MeshBasicMaterial({ color: 0xffc233, transparent: true, opacity: 0.62, side: THREE.DoubleSide, depthWrite: false }), impact: std({ color: 0xff5533, emissive: 0xd23c14, emissiveIntensity: 0.9, roughness: 0.6 }), scan: new THREE.MeshBasicMaterial({ map: canvasTex(256, drawScanBand), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, }), }; } /* alpha-fade note: the grass disc uses alphaMap + depthWrite:false so the lawn dissolves radially into the page background instead of ending in a hard edge */