From 4a38ce8d14c07a45a6798efdc9e45f726f179f22 Mon Sep 17 00:00:00 2001 From: David Reeves Date: Fri, 31 Oct 2025 12:10:23 +0000 Subject: [PATCH 1/5] Add camera matrix helpers Allows creation of view and projection matrices that are consistent with what's used in engine --- src/clientSideScene/CameraControls.ts | 138 +++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index 01a8b2cfe3f..8f59cc92d9b 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -4,7 +4,7 @@ import { isArray } from '@src/lib/utils' import * as TWEEN from '@tweenjs/tween.js' import Hammer from 'hammerjs' -import type { Camera } from 'three' +import type { Camera, CoordinateSystem} from 'three' import { Euler, MathUtils, @@ -15,6 +15,8 @@ import { Spherical, Vector2, Vector3, + WebGLCoordinateSystem, + WebGPUCoordinateSystem, } from 'three' import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType' @@ -1852,3 +1854,137 @@ export async function letEngineAnimateAndSyncCamAfter( }, }) } + +function createOrthographicProjectionMatrix( + left: number, + right: number, + bottom: number, + top: number, + near: number, + far: number, + coordSystem: CoordinateSystem +): Matrix4 { + const invW = 1.0 / (right - left); + const invH = 1.0 / (top - bottom); + const invD = 1.0 / (near - far); + + switch (coordSystem) { + case WebGLCoordinateSystem: + // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to + // [-1, 1] in NDC space + + // prettier-ignore + return new Matrix4( + 2.0 * invW, 0.0, 0.0, -(right + left) * invW, + 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, + 0.0, 0.0, 2.0 * invD, (far + near) *invD, + 0.0, 0.0, 0.0, 1.0 + ); + case WebGPUCoordinateSystem: + // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to + // [0, 1] in NDC space + + // prettier-ignore + return new Matrix4( + 2.0 * invW, 0.0, 0.0, -(right + left) * invW, + 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, + 0.0, 0.0, invD, near *invD, + 0.0, 0.0, 0.0, 1.0 + ); + } +} + +function createPerspectiveProjectionMatrix( + fovY: number, + aspect: number, + near: number, + far: number, + coordSystem: CoordinateSystem +): Matrix4 { + const y = Math.tan(0.5 * degToRad(fovY)); + const x = y * aspect; + + switch (coordSystem) { + case WebGLCoordinateSystem: + // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to + // [-1, 1] in NDC space + + // prettier-ignore + return new Matrix4( + 1.0 / x, 0.0, 0.0, 0.0, + 0.0, 1.0 / y, 0.0, 0.0, + 0.0, 0.0, -(far + near) / (far - near), -2.0 * far * near / (far - near), + 0.0, 0.0, -1.0, 0.0 + ); + case WebGPUCoordinateSystem: + // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to + // [0, 1] in NDC space + + // prettier-ignore + return new Matrix4( + 1.0 / x, 0.0, 0.0, 0.0, + 0.0, 1.0 / y, 0.0, 0.0, + 0.0, 0.0, far / (near - far), -far * near / (far - near), + 0.0, 0.0, -1.0, 0.0 + ); + } +} + +function toVector3(obj: { x: number; y: number; z: number }) { + return new Vector3(obj.x, obj.y, obj.z); +} + +function toQuaternion(obj: { w: number; x: number; y: number; z: number }) { + return new Quaternion(obj.x, obj.y, obj.z, obj.w); +} + +/** + * Creates a projection matrix from the given CameraViewState in a way that is consistent with engine + */ +export function createProjectionMatrix(cam: CameraViewState): Matrix4 { + const invRot = new Matrix4(); + invRot.makeRotationFromQuaternion( + toQuaternion(cam.pivot_rotation).conjugate() + ); + + const invTrans = new Matrix4(); + invTrans.makeTranslation(toVector3(cam.pivot_position).negate()); + + const result = new Matrix4(); + result.multiplyMatrices(invRot, invTrans); + + return result; +} + +/** + * Creates a view matrix from the given CameraViewState in a way that is consistent with engine + */ +export function createViewMatrix( + camera: CameraViewState, + aspectRatio: number, + nearClip: number, + farClip: number, + coordSystem: CoordinateSystem = WebGLCoordinateSystem +): Matrix4 { + if (camera.is_ortho) { + const height = Math.tan(0.5 * degToRad(camera.fov_y)) * camera.eye_offset; + const width = height * aspectRatio; + return createOrthographicProjectionMatrix( + -width, + width, + -height, + height, + nearClip, + farClip, + coordSystem + ); + } else { + return createPerspectiveProjectionMatrix( + camera.fov_y, + aspectRatio, + nearClip, + farClip, + coordSystem + ); + } +} From 75bdb6cdeeb5d318402063890155531e44a3586a Mon Sep 17 00:00:00 2001 From: David Reeves Date: Fri, 31 Oct 2025 13:19:02 +0000 Subject: [PATCH 2/5] Fix lint --- src/clientSideScene/CameraControls.ts | 52 ++++++++++++--------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index 8f59cc92d9b..1721fb53b9c 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -1864,33 +1864,29 @@ function createOrthographicProjectionMatrix( far: number, coordSystem: CoordinateSystem ): Matrix4 { - const invW = 1.0 / (right - left); - const invH = 1.0 / (top - bottom); - const invD = 1.0 / (near - far); + const invW = 1.0 / (right - left) + const invH = 1.0 / (top - bottom) + const invD = 1.0 / (near - far) switch (coordSystem) { case WebGLCoordinateSystem: // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to // [-1, 1] in NDC space - - // prettier-ignore return new Matrix4( 2.0 * invW, 0.0, 0.0, -(right + left) * invW, 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, 0.0, 0.0, 2.0 * invD, (far + near) *invD, 0.0, 0.0, 0.0, 1.0 - ); + ) case WebGPUCoordinateSystem: // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to // [0, 1] in NDC space - - // prettier-ignore return new Matrix4( 2.0 * invW, 0.0, 0.0, -(right + left) * invW, 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, 0.0, 0.0, invD, near *invD, 0.0, 0.0, 0.0, 1.0 - ); + ) } } @@ -1901,59 +1897,55 @@ function createPerspectiveProjectionMatrix( far: number, coordSystem: CoordinateSystem ): Matrix4 { - const y = Math.tan(0.5 * degToRad(fovY)); - const x = y * aspect; + const y = Math.tan(0.5 * degToRad(fovY)) + const x = y * aspect switch (coordSystem) { case WebGLCoordinateSystem: // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to // [-1, 1] in NDC space - - // prettier-ignore return new Matrix4( 1.0 / x, 0.0, 0.0, 0.0, 0.0, 1.0 / y, 0.0, 0.0, 0.0, 0.0, -(far + near) / (far - near), -2.0 * far * near / (far - near), 0.0, 0.0, -1.0, 0.0 - ); + ) case WebGPUCoordinateSystem: // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to // [0, 1] in NDC space - - // prettier-ignore return new Matrix4( 1.0 / x, 0.0, 0.0, 0.0, 0.0, 1.0 / y, 0.0, 0.0, 0.0, 0.0, far / (near - far), -far * near / (far - near), 0.0, 0.0, -1.0, 0.0 - ); + ) } } function toVector3(obj: { x: number; y: number; z: number }) { - return new Vector3(obj.x, obj.y, obj.z); + return new Vector3(obj.x, obj.y, obj.z) } function toQuaternion(obj: { w: number; x: number; y: number; z: number }) { - return new Quaternion(obj.x, obj.y, obj.z, obj.w); + return new Quaternion(obj.x, obj.y, obj.z, obj.w) } /** * Creates a projection matrix from the given CameraViewState in a way that is consistent with engine */ export function createProjectionMatrix(cam: CameraViewState): Matrix4 { - const invRot = new Matrix4(); + const invRot = new Matrix4() invRot.makeRotationFromQuaternion( toQuaternion(cam.pivot_rotation).conjugate() - ); + ) - const invTrans = new Matrix4(); - invTrans.makeTranslation(toVector3(cam.pivot_position).negate()); + const invTrans = new Matrix4() + invTrans.makeTranslation(toVector3(cam.pivot_position).negate()) - const result = new Matrix4(); - result.multiplyMatrices(invRot, invTrans); + const result = new Matrix4() + result.multiplyMatrices(invRot, invTrans) - return result; + return result } /** @@ -1967,8 +1959,8 @@ export function createViewMatrix( coordSystem: CoordinateSystem = WebGLCoordinateSystem ): Matrix4 { if (camera.is_ortho) { - const height = Math.tan(0.5 * degToRad(camera.fov_y)) * camera.eye_offset; - const width = height * aspectRatio; + const height = Math.tan(0.5 * degToRad(camera.fov_y)) * camera.eye_offset + const width = height * aspectRatio return createOrthographicProjectionMatrix( -width, width, @@ -1977,7 +1969,7 @@ export function createViewMatrix( nearClip, farClip, coordSystem - ); + ) } else { return createPerspectiveProjectionMatrix( camera.fov_y, @@ -1985,6 +1977,6 @@ export function createViewMatrix( nearClip, farClip, coordSystem - ); + ) } } From 374be2f7d55e7128d538b4951364bc6fbb8e1e81 Mon Sep 17 00:00:00 2001 From: David Reeves Date: Fri, 31 Oct 2025 13:24:47 +0000 Subject: [PATCH 3/5] Tidy comments --- src/clientSideScene/CameraControls.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index 1721fb53b9c..18eccaae607 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -1870,8 +1870,8 @@ function createOrthographicProjectionMatrix( switch (coordSystem) { case WebGLCoordinateSystem: - // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to - // [-1, 1] in NDC space + // NOTE: This assumes a right-handed y-up view space and that [-near, -far] in view space maps + // to [-1, 1] in NDC space return new Matrix4( 2.0 * invW, 0.0, 0.0, -(right + left) * invW, 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, @@ -1879,8 +1879,8 @@ function createOrthographicProjectionMatrix( 0.0, 0.0, 0.0, 1.0 ) case WebGPUCoordinateSystem: - // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to - // [0, 1] in NDC space + // NOTE: This assumes a right-handed y-up view space and that [-near, -far] in view space maps + // to [0, 1] in NDC space return new Matrix4( 2.0 * invW, 0.0, 0.0, -(right + left) * invW, 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, @@ -1902,8 +1902,8 @@ function createPerspectiveProjectionMatrix( switch (coordSystem) { case WebGLCoordinateSystem: - // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to - // [-1, 1] in NDC space + // NOTE: This assumes a right-handed y-up view space and that [-near, -far] in view space maps + // to [-1, 1] in NDC space return new Matrix4( 1.0 / x, 0.0, 0.0, 0.0, 0.0, 1.0 / y, 0.0, 0.0, @@ -1911,8 +1911,8 @@ function createPerspectiveProjectionMatrix( 0.0, 0.0, -1.0, 0.0 ) case WebGPUCoordinateSystem: - // NOTE: This assumes a right-handed y-up view space i.e. [-near, -far] in view space maps to - // [0, 1] in NDC space + // NOTE: This assumes a right-handed y-up view space and that [-near, -far] in view space maps + // to [0, 1] in NDC space return new Matrix4( 1.0 / x, 0.0, 0.0, 0.0, 0.0, 1.0 / y, 0.0, 0.0, From ead1aec1c58ffc76fdaa92cb1dbe66fe14e87067 Mon Sep 17 00:00:00 2001 From: David Reeves Date: Mon, 3 Nov 2025 11:05:06 +0000 Subject: [PATCH 4/5] Fix ortho near plane and add notes --- src/clientSideScene/CameraControls.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index 18eccaae607..c607b150349 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -4,7 +4,7 @@ import { isArray } from '@src/lib/utils' import * as TWEEN from '@tweenjs/tween.js' import Hammer from 'hammerjs' -import type { Camera, CoordinateSystem} from 'three' +import type { Camera, CoordinateSystem } from 'three' import { Euler, MathUtils, @@ -1875,7 +1875,7 @@ function createOrthographicProjectionMatrix( return new Matrix4( 2.0 * invW, 0.0, 0.0, -(right + left) * invW, 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, - 0.0, 0.0, 2.0 * invD, (far + near) *invD, + 0.0, 0.0, 2.0 * invD, (far + near) * invD, 0.0, 0.0, 0.0, 1.0 ) case WebGPUCoordinateSystem: @@ -1884,7 +1884,7 @@ function createOrthographicProjectionMatrix( return new Matrix4( 2.0 * invW, 0.0, 0.0, -(right + left) * invW, 0.0, 2.0 * invH, 0.0, -(top + bottom) * invH, - 0.0, 0.0, invD, near *invD, + 0.0, 0.0, invD, near * invD, 0.0, 0.0, 0.0, 1.0 ) } @@ -1959,14 +1959,19 @@ export function createViewMatrix( coordSystem: CoordinateSystem = WebGLCoordinateSystem ): Matrix4 { if (camera.is_ortho) { + // NOTE: The height of the ortho frustum is derived from FOV and eye distance to ensure + // consistent scale when switching between projection modes const height = Math.tan(0.5 * degToRad(camera.fov_y)) * camera.eye_offset const width = height * aspectRatio + + // NOTE: A mirrored far plane is used as the near plane in ortho mode to avoid clipping scene + // geometry when zooming in return createOrthographicProjectionMatrix( -width, width, -height, height, - nearClip, + -farClip, farClip, coordSystem ) From 0d1dfaa1edadf5d0b4f2095a0a07b832363fae08 Mon Sep 17 00:00:00 2001 From: David Reeves Date: Mon, 3 Nov 2025 19:18:47 +0000 Subject: [PATCH 5/5] Fix function names --- src/clientSideScene/CameraControls.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index c607b150349..97a5aa2623f 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -1931,9 +1931,9 @@ function toQuaternion(obj: { w: number; x: number; y: number; z: number }) { } /** - * Creates a projection matrix from the given CameraViewState in a way that is consistent with engine + * Creates a view matrix from the given CameraViewState in a way that is consistent with engine */ -export function createProjectionMatrix(cam: CameraViewState): Matrix4 { +export function createViewMatrix(cam: CameraViewState): Matrix4 { const invRot = new Matrix4() invRot.makeRotationFromQuaternion( toQuaternion(cam.pivot_rotation).conjugate() @@ -1949,9 +1949,9 @@ export function createProjectionMatrix(cam: CameraViewState): Matrix4 { } /** - * Creates a view matrix from the given CameraViewState in a way that is consistent with engine + * Creates a projection matrix from the given CameraViewState in a way that is consistent with engine */ -export function createViewMatrix( +export function createProjectionMatrix( camera: CameraViewState, aspectRatio: number, nearClip: number,