CRM64Pro Tutorial 11

2D lightmaps.

Combine ambient light, animated point lights, a spotlight, and three occluder shapes in one lightmap pass.

  • Intermediate
  • GFX
  • Lighting
  • Shadows

Overview

This tutorial introduces the GFX 2D lighting pass. It draws the scene first, then builds and applies a lightmap from ambient color, three light types, and optional rectangle, circle and segment occluders.

The lights demonstrate candle animation, a mouse-controlled point light, and a spotlight placed above the segment so its shadow is visible.

Final result

CRM64Pro Tutorial 11 lightmap
Runtime controls

The mouse moves the warm light; L toggles lighting; O toggles occluders; P cycles ambient presets; A/Z changes ambient intensity; I/K changes mouse-light intensity; D toggles debug; grave toggles the console; and Q or ESC exits.

Prerequisites

  • CRM64Pro GDK installed and configured with a supported C++17 compiler.
  • Tutorial package downloaded and fully extracted, preserving its folder structure.
  • Run Tutorials 02 and 04 so Tutorial.cdc contains the background and cursor.
  • A renderer supported by the GFX lightmap implementation.

What you will learn

  • Begin and finish a GFX lightmap pass.
  • Add rectangle, circle and segment occluders.
  • Configure point and spot lights with GFXLight.
  • Animate light color, intensity and radius.
  • Always finish an active pass after intermediate failures.
  • Keep diagnostics and HUD rendering outside lighting.

Step by step

Step 1: Define ambient presets

Each preset combines a base RGB color with its recommended starting intensity.

static const AmbientPreset vAmbientPresets[] =
{
    { "Daylight",      255, 255, 255, 1.00f },
    { "Neutral dim",   255, 255, 255, 0.25f },
    { "Dusk",          154, 168, 200, 0.45f },
    { "Moonlight",     113, 136, 184, 0.20f },
    { "Warm interior", 208, 144,  88, 0.20f },
    { "Torch",          58,  32,   8, 0.08f },
    { "Blackout",        0,   0,   0, 0.00f }
};

Step 2: Convert ambient values safely

Clamp and round each floating-point color result before passing it to beginLighting().

// Convert a light value to a valid colour byte.
static Uint8 lightByte(float fValue)
{
    if(fValue <= 0.0f) return 0;
    if(fValue >= 255.0f) return 255;
    return static_cast<Uint8>(fValue + 0.5f);
}

Step 3: Draw the scene before lighting

These normal scene shapes are rendered before the lightmap is accumulated.

// Draw the scene objects lit by the lightmap.
static void renderSceneProps(GFX& mGFX)
{
    mGFX.rectFilled(260, 205, 390, 295, 62, 76, 88, 255);
    mGFX.rect(260, 205, 390, 295, 210, 220, 230, 255);
    mGFX.circleFilled(610, 350, 56, 56, 72, 74, 255);
    mGFX.circle(610, 350, 56, 210, 220, 230, 255);
    mGFX.line(485, 130, 760, 260, 210, 220, 230, 255);
}

Step 4: Begin with ambient light

When lighting is enabled, begin the lightmap with the selected ambient color and intensity.

if(!rState.bLighting) return true;

const AmbientPreset& Ambient = vAmbientPresets[rState.iAmbientPreset];
bool bOk = mGFX.beginLighting(lightByte(Ambient.iR * rState.fAmbientIntensity),
    lightByte(Ambient.iG * rState.fAmbientIntensity), lightByte(Ambient.iB * rState.fAmbientIntensity));
if(!bOk) return false;

Step 5: Add matching occluders

The occluder geometry matches the visible scene shapes. Results accumulate without skipping later cleanup.

if(rState.bOccluders)
{
    bOk = mGFX.addLightOccluderRect(260, 205, 390, 295) && bOk;
    bOk = mGFX.addLightOccluderCircle(610, 350, 56) && bOk;
    bOk = mGFX.addLightOccluderSegment(485, 130, 760, 260) && bOk;
}

Step 6: Configure an animated candle

GLA_CANDLE varies intensity, blends toward a secondary color, and changes radius using a stable animation ID.

