Better PBR env map generation

empirephoenix-patch-1
Nehon 7 years ago
parent 171007693b
commit e4b6bf82a2
  1. 6
      jme3-core/src/main/java/com/jme3/environment/EnvironmentCamera.java
  2. 40
      jme3-core/src/main/java/com/jme3/environment/LightProbeFactory.java
  3. 189
      jme3-core/src/main/java/com/jme3/environment/generation/PrefilteredEnvMapFaceGenerator.java
  4. 34
      jme3-core/src/main/java/com/jme3/environment/util/CubeMapWrapper.java
  5. 22
      jme3-core/src/main/java/com/jme3/environment/util/EnvMapUtils.java
  6. 2
      jme3-core/src/main/java/com/jme3/texture/image/ByteAlignedImageCodec.java
  7. 14
      jme3-core/src/main/java/com/jme3/util/MipMapGenerator.java
  8. 21
      jme3-examples/src/main/java/jme3test/light/pbr/RefEnv.java
  9. BIN
      jme3-testdata/src/main/resources/Scenes/PBR/ref/scene.bin
  10. 2466
      jme3-testdata/src/main/resources/Scenes/PBR/ref/scene.gltf

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

@ -31,13 +31,14 @@
*/ */
package com.jme3.environment; package com.jme3.environment;
import com.jme3.app.Application;
import com.jme3.environment.generation.*; import com.jme3.environment.generation.*;
import com.jme3.light.LightProbe;
import com.jme3.environment.util.EnvMapUtils; 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.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.texture.TextureCubeMap; import com.jme3.texture.TextureCubeMap;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
/** /**
@ -106,10 +107,11 @@ public class LightProbeFactory {
* @param envCam the EnvironmentCamera * @param envCam the EnvironmentCamera
* @param scene the Scene * @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. * @param listener the listener of the genration progress.
* @return the created LightProbe * @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(); final LightProbe probe = new LightProbe();
probe.setPosition(envCam.getPosition()); probe.setPosition(envCam.getPosition());
probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat())); probe.setPrefilteredMap(EnvMapUtils.createPrefilteredEnvMap(envCam.getSize(), envCam.getImageFormat()));
@ -117,33 +119,37 @@ public class LightProbeFactory {
@Override @Override
public void done(TextureCubeMap map) { public void done(TextureCubeMap map) {
generatePbrMaps(map, probe, envCam.getApplication(), listener); generatePbrMaps(map, probe, envCam.getApplication(), genType, listener);
} }
}); });
return probe; return probe;
} }
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 giver EnvironmentCamera in the given scene. * 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. * Note that this is an assynchronous process that will run on multiple threads.
* The process is thread safe. * The process is thread safe.
* The created lightProbe will only be marked as ready when the rendering process is done. * The created lightProbe will only be marked as ready when the rendering process is done.
* * <p>
* The JobProgressListener will be notified of the progress of the generation. * The JobProgressListener will be notified of the progress of the generation.
* Note that you can also use a {@link JobProgressAdapter}. * Note that you can also use a {@link JobProgressAdapter}.
* *
* @see LightProbe
* @see EnvironmentCamera
* @see JobProgressListener
*
* @param probe the Light probe to update * @param probe the Light probe to update
* @param envCam the EnvironmentCamera * @param envCam the EnvironmentCamera
* @param scene the Scene * @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. * @param listener the listener of the genration progress.
* @return the created LightProbe * @return the created LightProbe
* @see LightProbe
* @see EnvironmentCamera
* @see JobProgressListener
*/ */
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()); envCam.setPosition(probe.getPosition());
@ -159,12 +165,16 @@ public class LightProbeFactory {
@Override @Override
public void done(TextureCubeMap map) { public void done(TextureCubeMap map) {
generatePbrMaps(map, probe, envCam.getApplication(), listener); generatePbrMaps(map, probe, envCam.getApplication(), genType, listener);
} }
}); });
return probe; 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. * 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). * This method will spawn 7 thread (one for the Irradiance spherical harmonics generator, and one for each face of the prefiltered env map).
@ -175,7 +185,7 @@ public class LightProbeFactory {
* @param app the Application * @param app the Application
* @param listener a progress listener. (can be null if no progress reporting is needed) * @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; IrradianceSphericalHarmonicsGenerator irrShGenerator;
PrefilteredEnvMapFaceGenerator[] pemGenerators = new PrefilteredEnvMapFaceGenerator[6]; PrefilteredEnvMapFaceGenerator[] pemGenerators = new PrefilteredEnvMapFaceGenerator[6];
@ -189,7 +199,7 @@ public class LightProbeFactory {
for (int i = 0; i < pemGenerators.length; i++) { for (int i = 0; i < pemGenerators.length; i++) {
pemGenerators[i] = new PrefilteredEnvMapFaceGenerator(app, i, new JobListener(listener, jobState, probe, 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]); jobState.executor.execute(pemGenerators[i]);
} }
} }

