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).

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