Adding Per‑Pixel Normal Maps to Source Engine Models for Better Lighting
Learn how to add per‑pixel normal maps to Source Engine models for richer lighting, with a step‑by‑step workflow, verification commands, and performance considerations.
14 Aug 2025, 08:55 UTC

Problem: Flat lighting hides detail
When a model uses only a diffuse texture, the lighting calculation is per‑vertex. On low‑poly geometry this makes surfaces look uniformly lit, hiding small surface variations like scratches, rivets, or wood grain. The result is a dull appearance even when dynamic lights move around the object.
Thesis: Normal maps enable per‑pixel lighting without extra geometry
By supplying a tangent‑space normal map and referencing it in a material file ($basetexture for the diffuse, $normalmap for the map), the Source Engine shader evaluates the light direction for each pixel. The model keeps its original triangle count while gaining the visual fidelity of a high‑poly sculpt.
Workflow: From high‑poly sculpt to in‑game material
- Create a high‑poly version of your asset (e.g., a wooden crate with extra planks and rivets).
- Generate a low‑poly version that will be used in the game; unwrap its UVs.
- Bake a tangent‑space normal map from the high‑poly onto the low‑poly UV layout using your preferred baking tool (xNormal, Substance Painter, Blender, etc.). Export the map as a VTF or TGA.
- Place the diffuse texture and the normal map in
game/materials/models/yourfolder/. - Create a VMT file (e.g.,
crate_normal.vmt) with the following content:"VertexLitGeneric" { "$basetexture" "models/yourfolder/crate_diffuse" "$normalmap" "models/yourfolder/crate_normal" "$model" "1" } - Compile the model with
studiomdl(run from the SDK tools directory):studiomdl.exe -game "path/to/your/game" "path/to/your/model.qc" - Place the resulting
.mdlfile ingame/models/yourfolder/and load it into a map.
Worked example: Testing the effect in‑game
Take a simple wooden crate:
- High‑poly sculpt adds extra plank lines and rivet heads.
- Baked normal map is 1024×1024 tangent‑space.
- Material references both textures as shown above.
After placing the crate in a map, open the console (~) and run:
mat_fullbright 0 // ensure lighting is active
r_drawdebugnormals 1 // view the normal map overlay (colors = XYZ normal)
You should see a colorful overlay that matches the baked detail. Then move a dynamic light entity (light_dynamic) around the crate; the shading on the crate will shift to reveal the planks and rivets, confirming per‑pixel lighting.
Trade‑offs and verification
Performance impact
Normal maps increase texture memory (one extra texture per material) and add a small per‑pixel shader cost. On older hardware or when many normal‑mapped models are visible simultaneously, frame‑rate can drop noticeably.
Verification steps
- Start the map with
mat_fullbright 0. - Toggle
r_drawdebugnormals 1; the overlay should appear without seams or distortion. - Run
r_drawbatch 1or launch the built‑in profiler (profiler) to capture draw‑call and shader‑cost numbers. Compare these values with and without the normal map enabled to assess the impact. - If you notice seams or incorrect shading, verify that the normal map is tangent‑space and matches the low‑poly UV layout; any mismatch will produce visible artifacts.
Limitations
- Texture memory: a 2048² normal map consumes ~4 MB (plus mipmaps). Keep an eye on the engine’s texture budget, especially on consoles or low‑end PCs.
- UV seams: mismatched tangents or UV islands can cause lighting seams; re‑bake with proper tangents or adjust the UV layout.
- Shader complexity: very complex materials (e.g., parallax mapping combined with normal maps) may exceed the shader limits of older Source branches.
Actionable closing
To add per‑pixel detail to your Source Engine models:
- Bake a tangent‑space normal map that matches your low‑poly UVs.
- Reference it in a VMT with
$basetextureand$normalmap. - Compile the model and place it in a map.
- Verify with
mat_fullbright 0,r_drawdebugnormals 1, and a performance check (r_drawbatch 1or profiler). - Iterate on texture resolution if you observe frame‑rate drops or texture‑budget warnings.
When the normal map is correctly applied and the UV layout matches, you’ll see lighting react to fine surface detail without raising the model’s triangle count—a practical way to improve visual fidelity while keeping performance in check.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.