Ray, Plane & Noise
Three small 3D math/utility classes. Ray + Plane are the tap-to-place toolkit: turn a screen
tap into a camera ray, intersect it with a ground plane, place at the hit point. For raycasting
against bodies instead of a mathematical plane, use
Physics.raycast (it takes a Ray's origin/dir). Noise is a native
Perlin/fractal sampler for terrain, wobble, and organic motion.
At a glance
const ground = new Plane([0, 1, 0], [0, 0, 0]) // Y-up plane through the origin
scene.addEventListener('click', ev => {
const ray = scene.camera.getRay(ev.clientX, ev.clientY)
const t = ground.intersectRay(ray)
if (t !== null) marker.position = ray.getPoint(t) // place at the tapped floor point
})Ray
new Ray(origin: Vec3Like, dir: Vec3Like) // dir need not be normalized
ray.origin // Vec3
ray.dir // Vec3
ray.getPoint(t) // Vec3 — origin + t·dirThe usual source is the camera: scene.camera.getRay(screenX, screenY) builds the pick ray from
the camera through a screen pixel (logical px — ev.clientX/clientY plug straight in). See
Camera.
getPoint(t)'s parameter is in units of dir's length — world units only when dir
is normalized. Camera rays' dir comes from the host view direction; normalize
(ray.dir.normalize()) before treating t as a distance in meters.
Plane
An infinite geometric plane stored as a unit normal + scalar offset d (dot(normal, point)).
new Plane(normal: Vec3Like, point: Vec3Like) // normal is normalized for you
Plane.fromPoints(a, b, c): Plane // through three points (normal from winding)
Plane.fromCoefficients(a, b, c, d): Plane // from ax + by + cz = d
plane.normal // Vec3, unit length (readonly)
plane.d // number (readonly)
plane.intersectRay(ray): number | null // param t ≥ 0, or null (parallel / behind the origin)
plane.intersectLine(ray): number | null // same, but hits behind the origin allowed (t may be < 0)
plane.signedDistance(point): number // positive on the normal's side
plane.projectPoint(point): Vec3 // closest point on the planeBoth intersection methods return the ray parameter t, not the point — feed it to
ray.getPoint(t).
// keep a character above a tilted platform
const surface = Plane.fromPoints(p0, p1, p2)
if (surface.signedDistance(hero.position) < 0) {
hero.position = surface.projectPoint(hero.position)
}Noise
Native FastNoise Perlin/fractal sampler. Deterministic: the same coordinates always return the same value, so animate by moving the sample point (e.g. use time as one axis).
const noise = new Noise()
noise.get(x, y) // sample 2D noise
noise.get3d(x, y, z) // sample 3D noise
noise.frequency = 0.01 // get/set; default 0.01 — spatial scale (higher = busier)
noise.octaves = 4 // get/set; default 1 — fractal layers (>1 adds detail)
noise.fractalLacunarity = 2 // get/set; default 2 — frequency step between octaves
noise.fractalGain = 0.5 // get/set; default 0.5 — amplitude step between octaves// gentle idle bob + sway
const n = new Noise()
n.frequency = 0.5
let t = 0
setLoop(dt => {
t += dt
ghost.y = 1 + n.get(t, 0) * 0.2
ghost.eulerAngles = [0, n.get(t, 100) * 15, 0]
})Pitfalls
// ✗ treating intersectRay's result as the hit point
marker.position = ground.intersectRay(ray) // it's the scalar t
// ✓
const t = ground.intersectRay(ray)
if (t !== null) marker.position = ray.getPoint(t)
// ✗ expecting a plane to block Physics.raycast — Plane is math, not a body
// ✓ give the floor a Shape (+ Physics { motion: 'static' }) to make it hittable/solidSee also
- Physics & shapes —
Physics.raycastagainst real bodies,RayHit - Camera —
scene.camera.getRay(x, y) - Pointer events — where
clientX/clientYcome from - Vec3 & math — value semantics of the returned vectors