Skip to content

Commit

Permalink
specialized editor controls
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanebert committed Dec 10, 2023
1 parent c941ced commit 31b363d
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 13 deletions.
265 changes: 265 additions & 0 deletions examples/editor/src/controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
import * as SPLAT from "gsplat";

class Controls {
minAngle: number = -90;
maxAngle: number = 90;
minZoom: number = 0.1;
maxZoom: number = 30;
orbitSpeed: number = 2;
panSpeed: number = 1.25;
zoomSpeed: number = 2;
dampening: number = 0.5;
setCameraTarget: (newTarget: SPLAT.Vector3) => void = () => {};
update: () => void;
dispose: () => void;

constructor(
scene: SPLAT.Scene,
domElement: HTMLElement,
alpha: number = 0.5,
beta: number = 0.5,
radius: number = 5,
inputTarget: SPLAT.Vector3 = new SPLAT.Vector3(),
) {
const camera = scene.findObjectOfType(SPLAT.Camera) as SPLAT.Camera;
if (!camera) {
throw new Error("No camera found in scene");
}

let target = inputTarget.clone();

let desiredTarget = target.clone();
let desiredAlpha = alpha;
let desiredBeta = beta;
let desiredRadius = radius;

let dragging = false;
let panning = false;
let lastDist = 0;
let lastX = 0;
let lastY = 0;

let isUpdatingCamera = false;

const onCameraChange = () => {
if (isUpdatingCamera) return;

const eulerRotation = camera.rotation.toEuler();
desiredAlpha = -eulerRotation.y;
desiredBeta = -eulerRotation.x;

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 SPLAT.Vector3(x, y, z);
};

camera.addEventListener("change", onCameraChange);

this.setCameraTarget = (newTarget: SPLAT.Vector3) => {
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);
desiredTarget = new SPLAT.Vector3(newTarget.x, newTarget.y, newTarget.z);
};

const computeZoomNorm = () => {
return 0.1 + (0.9 * (desiredRadius - this.minZoom)) / (this.maxZoom - this.minZoom);
};

const onMouseDown = (e: MouseEvent) => {
preventDefault(e);

if (e.button === 1) {
dragging = true;
panning = e.shiftKey;
lastX = e.clientX;
lastY = e.clientY;
}
};

const onMouseUp = (e: MouseEvent) => {
preventDefault(e);

if (e.button === 1) {
dragging = false;
panning = false;
}
};

const onMouseMove = (e: MouseEvent) => {
preventDefault(e);

if (!dragging || !camera) return;

const dx = e.clientX - lastX;
const dy = e.clientY - lastY;

if (panning) {
const zoomNorm = computeZoomNorm();
const panX = -dx * this.panSpeed * 0.01 * zoomNorm;
const panY = -dy * this.panSpeed * 0.01 * zoomNorm;
const R = SPLAT.Matrix3.RotationFromQuaternion(camera.rotation).buffer;
const right = new SPLAT.Vector3(R[0], R[3], R[6]);
const up = new SPLAT.Vector3(R[1], R[4], R[7]);
desiredTarget = desiredTarget.add(right.multiply(panX));
desiredTarget = desiredTarget.add(up.multiply(panY));
} else {
desiredAlpha -= dx * this.orbitSpeed * 0.003;
desiredBeta += dy * this.orbitSpeed * 0.003;
desiredBeta = Math.min(
Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
(this.maxAngle * Math.PI) / 180,
);
}

lastX = e.clientX;
lastY = e.clientY;
};

const onWheel = (e: WheelEvent) => {
preventDefault(e);

const zoomNorm = computeZoomNorm();
desiredRadius += e.deltaY * this.zoomSpeed * 0.025 * zoomNorm;
desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
};

const onTouchStart = (e: TouchEvent) => {
preventDefault(e);

if (e.touches.length === 1) {
dragging = true;
panning = false;
lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
lastDist = 0;
} else if (e.touches.length === 2) {
dragging = true;
panning = true;
lastX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
lastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const distX = e.touches[0].clientX - e.touches[1].clientX;
const distY = e.touches[0].clientY - e.touches[1].clientY;
lastDist = Math.sqrt(distX * distX + distY * distY);
}
};

const onTouchEnd = (e: TouchEvent) => {
preventDefault(e);

dragging = false;
panning = false;
};

const onTouchMove = (e: TouchEvent) => {
preventDefault(e);

if (!dragging || !camera) return;

if (panning) {
const zoomNorm = computeZoomNorm();

const distX = e.touches[0].clientX - e.touches[1].clientX;
const distY = e.touches[0].clientY - e.touches[1].clientY;
const dist = Math.sqrt(distX * distX + distY * distY);
const delta = lastDist - dist;
desiredRadius += delta * this.zoomSpeed * 0.1 * zoomNorm;
desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
lastDist = dist;

const touchX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const touchY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const dx = touchX - lastX;
const dy = touchY - lastY;
const R = SPLAT.Matrix3.RotationFromQuaternion(camera.rotation).buffer;
const right = new SPLAT.Vector3(R[0], R[3], R[6]);
const up = new SPLAT.Vector3(R[1], R[4], R[7]);
desiredTarget = desiredTarget.add(right.multiply(-dx * this.panSpeed * 0.025 * zoomNorm));
desiredTarget = desiredTarget.add(up.multiply(-dy * this.panSpeed * 0.025 * zoomNorm));
lastX = touchX;
lastY = touchY;
} else {
const dx = e.touches[0].clientX - lastX;
const dy = e.touches[0].clientY - lastY;

desiredAlpha -= dx * this.orbitSpeed * 0.003;
desiredBeta += dy * this.orbitSpeed * 0.003;
desiredBeta = Math.min(
Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
(this.maxAngle * Math.PI) / 180,
);

lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
}
};

const lerp = (a: number, b: number, t: number) => {
return (1 - t) * a + t * b;
};

this.update = () => {
isUpdatingCamera = true;

alpha = lerp(alpha, desiredAlpha, this.dampening);
beta = lerp(beta, desiredBeta, this.dampening);
radius = lerp(radius, desiredRadius, this.dampening);
target = target.lerp(desiredTarget, this.dampening);

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.position = new SPLAT.Vector3(x, y, z);

const direction = target.subtract(camera.position).normalize();
const rx = Math.asin(-direction.y);
const ry = Math.atan2(direction.x, direction.z);
camera.rotation = SPLAT.Quaternion.FromEuler(new SPLAT.Vector3(rx, ry, 0));

isUpdatingCamera = false;
};

const preventDefault = (e: Event) => {
e.preventDefault();
e.stopPropagation();
};

this.dispose = () => {
domElement.removeEventListener("dragenter", preventDefault);
domElement.removeEventListener("dragover", preventDefault);
domElement.removeEventListener("dragleave", preventDefault);
domElement.removeEventListener("contextmenu", preventDefault);

domElement.removeEventListener("mousedown", onMouseDown);
domElement.removeEventListener("mousemove", onMouseMove);
domElement.removeEventListener("wheel", onWheel);

domElement.removeEventListener("touchstart", onTouchStart);
domElement.removeEventListener("touchend", onTouchEnd);
domElement.removeEventListener("touchmove", onTouchMove);
};

domElement.addEventListener("dragenter", preventDefault);
domElement.addEventListener("dragover", preventDefault);
domElement.addEventListener("dragleave", preventDefault);
domElement.addEventListener("contextmenu", preventDefault);

domElement.addEventListener("mousedown", onMouseDown);
domElement.addEventListener("mouseup", onMouseUp);
domElement.addEventListener("mousemove", onMouseMove);
domElement.addEventListener("wheel", onWheel);

domElement.addEventListener("touchstart", onTouchStart);
domElement.addEventListener("touchend", onTouchEnd);
domElement.addEventListener("touchmove", onTouchMove);

this.update();
}
}

