Tutorial 32: BasicEffect and 3D Lighting

3D Rendering  ·  Intermediate

Colour convention: Lighting properties on BasicEffect (AmbientLightColor, DiffuseColor, EmissiveColor, light colours) use Vector3(R, G, B) with components in the range 0–1. This is different from the Color struct used in 2D, which packs R/G/B/A as bytes 0–255.

BasicEffect Properties Overview

BasicEffect is the standard shader bundled with CNA, modelled directly on XNA's BasicEffect. It handles the most common rendering scenarios without requiring a custom GLSL/HLSL shader. Its properties fall into four groups:

GroupKey Setters
TransformsetWorld, setView, setProjection
LightingsetLightingEnabled, setAmbientLightColor, DirectionalLight0/1/2, EnableDefaultLighting()
MaterialsetDiffuseColor, setEmissiveColor, setAlpha
ModesetVertexColorEnabled, setTextureEnabled, setTexture

All properties are accessed through getter/setter methods. Changes take effect on the next pass.Apply() call.

LightingEnabled

Call setLightingEnabled(true) to activate the Blinn-Phong lighting model. Without it the shader ignores all light and simply renders the raw DiffuseColor (or vertex colour if VertexColorEnabled). Lighting also requires VertexPositionNormalTexture or another vertex type that carries a normal — without a normal the GPU has no surface direction to shade.

effect_->setLightingEnabled(true);   // activate lighting pipeline
// effect_->setLightingEnabled(false); // flat colour — no normals needed

DirectionalLight0/1/2 Setup

BasicEffect supports exactly three directional lights (DirectionalLight0, DirectionalLight1, DirectionalLight2). Each is infinite distance — it casts parallel rays with no falloff. The direction vector points from the light source toward the scene (i.e. the direction light travels).

// Key light: warm sun from upper-right
auto& sun = effect_->DirectionalLight0;
sun.setEnabled(true);
sun.setDirection(Vector3::Normalize(Vector3(1.0f, -1.0f, -0.5f)));
sun.setDiffuseColor(Vector3(1.0f, 0.95f, 0.8f));   // warm white
sun.setSpecularColor(Vector3(0.9f, 0.9f, 0.8f));

// Fill light: cool blue from the left
auto& fill = effect_->DirectionalLight1;
fill.setEnabled(true);
fill.setDirection(Vector3::Normalize(Vector3(-1.0f, -0.3f, 0.5f)));
fill.setDiffuseColor(Vector3(0.3f, 0.4f, 0.6f));   // cool blue
fill.setSpecularColor(Vector3(0.0f, 0.0f, 0.0f));  // no specular on fill

// Rim light: white from behind
auto& rim = effect_->DirectionalLight2;
rim.setEnabled(true);
rim.setDirection(Vector3::Normalize(Vector3(0.0f, 0.5f, 1.0f)));
rim.setDiffuseColor(Vector3(0.5f, 0.5f, 0.5f));
rim.setSpecularColor(Vector3(0.2f, 0.2f, 0.2f));

AmbientLightColor

Ambient light adds a constant base colour to every fragment regardless of surface normal. Keep it low (0.05–0.2 per channel) — too high and the scene looks washed out with no shadows. It's the minimum light that reaches every surface.

// Slightly warm ambient — prevents pure black shadows
effect_->setAmbientLightColor(Vector3(0.15f, 0.12f, 0.10f));

EnableDefaultLighting()

A convenience method that configures all three lights and the ambient to a set of XNA-standard defaults: a bright white key from upper-right, a dim blue fill from the left, and a very soft back. Good starting point for quick prototypes.

effect_->EnableDefaultLighting();

// Override just the key-light colour afterward:
effect_->DirectionalLight0.setDiffuseColor(Vector3(1.0f, 0.9f, 0.7f));

DiffuseColor

setDiffuseColor(Vector3) is the base material colour. It multiplies the incoming light colour per-channel. If texturing is enabled it modulates the texture sample instead.

