Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The app supports a few useful URL parameters (though please note these are subje
| `settings` | URL of the `settings.json` file | `./settings.json` |
| `content` | URL of the scene file (`.ply`, `.sog`, `.meta.json`, `.lod-meta.json`) | `./scene.compressed.ply` |
| `skybox` | URL of an equirectangular skybox image | |
| `skyprojection` | Projection for `skybox` (`box` or `dome`) | |
| `skydome` | Alias for `skybox` with `skyprojection=dome` | |
| `poster` | URL of an image to show while loading | |
| `noui` | Hide UI | |
| `noanim` | Start with animation paused | |
Expand Down Expand Up @@ -101,7 +103,13 @@ type ExperienceSettings = {
animTrack: string
},
background: {
color?: number[]
color?: number[],
skybox?: {
url?: string,
projection?: 'box' | 'dome',
scale?: number,
center?: number[]
}
},
animTracks: AnimTrack[]
};
Expand Down
7 changes: 6 additions & 1 deletion src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@

const posterUrl = url.searchParams.get('poster');
const skyboxUrl = url.searchParams.get('skybox');
const skydomeUrl = url.searchParams.get('skydome');
const skyboxProjection = url.searchParams.get('skyprojection');
const resolvedSkyboxUrl = skyboxUrl || skydomeUrl;
const resolvedProjection = skyboxProjection || (skydomeUrl ? 'dome' : undefined);
const settingsUrl = url.searchParams.has('settings') ? url.searchParams.get('settings') : './settings.json';
const contentUrl = url.searchParams.has('content') ? url.searchParams.get('content') : './scene.compressed.ply';

const sseConfig = {
poster: posterUrl && createImage(posterUrl),
skyboxUrl,
skyboxUrl: resolvedSkyboxUrl,
skyboxProjection: resolvedProjection,
contentUrl,
contents: fetch(contentUrl),
noui: url.searchParams.has('noui'),
Expand Down
45 changes: 40 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ const loadSkybox = (app: AppBase, url: string) => {
});
};

const loadSkydome = (app: AppBase, url: string) => {
return new Promise<Asset>((resolve, reject) => {
const asset = new Asset('skydome', 'texture', {
url
}, {
type: 'rgbp',
mipmaps: false,
addressu: 'repeat',
addressv: 'clamp'
});

asset.on('load', () => {
resolve(asset);
});

asset.on('error', (err) => {
console.log(err);
reject(err);
});

app.assets.add(asset);
app.assets.load(asset);
});
};

const main = (app: AppBase, camera: Entity, settingsJson: any, config: Config) => {
const events = new EventHandler();

Expand Down Expand Up @@ -133,11 +158,21 @@ const main = (app: AppBase, camera: Entity, settingsJson: any, config: Config) =
}
);

// Load skybox
const skyboxLoad = config.skyboxUrl &&
loadSkybox(app, config.skyboxUrl).then((asset) => {
const settingsSkybox = global.settings.background?.skybox;
const skyboxUrl = config.skyboxUrl ?? settingsSkybox?.url;
const skyboxProjection = config.skyboxProjection ?? settingsSkybox?.projection;

// Load skybox (box projection) for background/IBL
const skyboxLoad = skyboxUrl && skyboxProjection !== 'dome' ?
loadSkybox(app, skyboxUrl).then((asset) => {
app.scene.envAtlas = asset.resource as Texture;
});
}) :
Promise.resolve();

// Load skydome (dome projection) for background only
const skydomeLoad = skyboxUrl && skyboxProjection === 'dome' ?
loadSkydome(app, skyboxUrl).then((asset) => asset.resource as Texture) :
Promise.resolve(null);

// Load and play sound
if (global.settings.soundUrl) {
Expand All @@ -154,7 +189,7 @@ const main = (app: AppBase, camera: Entity, settingsJson: any, config: Config) =
}

// Create the viewer
return new Viewer(global, gsplatLoad, skyboxLoad);
return new Viewer(global, gsplatLoad, skyboxLoad, skydomeLoad);
};

