Explorar el Código

Make lightprobes serializable on demand when baked with accelerated baking. Make them serializable by default if created with LightProbeFactory2.

Accelerate Spherical Harmonics generator.
Riccardo Balbo hace 2 años
padre
commit
be89b88e02

+ 18 - 2
jme3-core/src/main/java/com/jme3/environment/EnvironmentProbeControl.java

@@ -1,10 +1,10 @@
 package com.jme3.environment;
 
 import java.util.function.Function;
-import java.util.function.Predicate;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.environment.baker.IBLGLEnvBakerLight;
+import com.jme3.environment.baker.IBLHybridEnvBakerLight;
 import com.jme3.light.LightProbe;
 import com.jme3.math.Vector3f;
 import com.jme3.renderer.RenderManager;
@@ -28,6 +28,8 @@ public class EnvironmentProbeControl extends LightProbe implements Control {
     private int envMapSize;
     private Spatial spatial;
     private boolean BAKE_NEEDED = true;
+    private boolean USE_GL_IR = true;
+    private boolean serializable = false;
 
     private Function<Geometry, Boolean> filter = (s) -> {
         return s.getUserData("tags.env") != null;
@@ -62,6 +64,14 @@ public class EnvironmentProbeControl extends LightProbe implements Control {
         return null;
     }
 
+    public void setSerializeBakeResults(boolean v) {
+        serializable = v;
+    }
+
+    public boolean isSerializeBakeResults() {
+        return serializable;
+    }
+
     @Override
     public void setSpatial(Spatial spatial) {
 
@@ -92,7 +102,13 @@ public class EnvironmentProbeControl extends LightProbe implements Control {
 
     void rebakeNow() {
 
-        IBLGLEnvBakerLight baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, Format.Depth, envMapSize, envMapSize);
+        IBLHybridEnvBakerLight baker;
+        if(!USE_GL_IR){
+            baker = new IBLHybridEnvBakerLight(renderManager, assetManager, Format.RGB16F, Format.Depth, envMapSize, envMapSize);
+        } else {
+            baker = new IBLGLEnvBakerLight(renderManager, assetManager, Format.RGB16F, Format.Depth, envMapSize, envMapSize);
+        }
+        baker.setTexturePulling(isSerializeBakeResults());
 
         baker.bakeEnvironment(spatial, Vector3f.ZERO, 0.001f, 1000f, filter);
         baker.bakeSpecularIBL();

+ 3 - 3
jme3-core/src/main/java/com/jme3/environment/LightProbeFactory2.java

@@ -33,6 +33,7 @@ package com.jme3.environment;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.environment.baker.IBLGLEnvBakerLight;
+import com.jme3.environment.baker.IBLHybridEnvBakerLight;
 import com.jme3.environment.util.EnvMapUtils;
 import com.jme3.light.LightProbe;
 import com.jme3.math.Vector3f;
@@ -61,10 +62,9 @@ public class LightProbeFactory2 {
      */
     public static LightProbe makeProbe(RenderManager rm,
     AssetManager am, int size,Vector3f pos, float frustumNear,float frustumFar,Spatial scene) {
-        IBLGLEnvBakerLight baker=new IBLGLEnvBakerLight(rm,
-         am, Format.RGB16F, Format.Depth, 
-         size, size);
+        IBLHybridEnvBakerLight baker=new IBLGLEnvBakerLight(rm, am, Format.RGB16F, Format.Depth, size, size);
 
+        baker.setTexturePulling(true);
         baker.bakeEnvironment(scene,pos, frustumNear,frustumFar,null);
         baker.bakeSpecularIBL();
         baker.bakeSphericalHarmonicsCoefficients();

+ 16 - 1
jme3-core/src/main/java/com/jme3/environment/baker/EnvBaker.java

@@ -34,5 +34,20 @@ public interface EnvBaker {
      * This method should be called when the baker is no longer needed
      * It will clean up all the resources
      */
-    public void clean();    
+    public void clean();
+    
+
+
+    /**
+     * Set if textures should be pulled from the GPU
+     * @param v
+     */
+    public void setTexturePulling(boolean v);
+
+
+    /**
+     * Get if textures should be pulled from the GPU
+     * @return
+     */
+    public boolean isTexturePulling();
 }

+ 84 - 11
jme3-core/src/main/java/com/jme3/environment/baker/GenericEnvBaker.java

@@ -32,8 +32,13 @@
 
 package com.jme3.environment.baker;
 
+import java.io.ByteArrayOutputStream;
 import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import com.jme3.asset.AssetManager;
 import com.jme3.math.ColorRGBA;
@@ -45,6 +50,7 @@ import com.jme3.renderer.ViewPort;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Spatial;
 import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.Texture;
 import com.jme3.texture.FrameBuffer.FrameBufferTarget;
 import com.jme3.texture.Image.Format;
 import com.jme3.texture.Texture.MagFilter;
@@ -97,10 +103,13 @@ public abstract class GenericEnvBaker implements EnvBaker {
     protected final RenderManager renderManager;
     protected final AssetManager assetManager;
     protected final Camera cam;
-    protected final boolean copyToRam;
+    protected  boolean texturePulling=false;
+    protected List<ByteArrayOutputStream> bos = new ArrayList<>();
+    private static final Logger LOG=Logger.getLogger(GenericEnvBaker.class.getName());
 
-    public GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat, Format depthFormat, int env_size, boolean copyToRam) {
-        this.copyToRam = copyToRam;
+ 
+
+    public GenericEnvBaker(RenderManager rm, AssetManager am, Format colorFormat, Format depthFormat, int env_size) {
         this.depthFormat = depthFormat;
 
         renderManager = rm;
@@ -115,6 +124,16 @@ public abstract class GenericEnvBaker implements EnvBaker {
         env.getImage().setColorSpace(ColorSpace.Linear);
     }
 
+    @Override
+    public void setTexturePulling(boolean v) {
+        texturePulling = v;
+    }
+
+    @Override
+    public boolean isTexturePulling() {
+        return texturePulling;
+    }
+
     public TextureCubeMap getEnvMap() {
         return env;
     }
@@ -141,6 +160,8 @@ public abstract class GenericEnvBaker implements EnvBaker {
         envbaker.setDepthTarget(FrameBufferTarget.newTarget(depthFormat));
         envbaker.setSrgb(false);
 
+        if(isTexturePulling())startPulling();
+
         for (int i = 0; i < 6; i++) envbaker.addColorTarget(FrameBufferTarget.newTarget(env).face(TextureCubeMap.Face.values()[i]));
 
         for (int i = 0; i < 6; i++) {
@@ -164,18 +185,70 @@ public abstract class GenericEnvBaker implements EnvBaker {
             renderManager.renderViewPort(viewPort, 0.16f);
             renderManager.setRenderFilter(ofilter);
 
-            if (copyToRam) {
-                ByteBuffer face = BufferUtils.createByteBuffer((env.getImage().getWidth() * env.getImage().getHeight() * (env.getImage().getFormat().getBitsPerPixel() / 8)));
-                renderManager.getRenderer().readFrameBufferWithFormat(envbaker, face, env.getImage().getFormat());
-                face.rewind();
-                env.getImage().setData(i, face);
+            if (isTexturePulling()) pull(envbaker, env, i);
+            
+        }
 
-            }
+        if (isTexturePulling()) endPulling(env);
+        env.getImage().clearUpdateNeeded();           
+        envbaker.dispose();
+    }
+    
+
+    /**
+     * Starts pulling the data from the framebuffer into the texture
+     */
+    protected void startPulling() {
+        bos.clear();
+    }
+
+    /**
+     * Pulls the data from the framebuffer into the texture
+     * Nb. mipmaps must be pulled sequentially on the same faceId
+     * @param fb the framebuffer to pull from
+     * @param env the texture to pull into
+     * @param faceId id of face if cubemap or 0 otherwise
+     * @return
+     */
+    protected ByteBuffer pull(FrameBuffer fb, Texture env, int faceId) {
+
+        if (fb.getColorTarget().getFormat() != env.getImage().getFormat())
+            throw new IllegalArgumentException("Format mismatch: " + fb.getColorTarget().getFormat() + "!=" + env.getImage().getFormat());
+
+        ByteBuffer face = BufferUtils.createByteBuffer(fb.getWidth() * fb.getHeight() * (fb.getColorTarget().getFormat().getBitsPerPixel() / 8));
+        renderManager.getRenderer().readFrameBufferWithFormat(fb, face, fb.getColorTarget().getFormat());
+        face.rewind();
+
+        while (bos.size() <= faceId) bos.add(null);
+        ByteArrayOutputStream bo = bos.get(faceId);
+        if (bo == null) bos.set(faceId, bo = new ByteArrayOutputStream());
+        try {
+            byte array[] = new byte[face.limit()];
+            face.get(array);
+            bo.write(array);
+        } catch (Exception ex) {
+            LOG.log(Level.SEVERE, null, ex);
         }
+        return face;
+    }
 
-        env.getImage().clearUpdateNeeded();
 
-        envbaker.dispose();
+    /**
+     * Ends pulling the data into the texture
+     * @param tx the texture to pull into
+     */
+    protected void endPulling(Texture tx) {
+        for (int i = 0; i < bos.size(); i++) {
+            ByteArrayOutputStream bo = bos.get(i);
+            if (bo == null) {
+                LOG.log(Level.SEVERE, "Missing face {0}. Pulling incomplete!", i);
+                continue;
+            }
+            ByteBuffer faceMip = ByteBuffer.wrap(bo.toByteArray());
+            tx.getImage().setData(i, faceMip);
+        }
+        bos.clear();
+        tx.getImage().clearUpdateNeeded();
     }
 
 }

+ 39 - 18
jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBaker.java

@@ -54,7 +54,7 @@ import com.jme3.ui.Picture;
 
 
 /**
- *  An env baker for IBL that runs entirely on the GPU 
+ * Fully accelerated env baker for IBL that runs entirely on the GPU
  * 
  * @author Riccardo Balbo
  */
@@ -62,7 +62,6 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
     protected Texture2D brtf;
     protected TextureCubeMap irradiance;
     protected TextureCubeMap specular;
-
     public IBLGLEnvBaker(RenderManager rm,AssetManager am,
                         Format format,
                         Format depthFormat,
@@ -70,7 +69,7 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
                         int irradiance_size,
                         int brtf_size
     ){
-        super(rm,am,format,depthFormat,env_size,false);  
+        super(rm,am,format,depthFormat,env_size);  
 
         irradiance=new TextureCubeMap(irradiance_size,irradiance_size,format);
         irradiance.setMagFilter(MagFilter.Bilinear);
@@ -80,7 +79,7 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
 
         specular=new TextureCubeMap(specular_size,specular_size,format);
         specular.setMagFilter(MagFilter.Bilinear);
-        specular.setMinFilter(MinFilter.BilinearNoMipMaps);
+        specular.setMinFilter(MinFilter.Trilinear);
         specular.setWrap(WrapMode.EdgeClamp);
         specular.getImage().setColorSpace(ColorSpace.Linear);
         int nbMipMaps=(int)(Math.log(specular_size)/Math.log(2)+1);
@@ -117,33 +116,41 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
         mat.setBoolean("UseSpecularIBL",true);
         mat.setTexture("EnvMap",env);
         screen.setMaterial(mat);
-    
-        for(int mip=0;mip<specular.getImage().getMipMapSizes().length;mip++){
-            int mipWidth=(int)(specular.getImage().getWidth()*FastMath.pow(0.5f,mip));
-            int mipHeight=(int)(specular.getImage().getHeight()*FastMath.pow(0.5f,mip));
 
-            FrameBuffer specularbaker=new FrameBuffer(mipWidth,mipHeight,1);
+        if (isTexturePulling())startPulling();
+        
+        for (int mip = 0; mip < specular.getImage().getMipMapSizes().length; mip++) {
+            int mipWidth = (int) (specular.getImage().getWidth() * FastMath.pow(0.5f, mip));
+            int mipHeight = (int) (specular.getImage().getHeight() * FastMath.pow(0.5f, mip));
+
+            FrameBuffer specularbaker = new FrameBuffer(mipWidth, mipHeight, 1);
             specularbaker.setSrgb(false);
 
-            for(int i=0;i<6;i++)specularbaker.addColorTarget( FrameBufferTarget.newTarget(specular).level(mip).face(i) );
-            
-            float roughness=(float)mip/(float)(specular.getImage().getMipMapSizes().length-1);
-            mat.setFloat("Roughness",roughness);
+            for (int i = 0; i < 6; i++) specularbaker.addColorTarget(FrameBufferTarget.newTarget(specular).level(mip).face(i));
 
-            for(int i=0;i<6;i++){
+            float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1);
+            mat.setFloat("Roughness", roughness);
+
+            for (int i = 0; i < 6; i++) {
                 specularbaker.setTargetIndex(i);
-                mat.setInt("FaceId",i);
+                mat.setInt("FaceId", i);
 
                 screen.updateLogicalState(0);
                 screen.updateGeometricState();
 
-                renderManager.setCamera(getCam(i,specularbaker.getWidth(),specularbaker.getHeight(),Vector3f.ZERO,1,1000),false);
+                renderManager.setCamera(getCam(i, specularbaker.getWidth(), specularbaker.getHeight(), Vector3f.ZERO, 1, 1000), false);
                 renderManager.getRenderer().setFrameBuffer(specularbaker);
                 renderManager.renderGeometry(screen);
+
+                if (isTexturePulling())  pull(specularbaker, specular,i);               
+                
             }
             specularbaker.dispose();
-        }        
-        specular.setMinFilter(MinFilter.Trilinear);        
+        }
+        
+        if (isTexturePulling())endPulling(specular);
+        specular.getImage().clearUpdateNeeded();
+        // specular.setMinFilter(MinFilter.Trilinear);        
     }
 
     @Override
@@ -157,6 +164,8 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
         brtfbaker.setSrgb(false);
         brtfbaker.addColorTarget(FrameBufferTarget.newTarget(brtf));
 
+        if(isTexturePulling())startPulling();
+
         Camera envcam=getCam(0,brtf.getImage().getWidth(),brtf.getImage().getHeight(),Vector3f.ZERO,1,1000);
 
         Material mat=new Material(assetManager,"Common/IBL/IBLKernels.j3md");
@@ -169,8 +178,13 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
         screen.updateLogicalState(0);
         screen.updateGeometricState();       
         renderManager.renderGeometry(screen);
+
+        if(isTexturePulling()) pull(brtfbaker,brtf,0);
        
         brtfbaker.dispose();
+
+        if (isTexturePulling()) endPulling(brtf);
+        brtf.getImage().clearUpdateNeeded();
      
         return brtf;
     }
@@ -184,6 +198,8 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
 
         FrameBuffer irradiancebaker=new FrameBuffer(irradiance.getImage().getWidth(),irradiance.getImage().getHeight(),1);
         irradiancebaker.setSrgb(false);
+
+        if(isTexturePulling())startPulling();
         
         for(int i=0;i<6;i++) irradiancebaker.addColorTarget(FrameBufferTarget.newTarget(irradiance).face(TextureCubeMap.Face.values()[i]));
 
@@ -205,10 +221,15 @@ public class IBLGLEnvBaker extends GenericEnvBaker implements IBLEnvBaker{
             ,false);
             renderManager.getRenderer().setFrameBuffer(irradiancebaker);
             renderManager.renderGeometry(screen);
+
+            if(isTexturePulling()) pull(irradiancebaker,irradiance,i);
         }
 
         irradiancebaker.dispose();
 
+        if (isTexturePulling()) endPulling(irradiance);
+        irradiance.getImage().clearUpdateNeeded();
+
     }
 
 

+ 100 - 66
jme3-core/src/main/java/com/jme3/environment/baker/IBLGLEnvBakerLight.java

@@ -29,107 +29,141 @@
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
-
 package com.jme3.environment.baker;
 
+import java.nio.ByteBuffer;
+import java.util.logging.Logger;
+
 import com.jme3.asset.AssetManager;
-import com.jme3.environment.util.EnvMapUtils;
 import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
 import com.jme3.math.FastMath;
+import com.jme3.math.Vector2f;
 import com.jme3.math.Vector3f;
+import com.jme3.renderer.Caps;
 import com.jme3.renderer.RenderManager;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.shape.Box;
 import com.jme3.texture.FrameBuffer;
-import com.jme3.texture.TextureCubeMap;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture2D;
 import com.jme3.texture.FrameBuffer.FrameBufferTarget;
 import com.jme3.texture.Image.Format;
-import com.jme3.texture.Texture.MagFilter;
-import com.jme3.texture.Texture.MinFilter;
-import com.jme3.texture.Texture.WrapMode;
 import com.jme3.texture.image.ColorSpace;
+import com.jme3.texture.image.ImageRaster;
+import com.jme3.util.BufferUtils;
 
 /**
- * An env baker for IBL that bakes the specular map on the GPU and uses
- * spherical harmonics for the irradiance map.
+ * Fully accelerated env baker for IBL that bakes the specular map and spherical harmonics
+ * on the GPU.
  * 
- * This is lighter on VRAM but uses the CPU to compute the irradiance map.
+ * This is lighter on VRAM but it is not as parallelized as IBLGLEnvBaker
  * 
  * @author Riccardo Balbo
  */
-public class IBLGLEnvBakerLight extends GenericEnvBaker implements IBLEnvBakerLight {
-    protected TextureCubeMap specular;
-    protected Vector3f[] shCoef;
-
-    public IBLGLEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size
-
-    ) {
-        super(rm, am, format, depthFormat, env_size, true);
-
-        specular = new TextureCubeMap(specular_size, specular_size, format);
-        specular.setMagFilter(MagFilter.Bilinear);
-        specular.setMinFilter(MinFilter.BilinearNoMipMaps);
-        specular.setWrap(WrapMode.EdgeClamp);
-        specular.getImage().setColorSpace(ColorSpace.Linear);
-        int nbMipMaps = (int) (Math.log(specular_size) / Math.log(2) + 1);
-        if (nbMipMaps > 6) nbMipMaps = 6;
-        int[] sizes = new int[nbMipMaps];
-        for (int i = 0; i < nbMipMaps; i++) {
-            int size = (int) FastMath.pow(2, nbMipMaps - 1 - i);
-            sizes[i] = size * size * (specular.getImage().getFormat().getBitsPerPixel() / 8);
-        }
-        specular.getImage().setMipMapSizes(sizes);
+public class IBLGLEnvBakerLight extends IBLHybridEnvBakerLight {
+    public final static int NUM_SH_COEFFICIENT = 9;
+    private static final Logger LOG = Logger.getLogger(IBLGLEnvBakerLight.class.getName());
+
+    public IBLGLEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size) {
+        super(rm, am, format, depthFormat, env_size, specular_size);
+    }
+
+    @Override
+    public boolean isTexturePulling() { 
+        return this.texturePulling;
     }
 
     @Override
-    public void bakeSpecularIBL() {
+    public void bakeSphericalHarmonicsCoefficients() {
         Box boxm = new Box(1, 1, 1);
         Geometry screen = new Geometry("BakeBox", boxm);
 
-        Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md");
-        mat.setBoolean("UseSpecularIBL", true);
-        mat.setTexture("EnvMap", env);
+        Material mat = new Material(assetManager, "Common/IBLSphH/IBLSphH.j3md");
+        mat.setTexture("Texture", env);
+        mat.setVector2("Resolution", new Vector2f(env.getImage().getWidth(), env.getImage().getHeight()));
         screen.setMaterial(mat);
+        
+        
+        float remapMaxValue = 0;
+        Format format = Format.RGBA32F;
+        if (!renderManager.getRenderer().getCaps().contains(Caps.FloatTexture)) {
+            LOG.warning("Float textures not supported, using RGB8 instead. This may cause accuracy issues.");
+            format = Format.RGBA8;
+            remapMaxValue = 0.05f;
+        }
 
-        for (int mip = 0; mip < specular.getImage().getMipMapSizes().length; mip++) {
-            int mipWidth = (int) (specular.getImage().getWidth() * FastMath.pow(0.5f, mip));
-            int mipHeight = (int) (specular.getImage().getHeight() * FastMath.pow(0.5f, mip));
+        
+        if (remapMaxValue > 0) {
+            mat.setFloat("RemapMaxValue", remapMaxValue);
+        } else {
+            mat.clearParam("RemapMaxValue");
+        }
 
-            FrameBuffer specularbaker = new FrameBuffer(mipWidth, mipHeight, 1);
-            specularbaker.setSrgb(false);
-            for (int i = 0; i < 6; i++) specularbaker.addColorTarget(FrameBufferTarget.newTarget(specular).level(mip).face(i));
+        Texture2D shCoefTx[] = {
+            new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format),
+            new Texture2D(NUM_SH_COEFFICIENT, 1, 1, format)
+        };
 
-            float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1);
-            mat.setFloat("Roughness", roughness);
 
-            for (int i = 0; i < 6; i++) {
-                specularbaker.setTargetIndex(i);
-                mat.setInt("FaceId", i);
+        FrameBuffer shbaker = new FrameBuffer(NUM_SH_COEFFICIENT, 1, 1);
+        shbaker.setSrgb(false);
+        shbaker.addColorTarget(FrameBufferTarget.newTarget(shCoefTx[0]));
+        shbaker.addColorTarget(FrameBufferTarget.newTarget(shCoefTx[1]));
 
-                screen.updateLogicalState(0);
-                screen.updateGeometricState();
+        int renderOnT = -1;
 
-                renderManager.setCamera(getCam(i, specularbaker.getWidth(), specularbaker.getHeight(), Vector3f.ZERO, 1, 1000), false);
-                renderManager.getRenderer().setFrameBuffer(specularbaker);
-                renderManager.renderGeometry(screen);
+        for (int faceId = 0; faceId < 6; faceId++) {
+            if (renderOnT != -1) {
+                int s = renderOnT;
+                renderOnT = renderOnT == 0 ? 1 : 0;
+                mat.setTexture("ShCoef", shCoefTx[s]);
+                mat.setInt("FaceId", faceId);
+            } else {
+                renderOnT = 0;
             }
-            specularbaker.dispose();
+
+            screen.updateLogicalState(0);
+            screen.updateGeometricState();
+
+            shbaker.setTargetIndex(renderOnT);  
+            
+            renderManager.setCamera(getCam(0, shbaker.getWidth(), shbaker.getHeight(), Vector3f.ZERO, 1, 1000), false);
+            renderManager.getRenderer().setFrameBuffer(shbaker);
+            renderManager.renderGeometry(screen);
         }
-        specular.setMinFilter(MinFilter.Trilinear);
-    }
 
-    @Override
-    public TextureCubeMap getSpecularIBL() {
-        return specular;
-    }
+            
+        ByteBuffer shCoefRaw = BufferUtils.createByteBuffer(
+            NUM_SH_COEFFICIENT * 1 * ( shbaker.getColorTarget().getFormat().getBitsPerPixel() / 8)
+        );
+        renderManager.getRenderer().readFrameBufferWithFormat(shbaker, shCoefRaw, shbaker.getColorTarget().getFormat());
+        shCoefRaw.rewind();
+
+        Image img = new Image(format, NUM_SH_COEFFICIENT, 1, shCoefRaw, ColorSpace.Linear);
+        ImageRaster imgr=ImageRaster.create(img);
+
+        shCoef = new Vector3f[NUM_SH_COEFFICIENT];
+        float weightAccum = 0.0f;
+
+        for (int i = 0; i < shCoef.length; i++) {
+            ColorRGBA c = imgr.getPixel(i, 0);
+            shCoef[i] = new Vector3f(c.r, c.g, c.b);
+            if (weightAccum == 0) weightAccum = c.a;
+            else if (weightAccum != c.a) {
+                LOG.warning("SH weight is not uniform, this may cause issues.");
+            }
 
-    @Override
-    public void bakeSphericalHarmonicsCoefficients() {
-        shCoef = EnvMapUtils.getSphericalHarmonicsCoefficents(getEnvMap());
-    }
+        }
+        
+        if (remapMaxValue > 0) weightAccum /= remapMaxValue;
+
+        for (int i = 0; i < NUM_SH_COEFFICIENT; ++i) {
+            if (remapMaxValue > 0)  shCoef[i].divideLocal(remapMaxValue);
+            shCoef[i].multLocal(4.0f * FastMath.PI / weightAccum);
+        }
+        
+        img.dispose();
 
-    @Override
-    public Vector3f[] getSphericalHarmonicsCoefficients() {
-        return shCoef;
     }
-}
+}

+ 148 - 0
jme3-core/src/main/java/com/jme3/environment/baker/IBLHybridEnvBakerLight.java

@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2009-2023 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.jme3.environment.baker;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.environment.util.EnvMapUtils;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Box;
+import com.jme3.texture.FrameBuffer;
+import com.jme3.texture.TextureCubeMap;
+import com.jme3.texture.FrameBuffer.FrameBufferTarget;
+import com.jme3.texture.Image.Format;
+import com.jme3.texture.Texture.MagFilter;
+import com.jme3.texture.Texture.MinFilter;
+import com.jme3.texture.Texture.WrapMode;
+import com.jme3.texture.image.ColorSpace;
+
+/**
+ * An env baker for IBL that bakes the specular map on the GPU and uses
+ * spherical harmonics generated on the CPU for the irradiance map.
+ * 
+ * This is lighter on VRAM but uses the CPU to compute the irradiance map.
+ * 
+ * @author Riccardo Balbo
+ */
+public class IBLHybridEnvBakerLight extends GenericEnvBaker implements IBLEnvBakerLight {
+    protected TextureCubeMap specular;
+    protected Vector3f[] shCoef;
+
+    public IBLHybridEnvBakerLight(RenderManager rm, AssetManager am, Format format, Format depthFormat, int env_size, int specular_size
+
+    ) {
+        super(rm, am, format, depthFormat, env_size);
+
+        specular = new TextureCubeMap(specular_size, specular_size, format);
+        specular.setMagFilter(MagFilter.Bilinear);
+        specular.setMinFilter(MinFilter.Trilinear);
+        specular.setWrap(WrapMode.EdgeClamp);
+        specular.getImage().setColorSpace(ColorSpace.Linear);
+        int nbMipMaps = (int) (Math.log(specular_size) / Math.log(2) + 1);
+        if (nbMipMaps > 6) nbMipMaps = 6;
+        int[] sizes = new int[nbMipMaps];
+        for (int i = 0; i < nbMipMaps; i++) {
+            int size = (int) FastMath.pow(2, nbMipMaps - 1 - i);
+            sizes[i] = size * size * (specular.getImage().getFormat().getBitsPerPixel() / 8);
+        }
+        specular.getImage().setMipMapSizes(sizes);
+    }
+
+    @Override
+    public boolean isTexturePulling() { // always pull textures from gpu
+        return true;
+    }
+
+    @Override
+    public void bakeSpecularIBL() {
+        Box boxm = new Box(1, 1, 1);
+        Geometry screen = new Geometry("BakeBox", boxm);
+
+        Material mat = new Material(assetManager, "Common/IBL/IBLKernels.j3md");
+        mat.setBoolean("UseSpecularIBL", true);
+        mat.setTexture("EnvMap", env);
+        screen.setMaterial(mat);
+
+        if (isTexturePulling()) startPulling();
+
+        for (int mip = 0; mip < specular.getImage().getMipMapSizes().length; mip++) {
+            int mipWidth = (int) (specular.getImage().getWidth() * FastMath.pow(0.5f, mip));
+            int mipHeight = (int) (specular.getImage().getHeight() * FastMath.pow(0.5f, mip));
+
+            FrameBuffer specularbaker = new FrameBuffer(mipWidth, mipHeight, 1);
+            specularbaker.setSrgb(false);
+            for (int i = 0; i < 6; i++) specularbaker.addColorTarget(FrameBufferTarget.newTarget(specular).level(mip).face(i));
+
+            float roughness = (float) mip / (float) (specular.getImage().getMipMapSizes().length - 1);
+            mat.setFloat("Roughness", roughness);
+
+            for (int i = 0; i < 6; i++) {
+                specularbaker.setTargetIndex(i);
+                mat.setInt("FaceId", i);
+
+                screen.updateLogicalState(0);
+                screen.updateGeometricState();
+
+                renderManager.setCamera(getCam(i, specularbaker.getWidth(), specularbaker.getHeight(), Vector3f.ZERO, 1, 1000), false);
+                renderManager.getRenderer().setFrameBuffer(specularbaker);
+                renderManager.renderGeometry(screen);
+
+                if (isTexturePulling()) pull(specularbaker, specular, i);
+
+            }
+            specularbaker.dispose();
+        }
+        
+        if (isTexturePulling()) endPulling(specular);
+        specular.getImage().clearUpdateNeeded();
+
+    }
+
+    @Override
+    public TextureCubeMap getSpecularIBL() {
+        return specular;
+    }
+
+    @Override
+    public void bakeSphericalHarmonicsCoefficients() {
+        shCoef = EnvMapUtils.getSphericalHarmonicsCoefficents(getEnvMap());
+    }
+
+    @Override
+    public Vector3f[] getSphericalHarmonicsCoefficients() {
+        return shCoef;
+    }
+}

+ 1 - 0
jme3-core/src/main/resources/Common/IBL/IBLKernels.frag

@@ -82,6 +82,7 @@ void prefilteredEnvKernel(){
         vec3 L  = normalize(2.0 * dot(V, H) * H - V);
         float NdotL = max(dot(N, L), 0.0);
         if(NdotL > 0.0) {
+            // TODO: use mipmap
             prefilteredColor += texture(m_EnvMap, L).rgb * NdotL;
             totalWeight      += NdotL;
         }

+ 27 - 1
jme3-core/src/main/resources/Common/IBL/Math.glsllib

@@ -17,7 +17,33 @@ float RadicalInverse_VdC(uint bits) {
 
 vec2 Hammersley(uint i, uint N){
     return vec2(float(i)/float(N), RadicalInverse_VdC(i));
-}  
+} 
+ 
+/*
+Compatible with GL ES 2
+float VanDerCorput(uint n, uint base){
+    float invBase = 1.0 / float(base);
+    float denom   = 1.0;
+    float result  = 0.0;
+
+    for(uint i = 0u; i < 32u; ++i)
+    {
+        if(n > 0u)
+        {
+            denom   = mod(float(n), 2.0);
+            result += denom * invBase;
+            invBase = invBase / 2.0;
+            n       = uint(float(n) / 2.0);
+        }
+    }
+
+    return result;
+}
+
+vec2 Hammersley(uint i, uint N){
+    return vec2(float(i)/float(N), VanDerCorput(i, 2u));
+}
+*/
 
 
 vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness){

+ 191 - 0
jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.frag

@@ -0,0 +1,191 @@
+/**
+
+*   - Riccardo Balbo
+*/
+#import "Common/IBL/Math.glsllib"
+
+// #define NUM_SH_COEFFICIENT 9
+#ifndef PI
+    #define PI 3.1415926535897932384626433832795
+#endif
+
+out vec4 outFragColor;
+in vec2 TexCoords;
+in vec3 LocalPos;
+
+
+uniform samplerCube m_Texture;
+#ifdef SH_COEF
+    uniform sampler2D m_ShCoef;
+#endif
+uniform vec2 m_Resolution;
+uniform int m_FaceId;
+
+const float sqrtPi = sqrt(PI);
+const float sqrt3Pi = sqrt(3 / PI);
+const float sqrt5Pi = sqrt(5 / PI);
+const float sqrt15Pi = sqrt(15 / PI);
+
+#ifdef REMAP_MAX_VALUE
+    uniform float m_RemapMaxValue;
+#endif
+
+
+vec3 getVectorFromCubemapFaceTexCoord(float x, float y, float mapSize, int face) {
+    float u;
+    float v;
+
+    /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)]
+        * (+ 0.5f is for texel center addressing) */
+    u = (2.0 * (x + 0.5) / mapSize) - 1.0;
+    v = (2.0 * (y + 0.5) / mapSize) - 1.0;
+    
+
+    // Warp texel centers in the proximity of the edges.
+    float a = pow(mapSize, 2.0) / pow(mapSize - 1, 3.0);
+
+    u = a * pow(u, 3) + u;
+    v = a * pow(v, 3) + v;
+    //compute vector depending on the face
+    // Code from Nvtt : https://github.com/castano/nvidia-texture-tools/blob/master/src/nvtt/CubeSurface.cpp#L101
+    vec3 o =vec3(0);
+    switch(face) {
+        case 0:
+            o= normalize(vec3(1, -v, -u));
+            break;
+        case 1:
+            o= normalize(vec3(-1, -v, u));
+            break;
+        case 2:
+            o= normalize(vec3(u, 1, v));
+            break;
+        case 3:
+            o= normalize(vec3(u, -1, -v));
+            break;
+        case 4:
+            o= normalize(vec3(u, -v, 1));
+            break;
+        case 5:
+            o= normalize(vec3(-u, -v, -1.0));
+            break;
+    }
+
+    return o;
+}
+
+float atan2(in float y, in float x) {
+    bool s = (abs(x) > abs(y));
+    return mix(PI / 2.0 - atan(x, y), atan(y, x), s);
+}
+
+float areaElement(float x, float y) {
+    return atan2(x * y, sqrt(x * x + y * y + 1.));
+}
+
+float getSolidAngleAndVector(float x, float y, float mapSize, int face, out vec3 store) {
+    /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)]
+        (+ 0.5f is for texel center addressing) */
+    float u = (2.0 * (x + 0.5) / mapSize) - 1.0;
+    float v = (2.0 * (y + 0.5) / mapSize) - 1.0;
+
+    store = getVectorFromCubemapFaceTexCoord(x, y, mapSize, face);
+
+    /* Solid angle weight approximation :
+        * U and V are the -1..1 texture coordinate on the current face.
+        * Get projected area for this texel */
+    float x0, y0, x1, y1;
+    float invRes = 1.0 / mapSize;
+    x0 = u - invRes;
+    y0 = v - invRes;
+    x1 = u + invRes;
+    y1 = v + invRes;
+
+    return areaElement(x0, y0) - areaElement(x0, y1) - areaElement(x1, y0) + areaElement(x1, y1);
+}
+
+void evalShBasis(vec3 texelVect, int i, out float shDir) {
+    float xV = texelVect.x;
+    float yV = texelVect.y;
+    float zV = texelVect.z;
+
+    float x2 = xV * xV;
+    float y2 = yV * yV;
+    float z2 = zV * zV;
+
+    if(i==0) shDir = (1. / (2. * sqrtPi));
+    else if(i==1) shDir = -(sqrt3Pi * yV) / 2.;
+    else if(i == 2) shDir = (sqrt3Pi * zV) / 2.;
+    else if(i == 3) shDir = -(sqrt3Pi * xV) / 2.;
+    else if(i == 4) shDir = (sqrt15Pi * xV * yV) / 2.;
+    else if(i == 5) shDir = -(sqrt15Pi * yV * zV) / 2.;
+    else if(i == 6) shDir = (sqrt5Pi * (-1. + 3. * z2)) / 4.;
+    else if(i == 7) shDir = -(sqrt15Pi * xV * zV) / 2.;
+    else shDir = sqrt15Pi * (x2 - y2) / 4.;
+}
+
+vec3 pixelFaceToV(int faceId, float pixelX, float pixelY, float cubeMapSize) {
+    vec2 normalizedCoords = vec2((2.0 * pixelX + 1.0) / cubeMapSize, (2.0 * pixelY + 1.0) / cubeMapSize);
+
+    vec3 direction;
+    if(faceId == 0) {
+        direction = vec3(1.0, -normalizedCoords.y, -normalizedCoords.x);
+    } else if(faceId == 1) {
+        direction = vec3(-1.0, -normalizedCoords.y, normalizedCoords.x);
+    } else if(faceId == 2) {
+        direction = vec3(normalizedCoords.x, 1.0, normalizedCoords.y);
+    } else if(faceId == 3) {
+        direction = vec3(normalizedCoords.x, -1.0, -normalizedCoords.y);
+    } else if(faceId == 4) {
+        direction = vec3(normalizedCoords.x, -normalizedCoords.y, 1.0);
+    } else if(faceId == 5) {
+        direction = vec3(-normalizedCoords.x, -normalizedCoords.y, -1.0);
+    }
+
+    return normalize(direction);
+}
+
+void sphKernel() {
+    int width = int(m_Resolution.x);
+    int height = int(m_Resolution.y);
+    vec3 texelVect=vec3(0);    
+    float shDir=0;
+    float weight=0;
+    vec4 color=vec4(0);
+
+    int i=int(gl_FragCoord.x);
+
+    #ifdef SH_COEF
+        vec4 r=texelFetch(m_ShCoef, ivec2(i, 0), 0);
+        vec3 shCoef=r.rgb;
+        float weightAccum = r.a;
+    #else
+        vec3 shCoef=vec3(0.0);
+        float weightAccum = 0.0;
+    #endif
+
+    for(int y = 0; y < height; y++) {
+        for(int x = 0; x < width; x++) {
+            weight = getSolidAngleAndVector(float(x), float(y), float(width), m_FaceId, texelVect);
+            evalShBasis(texelVect, i, shDir);
+            color = texture(m_Texture, texelVect);
+            shCoef.x = (shCoef.x + color.r * shDir * weight);
+            shCoef.y = (shCoef.y + color.g * shDir * weight);
+            shCoef.z = (shCoef.z + color.b * shDir * weight);
+            weightAccum += weight;
+        }
+    }
+
+
+
+    #ifdef REMAP_MAX_VALUE
+        shCoef.xyz=shCoef.xyz*m_RemapMaxValue;
+        weightAccum=weightAccum*m_RemapMaxValue;
+    #endif
+
+    outFragColor = vec4(shCoef.xyz,weightAccum);
+
+}
+
+void main() {
+    sphKernel();
+}

+ 32 - 0
jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.j3md

@@ -0,0 +1,32 @@
+MaterialDef IBLSphH {
+    
+    MaterialParameters {
+        TextureCubeMap Texture -LINEAR
+        Int FaceId : 0
+        Texture2D ShCoef -LINEAR
+        Vector2 Resolution
+        Float RemapMaxValue
+    }
+
+    Technique {
+    
+        VertexShader GLSL150:  Common/IBLSphH/IBLSphH.vert
+        FragmentShader GLSL150:  Common/IBLSphH/IBLSphH.frag
+
+        WorldParameters {
+        }
+        
+        RenderState {
+            DepthWrite Off
+            DepthTest Off
+            DepthFunc Equal
+            FaceCull Off
+        }
+
+        Defines {
+            REMAP_MAX_VALUE: RemapMaxValue
+            SH_COEF: ShCoef
+        }
+
+    }
+}

+ 16 - 0
jme3-core/src/main/resources/Common/IBLSphH/IBLSphH.vert

@@ -0,0 +1,16 @@
+/**
+*- Riccardo Balbo
+*/
+in vec3 inPosition;
+in vec2 inTexCoord;
+
+out vec2 TexCoords;
+out vec3 LocalPos;
+
+
+void main() {
+    LocalPos = inPosition.xyz;
+    TexCoords = inTexCoord.xy;
+    vec2 pos = inPosition.xy * 2.0 - 1.0;
+    gl_Position = vec4(pos, 0.0, 1.0);  
+}

+ 36 - 6
jme3-examples/src/main/java/jme3test/light/pbr/TestPBRSimple.java

@@ -1,8 +1,11 @@
 package jme3test.light.pbr;
 
+
 import com.jme3.app.SimpleApplication;
 import com.jme3.environment.EnvironmentProbeControl;
+import com.jme3.input.ChaseCamera;
 import com.jme3.material.Material;
+import com.jme3.math.FastMath;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Spatial;
 import com.jme3.util.SkyFactory;
@@ -11,7 +14,8 @@ import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
 /**
  * TestPBRSimple
  */
-public class TestPBRSimple extends SimpleApplication{
+public class TestPBRSimple extends SimpleApplication {
+    private boolean REALTIME_BAKING = false;
 
     public static void main(String[] args) {
         new TestPBRSimple().start();
@@ -20,21 +24,47 @@ public class TestPBRSimple extends SimpleApplication{
     @Override
     public void simpleInitApp() {
  
+        
         Geometry model = (Geometry) assetManager.loadModel("Models/Tank/tank.j3o");
         MikktspaceTangentGenerator.generate(model);
 
         Material pbrMat = assetManager.loadMaterial("Models/Tank/tank.j3m");
         model.setMaterial(pbrMat);
-
         rootNode.attachChild(model);
 
-        
-        EnvironmentProbeControl envProbe=new EnvironmentProbeControl(renderManager,assetManager,256);
-        rootNode.addControl(envProbe);
-        
+        ChaseCamera chaseCam = new ChaseCamera(cam, model, inputManager);
+        chaseCam.setDragToRotate(true);
+        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
+        chaseCam.setMaxDistance(1000);
+        chaseCam.setSmoothMotion(true);
+        chaseCam.setRotationSensitivity(10);
+        chaseCam.setZoomSensitivity(5);
+        flyCam.setEnabled(false);
+
         Spatial sky = SkyFactory.createSky(assetManager, "Textures/Sky/Path.hdr", SkyFactory.EnvMapType.EquirectMap);
         rootNode.attachChild(sky);
+
+        // Create baker control
+        EnvironmentProbeControl envProbe=new EnvironmentProbeControl(renderManager,assetManager,256);
+        rootNode.addControl(envProbe);
+       
+        // Tag the sky, only the tagged spatials will be rendered in the env map
         EnvironmentProbeControl.tag(sky);
+
+
+        
     }
     
+
+    float lastBake = 0;
+    @Override
+    public void simpleUpdate(float tpf) {
+        if (REALTIME_BAKING) {
+            lastBake += tpf;
+            if (lastBake > 1.4f) {
+                rootNode.getControl(EnvironmentProbeControl.class).rebake();
+                lastBake = 0;
+            }
+        }
+    }
 }