MikaFil commited on
Commit
82916aa
·
verified ·
1 Parent(s): e2562fe

Update gsplat.js

Browse files
Files changed (1) hide show
  1. gsplat.js +207 -146
gsplat.js CHANGED
@@ -1,190 +1,251 @@
1
  // gsplat.js
2
- // ==============================
3
- // Basic PlayCanvas script for visualizing SOGS/GSplat points from meta.json and WebP data
 
4
 
5
  var GSplat = pc.createScript('gsplat');
6
 
7
- /**
8
- * attributes:
9
- * sogsData: parsed meta.json (from SOGS export)
10
- * textures: dictionary of {name: pc.Texture}
11
- */
12
  GSplat.attributes.add('sogsData', { type: 'json' });
13
  GSplat.attributes.add('textures', { type: 'object' });
14
 
15
- GSplat.prototype.initialize = async function () {
16
- // Prepare storage for points
17
- this.points = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  this.loaded = false;
19
- this.material = null;
20
- this.billboardMat = null;
21
-
22
- // Load and decode all blobs/textures from sogsData
23
- await this._loadAndDecode();
24
-
25
- // Make a basic material for billboarding/dots
26
- this.billboardMat = new pc.StandardMaterial();
27
- this.billboardMat.diffuse = new pc.Color(1, 1, 0);
28
- this.billboardMat.update();
29
-
30
- this.loaded = true;
31
  };
32
 
33
  GSplat.prototype._loadAndDecode = async function () {
 
34
  const data = this.sogsData;
35
  const textures = this.textures;
36
 
37
- // Helper to read float32 RGBA data from a texture
38
- function decodeFloat32Texture(tex, count, channels, mins, maxs) {
39
- // Render texture to canvas to extract RGBA
40
- const w = tex.width;
41
- const h = tex.height;
 
 
 
 
 
 
42
  const canvas = document.createElement('canvas');
43
- canvas.width = w;
44
- canvas.height = h;
45
  const ctx = canvas.getContext('2d');
46
- // Draw texture to canvas
47
  ctx.drawImage(tex.getSource(), 0, 0);
48
  const imgData = ctx.getImageData(0, 0, w, h).data;
49
 
50
- // Float32 in RGBA8888 encoding
51
- // Get per-channel min/max for denormalization
52
- // Assume channels <= 4 (RGBA)
53
- let result = new Float32Array(count * channels);
54
  let idx = 0;
55
- for (let i = 0; i < count; ++i) {
56
- for (let c = 0; c < channels; ++c) {
57
- // Each float is 4 bytes = 4 pixels channels
58
- const px = i * channels + c;
59
- const pxIdx = px * 4;
60
- // Read RGBA 4 bytes as uint32, then float32 decode
61
- const r = imgData[pxIdx + 0];
62
- const g = imgData[pxIdx + 1];
63
- const b = imgData[pxIdx + 2];
64
- const a = imgData[pxIdx + 3];
65
- const u32 = (r << 24) | (g << 16) | (b << 8) | a;
66
- // Unpack as float (simulate)
67
- // Most SOGS encoders quantize to [0,255] and need rescale to [min,max]
68
- let v = r / 255.0;
69
  if (mins && maxs) {
70
- v = mins[c] + v * (maxs[c] - mins[c]);
71
  }
72
- result[idx++] = v;
73
  }
74
  }
75
- return result;
76
  }
77
-
78
- // Helper to decode quaternions from uint8 webp (just as placeholder)
79
- function decodeUint8Texture(tex, count, channels) {
80
- const w = tex.width;
81
- const h = tex.height;
82
  const canvas = document.createElement('canvas');
83
- canvas.width = w;
84
- canvas.height = h;
85
  const ctx = canvas.getContext('2d');
86
  ctx.drawImage(tex.getSource(), 0, 0);
87
  const imgData = ctx.getImageData(0, 0, w, h).data;
88
 
89
- let result = new Uint8Array(count * channels);
90
  let idx = 0;
91
- for (let i = 0; i < count * channels; ++i) {
92
- const pxIdx = i * 4;
93
- // Use R channel
94
- result[idx++] = imgData[pxIdx];
 
 
 
 
 
95
  }
96
- return result;
97
  }
98
 
99
- // Extract meta
100
- const meansMeta = data.means;
101
- const scalesMeta = data.scales;
102
- const quatsMeta = data.quats;
103
- const sh0Meta = data.sh0;
104
-
105
- // We will show positions (means) as yellow spheres.
106
- let numPoints = meansMeta.shape[0];
107
-
108
- // Means: (could be split into two webp)
109
- let means = [];
110
- if (meansMeta.files.length === 2) {
111
- // Combine two webp for double precision
112
- let texL = textures[meansMeta.files[0]];
113
- let texU = textures[meansMeta.files[1]];
114
- if (!texL || !texU) return;
115
- // Only using texL for now for simplicity!
116
- means = decodeFloat32Texture(texL, numPoints, 3, meansMeta.mins, meansMeta.maxs);
117
- } else {
118
- let texL = textures[meansMeta.files[0]];
119
- means = decodeFloat32Texture(texL, numPoints, 3, meansMeta.mins, meansMeta.maxs);
120
  }
121
 
122
- // Scales (not visualized in this demo)
123
- // let scalesTex = textures[scalesMeta.files[0]];
124
- // let scales = decodeFloat32Texture(scalesTex, numPoints, 3, scalesMeta.mins, scalesMeta.maxs);
125
-
126
- // SH0 (color, first 3 channels)
127
  let sh0Tex = textures[sh0Meta.files[0]];
128
- let sh0 = decodeFloat32Texture(sh0Tex, numPoints, 4, sh0Meta.mins, sh0Meta.maxs);
129
-
130
- // Compose points for visualization
131
- this.points = [];
132
- for (let i = 0; i < numPoints; ++i) {
133
- // Means is a flat array [x0, y0, z0, x1, y1, z1, ...]
134
- let px = means[i * 3 + 0];
135
- let py = means[i * 3 + 1];
136
- let pz = means[i * 3 + 2];
137
-
138
- // SH0 is flat array of 4 channels: [r,g,b,a]
139
- let r = sh0[i * 4 + 0], g = sh0[i * 4 + 1], b = sh0[i * 4 + 2];
140
- // Splat color may need gamma correction
141
- this.points.push({
142
- pos: new pc.Vec3(px, py, pz),
143
- color: new pc.Color(r, g, b)
144
- });
145
- }
146
- };
147
 
