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

Mesh

Geometry uploaded to the GPU once and drawn many times. Vertices are interleaved and described by a layout: [3, 2] means each vertex is three floats of position followed by two of texture coordinate, bound to shader locations 0 and 1 in that order. Indices select vertices, so shared corners are stored once. Upload cost is paid at construction; Draw() is a single native call regardless of how many triangles it draws. That is deliberate -- per-vertex calls across the native boundary would be orders of magnitude slower.

Implements: Freeable

Example

# a unit quad: position(3) + texcoord(2)
vertices := [-0.5, -0.5, 0.0, 0.0, 0.0,
              0.5, -0.5, 0.0, 1.0, 0.0,
              0.5,  0.5, 0.0, 1.0, 1.0,
             -0.5,  0.5, 0.0, 0.0, 1.0];
indices := [0, 1, 2, 2, 3, 0];
mesh := Mesh->New(vertices, indices, [3, 2]);

Operations

Build #

What both constructors do. Objeck constructors cannot delegate to one another, so the body lives here rather than being written twice.

method : private : Build() ~ Nil

Parameters

NameTypeDescription

Corner # function

Write one interleaved position/normal/texcoord vertex.

function : Corner() ~ Nil

Parameters

NameTypeDescription

Corner3 # function

Write one face corner from an OBJ "v", "v/vt", "v//vn" or "v/vt/vn" token.

function : Corner3() ~ Int

Parameters

NameTypeDescription

Return

TypeDescription
Intthe next write offset

Cube # function

A cube spanning -1..1 on every axis, ready for Scene and Box. Not a "unit" cube: it is 2x2x2, because Box multiplies it by a HALF-extent. A cube built to -0.5..0.5 would draw every box at half the size it claims while collision kept using the full extents. See the note on Box. Culling-safe: every face is wound counter-clockwise seen from outside, so GL_CULL_FACE can be enabled without anything vanishing.

function : Cube() ~ Mesh

Return

TypeDescription
Meshthe mesh; check IsOk()

Cube # function

A cube of an arbitrary half-extent.

function : Cube(half:Float) ~ Mesh

Parameters

NameTypeDescription
halfFloatdistance from the centre to each face

Return

TypeDescription
Meshthe mesh; check IsOk()

CubeInterior # function

A cube seen from the INSIDE: a room, or a skybox. Mesh->Cube is wound counter-clockwise from outside, which is right for a solid object and useless for an enclosure -- put the camera inside one and every wall is back-facing, so GL_CULL_FACE removes the whole room and you are left looking through it. This is the same cube with its winding reversed and its normals turned inward, so it is both visible and lit from within. None of that is discoverable from the outside: building a room out of Cube and wondering where it went is the failure this exists to prevent.

function : CubeInterior(half:Float) ~ Mesh

Parameters

NameTypeDescription
halfFloatdistance from the centre to each wall

Return

TypeDescription
Meshthe mesh; check IsOk()

Draw #

Draw the whole mesh. Bind a Shader first.

method : public : Draw() ~ Nil

Draw #

Draw with a different topology. The same vertices and indices read as lines instead of triangles, which is how a wireframe, a debug ray, a grid or a plotted curve gets drawn -- and all of that was unreachable while the draw call hardcoded triangles. Note the indices have to suit the mode: an index list built as triangles (0,1,2, 2,3,0) drawn as GL_LINES pairs them up as (0,1), (2,2), (3,0), which is a mesh's outline plus some degenerate segments rather than a tidy wireframe. Build indices for the mode you mean.

method : public : Draw(mode:Int) ~ Nil

Parameters

NameTypeDescription
modeInta DrawMode

DrawInstanced #

Draw many copies in one call, using the data from SetInstanceMatrices.

method : public : DrawInstanced(count:Int) ~ Nil

Parameters

NameTypeDescription
countInthow many instances; 1 or fewer draws once, normally

DrawInstanced #

Draw many copies with a given topology.

method : public : DrawInstanced(count:Int, mode:Int) ~ Nil

Parameters

NameTypeDescription
countInthow many instances
modeInta DrawMode

FaceField # function

One slash-separated field of an OBJ face token: "12", "12/3", "12//4" or "12/3/4". Walks the string rather than using String->Split, which is wrong for this in two separate ways -- both found by a triangle that loaded successfully and rendered nothing: * A token containing NO separator splits into ZERO parts, not one. So "-3" yielded no fields at all, the position index came back missing, and every vertex of that face was left at the origin. A degenerate triangle draws nothing and raises no error. * Consecutive separators do not produce an empty field. "1//3" splits into "1" and "/3", so the `v//vn` form -- position and normal with no texture coordinate, which is what an exporter writes for an untextured model -- read "/3" as the texture index and lost the normal.

function : FaceField(token:String, index:Int) ~ String

Parameters

NameTypeDescription
tokenStringthe face token
indexIntwhich field, 0 for the position

Return

TypeDescription
Stringthe field, or an empty string when absent

FlatNormal # function

Give three consecutive vertices the normal of the plane they lie in, for a file that carried none.

function : FlatNormal() ~ Nil

