From 7ebd9f9051199a574a942e447e481ed7c1d0f967 Mon Sep 17 00:00:00 2001 From: Evan Scamehorn Date: Wed, 26 Nov 2025 01:45:14 -0600 Subject: [PATCH] reorganize graphics code --- src/main.js | 284 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 200 insertions(+), 84 deletions(-) diff --git a/src/main.js b/src/main.js index 652a7aa..4347718 100644 --- a/src/main.js +++ b/src/main.js @@ -1,119 +1,235 @@ import * as THREE from 'three'; import { MapControls } from 'three/addons/controls/MapControls.js'; -// 1. Setup Scene -const scene = new THREE.Scene(); -scene.background = new THREE.Color(0xcccccc); -scene.fog = new THREE.FogExp2(0xcccccc, 0.0001); +// ========================================== +// 1. Configuration & Constants +// ========================================== +const SETTINGS = { + colors: { + background: 0xcccccc, + ground: 0x999999, + building: 0xccffff, + sun: 0xffffff, + ambient: 0x444444, + }, + camera: { + fov: 60, + near: 0.1, + far: 20000, + initialPos: { x: 500, y: 400, z: 400 } + }, + shadows: { + enabled: true, + mapSize: 4096, + areaSize: 2000, // Size of the shadow camera view + bias: -0.0005 + }, + dataUrl: './city_data.json' +}; -const camera = new THREE.PerspectiveCamera( - 60, - window.innerWidth / window.innerHeight, - 0.1, - 20000); +// Global variables for core components +let scene, camera, renderer, controls; +let sunLight; // Needs to be global to update position in render loop -const renderer = new THREE.WebGLRenderer({ - antialias: true, - logarithmicDepthBuffer: true -}); -renderer.setSize(window.innerWidth, window.innerHeight); -renderer.shadowMap.enabled = true; -renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Optional: Makes them look nicer -document.body.appendChild(renderer.domElement); +// ========================================== +// 2. Initialization Logic +// ========================================== -// 2. Lights +function init() { + // Setup core Three.js components + setupScene(); + setupLighting(); + createGround(); -const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6); -scene.add(hemiLight); + // Initialize controls + setupControls(); -const dirLight = new THREE.DirectionalLight(0xffffff, 1.5); -dirLight.position.set(200, 400, 100); -dirLight.castShadow = true; + // Load external data + loadCityData(); -dirLight.shadow.mapSize.width = 4096; -dirLight.shadow.mapSize.height = 4096; -dirLight.shadow.camera.near = 0.5; -dirLight.shadow.camera.far = 4000; + // Start event listeners and loop + window.addEventListener('resize', onWindowResize); + animate(); +} -const d = 2000; -dirLight.shadow.camera.left = -d; -dirLight.shadow.camera.right = d; -dirLight.shadow.camera.top = d; -dirLight.shadow.camera.bottom = -d; -dirLight.shadow.bias = -0.0005; +/** + * Sets up the Scene, Camera, and Renderer. + */ +function setupScene() { + scene = new THREE.Scene(); + scene.background = new THREE.Color(SETTINGS.colors.background); + // Fog blends the floor into the background at distance for depth perception + scene.fog = new THREE.FogExp2(SETTINGS.colors.background, 0.0001); -scene.add(dirLight); + camera = new THREE.PerspectiveCamera( + SETTINGS.camera.fov, + window.innerWidth / window.innerHeight, + SETTINGS.camera.near, + SETTINGS.camera.far + ); + camera.position.set( + SETTINGS.camera.initialPos.x, + SETTINGS.camera.initialPos.y, + SETTINGS.camera.initialPos.z + ); -// 3. Helpers (Ground) -const plane = new THREE.Mesh( - new THREE.PlaneGeometry(5000, 5000), - new THREE.MeshStandardMaterial({ color: 0x999999 }) -); -plane.rotation.x = -Math.PI / 2; -plane.receiveShadow = true; -scene.add(plane); + renderer = new THREE.WebGLRenderer({ + antialias: true, + logarithmicDepthBuffer: true // Prevents z-fighting on large scale scenes + }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.shadowMap.enabled = SETTINGS.shadows.enabled; + renderer.shadowMap.type = THREE.PCFSoftShadowMap; -// 4. Load Data & Create Buildings -fetch('./city_data.json') - .then(res => res.json()) - .then(buildings => { - createCity(buildings); - }) - .catch(e => console.error("Data load failed", e)); + document.body.appendChild(renderer.domElement); +} -function createCity(data) { +/** + * Creates ambient and directional lighting. + * Configures shadow properties for the directional light. + */ +function setupLighting() { + // Ambient light for general illumination (so shadows aren't pitch black) + const hemiLight = new THREE.HemisphereLight( + SETTINGS.colors.sun, + SETTINGS.colors.ambient, + 0.6 + ); + scene.add(hemiLight); + + // Directional light acting as the Sun + sunLight = new THREE.DirectionalLight(SETTINGS.colors.sun, 1.5); + sunLight.position.set(200, 400, 100); + sunLight.castShadow = true; + + // Optimize shadow quality + sunLight.shadow.mapSize.width = SETTINGS.shadows.mapSize; + sunLight.shadow.mapSize.height = SETTINGS.shadows.mapSize; + + // Define the box in which shadows are calculated + const d = SETTINGS.shadows.areaSize; + sunLight.shadow.camera.left = -d; + sunLight.shadow.camera.right = d; + sunLight.shadow.camera.top = d; + sunLight.shadow.camera.bottom = -d; + + sunLight.shadow.camera.near = 0.5; + sunLight.shadow.camera.far = 4000; + sunLight.shadow.bias = SETTINGS.shadows.bias; // Reduces shadow artifacts (striping) + + scene.add(sunLight); +} + +function setupControls() { + controls = new MapControls(camera, renderer.domElement); + controls.enableDamping = true; // Adds weight to movement for smoother feel + controls.dampingFactor = 0.05; + controls.target.set(-100, 0, 200); +} + +// ========================================== +// 3. Scene Content Generation +// ========================================== + +function createGround() { + const geometry = new THREE.PlaneGeometry(5000, 5000); + const material = new THREE.MeshStandardMaterial({ + color: SETTINGS.colors.ground + }); + + const ground = new THREE.Mesh(geometry, material); + ground.rotation.x = -Math.PI / 2; // Rotate to lie flat + ground.receiveShadow = true; + scene.add(ground); +} + +function loadCityData() { + fetch(SETTINGS.dataUrl) + .then(res => { + if (!res.ok) throw new Error(`HTTP Error: ${res.status}`); + return res.json(); + }) + .then(buildingData => { + generateCityMesh(buildingData); + }) + .catch(error => { + console.error("Failed to load city data:", error); + // Optional: Add UI feedback here + }); +} + +/** + * efficiently renders thousands of buildings using InstancedMesh. + * @param {Array} data - Array of arrays [x, z, width, depth, height] + */ +function generateCityMesh(data) { + if (!data || data.length === 0) return; + + // Create a single geometry template const geometry = new THREE.BoxGeometry(1, 1, 1); - // Move pivot to bottom of box so scaling works comfortably + // Shift pivot point to bottom of box so scaling grows upwards geometry.translate(0, 0.5, 0); const material = new THREE.MeshStandardMaterial({ - color: 0xccffff, + color: SETTINGS.colors.building, roughness: 0.5, metalness: 0.1 }); - const mesh = new THREE.InstancedMesh(geometry, material, data.length); - mesh.castShadow = true; // Buildings cast shadows - mesh.receiveShadow = true; // Buildings receive shadows from others - mesh.frustumCulled = false; + const instancedMesh = new THREE.InstancedMesh(geometry, material, data.length); + instancedMesh.castShadow = true; + instancedMesh.receiveShadow = true; - const dummy = new THREE.Object3D(); + // Disable frustum culling to prevent flickering if the bounding sphere + // calculation is off for the entire group. Re-enable if performance is an issue. + instancedMesh.frustumCulled = false; - data.forEach((b, i) => { - const [x, z, w, d, h] = b; + const transformHelper = new THREE.Object3D(); - dummy.position.set(x, 0, z); - dummy.scale.set(w, h, d); - dummy.updateMatrix(); + data.forEach((building, index) => { + const [posX, posZ, width, depth, height] = building; - mesh.setMatrixAt(i, dummy.matrix); + transformHelper.position.set(posX, 0, posZ); + transformHelper.scale.set(width, height, depth); + transformHelper.updateMatrix(); + + instancedMesh.setMatrixAt(index, transformHelper.matrix); }); - scene.add(mesh); + scene.add(instancedMesh); } -// 5. Controls & Animation -const controls = new MapControls(camera, renderer.domElement); -controls.enableDamping = true; -controls.target.set(-100, 0, 200); -camera.position.set(500, 400, 400); +// ========================================== +// 4. Animation & Event Loop +// ========================================== -function animate() { - requestAnimationFrame(animate); - controls.update(); - dirLight.position.x = camera.position.x + 100; - dirLight.position.z = camera.position.z + 100; - - // You also need to move the shadow target to match - dirLight.target.position.set(camera.position.x, 0, camera.position.z); - dirLight.target.updateMatrixWorld(); - renderer.render(scene, camera); -} - -window.addEventListener('resize', () => { +function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); -}); +} -animate(); +function updateSunPosition() { + // Move the light with the camera to simulate a "sun" that always + // casts high-res shadows near the player (Pseudo-Cascaded Shadow Map) + if (sunLight && camera) { + sunLight.position.x = camera.position.x + 100; + sunLight.position.z = camera.position.z + 100; + + // Ensure the light points at the camera's ground position + sunLight.target.position.set(camera.position.x, 0, camera.position.z); + sunLight.target.updateMatrixWorld(); + } +} + +function animate() { + requestAnimationFrame(animate); + + controls.update(); + updateSunPosition(); + + renderer.render(scene, camera); +} + +// Start the application +init();