Quellcode durchsuchen

Better PBR env map generation

Nehon vor 7 Jahren
Ursprung
Commit
e4b6bf82a2

+ 5 - 1
jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java

@@ -48,6 +48,7 @@ import com.jme3.texture.Texture2D;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.MipMapGenerator;
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -72,6 +73,8 @@ public class EnvironmentCamera extends BaseAppState {
 
     protected Image.Format imageFormat = Image.Format.RGB16F;
 
+    public TextureCubeMap debugEnv;
+
     //Axis for cameras
     static {
         //PositiveX axis(left, up, direction)
@@ -188,10 +191,11 @@ public class EnvironmentCamera extends BaseAppState {
             buffers[i] = BufferUtils.createByteBuffer(size * size * imageFormat.getBitsPerPixel() / 8);
             renderManager.getRenderer().readFrameBufferWithFormat(framebuffers[i], buffers[i], imageFormat);
             images[i] = new Image(imageFormat, size, size, buffers[i], ColorSpace.Linear);
+            MipMapGenerator.generateMipMaps(images[i]);
         }
 
         final TextureCubeMap map = EnvMapUtils.makeCubeMap(images[0], images[1], images[2], images[3], images[4], images[5], imageFormat);
-
+            debugEnv = map;
         job.callback.done(map);
         map.getImage().dispose();
         jobs.remove(0);

+ 33 - 23
jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java

@@ -31,13 +31,14 @@
  */
 package com.jme3.environment;
 
+import com.jme3.app.Application;
 import com.jme3.environment.generation.*;
-import com.jme3.light.LightProbe;
 import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.app.Application;
+import com.jme3.light.LightProbe;
 import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
 import com.jme3.texture.TextureCubeMap;
+
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 
 /**
@@ -106,10 +107,11 @@ public class LightProbeFactory {
      
      * @param envCam the EnvironmentCamera
      * @param scene the Scene
+     * @param genType Fast or HighQuality. Fast may be ok for many types of environment, but you may need high quality when an environment map has very high lighting values.
      * @param listener the listener of the genration progress.
      * @return the created LightProbe
      */
-    public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+    public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final EnvMapUtils.GenerationType genType, final JobProgressListener<LightProbe> listener) {
         final LightProbe probe = new LightProbe();
         probe.setPosition(envCam.getPosition());
         probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat()));
@@ -117,33 +119,37 @@ public class LightProbeFactory {
 
             @Override
             public void done(TextureCubeMap map) {
-                generatePbrMaps(map, probe, envCam.getApplication(), listener);
+                generatePbrMaps(map, probe, envCam.getApplication(), genType, listener);
             }
         });
         return probe;
     }
-    
-     /**
-     * Updates a LightProbe with the giver EnvironmentCamera in the given scene.
-     * 
+
+    public static LightProbe makeProbe(final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+        return makeProbe(envCam, scene, EnvMapUtils.GenerationType.Fast, listener);
+    }
+
+    /**
+     * Updates a LightProbe with the given EnvironmentCamera in the given scene.
+     * <p>
      * Note that this is an assynchronous process that will run on multiple threads.
      * The process is thread safe.
      * The created lightProbe will only be marked as ready when the rendering process is done.
-     *      
-     * The JobProgressListener will be notified of the progress of the generation. 
-     * Note that you can also use a {@link JobProgressAdapter}. 
-     *      
+     * <p>
+     * The JobProgressListener will be notified of the progress of the generation.
+     * Note that you can also use a {@link JobProgressAdapter}.
+     *
+     * @param probe    the Light probe to update
+     * @param envCam   the EnvironmentCamera
+     * @param scene    the Scene
+     * @param genType  Fast or HighQuality. Fast may be ok for many types of environment, but you may need high quality when an environment map has very high lighting values.
+     * @param listener the listener of the genration progress.
+     * @return the created LightProbe
      * @see LightProbe
      * @see EnvironmentCamera
      * @see JobProgressListener
-     * 
-     * @param probe the Light probe to update
-     * @param envCam the EnvironmentCamera
-     * @param scene the Scene
-     * @param listener the listener of the genration progress.
-     * @return the created LightProbe
      */
-    public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+    public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCamera envCam, Spatial scene, final EnvMapUtils.GenerationType genType, final JobProgressListener<LightProbe> listener) {
         
         envCam.setPosition(probe.getPosition());
         
@@ -159,23 +165,27 @@ public class LightProbeFactory {
 
             @Override
             public void done(TextureCubeMap map) {
-                generatePbrMaps(map, probe, envCam.getApplication(), listener);
+                generatePbrMaps(map, probe, envCam.getApplication(), genType, listener);
             }
         });
         return probe;
     }
 
+    public static LightProbe updateProbe(final LightProbe probe, final EnvironmentCamera envCam, Spatial scene, final JobProgressListener<LightProbe> listener) {
+        return updateProbe(probe, envCam, scene, EnvMapUtils.GenerationType.Fast, listener);
+    }
+
     /**
      * Internally called to generate the maps.
      * This method will spawn 7 thread (one for the Irradiance spherical harmonics generator, and one for each face of the prefiltered env map).
      * Those threads will be executed in a ScheduledThreadPoolExecutor that will be shutdown when the genration is done.
-     * 
+     *
      * @param envMap the raw env map rendered by the env camera
      * @param probe the LigthProbe to generate maps for
      * @param app the Application
      * @param listener a progress listener. (can be null if no progress reporting is needed)
      */
