v2026.9.4
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. Targets the **3.3 core profile**, forward-compatible. That is the highest common denominator across the platforms Objeck ships: Windows, Linux and macOS desktop all reach it, and macOS caps at 4.1 core so nothing above 4.1 is portable there. GLES-only hardware (Raspberry Pi and similar) is out of scope by construction. Layers, from thinnest to friendliest -- all public, so nothing is capped: * GLWindow -- a window and a 3.3 core context, opened and paced for you. * GL -- static state operations. Thin, but never per-vertex. * Shader -- a linked program; Flat/Textured/TexturedFog/Normals built in, so the common cases need no GLSL at all. * Mesh -- geometry uploaded once into buffer objects, drawn many times, with Cube/Plane/Quad/Sphere built in, OBJ loading, and instancing. * Texture2D -- a texture, from a file or generated; Solid/Checker built in. * Light -- one directional light plus ambient, written into a shader. * RenderTarget -- draw into a texture instead of the window, then sample it. * Material -- a surface: its texture, tint and shininess. * Frustum -- the six planes of a view, for skipping what is off screen. * ShadowMap -- shadows from a directional or spot light, depth pass and all. * PointShadow -- shadows from a point light, in every direction, via a cube map. * Overlay -- text and rectangles over the scene, positioned in pixels. * Transform -- position, rotation and scale, with a cached matrix. * Box, Scene -- a world of boxes: draws itself, and answers collision. ## Why the API is shaped this way The native call boundary is expensive: the VM resolves each native symbol by string on EVERY call (GetProcAddress/dlsym) and boxes every argument into a fresh holder. A 1:1 mapping of OpenGL onto that would be thousands of lookups and allocations per frame. So each call here does real work -- 'compile a program from two sources' is one call, not the five GL calls it decomposes into -- and bulk data crosses as whole arrays, never element by element. This happens to be exactly what GL 3.3 core wants anyway: upload geometry once, then draw with few calls. ## Adding a call One `void fn(VMContext&)` in the OpenGL section of core/lib/sdl/sdl.cpp, and one method here that names it. See that file's header for the two rules that matter (positional slot indices, and keeping each call coarse).

ShadowMap

Shadows, from one directional light. The technique is standard and the fiddly parts are all here rather than in each program: render the scene from the light's point of view into a depth texture, then when drawing normally, ask that texture whether each fragment is further from the light than whatever the light could see. If it is, something is in the way. Use it with Shader->LitShadowed or LitTexturedShadowed, which declare the uniforms ApplyTo writes. ## What Cover is for, and why it is not automatic A directional light has no position, so its view of the scene has no natural extent -- something has to say how much world the map covers. Too little and shadows stop at an invisible line; too much and the map's resolution is spread so thin that shadow edges turn to staircases. There is no good default, because the right answer is the size of the thing being lit, so it is a parameter rather than a guess. ## Bias, and why there is any A depth map is a grid of samples, so a surface's own recorded depth is almost never exactly its depth when looked up again. Without a margin, half of every lit surface reads as being behind itself and the result is a fine dark stipple over everything -- "shadow acne". The bias is that margin. Too small brings the acne back; too large and shadows detach from what casts them, which is why the default is small and adjustable rather than generous.

Implements: Freeable

Example

shadows := ShadowMap->New(1024);
shadows->SetLight(light);
shadows->Cover(Vector3->New(0.0, 0.0, 0.0), 14.0);

# per frame, before drawing anything else
shadows->Begin();
shadows->DrawCaster(floor_mesh, floor);
shadows->DrawCaster(ball_mesh, ball);
shadows->End(window);

# then the normal pass
shadowed->Use();
shadows->ApplyTo(shadowed);
# ... draw as usual ...

Operations

ApplyTo #

Bind the map and tell a shader how to read it. Writes "light_space", "shadow_map", "shadow_bias" and "shadow_texel" -- skipping any the shader does not declare, so this can be handed an unshadowed program without filing a diagnostic.

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

Parameters

NameTypeDescription
shaderShaderthe program about to draw

