The orbit camera's eye¶
The orbit camera's eye (Boules's table and any inspection view).
The eye is distance back from the target along the line of sight lusine.camera.look gives for the heading: eye = target - look(yaw, pitch) distance, so the camera at the eye, looking at the target, sees it along look(yaw, pitch). A game turns yaw and pitch and changes the distance by its input; the target is any point it chooses, a ball or the jack. 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.
orbit.trait
namespace lusine.camera;
vector<float<m>, 3> orbit(vector<float<m>, 3> target, float<rad> yaw, float<rad> pitch, float<m> distance)
{
return target - look(yaw = yaw, pitch = pitch) * distance;
}
The eye a distance back from a target along the line of sight, so the camera there looks at the target. 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/orbit.trait'))
// lusine.camera.orbit: the orbit camera's eye (Boules's table and any inspection view). The eye is
// `distance` back from the target along the line of sight lusine.camera.look gives for the heading:
// eye = target - look(yaw, pitch) distance,
// so the camera at the eye, looking at the target, sees it along look(yaw, pitch). A game turns yaw and pitch and
// changes the distance by its input; the target is any point it chooses, a ball or the jack.
// 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> orbit(vector<float<m>, 3> target, float<rad> yaw, float<rad> pitch, float<m> distance)
{
return target - look(yaw = yaw, pitch = pitch) * distance;
}
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/orbit.trait', 'orbit'))
from lusine.kernels.camera.orbit import orbit as reference
def law(**kw):
return trait_run.law('camera/orbit.trait', 'orbit', **kw)
namespace probe;
lusine.version version = { { number = 1 } };
struct item {
host vector<float<m>, 3> target;
host float<rad> yaw;
host float<rad> pitch;
host float<m> distance;
vector<float<m>, 3> result_0;
};
lattice<item, 1> items(spacing = 1 m, boundary = wall);
void main() { items.result_0 = lusine.camera.orbit(items.target, items.yaw, items.pitch, items.distance); }
Reference: round the jack¶
Headings and distances round a jack on the ground: the kernel within binary32's roundings of the reference, measured against the distance.
jack = np.array([1.0, 0.03, 0.2])
y, p, r = rng.uniform(-np.pi, np.pi, 300), rng.uniform(-1.4, 1.4, 300), rng.uniform(0.2, 8.0, 300)
got = law(target=np.tile(jack, (300, 1)), yaw=y, pitch=p, distance=r)
want = reference(jack, y, p, r)
err = np.max(np.abs(got - want) / r[:, None])
assert err <= 16 * U, err / U
print(f'within {err / U:.1f} x 2^-24 of the distance over {len(y)} eyes')
within 3.6 x 2^-24 of the distance over 300 eyes
Invariant: the distance, and the target in view¶
The eye is the distance from the target, and from the eye the target lies along look(yaw, pitch).
from lusine.kernels.camera.look import look
gap = jack - got
assert np.allclose(np.linalg.norm(gap, axis=1), r, rtol=1e-6)
assert np.allclose(gap / np.linalg.norm(gap, axis=1, keepdims=True), look(y, p), atol=2e-6)
print('the distance away, looking at the target')
the distance away, looking at the target
Limit: level and behind¶
No yaw and no pitch puts the eye straight behind the target along -z, level with it; no distance puts it at the target.
e = law(target=np.tile(jack, (2, 1)), yaw=np.zeros(2), pitch=np.zeros(2), distance=np.array([2.0, 0.0]))
assert np.allclose(e, [jack - [0, 0, 2.0], jack], atol=1e-6)
print('behind along -z; at no distance, the target')
behind along -z; at no distance, the target