diff --git a/.gitignore b/.gitignore index 19c8549..f6e907d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,17 @@ /jsplat-examples/.project /jsplat-examples/.settings /jsplat-examples/.classpath - /jsplat-examples/data/* !/jsplat-examples/data/unitCube-ascii.ply +/jsplat-examples-gltf/target +/jsplat-examples-gltf/.project +/jsplat-examples-gltf/.settings +/jsplat-examples-gltf/.classpath +/jsplat-examples-gltf/data/* + + /jsplat/target /jsplat/.project /jsplat/.settings diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/DataSet.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/DataSet.java index 5b49268..c7d2423 100644 --- a/jsplat-app/src/main/java/de/javagl/jsplat/app/DataSet.java +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/DataSet.java @@ -99,6 +99,16 @@ void setTransform(Transform transform) { this.currentTransform = transform; double[] matrix = Transforms.toMatrix(transform); + setTransformMatrixInternal(matrix); + } + + /** + * Highly preliminary, internal method to set the transform matrix. + * + * @param matrix The matrix + */ + void setTransformMatrixInternal(double matrix[]) + { int shDimensions = Splats.dimensionsForDegree(shDegree); Consumer t = SplatTransforms.createTransform(matrix, shDimensions); 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 ee99b99..35be306 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 @@ -71,7 +71,7 @@ public List readList(InputStream inputStream) * @param glbData The GLB data * @return The result */ - private static boolean usesSpz(byte glbData[]) + static boolean usesSpz(byte glbData[]) { GltfAssetReader ar = new GltfAssetReader(); try (ByteArrayInputStream bais = new ByteArrayInputStream(glbData)) @@ -103,7 +103,7 @@ private static boolean usesSpz(byte glbData[]) * @return The byte array * @throws IOException If an IO error occurs */ - private static byte[] readFully(InputStream inputStream) throws IOException + static byte[] readFully(InputStream inputStream) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] data = new byte[16384]; 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 4d3c0cc..bccc8fd 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 @@ -30,6 +30,7 @@ import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -41,7 +42,10 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.function.Function; import java.util.logging.Logger; @@ -57,6 +61,7 @@ import javax.swing.JSeparator; import javax.swing.filechooser.FileNameExtensionFilter; +import de.javagl.jgltf.model.NodeModel; import de.javagl.jsplat.MutableSplat; import de.javagl.jsplat.Splat; import de.javagl.jsplat.SplatListReader; @@ -64,7 +69,10 @@ 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.GltfSplatReader; +import de.javagl.jsplat.io.gltf.GltfSplatReader.Result; 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; @@ -361,7 +369,15 @@ void openUrisInBackground(List uris) try { long beforeNs = System.nanoTime(); - List result = reader.readList(inputStream); + List result; + if (reader instanceof GlbSplatListReader) + { + result = processGltfSplats(inputStream); + } + else + { + result = reader.readList(inputStream); + } long afterNs = System.nanoTime(); double ms = (afterNs - beforeNs) / 1e6; logger.info( @@ -380,6 +396,68 @@ void openUrisInBackground(List uris) }); } + /** + * Quirky, proof-of-concept function for splats that are contained in + * glTF with node animations + * + * @param inputStream The input stream + * @return An empty list. I said it's quirky... + * @throws IOException If an IO error occurs + */ + private List processGltfSplats(InputStream inputStream) + throws IOException + { + byte data[] = GlbSplatListReader.readFully(inputStream); + boolean usesSpz = GlbSplatListReader.usesSpz(data); + if (usesSpz) + { + GltfSpzSplatReader sr = new GltfSpzSplatReader(); + return sr.readList(new ByteArrayInputStream(data)); + } + GltfSplatReader sr = new GltfSplatReader(); + Result result = sr.read(new ByteArrayInputStream(data)); + Map> nodeSplatLists = result.splatLists; + + // The result here contains mappings from nodes to splat lists. + // Assign consecutive names to the nodes, and add the splat + // lists to the application panel + int counterA = 0; + List names = new ArrayList(); + List> splatLists = + new ArrayList>(); + for (Entry> entry : nodeSplatLists + .entrySet()) + { + String name = "node-" + counterA; + List splatList = entry.getValue(); + names.add(name); + splatLists.add(splatList); + counterA++; + } + + // Returns a mapping from names to data sets + Map dataSets = + applicationPanel.addSplatLists(names, splatLists); + + // Convert the mapping from "names to data sets" into a mapping + // of "nodes to data sets", and pass that to the animation handler + Map nodeDataSets = + new LinkedHashMap(); + int counterB = 0; + for (Entry> entry : nodeSplatLists + .entrySet()) + { + String name = "node-" + counterB; + DataSet dataSet = dataSets.get(name); + NodeModel key = entry.getKey(); + nodeDataSets.put(key, dataSet); + counterB++; + } + SplatAnimationHandler.handleAnimation(result.gltfModel, nodeDataSets, + () -> applicationPanel.updateSplats()); + return Collections.emptyList(); + } + /** * Process the given splats that have been loaded from a URI * diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplicationPanel.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplicationPanel.java index 2f4fd3e..8988311 100644 --- a/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplicationPanel.java +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/JSplatApplicationPanel.java @@ -35,8 +35,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.function.Supplier; import java.util.logging.Logger; @@ -464,20 +466,31 @@ private JPanel createTransformPanel() transformPanel.addChangeListener(listener); return transformPanel; } + + /** + * Update the splats in the viewer for the case that the properties + * of the splat lists changed + */ + void updateSplats() + { + splatViewer.updateSplats(); + } /** * Add the splats that should be displayed * * @param names The name * @param splatLists The splat lists + * @return A mapping from names to data sets */ - void addSplatLists(List names, + Map addSplatLists(List names, List> splatLists) { + Map dataSets = new LinkedHashMap(); if (splatViewer == null) { logger.warning("No SplatViewer was created"); - return; + return dataSets; } List> currentSplatLists = @@ -492,6 +505,8 @@ void addSplatLists(List names, List currentSplats = dataSet.getCurrentSplats(); currentSplatLists.add(currentSplats); + + dataSets.put(name, dataSet); } splatViewer.addSplatLists(currentSplatLists); @@ -501,6 +516,7 @@ void addSplatLists(List names, doFit = false; } updateStatus(); + return dataSets; } /** diff --git a/jsplat-app/src/main/java/de/javagl/jsplat/app/SplatAnimationHandler.java b/jsplat-app/src/main/java/de/javagl/jsplat/app/SplatAnimationHandler.java new file mode 100644 index 0000000..c783bf5 --- /dev/null +++ b/jsplat-app/src/main/java/de/javagl/jsplat/app/SplatAnimationHandler.java @@ -0,0 +1,113 @@ +/* + * 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.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicLong; + +import de.javagl.jgltf.model.GltfAnimations; +import de.javagl.jgltf.model.GltfModel; +import de.javagl.jgltf.model.NodeModel; +import de.javagl.jgltf.model.animation.Animation; +import de.javagl.jgltf.model.animation.AnimationManager; +import de.javagl.jgltf.model.animation.AnimationManager.AnimationPolicy; +import de.javagl.jgltf.model.animation.AnimationRunner; + +/** + * HIGHLY preliminary, internal class for handling splats that are attached to + * animated glTF nodes. + */ +class SplatAnimationHandler +{ + /** + * EXPERIMENTAL: Handle the animations in the given model + * + * @param gltfModel The model + * @param nodeDataSets Mapping from nodes to splat data sets + * @param callback A callback for updates + */ + public static void handleAnimation(GltfModel gltfModel, + Map nodeDataSets, Runnable callback) + { + List animations = GltfAnimations + .createModelAnimations(gltfModel.getAnimationModels()); + if (animations.isEmpty()) + { + return; + } + + long updateNanos = 50 * 1000 * 1000; + AtomicLong previousUpdateNs = new AtomicLong(); + AnimationManager animationManager = + GltfAnimations.createAnimationManager(AnimationPolicy.LOOP); + animationManager.addAnimationManagerListener(a -> + { + long p = previousUpdateNs.get(); + long c = System.nanoTime(); + if (c < p + updateNanos) + { + return; + } + previousUpdateNs.set(c); + for (Entry entry : nodeDataSets.entrySet()) + { + NodeModel nodeModel = entry.getKey(); + DataSet dataSet = entry.getValue(); + float[] transformMatrix = new float[16]; + nodeModel.computeGlobalTransform(transformMatrix); + dataSet.setTransformMatrixInternal(toDouble(transformMatrix)); + } + callback.run(); + }); + AnimationRunner animationRunner = new AnimationRunner(animationManager); + animationManager.addAnimations(animations); + animationRunner.start(); + } + + /** + * Convert the given array into a double array + * + * @param array The array + * @return The result + */ + private static double[] toDouble(float array[]) + { + if (array == null) + { + return null; + } + double result[] = new double[array.length]; + for (int i = 0; i < array.length; i++) + { + result[i] = array[i]; + } + return result; + } + +} diff --git a/jsplat-examples-gltf/README.md b/jsplat-examples-gltf/README.md new file mode 100644 index 0000000..84261db --- /dev/null +++ b/jsplat-examples-gltf/README.md @@ -0,0 +1,3 @@ +# jsplat-examples-gltf + +Examples for creating glTF files with splats diff --git a/jsplat-examples-gltf/pom.xml b/jsplat-examples-gltf/pom.xml new file mode 100644 index 0000000..f914629 --- /dev/null +++ b/jsplat-examples-gltf/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + + jsplat-examples-gltf + + + de.javagl + jsplat-parent + 0.0.1-SNAPSHOT + + + + + de.javagl + jsplat + 0.0.1-SNAPSHOT + + + de.javagl + jsplat-processing + 0.0.1-SNAPSHOT + + + de.javagl + jsplat-examples + 0.0.1-SNAPSHOT + + + de.javagl + jsplat-io-ply + 0.0.1-SNAPSHOT + + + de.javagl + jsplat-io-gltf + 0.0.1-SNAPSHOT + + + de.javagl + jsplat-io-gltf-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-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/CreateSplatGltfExamples.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/CreateSplatGltfExamples.java new file mode 100644 index 0000000..a7355c8 --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/CreateSplatGltfExamples.java @@ -0,0 +1,413 @@ +/* + * 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.examples.gltf; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +import de.javagl.jgltf.model.impl.DefaultMeshPrimitiveModel; +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.SplatListWriter; +import de.javagl.jsplat.Splats; +import de.javagl.jsplat.examples.UnitCubeSplats; +import de.javagl.jsplat.examples.UnitShSplats; +import de.javagl.jsplat.io.ply.PlySplatWriter; +import de.javagl.jsplat.io.ply.PlySplatWriter.PlyFormat; +import de.javagl.jsplat.io.spz.SpzSplatWriter; +import de.javagl.jsplat.processing.SplatTransforms; + +/** + * Methods to create splat glTF example files + */ +public class CreateSplatGltfExamples +{ + /** + * The entry point of the application + * + * @param args Not used + * @throws IOException When an IO error occurs + */ + public static void main(String[] args) throws IOException + { + createGltfs(); + //createCustom(); + } + + /** + * Create the main glTF examples + * + * @throws IOException When an IO error occurs + */ + private static void createGltfs() throws IOException + { + String baseDir = "./data/"; + Files.createDirectories(Paths.get(baseDir)); + + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + createSplatsInMesh(w); + w.write(new FileOutputStream(baseDir + "SplatsInMesh.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + createMeshInSplats(w); + w.write(new FileOutputStream(baseDir + "MeshInSplats.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + createShGrid(w); + w.write(new FileOutputStream(baseDir + "ShGrid.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + w.addSplats(SplatRotationTests.createRotationsX(), null); + w.write(new FileOutputStream(baseDir + "RotationsX.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + w.addSplats(SplatRotationTests.createRotationsY(), null); + w.write(new FileOutputStream(baseDir + "RotationsY.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + w.addSplats(SplatRotationTests.createRotationsZ(), null); + w.write(new FileOutputStream(baseDir + "RotationsZ.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + w.addSplats(SplatScaleTests.createScales(), null); + w.write(new FileOutputStream(baseDir + "Scales.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + createMixedDegrees(w); + w.write(new FileOutputStream(baseDir + "MixedDegrees.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + w.addSplats(SplatDepthTests.createDepthTest(), null); + w.write(new FileOutputStream(baseDir + "Depths.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + createScaledScales(w); + w.write(new FileOutputStream(baseDir + "ScaledScales.glb")); + } + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + w.addSplats(SplatBlendingTests.createBlendingTest(), null); + w.write(new FileOutputStream(baseDir + "Blending.glb")); + } + + { + GenericGltfSplatWriter w = new GenericGltfSplatWriter(); + createAnimated(w); + w.write(new FileOutputStream(baseDir + "Animated.glb")); + } + + } + + /** + * Create some highly specific tests + * + * @throws IOException When an IO error occurs + */ + private static void createCustom() throws IOException + { + String baseDir = "./data/"; + Files.createDirectories(Paths.get(baseDir)); + + { + SplatListWriter w = new SpzSplatWriter(); + List splats = SplatRotationTests.createRotationsX(); + w.writeList(splats, + new FileOutputStream(baseDir + "RotationsX.spz")); + } + { + SplatListWriter w = new SpzSplatWriter(); + List splats = SplatRotationTests.createRotationsY(); + w.writeList(splats, + new FileOutputStream(baseDir + "RotationsY.spz")); + } + { + SplatListWriter w = new SpzSplatWriter(); + List splats = SplatRotationTests.createRotationsZ(); + w.writeList(splats, + new FileOutputStream(baseDir + "RotationsZ.spz")); + } + { + SplatListWriter w = + new PlySplatWriter(PlyFormat.BINARY_LITTLE_ENDIAN); + List splats = SplatDepthTests.createDepthTest(); + w.writeList(splats, new FileOutputStream(baseDir + "Depths.ply")); + } + { + SplatListWriter w = + new PlySplatWriter(PlyFormat.BINARY_LITTLE_ENDIAN); + List splats = SplatBlendingTests.createBlendingTest(); + w.writeList(splats, new FileOutputStream(baseDir + "Blending.ply")); + } + { + SplatListWriter w = new PlySplatWriter(PlyFormat.ASCII); + List splats = SplatBlendingTests.createBlendingTest(); + w.writeList(splats, + new FileOutputStream(baseDir + "Blending-ASCII.ply")); + } + } + + /** + * Fill the given writer with an example splat data set and a mesh that + * fully encloses the splats. + * + * @param w The writer + */ + private static void createSplatsInMesh(GenericGltfSplatWriter w) + { + List splats = UnitCubeSplats.create(); + w.addSplats(splats, null); + + DefaultMeshPrimitiveModel p = GltfModelElements.createUnitCube(); + double[] matrix = Matrices.createMatrixScale(20.0f); + w.addMeshPrimitive(p, matrix); + + } + + /** + * Fill the given writer with an example splat data set and a mesh that is + * contained in the splats. + * + * @param w The writer + */ + private static void createMeshInSplats(GenericGltfSplatWriter w) + { + List splats = UnitCubeSplats.create(); + w.addSplats(splats, null); + + DefaultMeshPrimitiveModel p = GltfModelElements.createUnitCube(); + double[] matrix = Matrices.createMatrixScale(5.0f); + w.addMeshPrimitive(p, matrix); + } + + /** + * Fill the given writer with an example splat data set that consists of 6 + * spherical harmonics test splat mesh primitives. + * + * Each mesh primitive will contain splats that indicate the corners of a + * cube, and contain one splat at the center that looks red from the right, + * cyan from the left, green from the top, magenta from the bottom, blue + * from the front, yellow from the back. + * + * The primitives will be attached to matrices that describe rotations of + * (x:0), (x:90), (x:180), (y:-90), (y:90), and (x:270) degrees, causing all + * different faces to face the viewer. + * + * @param w The writer + */ + private static void createShGrid(GenericGltfSplatWriter w) + { + List splats = UnitShSplats.createDeg3(); + { + double[] matrix = Matrices.createMatrixX(0, -150, -75, 0); + w.addSplats(splats, matrix); + } + { + double[] matrix = Matrices.createMatrixX(90, -0, -75, 0); + w.addSplats(splats, matrix); + } + { + double[] matrix = Matrices.createMatrixX(180, 150, -75, 0); + w.addSplats(splats, matrix); + } + + { + double[] matrix = Matrices.createMatrixY(-90, -150, 75, 0); + w.addSplats(splats, matrix); + } + { + double[] matrix = Matrices.createMatrixY(90, -0, 75, 0); + w.addSplats(splats, matrix); + } + { + double[] matrix = Matrices.createMatrixX(270, 150, 75, 0); + w.addSplats(splats, matrix); + } + } + + /** + * Fill the given writer with two splat mesh primitives, the first one + * having SH degree 3, and the second one having SH degree 2. + * + * @param w The writer + */ + private static void createMixedDegrees(GenericGltfSplatWriter w) + { + { + double[] matrix = + Matrices.createMatrixTranslation(-75.0f, 0.0f, 0.0f); + List splats = UnitShSplats.createDeg2(); + w.addSplats(splats, matrix); + } + { + double[] matrix = + Matrices.createMatrixTranslation(75.0f, 0.0f, 0.0f); + List splats = UnitShSplats.createDeg3(); + w.addSplats(splats, matrix); + } + } + + /** + * TODO + * + * @param w The writer + */ + private static void createScaledScales(GenericGltfSplatWriter w) + { + List splats = SplatScaleTests.createScales(); + { + double[] matrix = Matrices.createMatrixScale(0.5f); + matrix[12] = -200.0f; + w.addSplats(splats, matrix); + } + { + double[] matrix = Matrices.createMatrixScale(1.0f); + w.addSplats(splats, matrix); + } + { + double[] matrix = Matrices.createMatrixScale(5.0f); + matrix[12] = 450.0f; + w.addSplats(splats, matrix); + } + } + + /** + * TODO + * + * @param w The writer + */ + private static void createAnimated(GenericGltfSplatWriter w) + { + List splats = UnitShSplats.createDeg3(); + { + List localSplats = Splats.copyList(splats); + SplatTransforms.transformList(localSplats, + Matrices.createMatrixTranslation(-225.0, 0.0, 0.0)); + w.addSplats(localSplats, null); + } + { + List localSplats = Splats.copyList(splats); + SplatTransforms.transformList(localSplats, + Matrices.createMatrixTranslation(-75.0, 0.0, 0.0)); + double times[] = + { 0.0, 1.0, 2.0, 3.0, 4.0 }; + double translations[][] = + { + { 0.0, 0.0, 0.0 }, + { 25.0, 0.0, 0.0 }, + { 25.0, 75.0, 0.0 }, + { 0.0, 75.0, 0.0 }, + { 0.0, 0.0, 0.0 }, }; + SplatAnimation splatAnimation = new SplatAnimation(); + splatAnimation.times = times; + splatAnimation.translations = translations; + w.addAnimatedSplats(localSplats, splatAnimation); + } + { + List localSplats = Splats.copyList(splats); + SplatTransforms.transformList(localSplats, + Matrices.createMatrixTranslation(75.0, 0.0, 0.0)); + double times[] = + { 0.0, 1.0, 2.0, 3.0, 4.0 }; + double rotations[][] = + { rotation(1.0, 0.0, 0.0, Math.toRadians(0.0)), + rotation(1.0, 0.0, 0.0, Math.toRadians(90.0)), + rotation(1.0, 0.0, 0.0, Math.toRadians(180.0)), + rotation(1.0, 0.0, 0.0, Math.toRadians(270.0)), + rotation(1.0, 0.0, 0.0, Math.toRadians(0.0)) }; + SplatAnimation splatAnimation = new SplatAnimation(); + splatAnimation.times = times; + splatAnimation.rotations = rotations; + w.addAnimatedSplats(localSplats, splatAnimation); + } + { + List localSplats = Splats.copyList(splats); + SplatTransforms.transformList(localSplats, + Matrices.createMatrixTranslation(225.0, 0.0, 0.0)); + double times[] = + { 0.0, 1.0, 2.0, 3.0, 4.0 }; + double scales[][] = + { + { 1.0, 1.0, 1.0 }, + { 1.0, 2.0, 1.0 }, + { 1.0, 3.0, 1.0 }, + { 1.0, 2.0, 1.0 }, + { 1.0, 1.0, 1.0 }, }; + SplatAnimation splatAnimation = new SplatAnimation(); + splatAnimation.times = times; + splatAnimation.scales = scales; + w.addAnimatedSplats(localSplats, splatAnimation); + } + + } + + /** + * Create the components of a quaternion that describes a rotation around + * the given axis, about the given angle + * + * @param x The x-component of the axis + * @param y The y-component of the axis + * @param z The z-component of the axis + * @param angleRad The angle, in radians + * @return The quaternion, in scalar-last representation + */ + private static double[] rotation(double x, double y, double z, + double angleRad) + { + double halfAngleRad = angleRad * 0.5f; + double s = Math.sin(halfAngleRad); + + double lenSquared = x * x + y * y + z * z; + if (lenSquared < 1e-6) + { + return new double[] + { 0.0, 0.0, 0.0, 1.0 }; + } + + double invLen = 1.0 / Math.sqrt(lenSquared); + double qx = x * invLen * s; + double qy = y * invLen * s; + double qz = z * invLen * s; + double qw = Math.cos(halfAngleRad); + + double q[] = new double[] + { qx, qy, qz, qw }; + return q; + } + +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/GenericGltfSplatWriter.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/GenericGltfSplatWriter.java new file mode 100644 index 0000000..7761a56 --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/GenericGltfSplatWriter.java @@ -0,0 +1,529 @@ +/* + * 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.examples.gltf; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.FloatBuffer; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +import de.javagl.jgltf.model.AnimationModel; +import de.javagl.jgltf.model.AnimationModel.Interpolation; +import de.javagl.jgltf.model.MeshPrimitiveModel; +import de.javagl.jgltf.model.NodeModel; +import de.javagl.jgltf.model.creation.AccessorModels; +import de.javagl.jgltf.model.creation.GltfModelBuilder; +import de.javagl.jgltf.model.impl.DefaultAccessorModel; +import de.javagl.jgltf.model.impl.DefaultAnimationModel; +import de.javagl.jgltf.model.impl.DefaultExtensionsModel; +import de.javagl.jgltf.model.impl.DefaultGltfModel; +import de.javagl.jgltf.model.impl.DefaultMeshModel; +import de.javagl.jgltf.model.impl.DefaultMeshPrimitiveModel; +import de.javagl.jgltf.model.impl.DefaultNodeModel; +import de.javagl.jgltf.model.impl.DefaultSceneModel; +import de.javagl.jgltf.model.io.GltfModelWriter; +import de.javagl.jsplat.Splat; +import de.javagl.jsplat.io.gltf.GltfSplatWriter; + +/** + * A class that can write glTF assets with one or more mesh primitives that use + * the KHR_gaussian_splatting extension. + * + * NOTE: This class is preliminary + */ +class GenericGltfSplatWriter +{ + /** + * The extension name (and attribute prefix) + */ + private static final String NAME = "KHR_gaussian_splatting"; + + /** + * The default color space for the extension objects + */ + private static final String DEFAULT_COLOR_SPACE = "srgb_rec709_display"; + + /** + * Key for mesh lookups, consisting of a list of splats and the color space + */ + class MeshKey extends SimpleEntry, String> + { + /** + * Serial UID + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance + * + * @param key The key + * @param value The value + */ + public MeshKey(List key, String value) + { + super(key, value); + } + } + + /** + * An instantiation of a splat primitive mesh + */ + private static class Instantiation + { + /** + * The matrix for the node + */ + private double[] matrix; + + /** + * The animation for the node + */ + private SplatAnimation splatAnimation; + } + + /** + * The mapping of keys to the instantiations + */ + private final Map> instantiations; + + /** + * The list of mesh primitives that have been added + */ + private final List meshPrimitiveModels; + + /** + * The list of matrices that are associated with the mesh primitives + */ + private final List meshPrimitiveMatrices; + + /** + * Creates a new instance, using an unspecified default color space + */ + public GenericGltfSplatWriter() + { + instantiations = new LinkedHashMap>(); + meshPrimitiveModels = new ArrayList(); + meshPrimitiveMatrices = new ArrayList(); + } + + /** + * Experimental + * + * @param meshPrimitiveModel Do not use + * @param matrix Do not use + */ + public void addMeshPrimitive(MeshPrimitiveModel meshPrimitiveModel, + double matrix[]) + { + Objects.requireNonNull(meshPrimitiveModel, + "The meshPrimitiveModel may not be null"); + meshPrimitiveModels.add(meshPrimitiveModel); + if (matrix != null) + { + if (matrix.length != 16) + { + throw new IllegalArgumentException( + "Expected matrix to have a length of 16, " + + "but it has a length of " + matrix.length); + } + } + meshPrimitiveMatrices.add(matrix); + } + + /** + * Add the given splats to this instance, using an unspecified default color + * space. + * + * The given splats will be contained in one mesh primitive that is part of + * a mesh that is attached to a node that has the given matrix. + * + * @param splats The splats + * @param matrix An optional matrix + * @throws IllegalArgumentException If the given matrix is not + * null and has a length that is not 16 + */ + public void addSplats(List splats, double matrix[]) + { + addSplats(splats, matrix, DEFAULT_COLOR_SPACE); + } + + /** + * Add the given splats to this instance. + * + * The given splats will be contained in one mesh primitive that is part of + * a mesh that is attached to a node that has the given matrix. + * + * @param splats The splats + * @param matrix An optional matrix + * @param colorSpace The color space for the splat extension + * @throws IllegalArgumentException If the given matrix is not + * null and has a length that is not 16 + */ + public void addSplats(List splats, double matrix[], + String colorSpace) + { + Objects.requireNonNull(splats, "The splats may not be null"); + if (matrix != null) + { + if (matrix.length != 16) + { + throw new IllegalArgumentException( + "Expected matrix to have a length of 16, " + + "but it has a length of " + matrix.length); + } + } + + Instantiation instantiation = new Instantiation(); + instantiation.matrix = matrix == null ? null : matrix.clone(); + + MeshKey meshKey = new MeshKey(splats, colorSpace); + List list = instantiations.computeIfAbsent(meshKey, + (k) -> new ArrayList()); + list.add(instantiation); + } + + /** + * Add the given splats to this instance, using an unspecified default color + * space. + * + * The given splats will be contained in one mesh primitive that is part of + * a mesh that is attached to a node that has the given animation. + * + * @param splats The splats + * @param splatAnimation The animation + * @throws IllegalArgumentException If the given matrix is not + * null and has a length that is not 16 + */ + public void addAnimatedSplats(List splats, + SplatAnimation splatAnimation) + { + addAnimatedSplats(splats, splatAnimation, DEFAULT_COLOR_SPACE); + } + + /** + * Add the given splats to this instance. + * + * The given splats will be contained in one mesh primitive that is part of + * a mesh that is attached to a node that has the given animation. + * + * @param splats The splats + * @param splatAnimation The animation + * @param colorSpace The color space for the splat extension + * @throws IllegalArgumentException If the given matrix is not + * null and has a length that is not 16 + */ + public void addAnimatedSplats(List splats, + SplatAnimation splatAnimation, String colorSpace) + { + Objects.requireNonNull(splats, "The splats may not be null"); + + Instantiation instantiation = new Instantiation(); + instantiation.splatAnimation = splatAnimation; + + MeshKey meshKey = new MeshKey(splats, colorSpace); + List list = instantiations.computeIfAbsent(meshKey, + (k) -> new ArrayList()); + list.add(instantiation); + } + + /** + * Write the glTF asset, as a binary glTF (GLB) file to the given output + * stream. + * + * @param outputStream The output stream + * @throws IOException If an IO error occurs + */ + public void write(OutputStream outputStream) throws IOException + { + DefaultSceneModel sceneModel = new DefaultSceneModel(); + + // Create a mapping from the mesh keys to the respective mesh + // models, each containing one mesh primitive with the + // splat extension + Map splatMeshModels = + new LinkedHashMap(); + for (MeshKey meshKey : instantiations.keySet()) + { + List splats = meshKey.getKey(); + String colorSpace = meshKey.getValue(); + + DefaultMeshPrimitiveModel meshPrimitiveModel = + GltfSplatWriter.createMeshPrimitiveModel(splats); + + // Manually add the extension (there is no model-level + // representation of this extension yet) + Map extension = + GltfSplatWriter.createExtension(colorSpace); + meshPrimitiveModel.addExtension(NAME, extension); + + DefaultMeshModel meshModel = new DefaultMeshModel(); + meshModel.addMeshPrimitiveModel(meshPrimitiveModel); + + splatMeshModels.put(meshKey, meshModel); + } + + // For each "instantiation", create a node with the corresponding + // matrix or animation, and attach the corresponding splat mesh + // to that node + List animationModels = + new ArrayList(); + for (Entry> entry : instantiations + .entrySet()) + { + MeshKey meshKey = entry.getKey(); + DefaultMeshModel meshModel = splatMeshModels.get(meshKey); + + List instantiationsList = entry.getValue(); + for (Instantiation instantiation : instantiationsList) + { + SplatAnimation splatAnimation = instantiation.splatAnimation; + if (splatAnimation != null) + { + DefaultNodeModel nodeModel = new DefaultNodeModel(); + nodeModel.addMeshModel(meshModel); + sceneModel.addNode(nodeModel); + DefaultAnimationModel animationModel = + createAnimationModel(nodeModel, splatAnimation); + if (animationModel != null) + { + animationModels.add(animationModel); + } + } + else + { + double matrix[] = instantiation.matrix; + DefaultNodeModel nodeModel = new DefaultNodeModel(); + nodeModel.setMatrix(toFloat(matrix)); + nodeModel.addMeshModel(meshModel); + sceneModel.addNode(nodeModel); + } + } + } + + // Add the mesh primitives that have been added manually + for (int i = 0; i < meshPrimitiveModels.size(); i++) + { + MeshPrimitiveModel meshPrimitiveModel = meshPrimitiveModels.get(i); + double matrix[] = meshPrimitiveMatrices.get(i); + + DefaultMeshModel meshModel = new DefaultMeshModel(); + meshModel.addMeshPrimitiveModel(meshPrimitiveModel); + + DefaultNodeModel nodeModel = new DefaultNodeModel(); + nodeModel.setMatrix(toFloat(matrix)); + nodeModel.addMeshModel(meshModel); + sceneModel.addNode(nodeModel); + } + + GltfModelBuilder b = GltfModelBuilder.create(); + b.addSceneModel(sceneModel); + for (AnimationModel animationModel : animationModels) + { + b.addAnimationModel(animationModel); + } + DefaultGltfModel gltfModel = b.build(); + + DefaultExtensionsModel extensionsModel = gltfModel.getExtensionsModel(); + extensionsModel.addExtensionsUsed(Arrays.asList(NAME)); + + GltfModelWriter w = new GltfModelWriter(); + w.writeBinary(gltfModel, outputStream); + } + + /** + * Create an animation model from the given data + * @param nodeModel The node model + * @param splatAnimation The splat animation + * @return The animation + */ + private static DefaultAnimationModel + createAnimationModel(NodeModel nodeModel, SplatAnimation splatAnimation) + { + float times[] = toFloat(splatAnimation.times); + if (splatAnimation.translations != null) + { + float translations[] = toFloat1D(splatAnimation.translations); + return createTranslationAnimationModel(nodeModel, times, + translations); + } + if (splatAnimation.rotations != null) + { + float rotations[] = toFloat1D(splatAnimation.rotations); + return createRotationAnimationModel(nodeModel, times, + rotations); + } + if (splatAnimation.scales != null) + { + float scales[] = toFloat1D(splatAnimation.scales); + return createScaleAnimationModel(nodeModel, times, + scales); + } + return null; + } + + /** + * Create an animation model from the given data + * + * @param nodeModel The node + * @param times The times + * @param translations The translations + * @return The animation + */ + private static DefaultAnimationModel createTranslationAnimationModel( + NodeModel nodeModel, float times[], float translations[]) + { + DefaultAccessorModel timesAccessorModel = + AccessorModels.createFloatScalar(FloatBuffer.wrap(times)); + DefaultAccessorModel valuesAccessorModel = + AccessorModels.createFloat3D(FloatBuffer.wrap(translations)); + + DefaultAnimationModel animationModel = new DefaultAnimationModel(); + DefaultAnimationModel.DefaultSampler sampler = + new DefaultAnimationModel.DefaultSampler(timesAccessorModel, + Interpolation.LINEAR, valuesAccessorModel); + DefaultAnimationModel.DefaultChannel channel = + new DefaultAnimationModel.DefaultChannel(sampler, nodeModel, + "translation"); + animationModel.addChannel(channel); + + return animationModel; + } + + /** + * Create an animation model from the given data + * + * @param nodeModel The node + * @param times The times + * @param rotations The rotations + * @return The animation + */ + private static DefaultAnimationModel createRotationAnimationModel( + NodeModel nodeModel, float times[], float rotations[]) + { + DefaultAccessorModel timesAccessorModel = + AccessorModels.createFloatScalar(FloatBuffer.wrap(times)); + DefaultAccessorModel valuesAccessorModel = + AccessorModels.createFloat4D(FloatBuffer.wrap(rotations)); + + DefaultAnimationModel animationModel = new DefaultAnimationModel(); + DefaultAnimationModel.DefaultSampler sampler = + new DefaultAnimationModel.DefaultSampler(timesAccessorModel, + Interpolation.LINEAR, valuesAccessorModel); + DefaultAnimationModel.DefaultChannel channel = + new DefaultAnimationModel.DefaultChannel(sampler, nodeModel, + "rotation"); + animationModel.addChannel(channel); + + return animationModel; + } + + /** + * Create an animation model from the given data + * + * @param nodeModel The node + * @param times The times + * @param scales The scales + * @return The animation + */ + private static DefaultAnimationModel createScaleAnimationModel( + NodeModel nodeModel, float times[], float scales[]) + { + DefaultAccessorModel timesAccessorModel = + AccessorModels.createFloatScalar(FloatBuffer.wrap(times)); + DefaultAccessorModel valuesAccessorModel = + AccessorModels.createFloat3D(FloatBuffer.wrap(scales)); + + DefaultAnimationModel animationModel = new DefaultAnimationModel(); + DefaultAnimationModel.DefaultSampler sampler = + new DefaultAnimationModel.DefaultSampler(timesAccessorModel, + Interpolation.LINEAR, valuesAccessorModel); + DefaultAnimationModel.DefaultChannel channel = + new DefaultAnimationModel.DefaultChannel(sampler, nodeModel, + "scale"); + animationModel.addChannel(channel); + + return animationModel; + } + + /** + * Convert the given array into a float array + * + * @param array The array + * @return The result + */ + private static float[] toFloat(double array[]) + { + if (array == null) + { + return null; + } + float result[] = new float[array.length]; + for (int i = 0; i < array.length; i++) + { + result[i] = (float) array[i]; + } + return result; + } + + /** + * Convert the given array into a 1D float array + * + * @param array The array + * @return The result + */ + private static float[] toFloat1D(double array[][]) + { + if (array == null) + { + return null; + } + int length = 0; + for (int i = 0; i < array.length; i++) + { + length += array[i].length; + } + float result[] = new float[length]; + int index = 0; + for (int i = 0; i < array.length; i++) + { + double e[] = array[i]; + for (int j = 0; j < e.length; j++) + { + result[index++] = (float) e[j]; + } + } + return result; + } + +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/GltfModelElements.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/GltfModelElements.java new file mode 100644 index 0000000..605561e --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/GltfModelElements.java @@ -0,0 +1,99 @@ +/* + * 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.examples.gltf; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +import de.javagl.jgltf.model.MaterialModel; +import de.javagl.jgltf.model.creation.MaterialBuilder; +import de.javagl.jgltf.model.creation.MeshPrimitiveBuilder; +import de.javagl.jgltf.model.impl.DefaultMeshPrimitiveModel; +import de.javagl.jgltf.model.v2.MaterialModelV2; + +/** + * Methods to create parts of a glTF model + */ +class GltfModelElements +{ + /** + * Creates a mesh primitive model that describes a unit cube + * + * @return The mesh primitive model + */ + static DefaultMeshPrimitiveModel createUnitCube() + { + int indices[] = new int[] + { 0, 2, 1, 0, 3, 2, 4, 6, 5, 4, 7, 6, 8, 10, 9, 8, 11, 10, 12, 14, 13, + 12, 15, 14, 16, 18, 17, 16, 19, 18, 20, 22, 21, 20, 23, 22 }; + float positions[] = new float[] + { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, + 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; + float normals[] = new float[] + { 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f }; + MeshPrimitiveBuilder b = MeshPrimitiveBuilder.create(); + b.setIntIndicesAsShort(IntBuffer.wrap(indices)); + b.addPositions3D(FloatBuffer.wrap(positions)); + b.addNormals3D(FloatBuffer.wrap(normals)); + DefaultMeshPrimitiveModel m = b.build(); + m.setMaterialModel(createMaterial()); + return m; + } + + /** + * Create a simple, two-sides material + * + * @return The material + */ + private static MaterialModel createMaterial() + { + MaterialBuilder b = MaterialBuilder.create(); + b.setDoubleSided(true); + MaterialModelV2 m = b.build(); + return m; + } + + /** + * Private constructor to prevent instantiation + */ + private GltfModelElements() + { + // Private constructor to prevent instantiation + } + +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/Matrices.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/Matrices.java new file mode 100644 index 0000000..7922ee0 --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/Matrices.java @@ -0,0 +1,174 @@ +/* + * 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.examples.gltf; + +/** + * Utility methods to create matrices. + * + * All matrices will be 16-element arrays representing 4x4 matrices in + * column-major order. + */ +class Matrices +{ + /** + * Create a matrix describing a rotation around the x-axis, and the given + * translation + * + * @param angleDeg The angle, in degrees + * @param tx The translation along x + * @param ty The translation along y + * @param tz The translation along z + * @return The matrix + */ + static double[] createMatrixX(double angleDeg, double tx, double ty, + double tz) + { + double matrix[] = new double[16]; + + double angleRad = Math.toRadians(angleDeg); + double c = Math.cos(angleRad); + double s = Math.sin(angleRad); + + // Column 0 + matrix[0] = 1.0; + matrix[1] = 0.0; + matrix[2] = 0.0; + matrix[3] = 0.0; + + // Column 1 + matrix[4] = 0.0; + matrix[5] = c; + matrix[6] = s; + matrix[7] = 0.0; + + // Column 2 + matrix[8] = 0.0; + matrix[9] = -s; + matrix[10] = c; + matrix[11] = 0.0; + + // Column 3 (Translation) + matrix[12] = tx; + matrix[13] = ty; + matrix[14] = tz; + matrix[15] = 1.0; + + return matrix; + } + + /** + * Create a matrix describing a rotation around the y-axis, and the given + * translation + * + * @param angleDeg The angle, in degrees + * @param tx The translation along x + * @param ty The translation along y + * @param tz The translation along z + * @return The matrix + */ + static double[] createMatrixY(double angleDeg, double tx, double ty, + double tz) + { + double matrix[] = new double[16]; + + double angleRad = Math.toRadians(angleDeg); + double c = Math.cos(angleRad); + double s = Math.sin(angleRad); + + // Column 0 + matrix[0] = c; + matrix[1] = 0.0; + matrix[2] = -s; + matrix[3] = 0.0; + + // Column 1 + matrix[4] = 0.0; + matrix[5] = 1.0; + matrix[6] = 0.0; + matrix[7] = 0.0; + + // Column 2 + matrix[8] = s; + matrix[9] = 0.0; + matrix[10] = c; + matrix[11] = 0.0; + + // Column 3 (Translation) + matrix[12] = tx; + matrix[13] = ty; + matrix[14] = tz; + matrix[15] = 1.0; + + return matrix; + } + + /** + * Create a matrix describing uniform scaling + * + * @param s The scaling factor + * @return The matrix + */ + static double[] createMatrixScale(double s) + { + double[] m = new double[16]; + m[0] = s; + m[5] = s; + m[10] = s; + m[15] = 1.0; + return m; + } + + /** + * Create a matrix describing a translation + * + * @param x The translation in x-direction + * @param y The translation in y-direction + * @param z The translation in z-direction + * @return The matrix + */ + static double[] createMatrixTranslation(double x, double y, double z) + { + double[] m = new double[16]; + m[0] = 1.0; + m[5] = 1.0; + m[10] = 1.0; + m[15] = 1.0; + m[12] = x; + m[13] = y; + m[14] = z; + return m; + } + + /** + * Private constructor to prevent instantiation + */ + private Matrices() + { + // Private constructor to prevent instantiation + } + +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatBlendingTests.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatBlendingTests.java new file mode 100644 index 0000000..2d2537b --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatBlendingTests.java @@ -0,0 +1,158 @@ +/* + * 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.examples.gltf; + +import java.util.ArrayList; +import java.util.List; + +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.Splats; +import de.javagl.jsplat.examples.SplatSetters; + +/** + * Methods to create test data set for splat blending + */ +class SplatBlendingTests +{ + /** + * The cube size + */ + private static final float size = 100.0f; + + /** + * Create a test data set for splat depth sorting tests. + * + * Some details are unspecified here. + * + * @return The test data + */ + static List createBlendingTest() + { + List splats = new ArrayList(); + + // A with SH sum 5.0 and opacity 1.0 + { + MutableSplat s = Splats.create(0); + SplatSetters.setDefaults(s); + + s.setPositionX(-0.25f * size); + s.setPositionY(0.0f); + s.setPositionZ(0.0f); + + s.setScaleX(2.0f); + s.setScaleY(2.0f); + s.setScaleZ(0.01f); + + s.setOpacity(Splats.alphaToOpacity(1.0f)); + + s.setShX(0, Splats.colorToDirectCurrent(5.0f)); + s.setShY(0, Splats.colorToDirectCurrent(5.0f)); + s.setShZ(0, Splats.colorToDirectCurrent(5.0f)); + splats.add(s); + } + + // B with SH sum 0.0 and opacity 0.9 + { + MutableSplat s = Splats.create(0); + SplatSetters.setDefaults(s); + + s.setPositionX(-0.25f * size); + s.setPositionY(0.0f); + s.setPositionZ(0.05f * size); + + s.setScaleX(2.0f); + s.setScaleY(2.0f); + s.setScaleZ(0.01f); + + s.setOpacity(Splats.alphaToOpacity(0.9f)); + + s.setShX(0, Splats.colorToDirectCurrent(0.0f)); + s.setShY(0, Splats.colorToDirectCurrent(0.0f)); + s.setShZ(0, Splats.colorToDirectCurrent(0.0f)); + splats.add(s); + } + + + + + // A with SH sum 5.0 and opacity 0.1 + { + MutableSplat s = Splats.create(0); + SplatSetters.setDefaults(s); + + s.setPositionX(0.25f * size); + s.setPositionY(0.0f); + s.setPositionZ(0.0f); + + s.setScaleX(2.0f); + s.setScaleY(2.0f); + s.setScaleZ(0.01f); + + s.setOpacity(Splats.alphaToOpacity(0.1f)); + + s.setShX(0, Splats.colorToDirectCurrent(5.0f)); + s.setShY(0, Splats.colorToDirectCurrent(5.0f)); + s.setShZ(0, Splats.colorToDirectCurrent(5.0f)); + splats.add(s); + } + + // B with SH sum 0.0 and opacity 0.9 + { + MutableSplat s = Splats.create(0); + SplatSetters.setDefaults(s); + + s.setPositionX(0.25f * size); + s.setPositionY(0.0f); + s.setPositionZ(0.05f * size); + + s.setScaleX(2.0f); + s.setScaleY(2.0f); + s.setScaleZ(0.01f); + + s.setOpacity(Splats.alphaToOpacity(0.9f)); + + s.setShX(0, Splats.colorToDirectCurrent(0.0f)); + s.setShY(0, Splats.colorToDirectCurrent(0.0f)); + s.setShZ(0, Splats.colorToDirectCurrent(0.0f)); + splats.add(s); + } + + + + splats.addAll(SplatTests.createCorners(0, size)); + return splats; + } + + /** + * Private constructor to prevent instantiation + */ + private SplatBlendingTests() + { + // Private constructor to prevent instantiation + } + +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatDepthTests.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatDepthTests.java new file mode 100644 index 0000000..52feb95 --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatDepthTests.java @@ -0,0 +1,106 @@ +/* + * 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.examples.gltf; + +import java.util.ArrayList; +import java.util.List; + +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.Splats; + +/** + * Methods to create test data set for splat depth sorting + */ +class SplatDepthTests +{ + /** + * The cube size + */ + private static final float size = 100.0f; + + /** + * Create a test data set for splat depth sorting tests. + * + * Some details are unspecified here. + * + * @return The test data + */ + static List createDepthTest() + { + List splats = new ArrayList(); + + int n = 8; + float minX = 0.25f; + float minY = 0.25f; + float minZ = 0.25f; + float maxX = 0.75f; + float maxY = 0.75f; + float maxZ = 0.75f; + for (int i = 1; i < n; i++) + { + float rel = (float) (i - 1) / (n - 2); + MutableSplat s = Splats.create(0); + + float x = minX + rel * (maxX - minX); + float y = minY + rel * (maxY - minY); + float z = minZ + rel * (maxZ - minZ); + s.setPositionX(-0.5f * size + x * size); + s.setPositionY(-0.5f * size + y * size); + s.setPositionZ(-0.5f * size + z * size); + + s.setScaleX(2.5f); + s.setScaleY(2.5f); + s.setScaleZ(0.01f); + s.setRotationX(0.0f); + s.setRotationY(0.0f); + s.setRotationZ(0.0f); + s.setRotationW(1.0f); + s.setOpacity(Splats.alphaToOpacity(1.0f)); + + float r = ((i & 1) == 0 ? 0.0f : 1.0f); + float g = ((i & 2) == 0 ? 0.0f : 1.0f); + float b = ((i & 4) == 0 ? 0.0f : 1.0f); + + s.setShX(0, Splats.colorToDirectCurrent(r)); + s.setShY(0, Splats.colorToDirectCurrent(g)); + s.setShZ(0, Splats.colorToDirectCurrent(b)); + splats.add(s); + } + + splats.addAll(SplatTests.createCorners(0, size)); + return splats; + } + + /** + * Private constructor to prevent instantiation + */ + private SplatDepthTests() + { + // Private constructor to prevent instantiation + } + +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatRotationTests.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatRotationTests.java new file mode 100644 index 0000000..6d04a93 --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatRotationTests.java @@ -0,0 +1,202 @@ +/* + * 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.examples.gltf; + +import static de.javagl.jsplat.examples.SplatSetters.setColor; +import static de.javagl.jsplat.examples.SplatSetters.setDefaults; +import static de.javagl.jsplat.examples.SplatSetters.setPosition; +import static de.javagl.jsplat.examples.SplatSetters.setRotationAxisAngleRad; +import static de.javagl.jsplat.examples.SplatSetters.setScale; + +import java.util.ArrayList; +import java.util.List; + +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.Splats; + +/** + * A class creating different test data sets + */ +public class SplatRotationTests +{ + /** + * The size of the cube that contains the splats + */ + private static final float cubeSize = 100.0f; + + /** + * The minimum X-coordinate + */ + private static final float minX = -0.5f * cubeSize; + + /** + * The minimum Y-coordinate + */ + private static final float minY = -0.5f * cubeSize; + + /** + * The minimum Z-coordinate + */ + private static final float minZ = -0.5f * cubeSize; + + /** + * The maximum X-coordinate + */ + private static final float maxX = 0.5f * cubeSize; + + /** + * The maximum Y-coordinate + */ + private static final float maxY = 0.5f * cubeSize; + + /** + * The maximum Z-coordinate + */ + private static final float maxZ = 0.5f * cubeSize; + + /** + * Create a list of splats for rotation tests. + * + * The list will contain 10 splats, at positions that are interpolated along + * the x-axis. Each splat will have a scaling factor of 3.0 along the + * z-axis. The rotation of each splat will be interpolated between 0.0 + * degrees and 90.0 degrees around x. The color will be interpolated from + * white to red. + * + * The list will include splats that indicate the corners of a containing + * cube. + * + * @return The splats + */ + static List createRotationsX() + { + int shDegree = 0; + List splats = new ArrayList(); + + int n = 10; + for (int i = 0; i < n; i++) + { + float a = (float) i / (n - 1); + float x = minX + a * (maxX - minX); + float angleRad = 0.0f + a * (float) (Math.PI / 2.0); + + MutableSplat s0 = Splats.create(shDegree); + setDefaults(s0); + setPosition(s0, x, 0.0f, 0.0f); + setColor(s0, 1.0f, 1.0f - a, 1.0f - a); + setScale(s0, 1.0f, 1.0f, 3.0f); + setRotationAxisAngleRad(s0, 1.0f, 0.0f, 0.0f, angleRad); + splats.add(s0); + } + splats.addAll(SplatTests.createCorners(shDegree, cubeSize)); + return splats; + } + + /** + * Create a list of splats for rotation tests. + * + * The list will contain 10 splats, at positions that are interpolated along + * the y-axis. Each splat will have a scaling factor of 3.0 along the + * x-axis. The rotation of each splat will be interpolated between 0.0 + * degrees and 90.0 degrees around y. The color will be interpolated from + * white to green. + * + * The list will include splats that indicate the corners of a containing + * cube. + * + * @return The splats + */ + static List createRotationsY() + { + int shDegree = 0; + List splats = new ArrayList(); + + int n = 10; + for (int i = 0; i < n; i++) + { + float a = (float) i / (n - 1); + float y = minY + a * (maxY - minY); + float angleRad = 0.0f + a * (float) (Math.PI / 2.0); + + MutableSplat s0 = Splats.create(shDegree); + setDefaults(s0); + setPosition(s0, 0.0f, y, 0.0f); + setColor(s0, 1.0f - a, 1.0f, 1.0f - a); + setScale(s0, 3.0f, 1.0f, 1.0f); + setRotationAxisAngleRad(s0, 0.0f, 1.0f, 0.0f, angleRad); + splats.add(s0); + } + splats.addAll(SplatTests.createCorners(shDegree, cubeSize)); + return splats; + } + + /** + * Create a list of splats for rotation tests. + * + * The list will contain 10 splats, at positions that are interpolated along + * the z-axis. Each splat will have a scaling factor of 3.0 along the + * y-axis. The rotation of each splat will be interpolated between 0.0 + * degrees and 90.0 degrees around z. The color will be interpolated from + * white to blue. + * + * The list will include splats that indicate the corners of a containing + * cube. + * + * @return The splats + */ + static List createRotationsZ() + { + int shDegree = 0; + List splats = new ArrayList(); + + int n = 10; + for (int i = 0; i < n; i++) + { + float a = (float) i / (n - 1); + float z = minZ + a * (maxZ - minZ); + float angleRad = 0.0f + a * (float) (Math.PI / 2.0); + + MutableSplat s0 = Splats.create(shDegree); + setDefaults(s0); + setPosition(s0, 0.0f, 0.0f, z); + setColor(s0, 1.0f - a, 1.0f - a, 1.0f); + setScale(s0, 1.0f, 3.0f, 1.0f); + setRotationAxisAngleRad(s0, 0.0f, 0.0f, 1.0f, angleRad); + splats.add(s0); + } + splats.addAll(SplatTests.createCorners(shDegree, cubeSize)); + return splats; + } + + /** + * Private constructor to prevent instantiation + */ + private SplatRotationTests() + { + // Private constructor to prevent instantiation + } +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatScaleTests.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatScaleTests.java new file mode 100644 index 0000000..51f9097 --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatScaleTests.java @@ -0,0 +1,194 @@ +/* + * 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.examples.gltf; + +import static de.javagl.jsplat.examples.SplatSetters.setColor; +import static de.javagl.jsplat.examples.SplatSetters.setDefaults; +import static de.javagl.jsplat.examples.SplatSetters.setPosition; +import static de.javagl.jsplat.examples.SplatSetters.setScaleLinear; + +import java.util.ArrayList; +import java.util.List; + +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.Splats; + +/** + * A class creating different test data sets + */ +public class SplatScaleTests +{ + /** + * The size of the cube that contains the splats + */ + private static final float cubeSize = 100.0f; + + /** + * The minimum X-coordinate + */ + private static final float minX = -0.5f * cubeSize; + + /** + * The minimum Y-coordinate + */ + private static final float minY = -0.5f * cubeSize; + + /** + * The minimum Z-coordinate + */ + private static final float minZ = -0.5f * cubeSize; + + /** + * The maximum X-coordinate + */ + private static final float maxX = 0.5f * cubeSize; + + /** + * The maximum Y-coordinate + */ + private static final float maxY = 0.5f * cubeSize; + + /** + * The maximum Z-coordinate + */ + private static final float maxZ = 0.5f * cubeSize; + + /** + * Create a list of splats for scaling tests. + * + * The list will contain 10 scaling test splats for each axis. The scaling + * factors will be interpolated between 1.0 and 25.0. There will be a + * small, white splat at the "end" of each scaling test splat. + * + * Note: This does not make sense. Splats are infinitely large. + * + * @return The splats + */ + static List createScales() + { + int shDegree = 0; + List splats = new ArrayList(); + + int n = 10; + float dotScale = 0.5f; + float minScale = 1.0f; + float maxScale = 25.0f; + for (int i = 0; i < n; i++) + { + float a = (float) i / (n - 1); + float scale = minScale + a * (maxScale - minScale); + float y = minY + a * (maxY - minY); + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setPosition(s, -scale * 2.0f, y, minZ); + setScaleLinear(s, dotScale, dotScale, dotScale); + splats.add(s); + } + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setColor(s, 1.0f, 0.0f, 0.0f); + setPosition(s, 0.0f, y, minZ); + setScaleLinear(s, scale, dotScale, dotScale); + splats.add(s); + } + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setPosition(s, scale * 2.0f, y, minZ); + setScaleLinear(s, dotScale, dotScale, dotScale); + splats.add(s); + } + } + for (int i = 0; i < n; i++) + { + float a = (float) i / (n - 1); + float scale = minScale + a * (maxScale - minScale); + float x = minX + a * (maxX - minX); + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setPosition(s, x, minY, -scale * 2.0f); + setScaleLinear(s, dotScale, dotScale, dotScale); + splats.add(s); + } + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setColor(s, 0.0f, 0.0f, 1.0f); + setPosition(s, x, minY, 0.0f); + setScaleLinear(s, dotScale, dotScale, scale); + splats.add(s); + } + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setPosition(s, x, minY, scale * 2.0f); + setScaleLinear(s, dotScale, dotScale, dotScale); + splats.add(s); + } + } + for (int i = 0; i < n; i++) + { + float a = (float) i / (n - 1); + float scale = minScale + a * (maxScale - minScale); + float z = minZ + a * (maxZ - minZ); + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setPosition(s, minX, -scale * 2.0f, z); + setScaleLinear(s, dotScale, dotScale, dotScale); + splats.add(s); + } + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setColor(s, 0.0f, 1.0f, 0.0f); + setPosition(s, minX, 0.0f, z); + setScaleLinear(s, dotScale, scale, dotScale); + splats.add(s); + } + { + MutableSplat s = Splats.create(shDegree); + setDefaults(s); + setPosition(s, minX, scale * 2.0f, z); + setScaleLinear(s, dotScale, dotScale, dotScale); + splats.add(s); + } + } + return splats; + } + + /** + * Private constructor to prevent instantiation + */ + private SplatScaleTests() + { + // Private constructor to prevent instantiation + } +} diff --git a/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatTests.java b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatTests.java new file mode 100644 index 0000000..583c299 --- /dev/null +++ b/jsplat-examples-gltf/src/main/java/de/javagl/jsplat/examples/gltf/SplatTests.java @@ -0,0 +1,116 @@ +/* + * 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.examples.gltf; + +import java.util.ArrayList; +import java.util.List; + +import de.javagl.jsplat.MutableSplat; +import de.javagl.jsplat.Splats; + +/** + * Methods to create test data for splats + */ +class SplatTests +{ + /** + * Create a list of splats, representing a cube. + * + * Many details are intentionally not specified. + * + * @param size The size of the cube + * @param degree The degree + * @return The splats + */ + static List createCorners(int degree, float size) + { + List splats = new ArrayList(); + for (int c = 0; c < 8; c++) + { + float x = -0.5f + ((c & 1) == 0 ? 0.0f : 1.0f); + float y = -0.5f + ((c & 2) == 0 ? 0.0f : 1.0f); + float z = -0.5f + ((c & 4) == 0 ? 0.0f : 1.0f); + add(splats, degree, size, x, y, z); + } + return splats; + } + + /** + * Add a splat to the given list, with properties derived from the given + * parameters and some constants. Details are not specified. + * + * @param splats The splats + * @param degree The degree + * @param size The size of the cube + * @param npx The normalized x-coordinate + * @param npy The normalized y-coordinate + * @param npz The normalized z-coordinate + */ + private static void add(List splats, int degree, float size, + float npx, float npy, float npz) + { + MutableSplat splat = Splats.create(degree); + + splat.setPositionX(npx * size); + splat.setPositionY(npy * size); + splat.setPositionZ(npz * size); + + splat.setScaleX(1.0f); + splat.setScaleY(1.0f); + splat.setScaleZ(1.0f); + + splat.setRotationX(0.0f); + splat.setRotationY(0.0f); + splat.setRotationZ(0.0f); + splat.setRotationW(1.0f); + + splat.setOpacity(Splats.alphaToOpacity(1.0f)); + + if (npx == -0.5f && npy == -0.5f && npz == -0.5f) + { + splat.setShX(0, Splats.colorToDirectCurrent(0.1f)); + splat.setShY(0, Splats.colorToDirectCurrent(0.1f)); + splat.setShZ(0, Splats.colorToDirectCurrent(0.1f)); + } + else + { + splat.setShX(0, Splats.colorToDirectCurrent(npx + 0.5f)); + splat.setShY(0, Splats.colorToDirectCurrent(npy + 0.5f)); + splat.setShZ(0, Splats.colorToDirectCurrent(npz + 0.5f)); + } + + splats.add(splat); + } + + /** + * Private constructor to prevent instantiation + */ + private SplatTests() + { + // Private constructor to prevent instantiation + } +} 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 c763755..f2c8032 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 @@ -31,6 +31,7 @@ import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; @@ -68,6 +69,24 @@ public final class GltfSplatReader implements SplatListReader */ private static final String NAME = "KHR_gaussian_splatting"; + /** + * Internal, preliminary structure for reading splats from glTF. + * + * Not supposed to be used by clients. + */ + public static class Result + { + /** + * The glTF model + */ + public GltfModel gltfModel; + + /** + * The mapping from nodes to splat lists + */ + public Map> splatLists; + } + /** * Whether all splats from all mesh primitives should be read, transformed * according to the global transform matrix of the respective node, and @@ -110,6 +129,73 @@ public List readList(InputStream inputStream) return readFirstUntransformed(inputStream); } + /** + * Preliminary method to read splats from glTF + * + * @param inputStream The input stream + * @return The result + * @throws IOException If an IO error occurs + */ + public Result read(InputStream inputStream) throws IOException + { + GltfModelReader r = new GltfModelReader(); + GltfModel gltfModel = r.readWithoutReferences(inputStream); + + Result result = new Result(); + result.gltfModel = gltfModel; + result.splatLists = new LinkedHashMap>(); + + List sceneModels = gltfModel.getSceneModels(); + for (SceneModel sceneModel : sceneModels) + { + List nodeModels = sceneModel.getNodeModels(); + for (NodeModel nodeModel : nodeModels) + { + float[] globalTransformF = + nodeModel.computeGlobalTransform(null); + double globalTransform[] = new double[16]; + for (int i = 0; i < 16; i++) + { + globalTransform[i] = globalTransformF[i]; + } + List meshModels = nodeModel.getMeshModels(); + for (MeshModel meshModel : meshModels) + { + List meshPrimitiveModels = + meshModel.getMeshPrimitiveModels(); + for (MeshPrimitiveModel meshPrimitiveModel : meshPrimitiveModels) + { + Map extensions = + meshPrimitiveModel.getExtensions(); + if (extensions != null) + { + Object extension = extensions.get(NAME); + if (extension != null) + { + List splats = + readListFrom(meshPrimitiveModel); + if (splats != null && !splats.isEmpty()) + { + SplatTransforms.transformList(splats, + globalTransform); + + List mergedSplats = + result.splatLists.computeIfAbsent( + nodeModel, + n -> new ArrayList()); + mergedSplats = merge(mergedSplats, splats); + result.splatLists.put(nodeModel, + mergedSplats); + } + } + } + } + } + } + } + return result; + } + /** * Read all splats that are contained in the glTF that is read from the * given input stream, transforming them according to the global transform @@ -137,7 +223,7 @@ public List readList(InputStream inputStream) float[] globalTransformF = nodeModel.computeGlobalTransform(null); double globalTransform[] = new double[16]; - for (int i=0; i<16; i++) + for (int i = 0; i < 16; i++) { globalTransform[i] = globalTransformF[i]; }