Skip to main content

forge-gobo-spotlight

Add a projected-texture (gobo/cookie) spotlight with cone falloff, shadow map, and pattern projection to an SDL GPU project

Zur Installation springen

Quellinformationen

Repository
Nebulavenus/forge-gpu
Letzte Quellaktivität
22. März 2026 um 03:50
Erkannte Sprache von SKILL.md
Englisch
Sterne
38
Forks
7

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
forge-gobo-spotlight
description
Add a projected-texture (gobo/cookie) spotlight with cone falloff, shadow map, and pattern projection to an SDL GPU project
Add a spotlight with inner/outer cone angles, projected gobo texture, and a single 2D shadow map. The spotlight's view-projection matrix serves triple duty — shadow mapping, gobo UV projection, and cone masking. Based on [GPU Lesson 24](../../../lessons/gpu/24-gobo-spotlight/). ## When to use - You need a focused light source with a defined cone (not omnidirectional) - You want to project a pattern (window, foliage, abstract) through a light - You need shadows from a spotlight (simpler than point light cube maps) - You are building theatrical, cinematic, or stage-style lighting - You already have an HDR rendering pipeline and Blinn-Phong lighting ## Key API calls - `SDL_CreateGPUTexture` — shadow depth map (D32_FLOAT, 2D, `SAMPLER | DEPTH_STENCIL_TARGET`) - `SDL_CreateGPUTexture` — gobo texture (R8G8B8A8_UNORM, 2D, `SAMPLER`) - `SDL_CreateGPUSampler` — NEAREST + CLAMP for shadow, LINEAR + CLAMP for gobo - `SDL_CreateGPUGraphicsPipeline` — shadow pipeline (depth-only, no color targets) - `SDL_BeginGPURenderPass` — shadow pass with depth-only attachment - `SDL_BindGPUFragmentSamplers` — bind diffuse + shadow + gobo textures in scene pass ## Correct order 1. **Define spotlight parameters** — position, target, inner/outer cone angles, color, intensity 2. **Compute light view-projection** — `mat4_look_at(pos, target, up)` + `mat4_perspective(2 * outer_angle, 1.0, near, far)` 3. **Create shadow depth texture** — D32_FLOAT, same resolution for shadow map (e.g. 1024x1024) 4. **Create shadow sampler** — NEAREST filter, CLAMP_TO_EDGE 5. **Load gobo texture** — grayscale PNG as UNORM (not sRGB), LINEAR sampler, CLAMP_TO_EDGE 6. **Create shadow pipeline** — vertex shader transforms by light_mvp, empty fragment shader, depth-only 7. **Create scene pipeline** — increase `num_samplers` to include shadow + gobo (e.g. 3 total) 8. **Each frame:** a. Shadow pass: render shadow casters from spotlight's perspective (depth-only) b. Scene pass: bind shadow map + gobo texture, fragment shader applies cone + gobo + shadow ## Key concepts 1. **Spotlight cone**: `smoothstep(cos_outer, cos_inner, cos_angle)` — cosine decreases with angle, so outer < inner 2. **Gobo projection**: light_vp transforms world pos → clip → NDC → UV for gobo texture sampling 3. **Triple-duty matrix**: the same light_vp handles shadow mapping, gobo UV mapping, and cone bounds 4. **Single 2D shadow map**: a spotlight's perspective frustum captures everything in one pass (unlike cube maps for point lights) 5. **UNORM gobo texture**: the gobo is a linear attenuation mask, not a color — use UNORM to avoid sRGB gamma 6. **Cone masking via UV bounds**: fragments projecting outside [0,1] UV get zero light via `step()` functions ## Common mistakes - **Using sRGB format for the gobo texture** — the gobo is a light attenuation mask sampled linearly. sRGB applies an unwanted gamma curve. Use `R8G8B8A8_UNORM`. - **Wrong smoothstep parameter order** — cosine is decreasing, so `smoothstep(cos_outer, cos_inner, ...)` with outer < inner. Swapping them inverts the cone. - **Forgetting the Y-flip in UV remapping** — texture V increases downward; NDC Y increases upward. Always flip: `gobo_uv.y = 1.0 - gobo_uv.y`. - **Shadow acne from insufficient bias** — add a small constant bias (0.002) when comparing depths in the shadow test. - **Including the light source model as a shadow caster** — the searchlight/lamp model sits at the light position and would block its own light. Exclude it from the shadow pass. - **FOV not matching outer cone angle** — the shadow/projection FOV must be `2 * outer_angle` to cover the full spotlight cone. Too narrow clips shadows; too wide wastes resolution. ## Spotlight definition ```c #define SPOT_INNER_DEG 20.0f /* full-intensity inner cone half-angle */ #define SPOT_OUTER_DEG 30.0f /* falloff-to-zero outer cone half-angle */ #define SPOT_INTENSITY 5.0f /* HDR brightness */ #define SPOT_NEAR 0.5f #define SPOT_FAR 30.0f #define SHADOW_MAP_SIZE 1024 /* Compute spotlight view-projection (static light). */ vec3 spot_pos = vec3_create(6.0f, 5.0f, 4.0f); vec3 spot_target = vec3_create(0.0f, 0.0f, 0.0f); vec3 spot_up = vec3_create(0.0f, 1.0f, 0.0f); mat4 light_view = mat4_look_at(spot_pos, spot_target, spot_up); float outer_rad = SPOT_OUTER_DEG * FORGE_DEG2RAD; mat4 light_proj = mat4_perspective(2.0f * outer_rad, 1.0f, SPOT_NEAR, SPOT_FAR); mat4 light_vp = mat4_multiply(light_proj, light_view); vec3 spot_dir = vec3_normalize(vec3_sub(spot_target, spot_pos)); ``` ## Shadow depth texture ```c SDL_GPUTextureCreateInfo ti; SDL_zero(ti); ti.type = SDL_GPU_TEXTURETYPE_2D; ti.format = SDL_GPU_TEXTUREFORMAT_D32_FLOAT; ti.width = SHADOW_MAP_SIZE; ti.height = SHADOW_MAP_SIZE; ti.layer_count_or_depth = 1; ti.num_levels = 1; ti.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET; SDL_GPUTexture *shadow_depth = SDL_CreateGPUTexture(device, &ti); ``` ## Gobo texture loading ```c /* Use UNORM (not sRGB) — the gobo is a linear attenuation mask. */ SDL_GPUTextureCreateInfo tex_info; SDL_zero(tex_info); tex_info.type = SDL_GPU_TEXTURETYPE_2D; tex_info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; tex_info.width = w; tex_info.height = h; tex_info.layer_count_or_depth = 1; tex_info.num_levels = 1; tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER; /* Gobo sampler — linear filtering for smooth projection, clamp to edge. */ SDL_GPUSamplerCreateInfo si; SDL_zero(si); si.min_filter = SDL_GPU_FILTER_LINEAR; si.mag_filter = SDL_GPU_FILTER_LINEAR; si.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; si.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; si.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; ``` ## Fragment shader — spotlight with gobo and shadow ```hlsl /* Direction from spotlight to fragment. */ float3 to_frag = world_pos - spot_pos; float dist = length(to_frag); float3 L_frag = to_frag / dist; /* Cone falloff. */ float cos_angle = dot(L_frag, normalize(spot_dir)); float cone = smoothstep(cos_outer, cos_inner, cos_angle); if (cone > 0.0) { /* Transform fragment into spotlight clip space. */ float4 light_clip = mul(light_vp, float4(world_pos, 1.0)); float3 light_ndc = light_clip.xyz / light_clip.w; /* Gobo texture projection — remap NDC to UV space. */ float2 gobo_uv = light_ndc.xy * 0.5 + 0.5; gobo_uv.y = 1.0 - gobo_uv.y; /* Cone masking — clamp to spotlight bounds. */ float in_bounds = step(0.0, gobo_uv.x) * step(gobo_uv.x, 1.0) * step(0.0, gobo_uv.y) * step(gobo_uv.y, 1.0); /* Sample gobo pattern (grayscale). */ float gobo = gobo_tex.Sample(gobo_smp, gobo_uv).r; /* Shadow test with PCF. */ float shadow = sample_shadow(light_ndc, texel_size); /* Blinn-Phong from spotlight. */ float3 L = normalize(spot_pos - world_pos); float NdotL = max(dot(N, L), 0.0); float3 H = normalize(L + V); float NdotH = max(dot(N, H), 0.0); /* Quadratic attenuation. */ float atten = 1.0 / (1.0 + 0.09 * dist + 0.032 * dist * dist); total_light += (albedo * NdotL + spec) * cone * gobo * shadow * in_bounds * atten * spot_intensity * spot_color; } ``` ## Shadow pass — depth-only render ```c /* Shadow pipeline: no color targets, depth-only. */ SDL_GPUDepthStencilTargetInfo shadow_depth_info; SDL_zero(shadow_depth_info); shadow_depth_info.texture = shadow_depth_texture; shadow_depth_info.load_op = SDL_GPU_LOADOP_CLEAR; shadow_depth_info.store_op = SDL_GPU_STOREOP_STORE; shadow_depth_info.clear_depth = 1.0f; SDL_GPURenderPass *shadow_pass = SDL_BeginGPURenderPass( cmd, NULL, 0, &shadow_depth_info); SDL_BindGPUGraphicsPipeline(shadow_pass, shadow_pipeline); /* Draw shadow casters (exclude the light source model). */ draw_model_shadow(shadow_pass, cmd, &truck, &truck_mat, &light_vp); for (int i = 0; i < BOX_COUNT; i++) { draw_model_shadow(shadow_pass, cmd, &box, &box_mat[i], &light_vp); } SDL_EndGPURenderPass(shadow_pass); ``` ## Scene pass texture bindings ```c /* Bind 3 samplers: diffuse, shadow depth, gobo pattern. */ SDL_GPUTextureSamplerBinding tex_binds[3]; tex_binds[0] = (SDL_GPUTextureSamplerBinding){ .texture = diffuse_tex, .sampler = sampler }; tex_binds[1] = (SDL_GPUTextureSamplerBinding){ .texture = shadow_depth_texture, .sampler = shadow_sampler }; tex_binds[2] = (SDL_GPUTextureSamplerBinding){ .texture = gobo_texture, .sampler = gobo_sampler }; SDL_BindGPUFragmentSamplers(pass, 0, tex_binds, 3); ```
Auf GitHub ansehen