Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Math & Coordinates

Nightshade uses nalgebra_glm for every piece of linear algebra. There is no in-house math library. Vectors, matrices, and quaternions all come from nalgebra_glm, and the prelude re-exports the core types.

Core Types

TypeDescriptionExample
Vec22D vectorScreen positions, UV coordinates
Vec33D vectorPositions, directions, colors
Vec44D vectorHomogeneous coordinates, RGBA colors
Mat44x4 matrixTransform matrices
QuatQuaternionRotations
#![allow(unused)]
fn main() {
use nightshade::prelude::*;

let position = Vec3::new(1.0, 2.0, 3.0);
let direction = Vec3::y();
let identity = Mat4::identity();
let rotation = Quat::identity();
}

Coordinate System

The coordinate system is right-handed Y-up. Positive X is right, positive Y is up, positive Z points out of the screen toward the camera, and negative Z points into the scene.

    +Y (up)
     |
     |
     +--- +X (right)
    /
   /
  +Z (forward, toward camera)

This matches the glTF convention and nalgebra_glm's default handedness, which is why no flips or swaps are needed when loading glTF assets.

Vector Operations

Construction

#![allow(unused)]
fn main() {
let a = Vec3::new(1.0, 2.0, 3.0);
let zero = Vec3::zeros();
let one = Vec3::new(1.0, 1.0, 1.0);
let up = Vec3::y();
let right = Vec3::x();
let forward = -Vec3::z();
}

Arithmetic

+, -, and scalar * work as expected. The catch is element-wise multiplication, where * between two vectors does scalar multiplication, not per-component. For element-wise, use component_mul.

#![allow(unused)]
fn main() {
let sum = a + b;
let difference = a - b;
let scaled = a * 2.0;

let element_wise = a.component_mul(&b);
}

This trips up newcomers from glsl, where vec3 * vec3 means per-component multiplication. In nalgebra_glm, * follows linear-algebra conventions, and component_mul is the explicit form.

Common Operations

#![allow(unused)]
fn main() {
let length = nalgebra_glm::length(&v);
let normalized = nalgebra_glm::normalize(&v);
let dot = nalgebra_glm::dot(&a, &b);
let cross = nalgebra_glm::cross(&a, &b);
let distance = nalgebra_glm::distance(&a, &b);
let lerped = nalgebra_glm::lerp(&a, &b, 0.5);
}

Quaternions

Rotations are always quaternions. They avoid gimbal lock, compose cleanly, and interpolate smoothly with slerp. Euler angles are converted to quaternions when needed.

#![allow(unused)]
fn main() {
let rotation = nalgebra_glm::quat_angle_axis(
    std::f32::consts::FRAC_PI_4,
    &Vec3::y(),
);

let forward = nalgebra_glm::normalize(&(target - position));
let rotation = nalgebra_glm::quat_look_at(&forward, &Vec3::y());

let blended = rotation_a.slerp(&rotation_b, 0.5);

let rotated = nalgebra_glm::quat_rotate_vec3(&rotation, &direction);
}

quat_angle_axis builds a rotation of a given angle around a given axis. quat_look_at builds a rotation that orients the local forward toward a target direction with a specified up vector. slerp is spherical linear interpolation, the right way to blend two rotations. quat_rotate_vec3 applies a quaternion to a vector.

Transform Matrices

GlobalTransform stores a single 4x4 matrix. LocalTransform stores the decomposed translation, rotation, and scale.

#![allow(unused)]
fn main() {
pub struct LocalTransform {
    pub translation: Vec3,
    pub rotation: Quat,
    pub scale: Vec3,
}

pub struct GlobalTransform(pub Mat4);
}

The split is deliberate. Local transforms are easy to edit because translation, rotation, and scale are separate fields. Global transforms are easy to use because they bake the entire parent chain into one matrix multiplied through clip space by the renderer.

Building Matrices

#![allow(unused)]
fn main() {
let translation = nalgebra_glm::translation(&Vec3::new(1.0, 2.0, 3.0));
let rotation = nalgebra_glm::quat_to_mat4(&some_quat);
let scale = nalgebra_glm::scaling(&Vec3::new(2.0, 2.0, 2.0));
let combined = translation * rotation * scale;
}

Order matters. translation * rotation * scale is the standard TRS order, applied to a vector right-to-left. The scale is applied first, then the rotation, then the translation.

Extracting Position

The translation column of a 4x4 transform matrix is the fourth column. xyz() extracts the first three components.

#![allow(unused)]
fn main() {
let global = world.core.get_global_transform(entity).unwrap();
let position = global.0.column(3).xyz();
}

Angles

nalgebra_glm works in radians. Convert to and from degrees explicitly.

#![allow(unused)]
fn main() {
let radians = nalgebra_glm::radians(&nalgebra_glm::vec1(45.0)).x;
let degrees = nalgebra_glm::degrees(&nalgebra_glm::vec1(std::f32::consts::FRAC_PI_4)).x;
}

Depth Range and Reversed-Z

wgpu's depth range is [0, 1], not OpenGL's [-1, 1]. Nightshade goes one step further. The depth buffer is reversed-Z, where 0.0 is the far plane and 1.0 is the near plane, the opposite of the traditional convention.

The reason is floating-point precision. Floats have more precision near zero and less precision near one because of how the exponent and mantissa are distributed. In a standard depth buffer, the near plane maps to zero (high precision) and the far plane maps to one (low precision). Perspective projection is nonlinear, and most of the [0, 1] range is already consumed by geometry near the near plane. Combine the two distortions and there is almost no precision left for distant objects, which is what produces z-fighting in the middle distance of large scenes.

Reversed-Z flips the mapping. The far plane goes to zero (where float precision is highest) and the near plane goes to one. The perspective nonlinearity and the floating-point precision curve partially cancel, and the result is nearly uniform depth precision across the entire view range. The difference is dramatic for large outdoor scenes, where z-fighting at the horizon vanishes.

The practical consequences are three. The depth clear value is 0.0, since that is the far plane. The depth comparison function is Greater or GreaterEqual, since closer objects have larger depth values. Projection matrices are constructed with reversed_infinite_perspective_rh_zo rather than the standard perspective_rh_zo.