Lighting
The engine provides a Blinn-Phong lighting pipeline supporting five light types. Lights are ECS components collected each frame into a GPU uniform buffer and consumed by the lit.hlsl shader.
Light types
SafiDirectionalLight
Infinite-distance parallel light (sunlight). Does not require a SafiTransform.
Usage:
SafiPointLight
Omni-directional point light (bulb). Position comes from SafiTransform.
Usage:
SafiSpotLight
Cone-shaped spotlight. Position and direction come from SafiTransform (forward = rotated -Z).
The cone is defined by cosine values: inner_angle is the cosine of the inner cone half-angle (full brightness), outer_angle is the cosine of the outer cone half-angle (falloff to zero). Light smoothly interpolates between the two via smoothstep.
SafiRectLight
Rectangular area light (panel). Position and orientation come from SafiTransform.
Uses a representative-point approximation: the fragment position is projected onto the rectangle's plane and clamped to bounds, then treated as a point light source. This produces soft, area-like falloff without Monte Carlo sampling.
SafiSkyLight
Uniform ambient environment light. Provides a base illumination so geometry is never fully black.
Only the first SafiSkyLight entity is used. It sets the ambient_color and ambient_intensity fields in the light buffer rather than occupying a light slot.
Architecture
Per-frame light collection
safi_light_buffer_collect() queries the ECS world for all light entities, reads their component data (and SafiTransform where applicable), and packs them into a SafiLightBuffer struct ready for GPU upload. Up to 16 lights are supported per frame (SAFI_MAX_LIGHTS).
GPU uniform layout
The light data is uploaded via SDL_PushGPUFragmentUniformData as two uniform buffers:
The vertex shader receives a single uniform buffer at b0, space1 containing the model matrix, MVP matrix, and normal matrix (SafiLitVSUniforms).
Each SafiGPULight is 64 bytes (4 × float4) containing position, direction, color, intensity, range, cone angles, rect dimensions, and a type discriminator.
Shading model
The lit.hlsl fragment shader implements Blinn-Phong:
- Diffuse:
max(0, dot(N, L)) - Specular:
pow(max(0, dot(N, H)), 32) × 0.5whereH = normalize(L + V) - Attenuation (point/spot/rect):
saturate(1 - (dist/range)²)² - Spot cone:
smoothstep(outer_angle, inner_angle, dot(-L, spot_dir)) - Ambient:
ambient_color × ambient_intensityadded after all light contributions
The architecture is designed for a future PBR upgrade: the SafiLightBuffer layout is shading-model-agnostic — only the shader needs to change.
API reference
Material
Creates a lit graphics pipeline. Same vertex layout as safi_material_create_unlit but with additional fragment uniform buffer slots for camera and light data. Uses back-face culling.
Model loading
Loads a glTF file with the lit pipeline. Internally calls safi_model_load() for geometry and textures, then swaps the unlit pipeline for a lit one.
Drawing
Draws all primitives with lit shading. Must be called inside an active render pass. Pushes vertex uniforms (model + MVP + normal matrix), camera buffer, and light buffer before issuing draw calls.
Normal matrix helper
Computes the inverse-transpose of the model matrix and stores it as a float4x4 for HLSL compatibility. Defined in light_buffer.h.
Inspector
All five light components are editable in the MicroUI debug UI inspector. Select a light entity in the Scene panel to view and modify its properties in real time.
Limits
Future work
- PBR (Cook-Torrance): Replace Blinn-Phong shader; light buffer unchanged
- Shadow mapping: Cascaded shadow maps for directional, cubemap for point, standard for spot
- Light baking: Lightmaps and irradiance probes for static lights
- Light culling: Per-tile or clustered culling for scenes exceeding 16 lights