Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 134 additions & 1 deletion src/clientSideScene/CameraControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -15,6 +15,8 @@ import {
Spherical,
Vector2,
Vector3,
WebGLCoordinateSystem,
WebGPUCoordinateSystem,
} from 'three'

import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
Expand Down Expand Up @@ -1852,3 +1854,134 @@ 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 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,
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 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,
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 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,
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 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,
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 view matrix from the given CameraViewState in a way that is consistent with engine
*/
export function createViewMatrix(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 projection matrix from the given CameraViewState in a way that is consistent with engine
*/
export function createProjectionMatrix(
camera: CameraViewState,
aspectRatio: number,
nearClip: number,
farClip: number,
coordSystem: CoordinateSystem = WebGLCoordinateSystem
Copy link
Contributor

@andrewvarga andrewvarga Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we be able get the coordSystem from the engine? Or for now we can just assume WebGL

Copy link
Contributor Author

@davreev davreev Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be determined by the active renderer on the FE (assumed WebGL).

): 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,
-farClip,
Copy link
Contributor

@andrewvarga andrewvarga Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be using nearClip? Oh, I just saw the comment above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a sneaky trick to allow "infinite" zoom in ortho mode.

farClip,
coordSystem
)
} else {
return createPerspectiveProjectionMatrix(
camera.fov_y,
aspectRatio,
nearClip,
farClip,
coordSystem
)
}
}
Loading