| name | lens-studio-math |
| description | Reference guide for Lens Studio's math types and utilities — vec2/vec3/vec4/quat/mat4, MathUtils (DegToRad, RadToDeg, clamp, remap, lerp, inverseLerp), ScreenTransform 2D anchor/offset/size system, and Lens Studio's right-handed coordinate system (vec3.forward()=(0,0,-1)). Covers construction, arithmetic (uniformScale, dot, cross, normalize, distance), quaternion creation from Euler angles (pitch/yaw/roll XYZ order), quat.lookAt with parallel-vector guard, quat.slerp, combining rotations with multiply, getWorldTransform/setWorldTransform, mat4 inverse and transform extraction, and practical recipes: billboard, frame-rate-independent smooth follow, angle between directions, world-to-screen pixel position, project-to-plane, color lerp via vec4, ScreenTransform coordinate conversions, and parsing BLE sensor quaternion bytes. Use this skill for any 3D math, 2D UI positioning, position/rotation/scale arithmetic, or coordinate-space conversion in a Lens Studio TypeScript script. |
Lens Studio Math — Reference Guide
Lens Studio scripts use custom math types rather than plain JavaScript numbers. This guide covers the most common types and operations.
vec3 — 3D Vector
Construction
const a = new vec3(1, 0, 0)
const b = vec3.zero()
const c = vec3.one()
const up = vec3.up()
const forward = vec3.forward()
const right = vec3.right()
Arithmetic
const sum = a.add(b)
const diff = a.sub(b)
const scaled = a.uniformScale(2.5)
const divided = a.uniformScale(1/2.5)
const negated = a.uniformScale(-1)
const product = new vec3(a.x * b.x, a.y * b.y, a.z * b.z)
Geometry
const len = a.length
const unit = a.normalize()
const dot = a.dot(b)
const cross = a.cross(b)
const dist = a.distance(b)
const lerped = a.add(b.sub(a).uniformScale(t))
import { mix } from 'SpectaclesInteractionKit.lspkg/Utils/animate'
const lerped = mix(a, b, t)
vec2 — 2D Vector
const screen = new vec2(0.5, 0.5)
const offset = screen.add(new vec2(0.1, 0))
const mag = screen.length
vec4 — 4D Vector (colours, homogeneous coords)
const red = new vec4(1, 0, 0, 1)
const clear = new vec4(0, 0, 0, 0)
material.mainPass.baseColor = new vec4(0.2, 0.8, 0.4, 1.0)
material.mainPass.opacity = 0.5
quat — Quaternion (rotation)
Construction
const DEG = Math.PI / 180
const rot = quat.fromEulerAngles(45 * DEG, 0, 0)
const rot2 = quat.fromEulerAngles(
30 * MathUtils.DegToRad,
90 * MathUtils.DegToRad,
0
)
const identity = quat.quatIdentity()
function safeLookAt(forward: vec3, up: vec3): quat {
const dot = forward.normalize().dot(up.normalize())
if (Math.abs(dot) > 0.999) {
up = Math.abs(forward.dot(vec3.right())) < 0.999
? vec3.right()
: vec3.forward()
}
return quat.lookAt(forward, up)
}
const axisRot = quat.angleAxis(45 * MathUtils.DegToRad, vec3.up())
Combining rotations
const combined = rotB.multiply(rotA)
const inv = rot.invert()
const slerpd = quat.slerp(startRot, endRot, t)
Extracting info
const fwd = rot.multiplyVec3(vec3.forward())
const euler = rot.toEulerAngles()
const yawDeg = euler.y * (180 / Math.PI)
mat4 — 4×4 Matrix
const matrix: mat4 = sceneObject.getTransform().getWorldTransform()
const invMatrix = matrix.inverse()
const worldPoint = matrix.multiplyPoint(localPoint)
const worldDir = matrix.multiplyDirection(localDir)
sceneObject.getTransform().setWorldTransform(someMatrix)
MathUtils
MathUtils.DegToRad
MathUtils.RadToDeg
MathUtils.PI
MathUtils.TwoPI
MathUtils.HalfPI
const clamped = MathUtils.clamp(value, 0, 1)
const remapped = MathUtils.remap(value, inMin, inMax, outMin, outMax)
const t = MathUtils.lerp(a, b, alpha)
const alpha = MathUtils.inverseLerp(a, b, value)
Transform: Position, Rotation, Scale
const t = sceneObject.getTransform()
const worldPos = t.getWorldPosition()
const worldRot = t.getWorldRotation()
const worldScale = t.getWorldScale()
t.setWorldPosition(new vec3(0, 1, -2))
t.setWorldRotation(quat.quatIdentity())
t.setWorldScale(vec3.one())
const localPos = t.getLocalPosition()
t.setLocalPosition(new vec3(0, 0.5, 0))
t.setLocalRotation(quat.fromEulerAngles(0, 45 * MathUtils.DegToRad, 0))
t.setLocalScale(new vec3(2, 2, 2))
t.setWorldTransform(someMatrix)
const matrix = t.getWorldTransform()
Practical Recipes
Billboard (always face camera)
const camPos = mainCamera.getSceneObject().getTransform().getWorldPosition()
const objPos = this.sceneObject.getTransform().getWorldPosition()
const dir = camPos.sub(objPos).normalize()
const rot = safeLookAt(dir, vec3.up())
this.sceneObject.getTransform().setWorldRotation(rot)
Smooth follow (lerp toward target)
const SPEED = 5
const current = this.sceneObject.getTransform().getWorldPosition()
const target = this.targetObject.getTransform().getWorldPosition()
const alpha = MathUtils.clamp(getDeltaTime() * SPEED, 0, 1)
this.sceneObject.getTransform().setWorldPosition(
current.add(target.sub(current).uniformScale(alpha))
)
Angle between two directions
function angleBetween(a: vec3, b: vec3): number {
const dot = a.normalize().dot(b.normalize())
return Math.acos(MathUtils.clamp(dot, -1, 1)) * MathUtils.RadToDeg
}
Project world position to screen pixel position
function worldToScreenPixels(cam: Camera, worldPos: vec3): vec2 {
return cam.worldSpaceToScreenSpace(worldPos)
}
Flatten a vector to the XZ plane (ignore Y)
function flattenXZ(v: vec3): vec3 {
return new vec3(v.x, 0, v.z).normalize()
}
ScreenTransform — 2D UI Math
ScreenTransform positions 2D elements (images, text) in screen space. Its coordinate system uses normalised values: (0, 0) is the canvas centre; (1, 1) is the top-right; (-1, -1) is the bottom-left.
const st = this.sceneObject.getComponent('Component.ScreenTransform')
st.anchors.setMin(new vec2(0, 0))
st.anchors.setMax(new vec2(1, 1))
st.offsets.setLeft(-50)
st.offsets.setRight(50)
st.offsets.setBottom(-30)
st.offsets.setTop(30)
st.anchors.setCenter(new vec2(0.5, 0.5))
st.position = new vec2(0, 0.25)
st.size = new vec2(200, 100)
Coordinate conversion
const screenPos: vec2 = st.localPointToScreenPoint(new vec2(0, 0))
const localPt: vec2 = st.screenPointToLocalPoint(touchScreenPos)
const worldPt: vec3 = st.localPointToWorldPoint(new vec2(0, 0))
const localPt2: vec2 = st.worldPointToLocalPoint(someWorldPos)
Measuring a ScreenTransform element's size in world units
const a = st.localPointToWorldPoint(new vec2(-1, -1))
const b = st.localPointToWorldPoint(new vec2( 1, 1))
const width = Math.abs(b.x - a.x)
const height = Math.abs(b.y - a.y)
Common Gotchas
vec3.forward() is (0, 0, -1) in Lens Studio (right-handed, Z pointing toward viewer). Don't assume (0, 0, 1).
- Never modify vec3/quat in-place — most ops return new instances.
a.add(b) returns a new vec3; a is unchanged.
- Euler angle order in
quat.fromEulerAngles(x, y, z) is pitch, yaw, roll (XYZ). Mixing up the order causes unexpected rotations.
quat.lookAt(forward, up) is undefined when forward ≈ up — always guard with a dot product check and swap to a fallback up vector.
camera.worldSpaceToScreenSpace() returns screen pixels, not UV coordinates. Divide by screen dimensions to get [0,1] UV.
getDeltaTime() returns seconds as a float — multiply by it for frame-rate-independent speeds.
mat4.inverse() can return garbage if the matrix is singular (e.g., zero scale). Check for near-zero scale before inverting.