Loading...
Searching...
No Matches
GaussianSplatNode.hpp
1#pragma once
2
3#include <Gfx/Graph/Node.hpp>
4#include <Gfx/Graph/NodeRenderer.hpp>
5#include <Gfx/Graph/RenderList.hpp>
6#include <Gfx/Graph/Utils.hpp>
7
8#include <ossia/detail/pod_vector.hpp>
9
10// clang-format off
11#if defined(near)
12#undef near
13#undef far
14#endif
15// clang-format on
16
17namespace score::gfx
18{
19
41{
42public:
44 virtual ~GaussianSplatNode();
45
46 score::gfx::NodeRenderer* createRenderer(RenderList&) const noexcept override;
47 void process(Message&& msg) override;
48
49 int splatCount{};
50 float scaleFactor{1.0f};
51 bool enableSorting{true};
52 uint32_t shDegree{3}; // 0, 1, 2, or 3
53
54 // Model transform
55 ossia::vec3f modelPosition{0.f, 0.f, 0.f};
56 ossia::vec3f modelRotation{0.f, 0.f, 0.f}; // Euler angles in degrees (pitch, yaw, roll)
57 ossia::vec3f modelScale{1.f, 1.f, 1.f};
58
59 // Camera parameters
60 ossia::vec3f position{-1.f, -1.f, -1.f};
61 ossia::vec3f center{0.f, 0.f, 0.f};
62 float fov{90.f};
63 float near{0.001f};
64 float far{10000.f};
65};
66
75{
76public:
77 explicit GaussianSplatRenderer(const GaussianSplatNode& node);
79
80 void init(RenderList& renderer, QRhiResourceUpdateBatch& res) override;
81 void initState(RenderList& renderer, QRhiResourceUpdateBatch& res) override;
82 void addOutputPass(
83 RenderList& renderer, Edge& edge, QRhiResourceUpdateBatch& res) override;
84 void removeOutputPass(RenderList& renderer, Edge& edge) override;
85 bool hasOutputPassForEdge(Edge& edge) const override;
86 void update(RenderList& renderer, QRhiResourceUpdateBatch& res, Edge* edge) override;
87 void runInitialPasses(
88 RenderList&, QRhiCommandBuffer& commands, QRhiResourceUpdateBatch*& res,
89 Edge& edge) override;
90 void runRenderPass(RenderList&, QRhiCommandBuffer& cb, Edge& edge) override;
91 void releaseState(RenderList&) override;
92 void release(RenderList&) override;
93
94private:
95 void createPreprocessPipeline(RenderList& renderer);
96 void createRenderPipeline(RenderList& renderer);
97 void createSortPipelines(RenderList& renderer);
98
99 const GaussianSplatNode& m_node;
100
101 // Render pipeline resources
102 QRhiBuffer* m_uniformBuffer{};
103 QRhiBuffer* m_dummyStorageBuffer{}; // Small buffer for unused bindings
104 QRhiGraphicsPipeline* m_pipeline{};
105 QRhiShaderResourceBindings* m_bindings{};
106
107 // SH preprocessing compute resources
108 // Converts raw 256-byte splats → compact 64-byte rendering splats
109 QRhiBuffer* m_rawSplatBuffer{}; // Input: raw PLY data (256 bytes/splat)
110 QRhiBuffer* m_renderSplatBuffer{}; // Output: compact (64 bytes/splat)
111 QRhiBuffer* m_preprocessUniformBuffer{};
112 QRhiComputePipeline* m_preprocessPipeline{};
113 QRhiShaderResourceBindings* m_preprocessSrb{};
114
115 // Sorting compute resources
116 QRhiBuffer* m_sortKeysBuffer{}; // Depth keys (float -> uint for sorting)
117 QRhiBuffer* m_sortKeysAltBuffer{}; // Double buffer for key ping-pong
118 QRhiBuffer* m_sortIndicesBuffer{}; // Sorted indices
119 QRhiBuffer* m_sortIndicesAltBuffer{}; // Double buffer for index ping-pong
120 QRhiBuffer* m_histogramBuffer{}; // Histogram for radix sort
121 QRhiBuffer* m_sortUniformBuffer{}; // Depth key pass uniforms
122 QRhiBuffer* m_sortPassUniformBuffer{}; // Histogram/scatter/prefix uniforms
123 QRhiBuffer* m_prefixSumUniformBuffer{}; // Prefix sum uniforms
124
125 QRhiComputePipeline* m_depthKeyPipeline{};
126 QRhiComputePipeline* m_histogramPipeline{};
127 QRhiComputePipeline* m_prefixSumPipeline{};
128 QRhiComputePipeline* m_sortPipeline{};
129
130 QRhiShaderResourceBindings* m_depthKeySrb{};
131 QRhiShaderResourceBindings* m_histogramSrb{};
132 QRhiShaderResourceBindings* m_histogramSrbAlt{}; // For odd passes
133 QRhiShaderResourceBindings* m_prefixSumSrb{};
134 QRhiShaderResourceBindings* m_sortSrb{};
135 QRhiShaderResourceBindings* m_sortSrbAlt{}; // For ping-pong
136
137 ossia::small_vector<Sampler, 8> m_samplers;
138
139 int64_t m_lastSplatCount{0};
140 bool m_preprocessResourcesCreated{false};
141 bool m_sortResourcesCreated{false};
142
143 static constexpr int64_t MAX_SPLATS = 50000000;
144 static constexpr int SORT_WORKGROUP_SIZE = 256;
145 static constexpr int RADIX_BITS = 8;
146 static constexpr int NUM_BUCKETS = 256; // 2^RADIX_BITS
147};
148
149// Shader sources
150namespace GaussianSplatShaders
151{
152
153//=============================================================================
154// COMPUTE SHADER: SH PREPROCESSING (raw 256B → compact 64B per splat)
155//=============================================================================
156
167static constexpr auto preprocess_shader = R"_(#version 450
168layout(local_size_x = 256) in;
169
170// Raw splat: 64 floats = 256 bytes (matches PLY loader output)
171// [0..2] position (x,y,z)
172// [3..5] normal (nx,ny,nz) — unused
173// [6..8] SH DC (f_dc_0, f_dc_1, f_dc_2)
174// [9..53] SH rest (f_rest_0 .. f_rest_44)
175// [54] opacity (pre-sigmoid)
176// [55..57] scale (log-space)
177// [58..61] rotation (w,x,y,z)
178// [62..63] padding
179
180layout(std430, binding = 0) readonly buffer RawSplatBuffer {
181 float rawData[]; // 64 floats per splat
182};
183
184// Compact rendering splat: 16 floats = 64 bytes
185// vec4 position (xyz, 0)
186// vec4 scale (xyz, 0) — already exp'd
187// vec4 rotation (x,y,z,w) — normalized
188// vec4 color (r,g,b,a) — SH evaluated, alpha = sigmoid(opacity)
189
190struct RenderSplat {
191 vec4 position;
192 vec4 scale;
193 vec4 rotation;
194 vec4 color;
195};
196
197layout(std430, binding = 1) writeonly buffer RenderSplatBuffer {
198 RenderSplat renderSplats[];
199};
200
201layout(std140, binding = 2) uniform Params {
202 mat4 view;
203 vec3 camPos; // Camera position in world space
204 uint splatCount;
205 uint shDegree; // 0, 1, 2, or 3
206 float scaleMod;
207 uint _pad0;
208 uint _pad1;
209};
210
211// Spherical harmonics constants
212const float SH_C0 = 0.28209479177387814;
213
214const float SH_C1 = 0.4886025119029199;
215
216const float SH_C2[5] = float[5](
217 1.0925484305920792,
218 -1.0925484305920792,
219 0.31539156525252005,
220 -1.0925484305920792,
221 0.5462742152960396
222);
223
224const float SH_C3[7] = float[7](
225 -0.5900435899266435,
226 2.890611442640554,
227 -0.4570457994644658,
228 0.3731763325901154,
229 -0.4570457994644658,
230 1.445305721320277,
231 -0.5900435899266435
232);
233
234vec3 evaluateSH(uint base, vec3 dir) {
235 // Degree 0
236 vec3 result = SH_C0 * vec3(
237 rawData[base + 6],
238 rawData[base + 7],
239 rawData[base + 8]
240 );
241
242 if (shDegree < 1) {
243 return result + 0.5;
244 }
245
246 // Degree 1
247 float x = dir.x, y = dir.y, z = dir.z;
248
249 // f_rest layout: [0..14] = R channel rest, [15..29] = G, [30..44] = B
250 // But the INRIA convention interleaves: [0..2] = degree1 for R,G,B etc.
251 // Actually the standard layout is:
252 // f_rest[0..14]: coeffs 1..15 for channel 0 (R)
253 // f_rest[15..29]: coeffs 1..15 for channel 1 (G)
254 // f_rest[30..44]: coeffs 1..15 for channel 2 (B)
255
256 uint r = base + 9; // f_rest_0 start
257 // Degree 1: 3 coefficients per channel, interleaved as RGB triplets
258 // Coeff indices in f_rest: R=[0,1,2], G=[15,16,17], B=[30,31,32]
259 result += SH_C1 * (
260 - y * vec3(rawData[r+0], rawData[r+15], rawData[r+30])
261 + z * vec3(rawData[r+1], rawData[r+16], rawData[r+31])
262 - x * vec3(rawData[r+2], rawData[r+17], rawData[r+32])
263 );
264
265 if (shDegree < 2) {
266 return result + 0.5;
267 }
268
269 // Degree 2: 5 coefficients per channel
270 // R=[3..7], G=[18..22], B=[33..37]
271 float xx = x*x, yy = y*y, zz = z*z, xy = x*y, yz = y*z, xz = x*z;
272
273 result += SH_C2[0] * xy * vec3(rawData[r+3], rawData[r+18], rawData[r+33]);
274 result += SH_C2[1] * yz * vec3(rawData[r+4], rawData[r+19], rawData[r+34]);
275 result += SH_C2[2] * (2.*zz - xx - yy)
276 * vec3(rawData[r+5], rawData[r+20], rawData[r+35]);
277 result += SH_C2[3] * xz * vec3(rawData[r+6], rawData[r+21], rawData[r+36]);
278 result += SH_C2[4] * (xx - yy)* vec3(rawData[r+7], rawData[r+22], rawData[r+37]);
279
280 if (shDegree < 3) {
281 return result + 0.5;
282 }
283
284 // Degree 3: 7 coefficients per channel
285 // R=[8..14], G=[23..29], B=[38..44]
286 result += SH_C3[0] * y*(3.*xx - yy)
287 * vec3(rawData[r+8], rawData[r+23], rawData[r+38]);
288 result += SH_C3[1] * xy*z * vec3(rawData[r+9], rawData[r+24], rawData[r+39]);
289 result += SH_C3[2] * y*(4.*zz - xx - yy)
290 * vec3(rawData[r+10], rawData[r+25], rawData[r+40]);
291 result += SH_C3[3] * z*(2.*zz - 3.*xx - 3.*yy)
292 * vec3(rawData[r+11], rawData[r+26], rawData[r+41]);
293 result += SH_C3[4] * x*(4.*zz - xx - yy)
294 * vec3(rawData[r+12], rawData[r+27], rawData[r+42]);
295 result += SH_C3[5] * z*(xx - yy)
296 * vec3(rawData[r+13], rawData[r+28], rawData[r+43]);
297 result += SH_C3[6] * x*(xx - 3.*yy)
298 * vec3(rawData[r+14], rawData[r+29], rawData[r+44]);
299
300 return result + 0.5;
301}
302
303void main() {
304 uint idx = gl_GlobalInvocationID.x;
305 if (idx >= splatCount) return;
306
307 uint base = idx * 64; // 64 floats per raw splat
308
309 // Position
310 vec3 pos = vec3(rawData[base], rawData[base+1], rawData[base+2]);
311
312 // View direction for SH evaluation (world space, from camera towards splat)
313 // Must match the INRIA training convention: dir = pos - campos
314 vec3 dir = normalize(pos - camPos);
315
316 // Evaluate SH for view-dependent color
317 vec3 color = evaluateSH(base, dir);
318 color = clamp(color, 0.0, 1.0);
319
320 // Opacity: sigmoid(raw_opacity)
321 float rawOpacity = rawData[base + 54];
322 float alpha = 1.0 / (1.0 + exp(-rawOpacity));
323
324 // Scale: exp(log_scale) * scaleMod
325 vec3 scale = vec3(
326 exp(rawData[base + 55]),
327 exp(rawData[base + 56]),
328 exp(rawData[base + 57])
329 ) * scaleMod;
330
331 // Rotation: PLY stores (w,x,y,z), shader expects (x,y,z,w)
332 // Normalize quaternion
333 vec4 rawRot = vec4(
334 rawData[base + 58], // w
335 rawData[base + 59], // x
336 rawData[base + 60], // y
337 rawData[base + 61] // z
338 );
339 rawRot = normalize(rawRot);
340 vec4 rot = vec4(rawRot.y, rawRot.z, rawRot.w, rawRot.x); // xyzw
341
342 // Write compact rendering splat
343 renderSplats[idx].position = vec4(pos, 0.0);
344 renderSplats[idx].scale = vec4(scale, 0.0);
345 renderSplats[idx].rotation = rot;
346 renderSplats[idx].color = vec4(color, alpha);
347}
348)_";
349
350//=============================================================================
351// COMPUTE SHADERS FOR DEPTH SORTING
352//=============================================================================
353
358static constexpr auto depth_key_shader = R"_(#version 450
359layout(local_size_x = 256) in;
360
361struct RenderSplat {
362 vec4 position;
363 vec4 scale;
364 vec4 rotation;
365 vec4 color;
366};
367
368layout(std430, binding = 0) readonly buffer SplatBuffer {
369 RenderSplat splats[];
370};
371
372layout(std430, binding = 1) writeonly buffer KeyBuffer {
373 uint keys[];
374};
375
376layout(std430, binding = 2) writeonly buffer IndexBuffer {
377 uint indices[];
378};
379
380layout(std140, binding = 3) uniform Params {
381 mat4 view;
382 uint splatCount;
383 float nearPlane;
384 float farPlane;
385 uint _pad;
386};
387
388void main() {
389 uint idx = gl_GlobalInvocationID.x;
390 if (idx >= splatCount) return;
391
392 // Transform to view space
393 vec4 viewPos = view * vec4(splats[idx].position.xyz, 1.0);
394 float depth = -viewPos.z; // Negate because view space Z is negative
395
396 // Front-to-back sort key: top 16 bits = depth, bottom 16 bits = splat index.
397 // The depth gives correct rendering order; the index provides stable
398 // tie-breaking for splats at similar depths (same buffer order every frame).
399 // This eliminates the "wave" artifact from coherent sort-order swaps.
400 // Combined with "under" blending for correct front-to-back compositing.
401 const uint keyMax = 0xFFFFFFFFu;
402 uint key;
403 if (depth <= nearPlane) {
404 // Behind camera: draw last, but keep stable index-based sub-order
405 key = (0xFFFFu << 16u) | (idx & 0xFFFFu);
406 } else {
407 float t = log2(depth / nearPlane) / log2(farPlane / nearPlane);
408 t = clamp(t, 0.0, 1.0);
409 uint depthKey = uint(t * 65535.0);
410 key = (depthKey << 16u) | (idx & 0xFFFFu);
411 }
412
413 keys[idx] = key;
414 indices[idx] = idx;
415}
416)_";
417
422static constexpr auto histogram_shader = R"_(#version 450
423layout(local_size_x = 256) in;
424
425layout(std430, binding = 0) readonly buffer KeyBuffer {
426 uint keys[];
427};
428
429layout(std430, binding = 1) buffer HistogramBuffer {
430 uint histogram[]; // 256 buckets * num_workgroups
431};
432
433layout(std140, binding = 2) uniform Params {
434 uint splatCount;
435 uint bitOffset; // Which 8 bits to sort (0, 8, 16, 24)
436 uint numWorkgroups;
437 uint _pad;
438};
439
440shared uint localHistogram[256];
441
442void main() {
443 uint localId = gl_LocalInvocationID.x;
444 uint globalId = gl_GlobalInvocationID.x;
445 uint workgroupId = gl_WorkGroupID.x;
446
447 // Clear local histogram
448 localHistogram[localId] = 0;
449 barrier();
450
451 // Count digits in this workgroup
452 if (globalId < splatCount) {
453 uint key = keys[globalId];
454 uint digit = (key >> bitOffset) & 0xFFu;
455 atomicAdd(localHistogram[digit], 1);
456 }
457 barrier();
458
459 // Write local histogram to global memory
460 histogram[workgroupId * 256 + localId] = localHistogram[localId];
461}
462)_";
463
468static constexpr auto sort_scatter_shader = R"_(#version 450
469layout(local_size_x = 256) in;
470
471layout(std430, binding = 0) readonly buffer KeyBufferIn {
472 uint keysIn[];
473};
474
475layout(std430, binding = 1) readonly buffer IndexBufferIn {
476 uint indicesIn[];
477};
478
479layout(std430, binding = 2) writeonly buffer KeyBufferOut {
480 uint keysOut[];
481};
482
483layout(std430, binding = 3) writeonly buffer IndexBufferOut {
484 uint indicesOut[];
485};
486
487layout(std430, binding = 4) buffer HistogramBuffer {
488 uint histogram[]; // Global prefix sums
489};
490
491layout(std140, binding = 5) uniform Params {
492 uint splatCount;
493 uint bitOffset;
494 uint numWorkgroups;
495 uint _pad;
496};
497
498shared uint localDigits[256];
499shared uint localOffset[256];
500
501void main() {
502 uint localId = gl_LocalInvocationID.x;
503 uint globalId = gl_GlobalInvocationID.x;
504 uint workgroupId = gl_WorkGroupID.x;
505
506 // Load global prefix sum for this workgroup's digit
507 localOffset[localId] = histogram[workgroupId * 256 + localId];
508
509 // Load this thread's element
510 uint key = 0u;
511 uint idx = 0u;
512 uint digit = 256u; // invalid sentinel (> any real digit)
513 bool valid = globalId < splatCount;
514 if (valid) {
515 key = keysIn[globalId];
516 idx = indicesIn[globalId];
517 digit = (key >> bitOffset) & 0xFFu;
518 }
519 localDigits[localId] = digit;
520 barrier();
521
522 if (valid) {
523 // Stable rank: count threads with LOWER ID that share the same digit.
524 // This is deterministic (no atomicAdd race), so the sort is stable
525 // and identical across frames — eliminates flickering.
526 uint rank = 0u;
527 for (uint i = 0u; i < localId; i++) {
528 if (localDigits[i] == digit)
529 rank++;
530 }
531
532 uint globalPos = localOffset[digit] + rank;
533 if (globalPos < splatCount) {
534 keysOut[globalPos] = key;
535 indicesOut[globalPos] = idx;
536 }
537 }
538}
539)_";
540
555static constexpr auto prefix_sum_shader = R"_(#version 450
556layout(local_size_x = 256) in;
557
558layout(std430, binding = 0) buffer HistogramBuffer {
559 uint histogram[]; // Layout: histogram[workgroup * 256 + digit]
560};
561
562layout(std140, binding = 1) uniform Params {
563 uint numWorkgroups;
564 uint _pad0;
565 uint _pad1;
566 uint _pad2;
567};
568
569shared uint digitTotal[256];
570shared uint digitPrefix[256];
571
572void main() {
573 uint digit = gl_LocalInvocationID.x; // 0-255, one thread per digit
574
575 // Step 1: Sum all workgroup counts for this digit
576 uint total = 0;
577 for (uint wg = 0; wg < numWorkgroups; wg++) {
578 total += histogram[wg * 256 + digit];
579 }
580 digitTotal[digit] = total;
581 barrier();
582
583 // Step 2: Thread 0 computes exclusive prefix sum across all digits
584 // This determines the global starting offset for each digit bucket
585 if (digit == 0) {
586 digitPrefix[0] = 0;
587 for (uint d = 1; d < 256; d++) {
588 digitPrefix[d] = digitPrefix[d-1] + digitTotal[d-1];
589 }
590 }
591 barrier();
592
593 // Step 3: Convert per-workgroup counts to global offsets
594 // For each workgroup: offset = digitPrefix[digit] + sum of same-digit counts in earlier workgroups
595 uint running = digitPrefix[digit];
596 for (uint wg = 0; wg < numWorkgroups; wg++) {
597 uint idx = wg * 256 + digit;
598 uint val = histogram[idx];
599 histogram[idx] = running;
600 running += val;
601 }
602}
603)_";
604
605//=============================================================================
606// RENDER SHADERS
607//=============================================================================
608
609static constexpr auto vertex_shader = R"_(#version 450
610
611// Quad vertex positions
612const vec2 positions[6] = vec2[6](
613 vec2(-1.0, -1.0),
614 vec2( 1.0, -1.0),
615 vec2( 1.0, 1.0),
616 vec2(-1.0, -1.0),
617 vec2( 1.0, 1.0),
618 vec2(-1.0, 1.0)
619);
620
621// Compact rendering splat (output of preprocess compute shader)
622struct RenderSplat {
623 vec4 position; // xyz = position
624 vec4 scale; // xyz = scale (already exp'd)
625 vec4 rotation; // quaternion xyzw (already normalized)
626 vec4 color; // RGBA (SH evaluated, sigmoid applied)
627};
628
629layout(std430, binding = 0) readonly buffer SplatBuffer {
630 RenderSplat splats[];
631};
632
633// Sorted indices from depth sort pass
634layout(std430, binding = 1) readonly buffer SortedIndices {
635 uint sortedIndices[];
636};
637
638layout(std140, binding = 2) uniform Uniforms {
639 mat4 view;
640 mat4 projection;
641 mat4 clipSpaceCorr;
642 vec2 viewport;
643 float _pad0;
644 uint useSorting; // 0 = no sorting, 1 = use sorted indices
645};
646
647layout(location = 0) out vec2 f_offset; // offset from the splat centre, NDC pixels (x right, y up)
648layout(location = 1) out vec4 f_color;
649layout(location = 2) out vec3 f_conic;
650
651mat3 quatToMat(vec4 q) {
652 float x = q.x, y = q.y, z = q.z, w = q.w;
653 // GLSL mat3 is column-major: mat3(col0, col1, col2)
654 return mat3(
655 1.0 - 2.0*(y*y + z*z), 2.0*(x*y + w*z), 2.0*(x*z - w*y), // col 0
656 2.0*(x*y - w*z), 1.0 - 2.0*(x*x + z*z), 2.0*(y*z + w*x), // col 1
657 2.0*(x*z + w*y), 2.0*(y*z - w*x), 1.0 - 2.0*(x*x + y*y) // col 2
658 );
659}
660
661void main() {
662 // Get splat index (sorted or unsorted)
663 uint splatIdx = useSorting != 0 ? sortedIndices[gl_InstanceIndex] : gl_InstanceIndex;
664 RenderSplat splat = splats[splatIdx];
665 vec2 quadPos = positions[gl_VertexIndex];
666
667 // Early opacity cull: skip splats that are nearly invisible
668 if (splat.color.a < 1.0 / 255.0) {
669 gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
670 return;
671 }
672
673 // View space position
674 vec4 viewPos = view * vec4(splat.position.xyz, 1.0);
675
676 // Focal lengths in pixels
677 float focal = projection[0][0] * viewport.x * 0.5;
678 float focal_y = projection[1][1] * viewport.y * 0.5;
679 float tanFovX = 0.5 * viewport.x / focal;
680 float tanFovY = 0.5 * viewport.y / focal_y;
681
682 // Frustum culling: project to clip space and check NDC bounds
683 // (matches INRIA reference: cull behind camera + outside 1.3x viewport)
684 vec4 clipPos = projection * viewPos;
685 if (clipPos.w <= 0.2) {
686 gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
687 return;
688 }
689 vec3 ndc = clipPos.xyz / clipPos.w;
690 if (abs(ndc.x) > 1.3 || abs(ndc.y) > 1.3) {
691 gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
692 return;
693 }
694
695 // Clamp view-space position to prevent numerical issues at screen edges
696 // (matches INRIA CUDA reference: 1.3x FOV tangent)
697 float limX = 1.3 * tanFovX;
698 float limY = 1.3 * tanFovY;
699 float txtz = viewPos.x / viewPos.z;
700 float tytz = viewPos.y / viewPos.z;
701 viewPos.x = clamp(txtz, -limX, limX) * viewPos.z;
702 viewPos.y = clamp(tytz, -limY, limY) * viewPos.z;
703
704 // Build 3D covariance from scale and rotation (already preprocessed)
705 // INRIA convention: Sigma = R * S * S^T * R^T = R * S² * R^T
706 // The principal axes are the COLUMNS of R.
707 vec3 scale = splat.scale.xyz;
708 mat3 R = quatToMat(splat.rotation);
709 mat3 S = mat3(scale.x, 0, 0, 0, scale.y, 0, 0, 0, scale.z);
710 mat3 M = R * S;
711 mat3 Sigma = M * transpose(M);
712
713 // 2D covariance via EWA projection
714 mat3 W = mat3(view);
715 float z2 = viewPos.z * viewPos.z;
716
717 // Jacobian of projection (column-major: mat3(col0, col1, col2))
718 mat3 J = mat3(
719 focal / viewPos.z, 0.0, 0.0, // col 0
720 0.0, focal_y / viewPos.z, 0.0, // col 1
721 -focal * viewPos.x / z2, -focal_y * viewPos.y / z2, 0.0 // col 2
722 );
723
724 mat3 T = J * W;
725 mat3 cov = T * Sigma * transpose(T);
726
727 float cov_xx = cov[0][0], cov_xy = cov[0][1], cov_yy = cov[1][1];
728
729 // Mip-Splatting 2D filter (Yu et al. 2024): approximate the pixel box filter
730 // as a Gaussian and convolve with the projected 2D covariance.
731 // Opacity is compensated to preserve each splat's total contribution:
732 // alpha' = alpha * sqrt(det(Sigma) / det(Sigma + kernel_size * I))
733 float kernel_size = 0.3;
734 float det_0 = max(1e-6, cov_xx * cov_yy - cov_xy * cov_xy);
735 cov_xx += kernel_size;
736 cov_yy += kernel_size;
737 float det_1 = max(1e-6, cov_xx * cov_yy - cov_xy * cov_xy);
738 float mipCoef = sqrt(det_0 / det_1);
739
740 float det = cov_xx * cov_yy - cov_xy * cov_xy;
741 float mid = 0.5 * (cov_xx + cov_yy);
742 float disc = max(0.0, mid * mid - det);
743 float lambda1 = mid + sqrt(disc);
744 float lambda2 = mid - sqrt(disc);
745
746 // Eigenvectors of 2D covariance for ellipse-aligned quad
747 vec2 eigVec1;
748 if (abs(cov_xy) > 1e-6) {
749 eigVec1 = normalize(vec2(cov_xy, lambda1 - cov_xx));
750 } else {
751 eigVec1 = (cov_xx >= cov_yy) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
752 }
753 vec2 eigVec2 = vec2(-eigVec1.y, eigVec1.x);
754
755 float maxExtent = 2048.0;
756 float r1 = min(ceil(3.0 * sqrt(max(lambda1, 0.0))), maxExtent);
757 float r2 = min(ceil(3.0 * sqrt(max(lambda2, 0.0))), maxExtent);
758
759 // Cull degenerate or invisible splats
760 if (det < 1e-3 || max(r1, r2) < 0.1) {
761 gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
762 return;
763 }
764
765 // Inverse covariance (conic) for fragment Gaussian evaluation, in the same
766 // space as the quad: NDC scaled to pixels (x right, y up). J-space is that
767 // space negated on both axes (view z < 0), which leaves cov_xy unchanged.
768 // The fragment stage evaluates the Gaussian on the interpolated quad
769 // offset, never on gl_FragCoord: the window origin is bottom-left on
770 // OpenGL and top-left elsewhere, and its relation to NDC differs again
771 // between Vulkan (y down) and Metal/D3D (y up), so a gl_FragCoord-based
772 // centre and cross-term sign cannot be right on every backend (it put
773 // every splat off its quad on Metal/D3D, i.e. a black image).
774 float inv_det = 1.0 / det;
775 f_conic = vec3(cov_yy * inv_det, -cov_xy * inv_det, cov_xx * inv_det);
776
777 // Oriented quad: major axis along eigVec1, minor along eigVec2
778 vec2 pixelOffset = quadPos.x * r1 * eigVec1 + quadPos.y * r2 * eigVec2;
779 vec2 center = ndc.xy;
780 vec2 ndcOffset = pixelOffset * 2.0 / viewport;
781
782 gl_Position = clipSpaceCorr * vec4(center + ndcOffset, ndc.z, 1.0);
783
784 // Published images have render-target row order: on Vulkan that means
785 // undoing clipSpaceCorr's Y-flip, on OpenGL flipping the GL-convention
786 // NDC. Metal and D3D already get render-target order from clipSpaceCorr
787 // (no Y term there); negating unconditionally drew them upside down.
788#if !defined(QSHADER_HLSL) && !defined(QSHADER_MSL)
789 gl_Position.y = -gl_Position.y;
790#endif
791
792 // gl_Position.w is 1, so this interpolates linearly in screen space and
793 // is exact at every fragment.
794 f_offset = pixelOffset;
795
796 // Fade out excessively large projected splats.
797 float alpha = splat.color.a * mipCoef;
798 float maxR = max(r1, r2);
799 float fadeRadius = 512.0;
800 if (maxR > fadeRadius) {
801 float fade = fadeRadius / maxR;
802 alpha *= fade;
803 if (alpha < 1.0 / 255.0) {
804 gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
805 return;
806 }
807 }
808 f_color = vec4(splat.color.rgb, alpha);
809}
810)_";
811
812static constexpr auto fragment_shader = R"_(#version 450
813
814layout(location = 0) in vec2 f_offset; // offset from the splat centre, NDC pixels
815layout(location = 1) in vec4 f_color;
816layout(location = 2) in vec3 f_conic;
817
818layout(location = 0) out vec4 fragColor;
819
820void main() {
821 // Offset from the splat centre in the conic's own space (see the vertex
822 // stage), identical on every backend.
823 vec2 d = f_offset;
824
825 float power = -0.5 * (f_conic.x * d.x * d.x +
826 2.0 * f_conic.y * d.x * d.y +
827 f_conic.z * d.y * d.y);
828
829 if (power > 0.0) discard;
830
831 float gaussian = exp(power);
832 float alpha = min(0.99, gaussian * f_color.a);
833 if (alpha < 1.0/255.0) discard;
834
835 fragColor = vec4(f_color.rgb * alpha, alpha);
836}
837)_";
838
839} // namespace GaussianSplatShaders
840
841} // namespace score::gfx
Renderer for GaussianSplatNode.
Definition GaussianSplatNode.hpp:75
void initState(RenderList &renderer, QRhiResourceUpdateBatch &res) override
Definition GaussianSplatNode.cpp:552
void removeOutputPass(RenderList &renderer, Edge &edge) override
Definition GaussianSplatNode.cpp:606
void addOutputPass(RenderList &renderer, Edge &edge, QRhiResourceUpdateBatch &res) override
Create a pass for a new output edge (pipeline, SRB, processUBO).
Definition GaussianSplatNode.cpp:599
void releaseState(RenderList &) override
Definition GaussianSplatNode.cpp:1048
bool hasOutputPassForEdge(Edge &edge) const override
Check if this renderer already has an output pass for the given edge.
Definition GaussianSplatNode.cpp:614
Generic renderer.
Definition NodeRenderer.hpp:337
Common base class for most single-pass, simple nodes.
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:329
Renderer for a given node.
Definition NodeRenderer.hpp:11
List of nodes to be rendered to an output.
Definition RenderList.hpp:36
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
Connection between two score::gfx::Port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:236
Gaussian Splat rendering node.
Definition GaussianSplatNode.hpp:41
score::gfx::NodeRenderer * createRenderer(RenderList &) const noexcept override
Create a renderer in a given context for this node.
Definition GaussianSplatNode.cpp:80
void process(Message &&msg) override
Process a message from the execution engine.
Definition GaussianSplatNode.cpp:37
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:60