console.log(`SuperSplat Viewer v${appVersion} | Engine v${engineVersion} (${engineRevision})`);
Expand Down
7 changes: 6 additions & 1 deletion src/schemas/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ type ExperienceSettings = {
soundUrl?: string,
background: {
color: [number, number, number],
skyboxUrl?: string
skybox?: {
url?: string,
projection?: 'box' | 'dome',
scale?: number,
center?: number[]
}
},
postEffectSettings: PostEffectSettings,

Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type InputMode = 'desktop' | 'touch';
type Config = {
poster?: HTMLImageElement;
skyboxUrl?: string;
skyboxProjection?: 'box' | 'dome';
contentUrl?: string;
contents?: Promise<Response>;

Expand Down
67 changes: 62 additions & 5 deletions src/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import {
CameraFrame,
type CameraComponent,
Color,
type Entity,
Entity,
type Layer,
type Texture,
RenderTarget,
Mat4,
Mesh,
MeshInstance,
MiniStats,
ShaderChunks,
StandardMaterial,
type TextureHandler,
PIXELFORMAT_RGBA16F,
PIXELFORMAT_RGBA32F,
Expand All @@ -21,7 +25,10 @@ import {
TONEMAP_NEUTRAL,
Vec3,
GSplatComponent,
platform
platform,
DomeGeometry,
CULLFACE_FRONT,
LAYERID_SKYBOX
} from 'playcanvas';

import { Annotations } from './annotations';
Expand Down Expand Up @@ -134,7 +141,9 @@ class Viewer {

forceRenderNextFrame = false;

constructor(global: Global, gsplatLoad: Promise<Entity>, skyboxLoad: Promise<void>) {
skydomeEntity: Entity | null = null;

constructor(global: Global, gsplatLoad: Promise<Entity>, skyboxLoad: Promise<void>, skydomeLoad: Promise<Texture | null>) {
this.global = global;

const { app, settings, config, events, state, camera } = global;
Expand Down Expand Up @@ -244,6 +253,14 @@ class Viewer {
}
});

const skydomeCenter = new Vec3(0, 0, 0);
const skyboxSettings = settings.background?.skybox;
if (skyboxSettings?.center && skyboxSettings.center.length >= 3) {
skydomeCenter.set(skyboxSettings.center[0], skyboxSettings.center[1], skyboxSettings.center[2]);
}

let skydomeRadius = 0;

const applyCamera = (camera: Camera) => {
const cameraEntity = global.camera;

Expand All @@ -258,9 +275,15 @@ class Viewer {
vec.sub2(sceneBound.center, camera.position);
const dist = vec.dot(cameraEntity.forward);

const far = Math.max(dist + boundRadius, 1e-2);
let far = Math.max(dist + boundRadius, 1e-2);
const near = Math.max(dist - boundRadius, far / (1024 * 16));

if (skydomeRadius > 0) {
vec.sub2(skydomeCenter, camera.position);
const domeFar = vec.length() + skydomeRadius*2;
far = Math.max(far, domeFar);
}

cameraEntity.camera.farClip = far;
cameraEntity.camera.nearClip = near;
};
Expand Down Expand Up @@ -290,8 +313,9 @@ class Viewer {
});

// wait for the model to load
Promise.all([gsplatLoad, skyboxLoad]).then((results) => {
Promise.all([gsplatLoad, skyboxLoad, skydomeLoad]).then((results) => {
const gsplat = results[0].gsplat as GSplatComponent;
const skydomeTexture = results[2];

// get scene bounding box
const gsplatBbox = gsplat.customAabb;
Expand All @@ -303,6 +327,39 @@ class Viewer {
this.annotations = new Annotations(global, this.cameraFrame != null);
}

if (skydomeTexture) {
const geometry = new DomeGeometry({
latitudeBands: 32,
longitudeBands: 64
});
const mesh = Mesh.fromGeometry(graphicsDevice, geometry);
const material = new StandardMaterial();
material.useLighting = false;
material.emissive = new Color(1, 1, 1);
material.emissiveMap = skydomeTexture;
material.cull = CULLFACE_FRONT;
material.depthWrite = false;
material.depthTest = false;
material.update();

const meshInstance = new MeshInstance(mesh, material);
const skydomeEntity = new Entity('skydome');
skydomeEntity.addComponent('render', {
layers: [LAYERID_SKYBOX],
meshInstances: [meshInstance]
});

const boundRadius = sceneBound.halfExtents.length();
const scaleSetting = skyboxSettings?.scale;
const scale = scaleSetting ?? Math.max(boundRadius * 4, 10);
skydomeRadius = scale * 0.5;
skydomeEntity.setLocalScale(scale, scale, scale);
skydomeEntity.setPosition(skydomeCenter);

app.root.addChild(skydomeEntity);
this.skydomeEntity = skydomeEntity;
}

this.inputController = new InputController(global);

this.cameraManager = new CameraManager(global, sceneBound);
Expand Down