diff --git a/examples/camera-updates/src/main.ts b/examples/camera-updates/src/main.ts index 50ec5e4..faf2562 100644 --- a/examples/camera-updates/src/main.ts +++ b/examples/camera-updates/src/main.ts @@ -6,7 +6,7 @@ const progressIndicator = document.getElementById("progress-indicator") as HTMLP const renderer = new SPLAT.WebGLRenderer(canvas); const scene = new SPLAT.Scene(); -const camera = scene.camera!.object; +const camera = scene.findObjectOfType(SPLAT.Camera) as SPLAT.Camera; const controls = new SPLAT.OrbitControls(scene, canvas); async function main() { diff --git a/examples/editor/src/main.ts b/examples/editor/src/main.ts index 5557726..7a09d43 100644 --- a/examples/editor/src/main.ts +++ b/examples/editor/src/main.ts @@ -6,8 +6,9 @@ const progressIndicator = document.getElementById("progress-indicator") as HTMLP const renderer = new SPLAT.WebGLRenderer(canvas); const scene = new SPLAT.Scene(); +const camera = scene.findObjectOfType(SPLAT.Camera) as SPLAT.Camera; const controls = new SPLAT.OrbitControls(scene, canvas, 0, 0); -let object: SPLAT.Object3D; +let splat: SPLAT.Splat; function getSplatCorners(position: SPLAT.Vector3, rotation: SPLAT.Quaternion, scale: SPLAT.Vector3) { const localCorners = [ @@ -77,19 +78,23 @@ function findIntersectedSplat(origin: SPLAT.Vector3, direction: SPLAT.Vector3) { let closestSplat = null; let minDistance = Infinity; - for (let i = 0; i < object.vertexCount; i++) { + for (let i = 0; i < splat.data.vertexCount; i++) { const position = new SPLAT.Vector3( - object.positions[3 * i + 0], - object.positions[3 * i + 1], - object.positions[3 * i + 2], + splat.data.positions[3 * i + 0], + splat.data.positions[3 * i + 1], + splat.data.positions[3 * i + 2], ); const rotation = new SPLAT.Quaternion( - object.rotations[4 * i + 1], - object.rotations[4 * i + 2], - object.rotations[4 * i + 3], - -object.rotations[4 * i + 0], + splat.data.rotations[4 * i + 1], + splat.data.rotations[4 * i + 2], + splat.data.rotations[4 * i + 3], + -splat.data.rotations[4 * i + 0], + ); + const scale = new SPLAT.Vector3( + splat.data.scales[3 * i + 0], + splat.data.scales[3 * i + 1], + splat.data.scales[3 * i + 2], ); - const scale = new SPLAT.Vector3(object.scales[3 * i + 0], object.scales[3 * i + 1], object.scales[3 * i + 2]); const corners = getSplatCorners(position, rotation, scale); const triangles = [ [corners[0], corners[1], corners[2]], @@ -109,7 +114,7 @@ function findIntersectedSplat(origin: SPLAT.Vector3, direction: SPLAT.Vector3) { async function main() { const url = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k-mini.splat"; - object = await SPLAT.Loader.LoadAsync(url, scene, (progress) => (progressIndicator.value = progress * 100)); + splat = await SPLAT.Loader.LoadAsync(url, scene, (progress) => (progressIndicator.value = progress * 100)); progressDialog.close(); const handleResize = () => { @@ -121,10 +126,10 @@ async function main() { const y = -(event.clientY / canvas.clientHeight) * 2 + 1; const clipSpaceCoords = new SPLAT.Vector4(x, y, -1, 1); - const inverseProjectionMatrix = camera.projectionMatrix.invert(); + const inverseProjectionMatrix = camera.data.projectionMatrix.invert(); const cameraSpaceCoords = clipSpaceCoords.multiply(inverseProjectionMatrix); - const inverseViewMatrix = camera.viewMatrix.invert(); + const inverseViewMatrix = camera.data.viewMatrix.invert(); const worldSpaceCoords = cameraSpaceCoords.multiply(inverseViewMatrix); const worldSpacePosition = new SPLAT.Vector3( worldSpaceCoords.x / worldSpaceCoords.w, @@ -137,7 +142,7 @@ async function main() { const closestSplat = findIntersectedSplat(origin, direction); if (closestSplat !== null) { - object.removeSplat(closestSplat); + console.log(`Clicked on splat ${closestSplat}`); } }; @@ -147,13 +152,14 @@ async function main() { const rotation = SPLAT.Quaternion.FromEuler(new SPLAT.Vector3(0.01, 0.01, 0.01)); const scale = new SPLAT.Vector3(1, 1, 1).multiply(0.02); const color = new SPLAT.Color32(255, 0, 0); - object.addSplat(position, rotation, scale, color); + console.log(`Adding splat at position ${position}, rotation ${rotation}, scale ${scale}, color ${color}`); + // splat.addSplat(position, rotation, scale, color); } }; const frame = () => { controls.update(); - renderer.render(scene, camera); + renderer.render(scene); requestAnimationFrame(frame); }; diff --git a/examples/scene-transformations/src/main.ts b/examples/scene-transformations/src/main.ts index 49f6cf3..212aaf8 100644 --- a/examples/scene-transformations/src/main.ts +++ b/examples/scene-transformations/src/main.ts @@ -12,22 +12,21 @@ async function main() { // Load the scene const name = "bonsai"; const url = `https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/${name}/${name}-7k.splat`; - const object = await SPLAT.Loader.LoadAsync(url, scene, (progress) => (progressIndicator.value = progress * 100)); + const splat = await SPLAT.Loader.LoadAsync(url, scene, (progress) => (progressIndicator.value = progress * 100)); progressDialog.close(); // Transform it - const splatFilter = object.getComponent(SPLAT.SplatFilter)!; const rotation = new SPLAT.Vector3(0, 0, 0); const translation = new SPLAT.Vector3(-0.2, 0.2, 0); const scaling = new SPLAT.Vector3(1.5, 1.5, 1.5); const limitSize = 3.0; - object.rotation = SPLAT.Quaternion.FromEuler(rotation); - object.position = translation; - object.scale = scaling; - splatFilter.limitBox(-limitSize, limitSize, -limitSize, limitSize, -limitSize, limitSize); - splatFilter.applyPosition(); - splatFilter.applyRotation(); - splatFilter.applyScale(); + splat.rotation = SPLAT.Quaternion.FromEuler(rotation); + splat.position = translation; + splat.scale = scaling; + splat.data.limitBox(-limitSize, limitSize, -limitSize, limitSize, -limitSize, limitSize); + splat.applyPosition(); + splat.applyRotation(); + splat.applyScale(); const handleResize = () => { renderer.setSize(canvas.clientWidth, canvas.clientHeight); @@ -42,11 +41,11 @@ async function main() { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "PageUp") { - object.scale = new SPLAT.Vector3(1.1, 1.1, 1.1); - splatFilter.applyScale(); + splat.scale = new SPLAT.Vector3(1.1, 1.1, 1.1); + splat.applyScale(); } else if (e.key === "PageDown") { - object.scale = new SPLAT.Vector3(0.9, 0.9, 0.9); - splatFilter.applyScale(); + splat.scale = new SPLAT.Vector3(0.9, 0.9, 0.9); + splat.applyScale(); } }; diff --git a/src/cameras/Camera.ts b/src/cameras/Camera.ts index ba9bffb..706392e 100644 --- a/src/cameras/Camera.ts +++ b/src/cameras/Camera.ts @@ -1,76 +1,21 @@ +import { CameraData } from "./CameraData"; import { Object3D } from "../core/Object3D"; -import { Quaternion } from "../math/Quaternion"; -import { Matrix3 } from "../math/Matrix3"; -import { Matrix4 } from "../math/Matrix4"; -import { Vector3 } from "../math/Vector3"; -import { Component } from "../core/Component"; -class Camera extends Component { - fx: number = 1132; - fy: number = 1132; - near: number = 0.1; - far: number = 100; +class Camera extends Object3D { + private _data: CameraData; - projectionMatrix: Matrix4; - viewMatrix: Matrix4; - viewProj: Matrix4; + constructor(camera: CameraData | undefined = undefined) { + super(); - update: (width: number, height: number) => void; + this._data = camera ? camera : new CameraData(); - constructor(object: Object3D) { - super(object); - - const getViewMatrix = (): Matrix4 => { - const R = Matrix3.RotationFromQuaternion(this.object.rotation).buffer; - const t = this.object.position.flat(); - const camToWorld = [ - [R[0], R[1], R[2], 0], - [R[3], R[4], R[5], 0], - [R[6], R[7], R[8], 0], - [ - -t[0] * R[0] - t[1] * R[3] - t[2] * R[6], - -t[0] * R[1] - t[1] * R[4] - t[2] * R[7], - -t[0] * R[2] - t[1] * R[5] - t[2] * R[8], - 1, - ], - ].flat(); - return new Matrix4(...camToWorld); - }; - - this.projectionMatrix = new Matrix4(); - this.viewMatrix = new Matrix4(); - this.viewProj = new Matrix4(); - - this.update = (width: number, height: number) => { - // prettier-ignore - this.projectionMatrix = new Matrix4( - 2 * this.fx / width, 0, 0, 0, - 0, -2 * this.fy / height, 0, 0, - 0, 0, this.far / (this.far - this.near), 1, - 0, 0, -(this.far * this.near) / (this.far - this.near), 0 - ); - this.viewMatrix = getViewMatrix(); - this.viewProj = this.projectionMatrix.multiply(this.viewMatrix); + this.update = () => { + this.data.update(this.position, this.rotation); }; } - static Create( - position: Vector3 = new Vector3(0, 0, -5), - rotation: Quaternion = new Quaternion(), - fx: number = 1132, - fy: number = 1132, - near: number = 0.1, - far: number = 100, - ): Camera { - const object = new Object3D(); - object.position = position; - object.rotation = rotation; - const camera = object.addComponent(Camera); - camera.fx = fx; - camera.fy = fy; - camera.near = near; - camera.far = far; - return camera; + get data() { + return this._data; } } diff --git a/src/cameras/CameraData.ts b/src/cameras/CameraData.ts new file mode 100644 index 0000000..8a9071c --- /dev/null +++ b/src/cameras/CameraData.ts @@ -0,0 +1,127 @@ +import { Quaternion } from "../math/Quaternion"; +import { Matrix3 } from "../math/Matrix3"; +import { Matrix4 } from "../math/Matrix4"; +import { Vector3 } from "../math/Vector3"; + +class CameraData { + private _fx: number = 1132; + private _fy: number = 1132; + private _near: number = 0.1; + private _far: number = 100; + + private _width: number = 512; + private _height: number = 512; + + private _projectionMatrix: Matrix4 = new Matrix4(); + private _viewMatrix: Matrix4 = new Matrix4(); + private _viewProj: Matrix4 = new Matrix4(); + + update: (position: Vector3, rotation: Quaternion) => void; + setSize: (width: number, height: number) => void; + + private _updateProjectionMatrix: () => void; + + constructor() { + this._updateProjectionMatrix = () => { + // prettier-ignore + this._projectionMatrix = new Matrix4( + 2 * this.fx / this.width, 0, 0, 0, + 0, -2 * this.fy / this.height, 0, 0, + 0, 0, this.far / (this.far - this.near), 1, + 0, 0, -(this.far * this.near) / (this.far - this.near), 0 + ); + + this._viewProj = this.projectionMatrix.multiply(this.viewMatrix); + }; + + this.update = (position: Vector3, rotation: Quaternion) => { + const R = Matrix3.RotationFromQuaternion(rotation).buffer; + const t = position.flat(); + + // prettier-ignore + this._viewMatrix = new Matrix4( + R[0], R[1], R[2], 0, + R[3], R[4], R[5], 0, + R[6], R[7], R[8], 0, + -t[0] * R[0] - t[1] * R[3] - t[2] * R[6], + -t[0] * R[1] - t[1] * R[4] - t[2] * R[7], + -t[0] * R[2] - t[1] * R[5] - t[2] * R[8], + 1, + ); + + this._viewProj = this.projectionMatrix.multiply(this.viewMatrix); + }; + + this.setSize = (width: number, height: number) => { + this._width = width; + this._height = height; + this._updateProjectionMatrix(); + }; + } + + get fx() { + return this._fx; + } + + set fx(fx: number) { + if (this._fx !== fx) { + this._fx = fx; + this._updateProjectionMatrix(); + } + } + + get fy() { + return this._fy; + } + + set fy(fy: number) { + if (this._fy !== fy) { + this._fy = fy; + this._updateProjectionMatrix(); + } + } + + get near() { + return this._near; + } + + set near(near: number) { + if (this._near !== near) { + this._near = near; + this._updateProjectionMatrix(); + } + } + + get far() { + return this._far; + } + + set far(far: number) { + if (this._far !== far) { + this._far = far; + this._updateProjectionMatrix(); + } + } + + get width() { + return this._width; + } + + get height() { + return this._height; + } + + get projectionMatrix() { + return this._projectionMatrix; + } + + get viewMatrix() { + return this._viewMatrix; + } + + get viewProj() { + return this._viewProj; + } +} + +export { CameraData }; diff --git a/src/controls/OrbitControls.ts b/src/controls/OrbitControls.ts index 09f038f..f9e43d7 100644 --- a/src/controls/OrbitControls.ts +++ b/src/controls/OrbitControls.ts @@ -1,4 +1,4 @@ -import type { Camera } from "../cameras/Camera"; +import { Camera } from "../cameras/Camera"; import { Scene } from "../core/Scene"; import { Matrix3 } from "../math/Matrix3"; import { Quaternion } from "../math/Quaternion"; @@ -14,8 +14,6 @@ class OrbitControls { zoomSpeed: number = 1; dampening: number = 0.12; setCameraTarget: (newTarget: Vector3) => void = () => {}; - attach: (newCamera: Camera) => void = () => {}; - detach: () => void = () => {}; update: () => void; dispose: () => void; @@ -28,8 +26,9 @@ class OrbitControls { enableKeyboardControls: boolean = true, inputTarget: Vector3 = new Vector3(), ) { - if (!scene.camera) { - throw new Error("Scene does not have a camera"); + const camera = scene.findObjectOfType(Camera) as Camera; + if (!camera) { + throw new Error("No camera found in scene"); } let target = inputTarget.clone(); @@ -47,45 +46,28 @@ class OrbitControls { const keys: { [key: string]: boolean } = {}; - let camera: Camera | null = null; let isUpdatingCamera = false; - const onCameraObjectChange = () => { - if (!camera || isUpdatingCamera) return; + const onCameraChange = () => { + if (isUpdatingCamera) return; - const eulerRotation = camera.object.rotation.toEuler(); + const eulerRotation = camera.rotation.toEuler(); desiredAlpha = -eulerRotation.y; desiredBeta = -eulerRotation.x; - const x = camera.object.position.x - desiredRadius * Math.sin(desiredAlpha) * Math.cos(desiredBeta); - const y = camera.object.position.y + desiredRadius * Math.sin(desiredBeta); - const z = camera.object.position.z + desiredRadius * Math.cos(desiredAlpha) * Math.cos(desiredBeta); + const x = camera.position.x - desiredRadius * Math.sin(desiredAlpha) * Math.cos(desiredBeta); + const y = camera.position.y + desiredRadius * Math.sin(desiredBeta); + const z = camera.position.z + desiredRadius * Math.cos(desiredAlpha) * Math.cos(desiredBeta); desiredTarget = new Vector3(x, y, z); }; - this.attach = (newCamera: Camera) => { - if (camera) { - this.detach(); - } - camera = newCamera; - camera.object.addEventListener("change", onCameraObjectChange); - }; - - this.detach = () => { - if (!camera) return; - - camera.object.removeEventListener("change", onCameraObjectChange); - camera = null; - }; - - this.attach(scene.camera); + camera.addEventListener("change", onCameraChange); this.setCameraTarget = (newTarget: Vector3) => { - if (!camera) return; - const dx = newTarget.x - camera.object.position.x; - const dy = newTarget.y - camera.object.position.y; - const dz = newTarget.z - camera.object.position.z; + const dx = newTarget.x - camera.position.x; + const dy = newTarget.y - camera.position.y; + const dz = newTarget.z - camera.position.z; desiredRadius = Math.sqrt(dx * dx + dy * dy + dz * dz); desiredBeta = Math.atan2(dy, Math.sqrt(dx * dx + dz * dz)); desiredAlpha = -Math.atan2(dx, dz); @@ -143,7 +125,7 @@ class OrbitControls { const zoomNorm = computeZoomNorm(); const panX = -dx * this.panSpeed * 0.01 * zoomNorm; const panY = -dy * this.panSpeed * 0.01 * zoomNorm; - const R = Matrix3.RotationFromQuaternion(camera.object.rotation).buffer; + const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer; const right = new Vector3(R[0], R[3], R[6]); const up = new Vector3(R[1], R[4], R[7]); desiredTarget = desiredTarget.add(right.multiply(panX)); @@ -216,7 +198,7 @@ class OrbitControls { const touchY = (e.touches[0].clientY + e.touches[1].clientY) / 2; const dx = touchX - lastX; const dy = touchY - lastY; - const R = Matrix3.RotationFromQuaternion(camera.object.rotation).buffer; + const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer; const right = new Vector3(R[0], R[3], R[6]); const up = new Vector3(R[1], R[4], R[7]); desiredTarget = desiredTarget.add(right.multiply(-dx * this.panSpeed * 0.025 * zoomNorm)); @@ -244,8 +226,6 @@ class OrbitControls { }; this.update = () => { - if (!camera) return; - isUpdatingCamera = true; alpha = lerp(alpha, desiredAlpha, this.dampening); @@ -256,18 +236,18 @@ class OrbitControls { const x = target.x + radius * Math.sin(alpha) * Math.cos(beta); const y = target.y - radius * Math.sin(beta); const z = target.z - radius * Math.cos(alpha) * Math.cos(beta); - camera.object.position = new Vector3(x, y, z); + camera.position = new Vector3(x, y, z); - const direction = target.subtract(camera.object.position).normalize(); + const direction = target.subtract(camera.position).normalize(); const rx = Math.asin(-direction.y); const ry = Math.atan2(direction.x, direction.z); - camera.object.rotation = Quaternion.FromEuler(new Vector3(rx, ry, 0)); + camera.rotation = Quaternion.FromEuler(new Vector3(rx, ry, 0)); // Just spit balling here on the values const moveSpeed = 0.025; const rotateSpeed = 0.01; - const R = Matrix3.RotationFromQuaternion(camera.object.rotation).buffer; + const R = Matrix3.RotationFromQuaternion(camera.rotation).buffer; const forward = new Vector3(-R[2], -R[5], -R[8]); const right = new Vector3(R[0], R[3], R[6]); diff --git a/src/core/Component.ts b/src/core/Component.ts deleted file mode 100644 index 1db218f..0000000 --- a/src/core/Component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { EventDispatcher } from "./EventDispatcher"; -import { Object3D } from "./Object3D"; - -abstract class Component extends EventDispatcher { - private _object: Object3D; - - constructor(object: Object3D) { - super(); - - if (!object || !object.flag) { - throw new Error("Can't create component outside of Object3D.addComponent"); - } - - this._object = object; - } - - get object() { - return this._object; - } -} - -export { Component }; diff --git a/src/core/Object3D.ts b/src/core/Object3D.ts index 7de7acd..8089f76 100644 --- a/src/core/Object3D.ts +++ b/src/core/Object3D.ts @@ -1,55 +1,36 @@ import { Vector3 } from "../math/Vector3"; import { Quaternion } from "../math/Quaternion"; import { EventDispatcher } from "./EventDispatcher"; -import { Component } from "./Component"; -class Object3D extends EventDispatcher { - private _components: Component[] = []; - private _position: Vector3; - private _rotation: Quaternion; - private _scale: Vector3; - private _flag = false; +abstract class Object3D extends EventDispatcher { + protected _position: Vector3 = new Vector3(); + protected _rotation: Quaternion = new Quaternion(); + protected _scale: Vector3 = new Vector3(1, 1, 1); + protected _changeEvent: Event; - private _changeEvent: Event; + update: () => void; + applyPosition: () => void; + applyRotation: () => void; + applyScale: () => void; constructor() { super(); - this._position = new Vector3(); - this._rotation = new Quaternion(); - this._scale = new Vector3(1, 1, 1); this._changeEvent = new Event("change"); - } - addComponent(component: new (object: Object3D) => T) { - const existingComponent = this.getComponent(component); - if (existingComponent) { - throw new Error("Object already has this component"); - } - this._flag = true; - const instance = new component(this); - this._flag = false; - this._components.push(instance); - return instance; - } + this.update = () => {}; - removeComponent(component: new (object: Object3D) => T) { - const existingComponent = this.getComponent(component); - if (!existingComponent) { - throw new Error("Object does not have this component"); - } - const index = this._components.indexOf(existingComponent); - if (index >= 0) { - this._components.splice(index, 1); - } - } + this.applyPosition = () => { + this.position = new Vector3(); + }; - hasComponent(component: new (object: Object3D) => T) { - return this._components.some((c) => c instanceof component); - } + this.applyRotation = () => { + this.rotation = new Quaternion(); + }; - getComponent(component: new (object: Object3D) => T) { - return this._components.find((c) => c instanceof component) as T | undefined; + this.applyScale = () => { + this.scale = new Vector3(1, 1, 1); + }; } get position() { @@ -84,10 +65,6 @@ class Object3D extends EventDispatcher { this.dispatchEvent(this._changeEvent); } } - - get flag() { - return this._flag; - } } export { Object3D }; diff --git a/src/core/Scene.ts b/src/core/Scene.ts index 9d000ce..f46dfdc 100644 --- a/src/core/Scene.ts +++ b/src/core/Scene.ts @@ -1,73 +1,54 @@ import { Camera } from "../cameras/Camera"; -import { EventDispatcher } from "./EventDispatcher"; import { Object3D } from "./Object3D"; +import { SplatData } from "./SplatData"; import { Splat } from "./Splat"; -import { SplatFilter } from "./SplatFilter"; -class Scene extends EventDispatcher { +class Scene { private _objects: Object3D[] = []; - private _camera: Camera | null = null; - appendObject: (object: Object3D) => void; + addObject: (object: Object3D) => void; removeObject: (object: Object3D) => void; + findObject: (predicate: (object: Object3D) => boolean) => Object3D | undefined; + findObjectOfType: (type: { new (): T }) => T | undefined; reset: () => void; saveToFile: (name: string) => void; constructor(addDefaultObjects = true) { - super(); - - const changeEvent = { type: "change" } as Event; - - const handleObjectChange = () => { - this.dispatchEvent(changeEvent); + this.addObject = (object: Object3D) => { + this.objects.push(object); }; - this.appendObject = (object: Object3D) => { - const camera = object.getComponent(Camera); - if (camera) { - if (this._camera) { - throw new Error("Scene already has a camera"); - } - this._camera = camera; - } - this._objects.push(object); - if (!camera) { - object.addEventListener("change", handleObjectChange); + this.removeObject = (object: Object3D) => { + const index = this.objects.indexOf(object); + if (index >= 0) { + this.objects.splice(index, 1); } - this.dispatchEvent(changeEvent); }; - const addDefault = () => { - const camera = Camera.Create(); - this.appendObject(camera.object); + this.findObject = (predicate: (object: Object3D) => boolean) => { + for (const object of this.objects) { + if (predicate(object)) { + return object; + } + } + return undefined; }; - this.removeObject = (object: Object3D) => { - const camera = object.getComponent(Camera); - if (camera) { - if (this._camera !== camera) { - throw new Error("Scene does not have this camera"); + this.findObjectOfType = (type: { new (): T }) => { + for (const object of this.objects) { + if (object instanceof type) { + return object; } - this._camera = null; - } - const index = this._objects.indexOf(object); - if (index >= 0) { - object.removeEventListener("change", handleObjectChange); - this._objects.splice(index, 1); - this.dispatchEvent(changeEvent); } + return undefined; }; this.reset = () => { - for (const object of this._objects) { - object.removeEventListener("change", handleObjectChange); - } this._objects = []; - this._camera = null; if (addDefaultObjects) { - addDefault(); + const defaultCamera = new Camera(); + this.addObject(defaultCamera); } - this.dispatchEvent(changeEvent); }; this.saveToFile = (name: string) => { @@ -76,15 +57,15 @@ class Scene extends EventDispatcher { const buffers: Uint8Array[] = []; let vertexCount = 0; - for (const object of this._objects) { - const splatFilter = object.getComponent(SplatFilter); - if (!splatFilter || !splatFilter.splat) continue; - const buffer = splatFilter.serialize(); - buffers.push(buffer); - vertexCount += splatFilter.splat.vertexCount; + for (const object of this.objects) { + if (object instanceof Splat) { + const buffer = object.data.serialize(); + buffers.push(buffer); + vertexCount += object.data.vertexCount; + } } - const data = new Uint8Array(vertexCount * Splat.RowLength); + const data = new Uint8Array(vertexCount * SplatData.RowLength); let offset = 0; for (const buffer of buffers) { data.set(buffer, offset); @@ -98,20 +79,12 @@ class Scene extends EventDispatcher { link.click(); }; - this._objects = []; - - if (addDefaultObjects) { - addDefault(); - } + this.reset(); } get objects() { return this._objects; } - - get camera() { - return this._camera; - } } export { Scene }; diff --git a/src/core/Splat.ts b/src/core/Splat.ts index f9bd03b..478a6be 100644 --- a/src/core/Splat.ts +++ b/src/core/Splat.ts @@ -1,44 +1,86 @@ -class Splat { - static RowLength = 3 * 4 + 3 * 4 + 4 + 4; - - private _vertexCount: number; - private _positions: Float32Array; - private _rotations: Float32Array; - private _scales: Float32Array; - private _colors: Uint8Array; - - constructor( - vertexCount: number = 0, - positions: Float32Array | null = null, - rotations: Float32Array | null = null, - scales: Float32Array | null = null, - colors: Uint8Array | null = null, - ) { - this._vertexCount = vertexCount; - this._positions = positions || new Float32Array(0); - this._rotations = rotations || new Float32Array(0); - this._scales = scales || new Float32Array(0); - this._colors = colors || new Uint8Array(0); - } +import { SplatData } from "./SplatData"; +import { Object3D } from "./Object3D"; +import { Vector3 } from "../math/Vector3"; +import { Quaternion } from "../math/Quaternion"; +import { Matrix3 } from "../math/Matrix3"; + +class Splat extends Object3D { + private _data: SplatData; + + constructor(splat: SplatData | undefined = undefined) { + super(); + + this._data = splat || new SplatData(); + + this.applyPosition = () => { + this.data.translate(this.position); + this.position = new Vector3(); + }; + + this.applyRotation = () => { + this.data.rotate(this.rotation); + this.rotation = new Quaternion(); + }; - get vertexCount() { - return this._vertexCount; + this.applyScale = () => { + this.data.scale(this.scale); + this.scale = new Vector3(1, 1, 1); + }; } - get positions() { - return this._positions; + get data() { + return this._data; } - get rotations() { - return this._rotations; + get worldPositions() { + const result = new Float32Array(this.data.vertexCount * 3); + const R = Matrix3.RotationFromQuaternion(this.rotation).buffer; + for (let i = 0; i < this.data.vertexCount; i++) { + const x = this.data.positions[3 * i + 0]; + const y = this.data.positions[3 * i + 1]; + const z = this.data.positions[3 * i + 2]; + + result[3 * i + 0] = R[0] * x + R[1] * y + R[2] * z; + result[3 * i + 1] = R[3] * x + R[4] * y + R[5] * z; + result[3 * i + 2] = R[6] * x + R[7] * y + R[8] * z; + + result[3 * i + 0] = result[3 * i + 0] * this.scale.x + this.position.x; + result[3 * i + 1] = result[3 * i + 1] * this.scale.y + this.position.y; + result[3 * i + 2] = result[3 * i + 2] * this.scale.z + this.position.z; + } + + return result; } - get scales() { - return this._scales; + get worldRotations() { + const result = new Float32Array(this.data.vertexCount * 4); + for (let i = 0; i < this.data.vertexCount; i++) { + const currentRotation = new Quaternion( + this.data.rotations[4 * i + 1], + this.data.rotations[4 * i + 2], + this.data.rotations[4 * i + 3], + this.data.rotations[4 * i + 0], + ); + + const newRot = this.rotation.multiply(currentRotation); + result[4 * i + 1] = newRot.x; + result[4 * i + 2] = newRot.y; + result[4 * i + 3] = newRot.z; + result[4 * i + 0] = newRot.w; + } + + return result; } - get colors() { - return this._colors; + get worldScales() { + const result = new Float32Array(this.data.vertexCount * 3); + for (let i = 0; i < this.data.vertexCount; i++) { + result[3 * i + 0] = this.scale.x * this.data.scales[3 * i + 0]; + result[3 * i + 1] = this.scale.y * this.data.scales[3 * i + 1]; + result[3 * i + 2] = this.scale.z * this.data.scales[3 * i + 2]; + } + + return result; } } diff --git a/src/core/SplatData.ts b/src/core/SplatData.ts new file mode 100644 index 0000000..6c12544 --- /dev/null +++ b/src/core/SplatData.ts @@ -0,0 +1,217 @@ +import { Vector3 } from "../math/Vector3"; +import { Quaternion } from "../math/Quaternion"; +import { Matrix3 } from "../math/Matrix3"; + +class SplatData { + static RowLength = 3 * 4 + 3 * 4 + 4 + 4; + + private _vertexCount: number; + private _positions: Float32Array; + private _rotations: Float32Array; + private _scales: Float32Array; + private _colors: Uint8Array; + + translate: (translation: Vector3) => void; + rotate: (rotation: Quaternion) => void; + scale: (scale: Vector3) => void; + limitBox: (xMin: number, xMax: number, yMin: number, yMax: number, zMin: number, zMax: number) => void; + serialize: () => Uint8Array; + + constructor( + vertexCount: number = 0, + positions: Float32Array | null = null, + rotations: Float32Array | null = null, + scales: Float32Array | null = null, + colors: Uint8Array | null = null, + ) { + this._vertexCount = vertexCount; + this._positions = positions || new Float32Array(0); + this._rotations = rotations || new Float32Array(0); + this._scales = scales || new Float32Array(0); + this._colors = colors || new Uint8Array(0); + + this.translate = (translation: Vector3) => { + for (let i = 0; i < this.vertexCount; i++) { + this.positions[3 * i + 0] += translation.x; + this.positions[3 * i + 1] += translation.y; + this.positions[3 * i + 2] += translation.z; + } + }; + + this.rotate = (rotation: Quaternion) => { + const R = Matrix3.RotationFromQuaternion(rotation).buffer; + for (let i = 0; i < this.vertexCount; i++) { + const x = this.positions[3 * i + 0]; + const y = this.positions[3 * i + 1]; + const z = this.positions[3 * i + 2]; + + this.positions[3 * i + 0] = R[0] * x + R[1] * y + R[2] * z; + this.positions[3 * i + 1] = R[3] * x + R[4] * y + R[5] * z; + this.positions[3 * i + 2] = R[6] * x + R[7] * y + R[8] * z; + + const currentRotation = new Quaternion( + this.rotations[4 * i + 1], + this.rotations[4 * i + 2], + this.rotations[4 * i + 3], + this.rotations[4 * i + 0], + ); + + const newRot = rotation.multiply(currentRotation); + this.rotations[4 * i + 1] = newRot.x; + this.rotations[4 * i + 2] = newRot.y; + this.rotations[4 * i + 3] = newRot.z; + this.rotations[4 * i + 0] = newRot.w; + } + }; + + this.scale = (scale: Vector3) => { + for (let i = 0; i < this.vertexCount; i++) { + this.positions[3 * i + 0] *= scale.x; + this.positions[3 * i + 1] *= scale.y; + this.positions[3 * i + 2] *= scale.z; + + this.scales[3 * i + 0] *= scale.x; + this.scales[3 * i + 1] *= scale.y; + this.scales[3 * i + 2] *= scale.z; + } + }; + + this.limitBox = (xMin: number, xMax: number, yMin: number, yMax: number, zMin: number, zMax: number) => { + if (xMin >= xMax) { + throw new Error(`xMin (${xMin}) must be smaller than xMax (${xMax})`); + } + if (yMin >= yMax) { + throw new Error(`yMin (${yMin}) must be smaller than yMax (${yMax})`); + } + if (zMin >= zMax) { + throw new Error(`zMin (${zMin}) must be smaller than zMax (${zMax})`); + } + + const mask = new Uint8Array(this.vertexCount); + for (let i = 0; i < this.vertexCount; i++) { + const x = this.positions[3 * i + 0]; + const y = this.positions[3 * i + 1]; + const z = this.positions[3 * i + 2]; + + if (x >= xMin && x <= xMax && y >= yMin && y <= yMax && z >= zMin && z <= zMax) { + mask[i] = 1; + } + } + + let newIndex = 0; + for (let i = 0; i < this.vertexCount; i++) { + if (mask[i] === 0) continue; + + this.positions[3 * newIndex + 0] = this.positions[3 * i + 0]; + this.positions[3 * newIndex + 1] = this.positions[3 * i + 1]; + this.positions[3 * newIndex + 2] = this.positions[3 * i + 2]; + + this.rotations[4 * newIndex + 0] = this.rotations[4 * i + 0]; + this.rotations[4 * newIndex + 1] = this.rotations[4 * i + 1]; + this.rotations[4 * newIndex + 2] = this.rotations[4 * i + 2]; + this.rotations[4 * newIndex + 3] = this.rotations[4 * i + 3]; + + this.scales[3 * newIndex + 0] = this.scales[3 * i + 0]; + this.scales[3 * newIndex + 1] = this.scales[3 * i + 1]; + this.scales[3 * newIndex + 2] = this.scales[3 * i + 2]; + + this.colors[4 * newIndex + 0] = this.colors[4 * i + 0]; + this.colors[4 * newIndex + 1] = this.colors[4 * i + 1]; + this.colors[4 * newIndex + 2] = this.colors[4 * i + 2]; + this.colors[4 * newIndex + 3] = this.colors[4 * i + 3]; + + newIndex += 1; + } + + this._vertexCount = newIndex; + this._positions = new Float32Array(this.positions.buffer, 0, 3 * newIndex); + this._rotations = new Float32Array(this.rotations.buffer, 0, 4 * newIndex); + this._scales = new Float32Array(this.scales.buffer, 0, 3 * newIndex); + this._colors = new Uint8Array(this.colors.buffer, 0, 4 * newIndex); + }; + + this.serialize = () => { + const data = new Uint8Array(this.vertexCount * SplatData.RowLength); + + const f_buffer = new Float32Array(data.buffer); + const u_buffer = new Uint8Array(data.buffer); + + for (let i = 0; i < this.vertexCount; i++) { + f_buffer[8 * i + 0] = this.positions[3 * i + 0]; + f_buffer[8 * i + 1] = this.positions[3 * i + 1]; + f_buffer[8 * i + 2] = this.positions[3 * i + 2]; + + u_buffer[32 * i + 24 + 0] = this.colors[4 * i + 0]; + u_buffer[32 * i + 24 + 1] = this.colors[4 * i + 1]; + u_buffer[32 * i + 24 + 2] = this.colors[4 * i + 2]; + u_buffer[32 * i + 24 + 3] = this.colors[4 * i + 3]; + + f_buffer[8 * i + 3 + 0] = this.scales[3 * i + 0]; + f_buffer[8 * i + 3 + 1] = this.scales[3 * i + 1]; + f_buffer[8 * i + 3 + 2] = this.scales[3 * i + 2]; + + u_buffer[32 * i + 28 + 0] = (this.rotations[4 * i + 0] * 128 + 128) & 0xff; + u_buffer[32 * i + 28 + 1] = (this.rotations[4 * i + 1] * 128 + 128) & 0xff; + u_buffer[32 * i + 28 + 2] = (this.rotations[4 * i + 2] * 128 + 128) & 0xff; + u_buffer[32 * i + 28 + 3] = (this.rotations[4 * i + 3] * 128 + 128) & 0xff; + } + + return data; + }; + } + + static Deserialize(data: Uint8Array): SplatData { + const vertexCount = data.length / SplatData.RowLength; + const positions = new Float32Array(3 * vertexCount); + const rotations = new Float32Array(4 * vertexCount); + const scales = new Float32Array(3 * vertexCount); + const colors = new Uint8Array(4 * vertexCount); + + const f_buffer = new Float32Array(data.buffer); + const u_buffer = new Uint8Array(data.buffer); + + for (let i = 0; i < vertexCount; i++) { + positions[3 * i + 0] = f_buffer[8 * i + 0]; + positions[3 * i + 1] = f_buffer[8 * i + 1]; + positions[3 * i + 2] = f_buffer[8 * i + 2]; + + rotations[4 * i + 0] = (u_buffer[32 * i + 28 + 0] - 128) / 128; + rotations[4 * i + 1] = (u_buffer[32 * i + 28 + 1] - 128) / 128; + rotations[4 * i + 2] = (u_buffer[32 * i + 28 + 2] - 128) / 128; + rotations[4 * i + 3] = (u_buffer[32 * i + 28 + 3] - 128) / 128; + + scales[3 * i + 0] = f_buffer[8 * i + 3 + 0]; + scales[3 * i + 1] = f_buffer[8 * i + 3 + 1]; + scales[3 * i + 2] = f_buffer[8 * i + 3 + 2]; + + colors[4 * i + 0] = u_buffer[32 * i + 24 + 0]; + colors[4 * i + 1] = u_buffer[32 * i + 24 + 1]; + colors[4 * i + 2] = u_buffer[32 * i + 24 + 2]; + colors[4 * i + 3] = u_buffer[32 * i + 24 + 3]; + } + + return new SplatData(vertexCount, positions, rotations, scales, colors); + } + + get vertexCount() { + return this._vertexCount; + } + + get positions() { + return this._positions; + } + + get rotations() { + return this._rotations; + } + + get scales() { + return this._scales; + } + + get colors() { + return this._colors; + } +} + +export { SplatData }; diff --git a/src/core/SplatFilter.ts b/src/core/SplatFilter.ts deleted file mode 100644 index 10410d2..0000000 --- a/src/core/SplatFilter.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { Vector3 } from "../math/Vector3"; -import { Quaternion } from "../math/Quaternion"; -import { Matrix3 } from "../math/Matrix3"; -import { Component } from "./Component"; -import { Object3D } from "./Object3D"; -import { Splat } from "./Splat"; - -class SplatFilter extends Component { - public splat: Splat | null = null; - - applyPosition: () => void; - applyRotation: () => void; - applyScale: () => void; - limitBox: (xMin: number, xMax: number, yMin: number, yMax: number, zMin: number, zMax: number) => void; - serialize: () => Uint8Array; - - constructor(object: Object3D) { - super(object); - - const changeEvent = { type: "change" } as Event; - - this.applyPosition = () => { - if (this.splat === null) return; - - for (let i = 0; i < this.splat.vertexCount; i++) { - this.splat.positions[3 * i + 0] += this.object.position.x; - this.splat.positions[3 * i + 1] += this.object.position.y; - this.splat.positions[3 * i + 2] += this.object.position.z; - } - - this.object.position = new Vector3(); - this.dispatchEvent(changeEvent); - }; - - this.applyRotation = () => { - if (this.splat === null) return; - - const R = Matrix3.RotationFromQuaternion(this.object.rotation).buffer; - for (let i = 0; i < this.splat.vertexCount; i++) { - const x = this.splat.positions[3 * i + 0]; - const y = this.splat.positions[3 * i + 1]; - const z = this.splat.positions[3 * i + 2]; - - this.splat.positions[3 * i + 0] = R[0] * x + R[1] * y + R[2] * z; - this.splat.positions[3 * i + 1] = R[3] * x + R[4] * y + R[5] * z; - this.splat.positions[3 * i + 2] = R[6] * x + R[7] * y + R[8] * z; - - const currentRotation = new Quaternion( - this.splat.rotations[4 * i + 1], - this.splat.rotations[4 * i + 2], - this.splat.rotations[4 * i + 3], - this.splat.rotations[4 * i + 0], - ); - - const newRot = this.object.rotation.multiply(currentRotation); - this.splat.rotations[4 * i + 1] = newRot.x; - this.splat.rotations[4 * i + 2] = newRot.y; - this.splat.rotations[4 * i + 3] = newRot.z; - this.splat.rotations[4 * i + 0] = newRot.w; - } - - this.object.rotation = new Quaternion(); - this.dispatchEvent(changeEvent); - }; - - this.applyScale = () => { - if (this.splat === null) return; - - for (let i = 0; i < this.splat.vertexCount; i++) { - this.splat.positions[3 * i + 0] *= this.object.scale.x; - this.splat.positions[3 * i + 1] *= this.object.scale.y; - this.splat.positions[3 * i + 2] *= this.object.scale.z; - - this.splat.scales[3 * i + 0] *= this.object.scale.x; - this.splat.scales[3 * i + 1] *= this.object.scale.y; - this.splat.scales[3 * i + 2] *= this.object.scale.z; - } - - this.object.scale = new Vector3(1, 1, 1); - this.dispatchEvent(changeEvent); - }; - - this.limitBox = (xMin: number, xMax: number, yMin: number, yMax: number, zMin: number, zMax: number) => { - if (xMin >= xMax) { - throw new Error(`xMin (${xMin}) must be smaller than xMax (${xMax})`); - } - if (yMin >= yMax) { - throw new Error(`yMin (${yMin}) must be smaller than yMax (${yMax})`); - } - if (zMin >= zMax) { - throw new Error(`zMin (${zMin}) must be smaller than zMax (${zMax})`); - } - - if (this.splat === null) return; - - const mask = new Uint8Array(this.splat.vertexCount); - for (let i = 0; i < this.splat.vertexCount; i++) { - const x = this.splat.positions[3 * i + 0]; - const y = this.splat.positions[3 * i + 1]; - const z = this.splat.positions[3 * i + 2]; - - if (x >= xMin && x <= xMax && y >= yMin && y <= yMax && z >= zMin && z <= zMax) { - mask[i] = 1; - } - } - - let newIndex = 0; - for (let i = 0; i < this.splat.vertexCount; i++) { - if (mask[i] === 0) continue; - - this.splat.positions[3 * newIndex + 0] = this.splat.positions[3 * i + 0]; - this.splat.positions[3 * newIndex + 1] = this.splat.positions[3 * i + 1]; - this.splat.positions[3 * newIndex + 2] = this.splat.positions[3 * i + 2]; - - this.splat.rotations[4 * newIndex + 0] = this.splat.rotations[4 * i + 0]; - this.splat.rotations[4 * newIndex + 1] = this.splat.rotations[4 * i + 1]; - this.splat.rotations[4 * newIndex + 2] = this.splat.rotations[4 * i + 2]; - this.splat.rotations[4 * newIndex + 3] = this.splat.rotations[4 * i + 3]; - - this.splat.scales[3 * newIndex + 0] = this.splat.scales[3 * i + 0]; - this.splat.scales[3 * newIndex + 1] = this.splat.scales[3 * i + 1]; - this.splat.scales[3 * newIndex + 2] = this.splat.scales[3 * i + 2]; - - this.splat.colors[4 * newIndex + 0] = this.splat.colors[4 * i + 0]; - this.splat.colors[4 * newIndex + 1] = this.splat.colors[4 * i + 1]; - this.splat.colors[4 * newIndex + 2] = this.splat.colors[4 * i + 2]; - this.splat.colors[4 * newIndex + 3] = this.splat.colors[4 * i + 3]; - - this.splat.colors[4 * newIndex + 0] = this.splat.colors[4 * i + 0]; - this.splat.colors[4 * newIndex + 1] = this.splat.colors[4 * i + 1]; - this.splat.colors[4 * newIndex + 2] = this.splat.colors[4 * i + 2]; - this.splat.colors[4 * newIndex + 3] = this.splat.colors[4 * i + 3]; - - newIndex += 1; - } - - const newVertexCount = newIndex; - const newPositions = new Float32Array(this.splat.positions.buffer, 0, 3 * newVertexCount); - const newRotations = new Float32Array(this.splat.rotations.buffer, 0, 4 * newVertexCount); - const newScales = new Float32Array(this.splat.scales.buffer, 0, 3 * newVertexCount); - const newColors = new Uint8Array(this.splat.colors.buffer, 0, 4 * newVertexCount); - - this.splat = new Splat(newVertexCount, newPositions, newRotations, newScales, newColors); - - this.dispatchEvent(changeEvent); - }; - - this.serialize = () => { - if (this.splat === null) return new Uint8Array(0); - - const data = new Uint8Array(this.splat.vertexCount * Splat.RowLength); - - const f_buffer = new Float32Array(data.buffer); - const u_buffer = new Uint8Array(data.buffer); - - for (let i = 0; i < this.splat.vertexCount; i++) { - f_buffer[8 * i + 0] = this.splat.positions[3 * i + 0]; - f_buffer[8 * i + 1] = this.splat.positions[3 * i + 1]; - f_buffer[8 * i + 2] = this.splat.positions[3 * i + 2]; - - u_buffer[32 * i + 24 + 0] = this.splat.colors[4 * i + 0]; - u_buffer[32 * i + 24 + 1] = this.splat.colors[4 * i + 1]; - u_buffer[32 * i + 24 + 2] = this.splat.colors[4 * i + 2]; - u_buffer[32 * i + 24 + 3] = this.splat.colors[4 * i + 3]; - - f_buffer[8 * i + 3 + 0] = this.splat.scales[3 * i + 0]; - f_buffer[8 * i + 3 + 1] = this.splat.scales[3 * i + 1]; - f_buffer[8 * i + 3 + 2] = this.splat.scales[3 * i + 2]; - - u_buffer[32 * i + 28 + 0] = (this.splat.rotations[4 * i + 0] * 128 + 128) & 255; - u_buffer[32 * i + 28 + 1] = (this.splat.rotations[4 * i + 1] * 128 + 128) & 255; - u_buffer[32 * i + 28 + 2] = (this.splat.rotations[4 * i + 2] * 128 + 128) & 255; - u_buffer[32 * i + 28 + 3] = (this.splat.rotations[4 * i + 3] * 128 + 128) & 255; - } - - return data; - }; - } - - static Create( - vertexCount: number, - positions: Float32Array, - rotations: Float32Array, - scales: Float32Array, - colors: Uint8Array, - ) { - const object = new Object3D(); - const splat = new Splat(vertexCount, positions, rotations, scales, colors); - const splatFilter = object.addComponent(SplatFilter); - splatFilter.splat = splat; - return object; - } - - static FromData(data: Uint8Array) { - const vertexCount = data.length / Splat.RowLength; - const positions = new Float32Array(3 * vertexCount); - const rotations = new Float32Array(4 * vertexCount); - const scales = new Float32Array(3 * vertexCount); - const colors = new Uint8Array(4 * vertexCount); - - const f_buffer = new Float32Array(data.buffer); - const u_buffer = new Uint8Array(data.buffer); - - for (let i = 0; i < vertexCount; i++) { - positions[3 * i + 0] = f_buffer[8 * i + 0]; - positions[3 * i + 1] = f_buffer[8 * i + 1]; - positions[3 * i + 2] = f_buffer[8 * i + 2]; - - rotations[4 * i + 0] = (u_buffer[32 * i + 28 + 0] - 128) / 128; - rotations[4 * i + 1] = (u_buffer[32 * i + 28 + 1] - 128) / 128; - rotations[4 * i + 2] = (u_buffer[32 * i + 28 + 2] - 128) / 128; - rotations[4 * i + 3] = (u_buffer[32 * i + 28 + 3] - 128) / 128; - - scales[3 * i + 0] = f_buffer[8 * i + 3 + 0]; - scales[3 * i + 1] = f_buffer[8 * i + 3 + 1]; - scales[3 * i + 2] = f_buffer[8 * i + 3 + 2]; - - colors[4 * i + 0] = u_buffer[32 * i + 24 + 0]; - colors[4 * i + 1] = u_buffer[32 * i + 24 + 1]; - colors[4 * i + 2] = u_buffer[32 * i + 24 + 2]; - colors[4 * i + 3] = u_buffer[32 * i + 24 + 3]; - } - - return SplatFilter.Create(vertexCount, positions, rotations, scales, colors); - } - - get worldPositions() { - if (this.splat === null) return new Float32Array(0); - - const worldPositions = new Float32Array(this.splat.vertexCount * 3); - const R = Matrix3.RotationFromQuaternion(this.object.rotation).buffer; - for (let i = 0; i < this.splat.vertexCount; i++) { - const x = this.splat.positions[3 * i + 0]; - const y = this.splat.positions[3 * i + 1]; - const z = this.splat.positions[3 * i + 2]; - - worldPositions[3 * i + 0] = R[0] * x + R[1] * y + R[2] * z; - worldPositions[3 * i + 1] = R[3] * x + R[4] * y + R[5] * z; - worldPositions[3 * i + 2] = R[6] * x + R[7] * y + R[8] * z; - - worldPositions[3 * i + 0] = worldPositions[3 * i + 0] * this.object.scale.x + this.object.position.x; - worldPositions[3 * i + 1] = worldPositions[3 * i + 1] * this.object.scale.y + this.object.position.y; - worldPositions[3 * i + 2] = worldPositions[3 * i + 2] * this.object.scale.z + this.object.position.z; - } - - return worldPositions; - } - - get worldRotations() { - if (this.splat === null) return new Float32Array(0); - - const worldRotations = new Float32Array(this.splat.vertexCount * 4); - for (let i = 0; i < this.splat.vertexCount; i++) { - const currentRotation = new Quaternion( - this.splat.rotations[4 * i + 1], - this.splat.rotations[4 * i + 2], - this.splat.rotations[4 * i + 3], - this.splat.rotations[4 * i + 0], - ); - - const newRot = this.object.rotation.multiply(currentRotation); - worldRotations[4 * i + 1] = newRot.x; - worldRotations[4 * i + 2] = newRot.y; - worldRotations[4 * i + 3] = newRot.z; - worldRotations[4 * i + 0] = newRot.w; - } - - return worldRotations; - } - - get worldScales() { - if (this.splat === null) return new Float32Array(0); - - const worldScales = new Float32Array(this.splat.vertexCount * 3); - for (let i = 0; i < this.splat.vertexCount; i++) { - worldScales[3 * i + 0] = this.object.scale.x * this.splat.scales[3 * i + 0]; - worldScales[3 * i + 1] = this.object.scale.y * this.splat.scales[3 * i + 1]; - worldScales[3 * i + 2] = this.object.scale.z * this.splat.scales[3 * i + 2]; - } - - return worldScales; - } -} - -export { SplatFilter }; diff --git a/src/index.ts b/src/index.ts index 5a15565..d1c96b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export { Object3D } from "./core/Object3D"; +export { SplatData } from "./core/SplatData"; export { Splat } from "./core/Splat"; -export { SplatFilter } from "./core/SplatFilter"; +export { CameraData } from "./cameras/CameraData"; export { Camera } from "./cameras/Camera"; export { Scene } from "./core/Scene"; export { Loader } from "./loaders/Loader"; diff --git a/src/loaders/Loader.ts b/src/loaders/Loader.ts index 27ca40f..2d51823 100644 --- a/src/loaders/Loader.ts +++ b/src/loaders/Loader.ts @@ -1,9 +1,9 @@ -import { SplatFilter } from "../core/SplatFilter"; import type { Scene } from "../core/Scene"; -import { Object3D } from "../core/Object3D"; +import { Splat } from "../core/Splat"; +import { SplatData } from "../core/SplatData"; class Loader { - static async LoadAsync(url: string, scene: Scene, onProgress?: (progress: number) => void): Promise { + static async LoadAsync(url: string, scene: Scene, onProgress?: (progress: number) => void): Promise { const req = await fetch(url, { mode: "cors", credentials: "omit", @@ -30,22 +30,20 @@ class Loader { onProgress?.(bytesRead / contentLength); } - const result = SplatFilter.FromData(data); - scene.appendObject(result); + const splat = SplatData.Deserialize(data); + const result = new Splat(splat); + scene.addObject(result); return result; } - static async LoadFromFileAsync( - file: File, - scene: Scene, - onProgress?: (progress: number) => void, - ): Promise { + static async LoadFromFileAsync(file: File, scene: Scene, onProgress?: (progress: number) => void): Promise { const reader = new FileReader(); - let result = new Object3D(); + let result = new Splat(); reader.onload = (e) => { const data = new Uint8Array(e.target!.result as ArrayBuffer); - result = SplatFilter.FromData(data); - scene.appendObject(result); + const splat = SplatData.Deserialize(data); + result = new Splat(splat); + scene.addObject(result); }; reader.onprogress = (e) => { onProgress?.(e.loaded / e.total); diff --git a/src/loaders/PLYLoader.ts b/src/loaders/PLYLoader.ts index 841f456..bdce095 100644 --- a/src/loaders/PLYLoader.ts +++ b/src/loaders/PLYLoader.ts @@ -1,8 +1,7 @@ import { Scene } from "../core/Scene"; import { Vector3 } from "../math/Vector3"; import { Quaternion } from "../math/Quaternion"; -import { SplatFilter } from "../core/SplatFilter"; -import { Object3D } from "../core/Object3D"; +import { SplatData } from "../core/SplatData"; import { Splat } from "../core/Splat"; class PLYLoader { @@ -13,7 +12,7 @@ class PLYLoader { scene: Scene, onProgress?: (progress: number) => void, format: string = "", - ): Promise { + ): Promise { const req = await fetch(url, { mode: "cors", credentials: "omit", @@ -45,8 +44,9 @@ class PLYLoader { } const data = new Uint8Array(this._ParsePLYBuffer(plyData.buffer, format)); - const result = SplatFilter.FromData(data); - scene.appendObject(result); + const splat = SplatData.Deserialize(data); + const result = new Splat(splat); + scene.addObject(result); return result; } @@ -55,13 +55,14 @@ class PLYLoader { scene: Scene, onProgress?: (progress: number) => void, format: string = "", - ): Promise { + ): Promise { const reader = new FileReader(); - let result = new Object3D(); + let result = new Splat(); reader.onload = (e) => { const data = new Uint8Array(this._ParsePLYBuffer(e.target!.result as ArrayBuffer, format)); - result = SplatFilter.FromData(data); - scene.appendObject(result); + const splat = SplatData.Deserialize(data); + result = new Splat(splat); + scene.addObject(result); }; reader.onprogress = (e) => { onProgress?.(e.loaded / e.total); @@ -114,15 +115,15 @@ class PLYLoader { } const dataView = new DataView(inputBuffer, header_end_index + header_end.length); - const buffer = new ArrayBuffer(Splat.RowLength * vertexCount); + const buffer = new ArrayBuffer(SplatData.RowLength * vertexCount); const q_polycam = Quaternion.FromEuler(new Vector3(Math.PI / 2, 0, 0)); for (let i = 0; i < vertexCount; i++) { - const position = new Float32Array(buffer, i * Splat.RowLength, 3); - const scale = new Float32Array(buffer, i * Splat.RowLength + 12, 3); - const rgba = new Uint8ClampedArray(buffer, i * Splat.RowLength + 24, 4); - const rot = new Uint8ClampedArray(buffer, i * Splat.RowLength + 28, 4); + const position = new Float32Array(buffer, i * SplatData.RowLength, 3); + const scale = new Float32Array(buffer, i * SplatData.RowLength + 12, 3); + const rgba = new Uint8ClampedArray(buffer, i * SplatData.RowLength + 24, 4); + const rot = new Uint8ClampedArray(buffer, i * SplatData.RowLength + 28, 4); let r0: number = 255; let r1: number = 0; diff --git a/src/math/Color32.ts b/src/math/Color32.ts index 2f63c89..b29be26 100644 --- a/src/math/Color32.ts +++ b/src/math/Color32.ts @@ -10,6 +10,14 @@ class Color32 { this.b = b; this.a = a; } + + flat(): number[] { + return [this.r, this.g, this.b, this.a]; + } + + toString(): string { + return `[${this.flat().join(", ")}]`; + } } export { Color32 }; diff --git a/src/math/Matrix3.ts b/src/math/Matrix3.ts index 9a401a3..4371e1f 100644 --- a/src/math/Matrix3.ts +++ b/src/math/Matrix3.ts @@ -101,6 +101,10 @@ class Matrix3 { return new Matrix3(...rotationMatrix); } + + toString(): string { + return `[${this.buffer.join(", ")}]`; + } } export { Matrix3 }; diff --git a/src/math/Matrix4.ts b/src/math/Matrix4.ts index a76b73b..d5e69d5 100644 --- a/src/math/Matrix4.ts +++ b/src/math/Matrix4.ts @@ -135,6 +135,10 @@ class Matrix4 { ), ); } + + toString(): string { + return `[${this.buffer.join(", ")}]`; + } } export { Matrix4 }; diff --git a/src/math/Quaternion.ts b/src/math/Quaternion.ts index dd5ebc5..9fccf44 100644 --- a/src/math/Quaternion.ts +++ b/src/math/Quaternion.ts @@ -140,6 +140,10 @@ class Quaternion { } return new Quaternion(x, y, z, w); } + + toString(): string { + return `[${this.flat().join(", ")}]`; + } } export { Quaternion }; diff --git a/src/math/Vector3.ts b/src/math/Vector3.ts index 1afeae2..873fcc5 100644 --- a/src/math/Vector3.ts +++ b/src/math/Vector3.ts @@ -99,6 +99,10 @@ class Vector3 { clone(): Vector3 { return new Vector3(this.x, this.y, this.z); } + + toString(): string { + return `[${this.flat().join(", ")}]`; + } } export { Vector3 }; diff --git a/src/math/Vector4.ts b/src/math/Vector4.ts index b1fca81..e127603 100644 --- a/src/math/Vector4.ts +++ b/src/math/Vector4.ts @@ -102,6 +102,10 @@ class Vector4 { clone(): Vector4 { return new Vector4(this.x, this.y, this.z, this.w); } + + toString(): string { + return `[${this.flat().join(", ")}]`; + } } export { Vector4 }; diff --git a/src/renderers/WebGLRenderer.ts b/src/renderers/WebGLRenderer.ts index 5ba36c0..806a607 100644 --- a/src/renderers/WebGLRenderer.ts +++ b/src/renderers/WebGLRenderer.ts @@ -7,6 +7,7 @@ import { frag } from "./webgl/shaders/frag.glsl"; import { ShaderPass } from "./webgl/passes/ShaderPass"; import { FadeInPass } from "./webgl/passes/FadeInPass"; import { RenderTexture } from "./webgl/utils/RenderTexture"; +import { Camera } from "../cameras/Camera"; export class WebGLRenderer { domElement: HTMLCanvasElement; @@ -40,6 +41,7 @@ export class WebGLRenderer { } let activeScene: Scene; + let activeCamera: Camera; let renderTexture: RenderTexture; let worker: Worker; @@ -78,23 +80,19 @@ export class WebGLRenderer { canvas.width = width; canvas.height = height; - if (!activeScene || !activeScene.camera) return; + if (!activeCamera) return; gl.viewport(0, 0, canvas!.width, canvas.height); - activeScene.camera.update(canvas.width, canvas.height); + activeCamera.data.setSize(canvas.width, canvas.height); u_projection = gl.getUniformLocation(program, "projection") as WebGLUniformLocation; - gl.uniformMatrix4fv(u_projection, false, activeScene.camera.projectionMatrix.buffer); + gl.uniformMatrix4fv(u_projection, false, activeCamera.data.projectionMatrix.buffer); u_viewport = gl.getUniformLocation(program, "viewport") as WebGLUniformLocation; gl.uniform2fv(u_viewport, new Float32Array([canvas.width, canvas.height])); }; const initWebGL = () => { - if (!activeScene.camera) { - throw new Error("Scene does not have a camera"); - } - renderTexture = new RenderTexture(activeScene); worker = new SortWorker(); @@ -135,19 +133,17 @@ export class WebGLRenderer { gl.blendFuncSeparate(gl.ONE_MINUS_DST_ALPHA, gl.ONE, gl.ONE_MINUS_DST_ALPHA, gl.ONE); gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); - activeScene.camera.update(canvas.width, canvas.height); - u_projection = gl.getUniformLocation(program, "projection") as WebGLUniformLocation; - gl.uniformMatrix4fv(u_projection, false, activeScene.camera.projectionMatrix.buffer); + gl.uniformMatrix4fv(u_projection, false, activeCamera.data.projectionMatrix.buffer); u_viewport = gl.getUniformLocation(program, "viewport") as WebGLUniformLocation; gl.uniform2fv(u_viewport, new Float32Array([canvas.width, canvas.height])); u_focal = gl.getUniformLocation(program, "focal") as WebGLUniformLocation; - gl.uniform2fv(u_focal, new Float32Array([activeScene.camera.fx, activeScene.camera.fy])); + gl.uniform2fv(u_focal, new Float32Array([activeCamera.data.fx, activeCamera.data.fy])); u_view = gl.getUniformLocation(program, "view") as WebGLUniformLocation; - gl.uniformMatrix4fv(u_view, false, activeScene.camera.viewMatrix.buffer); + gl.uniformMatrix4fv(u_view, false, activeCamera.data.viewMatrix.buffer); const triangleVertices = new Float32Array([-2, -2, 2, -2, 2, 2, -2, 2]); vertexBuffer = gl.createBuffer() as WebGLBuffer; @@ -206,14 +202,6 @@ export class WebGLRenderer { initialized = true; }; - const onSceneChange = () => { - if (initialized) { - this.dispose(); - } - - initWebGL(); - }; - this.render = (scene: Scene) => { if (scene !== activeScene) { if (initialized) { @@ -221,28 +209,26 @@ export class WebGLRenderer { } if (scene !== activeScene) { - if (activeScene) { - activeScene.removeEventListener("change", onSceneChange); - } activeScene = scene; - activeScene.addEventListener("change", onSceneChange); + activeCamera = scene.findObjectOfType(Camera) as Camera; + if (!activeCamera) { + throw new Error("No camera found in scene"); + } + activeCamera.data.setSize(canvas.width, canvas.height); + activeCamera.update(); } initWebGL(); } - if (!activeScene.camera) { - throw new Error("Scene does not have a camera"); - } - - activeScene.camera.update(canvas.width, canvas.height); - worker.postMessage({ viewProj: activeScene.camera.viewProj }); + activeCamera.update(); + worker.postMessage({ viewProj: activeCamera.data.viewProj }); if (renderTexture.vertexCount > 0) { for (const shaderPass of shaderPasses) { shaderPass.render(); } - gl.uniformMatrix4fv(u_view, false, activeScene.camera.viewMatrix.buffer); + gl.uniformMatrix4fv(u_view, false, activeCamera.data.viewMatrix.buffer); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, 4, renderTexture.vertexCount); } else { diff --git a/src/renderers/webgl/utils/RenderTexture.ts b/src/renderers/webgl/utils/RenderTexture.ts index 69bf183..28fff28 100644 --- a/src/renderers/webgl/utils/RenderTexture.ts +++ b/src/renderers/webgl/utils/RenderTexture.ts @@ -1,5 +1,5 @@ import { Scene } from "../../../core/Scene"; -import { SplatFilter } from "../../../core/SplatFilter"; +import { Splat } from "../../../core/Splat"; import { Matrix3 } from "../../../math/Matrix3"; import { Quaternion } from "../../../math/Quaternion"; import { Vector3 } from "../../../math/Vector3"; @@ -48,73 +48,77 @@ class RenderTexture { return (floatToHalf(x) | (floatToHalf(y) << 16)) >>> 0; }; - this._vertexCount = 0; + let vertexCount = 0; for (const object of scene.objects) { - const splatFilter = object.getComponent(SplatFilter); - if (!splatFilter || !splatFilter.splat) continue; - this._vertexCount += splatFilter.splat.vertexCount; + if (object instanceof Splat) { + vertexCount += object.data.vertexCount; + } } + this._vertexCount = vertexCount; this._width = 2048; - this._height = Math.ceil((2 * this._vertexCount) / this._width); - this._buffer = new Uint32Array(this._width * this._height * 4); - this._positions = new Float32Array(this._vertexCount * 3); + this._height = Math.ceil((2 * this.vertexCount) / this.width); + this._buffer = new Uint32Array(this.width * this.height * 4); + this._positions = new Float32Array(this.vertexCount * 3); const data_f = new Float32Array(this._buffer.buffer); const data_c = new Uint8Array(this._buffer.buffer); let offset = 0; for (const object of scene.objects) { - const splatFilter = object.getComponent(SplatFilter); - if (!splatFilter || !splatFilter.splat) continue; - const splat = splatFilter.splat; - const positions = splatFilter.worldPositions; - const rotations = splatFilter.worldRotations; - const scales = splatFilter.worldScales; - for (let i = 0; i < splatFilter.splat.vertexCount; i++) { - const index = offset + i; - - data_f[8 * index + 0] = positions[3 * i + 0]; - data_f[8 * index + 1] = positions[3 * i + 1]; - data_f[8 * index + 2] = positions[3 * i + 2]; - - this._positions[3 * index + 0] = positions[3 * i + 0]; - this._positions[3 * index + 1] = positions[3 * i + 1]; - this._positions[3 * index + 2] = positions[3 * i + 2]; - - data_c[4 * (8 * index + 7) + 0] = splat.colors[4 * i + 0]; - data_c[4 * (8 * index + 7) + 1] = splat.colors[4 * i + 1]; - data_c[4 * (8 * index + 7) + 2] = splat.colors[4 * i + 2]; - data_c[4 * (8 * index + 7) + 3] = splat.colors[4 * i + 3]; - - const rot = Matrix3.RotationFromQuaternion( - new Quaternion( - rotations[4 * i + 1], - rotations[4 * i + 2], - rotations[4 * i + 3], - -rotations[4 * i + 0], - ), - ); - - const scale = Matrix3.Diagonal(new Vector3(scales[3 * i + 0], scales[3 * i + 1], scales[3 * i + 2])); - - const M = scale.multiply(rot).buffer; - - const sigma = [ - M[0] * M[0] + M[3] * M[3] + M[6] * M[6], - M[0] * M[1] + M[3] * M[4] + M[6] * M[7], - M[0] * M[2] + M[3] * M[5] + M[6] * M[8], - M[1] * M[1] + M[4] * M[4] + M[7] * M[7], - M[1] * M[2] + M[4] * M[5] + M[7] * M[8], - M[2] * M[2] + M[5] * M[5] + M[8] * M[8], - ]; - - this._buffer[8 * index + 4] = packHalf2x16(4 * sigma[0], 4 * sigma[1]); - this._buffer[8 * index + 5] = packHalf2x16(4 * sigma[2], 4 * sigma[3]); - this._buffer[8 * index + 6] = packHalf2x16(4 * sigma[4], 4 * sigma[5]); - } + if (object instanceof Splat) { + const splat = object.data; + const positions = splat.positions; + const rotations = splat.rotations; + const scales = splat.scales; + const colors = splat.colors; + for (let i = 0; i < object.data.vertexCount; i++) { + const index = offset + i; + + data_f[8 * index + 0] = positions[3 * i + 0]; + data_f[8 * index + 1] = positions[3 * i + 1]; + data_f[8 * index + 2] = positions[3 * i + 2]; + + this.positions[3 * index + 0] = positions[3 * i + 0]; + this.positions[3 * index + 1] = positions[3 * i + 1]; + this.positions[3 * index + 2] = positions[3 * i + 2]; + + data_c[4 * (8 * index + 7) + 0] = colors[4 * i + 0]; + data_c[4 * (8 * index + 7) + 1] = colors[4 * i + 1]; + data_c[4 * (8 * index + 7) + 2] = colors[4 * i + 2]; + data_c[4 * (8 * index + 7) + 3] = colors[4 * i + 3]; + + const rot = Matrix3.RotationFromQuaternion( + new Quaternion( + rotations[4 * i + 1], + rotations[4 * i + 2], + rotations[4 * i + 3], + -rotations[4 * i + 0], + ), + ); + + const scale = Matrix3.Diagonal( + new Vector3(scales[3 * i + 0], scales[3 * i + 1], scales[3 * i + 2]), + ); + + const M = scale.multiply(rot).buffer; + + const sigma = [ + M[0] * M[0] + M[3] * M[3] + M[6] * M[6], + M[0] * M[1] + M[3] * M[4] + M[6] * M[7], + M[0] * M[2] + M[3] * M[5] + M[6] * M[8], + M[1] * M[1] + M[4] * M[4] + M[7] * M[7], + M[1] * M[2] + M[4] * M[5] + M[7] * M[8], + M[2] * M[2] + M[5] * M[5] + M[8] * M[8], + ]; + + this.buffer[8 * index + 4] = packHalf2x16(4 * sigma[0], 4 * sigma[1]); + this.buffer[8 * index + 5] = packHalf2x16(4 * sigma[2], 4 * sigma[3]); + this.buffer[8 * index + 6] = packHalf2x16(4 * sigma[4], 4 * sigma[5]); + } - offset += splat.vertexCount; + offset += splat.vertexCount; + } } }