/*
* Copyright (c) 2009-2015 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.generation;
import com.jme3.environment.util.CubeMapWrapper;
import com.jme3.environment.util.EnvMapUtils;
import com.jme3.app.Application;
import com.jme3.math.ColorRGBA;
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.math.Vector3f;
import com.jme3.math.Vector4f;
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;
/**
*
* Generates one face of the prefiltered environnement map for PBR. This job can
* be lauched from a separate thread.
*
* TODO there is a lot of duplicate code here with the EnvMapUtils.
*
* @author Nehon
*/
//TODO there is a lot of duplicate code here with the EnvMapUtils. We should,
//either leverage the code from the util class either remove it and only allow
//parallel generation using this runnable.
public class PrefilteredEnvMapFaceGenerator extends RunnableWithProgress {
private final static Logger log = Logger.getLogger(PrefilteredEnvMapFaceGenerator.class.getName());
private int targetMapSize;
private EnvMapUtils.FixSeamsMethod fixSeamsMethod;
private TextureCubeMap sourceMap;
private TextureCubeMap store;
private final Application app;
private int face = 0;
Vector4f Xi = new Vector4f();
Vector3f H = new Vector3f();
Vector3f tmp = new Vector3f();
ColorRGBA c = new ColorRGBA();
Vector3f tmp1 = new Vector3f();
Vector3f tmp2 = new Vector3f();
Vector3f tmp3 = new Vector3f();
/**
* Creates a pem generator for the given face. The app is needed to enqueue
* the call to the EnvironmentCamera when the generation is done, so that
* this process is thread safe.
*
* @param app the Application
* @param face the face to generate
* @param listener
*/
public PrefilteredEnvMapFaceGenerator(Application app, int face, JobProgressListener<Integer> listener) {
super(listener);
this.app = app;
this.face = face;
}
/**
* Fills all the genration parameters
*
* @param sourceMap the source cube map
* @param targetMapSize the size of the generated map (width or height in
* pixel)
* @param fixSeamsMethod the method used to fix seams as described here
* {@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) {
this.sourceMap = sourceMap;
this.targetMapSize = targetMapSize;
this.fixSeamsMethod = fixSeamsMethod;
this.store = store;
init();
}
private void init(){
Xi.set(0, 0, 0, 0);
H.set(0, 0, 0);
tmp.set(0, 0, 0);
c.set(1, 1, 1, 1);
tmp1.set(0, 0, 0);
tmp2.set(0, 0, 0);
tmp3.set(0, 0, 0);
reset();
}
@Override
public void run() {
app.enqueue(new Callable<Void>() {
@Override
public Void call() throws Exception {
listener.start();
return null;
}
});
store = generatePrefilteredEnvMap(sourceMap, targetMapSize, fixSeamsMethod, store);
app.enqueue(new Callable<Void>() {
@Override
public Void call() throws Exception {
listener.done(face);
return null;
}
});
}
/**
* Generates the prefiltered env map (used for image based specular
* lighting) With the GGX/Shlick brdf
* {@link EnvMapUtils#getSphericalHarmonicsCoefficents(com.jme3.texture.TextureCubeMap)}
* Note that the output cube map is in RGBA8 format.
*
* @param sourceEnvMap
* @param targetMapSize the size of the irradiance map to generate
* @param store
* @param fixSeamsMethod the method to fix seams
* @return The irradiance cube map for the given coefficients
*/
private TextureCubeMap generatePrefilteredEnvMap(TextureCubeMap sourceEnvMap, int targetMapSize, EnvMapUtils.FixSeamsMethod fixSeamsMethod, TextureCubeMap store) {
TextureCubeMap pem = store;
int nbMipMap = (int) (Math.log(targetMapSize) / Math.log(2) - 1);
setEnd(nbMipMap);
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);
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, EnvMapUtils.FixSeamsMethod.Wrap);
prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, 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);
}
}
progress();
}
return pem;
}
private Vector3f prefilterEnvMapTexel(CubeMapWrapper envMapReader, float roughness, Vector3f N, int numSamples, Vector3f store) {
Vector3f prefilteredColor = store;
float totalWeight = 0.0f;
// a = roughness² and a2 = a²
float a2 = roughness * roughness;
a2 *= a2;
a2 *= 10;
for (int i = 0; i < numSamples; i++) {
Xi = getHammersleyPoint(i, numSamples, Xi);
H = importanceSampleGGX(Xi, a2, N, 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;
}
}
return prefilteredColor.divideLocal(totalWeight);
}
public Vector3f importanceSampleGGX(Vector4f xi, float a2, Vector3f normal, Vector3f store) {
if (store == null) {
store = new Vector3f();
}
float cosTheta = sqrt((1f - xi.x) / (1f + (a2 - 1f) * xi.x));
float sinTheta = sqrt(1f - cosTheta * cosTheta);
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);
return store;
}
}