2025-11-07

The math in the demos/examples and the engine got a thorough scrubbing.

https://gitlab.com/keid/meta/-/raw/main/resolvers/keid-engine-2025-11-07.yaml (GHC 9.10.3 / LTS-24.18)

Following the MoltenVK changes, the compatibility extension is now required on MacOS. But it isn’t even optional for the rest and is guarded by CPP instead. This allows RenderDoc to take snapshots.

New packages:

The scrubbing

The rewrite was guided by a refusal to remain confused and a fixed set of “I want that to behave like this” targets.

Matrix-matrix and matrix-vector multiplication must match GLSL

Precisely, what it would do with P * V * M * pos.

One of the huge sources of confusion was GLSL doing Aᵀ * v inside. But so did the SIMD implementation for efficiency. Taken together, they kinda worked it out. While pre-transposing the data may be a cool trick, it does screw up the composition. In effect, they do pos * M * V * P, but lie about it.

Both sides claim to be intuitive, so I had to pick one and stick to it:

  • The newPos = oldPos * Tr reads as “oldPos transformed by Tr”. And then transformed by Tr2, Tr3, etc.
  • The newPos = Tr * oldPos reads as “transformation Tr applied to oldPos”. Yeah, like the functions: P(V(M(pos))).

Well, the functions they are, then! (!*) is application, (<>) is composition (and there’s an identity to it, nice).

(A <> B) !* v should be equal to A !* (B !* v) and this is where the original design was broken – on a premise that the composition is equivalent to putting a transform on a stack mconcat (tr : stack). But we don’t actually want to keep the stack and re-concat it each time. So doing it the correct way (stack <> tr) is even better!

The “aquarium” model of a screen.

The 2D window provided by the system has the (0,0) origin in the top-left corner. This remains the same for the textures, framebuffer, etc. 3D adds depth to it by extending away from the viewer, behind the display surface.

The “first-person” camera.

The (0,0) shifts to being right in the middle of the view. It departs from the memory model, but only by a trivial shift (no flips/rotations). Following the aquarium model for depth, the “away” direction is +Z. So, the ray cast from the middle of the screen has exactly the direction of (0,0,1).

Reverse-depth by default.

Perspective projection has the natural infinite far plane. The “forward” remains (0,0,1). The “far” points converge towards (0,0) on the screen by virtue of being multiplied by their depth.

This allows quite dramatic expansion of the scene range, deferring the need for multilayer composition for longer.

The projections provided by geomancy are now flipping the depth range.

This is invisible to the world and only concerns the depth testing. The pipelines should inform the GPU to do the different test. Shadowmap samplers, too, will have to be adjusted too, but that’s it.

Rotation in the screen plane is clockwise.

An angle that’s increasing with time will turn the clock hands drawn on the screen in the expected direction. This happily matches the quaternion rotation around the “forward” axis.

The axis rotations are quaternion rotations around that axis.

This also happily matches the expectation of the Sun rising in the morning (rotation around the “right” direction). And also turning left (towards -X) is a negative rotation, while turning right (towards +X) is a positive one.

The +Y is down.

This is the most contentious thing. When you have seen those “coordinate systems used by …” charts, there’s no “-Y up”.

But this flows from the aquarium screen and the first-person camera. If you take a screenshot of the world, “print” it in a game, and keep it at your nose (the near plane), it should match exactly. This should make hopping between the world-space, view-space, and “HUD”-space easier.

Together with sticking to quaternion rotations, this gives you a familiar “airplane” controls roll-pitch-yaw.

And “down” is the direction of where a thing would fall: more time - more Y.

If this is unpalatable, you can trivially recover “+Z up” and join Blender, Source, etc by attaching a rotateX (pi/2) transformation to the perspective. Then it can keep using the right-handed transforms, but with “+Y forward” controls. Which kinda makes sense too, if you disregard the screen or keep it as a separate magisterium.