@ -31,27 +31,19 @@
*/ */
package com.jme3.environment.generation; package com.jme3.environment.generation;
import com.jme3.app.Application;
import com.jme3.environment.util.CubeMapWrapper; import com.jme3.environment.util.CubeMapWrapper;
import com.jme3.environment.util.EnvMapUtils; import com.jme3.environment.util.EnvMapUtils;
import com.jme3.app.Application;
import com.jme3.math.*; 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 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.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger; 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 * Generates one face of the prefiltered environnement map for PBR. This job can
* be lauched from a separate thread. * be lauched from a separate thread.
@ -69,6 +61,7 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
private int targetMapSize; private int targetMapSize;
private EnvMapUtils.FixSeamsMethod fixSeamsMethod; private EnvMapUtils.FixSeamsMethod fixSeamsMethod;
private EnvMapUtils.GenerationType genType;
private TextureCubeMap sourceMap; private TextureCubeMap sourceMap;
private TextureCubeMap store; private TextureCubeMap store;
private final Application app; private final Application app;
@ -107,11 +100,12 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
* {@link EnvMapUtils.FixSeamsMethod} * {@link EnvMapUtils.FixSeamsMethod}
* @param store The cube map to store the result in. * @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.sourceMap = sourceMap;
this.targetMapSize = targetMapSize; this.targetMapSize = targetMapSize;
this.fixSeamsMethod = fixSeamsMethod; this.fixSeamsMethod = fixSeamsMethod;
this.store = store; this.store = store;
this.genType = genType;
init(); init();
} }
@ -162,12 +156,16 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
* @return The irradiance cube map for the given coefficients * @return The irradiance cube map for the given coefficients
*/ */
private TextureCubeMap generatePrefilteredEnvMap(TextureCubeMap sourceEnvMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) { private TextureCubeMap generatePrefilteredEnvMap(TextureCubeMap sourceEnvMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
try {
TextureCubeMap pem = store; 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 sourceWrapper = new CubeMapWrapper(sourceEnvMap);
CubeMapWrapper targetWrapper = new CubeMapWrapper(pem); CubeMapWrapper targetWrapper = new CubeMapWrapper(pem);
@ -175,55 +173,88 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
Vector3f texelVect = new Vector3f(); Vector3f texelVect = new Vector3f();
Vector3f color = new Vector3f(); Vector3f color = new Vector3f();
ColorRGBA outColor = new ColorRGBA(); ColorRGBA outColor = new ColorRGBA();
int targetMipMapSize = targetMapSize;
for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) { for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) {
float roughness = getRoughnessFromMip(mipLevel, nbMipMap); float roughness = getRoughnessFromMip(mipLevel, nbMipMap);
int nbSamples = getSampleFromMip(mipLevel, nbMipMap); int nbSamples = getSampleFromMip(mipLevel, nbMipMap);
int targetMipMapSize = (int) pow(2, nbMipMap + 1 - mipLevel);
for (int y = 0; y < targetMipMapSize; y++) { for (int y = 0; y < targetMipMapSize; y++) {
for (int x = 0; x < targetMipMapSize; x++) { for (int x = 0; x < targetMipMapSize; x++) {
color.set(0, 0, 0); color.set(0, 0, 0);
getVectorFromCubemapFaceTexCoord(x, y, targetMipMapSize, face, texelVect, fixSeamsMethod); getVectorFromCubemapFaceTexCoord(x, y, targetMipMapSize, face, texelVect, fixSeamsMethod);
prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, color); 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); 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); 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; Vector3f prefilteredColor = store;
float totalWeight = 0.0f; 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² // a = roughness² and a2 = a²
float a2 = roughness * roughness; float a2 = roughness * roughness;
a2 *= a2; 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++) { for (int i = 0; i < numSamples; i++) {
Xi = getHammersleyPoint(i, numSamples, Xi); Xi = getHammersleyPoint(i, numSamples, Xi);
H = importanceSampleGGX(Xi, a2, N, H); H = importanceSampleGGX(Xi, a2, H);
H.normalizeLocal(); H.normalizeLocal();
tmp.set(H); float VoH = H.z;
float NoH = N.dot(tmp); Vector3f L = H.multLocal(VoH * 2f).subtractLocal(V);
float NoL = L.z;
Vector3f L = tmp.multLocal(NoH * 2).subtractLocal(N);
float NoL = clamp(N.dot(L), 0.0f, 1.0f); float computedMipLevel = mipLevel;
if (NoL > 0) { if (mipLevel != 0) {
envMapReader.getPixel(L, c); computedMipLevel = computeMipLevel(roughness, numSamples, this.targetMapSize, VoH);
prefilteredColor.setX(prefilteredColor.x + c.r * NoL);
prefilteredColor.setY(prefilteredColor.y + c.g * NoL);
prefilteredColor.setZ(prefilteredColor.z + c.b * NoL);
totalWeight += NoL;
} }
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) { if (totalWeight > 0) {
prefilteredColor.divideLocal(totalWeight); prefilteredColor.divideLocal(totalWeight);
@ -232,7 +263,78 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
return prefilteredColor; 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) { if (store == null) {
store = new Vector3f(); store = new Vector3f();
} }
@ -243,22 +345,9 @@ public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
float sinThetaCosPhi = sinTheta * xi.z;//xi.z is cos(phi) float sinThetaCosPhi = sinTheta * xi.z;//xi.z is cos(phi)
float sinThetaSinPhi = sinTheta * xi.w;//xi.w is sin(phi) float sinThetaSinPhi = sinTheta * xi.w;//xi.w is sin(phi)
Vector3f upVector = Vector3f.UNIT_X; store.x = sinThetaCosPhi;
store.y = sinThetaSinPhi;
if (abs(normal.z) < 0.999) { store.z = cosTheta;
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);
return store; return store;
} }

@ -31,17 +31,15 @@
*/ */
package com.jme3.environment.util; package com.jme3.environment.util;
import com.jme3.environment.util.EnvMapUtils; import com.jme3.math.*;
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.texture.Image; import com.jme3.texture.Image;
import com.jme3.texture.TextureCubeMap; import com.jme3.texture.TextureCubeMap;
import com.jme3.texture.image.DefaultImageRaster; import com.jme3.texture.image.DefaultImageRaster;
import com.jme3.texture.image.MipMapImageRaster; import com.jme3.texture.image.MipMapImageRaster;
import com.jme3.util.BufferUtils; 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. * 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 Vector2f uvs = new Vector2f();
private final Image image; private final Image image;
private final ColorRGBA tmpColor = new ColorRGBA();
/** /**
* Creates a CubeMapWrapper for the given cube map * Creates a CubeMapWrapper for the given cube map
* Note that the cube map must be initialized, and the mipmaps sizes should * 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. * @param store the color in which to store the pixel color read.
* @return the color of the pixel 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) { if (mipMapRaster == null) {
throw new IllegalArgumentException("This cube map has no mip maps"); throw new IllegalArgumentException("This cube map has no mip maps");
} }
@ -113,10 +113,26 @@ public class CubeMapWrapper {
store = new ColorRGBA(); 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.setSlice(face);
mipMapRaster.setMipLevel(mipLevel); mipMapRaster.setMipLevel(lowerMipLevel);
return mipMapRaster.getPixel((int) uvs.x, (int) uvs.y, store); 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;
} }
/** /**

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

@ -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) { 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[0] = readComponent(tmp, ap, az);
components[1] = readComponent(tmp, rp, rz); components[1] = readComponent(tmp, rp, rz);
components[2] = readComponent(tmp, gp, gz); components[2] = readComponent(tmp, gp, gz);

@ -34,8 +34,8 @@ package com.jme3.util;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.texture.Image; import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.texture.image.ImageRaster; import com.jme3.texture.image.ImageRaster;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
@ -59,7 +59,7 @@ public class MipMapGenerator {
float xRatio = ((float)(input.getWidth() - 1)) / output.getWidth(); float xRatio = ((float)(input.getWidth() - 1)) / output.getWidth();
float yRatio = ((float)(input.getHeight() - 1)) / output.getHeight(); 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 bottomLeft = new ColorRGBA();
ColorRGBA bottomRight = new ColorRGBA(); ColorRGBA bottomRight = new ColorRGBA();
ColorRGBA topLeft = new ColorRGBA(); ColorRGBA topLeft = new ColorRGBA();
@ -73,22 +73,14 @@ public class MipMapGenerator {
int x2 = (int)x2f; int x2 = (int)x2f;
int y2 = (int)y2f; int y2 = (int)y2f;
float xDiff = x2f - x2;
float yDiff = y2f - y2;
input.getPixel(x2, y2, bottomLeft); input.getPixel(x2, y2, bottomLeft);
input.getPixel(x2 + 1, y2, bottomRight); input.getPixel(x2 + 1, y2, bottomRight);
input.getPixel(x2, y2 + 1, topLeft); input.getPixel(x2, y2 + 1, topLeft);
input.getPixel(x2 + 1, y2 + 1, topRight); 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) outputColor.set(bottomLeft).addLocal(bottomRight)
.addLocal(topLeft).addLocal(topRight); .addLocal(topLeft).addLocal(topRight);
outputColor.multLocal(1f / 4f);
output.setPixel(x, y, outputColor); output.setPixel(x, y, outputColor);
} }
} }

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

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save