The first-person camera's step¶
The first-person camera's step on the floor.
The heading's forward along the floor, f = (sin(yaw), 0, cos(yaw)), and its right, r = f x up = (-cos(yaw), 0, sin(yaw)) (as lusine.render. camera_ray's right), move the position by the inputs over one frame: p' = p + (f ahead + r aside) speed dt, ahead and aside each -1 to 1 (a key or a stick), its height kept whatever the pitch; a game normalises a diagonal if it wants the same speed both ways. Source: Lengyel, Mathematics for 3D Game Programming and Computer Graphics, 3rd ed., section 4.3. Checked against a reference in numpy, which has its own tests.
- Source
- Lengyel, Mathematics for 3D Game Programming and Computer Graphics, 3rd ed., section 4.3.
- Proof
- reference, invariant, limit.
walk.trait
namespace lusine.camera;
vector<float<m>, 3> walk(vector<float<m>, 3> position, float<rad> yaw, float ahead, float aside,
float<m/s> speed, float<s> dt)
{
let forward = vector<float, 3>(sin(yaw), 0.0, cos(yaw));
let right = vector<float, 3>(-cos(yaw), 0.0, sin(yaw));
return position + (forward * ahead + right * aside) * (speed * dt);
}
The position after a frame of walking forward and to the right of the heading, its height kept. One of the reference camera's kinds: a camera is a game's own state, the eye and target lusine.render.table's view reads, and a kind is a kernel its procedure calls to move it.
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
from lusine import trait_run
F32 = np.float32
U = float(np.finfo(F32).eps) / 2 # binary32's unit roundoff, 2^-24
rng = np.random.default_rng(2026)
The kernel¶
The law as the library states it.
print(trait_run.source('camera/walk.trait'))
// lusine.camera.walk: the first-person camera's step on the floor. The heading's forward along the
// floor, f = (sin(yaw), 0, cos(yaw)), and its right, r = f x up = (-cos(yaw), 0, sin(yaw)) (as lusine.render.
// camera_ray's right), move the position by the inputs over one frame:
// p' = p + (f ahead + r aside) speed dt,
// ahead and aside each -1 to 1 (a key or a stick), its height kept whatever the pitch; a game normalises a diagonal if
// it wants the same speed both ways.
// Source: Lengyel, Mathematics for 3D Game Programming and Computer Graphics, 3rd ed., section 4.3.
// Checked against a reference in numpy, which has its own tests.
namespace lusine.camera;
vector<float<m>, 3> walk(vector<float<m>, 3> position, float<rad> yaw, float ahead, float aside,
float<m/s> speed, float<s> dt)
{
let forward = vector<float, 3>(sin(yaw), 0.0, cos(yaw));
let right = vector<float, 3>(-cos(yaw), 0.0, sin(yaw));
return position + (forward * ahead + right * aside) * (speed * dt);
}
The program¶
Each section runs the kernel over a lattice of items, one a case: the host sets every parameter, and main() binds the kernel over the items.
print(trait_run.law_program('camera/walk.trait', 'walk'))
from lusine.kernels.camera.walk import walk as reference
def law(**kw):
return trait_run.law('camera/walk.trait', 'walk', **kw)
namespace probe;
lusine.version version = { { number = 1 } };
struct item {
host vector<float<m>, 3> position;
host float<rad> yaw;
host float ahead;
host float aside;
host float<m/s> speed;
host float<s> dt;
vector<float<m>, 3> result_0;
};
lattice<item, 1> items(spacing = 1 m, boundary = wall);
void main() { items.result_0 = lusine.camera.walk(items.position, items.yaw, items.ahead, items.aside, items.speed, items.dt); }
Reference: steps every way¶
Headings, inputs and frames: the kernel within binary32's roundings of the reference.
n = 300
p0 = rng.uniform(-5, 5, (n, 3))
y, a, s = rng.uniform(-np.pi, np.pi, n), rng.uniform(-1, 1, n), rng.uniform(-1, 1, n)
got = law(position=p0, yaw=y, ahead=a, aside=s, speed=1.4, dt=1 / 60)
want = reference(p0, y, a, s, 1.4, 1 / 60)
err = np.max(np.abs(got - want))
assert err <= 16 * U * 5, err / U
print(f'within {err / U:.1f} x 2^-24 over {n} steps')
within 7.1 x 2^-24 over 300 steps
Invariant: along the floor, as far as the speed¶
A step keeps the height, and a step ahead alone moves speed x dt whatever the heading.
assert np.allclose(got[:, 1], p0[:, 1], atol=1e-6)
ahead = law(position=np.zeros((n, 3)), yaw=y, ahead=np.ones(n), aside=np.zeros(n), speed=1.4, dt=0.5)
assert np.allclose(np.linalg.norm(ahead, axis=1), 0.7, atol=1e-6)
print('the height kept; ahead moves speed x dt')
the height kept; ahead moves speed x dt
Limit: still, and the right¶
No input leaves the position; aside with no yaw walks along -x, the heading's right, f x up.
still = law(position=p0[:3], yaw=y[:3], ahead=np.zeros(3), aside=np.zeros(3), speed=1.4, dt=1.0)
assert np.allclose(still, p0[:3], atol=1e-6)
right = law(position=np.zeros((1, 3)), yaw=np.zeros(1), ahead=np.zeros(1), aside=np.ones(1), speed=1.0, dt=1.0)
assert np.allclose(right, [[-1.0, 0.0, 0.0]], atol=1e-6)
print('still without input; aside along -x')
still without input; aside along -x