Light probes change

I just pushed a change a change on master that changes a bit hos light probes are handled

Light probes had a bounding sphere as the area of effect before and now have a specific class for area of effect. One for SphericalArea and one for OrientedBoxArea.

This change may break user code, specifically if you are using the bounds to change the radius of the probe.

So if you were using :

((BoundingSphere)lightProbe.getBounds()).setRaduis(radius);

This won’t crash, but this won’t work neither (the call to setBounds is deprecated)
You should use instead

lightProbe.getArea().setRadius(radius);

@Darkchaos, and @javasabr this may have an impact on your editors ^

Related posts:

15 Likes

Thank you nehon. Great Job!

I’m currently testing it and adjust my shaders to match the new system. So far it looks somehow better, especially on terrain… but that’s just a feeling.

One suggestion:
you introduced in the shader the function renderProbe, why is this not included in the pbr.glsllib ? I know it has a dependency on wPosition, but why not let one call it with the value as argument?

Regards and thanks

Because I’m a lazy ass…

You’re right it should be in PBR.glsllib and wPosition should be passed as a param.
I’ll change it asap and I’ll ping you back.

@b00n Done :wink:

I’m eager for feedback too :wink:

1 Like

Hi!

I have an issue while updating the light probes which I think I know the reason but I am not sure whats the best approach to solve it.

While updating a light probe using

LightProbeFactory.updateProbe()

the scene (at least the objects that are within the radius of this probe) get darker for a split second and then get “properly” lit afterwards again. I guess this is because, one of the first statements in updateProbe is:

probe.setReady(false);

which i guess causes the shader to not use this probe data as long as it takes to generate the new maps and the probe is “ready” again.

I need to (at least i think so) “regulary” update all the probes because the lighting changes due to a day/night cycle and this flickering is a little bit of a nuisance.

I thought of something like cloning the probe before the update, but I kind of don’t like this;) Do you have a suggestion how to best deal with this?

Currently i’m having two sets auf environment maps which i swap every time i need to recalculate. The biggest problem here is that all calculations of the map are done on the cpu and not on the graphic card, which can take seconds. I do not have the color changing, but noticing big cpu throttling due to calculation. but if you are interested i can share my approach.

Thanks for your answer! But this would mean twice the amount of probes/textures just for updating, right? I was also thinking about a solution where I have one probe for updating only - meaning, when a “real” probe needs an update, I set all the parameters in the “update probe”, generate the maps and afterwards switch them up/copy them to the original one (in the jme thread, so i assume this will be no problem and i don’t need to set the “ready” to false to do so).

I am not sure that this would work, but currently i don’t see a reason why it wouldn’t and this would mean only one probe overhead for updating… Maybe I do some prototype in the next few days - but maybe someone has a better idea/approach in the meantime…

Mhhh… actually, maybe it could simply work without setting the ready flag to false… could you try?
This process was not really meant to be used at runtime though… it’s slow and may produce garbage…

Thanks! Dunno, if i can manage today - but i am pretty sure, i can test it tomorrow. Will let you know the result!

1 Like

Don’t know what exactly the problem is, but when i remove the setReady(false) i get an exception:

java.lang.IllegalArgumentException

	at java.nio.Buffer.position(Buffer.java:244)

	at com.jme3.texture.image.ByteAlignedImageCodec.writePixelRaw(ByteAlignedImageCodec.java:67)

	at com.jme3.texture.image.ByteAlignedImageCodec.writeComponents(ByteAlignedImageCodec.java:125)

	at com.jme3.texture.image.MipMapImageRaster.setPixel(MipMapImageRaster.java:130)

	at com.jme3.environment.util.CubeMapWrapper.setPixel(CubeMapWrapper.java:230)

	at com.jme3.environment.generation.PrefilteredEnvMapFaceGenerator.generatePrefilteredEnvMap(PrefilteredEnvMapFaceGenerator.java:188)

	at com.jme3.environment.generation.PrefilteredEnvMapFaceGenerator.run(PrefilteredEnvMapFaceGenerator.java:135)

	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)

	at java.util.concurrent.FutureTask.run(FutureTask.java:266)

	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)

	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)

	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)

	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

	at java.lang.Thread.run(Thread.java:745)

