@myned-ai/gsplat-flame-avatar-renderer 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +4 -29
  2. package/dist/gsplat-flame-avatar-renderer.cjs.js +12875 -0
  3. package/dist/{gsplat-flame-avatar-renderer.umd.js.map → gsplat-flame-avatar-renderer.cjs.js.map} +1 -1
  4. package/dist/gsplat-flame-avatar-renderer.esm.js +1 -1
  5. package/package.json +5 -3
  6. package/src/api/index.js +7 -0
  7. package/src/buffers/SplatBuffer.js +1394 -0
  8. package/src/buffers/SplatBufferGenerator.js +41 -0
  9. package/src/buffers/SplatPartitioner.js +110 -0
  10. package/src/buffers/UncompressedSplatArray.js +106 -0
  11. package/src/buffers/index.js +11 -0
  12. package/src/core/SplatGeometry.js +48 -0
  13. package/src/core/SplatMesh.js +2620 -0
  14. package/src/core/SplatScene.js +43 -0
  15. package/src/core/SplatTree.js +200 -0
  16. package/src/core/Viewer.js +2895 -0
  17. package/src/core/index.js +13 -0
  18. package/src/enums/EngineConstants.js +58 -0
  19. package/src/enums/LogLevel.js +13 -0
  20. package/src/enums/RenderMode.js +11 -0
  21. package/src/enums/SceneFormat.js +21 -0
  22. package/src/enums/SceneRevealMode.js +11 -0
  23. package/src/enums/SplatRenderMode.js +10 -0
  24. package/src/enums/index.js +13 -0
  25. package/src/flame/FlameAnimator.js +271 -0
  26. package/src/flame/FlameConstants.js +21 -0
  27. package/src/flame/FlameTextureManager.js +293 -0
  28. package/src/flame/index.js +22 -0
  29. package/src/flame/utils.js +50 -0
  30. package/src/index.js +39 -0
  31. package/src/loaders/DirectLoadError.js +14 -0
  32. package/src/loaders/INRIAV1PlyParser.js +223 -0
  33. package/src/loaders/PlyLoader.js +261 -0
  34. package/src/loaders/PlyParser.js +19 -0
  35. package/src/loaders/PlyParserUtils.js +311 -0
  36. package/src/loaders/index.js +13 -0
  37. package/src/materials/SplatMaterial.js +1065 -0
  38. package/src/materials/SplatMaterial2D.js +358 -0
  39. package/src/materials/SplatMaterial3D.js +278 -0
  40. package/src/materials/index.js +11 -0
  41. package/src/raycaster/Hit.js +37 -0
  42. package/src/raycaster/Ray.js +123 -0
  43. package/src/raycaster/Raycaster.js +175 -0
  44. package/src/raycaster/index.js +10 -0
  45. package/src/renderer/AnimationManager.js +574 -0
  46. package/src/renderer/AppConstants.js +101 -0
  47. package/src/renderer/GaussianSplatRenderer.js +695 -0
  48. package/src/renderer/index.js +24 -0
  49. package/src/utils/LoaderUtils.js +65 -0
  50. package/src/utils/Util.js +375 -0
  51. package/src/utils/index.js +9 -0
  52. package/src/worker/SortWorker.js +284 -0
  53. package/src/worker/index.js +8 -0
  54. package/dist/gsplat-flame-avatar-renderer.esm.min.js +0 -2
  55. package/dist/gsplat-flame-avatar-renderer.esm.min.js.map +0 -1
  56. package/dist/gsplat-flame-avatar-renderer.umd.js +0 -12876
  57. package/dist/gsplat-flame-avatar-renderer.umd.min.js +0 -2
  58. package/dist/gsplat-flame-avatar-renderer.umd.min.js.map +0 -1
  59. package/dist/gsplat-flame-avatar.esm.js +0 -12755
  60. package/dist/gsplat-flame-avatar.esm.js.map +0 -1
  61. package/dist/gsplat-flame-avatar.esm.min.js +0 -2
  62. package/dist/gsplat-flame-avatar.esm.min.js.map +0 -1
  63. package/dist/gsplat-flame-avatar.umd.js +0 -12876
  64. package/dist/gsplat-flame-avatar.umd.js.map +0 -1
  65. package/dist/gsplat-flame-avatar.umd.min.js +0 -2
  66. package/dist/gsplat-flame-avatar.umd.min.js.map +0 -1
