Skip to content

First mathematical operations

Install current repository code, then run these independent Python snippets using that environment. Each example is checked against an isolated installed wheel, with no source-tree gem imports, renderer or GPU. Examples are executed on CPython 3.10–3.14/Linux x86_64 against wheel and sdist installs. Python 2.7 is unsupported; see the packaging guide.

Vectors and ownership

from gem.vector import Vector, cross

values = [3.0, 4.0, 0.0]
v = Vector(3, values)
unit = v.normalize()
assert v.vector is values            # construction retains supplied storage
assert values == [3.0, 4.0, 0.0]     # returning normalization preserves it
assert unit.vector is not values
print(v.magnitude())                 # 5.0
print(unit.vector)                   # [0.6, 0.8, 0.0]
print(v.dot(Vector(3, [1, 0, 0])))    # 3.0
print(cross(Vector(3, [1, 0, 0]), Vector(3, [0, 1, 0])).vector)
# [0, 0, 1]

Use matching dimensions for arithmetic. Vector equality is exact, including dimension semantics. normalize() returns a new Vector; i_normalize() changes the receiver and returns it. Direct zero-Vector normalization yields zero, but geometric callers can still reject a degenerate direction. Constructor aliasing and receiver mutation are deliberate distinctions, not a universal copy policy.

Matrix transformation order

Matrices store nested rows. M * v implements the mathematical row-vector product vM. A product translationrotation applies translation then* rotation. This example starts at +X, translates two units along +X and rotates +90 degrees about +Z, producing +3Y:

from gem.matrix import Matrix
from gem.vector import Vector

translation = Matrix(4).translate(Vector(3, [2, 0, 0]))
rotation = Matrix(4).rotate(Vector(3, [0, 0, 1]), 90)  # degrees
position = Vector(4, [1, 0, 0, 1])
after = (translation * rotation) * position
assert abs(after.vector[0]) < 1e-14
assert abs(after.vector[1] - 3.0) < 1e-14
assert after.vector[2:] == [0.0, 1.0]
print([round(value, 6) for value in after.vector])    # [0.0, 3.0, 0.0, 1.0]

other_order = (rotation * translation) * position
assert abs(other_order.vector[0] - 2.0) < 1e-14
assert abs(other_order.vector[1] - 1.0) < 1e-14
assert position.vector == [1, 0, 0, 1]

Use explicit w=1 positions and w=0 directions with general Matrix4 multiplication; it does not implicitly promote Vector3. Translation is stored in the final row. The separate Vector transform helper has local affine promotion and no automatic perspective divide. Read projection and transform conventions before combining camera/projective matrices.

Matrix inversion

from gem.matrix import Matrix

matrix = Matrix(2, [[4.0, 7.0], [2.0, 6.0]])
inverse = matrix.inverse()
expected = [[0.6, -0.7], [-0.2, 0.4]]
assert all(abs(inverse.matrix[i][j] - expected[i][j]) < 1e-14
           for i in range(2) for j in range(2))
for identity in (matrix * inverse, inverse * matrix):
    assert all(abs(identity.matrix[i][j] - (1 if i == j else 0)) < 1e-14
               for i in range(2) for j in range(2))
assert matrix.matrix == [[4.0, 7.0], [2.0, 6.0]]

Inverse/determinant dispatch supports dimensions 2/3/4. Singular inversion raises ZeroDivisionError. Matrix3/4 inverses have scaled extreme-value handling; that does not establish identical robustness for public determinants or Matrix2 inverse. Use i_inverse() only when receiver mutation is intended.

Quaternion orientation

import math
from gem.quaternion import quat_from_axis_angle, quat_rotate_vector
from gem.vector import Vector

axis = [0, 0, 2]
q = quat_from_axis_angle(axis, 90)      # degrees, temporary axis normalization
assert axis == [0, 0, 2]
assert abs(q.data[0] - math.sqrt(0.5)) < 1e-14  # [w,x,y,z]
turned = quat_rotate_vector(q, Vector(3, [1, 0, 0]))
assert abs(turned.vector[0]) < 1e-14 and abs(turned.vector[1] - 1.0) < 1e-14
print([round(value, 6) for value in turned.vector])   # [0.0, 1.0, 0.0]

