/* * 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.util; import com.jme3.asset.AssetManager; import com.jme3.material.Material; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.math.Vector4f; import com.jme3.scene.Geometry; import com.jme3.scene.Node; import com.jme3.scene.shape.Quad; import com.jme3.texture.Image; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.texture.TextureCubeMap; import com.jme3.texture.image.ColorSpace; import com.jme3.ui.Picture; import com.jme3.util.BufferUtils; import java.nio.ByteBuffer; import java.util.ArrayList; import static com.jme3.math.FastMath.*; import com.jme3.math.Quaternion; import com.jme3.math.Vector2f; import com.jme3.util.TempVars; /** * * This class holds several utility method unseful for Physically Based * Rendering. It alloaws to compute useful pre filtered maps from an env map. * * @author Nehon */ public class EnvMapUtils { public final static int NUM_SH_COEFFICIENT = 9; // See Peter-Pike Sloan paper for these coefficients //http://www.ppsloan.org/publications/StupidSH36.pdf public static float[] shBandFactor = {1.0f, 2.0f / 3.0f, 2.0f / 3.0f, 2.0f / 3.0f, 1.0f / 4.0f, 1.0f / 4.0f, 1.0f / 4.0f, 1.0f / 4.0f, 1.0f / 4.0f}; public static enum FixSeamsMethod { /** * wrap texture coordinates */ Wrap, /** * stretch texture coordinates */ Stretch, /** * No seams fix */ None; } /** * Creates a cube map from 6 images * * @param leftImg the west side image, also called negative x (negX) or left * image * @param rightImg the east side image, also called positive x (posX) or * right image * @param downImg the bottom side image, also called negative y (negY) or * down image * @param upImg the up side image, also called positive y (posY) or up image * @param backImg the south side image, also called positive z (posZ) or * back image * @param frontImg the north side image, also called negative z (negZ) or * front image * @param format the format of the image * @return a cube map */ public static TextureCubeMap makeCubeMap(Image rightImg, Image leftImg, Image upImg, Image downImg, Image backImg, Image frontImg, Image.Format format) { Image cubeImage = new Image(format, leftImg.getWidth(), leftImg.getHeight(), null, ColorSpace.Linear); cubeImage.addData(rightImg.getData(0)); cubeImage.addData(leftImg.getData(0)); cubeImage.addData(upImg.getData(0)); cubeImage.addData(downImg.getData(0)); cubeImage.addData(backImg.getData(0)); cubeImage.addData(frontImg.getData(0)); if (leftImg.getEfficentData() != null) { // also consilidate efficient data ArrayList<Object> efficientData = new ArrayList<Object>(6); efficientData.add(rightImg.getEfficentData()); efficientData.add(leftImg.getEfficentData()); efficientData.add(upImg.getEfficentData()); efficientData.add(downImg.getEfficentData()); efficientData.add(backImg.getEfficentData()); efficientData.add(frontImg.getEfficentData()); cubeImage.setEfficentData(efficientData); } TextureCubeMap cubeMap = new TextureCubeMap(cubeImage); cubeMap.setAnisotropicFilter(0); cubeMap.setMagFilter(Texture.MagFilter.Bilinear); cubeMap.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); cubeMap.setWrap(Texture.WrapMode.EdgeClamp); return cubeMap; } /** * Make a duplicate of this cube Map. That means that it's another instant * od TextureCubeMap, but the underlying buffers are duplicates of the * original ones. see {@link ByteBuffer#duplicate()} * * Use this if you need to read from the map from multiple threads, it * should garanty the thread safety. Note that if you want to write to the * cube map you have to make sure that the different thread do not write to * the same area of the buffer. The position, limit and mark are not an * issue. * * @param sourceMap * @return */ public static TextureCubeMap duplicateCubeMap(TextureCubeMap sourceMap) { Image srcImg = sourceMap.getImage(); Image cubeImage = new Image(srcImg.getFormat(), srcImg.getWidth(), srcImg.getHeight(), null, srcImg.getColorSpace()); for (ByteBuffer d : srcImg.getData()) { cubeImage.addData(d.duplicate()); } if (srcImg.getEfficentData() != null) { // also consilidate efficient data ArrayList<Object> efficientData = new ArrayList<Object>(6); efficientData.add(srcImg.getEfficentData()); cubeImage.setEfficentData(efficientData); } TextureCubeMap cubeMap = new TextureCubeMap(cubeImage); cubeMap.setAnisotropicFilter(sourceMap.getAnisotropicFilter()); cubeMap.setMagFilter(sourceMap.getMagFilter()); cubeMap.setMinFilter(sourceMap.getMinFilter()); cubeMap.setWrap(sourceMap.getWrap(Texture.WrapAxis.S)); return cubeMap; } /** * Computes the vector coordinates, for the given x,y texture coordinates * and the given cube map face. * * Also computes the solid angle for those coordinates and returns it. * * To know what the solid angle is please read this. * http://www.codinglabs.net/article_physically_based_rendering.aspx * * * Original solid angle calculation code is from Ignacio Castaño. This * formula is from Manne Öhrström's thesis. It takes two coordiantes in the * range [-1, 1] that define a portion of a cube face and return the area of * the projection of that portion on the surface of the sphere. * * @param x texture coordinate from 0 to 1 in the given cube map face * @param y texture coordinate from 0 to 1 in the given cube map face * @param mapSize the size of the cube map * @param face the face id of the cube map * @param store the vector3f where the vector will be stored. don't provide * null for this param * @return the solid angle for the give parameters */ static float getSolidAngleAndVector(int x, int y, int mapSize, int face, Vector3f store, FixSeamsMethod fixSeamsMethod) { if (store == null) { throw new IllegalArgumentException("the store parameter ust not be null"); } /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)] (+ 0.5f is for texel center addressing) */ float u = (2.0f * ((float) x + 0.5f) / (float) mapSize) - 1.0f; float v = (2.0f * ((float) y + 0.5f) / (float) mapSize) - 1.0f; getVectorFromCubemapFaceTexCoord(x, y, mapSize, face, store, fixSeamsMethod); /* Solid angle weight approximation : * U and V are the -1..1 texture coordinate on the current face. * Get projected area for this texel */ float x0, y0, x1, y1; float invRes = 1.0f / (float) mapSize; x0 = u - invRes; y0 = v - invRes; x1 = u + invRes; y1 = v + invRes; return areaElement(x0, y0) - areaElement(x0, y1) - areaElement(x1, y0) + areaElement(x1, y1); } /** * used to compute the solid angle * * @param x tex coordinates * @param y tex coordinates * @return */ private static float areaElement(float x, float y) { return (float) Math.atan2(x * y, sqrt(x * x + y * y + 1)); } /** * * Computes the 3 component vector coordinates for the given face and coords * * @param x the x texture coordinate * @param y the y texture coordinate * @param mapSize the size of a face of the cube map * @param face the face to consider * @param store a vector3f where the resulting vector will be stored * @param fixSeamsMethod the method to fix the seams * @return */ public static Vector3f getVectorFromCubemapFaceTexCoord(int x, int y, int mapSize, int face, Vector3f store, FixSeamsMethod fixSeamsMethod) { if (store == null) { store = new Vector3f(); } float u; float v; if (fixSeamsMethod == FixSeamsMethod.Stretch) { /* Code from Nvtt : http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvtt/CubeSurface.cpp * transform from [0..res - 1] to [-1 .. 1], match up edges exactly. */ u = (2.0f * (float) x / ((float) mapSize - 1.0f)) - 1.0f; v = (2.0f * (float) y / ((float) mapSize - 1.0f)) - 1.0f; } else { //Done if any other fix method or no fix method is set /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)] * (+ 0.5f is for texel center addressing) */ u = (2.0f * ((float) x + 0.5f) / (float) (mapSize)) - 1.0f; v = (2.0f * ((float) y + 0.5f) / (float) (mapSize)) - 1.0f; } if (fixSeamsMethod == FixSeamsMethod.Wrap) { // Warp texel centers in the proximity of the edges. float a = pow((float) mapSize, 2.0f) / pow(((float) mapSize - 1f), 3.0f); u = a * pow(u, 3f) + u; v = a * pow(v, 3f) + v; } //compute vector depending on the face // Code from Nvtt : http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvtt/CubeSurface.cpp switch (face) { case 0: store.set(1f, -v, -u); break; case 1: store.set(-1f, -v, u); break; case 2: store.set(u, 1f, v); break; case 3: store.set(u, -1f, -v); break; case 4: store.set(u, -v, 1f); break; case 5: store.set(-u, -v, -1.0f); break; } return store.normalizeLocal(); } /** * * Computes the texture coortinates and the face of the cube map from the * given vector * * @param texelVect the vector to fetch texelt from the cube map * @param fixSeamsMethod the method to fix the seams * @param mapSize the size of one face of the cube map * @param store a Vector2f where the texture coordinates will be stored * @return the face from which to fetch the texel */ public static int getCubemapFaceTexCoordFromVector(Vector3f texelVect, int mapSize, Vector2f store, FixSeamsMethod fixSeamsMethod) { float u = 0, v = 0, bias = 0; int face; float absX = abs(texelVect.x); float absY = abs(texelVect.y); float absZ = abs(texelVect.z); float max = Math.max(Math.max(absX, absY), absZ); if (max == absX) { face = texelVect.x > 0 ? 0 : 1; } else if (max == absY) { face = texelVect.y > 0 ? 2 : 3; } else { face = texelVect.z > 0 ? 4 : 5; } //compute vector depending on the face // Code from Nvtt : http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvtt/CubeSurface.cpp switch (face) { case 0: //store.set(1f, -v, -u, 0); bias = 1f / texelVect.x; u = -texelVect.z; v = -texelVect.y; break; case 1: // store.set(-1f, -v, u, 0); bias = -1f / texelVect.x; u = texelVect.z; v = -texelVect.y; break; case 2: //store.set(u, 1f, v, 0); bias = 1f / texelVect.y; u = texelVect.x; v = texelVect.z; break; case 3: //store.set(u, -1f, -v, 0); bias = -1f / texelVect.y; u = texelVect.x; v = -texelVect.z; break; case 4: //store.set(u, -v, 1f, 0); bias = 1f / texelVect.z; u = texelVect.x; v = -texelVect.y; break; case 5: //store.set(-u, -v, -1.0f, 0); bias = -1f / texelVect.z; u = -texelVect.x; v = -texelVect.y; break; } u *= bias; v *= bias; if (fixSeamsMethod == FixSeamsMethod.Stretch) { /* Code from Nvtt : http://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvtt/CubeSurface.cpp * transform from [0..res - 1] to [-1 .. 1], match up edges exactly. */ u = Math.round((u + 1.0f) * ((float) mapSize - 1.0f) * 0.5f); v = Math.round((v + 1.0f) * ((float) mapSize - 1.0f) * 0.5f); } else { //Done if any other fix method or no fix method is set /* transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)] * (+ 0.5f is for texel center addressing) */ u = Math.round((u + 1.0f) * ((float) mapSize) * 0.5f - 0.5f); v = Math.round((v + 1.0f) * ((float) mapSize) * 0.5f - 0.5f); } store.set(u, v); return face; } /* public static void main(String... argv) { // for (int givenFace = 0; givenFace < 6; givenFace++) { // // //int givenFace = 1; // for (int x = 0; x < 128; x++) { // for (int y = 0; y < 128; y++) { // Vector3f v = EnvMapUtils.getVectorFromCubemapFaceTexCoord(x, y, 128, givenFace, null, FixSeamsMethod.None); // Vector2f uvs = new Vector2f(); // int face = EnvMapUtils.getCubemapFaceTexCoordFromVector(v, 128, uvs, FixSeamsMethod.None); // // if ((int) uvs.x != x || (int) uvs.y != y) { // System.err.println("error " + uvs + " should be " + x + "," + y + " vect was " + v); // } // if (givenFace != face) { // System.err.println("error face: " + face + " should be " + givenFace); // } // } // } // } // System.err.println("done "); int total = 0; for (int i = 0; i < 6; i++) { int size = (int) pow(2, 7 - i); int samples = EnvMapUtils.getSampleFromMip(i, 6); int iterations = (samples * size * size); total += iterations; float roughness = EnvMapUtils.getRoughnessFromMip(i, 6); System.err.println("roughness " + i + " : " + roughness + " , map : " + size + " , samples : " + samples + " , iterations : " + iterations); System.err.println("reverse " + EnvMapUtils.getMipFromRoughness(roughness, 6)); } System.err.println("total " + total); System.err.println(128 * 128 * 1024); System.err.println("test " + EnvMapUtils.getMipFromRoughness(0.9999f, 6)); System.err.println("nb mip = " + (Math.log(128) / Math.log(2) - 1)); }*/ public static int getSampleFromMip(int mipLevel, int miptot) { return mipLevel==0?1:Math.min(1 << (miptot - 1 + (mipLevel) * 2 ), 8192); } public static float getRoughnessFromMip(int miplevel, int miptot) { float mipScale = 1.0f; float mipOffset = -0.3f; return pow(2, (miplevel - (miptot - 1) + mipOffset) / mipScale); } public static float getMipFromRoughness(float roughness, int miptot) { float mipScale = 1.0f; float Lod = (float) (Math.log(roughness) / Math.log(2)) * mipScale + miptot - 1.0f; return (float) Math.max(0.0, Lod); } /** * same as * {@link EnvMapUtils#getSphericalHarmonicsCoefficents(com.jme3.texture.TextureCubeMap, com.jme3.utils.EnvMapUtils.FixSeamsMethod)} * the fix method used is {@link FixSeamsMethod#Wrap} * * @param cubeMap the environment cube map to compute SH for * @return an array of 9 vector3f representing thos coefficients for each * r,g,b channnel */ public static Vector3f[] getSphericalHarmonicsCoefficents(TextureCubeMap cubeMap) { return getSphericalHarmonicsCoefficents(cubeMap, FixSeamsMethod.Wrap); } /** * Returns the Spherical Harmonics coefficients for this cube map. * * The method used is the one from this article : * http://graphics.stanford.edu/papers/envmap/envmap.pdf * * Also good resources on spherical harmonics * http://dickyjim.wordpress.com/2013/09/04/spherical-harmonics-for-beginners/ * * @param cubeMap the environment cube map to compute SH for * @param fixSeamsMethod method to fix seams when computing the SH * coefficients * @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]; float[] shDir = new float[9]; float weightAccum = 0.0f; 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 plase use renderer.readFrameBuffer, to create a CPU image"); } int width = cubeMap.getImage().getWidth(); int height = cubeMap.getImage().getHeight(); Vector3f texelVect = new Vector3f(); ColorRGBA color = new ColorRGBA(); CubeMapWrapper envMapReader = new CubeMapWrapper(cubeMap); 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); } weightAccum += weight; } } } /* Normalization - The sum of solid angle should be equal to the solid angle of the sphere (4 PI), so * normalize in order our weightAccum exactly match 4 PI. */ for (int i = 0; i < NUM_SH_COEFFICIENT; ++i) { shCoef[i].multLocal(4.0f * PI / weightAccum); } return shCoef; } /** * Computes SH coefficient for a given textel dir The method used is the one * from this article : http://graphics.stanford.edu/papers/envmap/envmap.pdf * * @param texelVect * @param shDir */ public static void evalShBasis(Vector3f texelVect, float[] shDir) { float xV = texelVect.x; float yV = texelVect.y; float zV = texelVect.z; float pi = PI; float sqrtPi = sqrt(pi); float sqrt3Pi = sqrt(3f / pi); float sqrt5Pi = sqrt(5f / pi); float sqrt15Pi = sqrt(15f / pi); float x2 = xV * xV; float y2 = yV * yV; float z2 = zV * zV; shDir[0] = (1f / (2f * sqrtPi)); shDir[1] = -(sqrt3Pi * yV) / 2f; shDir[2] = (sqrt3Pi * zV) / 2f; shDir[3] = -(sqrt3Pi * xV) / 2f; shDir[4] = (sqrt15Pi * xV * yV) / 2f; shDir[5] = -(sqrt15Pi * yV * zV) / 2f; shDir[6] = (sqrt5Pi * (-1f + 3f * z2)) / 4f; shDir[7] = -(sqrt15Pi * xV * zV) / 2f; shDir[8] = sqrt15Pi * (x2 - y2) / 4f; // shDir[0] = (1f/(2.f*sqrtPi)); // // shDir[1] = -(sqrt(3f/pi)*yV)/2.f; // shDir[2] = (sqrt(3/pi)*zV)/2.f; // shDir[3] = -(sqrt(3/pi)*xV)/2.f; // // shDir[4] = (sqrt(15f/pi)*xV*yV)/2.f; // shDir[5] = -(sqrt(15f/pi)*yV*zV)/2.f; // shDir[6] = (sqrt(5f/pi)*(-1 + 3f*z2))/4.f; // shDir[7] = -(sqrt(15f/pi)*xV*zV)/2.f; // shDir[8] = sqrt(15f/pi)*(x2 - y2)/4.f; } /** * {@link EnvMapUtils#generateIrradianceMap(com.jme3.math.Vector3f[], com.jme3.texture.TextureCubeMap, int, com.jme3.utils.EnvMapUtils.FixSeamsMethod) * } * * @param shCoeffs the spherical harmonics coefficients to use * @param targetMapSize the size of the target map * @return the irradiance map. */ public static TextureCubeMap generateIrradianceMap(Vector3f[] shCoeffs, int targetMapSize) { return generateIrradianceMap(shCoeffs, targetMapSize, FixSeamsMethod.Wrap, null); } /** * Generates the Irradiance map (used for image based difuse lighting) from * Spherical Harmonics coefficients previously computed with * {@link EnvMapUtils#getSphericalHarmonicsCoefficents(com.jme3.texture.TextureCubeMap)} * Note that the output cube map is in RGBA8 format. * * @param shCoeffs the SH coeffs * @param targetMapSize the size of the irradiance map to generate * @param fixSeamsMethod the method to fix seams * @param store * @return The irradiance cube map for the given coefficients */ public static TextureCubeMap generateIrradianceMap(Vector3f[] shCoeffs, int targetMapSize, FixSeamsMethod fixSeamsMethod, TextureCubeMap store) { TextureCubeMap irrCubeMap = store; if (irrCubeMap == null) { irrCubeMap = new TextureCubeMap(targetMapSize, targetMapSize, Image.Format.RGB16F); irrCubeMap.setMagFilter(Texture.MagFilter.Bilinear); irrCubeMap.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); irrCubeMap.getImage().setColorSpace(ColorSpace.Linear); } for (int i = 0; i < 6; i++) { ByteBuffer buf = BufferUtils.createByteBuffer(targetMapSize * targetMapSize * irrCubeMap.getImage().getFormat().getBitsPerPixel()/8); irrCubeMap.getImage().setData(i, buf); } Vector3f texelVect = new Vector3f(); ColorRGBA color = new ColorRGBA(ColorRGBA.Black); float[] shDir = new float[9]; CubeMapWrapper envMapWriter = new CubeMapWrapper(irrCubeMap); for (int face = 0; face < 6; face++) { for (int y = 0; y < targetMapSize; y++) { for (int x = 0; x < targetMapSize; x++) { getVectorFromCubemapFaceTexCoord(x, y, targetMapSize, face, texelVect, fixSeamsMethod); evalShBasis(texelVect, shDir); color.set(0, 0, 0, 0); for (int i = 0; i < NUM_SH_COEFFICIENT; i++) { color.set(color.r + shCoeffs[i].x * shDir[i] * shBandFactor[i], color.g + shCoeffs[i].y * shDir[i] * shBandFactor[i], color.b + shCoeffs[i].z * shDir[i] * shBandFactor[i], 1.0f); } //clamping the color because very low value close to zero produce artifacts color.r = Math.max(0.0001f, color.r); color.g = Math.max(0.0001f, color.g); color.b = Math.max(0.0001f, color.b); envMapWriter.setPixel(x, y, face, color); } } } return irrCubeMap; } /** * 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 */ public static TextureCubeMap generatePrefilteredEnvMap(TextureCubeMap sourceEnvMap, int targetMapSize, FixSeamsMethod fixSeamsMethod, TextureCubeMap store) { TextureCubeMap pem = store; if (pem == null) { pem = new TextureCubeMap(targetMapSize, targetMapSize, Image.Format.RGB16F); pem.setMagFilter(Texture.MagFilter.Bilinear); pem.setMinFilter(Texture.MinFilter.Trilinear); pem.getImage().setColorSpace(ColorSpace.Linear); } int nbMipMap = (int) (Math.log(targetMapSize) / Math.log(2) - 1); CubeMapWrapper sourceWrapper = new CubeMapWrapper(sourceEnvMap); CubeMapWrapper targetWrapper = new CubeMapWrapper(pem); targetWrapper.initMipMaps(nbMipMap); Vector3f texelVect = new Vector3f(); Vector3f color = new Vector3f(); ColorRGBA outColor = new ColorRGBA(); for (int mipLevel = 0; mipLevel < nbMipMap; mipLevel++) { System.err.println("mip level " + mipLevel); float roughness = getRoughnessFromMip(mipLevel, nbMipMap); int nbSamples = getSampleFromMip(mipLevel, nbMipMap); int targetMipMapSize = (int) pow(2, nbMipMap + 1 - mipLevel); for (int face = 0; face < 6; face++) { System.err.println("face " + face); 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.Wrap); prefilterEnvMapTexel(sourceWrapper, roughness, texelVect, nbSamples, color); outColor.set(color.x, color.y, color.z, 1.0f); // System.err.println("coords " + x + "," + y); targetWrapper.setPixel(x, y, face, mipLevel, outColor); } } } } return pem; } public static Vector4f getHammersleyPoint(int i, final int nbrSample, Vector4f store) { if (store == null) { store = new Vector4f(); } float phi; long ui = i; store.setX((float) i / (float) nbrSample); /* From http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html * Radical Inverse : Van der Corput */ ui = (ui << 16) | (ui >> 16); ui = ((ui & 0x55555555) << 1) | ((ui & 0xAAAAAAAA) >>> 1); ui = ((ui & 0x33333333) << 2) | ((ui & 0xCCCCCCCC) >>> 2); ui = ((ui & 0x0F0F0F0F) << 4) | ((ui & 0xF0F0F0F0) >>> 4); ui = ((ui & 0x00FF00FF) << 8) | ((ui & 0xFF00FF00) >>> 8); ui = ui & 0xffffffff; store.setY(2.3283064365386963e-10f * (float) (ui)); /* 0x100000000 */ phi = 2.0f * PI * store.y; store.setZ(cos(phi)); store.setW(sin(phi)); return store; } private static Vector3f prefilterEnvMapTexel(CubeMapWrapper envMapReader, float roughness, Vector3f N, int numSamples, Vector3f store) { Vector3f prefilteredColor = store; float totalWeight = 0.0f; TempVars vars = TempVars.get(); Vector4f Xi = vars.vect4f1; Vector3f H = vars.vect1; Vector3f tmp = vars.vect2; ColorRGBA c = vars.color; // 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, vars); 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; } } vars.release(); return prefilteredColor.divideLocal(totalWeight); } public static Vector3f importanceSampleGGX(Vector4f xi, float a2, Vector3f normal, Vector3f store, TempVars vars) { 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 = vars.vect3.set(upVector).crossLocal(normal).normalizeLocal(); Vector3f tangentY = vars.vect4.set(normal).crossLocal(tangentX); // Tangent to world space tangentX.multLocal(sinThetaCosPhi); tangentY.multLocal(sinThetaSinPhi); vars.vect5.set(normal).multLocal(cosTheta); // Tangent to world space store.set(tangentX).addLocal(tangentY).addLocal(vars.vect5); return store; } /** * Creates a debug Node of the given cube map to attach to the gui node * * the cube map is layered this way : * <pre> * _____ * | | * | +Y | * _____|_____|_____ _____ * | | | | | * | -X | +Z | +X | -Z | * |_____|_____|_____|_____| * | | * | -Y | * |_____| * *</pre> * * @param cubeMap the cube map * @param assetManager the asset Manager * @return */ public static Node getCubeMapCrossDebugView(TextureCubeMap cubeMap, AssetManager assetManager) { Node n = new Node("CubeMapDebug" + cubeMap.getName()); int size = cubeMap.getImage().getWidth(); Picture[] pics = new Picture[6]; float ratio = 128f / (float) size; for (int i = 0; i < 6; i++) { pics[i] = new Picture("bla"); Texture2D tex = new Texture2D(new Image(cubeMap.getImage().getFormat(), size, size, cubeMap.getImage().getData(i), cubeMap.getImage().getColorSpace())); pics[i].setTexture(assetManager, tex, true); pics[i].setWidth(size); pics[i].setHeight(size); n.attachChild(pics[i]); } pics[0].setLocalTranslation(size, size * 2, 1); pics[0].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[1].setLocalTranslation(size * 3, size * 2, 1); pics[1].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[2].setLocalTranslation(size * 2, size * 3, 1); pics[2].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[3].setLocalTranslation(size * 2, size, 1); pics[3].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[4].setLocalTranslation(size * 2, size * 2, 1); pics[4].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[5].setLocalTranslation(size * 4, size * 2, 1); pics[5].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); Quad q = new Quad(size * 4, size * 3); Geometry g = new Geometry("bg", q); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", ColorRGBA.Black); g.setMaterial(mat); g.setLocalTranslation(0, 0, 0); n.attachChild(g); n.setLocalScale(ratio); return n; } public static Node getCubeMapCrossDebugViewWithMipMaps(TextureCubeMap cubeMap, AssetManager assetManager) { Node n = new Node("CubeMapDebug" + cubeMap.getName()); int size = cubeMap.getImage().getWidth(); int nbMips = cubeMap.getImage().getMipMapSizes().length; Picture[] pics = new Picture[6*nbMips]; float ratio = 1f;// 128f / (float) size; int offset = 0; int guiOffset = 0; for (int mipLevel = 0; mipLevel < nbMips; mipLevel++) { size = Math.max(1, cubeMap.getImage().getWidth() >> mipLevel); int dataSize = cubeMap.getImage().getMipMapSizes()[mipLevel]; byte[] dataArray = new byte[dataSize]; for (int i = 0; i < 6; i++) { ByteBuffer bb = cubeMap.getImage().getData(i); bb.rewind(); bb.position(offset); bb.get(dataArray, 0, dataSize); ByteBuffer data = BufferUtils.createByteBuffer(dataArray); pics[i] = new Picture("bla"); Texture2D tex = new Texture2D(new Image(cubeMap.getImage().getFormat(), size, size, data, cubeMap.getImage().getColorSpace())); pics[i].setTexture(assetManager, tex, true); pics[i].setWidth(size); pics[i].setHeight(size); n.attachChild(pics[i]); } pics[0].setLocalTranslation(guiOffset + size, guiOffset + size * 2, 1); pics[0].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[1].setLocalTranslation(guiOffset + size * 3, guiOffset + size * 2, 1); pics[1].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[2].setLocalTranslation(guiOffset + size * 2, guiOffset + size * 3, 1); pics[2].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[3].setLocalTranslation(guiOffset + size * 2, guiOffset + size, 1); pics[3].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[4].setLocalTranslation(guiOffset + size * 2, guiOffset + size * 2, 1); pics[4].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); pics[5].setLocalTranslation(guiOffset + size * 4, guiOffset + size * 2, 1); pics[5].setLocalRotation(new Quaternion().fromAngleAxis(PI, Vector3f.UNIT_Z)); guiOffset+=size *2+1; offset += dataSize; } Quad q = new Quad(cubeMap.getImage().getWidth() * 4 + nbMips, guiOffset + size); Geometry g = new Geometry("bg", q); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", ColorRGBA.Black); g.setMaterial(mat); g.setLocalTranslation(0, 0, 0); n.attachChild(g); n.setLocalScale(ratio); return n; } /** * initialize the Irradiancemap * @param size the size of the map * @param imageFormat the format of the image * @return the initialized Irradiance map */ public static TextureCubeMap createIrradianceMap(int size, Image.Format imageFormat) { TextureCubeMap irrMap = new TextureCubeMap(size, size, imageFormat); irrMap.setMagFilter(Texture.MagFilter.Bilinear); irrMap.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); irrMap.getImage().setColorSpace(ColorSpace.Linear); return irrMap; } /** * initialize the pem map * @param size the size of the map * @param imageFormat the format of the image * @return the initialized prefiltered env map */ public static TextureCubeMap createPrefilteredEnvMap(int size, Image.Format imageFormat) { TextureCubeMap pem = new TextureCubeMap(size, size, imageFormat); pem.setMagFilter(Texture.MagFilter.Bilinear); pem.setMinFilter(Texture.MinFilter.Trilinear); pem.getImage().setColorSpace(ColorSpace.Linear); int nbMipMap = (int) (Math.log(size) / Math.log(2) - 1); CubeMapWrapper targetWrapper = new CubeMapWrapper(pem); targetWrapper.initMipMaps(nbMipMap); return pem; } }