Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ splat-transform [GLOBAL] input [ACTIONS] ... output [ACTIONS]

Actions can be repeated and applied in any order:

```bash
```none
-t, --translate <x,y,z> Translate splats by (x, y, z).
-r, --rotate <x,y,z> Rotate splats by Euler angles (x, y, z), in degrees.
-s, --scale <factor> Uniformly scale splats by factor.
Expand All @@ -69,16 +69,18 @@ Actions can be repeated and applied in any order:

## Global Options

```bash
```none
-h, --help Show this help and exit.
-v, --version Show version and exit.
-w, --overwrite Overwrite output file if it exists.
-c, --cpu Use CPU for spherical harmonic compression.
-c, --cpu Use CPU for SOG SH compression.
-i, --iterations <n> Iterations for SOG SH compression (more = better). Default: 10.
-C, --camera-pos <x,y,z> HTML viewer camera position. Default: (2, 2, -2).
-T, --camera-target <x,y,z> HTML viewer target position. Default: (0, 0, 0).
-E, --viewer-settings <settings.json> HTML viewer settings JSON file.
```

> [!NOTE]
> See the [SuperSplat Viewer Settings Schema](https://github.com/playcanvas/supersplat-viewer?tab=readme-ov-file#settings-schema) for details on how to pass data to the `-E` option.

## Examples

### Basic Operations
Expand Down Expand Up @@ -112,8 +114,11 @@ splat-transform scene.sog restored.ply
# Convert from SOG (unbundled folder) back to PLY
splat-transform output/meta.json restored.ply

# Convert to HTML viewer with target and camera location
splat-transform -C 0,0,0 -T 0,0,10 input.ply output.html
# Convert to standalone HTML viewer
splat-transform input.ply output.html

# Convert to HTML viewer with custom settings
splat-transform -E settings.json input.ply output.html
```

### Transformations
Expand Down
28 changes: 17 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ type Options = {
version: boolean,
cpu: boolean,
iterations: number,
cameraPos: Vec3,
cameraTarget: Vec3
viewerSettingsPath?: string
};

const fileExists = async (filename: string) => {
Expand Down Expand Up @@ -136,7 +135,13 @@ const writeFile = async (filename: string, dataTable: DataTable, options: Option
});
break;
case 'html':
await writeHtml(outputFile, dataTable, options.cameraPos, options.cameraTarget, options.iterations, options.cpu ? 'cpu' : 'gpu');
await writeHtml(
outputFile,
dataTable,
options.iterations,
options.cpu ? 'cpu' : 'gpu',
options.viewerSettingsPath
);
break;
}