java.lang.IllegalArgumentException

	at java.nio.Buffer.position(Buffer.java:244)

	at com.jme3.texture.image.ByteAlignedImageCodec.writePixelRaw(ByteAlignedImageCodec.java:67)

	at com.jme3.texture.image.ByteAlignedImageCodec.writeComponents(ByteAlignedImageCodec.java:125)

	at com.jme3.texture.image.MipMapImageRaster.setPixel(MipMapImageRaster.java:130)

	at com.jme3.environment.util.CubeMapWrapper.setPixel(CubeMapWrapper.java:230)

	at com.jme3.environment.generation.PrefilteredEnvMapFaceGenerator.generatePrefilteredEnvMap(PrefilteredEnvMapFaceGenerator.java:188)

	at com.jme3.environment.generation.PrefilteredEnvMapFaceGenerator.run(PrefilteredEnvMapFaceGenerator.java:135)

	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)

	at java.util.concurrent.FutureTask.run(FutureTask.java:266)

	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)

	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)

	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)

	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)

	at java.lang.Thread.run(Thread.java:745)

I guess its some concurrency issue

I opted not to change the original LightProbeFactory, but made an independent alternate approach. The main difference is, that this is intended for online updates, so it preserves the maps and should work pretty memory efficient. It utilizes only one background thread but does all the render relevant changes in the JME thread, so it works without the need to use “setReady(false)” and therefore does not produce a flickering while updating.

At least in my (still limited) tests it worked out nicely, so without further ado:

package at.illumine.effect;

import com.jme3.app.Application;
import com.jme3.environment.EnvironmentCamera;
import com.jme3.environment.generation.*;
import com.jme3.environment.util.EnvMapUtils;
import com.jme3.light.LightProbe;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.texture.TextureCubeMap;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LightProbeUpdateManager {

private static final Logger _logger = Logger.getLogger(LightProbeUpdateManager.class.toString());
private final Application _application;
private final EnvironmentCamera _environmentCamera;
private final Spatial _scene;
private final Thread _workerThread;
private final Queue<LightProbe> _updateProbes;
private final HashMap<LightProbe, JobProgressListener> _updateListeners;
private final Vector3f[] _shCoeffs;
private final PrefilteredEnvMapFaceGenerator[] _pemGenerators;
private boolean _workerRunning;
private final JobProgressAdapter<TextureCubeMap> _snapshotProgressAdapter;
private transient TextureCubeMap _snapshotResult;
private final TextureCubeMap _envMap;

public LightProbeUpdateManager(final EnvironmentCamera envCam, Spatial scene) {
    _environmentCamera = envCam;
    _application = _environmentCamera.getApplication();
    _scene = scene;
    _shCoeffs = new Vector3f[EnvMapUtils.NUM_SH_COEFFICIENT];
    for (int i = 0; i < _shCoeffs.length; i++) {
        _shCoeffs[i] = Vector3f.ZERO.clone();
    }

    _envMap = EnvMapUtils.createPrefilteredEnvMap(_environmentCamera.getSize(), _environmentCamera.getImageFormat());
    _pemGenerators = new PrefilteredEnvMapFaceGenerator[6];
    JobProgressListener<Integer> pmListener = new JobProgressListener<Integer>() {
        @Override
        public void start() {
        }

        @Override
        public void step(String message) {
        }

        @Override
        public void progress(double value) {
        }

        @Override
        public void done(Integer result) {
        }
    };

    for (int i = 0; i < _pemGenerators.length; i++) {
        _pemGenerators[i] = new PrefilteredEnvMapFaceGenerator(_application, i, pmListener);
    }
    _updateProbes = new LinkedList<>();
    _updateListeners = new HashMap<>();
    _workerRunning = true;

    _workerThread = new Thread(() -> {
        while (_workerRunning) {
            if (_updateProbes.peek() != null) {
                LightProbe t;

                synchronized (_updateProbes) {
                    t = _updateProbes.poll();
                    if (_updateListeners.containsKey(t)) {
                        _application.enqueue(() -> {
                            _updateListeners.get(t).done(t);
                            synchronized (_updateProbes) {
                                _updateListeners.remove(t);
                            }
                        });
                    }
                }
                try {
                    updateProbe(t);
                } catch (Exception ex) {
                    _logger.log(Level.SEVERE, ex.getMessage(), ex);
                }
            } else {
                try {
                    synchronized (_updateProbes) {
                        _updateProbes.wait(1000);
                    }
                } catch (Exception ex) {
                    _logger.log(Level.SEVERE, ex.getMessage(), ex);
                }
            }
        }
    });
    //_workerThread.setPriority(Thread.MIN_PRIORITY);
    _workerThread.setDaemon(true);
    _snapshotProgressAdapter = new JobProgressAdapter<TextureCubeMap>() {
        @Override
        public void done(TextureCubeMap result) {
            _snapshotResult = result;
            synchronized (_snapshotProgressAdapter) {
                _snapshotProgressAdapter.notify();
            }
        }
    };
}

public void start() {
    _workerThread.start();
}

public LightProbe createProbe(final EnvMapUtils.GenerationType genType, final JobProgressListener<LightProbe> listener) {
    final LightProbe probe = new LightProbe();
    final Vector3f[] shCoeffs;

    shCoeffs = new Vector3f[EnvMapUtils.NUM_SH_COEFFICIENT];
    for (int i = 0; i < shCoeffs.length; i++) {
        shCoeffs[i] = Vector3f.ZERO.clone();
    }
    probe.setShCoeffs(shCoeffs);

    probe.setPosition(Vector3f.ZERO);
    probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(_environmentCamera.getSize(), _environmentCamera.getImageFormat()));
    enqueueProbeUpdate(probe, listener);
    return probe;
}

