-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Gaussian splat spherical harmonics support #12790
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
7c6a2a8
a78ab94
94165d2
faf0111
2448e21
f9f99aa
42e0c8d
81d252b
9026e4a
1d03ac0
40e4fe6
8ecb57c
0b099c3
3e0ac2b
c3cc59c
abc8209
a18a935
a0baf6c
b725db4
2836299
38bba9a
032fb1f
ae0adcc
a942986
7998433
59d8757
fb27194
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,6 +78,20 @@ function GaussianSplat3DTileContent(loader, tileset, tile, resource) { | |
* @private | ||
*/ | ||
this._transformed = false; | ||
|
||
/** | ||
* The degree of the spherical harmonics used for the Gaussian splats. | ||
* @type {number} | ||
* @private | ||
*/ | ||
this.shDegree = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are correct it can be derived, but it's nice to not have to and keeps things more explicit. They are used in different ways when making checks and indexing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we set one and use a getter function for the other? I'm mostly concerned about these values getting out of sync since they can both be set. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I see now they are set privately and only exposed via the getter. Should all be fine then. 👍 |
||
|
||
/** | ||
* The number of spherical harmonic coefficients used for the Gaussian splats. | ||
* @type {number} | ||
* @private | ||
*/ | ||
this.shCoefficientCount = 0; | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
Object.defineProperties(GaussianSplat3DTileContent.prototype, { | ||
|
@@ -311,6 +325,144 @@ Object.defineProperties(GaussianSplat3DTileContent.prototype, { | |
}, | ||
}); | ||
|
||
/** | ||
* Determine Spherical Harmonics degree and coefficient count from attributes | ||
* @param {Array} attributes - The list of attributes. | ||
* @returns {Object} An object containing the degree (l) and coefficient (n). | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
function degreeAndCoefFromAttributes(attributes) { | ||
const prefix = "_SH_DEGREE_"; | ||
const shAttributes = attributes.filter((attr) => | ||
attr.name.startsWith(prefix), | ||
); | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
switch (shAttributes.length) { | ||
default: | ||
case 0: | ||
return { l: 0, n: 0 }; | ||
case 3: | ||
return { l: 1, n: 9 }; | ||
case 8: | ||
return { l: 2, n: 24 }; | ||
case 15: | ||
return { l: 3, n: 45 }; | ||
} | ||
} | ||
|
||
/** | ||
* Converts a 32-bit floating point number to a 16-bit floating point number. | ||
* @param {Float} float32 input | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @returns {number} Half precision float | ||
* @private | ||
*/ | ||
const buffer = new ArrayBuffer(4); | ||
const floatView = new Float32Array(buffer); | ||
const intView = new Uint32Array(buffer); | ||
|
||
function float32ToFloat16(float32) { | ||
floatView[0] = float32; | ||
const bits = intView[0]; | ||
|
||
const sign = (bits >> 31) & 0x1; | ||
const exponent = (bits >> 23) & 0xff; | ||
const mantissa = bits & 0x7fffff; | ||
|
||
let half; | ||
|
||
if (exponent === 0xff) { | ||
half = (sign << 15) | (0x1f << 10) | (mantissa ? 0x200 : 0); | ||
} else if (exponent === 0) { | ||
half = sign << 15; | ||
} else { | ||
const newExponent = exponent - 127 + 15; | ||
if (newExponent >= 31) { | ||
half = (sign << 15) | (0x1f << 10); | ||
} else if (newExponent <= 0) { | ||
half = sign << 15; | ||
} else { | ||
half = (sign << 15) | (newExponent << 10) | (mantissa >>> 13); | ||
} | ||
} | ||
|
||
return half; | ||
} | ||
|
||
/** | ||
* Extracts the spherical harmonic degree and coefficient from the attribute name. | ||
* @param {String} attribute - The attribute name. | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @returns {Object} An object containing the degree (l) and coefficient (n). | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @private | ||
*/ | ||
function extractSHDegreeAndCoef(attribute) { | ||
const prefix = "_SH_DEGREE_"; | ||
const separator = "_COEF_"; | ||
|
||
const lStart = prefix.length; | ||
const coefIndex = attribute.indexOf(separator, lStart); | ||
|
||
const l = parseInt(attribute.slice(lStart, coefIndex), 10); | ||
const n = parseInt(attribute.slice(coefIndex + separator.length), 10); | ||
|
||
return { l, n }; | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Packs spherical harmonic data into half-precision floats. | ||
* @param {*} data - The input data to pack. | ||
* @param {*} shDegree - The spherical harmonic degree. | ||
* @returns {Uint32Array} - The packed data. | ||
*/ | ||
function packSphericalHarmonicData(tileContent) { | ||
const degree = tileContent.shDegree; | ||
const coefs = tileContent.shCoefficientCount; | ||
const totalLength = tileContent.pointsLength * (coefs * (2 / 3)); //3 packs into 2 | ||
const packedData = new Uint32Array(totalLength); | ||
|
||
const shAttributes = tileContent.splatPrimitive.attributes.filter((attr) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's defined in the GS content class. Very ambiguous, but it's a reference to the glTF primitive. I renamed it for better clarity. |
||
attr.name.startsWith("_SH_DEGREE_"), | ||
); | ||
keyboardspecialist marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let stride = 0; | ||
const base = [0, 9, 24]; | ||
switch (degree) { | ||
case 1: | ||
stride = 9; | ||
break; | ||
case 2: | ||
stride = 24; | ||
break; | ||
case 3: | ||
stride = 45; | ||
break; | ||
} | ||
shAttributes.sort((a, b) => { | ||
if (a.name < b.name) { | ||
return -1; | ||
} | ||
if (a.name > b.name) { | ||
return 1; | ||
} | ||
|
||
return 0; | ||
}); | ||
const packedStride = stride * (2 / 3); | ||
for (let i = 0; i < shAttributes.length; i++) { | ||
const { l, n } = extractSHDegreeAndCoef(shAttributes[i].name); | ||
for (let j = 0; j < tileContent.pointsLength; j++) { | ||
//interleave the data | ||
const packedBase = (base[l - 1] * 2) / 3; | ||
const idx = j * packedStride + packedBase + n * 2; | ||
const src = j * 3; | ||
packedData[idx] = | ||
float32ToFloat16(shAttributes[i].typedArray[src]) | | ||
(float32ToFloat16(shAttributes[i].typedArray[src + 1]) << 16); | ||
packedData[idx + 1] = float32ToFloat16( | ||
shAttributes[i].typedArray[src + 2], | ||
); | ||
} | ||
} | ||
return packedData; | ||
} | ||
|
||
/** | ||
* Creates a new instance of {@link GaussianSplat3DTileContent} from a glTF or glb resource. | ||
* | ||
|
@@ -412,6 +564,14 @@ GaussianSplat3DTileContent.prototype.update = function (primitive, frameState) { | |
).typedArray, | ||
); | ||
|
||
const { l, n } = degreeAndCoefFromAttributes( | ||
this.splatPrimitive.attributes, | ||
); | ||
this.shDegree = l; | ||
this.shCoefficientCount = n; | ||
|
||
this._packedShData = packSphericalHarmonicData(this); | ||
|
||
return; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where can we find a spec or description of the SPZ format? Is the format stable?
A link to the spec, perhaps in
GltfVertexBufferLoader
, could go a long way to shortening the spin-up time for future maintainers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing that is closest to a spec is the README of https://github.com/nianticlabs/spz . The actual truth is in the code. Request for clarification are in nianticlabs/spz#42 . This does not yet cover the details of SHs and their layout, but maybe what's in the README is enough... (?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should reword that... we load them from SPZ currently, but the rendering side is not tied to SPZ.