Skip to content

Commit

Permalink
Support loading multiple splats and use File System Api (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
slimbuck authored Jun 6, 2024
1 parent 0e8d95a commit 46a0368
Show file tree
Hide file tree
Showing 26 changed files with 1,628 additions and 883 deletions.
19 changes: 13 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "supersplat",
"version": "0.18.1",
"version": "0.19.0",
"author": "PlayCanvas<support@playcanvas.com>",
"homepage": "https://playcanvas.com/supersplat/editor",
"description": "3D Gaussian Splat Editor",
Expand Down Expand Up @@ -60,14 +60,15 @@
"@rollup/plugin-strip": "^3.0.4",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@types/wicg-file-system-access": "^2023.10.5",
"@typescript-eslint/eslint-plugin": "^7.10.0",
"@typescript-eslint/parser": "^7.10.0",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"jest": "^29.7.0",
"playcanvas": "^1.71.2",
"playcanvas": "^1.71.5",
"rollup": "^4.18.0",
"rollup-plugin-sass": "^1.12.22",
"rollup-plugin-visualizer": "^5.12.0",
Expand Down
27 changes: 19 additions & 8 deletions src/camera.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import {
math,
ADDRESS_CLAMP_TO_EDGE,
BLEND_NONE,
CULLFACE_NONE,
FILTER_NEAREST,
PIXELFORMAT_RGBA8,
PIXELFORMAT_DEPTH,
drawTexture,
Color,
Entity,
EventHandler,
Material,
Picker,
RenderTarget,
Texture,
Expand All @@ -22,10 +19,11 @@ import {
TONEMAP_ACES,
TONEMAP_ACES2
} from 'playcanvas';
import {Element, ElementType} from './element';
import {TweenValue} from './tween-value';
import {Serializer} from './serializer';
import {MouseController, TouchController} from './controllers';
import { Element, ElementType } from './element';
import { TweenValue } from './tween-value';
import { Serializer } from './serializer';
import { MouseController, TouchController } from './controllers';
import { Splat } from './splat';

// calculate the forward vector given azimuth and elevation
const calcForwardVec = (result: Vec3, azim: number, elev: number) => {
Expand Down Expand Up @@ -403,15 +401,28 @@ class Camera extends Element {
// pick mode

// render picker contents
pickPrep(alpha = 0.0) {
pickPrep(splat: Splat) {
const { width, height } = this.scene.targetSize;
const worldLayer = this.scene.app.scene.layers.getLayerByName('World');

const device = this.scene.graphicsDevice as WebglGraphicsDevice;
const events = this.scene.events;
const alpha = events.invoke('camera.mode') === 'rings' ? 0.0 : 0.2;

// hide non-selected elements
const splats = this.scene.getElementsByType(ElementType.splat);
splats.forEach((s: Splat) => {
s.entity.enabled = s === splat;
});

device.scope.resolve('pickerAlpha').setValue(alpha);
this.picker.resize(width, height);
this.picker.prepare(this.entity.camera, this.scene.app.scene, [worldLayer]);

// re-enable all splats
splats.forEach((splat: Splat) => {
splat.entity.enabled = true;
});
}

pick(x: number, y: number) {
Expand Down
77 changes: 53 additions & 24 deletions src/controllers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {Camera} from './camera';
import { Camera } from './camera';
import { ElementType } from './element';
import { Splat } from './splat';
import {
EVENT_MOUSEDOWN,
EVENT_MOUSEUP,
Expand All @@ -24,6 +26,7 @@ import {
const plane = new Plane();
const ray = new Ray();
const vec = new Vec3();
const vecb = new Vec3();
const fromWorldPoint = new Vec3();
const toWorldPoint = new Vec3();
const worldDiff = new Vec3();
Expand All @@ -49,32 +52,58 @@ class MouseController {
};

onDblClick = (event: globalThis.MouseEvent) => {
// @ts-ignore
const target = this.camera.scene.app.mouse._target;
const sx = event.offsetX / target.clientWidth * this.camera.scene.targetSize.width;
const sy = event.offsetY / target.clientHeight * this.camera.scene.targetSize.height;

this.camera.pickPrep(this.camera.scene.events.invoke('camera.mode') === 'rings' ? 0.0 : 0.2);
const pickId = this.camera.pick(sx, sy);

if (pickId !== -1) {
const splatPos = this.camera.scene.events.invoke('splat.getWorldPosition', pickId);

// create a plane at the world position facing perpendicular to the camera
plane.setFromPointNormal(splatPos, this.camera.entity.forward);
const scene = this.camera.scene;
const cameraPos = this.camera.entity.getPosition();

// create the pick ray in world space
const cameraPos = this.camera.entity.getPosition();
const res = this.camera.entity.camera.screenToWorld(event.offsetX, event.offsetY, 1.0, vec);
vec.sub2(res, cameraPos);
vec.normalize();
ray.set(cameraPos, vec);

// find intersection
if (plane.intersectsRay(ray, vec)) {
this.camera.setFocalPoint(vec);
// @ts-ignore
const target = scene.app.mouse._target;
const sx = event.offsetX / target.clientWidth * scene.targetSize.width;
const sy = event.offsetY / target.clientHeight * scene.targetSize.height;

const splats = scene.getElementsByType(ElementType.splat);

const dist = (a: Vec3, b: Vec3) => {
return vecb.sub2(a, b).length();
};

let closestD = 0;
let closestP = new Vec3();
let closestSplat = null;

for (let i = 0; i < splats.length; ++i) {
const splat = splats[i] as Splat;

this.camera.pickPrep(splat);
const pickId = this.camera.pick(sx, sy);

if (pickId !== -1) {
splat.getSplatWorldPosition(pickId, vec);

// create a plane at the world position facing perpendicular to the camera
plane.setFromPointNormal(vec, this.camera.entity.forward);

// create the pick ray in world space
const res = this.camera.entity.camera.screenToWorld(event.offsetX, event.offsetY, 1.0, vec);
vec.sub2(res, cameraPos);
vec.normalize();
ray.set(cameraPos, vec);

// find intersection
if (plane.intersectsRay(ray, vec)) {
const distance = dist(vec, cameraPos);
if (!closestSplat || distance < closestD) {
closestD = distance;
closestP.copy(vec);
closestSplat = splat;
}
}
}
}

if (closestSplat) {
this.camera.setFocalPoint(closestP);
scene.events.fire('selection', closestSplat);
}
};

constructor(camera: Camera) {
Expand Down
4 changes: 2 additions & 2 deletions src/custom-shadow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,13 @@ class CustomShadow extends Element {
this.scene.app.root.addChild(this.plane);
this.scene.app.root.addChild(this.camera);

this.scene.events.on('scene.boundUpdated', this.regenerate, this);
this.scene.events.on('scene.boundChanged', this.regenerate, this);
this.scene.graphicsDevice.on('devicerestored', this.regenerate, this);
}

remove() {
this.scene.graphicsDevice.off('devicerestored', this.regenerate, this);
this.scene.events.off('scene.boundUpdated', this.regenerate, this);
this.scene.events.off('scene.boundChanged', this.regenerate, this);

this.scene.app.root.removeChild(this.camera);
this.scene.app.root.removeChild(this.plane);
Expand Down
51 changes: 29 additions & 22 deletions src/edit-ops.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Entity, GSplatData, Quat, Vec3 } from 'playcanvas';
import { GSplatData, Quat, Vec3 } from 'playcanvas';
import { Scene } from './scene';
import { Element } from './element';
import { Splat } from './splat';

enum State {
selected = 1,
Expand Down Expand Up @@ -27,66 +29,76 @@ const buildIndex = (splatData: GSplatData, pred: (i: number) => boolean) => {

class DeleteSelectionEditOp {
name = 'deleteSelection';
splatData: GSplatData;
splat: Splat;
indices: Uint32Array;

constructor(splatData: GSplatData) {
constructor(splat: Splat) {
const splatData = splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
const indices = buildIndex(splatData, (i) => !!(state[i] & State.selected));

this.splatData = splatData;
this.splat = splat;
this.indices = indices;
}

do() {
const state = this.splatData.getProp('state') as Uint8Array;
const splatData = this.splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
for (let i = 0; i < this.indices.length; ++i) {
state[this.indices[i]] |= State.deleted;
}
this.splat.updateState(true);
}

undo() {
const state = this.splatData.getProp('state') as Uint8Array;
const splatData = this.splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
for (let i = 0; i < this.indices.length; ++i) {
state[this.indices[i]] &= ~State.deleted;
}
this.splat.updateState(true);
}

destroy() {
this.splatData = null;
this.splat = null;
this.indices = null;
}
}

class ResetEditOp {
name = 'reset';
splatData: GSplatData;
splat: Splat;
indices: Uint32Array;

constructor(splatData: GSplatData) {
constructor(splat: Splat) {
const splatData = splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
const indices = buildIndex(splatData, (i) => !!(state[i] & State.deleted));

this.splatData = splatData;
this.splat = splat;
this.indices = indices;
}

do() {
const state = this.splatData.getProp('state') as Uint8Array;
const splatData = this.splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
for (let i = 0; i < this.indices.length; ++i) {
state[this.indices[i]] &= ~State.deleted;
}
this.splat.updateState(true);
}

undo() {
const state = this.splatData.getProp('state') as Uint8Array;
const splatData = this.splat.splatData;
const state = splatData.getProp('state') as Uint8Array;
for (let i = 0; i < this.indices.length; ++i) {
state[this.indices[i]] |= State.deleted;
}
this.splat.updateState(true);
}

destroy() {
this.splatData = null;
this.splat = null;
this.indices = null;
}
}
Expand All @@ -98,7 +110,7 @@ interface EntityTransform {
};

interface EntityOp {
entity: Entity;
splat: Splat;
old: EntityTransform;
new: EntityTransform;
}
Expand All @@ -115,20 +127,14 @@ class EntityTransformOp {

do() {
this.entityOps.forEach((entityOp) => {
entityOp.entity.setLocalPosition(entityOp.new.position);
entityOp.entity.setLocalRotation(entityOp.new.rotation);
entityOp.entity.setLocalScale(entityOp.new.scale);
entityOp.splat.move(entityOp.new.position, entityOp.new.rotation, entityOp.new.scale);
});
this.scene.updateBound();
}

undo() {
this.entityOps.forEach((entityOp) => {
entityOp.entity.setLocalPosition(entityOp.old.position);
entityOp.entity.setLocalRotation(entityOp.old.rotation);
entityOp.entity.setLocalScale(entityOp.old.scale);
entityOp.splat.move(entityOp.old.position, entityOp.old.rotation, entityOp.old.scale);
});
this.scene.updateBound();
}

destroy() {
Expand All @@ -140,5 +146,6 @@ export {
State,
DeleteSelectionEditOp,
ResetEditOp,
EntityOp,
EntityTransformOp
};
Loading

0 comments on commit 46a0368

Please sign in to comment.