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

PropBatch

Many copies of one mesh in a single draw call. Instancing has worked at the Mesh level since it landed -- SetInstanceMatrices and DrawInstanced, with Shader->LitTexturedInstanced to read the per-instance matrices. What was missing was any route to it from the object layer: Scene and Prop both set an "mvp" uniform per object, and the instanced shader has no "mvp", so pointing a Scene at it drew nothing at all. This is that route. A batch is one mesh, one material, many placements: 400 trees, one draw call, one uniform upload. ## One mesh, one material -- not a limitation of this class That is what the hardware does. Every instance runs the same program over the same vertices and the only thing that varies is the per-instance matrix, so a batch cannot hold two different meshes or give one tree different bark. Two kinds of tree are two batches. Worth stating plainly, because the obvious expectation of a thing called a batch is a container that accepts anything. ## Static versus dynamic The transforms are objects the caller keeps and can move at any time, and nothing tells the batch when that happens. So by default the matrices are repacked and re-uploaded every frame, which is always correct and still costs exactly one draw call -- the saving that matters most. A batch that does not move should say so with SetDynamic(false): it then packs once and never again, and the caller calls MarkMoved() if that ever stops being true. Getting that wrong is visible immediately -- the batch renders at its old placements -- rather than subtly, which is why the fast path is the one you opt into rather than the default. ## Culling is all or nothing GetBoundsRadius covers every instance, so a batch is either wholly submitted or wholly skipped. Per-instance culling would mean repacking and re-uploading the buffer whenever visibility changed, which costs more than the draw calls it saves. Batch things that are near each other; a batch spread across the whole world is never off screen and so never culls.

Implements: Freeable

Example

trees := PropBatch->New(trunk_mesh);
trees->SetMaterial(bark);
for(i := 0; i < 400; i += 1;) {
trees->Add(x, 0.0, z);
};
trees->SetDynamic(false);
...
trees->Draw(camera->GetViewProjection());

Operations

Add #

Place another instance.

method : public : Add(transform:Transform) ~ Nil

Parameters

NameTypeDescription
transformTransformwhere it goes; held, not copied, so moving it later moves the instance

Add #

Place another instance at a point, unrotated and unscaled.

method : public : Add(x:Float, y:Float, z:Float) ~ Transform

Parameters

NameTypeDescription
xFloatwhere to put it
yFloatwhere to put it
zFloatwhere to put it

Return

TypeDescription
Transformthe new transform, to turn or scale it

Clear #

Drop every instance. The mesh, shader and material are untouched.

method : public : Clear() ~ Nil

Draw #

Draw every instance, using the batch's own light.

method : public : Draw(view_projection:Float[]) ~ Nil

Parameters

NameTypeDescription
view_projectionFloatthe camera matrix

Draw #

Draw every instance under a light supplied by the caller. This is what Scene calls, so a batch added to a lit scene does not have to be told about the light twice. A batch with its own light or rig keeps it: the arguments are what to use when the batch has none.

method : public : Draw(view_projection:Float[], light:Light, rig:LightRig) ~ Nil

Parameters

NameTypeDescription
view_projectionFloatthe camera matrix
lightLighta light to fall back on, or Nil
rigLightRiga rig to fall back on, or Nil; wins over the light

Free #

Release the shader, if this batch built it. The mesh is never freed here: it is the caller's, and is very often shared with a Prop or another batch. Idempotent.

method : public : Free() ~ Nil

Get #

method : public : Get(index:Int) ~ Transform

Parameters

NameTypeDescription
indexIntwhich instance

Return

TypeDescription
Transformits transform, or Nil if the index is out of range

GetBoundsRadius #

Radius of a sphere containing every instance. Covers the instance PLACEMENTS plus the mesh's own extent at the largest scale any instance uses, so it holds however the instances are turned.

method : public : GetBoundsRadius() ~ Float

Return

TypeDescription
Floatthe radius in world units, or 0 if nothing has been packed yet

GetBoundsX #

Centre of a sphere containing every instance, on the x axis. Valid once the batch has packed, which happens on its first draw. Zero before then -- which is why Scene tests a batch's visibility only after it has drawn at least once.

method : public : GetBoundsX() ~ Float

Return

TypeDescription
Floatthe x coordinate

GetBoundsY #

method : public : GetBoundsY() ~ Float

Return

TypeDescription
Floatthe y coordinate of the bounding sphere's centre

GetBoundsZ #

method : public : GetBoundsZ() ~ Float

Return

TypeDescription
Floatthe z coordinate of the bounding sphere's centre

GetCamera #

method : public : GetCamera() ~ Camera

Return

TypeDescription
Camerathe camera, or Nil

GetCount #

method : public : GetCount() ~ Int

Return

TypeDescription
Inthow many instances the batch holds

