From ea605ab6bd99b9e00ab4e88aa7451010aad0c3bd Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 9 Nov 2025 16:07:34 +0100 Subject: [PATCH 1/7] First pass for base extension updates --- .gitignore | 5 + jsplat-examples/pom.xml | 5 + .../JSplatGltfRoundtripExperiments.java | 9 +- .../jsplat/examples/JSplatRoundtrips.java | 27 +- .../javagl/jsplat/examples/SplatFormat.java | 8 +- .../java/de/javagl/jsplat/examples/Utils.java | 7 + jsplat-io-gltf/README.md | 4 + jsplat-io-gltf/pom.xml | 41 +++ .../jsplat/io/gltf/GltfSplatReader.java | 319 ++++++++++++++++++ .../jsplat/io/gltf/GltfSplatWriter.java | 287 ++++++++++++++++ .../javagl/jsplat/io/gltf/package-info.java | 4 + jsplat-io-spz-gltf/README.md | 2 +- .../jsplat/io/spz/gltf/SpzGltfConfig.java | 88 ----- .../io/spz/gltf/SpzGltfSplatReader.java | 211 ++++++++---- .../io/spz/gltf/SpzGltfSplatWriter.java | 117 ++----- .../main/java/de/javagl/jsplat/Splats.java | 68 +++- 16 files changed, 929 insertions(+), 273 deletions(-) create mode 100644 jsplat-io-gltf/README.md create mode 100644 jsplat-io-gltf/pom.xml create mode 100644 jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java create mode 100644 jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java create mode 100644 jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/package-info.java delete mode 100644 jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfConfig.java diff --git a/.gitignore b/.gitignore index 3d9235f..91a09d8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,11 @@ /jsplat-io-spz-gltf/.settings /jsplat-io-spz-gltf/.classpath +/jsplat-io-gltf/target +/jsplat-io-gltf/.project +/jsplat-io-gltf/.settings +/jsplat-io-gltf/.classpath + /jsplat-viewer/target /jsplat-viewer/.project /jsplat-viewer/.settings diff --git a/jsplat-examples/pom.xml b/jsplat-examples/pom.xml index dbe8569..331a84f 100644 --- a/jsplat-examples/pom.xml +++ b/jsplat-examples/pom.xml @@ -32,6 +32,11 @@ jsplat-io-spz 0.0.1-SNAPSHOT + + de.javagl + jsplat-io-gltf + 0.0.1-SNAPSHOT + de.javagl jsplat-io-spz-gltf diff --git a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java index 842ca97..843a6b5 100644 --- a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java +++ b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java @@ -32,16 +32,13 @@ public class JSplatGltfRoundtripExperiments */ public static void main(String[] args) throws IOException { - // Whether the KHR_gaussian_splatting base extension should be used. - boolean useBaseExtension = true; - List splatsA = UnitCubeSplats.create(); - SpzGltfSplatWriter w = new SpzGltfSplatWriter(useBaseExtension); + SpzGltfSplatWriter w = new SpzGltfSplatWriter(); ByteArrayOutputStream os = new ByteArrayOutputStream(); w.writeList(splatsA, os); - - SpzGltfSplatReader r = new SpzGltfSplatReader(useBaseExtension); + + SpzGltfSplatReader r = new SpzGltfSplatReader(); ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); List splatsB = r.readList(is); diff --git a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java index a848c50..2eb42e4 100644 --- a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java +++ b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java @@ -46,24 +46,27 @@ public class JSplatRoundtrips public static void main(String[] args) throws IOException { LoggerUtil.initLogging(); - + Files.createDirectories(Paths.get(BASE_DIRECTORY)); FORMATS = Arrays.asList(SplatFormat.GSPLAT, SplatFormat.PLY_BINARY_LE, - SplatFormat.PLY_ASCII, SplatFormat.SPZ); + SplatFormat.PLY_ASCII, SplatFormat.SPZ, SplatFormat.SPZ_GLTF, + SplatFormat.GLTF); + +// writeAll("rotations2D", SplatGrids.createRotations2D()); +// writeAll("rotations", SplatGrids.createRotations()); +// writeAll("shs1", SplatGrids.createShs1()); +// writeAll("shs2", SplatGrids.createShs2()); +// writeAll("shs3", SplatGrids.createShs3()); +// writeAll("unitCube", UnitCubeSplats.create()); + - writeAll("rotations2D", SplatGrids.createRotations2D()); - writeAll("rotations", SplatGrids.createRotations()); - writeAll("shs1", SplatGrids.createShs1()); - writeAll("shs2", SplatGrids.createShs2()); - writeAll("shs3", SplatGrids.createShs3()); writeAll("unitCube", UnitCubeSplats.create()); - - //roundtripAll("unitCube"); + roundtripAll("unitCube"); - //float epsilon = 1e-6f; - //float epsilon = 0.02f; - //verifySingle(SplatGrids.createRotations(), SplatFormat.SPZ, epsilon); + // float epsilon = 1e-6f; + // float epsilon = 0.02f; + // verifySingle(SplatGrids.createRotations(), SplatFormat.SPZ, epsilon); } private static List createFormats() diff --git a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/SplatFormat.java b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/SplatFormat.java index 256d2b8..91a085a 100644 --- a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/SplatFormat.java +++ b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/SplatFormat.java @@ -38,5 +38,11 @@ enum SplatFormat /** * SPZ format embedded in binary glTF */ - SPZ_GLTF + SPZ_GLTF, + + /** + * glTF format (without compression), stored as binary glTF + */ + GLTF + } \ No newline at end of file diff --git a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java index 4c6a810..93dd252 100644 --- a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java +++ b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java @@ -18,6 +18,8 @@ import de.javagl.jsplat.Splat; import de.javagl.jsplat.SplatListReader; import de.javagl.jsplat.SplatListWriter; +import de.javagl.jsplat.io.gltf.GltfSplatReader; +import de.javagl.jsplat.io.gltf.GltfSplatWriter; import de.javagl.jsplat.io.gsplat.GsplatSplatReader; import de.javagl.jsplat.io.gsplat.GsplatSplatWriter; import de.javagl.jsplat.io.ply.PlySplatReader; @@ -59,6 +61,8 @@ static SplatListReader createReader(SplatFormat format) return new SpzSplatReader(); case SPZ_GLTF: return new SpzGltfSplatReader(); + case GLTF: + return new GltfSplatReader(); } logger.severe("Unknown format: " + format); return null; @@ -86,6 +90,8 @@ static SplatListWriter createWriter(SplatFormat format) return new SpzSplatWriter(); case SPZ_GLTF: return new SpzGltfSplatWriter(); + case GLTF: + return new GltfSplatWriter(); } logger.severe("Unknown format: " + format); return null; @@ -127,6 +133,7 @@ static String createFileExtension(SplatFormat format) case SPZ: return "spz"; case SPZ_GLTF: + case GLTF: return "glb"; } logger.severe("Unknown format: " + format); diff --git a/jsplat-io-gltf/README.md b/jsplat-io-gltf/README.md new file mode 100644 index 0000000..26a9364 --- /dev/null +++ b/jsplat-io-gltf/README.md @@ -0,0 +1,4 @@ +# jsplat-io-gltf + +Classes for reading and writing Gaussian splats in glTF format, +using the KHR_gaussian_splattting extension. \ No newline at end of file diff --git a/jsplat-io-gltf/pom.xml b/jsplat-io-gltf/pom.xml new file mode 100644 index 0000000..3d517db --- /dev/null +++ b/jsplat-io-gltf/pom.xml @@ -0,0 +1,41 @@ + + 4.0.0 + + jsplat-io-gltf + + ${project.groupId}:${project.artifactId} + A splat library for Java + https://github.com/javagl/JSplat + + + de.javagl + jsplat-parent + 0.0.1-SNAPSHOT + + + + + de.javagl + jsplat + 0.0.1-SNAPSHOT + + + de.javagl + jsplat-io-spz + 0.0.1-SNAPSHOT + + + de.javagl + jgltf-model + 2.0.4 + + + de.javagl + jgltf-model-builder + 2.0.4 + + + + \ No newline at end of file diff --git a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java new file mode 100644 index 0000000..d6d493d --- /dev/null +++ b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java @@ -0,0 +1,319 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import de.javagl.jgltf.model.AccessorData; +import de.javagl.jgltf.model.AccessorModel; +import de.javagl.jgltf.model.GltfModel; +import de.javagl.jgltf.model.MeshModel; +import de.javagl.jgltf.model.MeshPrimitiveModel; +import de.javagl.jgltf.model.io.GltfModelReader; +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.SplatListReader; +import de.javagl.jsplat.Splats; + +/** + * Implementation of a {@link SplatListReader} that reads glTF data with the + * KHR_gaussian_splatting Gaussian splat extension. + * + * NOTE: This class is preliminary and limited in terms of its capabilities: It + * assumes a single mesh primitive that contains splat attributes for now, and + * simply returns the splats from the first primitive. + */ +public final class GltfSplatReader implements SplatListReader +{ + /** + * The logger used in this class + */ + private static final Logger logger = + Logger.getLogger(GltfSplatReader.class.getName()); + + /** + * The extension name (and attribute prefix) + */ + private static final String NAME = "KHR_gaussian_splatting"; + + /** + * Creates a new instance + */ + public GltfSplatReader() + { + // Default constructor + } + + @Override + public List readList(InputStream inputStream) + throws IOException + { + GltfModelReader r = new GltfModelReader(); + GltfModel gltfModel = r.readWithoutReferences(inputStream); + + List meshModels = gltfModel.getMeshModels(); + for (MeshModel meshModel : meshModels) + { + List meshPrimitiveModels = + meshModel.getMeshPrimitiveModels(); + for (MeshPrimitiveModel meshPrimitiveModel : meshPrimitiveModels) + { + Map extensions = + meshPrimitiveModel.getExtensions(); + Object extension = extensions.get(NAME); + if (extension != null) + { + return readListFrom(meshPrimitiveModel); + } + } + } + throw new IOException( + "No mesh primitive with Gaussian splats found in input data"); + } + + /** + * Read a list of splats from the given mesh primitive model, assuming + * that it contains valid KHR_gaussian_splatting attributes. + * + * @param meshPrimitiveModel The mesh primitive model + * @return The splats + */ + private static List + readListFrom(MeshPrimitiveModel meshPrimitiveModel) + { + Map attributes = + meshPrimitiveModel.getAttributes(); + + String positionName = "POSITION"; + AccessorModel positionAccessor = attributes.get(positionName); + if (positionAccessor == null) + { + logger.severe("No POSITION accessor found in mesh primitive"); + return Collections.emptyList(); + } + + String scaleName = NAME + ":" + "SCALE"; + AccessorModel scaleAccessor = attributes.get(scaleName); + if (scaleAccessor == null) + { + logger.severe( + "No " + scaleName + " accessor found in mesh primitive"); + return Collections.emptyList(); + } + + String rotationName = NAME + ":" + "ROTATION"; + AccessorModel rotationAccessor = attributes.get(rotationName); + if (rotationAccessor == null) + { + logger.severe( + "No " + rotationName + " accessor found in mesh primitive"); + return Collections.emptyList(); + } + + String opacityName = NAME + ":" + "OPACITY"; + AccessorModel opacityAccessor = attributes.get(opacityName); + if (opacityAccessor == null) + { + logger.severe( + "No " + opacityName + " accessor found in mesh primitive"); + return Collections.emptyList(); + } + + List shAccessors = new ArrayList(); + int maxDegrees = 4; + int shDegree = 0; + for (int d = 0; d < maxDegrees; d++) + { + int numCoefficients = Splats.coefficientsForDegree(d); + for (int c = 0; c < numCoefficients; c++) + { + String name = NAME + ":" + "SH_DEGREE_" + d + "_COEF_" + c; + AccessorModel shAccessor = attributes.get(name); + if (shAccessor != null) + { + shAccessors.add(shAccessor); + shDegree = d; + } + } + } + + // There are no sanity checks here. It simply assumes that all the + // accessors have the same counts. Leave that to the validator... + int count = positionAccessor.getCount(); + List splats = new ArrayList(); + for (int i = 0; i < count; i++) + { + MutableSplat splat = Splats.create(shDegree); + splats.add(splat); + } + + FloatBuffer positionBuffer = readAsFloatBuffer(positionAccessor); + writePositions(positionBuffer, splats); + + FloatBuffer scaleBuffer = readAsFloatBuffer(scaleAccessor); + writeScales(scaleBuffer, splats); + + FloatBuffer rotationBuffer = readAsFloatBuffer(rotationAccessor); + writeRotations(rotationBuffer, splats); + + FloatBuffer opacityBuffer = readAsFloatBuffer(opacityAccessor); + writeOpacities(opacityBuffer, splats); + + int shIndex = 0; + for (int d = 0; d <= shDegree; d++) + { + int numCoefficients = Splats.coefficientsForDegree(d); + for (int c = 0; c < numCoefficients; c++) + { + AccessorModel shAccessor = shAccessors.get(shIndex); + FloatBuffer shBuffer = readAsFloatBuffer(shAccessor); + writeSh(shBuffer, splats, d, c); + shIndex++; + } + } + + return splats; + } + + /** + * Returns the data from the given accessor model as a float buffer, tightly + * packed. + * + * @param accessorModel The accessor model + * @return The buffer + */ + private static FloatBuffer readAsFloatBuffer(AccessorModel accessorModel) + { + AccessorData accessorData = accessorModel.getAccessorData(); + ByteBuffer inputByteBuffer = accessorData.createByteBuffer(); + return inputByteBuffer.asFloatBuffer(); + } + + /** + * Write the positions from the given buffer with splats.size() * 3 elements + * into the given splats + * + * @param b The buffer + * @param splats The splats + */ + private static void writePositions(FloatBuffer b, + List splats) + { + for (int i = 0; i < splats.size(); i++) + { + MutableSplat s = splats.get(i); + s.setPositionX(b.get(i * 3 + 0)); + s.setPositionY(b.get(i * 3 + 1)); + s.setPositionZ(b.get(i * 3 + 2)); + } + } + + /** + * Write the scales from the given buffer with splats.size() * 3 elements + * into the given splats + * + * @param b The buffer + * @param splats The splats + */ + private static void writeScales(FloatBuffer b, + List splats) + { + for (int i = 0; i < splats.size(); i++) + { + MutableSplat s = splats.get(i); + s.setScaleX(b.get(i * 3 + 0)); + s.setScaleY(b.get(i * 3 + 1)); + s.setScaleZ(b.get(i * 3 + 2)); + } + } + + /** + * Write the rotations from the given buffer with splats.size() * 4 elements + * into the given splats + * + * @param b The buffer + * @param splats The splats + */ + private static void writeRotations(FloatBuffer b, + List splats) + { + for (int i = 0; i < splats.size(); i++) + { + MutableSplat s = splats.get(i); + s.setRotationX(b.get(i * 4 + 0)); + s.setRotationY(b.get(i * 4 + 1)); + s.setRotationZ(b.get(i * 4 + 2)); + s.setRotationW(b.get(i * 4 + 3)); + } + } + + /** + * Write the opacities from the given buffer with splats.size() elements + * into the given splats + * + * @param b The buffer + * @param splats The splats + */ + private static void writeOpacities(FloatBuffer b, + List splats) + { + for (int i = 0; i < splats.size(); i++) + { + MutableSplat s = splats.get(i); + s.setOpacity(b.get(i)); + } + } + + /** + * Write the specified spherical harmonics from the given buffer with + * splats.size() * 3 elements into the given splats + * + * @param b The buffer + * @param splats The splats + * @param degree The degree + * @param coefficient The coefficient + */ + private static void writeSh(FloatBuffer b, + List splats, int degree, int coefficient) + { + int index = Splats.dimensionForCoefficient(degree, coefficient); + for (int i = 0; i < splats.size(); i++) + { + MutableSplat s = splats.get(i); + s.setShX(index, b.get(i * 3 + 0)); + s.setShY(index, b.get(i * 3 + 1)); + s.setShZ(index, b.get(i * 3 + 2)); + } + } +} diff --git a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java new file mode 100644 index 0000000..965ec13 --- /dev/null +++ b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java @@ -0,0 +1,287 @@ +/* + * 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.io.IOException; +import java.io.OutputStream; +import java.nio.FloatBuffer; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import de.javagl.jgltf.model.GltfModel; +import de.javagl.jgltf.model.SceneModel; +import de.javagl.jgltf.model.creation.AccessorModels; +import de.javagl.jgltf.model.creation.GltfModelBuilder; +import de.javagl.jgltf.model.creation.MeshPrimitiveBuilder; +import de.javagl.jgltf.model.creation.SceneModels; +import de.javagl.jgltf.model.impl.DefaultAccessorModel; +import de.javagl.jgltf.model.impl.DefaultExtensionsModel; +import de.javagl.jgltf.model.impl.DefaultGltfModel; +import de.javagl.jgltf.model.impl.DefaultMeshPrimitiveModel; +import de.javagl.jgltf.model.io.GltfModelWriter; +import de.javagl.jsplat.Splat; +import de.javagl.jsplat.SplatListWriter; +import de.javagl.jsplat.Splats; + +/** + * Implementation of a {@link SplatListWriter} that writes glTF data with + * Gaussian splats using the KHR_gaussian_splatting extension + * + * NOTE: This class is preliminary and limited in terms of its capabilities: It + * assumes a single mesh primitive that contains splat attributes for now. + */ +public final class GltfSplatWriter implements SplatListWriter +{ + /** + * The extension name (and attribute prefix) + */ + private static final String NAME = "KHR_gaussian_splatting"; + + /** + * Creates a new instance + */ + public GltfSplatWriter() + { + // Default constructor + } + + @Override + public void writeList(List splats, + OutputStream outputStream) throws IOException + { + GltfModel gltfModel = createGltfModel(splats); + GltfModelWriter w = new GltfModelWriter(); + w.writeBinary(gltfModel, outputStream); + } + + /** + * Create a binary glTF asset that uses the + * KHR_gaussian_splatting extension to define Gaussian Splats + * + * @param splats The splats + * @return The asset + */ + private DefaultGltfModel createGltfModel(List splats) + { + DefaultMeshPrimitiveModel meshPrimitiveModel = + createMeshPrimitiveModel(splats); + + SceneModel sceneModel = + SceneModels.createFromMeshPrimitive(meshPrimitiveModel); + + GltfModelBuilder b = GltfModelBuilder.create(); + b.addSceneModel(sceneModel); + DefaultGltfModel gltfModel = b.build(); + + DefaultExtensionsModel extensionsModel = gltfModel.getExtensionsModel(); + extensionsModel + .addExtensionsUsed(Arrays.asList(NAME)); + + return gltfModel; + } + + /** + * Create a mesh primitive model from the given splats, using the attribtes + * that are defined in the KHR_gaussian_splatting extension + * + * @param splats The splats + * @return The mesh primitive model + */ + private DefaultMeshPrimitiveModel + createMeshPrimitiveModel(List splats) + { + MeshPrimitiveBuilder mpb = MeshPrimitiveBuilder.create(); + + FloatBuffer position = readPositions(splats); + DefaultAccessorModel positionAccessor = + AccessorModels.createFloat3D(position); + mpb.addAttribute("POSITION", positionAccessor); + + FloatBuffer scale = readScales(splats); + DefaultAccessorModel scaleAccessor = + AccessorModels.createFloat3D(scale); + mpb.addAttribute(NAME + ":" + "SCALE", scaleAccessor); + + FloatBuffer rotation = readRotations(splats); + DefaultAccessorModel rotationAccessor = + AccessorModels.createFloat4D(rotation); + mpb.addAttribute(NAME + ":" + "ROTATION", rotationAccessor); + + FloatBuffer opacity = readOpacities(splats); + DefaultAccessorModel opacityAccessor = + AccessorModels.createFloatScalar(opacity); + mpb.addAttribute(NAME + ":" + "OPACITY", opacityAccessor); + + int shDegree = splats.get(0).getShDegree(); + for (int d = 0; d <= shDegree; d++) + { + int numCoefficients = Splats.coefficientsForDegree(d); + for (int c = 0; c < numCoefficients; c++) + { + FloatBuffer sh = readSh(splats, d, c); + DefaultAccessorModel shAccessor = + AccessorModels.createFloat3D(sh); + + String name = NAME + ":" + "SH_DEGREE_" + d + "_COEF_" + c; + mpb.addAttribute(name, shAccessor); + } + } + + mpb.setPoints(); + + // Create the mesh primitive + DefaultMeshPrimitiveModel meshPrimitiveModel = mpb.build(); + + // Manually add the extension (there is no model-level representation + // of this extension yet) + Map extension = createExtension(); + meshPrimitiveModel.addExtension(NAME, extension); + return meshPrimitiveModel; + } + + /** + * Create a map that represents an unspecified default + * KHR_gaussian_splatting extension object + * + * @return The extension object + */ + private Map createExtension() + { + Map extension = new LinkedHashMap(); + 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 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 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 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 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 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/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 index beaeb4a..7753410 100644 --- a/jsplat-io-spz-gltf/README.md +++ b/jsplat-io-spz-gltf/README.md @@ -1,4 +1,4 @@ # 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 +using the KHR_gaussian_splatting_compression_spz_2 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-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java b/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java index c26cda8..5311e37 100644 --- a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java +++ b/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java @@ -30,9 +30,11 @@ import java.io.InputStream; import java.nio.Buffer; import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.logging.Logger; import de.javagl.jgltf.impl.v2.BufferView; import de.javagl.jgltf.impl.v2.GlTF; @@ -50,36 +52,34 @@ /** * Implementation of a {@link SplatListReader} that reads glTF data with the - * SPZ-based Gaussian splat extension. - * - * NOTE: This class is preliminary, and tries to track the development of - * https://github.com/KhronosGroup/glTF/pull/2490. Some details of this - * class depend on compile-time flags that may change in the future. + * Gaussian splat data using the KHR_gaussian_splatting_compression_spz_2 + * extension. */ public final class SpzGltfSplatReader implements SplatListReader { /** - * Preliminary configuration settings + * The logger used in this class */ - private final SpzGltfConfig config; - + private static final Logger logger = + Logger.getLogger(SpzGltfSplatReader.class.getName()); + /** - * Creates a new instance + * The base extension name (and attribute prefix) */ - public SpzGltfSplatReader() - { - this(false); - } + private static final String BASE_NAME = "KHR_gaussian_splatting"; + + /** + * The extension name + */ + private static final String NAME = + "KHR_gaussian_splatting_compression_spz_2"; /** * Creates a new instance - * - * @param useBaseExtension Experimental - * @deprecated Experimental support for the base extension */ - public SpzGltfSplatReader(boolean useBaseExtension) + public SpzGltfSplatReader() { - this.config = new SpzGltfConfig(useBaseExtension); + // Default constructor } @Override @@ -157,83 +157,152 @@ private static ByteBuffer extractBufferViewData(GltfAssetV2 gltfAsset, byteOffset = 0; } Integer byteLength = bufferView.getByteLength(); - - // Workaround for NoSuchMethodError when compiling and + + // Workaround for NoSuchMethodError when compiling and // using with newer JDKs - ((Buffer)binaryData).limit(byteOffset + byteLength); - ((Buffer)binaryData).position(byteOffset); + ((Buffer) binaryData).limit(byteOffset + byteLength); + ((Buffer) binaryData).position(byteOffset); ByteBuffer spzData = binaryData.slice(); return spzData; } /** - * Returns the bufferView from the - * KHR_spz_gaussian_splats_compression extension object of the - * given primitive, or null if this object does not have this - * extension. + * Returns the bufferView index for the SPZ data from the given + * mesh primitive. + * + * This will try to fall back to different legacy extension versions. Not + * all of these legacy versions may be fully supported... * - * @param primitive The {@link MeshPrimitive} + * @param meshPrimitive The {@link MeshPrimitive} * @return The buffer view index */ - private Integer getExtensionBufferViewIndex(MeshPrimitive primitive) + private Integer getExtensionBufferViewIndex(MeshPrimitive meshPrimitive) + { + Integer bufferViewIndex = + getFinalExtensionBufferViewIndex(meshPrimitive); + if (bufferViewIndex != null) + { + return bufferViewIndex; + } + return getLegacyExtensionBufferViewIndex(meshPrimitive); + } + + /** + * Returns the index of the buffer view that stores the SPZ data, using the + * "final" form of the extension specification. + * + * If it can not be found, then null is returned. + * + * Details omitted here. + * + * @param meshPrimitive The mesh primitive + * @return The index + */ + private static Integer + getFinalExtensionBufferViewIndex(MeshPrimitive meshPrimitive) + { + Map extensions = meshPrimitive.getExtensions(); + if (extensions == null) + { + return null; + } + Map baseExtension = getMapOptional(extensions, BASE_NAME); + Map baseExtensionExtensions = + getMapOptional(baseExtension, "extensions"); + Map extension = getMapOptional(baseExtensionExtensions, NAME); + Integer bufferViewIndex = getIntegerOptional(extension, "bufferView"); + return bufferViewIndex; + } + + /** + * Returns the index of the buffer view that stores the SPZ data, using + * various "legacy" forms of the extension. + * + * If it can not be found, then null is returned. + * + * Details omitted here. + * + * @param meshPrimitive The mesh primitive + * @return The index + */ + private static Integer + getLegacyExtensionBufferViewIndex(MeshPrimitive meshPrimitive) { - Map extensions = primitive.getExtensions(); + Map extensions = meshPrimitive.getExtensions(); if (extensions == null) { return null; } - - if (config.USE_BASE_EXTENSION) + List legacyNames = Arrays.asList("KHR_spz_compression", + "KHR_spz_gaussian_splats_compression", + "KHR_gaussian_splatting_spz_compression"); + for (String legacyName : legacyNames) { - // Lots of ugly, untyped code here. - // It could be worse. - // It could be JavaScript. - Object baseExtensionObject = - extensions.get("KHR_gaussian_splatting"); - if (baseExtensionObject == null) - { - return null; - } - if (!(baseExtensionObject instanceof Map)) - { - return null; - } - Map baseExtension = (Map) baseExtensionObject; - Object baseExtensionsObject = baseExtension.get("extensions"); - if (!(baseExtensionsObject instanceof Map)) - { - return null; - } - Map baseExtensions = (Map) baseExtensionsObject; - Object extension = - baseExtensions.get(config.SPZ_EXTENSION_NAME); - if (!(extension instanceof Map)) - { - return null; - } - Map extensionMap = (Map) extension; - Object bufferViewIndexObject = extensionMap.get("bufferView"); - if (!(bufferViewIndexObject instanceof Number)) + Map extension = getMapOptional(extensions, legacyName); + Integer bufferViewIndex = + getIntegerOptional(extension, "bufferView"); + if (bufferViewIndex != null) { - return null; + logger.warning( + "Fetching SPZ data from legacy extension with name " + + legacyName + " - this extension version may " + + "not be fully supported"); + return bufferViewIndex; } - Number bufferViewIndexNumber = (Number) bufferViewIndexObject; - return bufferViewIndexNumber.intValue(); } - Object extension = - extensions.get(config.SPZ_EXTENSION_NAME); - if (!(extension instanceof Map)) + return null; + } + + /** + * Returns the value for the given key from the given map, if that value is + * a map. If the given map is null or the value is not a map, + * then null is returned. + * + * It could be worse. It could be JavaScript. + * + * @param map The map + * @param key The key + * @return The result + */ + private static Map getMapOptional(Map map, String key) + { + if (map == null) + { + return null; + } + Object object = map.get(key); + if (!(object instanceof Map)) + { + return null; + } + Map result = (Map) object; + return result; + } + + /** + * Returns the integer value for the given key from the given map, if that + * value is a number. If the given map is null or the value is + * not a number, then null is returned. + * + * It could be worse. It could be JavaScript. + * + * @param map The map + * @param key The key + * @return The result + */ + private static Integer getIntegerOptional(Map map, String key) + { + if (map == null) { return null; } - Map extensionMap = (Map) extension; - Object bufferViewIndexObject = extensionMap.get("bufferView"); - if (!(bufferViewIndexObject instanceof Number)) + Object object = map.get(key); + if (!(object instanceof Number)) { return null; } - Number bufferViewIndexNumber = (Number) bufferViewIndexObject; - return bufferViewIndexNumber.intValue(); + Number result = (Number) object; + return result.intValue(); } /** diff --git a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java b/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java index b58cc72..47af3a8 100644 --- a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java +++ b/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java @@ -49,43 +49,35 @@ import de.javagl.jgltf.model.io.v2.GltfAssetV2; import de.javagl.jsplat.Splat; import de.javagl.jsplat.SplatListWriter; +import de.javagl.jsplat.Splats; import de.javagl.jsplat.io.spz.GaussianCloudSplats; import de.javagl.jspz.GaussianCloud; import de.javagl.jspz.SpzWriter; import de.javagl.jspz.SpzWriters; /** - * Implementation of a {@link SplatListWriter} that writes glTF data with + * Implementation of a {@link SplatListWriter} that writes glTF data with * SPZ-compressed Gaussian splats. - * - * NOTE: This class is preliminary, and tries to track the development of - * https://github.com/KhronosGroup/glTF/pull/2490. Some details of this - * class depend on compile-time flags that may change in the future. */ public final class SpzGltfSplatWriter implements SplatListWriter { /** - * Preliminary configuration settings + * The base extension name (and attribute prefix) */ - private final SpzGltfConfig config; - + private static final String BASE_NAME = "KHR_gaussian_splatting"; + /** - * Creates a new instance + * The extension name */ - public SpzGltfSplatWriter() - { - this(false); - } + private static final String NAME = + "KHR_gaussian_splatting_compression_spz_2"; /** * Creates a new instance - * - * @param useBaseExtension Experimental - * @deprecated Experimental support for the base extension */ - public SpzGltfSplatWriter(boolean useBaseExtension) + public SpzGltfSplatWriter() { - this.config = new SpzGltfConfig(useBaseExtension); + // Default constructor } @Override @@ -109,11 +101,11 @@ public void writeList(List splats, /** * Create a binary glTF asset that uses the - * KHR_spz_gaussian_splats_compression extension to define + * KHR_gaussian_splatting_compression_spz_2 extension to define * Gaussian Splats * * @param numPoints The number of points - * @param shDegree The shpherical harmonics degree + * @param shDegree The spherical harmonics degree * @param boundingBox The bounding box * @param spzBytes The SPZ data * @return The asset @@ -163,11 +155,9 @@ private GltfAssetV2 createGltfAsset(int numPoints, int shDegree, gltf.addAccessors(scale); // Add the spherical harmonics accessors - int numCoeffsPerDegree[] = - { 3, 5, 7 }; for (int d = 0; d < shDegree; d++) { - int numCoeffs = numCoeffsPerDegree[d]; + int numCoeffs = Splats.coefficientsForDegree(d + 1); for (int n = 0; n < numCoeffs; n++) { Accessor sh = new Accessor(); @@ -196,40 +186,31 @@ private GltfAssetV2 createGltfAsset(int numPoints, int shDegree, // Add all accessors to the mesh primitive int a = 0; primitive.addAttributes("POSITION", a++); - primitive.addAttributes("COLOR_0", a++); - String ATTRIBUTE_PREFIX = config.ATTRIBUTE_PREFIX; - primitive.addAttributes(ATTRIBUTE_PREFIX + "ROTATION", a++); - primitive.addAttributes(ATTRIBUTE_PREFIX + "SCALE", a++); + primitive.addAttributes(BASE_NAME + ":" + "SCALE", a++); + primitive.addAttributes(BASE_NAME + ":" + "ROTATION", a++); + primitive.addAttributes(BASE_NAME + ":" + "OPACITY", a++); - for (int d = 0; d < shDegree; d++) + for (int d = 0; d <= shDegree; d++) { - int numCoeffs = numCoeffsPerDegree[d]; + int numCoeffs = Splats.coefficientsForDegree(d); for (int n = 0; n < numCoeffs; n++) { - String s = "SH_DEGREE_" + (d + 1) + "_COEF_" + n; - primitive.addAttributes(ATTRIBUTE_PREFIX + s, a++); + String s = "SH_DEGREE_" + d + "_COEF_" + n; + primitive.addAttributes(BASE_NAME + ":" + s, a++); } } // Add the extension object to the primitive Map spzExtension = new LinkedHashMap(); spzExtension.put("bufferView", 0); - if (config.USE_BASE_EXTENSION) - { - Map baseExtension = - new LinkedHashMap(); - Map innerExtensions = - new LinkedHashMap(); - innerExtensions.put(config.SPZ_EXTENSION_NAME, spzExtension); - baseExtension.put("extensions", innerExtensions); - primitive.addExtensions("KHR_gaussian_splatting", - baseExtension); - } - else - { - primitive.addExtensions(config.SPZ_EXTENSION_NAME, spzExtension); - } - + + Map baseExtension = new LinkedHashMap(); + Map innerExtensions = + new LinkedHashMap(); + innerExtensions.put(NAME, spzExtension); + baseExtension.put("extensions", innerExtensions); + primitive.addExtensions(BASE_NAME, baseExtension); + // Add the mesh Mesh mesh = new Mesh(); mesh.addPrimitives(primitive); @@ -238,35 +219,8 @@ private GltfAssetV2 createGltfAsset(int numPoints, int shDegree, // Add the node Node node = new Node(); node.setMesh(0); - - // The node needs a matrix, and it may have to be one that - // converts Z-up to Y-up, as of version 1.131 of CesiumJS - // @formatter:off - float zUpToYup[] = new float[] - { - 1.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 0.0f, -1.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f - }; - float identity[] = new float[] - { - 1.0f, 0.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 0.0f, 1.0f - }; - // @formatter:on - if (config.APPLY_UP_AXIS_TRANSFORMS) - { - node.setMatrix(zUpToYup); - } - else - { - node.setMatrix(identity); - } gltf.addNodes(node); - + // Add the scene Scene scene = new Scene(); scene.addNodes(0); @@ -274,18 +228,13 @@ private GltfAssetV2 createGltfAsset(int numPoints, int shDegree, gltf.setScene(0); // Add information about the used/required extension - if (config.USE_BASE_EXTENSION) - { - gltf.addExtensionsUsed("KHR_gaussian_splatting"); - gltf.addExtensionsRequired("KHR_gaussian_splatting"); - } - gltf.addExtensionsUsed(config.SPZ_EXTENSION_NAME); - gltf.addExtensionsRequired(config.SPZ_EXTENSION_NAME); - + gltf.addExtensionsUsed(BASE_NAME); + gltf.addExtensionsUsed(NAME); + // Build the actual asset ByteBuffer binaryData = ByteBuffer.wrap(spzBytes); GltfAssetV2 gltfAsset = new GltfAssetV2(gltf, binaryData); - + return gltfAsset; } diff --git a/jsplat/src/main/java/de/javagl/jsplat/Splats.java b/jsplat/src/main/java/de/javagl/jsplat/Splats.java index 7d8be7f..5dcce98 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 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 From 9595a4f82fab158207e6a2bf958e42b39c079546 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Nov 2025 13:56:05 +0100 Subject: [PATCH 2/7] Basic dequantization support --- .../jsplat/io/gltf/GltfSplatReader.java | 11 +- .../jsplat/io/gltf/GltfSplatWriter.java | 5 +- .../javagl/jsplat/io/gltf/Quantization.java | 226 ++++++++++++++++++ 3 files changed, 233 insertions(+), 9 deletions(-) create mode 100644 jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/Quantization.java diff --git a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java index d6d493d..2552a12 100644 --- a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java +++ b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatReader.java @@ -28,7 +28,6 @@ import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Collections; @@ -36,7 +35,6 @@ import java.util.Map; import java.util.logging.Logger; -import de.javagl.jgltf.model.AccessorData; import de.javagl.jgltf.model.AccessorModel; import de.javagl.jgltf.model.GltfModel; import de.javagl.jgltf.model.MeshModel; @@ -206,18 +204,19 @@ public List readList(InputStream inputStream) return splats; } + /** * Returns the data from the given accessor model as a float buffer, tightly - * packed. + * 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. */ private static FloatBuffer readAsFloatBuffer(AccessorModel accessorModel) { - AccessorData accessorData = accessorModel.getAccessorData(); - ByteBuffer inputByteBuffer = accessorData.createByteBuffer(); - return inputByteBuffer.asFloatBuffer(); + return Quantization.readAsFloatBuffer(accessorModel); } /** diff --git a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java index 965ec13..97dabcc 100644 --- a/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java +++ b/jsplat-io-gltf/src/main/java/de/javagl/jsplat/io/gltf/GltfSplatWriter.java @@ -100,8 +100,7 @@ private DefaultGltfModel createGltfModel(List splats) DefaultGltfModel gltfModel = b.build(); DefaultExtensionsModel extensionsModel = gltfModel.getExtensionsModel(); - extensionsModel - .addExtensionsUsed(Arrays.asList(NAME)); + extensionsModel.addExtensionsUsed(Arrays.asList(NAME)); return gltfModel; } @@ -168,7 +167,7 @@ private DefaultGltfModel createGltfModel(List splats) /** * Create a map that represents an unspecified default * KHR_gaussian_splatting extension object - * + * * @return The extension object */ private Map createExtension() 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 + } + +} From edfcbfa98afada20a0e8c57caebb2d28a2edd47d Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Nov 2025 14:28:27 +0100 Subject: [PATCH 3/7] Generalize application for glTF base extension --- jsplat-app/pom.xml | 5 + .../jsplat/app/ExtensionBasedSaveOptions.java | 190 ++++++++++++++++++ .../de/javagl/jsplat/app/GlbSaveOptions.java | 91 +++++++++ .../javagl/jsplat/app/GlbSplatListReader.java | 123 ++++++++++++ .../javagl/jsplat/app/JSplatApplication.java | 29 ++- .../de/javagl/jsplat/app/PlySaveOptions.java | 130 +----------- 6 files changed, 434 insertions(+), 134 deletions(-) create mode 100644 jsplat-app/src/main/java/de/javagl/jsplat/app/ExtensionBasedSaveOptions.java create mode 100644 jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSaveOptions.java create mode 100644 jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java diff --git a/jsplat-app/pom.xml b/jsplat-app/pom.xml index 342c20e..34440fe 100644 --- a/jsplat-app/pom.xml +++ b/jsplat-app/pom.xml @@ -38,6 +38,11 @@ jsplat-io-spz-gltf 0.0.1-SNAPSHOT
+ + de.javagl + jsplat-io-gltf + 0.0.1-SNAPSHOT + de.javagl jsplat-viewer diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/ExtensionBasedSaveOptions.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/ExtensionBasedSaveOptions.java new file mode 100644 index 0000000..bc6727b --- /dev/null +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/ExtensionBasedSaveOptions.java @@ -0,0 +1,190 @@ +/* + * 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.app; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.io.File; + +import javax.swing.BorderFactory; +import javax.swing.JFileChooser; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; + +import de.javagl.common.ui.GuiUtils; + +/** + * A base class for a panel that serves as an accessory for the save file + * chooser, enabled based on the current file extension. + */ +class ExtensionBasedSaveOptions extends JPanel +{ + /** + * Serial UID + */ + private static final long serialVersionUID = 370867818308684812L; + + /** + * The file chooser + */ + private JFileChooser fileChooser; + + /** + * The extension that should cause this panel to become enabled, including + * the dot. + */ + private String extensionWithDot; + + /** + * The file name text field, obtained from the file chooser (quirky) + */ + private JTextField fileNameTextField; + + /** + * Creates a new instance + * + * @param fileChooser The file chooser + * @param title The title + * @param extensionWithDot The file extension, including the dot + */ + ExtensionBasedSaveOptions(JFileChooser fileChooser, String title, + String extensionWithDot) + { + super(new BorderLayout()); + this.fileChooser = fileChooser; + this.extensionWithDot = extensionWithDot.toLowerCase(); + + setBorder(BorderFactory.createTitledBorder(title)); + + fileChooser.addPropertyChangeListener(e -> updateOptions()); + + fileNameTextField = findFileNameTextField(fileChooser); + installDocumentListener(); + + updateOptions(); + } + + /** + * Install a document listener to the file name text field to update the + * options when the file name changes + */ + private void installDocumentListener() + { + if (fileNameTextField == null) + { + return; + } + Document document = fileNameTextField.getDocument(); + document.addDocumentListener(new DocumentListener() + { + @Override + public void removeUpdate(DocumentEvent e) + { + updateOptions(); + } + + @Override + public void insertUpdate(DocumentEvent e) + { + updateOptions(); + } + + @Override + public void changedUpdate(DocumentEvent e) + { + updateOptions(); + } + }); + } + + /** + * Returns the current file name from the file name text field + * + * @return The file name + */ + private String getCurrentFileName() + { + if (fileNameTextField != null) + { + return fileNameTextField.getText(); + } + File file = fileChooser.getSelectedFile(); + if (file != null) + { + return file.toString(); + } + return null; + } + + /** + * Update the options based on the current file name + */ + private void updateOptions() + { + String fileName = getCurrentFileName(); + if (fileName == null) + { + GuiUtils.setDeepEnabled(this, false); + return; + } + boolean isPly = fileName.toLowerCase().endsWith(extensionWithDot); + GuiUtils.setDeepEnabled(this, isPly); + } + + /** + * Find the first text field in the given container, returning + * null if none is found + * + * @param container The container + * @return The text field + */ + private static JTextField findFileNameTextField(Container container) + { + for (Component component : container.getComponents()) + { + if (component instanceof JTextField) + { + return (JTextField) component; + } + else if (component instanceof Container) + { + Container childContainer = (Container) component; + JTextField textField = findFileNameTextField(childContainer); + if (textField != null) + { + return textField; + } + } + } + return null; + } + +} diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSaveOptions.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSaveOptions.java new file mode 100644 index 0000000..5a27a88 --- /dev/null +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSaveOptions.java @@ -0,0 +1,91 @@ +/* + * 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.app; + +import java.awt.BorderLayout; +import java.awt.GridLayout; + +import javax.swing.ButtonGroup; +import javax.swing.JFileChooser; +import javax.swing.JPanel; +import javax.swing.JRadioButton; + +/** + * A panel that serves as an accessory for the save file chooser, to select the + * compression that should be applied in GLB files + */ +class GlbSaveOptions extends ExtensionBasedSaveOptions +{ + /** + * Serial UID + */ + private static final long serialVersionUID = 37867883086984812L; + + /** + * The button for no compression + */ + private JRadioButton noneButton; + + /** + * The button for SPZ compression + */ + private JRadioButton spzButton; + + /** + * Creates a new instance + * + * @param fileChooser The file chooser + */ + GlbSaveOptions(JFileChooser fileChooser) + { + super(fileChooser, "GLB save options", ".glb"); + + noneButton = new JRadioButton("No compression"); + noneButton.setSelected(true); + spzButton = new JRadioButton("SPZ compression"); + + ButtonGroup group = new ButtonGroup(); + group.add(noneButton); + group.add(spzButton); + + JPanel panel = new JPanel(new GridLayout(0, 1)); + panel.add(noneButton); + panel.add(spzButton); + add(panel, BorderLayout.NORTH); + } + + /** + * Returns whether SPZ compression should be applied + * + * @return The state + */ + boolean shouldApplySpzCompression() + { + return spzButton.isSelected(); + } + +} diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java new file mode 100644 index 0000000..d2476be --- /dev/null +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java @@ -0,0 +1,123 @@ +/* + * 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.app; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import de.javagl.jgltf.impl.v2.GlTF; +import de.javagl.jgltf.model.io.GltfAsset; +import de.javagl.jgltf.model.io.GltfAssetReader; +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.SplatListReader; +import de.javagl.jsplat.io.gltf.GltfSplatReader; +import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatReader; + +/** + * Internal implementation of a SplatListReader for GLB data. + * + * Yeah, it's a bit quirky: It reads the GLB data and creates a glTF asset, + * checks whether this asset uses the SPZ compression extension, and either + * dispatches to a {@link GltfSplatReader} or {@link SpzGltfSplatReader}. + */ +class GlbSplatListReader implements SplatListReader +{ + @Override + public List readList(InputStream inputStream) + throws IOException + { + byte data[] = readFully(inputStream); + boolean usesSpz = usesSpz(data); + if (usesSpz) + { + System.out.println("Uses SPZ"); + SpzGltfSplatReader sr = new SpzGltfSplatReader(); + return sr.readList(new ByteArrayInputStream(data)); + } + System.out.println("Does not use SPZ"); + GltfSplatReader sr = new GltfSplatReader(); + return sr.readList(new ByteArrayInputStream(data)); + } + + /** + * Returns whether the given GLB data uses the + * KHR_gaussian_splatting_compression_spz_2 extension + * + * @param glbData The GLB data + * @return The result + */ + private static boolean usesSpz(byte glbData[]) + { + GltfAssetReader ar = new GltfAssetReader(); + try (ByteArrayInputStream bais = new ByteArrayInputStream(glbData)) + { + GltfAsset gltfAsset = ar.readWithoutReferences(bais); + GlTF gltf = (GlTF) gltfAsset.getGltf(); + List extensionsUsed = gltf.getExtensionsUsed(); + if (extensionsUsed == null) + { + return false; + } + if (extensionsUsed + .contains("KHR_gaussian_splatting_compression_spz_2")) + { + return true; + } + } + catch (IOException e) + { + return false; + } + return false; + } + + /** + * Read the given input stream into a byte array + * + * @param inputStream The input stream + * @return The byte array + * @throws IOException If an IO error occurs + */ + private static byte[] readFully(InputStream inputStream) throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] data = new byte[16384]; + while (true) + { + int read = inputStream.read(data, 0, data.length); + if (read == -1) + { + break; + } + baos.write(data, 0, read); + } + return baos.toByteArray(); + } +} diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java index 6b3a162..24298b2 100644 --- a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java @@ -26,6 +26,7 @@ */ package de.javagl.jsplat.app; +import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.io.File; @@ -49,6 +50,7 @@ import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; +import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.filechooser.FileNameExtensionFilter; @@ -65,7 +67,6 @@ import de.javagl.jsplat.io.ply.PlySplatWriter.PlyFormat; import de.javagl.jsplat.io.spz.SpzSplatReader; import de.javagl.jsplat.io.spz.SpzSplatWriter; -import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatReader; import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatWriter; import de.javagl.swing.tasks.SwingTask; import de.javagl.swing.tasks.SwingTaskExecutors; @@ -184,7 +185,12 @@ public void actionPerformed(ActionEvent e) * The {@link PlySaveOptions} used as an accessory for the save file chooser */ private PlySaveOptions plySaveOptions; - + + /** + * The {@link GlbSaveOptions} used as an accessory for the save file chooser + */ + private GlbSaveOptions glbSaveOptions; + /** * The {@link JSplatApplicationPanel} */ @@ -195,7 +201,6 @@ public void actionPerformed(ActionEvent e) */ private List currentSplats; - /** * Default constructor */ @@ -218,8 +223,14 @@ public void actionPerformed(ActionEvent e) "glb")); saveFileChooser = new JFileChooser("."); + + JPanel accessory = new JPanel(new GridLayout(0, 1)); plySaveOptions = new PlySaveOptions(saveFileChooser); - saveFileChooser.setAccessory(plySaveOptions); + glbSaveOptions = new GlbSaveOptions(saveFileChooser); + accessory.add(plySaveOptions); + accessory.add(glbSaveOptions); + saveFileChooser.setAccessory(accessory); + saveFileChooser.setFileFilter(new FileNameExtensionFilter( "Splat Files (.splat, .ply, .spz, .glb)", "splat", "ply", "spz", "glb")); @@ -394,7 +405,7 @@ private static SplatListReader findReader(String fileName) } if (name.endsWith("glb")) { - return new SpzGltfSplatReader(); + return new GlbSplatListReader(); } logger.warning( "Could not determine type from file name for '" + fileName + "'"); @@ -442,7 +453,7 @@ private void saveFile(File file) } saveInBackground(writer, file); } - + /** * Find a writer for the file with the given name, based on the file * extension, case-insensitively. If no writer can be found, then @@ -460,7 +471,7 @@ private SplatListWriter findWriter(String fileName) } if (name.endsWith("ply")) { - PlyFormat plyFormat = plySaveOptions.getPlyFormat(); + PlyFormat plyFormat = plySaveOptions.getPlyFormat(); return new PlySplatWriter(plyFormat); } if (name.endsWith("spz")) @@ -469,6 +480,10 @@ private SplatListWriter findWriter(String fileName) } if (name.endsWith("glb")) { + if (glbSaveOptions.shouldApplySpzCompression()) + { + return new SpzGltfSplatWriter(); + } return new SpzGltfSplatWriter(); } logger.warning( diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/PlySaveOptions.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/PlySaveOptions.java index 7528af8..bfb7364 100644 --- a/jsplat-app/src/main/java/de/javagl/jsplat/app/PlySaveOptions.java +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/PlySaveOptions.java @@ -27,39 +27,25 @@ package de.javagl.jsplat.app; import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.Container; import java.awt.GridLayout; -import java.io.File; -import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JFileChooser; import javax.swing.JPanel; import javax.swing.JRadioButton; -import javax.swing.JTextField; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.Document; -import de.javagl.common.ui.GuiUtils; import de.javagl.jsplat.io.ply.PlySplatWriter.PlyFormat; /** * A panel that serves as an accessory for the save file chooser, to select the * {@link PlyFormat} that should be used for saving a PLY file */ -class PlySaveOptions extends JPanel +class PlySaveOptions extends ExtensionBasedSaveOptions { /** * Serial UID */ - private static final long serialVersionUID = 3708678183086984812L; - - /** - * The file chooser - */ - private JFileChooser fileChooser; + private static final long serialVersionUID = 370867883086984812L; /** * The binary LE button @@ -76,11 +62,6 @@ class PlySaveOptions extends JPanel */ private JRadioButton asciiButton; - /** - * The file name text field, obtained from the file chooser (quirky) - */ - private JTextField fileNameTextField; - /** * Creates a new instance * @@ -88,10 +69,7 @@ class PlySaveOptions extends JPanel */ PlySaveOptions(JFileChooser fileChooser) { - super(new BorderLayout()); - this.fileChooser = fileChooser; - - setBorder(BorderFactory.createTitledBorder("PLY save options")); + super(fileChooser, "PLY save options", ".ply"); binaryLeButton = new JRadioButton("Binary (Little Endian)"); binaryLeButton.setSelected(true); @@ -108,13 +86,6 @@ class PlySaveOptions extends JPanel panel.add(binaryBeButton); panel.add(asciiButton); add(panel, BorderLayout.NORTH); - - fileChooser.addPropertyChangeListener(e -> updateOptions()); - - fileNameTextField = findFileNameTextField(fileChooser); - installDocumentListener(); - - updateOptions(); } /** @@ -139,99 +110,4 @@ PlyFormat getPlyFormat() return PlyFormat.BINARY_LITTLE_ENDIAN; } - /** - * Install a document listener to the file name text field to update the - * options when the file name changes - */ - private void installDocumentListener() - { - if (fileNameTextField == null) - { - return; - } - Document document = fileNameTextField.getDocument(); - document.addDocumentListener(new DocumentListener() - { - @Override - public void removeUpdate(DocumentEvent e) - { - updateOptions(); - } - - @Override - public void insertUpdate(DocumentEvent e) - { - updateOptions(); - } - - @Override - public void changedUpdate(DocumentEvent e) - { - updateOptions(); - } - }); - } - - /** - * Returns the current file name from the file name text field - * - * @return The file name - */ - private String getCurrentFileName() - { - if (fileNameTextField != null) - { - return fileNameTextField.getText(); - } - File file = fileChooser.getSelectedFile(); - if (file != null) - { - return file.toString(); - } - return null; - } - - /** - * Update the options based on the current file name - */ - private void updateOptions() - { - String fileName = getCurrentFileName(); - if (fileName == null) - { - GuiUtils.setDeepEnabled(this, false); - return; - } - boolean isPly = fileName.toLowerCase().endsWith(".ply"); - GuiUtils.setDeepEnabled(this, isPly); - } - - /** - * Find the first text field in the given container, returning - * null if none is found - * - * @param container The container - * @return The text field - */ - private static JTextField findFileNameTextField(Container container) - { - for (Component component : container.getComponents()) - { - if (component instanceof JTextField) - { - return (JTextField) component; - } - else if (component instanceof Container) - { - Container childContainer = (Container) component; - JTextField textField = findFileNameTextField(childContainer); - if (textField != null) - { - return textField; - } - } - } - return null; - } - } From f14a2bdd9cf3eea33d96d9ec8a42680aafae4acd Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Nov 2025 14:35:53 +0100 Subject: [PATCH 4/7] Consistent naming --- .gitignore | 10 +++++----- README.md | 3 ++- jsplat-app/pom.xml | 2 +- .../java/de/javagl/jsplat/app/GlbSplatListReader.java | 8 +++----- .../java/de/javagl/jsplat/app/JSplatApplication.java | 6 +++--- jsplat-examples/pom.xml | 2 +- .../examples/JSplatGltfRoundtripExperiments.java | 8 ++++---- .../de/javagl/jsplat/examples/JSplatRoundtrips.java | 4 ++-- .../src/main/java/de/javagl/jsplat/examples/Utils.java | 8 ++++---- {jsplat-io-spz-gltf => jsplat-io-gltf-spz}/README.md | 2 +- {jsplat-io-spz-gltf => jsplat-io-gltf-spz}/pom.xml | 2 +- .../javagl/jsplat/io/gltf/spz/GltfSpzSplatReader.java | 8 ++++---- .../javagl/jsplat/io/gltf/spz/GltfSpzSplatWriter.java | 6 +++--- .../de/javagl/jsplat/io/gltf/spz}/package-info.java | 2 +- 14 files changed, 35 insertions(+), 36 deletions(-) rename {jsplat-io-spz-gltf => jsplat-io-gltf-spz}/README.md (85%) rename {jsplat-io-spz-gltf => jsplat-io-gltf-spz}/pom.xml (96%) rename jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java => jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatReader.java (98%) rename jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java => jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatWriter.java (98%) rename {jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf => jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz}/package-info.java (60%) diff --git a/.gitignore b/.gitignore index 91a09d8..3eabad0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,16 +37,16 @@ /jsplat-io-spz/.settings /jsplat-io-spz/.classpath -/jsplat-io-spz-gltf/target -/jsplat-io-spz-gltf/.project -/jsplat-io-spz-gltf/.settings -/jsplat-io-spz-gltf/.classpath - /jsplat-io-gltf/target /jsplat-io-gltf/.project /jsplat-io-gltf/.settings /jsplat-io-gltf/.classpath +/jsplat-io-gltf-spz/target +/jsplat-io-gltf-spz/.project +/jsplat-io-gltf-spz/.settings +/jsplat-io-gltf-spz/.classpath + /jsplat-viewer/target /jsplat-viewer/.project /jsplat-viewer/.settings diff --git a/README.md b/README.md index eb05346..ec4db96 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ Java libraries for Gaussian splats. - [`jsplat-io-gsplat`](./jsplat-io-gsplat) - [gsplat](https://github.com/antimatter15/splat) format - [`jsplat-io-ply`](./jsplat-io-ply) - PLY files - [`jsplat-io-spz`](./jsplat-io-spz) - [SPZ](https://github.com/nianticlabs/spz) format - - [`jsplat-io-spz-gltf`](./jsplat-io-spz-gltf) - glTF with [`KHR_spz_gaussian_splats_compression`](https://github.com/KhronosGroup/glTF/pull/2490) + - [`jsplat-io-gltf`](./jsplat-io-gltf) - glTF with [`KHR_gaussian_splatting`](https://github.com/KhronosGroup/glTF/pull/2490) + - [`jsplat-io-gltf-spz`](./jsplat-io-gltf-spz) - glTF with [`KHR_spz_gaussian_splatting_compression_spz_2`](https://github.com/KhronosGroup/glTF/pull/2531) A basic viewer implementation can be found in [`jsplat-viewer`](./jsplat-viewer), with the actual implementation based on LWJGL in [`jsplat-viewer-lwjgl`](./jsplat-viewer-lwjgl). diff --git a/jsplat-app/pom.xml b/jsplat-app/pom.xml index 34440fe..df42899 100644 --- a/jsplat-app/pom.xml +++ b/jsplat-app/pom.xml @@ -35,7 +35,7 @@ de.javagl - jsplat-io-spz-gltf + jsplat-io-gltf-spz 0.0.1-SNAPSHOT diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java index d2476be..8b55689 100644 --- a/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/GlbSplatListReader.java @@ -38,14 +38,14 @@ import de.javagl.jsplat.MutableSplat; import de.javagl.jsplat.SplatListReader; import de.javagl.jsplat.io.gltf.GltfSplatReader; -import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatReader; +import de.javagl.jsplat.io.gltf.spz.GltfSpzSplatReader; /** * Internal implementation of a SplatListReader for GLB data. * * Yeah, it's a bit quirky: It reads the GLB data and creates a glTF asset, * checks whether this asset uses the SPZ compression extension, and either - * dispatches to a {@link GltfSplatReader} or {@link SpzGltfSplatReader}. + * dispatches to a {@link GltfSplatReader} or {@link GltfSpzSplatReader}. */ class GlbSplatListReader implements SplatListReader { @@ -57,11 +57,9 @@ public List readList(InputStream inputStream) boolean usesSpz = usesSpz(data); if (usesSpz) { - System.out.println("Uses SPZ"); - SpzGltfSplatReader sr = new SpzGltfSplatReader(); + GltfSpzSplatReader sr = new GltfSpzSplatReader(); return sr.readList(new ByteArrayInputStream(data)); } - System.out.println("Does not use SPZ"); GltfSplatReader sr = new GltfSplatReader(); return sr.readList(new ByteArrayInputStream(data)); } diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java index 24298b2..e50cf7e 100644 --- a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java @@ -60,6 +60,7 @@ import de.javagl.jsplat.app.common.UriLoading; import de.javagl.jsplat.app.common.UriTransferHandler; import de.javagl.jsplat.app.common.UriUtils; +import de.javagl.jsplat.io.gltf.spz.GltfSpzSplatWriter; import de.javagl.jsplat.io.gsplat.GsplatSplatReader; import de.javagl.jsplat.io.gsplat.GsplatSplatWriter; import de.javagl.jsplat.io.ply.PlySplatReader; @@ -67,7 +68,6 @@ import de.javagl.jsplat.io.ply.PlySplatWriter.PlyFormat; import de.javagl.jsplat.io.spz.SpzSplatReader; import de.javagl.jsplat.io.spz.SpzSplatWriter; -import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatWriter; import de.javagl.swing.tasks.SwingTask; import de.javagl.swing.tasks.SwingTaskExecutors; @@ -482,9 +482,9 @@ private SplatListWriter findWriter(String fileName) { if (glbSaveOptions.shouldApplySpzCompression()) { - return new SpzGltfSplatWriter(); + return new GltfSpzSplatWriter(); } - return new SpzGltfSplatWriter(); + return new GltfSpzSplatWriter(); } logger.warning( "Could not determine type from file name for '" + fileName + "'"); diff --git a/jsplat-examples/pom.xml b/jsplat-examples/pom.xml index 331a84f..5cf9b26 100644 --- a/jsplat-examples/pom.xml +++ b/jsplat-examples/pom.xml @@ -39,7 +39,7 @@ de.javagl - jsplat-io-spz-gltf + jsplat-io-gltf-spz 0.0.1-SNAPSHOT diff --git a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java index 843a6b5..42cfc75 100644 --- a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java +++ b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatGltfRoundtripExperiments.java @@ -13,8 +13,8 @@ import de.javagl.jsplat.MutableSplat; import de.javagl.jsplat.Splat; import de.javagl.jsplat.Splats; -import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatReader; -import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatWriter; +import de.javagl.jsplat.io.gltf.spz.GltfSpzSplatReader; +import de.javagl.jsplat.io.gltf.spz.GltfSpzSplatWriter; /** * Experiments for writing and reading SPZ-compressed Gaussian splats in glTF. @@ -34,11 +34,11 @@ public static void main(String[] args) throws IOException { List splatsA = UnitCubeSplats.create(); - SpzGltfSplatWriter w = new SpzGltfSplatWriter(); + GltfSpzSplatWriter w = new GltfSpzSplatWriter(); ByteArrayOutputStream os = new ByteArrayOutputStream(); w.writeList(splatsA, os); - SpzGltfSplatReader r = new SpzGltfSplatReader(); + GltfSpzSplatReader r = new GltfSpzSplatReader(); ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray()); List splatsB = r.readList(is); diff --git a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java index 2eb42e4..40ecc60 100644 --- a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java +++ b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/JSplatRoundtrips.java @@ -50,8 +50,8 @@ public static void main(String[] args) throws IOException Files.createDirectories(Paths.get(BASE_DIRECTORY)); FORMATS = Arrays.asList(SplatFormat.GSPLAT, SplatFormat.PLY_BINARY_LE, - SplatFormat.PLY_ASCII, SplatFormat.SPZ, SplatFormat.SPZ_GLTF, - SplatFormat.GLTF); + SplatFormat.PLY_BINARY_BE, SplatFormat.PLY_ASCII, SplatFormat.SPZ, + SplatFormat.SPZ_GLTF, SplatFormat.GLTF); // writeAll("rotations2D", SplatGrids.createRotations2D()); // writeAll("rotations", SplatGrids.createRotations()); diff --git a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java index 93dd252..f273db3 100644 --- a/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java +++ b/jsplat-examples/src/main/java/de/javagl/jsplat/examples/Utils.java @@ -20,6 +20,8 @@ import de.javagl.jsplat.SplatListWriter; import de.javagl.jsplat.io.gltf.GltfSplatReader; import de.javagl.jsplat.io.gltf.GltfSplatWriter; +import de.javagl.jsplat.io.gltf.spz.GltfSpzSplatReader; +import de.javagl.jsplat.io.gltf.spz.GltfSpzSplatWriter; import de.javagl.jsplat.io.gsplat.GsplatSplatReader; import de.javagl.jsplat.io.gsplat.GsplatSplatWriter; import de.javagl.jsplat.io.ply.PlySplatReader; @@ -27,8 +29,6 @@ import de.javagl.jsplat.io.ply.PlySplatWriter.PlyFormat; import de.javagl.jsplat.io.spz.SpzSplatReader; import de.javagl.jsplat.io.spz.SpzSplatWriter; -import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatReader; -import de.javagl.jsplat.io.spz.gltf.SpzGltfSplatWriter; /** * Utilities for the JSplat examples @@ -60,7 +60,7 @@ static SplatListReader createReader(SplatFormat format) case SPZ: return new SpzSplatReader(); case SPZ_GLTF: - return new SpzGltfSplatReader(); + return new GltfSpzSplatReader(); case GLTF: return new GltfSplatReader(); } @@ -89,7 +89,7 @@ static SplatListWriter createWriter(SplatFormat format) case SPZ: return new SpzSplatWriter(); case SPZ_GLTF: - return new SpzGltfSplatWriter(); + return new GltfSpzSplatWriter(); case GLTF: return new GltfSplatWriter(); } diff --git a/jsplat-io-spz-gltf/README.md b/jsplat-io-gltf-spz/README.md similarity index 85% rename from jsplat-io-spz-gltf/README.md rename to jsplat-io-gltf-spz/README.md index 7753410..5aa4659 100644 --- a/jsplat-io-spz-gltf/README.md +++ b/jsplat-io-gltf-spz/README.md @@ -1,4 +1,4 @@ -# jsplat-io-spz-gltf +# jsplat-io-gltf-spz Classes for reading and writing Gaussian splats in glTF format, using the KHR_gaussian_splatting_compression_spz_2 extension. \ No newline at end of file diff --git a/jsplat-io-spz-gltf/pom.xml b/jsplat-io-gltf-spz/pom.xml similarity index 96% rename from jsplat-io-spz-gltf/pom.xml rename to jsplat-io-gltf-spz/pom.xml index f5256b8..982736e 100644 --- a/jsplat-io-spz-gltf/pom.xml +++ b/jsplat-io-gltf-spz/pom.xml @@ -3,7 +3,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - jsplat-io-spz-gltf + jsplat-io-gltf-spz ${project.groupId}:${project.artifactId} A splat library for Java diff --git a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java b/jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatReader.java similarity index 98% rename from jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java rename to jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatReader.java index 5311e37..0e2f992 100644 --- a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatReader.java +++ b/jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatReader.java @@ -24,7 +24,7 @@ * 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; +package de.javagl.jsplat.io.gltf.spz; import java.io.IOException; import java.io.InputStream; @@ -55,13 +55,13 @@ * Gaussian splat data using the KHR_gaussian_splatting_compression_spz_2 * extension. */ -public final class SpzGltfSplatReader implements SplatListReader +public final class GltfSpzSplatReader implements SplatListReader { /** * The logger used in this class */ private static final Logger logger = - Logger.getLogger(SpzGltfSplatReader.class.getName()); + Logger.getLogger(GltfSpzSplatReader.class.getName()); /** * The base extension name (and attribute prefix) @@ -77,7 +77,7 @@ public final class SpzGltfSplatReader implements SplatListReader /** * Creates a new instance */ - public SpzGltfSplatReader() + public GltfSpzSplatReader() { // Default constructor } diff --git a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java b/jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatWriter.java similarity index 98% rename from jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java rename to jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatWriter.java index 47af3a8..29b24b3 100644 --- a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/SpzGltfSplatWriter.java +++ b/jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/GltfSpzSplatWriter.java @@ -24,7 +24,7 @@ * 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; +package de.javagl.jsplat.io.gltf.spz; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -59,7 +59,7 @@ * Implementation of a {@link SplatListWriter} that writes glTF data with * SPZ-compressed Gaussian splats. */ -public final class SpzGltfSplatWriter implements SplatListWriter +public final class GltfSpzSplatWriter implements SplatListWriter { /** * The base extension name (and attribute prefix) @@ -75,7 +75,7 @@ public final class SpzGltfSplatWriter implements SplatListWriter /** * Creates a new instance */ - public SpzGltfSplatWriter() + public GltfSpzSplatWriter() { // Default constructor } diff --git a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/package-info.java b/jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/package-info.java similarity index 60% rename from jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/package-info.java rename to jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/package-info.java index 49399af..e3e1aa3 100644 --- a/jsplat-io-spz-gltf/src/main/java/de/javagl/jsplat/io/spz/gltf/package-info.java +++ b/jsplat-io-gltf-spz/src/main/java/de/javagl/jsplat/io/gltf/spz/package-info.java @@ -1,4 +1,4 @@ /** * Readers and writers for Gaussian splats in glTF */ -package de.javagl.jsplat.io.spz.gltf; +package de.javagl.jsplat.io.gltf.spz; From e8f2e206072379bcb86aace708b4561b0eeb24bb Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Nov 2025 14:38:33 +0100 Subject: [PATCH 5/7] Return proper writer for base extension --- .../src/main/java/de/javagl/jsplat/app/JSplatApplication.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java index e50cf7e..2033099 100644 --- a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplication.java @@ -60,6 +60,7 @@ import de.javagl.jsplat.app.common.UriLoading; import de.javagl.jsplat.app.common.UriTransferHandler; import de.javagl.jsplat.app.common.UriUtils; +import de.javagl.jsplat.io.gltf.GltfSplatWriter; import de.javagl.jsplat.io.gltf.spz.GltfSpzSplatWriter; import de.javagl.jsplat.io.gsplat.GsplatSplatReader; import de.javagl.jsplat.io.gsplat.GsplatSplatWriter; @@ -484,7 +485,7 @@ private SplatListWriter findWriter(String fileName) { return new GltfSpzSplatWriter(); } - return new GltfSpzSplatWriter(); + return new GltfSplatWriter(); } logger.warning( "Could not determine type from file name for '" + fileName + "'"); From ce771070725e5cfdb63ef733cbd40a079382b05b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Nov 2025 14:47:57 +0100 Subject: [PATCH 6/7] Fix JavaDoc formatting --- jsplat/src/main/java/de/javagl/jsplat/Splats.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsplat/src/main/java/de/javagl/jsplat/Splats.java b/jsplat/src/main/java/de/javagl/jsplat/Splats.java index 5dcce98..600555f 100644 --- a/jsplat/src/main/java/de/javagl/jsplat/Splats.java +++ b/jsplat/src/main/java/de/javagl/jsplat/Splats.java @@ -285,7 +285,7 @@ public static int coefficientsForDegree(int degree) * Returns the index of the dimension for the specified spherical harmonics * coefficient. * - *
+     * 

      * (degree 0, coefficient 0) : 0
      * 
      * (degree 1, coefficient 0) : 1
@@ -301,7 +301,7 @@ public static int coefficientsForDegree(int degree)
      * (degree 3, coefficient 0) : 9
      * ...
      * (degree 3, coefficient 6) : 15
-     * 
+ *
* * @param degree The degree * @param coefficient The coefficient From 24162f36bf6f00bdb6ac007dde9562ec7b778ebe Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Nov 2025 14:48:07 +0100 Subject: [PATCH 7/7] Proper modules definition --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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