From 966a12f4af23a26f2f44ba464966da60bc76f4a9 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 22:20:07 -0500 Subject: [PATCH] feat(dashboard): terrain lit by the zone's lights, blending with the scenery The textured terrain in the world and property 3D views now takes the lighting the scenery uses (the scene's sun and ambient light, blending as the focus moves between scenes), as TiledDetailDiffuse_4_PS does: the blended textures times the diffuse map times two times the clamped vertex light, times the blend alpha. Until a manifest brings the lighting, the terrain keeps its own light. Checked against the client (RAWReadColorandLightMaps 0x0102e3e0 and the texture map reader at 0x0103aaf0): the game's diffuse map and blend map are DDS copies of the color and texture maps the views use (same pixels on the Avant Gardens chunks compared), so the dark patches and the steep drops at the plateau's edges are in the terrain file itself. Co-Authored-By: Claude Opus 5.5 --- dDashboardServer/static/js/lddviewer.js | 22 +++++++++++++++---- dDashboardServer/static/js/scenery.js | 8 ++++++- dDashboardServer/static/js/world3d.js | 2 +- dDashboardServer/templates/property-3d.jinja2 | 2 +- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/dDashboardServer/static/js/lddviewer.js b/dDashboardServer/static/js/lddviewer.js index 5c216a8f7..2d0cc8900 100644 --- a/dDashboardServer/static/js/lddviewer.js +++ b/dDashboardServer/static/js/lddviewer.js @@ -304,6 +304,9 @@ void main() { const TERRAIN_FRAGMENT = ` uniform sampler2D texture1, texture2, texture3, texture4, blendMap, colorMap, sceneMap, scenePalette; uniform vec3 lightDirection; +// The zone's lights as the game's TerrainDiffuse.fx gets them (scenery.js gameLights), when gameLightOn +uniform vec3 gameLightColor, gameAmbient, gameLightVec; +uniform float gameLightOn; uniform int look; uniform float sceneSize; varying vec2 vUv; @@ -327,6 +330,12 @@ void main() { color = mix(color, texture2D(texture2, tiled), blend.r); color = mix(color, texture2D(texture3, tiled), blend.g); color = mix(color, texture2D(texture4, tiled), blend.b); + if (gameLightOn > 0.5) { + // TiledDetailDiffuse_4_PS: textures * diffuse map * 2 * (sun * N.L + ambient, clamped as a vertex color), * blend alpha + vec3 lit = clamp(gameLightColor * max(0.0, dot(normalize(vNormal), gameLightVec)) + gameAmbient, 0.0, 1.0); + gl_FragColor = vec4(min(color.rgb * tint * lit * blend.a, 1.0), 1.0); + return; + } gl_FragColor = vec4(min(color.rgb * tint, 1.0) * light * blend.a, 1.0); }`; @@ -345,7 +354,7 @@ function sceneTexture(base64, size) { * Returns the group, a function giving the ground height at (x, z), and setLook(look, layers): switch to one of * TERRAIN_LOOKS; the scene look needs the zone's terrain layers (/api/world3d/:zone/terrain_layers). */ -export function buildTerrainChunks(data, loadTexture, lightDirection) { +export function buildTerrainChunks(data, loadTexture, lightDirection, gameLights = null) { const group = new THREE.Group(); const grids = []; const materials = []; @@ -380,7 +389,12 @@ export function buildTerrainChunks(data, loadTexture, lightDirection) { texture3: { value: loadTexture(chunk.textures[2]) }, texture4: { value: loadTexture(chunk.textures[3]) }, blendMap: { value: bgraTexture(chunk.blend, chunk.blendSize) }, colorMap: { value: bgraTexture(chunk.color, chunk.colorSize) }, sceneMap: { value: null }, scenePalette: { value: null }, sceneSize: { value: 1 }, - lightDirection: { value: lightDirection }, look: { value: 0 } + lightDirection: { value: lightDirection }, look: { value: 0 }, + // Shared with the scenery, so the terrain follows its lighting (and its blends between scenes) + gameLightColor: gameLights ? gameLights.gameLightColor : { value: new THREE.Vector3() }, + gameAmbient: gameLights ? gameLights.gameAmbient : { value: new THREE.Vector3() }, + gameLightVec: gameLights ? gameLights.gameLightVec : { value: new THREE.Vector3(0, 1, 0) }, + gameLightOn: gameLights ? gameLights.gameLightOn : { value: 0 } } }); const mesh = new THREE.Mesh(geometry, material); @@ -772,7 +786,7 @@ export function createViewer(container, { onProgress, onSelect, onTick, brickUrl * The zone's whole terrain as the game draws it (null removes it): data from /api/properties/:id/terrain_chunks, * textures from textureUrl(id). */ - setTerrainChunks(data, textureUrl) { + setTerrainChunks(data, textureUrl, gameLights = null) { removeTerrain(); if (data && data.chunks && data.chunks.length) { const loader = new THREE.TextureLoader(); @@ -788,7 +802,7 @@ export function createViewer(container, { onProgress, onSelect, onTick, brickUrl return textures.get(id); }; const sunDirection = sun.position.clone().sub(sun.target.position).normalize(); - const built = buildTerrainChunks(data, loadTexture, sunDirection); + const built = buildTerrainChunks(data, loadTexture, sunDirection, gameLights); terrain = built.group; groundHeight = built.heightAt; scene.add(terrain); diff --git a/dDashboardServer/static/js/scenery.js b/dDashboardServer/static/js/scenery.js index 26bcb717d..fa35572df 100644 --- a/dDashboardServer/static/js/scenery.js +++ b/dDashboardServer/static/js/scenery.js @@ -184,8 +184,12 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress } // The zone's lights as the game's shaders get them (manifest.lighting), shared by every lit material - const gameLights = { gameLightColor: { value: new THREE.Vector3(1, 1, 1) }, gameAmbient: { value: new THREE.Vector3() }, gameLightVec: { value: new THREE.Vector3(0, 1, 0) } }; + const gameLights = { + gameLightColor: { value: new THREE.Vector3(1, 1, 1) }, gameAmbient: { value: new THREE.Vector3() }, gameLightVec: { value: new THREE.Vector3(0, 1, 0) }, + gameLightOn: { value: 0 } // 1 once a manifest brought the zone's lighting (the terrain uses its own light until then) + }; function setGameLights(lighting) { + gameLights.gameLightOn.value = lighting ? 1 : 0; if (!lighting) return; gameLights.gameLightColor.value.fromArray(lighting.light); gameLights.gameAmbient.value.fromArray(lighting.ambient); @@ -569,6 +573,8 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress sceneState() { return { mode: sceneMode, scene: focusScene, shown: shownScenes, scenes: manifest && manifest.scenes ? manifest.scenes : [] }; }, + /** The zone's lights as uniforms (updated in place, blends too), for the terrain to be lit like the scenery. */ + gameLights() { return gameLights; }, /** How many objects the loaded manifest places. */ count() { return manifest ? manifest.objects.asset.length : 0; }, stats() { diff --git a/dDashboardServer/static/js/world3d.js b/dDashboardServer/static/js/world3d.js index c48fe7e6f..81dc75c28 100644 --- a/dDashboardServer/static/js/world3d.js +++ b/dDashboardServer/static/js/world3d.js @@ -417,7 +417,7 @@ function showTerrain() { } return textures.get(id); }; - const built = buildTerrainChunks(terrainData, loadTexture, sun.position.clone().normalize()); + const built = buildTerrainChunks(terrainData, loadTexture, sun.position.clone().normalize(), scenery.gameLights()); // A flat plane far from every object is a placeholder the game never shows: leave it out, and don't put heat // cells or players on it const terrainBox = new THREE.Box3().setFromObject(built.group); diff --git a/dDashboardServer/templates/property-3d.jinja2 b/dDashboardServer/templates/property-3d.jinja2 index ff7c2e7a5..15ac8777c 100644 --- a/dDashboardServer/templates/property-3d.jinja2 +++ b/dDashboardServer/templates/property-3d.jinja2 @@ -357,7 +357,7 @@ async function showTerrain() { chunkTerrain = t && t.chunks ? t : null; } if (chunkTerrain) { - viewer.setTerrainChunks(chunkTerrain, (id) => assetApi + '/terrain_textures/' + id); + viewer.setTerrainChunks(chunkTerrain, (id) => assetApi + '/terrain_textures/' + id, scenery.gameLights()); return; } if (terrainData === undefined) {