public LightProbe createProbe(final JobProgressListener<LightProbe> listener) {
    return createProbe(EnvMapUtils.GenerationType.Fast, listener);
}

public void enqueueProbeUpdate(LightProbe p, JobProgressListener<LightProbe> l) {
    synchronized (_updateProbes) {
        _updateProbes.add(p);
        _updateListeners.put(p, l);
        _updateProbes.notifyAll();
    }
}

public boolean isBusy() {
    return !_updateProbes.isEmpty();
}

public void stop() {
    _workerRunning = false;
}

@Override
public void finalize() throws Throwable {
    super.finalize();
    _workerRunning = false;
}

private void updateProbe(LightProbe probe) {
    synchronized (_snapshotProgressAdapter) {
        _application.enqueue(() -> {
            _environmentCamera.setPosition(probe.getPosition());
            _environmentCamera.snapshot(_scene, _snapshotProgressAdapter);
        }
        );
        try {
            _snapshotProgressAdapter.wait();
        } catch (InterruptedException ex) {
            _logger.log(Level.SEVERE, null, ex);
        }
    }

    generateIrradianceSphericalHarmonics(_snapshotResult, probe);
    generatePrefilteredEnvMapFace(_snapshotResult, probe);

    _application.enqueue(() -> {
        probe.setReady(true);//only relevant for initial update - for all the updates afterwards this can stay true, because all the probe data gets updates in the jme thread
        //_logger.info("probe updated");
    });
}

private void generateIrradianceSphericalHarmonics(TextureCubeMap sourceMap, LightProbe store) {
    EnvMapUtils.getSphericalHarmonicsCoefficents(sourceMap, EnvMapUtils.FixSeamsMethod.Wrap, _shCoeffs);
    EnvMapUtils.prepareShCoefs(_shCoeffs);
    synchronized (_shCoeffs) {
        _application.enqueue(() -> {
            for (int i = 0; i < _shCoeffs.length; i++) {
                store.getShCoeffs()[i].set(_shCoeffs[i]);
            }

            synchronized (_shCoeffs) {
                _shCoeffs.notify();
            }
        });
        try {
            _shCoeffs.wait();
        } catch (InterruptedException ex) {
            _logger.log(Level.SEVERE, null, ex);
        }
    }
}

private void generatePrefilteredEnvMapFace(TextureCubeMap sourceMap, LightProbe probe) {
    generatePrefilteredEnvMapFace(sourceMap, probe, EnvMapUtils.GenerationType.Fast);
}