GetError #

What went wrong, if anything. Empty when nothing has.

method : public : GetError() ~ String

Return

TypeDescription
Stringthe last error, or an empty string

GetLight #

method : public : GetLight() ~ Light

Return

TypeDescription
Lightthe light, or Nil

GetLightRig #

method : public : GetLightRig() ~ LightRig

Return

TypeDescription
LightRigthe rig, or Nil

GetMaterial #

method : public : GetMaterial() ~ Material

Return

TypeDescription
Materialthe material, or Nil

GetMesh #

method : public : GetMesh() ~ Mesh

Return

TypeDescription
Meshthe mesh every instance draws

GetShader #

method : public : GetShader() ~ Shader

Return

TypeDescription
Shaderthe program this batch draws with

IsAdditive #

method : public : IsAdditive() ~ Bool

Return

TypeDescription
Boolwhether this batch draws additively

IsDynamic #

method : public : IsDynamic() ~ Bool

Return

TypeDescription
Boolwhether the batch repacks every frame

IsOk #

Is the batch usable -- a live mesh and a live shader.

method : public : IsOk() ~ Bool

Return

TypeDescription
Booltrue when it can draw

IsVisible #

method : public : IsVisible() ~ Bool

Return

TypeDescription
Boolwhether the batch draws

MarkMoved #

A transform changed and the batch is not dynamic; repack on the next draw. Harmless on a dynamic batch, which repacks regardless.

method : public : MarkMoved() ~ Nil

New # constructor

A batch drawing the given mesh with a built-in instanced shader. The shader is created and OWNED here, so Free releases it. Use the two-argument constructor to supply your own, which is then the caller's to free.

New(mesh:Mesh)

Parameters

NameTypeDescription
meshMeshthe geometry every instance draws; NOT owned by the batch

New # constructor

A batch drawing the given mesh with a shader you supply. The shader must read the per-instance model matrix from attribute locations 3 to 6 and take the camera through a "view_projection" uniform. Shader->LitTexturedInstanced is the built-in one; anything with an "mvp" uniform is the ordinary per-object shape and will not work here.

New(mesh:Mesh, shader:Shader)

Parameters

NameTypeDescription
meshMeshthe geometry; NOT owned by the batch
shaderShaderan instanced program; NOT owned by the batch

Pack #

Repack the matrices, upload them, and recompute the bounding sphere. The bounds are computed here rather than in a method of their own because both walk the same transforms, and walking a thousand of them twice is the sort of thing this class exists to avoid.

method : private : Pack() ~ Nil

SetAdditive #

Draw this batch with ADDITIVE blending, and no depth writes. For anything that emits light rather than blocks it: sparks, fire, glows, muzzle flashes, magic. Colours accumulate, so bright places get brighter and black contributes nothing -- which is why a sprite sheet for additive work has a black background rather than a transparent one. The reason this is the blending mode offered here and ordinary transparency is not: addition COMMUTES, so additive geometry needs no back-to-front sort. Ordinary alpha blending does, and a batch is one draw call over an instance buffer with no per-instance ordering in it -- so offering it would mean offering something that renders wrongly whenever two sprites overlap.

method : public : SetAdditive(additive:Bool) ~ Nil

Parameters

NameTypeDescription
additiveBooltrue for additive blending

SetCamera #

The camera whose basis a billboard shader needs. Only read by a shader that declares a "view" uniform -- Shader->BillboardInstanced does; nothing else here does. Harmless otherwise.

method : public : SetCamera(camera:Camera) ~ Nil

Parameters

NameTypeDescription
cameraCamerathe eye, or Nil

SetDynamic #

Whether the matrices are repacked every frame. True by default, which is always correct. False packs once and is the right answer for anything that does not move -- scenery, a grid, a crowd that stands still -- but then a transform changed afterwards has no effect until MarkMoved is called.

method : public : SetDynamic(dynamic:Bool) ~ Nil

Parameters

NameTypeDescription
dynamicBooltrue to repack every frame

SetLight #

Light the batch. A batch drawn through a Scene inherits the scene's light when it has none of its own.

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

Parameters

NameTypeDescription
lightLightthe light, or Nil

SetLightRig #

Light the batch with a rig, which wins over a single light.

method : public : SetLightRig(rig:LightRig) ~ Nil

Parameters

NameTypeDescription
rigLightRigthe rig, or Nil

SetMaterial #

How every instance is drawn. One material for the whole batch -- see the note on the class.

method : public : SetMaterial(material:Material) ~ Nil

Parameters

NameTypeDescription
materialMaterialthe material, or Nil to inherit whatever is bound

SetVisible #

Draw or skip the whole batch.

method : public : SetVisible(visible:Bool) ~ Nil

Parameters

NameTypeDescription
visibleBoolfalse to skip it entirely