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

Transform

Where something is, how it is turned, and how big it is -- with its matrix built on demand and cached. This is what an object that MOVES wants, as opposed to Box, which is a static prop and caches a translate-and-scale. A program animating something otherwise has to keep its own scratch buffers and remember the order the three parts compose in. ## No allocation after construction The matrix is one buffer owned by the Transform, rebuilt only when something actually changed. Setting the same position twice costs nothing, and a static object costs nothing per frame at all. Translation, rotation and scale compose as T * R * S -- scale first, then rotate, then move -- which is the order that behaves the way people expect: scaling does not drag the object away from its position, and rotation happens about its own centre. Composing them in any other order is a common and confusing bug, so the order is not a choice here.

Example

ball := Transform->New();
ball->SetScale(0.5);

# per frame
ball->SetPosition(x, height, z);
ball->Rotate(0.0, spin * delta, 0.0);
ball->MvpInto(mvp, view_projection);
shader->SetMatrix4("mvp", mvp);
mesh->Draw();

Operations

GetMatrix #

The model matrix, T * R * S. Rebuilt only when something changed, into a buffer this object owns -- so the returned array is NOT a copy. Do not hold on to it across a change, and do not write to it.

method : public : GetMatrix() ~ Float[]

Return

TypeDescription
Floatthe model matrix, 16 floats column-major

GetMaxScale #

The largest of the three scale factors. What a bounding sphere needs: a sphere has one radius, so a non-uniform scale has to be bounded by its largest component or the bound is wrong in the direction that was scaled most.

method : public : GetMaxScale() ~ Float

Return

TypeDescription
Floatthe largest scale component

GetParent #

method : public : GetParent() ~ Transform

Return

TypeDescription
Transformthe parent, or Nil when this transform is in world space

GetPosition #

method : public : GetPosition() ~ Vector3

Return

TypeDescription
Vector3the position as a new Vector3

GetVersion #

How many times this transform's matrix has been rebuilt. A child reads its parent's number to decide whether its own composed matrix is still good. Exposed because the mechanism is worth being able to see; a program has no reason to call it.

method : public : GetVersion() ~ Int

Return

TypeDescription
Inta counter

GetWorldMatrix #

The matrix that places this transform in the WORLD. The same as GetMatrix for an unparented transform, and parent * local for a parented one, composed up the whole chain. Recomputed only when something actually changed -- this transform, or any ancestor. That check is a version comparison rather than a notification, so nothing has to register with anything and a parent does not need to know its children exist.

method : public : GetWorldMatrix() ~ Float[]

Return

TypeDescription
Floatthe world matrix; do not modify it

GetWorldMaxScale #

The largest scale factor this transform is under, parents included. Taken from the world matrix's own columns rather than by multiplying the chain's scale values: a rotation between two scales makes those two things different, and the columns are what the geometry is actually built from. Absolute, so a mirrored transform reports a positive size. A negative radius culls an object that is dead centre in view.

method : public : GetWorldMaxScale() ~ Float

Return

TypeDescription
Floatthe world scale, at least 0

GetWorldX #

Where this transform actually is, after its parents are taken into account. GetX and its pair report the LOCAL position -- what SetPosition was given, relative to the parent. These report the world one, which is what a cull test, a distance sort or anything else asking "where is it" wants. The two are the same for an unparented transform, and that is exactly why the difference is easy to miss: everything worked until the first parented object, and then it worked everywhere except at the edge of the screen.

method : public : GetWorldX() ~ Float

Return

TypeDescription
Floatthe x coordinate in world space

GetWorldY #

method : public : GetWorldY() ~ Float

Return

TypeDescription
Floatthe y coordinate in world space

GetWorldZ #

method : public : GetWorldZ() ~ Float

Return

TypeDescription
Floatthe z coordinate in world space

HasParent #

method : public : HasParent() ~ Bool

Return

TypeDescription
Booltrue when this transform hangs off another

Move #

Shift by an offset.

method : public : Move(dx:Float, dy:Float, dz:Float) ~ Nil

Parameters

NameTypeDescription
dxFloatx offset
dyFloaty offset
dzFloatz offset

MvpInto #

Model-view-projection, into a buffer the caller owns. The per-frame call. Uses the WORLD matrix, so a parented transform draws where the hierarchy puts it rather than where its local values say.

method : public : MvpInto(out:Float[], view_projection:Float[]) ~ Nil

Parameters

NameTypeDescription
outFloatdestination, at least 16 elements
view_projectionFloatthe camera's projection times view

New # constructor

At the origin, unrotated, unscaled.

New()

Rebuild #

T * R * S without a single matrix product. Two identities make this cheap. Scaling by a diagonal matrix on the RIGHT scales each COLUMN of the rotation, and translating on the LEFT only writes the last column -- so the whole composition is the rotation with its columns scaled and a translation dropped in. Spelling it as Multiply(Multiply(T, R), S) would allocate four matrices and do 128 multiply-adds to reach the same sixteen numbers. Checked against that composition in the regression suite, because this is the kind of shortcut that is wrong in a way that still looks like a transform.

method : private : Rebuild() ~ Nil

Rotate #

Turn by an offset -- the per-frame form, multiplied by a delta.

method : public : Rotate(dyaw:Float, dpitch:Float, droll:Float) ~ Nil

Parameters

NameTypeDescription
dyawFloatchange in yaw
dpitchFloatchange in pitch
drollFloatchange in roll

SetParent #

Hang this transform off another, so it moves when the parent does. This is what makes articulated motion possible at all: a turret on a hull, a hand on an arm, a moon around a planet. Without it every part has to have its world position recomputed by the program each frame, which is the same arithmetic written once per part rather than once. Position, rotation and scale become RELATIVE to the parent. A child at (0, 1, 0) under a parent at (5, 0, 0) is at (5, 1, 0) in the world, and turning the parent swings the child around it. A cycle is refused rather than accepted and then hung on: attaching a transform to its own descendant would make GetWorldMatrix recurse until the stack ran out, and the crash would name neither of the two transforms involved.

method : public : SetParent(parent:Transform) ~ Bool

Parameters

NameTypeDescription
parentTransformthe transform to hang off, or Nil to return to world space

Return

TypeDescription
Booltrue when attached; false when it would have made a cycle

SetPosition #

method : public : SetPosition(x:Float, y:Float, z:Float) ~ Nil

Parameters

NameTypeDescription
xFloatx position
yFloaty position
zFloatz position

SetPosition #

method : public : SetPosition(position:Vector3) ~ Nil

Parameters

NameTypeDescription
positionVector3the position

SetRotation #

method : public : SetRotation(yaw:Float, pitch:Float, roll:Float) ~ Nil

Parameters

NameTypeDescription
yawFloatrotation about Y, applied first
pitchFloatrotation about X
rollFloatrotation about Z, applied last

SetScale #

method : public : SetScale(s:Float) ~ Nil

Parameters

NameTypeDescription
sFloatuniform scale

SetScale #

method : public : SetScale(x:Float, y:Float, z:Float) ~ Nil

Parameters

NameTypeDescription
xFloatscale along x
yFloatscale along y
zFloatscale along z