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