v2026.9.7
All Bundles
Bundle

OpenGL 3.3 core rendering on top of SDL2. Builds on sdl2.obl for the window and context; this bundle is the GL side. Compile with -lib sdl2. Layers, thinnest first: GLWindow (window and context), GL (state), Shader, Mesh, Texture2D, Light, RenderTarget, Material, Frustum, ShadowMap, PointShadow, Overlay, Transform, and Box/Scene. All public, so nothing is capped. Targets the 3.3 core profile, forward-compatible: the highest common denominator across the desktop platforms Objeck ships, since macOS caps at 4.1 core. GLES-only hardware is out of scope by construction. See docs/opengl.md for setup, a minimal program, why the calls are coarse-grained, and how to add one.

PointShadow

Shadows from a point light, in every direction at once. A point light shines everywhere, so no single flat map can hold its shadows. The scene is rendered SIX times, once down each axis, into the six faces of a cube map -- and then sampled by direction rather than by a projected coordinate. Use it with Shader->LitTexturedPointShadowed.

What it costs

Six passes over the casters, against one for a directional or spot light. That is the price of omnidirectional shadows and there is no way around it at this level -- the cheaper techniques (rendering all six faces in one pass with a geometry shader, or dual-paraboloid maps) are a different design rather than an optimisation of this one. So: use a spot where a spot will do. A lamp in a room a viewer only sees one side of does not need six faces.

Distance, not depth

The faces store the DISTANCE from the light in world units, scaled by the light's range -- not the usual projected depth. That is the trick that makes this tractable: the six faces do not share a projection, so a comparison in projected depth would have to know which face a lookup landed on and undo that face's projection. A distance means the same thing on every face. It also disposes of the bias problem spot shadows have. Projected depth is non-linear so a constant bias means different things at different distances; a world-unit distance is linear, so one bias is right everywhere.

Implements: Freeable

Example

shadows := PointShadow->New(512);
shadows->SetLight(lamp);

# once a frame
shadows->Capture(scene);

# then the visible pass
lit->Use();
rig->ApplyTo(lit);
shadows->ApplyTo(lit);

Operations

ApplyTo #

Bind the cube and tell a shader how to read it. Writes "point_light_position", "point_light_range", "point_bias" and "shadow_cube", skipping any the shader does not declare.

method : public : ApplyTo(shader:Shader) ~ Nil

Parameters

NameTypeDescription
shaderShaderthe program about to draw

BeginFace #

Start drawing into ONE face. Faces run 0..5 in GL's order: +X, -X, +Y, -Y, +Z, -Z. Everything drawn until the next BeginFace or End goes into that face. Call Capture instead unless the casters are not in a Scene.

method : public : BeginFace(face:Int) ~ Nil

Parameters

NameTypeDescription
faceInt0..5

BindCubeTo #

Bind the cube for sampling on a chosen unit, without touching a shader. For inspecting what the six faces hold -- which is the only way to tell a rotated face from a correct one, since a symmetric scene looks the same either way on screen.

method : public : BindCubeTo(unit:Int) ~ Nil

Parameters

NameTypeDescription
unitIntthe texture unit

BuildFace #

The six views from the light, one per face. The up vectors are not a free choice: GL specifies how each cube face is oriented, and a face rendered with a different up is stored rotated. The lookup would then read the right face and the wrong part of it -- shadows that are present, plausible, and in the wrong place on two faces out of six.

method : private : BuildFace() ~ Nil

Parameters

NameTypeDescription

Capture #

Render a whole Scene into all six faces, then go back to the window. The one call that does the entire depth stage.

method : public : Capture(scene:Scene, window:GLWindow) ~ Nil

Parameters

NameTypeDescription
sceneScenethe casters
windowGLWindowthe window to return to

DrawCaster #

Draw one caster into the face that BeginFace selected.

method : public : DrawCaster(mesh:Mesh, transform:Transform) ~ Nil

Parameters

NameTypeDescription
meshMeshits geometry
transformTransformwhere it is

DrawCasterMatrix #

Draw a caster with a model matrix rather than a Transform.

method : public : DrawCasterMatrix(mesh:Mesh, model:Float[]) ~ Nil

Parameters

NameTypeDescription
meshMeshits geometry
modelFloatits model matrix

End #

Finish the depth stage and go back to drawing at the window.

method : public : End(window:GLWindow) ~ Nil

Parameters

NameTypeDescription
windowGLWindowthe window to return to

Free #

Release the cube map and its shader. Call it while the GL context is still current -- see the shutdown order on the GL class.

method : public : Free() ~ Nil

GetError #

method : public : GetError() ~ String

Return

TypeDescription
Stringwhy it could not be set up, or an empty string

IsOk #

method : public : IsOk() ~ Bool

Return

TypeDescription
Booltrue when the cube map and its shader are both usable

New # constructor

A cube shadow map.

New(size:Int)

Parameters

NameTypeDescription
sizeIntthe width and height of EACH of the six faces; 512 is a reasonable start, and remember there are six of them

SetBias #

The depth margin, in WORLD UNITS -- unlike ShadowMap's, which is in projected depth. Default 0.06.

method : public : SetBias(bias:Float) ~ Nil

Parameters

NameTypeDescription
biasFloatthe margin

SetLight #

Take the position and range from a point light. Worth doing rather than setting them twice: a cube rendered from somewhere other than the light puts every shadow in the wrong place, and the receiving shader matches the cube to a light BY POSITION, so a mismatch means the shadow is silently applied to no light at all.

method : public : SetLight(light:Light) ~ Nil

Parameters

NameTypeDescription
lightLighta point light

SetLightAt #

method : public : SetLightAt(position:Vector3, range:Float) ~ Nil

Parameters

NameTypeDescription
positionVector3where the light is
rangeFloathow far it reaches

SetTextureUnit #

Which texture unit the cube is bound to. Default 2, leaving 0 for the ordinary texture and 1 for a flat ShadowMap.

method : public : SetTextureUnit(unit:Int) ~ Nil

Parameters

NameTypeDescription
unitIntthe texture unit