From 31d3a4678f87beb01a2b81505b93c538f8814619 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 13 Apr 2026 22:01:28 +0200 Subject: [PATCH] fix(lighting): port ACME lighting constants replacing guessed values Replace guessed sun direction (0.5, 0.4, 0.6) with ACME's verified value (0.5, 0.3, -0.3) from GameScene.cs:238. Replace hardcoded ambient/diffuse (0.25/0.75) with ACME's ambient intensity 0.45 from LandscapeEditorSettings.cs:108. Terrain shaders now match ACME Landscape.vert/frag pattern: - Vertex shader computes Lambert term with xLightDirection uniform - Fragment shader applies: color * (clamp(lambert, 0, 1) + xAmbient) Static object shader matches ACME StaticObject.vert: - LightingFactor = max(dot(N, -L), 0) + ambient - Removed separate uDiffuseIntensity (ACME doesn't have one) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Rendering/InstancedMeshRenderer.cs | 13 +++++++------ .../Rendering/Shaders/mesh_instanced.vert | 16 +++++----------- src/AcDream.App/Rendering/Shaders/terrain.frag | 16 ++++++---------- src/AcDream.App/Rendering/Shaders/terrain.vert | 6 ++++++ .../Rendering/TerrainChunkRenderer.cs | 7 +++++++ 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs index f21cf57..30e346b 100644 --- a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs +++ b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs @@ -142,12 +142,13 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable var vp = camera.View * camera.Projection; _shader.SetMatrix4("uViewProjection", vp); - // Lighting uniforms — match the constants from mesh.frag so the visual - // output is identical to the non-instanced path. - var sunDir = Vector3.Normalize(new Vector3(0.5f, 0.4f, 0.6f)); - _shader.SetVec3("uLightDirection", sunDir); - _shader.SetFloat("uAmbientIntensity", 0.25f); - _shader.SetFloat("uDiffuseIntensity", 0.75f); + // Lighting uniforms matching ACME StaticObject.vert: + // LightingFactor = max(dot(Normal, -uLightDirection), 0.0) + uAmbientIntensity + // LightDirection (0.5, 0.3, -0.3) from ACME GameScene.cs:238. + // AmbientLightIntensity 0.45 from ACME LandscapeEditorSettings.cs:108. + var lightDir = Vector3.Normalize(new Vector3(0.5f, 0.3f, -0.3f)); + _shader.SetVec3("uLightDirection", lightDir); + _shader.SetFloat("uAmbientIntensity", 0.45f); // ── Collect and group instances ─────────────────────────────────────── CollectGroups(landblockEntries, frustum, neverCullLandblockId); diff --git a/src/AcDream.App/Rendering/Shaders/mesh_instanced.vert b/src/AcDream.App/Rendering/Shaders/mesh_instanced.vert index 9e551e4..e9c6896 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh_instanced.vert +++ b/src/AcDream.App/Rendering/Shaders/mesh_instanced.vert @@ -16,9 +16,8 @@ layout(location = 5) in vec4 aInstanceRow2; layout(location = 6) in vec4 aInstanceRow3; uniform mat4 uViewProjection; -uniform vec3 uLightDirection; // world-space sun direction (points toward sun) +uniform vec3 uLightDirection; // world-space light direction (points FROM sun, matching ACME) uniform float uAmbientIntensity; -uniform float uDiffuseIntensity; out vec2 vTex; out vec3 vWorldNormal; @@ -26,21 +25,16 @@ out float vLightingFactor; void main() { // Reconstruct the per-instance model matrix from its four row vectors. - // Column-major storage: OpenGL/GLSL mat4 columns are constructed from - // the rows we receive from the attribute buffer. mat4 model = mat4(aInstanceRow0, aInstanceRow1, aInstanceRow2, aInstanceRow3); vec4 worldPos = model * vec4(aPosition, 1.0); gl_Position = uViewProjection * worldPos; - // Transform normal into world space. For uniform-scale transforms the - // upper-left 3x3 is sufficient; non-uniform scale would require the - // inverse transpose, accepted as a future-phase concern (same as mesh.vert). + // Transform normal into world space. vWorldNormal = normalize(mat3(model) * aNormal); vTex = aTexCoord; - // Compute Lambert diffuse + ambient in the vertex shader so the fragment - // shader only needs a multiply. Matches ACME StaticObject.vert pattern. - float ndotl = max(dot(vWorldNormal, uLightDirection), 0.0); - vLightingFactor = uAmbientIntensity + uDiffuseIntensity * ndotl; + // Lambert + ambient matching ACME StaticObject.vert: + // LightingFactor = max(dot(Normal, -uLightDirection), 0.0) + uAmbientIntensity; + vLightingFactor = max(dot(vWorldNormal, -uLightDirection), 0.0) + uAmbientIntensity; } diff --git a/src/AcDream.App/Rendering/Shaders/terrain.frag b/src/AcDream.App/Rendering/Shaders/terrain.frag index d8b3ff9..f615929 100644 --- a/src/AcDream.App/Rendering/Shaders/terrain.frag +++ b/src/AcDream.App/Rendering/Shaders/terrain.frag @@ -6,6 +6,7 @@ in vec2 vBaseUV; in vec3 vWorldNormal; +in float vLightingFactor; in vec4 vOverlay0; in vec4 vOverlay1; in vec4 vOverlay2; @@ -17,11 +18,7 @@ out vec4 fragColor; uniform sampler2DArray uTerrain; // 33+ layers — TerrainAtlas.GlTexture uniform sampler2DArray uAlpha; // 8+ layers — TerrainAtlas.GlAlphaTexture - -// Phase 3a lighting (in sync with mesh.frag — update both together). -const vec3 SUN_DIR = normalize(vec3(0.5, 0.4, 0.6)); -const float AMBIENT = 0.25; -const float DIFFUSE = 0.75; +uniform float xAmbient; // ambient intensity (matching ACME Landscape.frag) // Per-texture tiling repeat count across a cell. WorldBuilder uses // uTexTiling[36] uploaded from the dats; we default to 1.0 (one tile per @@ -118,10 +115,9 @@ void main() { vec3 roadMasked = roads.rgb * roads.a; vec3 rgb = clamp(baseMasked + ovlMasked + roadMasked, 0.0, 1.0); - // Phase 3a/3b directional lighting (in sync with mesh.frag constants). - vec3 N = normalize(vWorldNormal); - float ndotl = max(dot(N, SUN_DIR), 0.0); - float lighting = AMBIENT + DIFFUSE * ndotl; + // Lighting matching ACME Landscape.frag: + // litColor = finalColor * (saturate(vLightingFactor) + xAmbient); + vec3 litColor = rgb * (clamp(vLightingFactor, 0.0, 1.0) + xAmbient); - fragColor = vec4(rgb * lighting, 1.0); + fragColor = vec4(litColor, 1.0); } diff --git a/src/AcDream.App/Rendering/Shaders/terrain.vert b/src/AcDream.App/Rendering/Shaders/terrain.vert index b92986a..c72295d 100644 --- a/src/AcDream.App/Rendering/Shaders/terrain.vert +++ b/src/AcDream.App/Rendering/Shaders/terrain.vert @@ -8,9 +8,11 @@ layout(location = 5) in uvec4 aPacked3; // bits: rot fields + splitDir (see bel uniform mat4 uView; uniform mat4 uProjection; +uniform vec3 xLightDirection; // world-space sun direction (matching ACME Landscape.vert) out vec2 vBaseUV; out vec3 vWorldNormal; +out float vLightingFactor; // Per-layer "UV.xy in cell-local 0..1 space, tex index .z, alpha index .w". // Negative .z means "layer not present, skip it in the fragment shader." out vec4 vOverlay0; @@ -91,6 +93,10 @@ void main() { // Vertices are baked in world space; normals need no model transform. vWorldNormal = normalize(aNormal); + // Lambert diffuse term matching ACME Landscape.vert: + // vLightingFactor = max(0.0, dot(vNormal, -normalize(xLightDirection))); + vLightingFactor = max(0.0, dot(vWorldNormal, -normalize(xLightDirection))); + float baseTex = float(aPacked0.x); if (baseTex >= 254.0) baseTex = -1.0; vBaseTexIdx = baseTex; diff --git a/src/AcDream.App/Rendering/TerrainChunkRenderer.cs b/src/AcDream.App/Rendering/TerrainChunkRenderer.cs index 026005d..ba619d9 100644 --- a/src/AcDream.App/Rendering/TerrainChunkRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainChunkRenderer.cs @@ -218,6 +218,13 @@ public sealed unsafe class TerrainChunkRenderer : IDisposable _shader.SetMatrix4("uView", camera.View); _shader.SetMatrix4("uProjection", camera.Projection); + // Lighting uniforms matching ACME Landscape.vert/frag. + // LightDirection (0.5, 0.3, -0.3) from ACME GameScene.cs:238. + // AmbientLightIntensity 0.45 from ACME LandscapeEditorSettings.cs:108. + var lightDir = Vector3.Normalize(new Vector3(0.5f, 0.3f, -0.3f)); + _shader.SetVec3("xLightDirection", lightDir); + _shader.SetFloat("xAmbient", 0.45f); + // Terrain atlas on unit 0, alpha atlas on unit 1. _gl.ActiveTexture(TextureUnit.Texture0); _gl.BindTexture(TextureTarget.Texture2DArray, _atlas.GlTexture);