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