feat(gameengine): add minimal BSP shaders so quake3 package can render

Quake 3 package was missing GPU shaders entirely. With these in place,
the engine boots quake3 successfully: pak0 loads, BSP parses, lightmap
atlas and geometry build, and shaders compile/bind correctly on the
Vulkan path.

bsp.vert: matches BspRenderVertex layout (position/uv/lmuv/normal) and
re-uses the existing VertexUniformData uniform block from the seed
pipeline so no C++ changes are needed.

bsp.frag: classic Q3 model — albedo × lightmap × overbright + ambient.
Skips PBR / shadow mapping entirely; Q3's baked lightmaps already encode
direct + bounce lighting and shadowing. Q3 shader-script effects
(animated sky, scrolling water, env-mapped chrome) render flat — adding
them is a follow-up.

SPIR-V only for now (Windows/Vulkan/D3D12 via SDL3 cross-compile). MSL
and DXIL still missing for Mac/native-D3D12 paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 15:07:29 +01:00
parent d9af1fb5bf
commit 2704e9d0e9
4 changed files with 89 additions and 0 deletions
@@ -0,0 +1,49 @@
#version 450
// Quake 3 BSP fragment shader (minimal).
// Q3 bakes radiosity into a lightmap atlas at compile time, so runtime lighting
// is just: albedo × lightmap × overbright. The classic Q3 "overbright bits = 1"
// doubles lightmap intensity — modern screens are brighter, so we expose it as
// part of the existing FragmentUniformData.material slot (z) with a 2.0 default.
//
// Fragment sampler bindings come from workflow_draw_map_step.cpp BSP path:
// set=2 binding=0 albedo (per-surface diffuse from extracted pk3 textures)
// set=2 binding=1 shadowMap (shadow map — ignored for BSP, lightmap is canonical)
// set=2 binding=2 lightmap (shared atlas built by bsp.lightmap_atlas)
layout(set = 2, binding = 0) uniform sampler2D albedoTex;
layout(set = 2, binding = 1) uniform sampler2D shadowMap;
layout(set = 2, binding = 2) uniform sampler2D lightmapTex;
layout(set = 3, binding = 0) uniform PBRUniforms {
vec4 u_lightDir;
vec4 u_lightColor; // a = exposure
vec4 u_ambient;
vec4 u_material; // z = lightmap overbright multiplier (defaults via C++)
vec4 u_flashPos;
vec4 u_flashDir;
vec4 u_flashColor;
};
layout(location = 0) in vec2 v_uv;
layout(location = 1) in vec2 v_lmuv;
layout(location = 2) in vec3 v_worldNormal;
layout(location = 3) in vec3 v_worldPos;
layout(location = 4) in vec3 v_cameraPos;
layout(location = 0) out vec4 o_color;
void main() {
vec3 albedo = texture(albedoTex, v_uv).rgb;
vec3 lightmap = texture(lightmapTex, v_lmuv).rgb;
// Q3 overbright. material.z falls back to 2.0 (engine pushes 0 today, so
// fall through to a sane default rather than rendering a black map).
float overbright = (u_material.z > 0.0) ? u_material.z : 2.0;
vec3 ambient = u_ambient.rgb * albedo;
vec3 lit = albedo * lightmap * overbright + ambient;
float exposure = (u_lightColor.a > 0.0) ? u_lightColor.a : 1.0;
o_color = vec4(lit * exposure, 1.0);
}
@@ -0,0 +1,40 @@
#version 450
// Quake 3 BSP vertex shader (minimal).
// Vertex format `position_uv_lmuv_normal` (BspRenderVertex):
// loc 0: vec3 position
// loc 1: vec2 diffuse uv
// loc 2: vec2 lightmap uv (already atlas-remapped by bsp.lightmap_atlas step)
// loc 3: vec3 world-space normal
// Uniform layout matches sdl3cpp::services::rendering::VertexUniformData
// so the same C++ uniform push call used by the seed pipeline works here.
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec2 a_uv;
layout(location = 2) in vec2 a_lmuv;
layout(location = 3) in vec3 a_normal;
layout(set = 1, binding = 0) uniform VertexUniforms {
mat4 u_modelViewProj;
mat4 u_model;
vec4 u_surfaceNormal; // unused for BSP — per-vertex normal wins
vec4 u_uvScale;
vec4 u_cameraPos;
mat4 u_shadowVP; // unused for BSP — lightmap already encodes shadowing
};
layout(location = 0) out vec2 v_uv;
layout(location = 1) out vec2 v_lmuv;
layout(location = 2) out vec3 v_worldNormal;
layout(location = 3) out vec3 v_worldPos;
layout(location = 4) out vec3 v_cameraPos;
void main() {
gl_Position = u_modelViewProj * vec4(a_position, 1.0);
v_uv = a_uv * u_uvScale.xy;
v_lmuv = a_lmuv;
vec4 wp = u_model * vec4(a_position, 1.0);
v_worldPos = wp.xyz;
v_worldNormal = mat3(u_model) * a_normal;
v_cameraPos = u_cameraPos.xyz;
}