optimizations to rendering
All checks were successful
Deploy to GitHub Pages / deploy (push) Has been skipped
All checks were successful
Deploy to GitHub Pages / deploy (push) Has been skipped
This commit is contained in:
271
src/main.js
271
src/main.js
@@ -30,12 +30,22 @@ const SETTINGS = {
|
|||||||
files: {
|
files: {
|
||||||
visual: './city_data.json',
|
visual: './city_data.json',
|
||||||
routing: './routing_graph.json'
|
routing: './routing_graph.json'
|
||||||
|
},
|
||||||
|
graphics: {
|
||||||
|
shadows: true,
|
||||||
|
antialias: true,
|
||||||
|
maxPixelRatio: 1.0, // lower == more blurry, 2 or 3 for high res
|
||||||
|
materialType: 'standard', // or 'standard'
|
||||||
|
farClip: 9000, // view distance limit
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let scene, camera, renderer, controls;
|
let scene, camera, renderer, controls;
|
||||||
let inputManager, routeManager, uiManager, gameManager, vehicleSystem;
|
let inputManager, routeManager, uiManager, gameManager, vehicleSystem;
|
||||||
|
|
||||||
|
let cityMesh; // The single mesh containing all buildings
|
||||||
|
let buildingRegistry = []; // Stores { data, nearestNodeId, startIndex, count } for each building
|
||||||
|
|
||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
|
|
||||||
let currentViewMode = 'none'; // 'none', 'zoning', 'approval'
|
let currentViewMode = 'none'; // 'none', 'zoning', 'approval'
|
||||||
@@ -101,67 +111,74 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateBuildingColors() {
|
function updateBuildingColors() {
|
||||||
scene.traverse((obj) => {
|
if (!cityMesh || !buildingRegistry.length) return;
|
||||||
if (obj.name === 'BUILDING_MESH') {
|
|
||||||
const data = obj.userData.cityData;
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// 1. STANDARD VIEW
|
const colorAttribute = cityMesh.geometry.attributes.color;
|
||||||
|
const colorArray = colorAttribute.array;
|
||||||
|
|
||||||
|
// Temp variables to avoid creating objects in loop
|
||||||
|
const _color = new THREE.Color();
|
||||||
|
|
||||||
|
// Iterate through every building in our registry
|
||||||
|
for (let i = 0; i < buildingRegistry.length; i++) {
|
||||||
|
const entry = buildingRegistry[i];
|
||||||
|
const data = entry.data;
|
||||||
|
|
||||||
|
// --- 1. Determine Target Color based on Mode ---
|
||||||
|
|
||||||
|
// STANDARD VIEW
|
||||||
if (currentViewMode === 'none') {
|
if (currentViewMode === 'none') {
|
||||||
obj.material.color.copy(SETTINGS.colors.building);
|
_color.copy(SETTINGS.colors.building);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ZONING VIEW
|
// ZONING VIEW
|
||||||
else if (currentViewMode === 'zoning') {
|
else if (currentViewMode === 'zoning') {
|
||||||
if (data.type === 'residential') {
|
if (data.type === 'residential') {
|
||||||
const color = SETTINGS.colors.building.clone();
|
_color.copy(SETTINGS.colors.building).lerp(SETTINGS.colors.zoningRes, data.density || 0.5);
|
||||||
color.lerp(SETTINGS.colors.zoningRes, data.density || 0.5);
|
|
||||||
obj.material.color.copy(color);
|
|
||||||
} else if (data.type === 'commercial') {
|
} else if (data.type === 'commercial') {
|
||||||
const color = SETTINGS.colors.building.clone();
|
_color.copy(SETTINGS.colors.building).lerp(SETTINGS.colors.zoningCom, data.density || 0.5);
|
||||||
color.lerp(SETTINGS.colors.zoningCom, data.density || 0.5);
|
|
||||||
obj.material.color.copy(color);
|
|
||||||
} else {
|
} else {
|
||||||
obj.material.color.copy(SETTINGS.colors.building);
|
_color.copy(SETTINGS.colors.building);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. APPROVAL / COVERAGE VIEW (GRADIENT)
|
// APPROVAL VIEW
|
||||||
else if (currentViewMode === 'approval') {
|
else if (currentViewMode === 'approval') {
|
||||||
// Get graph node position
|
// Use the pre-calculated nearest ID from registry
|
||||||
const nearestId = obj.userData.nearestNodeId;
|
const node = routeManager.graphData.nodes[entry.nearestNodeId];
|
||||||
// RouteManager has logic for this
|
|
||||||
const node = routeManager.graphData.nodes[nearestId];
|
|
||||||
|
|
||||||
if (node) {
|
if (node) {
|
||||||
// Calculate distance to nearest transit
|
|
||||||
// node.y is Z in world space
|
|
||||||
const dist = routeManager.getDistanceToNearestTransit(node.x, node.y);
|
const dist = routeManager.getDistanceToNearestTransit(node.x, node.y);
|
||||||
|
|
||||||
// Color Logic:
|
|
||||||
// < 100m = Green (Great)
|
|
||||||
// < 300m = Yellow (Okay)
|
|
||||||
// > 600m = Red (Bad)
|
|
||||||
|
|
||||||
if (dist === Infinity) {
|
if (dist === Infinity) {
|
||||||
obj.material.color.copy(SETTINGS.colors.coverageBad); // Deep Red
|
_color.copy(SETTINGS.colors.coverageBad);
|
||||||
} else {
|
} else {
|
||||||
const MAX_DIST = 600;
|
const MAX_DIST = 600;
|
||||||
const factor = Math.min(1.0, dist / MAX_DIST); // 0.0 (Close) to 1.0 (Far)
|
const factor = Math.min(1.0, dist / MAX_DIST);
|
||||||
|
// Lerp Good -> Bad
|
||||||
|
_color.copy(SETTINGS.colors.coverageGood).lerp(SETTINGS.colors.coverageBad, factor);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback if node not found
|
||||||
|
_color.copy(SETTINGS.colors.coverageBad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lerp from Green to Red
|
// --- 2. Apply Color to Vertices ---
|
||||||
// (Green at 0, Red at 1)
|
// We update the specific range of vertices belonging to this building
|
||||||
const color = SETTINGS.colors.coverageGood.clone();
|
const start = entry.startIndex;
|
||||||
// We can lerp to Red.
|
const end = start + entry.count;
|
||||||
// Or use a Yellow midpoint?
|
|
||||||
// Simple lerp: Green -> Red
|
for (let v = start; v < end; v++) {
|
||||||
color.lerp(SETTINGS.colors.coverageBad, factor);
|
const idx = v * 3;
|
||||||
obj.material.color.copy(color);
|
colorArray[idx] = _color.r;
|
||||||
|
colorArray[idx + 1] = _color.g;
|
||||||
|
colorArray[idx + 2] = _color.b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
// Flag that the geometry colors have changed so GPU updates
|
||||||
});
|
colorAttribute.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -173,12 +190,21 @@ function setupScene() {
|
|||||||
scene.background = new THREE.Color(SETTINGS.colors.background);
|
scene.background = new THREE.Color(SETTINGS.colors.background);
|
||||||
scene.fog = new THREE.FogExp2(SETTINGS.colors.background, 0.0002);
|
scene.fog = new THREE.FogExp2(SETTINGS.colors.background, 0.0002);
|
||||||
|
|
||||||
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 20000);
|
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, SETTINGS.graphics.farClip);
|
||||||
camera.position.set(0, 800, 800);
|
camera.position.set(0, 800, 800);
|
||||||
|
|
||||||
renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
|
renderer = new THREE.WebGLRenderer({ antialias: SETTINGS.graphics.antialias, logarithmicDepthBuffer: true });
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
renderer.domElement.style.width = '100%';
|
||||||
renderer.shadowMap.enabled = true;
|
renderer.domElement.style.height = '100%';
|
||||||
|
renderer.domElement.style.display = 'block';
|
||||||
|
|
||||||
|
document.documentElement.style.margin = '0';
|
||||||
|
document.documentElement.style.height = '100%';
|
||||||
|
document.body.style.margin = '0';
|
||||||
|
document.body.style.height = '100%';
|
||||||
|
document.body.style.overflow = 'hidden'; // Prevents scrollbars
|
||||||
|
|
||||||
|
renderer.shadowMap.enabled = SETTINGS.graphics.shadows;
|
||||||
document.body.appendChild(renderer.domElement);
|
document.body.appendChild(renderer.domElement);
|
||||||
|
|
||||||
const ambient = new THREE.HemisphereLight(0xffffff, 0x555555, 0.7);
|
const ambient = new THREE.HemisphereLight(0xffffff, 0x555555, 0.7);
|
||||||
@@ -186,7 +212,8 @@ function setupScene() {
|
|||||||
|
|
||||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
dirLight.position.set(500, 1000, 500);
|
dirLight.position.set(500, 1000, 500);
|
||||||
dirLight.castShadow = true;
|
dirLight.castShadow = SETTINGS.graphics.shadows;
|
||||||
|
if (SETTINGS.graphics.shadows) {
|
||||||
dirLight.shadow.mapSize.set(2048, 2048);
|
dirLight.shadow.mapSize.set(2048, 2048);
|
||||||
dirLight.shadow.camera.left = -1500;
|
dirLight.shadow.camera.left = -1500;
|
||||||
dirLight.shadow.camera.right = 1500;
|
dirLight.shadow.camera.right = 1500;
|
||||||
@@ -194,7 +221,8 @@ function setupScene() {
|
|||||||
dirLight.shadow.camera.bottom = -1500;
|
dirLight.shadow.camera.bottom = -1500;
|
||||||
dirLight.shadow.camera.near = 0.5;
|
dirLight.shadow.camera.near = 0.5;
|
||||||
dirLight.shadow.camera.far = 3000; // Must be > 1225 to reach the ground
|
dirLight.shadow.camera.far = 3000; // Must be > 1225 to reach the ground
|
||||||
dirLight.shadow.bias = -0.01; // Clean up shadow artifacts
|
dirLight.shadow.bias = -0.0001; // Clean up shadow artifacts
|
||||||
|
}
|
||||||
|
|
||||||
scene.add(dirLight);
|
scene.add(dirLight);
|
||||||
|
|
||||||
@@ -205,26 +233,45 @@ function setupScene() {
|
|||||||
plane.rotation.x = -Math.PI / 2;
|
plane.rotation.x = -Math.PI / 2;
|
||||||
plane.position.y = -0.5;
|
plane.position.y = -0.5;
|
||||||
plane.name = "GROUND";
|
plane.name = "GROUND";
|
||||||
plane.receiveShadow = true;
|
plane.receiveShadow = SETTINGS.graphics.shadows;
|
||||||
scene.add(plane);
|
scene.add(plane);
|
||||||
|
|
||||||
controls = new MapControls(camera, renderer.domElement);
|
controls = new MapControls(camera, renderer.domElement);
|
||||||
controls.dampingFactor = 0.05;
|
controls.dampingFactor = 0.07;
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
}
|
||||||
camera.aspect = window.innerWidth / window.innerHeight;
|
|
||||||
camera.updateProjectionMatrix();
|
function resizeRendererToDisplaySize() {
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
const canvas = renderer.domElement;
|
||||||
});
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const pixelRatio = Math.min(window.devicePixelRatio, SETTINGS.graphics.maxPixelRatio);
|
||||||
|
|
||||||
|
// Calculate the required resolution
|
||||||
|
const width = Math.round(rect.width * pixelRatio);
|
||||||
|
const height = Math.round(rect.height * pixelRatio);
|
||||||
|
|
||||||
|
// Check if the canvas is already the right size
|
||||||
|
const needResize = canvas.width !== width || canvas.height !== height;
|
||||||
|
|
||||||
|
if (needResize) {
|
||||||
|
// Resize the render buffer, but do NOT change CSS style (false)
|
||||||
|
renderer.setSize(width, height, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return needResize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. Visual Rendering
|
// 3. Visual Rendering
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
function renderCity(data) {
|
function renderCity(data) {
|
||||||
const createLayer = (items, color, height, lift, isExtruded) => {
|
// Helper for non-interactive layers (Water, Parks, Roads) - Optimizing these too is good practice
|
||||||
|
// We will merge these per type as well to keep draw calls low
|
||||||
|
const createMergedLayer = (items, color, height, lift, isExtruded) => {
|
||||||
if (!items || !items.length) return;
|
if (!items || !items.length) return;
|
||||||
const geometries = [];
|
const geometries = [];
|
||||||
|
|
||||||
@@ -246,39 +293,40 @@ function renderCity(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let geom;
|
||||||
if (isExtruded) {
|
if (isExtruded) {
|
||||||
const geom = new THREE.ExtrudeGeometry(shape, { depth: item.height || height, bevelEnabled: false });
|
geom = new THREE.ExtrudeGeometry(shape, { depth: item.height || height, bevelEnabled: false });
|
||||||
geom.rotateX(-Math.PI / 2);
|
|
||||||
geometries.push(geom);
|
|
||||||
} else {
|
} else {
|
||||||
const geom = new THREE.ShapeGeometry(shape);
|
geom = new THREE.ShapeGeometry(shape);
|
||||||
geom.rotateX(-Math.PI / 2);
|
|
||||||
geom.translate(0, lift, 0);
|
|
||||||
geometries.push(geom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
geom.rotateX(-Math.PI / 2);
|
||||||
|
if (!isExtruded) geom.translate(0, lift, 0);
|
||||||
|
|
||||||
|
geometries.push(geom);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!geometries.length) return;
|
if (geometries.length === 0) return;
|
||||||
const merged = BufferGeometryUtils.mergeGeometries(geometries);
|
|
||||||
const mat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.8, side: THREE.DoubleSide });
|
const mergedGeom = BufferGeometryUtils.mergeGeometries(geometries);
|
||||||
const mesh = new THREE.Mesh(merged, mat);
|
const mat = new THREE.MeshLambertMaterial({ color: color }); // Simple color for static layers
|
||||||
mesh.receiveShadow = true;
|
const mesh = new THREE.Mesh(mergedGeom, mat);
|
||||||
if (isExtruded) mesh.castShadow = true;
|
mesh.receiveShadow = SETTINGS.graphics.shadows;
|
||||||
|
if (isExtruded) mesh.castShadow = SETTINGS.graphics.shadows;
|
||||||
scene.add(mesh);
|
scene.add(mesh);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dedicated Building Creator to cache Nearest Node ID
|
// --- OPTIMIZED BUILDING GENERATION ---
|
||||||
const createBuildingLayer = (buildings) => {
|
const createBuildingLayer = (buildings) => {
|
||||||
if (!buildings || !buildings.length) return;
|
if (!buildings || !buildings.length) return;
|
||||||
|
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
const geometries = [];
|
||||||
color: SETTINGS.colors.building,
|
buildingRegistry = []; // Reset registry
|
||||||
roughness: 0.6,
|
|
||||||
side: THREE.DoubleSide,
|
let currentVertexOffset = 0;
|
||||||
shadowSide: THREE.DoubleSide
|
|
||||||
});
|
|
||||||
|
|
||||||
buildings.forEach(b => {
|
buildings.forEach(b => {
|
||||||
|
// 1. Create Shape
|
||||||
const shape = new THREE.Shape();
|
const shape = new THREE.Shape();
|
||||||
if (b.shape.outer.length < 3) return;
|
if (b.shape.outer.length < 3) return;
|
||||||
shape.moveTo(b.shape.outer[0][0], b.shape.outer[0][1]);
|
shape.moveTo(b.shape.outer[0][0], b.shape.outer[0][1]);
|
||||||
@@ -293,36 +341,85 @@ function renderCity(data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Create Geometry
|
||||||
const geom = new THREE.ExtrudeGeometry(shape, { depth: b.height, bevelEnabled: false });
|
const geom = new THREE.ExtrudeGeometry(shape, { depth: b.height, bevelEnabled: false });
|
||||||
geom.rotateX(-Math.PI / 2);
|
geom.rotateX(-Math.PI / 2);
|
||||||
|
|
||||||
const mesh = new THREE.Mesh(geom, mat.clone());
|
// 3. Pre-calculate Logic Data (Nearest Node)
|
||||||
mesh.castShadow = true;
|
|
||||||
mesh.receiveShadow = true;
|
|
||||||
mesh.name = 'BUILDING_MESH';
|
|
||||||
mesh.userData.cityData = b.data;
|
|
||||||
|
|
||||||
// CALCULATE NEAREST NODE FOR APPROVAL MECHANIC
|
|
||||||
// We use the first point of the outer ring as a proxy for position
|
|
||||||
const bx = b.shape.outer[0][0];
|
const bx = b.shape.outer[0][0];
|
||||||
const by = b.shape.outer[0][1];
|
const by = b.shape.outer[0][1];
|
||||||
|
|
||||||
const nearestId = routeManager.findNearestNode(bx, -by);
|
const nearestId = routeManager.findNearestNode(bx, -by);
|
||||||
mesh.userData.nearestNodeId = nearestId;
|
|
||||||
|
|
||||||
scene.add(mesh);
|
// 4. Register Metadata
|
||||||
|
// We need to know how many vertices this building has to color it later
|
||||||
|
const vertexCount = geom.attributes.position.count;
|
||||||
|
|
||||||
|
buildingRegistry.push({
|
||||||
|
data: b.data, // Zoning/Density data
|
||||||
|
nearestNodeId: nearestId, // For approval view
|
||||||
|
startIndex: currentVertexOffset,
|
||||||
|
count: vertexCount
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentVertexOffset += vertexCount;
|
||||||
|
geometries.push(geom);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (geometries.length === 0) return;
|
||||||
|
|
||||||
|
// 5. Merge
|
||||||
|
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
|
||||||
|
|
||||||
|
// 6. Initialize Vertex Colors Attribute
|
||||||
|
// Create a color buffer filled with white (1,1,1) by default
|
||||||
|
const count = mergedGeometry.attributes.position.count;
|
||||||
|
const colors = new Float32Array(count * 3);
|
||||||
|
for (let i = 0; i < count * 3; i++) {
|
||||||
|
colors[i] = 1;
|
||||||
|
}
|
||||||
|
mergedGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||||
|
|
||||||
|
// 7. Material Setup
|
||||||
|
let mat;
|
||||||
|
if (SETTINGS.graphics.materialType === 'standard') {
|
||||||
|
mat = new THREE.MeshStandardMaterial({
|
||||||
|
vertexColors: true, // IMPORTANT: valid for merged mesh
|
||||||
|
roughness: 0.6,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
shadowSide: THREE.BackSide
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mat = new THREE.MeshLambertMaterial({
|
||||||
|
vertexColors: true, // IMPORTANT
|
||||||
|
roughness: 0.6,
|
||||||
|
shadowSide: THREE.BackSide
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Create and Add Single Mesh
|
||||||
|
cityMesh = new THREE.Mesh(mergedGeometry, mat);
|
||||||
|
cityMesh.castShadow = SETTINGS.graphics.shadows;
|
||||||
|
cityMesh.receiveShadow = SETTINGS.graphics.shadows;
|
||||||
|
cityMesh.name = 'CITY_MESH';
|
||||||
|
scene.add(cityMesh);
|
||||||
};
|
};
|
||||||
|
|
||||||
createBuildingLayer(data.buildings);
|
createBuildingLayer(data.buildings);
|
||||||
createLayer(data.water, SETTINGS.colors.water, 0, 0.1, false);
|
createMergedLayer(data.water, SETTINGS.colors.water, 0, 0.1, false);
|
||||||
createLayer(data.parks, SETTINGS.colors.park, 0, 0.2, false);
|
createMergedLayer(data.parks, SETTINGS.colors.park, 0, 0.2, false);
|
||||||
createLayer(data.roads, SETTINGS.colors.road, 0, 0.3, false);
|
createMergedLayer(data.roads, SETTINGS.colors.road, 0, 0.3, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
if (resizeRendererToDisplaySize()) {
|
||||||
|
const canvas = renderer.domElement;
|
||||||
|
camera.aspect = canvas.clientWidth / canvas.clientHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
const delta = clock.getDelta(); // Get time since last frame
|
const delta = clock.getDelta(); // Get time since last frame
|
||||||
|
|
||||||
controls.update();
|
controls.update();
|
||||||
|
|||||||
Reference in New Issue
Block a user