Parameters

NameTypeDescription

Free #

Release the buffers. Call it while the GL context is still current -- see the shutdown order on the GL class.

method : public : Free() ~ Nil

FromObj # function

Load geometry from a Wavefront OBJ file. The gap this fills: every mesh was either a built-in primitive or an array of numbers written by hand, so nothing beyond boxes and spheres had any route in at all. OBJ is the right format to accept first -- it is text, every modelling tool exports it, and it needs no library. Produces the same [3, 3, 2] layout as the primitives, so it draws with the same shaders and no special handling. ## What it reads, and what it ignores Reads `v`, `vt`, `vn` and `f`. Faces may be triangles, quads or larger n-gons -- anything with more than three corners is fanned into triangles, which is correct for the convex faces a modeller produces. Indices may be negative, meaning relative to the end, which some exporters emit. Ignores `mtllib`, `usemtl`, `o`, `g`, `s` and comments. Materials are not read: a Mesh is geometry, and the texture is something the caller binds. That means a multi-material model loads as one mesh with one texture rather than as several -- worth knowing before wondering why a model came out one colour. ## Normals If the file has none -- and plenty do not -- a flat normal is computed per face from its winding. That is why every corner gets its own vertex rather than shared ones being deduplicated: sharing them would average the normals of adjoining faces and turn a hard edge into a smooth one, so a cube would shade like a ball. The cost is three vertices per triangle with no reuse, which for a ten-thousand-triangle model is a couple of megabytes. Deliberate: correct shading on arbitrary input beats a smaller buffer, and a loader that gets this wrong is very hard to diagnose from the picture. ## Winding OBJ is counter-clockwise seen from outside, the same convention as every primitive here, so GL_CULL_FACE works without anything extra. A model that vanishes under culling was exported inside out.

function : FromObj(file:String) ~ Mesh

Parameters

NameTypeDescription
fileStringthe .obj path

Return

TypeDescription
Meshthe mesh; IsOk() is false when the file could not be read, held no faces, or was rejected by GL. Note the first two are indistinguishable from here -- GL->GetLastError only sees what the native side was handed, so a missing file and an empty one look the same. Check the path yourself if telling them apart matters.

Example

model := Mesh->FromObj("ship.obj");
if(<>model->IsOk()) {
GL->GetLastError()->ErrorLine();
return;
};

# fit it in the scene, whatever size the file happened to use
transform := Transform->New();
transform->SetScale(2.0 / model->GetRadius());

GetCenter #

The middle of the bounding box. Rarely the origin for a loaded model -- a file's author had no reason to centre it -- so this is what to translate BY (negated) to bring a model to the origin before rotating it, or it will orbit rather than spin.

method : public : GetCenter() ~ Vector3

Return

TypeDescription
Vector3the centre

GetInstanceGeneration #

How many times this mesh's instance data has been replaced. The instance buffer belongs to the mesh. Anything holding a copy of what it last uploaded can compare this against the value it saw then, and know its data is still there rather than assuming so -- which is what PropBatch does to make a static batch safe when two batches share one mesh.

method : public : GetInstanceGeneration() ~ Int

Return

TypeDescription
Inta counter, starting at 0

GetMax #

The high corner of the bounding box.

method : public : GetMax() ~ Vector3

Return

TypeDescription
Vector3the maximum on each axis

GetMin #

The low corner of the bounding box, in model space.

method : public : GetMin() ~ Vector3

Return

TypeDescription
Vector3the minimum on each axis

GetRadius #

Distance from the centre to the furthest corner. The number that makes a loaded model usable: a file has whatever scale its author worked in, and a program that hardcodes a scale fits exactly one model. Dividing a target size by this fits any of them.

method : public : GetRadius() ~ Float

Return

TypeDescription
Floatthe radius, or 0 for an empty mesh

Example

transform->SetScale(2.0 / mesh->GetRadius());

GetSize #

The width, height and depth of the bounding box.

method : public : GetSize() ~ Vector3

Return

TypeDescription
Vector3the extents on each axis

IsOk #

method : public : IsOk() ~ Bool

Return

TypeDescription
Booltrue when the geometry uploaded

Measure #

Work out the bounding box from the vertex data. Assumes the FIRST attribute is the position, which is the convention every primitive here follows and what PositionNormalTexcoord documents. A layout that puts something else first gets bounds of that instead -- there is no way to tell from a list of component counts which one is the position.

method : private : Measure() ~ Nil

Parameters

NameTypeDescription

New # constructor

Upload geometry.

New(vertices:Float[], indices:Int[], layout:Int[])

Parameters

NameTypeDescription
verticesFloatinterleaved vertex data
indicesInttriangle indices into the vertex list
layoutIntcomponent count per attribute, in shader location order

New # constructor

Upload geometry with NO index buffer. The vertices are drawn in the order they are given, which is what a point cloud, a particle buffer, a debug line list and most generated geometry actually want. Building 0, 1, 2, 3, ... by hand purely to satisfy glDrawElements costs a second buffer and a loop to express the default. Which draw call is used is decided by the MESH, not by the caller: one built with indices always draws through them and one built without never does, so there is no way to ask for the wrong one. The vertex order still has to suit the topology -- six vertices drawn as GL_TRIANGLES are two triangles, and drawn as GL_LINES are three separate segments.

