Skip to content

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:

Kotlin
val forward = Vec3f(0f, 3f, 0f)
val returned = forward.normalize()

Use the new-vector form when you still need the original:

Kotlin
val direction = Vec3f(0f, 3f, 0f)
val unit = direction.normalized()
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

Kotlin
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:

Kotlin
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:

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

Kotlin
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