Agent skill
forge-basic-lighting
Add Blinn-Phong lighting (ambient + diffuse + specular) to a 3D scene with world-space normals, light direction, and camera position uniforms. Use when someone needs to light a mesh, add shading, or implement basic real-time lighting in SDL3 GPU.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/forge-basic-lighting
SKILL.md
Basic Lighting — Blinn-Phong Shading
This skill teaches how to add Blinn-Phong lighting to a rendered 3D scene.
It builds on scene-loading (Lesson 09) and adds world-space lighting
calculations in the fragment shader.
When to use
- Adding lighting/shading to a 3D model or scene
- Implementing ambient, diffuse, or specular shading
- Passing light direction and camera position as shader uniforms
- Transforming normals from object space to world space
- Setting up a fragment shader with per-pixel lighting
Key API calls (ordered)
forge_gltf_load(path, &scene)— parse glTF (provides normals)- Upload vertex + index buffers to GPU (same as scene-loading skill)
SDL_CreateGPUGraphicsPipeline— 3 vertex attributes (pos, normal, uv)SDL_PushGPUVertexUniformData— push MVP matrix + Model matrix (128 bytes)SDL_PushGPUFragmentUniformData— push lighting params (64 bytes)SDL_DrawGPUIndexedPrimitives— draw with lighting
Uniform layouts
Vertex uniforms (128 bytes)
Both matrices must be column_major in HLSL to match C mat4 layout.
typedef struct VertUniforms {
mat4 mvp; /* combined Model-View-Projection for clip space */
mat4 model; /* model (world) matrix for lighting calculations */
} VertUniforms;
cbuffer VertUniforms : register(b0, space1)
{
column_major float4x4 mvp;
column_major float4x4 model;
};
Fragment uniforms (64 bytes)
Use float4 (not float3) for vectors to avoid HLSL cbuffer packing issues.
typedef struct FragUniforms {
float base_color[4]; /* material color (RGBA) */
float light_dir[4]; /* normalized, toward light (xyz) */
float eye_pos[4]; /* camera world position (xyz) */
Uint32 has_texture; /* 0 = solid color, 1 = sample tex */
float shininess; /* specular exponent (32, 64, 128...) */
float ambient; /* ambient intensity [0..1] */
float specular_str; /* specular intensity [0..1] */
} FragUniforms;
cbuffer FragUniforms : register(b0, space3)
{
float4 base_color;
float4 light_dir;
float4 eye_pos;
uint has_texture;
float shininess;
float ambient;
float specular_str;
};
Code template
Vertex shader — normal transformation (adjugate transpose)
/* World-space position for view direction calculation */
float4 wp = mul(model, float4(input.position, 1.0));
output.world_pos = wp.xyz;
/* Transform normal by the ADJUGATE TRANSPOSE of the model matrix's
* upper-left 3x3. Unlike (float3x3)model, this preserves
* perpendicularity even under non-uniform scale. The rows of the
* adjugate transpose are cross products of pairs of model matrix rows.
* Do NOT normalize here — the rasterizer will interpolate, and we
* normalize per-pixel in the fragment shader. */
float3x3 m = (float3x3)model;
float3x3 adj_t;
adj_t[0] = cross(m[1], m[2]);
adj_t[1] = cross(m[2], m[0]);
adj_t[2] = cross(m[0], m[1]);
output.world_norm = mul(adj_t, input.normal);
Fragment shader — Blinn-Phong
/* MUST normalize after interpolation — interpolated normals aren't unit length */
float3 N = normalize(input.world_norm);
float3 L = normalize(light_dir.xyz); /* toward light */
float3 V = normalize(eye_pos.xyz - input.world_pos); /* toward camera */
/* Ambient: constant minimum brightness */
float3 ambient_term = ambient * surface_color.rgb;
/* Diffuse: Lambert's cosine law */
float NdotL = max(dot(N, L), 0.0);
float3 diffuse_term = NdotL * surface_color.rgb;
/* Specular: Blinn half-vector */
float3 H = normalize(L + V);
float NdotH = max(dot(N, H), 0.0);
float3 specular_term = specular_str * pow(NdotH, shininess) * float3(1, 1, 1);
float3 final = ambient_term + diffuse_term + specular_term;
C side — pushing lighting uniforms
/* Vertex: MVP + model matrix */
VertUniforms vu;
vu.mvp = mat4_multiply(vp, node->world_transform);
vu.model = node->world_transform;
SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu));
/* Fragment: material + lighting */
FragUniforms fu;
fu.base_color[0] = mat->base_color[0]; /* ... etc */
fu.light_dir[0] = light_dir.x;
fu.light_dir[1] = light_dir.y;
fu.light_dir[2] = light_dir.z;
fu.light_dir[3] = 0.0f;
fu.eye_pos[0] = cam_pos.x;
fu.eye_pos[1] = cam_pos.y;
fu.eye_pos[2] = cam_pos.z;
fu.eye_pos[3] = 0.0f;
fu.shininess = 64.0f;
fu.ambient = 0.15f;
fu.specular_str = 0.5f;
SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu));
Common mistakes
-
Forgetting to normalize interpolated normals — The rasterizer interpolates vertex shader outputs linearly. Even if every vertex normal is unit length, the interpolated result won't be. Always
normalize()in the fragment shader. -
Missing
column_majoron both matrices — If you add the model matrix but forgetcolumn_major, the multiplication will be wrong. Bothmvpandmodelmust have it. -
Using
float3in the cbuffer — HLSL packsfloat3to 16 bytes with 4 bytes of padding, which silently misaligns subsequent fields. Usefloat4and explicitly pad the w component to 0. -
Light direction convention — Our convention: light_dir points FROM the surface TOWARD the light. Some tutorials use the opposite. If your model looks like the dark side is lit, negate the direction.
-
Normalizing in the vertex shader — Don't normalize normals in the vertex shader. The rasterizer will interpolate them anyway, making the normalization pointless. Save it for the fragment shader.
-
Normal transformation with non-uniform scale —
(float3x3)modelonly works correctly for rotation + uniform scale. Always use the adjugate transpose instead — three cross products of the matrix rows:adj_t[0] = cross(m[1], m[2])etc. This is correct for ALL matrices (including singular ones) and cheaper than inverse-transpose.
Typical parameter values
| Parameter | Value | Effect |
|---|---|---|
| shininess | 8-16 | Rough, matte surface |
| shininess | 32-64 | Typical plastic / painted |
| shininess | 128-256 | Polished, metallic |
| ambient | 0.05-0.15 | Subtle fill light |
| ambient | 0.2-0.3 | Bright ambient (indoor) |
| specular_str | 0.3-0.5 | Moderate shine |
| specular_str | 0.8-1.0 | Very shiny / wet |
References
- GPU Lesson 10 — Basic Lighting (full implementation)
- Math Lesson 01 — Vectors (dot product, normalize)
- Math Lesson 02 — Coordinate Spaces (object, world, view)
- Math Lesson 05 — Matrices (model matrix transformation)
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?