Begin #

Start the depth pass: draw into the map, from the light. Clears it, points the viewport at it, and makes the depth shader current. Everything drawn until End goes into the map. Culling is left exactly as the caller set it. Reversing it for this pass -- recording each object's far side rather than its near side -- is a known way to reduce self-shadowing without a large bias, but it only works for closed solids: a flat floor has no far side and would stop casting altogether. Since a floor is usually the first thing anyone wants to cast onto AND from, the bias is the safer lever, and SetBias is there for it.

method : public : Begin() ~ Nil

Capture #

Draw every box in a Scene into the map.

method : public : Capture(scene:Scene) ~ Nil

Parameters

NameTypeDescription
sceneScenethe scene whose boxes cast shadows

Cover #

What area of the world the map covers.

method : public : Cover(center:Vector3, extent:Float) ~ Nil

Parameters

NameTypeDescription
centerVector3the middle of the shadowed area
extentFloathalf its width, in world units

DrawCaster #

Draw one caster into the map.

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

Parameters

NameTypeDescription
meshMeshits geometry
transformTransformwhere it is

DrawCasterMatrix #

Draw one 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 #

End the depth pass and go back to drawing at the window.

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

Parameters

NameTypeDescription
windowGLWindowthe window to return to

Free #

Release the depth target and the depth shader. Call it while the GL context is still current -- see the shutdown order on the GL class.

method : public : Free() ~ Nil

GetError #

Why it could not be set up.

method : public : GetError() ~ String

Return

TypeDescription
Stringthe reason, or an empty string

GetLightSpaceMatrix #

The light's projection times its view -- what a caster's position has to be multiplied by to find where it lands in the map.

method : public : GetLightSpaceMatrix() ~ Float[]

Return

TypeDescription
Floatthe matrix, 16 floats column-major

GetSpotOuterRadians #

The spot's outer half-angle in radians, recovered from the cosine it is stored as.

method : private : GetSpotOuterRadians() ~ Float

GetTexture #

The depth map as a texture, for inspecting what is in it.

method : public : GetTexture() ~ Texture2D

Return

TypeDescription
Texture2Dthe depth texture, or Nil when the map is not usable

IsOk #

method : public : IsOk() ~ Bool

Return

TypeDescription
Booltrue when the depth target and its shader are both usable

IsSpot #

method : public : IsSpot() ~ Bool

Return

TypeDescription
Booltrue when casting from a spot rather than a directional light

New # constructor

A square shadow map.

New(size:Int)

Parameters

NameTypeDescription
sizeIntthe map's width and height in pixels; 1024 is a reasonable start, 2048 for sharper edges over a large area

RebuildSpot #

The light-space matrix for a spot: a perspective projection from where the spot is, along where it points, as wide as its outer cone.

method : private : RebuildSpot() ~ Nil

SetBias #

The depth margin that keeps a lit surface from shadowing itself. Default 0.0025.

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

Parameters

NameTypeDescription
biasFloatthe margin in light-space depth

SetDirection #

method : public : SetDirection(direction:Vector3) ~ Nil

Parameters

NameTypeDescription
directionVector3toward the light, matching Light's convention

SetLight #

Take the direction from a Light, so the shadows and the shading agree. Worth doing rather than setting the direction twice: a shadow map pointing somewhere other than the light produces shadows on the wrong side of everything, which looks like a bug in the shadows and is really two numbers that were meant to be one.

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

Parameters

NameTypeDescription
lightLightthe light casting them

SetSpotLight #

Cast from a spot light, using a perspective projection matched to its cone. The map covers exactly what the spot lights, so its resolution goes where the light goes -- which is why a spot shadow can look sharp at a size where a directional one covering the same scene would be blocky. Cover is ignored in this mode: the spot's own range and cone say what it reaches.

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

Parameters

NameTypeDescription
lightLighta spot light; anything else is treated as directional

SetTextureUnit #

Which texture unit the map is bound to. Default 1, because unit 0 is where the ordinary texture goes.

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

Parameters

NameTypeDescription
unitIntthe texture unit