effect_->setDiffuseColor(Vector3(0.8f, 0.2f, 0.2f));  // reddish material

EmissiveColor

setEmissiveColor(Vector3) adds a constant colour to every fragment after lighting, regardless of light direction or shadow. Use it for glowing objects — a lava rock, a neon sign, an LED. It adds to the final colour, so keep it moderate.

// Faint orange glow (lava)
effect_->setEmissiveColor(Vector3(0.4f, 0.1f, 0.0f));

VertexColorEnabled and TextureEnabled

BasicEffect has three surface colour modes. Only one can be active at a time:

ModeSetterVertex type needed
DiffuseColor only(default)Any (Position or PositionNormal)
Per-vertex coloursetVertexColorEnabled(true)VertexPositionColor
TexturesetTextureEnabled(true) + setTexture(tex)VertexPositionTexture or VertexPositionNormalTexture
// Textured mode
effect_->setTextureEnabled(true);
effect_->setTexture(*myTexture_);
// Vertex colour mode
effect_->setVertexColorEnabled(true);
// Back to flat DiffuseColor
effect_->setVertexColorEnabled(false);
effect_->setTextureEnabled(false);

Complete Example — Lit Rotating Cube

An 8-vertex, 12-triangle cube rendered with VertexPositionNormal and three directional lights. A helper function builds the vertex and index arrays.

#include <memory>
#include <array>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GameTime.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/MathHelper.hpp"
#include "Microsoft/Xna/Framework/Matrix.hpp"
#include "Microsoft/Xna/Framework/Vector3.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/Graphics/PrimitiveType.hpp"
#include "Microsoft/Xna/Framework/Graphics/BufferUsage.hpp"
#include "Microsoft/Xna/Framework/Graphics/IndexElementSize.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

// 24 vertices (4 per face × 6 faces) so each face gets its own flat normal.
static void BuildCube(std::vector<VertexPositionNormal>& verts,
                      std::vector<uint16_t>& indices)
{
    struct Face { Vector3 n; Vector3 c[4]; };
    const Face faces[6] = {
        // +X
        { { 1,0,0}, {{ 1,-1,-1},{ 1, 1,-1},{ 1, 1, 1},{ 1,-1, 1}} },
        // -X
        { {-1,0,0}, {{-1,-1, 1},{-1, 1, 1},{-1, 1,-1},{-1,-1,-1}} },
        // +Y
        { { 0,1,0}, {{-1, 1,-1},{-1, 1, 1},{ 1, 1, 1},{ 1, 1,-1}} },
        // -Y
        { { 0,-1,0},{{-1,-1, 1},{-1,-1,-1},{ 1,-1,-1},{ 1,-1, 1}} },
        // +Z
        { { 0,0,1}, {{-1,-1, 1},{ 1,-1, 1},{ 1, 1, 1},{-1, 1, 1}} },
        // -Z
        { { 0,0,-1},{{ 1,-1,-1},{-1,-1,-1},{-1, 1,-1},{ 1, 1,-1}} },
    };
    for (auto& f : faces) {
        uint16_t base = (uint16_t)verts.size();
        for (auto& p : f.c)
            verts.push_back({ p * 0.5f, f.n });
        // Two triangles per face
        indices.insert(indices.end(),
            { base, (uint16_t)(base+1), (uint16_t)(base+2),
              base, (uint16_t)(base+2), (uint16_t)(base+3) });
    }
}

