Skip to content

Commit

Permalink
Save as ply, convert splat to ply (#52)
Browse files Browse the repository at this point in the history
* add conversion to ply and save as ply

* write extra ply properties

* update readme, package version
  • Loading branch information
dylanebert authored Jan 19, 2024
1 parent 306833d commit 4a6c6b3
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 14 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ A: Yes, gsplat.js supports `.ply` files. See the [ply-converter example](https:/
A: `.splat` files are a compact form of the splat data, offering quicker loading times than `.ply` files. They consist of a raw Uint8Array buffer.
> ⚠️ The `.splat` format does not contain SH coefficients, so colors are not view-dependent.
**Q: Can I convert .splat files to .ply?**
A: Yes, see the commented code in the [ply-converter example](https://github.com/dylanebert/gsplat.js/blob/main/examples/ply-converter/src/main.ts). Alternatively, convert `.splat` to `.ply` from URL in this [jsfiddle example](https://jsfiddle.net/aL81ds3e/).
> ⚠️ When converting `.ply` -> `.splat` -> `.ply`, SH coefficients will be lost.
### License
This project is released under the MIT license. It is built upon several other open-source projects:
Expand Down
7 changes: 7 additions & 0 deletions examples/ply-converter/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ async function main() {
progressDialog.close();
scene.saveToFile("bonsai.splat");

// Alternatively, uncomment below to convert from splat to ply
// NOTE: Data like SH coefficients will be lost when converting ply -> splat -> ply
/* const url = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k-mini.splat";
await SPLAT.Loader.LoadAsync(url, scene, (progress) => (progressIndicator.value = progress * 100));
progressDialog.close();
scene.saveToFile("bonsai-7k-mini.ply", "ply"); */

// Render loop
const frame = () => {
controls.update();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gsplat",
"version": "1.0.3",
"version": "1.0.4",
"description": "JavaScript Gaussian Splatting library",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
20 changes: 17 additions & 3 deletions src/core/Scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SplatData } from "../splats/SplatData";
import { Splat } from "../splats/Splat";
import { EventDispatcher } from "../events/EventDispatcher";
import { ObjectAddedEvent, ObjectRemovedEvent } from "../events/Events";
import { Converter } from "../utils/Converter";

class Scene extends EventDispatcher {
private _objects: Object3D[] = [];
Expand Down Expand Up @@ -58,12 +59,18 @@ class Scene extends EventDispatcher {
this.reset();
}

saveToFile(name: string | null = null) {
saveToFile(name: string | null = null, format: string | null = null) {
if (!document) return;

if (!format) {
format = "splat";
} else if (format !== "splat" && format !== "ply") {
throw new Error("Invalid format. Must be 'splat' or 'ply'");
}

if (!name) {
const now = new Date();
name = `scene-${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}.splat`;
name = `scene-${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}.${format}`;
}

const buffers: Uint8Array[] = [];
Expand All @@ -87,7 +94,14 @@ class Scene extends EventDispatcher {
offset += buffer.length;
}

const blob = new Blob([data.buffer], { type: "application/octet-stream" });
let blob;
if (format === "ply") {
const plyData = Converter.SplatToPLY(data.buffer, vertexCount);
blob = new Blob([plyData], { type: "application/octet-stream" });
} else {
blob = new Blob([data.buffer], { type: "application/octet-stream" });
}

const link = document.createElement("a");
link.download = name;
link.href = URL.createObjectURL(blob);
Expand Down
11 changes: 5 additions & 6 deletions src/loaders/PLYLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { Vector3 } from "../math/Vector3";
import { Quaternion } from "../math/Quaternion";
import { SplatData } from "../splats/SplatData";
import { Splat } from "../splats/Splat";
import { Converter } from "../utils/Converter";

class PLYLoader {
static SH_C0 = 0.28209479177387814;

static async LoadAsync(
url: string,
scene: Scene,
Expand Down Expand Up @@ -174,16 +173,16 @@ class PLYLoader {
rgba[2] = value;
break;
case "f_dc_0":
rgba[0] = (0.5 + this.SH_C0 * value) * 255;
rgba[0] = (0.5 + Converter.SH_C0 * value) * 255;
break;
case "f_dc_1":
rgba[1] = (0.5 + this.SH_C0 * value) * 255;
rgba[1] = (0.5 + Converter.SH_C0 * value) * 255;
break;
case "f_dc_2":
rgba[2] = (0.5 + this.SH_C0 * value) * 255;
rgba[2] = (0.5 + Converter.SH_C0 * value) * 255;
break;
case "f_dc_3":
rgba[3] = (0.5 + this.SH_C0 * value) * 255;
rgba[3] = (0.5 + Converter.SH_C0 * value) * 255;
break;
case "opacity":
rgba[3] = (1 / (1 + Math.exp(-value))) * 255;
Expand Down
22 changes: 18 additions & 4 deletions src/splats/Splat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SplatData } from "./SplatData";
import { Object3D } from "../core/Object3D";
import { Vector3 } from "../math/Vector3";
import { Quaternion } from "../math/Quaternion";
import { Converter } from "../utils/Converter";

class Splat extends Object3D {
public selectedChanged: boolean = false;
Expand Down Expand Up @@ -30,20 +31,33 @@ class Splat extends Object3D {
};
}

saveToFile(name: string | null = null) {
saveToFile(name: string | null = null, format: string | null = null) {
if (!document) return;

if (!format) {
format = "splat";
} else if (format !== "splat" && format !== "ply") {
throw new Error("Invalid format. Must be 'splat' or 'ply'");
}

if (!name) {
const now = new Date();
name = `splat-${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}.splat`;
name = `splat-${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}.${format}`;
}

this.applyRotation();
this.applyScale();
this.applyPosition();

const buffer = this.data.serialize();
const blob = new Blob([buffer], { type: "application/octet-stream" });
const data = this.data.serialize();
let blob;
if (format === "ply") {
const plyData = Converter.SplatToPLY(data.buffer, this.data.vertexCount);
blob = new Blob([plyData], { type: "application/octet-stream" });
} else {
blob = new Blob([data.buffer], { type: "application/octet-stream" });
}

const link = document.createElement("a");
link.download = name;
link.href = URL.createObjectURL(blob);
Expand Down
96 changes: 96 additions & 0 deletions src/utils/Converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Quaternion } from "../math/Quaternion";

class Converter {
public static SH_C0 = 0.28209479177387814;

public static SplatToPLY(buffer: ArrayBuffer, vertexCount: number): ArrayBuffer {
let header = "ply\nformat binary_little_endian 1.0\n";
header += `element vertex ${vertexCount}\n`;

const properties = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2"];
for (let i = 0; i < 45; i++) {
properties.push(`f_rest_${i}`);
}
properties.push("opacity");
properties.push("scale_0");
properties.push("scale_1");
properties.push("scale_2");
properties.push("rot_0");
properties.push("rot_1");
properties.push("rot_2");
properties.push("rot_3");

for (const property of properties) {
header += `property float ${property}\n`;
}
header += "end_header\n";

const headerBuffer = new TextEncoder().encode(header);

const plyRowLength = 4 * 3 + 4 * 3 + 4 * 3 + 4 * 45 + 4 + 4 * 3 + 4 * 4;
const plyLength = vertexCount * plyRowLength;
const output = new DataView(new ArrayBuffer(headerBuffer.length + plyLength));
new Uint8Array(output.buffer).set(headerBuffer, 0);

const f_buffer = new Float32Array(buffer);
const u_buffer = new Uint8Array(buffer);

const offset = headerBuffer.length;
const f_dc_offset = 4 * 3 + 4 * 3;
const opacity_offset = f_dc_offset + 4 * 3 + 4 * 45;
const scale_offset = opacity_offset + 4;
const rot_offset = scale_offset + 4 * 3;
for (let i = 0; i < vertexCount; i++) {
const pos0 = f_buffer[8 * i + 0];
const pos1 = f_buffer[8 * i + 1];
const pos2 = f_buffer[8 * i + 2];

const f_dc_0 = (u_buffer[32 * i + 24 + 0] / 255 - 0.5) / this.SH_C0;
const f_dc_1 = (u_buffer[32 * i + 24 + 1] / 255 - 0.5) / this.SH_C0;
const f_dc_2 = (u_buffer[32 * i + 24 + 2] / 255 - 0.5) / this.SH_C0;

const alpha = u_buffer[32 * i + 24 + 3] / 255;
const opacity = Math.log(alpha / (1 - alpha));

const scale0 = Math.log(f_buffer[8 * i + 3 + 0]);
const scale1 = Math.log(f_buffer[8 * i + 3 + 1]);
const scale2 = Math.log(f_buffer[8 * i + 3 + 2]);

let q = new Quaternion(
(u_buffer[32 * i + 28 + 1] - 128) / 128,
(u_buffer[32 * i + 28 + 2] - 128) / 128,
(u_buffer[32 * i + 28 + 3] - 128) / 128,
(u_buffer[32 * i + 28 + 0] - 128) / 128,
);
q = q.normalize();

const rot0 = q.w;
const rot1 = q.x;
const rot2 = q.y;
const rot3 = q.z;

output.setFloat32(offset + plyRowLength * i + 0, pos0, true);
output.setFloat32(offset + plyRowLength * i + 4, pos1, true);
output.setFloat32(offset + plyRowLength * i + 8, pos2, true);

output.setFloat32(offset + plyRowLength * i + f_dc_offset + 0, f_dc_0, true);
output.setFloat32(offset + plyRowLength * i + f_dc_offset + 4, f_dc_1, true);
output.setFloat32(offset + plyRowLength * i + f_dc_offset + 8, f_dc_2, true);

output.setFloat32(offset + plyRowLength * i + opacity_offset, opacity, true);

output.setFloat32(offset + plyRowLength * i + scale_offset + 0, scale0, true);
output.setFloat32(offset + plyRowLength * i + scale_offset + 4, scale1, true);
output.setFloat32(offset + plyRowLength * i + scale_offset + 8, scale2, true);

output.setFloat32(offset + plyRowLength * i + rot_offset + 0, rot0, true);
output.setFloat32(offset + plyRowLength * i + rot_offset + 4, rot1, true);
output.setFloat32(offset + plyRowLength * i + rot_offset + 8, rot2, true);
output.setFloat32(offset + plyRowLength * i + rot_offset + 12, rot3, true);
}

return output.buffer;
}
}

export { Converter };

0 comments on commit 4a6c6b3

Please sign in to comment.