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

Shader

A linked shader program, built from vertex and fragment source. One native call does the whole compile-and-link, so a failure comes back as a log rather than as five separate error checks. Check IsOk() before drawing; GetLog() carries the compiler's message when it is false.

Implements: Freeable

Example

shader := Shader->New(vertex_source, fragment_source);
if(<>shader->IsOk()) {
shader->GetLog()->ErrorLine();
return;
};
shader->Use();

Operations

AddLitVaryings # function

The outputs every lit vertex shader must write, declared in ONE place. Both lit vertex sources -- the ordinary one and the instanced one -- feed the same fragment shader, so they have to agree about this list exactly. They did not: world_position was added here for point lights and specular and the instanced source was not updated, which macOS's linker rejected outright while Windows and Linux linked it happily and left the value undefined. So point lights and specular on instanced geometry were reading garbage on two platforms out of three and only the strictest one said so. Declared once, and asserted by a test that reads a point light through the instanced shader rather than only checking that it compiles.

function : AddLitVaryings(source:ShaderSource, shadowed:Bool) ~ Nil

Parameters

NameTypeDescription
sourceShaderSourcethe shader being built
shadowedBoolwhether the shadow varying is needed too

BillboardInstanced # function

Camera-facing quads, drawn many at a time. A billboard is a quad that always turns to face the viewer, and it is what a particle, a spark, a sprite enemy, a distant tree and a floating label all are. None of them had any route here: every shader in this bundle puts geometry where its model matrix says, and a billboard is defined by refusing to. Drive it with a PropBatch over Mesh->Quad, which is why this is the instanced form and there is no single-billboard variant -- one sprite is a batch of one, and a thousand are still one draw call. sparks := PropBatch->New(Mesh->Quad(0.5), Shader->BillboardInstanced()); sparks->SetCamera(camera); sparks->SetAdditive(true); sparks->SetMaterial(Material->New(spark_texture)); ## How it faces you The instance matrix supplies the CENTRE and the SIZE and its rotation is ignored: the quad's corners are rebuilt in world space from the camera's own right and up vectors, which are the first two ROWS of the view matrix's upper 3x3. So SetScale(w, h, 1.0) on an instance's transform sets the sprite's width and height, and turning it does nothing -- deliberately. This is why SetCamera exists on PropBatch. The batch is handed a view_projection by Scene and that alone cannot be decomposed back into the camera's basis, so the view matrix comes separately. Uniforms: "view_projection", "view", the sampler "tex", "tint" and "opacity" -- the last two being what Material writes, so a material works here exactly as it does on a lit prop. Fragments below 2% alpha are DISCARDED rather than blended, so a sprite sheet's transparent border does not write depth and punch holes in whatever is drawn after it.

function : BillboardInstanced() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

DepthOnly # function

Writes depth and nothing else: the shadow map's own pass. No colour output at all, because there is no colour buffer attached to a depth-only target to write one into. Its only uniform is "mvp", which for this pass is the LIGHT's projection times view times the model matrix -- ShadowMap->DrawCaster does that arithmetic.

function : DepthOnly() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

Flat # function

A shader that fills with one colour, set through the "color" uniform as three floats. The uniform every built-in shader here shares is "mvp", a mat4. Set it with SetMatrix4 and nothing else is required.

function : Flat() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

Free #

Release the program. GL objects are invisible to the collector, so this is not optional. Call it while the GL context is still current -- see the shutdown order on the GL class. After GLContext->Delete there is no context for the underlying glDeleteProgram to act on.

method : public : Free() ~ Nil

FromFiles # function

Compile a program from two files. Worth it for anything beyond a few lines: a driver's error log reports GLSL line numbers, and those mean nothing when the source was assembled from string literals scattered through a method.

function : FromFiles(vertex_file:String, fragment_file:String) ~ Shader

Parameters

NameTypeDescription
vertex_fileStringpath to the vertex shader
fragment_fileStringpath to the fragment shader

Return

TypeDescription
Shaderthe program; IsOk() is false when either file could not be read, and GetLog says which

GetLog #

method : public : GetLog() ~ String

Return

TypeDescription
Stringthe compile/link log; empty when IsOk() is true

HasUniform #

Whether this program declares a uniform of this name. Ask before setting one that may not be there. A missing uniform is reported through GL->GetLastError, which is right for a typo and wrong for a deliberate "set it only if the shader wants it" -- Scene uses this to hand a model matrix to lit shaders without filing a diagnostic every frame for the unlit ones. Note GLSL removes uniforms it can prove are unused, so a declared but unreferenced uniform answers false. That is not a bug: there is nothing to set.

method : public : HasUniform(name:String) ~ Bool

Parameters

NameTypeDescription
nameStringthe uniform's name

Return

TypeDescription
Booltrue when it can be set

IsOk #

method : public : IsOk() ~ Bool