-    private static void generatePbrMaps(TextureCubeMap envMap, final LightProbe probe, Application app, final JobProgressListener<LightProbe> listener) {
+    private static void generatePbrMaps(TextureCubeMap envMap, final LightProbe probe, Application app, EnvMapUtils.GenerationType genType, final JobProgressListener<LightProbe> listener) {
         IrradianceSphericalHarmonicsGenerator irrShGenerator;
         PrefilteredEnvMapFaceGenerator[] pemGenerators = new PrefilteredEnvMapFaceGenerator[6];
 
@@ -189,7 +199,7 @@ public class LightProbeFactory {
 
         for (int i = 0; i < pemGenerators.length; i++) {
             pemGenerators[i] = new PrefilteredEnvMapFaceGenerator(app, i, new JobListener(listener, jobState, probe, i));
-            pemGenerators[i].setGenerationParam(EnvMapUtils.duplicateCubeMap(envMap), size, EnvMapUtils.FixSeamsMethod.None, probe.getPrefilteredEnvMap());
+            pemGenerators[i].setGenerationParam(EnvMapUtils.duplicateCubeMap(envMap), size, EnvMapUtils.FixSeamsMethod.None, genType, probe.getPrefilteredEnvMap());
             jobState.executor.execute(pemGenerators[i]);
         }
     }

+ 158 - 69
jme3-core/src/main/java/com/jme3/environment/generation/PrefilteredEnvMapFaceGenerator.java

@@ -31,27 +31,19 @@
  */
 package com.jme3.environment.generation;
 
+import com.jme3.app.Application;
 import com.jme3.environment.util.CubeMapWrapper;
 import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.app.Application;
 import com.jme3.math.*;
-
-import static com.jme3.math.FastMath.abs;
-import static com.jme3.math.FastMath.clamp;
-import static com.jme3.math.FastMath.pow;
-import static com.jme3.math.FastMath.sqrt;
-
 import com.jme3.texture.TextureCubeMap;
 
-import static com.jme3.environment.util.EnvMapUtils.getHammersleyPoint;
-import static com.jme3.environment.util.EnvMapUtils.getRoughnessFromMip;
-import static com.jme3.environment.util.EnvMapUtils.getSampleFromMip;
-import static com.jme3.environment.util.EnvMapUtils.getVectorFromCubemapFaceTexCoord;
-
 import java.util.concurrent.Callable;
-import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import static com.jme3.environment.util.EnvMapUtils.*;
+import static com.jme3.math.FastMath.abs;
+import static com.jme3.math.FastMath.sqrt;
+
 /**
  * Generates one face of the prefiltered environnement map for PBR. This job can
  * be lauched from a separate thread.
@@ -69,6 +61,7 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
 
     private int targetMapSize;
     private EnvMapUtils.FixSeamsMethod fixSeamsMethod;
+    private EnvMapUtils.GenerationType genType;
     private TextureCubeMap sourceMap;
     private TextureCubeMap store;
     private final Application app;
@@ -107,11 +100,12 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
      *                       {@link EnvMapUtils.FixSeamsMethod}
      * @param store          The cube map to store the result in.
      */
-    public void setGenerationParam(TextureCubeMap sourceMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
+    public void setGenerationParam(TextureCubeMap sourceMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, EnvMapUtils.GenerationType genType, TextureCubeMap store) {
         this.sourceMap = sourceMap;
         this.targetMapSize = targetMapSize;
         this.fixSeamsMethod = fixSeamsMethod;
         this.store = store;
+        this.genType = genType;
         init();
     }
 
@@ -162,68 +156,105 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
      * @return The irradiance cube map for the given coefficients
      */
     private TextureCubeMap generatePrefilteredEnvMap(TextureCubeMap sourceEnvMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
-        TextureCubeMap pem = store;
+        try {
+            TextureCubeMap pem = store;
 
-        int nbMipMap = (int) (Math.log(targetMapSize) / Math.log(2) - 1);
+            int nbMipMap = store.getImage().getMipMapSizes().length;
 
-        setEnd(nbMipMap);
+            setEnd(nbMipMap);
 
+            if (!sourceEnvMap.getImage().hasMipmaps() || sourceEnvMap.getImage().getMipMapSizes().length < nbMipMap) {
+                throw new IllegalArgumentException("The input cube map must have at least " + nbMipMap + "mip maps");
+            }
 
-        CubeMapWrapper sourceWrapper = new CubeMapWrapper(sourceEnvMap);
-        CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
+            CubeMapWrapper sourceWrapper = new CubeMapWrapper(sourceEnvMap);
+            CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
 
-        Vector3f texelVect = new Vector3f();
-        Vector3f color = new Vector3f();
-        ColorRGBA outColor = new ColorRGBA();
-        for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) {
-            float roughness = getRoughnessFromMip(mipLevel, nbMipMap);
-            int nbSamples = getSampleFromMip(mipLevel, nbMipMap);
-            int targetMipMapSize = (int) pow(2, nbMipMap + 1 - mipLevel);
+            Vector3f texelVect = new Vector3f();
+            Vector3f color = new Vector3f();
+            ColorRGBA outColor = new ColorRGBA();
+            int targetMipMapSize = targetMapSize;
+            for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) {
+                float roughness = getRoughnessFromMip(mipLevel, nbMipMap);
+                int nbSamples = getSampleFromMip(mipLevel, nbMipMap);
 
-            for (int y = 0; y < targetMipMapSize; y++) {
-                for (int x = 0; x < targetMipMapSize; x++) {
-                    color.set(0, 0, 0);
-                    getVectorFromCubemapFaceTexCoord(x, y, targetMipMapSize, face, texelVect, fixSeamsMethod);
-                    prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, color);
+                for (int y = 0; y < targetMipMapSize; y++) {
+                    for (int x = 0; x < targetMipMapSize; x++) {
+                        color.set(0, 0, 0);
+                        getVectorFromCubemapFaceTexCoord(x, y, targetMipMapSize, face, texelVect, fixSeamsMethod);
+                        prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, mipLevel, color);
 
-                    outColor.set(Math.max(color.x, 0.0001f), Math.max(color.y, 0.0001f), Math.max(color.z, 0.0001f), 1);
-                    log.log(Level.FINE, "coords {0},{1}", new Object[]{x, y});
-                    targetWrapper.setPixel(x, y, face, mipLevel, outColor);
+                        outColor.set(Math.max(color.x, 0.0001f), Math.max(color.y, 0.0001f), Math.max(color.z, 0.0001f), 1);
+                        targetWrapper.setPixel(x, y, face, mipLevel, outColor);
 
+                    }
                 }
+                targetMipMapSize /= 2;
+                progress();
             }
-            progress();
-        }
 
-        return pem;
+            return pem;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw e;
+        }
     }
 
-    private Vector3f prefilterEnvMapTexel(CubeMapWrapper envMapReader, float roughness, Vector3f N, int numSamples, Vector3f store) {
+    private Vector3f prefilterEnvMapTexel(CubeMapWrapper envMapReader, float roughness, Vector3f N, int numSamples, int mipLevel, Vector3f store) {
 
         Vector3f prefilteredColor = store;
         float totalWeight = 0.0f;
 
+        int nbRotations = 1;
+        if (genType == GenerationType.HighQuality) {
+            nbRotations = numSamples == 1 ? 1 : 18;
+        }
+
+        float rad = 2f * FastMath.PI / (float) nbRotations;
+        // offset rotation to avoid sampling pattern
+        float gi = (float) (FastMath.abs(N.z + N.x) * 256.0);
+        float offset = rad * (FastMath.cos((gi * 0.5f) % (2f * FastMath.PI)) * 0.5f + 0.5f);
+
         // a = roughness² and a2 = a²
         float a2 = roughness * roughness;
         a2 *= a2;
+
+        //Computing tangent frame
+        Vector3f upVector = Vector3f.UNIT_X;
+        if (abs(N.z) < 0.999) {
+            upVector = Vector3f.UNIT_Y;
+        }
+        Vector3f tangentX = tmp1.set(upVector).crossLocal(N).normalizeLocal();
+        Vector3f tangentY = tmp2.set(N).crossLocal(tangentX);
+
+        // https://placeholderart.wordpress.com/2015/07/28/implementation-notes-runtime-environment-map-filtering-for-image-based-lighting/
+        // in local space view == normal == 0,0,1
+        Vector3f V = new Vector3f(0, 0, 1);
+
+        Vector3f lWorld = new Vector3f();
         for (int i = 0; i < numSamples; i++) {
             Xi = getHammersleyPoint(i, numSamples, Xi);
-            H = importanceSampleGGX(Xi, a2, N, H);
-
+            H = importanceSampleGGX(Xi, a2, H);
             H.normalizeLocal();
-            tmp.set(H);
-            float NoH = N.dot(tmp);
-
-            Vector3f L = tmp.multLocal(NoH * 2).subtractLocal(N);
-            float NoL = clamp(N.dot(L), 0.0f, 1.0f);
-            if (NoL > 0) {
-                envMapReader.getPixel(L, c);
-                prefilteredColor.setX(prefilteredColor.x + c.r * NoL);
-                prefilteredColor.setY(prefilteredColor.y + c.g * NoL);
-                prefilteredColor.setZ(prefilteredColor.z + c.b * NoL);
-
-                totalWeight += NoL;
+            float VoH = H.z;
+            Vector3f L = H.multLocal(VoH * 2f).subtractLocal(V);
+            float NoL = L.z;
+
+            float computedMipLevel = mipLevel;
+            if (mipLevel != 0) {
+                computedMipLevel = computeMipLevel(roughness, numSamples, this.targetMapSize, VoH);
+            }
+
+            toWorld(L, N, tangentX, tangentY, lWorld);
+            totalWeight += samplePixel(envMapReader, lWorld, NoL, computedMipLevel, prefilteredColor);
+
+            for (int j = 1; j < nbRotations; j++) {
+                rotateDirection(offset + j * rad, L, lWorld);
+                L.set(lWorld);
+                toWorld(L, N, tangentX, tangentY, lWorld);
+                totalWeight += samplePixel(envMapReader, lWorld, NoL, computedMipLevel, prefilteredColor);
             }
+
         }
         if (totalWeight > 0) {
             prefilteredColor.divideLocal(totalWeight);
@@ -232,7 +263,78 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
         return prefilteredColor;
     }
 
-    public Vector3f importanceSampleGGX(Vector4f xi, float a2, Vector3f normal, Vector3f store) {
+    private float samplePixel(CubeMapWrapper envMapReader, Vector3f lWorld, float NoL, float computedMipLevel, Vector3f store) {
+
+        if (NoL <= 0) {
+            return 0;
+        }
+        envMapReader.getPixel(lWorld, computedMipLevel, c);
+        store.setX(store.x + c.r * NoL);
+        store.setY(store.y + c.g * NoL);
+        store.setZ(store.z + c.b * NoL);
+
+        return NoL;
+    }
+
+    private void toWorld(Vector3f L, Vector3f N, Vector3f tangentX, Vector3f tangentY, Vector3f store) {
+        store.set(tangentX).multLocal(L.x);
+        tmp.set(tangentY).multLocal(L.y);
+        store.addLocal(tmp);
+        tmp.set(N).multLocal(L.z);
+        store.addLocal(tmp);
+    }
+
+    private float computeMipLevel(float roughness, int numSamples, float size, float voH) {
+        // H[2] is NoH in local space
+        // adds 1.e-5 to avoid ggx / 0.0
+        float NoH = voH + 1E-5f;
+
+        // Probability Distribution Function
+        float Pdf = ggx(NoH, roughness) * NoH / (4.0f * voH);
+
+        // Solid angle represented by this sample
+        float omegaS = 1.0f / (numSamples * Pdf);
+
+        // Solid angle covered by 1 pixel with 6 faces that are EnvMapSize X EnvMapSize
+        float omegaP = 4.0f * FastMath.PI / (6.0f * size * size);
+
+        // Original paper suggest biasing the mip to improve the results
+        float mipBias = 1.0f; // I tested that the result is better with bias 1
+        double maxLod = Math.log(size) / Math.log(2f);
+        double log2 = Math.log(omegaS / omegaP) / Math.log(2);
+        return Math.min(Math.max(0.5f * (float) log2 + mipBias, 0.0f), (float) maxLod);
+    }
+
+
+    private float ggx(float NoH, float alpha) {
+        // use GGX / Trowbridge-Reitz, same as Disney and Unreal 4
+        // cf http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p3
+        float tmp = alpha / (NoH * NoH * (alpha * alpha - 1.0f) + 1.0f);
+        return tmp * tmp * (1f / FastMath.PI);
+    }
+
+    private Vector3f rotateDirection(float angle, Vector3f l, Vector3f store) {
+        float s, c, t;
+
+        s = FastMath.sin(angle);
+        c = FastMath.cos(angle);
+        t = 1.f - c;
+
+        store.x = l.x * c + l.y * s;
+        store.y = -l.x * s + l.y * c;
+        store.z = l.z * (t + c);
+        return store;
+    }
+
+    /**
+     * Computes GGX half vector in local space
+     *
+     * @param xi
+     * @param a2
+     * @param store
+     * @return
+     */
+    public Vector3f importanceSampleGGX(Vector4f xi, float a2, Vector3f store) {
         if (store == null) {
             store = new Vector3f();
         }
@@ -243,22 +345,9 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
         float sinThetaCosPhi = sinTheta * xi.z;//xi.z is cos(phi)
         float sinThetaSinPhi = sinTheta * xi.w;//xi.w is sin(phi)
 
-        Vector3f upVector = Vector3f.UNIT_X;
-
-        if (abs(normal.z) < 0.999) {
-            upVector = Vector3f.UNIT_Y;
-        }
-
-        Vector3f tangentX = tmp1.set(upVector).crossLocal(normal).normalizeLocal();
-        Vector3f tangentY = tmp2.set(normal).crossLocal(tangentX);
-
-        // Tangent to world space
-        tangentX.multLocal(sinThetaCosPhi);
-        tangentY.multLocal(sinThetaSinPhi);
-        tmp3.set(normal).multLocal(cosTheta);
-
-        // Tangent to world space
-        store.set(tangentX).addLocal(tangentY).addLocal(tmp3);
+        store.x = sinThetaCosPhi;
+        store.y = sinThetaSinPhi;
+        store.z = cosTheta;
 
         return store;
     }

+ 25 - 9
jme3-core/src/main/java/com/jme3/environment/util/CubeMapWrapper.java

@@ -31,17 +31,15 @@
  */
 package com.jme3.environment.util;
 
-import com.jme3.environment.util.EnvMapUtils;
-import com.jme3.math.ColorRGBA;
-import static com.jme3.math.FastMath.pow;
-import com.jme3.math.Vector2f;
-import com.jme3.math.Vector3f;
+import com.jme3.math.*;
 import com.jme3.texture.Image;
 import com.jme3.texture.TextureCubeMap;
 import com.jme3.texture.image.DefaultImageRaster;
 import com.jme3.texture.image.MipMapImageRaster;
 import com.jme3.util.BufferUtils;
 
+import static com.jme3.math.FastMath.pow;
+
 /**
  * Wraps a Cube map and allows to read from or write pixels into it.
  * 
@@ -57,6 +55,8 @@ public class CubeMapWrapper {
     private final Vector2f uvs = new Vector2f();
     private final Image image;
 
+    private final ColorRGBA tmpColor = new ColorRGBA();
+
     /**
      * Creates a CubeMapWrapper for the given cube map
      * Note that the cube map must be initialized, and the mipmaps sizes should 
@@ -105,7 +105,7 @@ public class CubeMapWrapper {
      * @param store the color in which to store the pixel color read.
      * @return the color of the pixel read.
      */
-    public ColorRGBA getPixel(Vector3f vector, int mipLevel, ColorRGBA store) {
+    public ColorRGBA getPixel(Vector3f vector, float mipLevel, ColorRGBA store) {
         if (mipMapRaster == null) {
             throw new IllegalArgumentException("This cube map has no mip maps");
         }
@@ -113,10 +113,26 @@ public class CubeMapWrapper {
             store = new ColorRGBA();
         }
 
-        int face = EnvMapUtils.getCubemapFaceTexCoordFromVector(vector, sizes[mipLevel], uvs, EnvMapUtils.FixSeamsMethod.Stretch);
+        int lowerMipLevel = (int) mipLevel;
+        int higherMipLevel = (int) FastMath.ceil(mipLevel);
+        float ratio = mipLevel - lowerMipLevel;
+
+        int face = EnvMapUtils.getCubemapFaceTexCoordFromVector(vector, sizes[lowerMipLevel], uvs, EnvMapUtils.FixSeamsMethod.Stretch);
         mipMapRaster.setSlice(face);
-        mipMapRaster.setMipLevel(mipLevel);
-        return mipMapRaster.getPixel((int) uvs.x, (int) uvs.y, store);
+        mipMapRaster.setMipLevel(lowerMipLevel);
+        mipMapRaster.getPixel((int) uvs.x, (int) uvs.y, store);
+
+        face = EnvMapUtils.getCubemapFaceTexCoordFromVector(vector, sizes[higherMipLevel], uvs, EnvMapUtils.FixSeamsMethod.Stretch);
+        mipMapRaster.setSlice(face);
+        mipMapRaster.setMipLevel(higherMipLevel);
+        mipMapRaster.getPixel((int) uvs.x, (int) uvs.y, tmpColor);
+
+        store.r = FastMath.interpolateLinear(ratio, store.r, tmpColor.r);
+        store.g = FastMath.interpolateLinear(ratio, store.g, tmpColor.g);
+        store.b = FastMath.interpolateLinear(ratio, store.b, tmpColor.b);
+        store.a = FastMath.interpolateLinear(ratio, store.a, tmpColor.a);
+
+        return store;
     }
 
     /**

+ 14 - 8
jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java

@@ -37,18 +37,15 @@ import com.jme3.math.*;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Node;
 import com.jme3.scene.shape.Quad;
-import com.jme3.texture.Image;
-import com.jme3.texture.Texture;
-import com.jme3.texture.Texture2D;
-import com.jme3.texture.TextureCubeMap;
+import com.jme3.texture.*;
 import com.jme3.texture.image.ColorSpace;
 import com.jme3.ui.Picture;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.TempVars;
+
 import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import static com.jme3.math.FastMath.*;
 
-import com.jme3.util.TempVars;
+import static com.jme3.math.FastMath.*;
 
 /**
  *
@@ -88,6 +85,11 @@ public class EnvMapUtils {
         None
     }
 
+    public static enum GenerationType {
+        Fast,
+        HighQuality
+    }
+
     /**
      * Creates a cube map from 6 images
      *
@@ -117,6 +119,8 @@ public class EnvMapUtils {
         cubeImage.addData(backImg.getData(0));
         cubeImage.addData(frontImg.getData(0));
 
+        cubeImage.setMipMapSizes(rightImg.getMipMapSizes());
+
         TextureCubeMap cubeMap = new TextureCubeMap(cubeImage);
         cubeMap.setAnisotropicFilter(0);
         cubeMap.setMagFilter(Texture.MagFilter.Bilinear);
@@ -148,6 +152,8 @@ public class EnvMapUtils {
             cubeImage.addData(d.duplicate());
         }
 
+        cubeImage.setMipMapSizes(srcImg.getMipMapSizes());
+
         TextureCubeMap cubeMap = new TextureCubeMap(cubeImage);
         cubeMap.setAnisotropicFilter(sourceMap.getAnisotropicFilter());
         cubeMap.setMagFilter(sourceMap.getMagFilter());
@@ -730,7 +736,7 @@ public class EnvMapUtils {
         pem.setMagFilter(Texture.MagFilter.Bilinear);
         pem.setMinFilter(Texture.MinFilter.Trilinear);
         pem.getImage().setColorSpace(ColorSpace.Linear);
-        int nbMipMap = (int) (Math.log(size) / Math.log(2) - 1);
+        int nbMipMap = Math.min(6, (int) (Math.log(size) / Math.log(2)));
         CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
         targetWrapper.initMipMaps(nbMipMap);
         return pem;

+ 1 - 1
jme3-core/src/main/java/com/jme3/texture/image/ByteAlignedImageCodec.java

@@ -110,7 +110,7 @@ class ByteAlignedImageCodec extends ImageCodec {
     }
     
     public void readComponents(ByteBuffer buf, int x, int y, int width, int offset, int[] components, byte[] tmp) {
-        readPixelRaw(buf, (x + y * width + offset) * bpp + offset, bpp, tmp);
+        readPixelRaw(buf, (x + y * width ) * bpp + offset, bpp, tmp);
         components[0] = readComponent(tmp, ap, az);
         components[1] = readComponent(tmp, rp, rz);
         components[2] = readComponent(tmp, gp, gz);

+ 7 - 15
jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java

@@ -34,8 +34,8 @@ package com.jme3.util;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.FastMath;
 import com.jme3.texture.Image;
-import com.jme3.texture.Image.Format;
 import com.jme3.texture.image.ImageRaster;
+
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
 
@@ -58,8 +58,8 @@ public class MipMapGenerator {
         
         float xRatio = ((float)(input.getWidth()  - 1)) / output.getWidth();
         float yRatio = ((float)(input.getHeight() - 1)) / output.getHeight();
-        
-        ColorRGBA outputColor = new ColorRGBA();
+
+        ColorRGBA outputColor = new ColorRGBA(0, 0, 0, 0);
         ColorRGBA bottomLeft = new ColorRGBA();
         ColorRGBA bottomRight = new ColorRGBA();
         ColorRGBA topLeft = new ColorRGBA();
@@ -72,29 +72,21 @@ public class MipMapGenerator {
                 
                 int x2 = (int)x2f;
                 int y2 = (int)y2f;
-                
-                float xDiff = x2f - x2;
-                float yDiff = y2f - y2;
-                
+
                 input.getPixel(x2,     y2,     bottomLeft);
                 input.getPixel(x2 + 1, y2,     bottomRight);
                 input.getPixel(x2,     y2 + 1, topLeft);
                 input.getPixel(x2 + 1, y2 + 1, topRight);
-                
-                bottomLeft.multLocal(  (1f - xDiff) * (1f - yDiff) );
-                bottomRight.multLocal( (xDiff)      * (1f - yDiff) );
-                topLeft.multLocal(     (1f - xDiff) * (yDiff) );
-                topRight.multLocal(    (xDiff)      * (yDiff) );
-                
+
                 outputColor.set(bottomLeft).addLocal(bottomRight)
                            .addLocal(topLeft).addLocal(topRight);
-                
+                outputColor.multLocal(1f / 4f);
                 output.setPixel(x, y, outputColor);
             }
         }
         return outputImage;
     }
-    
+
     public static Image resizeToPowerOf2(Image original){
         int potWidth = FastMath.nearestPowerOfTwo(original.getWidth());
         int potHeight = FastMath.nearestPowerOfTwo(original.getHeight());

+ 9 - 12
jme3-examples/src/main/java/jme3test/light/pbr/RefEnv.java

@@ -11,12 +11,8 @@ import com.jme3.input.controls.ActionListener;
 import com.jme3.input.controls.KeyTrigger;
 import com.jme3.light.LightProbe;
 import com.jme3.material.Material;
-import com.jme3.math.ColorRGBA;
-import com.jme3.math.Quaternion;
-import com.jme3.math.Vector3f;
-import com.jme3.scene.Geometry;
-import com.jme3.scene.Node;
-import com.jme3.scene.Spatial;
+import com.jme3.math.*;
+import com.jme3.scene.*;
 import com.jme3.ui.Picture;
 import com.jme3.util.SkyFactory;
 
@@ -39,11 +35,13 @@ public class RefEnv extends SimpleApplication {
     @Override
     public void simpleInitApp() {
 
-        cam.setLocation(new Vector3f(-17.713732f, 1.8661976f, 17.156784f));
-        cam.setRotation(new Quaternion(0.021403445f, 0.9428821f, -0.06178002f, 0.32664734f));
+        cam.setLocation(new Vector3f(-17.95047f, 4.917353f, -17.970531f));
+        cam.setRotation(new Quaternion(0.11724457f, 0.29356146f, -0.03630452f, 0.94802815f));
+//        cam.setLocation(new Vector3f(14.790441f, 7.164179f, 19.720007f));
+//        cam.setRotation(new Quaternion(-0.038261678f, 0.9578362f, -0.15233073f, -0.24058504f));
         flyCam.setDragToRotate(true);
         flyCam.setMoveSpeed(5);
-        Spatial sc = assetManager.loadModel("Models/gltf/ref/scene.gltf");
+        Spatial sc = assetManager.loadModel("Scenes/PBR/ref/scene.gltf");
         rootNode.attachChild(sc);
         Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap);
         rootNode.attachChild(sky);
@@ -68,7 +66,7 @@ public class RefEnv extends SimpleApplication {
             public void onAction(String name, boolean isPressed, float tpf) {
                 if (name.equals("tex") && isPressed) {
                     if (tex == null) {
-                        return;
+                        tex = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(stateManager.getState(EnvironmentCamera.class).debugEnv, assetManager);
                     }
                     if (tex.getParent() == null) {
                         guiNode.attachChild(tex);
@@ -120,13 +118,12 @@ public class RefEnv extends SimpleApplication {
         frame++;
 
         if (frame == 2) {
-            final LightProbe probe = LightProbeFactory.makeProbe(stateManager.getState(EnvironmentCamera.class), rootNode, new JobProgressAdapter<LightProbe>() {
+            final LightProbe probe = LightProbeFactory.makeProbe(stateManager.getState(EnvironmentCamera.class), rootNode, EnvMapUtils.GenerationType.Fast, new JobProgressAdapter<LightProbe>() {
 
                 @Override
                 public void done(LightProbe result) {
                     System.err.println("Done rendering env maps");
                     tex = EnvMapUtils.getCubeMapCrossDebugViewWithMipMaps(result.getPrefilteredEnvMap(), assetManager);
-                  //  guiNode.attachChild(tex);
                     rootNode.getChild(0).setCullHint(Spatial.CullHint.Dynamic);
                 }
             });

BIN
jme3-testdata/src/main/resources/Scenes/PBR/ref/scene.bin


+ 2466 - 0
jme3-testdata/src/main/resources/Scenes/PBR/ref/scene.gltf

@@ -0,0 +1,2466 @@
+{
+  "accessors": [
+    {
+      "bufferView": 1,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 5784,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 11568,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 17352,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 11520,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 23136,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 28920,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 23040,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 34704,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 40488,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 34560,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 46272,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 52056,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 46080,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 57840,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 63624,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 57600,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 69408,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 75192,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 69120,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 80976,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 86760,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 80640,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 92544,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 98328,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 92160,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 104112,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 109896,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 103680,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 115680,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 121464,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 115200,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 127248,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 133032,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 126720,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 138816,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 144600,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 138240,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 150384,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 156168,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 149760,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 161952,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 167736,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 161280,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 173520,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 179304,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 172800,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 185088,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 190872,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 184320,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 196656,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 202440,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 195840,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 208224,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 214008,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 207360,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 219792,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 225576,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 218880,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 231360,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 237144,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 230400,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 242928,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 1,
+      "byteOffset": 248712,
+      "componentType": 5126,
+      "count": 482,
+      "max": [
+        1,
+        1,
+        1
+      ],
+      "min": [
+        -1,
+        -1,
+        -1
+      ],
+      "type": "VEC3"
+    },
+    {
+      "bufferView": 0,
+      "byteOffset": 241920,
+      "componentType": 5125,
+      "count": 2880,
+      "max": [
+        481
+      ],
+      "min": [
+        0
+      ],
+      "type": "SCALAR"
+    }
+  ],
+  "asset": {
+    "generator": "Sketchfab (OSG glTF plugin)",
+    "version": "2.0"
+  },
+  "bufferViews": [
+    {
+      "buffer": 0,
+      "byteLength": 253440,
+      "byteOffset": 0,
+      "name": "ScalarBufferView",
+      "target": 34963
+    },
+    {
+      "buffer": 0,
+      "byteLength": 254496,
+      "byteOffset": 253440,
+      "byteStride": 12,
+      "name": "Vec4BufferView",
+      "target": 34962
+    }
+  ],
+  "buffers": [
+    {
+      "byteLength": 507936,
+      "uri": "scene.bin"
+    }
+  ],
+  "materials": [
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.022",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.021",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.90000000000000002
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.020",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 1
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.019",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.90000000000000002
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.010",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.011",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.20000000000000001
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.012",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.20000000000000001
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.013",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.29999999999999999
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.014",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.40000000000000002
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.018",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.80000000000000004
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.017",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.69999999999999996
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.016",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.59999999999999998
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.015",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0,
+          0,
+          0,
+          1
+        ],
+        "metallicFactor": 0,
+        "roughnessFactor": 0.5
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.006",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.5
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.007",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.59999999999999998
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.008",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.69999999999999996
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.009",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.80000000000000004
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.005",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.40000000000000002
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.004",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.29999999999999999
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.003",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.20000000000000001
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.002",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0.10000000000000001
+      }
+    },
+    {
+      "doubleSided": true,
+      "emissiveFactor": [
+        0,
+        0,
+        0
+      ],
+      "name": "Material.001",
+      "pbrMetallicRoughness": {
+        "baseColorFactor": [
+          0.80000001190000003,
+          0.80000001190000003,
+          0.80000001190000003,
+          1
+        ],
+        "metallicFactor": 1,
+        "roughnessFactor": 0
+      }
+    }
+  ],
+  "meshes": [
+    {
+      "name": "GeodeSphere",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 1,
+            "POSITION": 0
+          },
+          "indices": 2,
+          "material": 21,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.001",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 4,
+            "POSITION": 3
+          },
+          "indices": 5,
+          "material": 20,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.002",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 7,
+            "POSITION": 6
+          },
+          "indices": 8,
+          "material": 19,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.003",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 10,
+            "POSITION": 9
+          },
+          "indices": 11,
+          "material": 18,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.004",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 13,
+            "POSITION": 12
+          },
+          "indices": 14,
+          "material": 17,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.005",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 16,
+            "POSITION": 15
+          },
+          "indices": 17,
+          "material": 16,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.006",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 19,
+            "POSITION": 18
+          },
+          "indices": 20,
+          "material": 15,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.007",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 22,
+            "POSITION": 21
+          },
+          "indices": 23,
+          "material": 14,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.008",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 25,
+            "POSITION": 24
+          },
+          "indices": 26,
+          "material": 13,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.009",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 28,
+            "POSITION": 27
+          },
+          "indices": 29,
+          "material": 12,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.010",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 31,
+            "POSITION": 30
+          },
+          "indices": 32,
+          "material": 11,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.011",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 34,
+            "POSITION": 33
+          },
+          "indices": 35,
+          "material": 10,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.012",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 37,
+            "POSITION": 36
+          },
+          "indices": 38,
+          "material": 9,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.013",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 40,
+            "POSITION": 39
+          },
+          "indices": 41,
+          "material": 8,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.014",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 43,
+            "POSITION": 42
+          },
+          "indices": 44,
+          "material": 7,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.015",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 46,
+            "POSITION": 45
+          },
+          "indices": 47,
+          "material": 6,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.016",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 49,
+            "POSITION": 48
+          },
+          "indices": 50,
+          "material": 5,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.017",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 52,
+            "POSITION": 51
+          },
+          "indices": 53,
+          "material": 4,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.018",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 55,
+            "POSITION": 54
+          },
+          "indices": 56,
+          "material": 3,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.019",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 58,
+            "POSITION": 57
+          },
+          "indices": 59,
+          "material": 2,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.020",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 61,
+            "POSITION": 60
+          },
+          "indices": 62,
+          "material": 1,
+          "mode": 4
+        }
+      ]
+    },
+    {
+      "name": "GeodeSphere.021",
+      "primitives": [
+        {
+          "attributes": {
+            "NORMAL": 64,
+            "POSITION": 63
+          },
+          "indices": 65,
+          "material": 0,
+          "mode": 4
+        }
+      ]
+    }
+  ],
+  "nodes": [
+    {
+      "children": [
+        1
+      ],
+      "name": "RootNode (gltf orientation matrix)",
+      "rotation": [
+        -0.70710678118654746,
+        -0,
+        -0,
+        0.70710678118654757
+      ]
+    },
+    {
+      "children": [
+        2
+      ],
+      "name": "RootNode (model correction matrix)"
+    },
+    {
+      "children": [
+        3,
+        5,
+        7,
+        9,
+        11,
+        13,
+        15,
+        17,
+        19,
+        21,
+        23,
+        25,
+        27,
+        29,
+        31,
+        33,
+        35,
+        37,
+        39,
+        41,
+        43,
+        45
+      ],
+      "name": "Root"
+    },
+    {
+      "children": [
+        4
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -15,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere"
+    },
+    {
+      "mesh": 0,
+      "name": "GeodeSphere"
+    },
+    {
+      "children": [
+        6
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -12,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.001"
+    },
+    {
+      "mesh": 1,
+      "name": "GeodeSphere.001"
+    },
+    {
+      "children": [
+        8
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -9,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.002"
+    },
+    {
+      "mesh": 2,
+      "name": "GeodeSphere.002"
+    },
+    {
+      "children": [
+        10
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -6,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.003"
+    },
+    {
+      "mesh": 3,
+      "name": "GeodeSphere.003"
+    },
+    {
+      "children": [
+        12
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -3,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.004"
+    },
+    {
+      "mesh": 4,
+      "name": "GeodeSphere.004"
+    },
+    {
+      "children": [
+        14
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        9,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.005"
+    },
+    {
+      "mesh": 5,
+      "name": "GeodeSphere.005"
+    },
+    {
+      "children": [
+        16
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        6,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.006"
+    },
+    {
+      "mesh": 6,
+      "name": "GeodeSphere.006"
+    },
+    {
+      "children": [
+        18
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        3,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.007"
+    },
+    {
+      "mesh": 7,
+      "name": "GeodeSphere.007"
+    },
+    {
+      "children": [
+        20
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.008"
+    },
+    {
+      "mesh": 8,
+      "name": "GeodeSphere.008"
+    },
+    {
+      "children": [
+        22
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.009"
+    },
+    {
+      "mesh": 9,
+      "name": "GeodeSphere.009"
+    },
+    {
+      "children": [
+        24
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        3,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.010"
+    },
+    {
+      "mesh": 10,
+      "name": "GeodeSphere.010"
+    },
+    {
+      "children": [
+        26
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        6,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.011"
+    },
+    {
+      "mesh": 11,
+      "name": "GeodeSphere.011"
+    },
+    {
+      "children": [
+        28
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        9,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.012"
+    },
+    {
+      "mesh": 12,
+      "name": "GeodeSphere.012"
+    },
+    {
+      "children": [
+        30
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -3,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.013"
+    },
+    {
+      "mesh": 13,
+      "name": "GeodeSphere.013"
+    },
+    {
+      "children": [
+        32
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -6,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.014"
+    },
+    {
+      "mesh": 14,
+      "name": "GeodeSphere.014"
+    },
+    {
+      "children": [
+        34
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -9,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.015"
+    },
+    {
+      "mesh": 15,
+      "name": "GeodeSphere.015"
+    },
+    {
+      "children": [
+        36
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -12,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.016"
+    },
+    {
+      "mesh": 16,
+      "name": "GeodeSphere.016"
+    },
+    {
+      "children": [
+        38
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        -15,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.017"
+    },
+    {
+      "mesh": 17,
+      "name": "GeodeSphere.017"
+    },
+    {
+      "children": [
+        40
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        12,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.018"
+    },
+    {
+      "mesh": 18,
+      "name": "GeodeSphere.018"
+    },
+    {
+      "children": [
+        42
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        15,
+        0,
+        2,
+        1
+      ],
+      "name": "Sphere.019"
+    },
+    {
+      "mesh": 19,
+      "name": "GeodeSphere.019"
+    },
+    {
+      "children": [
+        44
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        12,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.020"
+    },
+    {
+      "mesh": 20,
+      "name": "GeodeSphere.020"
+    },
+    {
+      "children": [
+        46
+      ],
+      "matrix": [
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        0,
+        0,
+        0,
+        1,
+        0,
+        15,
+        0,
+        -2,
+        1
+      ],
+      "name": "Sphere.021"
+    },
+    {
+      "mesh": 21,
+      "name": "GeodeSphere.021"
+    }
+  ],
+  "scene": 0,
+  "scenes": [
+    {
+      "name": "OSG_Scene",
+      "nodes": [
+        0
+      ]
+    }
+  ]
+}
+