Expand Down Expand Up @@ -237,8 +242,7 @@ const parseArguments = () => {
version: { type: 'boolean', short: 'v' },
cpu: { type: 'boolean', short: 'c' },
iterations: { type: 'string', short: 'i' },
'camera-pos': { type: 'string', short: 'C' },
'camera-target': { type: 'string', short: 'T' },
'viewer-settings': { type: 'string', short: 'E' },

// file options
translate: { type: 'string', short: 't', multiple: true },
Expand Down Expand Up @@ -291,14 +295,14 @@ const parseArguments = () => {
};

const files: File[] = [];

const options: Options = {
overwrite: v.overwrite ?? false,
help: v.help ?? false,
version: v.version ?? false,
cpu: v.cpu ?? false,
iterations: parseInteger(v.iterations ?? '10'),
cameraPos: parseVec3((v as any)['camera-pos'] ?? '2,2,-2'),
cameraTarget: parseVec3((v as any)['camera-target'] ?? '0,0,0')
viewerSettingsPath: (v as any)['viewer-settings']
};

for (const t of tokens) {
Expand Down Expand Up @@ -447,8 +451,7 @@ GLOBAL OPTIONS
-w, --overwrite Overwrite output file if it exists.
-c, --cpu Use CPU for spherical harmonic compression.
-i, --iterations <n> Iterations for SOG SH compression (more = better). Default: 10.
-C, --camera-pos <x,y,z> HTML viewer camera position. Default: (2, 2, -2).
-T, --camera-target <x,y,z> HTML viewer target position. Default: (0, 0, 0).
-E, --viewer-settings <settings.json> HTML viewer settings JSON file.

EXAMPLES
# Scale then translate
Expand All @@ -457,8 +460,11 @@ EXAMPLES
# Merge two files with transforms
splat-transform -w cloudA.ply -r 0,90,0 cloudB.ply -s 2 merged.compressed.ply

# HTML viewer with custom camera
splat-transform -C 0,0,0 -T 0,0,10 bunny.ply bunny_app.html
# HTML viewer with default settings
splat-transform bunny.ply bunny_app.html

# HTML viewer with custom settings
splat-transform -E settings.json bunny.ply bunny_app.html

GENERATORS (beta)
# Generate synthetic splats
Expand Down
47 changes: 37 additions & 10 deletions src/writers/write-html.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,57 @@
import { open, unlink, FileHandle } from 'node:fs/promises';
import { open, readFile, unlink, FileHandle } from 'node:fs/promises';
import os from 'node:os';

import { html, css, js } from '@playcanvas/supersplat-viewer';
import { Vec3 } from 'playcanvas';

import { DataTable } from '../data-table';
import { writeSog } from './write-sog';

const writeHtml = async (fileHandle: FileHandle, dataTable: DataTable, camera: Vec3, target: Vec3, iterations: number, shMethod: 'cpu' | 'gpu') => {
type ViewerSettings = {
camera?: {
fov?: number;
position?: [number, number, number];
target?: [number, number, number];
startAnim?: string;
animTrack?: string;
};
background?: {
color?: [number, number, number];
};
animTracks?: unknown[];
};

const writeHtml = async (fileHandle: FileHandle, dataTable: DataTable, iterations: number, shMethod: 'cpu' | 'gpu', viewerSettingsPath?: string) => {
const pad = (text: string, spaces: number) => {
const whitespace = ' '.repeat(spaces);
return text.split('\n').map(line => whitespace + line).join('\n');
};

const experienceSettings = {
// Load viewer settings from file if provided
let viewerSettings: ViewerSettings = {};
if (viewerSettingsPath) {
const content = await readFile(viewerSettingsPath, 'utf-8');
try {
viewerSettings = JSON.parse(content);
} catch (e) {
throw new Error(`Failed to parse viewer settings JSON file: ${viewerSettingsPath}`);
}
}

// Merge provided settings with defaults
const mergedSettings = {
camera: {
fov: 50,
position: [camera.x, camera.y, camera.z],
target: [target.x, target.y, target.z],
position: [2, 2, -2] as [number, number, number],
target: [0, 0, 0] as [number, number, number],
startAnim: 'none',
animTrack: undefined as unknown as string | undefined
animTrack: undefined as string | undefined,
...viewerSettings.camera
},
background: {
color: [0.4, 0.4, 0.4]
color: [0.4, 0.4, 0.4] as [number, number, number],
...viewerSettings.background
},
animTracks: [] as unknown[]
animTracks: viewerSettings.animTracks ?? []
};

const tempSogPath = `${os.tmpdir()}/temp.sog`;
Expand All @@ -44,7 +71,7 @@ const writeHtml = async (fileHandle: FileHandle, dataTable: DataTable, camera: V
const generatedHtml = html
.replace(style, `<style>\n${pad(css, 12)}\n </style>`)
.replace(script, `<script type="module">\n${pad(js, 12)}\n </script>`)
.replace(settings, `settings: ${JSON.stringify(experienceSettings)}`)
.replace(settings, `settings: ${JSON.stringify(mergedSettings)}`)
.replace(content, `fetch("data:application/octet-stream;base64,${sogData}")`)
.replace('.compressed.ply', '.sog');

Expand Down