Return

TypeDescription
Booltrue when the program compiled and linked

Lit # function

Lit by one directional light, with a flat colour instead of a texture. Same uniforms as LitTextured, but "color" (a vec3) in place of "tex".

function : Lit() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

LitFragmentSource # function

The lit fragment program, optionally sampling a tangent-space normal map. ## Why there is no tangent vertex attribute Tangent-space normal mapping needs a tangent frame. The usual way is a fourth vertex attribute, which would change the vertex layout, the native upload path, and every mesh already built -- including OBJ files, which mostly do not carry tangents anyway. This derives the frame in the fragment shader from screen-space derivatives of the position and the texture coordinate instead. It costs a few instructions per pixel and it works with every mesh that already exists, unchanged. The trade is real: the frame is per-triangle rather than smoothly interpolated, so a very low-poly curved surface shows faceting that a vertex tangent would not. For dressing walls, floors and props -- what this library is for -- that is not visible.

function : LitFragmentSource(textured:Bool, shadowed:Bool, point_shadowed:Bool, normal_mapped:Bool) ~ String

Parameters

NameTypeDescription
texturedBoolsample an albedo texture rather than a flat colour
shadowedBoolsample a directional shadow map
point_shadowedBoolsample a point light's cube shadow
normal_mappedBoolperturb the normal with a tangent-space map

Return

TypeDescription
Stringthe fragment source

LitShader # function

Build a lit program and give its opacity a usable value. A GLSL uniform starts at ZERO, and `opacity` is declared and live in every lit fragment shader. So a program using one of these presets WITHOUT a Material or a Scene -- which is what three of the shipped examples do -- has alpha 0. It looks right only while blending is off; the moment anything calls GL->BeginTransparency the geometry vanishes, with no GL error to explain it.

function : LitShader() ~ Shader

Parameters

NameTypeDescription

LitShadowed # function

Lit and shadowed, with a flat colour instead of a texture.

function : LitShadowed() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

LitTextured # function

Textured and lit by one directional light. Uniforms: "mvp" and "model" (Scene sets both), the sampler "tex", and the three a Light writes -- "light_direction", "light_color" and "ambient". Optionally "fog_distance": set it to darken with distance, leave it alone for no fog. That is here rather than in a separate preset because Lit times Textured times Fogged would be eight programs for the sake of one multiply. ## The normal matrix, and why there is no Matrix3 here A surface normal cannot be transformed by the model matrix. Under non-uniform scale that tilts it off the surface, and the shading goes subtly wrong in a way that reads as a modelling mistake. The correct transform is the inverse transpose of the model matrix. Computing that would normally mean a Matrix3 type, a glUniformMatrix3fv binding and a per-object CPU inverse. GLSL has had `inverse` and `transpose` since 1.40, and this targets 3.30, so the shader does it instead -- one line, no new API, and nothing for a caller to forget.

function : LitTextured() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

LitTexturedInstanced # function

Textured and lit, taking its model matrix per INSTANCE rather than from a uniform. Use with Mesh->SetInstanceMatrices and DrawInstanced. Reads the matrix from attribute locations 3 to 6, and has no "model" uniform at all -- so Scene, which sets one, cannot drive this. That is the trade instancing makes: the placement stops being something the CPU sends per object and becomes part of the vertex stream.

function : LitTexturedInstanced() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

LitTexturedNormalMapped # function

Textured and lit, with a tangent-space normal map. Adds "normal_map" (texture unit 3) and "has_normal_map". Material sets both, so a material with no map drawn by this shader lights by its geometric normal exactly as LitTextured would. wall := Material->New(stone); wall->SetNormalMap(stone_bumps);

function : LitTexturedNormalMapped() ~ Shader

Return

TypeDescription
Shadera new program

LitTexturedNormalMappedPointShadowed # function

Textured, lit, shadowed by a POINT light's cube map, and normal mapped. The combination the lantern demo wants: a carried light, shadows in every direction, and stone that is not flat.

function : LitTexturedNormalMappedPointShadowed() ~ Shader

Return

TypeDescription
Shadera new program

LitTexturedNormalMappedShadowed # function

Textured, lit, shadowed by a directional light, and normal mapped.

function : LitTexturedNormalMappedShadowed() ~ Shader

Return

TypeDescription
Shadera new program

LitTexturedPointShadowed # function

Textured and lit, with shadows from a POINT light's cube map. Adds "shadow_cube", "point_light_position", "point_light_range" and "point_bias" to LitTextured's uniforms, all written by PointShadow->ApplyTo. Note this is a separate program from LitTexturedShadowed rather than an option on it: a samplerCube and a sampler2D are different types, and a shader carrying both would pay for a cube map lookup in scenes that have no point light. Pick the one the scene needs.

function : LitTexturedPointShadowed() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

LitTexturedShadowed # function

