diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index cf8404c..a6e2c1a 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1447,9 +1447,11 @@ public sealed class GameWindow : IDisposable } } - // N.5 Task 10: load mesh_modern when both extensions are present; - // fall back to mesh_instanced otherwise. Must be after capability - // detection so _bindlessSupport is known. + // N.5 Task 10/15: load mesh_modern when both extensions are present. + // If bindless is missing _meshShader stays null, _wbDrawDispatcher won't + // be constructed (its guard requires _bindlessSupport non-null), and + // rendering falls back to InstancedMeshRenderer — but only when + // _meshShader is non-null (see _staticMesh construction below). if (_bindlessSupport is not null) { _meshShader = new Shader(_gl, @@ -1457,12 +1459,7 @@ public sealed class GameWindow : IDisposable Path.Combine(shadersDir, "mesh_modern.frag")); Console.WriteLine("[N.5] mesh_modern shader loaded"); } - else - { - _meshShader = new Shader(_gl, - Path.Combine(shadersDir, "mesh_instanced.vert"), - Path.Combine(shadersDir, "mesh_instanced.frag")); - } + // else: bindless missing — _meshShader stays null. _textureCache = new TextureCache(_gl, _dats, _bindlessSupport); // Two persistent GL sampler objects (Repeat + ClampToEdge) so @@ -1538,14 +1535,21 @@ public sealed class GameWindow : IDisposable _worldState = new AcDream.App.Streaming.GpuWorldState(wbSpawnAdapter, wbEntitySpawnAdapter); } - _staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache, _wbMeshAdapter); + // Task 15: _meshShader is null when bindless is missing; skip constructing + // _staticMesh in that case. All downstream _staticMesh usages are already + // null-safe (null-conditional operators or explicit null guards). + if (_meshShader is not null && _textureCache is not null) + _staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache, _wbMeshAdapter); if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled && _wbMeshAdapter is not null && _wbEntitySpawnAdapter is not null && _bindlessSupport is not null) { + // _meshShader is non-null here: the _bindlessSupport guard implies + // the if(_bindlessSupport is not null) block above ran and assigned it. + // _textureCache is always non-null (assigned unconditionally above). _wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher( - _gl, _meshShader, _textureCache, _wbMeshAdapter, _wbEntitySpawnAdapter, _bindlessSupport); + _gl, _meshShader!, _textureCache!, _wbMeshAdapter, _wbEntitySpawnAdapter, _bindlessSupport); } // Phase G.1 sky renderer — its own shader (sky.vert / sky.frag) @@ -1555,7 +1559,7 @@ public sealed class GameWindow : IDisposable Path.Combine(shadersDir, "sky.vert"), Path.Combine(shadersDir, "sky.frag")); _skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer( - _gl, _dats, skyShader, _textureCache, _samplerCache); + _gl, _dats, skyShader, _textureCache!, _samplerCache); // Phase G.1 particle renderer — renders rain / snow / spell auras // spawned into the shared ParticleSystem as billboard quads. diff --git a/src/AcDream.App/Rendering/Shaders/mesh_instanced.frag b/src/AcDream.App/Rendering/Shaders/mesh_instanced.frag deleted file mode 100644 index 1719e2f..0000000 --- a/src/AcDream.App/Rendering/Shaders/mesh_instanced.frag +++ /dev/null @@ -1,98 +0,0 @@ -#version 430 core - -in vec2 vTex; -in vec3 vWorldNormal; -in vec3 vWorldPos; - -out vec4 fragColor; - -// One 2D texture per draw call — same binding point as mesh.frag so the -// C# side can use the same TextureCache without a texture-array pipeline. -uniform sampler2D uDiffuse; - -// Translucency kind — matches TranslucencyKind C# enum (same as mesh.frag): -// 0 = Opaque — depth write+test, no blend; shader never discards -// 1 = ClipMap — alpha-key discard at 0.5 (doors, windows, vegetation) -// 2 = AlphaBlend — GL blending handles compositing; do NOT discard -// 3 = Additive — GL additive blending; do NOT discard -// 4 = InvAlpha — GL inverted-alpha blending; do NOT discard -uniform int uTranslucencyKind; - -// Phase G.1+G.2: shared scene-lighting UBO (see mesh.frag for layout docs). -struct Light { - vec4 posAndKind; - vec4 dirAndRange; - vec4 colorAndIntensity; - vec4 coneAngleEtc; -}; -layout(std140, binding = 1) uniform SceneLighting { - Light uLights[8]; - vec4 uCellAmbient; - vec4 uFogParams; - vec4 uFogColor; - vec4 uCameraAndTime; -}; - -vec3 accumulateLights(vec3 N, vec3 worldPos) { - vec3 lit = uCellAmbient.xyz; - int activeLights = int(uCellAmbient.w); - for (int i = 0; i < 8; ++i) { - if (i >= activeLights) break; - - int kind = int(uLights[i].posAndKind.w); - vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w; - - if (kind == 0) { - vec3 Ldir = -uLights[i].dirAndRange.xyz; - float ndl = max(0.0, dot(N, Ldir)); - lit += Lcol * ndl; - } else { - vec3 toL = uLights[i].posAndKind.xyz - worldPos; - float d = length(toL); - float range = uLights[i].dirAndRange.w; - if (d < range && range > 1e-3) { - vec3 Ldir = toL / max(d, 1e-4); - float ndl = max(0.0, dot(N, Ldir)); - float atten = 1.0; - if (kind == 2) { - float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5); - float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz); - atten *= (cos_l > cos_edge) ? 1.0 : 0.0; - } - lit += Lcol * ndl * atten; - } - } - } - return lit; -} - -vec3 applyFog(vec3 lit, vec3 worldPos) { - int mode = int(uFogParams.w); - if (mode == 0) return lit; - float d = length(worldPos - uCameraAndTime.xyz); - float fogStart = uFogParams.x; - float fogEnd = uFogParams.y; - float span = max(1e-3, fogEnd - fogStart); - float fog = clamp((d - fogStart) / span, 0.0, 1.0); - return mix(lit, uFogColor.xyz, fog); -} - -void main() { - vec4 color = texture(uDiffuse, vTex); - - // Alpha cutout only for clip-map surfaces (doors, windows, vegetation). - if (uTranslucencyKind == 1 && color.a < 0.5) discard; - - vec3 N = normalize(vWorldNormal); - vec3 lit = accumulateLights(N, vWorldPos); - - // Lightning flash — additive scene bump. - lit += uFogParams.z * vec3(0.6, 0.6, 0.75); - - // Retail clamp per-channel to 1.0 (r13 §13.1). - lit = min(lit, vec3(1.0)); - - vec3 rgb = color.rgb * lit; - rgb = applyFog(rgb, vWorldPos); - fragColor = vec4(rgb, color.a); -} diff --git a/src/AcDream.App/Rendering/Shaders/mesh_instanced.vert b/src/AcDream.App/Rendering/Shaders/mesh_instanced.vert deleted file mode 100644 index a2f3893..0000000 --- a/src/AcDream.App/Rendering/Shaders/mesh_instanced.vert +++ /dev/null @@ -1,35 +0,0 @@ -#version 430 core - -// Per-vertex attributes -layout(location = 0) in vec3 aPosition; -layout(location = 1) in vec3 aNormal; -layout(location = 2) in vec2 aTexCoord; - -// Per-instance model matrix, split across four vec4 attribute slots. -// A mat4 consumes 4 consecutive attribute locations, so locations 3-6 are -// all occupied by this single logical matrix. The C# side must call -// VertexAttribPointer four times (one per row) and VertexAttribDivisor(loc, 1) -// on each of the four slots. -layout(location = 3) in vec4 aInstanceRow0; -layout(location = 4) in vec4 aInstanceRow1; -layout(location = 5) in vec4 aInstanceRow2; -layout(location = 6) in vec4 aInstanceRow3; - -uniform mat4 uViewProjection; - -out vec2 vTex; -out vec3 vWorldNormal; -out vec3 vWorldPos; - -void main() { - // Reconstruct the per-instance model matrix from its four row vectors. - mat4 model = mat4(aInstanceRow0, aInstanceRow1, aInstanceRow2, aInstanceRow3); - - vec4 worldPos = model * vec4(aPosition, 1.0); - gl_Position = uViewProjection * worldPos; - - vWorldPos = worldPos.xyz; - // Transform normal into world space. - vWorldNormal = normalize(mat3(model) * aNormal); - vTex = aTexCoord; -}