Tutorial 59: Shadow Mapping
What you’ll learn
- The two-pass shadow-map algorithm end to end.
- Rendering depth from the light into a
RenderTarget2Dand building the light-space matrix. - Sampling the shadow map, softening it with PCF, and tuning bias to kill shadow acne.
- Which renderers can run a hand-written shadow pass, and how to hand your map to CNA’s shadow-receiving stock effects instead of writing the lighting shader.
Before you start — Tutorial 23: Render Targets for Off-Screen Rendering (the depth pass renders to a target), Tutorial 52: Writing Custom Shaders (ShaderEffect) (the comparison happens in your own shader) and Tutorial 33: Matrices and Transformations (light-space transforms). Requires a 3D-capable renderer with off-screen render targets, such as OPENGLES3 or OPENGL33; the 2D-only SDL_RENDERER throws on 3D calls by default, and STUB draws nothing. The table under Platform Limitations names all 14 renderer identities.
Two routes to shadows. This tutorial first teaches the technique by hand — an off-screen RenderTarget2D and your own ShaderEffect source — because that is how you learn what every shadow system does. That route needs a renderer that has render targets, executes a custom ShaderEffect, and takes shader text in its own language: the GLSL ES 3.00 shown here runs as written on OPENGLES3 and WEBGL2 only, and the other 3D renderers need a port (see the table under Platform Limitations). The second route keeps your caster pass but lets CNA’s stock lit effects do the receiving: BasicEffect, SkinnedEffect, PbrEffect and SkinnedPbrEffect sample a map you render and hand them, as described in Feeding your map to the built-in receivers. That part is always compiled; CNA itself does not generate shadow maps.
The hand-written example stores depth packed into RGBA8 (SurfaceFormat::Color), which works on every renderer that can render to a texture. Float shadow maps are a HiDef capability you query, not a portable assumption; the built-in receivers read an unpacked value, so they want a SurfaceFormat::Single map where the device can render one.
Requirements: graphics profile. CNA’s default GraphicsProfile is Reach, and this snapshot enforces the Reach ceilings on every renderer. The hand-written route below runs on the default profile as long as the shadow map is an RGBA8 target of at most 2048 × 2048: Reach caps render-target edges at 2048, and a larger request throws NotSupportedException. The 4096 × 4096 map suggested below for large outdoor scenes and a SurfaceFormat::Single map both need HiDef. Request it in the constructor, before the device exists:
MyGame::MyGame() : graphics_(this) {
graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
// Or, for the whole project, before the Game is constructed:
// CNA::SetProjectGraphicsProfileEXT(GraphicsProfile::HiDef); // "CNA/ProjectGraphicsProfile.hpp"
}
The full list of profile ceilings, and the errors you will see when one is exceeded, is in Tutorial 152: Reach vs HiDef.
Without HiDef a request for a float render-target format does not throw: it silently becomes Color, so read getFormatProperty() on the target you actually got.
Shadow mapping is the dominant real-time shadow technique used in 3D games. The algorithm is conceptually elegant: before rendering the visible scene, render it from the light's point of view, recording only depth. This "shadow map" records how far the light can see in each direction. During the main render pass, for each fragment you compute where it would land in the light's view and compare its depth against the stored shadow map depth. If the fragment is farther from the light than what the map recorded, something else was in between — the fragment is in shadow.
Shadow Mapping Algorithm
The algorithm runs in two GPU passes per frame:
- Depth pass (light's POV): Set the render target to a depth-format texture (the shadow map). Render the entire scene using a minimal vertex shader that transforms positions into light space. The fragment shader is empty or writes only the depth. The hardware depth buffer is written automatically at the shadow map resolution.
- Lighting pass (camera's POV): Restore the normal render target (the back buffer). For each vertex, additionally compute its position in light space and pass it to the fragment shader as a varying. In the fragment shader, project that light-space position into shadow map texture coordinates and sample the shadow map. Compare the sampled depth against the current fragment's light-space depth. If the fragment is deeper (farther from the light), it is shadowed.
The shadow map is a Texture2D (not a cubemap) for directional and spot lights. Point lights require six shadow maps (one per cubemap face) or a special dual-paraboloid encoding; that is beyond the scope of the hand-written route here, but the built-in receivers accept a six-face cube map you render yourself (see Feeding your map to the built-in receivers).
Depth Render Pass to RenderTarget2D
The tutorial's portable target is SurfaceFormat::Color. Every renderer that can render to a texture can render to Color, so packing depth into RGBA8 works everywhere this technique does. Float targets are worth using when you can ask for them: on a HiDef device, test gd.SupportsCapability(GraphicsCapability::FloatRenderTargets) (or gd.SupportsSurfaceFormatAsRenderTargetEXT(SurfaceFormat::Single)), and fall back to the packed layout when it answers false. The capability is probed per device on OPENGLES3, OPENGL33, WEBGL2, VULKAN, SDL_GPU, WEBGPU, DIRECTX11 and METAL, always true on SOFTWARE, and false elsewhere.
Create the shadow map as a RenderTarget2D in SurfaceFormat::Color, with a real hardware depth buffer for the depth pass itself. A 1024×1024 shadow map is standard for local shadows; 2048×2048 or 4096×4096 for large outdoor scenes with sun shadows (4096 needs HiDef, see Requirements). Using a power-of-two size ensures hardware mip generation works correctly if needed.
// Create shadow map render target
shadowMap_ = std::make_unique<RenderTarget2D>(
gd,
1024, 1024,
false, // no mipmaps
SurfaceFormat::Color, // RGBA8 - works on every renderer with render targets
DepthFormat::Depth24 // hardware depth buffer for the depth pass
);
The colour target stores depth as a packed RGBA8 value written by the fragment shader; DepthFormat::Depth24 is the hardware depth buffer that performs the actual depth test during the pass. They serve different purposes and both are needed.
Packing 24 bits of depth into RGBA8
The standard trick spreads a normalised float across the four channels, giving roughly 32 bits of storage of which about 24 bits are usable in practice. Add these two helpers to your shaders:
// Pack a float in [0,1] into RGBA8.
vec4 packDepth(float depth) {
const vec4 bitShift = vec4(1.0, 255.0, 65025.0, 16581375.0);
const vec4 bitMask = vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 0.0);
vec4 res = fract(depth * bitShift);
res -= res.gbaa * bitMask;
return res;
}
// Unpack RGBA8 back into a float in [0,1].
float unpackDepth(vec4 rgba) {
const vec4 bitShift = vec4(1.0,
1.0 / 255.0,
1.0 / 65025.0,
1.0 / 16581375.0);
return dot(rgba, bitShift);
}
Precision is lower than a true 32-bit float target, which shows up as slightly coarser self-shadowing. Compensate with a marginally larger depth bias rather than a higher shadow map resolution.
During the depth pass, bind the shadow map render target, clear it to maximum depth (white = 1.0), and render the scene geometry using the depth effect:
gd.SetRenderTarget(shadowMap_.get());
gd.Clear(Color::White); // Clears depth to 1.0 (maximum distance)
// ... render scene geometry with depth-only effect ...
gd.SetRenderTarget(nullptr); // restore back buffer
Light Space Matrix
The light space matrix transforms world-space positions into the light's clip space. For a directional light (like the sun), use an orthographic projection centred on the scene's bounding volume. For a spot light, use a perspective projection with a field of view matching the cone angle:
// Directional light (sun-like)
Vector3 lightPos = Vector3(10.0f, 20.0f, 10.0f); // position the light above the scene
Vector3 lightTarget = Vector3::Zero;
Vector3 lightUp = Vector3::Up;
Matrix lightView = Matrix::CreateLookAt(lightPos, lightTarget, lightUp);
// Orthographic projection covering a 40x40 unit area, depth range 1..60
Matrix lightProj = Matrix::CreateOrthographic(40.0f, 40.0f, 1.0f, 60.0f);
// Combine: this is what gets uploaded to the depth-pass vertex shader
Matrix lightSpaceMatrix_ = lightView * lightProj;
The orthographic extents (40×40 in the example) should tightly wrap the visible scene. An overly large frustum wastes shadow map resolution — each shadow map texel covers a larger world-space area, making shadows appear blockier. For best quality, fit the volume tightly around what the camera can see (a "fitted shadow map"), or split the view frustum into several depth ranges with one map each ("cascaded shadow maps"). CNA computes neither for you, but the built-in receivers accept a cascade atlas of up to four slices that you render yourself (see Feeding your map to the built-in receivers).
Shadow Map Sampling in the Lighting Shader
In the lighting pass vertex shader, compute the fragment's position in light space and pass it to the fragment shader as a varying:
// In the lighting vertex shader
uniform mat4 u_lightSpace;
uniform mat4 u_world;
out vec4 v_lightSpacePos;
void main() {
vec4 worldPos = u_world * vec4(a_position, 1.0);
v_lightSpacePos = u_lightSpace * worldPos;
// ... compute camera-space position for gl_Position ...
}
In the fragment shader, project the light-space position to [0,1] UV coordinates and sample the shadow map:
vec3 proj = v_lightSpacePos.xyz / v_lightSpacePos.w; // perspective divide
proj = proj * 0.5 + 0.5; // NDC [-1,1] -> [0,1]
// Fragments outside the light frustum are not in shadow
if (proj.z > 1.0) { /* fully lit */ }
float shadowMapDepth = unpackDepth(texture(u_shadowMap, proj.xy));
float fragDepth = proj.z;
// If the fragment is deeper than what the shadow map recorded, it's in shadow
bool inShadow = (fragDepth > shadowMapDepth + bias);
Percentage-Closer Filtering (PCF)
A single shadow map sample produces hard-edged "blocky" shadows with visible aliasing at the shadow boundary, especially when the light frustum is large relative to the shadow map resolution. Percentage-Closer Filtering (PCF) mitigates this by sampling the shadow map at multiple nearby texel locations and averaging the binary shadow test results. This effectively blurs the shadow edge, producing a soft penumbra without a true area light calculation:
float shadowFactor(vec4 lsPos) {
vec3 proj = lsPos.xyz / lsPos.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0) return 1.0; // outside light frustum = lit
float bias = 0.005;
float shadow = 0.0;
vec2 texelSize = 1.0 / vec2(textureSize(u_shadowMap, 0));
// 3x3 PCF kernel
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
float pcfDepth = unpackDepth(texture(u_shadowMap,
proj.xy + vec2(x, y) * texelSize));
shadow += (proj.z - bias > pcfDepth) ? 0.0 : 1.0;
}
}
return shadow / 9.0; // average of 9 samples
}
A 3×3 kernel (9 samples) is a good balance between quality and performance. A 5×5 kernel (25 samples) produces noticeably softer shadows at roughly 2.8x the cost. For very high quality, use a Poisson disk kernel with 16–64 samples, optionally rotated randomly per pixel to break the regular pattern.
Bias to Avoid Shadow Acne
Shadow acne is a self-shadowing artifact: the surface appears to cast shadows on itself, producing a characteristic stripey or moire pattern. It arises because the shadow map depth is a discretised approximation — each texel covers a non-zero area, and the surface depth sampled during the depth pass may be slightly different from the depth computed during the lighting pass due to floating-point precision differences and the fact that neighbouring texels represent different points on the surface.
The fix is to add a small constant "bias" to the shadow map depth comparison. If the fragment's depth minus the bias is still greater than the shadow map depth, it is in shadow; otherwise it is lit. This pushes the comparison threshold slightly outward, eliminating the self-shadowing artifacts:
float bias = 0.005;
float inShadow = (proj.z - bias > shadowMapDepth) ? 0.0 : 1.0;
The bias value requires careful tuning. Too small: acne remains. Too large: "peter-panning" occurs — the shadow appears detached from the shadow-casting object, as if the object is floating. A common technique to automate bias selection is slope-based bias, which scales the bias with the angle between the light direction and the surface normal:
// Slope-scaled bias: larger bias on steep surfaces
float cosTheta = clamp(dot(N, L), 0.0, 1.0);
float bias = max(0.01 * (1.0 - cosTheta), 0.001);
Platform Limitations
Shadow mapping has the following platform and renderer constraints in CNA. Three separate questions decide whether it works on a given renderer: does it render 3D into an off-screen target, does it execute a custom ShaderEffect in a language you can supply, and (for the second route) do its stock lit effects sample a shadow map you supply. All 14 renderer identities are accounted for below.
| Renderer identities | Custom ShaderEffect source |
This tutorial’s hand-written route | Stock lit effects sample the map |
|---|---|---|---|
OPENGLES3, WEBGL2 | GLSL ES 3.00 | Runs as written | Yes |
OPENGL33 | Desktop GLSL | Runs after replacing the #version 300 es and precision lines with #version 330 core | Yes |
VULKAN | SPIR-V only; GLSL text is refused | Compile the GLSL to SPIR-V offline (or use a shader package); text as written cannot be passed | Yes |
SDL_GPU | SPIR-V; GLSL text only in builds where libshaderc was found (Linux, Android) | Needs a libshaderc build (CustomEffects is false otherwise), or a port to SPIR-V; GLSL dialect details are in Tutorial 52 | Yes |
WEBGPU | WGSL | Needs a WGSL port | Yes, once a device exists |
DIRECTX11 | HLSL (vs_5_0/ps_5_0, entry point main) | Needs an HLSL port | Yes at feature level 11_0 and above (source-verified) |
DIRECTX9 | HLSL, compiled for SpriteBatch effects; no 3D draw path was found that consumes a custom effect | Not verified; treat as unavailable | No (state accepted and ignored) |
METAL | Metal Shading Language, SpriteBatch effects only | Not available (a 3D draw with a custom effect throws; compiled XNA effects, opt-in on Metal, are the 3D route) | No |
FNA3D, SOFTWARE | None: CustomEffects is false | Not available | No |
HEADLESS | Accepted, recorded, never executed | Renders nothing | No |
STUB and the 2D-only SDL_RENDERER | None (no 3D pipeline) | Not supported: no off-screen depth render targets and no programmable 3D pipeline | No |
A renderer without a shadow-sampling shader accepts the shadow state and ignores it: you get an unshadowed image, not an error. gd.SupportsShadowSamplingEXT() tells you which case you are in.
- Surface format: the packed-RGBA8 example uses
SurfaceFormat::Color, which every render-target-capable renderer accepts. Float formats are checked twice — the profile must be HiDef and the renderer must classify the format as renderable — and a target that fails either check silently becomesColor. - Custom shader languages: the ShaderEffect string constructor passes your text to the renderer verbatim, so the dialect is the renderer’s (table above).
gd.GetShaderDialectEXT()reports it at run time, andgd.ExecutesShaderEffectSourceEXT()says whether the source will actually run. - Android / WebGL:
OPENGLES3on Android andWEBGL2in the browser share one internal implementation (EasyGL) over OpenGL ES 3.0, and the#version 300 essources on this page compile on both.
Feeding your map to the built-in receivers
Everything above is what a shadow system does internally, and CNA does not generate shadow maps for you: the caster pass is always yours. What this snapshot does ship is shadow receiving, as an always-compiled interface, IShadowReceiverEXT, implemented by BasicEffect, SkinnedEffect, PbrEffect and SkinnedPbrEffect (no CNA_CNAEXT needed). AlphaTestEffect, DualTextureEffect and EnvironmentMapEffect are not receivers. Once your depth pass has filled a render target, you can shade the scene with one of those stock effects instead of writing the lighting shader yourself.
The map holds light-space distance in an ordinary colour target, because CNA has no API for sampling a depth attachment as a texture. On the GL identities (EasyGL) the receiver reads the map’s red channel and compares it with the fragment’s light-space depth — z / w of the light view-projection you pass in, mapped to 0…1, which is exactly the value gl_FragCoord.z holds in the caster pass. So the receivers want depth unpacked: write fragColor = vec4(gl_FragCoord.z, 0.0, 0.0, 1.0); into a SurfaceFormat::Single target (HiDef, and only where GraphicsCapability::FloatRenderTargets is true) rather than the packed RGBA8 of the hand-written route, which only your own lighting shader knows how to decode. The other renderers’ receiver shaders were not inspected for this page.
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp" // implements IShadowReceiverEXT
// LoadContent: the caster target, written by a depth pass that outputs unpacked depth.
shadowMap_ = std::make_unique<RenderTarget2D>(
gd, 2048, 2048, false, SurfaceFormat::Single, DepthFormat::Depth24); // needs HiDef
receiver_ = std::make_unique<BasicEffect>(gd);
receiver_->setShadowMapEXT(shadowMap_.get()); // a RenderTarget2D is a Texture2D
receiver_->setShadowFilterRadiusEXT(1); // PCF: 0 = one tap, 1 = 3x3, 2 = 5x5
receiver_->setShadowDepthBiasEXT(0.002f); // same trade-off as the bias section above
receiver_->setShadowsEnabledEXT(gd.SupportsShadowSamplingEXT());
// Draw: run your depth pass into shadowMap_ first, with lightSpaceMatrix_, then
receiver_->setLightViewProjectionEXT(lightSpaceMatrix_);
// ... set World/View/Projection, lighting, and draw the receiving geometry with receiver_ ...
A renderer without a shadow-sampling shader accepts all of this and draws the scene unshadowed; gd.SupportsShadowSamplingEXT() tells you which case you are in (see the last column of the table above). setShadowsEnabledEXT(false) turns reception off for individual draws while the map stays attached.
Two more inputs exist for larger set-ups, both filled from maps you render yourself. For cascades, render up to ShadowCascadeStateEXT::kMaxCascades (4) slices side by side into one atlas, attach the atlas with setShadowMapEXT, and describe it with setShadowCascadesEXT: a ShadowCascadeStateEXT carrying Count, one WorldToAtlas matrix per cascade (with that cascade’s slice of the atlas already applied), the ascending view-space SplitDistance values, the CameraView the cascades were fitted to, and an optional BlendBand. For one point or spot light, setPunctualLightEXT takes a PunctualLightEXT whose ShadowCube (six faces) or ShadowMap plus ShadowViewProjection you have rendered (Tutorial 37 shows the light itself).
Complete GLSL Shaders
These sources are GLSL ES 3.00, the dialect of OPENGLES3 and WEBGL2. For OPENGL33 replace the first two lines of each file with #version 330 core. VULKAN needs the same programs compiled to SPIR-V, WEBGPU needs WGSL, and the Direct3D renderers need HLSL; the algorithm is identical, only the syntax changes.
Depth Pass — Vertex Shader (shadow_depth.vert)
#version 300 es
precision highp float;
layout(location = 0) in vec3 a_position;
uniform mat4 u_lightSpace;
uniform mat4 u_world;
void main() {
gl_Position = u_lightSpace * u_world * vec4(a_position, 1.0);
}
Depth Pass — Fragment Shader (shadow_depth.frag)
#version 300 es
precision highp float;
out vec4 fragColor;
vec4 packDepth(float depth) {
const vec4 bitShift = vec4(1.0, 255.0, 65025.0, 16581375.0);
const vec4 bitMask = vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 0.0);
vec4 res = fract(depth * bitShift);
res -= res.gbaa * bitMask;
return res;
}
void main() {
// The hardware depth buffer is written automatically. The colour target
// is SurfaceFormat::Color (RGBA8), so write depth in packed form.
fragColor = packDepth(gl_FragCoord.z);
}
Lighting Pass — Fragment Shader with PCF (shadow_lighting.frag)
#version 300 es
precision highp float;
in vec3 v_worldPos;
in vec3 v_normal;
in vec2 v_texcoord;
in vec4 v_lightSpacePos;
uniform sampler2D u_shadowMap;
uniform sampler2D u_diffuse;
uniform vec3 u_lightDir; // world-space, towards light source
uniform vec3 u_lightColor;
uniform vec3 u_ambientColor;
out vec4 fragColor;
float unpackDepth(vec4 rgba) {
const vec4 bitShift = vec4(1.0,
1.0 / 255.0,
1.0 / 65025.0,
1.0 / 16581375.0);
return dot(rgba, bitShift);
}
float shadowFactor(vec4 lsPos) {
vec3 proj = lsPos.xyz / lsPos.w;
proj = proj * 0.5 + 0.5;
if (proj.z > 1.0) return 1.0; // outside light frustum: fully lit
float cosTheta = clamp(dot(normalize(v_normal), normalize(u_lightDir)), 0.0, 1.0);
float bias = max(0.01 * (1.0 - cosTheta), 0.001);
float shadow = 0.0;
vec2 texelSize = 1.0 / vec2(textureSize(u_shadowMap, 0));
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
float pcfDepth = unpackDepth(texture(u_shadowMap,
proj.xy + vec2(x, y) * texelSize));
shadow += (proj.z - bias > pcfDepth) ? 0.0 : 1.0;
}
}
return shadow / 9.0;
}
void main() {
vec3 N = normalize(v_normal);
vec3 L = normalize(u_lightDir);
float diff = max(dot(N, L), 0.0);
float sf = shadowFactor(v_lightSpacePos);
vec4 albedo = texture(u_diffuse, v_texcoord);
vec3 ambient = u_ambientColor * albedo.rgb;
vec3 diffuse = u_lightColor * diff * sf * albedo.rgb;
fragColor = vec4(ambient + diffuse, albedo.a);
}
Complete C++ Two-Pass Draw Loop
Both passes in this tutorial use ShaderEffect, not the XNB compiled-effect path. This snapshot can also run compiled XNA/FNA Effect Framework bytecode — on FNA3D always, and on ten more identities in eight further families only when the matching default-OFF CNA_*_COMPILED_EFFECTS option was enabled at configure time — but that bytecode is not HLSL .fx source, and CNA does not compile .fx at run time. This example keeps its renderer-native shader text explicit. ShaderEffect exposes no Parameters collection; uniforms go through SetUniformXxx(), and Apply() first is the portable order (on OPENGLES3, OPENGL33 and WEBGL2 each setter also makes its own program current). See Tutorial 128.
#include "Microsoft/Xna/Framework/Graphics/ShaderEffect.hpp"
#include "System/IO/File.hpp"
// A minimal camera helper used by the examples in this series. It is application
// code, not a CNA type; Tutorial 34 builds a fuller FpsCamera.
struct Camera {
Matrix view = Matrix::CreateLookAt(Vector3(0.0f, 8.0f, 16.0f), Vector3::Zero, Vector3::Up);
Matrix projection = Matrix::CreatePerspectiveFieldOfView(
MathHelper::ToRadians(60.0f), 16.0f / 9.0f, 0.1f, 200.0f);
const Matrix& View() const { return view; }
const Matrix& Projection() const { return projection; }
void Update(const GameTime&) {}
};
class ShadowGame final : public Game {
GraphicsDeviceManager graphics_;
std::unique_ptr<RenderTarget2D> shadowMap_;
std::unique_ptr<ShaderEffect> depthEffect_;
std::unique_ptr<ShaderEffect> lightEffect_;
Matrix lightSpaceMatrix_;
Vector3 lightDir_;
struct SceneObj { VertexBuffer* VB; int TriCount; Matrix World; };
std::vector<SceneObj> scene_;
Camera camera_;
Vector3 lightPos_{ 10.0f, 20.0f, 10.0f };
// Small helper: ShaderEffect's matrix setter takes raw column-major floats.
static void SetMat4(ShaderEffect& fx, const char* name, const Matrix& m) {
float cm[16];
m.ToColumnMajor(cm);
fx.SetUniformMat4(name, cm);
}
public:
ShadowGame() : graphics_(this) {
// A 1024 x 1024 RGBA8 map is inside Reach's limits, so no HiDef request is
// needed here. Ask for HiDef only for 4096 maps or float targets:
// graphics_.setGraphicsProfileProperty(GraphicsProfile::HiDef);
}
protected:
void LoadContent() override {
auto& gd = getGraphicsDeviceProperty();
shadowMap_ = std::make_unique<RenderTarget2D>(
gd, 1024, 1024, false,
SurfaceFormat::Color, DepthFormat::Depth24);
// Three arguments: device, vertex source, fragment source. Not file paths.
depthEffect_ = std::make_unique<ShaderEffect>(
gd,
System::IO::File::ReadAllText("Content/shaders/shadow_depth.vert.glsl"),
System::IO::File::ReadAllText("Content/shaders/shadow_depth.frag.glsl"));
lightEffect_ = std::make_unique<ShaderEffect>(
gd,
System::IO::File::ReadAllText("Content/shaders/shadow_lighting.vert.glsl"),
System::IO::File::ReadAllText("Content/shaders/shadow_lighting.frag.glsl"));
// Neither constructor throws on a compile failure -- check both.
// GetCompileErrorEXT() returns the compiler log (it is also written to stderr).
if (!depthEffect_->IsEffectValid() || !lightEffect_->IsEffectValid()) {
// The shader did not compile. Do not draw with it.
}
// Static lighting uniforms. Apply() first is the portable order.
lightEffect_->Apply();
lightEffect_->SetUniformVec3("u_lightColor", 1.0f, 0.95f, 0.85f);
lightEffect_->SetUniformVec3("u_ambientColor", 0.15f, 0.15f, 0.2f);
lightEffect_->SetUniformInt ("u_shadowMap", 1); // sampler unit assignments
lightEffect_->SetUniformInt ("u_diffuse", 0);
BuildScene();
}
void Update(GameTime& gt) override {
// Recompute light space matrix each frame (for moving light)
Matrix lightView = Matrix::CreateLookAt(
lightPos_, Vector3::Zero, Vector3::Up);
Matrix lightProj = Matrix::CreateOrthographic(40.0f, 40.0f, 1.0f, 60.0f);
lightSpaceMatrix_ = lightView * lightProj;
lightDir_ = Vector3::Normalize(Vector3::Zero - lightPos_);
// Uniforms are pushed in Draw(), after each effect's own Apply().
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
// ---------------------------------------------------------------
// Pass 1: Render depth to shadow map from light's point of view
// ---------------------------------------------------------------
gd.SetRenderTarget(shadowMap_.get());
gd.Clear(Color::White);
gd.setRasterizerStateProperty(RasterizerState::CullCounterClockwise);
for (auto& obj : scene_) {
// Apply() first: the portable order for setting uniforms.
depthEffect_->Apply();
SetMat4(*depthEffect_, "u_lightSpace", lightSpaceMatrix_);
SetMat4(*depthEffect_, "u_world", obj.World);
gd.SetVertexBuffer(obj.VB);
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, obj.TriCount);
}
// ---------------------------------------------------------------
// Pass 2: Render scene with shadow test from camera's point of view
// ---------------------------------------------------------------
gd.SetRenderTarget(nullptr);
gd.Clear(Color::CornflowerBlue);
gd.setRasterizerStateProperty(RasterizerState::CullCounterClockwise);
for (auto& obj : scene_) {
lightEffect_->Apply();
SetMat4(*lightEffect_, "u_lightSpace", lightSpaceMatrix_);
SetMat4(*lightEffect_, "u_view", camera_.View());
SetMat4(*lightEffect_, "u_projection", camera_.Projection());
SetMat4(*lightEffect_, "u_world", obj.World);
lightEffect_->SetUniformVec3("u_lightDir",
lightDir_.X, lightDir_.Y, lightDir_.Z);
// Bind the shadow map on unit 1, matching the SetUniformInt above.
lightEffect_->SetTexture(1, *shadowMap_);
gd.SetVertexBuffer(obj.VB);
gd.DrawPrimitives(PrimitiveType::TriangleList, 0, obj.TriCount);
}
// No gd.Present() here: Game presents after Draw() returns (in EndDraw),
// so a manual Present() would present the frame twice.
}
};
SetTexture takes a reference, so the shadow map is passed as *shadowMap_. RenderTarget2D derives from Texture2D, so it binds like any other texture — just make sure it is no longer the active render target when you sample it.
Depth format caution: Two different depths are in play during the depth pass — the packed RGBA8 value your fragment shader writes to the colour target, and the hardware depth buffer created by DepthFormat::Depth24. Only the packed colour value is what you later sample, so every read of u_shadowMap must go through unpackDepth(). Sampling .r directly returns just the high byte and produces shadows that snap between 255 coarse depth steps. Because packed RGBA8 carries less usable precision than a true float target, expect to raise the depth bias slightly compared to the values you would use on a platform with float render targets.