Textured, lit, and shadowed. Use it with a ShadowMap. Adds "light_space", "shadow_map", "shadow_bias" and "shadow_texel" to LitTextured's uniforms, all of which ShadowMap->ApplyTo writes -- so a program sets none of them by hand.

function : LitTexturedShadowed() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

New # constructor

Compile and link a program.

New(vertex_source:String, fragment_source:String)

Parameters

NameTypeDescription
vertex_sourceStringGLSL vertex shader source, starting with "#version 330 core"
fragment_sourceStringGLSL fragment shader source

Normals # function

Draws each surface's normal as a colour: +X red, +Y green, +Z blue. A diagnostic, and the one worth reaching for first when geometry looks wrong. A face whose normal points the wrong way, a mesh whose attributes landed in the wrong slots, and a transform that has quietly mirrored something are all invisible under a texture and obvious here.

function : Normals() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

SetFloat #

Set a float uniform. Makes this program current -- see SetMatrix4.

method : public : SetFloat(name:String, value:Float) ~ Nil

Parameters

NameTypeDescription
nameStringthe uniform's name in the shader
valueFloatthe value

SetInt #

Set an int uniform. Also how a sampler is pointed at a texture unit. Makes this program current -- see SetMatrix4.

method : public : SetInt(name:String, value:Int) ~ Nil

Parameters

NameTypeDescription
nameStringthe uniform's name in the shader
valueIntthe value

SetMatrix4 #

Set a mat4 uniform. SETTING A UNIFORM MAKES THIS PROGRAM CURRENT, as if Use had been called. That is deliberate rather than incidental: glUniform* writes to whatever program is bound, not to the one the name was looked up in, so a setter that did not bind would silently write another shader's uniforms whenever two were alive. The consequence to know is that setting uniforms across several shaders leaves the last one bound -- so call Use immediately before Draw, not once at startup.

method : public : SetMatrix4(name:String, matrix:Float[]) ~ Nil

Parameters

NameTypeDescription
nameStringthe uniform's name in the shader
matrixFloat16 floats in column-major order, as Matrix4 produces

SetVec3 #

Set a vec3 uniform: a colour, a direction, a position. Makes this program current -- see SetMatrix4.

method : public : SetVec3(name:String, x:Float, y:Float, z:Float) ~ Nil

Parameters

NameTypeDescription
nameStringthe uniform's name in the shader
xFloatfirst component
yFloatsecond component
zFloatthird component

SetVec3 #

Set a vec3 uniform from a Vector3.

method : public : SetVec3(name:String, value:Vector3) ~ Nil

Parameters

NameTypeDescription
nameStringthe uniform's name in the shader
valueVector3the vector

SetVec4 #

Set a vec4 uniform: four floats packed into one, which is how a light's kind and cone travel without spending three more uniform arrays. Makes this program current -- see SetMatrix4.

method : public : SetVec4(name:String, x:Float, y:Float, z:Float, w:Float) ~ Nil

Parameters

NameTypeDescription
nameStringthe uniform's name in the shader
xFloatfirst component
yFloatsecond component
zFloatthird component
wFloatfourth component

Sky # function

The sky: a vertical gradient with a sun disc, evaluated per pixel from the view direction. Uniforms: "view_projection" (the camera's rotation only -- Skybox strips the translation), "horizon", "zenith", "sun_color", "sun_direction", "sun_size" and "has_sun". gl_Position uses xyww rather than xyzw so the depth after the perspective divide is exactly 1.0 -- the far plane. With GL_LEQUAL that would let the sky draw where nothing else has; Skybox turns depth WRITES off instead, which achieves the same thing without depending on the depth function the caller happens to have set.

function : Sky() ~ Shader

Return

TypeDescription
Shadera new sky program

Textured # function

A shader that samples a texture. Uniforms: "mvp" and the sampler "tex". Reads texture coordinates from LOCATION 2, matching every Mesh primitive -- location 1 is the normal. Bind a texture to unit 0 and call SetInt("tex", 0).

function : Textured() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

TexturedFog # function

Textured, darkening with distance. Without lighting, a textured 3D scene reads flat -- every surface is the same brightness however far away it is, so the eye cannot tell depth. Fog is the cheapest fix and both 3D examples had hand-rolled their own version of it. Uniforms: "mvp", "tex", and "fog_distance" if the default of 40 units is wrong for your scene.

function : TexturedFog() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

TexturedTinted # function

Textured, multiplied by a flat colour. What a HUD needs: one white texture drawn as any colour, and glyphs -- whose shape lives in their alpha -- drawn as any colour without re-rasterising them. Uniforms: "mvp", "tex", "overlay_tint" (a vec4, default opaque white).

function : TexturedTinted() ~ Shader

Return

TypeDescription
Shaderthe program; check IsOk()

Use #

Make this program current for subsequent draws.

method : public : Use() ~ Nil