Tutorial 37: Multiple Light Sources
BasicEffect supports exactly three directional lights. This tutorial shows how to configure them for cinematic three-point lighting, how to approximate point lights, and when to reach for a custom shader instead.
XNA 3-Directional-Light Limit
BasicEffect mirrors the original XNA design: it exposes three directional light slots (DirectionalLight0, DirectionalLight1, DirectionalLight2). Directional lights have a direction vector but no position — they model an infinitely distant source like the sun. Every vertex in the scene is lit identically regardless of its world position.
This design maps efficiently onto shader constant buffers and was a deliberate hardware-era trade-off. For many 3D games it is sufficient; the three-point lighting rig (key + fill + rim) has been a cinema industry standard for decades.
// Enable all three lights and set directions
effect.DirectionalLight0.setEnabled(true);
effect.DirectionalLight0.setDirection(Vector3::Normalize({-1, -1, -0.5f}));
effect.DirectionalLight0.setDiffuseColor({1.0f, 0.9f, 0.7f}); // warm sun
effect.DirectionalLight1.setEnabled(true);
effect.DirectionalLight1.setDirection(Vector3::Normalize({ 1, 0, -1}));
effect.DirectionalLight1.setDiffuseColor({0.3f, 0.4f, 0.6f}); // cool fill
effect.DirectionalLight2.setEnabled(true);
effect.DirectionalLight2.setDirection(Vector3::Normalize({ 0, 1, 1}));
effect.DirectionalLight2.setDiffuseColor({0.5f, 0.5f, 0.5f}); // rim
Simulating Point Lights (BasicEffect Workaround)
A point light emits in all directions from a single position in world space. BasicEffect has no native point-light support, but you can approximate one per object by computing the direction vector from the light position to the object's centre and updating DirectionalLight0 before each draw call:
// Per-object light direction update
Vector3 lightPos = {3, 4, 2};
Vector3 objectCentre = world.Translation();
Vector3 toLight = Vector3::Normalize(lightPos - objectCentre);
effect.DirectionalLight0.setDirection(-toLight); // direction points away from light
effect.setWorld(world);
DrawObject();
This is not physically correct — surfaces far from the light receive the same apparent intensity as close ones — but it is visually convincing for small scenes where objects do not span large world distances relative to the light.
Multiple Draw Calls with Different Effects
If objects truly need independent lighting (e.g., an outdoor scene and an indoor area rendered in the same frame), create multiple BasicEffect instances with different light configurations and assign each to the relevant ModelMeshPart:
// Assign different effect instances per mesh part
indoorModel->Meshes[0].MeshParts[0].Effect = indoorEffect;
outdoorModel->Meshes[0].MeshParts[0].Effect = outdoorEffect;
// Draw — each part uses its own lighting config
indoorModel->Draw(world, view, proj);
outdoorModel->Draw(world, view, proj);
Performance note: Effect state changes are relatively cheap in CNA, but each draw call has fixed CPU overhead. On mobile backends (SDL_RENDERER), keep the number of distinct effects and draw calls small. Batch geometry with the same lighting into a single draw call where possible.
EnvironmentMapEffect for Ambient
CNA includes EnvironmentMapEffect for cube-map based reflective ambient lighting. It reads from a TextureCube to give surfaces a sense of the surrounding environment without tracing rays:
auto envEffect = std::make_unique<EnvironmentMapEffect>(gd);
envEffect->setEnvironmentMap(skyCubeMap);
envEffect->setEnvironmentMapAmount(0.4f); // 40% reflection blend
envEffect->setFresnelFactor(1.0f);
mesh.MeshParts[0].Effect = std::move(envEffect);
Combine with BasicEffect for primary directional lighting — render the mesh twice or assign EnvironmentMapEffect only to reflective parts (glass, metal).
Custom Shader for More Lights
For scenes requiring true point lights, spotlights, or more than three light sources, write a custom GLSL/HLSL effect using CNA's Effect class. The fragment shader loops over a light array stored in a uniform buffer:
// GLSL fragment shader excerpt (multi-light forward rendering)
// uniform PointLight lights[8];
// ...
// vec3 result = ambientColor * albedo;
// for (int i = 0; i < lightCount; ++i) {
// vec3 L = normalize(lights[i].position - fragPos);
// float dist = length(lights[i].position - fragPos);
// float attenuation = 1.0 / (1.0 + 0.09*dist + 0.032*dist*dist);
// result += max(dot(N, L), 0.0) * lights[i].color * albedo * attenuation;
// }
// fragColor = vec4(result, 1.0);
Load the compiled effect with ContentManager::Load<Effect> and set uniform parameters via Effect::Parameters["lightCount"].
Full Example: Three-Point Lighting Rig
This demo implements classic three-point lighting: a warm yellow key light (simulated sun), a cool blue fill, and a white rim light from behind. The cube rotates slowly; pressing D animates the sun direction to simulate time of day.
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/IndexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionNormal.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;
class ThreeLightGame final : public Game {
public:
ThreeLightGame() : graphics_(this) {
graphics_.setPreferredBackBufferWidth(800);
graphics_.setPreferredBackBufferHeight(600);
}
protected:
void LoadContent() override {
effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
effect_->setLightingEnabled(true);
// Start from default and override
effect_->EnableDefaultLighting();
// Key light — warm yellow sun
effect_->DirectionalLight0.setEnabled(true);
effect_->DirectionalLight0.setDiffuseColor({1.0f, 0.9f, 0.6f});
effect_->DirectionalLight0.setSpecularColor({1.0f, 1.0f, 0.8f});
// Fill light — cool blue, dimmer
effect_->DirectionalLight1.setEnabled(true);
effect_->DirectionalLight1.setDiffuseColor({0.2f, 0.3f, 0.5f});
effect_->DirectionalLight1.setDirection(Vector3::Normalize({1, 0.5f, 0.5f}));
// Rim light — white, from behind
effect_->DirectionalLight2.setEnabled(true);
effect_->DirectionalLight2.setDiffuseColor({0.4f, 0.4f, 0.4f});
effect_->DirectionalLight2.setDirection(Vector3::Normalize({0, 1, 1}));
effect_->setAmbientLightColor({0.05f, 0.05f, 0.08f});
BuildCube();
}
void Update(GameTime& gt) override {
auto kb = Keyboard::GetState();
if (kb.IsKeyDown(Keys::Escape)) Exit();
float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());
rotation_ += dt * 0.4f;
// D key: animate sun (time-of-day rotation around Y axis)
if (kb.IsKeyDown(Keys::D))
dayAngle_ += dt * 0.5f;
float sy = std::sin(dayAngle_), cy = std::cos(dayAngle_);
sunDir_ = Vector3::Normalize({-cy, -0.7f, -sy});
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color(20, 20, 30, 255));
effect_->DirectionalLight0.setDirection(sunDir_);
effect_->setWorld(Matrix::CreateRotationY(rotation_));
effect_->setView(Matrix::CreateLookAt(
{0, 1.5f, 3}, Vector3::Zero, Vector3::Up));
effect_->setProjection(Matrix::CreatePerspectiveFieldOfView(
MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 100.0f));
gd.setVertexBuffer(*vb_);
gd.setIndexBuffer(*ib_);
for (auto& pass : effect_->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawIndexedPrimitives(PrimitiveType::TriangleList,
0, 0, 24, 0, 12);
}
gd.Present();
}
private:
void BuildCube() {
using V = VertexPositionNormal;
V verts[24];
uint16_t idx[36];
auto face = [&](int f, Vector3 n, Vector3 up, Vector3 right) {
Vector3 c = n * 0.5f;
verts[f*4+0] = { c - right*0.5f + up*0.5f, n };
verts[f*4+1] = { c + right*0.5f + up*0.5f, n };
verts[f*4+2] = { c + right*0.5f - up*0.5f, n };
verts[f*4+3] = { c - right*0.5f - up*0.5f, n };
int b = f*6, v = f*4;
idx[b+0]=v; idx[b+1]=v+1; idx[b+2]=v+2;
idx[b+3]=v; idx[b+4]=v+2; idx[b+5]=v+3;
};
face(0, Vector3::Forward, Vector3::Up, Vector3::Right);
face(1, Vector3::Backward, Vector3::Up, Vector3::Left);
face(2, Vector3::Left, Vector3::Up, Vector3::Forward);
face(3, Vector3::Right, Vector3::Up, Vector3::Backward);
face(4, Vector3::Up, Vector3::Backward, Vector3::Right);
face(5, Vector3::Down, Vector3::Forward, Vector3::Right);
auto& gd = getGraphicsDeviceProperty();
vb_ = std::make_unique<VertexBuffer>(gd,
VertexPositionNormal::VertexDeclaration, 24, BufferUsage::WriteOnly);
vb_->SetData(verts, 24);
ib_ = std::make_unique<IndexBuffer>(gd,
IndexElementSize::SixteenBits, 36, BufferUsage::WriteOnly);
ib_->SetData(idx, 36);
}
GraphicsDeviceManager graphics_;
std::unique_ptr<BasicEffect> effect_;
std::unique_ptr<VertexBuffer> vb_;
std::unique_ptr<IndexBuffer> ib_;
float rotation_ = 0.0f;
float dayAngle_ = 0.3f;
Vector3 sunDir_ = Vector3::Normalize({-1, -1, -0.5f});
};
int main() { ThreeLightGame g; g.Run(); }
Key Points
BasicEffecthas exactly three directional light slots; directional lights have no world position.- Call
EnableDefaultLighting()as a starting point, then override individual light colours and directions. - Approximate point lights per-object by computing a direction vector each draw call.
- For more than three lights, write a custom
Effectwith a light array in the fragment shader. - Keep draw call count low on mobile backends — each state change has CPU cost.