The first-person camera's line of sight¶
The first-person camera's line of sight from its heading.
yaw turns about the world's up (y) from +z toward +x, pitch lifts the line above the horizontal: d = (cos(pitch) sin(yaw), sin(pitch), cos(pitch) cos(yaw)), a unit vector; a game keeps yaw and pitch as its camera's state, moves them by its input, and hands eye + d to the view as the target. A pitch of +-pi/2 looks straight up or down, where the world's up is parallel to the line of sight and a view needs another up, so a game holds its pitch inside that. Source: Lengyel, Mathematics for 3D Game Programming and Computer Graphics, 3rd ed., section 4.3 (spherical coordinates of a direction). 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.
look.trait
namespace lusine.camera;
vector<float, 3> look(float<rad> yaw, float<rad> pitch)
{
let c = cos(pitch);
return vector<float, 3>(c * sin(yaw), sin(pitch), c * cos(yaw));
}
The unit direction a camera looks along from its yaw and pitch. 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/look.trait'))
// lusine.camera.look: the first-person camera's line of sight from its heading. yaw turns about the
// world's up (y) from +z toward +x, pitch lifts the line above the horizontal:
// d = (cos(pitch) sin(yaw), sin(pitch), cos(pitch) cos(yaw)),
// a unit vector; a game keeps yaw and pitch as its camera's state, moves them by its input, and hands eye + d to the
// view as the target. A pitch of +-pi/2 looks straight up or down, where the world's up is parallel to the line of
// sight and a view needs another up, so a game holds its pitch inside that.
// Source: Lengyel, Mathematics for 3D Game Programming and Computer Graphics, 3rd ed., section 4.3 (spherical
// coordinates of a direction).
// Checked against a reference in numpy, which has its own tests.
namespace lusine.camera;
vector<float, 3> look(float<rad> yaw, float<rad> pitch)
{
let c = cos(pitch);
return vector<float, 3>(c * sin(yaw), sin(pitch), c * cos(yaw));
}
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/look.trait', 'look'))
from lusine.kernels.camera.look import look as reference
def law(**kw):
return trait_run.law('camera/look.trait', 'look', **kw)
namespace probe;
lusine.version version = { { number = 1 } };
struct item {
host float<rad> yaw;
host float<rad> pitch;
vector<float, 3> result_0;
};
lattice<item, 1> items(spacing = 1 m, boundary = wall);
void main() { items.result_0 = lusine.camera.look(items.yaw, items.pitch); }
Reference: headings all round¶
Yaws all round and pitches short of straight up or down: the kernel within binary32's roundings of the float64 reference.
y, p = rng.uniform(-np.pi, np.pi, 400), rng.uniform(-1.5, 1.5, 400)
got, want = law(yaw=y, pitch=p), reference(y, p)
err = np.max(np.abs(got - want))
assert err <= 8 * U, err / U
print(f'within {err / U:.1f} x 2^-24 of the reference over {len(y)} headings')
within 2.4 x 2^-24 of the reference over 400 headings
Invariant: a unit vector, its elevation the pitch¶
Every line of sight has length 1, its elevation is the pitch and its bearing from +z toward +x the yaw.
d = law(yaw=y, pitch=p)
assert np.allclose(np.linalg.norm(d, axis=1), 1.0, atol=4e-7)
assert np.allclose(np.arcsin(np.clip(d[:, 1], -1, 1)), p, atol=2e-6) and np.allclose(np.arctan2(d[:, 0], d[:, 2]), y, atol=2e-6)
print('unit length; elevation the pitch; bearing the yaw')
unit length; elevation the pitch; bearing the yaw
Limit: the axes¶
No yaw and no pitch looks along +z, a quarter turn along +x, and a pitch of pi/2 straight up.
d = law(yaw=np.array([0.0, np.pi / 2, 0.3]), pitch=np.array([0.0, 0.0, np.pi / 2]))
assert np.allclose(d, [[0, 0, 1], [1, 0, 0], [0, 1, 0]], atol=1e-6)
print('+z, +x and straight up')
+z, +x and straight up