class LitCubeGame final : public Game {
public:
    LitCubeGame() : graphics_(this) {
        graphics_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

protected:
    void LoadContent() override {
        std::vector<VertexPositionNormal> verts;
        std::vector<uint16_t>            indices;
        BuildCube(verts, indices);

        vb_ = std::make_unique<VertexBuffer>(
            getGraphicsDeviceProperty(),
            VertexPositionNormal::VertexDeclaration,
            (int)verts.size(), BufferUsage::None);
        vb_->SetData(verts.data(), (int)verts.size());

        ib_ = std::make_unique<IndexBuffer>(
            getGraphicsDeviceProperty(),
            IndexElementSize::SixteenBits,
            (int)indices.size(), BufferUsage::None);
        ib_->SetData(indices.data(), (int)indices.size());

        effect_ = std::make_unique<BasicEffect>(getGraphicsDeviceProperty());
        effect_->setLightingEnabled(true);
        effect_->setDiffuseColor(Vector3(0.7f, 0.7f, 0.9f));  // pale blue
        effect_->setAmbientLightColor(Vector3(0.1f, 0.1f, 0.15f));

        // Warm key light (sun)
        effect_->DirectionalLight0.setEnabled(true);
        effect_->DirectionalLight0.setDirection(
            Vector3::Normalize(Vector3(1.0f, -1.5f, -1.0f)));
        effect_->DirectionalLight0.setDiffuseColor(Vector3(1.0f, 0.95f, 0.8f));
        effect_->DirectionalLight0.setSpecularColor(Vector3(0.8f, 0.8f, 0.7f));

        // Cool fill light
        effect_->DirectionalLight1.setEnabled(true);
        effect_->DirectionalLight1.setDirection(
            Vector3::Normalize(Vector3(-1.0f, -0.3f, 0.5f)));
        effect_->DirectionalLight1.setDiffuseColor(Vector3(0.25f, 0.35f, 0.55f));
        effect_->DirectionalLight1.setSpecularColor(Vector3(0.0f, 0.0f, 0.0f));

        // Rim light
        effect_->DirectionalLight2.setEnabled(true);
        effect_->DirectionalLight2.setDirection(
            Vector3::Normalize(Vector3(0.0f, 0.6f, 1.0f)));
        effect_->DirectionalLight2.setDiffuseColor(Vector3(0.4f, 0.4f, 0.4f));
        effect_->DirectionalLight2.setSpecularColor(Vector3(0.3f, 0.3f, 0.3f));

        effect_->setView(Matrix::CreateLookAt(
            Vector3(0.0f, 1.5f, 3.5f),
            Vector3::Zero, Vector3::Up));
        effect_->setProjection(Matrix::CreatePerspectiveFieldOfView(
            MathHelper::PiOver4, 800.0f / 600.0f, 0.1f, 100.0f));
    }

    void Update(GameTime& gameTime) override {
        float dt = (float)gameTime.getElapsedGameTime().TotalSeconds();
        angle_ += dt * 0.8f;
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color(18, 18, 28, 255));

        Matrix world = Matrix::CreateRotationY(angle_)
                     * Matrix::CreateRotationX(angle_ * 0.4f);
        effect_->setWorld(world);

        gd.setVertexBuffer(*vb_);
        gd.setIndexBuffer(*ib_);
        for (auto& pass : effect_->getCurrentTechnique().Passes) {
            pass.Apply();
            // 24 vertices, 12 triangles (2 per face × 6 faces)
            gd.DrawIndexedPrimitives(
                PrimitiveType::TriangleList,
                0,   // base vertex
                0,   // start index
                12); // primitive count
        }
        gd.Present();
    }

private:
    GraphicsDeviceManager         graphics_;
    std::unique_ptr<BasicEffect>  effect_;
    std::unique_ptr<VertexBuffer> vb_;
    std::unique_ptr<IndexBuffer>  ib_;
    float                         angle_ = 0.0f;
};

int main() {
    LitCubeGame game;
    game.Run();
    return 0;
}

Key Points

  • Lighting requires setLightingEnabled(true) and vertex normals — use VertexPositionNormal or VertexPositionNormalTexture.
  • Light directions point from the source toward the scene, not from the scene to the light.
  • Lighting colours are Vector3(R, G, B) in 0–1 range, not the Color byte struct.
  • Use EnableDefaultLighting() for a quick three-point setup, then override individual lights.
  • VertexColorEnabled and TextureEnabled are mutually exclusive — pick one mode per draw call.