The recommended constructor is quat_from_axis_angle, not the similarly named legacy quat_rotate_from_axis_angle. Rotation uses a unit Quaternion sandwich; q*Vector alone returns a Quaternion product, not rotated Vector. Other helpers use radians, so units are specified per API. See the quaternion guide for interpolation, return types and the legacy forward-axis distinction.

Bezier evaluation and sampling

from gem.bezier import BezierPath, cubicBezierPoint, quadraticBezierPoint
from gem.vector import Vector

controls = [Vector(2, [0, 0]), Vector(2, [1, 2]),
            Vector(2, [2, 2]), Vector(2, [3, 0])]
assert cubicBezierPoint(0.5, *controls).vector == [1.5, 1.5]
assert quadraticBezierPoint(0.5, 0.0, 2.0, 4.0) == 2.0
path = BezierPath()
path.setControlPoints(controls)
path.minimum_sqr_distance = 0.0001     # coordinate-distance tolerance 0.01
points = path.findDrawingPoints(0)
assert points[0].vector == [0, 0] and points[-1].vector == [3, 0]
assert controls[1].vector == [1, 2]

Parameters are not clamped. Cubic paths use 3k+1 controls and retain supplied control lists. Adaptive sampling supports finite scalar/Vector2/Vector3 controls, with squared-distance tolerance and a depth-16 cap; capped output can exceed tolerance. getDrawingPoints() returns nested per-segment lists rather than a single flat path. Source-point thinning is a different builder operation.

Constant-radiance SH reference

This exact L0 reference avoids introducing sampling error into the first example. Canonical Y00=1/sqrt(4pi), so constant RGB radiance [1,2,3] has the coefficient sqrt(4pi)[1,2,3]. The pipeline convolves once to get irradiance pi[1,2,3]:

import math
from gem.spherical_harmonics import convolve_diffuse, reconstruct

radiance = [[math.sqrt(4 * math.pi) * channel for channel in [1, 2, 3]]]
irradiance_coefficients = convolve_diffuse(radiance)
irradiance = reconstruct(irradiance_coefficients, [0, 0, 1])  # unit normal
assert all(abs(value - math.pi * channel) < 1e-12
           for value, channel in zip(irradiance, [1, 2, 3]))
albedo = [0.5, 0.5, 0.5]
reflected = [a * value / math.pi for a, value in zip(albedo, irradiance)]
assert all(abs(value - expected) < 1e-12
           for value, expected in zip(reflected, [0.5, 1.0, 1.5]))
assert radiance != irradiance_coefficients

Coefficients are canonical coefficient-by-RGB lists; full bands have bands² entries, index l(l+1)+m. Radiance, irradiance and reflected Lambertian radiance are distinct. Reconstruction does not normalize the supplied direction or convolve again. All calculations above remain linear, without tone mapping or display encoding. Historical probe coefficients require explicit legacy_to_canonical; changing import paths alone does not change their basis.

See the SH guide for projection, sampling, basis signs and analytical rotation, and the HDR/SH example for procedural environments, shader-ready exports and CPU visualization.

ctypes matrix export

import ctypes
from gem.matrix import Matrix
from gem.vector import Vector

matrix = Matrix(4).translate(Vector(3, [2, -3, 4]))
assert list(matrix.c_matrix[3]) == [2.0, -3.0, 4.0, 1.0]
pointer = ctypes.cast(matrix.c_matrix, ctypes.POINTER(ctypes.c_float))
assert [pointer[index] for index in range(12, 16)] == [2.0, -3.0, 4.0, 1.0]
matrix.i_translate(Vector(3, [1, 0, 0]))
assert list(matrix.c_matrix[3]) == [3.0, -3.0, 4.0, 1.0]

The export is a row-major float32 snapshot, not a live view of Python rows. Supported in-place operations refresh it; direct row edits do not. Keep the owning array alive while a foreign consumer uses its pointer, and reacquire the pointer after operations replace the snapshot. Match the consumer's layout and transpose policy explicitly; this example verifies memory values, not an OpenGL upload.

Next steps

Use the API reference to find actual signatures and documented constraints, conventions for composition/units and compatibility for support evidence. The canonical roadmap describes future features; none is implied by these examples. Return to getting started or installation for the beginner journey.