GFXLight CandleLight;
CandleLight.fX = 175.0f;
CandleLight.fY = 170.0f;
CandleLight.fRadius = 165.0f;
CandleLight.iColor = 0xFFF0B0FF;
CandleLight.fIntensity = 0.52f;
CandleLight.bUseOccluders = rState.bOccluders;
CandleLight.eAnimation = GLA_CANDLE;
CandleLight.fAnimationRate = 8.0f;
CandleLight.fAnimationAmount = 0.32f;
CandleLight.iSecondaryColor = 0xE85018FF;
CandleLight.fAnimationRadiusAmount = 0.10f;
CandleLight.iAnimationID = 1;
bOk = mGFX.drawLight(CandleLight) && bOk;

Step 7: Add a mouse-controlled point light

The second light follows the cursor and exposes its intensity through runtime controls.

GFXLight MouseLight;
MouseLight.fX = rState.fMouseX;
MouseLight.fY = rState.fMouseY;
MouseLight.fRadius = 180.0f;
MouseLight.iColor = 0xFFDCA0FF;
MouseLight.fIntensity = rState.fMouseIntensity;
MouseLight.bUseOccluders = rState.bOccluders;
bOk = mGFX.drawLight(MouseLight) && bOk;

Step 8: Add a spotlight and finish

The downward 70-degree cone crosses the segment occluder. endLighting() always runs after a successful begin.

GFXLight SpotLight;
SpotLight.fX = 640.0f;
SpotLight.fY = 70.0f;
SpotLight.fRadius = 430.0f;
SpotLight.fDirection = 90.0f;
SpotLight.fAngle = 70.0f;
SpotLight.iColor = 0xB0D8FFFF;
SpotLight.fIntensity = 0.65f;
SpotLight.bUseOccluders = rState.bOccluders;
bOk = mGFX.drawLight(SpotLight) && bOk;
bOk = mGFX.endLighting() && bOk;
return bOk;

Step 9: Keep diagnostics outside the pass

After applying lighting, draw any failure notice, occluder outlines, and the HUD.

if(!renderLighting(mGFX, rState))
{
    mGFX.rectFilled(10, 58, 330, 78, 160, 0, 0, 220);
    renderText(pFont, 14, 60, "Lightmap pass failed");
}

if(rState.bOccluders)
{
    mGFX.rect(260, 205, 390, 295, 255, 80, 80, 210);
    mGFX.circle(610, 350, 56, 255, 80, 80, 210);
    mGFX.line(485, 130, 760, 260, 255, 80, 80, 210);
}

renderHUD(pFont, rState);

Step 10: Change ambient intensity

Cycle presets or adjust the active ambient intensity within the 0.0-1.0 range.

else if(event.key.key == SDLK_P)
{
    state.iAmbientPreset = (state.iAmbientPreset + 1) % iAmbientPresetCount;
    state.fAmbientIntensity = vAmbientPresets[state.iAmbientPreset].fIntensity;
}
else if(event.key.key == SDLK_A)
{
    state.fAmbientIntensity += 0.05f;
    if(state.fAmbientIntensity > 1.0f) state.fAmbientIntensity = 1.0f;
}
else if(event.key.key == SDLK_Z)
{
    state.fAmbientIntensity -= 0.05f;
    if(state.fAmbientIntensity < 0.0f) state.fAmbientIntensity = 0.0f;
}

Step 11: Select the cursor and finish cleanly

Use the existing cursor hotspot, and exit before another state update after quitting.

mLog.msg(LL_INFO, "  Select cursor ... ");
if(mC64.cursorMgr().select(state.idCursor) < 0 || !mC64.cursorMgr().show())
{
    logTaskFailed(mLog);
    Main::terminate();
    return -1;
}
logTaskOk(mLog);
if(!bRunning) break;
state.fMouseX = mC64.cursorMgr().getX();
state.fMouseY = mC64.cursorMgr().getY();
state.iCurrentRenderRate = static_cast<Sint32>(mC64.timer().getCurrentRFR());
state.iCurrentLogicRate = static_cast<Sint32>(mC64.timer().getCurrentLFR());
updateLogic(state);

Complete source

Use the source file as the authoritative version of this tutorial.

  • View Tutorial_11_Lightmap.cpp
  • Input archive: Tutorial.cdc
  • Lighting API: GFXLight, GFX::beginLighting(), GFX::endLighting()
  • Log: Tutorial_11_Lightmap.log

Previous tutorial

Create and control particle emitters.

Go to Tutorial 10: Particles

Tutorial index

Back to tutorials

Next tutorial

Build panels and handle widget events.

Go to Tutorial 12: GUI