();
+ extension.put("kernel", "ellipse");
+ extension.put("colorSpace", "BT.709-sRGB");
+ extension.put("sortingMethod", "cameraDistance");
+ extension.put("projection", "perspective");
+ return extension;
+ }
+
+ /**
+ * Read the positions from the given splats, as a buffer with splats.size()
+ * * 3 elements
+ *
+ * @param splats The splats
+ * @return The result
+ */
+ private static FloatBuffer readPositions(List extends Splat> splats)
+ {
+ FloatBuffer b = FloatBuffer.allocate(splats.size() * 3);
+ for (int i = 0; i < splats.size(); i++)
+ {
+ Splat s = splats.get(i);
+ b.put(i * 3 + 0, s.getPositionX());
+ b.put(i * 3 + 1, s.getPositionY());
+ b.put(i * 3 + 2, s.getPositionZ());
+ }
+ return b;
+ }
+
+ /**
+ * Read the scales from the given splats, as a buffer with splats.size() * 3
+ * elements
+ *
+ * @param splats The splats
+ * @return The result
+ */
+ private static FloatBuffer readScales(List extends Splat> splats)
+ {
+ FloatBuffer b = FloatBuffer.allocate(splats.size() * 3);
+ for (int i = 0; i < splats.size(); i++)
+ {
+ Splat s = splats.get(i);
+ b.put(i * 3 + 0, s.getScaleX());
+ b.put(i * 3 + 1, s.getScaleY());
+ b.put(i * 3 + 2, s.getScaleZ());
+ }
+ return b;
+ }
+
+ /**
+ * Read the rotations from the given splats, as a buffer with splats.size()
+ * * 4 elements
+ *
+ * @param splats The splats
+ * @return The result
+ */
+ private static FloatBuffer readRotations(List extends Splat> splats)
+ {
+ FloatBuffer b = FloatBuffer.allocate(splats.size() * 4);
+ for (int i = 0; i < splats.size(); i++)
+ {
+ Splat s = splats.get(i);
+ b.put(i * 4 + 0, s.getRotationX());
+ b.put(i * 4 + 1, s.getRotationY());
+ b.put(i * 4 + 2, s.getRotationZ());
+ b.put(i * 4 + 3, s.getRotationW());
+ }
+ return b;
+ }
+
+ /**
+ * Read the opacities from the given splats, as a buffer with splats.size()
+ * elements
+ *
+ * @param splats The splats
+ * @return The result
+ */
+ private static FloatBuffer readOpacities(List extends Splat> splats)
+ {
+ FloatBuffer b = FloatBuffer.allocate(splats.size());
+ for (int i = 0; i < splats.size(); i++)
+ {
+ Splat s = splats.get(i);
+ b.put(i, s.getOpacity());
+ }
+ return b;
+ }
+
+ /**
+ * Read the specified spherical harmonics from the given splats, as a buffer
+ * with splats.size() * 3 elements
+ *
+ * @param splats The splats
+ * @param degree The degree
+ * @param coefficient The coefficient
+ * @return The result
+ */
+ private static FloatBuffer readSh(List extends Splat> splats, int degree,
+ int coefficient)
+ {
+ int index = Splats.dimensionForCoefficient(degree, coefficient);
+ FloatBuffer b = FloatBuffer.allocate(splats.size() * 3);
+ for (int i = 0; i < splats.size(); i++)
+ {
+ Splat s = splats.get(i);
+ b.put(i * 3 + 0, s.getShX(index));
+ b.put(i * 3 + 1, s.getShY(index));
+ b.put(i * 3 + 2, s.getShZ(index));
+ }
+ return b;
+ }
+
+}
diff --git a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/Quantization.java b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/Quantization.java
new file mode 100644
index 0000000..dee42f9
--- /dev/null
+++ b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/Quantization.java
@@ -0,0 +1,226 @@
+/*
+ * www.javagl.de - JSplat
+ *
+ * Copyright 2025 Marco Hutter - http://www.javagl.de
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
+package de.javagl.jsplat.io.gltf;
+
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.ShortBuffer;
+
+import de.javagl.jgltf.model.AccessorData;
+import de.javagl.jgltf.model.AccessorModel;
+import de.javagl.jgltf.model.GltfConstants;
+
+/**
+ * Internal utility methods for quantization.
+ *
+ * These methods will sooner or later become part of JglTF.
+ */
+class Quantization
+{
+ /**
+ * Returns the data from the given accessor model as a float buffer, tightly
+ * packed, applying dequantization as necessary.
+ *
+ * @param accessorModel The accessor model
+ * @return The buffer
+ * @throws IllegalArgumentException If the component type of the given
+ * accessor model is neither float, nor signed/unsigned byte/short.
+ */
+ static FloatBuffer readAsFloatBuffer(AccessorModel accessorModel)
+ {
+ AccessorData accessorData = accessorModel.getAccessorData();
+ int componentType = accessorModel.getComponentType();
+ if (componentType == GltfConstants.GL_FLOAT)
+ {
+ ByteBuffer inputByteBuffer = accessorData.createByteBuffer();
+ return inputByteBuffer.asFloatBuffer();
+ }
+ if (componentType == GltfConstants.GL_SHORT)
+ {
+ ByteBuffer inputByteBuffer = accessorData.createByteBuffer();
+ ShortBuffer shortBuffer = inputByteBuffer.asShortBuffer();
+ return dequantizeShortBuffer(shortBuffer);
+ }
+ if (componentType == GltfConstants.GL_UNSIGNED_SHORT)
+ {
+ ByteBuffer inputByteBuffer = accessorData.createByteBuffer();
+ ShortBuffer shortBuffer = inputByteBuffer.asShortBuffer();
+ return dequantizeUnsignedShortBuffer(shortBuffer);
+ }
+ if (componentType == GltfConstants.GL_BYTE)
+ {
+ ByteBuffer inputByteBuffer = accessorData.createByteBuffer();
+ return dequantizeByteBuffer(inputByteBuffer);
+ }
+ if (componentType == GltfConstants.GL_UNSIGNED_BYTE)
+ {
+ ByteBuffer inputByteBuffer = accessorData.createByteBuffer();
+ return dequantizeUnsignedByteBuffer(inputByteBuffer);
+ }
+ throw new IllegalArgumentException(
+ "Component type " + GltfConstants.stringFor(componentType)
+ + " cannot be converted to float");
+ }
+
+ /**
+ * Dequantize the given buffer into a float buffer, treating each element of
+ * the input as a signed byte.
+ *
+ * @param byteBuffer The input buffer
+ * @return The result
+ */
+ private static FloatBuffer dequantizeByteBuffer(ByteBuffer byteBuffer)
+ {
+ FloatBuffer floatBuffer = FloatBuffer.allocate(byteBuffer.capacity());
+ for (int i = 0; i < byteBuffer.capacity(); i++)
+ {
+ byte c = byteBuffer.get(i);
+ float f = dequantizeByte(c);
+ floatBuffer.put(i, f);
+ }
+ return floatBuffer;
+ }
+
+ /**
+ * Dequantize the given buffer into a float buffer, treating each element of
+ * the input as an unsigned byte.
+ *
+ * @param byteBuffer The input buffer
+ * @return The result
+ */
+ private static FloatBuffer
+ dequantizeUnsignedByteBuffer(ByteBuffer byteBuffer)
+ {
+ FloatBuffer floatBuffer = FloatBuffer.allocate(byteBuffer.capacity());
+ for (int i = 0; i < byteBuffer.capacity(); i++)
+ {
+ byte c = byteBuffer.get(i);
+ float f = dequantizeUnsignedByte(c);
+ floatBuffer.put(i, f);
+ }
+ return floatBuffer;
+ }
+
+ /**
+ * Dequantize the given buffer into a float buffer, treating each element of
+ * the input as a signed short.
+ *
+ * @param shortBuffer The input buffer
+ * @return The result
+ */
+ private static FloatBuffer dequantizeShortBuffer(ShortBuffer shortBuffer)
+ {
+ FloatBuffer floatBuffer = FloatBuffer.allocate(shortBuffer.capacity());
+ for (int i = 0; i < shortBuffer.capacity(); i++)
+ {
+ short c = shortBuffer.get(i);
+ float f = dequantizeShort(c);
+ floatBuffer.put(i, f);
+ }
+ return floatBuffer;
+ }
+
+ /**
+ * Dequantize the given buffer into a float buffer, treating each element of
+ * the input as an unsigned short.
+ *
+ * @param shortBuffer The input buffer
+ * @return The result
+ */
+ private static FloatBuffer
+ dequantizeUnsignedShortBuffer(ShortBuffer shortBuffer)
+ {
+ FloatBuffer floatBuffer = FloatBuffer.allocate(shortBuffer.capacity());
+ for (int i = 0; i < shortBuffer.capacity(); i++)
+ {
+ short c = shortBuffer.get(i);
+ float f = dequantizeUnsignedShort(c);
+ floatBuffer.put(i, f);
+ }
+ return floatBuffer;
+ }
+
+ /**
+ * Dequantize the given signed byte into a floating point value
+ *
+ * @param c The input
+ * @return The result
+ */
+ private static float dequantizeByte(byte c)
+ {
+ float f = Math.max(c / 127.0f, -1.0f);
+ return f;
+ }
+
+ /**
+ * Dequantize the given unsigned byte into a floating point value
+ *
+ * @param c The input
+ * @return The result
+ */
+ private static float dequantizeUnsignedByte(byte c)
+ {
+ int i = Byte.toUnsignedInt(c);
+ float f = i / 255.0f;
+ return f;
+ }
+
+ /**
+ * Dequantize the given signed short into a floating point value
+ *
+ * @param c The input
+ * @return The result
+ */
+ private static float dequantizeShort(short c)
+ {
+ float f = Math.max(c / 32767.0f, -1.0f);
+ return f;
+ }
+
+ /**
+ *
+ * Dequantize the given unsigned byte into a floating point value
+ *
+ * @param c The input
+ * @return The result
+ */
+ private static float dequantizeUnsignedShort(short c)
+ {
+ int i = Short.toUnsignedInt(c);
+ float f = i / 65535.0f;
+ return f;
+ }
+
+ /**
+ * Private constructor to prevent instantiation
+ */
+ private Quantization()
+ {
+ // Private constructor to prevent instantiation
+ }
+
+}
diff --git a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/package-info.java b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/package-info.java
new file mode 100644
index 0000000..a4e5425
--- /dev/null
+++ b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Readers and writers for Gaussian splats in glTF
+ */
+package de.javagl.jsplat.io.gltf;
diff --git a/jsplat-io-spz-gltf/README.md b/jsplat-io-spz-gltf/README.md
deleted file mode 100644
index beaeb4a..0000000
--- a/jsplat-io-spz-gltf/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# jsplat-io-spz-gltf
-
-Classes for reading and writing Gaussian splats in glTF format,
-using the KHR_spz_gaussian_splats_compression extension.
\ No newline at end of file
diff --git a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfConfig.java b/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfConfig.java
deleted file mode 100644
index da2e702..0000000
--- a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfConfig.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * www.javagl.de - JSplat
- *
- * Copyright 2025 Marco Hutter - http://www.javagl.de
- *
- * Permission is hereby granted, free of charge, to any person
- * obtaining a copy of this software and associated documentation
- * files (the "Software"), to deal in the Software without
- * restriction, including without limitation the rights to use,
- * copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following
- * conditions:
- *
- * The above copyright notice and this permission notice shall be
- * included in all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
- * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- * OTHER DEALINGS IN THE SOFTWARE.
- */
-package de.javagl.jsplat.io.spz.gltf;
-
-/**
- * Internal class with flags, attempting to track the development of
- * https://github.com/KhronosGroup/glTF/pull/2490 and certain quirks and
- * assumptions that are made by CesiumJS.
- */
-class SpzGltfConfig
-{
- /**
- * A flag to insert certain up-axis conversion matrices in the
- * glTF, to handle certain expectations that are made by CesiumJS.
- */
- final boolean APPLY_UP_AXIS_TRANSFORMS = true;
-
- /**
- * Whether the glTF should be created with the KHR_gaussian_splatting
- * base extension.
- *
- * This is currently false by default, because there is no
- * support for the base extension in CesiumJS (as of 2025-08-23).
- */
- boolean USE_BASE_EXTENSION;
-
- /**
- * The string that is used as a prefix for the glTF attribute names.
- *
- * This is used for disambiguating the attribute names, as part of
- * https://github.com/KhronosGroup/glTF/issues/2111
- */
- String ATTRIBUTE_PREFIX;
-
- /**
- * The name of the SPZ extension.
- *
- * This will be KHR_spz_gaussian_splats_compression by default,
- * and KHR_gaussian_splatting_compression_spz when
- * {@link #USE_BASE_EXTENSION} is true.
- */
- String SPZ_EXTENSION_NAME;
-
- /**
- * Default constructor
- *
- * @param useBaseExtension Whether the base extension should be used
- */
- SpzGltfConfig(boolean useBaseExtension)
- {
- this.USE_BASE_EXTENSION = useBaseExtension;
- if (USE_BASE_EXTENSION)
- {
- ATTRIBUTE_PREFIX = "KHR_gaussian_splatting:";
- SPZ_EXTENSION_NAME = "KHR_gaussian_splatting_compression_spz";
- }
- else
- {
- ATTRIBUTE_PREFIX = "_";
- SPZ_EXTENSION_NAME = "KHR_spz_gaussian_splats_compression";
- }
- }
-
-}
diff --git a/jsplat/src/main/java/de/javagl/jsplat/Splats.java b/jsplat/src/main/java/de/javagl/jsplat/Splats.java
index 7d8be7f..600555f 100644
--- a/jsplat/src/main/java/de/javagl/jsplat/Splats.java
+++ b/jsplat/src/main/java/de/javagl/jsplat/Splats.java
@@ -269,6 +269,56 @@ public static int dimensionsForDegree(int degree)
return (degree + 1) * (degree + 1);
}
+ /**
+ * Returns the number of spherical harmonics coefficients for the given
+ * degree.
+ *
+ * @param degree The degree
+ * @return The number of coefficients
+ */
+ public static int coefficientsForDegree(int degree)
+ {
+ return degree * 2 + 1;
+ }
+
+ /**
+ * Returns the index of the dimension for the specified spherical harmonics
+ * coefficient.
+ *
+ *
+ * (degree 0, coefficient 0) : 0
+ *
+ * (degree 1, coefficient 0) : 1
+ * (degree 1, coefficient 1) : 2
+ * (degree 1, coefficient 2) : 3
+ *
+ * (degree 2, coefficient 0) : 4
+ * (degree 2, coefficient 1) : 5
+ * (degree 2, coefficient 2) : 6
+ * (degree 2, coefficient 3) : 7
+ * (degree 2, coefficient 4) : 8
+ *
+ * (degree 3, coefficient 0) : 9
+ * ...
+ * (degree 3, coefficient 6) : 15
+ *
+ *
+ * @param degree The degree
+ * @param coefficient The coefficient
+ * @return The dimension
+ */
+ public static int dimensionForCoefficient(int degree, int coefficient)
+ {
+ int index = 0;
+ for (int d = 0; d < degree; d++)
+ {
+ int dd = Splats.coefficientsForDegree(d);
+ index += dd;
+ }
+ index += coefficient;
+ return index;
+ }
+
/**
* Returns whether the given lists of splats are epsilon-equal
*
@@ -303,10 +353,9 @@ public static boolean equalsEpsilon(List extends Splat> sas,
/**
* Returns whether the given splats are epsilon-equal.
*
- * This involves special treatment for the scalar (rotation) component
- * of the quaternions: It will treat them as actual rotation angles,
- * meaning that values like 1.0 and -1.0 will be considered to be
- * equal.
+ * This involves special treatment for the scalar (rotation) component of
+ * the quaternions: It will treat them as actual rotation angles, meaning
+ * that values like 1.0 and -1.0 will be considered to be equal.
*
* @param sa The first splat
* @param sb The second splat
@@ -367,7 +416,7 @@ public static boolean equalsEpsilon(Splat sa, Splat sb, float epsilon)
// Special treatment for rotation component: The difference
// modulo 1.0 is computed, and should be epsilon-equal to 0
float wa = sa.getRotationW();
- float wb = sb.getRotationW();
+ float wb = sb.getRotationW();
float wd = 1.0f - Math.abs(Math.abs(wa - wb) - 1.0f);
if (!equalsEpsilon(wd, 0.0f, epsilon))
{
@@ -398,14 +447,14 @@ public static boolean equalsEpsilon(Splat sa, Splat sb, float epsilon)
}
return true;
}
-
+
/**
* Returns whether the given splats are strictly epsilon-equal.
*
* This means that it will compare the actual values of the splats,
- * regardless of their semantics. For rotation quaternions, a
- * scalar value of 1.0 and -1.0 describe the same rotation, and this
- * will not be taken into account here.
+ * regardless of their semantics. For rotation quaternions, a scalar value
+ * of 1.0 and -1.0 describe the same rotation, and this will not be
+ * taken into account here.
*
* @param sa The first splat
* @param sb The second splat
@@ -491,7 +540,6 @@ public static boolean strictEqualsEpsilon(Splat sa, Splat sb, float epsilon)
}
return true;
}
-
/**
* Returns whether the given values are epsilon-equal
diff --git a/pom.xml b/pom.xml
index d63ef8f..dc0c63f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,7 +33,8 @@
jsplat-io-gsplat
jsplat-io-ply
jsplat-io-spz
- jsplat-io-spz-gltf
+ jsplat-io-gltf
+ jsplat-io-gltf-spz