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

Camera

A first-person camera: a position plus a yaw/pitch heading, producing the view matrix each frame. Yaw and pitch are held instead of a look-at target because that is what input drives -- turning is a change in yaw, not a new target point. Pitch is clamped just short of straight up/down, since at exactly vertical the view direction becomes parallel to 'up' and no camera basis exists. Movement is horizontal: MoveForward walks along the heading with the pitch ignored, so looking down does not sink you into the floor. That is what a walking character wants; a flying one would use ForwardVector directly.

Example

camera := Camera->New(Vector3->New(0.0, 1.7, 5.0));   # eye height
camera->Turn(0.03);
camera->MoveForward(0.1);
shader->SetMatrix4("view", camera->GetViewMatrix());

Operations

ForwardVector #

The direction the camera is looking, including pitch.

method : public : ForwardVector() ~ Vector3

Return

TypeDescription
Vector3a unit vector

GetProjection #

method : public : GetProjection() ~ Float[]

Return

TypeDescription
Floatthe projection matrix. BORROWED -- do not write to it or keep it across a SetAspect

GetViewMatrix #

The view matrix for this frame.

method : public : GetViewMatrix() ~ Float[]

Return

TypeDescription
Floata 4x4 view matrix, column-major

GetViewMatrixInto #

The view matrix, into a buffer the caller owns. The allocating form makes four objects every call -- three Vector3 and the Float[16] -- and a draw loop calls it every frame. This makes none: the basis is computed straight into the destination.

method : public : GetViewMatrixInto(out:Float[]) ~ Nil

Parameters

NameTypeDescription
outFloatdestination, at least 16 elements

GetViewProjection #

Projection times view, rebuilt from the current eye. Returns the camera's own buffer rather than a fresh array, so a draw loop calling this every frame allocates nothing. Do not write to it, and do not hold it across another call.

method : public : GetViewProjection() ~ Float[]

Return

TypeDescription
Floata 16-element column-major matrix, or Nil if SetPerspective has not been called

GetViewProjectionInto #

Projection times view, ready to hand to Scene->Draw. The two-step version -- build a projection, then multiply it by the view -- is what both 3D examples did, and it is the last per-frame allocation in a draw loop that is otherwise clean.

method : public : GetViewProjectionInto(out:Float[], projection:Float[], scratch:Float[]) ~ Nil

Parameters

NameTypeDescription
outFloatdestination, at least 16 elements
projectionFloata projection matrix, built once and kept
scratchFloata second 16-element buffer this may use; must not be out

GroundForward #

The heading on the ground plane, pitch ignored.

method : public : GroundForward() ~ Vector3

Return

TypeDescription
Vector3a unit vector with no vertical component

GroundRight #

The camera's right-hand direction on the ground plane.

method : public : GroundRight() ~ Vector3

Return

TypeDescription
Vector3a unit vector with no vertical component

HasProjection #

method : public : HasProjection() ~ Bool

Return

TypeDescription
Booltrue once SetPerspective has been called

Look #

Look up or down. Clamped to just under +/- 90 degrees, because at exactly vertical the view direction is parallel to 'up' and the camera basis collapses.

method : public : Look(radians:Float) ~ Nil

Parameters

NameTypeDescription
radiansFloatpositive looks up

LookAtPoint #

Turn to face a point, without moving. Camera is a yaw-and-pitch camera, which is right for a first-person view and awkward for anything that watches something: an orbiting or tracking shot knows where it wants to look, not what angles that is. This converts. Leaves roll alone, because there isn't any -- the up vector stays world-up, which is what keeps a scene from feeling tilted.

method : public : LookAtPoint(target:Vector3) ~ Nil

Parameters

NameTypeDescription
targetVector3the point to face

MoveForward #

Walk along the heading. Vertical component ignored, so looking down does not push the eye through the floor.

method : public : MoveForward(distance:Float) ~ Nil

Parameters

NameTypeDescription
distanceFloathow far; negative walks backwards

MoveRight #

Strafe sideways.

method : public : MoveRight(distance:Float) ~ Nil

Parameters

NameTypeDescription
distanceFloathow far; negative goes left

New # constructor

New(position:Vector3)

Parameters

NameTypeDescription
positionVector3where the eye starts

PlaceLookingAt #

Place the eye and face a point in one call -- an orbiting or tracking shot.

method : public : PlaceLookingAt(position:Vector3, target:Vector3) ~ Nil

Parameters

NameTypeDescription
positionVector3where the eye goes
targetVector3what it looks at

ScreenRay #

The ray running through a point on the screen, in world space. Scene->Raycast could already answer "what is in front of the camera", but only along the camera's own forward vector -- the middle of the screen. A mouse pick, a click-to-select, a drag onto a tile: all of them start from a PIXEL, and there was no way to turn one into a direction. Pass mouse coordinates and the WINDOW size, not the drawable size. Both come from the same space -- GetMouseX against GetWidth -- and mixing points with high-DPI pixels doubles the offset on a Retina display in a way that looks like a calibration error rather than a unit mismatch.

method : public : ScreenRay(x:Int, y:Int, width:Int, height:Int) ~ Vector3

Parameters

NameTypeDescription
xIntpixel across, from the left
yIntpixel down, from the TOP
widthIntthe window's width
heightIntthe window's height

Return

TypeDescription
Vector3a unit direction; the camera's forward vector if the view cannot be inverted, which keeps callers off a Nil check

SetAspect #

Update the aspect ratio, rebuilding the projection only if it changed. Cheap enough to call unconditionally every frame, which is the point: a window can be resized at any moment and the alternative is tracking a generation counter in every program.

method : public : SetAspect(aspect:Float) ~ Nil

Parameters

NameTypeDescription
aspectFloatwidth divided by height

SetPerspective #

Give the camera its own projection, so GetViewProjection can build the whole matrix without the caller keeping buffers.

method : public : SetPerspective(fov:Float, aspect:Float, near:Float, far:Float) ~ Nil

Parameters

NameTypeDescription
fovFloatvertical field of view, in RADIANS
aspectFloatwidth divided by height -- window->GetAspect()
nearFloatnear plane; too small a value wastes depth precision
farFloatfar plane

SetPerspectiveDegrees #

Same, in degrees, with the near and far planes most programs want.

method : public : SetPerspectiveDegrees(degrees:Float, aspect:Float) ~ Nil

Parameters

NameTypeDescription
degreesFloatvertical field of view in degrees
aspectFloatwidth divided by height

SetPosition #

Place the eye directly.

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

Parameters

NameTypeDescription
positionVector3the new position

Turn #

Turn left or right.

method : public : Turn(radians:Float) ~ Nil

Parameters

NameTypeDescription
radiansFloatpositive turns one way, negative the other

Unproject #

Where a point on the near or far plane lands in world space. The perspective divide is the part that matters: the inverse of a projection is not an affine transform, so w comes back as something other than 1 and dividing by it is what turns clip space into a world position.

method : private : Unproject() ~ Vector3

Parameters

NameTypeDescription