@@ -0,0 +1,1065 @@
1
+ /**
2
+ * SplatMaterial
3
+ *
4
+ * Based on @mkkellogg/gaussian-splats-3d (MIT License)
5
+ * https://github.com/mkkellogg/GaussianSplats3D
6
+ *
7
+ * HEAVILY MODIFIED for FLAME avatar support:
8
+ * - Added FLAME bone/pose uniforms and textures
9
+ * - Added expression blendshape support
10
+ * - Extended vertex shader with GPU skinning
11
+ * - Additional ~500 lines of FLAME-specific shader code
12
+ */
13
+
14
+ import { Color, DoubleSide, Matrix4, NormalBlending, ShaderMaterial, Vector2, Vector3 } from 'three';
15
+ import { Constants } from '../enums/EngineConstants.js';
16
+
17
+ export class SplatMaterial {
18
+
19
+ static buildVertexShaderBase(dynamicMode = false, enableOptionalEffects = false, maxSphericalHarmonicsDegree = 0, customVars = '', useFlame = true) {
20
+ let vertexShaderSource = ``;
21
+ if (useFlame == true) {
22
+ vertexShaderSource += `#define USE_FLAME`;
23
+ } else {
24
+ vertexShaderSource += `#define USE_SKINNING`;
25
+ }
26
+ vertexShaderSource += `
27
+ precision highp float;
28
+ #include <common>
29
+
30
+ attribute uint splatIndex;
31
+ uniform highp usampler2D flameModelTexture;
32
+ uniform highp usampler2D boneTexture;
33
+ uniform highp usampler2D boneWeightTexture;
34
+
35
+
36
+ uniform highp usampler2D centersColorsTexture;
37
+ uniform highp sampler2D sphericalHarmonicsTexture;
38
+ uniform highp sampler2D sphericalHarmonicsTextureR;
39
+ uniform highp sampler2D sphericalHarmonicsTextureG;
40
+ uniform highp sampler2D sphericalHarmonicsTextureB;
41
+
42
+ uniform highp usampler2D sceneIndexesTexture;
43
+ uniform vec2 sceneIndexesTextureSize;
44
+ uniform int sceneCount;
45
+ uniform int gaussianSplatCount;
46
+ uniform int bsCount;
47
+ uniform float headBoneIndex;
48
+ #ifdef USE_SKINNING
49
+ attribute vec4 skinIndex;
50
+ attribute vec4 skinWeight;
51
+ #endif
52
+ `;
53
+
54
+ if (enableOptionalEffects) {
55
+ vertexShaderSource += `
56
+ uniform float sceneOpacity[${Constants.MaxScenes}];
57
+ uniform int sceneVisibility[${Constants.MaxScenes}];
58
+ `;
59
+ }
60
+
61
+ if (dynamicMode) {
62
+ vertexShaderSource += `
63
+ uniform highp mat4 transforms[${Constants.MaxScenes}];
64
+ `;
65
+ }
66
+
67
+ vertexShaderSource += `
68
+ ${customVars}
69
+ uniform vec2 focal;
70
+ uniform float orthoZoom;
71
+ uniform int orthographicMode;
72
+ uniform int pointCloudModeEnabled;
73
+ uniform float inverseFocalAdjustment;
74
+ uniform vec2 viewport;
75
+ uniform vec2 basisViewport;
76
+ uniform vec2 centersColorsTextureSize;
77
+ uniform vec2 flameModelTextureSize;
78
+ uniform vec2 boneWeightTextureSize;
79
+ uniform vec2 boneTextureSize;
80
+
81
+ uniform int sphericalHarmonicsDegree;
82
+ uniform vec2 sphericalHarmonicsTextureSize;
83
+ uniform int sphericalHarmonics8BitMode;
84
+ uniform int sphericalHarmonicsMultiTextureMode;
85
+ uniform float visibleRegionRadius;
86
+ uniform float visibleRegionFadeStartRadius;
87
+ uniform float firstRenderTime;
88
+ uniform float currentTime;
89
+ uniform int fadeInComplete;
90
+ uniform vec3 sceneCenter;
91
+ uniform float splatScale;
92
+ uniform float sphericalHarmonics8BitCompressionRangeMin[${Constants.MaxScenes}];
93
+ uniform float sphericalHarmonics8BitCompressionRangeMax[${Constants.MaxScenes}];
94
+
95
+ varying vec4 vColor;
96
+ varying vec2 vUv;
97
+ varying vec2 vPosition;
98
+ varying vec2 vSplatIndex;
99
+ #ifdef USE_SKINNING
100
+ uniform mat4 bindMatrix;
101
+ uniform mat4 bindMatrixInverse;
102
+ uniform highp sampler2D boneTexture0;
103
+ mat4 getBoneMatrix0( const in float i ) {
104
+ int size = textureSize( boneTexture0, 0 ).x;
105
+ int j = int( i ) * 4;
106
+ int x = j % size;
107
+ int y = j / size;
108
+ vec4 v1 = texelFetch( boneTexture0, ivec2( x, y ), 0 );
109
+ vec4 v2 = texelFetch( boneTexture0, ivec2( x + 1, y ), 0 );
110
+ vec4 v3 = texelFetch( boneTexture0, ivec2( x + 2, y ), 0 );
111
+ vec4 v4 = texelFetch( boneTexture0, ivec2( x + 3, y ), 0 );
112
+ return mat4( v1, v2, v3, v4 );
113
+ }
114
+ #endif
115
+
116
+ mat3 quaternionToRotationMatrix(float x, float y, float z, float w) {
117
+ float s = 1.0 / sqrt(w * w + x * x + y * y + z * z);
118
+
119
+ return mat3(
120
+ 1. - 2. * (y * y + z * z),
121
+ 2. * (x * y + w * z),
122
+ 2. * (x * z - w * y),
123
+ 2. * (x * y - w * z),
124
+ 1. - 2. * (x * x + z * z),
125
+ 2. * (y * z + w * x),
126
+ 2. * (x * z + w * y),
127
+ 2. * (y * z - w * x),
128
+ 1. - 2. * (x * x + y * y)
129
+ );
130
+ }
131
+
132
+ const float sqrt8 = sqrt(8.0);
133
+ const float minAlpha = 1.0 / 255.0;
134
+
135
+ const vec4 encodeNorm4 = vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0);
136
+ const uvec4 mask4 = uvec4(uint(0x000000FF), uint(0x0000FF00), uint(0x00FF0000), uint(0xFF000000));
137
+ const uvec4 shift4 = uvec4(0, 8, 16, 24);
138
+ int internal = 1;//show a gaussian splatting point every internal points.
139
+ vec4 uintToRGBAVec (uint u) {
140
+ uvec4 urgba = mask4 & u;
141
+ urgba = urgba >> shift4;
142
+ vec4 rgba = vec4(urgba) * encodeNorm4;
143
+ return rgba;
144
+ }
145
+ float getRealIndex(int sIndex, int reducedFactor) {
146
+ int remainder = sIndex % reducedFactor;
147
+
148
+ if(remainder == int(0)) {
149
+ return float(sIndex);
150
+ }
151
+ else
152
+ {
153
+ return float(sIndex - remainder);
154
+ }
155
+ }
156
+
157
+ vec2 getDataUV(in int stride, in int offset, in vec2 dimensions) {
158
+ vec2 samplerUV = vec2(0.0, 0.0);
159
+ float d = float(uint(getRealIndex(int(splatIndex), internal)) * uint(stride) + uint(offset)) / dimensions.x;
160
+ samplerUV.y = float(floor(d)) / dimensions.y;
161
+ samplerUV.x = fract(d);
162
+ return samplerUV;
163
+ }
164
+
165
+ vec2 getFlameDataUV(in int stride, in int offset, in vec2 dimensions) {
166
+ vec2 samplerUV = vec2(0.0, 0.0);
167
+ float d = float(uint(int(splatIndex) / internal) * uint(stride) + uint(offset) + uint(gaussianSplatCount * bsCount)) / dimensions.x;
168
+ samplerUV.y = float(floor(d)) / dimensions.y;
169
+ samplerUV.x = fract(d);
170
+ return samplerUV;
171
+ }
172
+
173
+ vec2 getBoneWeightUV(in int stride, in int offset, in vec2 dimensions) {
174
+ vec2 samplerUV = vec2(0.0, 0.0);
175
+ float d = float(uint(int(splatIndex) / internal) * uint(stride) + uint(offset)) / dimensions.x;
176
+ samplerUV.y = float(floor(d)) / dimensions.y;
177
+ samplerUV.x = fract(d);
178
+ return samplerUV;
179
+ }
180
+
181
+ vec2 getBSFlameDataUV(in int bsInedex, in int stride, in int offset, in vec2 dimensions) {
182
+ vec2 samplerUV = vec2(0.0, 0.0);
183
+ float d = float(uint(int(splatIndex) / internal) * uint(stride) + uint(offset) + uint(gaussianSplatCount * bsInedex)) / dimensions.x;
184
+ samplerUV.y = float(floor(d)) / dimensions.y;
185
+ samplerUV.x = fract(d);
186
+ return samplerUV;
187
+ }
188
+
189
+ vec2 getDataUVF(in uint sIndex, in float stride, in uint offset, in vec2 dimensions) {
190
+ vec2 samplerUV = vec2(0.0, 0.0);
191
+ float d = float(uint(float(getRealIndex(int(sIndex), internal)) * stride) + offset) / dimensions.x;
192
+ samplerUV.y = float(floor(d)) / dimensions.y;
193
+ samplerUV.x = fract(d);
194
+ return samplerUV;
195
+ }
196
+
197
+ const float SH_C1 = 0.4886025119029199f;
198
+ const float[5] SH_C2 = float[](1.0925484, -1.0925484, 0.3153916, -1.0925484, 0.5462742);
199
+
200
+ mat4 getBoneMatrix( float i ) {
201
+ float y = i;
202
+ float x = 0.0;
203
+
204
+ vec2 samplerUV = vec2(0.0, 0.0);
205
+ float d = float(i * 4.0) / boneTextureSize.x;//4
206
+ samplerUV.y = float(floor(d)) / boneTextureSize.y;//5
207
+ samplerUV.x = fract(d);
208
+
209
+ vec4 v1 = uintBitsToFloat(texture( boneTexture, samplerUV ));
210
+ vec4 v2 = uintBitsToFloat(texture( boneTexture, vec2(samplerUV.x + 1.0 / boneTextureSize.x, samplerUV.y)));
211
+ vec4 v3 = uintBitsToFloat(texture( boneTexture, vec2(samplerUV.x + 2.0 / boneTextureSize.x, samplerUV.y) ));
212
+ vec4 v4 = uintBitsToFloat(texture( boneTexture, vec2(samplerUV.x + 3.0 / boneTextureSize.x, samplerUV.y)));
213
+
214
+ return mat4( v1, v2, v3, v4 );
215
+ }
216
+
217
+ void main () {
218
+
219
+ uint oddOffset = splatIndex & uint(0x00000001);
220
+ uint doubleOddOffset = oddOffset * uint(2);
221
+ bool isEven = oddOffset == uint(0);
222
+ uint nearestEvenIndex = splatIndex - oddOffset;
223
+ float fOddOffset = float(oddOffset);
224
+
225
+ uvec4 sampledCenterColor = texture(centersColorsTexture, getDataUV(1, 0, centersColorsTextureSize));
226
+ // vec3 splatCenter = uintBitsToFloat(uvec3(sampledCenterColor.gba));
227
+
228
+ uvec3 sampledCenter = texture(centersColorsTexture, getDataUV(1, 0, centersColorsTextureSize)).gba;
229
+ vec3 splatCenter = uintBitsToFloat(uvec3(sampledCenter));
230
+
231
+ vec2 flameTextureUV = getBSFlameDataUV(bsCount, 1, 0, flameModelTextureSize);
232
+ uvec3 sampledflamePos = texture(flameModelTexture, flameTextureUV).rgb;
233
+ // splatCenter += uintBitsToFloat(uvec3(sampledflamePos.rgb));
234
+
235
+ for(int i = 0; i < bsCount; ++i) {
236
+ vec2 flameBSTextureUV = getBSFlameDataUV(i, 1, 0, flameModelTextureSize);
237
+ uvec3 sampledBSPos = texture(flameModelTexture, flameBSTextureUV).rgb;
238
+
239
+ vec2 samplerUV = vec2(0.0, 0.0);
240
+ float d = float(i / 4 + 5 * 4) / boneTextureSize.x;//4
241
+ samplerUV.y = float(floor(d)) / boneTextureSize.y;//32
242
+ samplerUV.x = fract(d);
243
+
244
+ vec4 bsWeight = uintBitsToFloat(texture(boneTexture, samplerUV));
245
+ float weight = bsWeight.r;
246
+ if(i % 4 == 1) {
247
+ weight = bsWeight.g;
248
+ }
249
+ if(i % 4 == 2) {
250
+ weight = bsWeight.b;
251
+ }
252
+ if(i % 4 == 3) {
253
+ weight = bsWeight.a;
254
+ }
255
+
256
+ splatCenter = splatCenter + weight * uintBitsToFloat(sampledBSPos);
257
+ }
258
+
259
+
260
+ #ifdef USE_SKINNING
261
+ mat4 boneMatX = getBoneMatrix0( skinIndex.x );
262
+ mat4 boneMatY = getBoneMatrix0( skinIndex.y );
263
+ mat4 boneMatZ = getBoneMatrix0( skinIndex.z );
264
+ mat4 boneMatW = getBoneMatrix0( skinIndex.w );
265
+ #endif
266
+ #ifdef USE_SKINNING
267
+ mat4 skinMatrix = mat4( 0.0 );
268
+ skinMatrix += skinWeight.x * boneMatX;
269
+ skinMatrix += skinWeight.y * boneMatY;
270
+ skinMatrix += skinWeight.z * boneMatZ;
271
+ skinMatrix += skinWeight.w * boneMatW;
272
+ // skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;
273
+ #endif
274
+ vec3 transformed = vec3(splatCenter.xyz);
275
+ #ifdef USE_SKINNING
276
+ // vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );
277
+ vec4 skinVertex = vec4( transformed, 1.0 );
278
+
279
+ vec4 skinned = vec4( 0.0 );
280
+ // There is an offset between the Gaussian point and the mesh vertex,
281
+ // which will cause defects in the skeletal animation driving the Gaussian point.
282
+ //In order to circumvent this problem, only the head bone(index is 110 currently) is used to drive
283
+
284
+ if (headBoneIndex >= 0.0)
285
+ {
286
+ mat4 boneMat = getBoneMatrix0( headBoneIndex );
287
+ skinned += boneMat * skinVertex * 1.0;
288
+ }
289
+
290
+ // skinned += boneMatX * skinVertex * skinWeight.x;
291
+ // skinned += boneMatY * skinVertex * skinWeight.y;
292
+ // skinned += boneMatZ * skinVertex * skinWeight.z;
293
+ // skinned += boneMatW * skinVertex * skinWeight.w;
294
+
295
+ // transformed = ( bindMatrixInverse * skinned ).xyz;
296
+ transformed = skinned.xyz;
297
+
298
+ #endif
299
+ splatCenter = transformed.xyz;
300
+
301
+ #ifdef USE_FLAME
302
+ mat4 boneMatX = getBoneMatrix( 0.0 );
303
+ mat4 boneMatY = getBoneMatrix( 1.0 );
304
+ mat4 boneMatZ = getBoneMatrix( 2.0 );
305
+ mat4 boneMatW = getBoneMatrix( 3.0 );
306
+ mat4 boneMat0 = getBoneMatrix( 4.0 );
307
+
308
+ vec2 boneWeightUV0 = getBoneWeightUV(2, 0, boneWeightTextureSize);
309
+ vec2 boneWeightUV1 = getBoneWeightUV(2, 1, boneWeightTextureSize);
310
+
311
+ uvec4 sampledBoneMatrixValue = texture(boneWeightTexture, boneWeightUV0);
312
+ uvec4 sampledBoneMatrixValue0 = texture(boneWeightTexture, boneWeightUV1);
313
+
314
+ vec4 boneMatrixValue = uintBitsToFloat(sampledBoneMatrixValue);
315
+ vec4 boneMatrixValue0 = uintBitsToFloat(sampledBoneMatrixValue0);
316
+
317
+ vec4 skinVertex = vec4( splatCenter, 1.0 );
318
+ vec4 skinned = vec4( 0.0 );
319
+ float minWeight = min(boneMatrixValue.x,min(boneMatrixValue.y, min(boneMatrixValue.z, min(boneMatrixValue.w, boneMatrixValue0.x))));
320
+
321
+ if(boneMatrixValue.x > 0.0 && boneMatrixValue.x > minWeight)
322
+ skinned += boneMatX * skinVertex * boneMatrixValue.x;
323
+
324
+ if(boneMatrixValue.y > 0.0 && boneMatrixValue.y > minWeight)
325
+ skinned += boneMatY * skinVertex * boneMatrixValue.y;
326
+
327
+ if(boneMatrixValue.z > 0.0 && boneMatrixValue.z > minWeight)
328
+ skinned += boneMatZ * skinVertex * boneMatrixValue.z;
329
+
330
+ if(boneMatrixValue.w > 0.0 && boneMatrixValue.w > minWeight)
331
+ skinned += boneMatW * skinVertex * boneMatrixValue.w;
332
+
333
+ if(boneMatrixValue0.x > 0.0 && boneMatrixValue0.x > minWeight)
334
+ skinned += boneMat0 * skinVertex * boneMatrixValue0.x;
335
+
336
+ splatCenter = skinned.xyz;
337
+ #endif
338
+
339
+ uint sceneIndex = uint(0);
340
+ if (sceneCount > 1) {
341
+ sceneIndex = texture(sceneIndexesTexture, getDataUV(1, 0, sceneIndexesTextureSize)).r;
342
+ }
343
+ `;
344
+
345
+ if (enableOptionalEffects) {
346
+ vertexShaderSource += `
347
+ float splatOpacityFromScene = sceneOpacity[sceneIndex];
348
+ int sceneVisible = sceneVisibility[sceneIndex];
349
+ if (splatOpacityFromScene <= 0.01 || sceneVisible == 0) {
350
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
351
+ return;
352
+ }
353
+ `;
354
+ }
355
+
356
+ if (dynamicMode) {
357
+ vertexShaderSource += `
358
+ mat4 transform = transforms[sceneIndex];
359
+ mat4 transformModelViewMatrix = viewMatrix * transform;
360
+ #ifdef USE_SKINNING
361
+ transformModelViewMatrix = transformModelViewMatrix * skinMatrix;
362
+ #endif
363
+ `;
364
+ } else {
365
+ vertexShaderSource += `mat4 transformModelViewMatrix = modelViewMatrix;`;
366
+ }
367
+
368
+ vertexShaderSource += `
369
+ float sh8BitCompressionRangeMinForScene = sphericalHarmonics8BitCompressionRangeMin[sceneIndex];
370
+ float sh8BitCompressionRangeMaxForScene = sphericalHarmonics8BitCompressionRangeMax[sceneIndex];
371
+ float sh8BitCompressionRangeForScene = sh8BitCompressionRangeMaxForScene - sh8BitCompressionRangeMinForScene;
372
+ float sh8BitCompressionHalfRangeForScene = sh8BitCompressionRangeForScene / 2.0;
373
+ vec3 vec8BitSHShift = vec3(sh8BitCompressionRangeMinForScene);
374
+
375
+ vec4 viewCenter = transformModelViewMatrix * vec4(splatCenter, 1.0);
376
+
377
+ vec4 clipCenter = projectionMatrix * viewCenter;
378
+
379
+ float clip = 1.2 * clipCenter.w;
380
+ if (clipCenter.z < -clip || clipCenter.x < -clip || clipCenter.x > clip || clipCenter.y < -clip || clipCenter.y > clip) {
381
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
382
+ return;
383
+ }
384
+
385
+ vec3 ndcCenter = clipCenter.xyz / clipCenter.w;
386
+
387
+ vPosition = position.xy;
388
+ vSplatIndex = vec2(splatIndex, splatIndex);
389
+
390
+ vColor = uintToRGBAVec(sampledCenterColor.r);
391
+ `;
392
+
393
+ // Proceed to sampling and rendering 1st degree spherical harmonics
394
+ if (maxSphericalHarmonicsDegree >= 1) {
395
+
396
+ vertexShaderSource += `
397
+ if (sphericalHarmonicsDegree >= 1) {
398
+ `;
399
+
400
+ if (dynamicMode) {
401
+ vertexShaderSource += `
402
+ vec3 worldViewDir = normalize(splatCenter - vec3(inverse(transform) * vec4(cameraPosition, 1.0)));
403
+ `;
404
+ } else {
405
+ vertexShaderSource += `
406
+ vec3 worldViewDir = normalize(splatCenter - cameraPosition);
407
+ `;
408
+ }
409
+
410
+ vertexShaderSource += `
411
+ vec3 sh1;
412
+ vec3 sh2;
413
+ vec3 sh3;
414
+ `;
415
+
416
+ if (maxSphericalHarmonicsDegree >= 2) {
417
+ vertexShaderSource += `
418
+ vec3 sh4;
419
+ vec3 sh5;
420
+ vec3 sh6;
421
+ vec3 sh7;
422
+ vec3 sh8;
423
+ `;
424
+ }
425
+
426
+ // Determining how to sample spherical harmonics textures to get the coefficients for calculations for a given degree
427
+ // depends on how many total degrees (maxSphericalHarmonicsDegree) are present in the textures. This is because that
428
+ // number affects how they are packed in the textures, and therefore the offset & stride required to access them.
429
+
430
+ // Sample spherical harmonics textures with 1 degree worth of data for 1st degree calculations, and store in sh1, sh2, and sh3
431
+ if (maxSphericalHarmonicsDegree === 1) {
432
+ vertexShaderSource += `
433
+ if (sphericalHarmonicsMultiTextureMode == 0) {
434
+ vec2 shUV = getDataUVF(nearestEvenIndex, 2.5, doubleOddOffset, sphericalHarmonicsTextureSize);
435
+ vec4 sampledSH0123 = texture(sphericalHarmonicsTexture, shUV);
436
+ shUV = getDataUVF(nearestEvenIndex, 2.5, doubleOddOffset + uint(1), sphericalHarmonicsTextureSize);
437
+ vec4 sampledSH4567 = texture(sphericalHarmonicsTexture, shUV);
438
+ shUV = getDataUVF(nearestEvenIndex, 2.5, doubleOddOffset + uint(2), sphericalHarmonicsTextureSize);
439
+ vec4 sampledSH891011 = texture(sphericalHarmonicsTexture, shUV);
440
+ sh1 = vec3(sampledSH0123.rgb) * (1.0 - fOddOffset) + vec3(sampledSH0123.ba, sampledSH4567.r) * fOddOffset;
441
+ sh2 = vec3(sampledSH0123.a, sampledSH4567.rg) * (1.0 - fOddOffset) + vec3(sampledSH4567.gba) * fOddOffset;
442
+ sh3 = vec3(sampledSH4567.ba, sampledSH891011.r) * (1.0 - fOddOffset) + vec3(sampledSH891011.rgb) * fOddOffset;
443
+ } else {
444
+ vec2 sampledSH01R = texture(sphericalHarmonicsTextureR, getDataUV(2, 0, sphericalHarmonicsTextureSize)).rg;
445
+ vec2 sampledSH23R = texture(sphericalHarmonicsTextureR, getDataUV(2, 1, sphericalHarmonicsTextureSize)).rg;
446
+ vec2 sampledSH01G = texture(sphericalHarmonicsTextureG, getDataUV(2, 0, sphericalHarmonicsTextureSize)).rg;
447
+ vec2 sampledSH23G = texture(sphericalHarmonicsTextureG, getDataUV(2, 1, sphericalHarmonicsTextureSize)).rg;
448
+ vec2 sampledSH01B = texture(sphericalHarmonicsTextureB, getDataUV(2, 0, sphericalHarmonicsTextureSize)).rg;
449
+ vec2 sampledSH23B = texture(sphericalHarmonicsTextureB, getDataUV(2, 1, sphericalHarmonicsTextureSize)).rg;
450
+ sh1 = vec3(sampledSH01R.rg, sampledSH23R.r);
451
+ sh2 = vec3(sampledSH01G.rg, sampledSH23G.r);
452
+ sh3 = vec3(sampledSH01B.rg, sampledSH23B.r);
453
+ }
454
+ `;
455
+ // Sample spherical harmonics textures with 2 degrees worth of data for 1st degree calculations, and store in sh1, sh2, and sh3
456
+ } else if (maxSphericalHarmonicsDegree === 2) {
457
+ vertexShaderSource += `
458
+ vec4 sampledSH0123;
459
+ vec4 sampledSH4567;
460
+ vec4 sampledSH891011;
461
+
462
+ vec4 sampledSH0123R;
463
+ vec4 sampledSH0123G;
464
+ vec4 sampledSH0123B;
465
+
466
+ if (sphericalHarmonicsMultiTextureMode == 0) {
467
+ sampledSH0123 = texture(sphericalHarmonicsTexture, getDataUV(6, 0, sphericalHarmonicsTextureSize));
468
+ sampledSH4567 = texture(sphericalHarmonicsTexture, getDataUV(6, 1, sphericalHarmonicsTextureSize));
469
+ sampledSH891011 = texture(sphericalHarmonicsTexture, getDataUV(6, 2, sphericalHarmonicsTextureSize));
470
+ sh1 = sampledSH0123.rgb;
471
+ sh2 = vec3(sampledSH0123.a, sampledSH4567.rg);
472
+ sh3 = vec3(sampledSH4567.ba, sampledSH891011.r);
473
+ } else {
474
+ sampledSH0123R = texture(sphericalHarmonicsTextureR, getDataUV(2, 0, sphericalHarmonicsTextureSize));
475
+ sampledSH0123G = texture(sphericalHarmonicsTextureG, getDataUV(2, 0, sphericalHarmonicsTextureSize));
476
+ sampledSH0123B = texture(sphericalHarmonicsTextureB, getDataUV(2, 0, sphericalHarmonicsTextureSize));
477
+ sh1 = vec3(sampledSH0123R.rgb);
478
+ sh2 = vec3(sampledSH0123G.rgb);
479
+ sh3 = vec3(sampledSH0123B.rgb);
480
+ }
481
+ `;
482
+ }
483
+
484
+ // Perform 1st degree spherical harmonics calculations
485
+ vertexShaderSource += `
486
+ if (sphericalHarmonics8BitMode == 1) {
487
+ sh1 = sh1 * sh8BitCompressionRangeForScene + vec8BitSHShift;
488
+ sh2 = sh2 * sh8BitCompressionRangeForScene + vec8BitSHShift;
489
+ sh3 = sh3 * sh8BitCompressionRangeForScene + vec8BitSHShift;
490
+ }
491
+ float x = worldViewDir.x;
492
+ float y = worldViewDir.y;
493
+ float z = worldViewDir.z;
494
+ vColor.rgb += SH_C1 * (-sh1 * y + sh2 * z - sh3 * x);
495
+ `;
496
+
497
+ // Proceed to sampling and rendering 2nd degree spherical harmonics
498
+ if (maxSphericalHarmonicsDegree >= 2) {
499
+
500
+ vertexShaderSource += `
501
+ if (sphericalHarmonicsDegree >= 2) {
502
+ float xx = x * x;
503
+ float yy = y * y;
504
+ float zz = z * z;
505
+ float xy = x * y;
506
+ float yz = y * z;
507
+ float xz = x * z;
508
+ `;
509
+
510
+ // Sample spherical harmonics textures with 2 degrees worth of data for 2nd degree calculations,
511
+ // and store in sh4, sh5, sh6, sh7, and sh8
512
+ if (maxSphericalHarmonicsDegree === 2) {
513
+ vertexShaderSource += `
514
+ if (sphericalHarmonicsMultiTextureMode == 0) {
515
+ vec4 sampledSH12131415 = texture(sphericalHarmonicsTexture, getDataUV(6, 3, sphericalHarmonicsTextureSize));
516
+ vec4 sampledSH16171819 = texture(sphericalHarmonicsTexture, getDataUV(6, 4, sphericalHarmonicsTextureSize));
517
+ vec4 sampledSH20212223 = texture(sphericalHarmonicsTexture, getDataUV(6, 5, sphericalHarmonicsTextureSize));
518
+ sh4 = sampledSH891011.gba;
519
+ sh5 = sampledSH12131415.rgb;
520
+ sh6 = vec3(sampledSH12131415.a, sampledSH16171819.rg);
521
+ sh7 = vec3(sampledSH16171819.ba, sampledSH20212223.r);
522
+ sh8 = sampledSH20212223.gba;
523
+ } else {
524
+ vec4 sampledSH4567R = texture(sphericalHarmonicsTextureR, getDataUV(2, 1, sphericalHarmonicsTextureSize));
525
+ vec4 sampledSH4567G = texture(sphericalHarmonicsTextureG, getDataUV(2, 1, sphericalHarmonicsTextureSize));
526
+ vec4 sampledSH4567B = texture(sphericalHarmonicsTextureB, getDataUV(2, 1, sphericalHarmonicsTextureSize));
527
+ sh4 = vec3(sampledSH0123R.a, sampledSH4567R.rg);
528
+ sh5 = vec3(sampledSH4567R.ba, sampledSH0123G.a);
529
+ sh6 = vec3(sampledSH4567G.rgb);
530
+ sh7 = vec3(sampledSH4567G.a, sampledSH0123B.a, sampledSH4567B.r);
531
+ sh8 = vec3(sampledSH4567B.gba);
532
+ }
533
+ `;
534
+ }
535
+
536
+ // Perform 2nd degree spherical harmonics calculations
537
+ vertexShaderSource += `
538
+ if (sphericalHarmonics8BitMode == 1) {
539
+ sh4 = sh4 * sh8BitCompressionRangeForScene + vec8BitSHShift;
540
+ sh5 = sh5 * sh8BitCompressionRangeForScene + vec8BitSHShift;
541
+ sh6 = sh6 * sh8BitCompressionRangeForScene + vec8BitSHShift;
542
+ sh7 = sh7 * sh8BitCompressionRangeForScene + vec8BitSHShift;
543
+ sh8 = sh8 * sh8BitCompressionRangeForScene + vec8BitSHShift;
544
+ }
545
+
546
+ vColor.rgb +=
547
+ (SH_C2[0] * xy) * sh4 +
548
+ (SH_C2[1] * yz) * sh5 +
549
+ (SH_C2[2] * (2.0 * zz - xx - yy)) * sh6 +
550
+ (SH_C2[3] * xz) * sh7 +
551
+ (SH_C2[4] * (xx - yy)) * sh8;
552
+ }
553
+ `;
554
+ }
555
+
556
+ vertexShaderSource += `
557
+
558
+ vColor.rgb = clamp(vColor.rgb, vec3(0.), vec3(1.));
559
+
560
+ }
561
+
562
+ `;
563
+ }
564
+
565
+ return vertexShaderSource;
566
+ }
567
+
568
+ static getVertexShaderFadeIn() {
569
+ return `
570
+ if (fadeInComplete == 0) {
571
+ float opacityAdjust = 1.0;
572
+ float centerDist = length(splatCenter - sceneCenter);
573
+ float renderTime = max(currentTime - firstRenderTime, 0.0);
574
+
575
+ float fadeDistance = 0.75;
576
+ float distanceLoadFadeInFactor = step(visibleRegionFadeStartRadius, centerDist);
577
+ distanceLoadFadeInFactor = (1.0 - distanceLoadFadeInFactor) +
578
+ (1.0 - clamp((centerDist - visibleRegionFadeStartRadius) / fadeDistance, 0.0, 1.0)) *
579
+ distanceLoadFadeInFactor;
580
+ opacityAdjust *= distanceLoadFadeInFactor;
581
+ vColor.a *= opacityAdjust;
582
+ }
583
+ `;
584
+ }
585
+
586
+ static getUniforms(dynamicMode = false, enableOptionalEffects = false, maxSphericalHarmonicsDegree = 0,
587
+ splatScale = 1.0, pointCloudModeEnabled = false) {
588
+
589
+ const uniforms = {
590
+ 'sceneCenter': {
591
+ 'type': 'v3',
592
+ 'value': new Vector3()
593
+ },
594
+ 'fadeInComplete': {
595
+ 'type': 'i',
596
+ 'value': 0
597
+ },
598
+ 'orthographicMode': {
599
+ 'type': 'i',
600
+ 'value': 0
601
+ },
602
+ 'visibleRegionFadeStartRadius': {
603
+ 'type': 'f',
604
+ 'value': 0.0
605
+ },
606
+ 'visibleRegionRadius': {
607
+ 'type': 'f',
608
+ 'value': 0.0
609
+ },
610
+ 'bindMatrix': {
611
+ 'type': 'm4',
612
+ 'value': new Matrix4()
613
+ },
614
+ 'bindMatrixInverse': {
615
+ 'type': 'm4',
616
+ 'value': new Matrix4()
617
+ },
618
+ 'currentTime': {
619
+ 'type': 'f',
620
+ 'value': 0.0
621
+ },
622
+ 'firstRenderTime': {
623
+ 'type': 'f',
624
+ 'value': 0.0
625
+ },
626
+ 'centersColorsTexture': {
627
+ 'type': 't',
628
+ 'value': null
629
+ },
630
+ 'flameModelTexture': {
631
+ 'type': 't',
632
+ 'value': null
633
+ },
634
+ 'boneTexture': {
635
+ 'type': 't',
636
+ 'value': null
637
+ },
638
+ 'boneTexture0': {
639
+ 'type': 't',
640
+ 'value': null
641
+ },
642
+ 'boneWeightTexture': {
643
+ 'type': 't',
644
+ 'value': null
645
+ },
646
+ 'sphericalHarmonicsTexture': {
647
+ 'type': 't',
648
+ 'value': null
649
+ },
650
+ 'sphericalHarmonicsTextureR': {
651
+ 'type': 't',
652
+ 'value': null
653
+ },
654
+ 'sphericalHarmonicsTextureG': {
655
+ 'type': 't',
656
+ 'value': null
657
+ },
658
+ 'sphericalHarmonicsTextureB': {
659
+ 'type': 't',
660
+ 'value': null
661
+ },
662
+ 'sphericalHarmonics8BitCompressionRangeMin': {
663
+ 'type': 'f',
664
+ 'value': []
665
+ },
666
+ 'sphericalHarmonics8BitCompressionRangeMax': {
667
+ 'type': 'f',
668
+ 'value': []
669
+ },
670
+ 'focal': {
671
+ 'type': 'v2',
672
+ 'value': new Vector2()
673
+ },
674
+ 'orthoZoom': {
675
+ 'type': 'f',
676
+ 'value': 1.0
677
+ },
678
+ 'inverseFocalAdjustment': {
679
+ 'type': 'f',
680
+ 'value': 1.0
681
+ },
682
+ 'viewport': {
683
+ 'type': 'v2',
684
+ 'value': new Vector2()
685
+ },
686
+ 'basisViewport': {
687
+ 'type': 'v2',
688
+ 'value': new Vector2()
689
+ },
690
+ 'debugColor': {
691
+ 'type': 'v3',
692
+ 'value': new Color()
693
+ },
694
+ 'centersColorsTextureSize': {
695
+ 'type': 'v2',
696
+ 'value': new Vector2(1024, 1024)
697
+ },
698
+ 'flameModelTextureSize': {
699
+ 'type': 'v2',
700
+ 'value': new Vector2(4096, 2048)
701
+ },
702
+ 'boneTextureSize': {
703
+ 'type': 'v2',
704
+ 'value': new Vector2(4, 32)
705
+ },
706
+ 'boneWeightTextureSize': {
707
+ 'type': 'v2',
708
+ 'value': new Vector2(512, 512)
709
+ },
710
+
711
+ 'sphericalHarmonicsDegree': {
712
+ 'type': 'i',
713
+ 'value': maxSphericalHarmonicsDegree
714
+ },
715
+ 'sphericalHarmonicsTextureSize': {
716
+ 'type': 'v2',
717
+ 'value': new Vector2(1024, 1024)
718
+ },
719
+ 'sphericalHarmonics8BitMode': {
720
+ 'type': 'i',
721
+ 'value': 0
722
+ },
723
+ 'sphericalHarmonicsMultiTextureMode': {
724
+ 'type': 'i',
725
+ 'value': 0
726
+ },
727
+ 'splatScale': {
728
+ 'type': 'f',
729
+ 'value': splatScale
730
+ },
731
+ 'pointCloudModeEnabled': {
732
+ 'type': 'i',
733
+ 'value': pointCloudModeEnabled ? 1 : 0
734
+ },
735
+ 'sceneIndexesTexture': {
736
+ 'type': 't',
737
+ 'value': null
738
+ },
739
+ 'sceneIndexesTextureSize': {
740
+ 'type': 'v2',
741
+ 'value': new Vector2(1024, 1024)
742
+ },
743
+ 'sceneCount': {
744
+ 'type': 'i',
745
+ 'value': 1
746
+ },
747
+ 'gaussianSplatCount': {
748
+ 'type': 'i',
749
+ 'value': 1
750
+ },
751
+ 'bsCount': {
752
+ 'type': 'i',
753
+ 'value': 1
754
+ },
755
+ 'headBoneIndex': {
756
+ 'type': 'f',
757
+ 'value': -1.0
758
+ }
759
+ };
760
+ for (let i = 0; i < Constants.MaxScenes; i++) {
761
+ uniforms.sphericalHarmonics8BitCompressionRangeMin.value.push(-Constants.SphericalHarmonics8BitCompressionRange / 2.0);
762
+ uniforms.sphericalHarmonics8BitCompressionRangeMax.value.push(Constants.SphericalHarmonics8BitCompressionRange / 2.0);
763
+ }
764
+
765
+ if (enableOptionalEffects) {
766
+ const sceneOpacity = [];
767
+ for (let i = 0; i < Constants.MaxScenes; i++) {
768
+ sceneOpacity.push(1.0);
769
+ }
770
+ uniforms['sceneOpacity'] ={
771
+ 'type': 'f',
772
+ 'value': sceneOpacity
773
+ };
774
+
775
+ const sceneVisibility = [];
776
+ for (let i = 0; i < Constants.MaxScenes; i++) {
777
+ sceneVisibility.push(1);
778
+ }
779
+ uniforms['sceneVisibility'] ={
780
+ 'type': 'i',
781
+ 'value': sceneVisibility
782
+ };
783
+ }
784
+
785
+ if (dynamicMode) {
786
+ const transformMatrices = [];
787
+ for (let i = 0; i < Constants.MaxScenes; i++) {
788
+ transformMatrices.push(new Matrix4());
789
+ }
790
+ uniforms['transforms'] = {
791
+ 'type': 'mat4',
792
+ 'value': transformMatrices
793
+ };
794
+ }
795
+
796
+ return uniforms;
797
+ }
798
+
799
+ }
800
+
801
+ class SplatMaterial3D {
802
+
803
+ /**
804
+ * Build the Three.js material that is used to render the splats.
805
+ * @param {number} dynamicMode If true, it means the scene geometry represented by this splat mesh is not stationary or
806
+ * that the splat count might change
807
+ * @param {boolean} enableOptionalEffects When true, allows for usage of extra properties and attributes in the shader for effects
808
+ * such as opacity adjustment. Default is false for performance reasons.
809
+ * @param {boolean} antialiased If true, calculate compensation factor to deal with gaussians being rendered at a significantly
810
+ * different resolution than that of their training
811
+ * @param {number} maxScreenSpaceSplatSize The maximum clip space splat size
812
+ * @param {number} splatScale Value by which all splats are scaled in screen-space (default is 1.0)
813
+ * @param {number} pointCloudModeEnabled Render all splats as screen-space circles
814
+ * @param {number} maxSphericalHarmonicsDegree Degree of spherical harmonics to utilize in rendering splats
815
+ * @return {THREE.ShaderMaterial}
816
+ */
817
+ static build(dynamicMode = false, enableOptionalEffects = false, antialiased = false, maxScreenSpaceSplatSize = 2048,
818
+ splatScale = 1.0, pointCloudModeEnabled = false, maxSphericalHarmonicsDegree = 0, kernel2DSize = 0.3, useFlame = true) {
819
+
820
+ const customVertexVars = `
821
+ uniform vec2 covariancesTextureSize;
822
+ uniform highp sampler2D covariancesTexture;
823
+ uniform highp usampler2D covariancesTextureHalfFloat;
824
+ uniform int covariancesAreHalfFloat;
825
+
826
+ void fromCovarianceHalfFloatV4(uvec4 val, out vec4 first, out vec4 second) {
827
+ vec2 r = unpackHalf2x16(val.r);
828
+ vec2 g = unpackHalf2x16(val.g);
829
+ vec2 b = unpackHalf2x16(val.b);
830
+
831
+ first = vec4(r.x, r.y, g.x, g.y);
832
+ second = vec4(b.x, b.y, 0.0, 0.0);
833
+ }
834
+ `;
835
+
836
+ let vertexShaderSource = SplatMaterial.buildVertexShaderBase(dynamicMode, enableOptionalEffects,
837
+ maxSphericalHarmonicsDegree, customVertexVars, useFlame);
838
+ vertexShaderSource += SplatMaterial3D.buildVertexShaderProjection(antialiased, enableOptionalEffects,
839
+ maxScreenSpaceSplatSize, kernel2DSize);
840
+ const fragmentShaderSource = SplatMaterial3D.buildFragmentShader();
841
+
842
+ const uniforms = SplatMaterial.getUniforms(dynamicMode, enableOptionalEffects,
843
+ maxSphericalHarmonicsDegree, splatScale, pointCloudModeEnabled);
844
+
845
+ uniforms['covariancesTextureSize'] = {
846
+ 'type': 'v2',
847
+ 'value': new Vector2(1024, 1024)
848
+ };
849
+ uniforms['covariancesTexture'] = {
850
+ 'type': 't',
851
+ 'value': null
852
+ };
853
+ uniforms['covariancesTextureHalfFloat'] = {
854
+ 'type': 't',
855
+ 'value': null
856
+ };
857
+ uniforms['covariancesAreHalfFloat'] = {
858
+ 'type': 'i',
859
+ 'value': 0
860
+ };
861
+
862
+ const material = new ShaderMaterial({
863
+ uniforms: uniforms,
864
+ vertexShader: vertexShaderSource,
865
+ fragmentShader: fragmentShaderSource,
866
+ transparent: true,
867
+ alphaTest: 1.0,
868
+ blending: NormalBlending,
869
+ depthTest: true,
870
+ depthWrite: false,
871
+ side: DoubleSide
872
+ });
873
+
874
+ return material;
875
+ }
876
+
877
+ static buildVertexShaderProjection(antialiased, enableOptionalEffects, maxScreenSpaceSplatSize, kernel2DSize) {
878
+ let vertexShaderSource = `
879
+
880
+ vec4 sampledCovarianceA;
881
+ vec4 sampledCovarianceB;
882
+ vec3 cov3D_M11_M12_M13;
883
+ vec3 cov3D_M22_M23_M33;
884
+ if (covariancesAreHalfFloat == 0) {
885
+ sampledCovarianceA = texture(covariancesTexture, getDataUVF(nearestEvenIndex, 1.5, oddOffset,
886
+ covariancesTextureSize));
887
+ sampledCovarianceB = texture(covariancesTexture, getDataUVF(nearestEvenIndex, 1.5, oddOffset + uint(1),
888
+ covariancesTextureSize));
889
+
890
+ cov3D_M11_M12_M13 = vec3(sampledCovarianceA.rgb) * (1.0 - fOddOffset) +
891
+ vec3(sampledCovarianceA.ba, sampledCovarianceB.r) * fOddOffset;
892
+ cov3D_M22_M23_M33 = vec3(sampledCovarianceA.a, sampledCovarianceB.rg) * (1.0 - fOddOffset) +
893
+ vec3(sampledCovarianceB.gba) * fOddOffset;
894
+ } else {
895
+ uvec4 sampledCovarianceU = texture(covariancesTextureHalfFloat, getDataUV(1, 0, covariancesTextureSize));
896
+ fromCovarianceHalfFloatV4(sampledCovarianceU, sampledCovarianceA, sampledCovarianceB);
897
+ cov3D_M11_M12_M13 = sampledCovarianceA.rgb;
898
+ cov3D_M22_M23_M33 = vec3(sampledCovarianceA.a, sampledCovarianceB.rg);
899
+ }
900
+
901
+ // Construct the 3D covariance matrix
902
+ mat3 Vrk = mat3(
903
+ cov3D_M11_M12_M13.x, cov3D_M11_M12_M13.y, cov3D_M11_M12_M13.z,
904
+ cov3D_M11_M12_M13.y, cov3D_M22_M23_M33.x, cov3D_M22_M23_M33.y,
905
+ cov3D_M11_M12_M13.z, cov3D_M22_M23_M33.y, cov3D_M22_M23_M33.z
906
+ );
907
+
908
+ mat3 J;
909
+ if (orthographicMode == 1) {
910
+ // Since the projection is linear, we don't need an approximation
911
+ J = transpose(mat3(orthoZoom, 0.0, 0.0,
912
+ 0.0, orthoZoom, 0.0,
913
+ 0.0, 0.0, 0.0));
914
+ } else {
915
+ // Construct the Jacobian of the affine approximation of the projection matrix. It will be used to transform the
916
+ // 3D covariance matrix instead of using the actual projection matrix because that transformation would
917
+ // require a non-linear component (perspective division) which would yield a non-gaussian result.
918
+ float s = 1.0 / (viewCenter.z * viewCenter.z);
919
+ J = mat3(
920
+ focal.x / viewCenter.z, 0., -(focal.x * viewCenter.x) * s,
921
+ 0., focal.y / viewCenter.z, -(focal.y * viewCenter.y) * s,
922
+ 0., 0., 0.
923
+ );
924
+ }
925
+
926
+ // Concatenate the projection approximation with the model-view transformation
927
+ mat3 W = transpose(mat3(transformModelViewMatrix));
928
+ mat3 T = W * J;
929
+
930
+ // Transform the 3D covariance matrix (Vrk) to compute the 2D covariance matrix
931
+ mat3 cov2Dm = transpose(T) * Vrk * T;
932
+ `;
933
+
934
+ if (antialiased) {
935
+ vertexShaderSource += `
936
+ float detOrig = cov2Dm[0][0] * cov2Dm[1][1] - cov2Dm[0][1] * cov2Dm[0][1];
937
+ cov2Dm[0][0] += ${kernel2DSize};
938
+ cov2Dm[1][1] += ${kernel2DSize};
939
+ float detBlur = cov2Dm[0][0] * cov2Dm[1][1] - cov2Dm[0][1] * cov2Dm[0][1];
940
+ vColor.a *= sqrt(max(detOrig / detBlur, 0.0));
941
+ if (vColor.a < minAlpha) return;
942
+ `;
943
+ } else {
944
+ vertexShaderSource += `
945
+ cov2Dm[0][0] += ${kernel2DSize};
946
+ cov2Dm[1][1] += ${kernel2DSize};
947
+ `;
948
+ }
949
+
950
+ vertexShaderSource += `
951
+
952
+ // We are interested in the upper-left 2x2 portion of the projected 3D covariance matrix because
953
+ // we only care about the X and Y values. We want the X-diagonal, cov2Dm[0][0],
954
+ // the Y-diagonal, cov2Dm[1][1], and the correlation between the two cov2Dm[0][1]. We don't
955
+ // need cov2Dm[1][0] because it is a symetric matrix.
956
+ vec3 cov2Dv = vec3(cov2Dm[0][0], cov2Dm[0][1], cov2Dm[1][1]);
957
+
958
+ // We now need to solve for the eigen-values and eigen vectors of the 2D covariance matrix
959
+ // so that we can determine the 2D basis for the splat. This is done using the method described
960
+ // here: https://people.math.harvard.edu/~knill/teaching/math21b2004/exhibits/2dmatrices/index.html
961
+ // After calculating the eigen-values and eigen-vectors, we calculate the basis for rendering the splat
962
+ // by normalizing the eigen-vectors and then multiplying them by (sqrt(8) * sqrt(eigen-value)), which is
963
+ // equal to scaling them by sqrt(8) standard deviations.
964
+ //
965
+ // This is a different approach than in the original work at INRIA. In that work they compute the
966
+ // max extents of the projected splat in screen space to form a screen-space aligned bounding rectangle
967
+ // which forms the geometry that is actually rasterized. The dimensions of that bounding box are 3.0
968
+ // times the square root of the maximum eigen-value, or 3 standard deviations. They then use the inverse
969
+ // 2D covariance matrix (called 'conic') in the CUDA rendering thread to determine fragment opacity by
970
+ // calculating the full gaussian: exp(-0.5 * (X - mean) * conic * (X - mean)) * splat opacity
971
+ float a = cov2Dv.x;
972
+ float d = cov2Dv.z;
973
+ float b = cov2Dv.y;
974
+ float D = a * d - b * b;
975
+ float trace = a + d;
976
+ float traceOver2 = 0.5 * trace;
977
+ float term2 = sqrt(max(0.1f, traceOver2 * traceOver2 - D));
978
+ float eigenValue1 = traceOver2 + term2;
979
+ float eigenValue2 = traceOver2 - term2;
980
+
981
+ if (pointCloudModeEnabled == 1) {
982
+ eigenValue1 = eigenValue2 = 0.2;
983
+ }
984
+
985
+ if (eigenValue2 <= 0.0) return;
986
+
987
+ vec2 eigenVector1 = normalize(vec2(b, eigenValue1 - a));
988
+ // since the eigen vectors are orthogonal, we derive the second one from the first
989
+ vec2 eigenVector2 = vec2(eigenVector1.y, -eigenVector1.x);
990
+
991
+ // We use sqrt(8) standard deviations instead of 3 to eliminate more of the splat with a very low opacity.
992
+ vec2 basisVector1 = eigenVector1 * splatScale * min(sqrt8 * sqrt(eigenValue1), ${parseInt(maxScreenSpaceSplatSize)}.0);
993
+ vec2 basisVector2 = eigenVector2 * splatScale * min(sqrt8 * sqrt(eigenValue2), ${parseInt(maxScreenSpaceSplatSize)}.0);
994
+ `;
995
+
996
+ if (enableOptionalEffects) {
997
+ vertexShaderSource += `
998
+ vColor.a *= splatOpacityFromScene;
999
+ `;
1000
+ }
1001
+
1002
+ vertexShaderSource += `
1003
+ vec2 ndcOffset = vec2(vPosition.x * basisVector1 + vPosition.y * basisVector2) *
1004
+ basisViewport * 2.0 * inverseFocalAdjustment;
1005
+
1006
+ vec4 quadPos = vec4(ndcCenter.xy + ndcOffset, ndcCenter.z, 1.0);
1007
+ gl_Position = quadPos;
1008
+
1009
+ // Scale the position data we send to the fragment shader
1010
+ vPosition *= sqrt8;
1011
+ `;
1012
+
1013
+ vertexShaderSource += SplatMaterial.getVertexShaderFadeIn();
1014
+ vertexShaderSource += `}`;
1015
+
1016
+ return vertexShaderSource;
1017
+ }
1018
+
1019
+ static buildFragmentShader() {
1020
+ let fragmentShaderSource = `
1021
+ precision highp float;
1022
+ #include <common>
1023
+
1024
+ uniform vec3 debugColor;
1025
+
1026
+ varying vec4 vColor;
1027
+ varying vec2 vUv;
1028
+ varying vec2 vPosition;
1029
+ varying vec2 vSplatIndex;
1030
+
1031
+ `;
1032
+
1033
+ fragmentShaderSource += `
1034
+ void main () {
1035
+ // Compute the positional squared distance from the center of the splat to the current fragment.
1036
+ float A = dot(vPosition, vPosition);
1037
+ // Since the positional data in vPosition has been scaled by sqrt(8), the squared result will be
1038
+ // scaled by a factor of 8. If the squared result is larger than 8, it means it is outside the ellipse
1039
+ // defined by the rectangle formed by vPosition. It also means it's farther
1040
+ // away than sqrt(8) standard deviations from the mean.
1041
+
1042
+ // if(vSplatIndex.x > 20000.0) discard;
1043
+ // if (A > 8.0) discard;
1044
+ vec3 color = vColor.rgb;
1045
+
1046
+ // Since the rendered splat is scaled by sqrt(8), the inverse covariance matrix that is part of
1047
+ // the gaussian formula becomes the identity matrix. We're then left with (X - mean) * (X - mean),
1048
+ // and since 'mean' is zero, we have X * X, which is the same as A:
1049
+ float opacity = exp( -0.5*A) * vColor.a;
1050
+ if(opacity < 1.0 / 255.0)
1051
+ discard;
1052
+
1053
+ // uint a = uint(255);
1054
+ // vec3 c = vec3(vSplatIndex.x / 256.0 / 256.0, float(uint(vSplatIndex.x / 256.0 )% a) / 256.0, float(uint(vSplatIndex.x)% a) / 256.0);
1055
+ // gl_FragColor = vec4(c, 1.0);
1056
+ gl_FragColor = vec4(color, opacity);
1057
+
1058
+
1059
+ }
1060
+ `;
1061
+
1062
+ return fragmentShaderSource;
1063
+ }
1064
+
1065
+ }