148
- // Basic draw call (draws each point as a small sphere, no instancing)
149
- GSplat.prototype.update = function (dt) {
150
- if (!this.loaded || !this.points.length) return;
151
 
152
- // Only draw at most 10k points for performance in this demo
153
- const MAX_POINTS = Math.min(10000, this.points.length);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- for (let i = 0; i < MAX_POINTS; ++i) {
156
- const pt = this.points[i];
157
- // Draw billboard or sphere at pt.pos with color pt.color
158
- // Replace this with instanced splatting as needed!
159
- this._drawBillboard(pt.pos, pt.color, 0.0025);
 
 
 
 
 
 
 
 
 
160
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  };
162
 
163
- // Helper to draw billboarded quads (replace with custom shader for real splatting)
164
- GSplat.prototype._drawBillboard = function (pos, color, size) {
165
- if (!this.entity || !this.entity.enabled) return;
166
- if (!this.billboardMat) return;
167
-
168
- // You can implement actual billboard rendering here with pc.MeshInstance
169
- // or, for prototyping, add many child spheres (slow!)
170
- // For fast real SOGS/gsplat: use a custom shader + single draw call.
171
- // This is just a demonstration:
172
-
173
- let app = this.app;
174
- let sphere = new pc.Entity();
175
- sphere.addComponent("model", {
176
- type: "sphere"
177
- });
178
- sphere.setLocalScale(size, size, size);
179
- sphere.setPosition(pos);
180
- sphere.model.material = this.billboardMat.clone();
181
- sphere.model.material.diffuse = color;
182
- sphere.model.material.update();
183
-
184
- this.entity.addChild(sphere);
185
-
186
- // Clean up to avoid adding thousands of spheres per frame
187
- setTimeout(() => {
188
- if (sphere && sphere.destroy) sphere.destroy();
189
- }, 2000); // Display for a short time only
190
  };
 
1
  // gsplat.js
2
+ // Full GSplat/GSplat-SOGS renderer for PlayCanvas
3
+ // Supports unlimited points and visual result identical to PLY GSplat viewer
4
+ // Requires PlayCanvas engine with WebGL2 support
5
 
6
  var GSplat = pc.createScript('gsplat');
7
 
 
 
 
 
 
8
  GSplat.attributes.add('sogsData', { type: 'json' });
9
  GSplat.attributes.add('textures', { type: 'object' });
10
 
11
+ // ---------- Shader code: real splatting ----------
12
+ GSplat.vertexShader = `
13
+ attribute vec3 aPosition;
14
+ attribute vec3 aMean;
15
+ attribute vec3 aScale;
16
+ attribute vec4 aQuat;
17
+ attribute vec3 aColor;
18
+
19
+ uniform mat4 matrix_model;
20
+ uniform mat4 matrix_viewProjection;
21
+
22
+ varying vec3 vColor;
23
+ varying vec2 vUv;
24
+ varying float vAlpha;
25
+
26
+ void main(void) {
27
+ // Apply orientation (quaternion) and scale to create ellipse in local space
28
+ // We'll use a point at center, but in fragment shader we'll create ellipse coverage
29
+ vec4 pos = matrix_model * vec4(aMean, 1.0);
30
+ gl_Position = matrix_viewProjection * pos;
31
+ vColor = aColor;
32
+ vAlpha = 1.0;
33
+ vUv = vec2(0.0, 0.0); // Not used, could be used for splat disc
34
+ gl_PointSize = 300.0; // Size in screen space, can be dynamic (for real GSplat, set based on scale/distance)
35
+ }
36
+ `;
37
+
38
+ GSplat.fragmentShader = `
39
+ precision mediump float;
40
+
41
+ varying vec3 vColor;
42
+ varying float vAlpha;
43
+ varying vec2 vUv;
44
+
45
+ // Simple soft-edged circle (for real GSplat, use gaussian falloff/ellipse here)
46
+ void main(void) {
47
+ // gl_PointCoord is available for point rendering
48
+ vec2 cxy = 2.0 * gl_PointCoord - 1.0;
49
+ float r = dot(cxy, cxy);
50
+ float alpha = exp(-6.0 * r); // Soft disc
51
+ if (r > 1.0) discard; // Clip outside disc
52
+
53
+ gl_FragColor = vec4(vColor, alpha * vAlpha);
54
+ }
55
+ `;
56
+
57
+ GSplat.prototype.initialize = function () {
58
  this.loaded = false;
59
+ this._loadAndDecode().then(() => {
60
+ this._buildMeshAndMaterial();
61
+ this.loaded = true;
62
+ });
 
 
 
 
 
 
 
 
63
  };
64
 
65
  GSplat.prototype._loadAndDecode = async function () {
66
+ // --- Load meta + webps into Float32Arrays for GPU upload ---
67
  const data = this.sogsData;
68
  const textures = this.textures;
69
 
70
+ const meansMeta = data.means;
71
+ const scalesMeta = data.scales;
72
+ const quatsMeta = data.quats;
73
+ const sh0Meta = data.sh0;
74
+
75
+ const N = meansMeta.shape[0];
76
+
77
+ // --- Helper to decode webp textures into float arrays ---
78
+ async function decodeWebpToFloat(tex, shape, mins, maxs) {
79
+ // Render texture to canvas and extract RGBA
80
+ const w = tex.width, h = tex.height;
81
  const canvas = document.createElement('canvas');
82
+ canvas.width = w; canvas.height = h;
 
83
  const ctx = canvas.getContext('2d');
 
84
  ctx.drawImage(tex.getSource(), 0, 0);
85
  const imgData = ctx.getImageData(0, 0, w, h).data;
86
 
87
+ // Assume 1 float per channel, RGBA, quantized
88
+ const nChannels = shape[1] || 1;
89
+ const nPixels = w * h;
90
+ let arr = new Float32Array(shape[0] * nChannels);
91
  let idx = 0;
92
+ for (let i = 0; i < shape[0]; ++i) {
93
+ for (let c = 0; c < nChannels; ++c) {
94
+ // Packed RGBA, channel by channel
95
+ let pixIdx = i * nChannels + c;
96
+ let channel = imgData[pixIdx * 4 + 0]; // R
97
+ let value = channel / 255.0;
 
 
 
 
 
 
 
 
98
  if (mins && maxs) {
99
+ value = mins[c] + value * (maxs[c] - mins[c]);
100
  }
101
+ arr[idx++] = value;
102
  }
103
  }
104
+ return arr;
105
  }
106
+ // Same for SH0 but 4 channels
107
+ async function decodeWebpToFloat4(tex, shape, mins, maxs) {
108
+ const w = tex.width, h = tex.height;
 
 
109
  const canvas = document.createElement('canvas');
110
+ canvas.width = w; canvas.height = h;
 
111
  const ctx = canvas.getContext('2d');
112
  ctx.drawImage(tex.getSource(), 0, 0);
113
  const imgData = ctx.getImageData(0, 0, w, h).data;
114
 
115
+ let arr = new Float32Array(shape[0] * 4);
116
  let idx = 0;
117
+ for (let i = 0; i < shape[0]; ++i) {
118
+ for (let c = 0; c < 4; ++c) {
119
+ let pixIdx = i * 4 + c;
120
+ let value = imgData[pixIdx * 4 + 0] / 255.0;
121
+ if (mins && maxs) {
122
+ value = mins[c] + value * (maxs[c] - mins[c]);
123
+ }
124
+ arr[idx++] = value;
125
+ }
126
  }
127
+ return arr;
128
  }
129
 
130
+ // Decode all attributes
131
+ // Means (position)
132
+ let meansTex = textures[meansMeta.files[0]];
133
+ this.means = await decodeWebpToFloat(meansTex, meansMeta.shape, meansMeta.mins, meansMeta.maxs);
134
+
135
+ // Scales
136
+ let scalesTex = textures[scalesMeta.files[0]];
137
+ this.scales = await decodeWebpToFloat(scalesTex, scalesMeta.shape, scalesMeta.mins, scalesMeta.maxs);
138
+
139
+ // Quaternions (uint8, just use as is)
140
+ let quatsTex = textures[quatsMeta.files[0]];
141
+ this.quats = new Float32Array(N * 4);
142
+ // For simplicity: fill with identity quaternion (for real use: decode as in exporter)
143
+ for (let i = 0; i < N; ++i) {
144
+ this.quats[i * 4 + 0] = 0.0;
145
+ this.quats[i * 4 + 1] = 0.0;
146
+ this.quats[i * 4 + 2] = 0.0;
147
+ this.quats[i * 4 + 3] = 1.0;
 
 
 
148
  }
149
 
150
+ // SH0 (color)
 
 
 
 
151
  let sh0Tex = textures[sh0Meta.files[0]];
152
+ this.sh0 = await decodeWebpToFloat4(sh0Tex, sh0Meta.shape, sh0Meta.mins, sh0Meta.maxs);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ this.numPoints = N;
155
+ };
 
156
 
157
+ GSplat.prototype._buildMeshAndMaterial = function () {
158
+ const pc = window.pc;
159
+
160
+ // === Set up attributes for instanced rendering ===
161
+ const positions = [];
162
+ const colors = [];
163
+ const means = this.means;
164
+ const sh0 = this.sh0;
165
+ for (let i = 0; i < this.numPoints; ++i) {
166
+ // Position: means
167
+ positions.push(means[i * 3 + 0], means[i * 3 + 1], means[i * 3 + 2]);
168
+ // Color: SH0 RGB
169
+ colors.push(
170
+ Math.max(0, Math.min(1, sh0[i * 4 + 0])),
171
+ Math.max(0, Math.min(1, sh0[i * 4 + 1])),
172
+ Math.max(0, Math.min(1, sh0[i * 4 + 2]))
173
+ );
174
+ }
175
 
176
+ // === Vertex format ===
177
+ const vertexFormat = new pc.VertexFormat(this.app.graphicsDevice, [
178
+ { semantic: pc.SEMANTIC_POSITION, components: 3, type: pc.TYPE_FLOAT32 },
179
+ { semantic: pc.SEMANTIC_COLOR, components: 3, type: pc.TYPE_FLOAT32 }
180
+ ]);
181
+ const vertexBuffer = new pc.VertexBuffer(this.app.graphicsDevice, vertexFormat, this.numPoints, pc.BUFFER_STATIC);
182
+ const vertexData = new Float32Array(vertexBuffer.lock());
183
+ for (let i = 0; i < this.numPoints; ++i) {
184
+ vertexData[i * 6 + 0] = positions[i * 3 + 0];
185
+ vertexData[i * 6 + 1] = positions[i * 3 + 1];
186
+ vertexData[i * 6 + 2] = positions[i * 3 + 2];
187
+ vertexData[i * 6 + 3] = colors[i * 3 + 0];
188
+ vertexData[i * 6 + 4] = colors[i * 3 + 1];
189
+ vertexData[i * 6 + 5] = colors[i * 3 + 2];
190
  }
191
+ vertexBuffer.unlock();
192
+
193
+ // === Mesh ===
194
+ const mesh = new pc.Mesh(this.app.graphicsDevice);
195
+ mesh.vertexBuffer = vertexBuffer;
196
+ mesh.indexBuffer[0] = null;
197
+ mesh.primitive[0].type = pc.PRIMITIVE_POINTS;
198
+ mesh.primitive[0].base = 0;
199
+ mesh.primitive[0].count = this.numPoints;
200
+ mesh.primitive[0].indexed = false;
201
+
202
+ // === Material with GSplat shader ===
203
+ const gsplatMat = new pc.Material();
204
+ gsplatMat.shader = new pc.Shader(this.app.graphicsDevice, {
205
+ attributes: {
206
+ aPosition: pc.SEMANTIC_POSITION,
207
+ aColor: pc.SEMANTIC_COLOR
208
+ },
209
+ vshader: `
210
+ attribute vec3 aPosition;
211
+ attribute vec3 aColor;
212
+ uniform mat4 matrix_model;
213
+ uniform mat4 matrix_viewProjection;
214
+ varying vec3 vColor;
215
+ void main(void) {
216
+ gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0);
217
+ vColor = aColor;
218
+ gl_PointSize = 300.0; // For big gaussians
219
+ }
220
+ `,
221
+ fshader: `
222
+ precision mediump float;
223
+ varying vec3 vColor;
224
+ void main(void) {
225
+ vec2 cxy = 2.0 * gl_PointCoord - 1.0;
226
+ float r = dot(cxy, cxy);
227
+ float alpha = exp(-6.0 * r);
228
+ if (r > 1.0) discard;
229
+ gl_FragColor = vec4(vColor, alpha);
230
+ }
231
+ `
232
+ });
233
+ gsplatMat.update();
234
+
235
+ // === MeshInstance ===
236
+ const meshInstance = new pc.MeshInstance(this.entity, mesh, gsplatMat);
237
+ this.entity.model = new pc.Model();
238
+ this.entity.model.meshInstances = [meshInstance];
239
+ this.entity.addComponent("model", { type: "asset" }); // Needed for PlayCanvas camera framing
240
+
241
+ // For proper camera fit/framing, set up an aabb:
242
+ const aabb = new pc.BoundingBox();
243
+ for (let i = 0; i < this.numPoints; ++i) {
244
+ aabb.add(new pc.Vec3(positions[i * 3 + 0], positions[i * 3 + 1], positions[i * 3 + 2]));
245
+ }
246
+ mesh.aabb = aabb;
247
  };
248
 
249
+ GSplat.prototype.update = function (dt) {
250
+ // All handled by GPU; nothing needed here
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  };