LeCodesdocs

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

TypeScript
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

TypeScript
new Ray(origin: Vec3Like, dir: Vec3Like)   // dir need not be normalized

ray.origin           // Vec3
ray.dir              // Vec3
ray.getPoint(t)      // Vec3 — origin + t·dir

The 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.

Note

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)).

TypeScript
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 plane

Both intersection methods return the ray parameter t, not the point — feed it to ray.getPoint(t).

TypeScript
// 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).

TypeScript
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
TypeScript
// 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

TypeScript
// ✗ 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/solid

See also