Transforms & 3D Math
Understanding 3D transformations is crucial for WebXR development. Objects need to be positioned, rotated, and scaled correctly to feel natural in virtual space. This guide covers the fundamentals with practical examples.
The Three Pillars of 3D Transforms
Every 3D object has three fundamental properties:
Transform = Position + Rotation + Scale
Position: [x, y, z] → Where is it?
Rotation: [x, y, z, w] → How is it oriented? (quaternion)
Scale: [sx, sy, sz] → How big is it?
In IWSDK, these are stored in the Transform
component and automatically synced to Three.js.
Position (Translation)
Position determines where an object exists in 3D space.
Understanding 3D Coordinates
IWSDK uses a right-handed coordinate system:
+Y (up)
|
|
|
---- 0 ---- +X (right)
/
/
+Z (forward toward viewer)
Real-world scale: 1 unit = 1 meter (important for VR/AR)
Working with Position
// Set absolute position
entity.object3D.position.set(2, 1.7, -5);
// Object appears 2m right, 1.7m up, 5m away
// Move relative to current position
entity.object3D.position.x += 1; // Move 1 meter right
entity.object3D.position.y += 0; // Same height
entity.object3D.position.z -= 2; // Move 2 meters away
// Or using ECS Transform for data-driven updates
const pos = entity.getVectorView(Transform, 'position');
pos[0] += 1; // Move 1 meter right
pos[2] -= 2; // Move 2 meters away
Common Position Patterns
Moving Forward Based on Orientation:
// Create forward vector from object's rotation
const forward = new Vector3(0, 0, -1); // -Z is forward
forward.applyQuaternion(entity.object3D.quaternion);
// Move in that direction
entity.object3D.position.add(forward.multiplyScalar(speed * deltaTime));
Positioning Relative to Another Object:
// Place object 2 meters in front of another object
const forward = new Vector3(0, 0, -2); // 2 meters forward
forward.applyQuaternion(referenceObject.quaternion);
entity.object3D.position.copy(referenceObject.position).add(forward);
Rotation (Orientation)
Rotation determines how an object is oriented in 3D space.
Quaternions vs Euler Angles
IWSDK uses quaternions [x, y, z, w]
for rotation because:
✅ No gimbal lock - can represent any 3D rotation
✅ Smooth interpolation - natural animation between rotations
✅ Efficient composition - combining rotations is just multiplication
✅ WebXR standard - matches what VR/AR hardware provides
❌ Euler angles [pitch, yaw, roll]
have problems:
- Gimbal lock at certain angles
- Order dependency (XYZ vs YXZ gives different results)
- Difficult to interpolate smoothly
Working with Rotation
// Identity rotation (no rotation)
entity.object3D.quaternion.set(0, 0, 0, 1);
// Rotate around Y-axis (yaw/turn)
const yawQuat = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0), // Y-axis
Math.PI / 4, // 45 degrees in radians
);
entity.object3D.quaternion.copy(yawQuat);
// Combine rotations
const additionalRot = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0),
deltaTime,
);
entity.object3D.quaternion.multiply(additionalRot);
Common Rotation Patterns
Look At Target:
// Make object look at a target position
const targetPosition = new Vector3(5, 0, 0);
entity.object3D.lookAt(targetPosition);
// Or calculate direction manually
const direction = new Vector3()
.subVectors(targetPosition, entity.object3D.position)
.normalize();
const quaternion = new Quaternion().setFromUnitVectors(
new Vector3(0, 0, -1), // Forward vector
direction,
);
entity.object3D.quaternion.copy(quaternion);
Rotation Over Time:
// Rotate continuously around Y-axis
const rotationSpeed = Math.PI; // radians per second
const deltaQuat = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0), // Y-axis
rotationSpeed * deltaTime,
);
entity.object3D.quaternion.multiply(deltaQuat);
Convert from Euler Angles (when needed):
// If you have pitch/yaw/roll from some source
const euler = new Euler(pitch, yaw, roll, 'YXZ'); // Order matters!
const quaternion = new Quaternion().setFromEuler(euler);
entity.setValue(Transform, 'orientation', quaternion.toArray());
Scale
Scale determines how big an object appears.
Understanding Scale Values
// Uniform scale (same in all directions)
entity.object3D.scale.set(2, 2, 2); // 2x bigger
entity.object3D.scale.set(0.5, 0.5, 0.5); // Half size
// Non-uniform scale
entity.object3D.scale.set(2, 1, 0.5);
// 2x wider, same height, half depth
Scale in WebXR Context
Real-world considerations:
- Users expect consistent sizing (door ≈ 2m tall)
- Very small/large scales can cause rendering issues
- Consider user interaction when sizing objects
// Make object child-sized for interaction
entity.object3D.scale.set(0.6, 0.6, 0.6);
// Scale text based on distance for readability
const distance = entity.object3D.position.distanceTo(camera.position);
const textScale = Math.max(0.5, distance * 0.1);
entity.object3D.scale.set(textScale, textScale, textScale);
Local vs World Space
Understanding coordinate spaces is crucial for hierarchical objects.
Coordinate Spaces Explained
World Space: Global coordinate system (the scene)
├─ Car Entity (world position: [10, 0, 5])
└─ Local Space: Relative to car
├─ Wheel (local position: [-2, -0.5, 1])
└─ Door (local position: [0, 0, 1.5])
World positions: Where things actually are in the scene
Local positions: Where things are relative to their parent
Working with Coordinate Spaces
// Get world position of a child object
const worldPos = child.object3D!.getWorldPosition(new Vector3());
// Convert local point to world space
const localPoint = new Vector3(0, 0, -1); // 1 meter forward in local space
const worldPoint = localPoint.applyMatrix4(entity.object3D!.matrixWorld);
// Convert world point to local space
const worldPoint = new Vector3(5, 2, 0);
const localPoint = worldPoint.applyMatrix4(entity.object3D!.worldToLocal);
Hierarchical Movement Example
// Create a car with wheels as children
const car = world.createTransformEntity();
const wheel1 = new Mesh(wheelGeometry, wheelMaterial);
const wheel2 = new Mesh(wheelGeometry, wheelMaterial);
car.object3D.add(wheel1);
car.object3D.add(wheel2);
// Position wheels relative to car
wheel1.position.set(-1, -0.5, 1.2);
wheel2.position.set(1, -0.5, 1.2);
// Move car - wheels follow automatically
car.object3D.position.x += speed * deltaTime;
// Rotate wheels in local space
wheel1.rotateX((speed * deltaTime) / wheelRadius);
wheel2.rotateX((speed * deltaTime) / wheelRadius);
Performance Optimization
Direct Object3D vs ECS Updates
// For frequent updates, direct Three.js can be faster
entity.object3D.position.x += deltaX;
entity.object3D.position.y += deltaY;
entity.object3D.position.z += deltaZ;
// For data-driven updates, use ECS Transform
const pos = entity.getVectorView(Transform, 'position');
pos[0] += deltaX;
pos[1] += deltaY;
pos[2] += deltaZ;
Avoid Unnecessary Calculations
// ❌ Recalculating every frame
const forward = new Vector3(0, 0, -1).applyQuaternion(
entity.object3D.quaternion,
);
// ✅ Reuse vectors when possible
const forward = new Vector3(0, 0, -1);
forward.applyQuaternion(entity.object3D.quaternion);
// ✅ Cache complex calculations
let lastRotation = entity.object3D.quaternion.clone();
let cachedForward = new Vector3(0, 0, -1).applyQuaternion(lastRotation);
if (!entity.object3D.quaternion.equals(lastRotation)) {
lastRotation.copy(entity.object3D.quaternion);
cachedForward.set(0, 0, -1).applyQuaternion(lastRotation);
}
Summary
Key Takeaways:
- Position
[x, y, z]
- where objects exist in meters - Rotation
[x, y, z, w]
- quaternions for orientation (no gimbal lock) - Scale
[sx, sy, sz]
- size multipliers - Use vector views for performance in hot paths
- IWSDK syncs automatically - update ECS, visuals follow
- WebXR scale matters - 1 unit = 1 meter in real space
- Understand coordinate spaces - local vs world for hierarchical objects
Understanding these fundamentals enables you to build natural-feeling VR/AR experiences where objects move, rotate, and scale predictably in 3D space.