Math
Vectors, rotations, matrices and camera lenses shared by scenes, physics and rendering, written so a per-frame system can do its math without allocating.
module: com.awakekt.awake.core:math
Desktop · Android · iOS · Web
The math module has no dependencies beyond 2D vectors, and every other module speaks its types. The coordinate system is right-handed and Y-up, and the camera looks along -Z.
Normalize a direction
Vec3f has two forms of most operations. A bare verb such as normalize() changes the vector and
returns it; the -ed form such as normalized() returns a new vector and leaves the original alone.
Use the changing form in a frame loop, on a vector you own:
Use the new-vector form when you still need the original:
| Changes the receiver, returns it | Returns a new value |
|---|---|
set, add, sub, scale, lerp, normalize, moveTowards |
normalized(), +, -, * scalar, cross |
dot, length3, distanceTo and the other queries change nothing.
Named vectors
val ones = Vec3f() // (1, 1, 1)
val origin = Vec3f.ZERO // (0, 0, 0), a fresh instance each time
val up = Vec3f.UP // (0, 1, 0); FORWARD is (0, 0, -1)
Vec3f.ZERO, ONE, UP, DOWN, LEFT, RIGHT, FORWARD and BACK each return a new instance,
so changing one cannot change it for every other caller.
Rotate
A Transform stores rotation as three angles in radians, applied X, then Y, then Z. For a rotation
you compose or apply to a point, use a Quat:
val quarterTurn = Quat.fromAxisAngle(Vec3f.UP, 90.angleRad)
val turned = quarterTurn.rotate(Vec3f.FORWARD) // (-1, 0, 0): counter-clockwise from above
90.angleRad converts degrees to radians. Quat.fromEuler(radians) builds the same rotation a
Transform describes, toEuler() goes back, and Quat.nlerp(a, b, t) blends two rotations.
Project with a lens
A Lens is a camera's eye, target, up, field of view and clip distances. The renderer supplies the
clip-space convention when the matrix is built, so pass the one your renderer uses:
val lens = Lens.perspective(eye = Vec3f(0f, 2f, 6f), center = Vec3f.ZERO, fovYDegrees = 60f)
val viewProjection = lens.viewProjectionMatrix(aspect = 16f / 9f, clipSpace = ClipSpace.WebGpu)
val pixel = lens.projectToViewport(Vec3f.ZERO, viewProjection, 1280f, 720f, ClipSpace.WebGpu)
projectToViewport returns the pixel a world point lands on, or null when it is behind the
camera; rayThroughViewport goes the other way, from a pixel to a world Ray for picking.
ClipSpace |
Y axis | Depth |
|---|---|---|
Vulkan |
down | 0 to 1 |
WebGpu |
up | 0 to 1 |
OpenGl |
up | -1 to 1 |
Test two 2D shapes for overlap
Box2 (an axis-aligned rectangle) and Circle2 are for 2D games. overlaps answers whether two shapes
share area, and touching counts. penetration also says how to get out: it fills an Overlap2 with a unit
normal and a depth, and moving the shape by normal * depth takes it just clear. It works for any
pair, box against box, circle against circle, or one of each.
val ground = Box2(x = 0f, y = -1f, halfWidth = 10f, halfHeight = 1f) // its top is at y = 0
val player = Box2(x = 0f, y = 0.9f, halfWidth = 0.5f, halfHeight = 1f) // its feet are 0.1 below it
val coin = Circle2(x = 0.4f, y = 1.2f, radius = 0.3f)
val picksUpCoin = player.overlaps(coin)
val push = Overlap2() // kept and reused, so a frame of checks allocates nothing
if (player.penetration(ground, push)) {
player.x += push.normalX * push.depth
player.y += push.normalY * push.depth
}
The shapes are mutable and set rewrites one in place, like Vec3f, so a game updating hundreds of
colliders a frame allocates nothing; keep one Overlap2 and reuse it. This is overlap and separation only.
Gravity, jumping and ground-snapping are the game's own, because how a character feels is not the
engine's to decide.
Choose a type
| Need | Start with |
|---|---|
| Positions, directions, scales | Vec3f (alias Vec3) |
| Rotation without Euler-angle drift | Quat |
| World, view or projection transforms | Mat4 |
| Camera setup and projection | Lens, CameraMathUtils |
| Visibility and hit tests | Ray, Plane, Aabb, Frustum |
| 2D overlap and separation | Box2, Circle2, Overlap2 |
| Angles in degrees | Angle, angleRad, angleDeg |
How it works
The changing forms exist so a System.update can reuse scratch vectors held in fields and allocate
nothing per frame; the new-value forms exist so ordinary code reads naturally. Picking the wrong one
fails silently: v.normalized() with the result dropped does nothing.
Vec3f() is not zero
The no-argument constructor gives (1, 1, 1). Write Vec3f.ZERO or Vec3f(0f, 0f, 0f) for
the origin.
Choosing the wrong clip space
A matrix built for the wrong ClipSpace renders upside down or clips away near geometry, and looks
like a camera bug. Take it from the renderer (renderer.clipSpace) rather than choosing one.
See also
- Cameras and controls for lenses in a scene.
- ECS for writing allocation-free systems.