New(vertices:Float[], layout:Int[])

Parameters

NameTypeDescription
verticesFloatinterleaved vertex data
layoutIntcomponent count per attribute, in shader location order

NoMesh # function

A Mesh that reports IsOk() false, for a load that could not proceed. Empty arrays rather than Nil, because a bare Nil in Objeck infers as Nil rather than as the parameter's array type and will not compile.

function : NoMesh() ~ Mesh

Plane # function

A flat square on the XZ plane at y = 0, normal pointing up. Floors and ground planes.

function : Plane(half:Float, tiles:Float) ~ Mesh

Parameters

NameTypeDescription
halfFloatdistance from the centre to each edge
tilesFloathow many times the texture repeats across the square; needs a texture built with TextureWrap->GL_REPEAT to be visible

Return

TypeDescription
Meshthe mesh; check IsOk()

PositionNormalTexcoord # function

The layout every primitive above is built to: three floats of position, three of normal, two of texture coordinate, bound to shader locations 0, 1 and 2 in that order. Named rather than written as [3, 3, 2] at each site so a shader and the geometry it reads cannot drift apart silently -- a mismatch there binds the normal to the texcoord slot and produces a plausible-looking wrong picture.

function : PositionNormalTexcoord() ~ Int[]

Return

TypeDescription
Intthe component counts

Quad # function

A flat square on the XY plane at z = 0, normal pointing at the viewer. At half = 1.0 this exactly covers clip space, which is what a full-screen pass wants -- a post-process or a background drawn with an identity matrix.

function : Quad(half:Float) ~ Mesh

Parameters

NameTypeDescription
halfFloatdistance from the centre to each edge

Return

TypeDescription
Meshthe mesh; check IsOk()

Resolve # function

OBJ indices are 1-based, and may be NEGATIVE to mean "counting back from the end" -- which some exporters emit and a loader that assumes 1-based reads as a wild out-of-range index.

function : Resolve() ~ Int

Parameters

NameTypeDescription

Return

TypeDescription
Inta 0-based index, or -1 when there is none

SetInstanceMatrices #

Give this mesh a model matrix per instance, so many copies draw in one call. Each instance takes 16 floats, a column-major matrix, exactly as Matrix4 produces -- so an array of N*16 floats is N instances. Bound to attribute locations 3, 4, 5 and 6, because the primitives here use 0, 1 and 2 for position, normal and texture coordinate. Draw it with Shader->LitTexturedInstanced or an equivalent that declares the four mat4 slots. A shader expecting a "model" UNIFORM will not see these: instance data is per-vertex-stage input, not a uniform, which is the whole reason it does not cost a call per object. ## What this is for One draw call instead of one per object. A thousand boxes drawn the ordinary way is a thousand draw calls plus a thousand uniform uploads, and every native call here resolves its symbol by string -- so that cost lands twice. A grid, a particle system, a point cloud, a forest: anything where the same geometry appears many times with different placements. Below a few dozen objects it is not worth the bother. The break-even is lower here than in C, because of that per-call cost, but it is not zero.

method : public : SetInstanceMatrices(matrices:Float[]) ~ Bool

Parameters

NameTypeDescription
matricesFloatN * 16 floats, column-major

Return

TypeDescription
Booltrue when the upload succeeded; GL->GetLastError says why not

Example

grid := Float->New[count * 16];
for(i := 0; i < count; i += 1;) {
# write a matrix at i * 16
Matrix4->TranslationScaleInto(slot, x, y, z, s, s, s);
};
mesh->SetInstanceMatrices(grid);
# ... then, once per frame ...
mesh->DrawInstanced(count);

SetInstances #

Per-instance data with an explicit layout, for anything that is not a model matrix -- a colour per instance, a scalar, a texture index.

method : public : SetInstances(data:Float[], layout:Int[], base_location:Int) ~ Bool

Parameters

NameTypeDescription
dataFloatN * stride floats, where stride is the sum of the layout
layoutIntcomponent counts per attribute, each 1 to 4
base_locationIntthe first attribute location to use; 3 is the first free one after the primitives' position, normal and texture coordinate

Return

TypeDescription
Booltrue when the upload succeeded

Sphere # function

A UV sphere of radius 1, centred on the origin. Normals are the vertex positions, which is exact for a unit sphere and is why this reads as round under lighting rather than faceted.

function : Sphere(segments:Int, rings:Int) ~ Mesh

Parameters

NameTypeDescription
segmentsIntdivisions around the equator; 24 or more looks smooth
ringsIntdivisions from pole to pole; usually about half the segments

Return

TypeDescription
Meshthe mesh; check IsOk()

Words # function

Split on whitespace, dropping the empty pieces a run of spaces produces and stripping a trailing carriage return. Returns Nil for a blank or comment line, so callers can test one thing.

function : Words() ~ Vector<String>

Parameters

NameTypeDescription