3D Lighting
The built-in forward pipelines support four light types with Cook-Torrance PBR shading (surface shading that models how light reflects off real materials). Lights are added per-frame via fluent members inside your view function. (On raylib the pipeline is ForwardPbrPipeline; on MonoGame it is ForwardPipeline.)
What and Why
- Ambient light: Base illumination for the entire scene. One per frame.
- Directional light: Parallel rays (sun, moon). Supports shadow casting.
- Point light: Radial light with position, radius, and falloff. Supports shadow casting.
- Spot light: Cone-shaped light with inner/outer cutoff angles.
All lights are struct types with builder functions. The pipeline uploads them as shader uniforms each frame.
Quick start
let view (ctx: GameContext) (model: Model) (buffer: RenderBuffer3D) =
buffer
.beginCamera(camera)
// Ambient
.setAmbientLight(AmbientLight3D.create (Color(30, 30, 30, 255)))
// Directional (sun)
.addDirectionalLight(
DirectionalLight3D.create (Vector3(0.3f, -0.7f, -0.5f))
|> DirectionalLight3D.withIntensity 0.8f
)
// Point light (torch)
.addPointLight(
PointLight3D.create (torchPos, 10f)
|> PointLight3D.withColor Color.Orange
|> PointLight3D.withIntensity 1.5f
|> PointLight3D.withCastsShadows true
)
// Spot light (flashlight)
.addSpotLight(
SpotLight3D.create (camPos, camDir, 20f)
|> SpotLight3D.withIntensity 2.0f
)
.model(model.PlayerModel, model.PlayerTransform)
.endCamera()
.drop()
Light types
AmbientLight3D
Uniform base illumination applied to all surfaces.
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
(required) |
Base color |
|
|
|
Brightness multiplier |
AmbientLight3D.create (Color(30, 30, 30, 255))
|> AmbientLight3D.withIntensity 0.5f
DirectionalLight3D
Parallel light rays. Use for sun or moon.
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
(required) |
Direction rays travel (should be normalized) |
|
|
|
Light color |
|
|
|
Brightness multiplier |
|
|
|
Whether to cast shadows |
DirectionalLight3D.create (Vector3(0.3f, -0.7f, -0.5f))
|> DirectionalLight3D.withColor Color.White
|> DirectionalLight3D.withIntensity 0.8f
|> DirectionalLight3D.withCastsShadows true
PointLight3D
Radial light that emits in all directions from a position.
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
(required) |
World-space position |
|
|
|
Light color |
|
|
|
Brightness multiplier |
|
|
(required) |
Maximum distance of influence |
|
|
|
Decay exponent (1 = linear, 2 = quadratic) |
|
|
|
Whether to cast shadows |
|
|
|
Per-light bias override (uses pipeline default when |
PointLight3D.create (Vector3(10f, 5f, 0f), 15f)
|> PointLight3D.withColor Color.Orange
|> PointLight3D.withIntensity 1.5f
|> PointLight3D.withFalloff 2.0f
|> PointLight3D.withCastsShadows true
|> PointLight3D.withShadowBias 0.005f
_TIP_: Set
CastsShadows = truesparingly. Each shadow-casting point light renders its own shadow-map pass: a single-face capture aimed along the light'sShadowDirection, not a six-face cubemap. Two or three is a good target for performance.
SpotLight3D
Cone-shaped light with inner and outer cutoff angles.
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
(required) |
World-space position |
|
|
(required) |
Direction the cone points (should be normalized) |
|
|
|
Light color |
|
|
|
Brightness multiplier |
|
|
(required) |
Maximum distance of influence |
|
|
|
Cosine of inner cone half-angle (full brightness) |
|
|
|
Cosine of outer cone half-angle (fade to zero) |
|
|
|
Whether to cast shadows |
|
|
|
Per-light bias override |
SpotLight3D.create (camPos, camDir, 25f)
|> SpotLight3D.withIntensity 2.0f
|> SpotLight3D.withCutoff 0.9f 0.95f // tight beam
|> SpotLight3D.withCastsShadows true
Light limits
Both built-in pipelines default to the same light budgets:
Type |
Default max |
|---|---|
Ambient |
1 |
Directional |
1 |
Point lights |
8 |
Spot lights |
4 |
How you change them differs by backend:
raylib: the budgets are runtime-configurable via the ForwardPbrPipeline constructor:
let pipeline = ForwardPbrPipeline(
maxPointLights = 16,
maxSpotLights = 8
)
MonoGame: the budgets are baked into the compiled PBR shader (MAX_POINT_LIGHTS /
MAX_SPOT_LIGHTS constants in ForwardPbr.fx; note the file is named after the raylib
pipeline, but this is the MonoGame shader). The ForwardPipeline constructor takes no
light-count argument; to change them you recompile the .fx with different #defines.
Budgets apply per camera block: in buffers with more than one camera block, each block gets its own ambient slot and light arrays (see Lights across camera blocks); in single-camera buffers they apply to the whole frame.
Exceeding the limit silently drops extra lights on both backends. For directional lights specifically, only the first directional light in the active set is shaded, and only it can cast shadows; later directional lights in the same set are ignored entirely.
Lights across camera blocks
In single-camera buffers, lights are frame-global. In buffers with more than one camera block, lights are scoped per camera block: a block that issues its own light commands starts from the frame defaults (lights emitted before the first camera block or between blocks) and applies its own commands in order; a block that issues none inherits the previous block's set plus any lights emitted in between. A block can add lights but cannot remove inherited ones, and lights emitted after the last block affect nothing. See Buffers & Commands → Light scoping for the full placement rules.
Shadows follow the same scoping. Each camera block with shadow-casting lights renders its own
shadow map, so a multi-block buffer costs one shadow pass per block. .setShadowOrigin(...)
applies only to the block it appears in; the .enableShadows()/.disableShadows() toggle
carries into later blocks' initial state. Single-camera buffers behave exactly as single-pass
frames: one shadow map for the whole frame, no per-block cost.
Shadow configuration
Global bias
Shadow bias values control the tradeoff between two shadow artifacts: too low a bias and
surfaces speckle with self-shadowing noise (acne); too high and shadows detach from their
objects (peter-panning). Set via ShadowBiasConfig:
// raylib:
let pipeline = ForwardPbrPipeline(
shadowBiasConfig = {
DirectionalBias = 0.0005f // raylib default
PointBias = 0.01f
SpotBias = 0.001f
SlopeScaleBias = 0.0005f
}
)
// MonoGame:
let pipeline = ForwardPipeline(
shadowBias = {
DirectionalBias = 0.002f // MonoGame default (higher; native depth bias)
PointBias = 0.01f
SpotBias = 0.001f
SlopeScaleBias = 0.0005f
}
)
_NOTE_: On MonoGame,
SlopeScaleBiasmaps to the nativeRasterizerState.SlopeScaleDepthBias(hardware polygon offset) and the per-type biases map toRasterizerState.DepthBias; MonoGame can't use the GLSLdFdx/dFdyslope math the raylib shader relies on. On raylib the slope-scale bias is applied in the GLSL depth shader. The observable effect (tuning acne vs peter-panning) is the same.
Per-light bias
Point and spot lights can override the global bias:
PointLight3D.create (pos, radius)
|> PointLight3D.withCastsShadows true
|> PointLight3D.withShadowBias 0.005f // per-light override
Atlas configuration
The shadow atlas controls resolution and caster capacity. The single directional light is
the dominant shadow in most scenes, so by default it gets a dedicated region of the atlas
(DirectionalAtlasRatio, default 0.5) rather than sharing one tile of the caster grid;
this keeps directional shadows high-resolution without tuning MaxCasters to your light
count. Point/spot casters subdivide the remaining atlas area into a square grid. Set
DirectionalAtlasRatio to 1.0 for directional-only scenes (the directional light gets
the whole atlas) or 0.0 to restore the uniform grid (every caster shares a
1/MaxCasters tile). MaxCasters must be a perfect square (4, 9, 16, 25, 36); a
constraint of the uniform grid, but enforced at atlas construction either way.
// raylib:
let pipeline = ForwardPbrPipeline(
shadowAtlasConfig = {
ShadowAtlasConfig.defaults with
Resolution = 4096
DirectionalLightSize = ValueSome 30.f
}
)
// MonoGame (note the extra DirectionalOriginY field):
let pipeline = ForwardPipeline(
shadowAtlas = {
ShadowAtlasConfig.defaults with
Resolution = 4096
DirectionalOriginY = 0.0f // MonoGame-only: lock shadow frustum Y
}
)
Higher
Resolutionproduces sharper shadows but costs proportionally more: the shadow pass re-renders shadow-casting geometry into the directional region every frame, so doubling the resolution roughly doubles the shadow-pass fragment work. 4096 is a good high-quality default; 8192 is expensive. NOTE: directional far-plane coverage. The directional shadow camera's far plane is the light distance plus the full ortho size (DirectionalLightSize) plus a one-unit margin. Geometry farther than that from the light (measured along the light direction) is clipped and stops casting shadows; receivers there render fully lit. Deep scenes that slope away from the light (terrain, tall structures at the frustum edge) may need a largerDirectionalLightSize(at the cost of texel density) or aDirectionalOriginY/OriginStrategythat centers the frustum on the action.
Field |
Default |
raylib |
MonoGame |
Description |
|---|---|---|---|---|
|
2048 |
✓ |
✓ |
Atlas texture resolution (square) |
|
16 |
✓ |
✓ |
Maximum point/spot shadow casters (perfect square). Unused for the directional caster when |
|
0.5 |
✓ |
✓ |
Fraction of the atlas the directional light occupies. |
|
|
✓ |
✓ |
Where directional shadows are centered ( |
|
auto |
✓ |
✓ |
Distance to place the directional light camera behind the origin |
|
auto |
✓ |
✓ |
Full height of the directional shadow ortho window, in world units |
|
0.0 |
(none) |
✓ |
Lock the shadow frustum's vertical origin (prevents vertical sliding) |
|
2.0 |
✓ |
✓ |
Snap shadow origin to a grid to reduce shimmer |
_NOTE: shadow technique differs._ MonoGame cannot create a sampleable depth-only render target, so it writes shadow depth into an R32F color attachment (
DepthShadow.fx) and samples it with a manual 3×3 PCF over point-sampled depth; the same kernel the raylib backend runs against its real depth texture. The user-facing config and behavior (shadow quality, bias tuning) are equivalent; only the internal path differs.
See also
- Overview: Architecture and pipeline setup
- Buffer & Commands: The buffer pipeline pattern
- Materials: PBR material system
Mibo