private void generatePrefilteredEnvMapFace(TextureCubeMap sourceMap, LightProbe probe, EnvMapUtils.GenerationType genType) {
    int size = sourceMap.getImage().getWidth();

    for (PrefilteredEnvMapFaceGenerator _pemGenerator : _pemGenerators) {
        _pemGenerator.setGenerationParam(sourceMap, size, EnvMapUtils.FixSeamsMethod.None, genType, _envMap);
        _pemGenerator.run();
    }

    synchronized (_envMap) {
        _application.enqueue(() -> {
            List<ByteBuffer> target = probe.getPrefilteredEnvMap().getImage().getData();
            List<ByteBuffer> source = _envMap.getImage().getData();

            for (int i = 0; i < target.size(); i++) {
                ByteBuffer tBuf = target.get(i);
                ByteBuffer sBuf = source.get(i);

                sBuf.flip();
//                    tBuf.reset();
                tBuf.rewind();
                tBuf.put(sBuf);
            }
            synchronized (_envMap) {
                _envMap.notify();
            }
        });
        try {
            _envMap.wait();
        } catch (InterruptedException ex) {
            _logger.log(Level.SEVERE, null, ex);
        }
    }
}
}

I’ve also made a little change in EnvMapUtils getSphericalHarmonicsCoefficents, which enables to preserve the vector array. Should be put into core IMO, because its completely compatible and enables additional use-cases, here is the patch:

diff --git a/jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java b/jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java
index 91b65655e..11b3dcb9e 100644
--- a/jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java
+++ b/jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java
@@ -404,7 +404,7 @@ public class EnvMapUtils {
      * r,g,b channnel
      */
     public static Vector3f[] getSphericalHarmonicsCoefficents(TextureCubeMap cubeMap) {
-        return getSphericalHarmonicsCoefficents(cubeMap, FixSeamsMethod.Wrap);
+        return getSphericalHarmonicsCoefficents(cubeMap, FixSeamsMethod.Wrap, null);
     }
 
     /**
@@ -419,16 +419,14 @@ public class EnvMapUtils {
      * @param cubeMap the environment cube map to compute SH for
      * @param fixSeamsMethod method to fix seams when computing the SH
      * coefficients
+     * @param shCoef returning vectors - gets initialized if null
      * @return an array of 9 vector3f representing thos coefficients for each
      * r,g,b channnel
      */
-    public static Vector3f[] getSphericalHarmonicsCoefficents(TextureCubeMap cubeMap, FixSeamsMethod fixSeamsMethod) {
-
-        Vector3f[] shCoef = new Vector3f[NUM_SH_COEFFICIENT];
-
+    public static Vector3f[] getSphericalHarmonicsCoefficents(TextureCubeMap cubeMap, FixSeamsMethod fixSeamsMethod, Vector3f[] shCoef) {
         float[] shDir = new float[9];
         float weightAccum = 0.0f;
-        float weight;
+        float weight;        
 
         if (cubeMap.getImage().getData(0) == null) {
             throw new IllegalStateException("The cube map must contain Efficient data, if you rendered the cube map on the GPU please use renderer.readFrameBuffer, to create a CPU image");
@@ -441,22 +439,22 @@ public class EnvMapUtils {
         ColorRGBA color = new ColorRGBA();
 
         CubeMapWrapper envMapReader = new CubeMapWrapper(cubeMap);
+        
+        if( shCoef == null ) {
+            shCoef = new Vector3f[NUM_SH_COEFFICIENT];
+            for (int i = 0; i < NUM_SH_COEFFICIENT; i++) {
+                shCoef[i] = new Vector3f();
+            }
+        }
+        
         for (int face = 0; face < 6; face++) {
             for (int y = 0; y < height; y++) {
                 for (int x = 0; x < width; x++) {
-
                     weight = getSolidAngleAndVector(x, y, width, face, texelVect, fixSeamsMethod);
-
                     evalShBasis(texelVect, shDir);
-
                     envMapReader.getPixel(x, y, face, color);
 
                     for (int i = 0; i < NUM_SH_COEFFICIENT; i++) {
-
-                        if (shCoef[i] == null) {
-                            shCoef[i] = new Vector3f();
-                        }
-
                         shCoef[i].setX(shCoef[i].x + color.r * shDir[i] * weight);
                         shCoef[i].setY(shCoef[i].y + color.g * shDir[i] * weight);
                         shCoef[i].setZ(shCoef[i].z + color.b * shDir[i] * weight);

Edit: Fixed a small bug in LightProbeUpdateManager

1 Like

Thanks!
will look into it

1 Like

So does this mean that we could also set a “scale” for the OrientedBoxArea?
Because the SDK currently has a Box to scale and then calculates the radius of this.
This would enable us to make the OrientedBoxArea a non uniform scale?

Yes definitely. That’s the idea behind it.
In the video I linked, the box has non uniform scale.