export { Controls };
40 changes: 36 additions & 4 deletions examples/editor/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as SPLAT from "gsplat";
import { findIntersectedPoint, getPointerDirection } from "./utils";
import { Controls } from "./controls";

const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const progressDialog = document.getElementById("progress-dialog") as HTMLDialogElement;
Expand All @@ -8,12 +9,15 @@ 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);
const controls = new Controls(scene, canvas);

let mode = "";

async function main() {
const url = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k-mini.splat";
const bonsai = 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();
renderer.backgroundColor = new SPLAT.Color32(64, 64, 64, 255);

const handleResize = () => {
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
Expand All @@ -23,14 +27,41 @@ async function main() {
const x = (event.clientX / canvas.clientWidth) * 2 - 1;
const y = -(event.clientY / canvas.clientHeight) * 2 + 1;
const direction = getPointerDirection(x, y, camera);
const closestSplat = findIntersectedPoint(bonsai, camera.position, direction);
const closestSplat = findIntersectedPoint(splat, camera.position, direction);
if (closestSplat !== null) {
console.log(`Clicked on splat ${closestSplat}`);
}
};

const focusPosition = new SPLAT.Vector3();

let mousePosition = new SPLAT.Vector3();
let grabPosition = new SPLAT.Vector3();
let grabDistance = 0;

const handleKeyDown = (event: KeyboardEvent) => {
console.log(event.key);
if (event.key === "g" || event.key === "G") {
if (mode === "grab") {
mode = "";
} else {
mode = "grab";
grabPosition = mousePosition.subtract(splat.position);
grabDistance = focusPosition.subtract(camera.position).magnitude();
}
}
};

const handleMouseMove = (event: MouseEvent) => {
const x = (event.clientX / canvas.clientWidth) * 2 - 1;
const y = -(event.clientY / canvas.clientHeight) * 2 + 1;
const direction = getPointerDirection(x, y, camera);
if (mode === "grab") {
const position = camera.position.add(direction.multiply(grabDistance));
splat.position = position.subtract(grabPosition);
} else {
const distance = focusPosition.subtract(camera.position).magnitude();
mousePosition = camera.position.add(direction.multiply(distance));
}
};

const frame = () => {
Expand All @@ -43,6 +74,7 @@ async function main() {
handleResize();
window.addEventListener("resize", handleResize);
window.addEventListener("keydown", handleKeyDown);
canvas.addEventListener("mousemove", handleMouseMove);
canvas.addEventListener("contextmenu", handleClicked);

requestAnimationFrame(frame);
Expand Down
1 change: 0 additions & 1 deletion src/controls/OrbitControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ class OrbitControls {
const ry = Math.atan2(direction.x, direction.z);
camera.rotation = Quaternion.FromEuler(new Vector3(rx, ry, 0));

// Just spit balling here on the values
const moveSpeed = 0.025;
const rotateSpeed = 0.01;

Expand Down
9 changes: 9 additions & 0 deletions src/math/Color32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ class Color32 {
return [this.r, this.g, this.b, this.a];
}

toHexString(): string {
return (
"#" +
this.flat()
.map((x) => x.toString(16).padStart(2, "0"))
.join("")
);
}

toString(): string {
return `[${this.flat().join(", ")}]`;
}
Expand Down
4 changes: 2 additions & 2 deletions src/math/Vector3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Vector3 {
return new Vector3(this.x + (v.x - this.x) * t, this.y + (v.y - this.y) * t, this.z + (v.z - this.z) * t);
}

length(): number {
magnitude(): number {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}

Expand All @@ -87,7 +87,7 @@ class Vector3 {
}

normalize(): Vector3 {
const length = this.length();
const length = this.magnitude();

return new Vector3(this.x / length, this.y / length, this.z / length);
}
Expand Down
Loading

0 comments on commit 31b363d

Please sign in to comment.