perf(rendering): true DrawElementsInstanced — one draw call per (GfxObj × sub-mesh)

Replaces the per-entity glUniform uModel path with a shared instance VBO and
DrawElementsInstanced. All instance model matrices are uploaded to GPU once per
frame; the VAO's per-instance attribute pointers (locations 3–6, divisor=1) are
updated with a byte-offset re-point per group so a single VBO serves all groups
without requiring DrawElementsInstancedBaseInstance (not in Silk.NET 2.23).

Changes:
- InstancedMeshRenderer: add _instanceVbo, _instanceBuffer scratch; EnsureUploaded
  sets up mat4 instance attrs (locs 3–6) from the shared VBO; Draw builds the flat
  float[] of all instance matrices once then calls DrawElementsInstanced per sub-mesh.
  Drops the unused uint TerrainLayer attribute (loc 3 from vertex VBO) — mesh shaders
  never used it. Adds InstanceGroup helper to track per-group buffer offsets.
- mesh_instanced.frag: replace sampler2DArray+uTextureLayer with sampler2D uDiffuse,
  matching the existing TextureCache / individual-texture pipeline.
- mesh_instanced.vert+frag: track as committed files (were untracked).
- Shader.cs: add SetVec3 helper needed for uLightDirection uniform.
- GameWindow.cs: switch mesh shader load from mesh.vert/.frag to
  mesh_instanced.vert/.frag.

Visual output is identical: same entities, same textures, same lighting constants
(SUN_DIR=(0.5,0.4,0.6), AMBIENT=0.25, DIFFUSE=0.75 — moved from frag to vert).
Build: clean. Tests: 431/431 green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-13 18:51:49 +02:00
parent b5099e2b21
commit 6a55838a10
5 changed files with 288 additions and 109 deletions

View file

@ -0,0 +1,31 @@
#version 430 core
in vec2 vTex;
in vec3 vWorldNormal;
in float vLightingFactor;
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;
void main() {
vec4 color = texture(uDiffuse, vTex);
// Alpha cutout only for clip-map surfaces (doors, windows, vegetation).
// Blended surface types must NOT discard here — that kills every
// semi-transparent pixel before the blend stage runs.
if (uTranslucencyKind == 1 && color.a < 0.5) discard;
// Apply pre-computed Lambert + ambient lighting factor from the vertex shader.
fragColor = vec4(color.rgb * vLightingFactor, color.a);
}

View file

@ -0,0 +1,46 @@
#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;
uniform vec3 uLightDirection; // world-space sun direction (points toward sun)
uniform float uAmbientIntensity;
uniform float uDiffuseIntensity;
out vec2 vTex;
out vec3 vWorldNormal;
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).
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;
}