From 7720b458ff9dd458197f36b8392ea0a71602a1ba Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 7 Apr 2026 06:33:12 +0200 Subject: [PATCH] Add planar reflection implementation in JavaScript and TypeScript --- src/rendering/planar-reflection.js | 50 ++++++++++++++++++ src/rendering/planar-reflection.ts | 82 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/rendering/planar-reflection.js create mode 100644 src/rendering/planar-reflection.ts diff --git a/src/rendering/planar-reflection.js b/src/rendering/planar-reflection.js new file mode 100644 index 00000000..ee8869a7 --- /dev/null +++ b/src/rendering/planar-reflection.js @@ -0,0 +1,50 @@ +import { Euler, Matrix4, PerspectiveCamera, Plane, Quaternion, Vector3, Vector4 } from "three"; + +const SURFACE_UP = new Vector3(0, 1, 0); +const CAMERA_FORWARD = new Vector3(0, 0, -1); + +function createRotationQuaternion(rotationDegrees) { + return new Quaternion().setFromEuler(new Euler((rotationDegrees.x * Math.PI) / 180, (rotationDegrees.y * Math.PI) / 180, (rotationDegrees.z * Math.PI) / 180, "XYZ")); +} + +export function updatePlanarReflectionCamera(surface, sourceCamera, reflectionCamera, reflectionMatrix, clipBias = 0.003) { + const rotation = createRotationQuaternion(surface.rotationDegrees); + const surfaceNormal = SURFACE_UP.clone().applyQuaternion(rotation).normalize(); + const surfaceCenter = new Vector3(surface.center.x, surface.center.y, surface.center.z).add(surfaceNormal.clone().multiplyScalar(surface.size.y * 0.5)); + const cameraWorldPosition = new Vector3().setFromMatrixPosition(sourceCamera.matrixWorld); + const sourceRotationMatrix = new Matrix4().extractRotation(sourceCamera.matrixWorld); + const lookAtPosition = CAMERA_FORWARD.clone().applyMatrix4(sourceRotationMatrix).add(cameraWorldPosition); + const reflectedViewPosition = surfaceCenter.clone().sub(cameraWorldPosition); + if (reflectedViewPosition.dot(surfaceNormal) > 0) { + return false; + } + reflectedViewPosition.reflect(surfaceNormal).negate(); + reflectedViewPosition.add(surfaceCenter); + const reflectedTarget = surfaceCenter.clone().sub(lookAtPosition); + reflectedTarget.reflect(surfaceNormal).negate(); + reflectedTarget.add(surfaceCenter); + reflectionCamera.position.copy(reflectedViewPosition); + reflectionCamera.up.set(0, 1, 0).applyMatrix4(sourceRotationMatrix).reflect(surfaceNormal); + reflectionCamera.near = sourceCamera.near; + reflectionCamera.far = sourceCamera.far; + reflectionCamera.aspect = sourceCamera.aspect; + reflectionCamera.projectionMatrix.copy(sourceCamera.projectionMatrix); + reflectionCamera.projectionMatrixInverse.copy(sourceCamera.projectionMatrixInverse); + reflectionCamera.lookAt(reflectedTarget); + reflectionCamera.updateMatrixWorld(); + reflectionCamera.matrixWorldInverse.copy(reflectionCamera.matrixWorld).invert(); + reflectionMatrix.set(0.5, 0, 0, 0.5, 0, 0.5, 0, 0.5, 0, 0, 0.5, 0.5, 0, 0, 0, 1); + reflectionMatrix.multiply(reflectionCamera.projectionMatrix); + reflectionMatrix.multiply(reflectionCamera.matrixWorldInverse); + const clipPlane = new Plane().setFromNormalAndCoplanarPoint(surfaceNormal, surfaceCenter).applyMatrix4(reflectionCamera.matrixWorldInverse); + const clipVector = new Vector4(clipPlane.normal.x, clipPlane.normal.y, clipPlane.normal.z, clipPlane.constant); + const projectionElements = reflectionCamera.projectionMatrix.elements; + const q = new Vector4((Math.sign(clipVector.x) + projectionElements[8]) / projectionElements[0], (Math.sign(clipVector.y) + projectionElements[9]) / projectionElements[5], -1, (1 + projectionElements[10]) / projectionElements[14]); + clipVector.multiplyScalar(2 / clipVector.dot(q)); + projectionElements[2] = clipVector.x; + projectionElements[6] = clipVector.y; + projectionElements[10] = clipVector.z + 1 - clipBias; + projectionElements[14] = clipVector.w; + reflectionCamera.projectionMatrixInverse.copy(reflectionCamera.projectionMatrix).invert(); + return true; +} \ No newline at end of file diff --git a/src/rendering/planar-reflection.ts b/src/rendering/planar-reflection.ts new file mode 100644 index 00000000..5b8bf75e --- /dev/null +++ b/src/rendering/planar-reflection.ts @@ -0,0 +1,82 @@ +import { Euler, Matrix4, PerspectiveCamera, Plane, Quaternion, Vector3, Vector4 } from "three"; + +import type { Vec3 } from "../core/vector"; + +export interface PlanarReflectionSurface { + center: Vec3; + rotationDegrees: Vec3; + size: Vec3; +} + +const SURFACE_UP = new Vector3(0, 1, 0); +const CAMERA_FORWARD = new Vector3(0, 0, -1); + +function createRotationQuaternion(rotationDegrees: Vec3) { + return new Quaternion().setFromEuler( + new Euler((rotationDegrees.x * Math.PI) / 180, (rotationDegrees.y * Math.PI) / 180, (rotationDegrees.z * Math.PI) / 180, "XYZ") + ); +} + +export function updatePlanarReflectionCamera( + surface: PlanarReflectionSurface, + sourceCamera: PerspectiveCamera, + reflectionCamera: PerspectiveCamera, + reflectionMatrix: Matrix4, + clipBias = 0.003 +) { + const rotation = createRotationQuaternion(surface.rotationDegrees); + const surfaceNormal = SURFACE_UP.clone().applyQuaternion(rotation).normalize(); + const surfaceCenter = new Vector3(surface.center.x, surface.center.y, surface.center.z).add( + surfaceNormal.clone().multiplyScalar(surface.size.y * 0.5) + ); + const cameraWorldPosition = new Vector3().setFromMatrixPosition(sourceCamera.matrixWorld); + const sourceRotationMatrix = new Matrix4().extractRotation(sourceCamera.matrixWorld); + const lookAtPosition = CAMERA_FORWARD.clone().applyMatrix4(sourceRotationMatrix).add(cameraWorldPosition); + const reflectedViewPosition = surfaceCenter.clone().sub(cameraWorldPosition); + + if (reflectedViewPosition.dot(surfaceNormal) > 0) { + return false; + } + + reflectedViewPosition.reflect(surfaceNormal).negate(); + reflectedViewPosition.add(surfaceCenter); + + const reflectedTarget = surfaceCenter.clone().sub(lookAtPosition); + reflectedTarget.reflect(surfaceNormal).negate(); + reflectedTarget.add(surfaceCenter); + + reflectionCamera.position.copy(reflectedViewPosition); + reflectionCamera.up.set(0, 1, 0).applyMatrix4(sourceRotationMatrix).reflect(surfaceNormal); + reflectionCamera.near = sourceCamera.near; + reflectionCamera.far = sourceCamera.far; + reflectionCamera.aspect = sourceCamera.aspect; + reflectionCamera.projectionMatrix.copy(sourceCamera.projectionMatrix); + reflectionCamera.projectionMatrixInverse.copy(sourceCamera.projectionMatrixInverse); + reflectionCamera.lookAt(reflectedTarget); + reflectionCamera.updateMatrixWorld(); + reflectionCamera.matrixWorldInverse.copy(reflectionCamera.matrixWorld).invert(); + + reflectionMatrix.set(0.5, 0, 0, 0.5, 0, 0.5, 0, 0.5, 0, 0, 0.5, 0.5, 0, 0, 0, 1); + reflectionMatrix.multiply(reflectionCamera.projectionMatrix); + reflectionMatrix.multiply(reflectionCamera.matrixWorldInverse); + + const clipPlane = new Plane().setFromNormalAndCoplanarPoint(surfaceNormal, surfaceCenter).applyMatrix4(reflectionCamera.matrixWorldInverse); + const clipVector = new Vector4(clipPlane.normal.x, clipPlane.normal.y, clipPlane.normal.z, clipPlane.constant); + const projectionElements = reflectionCamera.projectionMatrix.elements; + const q = new Vector4( + (Math.sign(clipVector.x) + projectionElements[8]) / projectionElements[0], + (Math.sign(clipVector.y) + projectionElements[9]) / projectionElements[5], + -1, + (1 + projectionElements[10]) / projectionElements[14] + ); + + clipVector.multiplyScalar(2 / clipVector.dot(q)); + + projectionElements[2] = clipVector.x; + projectionElements[6] = clipVector.y; + projectionElements[10] = clipVector.z + 1 - clipBias; + projectionElements[14] = clipVector.w; + reflectionCamera.projectionMatrixInverse.copy(reflectionCamera.projectionMatrix).invert(); + + return true; +} \ No newline at end of file