1/** 2 * MUSEUM GEOMETRY 3 * Grand natural history museum inspired by the Field Museum. 4 * Single grand hall with four exhibit wings in a cross layout. 5 */ 6 7const Museum = (() => { 8 const WALL_HEIGHT = 8; 9 const WALL_THICKNESS = 0.4; 10 const DOOR_WIDTH = 4.5; 11 const DOOR_HEIGHT = 5.5; 12 13 // Grand hall â one big open space (Field Museum style) 14 const GRAND_HALL_WIDTH = 24; 15 const GRAND_HALL_DEPTH = 24; 16 17 // Exhibit wings 18 const HALL_WIDTH = 12; 19 const HALL_DEPTH = 22; 20 21 const collisionWalls = []; 22 const rooms = []; 23 24 // ===================== HIGH-QUALITY PROCEDURAL TEXTURES ===================== 25 26 function createMarbleTexture(light, dark, tileCount) { 27 const canvas = document.createElement('canvas'); 28 canvas.width = 512; 29 canvas.height = 512; 30 const ctx = canvas.getContext('2d'); 31 const ts = canvas.width / tileCount; 32 33 for (let tx = 0; tx < tileCount; tx++) { 34 for (let ty = 0; ty < tileCount; ty++) { 35 const isL = (tx + ty) % 2 === 0; 36 const c = isL ? light : dark; 37 ctx.fillStyle = `rgb(${c[0]},${c[1]},${c[2]})`; 38 ctx.fillRect(tx * ts, ty * ts, ts, ts); 39 40 // Marble veining 41 for (let v = 0; v < 3; v++) { 42 const sx = tx * ts + Math.random() * ts; 43 const sy = ty * ts + Math.random() * ts; 44 const grad = ctx.createLinearGradient(sx, sy, sx + Math.random() * 80 - 40, sy + Math.random() * 80 - 40); 45 grad.addColorStop(0, `rgba(${c[0] + 30},${c[1] + 25},${c[2] + 20},0.15)`); 46 grad.addColorStop(1, `rgba(${c[0] - 10},${c[1] - 10},${c[2] - 8},0.05)`); 47 ctx.strokeStyle = grad; 48 ctx.lineWidth = 0.5 + Math.random() * 1.5; 49 ctx.beginPath(); 50 ctx.moveTo(sx, sy); 51 const cp1x = sx + Math.random() * 100 - 50; 52 const cp1y = sy + Math.random() * 100 - 50; 53 const cp2x = sx + Math.random() * 120 - 60; 54 const cp2y = sy + Math.random() * 120 - 60; 55 ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, sx + Math.random() * 140 - 70, sy + Math.random() * 140 - 70); 56 ctx.stroke(); 57 } 58 59 // Subtle color variation patches 60 for (let p = 0; p < 2; p++) { 61 const px = tx * ts + Math.random() * ts; 62 const py = ty * ts + Math.random() * ts; 63 const r = 10 + Math.random() * 20; 64 const grd = ctx.createRadialGradient(px, py, 0, px, py, r); 65 grd.addColorStop(0, `rgba(${c[0] + 15},${c[1] + 12},${c[2] + 10},0.12)`); 66 grd.addColorStop(1, 'rgba(0,0,0,0)'); 67 ctx.fillStyle = grd; 68 ctx.fillRect(px - r, py - r, r * 2, r * 2); 69 } 70 71 // Grout lines 72 ctx.strokeStyle = `rgba(${Math.min(c[0] - 30, 255)},${Math.min(c[1] - 30, 255)},${Math.min(c[2] - 25, 255)},0.4)`; 73 ctx.lineWidth = 2; 74 ctx.strokeRect(tx * ts, ty * ts, ts, ts); 75 } 76 } 77 78 const texture = new THREE.CanvasTexture(canvas); 79 texture.wrapS = THREE.RepeatWrapping; 80 texture.wrapT = THREE.RepeatWrapping; 81 texture.anisotropy = 4; 82 return texture; 83 } 84 85 function createStoneWallTexture(baseColor) { 86 const canvas = document.createElement('canvas'); 87 canvas.width = 512; 88 canvas.height = 512; 89 const ctx = canvas.getContext('2d'); 90 const r = (baseColor >> 16) & 0xff; 91 const g = (baseColor >> 8) & 0xff;
92 const b = baseColor & 0xff; 93 94 ctx.fillStyle = `rgb(${r},${g},${b})`; 95 ctx.fillRect(0, 0, 512, 512); 96 97 // Stone block pattern 98 const blockH = 64; 99 for (let row = 0; row < 8; row++) { 100 const offset = (row % 2) * 40; 101 const y = row * blockH; 102 ctx.strokeStyle = `rgba(${r - 20},${g - 20},${b - 18},0.2)`; 103 ctx.lineWidth = 1; 104 ctx.beginPath(); 105 ctx.moveTo(0, y); 106 ctx.lineTo(512, y); 107 ctx.stroke(); 108 109 for (let col = 0; col < 5; col++) { 110 const x = offset + col * 110; 111 ctx.beginPath(); 112 ctx.moveTo(x, y); 113 ctx.lineTo(x, y + blockH); 114 ctx.stroke(); 115 } 116 } 117 118 // Surface variation 119 for (let i = 0; i < 200; i++) { 120 const px = Math.random() * 512; 121 const py = Math.random() * 512; 122 const v = Math.random() * 10 - 5; 123 ctx.fillStyle = `rgba(${r + v},${g + v},${b + v},0.25)`; 124 ctx.fillRect(px, py, 3 + Math.random() * 3, 3 + Math.random() * 3); 125 } 126 127 const texture = new THREE.CanvasTexture(canvas); 128 texture.wrapS = THREE.RepeatWrapping; 129 texture.wrapT = THREE.RepeatWrapping; 130 return texture; 131 } 132 133 function createCeilingTexture() { 134 const canvas = document.createElement('canvas'); 135 canvas.width = 512; 136 canvas.height = 512; 137 const ctx = canvas.getContext('2d'); 138 139 ctx.fillStyle = '#e8e2d8'; 140 ctx.fillRect(0, 0, 512, 512); 141 142 // Ornate coffered panels 143 const ps = 128; 144 for (let x = 0; x < 4; x++) { 145 for (let y = 0; y < 4; y++) { 146 const px = x * ps, py = y * ps; 147 ctx.strokeStyle = 'rgba(180,170,155,0.7)'; 148 ctx.lineWidth = 4; 149 ctx.strokeRect(px + 6, py + 6, ps - 12, ps - 12); 150 ctx.fillStyle = 'rgba(220,215,205,0.5)'; 151 ctx.fillRect(px + 14, py + 14, ps - 28, ps - 28); 152 ctx.strokeStyle = 'rgba(195,185,170,0.5)'; 153 ctx.lineWidth = 2; 154 ctx.strokeRect(px + 14, py + 14, ps - 28, ps - 28); 155 ctx.strokeStyle = 'rgba(190,180,165,0.3)'; 156 ctx.lineWidth = 1; 157 ctx.beginPath(); 158 ctx.arc(px + ps / 2, py + ps / 2, 15, 0, Math.PI * 2); 159 ctx.stroke(); 160 } 161 } 162 163 const texture = new THREE.CanvasTexture(canvas); 164 texture.wrapS = THREE.RepeatWrapping; 165 texture.wrapT = THREE.RepeatWrapping; 166 return texture; 167 } 168 169 // ===================== MATERIAL CACHE ===================== 170 171 let floorTex, ceilTex; 172 const wallTexCache = {}; 173 174 function floorMat() { 175 if (!floorTex) floorTex = createMarbleTexture([195, 185, 168], [155, 142, 125], 4); 176 return new THREE.MeshStandardMaterial({ 177 map: floorTex, roughness: 0.18, metalness: 0.25, side: THREE.DoubleSide, 178 envMapIntensity: 0.6 179 }); 180 } 181 182 function ceilMat() { 183 if (!ceilTex) ceilTex = createCeilingTexture(); 184 return new THREE.MeshStandardMaterial({ 185 map: ceilTex, roughness: 0.95, metalness: 0.02, side: THREE.DoubleSide 186 }); 187 } 188 189 function wallMat(color) { 190 if (!wallTexCache[color]) wallTexCache[color] = createStoneWallTexture(color); 191 return new THREE.MeshStandardMaterial({ 192 map: wallTexCache[color], roughness: 0.75, metalness: 0.05, side: THREE.DoubleSide 193 }); 194 } 195 196 const _woodMat = new THREE.MeshStandardMaterial({ color: 0x5c3a1e, roughness: 0.55, metalness: 0.08 }); 197 const _darkWoodMat = new THREE.MeshStandardMaterial({ color: 0x3a2210, roughness: 0.5, metalness: 0.1 }); 198 const _stoneMat = new THREE.MeshStandardMaterial({ color: 0xd0c8b8, roughness: 0.4, metalness: 0.12 }); 199 const _goldMat = new THREE.MeshStandardMaterial({ color: 0xc9a96e, roughness: 0.25, metalness: 0.65, emissive: 0x3a2800, emissiveIntensity: 0.3 }); 200 const _darkMetalMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.3, metalness: 0.8 }); 201 const _marbleMat = new THREE.MeshStandardMaterial({ color: 0xe8e0d0, roughness: 0.3, metalness: 0.12 }); 202 const _glassMat = new THREE.MeshStandardMaterial({ 203 color: 0xccddee, roughness: 0.05, metalness: 0.3, transparent: true, op
203acity: 0.15, emissive: 0x112233, emissiveIntensity: 0.08 204 }); 205 const _leatherMat = new THREE.MeshStandardMaterial({ color: 0x4a3020, roughness: 0.7, metalness: 0.05 }); 206 const _carpetMat = new THREE.MeshStandardMaterial({ color: 0x6b2020, roughness: 0.95, metalness: 0 }); 207 208 // Wall colors 209 const COLORS = { 210 hall: 0xc2b8a5, 211 trilobite: 0xb8a890, 212 archaeopteryx: 0xa5b598, 213 neanderthal: 0xb0a0b5, 214 info: 0xa5a5b8, 215 }; 216 217 // ===================== CORE GEOMETRY ===================== 218 219 function addWall(scene, x, y, z, w, h, d, color, col = true) { 220 const wall = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), wallMat(color)); 221 wall.position.set(x, y, z); 222 wall.receiveShadow = true; 223 scene.add(wall); 224 if (col) collisionWalls.push(wall); 225 return wall; 226 } 227 228 function addFloor(scene, x, z, w, d) { 229 const f = new THREE.Mesh(new THREE.PlaneGeometry(w, d), floorMat()); 230 f.rotation.x = -Math.PI / 2; 231 f.position.set(x, 0, z); 232 f.receiveShadow = true; 233 scene.add(f); 234 } 235 236 function addCeiling(scene, x, z, w, d) { 237 const c = new THREE.Mesh(new THREE.PlaneGeometry(w, d), ceilMat()); 238 c.rotation.x = Math.PI / 2; 239 c.position.set(x, WALL_HEIGHT, z); 240 scene.add(c); 241 } 242 243 // ===================== ARCHITECTURAL ELEMENTS ===================== 244 245 function addColumn(scene, x, z, height) { 246 const h = height || WALL_HEIGHT; 247 const plinth = new THREE.Mesh(new THREE.BoxGeometry(1, 0.2, 1), _stoneMat); 248 plinth.position.set(x, 0.1, z); 249 scene.add(plinth); 250 const base = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.45, 0.35, 10), _stoneMat); 251 base.position.set(x, 0.375, z); 252 scene.add(base); 253 const shaft = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.35, h - 1.2, 10), _marbleMat); 254 shaft.position.set(x, h / 2, z); 255 shaft.castShadow = true; 256 scene.add(shaft); 257 const cap = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.3, 0.4, 10), _stoneMat); 258 cap.position.set(x, h - 0.55, z); 259 scene.add(cap); 260 const ab = new THREE.Mesh(new THREE.BoxGeometry(1, 0.15, 1), _stoneMat); 261 ab.position.set(x, h - 0.275, z); 262 scene.add(ab); 263 const cb = new THREE.Mesh(new THREE.BoxGeometry(0.8, h, 0.8)); 264 cb.position.set(x, h / 2, z); 265 cb.visible = false; 266 scene.add(cb); 267 collisionWalls.push(cb); 268 } 269 270 function addBaseboard(scene, x, z, len, rot) { 271 const b = new THREE.Mesh(new THREE.BoxGeometry(len, 0.25, 0.12), _darkWoodMat); 272 b.position.set(x, 0.125, z); 273 if (rot) b.rotation.y = rot; 274 scene.add(b); 275 } 276 277 function addCrownMolding(scene, x, z, len, rot) { 278 const m = new THREE.Mesh(new THREE.BoxGeometry(len, 0.18, 0.18), _stoneMat); 279 m.position.set(x, WALL_HEIGHT - 0.09, z); 280 if (rot) m.rotation.y = rot; 281 scene.add(m); 282 } 283 284 function addTrim(scene, cx, cz, w, d, doorWalls) { 285 const hw = w / 2, hd = d / 2, off = 0.18; 286 const dw = doorWalls || {}; 287 288 function addSplit(fn, posX, posZ, len, rot, hasDoor) { 289 if (!hasDoor) { fn(scene, posX, posZ, len, rot); return; } 290 const segLen = (len - DOOR_WIDTH) / 2 - 0.1; 291 if (segLen <= 0) return; 292 if (rot) { 293 fn(scene, posX, posZ - len / 2 + segLen / 2, segLen, rot); 294 fn(scene, posX, posZ + len / 2 - segLen / 2, segLen, rot); 295 } else { 296 fn(scene, posX - len / 2 + segLen / 2, posZ, segLen, rot); 297 fn(scene, posX + len / 2 - segLen / 2, posZ, segLen, rot); 298 } 299 } 300 301 addSplit(addBaseboard, cx, cz - hd + off, w, 0, dw.north); 302 addSplit(addBaseboard, cx, cz + hd - off, w, 0, dw.south); 303 addSplit(addBaseboard, cx - hw + off, cz, d, Math.PI / 2, dw.west); 304 addSplit(addBaseboard, cx + hw - off, cz, d, Math.PI / 2, dw.east); 305 addSplit(addCrownMolding, cx, cz - hd + off, w, 0, dw.north); 306 addSplit(addCrownMolding, cx, cz + hd - off, w, 0, dw.south); 307 addSplit(addCrownMolding, cx - hw + off, cz, d, Math.PI / 2, dw.west); 308 addSplit(addCrownMolding, cx + hw - off, cz, d, Math.PI / 2, dw.east); 309 } 310 311 function addDoorFrame(scene, x, z, rotated) { 312 const thick = 0.18, fd = WALL_THICKNESS + 0.15; 313 if (!rotated) { 314 scene.add(new THREE.Mesh(new THREE.BoxGeometry(thick, DOOR_HEIGHT, fd), _goldMat).translateX(x - DOOR_WIDTH / 2).translateY(DOOR_HEIGHT / 2).translateZ(z)); 315 scene.add(new THREE.Mesh(new THREE.BoxGeometry(thick, DOOR_HEIGHT, fd), _goldMat).translateX(x + DOOR_WIDTH / 2).translateY(DOOR_HEIGHT / 2).translateZ(z)); 316 scene.add(new THREE.Mesh(new THREE.BoxGeometry(DOOR_WIDTH + thick * 2, thick * 1.5, fd), _goldMat).translateX(x).translateY(DOOR_HEIGHT + thick * 0.5).translateZ(z)); 317 } else { 318 scene.add(new THREE.Mesh(new THREE.BoxGeometry(fd, DOOR_HEIGHT, thick), _goldMat).translateX(x).translateY(DOOR_HEIGHT / 2).translateZ(z - DOOR_WIDTH / 2)); 319 scene.add(new THREE.Mesh(new THREE.BoxGeometry(fd, DOOR_HEIGHT, thick), _goldMat).translateX(x).translateY(DOOR_HEIGHT / 2).translateZ(z + DOOR_WIDTH / 2)); 320 scene.add(new THREE.Mesh(new THREE.BoxGeometry(fd, thick * 1.5, DOOR_WIDTH + thick * 2), _goldMat).translateX(x).translateY(DOOR_HEIGHT + thick * 0.5).translateZ(z)); 321 } 322 } 323 324 function addRoomSign(scene, text, x, y, z, rot) { 325 const canvas = document.createElement('canvas'); 326 canvas.width = 512; 327 canvas.height = 128; 328 const ctx = canvas.getContext('2d'); 329 ctx.fillStyle = '#2a2218'; 330 ctx.fillRect(0, 0, 512, 128); 331 ctx.strokeStyle = '#c9a96e'; 332 ctx.lineWidth = 3; 333 ctx.strokeRect(6, 6, 500, 116); 334 ctx.strokeStyle = '#c9a96e'; 335 ctx.lineWidth = 1; 336 ctx.strokeRect(12, 12, 488, 104);
337 ctx.fillStyle = '#e8d5b7'; 338 ctx.font = '32px Georgia, serif'; 339 ctx.textAlign = 'center'; 340 ctx.textBaseline = 'middle'; 341 ctx.fillText(text, 256, 64); 342 343 const sign = new THREE.Mesh( 344 new THREE.PlaneGeometry(3.2, 0.8), 345 new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(canvas) }) 346 ); 347 sign.position.set(x, y, z); 348 if (rot) sign.rotation.y = rot; 349 scene.add(sign); 350 } 351 352 function addWallSconce(scene, x, y, z) { 353 scene.add(new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.2, 0.16), _goldMat).translateX(x).translateY(y).translateZ(z)); 354 const shade = new THREE.Mesh( 355 new THREE.ConeGeometry(0.14, 0.22, 6, 1, true), 356 new THREE.MeshStandardMaterial({ color: 0xfff0dd, emissive: 0xffe8cc, emissiveIntensity: 0.8, roughness: 0.4 }) 357 ); 358 shade.position.set(x, y + 0.14, z); 359 shade.rotation.x = Math.PI; 360 scene.add(shade); 361 362 // Warm point light (cheap, no shadows) 363 const pl = new THREE.PointLight(0xffe0b0, 0.3, 8); 364 pl.position.set(x, y - 0.5, z); 365 scene.add(pl); 366 367 // Subtle glow orb 368 const glowOrb = new THREE.Mesh( 369 new THREE.SphereGeometry(0.06, 6, 6), 370 new THREE.MeshBasicMaterial({ color: 0xffe8cc, transparent: true, opacity: 0.6 }) 371 ); 372 glowOrb.position.set(x, y + 0.05, z); 373 scene.add(glowOrb); 374 } 375 376 // ===================== FURNITURE ===================== 377 378 function addBench(scene, x, z, rot) { 379 const group = new THREE.Group(); 380 const seat = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.08, 0.75), _leatherMat); 381 seat.position.y = 0.52; 382 seat.castShadow = true; 383 group.add(seat); 384 const lg = new THREE.CylinderGeometry(0.035, 0.035, 0.52, 6); 385 [[-1.1, -0.3], [-1.1, 0.3], [1.1, -0.3], [1.1, 0.3]].forEach(([lx, lz]) => { 386 group.add(new THREE.Mesh(lg, _darkMetalMat).translateX(lx).translateY(0.26).translateZ(lz)); 387 }); 388 group.add(new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.03, 0.03), _darkMetalMat).translateY(0.2)); 389 group.position.set(x, 0, z); 390 if (rot) group.rotation.y = rot; 391 scene.add(group); 392 const col = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.6, 0.75)); 393 col.position.set(x, 0.3, z); 394 if (rot) col.rotation.y = rot; 395 col.visible = false; 396 scene.add(col); 397 collisionWalls.push(col); 398 } 399 400 function addPottedPlant(scene, x, z) { 401 const pot = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.22, 0.6, 8), new THREE.MeshStandardMaterial({ color: 0x6b4226, roughness: 0.8 })); 402 pot.position.set(x, 0.3, z); 403 pot.castShadow = true; 404 scene.add(pot); 405 const leafMat = new THREE.MeshStandardMaterial({ color: 0x3d6b35, roughness: 0.8 }); 406 [[0, 0.95, 0, 0.2], [0.15, 0.82, 0.1, 0.16], [-0.12, 0.85, -0.08, 0.17], [0.08, 0.75, -0.12, 0.14], [-0.1, 0.78, 0.12, 0.15]].forEach(([px, py, pz, s]) => { 407 scene.add(new THREE.Mesh(new THREE.SphereGeometry(s, 6, 6), leafMat).translateX(x + px).translateY(py).translateZ(z + pz)); 408 }); 409 const col = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.35, 1.2, 6)); 410 col.position.set(x, 0.6, z); 411 col.visible = false; 412 scene.add(col); 413 collisionWalls.push(col); 414 } 415 416 function addRopeBarrier(scene, x1, z1, x2, z2) { 417 [{ x: x1, z: z1 }, { x: x2, z: z2 }].forEach(({ x, z }) => { 418 scene.add(new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 0.95, 6), _goldMat).translateX(x).translateY(0.475).translateZ(z)); 419 scene.add(new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.15, 0.06, 8), _goldMat).translateX(x).translateY(0.03).translateZ(z)); 420 scene.add(new THREE.Mesh(new THREE.SphereGeometry(0.055, 6, 6), _goldMat).translateX(x).translateY(0.97).translateZ(z)); 421 }); 422 const pts = []; 423 const dx = x2 - x1, dz = z2 - z1; 424 for (let t = 0; t <= 1; t += 0.1) pts.push(new THREE.Vector3(x1 + dx * t, 0.85 - Math.sin(t * Math.PI) * 0.1, z1 + dz * t)); 425 scene.add(new THREE.Mesh(
426 new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts), 8, 0.018, 4, false), 427 new THREE.MeshStandardMaterial({ color: 0x8B0000, roughness: 0.6 }) 428 )); 429 } 430 431 // ===================== GRAND HALL FURNITURE ===================== 432 433 function addReceptionDesk(scene, x, z) { 434 const group = new THREE.Group(); 435 group.add(new THREE.Mesh(new THREE.BoxGeometry(5, 1.1, 0.15), _darkWoodMat).translateY(0.55).translateZ(-0.6)); 436 group.add(new THREE.Mesh(new THREE.BoxGeometry(5.2, 0.06, 1.3), _marbleMat).translateY(1.12)); 437 group.add(new THREE.Mesh(new THREE.BoxGeometry(5, 0.9, 0.1), _darkWoodMat).translateY(0.45).translateZ(0.55)); 438 group.add(new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.1, 1.2), _darkWoodMat).translateX(-2.45).translateY(0.55)); 439 group.add(new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.1, 1.2), _darkWoodMat).translateX(2.45).translateY(0.55)); 440 group.add(new THREE.Mesh(new THREE.BoxGeometry(5.2, 0.35, 0.4), _darkWoodMat).translateY(1.3).translateZ(-0.45)); 441 group.add(new THREE.Mesh(new THREE.BoxGeometry(5.1, 0.04, 0.04), _goldMat).translateY(1.14).translateZ(-0.62)); 442 443 const canvas = document.createElement('canvas'); 444 canvas.width = 256; 445 canvas.height = 48; 446 const ctx = canvas.getContext('2d'); 447 ctx.fillStyle = '#2a2218'; 448 ctx.fillRect(0, 0, 256, 48); 449 ctx.fillStyle = '#c9a96e'; 450 ctx.font = '22px Georgia, serif'; 451 ctx.textAlign = 'center'; 452 ctx.textBaseline = 'middle'; 453 ctx.fillText('ADMISSIONS', 128, 24); 454 const signMesh = new THREE.Mesh( 455 new THREE.PlaneGeometry(1.5, 0.28), 456 new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(canvas) }) 457 ); 458 signMesh.position.set(0, 1.52, -0.66); 459 group.add(signMesh); 460 461 group.position.set(x, 0, z); 462 scene.add(group); 463 464 const col = new THREE.Mesh(new THREE.BoxGeometry(5.2, 1.5, 1.3)); 465 col.position.set(x, 0.75, z); 466 col.visible = false; 467 scene.add(col); 468 collisionWalls.push(col); 469 } 470 471 function addDisplayCase(scene, x, z, label) { 472 const ped = new THREE.Mesh(new THREE.BoxGeometry(1, 0.9, 1), _stoneMat); 473 ped.position.set(x, 0.45, z); 474 ped.castShadow = true; 475 scene.add(ped); 476 collisionWalls.push(ped); 477 const glass = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.6, 0.8), _glassMat); 478 glass.position.set(x, 1.2, z); 479 scene.add(glass); 480 if (label) { 481 const canvas = document.createElement('canvas'); 482 canvas.width = 256; 483 canvas.height = 64; 484 const ctx = canvas.getContext('2d'); 485 ctx.fillStyle = '#2a2218'; 486 ctx.fillRect(0, 0, 256, 64); 487 ctx.strokeStyle = '#c9a96e'; 488 ctx.lineWidth = 2; 489 ctx.strokeRect(4, 4, 248, 56); 490 ctx.fillStyle = '#e8d5b7'; 491 ctx.font = '16px Georgia, serif'; 492 ctx.textAlign = 'center'; 493 ctx.textBaseline = 'middle'; 494 ctx.fillText(label, 128, 32); 495 const plaque = new THREE.Mesh( 496 new THREE.PlaneGeometry(0.8, 0.2), 497 new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(canvas) }) 498 ); 499 plaque.position.set(x, 0.5, z + 0.51); 500 scene.add(plaque); 501 } 502 } 503 504 function addInfoKiosk(scene, x, z) { 505 const group = new THREE.Group(); 506 group.add(new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 1.3, 6), _darkMetalMat).translateY(0.65)); 507 group.add(new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.22, 0.04, 8), _darkMetalMat).translateY(0.02)); 508 group.add(new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.9, 0.04), _darkWoodMat).translateY(1.55)); 509 510 const canvas = document.createElement('canvas'); 511 canvas.width = 512; 512 canvas.height = 384; 513 const ctx = canvas.getContext('2d'); 514 ctx.fillStyle = '#1e1a14'; 515 ctx.fillRect(0, 0, 512, 384); 516 ctx.strokeStyle = '#c9a96e'; 517 ctx.lineWidth = 2; 518 ctx.strokeRect(10, 10, 492, 364); 519 ctx.fillStyle = '#c9a96e'; 520 ctx.font = 'bold 22px Georgia, serif'; 521 ctx.textAlign = 'center'; 522 ctx.fillText('MUSEUM DIRECTORY', 256, 50); 523 ctx.font = '16px Georgia, serif';
524 ctx.fillStyle = '#e8d5b7'; 525 ctx.textAlign = 'left'; 526 const dirs = [ 527 '\u2190 Trilobite Wing \u2014 Foundations', 528 '\u2192 Archaeopteryx Wing \u2014 Growth', 529 '\u2191 Neanderthal Wing \u2014 Engagement', 530 '\u2193 Info Wing \u2014 About', 531 ]; 532 dirs.forEach((d, i) => ctx.fillText(d, 40, 110 + i * 45)); 533 ctx.font = 'italic 14px Georgia, serif'; 534 ctx.fillStyle = '#a89b8c'; 535 ctx.textAlign = 'center'; 536 ctx.fillText('Matthew Kim\'s Museum of Science Communication', 256, 350); 537 538 const board = new THREE.Mesh( 539 new THREE.PlaneGeometry(1.15, 0.86), 540 new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(canvas) }) 541 ); 542 board.position.set(0, 1.55, 0.025); 543 group.add(board); 544 545 group.position.set(x, 0, z); 546 scene.add(group); 547 548 const col = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.3, 2, 6)); 549 col.position.set(x, 1, z); 550 col.visible = false; 551 scene.add(col); 552 collisionWalls.push(col); 553 } 554 555 function addWelcomeBanner(scene, x, y, z, rot) { 556 const canvas = document.createElement('canvas'); 557 canvas.width = 1024; 558 canvas.height = 256; 559 const ctx = canvas.getContext('2d'); 560 ctx.fillStyle = '#2a2218'; 561 ctx.fillRect(0, 0, 1024, 256); 562 ctx.strokeStyle = '#c9a96e'; 563 ctx.lineWidth = 4; 564 ctx.strokeRect(8, 8, 1008, 240); 565 ctx.strokeStyle = '#c9a96e'; 566 ctx.lineWidth = 1; 567 ctx.strokeRect(16, 16, 992, 224); 568 ctx.fillStyle = '#e8d5b7'; 569 ctx.font = 'bold 48px Georgia, serif'; 570 ctx.textAlign = 'center'; 571 ctx.fillText('MATTHEW KIM\'S MUSEUM OF SCIENCE COMMUNICATION', 512, 110); 572 ctx.font = 'italic 28px Georgia, serif'; 573 ctx.fillStyle = '#a89b8c'; 574 ctx.fillText('A Notation in Science Communication ePortfolio', 512, 170); 575 ctx.font = '20px Georgia, serif'; 576 ctx.fillStyle = '#8a7d6e'; 577 ctx.fillText('Stanford University', 512, 210); 578 579 const banner = new THREE.Mesh( 580 new THREE.PlaneGeometry(8, 2), 581 new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(canvas) }) 582 ); 583 banner.position.set(x, y, z); 584 if (rot) banner.rotation.y = rot; 585 scene.add(banner); 586 } 587 588 function addCarpetRunner(scene, x, z, w, d) { 589 const carpet = new THREE.Mesh(new THREE.PlaneGeometry(w, d), _carpetMat); 590 carpet.rotation.x = -Math.PI / 2; 591 carpet.position.set(x, 0.01, z); 592 scene.add(carpet); 593 const borderMat = new THREE.MeshStandardMaterial({ color: 0xc9a96e, roughness: 0.6 }); 594 const bw = 0.08; 595 const b1 = new THREE.Mesh(new THREE.PlaneGeometry(w, bw), borderMat); 596 b1.rotation.x = -Math.PI / 2; 597 b1.position.set(x, 0.012, z - d / 2); 598 scene.add(b1); 599 const b2 = new THREE.Mesh(new THREE.PlaneGeometry(w, bw), borderMat); 600 b2.rotation.x = -Math.PI / 2; 601 b2.position.set(x, 0.012, z + d / 2); 602 scene.add(b2); 603 } 604 605 // ===================== WALL WITH DOOR ===================== 606 607 function wallWithDoor(scene, wallLen, wallColor, doorX, doorZ, rotated) { 608 const hy = WALL_HEIGHT / 2; 609 const sectionLen = (wallLen - DOOR_WIDTH) / 2; 610 611 if (!rotated) { 612 addWall(scene, doorX - DOOR_WIDTH / 2 - sectionLen / 2, hy, doorZ, sectionLen, WALL_HEIGHT, WALL_THICKNESS, wallColor); 613 addWall(scene, doorX + DOOR_WIDTH / 2 + sectionLen / 2, hy, doorZ, sectionLen, WALL_HEIGHT, WALL_THICKNESS, wallColor); 614 addWall(scene, doorX, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, doorZ, DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT, WALL_THICKNESS, wallColor); 615 } else { 616 addWall(scene, doorX, hy, doorZ - DOOR_WIDTH / 2 - sectionLen / 2, WALL_THICKNESS, WALL_HEIGHT, sectionLen, wallColor); 617 addWall(scene, doorX, hy, doorZ + DOOR_WIDTH / 2 + sectionLen / 2, WALL_THICKNESS, WALL_HEIGHT, sectionLen, wallColor); 618 addWall(scene, doorX, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, doorZ, WALL_THICKNESS, WALL_HEIGHT - DOOR_HEIGHT, DOOR_WIDTH, wallColor); 619 } 620 addDoorFrame(scene, doorX, doorZ, rotated); 621 } 622 623 // ===================== GRAND HALL ===================== 624 625 function buildGrandHall(scene) { 626 const hw = GRAND_HALL_WIDTH / 2; // 12 627 const hd = GRAND_HALL_DEPTH / 2; // 12 628 const hy = WALL_HEIGHT / 2; 629 630 // Floor & ceiling 631 addFloor(scene, 0, 0, GRAND_HALL_WIDTH, GRAND_HALL_DEPTH); 632 addCeiling(scene, 0, 0, GRAND_HALL_WIDTH, GRAND_HALL_DEPTH); 633 634 // NORTH wall â door to Neanderthal 635 wallWithDoor(scene, GRAND_HALL_WIDTH, COLORS.hall, 0, -hd, false); 636 addRoomSign(scene, 'Neanderthal Wing', 0, DOOR_HEIGHT + 0.6, -hd + 0.25, 0); 637 638 // SOUTH wall â door to Info Wing 639 wallWithDoor(scene, GRAND_HALL_WIDTH, COLORS.hall, 0, hd, false); 640 addRoomSign(scene, 'Info Wing', 0, DOOR_HEIGHT + 0.6, hd - 0.25, Math.PI); 641 642 // WEST wall â door to Trilobite 643 wallWithDoor(scene, GRAND_HALL_DEPTH, COLORS.hall, -hw, 0, true); 644 addRoomSign(scene, 'Trilobite Wing', -hw + 0.25, DOOR_HEIGHT + 0.6, 0, Math.PI / 2); 645 646 // EAST wall â door to Archaeopteryx 647 wallWithDoor(scene, GRAND_HALL_DEPTH, COLORS.hall, hw, 0, true); 648 addRoomSign(scene, 'Archaeopteryx Wing', hw - 0.25, DOOR_HEIGHT + 0.6, 0, -Math.PI / 2); 649 650 // COLONNADE â Field Museum style rows of columns 651 [-8, -3, 3, 8].forEach(z => { 652 addColumn(scene, -9, z); 653 addColumn(scene, 9, z); 654 }); 655 656 // ENTRANCE AREA (south section, z > 4) 657 658 // Carpet runners on either side of skeleton platform 659 addCarpetRunner(scene, 0, 8, 3, 7); // south (entrance side) 660 addCarpetRunner(scene, 0, -8, 3, 7); // north 661 662 // Plants in corners 663 addPottedPlant(scene, -hw + 2, hd - 2); 664 addPottedPlant(scene, hw - 2, hd - 2); 665 addPottedPlant(scene, -hw + 2, -hd + 2); 666 addPottedPlant(scene, hw - 2, -hd + 2); 667 668 // Benches between columns 669 addBench(scene, -9, 6, Math.PI / 2); 670 addBench(scene, 9, 6, Math.PI / 2); 671 addBench(scene, -9, -6, Math.PI / 2); 672 addBench(scene, 9, -6, Math.PI / 2); 673 674 // Sconces on walls 675 addWallSconce(scene, -hw + 0.35, 5.5, -7); 676 addWallSconce(scene, -hw + 0.35, 5.5, 7); 677 addWallSconce(scene, hw - 0.35, 5.5, -7); 678 addWallSconce(scene, hw - 0.35, 5.5, 7); 679 addWallSconce(scene, -7, 5.5, -hd + 0.35); 680 addWallSconce(scene, 7, 5.5, -hd + 0.35); 681 addWallSconce(scene, -7, 5.5, hd - 0.35); 682 addWallSconce(scene, 7, 5.5, hd - 0.35); 683 684 // Trim 685 addTrim(scene, 0, 0, GRAND_HALL_WIDTH, GRAND_HALL_DEPTH, { north: true, south: true, west: true, east: true }); 686 687 rooms.push({ name: 'Grand Hall', minX: -hw, maxX: hw, minZ: -hd, maxZ: hd }); 688 } 689 690 // ===================== EXHIBIT HALLS ===================== 691 692 function buildExhibitHall(scene, offsetX, offsetZ, axis, wallColor, roomName) { 693 const hw = HALL_WIDTH / 2; 694 const hd = HALL_DEPTH / 2; 695 const hy = WALL_HEIGHT / 2; 696 697 let cx, cz; 698 if (axis === 'x') { 699 cx = offsetX + (offsetX < 0 ? -hd : hd); 700 cz = offsetZ; 701 addFloor(scene, cx, cz, HALL_DEPTH, HALL_WIDTH); 702 addCeiling(scene, cx, cz, HALL_DEPTH, HALL_WIDTH); 703 addWall(scene, cx, hy, cz - hw, HALL_DEPTH, WALL_HEIGHT, WALL_THICKNESS, wallColor); 704 addWall(scene, cx, hy, cz + hw, HALL_DEPTH, WALL_HEIGHT, WALL_THICKNESS, wallColor);
705 const farX = offsetX < 0 ? cx - hd : cx + hd; 706 addWall(scene, farX, hy, cz, WALL_THICKNESS, WALL_HEIGHT, HALL_WIDTH, wallColor); 707 addTrim(scene, cx, cz, HALL_DEPTH, HALL_WIDTH, offsetX < 0 ? { east: true } : { west: true }); 708 addBench(scene, cx + (offsetX < 0 ? 5 : -5), cz, Math.PI / 2); 709 if (offsetX < 0) addRopeBarrier(scene, farX + 2, cz - 2, farX + 2, cz + 2); 710 else addRopeBarrier(scene, farX - 2, cz - 2, farX - 2, cz + 2); 711 addWallSconce(scene, cx - 4, 5, cz - hw + 0.35); 712 addWallSconce(scene, cx + 4, 5, cz - hw + 0.35); 713 addWallSconce(scene, cx - 4, 5, cz + hw - 0.35); 714 addWallSconce(scene, cx + 4, 5, cz + hw - 0.35); 715 rooms.push({ name: roomName, minX: cx - hd, maxX: cx + hd, minZ: cz - hw, maxZ: cz + hw }); 716 } else { 717 cx = offsetX; 718 cz = offsetZ + (offsetZ < 0 ? -hd : hd); 719 addFloor(scene, cx, cz, HALL_WIDTH, HALL_DEPTH); 720 addCeiling(scene, cx, cz, HALL_WIDTH, HALL_DEPTH); 721 addWall(scene, cx - hw, hy, cz, WALL_THICKNESS, WALL_HEIGHT, HALL_DEPTH, wallColor); 722 addWall(scene, cx + hw, hy, cz, WALL_THICKNESS, WALL_HEIGHT, HALL_DEPTH, wallColor); 723 const farZ = offsetZ < 0 ? cz - hd : cz + hd; 724 addWall(scene, cx, hy, farZ, HALL_WIDTH, WALL_HEIGHT, WALL_THICKNESS, wallColor); 725 addTrim(scene, cx, cz, HALL_WIDTH, HALL_DEPTH, offsetZ < 0 ? { south: true } : { north: true }); 726 addBench(scene, cx, cz + (offsetZ < 0 ? 5 : -5), 0); 727 if (offsetZ < 0) addRopeBarrier(scene, cx - 2, farZ + 2, cx + 2, farZ + 2); 728 else addRopeBarrier(scene, cx - 2, farZ - 2, cx + 2, farZ - 2); 729 addWallSconce(scene, cx - hw + 0.35, 5, cz - 4); 730 addWallSconce(scene, cx - hw + 0.35, 5, cz + 4); 731 addWallSconce(scene, cx + hw - 0.35, 5, cz - 4); 732 addWallSconce(scene, cx + hw - 0.35, 5, cz + 4); 733 rooms.push({ name: roomName, minX: cx - hw, maxX: cx + hw, minZ: cz - hd, maxZ: cz + hd }); 734 } 735 return { cx, cz, axis }; 736 } 737 738 // ===================== LIGHTING ===================== 739 740 function addLighting(scene) { 741 scene.add(new THREE.HemisphereLight(0xdde8f0, 0x6b5840, 0.45)); 742 scene.add(new THREE.AmbientLight(0xfff5e6, 0.25)); 743 744 // Grand hall center light (shadow-casting) â main dramatic light 745 const mainLight = new THREE.PointLight(0xffe8cc, 1.2, 40); 746 mainLight.position.set(0, WALL_HEIGHT - 0.5, 0); 747 mainLight.castShadow = true; 748 mainLight.shadow.mapSize.width = 512; 749 mainLight.shadow.mapSize.height = 512; 750 mainLight.shadow.bias = -0.002; 751 mainLight.shadow.radius = 3; 752 scene.add(mainLight); 753 animatedLights.push(mainLight); 754 755 // (light beams removed for performance) 756 757 // One light per exhibit hall (no accent lights for performance) 758 const hw = GRAND_HALL_WIDTH / 2; 759 const hd = GRAND_HALL_DEPTH / 2; 760 const wingLights = [ 761 { pos: [-hw - HALL_DEPTH / 2, 0], color: 0xffe0c0 }, // Trilobite warm 762 { pos: [hw + HALL_DEPTH / 2, 0], color: 0xe8f0dd }, // Archaeopteryx green 763 { pos: [0, -hd - HALL_DEPTH / 2], color: 0xe0d8f0 }, // Neanderthal violet 764 { pos: [0, hd + HALL_DEPTH / 2], color: 0xd8e0f0 }, // Info blue 765 ]; 766 wingLights.forEach(({ pos, color }) => { 767 const l = new THREE.PointLight(color, 0.8, 30); 768 l.position.set(pos[0], WALL_HEIGHT - 0.5, pos[1]); 769 scene.add(l); 770 }); 771 772 // Directional fill light for soft overall shadows 773 const dirLight = new THREE.DirectionalLight(0xffeedd, 0.15); 774 dirLight.position.set(5, WALL_HEIGHT, 5); 775 scene.add(dirLight); 776 } 777 778 const animatedLights = []; 779 780 function addLightBeam(scene, x, z, color, radius, height) { 781 const beamGeo = new THREE.CylinderGeometry(radius * 0.3, radius, height, 8, 1, true); 782 const beamMat = new THREE.MeshBasicMaterial({ 783 color: color, 784 transparent: true, 785 opacity: 0.018, 786 side: THREE.DoubleSide, 787 depthWrite: false 788 }); 789 const beam = new THREE.Mesh(beamGeo, beamMat); 790 beam.position.set(x, height / 2, z); 791 scene.add(beam); 792 } 793 794 function getAnimatedLights() { return animatedLights; } 795 796 // ===================== FOSSIL CENTERPIECES ===================== 797 798 const _boneMat = new THREE.MeshStandardMaterial({ color: 0xe8dcc8, roughness: 0.6, metalness: 0.05 }); 799 const _darkBoneMat = new THREE.MeshStandardMaterial({ color: 0xc8b8a0, roughness: 0.65, metalness: 0.05 }); 800 const _fossilMat = new THREE.MeshStandardMaterial({ color: 0xd4c8b0, roughness: 0.55, metalness: 0.08 }); 801 802 function addFossilPedestal(scene, x, z) { 803 // Tiered base 804 scene.add(new THREE.Mesh(new THREE.CylinderGeometry(1.4, 1.5, 0.12, 16), _marbleMat).translateX(x).translateY(0.06).translateZ(z)); 805 scene.add(new THREE.Mesh(new THREE.CylinderGeometry(1.2, 1.3, 0.12, 16), _marbleMat).translateX(x).translateY(0.18).translateZ(z)); 806 const body = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 1.0, 0.7, 12), _stoneMat); 807 body.position.set(x, 0.59, z); 808 body.castShadow = true; 809 scene.add(body); 810 collisionWalls.push(body); 811 // Display platform top 812 scene.add(new THREE.Mesh(new THREE.CylinderGeometry(1.0, 0.9, 0.08, 16), _marbleMat).translateX(x).translateY(0.98).translateZ(z)); 813 814 // Point light on fossil (cheaper than SpotLight) 815 const pl = new THREE.PointLight(0xfff0dd, 0.6, 10); 816 pl.position.set(x, WALL_HEIGHT - 1, z); 817 scene.add(pl); 818 819 return { x, z, topY: 1.02 }; 820 } 821 822 // Wing image paths (for overlay click-through) 823 const WING_IMAGES = { 824 trilobite: 'assets/images/trilobite.png', 825 archaeopteryx: 'assets/images/archaeopteryx.png', 826 neanderthal: 'assets/images/neanderthal.png' 827 }; 828 // Base64 image data loaded from wing-images.js (WING_IMAGE_DATA global) 829
830 function addWallSign(scene, hall, exhibitKey, wingName, subtitle, description) { 831 const { cx, cz, axis } = hall; 832 const hd = HALL_DEPTH / 2; 833 834 // Determine far wall position and sign orientation 835 // Wall center is at farX/farZ, inner face is WALL_THICKNESS/2 closer to center 836 const wallOff = WALL_THICKNESS / 2 + 0.05; // just in front of inner wall face 837 let signX, signZ, rotY; 838 if (axis === 'x') { 839 const farX = cx > 0 ? cx + hd : cx - hd; 840 signX = farX + (cx > 0 ? -wallOff : wallOff); 841 signZ = cz; 842 rotY = cx > 0 ? -Math.PI / 2 : Math.PI / 2; 843 } else { 844 const farZ = cz > 0 ? cz + hd : cz - hd; 845 signX = cx; 846 signZ = farZ + (cz > 0 ? -wallOff : wallOff); 847 rotY = cz > 0 ? Math.PI : 0; 848 } 849 850 const group = new THREE.Group(); 851 852 // === PAINTING (framed image on wall) === 853 // Native aspect ratios: trilobite 4:3 landscape, others ~3:4 portrait 854 const IMG_ASPECTS = { trilobite: 512/384, archaeopteryx: 512/685, neanderthal: 512/678 }; 855 const aspect = IMG_ASPECTS[exhibitKey] || (4/3); 856 const maxDim = 3.5; // max width or height in world units 857 let imgW, imgH; 858 if (aspect >= 1) { 859 imgW = maxDim; 860 imgH = maxDim / aspect; 861 } else { 862 imgH = maxDim; 863 imgW = maxDim * aspect; 864 } 865 const imgCenterY = 1.0 + imgH / 2 + 0.8; // bottom of painting ~1.8m up 866 867 // Gold frame around painting 868 const frameMat = _goldMat; 869 const frameThick = 0.15; 870 const frameTop = new THREE.Mesh(new THREE.BoxGeometry(imgW + frameThick * 2, frameThick, 0.08), frameMat); 871 frameTop.position.set(0, imgCenterY + imgH / 2 + frameThick / 2, 0); 872 group.add(frameTop); 873 const frameBot = new THREE.Mesh(new THREE.BoxGeometry(imgW + frameThick * 2, frameThick, 0.08), frameMat); 874 frameBot.position.set(0, imgCenterY - imgH / 2 - frameThick / 2, 0); 875 group.add(frameBot); 876 const frameLeft = new THREE.Mesh(new THREE.BoxGeometry(frameThick, imgH + frameThick * 2, 0.08), frameMat); 877 frameLeft.position.set(-imgW / 2 - frameThick / 2, imgCenterY, 0); 878 group.add(frameLeft); 879 const frameRight = new THREE.Mesh(new THREE.BoxGeometry(frameThick, imgH + frameThick * 2, 0.08), frameMat); 880 frameRight.position.set(imgW / 2 + frameThick / 2, imgCenterY, 0); 881 group.add(frameRight); 882 883 // Image plane â use base64 data URL drawn onto canvas (works on file://) 884 const imgPath = WING_IMAGES[exhibitKey]; 885 const imgDataURL = (typeof WING_IMAGE_DATA !== 'undefined') ? WING_IMAGE_DATA[exhibitKey] : null; 886 const canvasW = 512, canvasH = Math.round(512 / aspect); 887 const imgCanvas = document.createElement('canvas'); 888 imgCanvas.width = canvasW; 889 imgCanvas.height = canvasH; 890 const imgCtx = imgCanvas.getContext('2d'); 891 imgCtx.fillStyle = '#1a1610'; 892 imgCtx.fillRect(0, 0, canvasW, canvasH); 893 const imgTex = new THREE.CanvasTexture(imgCanvas); 894 const imgMat = new THREE.MeshBasicMaterial({ map: imgTex, side: THREE.DoubleSide }); 895 const imgPlane = new THREE.Mesh(new THREE.PlaneGeometry(imgW, imgH), imgMat); 896 imgPlane.position.set(0, imgCenterY, 0.05); 897 group.add(imgPlane); 898 899 if (imgDataURL) { 900 const img = new Image(); 901 img.onload = function() { 902 imgCtx.drawImage(img, 0, 0, canvasW, canvasH); 903 imgTex.needsUpdate = true; 904 }; 905 img.src = imgDataURL; 906 } 907 908 // === MUSEUM PLACARD (small label below painting) === 909 const placardW = Math.min(imgW, 2.4), placardH = 0.7; 910 const placardY = imgCenterY - imgH / 2 - frameThick - 0.15 - placardH / 2; 911 912 // Placard canvas 913 const canvas = document.createElement('canvas'); 914 canvas.width = 512; 915 canvas.height = 160; 916 const ctx = canvas.getContext('2d'); 917 918 // White/cream background like a real museum label 919 ctx.fillStyle = '#f5f0e8'; 920 ctx.fillRect(0, 0, 512, 160); 921 922 // Thin border 923 ctx.strokeStyle = '#c9a96e'; 924 ctx.lineWidth = 2; 925 ctx.strokeRect(2, 2, 508, 156); 926
927 // Wing name (bold) 928 ctx.fillStyle = '#1a1610'; 929 ctx.font = 'bold 24px Georgia, serif'; 930 ctx.textAlign = 'center'; 931 ctx.fillText(wingName, 256, 35); 932 933 // Subtitle (italic) 934 ctx.fillStyle = '#5a4a30'; 935 ctx.font = 'italic 17px Georgia, serif'; 936 ctx.fillText(subtitle, 256, 60); 937 938 // Thin line 939 ctx.strokeStyle = '#c9a96e'; 940 ctx.lineWidth = 0.5; 941 ctx.beginPath(); 942 ctx.moveTo(80, 70); 943 ctx.lineTo(432, 70); 944 ctx.stroke(); 945 946 // Short description (truncated to 2 lines) 947 ctx.fillStyle = '#3a3020'; 948 ctx.font = '13px Georgia, serif'; 949 ctx.textAlign = 'center'; 950 const words = description.split(' '); 951 let line1 = '', line2 = ''; 952 let onLine1 = true; 953 for (const word of words) { 954 if (onLine1) { 955 const test = line1 + word + ' '; 956 if (ctx.measureText(test).width > 440) { 957 onLine1 = false; 958 line2 = word + ' '; 959 } else { 960 line1 = test; 961 } 962 } else { 963 const test = line2 + word + ' '; 964 if (ctx.measureText(test).width > 430) { 965 line2 = line2.trim() + '...'; 966 break; 967 } 968 line2 = test; 969 } 970 } 971 ctx.fillText(line1.trim(), 256, 92); 972 if (line2) ctx.fillText(line2.trim(), 256, 112); 973 974 // Click hint 975 ctx.fillStyle = '#9a8a6a'; 976 ctx.font = '11px Helvetica, sans-serif'; 977 ctx.fillText('Click to read more', 256, 145); 978 979 const placardTex = new THREE.CanvasTexture(canvas); 980 981 // Placard backing (slight depth) 982 const placardBack = new THREE.Mesh( 983 new THREE.BoxGeometry(placardW + 0.06, placardH + 0.06, 0.03), 984 new THREE.MeshStandardMaterial({ color: 0x2a2520, roughness: 0.7 }) 985 ); 986 placardBack.position.set(0, placardY, 0.01); 987 group.add(placardBack); 988 989 // Placard face 990 const placardFace = new THREE.Mesh( 991 new THREE.PlaneGeometry(placardW, placardH), 992 new THREE.MeshBasicMaterial({ map: placardTex, side: THREE.DoubleSide }) 993 ); 994 placardFace.position.set(0, placardY, 0.03); 995 group.add(placardFace); 996 997 group.position.set(signX, 0, signZ); 998 group.rotation.y = rotY; 999 scene.add(group); 1000 1001 // Update world matrix for raycasting 1002 group.updateMatrixWorld(true); 1003 1004 // Register as clickable exhibit 1005 const overlayData = { 1006 title: wingName, 1007 content: '<p style="color:#c9a96e; font-size:0.85rem; margin-bottom:1rem; letter-spacing:0.1em;">' + subtitle + '</p><p>' + description + '</p>', 1008 image: imgPath, 1009 reflection: null, 1010 type: 'info' 1011 }; 1012 if (imgPlane) Exhibits.registerExhibitMesh(imgPlane, overlayData); 1013 Exhibits.registerExhibitMesh(placardFace, overlayData); 1014 } 1015 1016 function buildTrilobiteFossil(scene, ped) { 1017 const group = new THREE.Group(); 1018 // Central body â segmented ellipsoid 1019 for (let i = 0; i < 9; i++) { 1020 const t = i / 8; 1021 const segW = 0.35 * Math.sin(t * Math.PI) + 0.12; 1022 const segH = 0.06; 1023 const seg = new THREE.Mesh( 1024 new THREE.CylinderGeometry(segW, segW * 0.95, segH, 8), 1025 _fossilMat 1026 ); 1027 seg.position.z = (t - 0.5) * 1.0; 1028 seg.position.y = 0.04; 1029 seg.castShadow = true; 1030 group.add(seg); 1031 1032 // Ridge line down center 1033 const ridge = new THREE.Mesh( 1034 new THREE.CylinderGeometry(segW * 0.35, segW * 0.3, segH + 0.02, 6), 1035 _boneMat 1036 ); 1037 ridge.position.z = seg.position.z; 1038 ridge.position.y = 0.07; 1039 group.add(ridge); 1040 1041 // Leg protrusions on each side 1042 if (i > 0 && i < 8) { 1043 [-1, 1].forEach(side => { 1044 const leg = new THREE.Mesh( 1045 new THREE.BoxGeometry(segW * 0.7, 0.015, 0.03), 1046 _darkBoneMat 1047 ); 1048 leg.position.set(side * (segW + segW * 0.3), 0.02, seg.position.z); 1049 leg.rotation.z = side * 0.3; 1050 group.add(leg); 1051 }); 1052 } 1053 } 1054 1055 // Head shield (cephalon) 1056 const head = new THREE.Mesh( 1057 new THREE.SphereGeometry(0.22, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2), 1058 _fossilMat 1059 ); 1060 head.position.set(0, 0.04, -0.55); 1061 head.rotation.x = -0.2; 1062 group.add(head); 1063 1064 // Eyes
1065 [-1, 1].forEach(side => { 1066 const eye = new THREE.Mesh( 1067 new THREE.SphereGeometry(0.04, 6, 6), 1068 new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.3, metalness: 0.4 }) 1069 ); 1070 eye.position.set(side * 0.12, 0.1, -0.52); 1071 group.add(eye); 1072 }); 1073 1074 // Tail (pygidium) 1075 const tail = new THREE.Mesh( 1076 new THREE.ConeGeometry(0.15, 0.25, 6), 1077 _fossilMat 1078 ); 1079 tail.position.set(0, 0.04, 0.6); 1080 tail.rotation.x = Math.PI / 2; 1081 group.add(tail); 1082 1083 group.position.set(ped.x, ped.topY, ped.z); 1084 group.rotation.x = -0.15; 1085 scene.add(group); 1086 } 1087 1088 function buildArchaeopteryxFossil(scene, ped) { 1089 const group = new THREE.Group(); 1090 const bone = _boneMat; 1091 1092 // --- Running pose: body leaning forward, one leg forward, wings back --- 1093 1094 // Spine (tilted forward ~40 degrees from horizontal) 1095 const spinePoints = []; 1096 for (let i = 0; i <= 8; i++) { 1097 const t = i / 8; 1098 spinePoints.push(new THREE.Vector3(0, t * 0.22, t * 0.38)); 1099 } 1100 const spineCurve = new THREE.CatmullRomCurve3(spinePoints); 1101 const spine = new THREE.Mesh(new THREE.TubeGeometry(spineCurve, 10, 0.018, 5, false), bone); 1102 group.add(spine); 1103 1104 // Ribcage (along tilted spine) 1105 for (let i = 0; i < 5; i++) { 1106 [-1, 1].forEach(side => { 1107 const rib = new THREE.Mesh( 1108 new THREE.TorusGeometry(0.06 + i * 0.004, 0.007, 4, 8, Math.PI * 0.55), 1109 _darkBoneMat 1110 ); 1111 const t = (i + 1) / 6; 1112 rib.position.set(0, t * 0.22, t * 0.38); 1113 rib.rotation.y = side * Math.PI / 2; 1114 rib.rotation.z = 0.5; // match spine tilt 1115 group.add(rib); 1116 }); 1117 } 1118 1119 // Pelvis 1120 const pelvis = new THREE.Mesh(new THREE.TorusGeometry(0.05, 0.012, 4, 8, Math.PI), _darkBoneMat); 1121 pelvis.position.set(0, 0.02, 0.04); 1122 pelvis.rotation.x = Math.PI / 2; 1123 group.add(pelvis); 1124 1125 // Short neck (Archaeopteryx had a relatively short neck) 1126 const neckPts = [ 1127 new THREE.Vector3(0, 0.22, 0.38), 1128 new THREE.Vector3(0, 0.27, 0.42), 1129 new THREE.Vector3(0, 0.30, 0.44), 1130 ]; 1131 const neckCurve = new THREE.CatmullRomCurve3(neckPts); 1132 group.add(new THREE.Mesh(new THREE.TubeGeometry(neckCurve, 6, 0.012, 5, false), bone)); 1133 1134 // Skull (compact, thrust forward â Archaeopteryx had a small toothed head) 1135 const skull = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 6), bone); 1136 skull.position.set(0, 0.31, 0.46); 1137 skull.scale.set(0.85, 0.7, 1.3); 1138 group.add(skull); 1139 1140 // Toothed jaw (distinctive Archaeopteryx feature â teeth, not a beak) 1141 const jaw = new THREE.Mesh(new THREE.ConeGeometry(0.012, 0.06, 4), bone); 1142 jaw.position.set(0, 0.30, 0.52); 1143 jaw.rotation.x = Math.PI / 2; 1144 group.add(jaw); 1145 1146 // Tiny teeth along jaw 1147 for (let t = 0; t < 3; t++) { 1148 const tooth = new THREE.Mesh(new THREE.ConeGeometry(0.003, 0.012, 3), 1149 new THREE.MeshStandardMaterial({ color: 0xf0e8d0, roughness: 0.4 })); 1150 tooth.position.set((t - 1) * 0.008, 0.285, 0.50 + t * 0.005); 1151 group.add(tooth); 1152 } 1153 1154 // Eyes 1155 const eyeMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.3 }); 1156 [-1, 1].forEach(side => { 1157 const eye = new THREE.Mesh(new THREE.SphereGeometry(0.008, 4, 4), eyeMat); 1158 eye.position.set(side * 0.025, 0.32, 0.49); 1159 group.add(eye); 1160 }); 1161 1162 // Wings (swept back in running pose, partially folded) 1163 const featherMat = new THREE.MeshStandardMaterial({ 1164 color: 0xb8a888, roughness: 0.7, side: THREE.DoubleSide, transparent: true, opacity: 0.55 1165 }); 1166 [-1, 1].forEach(side => { 1167 // Humerus (upper arm, swept back) 1168 const humerus = new THREE.Mesh(new THREE.CylinderGeometry(0.009, 0.007, 0.14, 4), bone); 1169 humerus.position.set(side * 0.09, 0.16, 0.26); 1170 humerus.rotation.z = side * 0.8; 1171 humerus.rotation.x = -0.4; 1172 group.add(humerus); 1173 1174 // Radius/ulna (forearm, folded) 1175 const radius = new THREE.Mesh(new THREE.CylinderGeometry(0.007, 0.005, 0.13, 4), bone); 1176 radius.position.set(side * 0.20, 0.12, 0.18); 1177 radius.rotation.z = side * 0.4; 1178 radius.rotation.x = -0.5; 1179 group.add(radius); 1180 1181 // Clawed fingers (3 clawed digits â key Archaeopteryx feature) 1182 for (let c = 0; c < 3; c++) { 1183 const claw = new THREE.Mesh(new THREE.ConeGeometry(0.004, 0.03, 3), _darkBoneMat); 1184 claw.position.set(side * (0.27 + c * 0.01), 0.09 - c * 0.01, 0.12 - c * 0.02); 1185 claw.rotation.z = side * 0.3; 1186 claw.rotation.x = -0.3; 1187 group.add(claw); 1188 } 1189 1190 // Feather impressions along wing (trailing back) 1191 for (let f = 0; f < 6; f++) { 1192 const feather = new THREE.Mesh(new THREE.PlaneGeometry(0.025, 0.10), featherMat); 1193 feather.position.set(side * (0.11 + f * 0.035), 0.14 - f * 0.008, 0.22 - f * 0.025); 1194 feather.rotation.z = side * (0.25 + f * 0.06); 1195 feather.rotation.x = -0.35; 1196 group.add(feather); 1197 } 1198 }); 1199 1200 // Bird legs using connected curves (proper bird anatomy: 1201 // femur short & close to body, tibiotarsus long going down, 1202 // tarsometatarsus angling forward to feet) 1203 1204 // LEFT LEG â forward stride 1205 const lLegPts = [ 1206 new THREE.Vector3(-0.03, 0.0, 0.06), // hip joint at pelvis 1207 new THREE.Vector3(-0.035, -0.08, 0.10), // femur end / knee 1208 new THREE.Vector3(-0.035, -0.22, 0.06), // tibiotarsus down 1209 new THREE.Vector3(-0.035, -0.28, 0.12), // ankle (bird "knee") 1210 new THREE.Vector3(-0.035, -0.32, 0.14), // foot 1211 ];
1212 const lLegCurve = new THREE.CatmullRomCurve3(lLegPts); 1213 const lLeg = new THREE.Mesh(new THREE.TubeGeometry(lLegCurve, 10, 0.008, 4, false), bone); 1214 group.add(lLeg); 1215 1216 // Left foot talons 1217 for (let t = 0; t < 3; t++) { 1218 const talon = new THREE.Mesh(new THREE.ConeGeometry(0.004, 0.03, 3), _darkBoneMat); 1219 talon.position.set(-0.035 + (t - 1) * 0.012, -0.34, 0.16 + t * 0.006); 1220 talon.rotation.x = 0.5; 1221 group.add(talon); 1222 } 1223 1224 // RIGHT LEG â back stride (pushing off) 1225 const rLegPts = [ 1226 new THREE.Vector3(0.03, 0.0, 0.02), // hip joint at pelvis 1227 new THREE.Vector3(0.035, -0.07, -0.03), // femur end / knee 1228 new THREE.Vector3(0.035, -0.22, -0.06), // tibiotarsus down 1229 new THREE.Vector3(0.035, -0.28, -0.02), // ankle 1230 new THREE.Vector3(0.035, -0.32, 0.01), // foot 1231 ]; 1232 const rLegCurve = new THREE.CatmullRomCurve3(rLegPts); 1233 const rLeg = new THREE.Mesh(new THREE.TubeGeometry(rLegCurve, 10, 0.008, 4, false), bone); 1234 group.add(rLeg); 1235 1236 // Right foot talons 1237 for (let t = 0; t < 3; t++) { 1238 const talon = new THREE.Mesh(new THREE.ConeGeometry(0.004, 0.03, 3), _darkBoneMat); 1239 talon.position.set(0.035 + (t - 1) * 0.012, -0.34, 0.03 + t * 0.006); 1240 talon.rotation.x = 0.5; 1241 group.add(talon); 1242 } 1243 1244 // Tail (extending back for balance, slightly raised) 1245 for (let i = 0; i < 10; i++) { 1246 const vert = new THREE.Mesh( 1247 new THREE.SphereGeometry(0.012 - i * 0.0008, 4, 4), 1248 bone 1249 ); 1250 vert.position.set(0, 0.02 + i * 0.008, -0.05 - i * 0.03); 1251 group.add(vert); 1252 } 1253 1254 // Tail feather fan (spread behind for balance) 1255 for (let f = 0; f < 5; f++) { 1256 const tf = new THREE.Mesh(new THREE.PlaneGeometry(0.025, 0.1), featherMat); 1257 tf.position.set((f - 2) * 0.02, 0.10, -0.35); 1258 tf.rotation.x = -0.3; 1259 group.add(tf); 1260 } 1261 1262 // Mount rod (thin metal rod from body down to pedestal) 1263 const rod = new THREE.Mesh(new THREE.CylinderGeometry(0.006, 0.006, 0.4, 4), _darkMetalMat); 1264 rod.position.set(0, -0.18, 0.04); 1265 group.add(rod); 1266 1267 group.position.set(ped.x, ped.topY + 0.45, ped.z); 1268 group.scale.set(1.5, 1.5, 1.5); 1269 scene.add(group); 1270 } 1271 1272 function buildNeanderthalSkull(scene, ped) { 1273 const group = new THREE.Group(); 1274 1275 // Cranium â elongated sphere with pronounced brow 1276 const cranium = new THREE.Mesh( 1277 new THREE.SphereGeometry(0.22, 12, 10), 1278 _fossilMat 1279 ); 1280 cranium.scale.set(1, 0.9, 1.15); 1281 cranium.position.y = 0.15; 1282 cranium.castShadow = true; 1283 group.add(cranium); 1284 1285 // Brow ridge â prominent 1286 const brow = new THREE.Mesh( 1287 new THREE.TorusGeometry(0.16, 0.035, 6, 12, Math.PI), 1288 _boneMat 1289 ); 1290 brow.position.set(0, 0.12, -0.17); 1291 brow.rotation.x = 0.3; 1292 group.add(brow); 1293 1294 // Eye sockets 1295 const eyeSocketMat = new THREE.MeshStandardMaterial({ color: 0x1a1512, roughness: 0.9 }); 1296 [-1, 1].forEach(side => { 1297 const socket = new THREE.Mesh( 1298 new THREE.SphereGeometry(0.055, 8, 8), 1299 eyeSocketMat 1300 ); 1301 socket.position.set(side * 0.08, 0.08, -0.2); 1302 socket.scale.z = 0.5; 1303 group.add(socket); 1304 }); 1305 1306 // Nasal cavity 1307 const nose = new THREE.Mesh( 1308 new THREE.BoxGeometry(0.05, 0.06, 0.04), 1309 eyeSocketMat 1310 ); 1311 nose.position.set(0, 0.01, -0.22); 1312 group.add(nose); 1313 1314 // Cheekbones 1315 [-1, 1].forEach(side => { 1316 const cheek = new THREE.Mesh( 1317 new THREE.SphereGeometry(0.04, 6, 6), 1318 _fossilMat 1319 ); 1320 cheek.position.set(side * 0.15, 0.02, -0.16); 1321 group.add(cheek); 1322 }); 1323 1324 // Upper jaw (maxilla) 1325 const maxilla = new THREE.Mesh( 1326 new THREE.BoxGeometry(0.16, 0.05, 0.1), 1327 _boneMat 1328 ); 1329 maxilla.position.set(0, -0.05, -0.18); 1330 group.add(maxilla); 1331 1332 // Teeth row 1333 for (let i = 0; i < 6; i++) { 1334 const tooth = new THREE.Mesh( 1335 new THREE.BoxGeometry(0.018, 0.025, 0.015), 1336 new THREE.MeshStandardMaterial({ color: 0xf0e8d0, roughness: 0.4 }) 1337 ); 1338 tooth.position.set(-0.05 + i * 0.02, -0.085, -0.2); 1339 group.add(tooth); 1340 } 1341 1342 // Lower jaw (mandible) 1343 const jaw = new THREE.Mesh( 1344 new THREE.TorusGeometry(0.1, 0.025, 6, 10, Math.PI), 1345 _boneMat 1346 ); 1347 jaw.position.set(0, -0.1, -0.14); 1348 jaw.rotation.x = Math.PI; 1349 jaw.rotation.z = Math.PI; 1350 group.add(jaw); 1351 1352 // Chin (not very pronounced for Neanderthal â receding) 1353 const chin = new THREE.Mesh( 1354 new THREE.SphereGeometry(0.03, 6, 6), 1355 _boneMat 1356 ); 1357 chin.position.set(0, -0.13, -0.18); 1358 group.add(chin); 1359 1360 // Temporal bones (sides)
1361 [-1, 1].forEach(side => { 1362 const temporal = new THREE.Mesh( 1363 new THREE.SphereGeometry(0.06, 6, 6), 1364 _fossilMat 1365 ); 1366 temporal.position.set(side * 0.2, 0.1, -0.02); 1367 temporal.scale.set(0.5, 0.8, 1); 1368 group.add(temporal); 1369 }); 1370 1371 group.position.set(ped.x, ped.topY + 0.15, ped.z); 1372 group.rotation.y = Math.PI; 1373 group.scale.set(1.4, 1.4, 1.4); 1374 scene.add(group); 1375 } 1376 1377 // ===================== SKELETON CENTERPIECE (GRAND HALL) ===================== 1378 1379 function buildTRexSkeleton(scene) { 1380 // Initial collision box â oversized to block NPCs before model loads; auto-fitted later 1381 const collBox = new THREE.Mesh(new THREE.BoxGeometry(5, 5, 8)); 1382 collBox.position.set(0, 2, 0); 1383 collBox.visible = false; 1384 scene.add(collBox); 1385 collisionWalls.push(collBox); 1386 1387 // Lighting for the centerpiece 1388 const spotAbove = new THREE.PointLight(0xfff5dd, 0.9, 18); 1389 spotAbove.position.set(0, WALL_HEIGHT - 0.5, 0); 1390 scene.add(spotAbove); 1391 1392 // Load GLB model (includes its own stand) 1393 const dracoLoader = new THREE.DRACOLoader(); 1394 dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/[email protected]/examples/js/libs/draco/'); 1395 const loader = new THREE.GLTFLoader(); 1396 loader.setDRACOLoader(dracoLoader); 1397 const boneMaterial = new THREE.MeshLambertMaterial({ 1398 color: 0xd8ccb4 1399 }); 1400 1401 loader.load( 1402 'assets/bambiraptor-skeleton.glb', 1403 function (gltf) { 1404 const model = gltf.scene; 1405 1406 // Apply bone material to all meshes (no shadows for performance) 1407 model.traverse(function (child) { 1408 if (child.isMesh) { 1409 child.material = boneMaterial; 1410 } 1411 }); 1412 1413 // Wrap in a pivot so rotation doesn't fight with positioning 1414 const pivot = new THREE.Group(); 1415 pivot.add(model); 1416 1417 // Fix orientation: Fusion 360 Z-up â Three.js Y-up 1418 model.rotation.x = -Math.PI / 2; 1419 // Rotate pivot so skull faces +Z (info wing) â skull was at +X 1420 pivot.rotation.y = -Math.PI / 2; 1421 1422 // Compute bounding box after rotation 1423 const box = new THREE.Box3().setFromObject(pivot); 1424 const size = new THREE.Vector3(); 1425 const center = new THREE.Vector3(); 1426 box.getSize(size); 1427 box.getCenter(center); 1428 1429 // Scale to fit Grand Hall (target ~6 units tall) 1430 const maxDim = Math.max(size.x, size.y, size.z); 1431 const scale = 6.0 / maxDim; 1432 pivot.scale.set(scale, scale, scale); 1433 1434 // Re-center after scaling 1435 box.setFromObject(pivot); 1436 box.getSize(size); 1437 box.getCenter(center); 1438 1439 // Position: centered on X/Z, base on floor
1440 pivot.position.x = -center.x; 1441 pivot.position.y = -box.min.y; 1442 pivot.position.z = -center.z; 1443 1444 scene.add(pivot); 1445 1446 // Auto-fit collision box to the actual model with margin 1447 collBox.geometry.dispose(); 1448 collBox.geometry = new THREE.BoxGeometry(size.x + 0.6, size.y, size.z + 0.6); 1449 collBox.position.set(0, size.y / 2, 0); 1450 collBox.updateMatrixWorld(true); 1451 }, 1452 null, 1453 function (err) { 1454 console.error('Model load error:', err); 1455 } 1456 ); 1457 } 1458 1459 function addFossilCenterpiece(scene, hall, exhibitKey) { 1460 const data = MUSEUM_DATA.exhibits[exhibitKey]; 1461 if (!data) return; 1462 1463 const { cx, cz, axis } = hall; 1464 const ped = addFossilPedestal(scene, cx, cz); 1465 1466 // Build the appropriate fossil 1467 if (exhibitKey === 'trilobite') buildTrilobiteFossil(scene, ped); 1468 else if (exhibitKey === 'archaeopteryx') buildArchaeopteryxFossil(scene, ped); 1469 else if (exhibitKey === 'neanderthal') buildNeanderthalSkull(scene, ped); 1470 1471 // Wall-mounted info sign on the far wall of the wing 1472 addWallSign(scene, hall, exhibitKey, data.name, data.subtitle, data.description); 1473 } 1474 1475 // ===================== BUILD ===================== 1476 1477 function build(scene) { 1478 buildGrandHall(scene); 1479 buildTRexSkeleton(scene); 1480 buildWelcomeSign(scene); 1481 buildBambiraptorSign(scene); 1482 1483 const hw = GRAND_HALL_WIDTH / 2; 1484 const hd = GRAND_HALL_DEPTH / 2; 1485 1486 // Four exhibit wings branching off the grand hall 1487 const trilobiteHall = buildExhibitHall(scene, -hw, 0, 'x', COLORS.trilobite, 'Trilobite Wing'); 1488 const archaeopteryxHall = buildExhibitHall(scene, hw, 0, 'x', COLORS.archaeopteryx, 'Archaeopteryx Wing'); 1489 const neanderthalHall = buildExhibitHall(scene, 0, -hd, 'z', COLORS.neanderthal, 'Neanderthal Wing'); 1490 const infoHall = buildExhibitHall(scene, 0, hd, 'z', COLORS.info, 'Info Wing'); 1491 1492 addLighting(scene); 1493 1494 // Fossil centerpieces in each exhibit wing 1495 addFossilCenterpiece(scene, trilobiteHall, 'trilobite'); 1496 addFossilCenterpiece(scene, archaeopteryxHall, 'archaeopteryx'); 1497 addFossilCenterpiece(scene, neanderthalHall, 'neanderthal'); 1498 1499 return { 1500 halls: { trilobite: trilobiteHall, archaeopteryx: archaeopteryxHall, neanderthal: neanderthalHall, info: infoHall } 1501 }; 1502 } 1503 1504 function getCollisionWalls() { return collisionWalls; } 1505 1506 function getCurrentRoom(x, z) { 1507 for (const room of rooms) { 1508 if (x >= room.minX && x <= room.maxX && z >= room.minZ && z <= room.maxZ) return room.name; 1509 } 1510 return 'Grand Hall'; 1511 } 1512 1513 function getRooms() { return rooms; } 1514 1515 // Tilted lectern-style info sign next to the skeleton centerpiece 1516 let _welcomeSignFace = null; 1517 1518 function buildWelcomeSign(scene) { 1519 const signX = -2.2, signZ = 0; 1520 const signW = 1.6, signH = 1.15; 1521 const postHeight = 0.85; 1522 const tiltAngle = -0.55; // ~31 degrees, lectern tilt 1523 1524 // Outer group: position + face toward the skeleton (+X) 1525 const signGroup = new THREE.Group(); 1526 signGroup.position.set(signX, 0, signZ); 1527 signGroup.rotation.y = -Math.PI / 2; // local +Z â world +X 1528 1529 // Base plate 1530 const base = new THREE.Mesh( 1531 new THREE.CylinderGeometry(0.22, 0.25, 0.04, 12), 1532 _darkMetalMat 1533 ); 1534 base.position.y = 0.02; 1535 signGroup.add(base); 1536 1537 // Vertical post 1538 const post = new THREE.Mesh( 1539 new THREE.CylinderGeometry(0.03, 0.03, postHeight, 8), 1540 _darkMetalMat 1541 ); 1542 post.position.y = postHeight / 2; 1543 signGroup.add(post); 1544 1545 // Canvas texture for the reading surface 1546 const canvas = document.createElement('canvas'); 1547 canvas.width = 640; 1548 canvas.height = 480; 1549 const ctx = canvas.getContext('2d'); 1550 ctx.fillStyle = '#1a1712'; 1551 ctx.fillRect(0, 0, 640, 480); 1552 ctx.strokeStyle = '#c9a96e'; 1553 ctx.lineWidth = 3; 1554 ctx.strokeRect(12, 12, 616, 456); 1555 ctx.fillStyle = '#c9a96e'; 1556 ctx.font = 'italic 46px Georgia, serif'; 1557 ctx.textAlign = 'center'; 1558 ctx.fillText('Welcome', 320, 80); 1559 ctx.strokeStyle = 'rgba(201,169,110,0.4)'; 1560 ctx.lineWidth = 1; 1561 ctx.beginPath(); 1562 ctx.moveTo(120, 108); 1563 ctx.lineTo(520, 108); 1564 ctx.stroke(); 1565 ctx.fillStyle = '#c0b3a1'; 1566 ctx.font = '24px Georgia, serif'; 1567 ctx.fillText("Matthew Kim's Museum of", 320, 165); 1568 ctx.fillText('Science Communication', 320, 198); 1569 ctx.fillStyle = '#a89b8c';
1570 ctx.font = 'italic 20px Georgia, serif'; 1571 ctx.fillText('A Notation in Science Communication ePortfolio', 320, 260); 1572 ctx.fillStyle = '#7a7060'; 1573 ctx.font = '19px Helvetica, sans-serif'; 1574 ctx.fillText('Click to read the welcome letter', 320, 440); 1575 const tex = new THREE.CanvasTexture(canvas); 1576 1577 // Sub-group for the tilted surface â everything tilts together 1578 const surface = new THREE.Group(); 1579 surface.position.y = postHeight; 1580 surface.rotation.x = tiltAngle; 1581 signGroup.add(surface); 1582 1583 // Sign body (solid box â also the raycasting target) 1584 const backMat = new THREE.MeshStandardMaterial({ color: 0x1a1712, roughness: 0.4, metalness: 0.3 }); 1585 const signBox = new THREE.Mesh( 1586 new THREE.BoxGeometry(signW, signH, 0.04), 1587 backMat 1588 ); 1589 surface.add(signBox); 1590 1591 // Text overlay on the front face of the box 1592 const textPlane = new THREE.Mesh( 1593 new THREE.PlaneGeometry(signW - 0.02, signH - 0.02), 1594 new THREE.MeshBasicMaterial({ map: tex, side: THREE.DoubleSide }) 1595 ); 1596 textPlane.position.z = 0.025; // slightly in front of the box face 1597 surface.add(textPlane); 1598 1599 // Gold trim frame edges 1600 const trimMat = new THREE.MeshStandardMaterial({ color: 0xc9a96e, roughness: 0.3, metalness: 0.6 }); 1601 const bw = 0.04; // border width 1602 // Top bar 1603 surface.add(new THREE.Mesh(new THREE.BoxGeometry(signW + bw * 2, bw, 0.05), trimMat) 1604 .translateY(signH / 2 + bw / 2)); 1605 // Bottom bar 1606 surface.add(new THREE.Mesh(new THREE.BoxGeometry(signW + bw * 2, bw, 0.05), trimMat) 1607 .translateY(-signH / 2 - bw / 2)); 1608 // Left bar 1609 surface.add(new THREE.Mesh(new THREE.BoxGeometry(bw, signH + bw * 2, 0.05), trimMat) 1610 .translateX(-signW / 2 - bw / 2)); 1611 // Right bar 1612 surface.add(new THREE.Mesh(new THREE.BoxGeometry(bw, signH + bw * 2, 0.05), trimMat) 1613 .translateX(signW / 2 + bw / 2)); 1614 1615 scene.add(signGroup); 1616 signGroup.updateMatrixWorld(true); // critical for raycasting before first render 1617 1618 _welcomeSignFace = signBox; 1619 } 1620 1621 function getWelcomeSignMesh() { 1622 return _welcomeSignFace; 1623 } 1624 1625 // Tilted lectern-style info sign on the opposite side of the skeleton 1626 let _bambiraptorSignFace = null; 1627 1628 function buildBambiraptorSign(scene) { 1629 const signX = 2.2, signZ = 0; 1630 const signW = 1.6, signH = 1.15; 1631 const postHeight = 0.85; 1632 const tiltAngle = -0.55; 1633 1634 // Outer group: position + face away from skeleton (+X), mirroring welcome sign 1635 const signGroup = new THREE.Group(); 1636 signGroup.position.set(signX, 0, signZ); 1637 signGroup.rotation.y = Math.PI / 2; 1638 1639 // Base plate 1640 const base = new THREE.Mesh( 1641 new THREE.CylinderGeometry(0.22, 0.25, 0.04, 12), 1642 _darkMetalMat 1643 ); 1644 base.position.y = 0.02; 1645 signGroup.add(base); 1646 1647 // Vertical post 1648 const post = new THREE.Mesh( 1649 new THREE.CylinderGeometry(0.03, 0.03, postHeight, 8), 1650 _darkMetalMat 1651 ); 1652 post.position.y = postHeight / 2; 1653 signGroup.add(post); 1654 1655 // Canvas texture for the reading surface 1656 const canvas = document.createElement('canvas'); 1657 canvas.width = 640; 1658 canvas.height = 480; 1659 const ctx = canvas.getContext('2d'); 1660 ctx.fillStyle = '#1a1712'; 1661 ctx.fillRect(0, 0, 640, 480); 1662 ctx.strokeStyle = '#c9a96e'; 1663 ctx.lineWidth = 3; 1664 ctx.strokeRect(12, 12, 616, 456); 1665 ctx.fillStyle = '#c9a96e'; 1666 ctx.font = 'italic 36px Georgia, serif'; 1667 ctx.textAlign = 'center'; 1668 ctx.fillText('Bambiraptor feinbergi', 320, 80); 1669 ctx.strokeStyle = 'rgba(201,169,110,0.4)'; 1670 ctx.lineWidth = 1; 1671 ctx.beginPath(); 1672 ctx.moveTo(120, 108); 1673 ctx.lineTo(520, 108); 1674 ctx.stroke(); 1675 ctx.fillStyle = '#c0b3a1'; 1676 ctx.font = '24px Georgia, serif'; 1677 ctx.fillText('Skeleton CAD Model', 320, 165); 1678 ctx.fillStyle = '#a89b8c'; 1679 ctx.font = 'italic 19px Georgia, serif'; 1680 ctx.fillText('Introduction to Engineering Design', 320, 220); 1681 ctx.fillText('Senior Year of High School', 320, 250); 1682 ctx.fillStyle = '#7a7060'; 1683 ctx.font = '19px Helvetica, sans-serif'; 1684 ctx.fillText('Click to read more', 320, 440);
1685 const tex = new THREE.CanvasTexture(canvas); 1686 1687 // Sub-group for the tilted surface 1688 const surface = new THREE.Group(); 1689 surface.position.y = postHeight; 1690 surface.rotation.x = tiltAngle; 1691 signGroup.add(surface); 1692 1693 // Sign body (raycasting target) 1694 const backMat = new THREE.MeshStandardMaterial({ color: 0x1a1712, roughness: 0.4, metalness: 0.3 }); 1695 const signBox = new THREE.Mesh( 1696 new THREE.BoxGeometry(signW, signH, 0.04), 1697 backMat 1698 ); 1699 surface.add(signBox); 1700 1701 // Text overlay 1702 const textPlane = new THREE.Mesh( 1703 new THREE.PlaneGeometry(signW - 0.02, signH - 0.02), 1704 new THREE.MeshBasicMaterial({ map: tex, side: THREE.DoubleSide }) 1705 ); 1706 textPlane.position.z = 0.025; 1707 surface.add(textPlane); 1708 1709 // Gold trim frame edges 1710 const trimMat = new THREE.MeshStandardMaterial({ color: 0xc9a96e, roughness: 0.3, metalness: 0.6 }); 1711 const bw = 0.04; 1712 surface.add(new THREE.Mesh(new THREE.BoxGeometry(signW + bw * 2, bw, 0.05), trimMat) 1713 .translateY(signH / 2 + bw / 2)); 1714 surface.add(new THREE.Mesh(new THREE.BoxGeometry(signW + bw * 2, bw, 0.05), trimMat) 1715 .translateY(-signH / 2 - bw / 2)); 1716 surface.add(new THREE.Mesh(new THREE.BoxGeometry(bw, signH + bw * 2, 0.05), trimMat) 1717 .translateX(-signW / 2 - bw / 2)); 1718 surface.add(new THREE.Mesh(new THREE.BoxGeometry(bw, signH + bw * 2, 0.05), trimMat) 1719 .translateX(signW / 2 + bw / 2)); 1720 1721 scene.add(signGroup); 1722 signGroup.updateMatrixWorld(true); 1723 1724 _bambiraptorSignFace = signBox; 1725 } 1726 1727 function getBambiraptorSignMesh() { 1728 return _bambiraptorSignFace; 1729 } 1730 1731 return { build, getCollisionWalls, getCurrentRoom, getRooms, getAnimatedLights, getWelcomeSignMesh, getBambiraptorSignMesh, WALL_HEIGHT, HALL_WIDTH, HALL_DEPTH }; 1732})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.