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

Particles

A pool of short-lived camera-facing sprites. Sparks, smoke, dust, embers, muzzle flashes, magic. Everything needed for these already existed -- billboards face the camera, a PropBatch draws thousands in one call, additive blending needs no sorting -- and every program still had to write the same three things by hand: a ring of slots, a life per slot, and somewhere to park the dead ones. The FPS demo that ships with Objeck wrote exactly that, which is what this replaces. ## Fixed capacity, on purpose The pool never grows. Emitting into a full one overwrites the OLDEST particle rather than allocating, so a frame costs no allocation however hard it is emitting -- which is the property that matters in a draw loop, and the reason a growable pool would be the wrong shape even though it would look more accommodating. Dead particles are parked far below the world rather than hidden, because a batch draws every instance in its buffer: there is no per-instance visibility to switch off, so "invisible" has to mean "nowhere you are looking". ## What varies per particle, and what does not Position, velocity, age and size are per particle. COLOUR is not: a batch has one material, so a system is one colour, set by SetTexture or SetMaterial. Two colours are two systems. Per-particle colour would mean a second instance attribute stream, which PropBatch does not carry. Additive by default, which is right for anything that emits light and wrong for smoke -- SetAdditive(false) for that, and then the sprite's own alpha does the work.

Implements: Freeable

Example

sparks := Particles->New(64);
sparks->SetCamera(camera);
sparks->SetTexture(glow);
scene->AddBatch(sparks->GetBatch());
...
sparks->Update(window->GetDelta());
sparks->Burst(x, y, z, 12, 2.5);

Operations

Burst #

Emit several at once, sprayed outwards. The usual shape for an impact: a handful of particles leaving one point in scattered directions.

method : public : Burst(x:Float, y:Float, z:Float, count:Int, speed:Float) ~ Nil

Parameters

NameTypeDescription
xFloatwhere
yFloatwhere
zFloatwhere
countInthow many
speedFloathow fast they leave, in world units per second

Clear #

Retire every particle at once.

method : public : Clear() ~ Nil

Draw #

Draw the system on its own, for one that is not part of a Scene. A system inside a Scene should NOT be drawn this way as well -- hand its batch to Scene->AddBatch and let the scene do it, so the additive state is set up around the draw.

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

Parameters

NameTypeDescription
view_projectionFloatthe camera matrix

Emit #

Emit one particle, motionless.

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

Parameters

NameTypeDescription
xFloatwhere
yFloatwhere
zFloatwhere

Emit #

Emit one particle with a velocity. Overwrites the oldest slot when the pool is full, rather than growing or refusing. A system emitting faster than its capacity allows shows shorter trails, which is a visible and recoverable symptom -- unlike an allocation in a draw loop.

method : public : Emit(x:Float, y:Float, z:Float, vx:Float, vy:Float, vz:Float) ~ Nil

Parameters

NameTypeDescription
xFloatwhere
yFloatwhere
zFloatwhere
vxFloatworld units per second
vyFloatworld units per second
vzFloatworld units per second

Free #

Release the batch, and the mesh and shader if this system built them. Idempotent. A mesh or shader handed in by the three-argument constructor belongs to the caller and is left alone.

method : public : Free() ~ Nil

GetAlive #

method : public : GetAlive() ~ Int

Return

TypeDescription
Inthow many particles are currently alive

GetBatch #

The batch this system draws through. Hand it to Scene->AddBatch and the scene draws it in the right place, with the additive state handled. Or call Draw directly for a system that is not part of a scene.

method : public : GetBatch() ~ PropBatch

Return

TypeDescription
PropBatchthe batch; do NOT free it, the system owns it

GetCapacity #

method : public : GetCapacity() ~ Int

Return

TypeDescription
Inthow many can be alive at once

GetError #

What went wrong, if anything.

method : public : GetError() ~ String

Return

TypeDescription
Stringthe batch's error, or an empty string

IsOk #

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

method : public : IsOk() ~ Bool

Return

TypeDescription
Booltrue when it can draw

New # constructor

A system with its own quad and billboard shader, both owned.

New(capacity:Int)

Parameters

NameTypeDescription
capacityInthow many particles can be alive at once

New # constructor

A system drawing geometry you supply. The shader must be an instanced one that reads a "view" uniform, which in practice means Shader->BillboardInstanced or something shaped like it. Neither the mesh nor the shader is owned, so Free leaves both alone.

New(capacity:Int, mesh:Mesh, shader:Shader)

Parameters

NameTypeDescription
capacityInthow many particles can be alive at once
meshMeshthe sprite geometry, usually Mesh->Quad
shaderShaderan instanced billboard program

NextSigned #

A signed random in -1..1, from a generator of this system's own.

method : private : NextSigned() ~ Float

PARKED # function

Where dead particles wait. Far enough below any plausible world that a camera would have to be looking for them.

function : PARKED() ~ Float

SetAdditive #

Additive blending, on by default. Right for anything that emits light. Turn it off for smoke or dust, where the sprite should obscure what is behind it rather than brighten it -- and note that non-additive particles are NOT sorted against each other, so overlapping soft-alpha sprites will blend in instance order.

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

Parameters

NameTypeDescription
additiveBoolfalse for ordinary blending

SetCamera #

The camera a billboard shader needs. Required -- without it the sprites collapse onto the origin's basis.

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

Parameters

NameTypeDescription
cameraCamerathe eye

SetDrag #

How quickly a particle loses speed, as a fraction per second. 0 keeps its velocity forever; 1.0 brings it to a near stop within a second. What makes a spray settle rather than fly off.

method : public : SetDrag(drag:Float) ~ Nil

Parameters

NameTypeDescription
dragFloat0.0 to about 4.0

SetGravity #

Downward acceleration, in world units per second squared. 0 by default, which suits sparks and magic. Around -9.8 for anything that should fall; a small positive value makes smoke rise.

method : public : SetGravity(gravity:Float) ~ Nil

Parameters

NameTypeDescription
gravityFloatacceleration on the y axis

SetLifetime #

How long a particle lives, in seconds.

method : public : SetLifetime(seconds:Float) ~ Nil

Parameters

NameTypeDescription
secondsFloatmust be above zero

SetMaterial #

The material, for a tint or an opacity as well as a texture.

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

Parameters

NameTypeDescription
materialMaterialhow every particle is drawn -- one for the whole system

SetSeed #

Make the spray reproducible. Bursts use a generator of their own rather than the system's, so the same seed gives the same spray on every machine and every run -- which is worth more in a demo people compare than novelty is.

method : public : SetSeed(seed:Int) ~ Nil

Parameters

NameTypeDescription
seedIntany non-zero value

SetSize #

The size a particle starts and ends at, in world units. Shrinking to zero is how a particle disappears here, because the alpha is the same for every instance -- see the note on the class.

method : public : SetSize(start:Float, finish:Float) ~ Nil

Parameters

NameTypeDescription
startFloatsize when emitted
finishFloatsize at the end of its life

SetTexture #

The sprite texture, untinted.

method : public : SetTexture(texture:Texture2D) ~ Nil

Parameters

NameTypeDescription
textureTexture2Dwhat each particle draws

Update #

Move, age and retire every particle. Call once per frame with the frame time. Nothing is allocated: the pool, the velocities and the lives are all fixed arrays, and the batch repacks into a buffer it already owns.

method : public : Update(delta:Float) ~ Nil

Parameters

NameTypeDescription
deltaFloatseconds since the last frame, e.g. GLWindow->GetDelta