package org.osm2world.core.target.jogl;
import static javax.media.opengl.GL.GL_CCW;
import static javax.media.opengl.GL.GL_COLOR_BUFFER_BIT;
import static javax.media.opengl.GL.GL_CULL_FACE;
import static javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT;
import static javax.media.opengl.GL.GL_DEPTH_TEST;
import static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_MODELVIEW;
import static javax.media.opengl.fixedfunc.GLMatrixFunc.GL_PROJECTION;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.media.opengl.GL;
import javax.media.opengl.GL3;
import jogamp.opengl.ProjectFloat;
import org.osm2world.core.math.AxisAlignedBoundingBoxXYZ;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.target.common.lighting.GlobalLightingParameters;
import org.osm2world.core.target.common.material.Material;
import org.osm2world.core.target.common.material.Material.Shadow;
import org.osm2world.core.target.common.material.Material.Transparency;
import org.osm2world.core.target.common.rendering.Projection;
import org.osm2world.viewer.model.Defaults;
import com.jogamp.opengl.math.FloatUtil;
import com.jogamp.opengl.util.PMVMatrix;
/**
* Shader to render the depth buffer into a texture that can be used to implement shadow maps later.
*/
public class ShadowMapShader extends DepthBufferShader {
protected int shadowMapWidth = 1024;
protected int shadowMapHeight = 1024;
/**
* Padding for the calculated bounding box around the camera frustum.
* This is needed to not cut away shadow casters outside but nearby the camera frustum
* that may cast shadows which lay within the camera frustum.
*/
private int cameraFrustumPadding = 8;
public int depthBufferHandle;
public int colorBufferHandle;
private int frameBufferHandle;
private int[] viewport = new int[4];
private boolean renderOpaque = true;
/**
* model view projection matrix of the shadow casting light source
*/
private PMVMatrix pmvMat;
public ShadowMapShader(GL3 gl) {
super(gl);
pmvMat = new PMVMatrix();
initializeShadowMap();
}
/**
* Setup the framebuffer and texture.
*/
private void initializeShadowMap() {
// create the shadow map texture / depth buffer
int[] tmp = new int[1];
gl.glGenTextures(1,tmp,0);
depthBufferHandle = tmp[0];
gl.glActiveTexture(GL.GL_TEXTURE0);
gl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);
gl.glTexImage2D(GL.GL_TEXTURE_2D, // target texture type
0, // mipmap LOD level
GL3.GL_DEPTH_COMPONENT, // internal pixel format
//GL_DEPTH_COMPONENT
shadowMapWidth, // width of generated image
shadowMapHeight, // height of generated image
0, // border of image
GL3.GL_DEPTH_COMPONENT, // external pixel format
GL.GL_UNSIGNED_BYTE, // datatype for each value
null); // buffer to store the texture in memory
// some settings for the shadow map texture
// GL_LINEAR might produce better results, but is slower. GL_NEAREST shows aliasing artifacts clearly
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
/* For texture access outside the shadow map use the highest depth value possible (1.0).
* This means the fragment lies outside of the lights frustum and no shadow should be applied.
* Therefore we use CLAMP_TO_BORDER with a border of (1.0, 0.0, 0.0, 0.0)
*/
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL3.GL_CLAMP_TO_BORDER);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL3.GL_CLAMP_TO_BORDER);
float [] border = {1.0f, 0.0f, 0.0f, 0.0f};
gl.glTexParameterfv(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_BORDER_COLOR, border, 0);
/* special for depth textures: do not retrieve the texture values, but the result of a comparison.
* compare the third value (r) of the texture coordinate against the depth value stored at the texture coordinate (s,t)
* result will be 1.0 if r is less than the texture value (which means the fragment is nearer) and 0.0 otherwise
*/
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_COMPARE_MODE, GL3.GL_COMPARE_REF_TO_TEXTURE);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL3.GL_TEXTURE_COMPARE_FUNC, GL.GL_LESS);
gl.glActiveTexture(GL.GL_TEXTURE0);
gl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);
/*gl.glGenTextures(1,tmp,0);
colorBufferHandle = tmp[0];
gl.glActiveTexture(GL.GL_TEXTURE1);
gl.glBindTexture(GL.GL_TEXTURE_2D, colorBufferHandle);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
gl.glTexImage2D(GL.GL_TEXTURE_2D, // target texture type
0, // mipmap LOD level
GL.GL_RGBA, // internal pixel format
//GL_DEPTH_COMPONENT
shadowMapWidth, // width of generated image
shadowMapHeight, // height of generated image
0, // border of image
GL.GL_RGBA, // external pixel format
GL.GL_UNSIGNED_BYTE, // datatype for each value
null); // buffer to store the texture in memory
*/
// create the frame buffer object (FBO)
gl.glGenFramebuffers(1, tmp, 0);
frameBufferHandle = tmp[0];
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, frameBufferHandle);
//Attach 2D texture to this FBO
gl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER,
GL.GL_DEPTH_ATTACHMENT,
GL.GL_TEXTURE_2D,
depthBufferHandle,0);
/*gl.glFramebufferTexture(GL.GL_FRAMEBUFFER,
GL.GL_DEPTH_ATTACHMENT,
depthBufferHandle,0);*/
/*gl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER,
GL.GL_COLOR_ATTACHMENT0,
GL.GL_TEXTURE_2D,
colorBufferHandle,0);*/
// set target for fragment shader output: not used, we only need the depth buffer
//int[] drawBuffers = {GL.GL_NONE};
//gl.glDrawBuffers(1, drawBuffers, 0);
//gl.glDrawBuffer(GL.GL_COLOR_ATTACHMENT0);
gl.glDrawBuffer(GL.GL_NONE);
gl.glReadBuffer(GL.GL_NONE);
//gl.glBindTexture(GL.GL_TEXTURE_2D, 0);
//Disable color buffer
//http://stackoverflow.com/questions/12546368/render-the-depth-buffer-into-a-texture-using-a-frame-buffer
//gl.glDrawBuffer(GL2.GL_NONE);
//gl.glReadBuffer(GL2.GL_NONE);
//Set pixels ((width*2)* (height*2))
//It has to have twice the size of shadowmap size
//pixels = GLBuffers.newDirectByteBuffer(shadowMapWidth*shadowMapHeight*4);
//Set default frame buffer before doing the check
//http://www.opengl.org/wiki/FBO#Completeness_Rules
//gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);
int status = gl.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER);
// Always check that our framebuffer is ok
if(gl.glCheckFramebufferStatus(GL.GL_FRAMEBUFFER) != GL.GL_FRAMEBUFFER_COMPLETE)
{
throw new RuntimeException("Can not use FBO! Status error:" + status);
}
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);
}
/**
* @see #cameraFrustumPadding
*/
public void setCameraFrustumPadding(int padding) {
this.cameraFrustumPadding = padding;
}
/**
* Change the size of the shadow map texture. This needs to be called before {@link #useShader()},
* as otherwise the viewport may be wrong.
* @param width the new texture width
* @param height the new texture height
*/
public void setShadowMapSize(int width, int height) {
resizeBuffer(width, height);
}
/**
* Resize the framebuffer backing texture, if size doesn't match.
*/
private void resizeBuffer(int width, int height) {
if (width != this.shadowMapWidth || height != this.shadowMapHeight) {
this.shadowMapWidth = width;
this.shadowMapHeight = height;
gl.glBindTexture(GL.GL_TEXTURE_2D, depthBufferHandle);
gl.glTexImage2D(GL.GL_TEXTURE_2D, // target texture type
0, // mipmap LOD level
GL3.GL_DEPTH_COMPONENT, // internal pixel format
//GL_DEPTH_COMPONENT
shadowMapWidth, // width of generated image
shadowMapHeight, // height of generated image
0, // border of image
GL3.GL_DEPTH_COMPONENT, // external pixel format
GL.GL_UNSIGNED_BYTE, // datatype for each value
null); // buffer to store the texture in memory
/*gl.glBindTexture(GL.GL_TEXTURE_2D, colorBufferHandle);
gl.glTexImage2D(GL.GL_TEXTURE_2D, // target texture type
0, // mipmap LOD level
GL.GL_RGBA, // internal pixel format
//GL_DEPTH_COMPONENT
shadowMapWidth, // width of generated image
shadowMapHeight, // height of generated image
0, // border of image
GL.GL_RGBA, // external pixel format
GL.GL_UNSIGNED_BYTE, // datatype for each value
null); // buffer to store the texture in memory
*/
}
}
/**
* Prepare everything to render the shadow map (bind framebuffer, update viewport, clear buffer, etc.)
*/
private void prepareShadowMapGeneration() {
// bind FBO
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, frameBufferHandle);
// set right viewport for the framebuffer size, store old to reset later
gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
gl.glViewport(0, 0, shadowMapWidth, shadowMapHeight);
// clear shadow map
gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// enable front face culling
gl.glFrontFace(GL_CCW);
gl.glCullFace(GL.GL_FRONT);
gl.glEnable (GL_CULL_FACE);
//gl.glDisable (GL_CULL_FACE);
gl.glEnable(GL_DEPTH_TEST);
}
public void saveColorBuffer(File file) {
// create buffer to store image
ByteBuffer buffer = ByteBuffer.allocate(shadowMapWidth*shadowMapHeight*4);
// load image in buffer
gl.glBindTexture(GL.GL_TEXTURE_2D, colorBufferHandle);
gl.glGetTexImage(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buffer);
buffer.rewind();
// create buffered image
BufferedImage img = new BufferedImage(shadowMapWidth, shadowMapHeight, BufferedImage.TYPE_INT_RGB);
// copy data to buffered image
for (int col=0; col<img.getWidth(); col++) {
for (int row=0; row<img.getHeight(); row++) {
byte r = buffer.get(row*shadowMapHeight*4+col*4+0);
byte g = buffer.get(row*shadowMapHeight*4+col*4+1);
byte b = buffer.get(row*shadowMapHeight*4+col*4+2);
byte a = buffer.get(row*shadowMapHeight*4+col*4+3);
int alpha = a & 0xFF;
int red = r & 0xFF;
int green = g & 0xFF;
int blue = b & 0xFF;
int rgb = (alpha << 24 | red << 16 | green << 8 | blue);
img.setRGB(col, shadowMapHeight-1-row, rgb);
}
}
// save to file
try {
ImageIO.write(img, "png", file);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* prepare and use PMVMatrix for rendering shadows from global lighting perspective using "Perspective Shadow Maps"
* (see http://www-sop.inria.fr/reves/Marc.Stamminger/psm/)
* @param lighting
*/
public void preparePMVMatrixPSM(GlobalLightingParameters lighting, PMVMatrix cameraPMV, AxisAlignedBoundingBoxXYZ primitivesBoundingBox) {
// camera PMV Matrix:
FloatBuffer camPMvMat = FloatBuffer.allocate(16);
FloatUtil.multMatrixf(cameraPMV.glGetPMatrixf(), cameraPMV.glGetMvMatrixf(), camPMvMat);
// transform light into camera space (unit cube)
float[] lightPos = {(float)lighting.lightFromDirection.x, (float)lighting.lightFromDirection.y, -(float)lighting.lightFromDirection.z, 0};
float[] lightPosCam = new float[4];
FloatUtil.multMatrixVecf(camPMvMat, lightPos, lightPosCam);
// set view and projection matrices to light source
PMVMatrix pmvMatL = new PMVMatrix();
pmvMatL.glMatrixMode(GL_MODELVIEW);
pmvMatL.glLoadIdentity();
pmvMatL.gluLookAt(lightPosCam[0], lightPosCam[1], lightPosCam[2],
0f, 0f, 0f,
0f, 1f, 0f);
pmvMatL.glMatrixMode(GL_PROJECTION);
pmvMatL.glLoadIdentity();
Projection projection = Defaults.PERSPECTIVE_PROJECTION;
pmvMatL.gluPerspective(
(float)(projection.getVertAngle()),
(float)(projection.getAspectRatio()),
(float)(projection.getNearClippingDistance()),
(float)(projection.getFarClippingDistance()));
//pmvMat.glOrthof(-1000,1000,-1000,1000,-1000,1500);
//float[] frustum;
/*frustum = intersectFrustum(calculateCameraLightFrustum(pmvMat, cameraPMV),
calculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox));*/
//frustum = calculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox);
//System.out.println("shadow map frustum: " + Arrays.toString(frustum));
//pmvMatL.glOrthof(frustum[0], frustum[1], frustum[2], frustum[3], frustum[4], frustum[5]);
// M = M_cam
pmvMat.glMatrixMode(GL_MODELVIEW);
pmvMat.glLoadMatrixf(cameraPMV.glGetMvMatrixf());
// P = P_light*Mv_light*P_cam
pmvMat.glMatrixMode(GL_PROJECTION);
pmvMat.glLoadMatrixf(pmvMatL.glGetPMatrixf());
pmvMat.glMultMatrixf(pmvMatL.glGetMvMatrixf());
pmvMat.glMultMatrixf(cameraPMV.glGetPMatrixf());
setPMVMatrix(pmvMat);
}
/**
* Prepare and use PMVMatrix for rendering shadows from global lighting perspective
* @param lighting contains the lights direction
* @param cameraPMV the current camera PMVMatrix used to tighten the lights view frustum on the visible part of the world
* @param primitivesBoundingBox bounding box around all relevant primitives in world coordinates. Also used to tighten the lights view frustum
*/
public void preparePMVMatrix(GlobalLightingParameters lighting, PMVMatrix cameraPMV, AxisAlignedBoundingBoxXYZ primitivesBoundingBox) {
// set view and projection matrices to light source
pmvMat.glMatrixMode(GL_MODELVIEW);
pmvMat.glLoadIdentity();
pmvMat.gluLookAt((float)lighting.lightFromDirection.x, (float)lighting.lightFromDirection.y, -(float)lighting.lightFromDirection.z,
0f, 0f, 0f,
0f, 1f, 0f);
pmvMat.glMatrixMode(GL_PROJECTION);
pmvMat.glLoadIdentity();
/*Projection projection = Defaults.PERSPECTIVE_PROJECTION;
pmvMat.gluPerspective(
(float)(projection.getVertAngle()),
(float)(projection.getAspectRatio()),
(float)(projection.getNearClippingDistance()),
(float)(projection.getFarClippingDistance()));*/
//pmvMat.glOrthof(-1000,1000,-1000,1000,-1000,1500);
AxisAlignedBoundingBoxXYZ frustum;
frustum = AxisAlignedBoundingBoxXYZ.intersect(calculateCameraLightFrustum(pmvMat, cameraPMV),
calculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox));
//frustum = calculatePrimitivesLightFrustum(pmvMat, primitivesBoundingBox);
//frustum = calculateCameraLightFrustum(pmvMat, cameraPMV);
pmvMat.glOrthof((float)frustum.minX, (float)frustum.maxX, (float)frustum.minY, (float)frustum.maxY, (float)frustum.minZ, (float)frustum.maxZ);
pmvMat.glMatrixMode(GL_MODELVIEW);
setPMVMatrix(pmvMat);
}
/**
* Calculate the frustum for the light projection matrix based on the bounding box of all primitives.
* Transforms the bounding box into lightspace and draws an axis aligned bounding box around it.
* @param lightPMV contains the lights ModelView matrix
* @param primitivesBoundingBox bounding box around all relevant primitives in world coordinates
* @return the optimal frustum for the light
*/
private AxisAlignedBoundingBoxXYZ calculatePrimitivesLightFrustum(PMVMatrix lightPMV, AxisAlignedBoundingBoxXYZ primitivesBoundingBox) {
ArrayList<VectorXYZ> corners = new ArrayList<VectorXYZ>();
for (VectorXYZ corner : primitivesBoundingBox.corners()) {
float[] result = new float[4];
FloatUtil.multMatrixVecf(lightPMV.glGetMvMatrixf(), new float[]{(float)corner.x, (float)corner.y, (float)corner.z, 1}, result);
corners.add(new VectorXYZ(result[0]/result[3], result[1]/result[3], result[2]/result[3]));
}
AxisAlignedBoundingBoxXYZ frustum = new AxisAlignedBoundingBoxXYZ(corners);
return frustum;
}
/**
* Calculate the frustum for the light projection matrix based on the frustum of the camera
* @param lightPMV contains the lights ModelView matrix
* @param cameraPMV the cameras PMVMatrix that defines the camera view frustum
* @return the optimal frustum for the light
*/
private AxisAlignedBoundingBoxXYZ calculateCameraLightFrustum(PMVMatrix lightPMV, PMVMatrix cameraPMV) {
/*
* calculate transform from screen space bounding box to light space:
* inverse projection -> inverse modelview -> modelview of light
*/
FloatBuffer cameraP_inverse = FloatBuffer.allocate(16);
FloatBuffer cameraPMV_inverse = FloatBuffer.allocate(16);
ProjectFloat p = new ProjectFloat();
p.gluInvertMatrixf(cameraPMV.glGetPMatrixf(), cameraP_inverse);
FloatUtil.multMatrixf(cameraPMV.glGetMviMatrixf(), cameraP_inverse, cameraPMV_inverse);
FloatBuffer NDC2light = FloatBuffer.allocate(16);
FloatUtil.multMatrixf(lightPMV.glGetMvMatrixf(), cameraPMV_inverse, NDC2light);
/*
* transform screen space bounding box to light space
* and calculate axis aligned bounding box
*/
ArrayList<VectorXYZ> corners = new ArrayList<VectorXYZ>();
for (int x = -1; x<=1; x+=2) {
for (int y = -1; y<=1; y+=2) {
for (int z = -1; z<=1; z+=2) {
float[] NDCcorner = {x, y, z, 1};
float[] result = new float[4];
FloatUtil.multMatrixVecf(NDC2light, NDCcorner, result);
corners.add(new VectorXYZ(result[0]/result[3], result[1]/result[3], -result[2]/result[3]));
}
}
}
AxisAlignedBoundingBoxXYZ frustum = new AxisAlignedBoundingBoxXYZ(corners).pad(cameraFrustumPadding);
return frustum;
}
/**
* {@inheritDoc}
* Only primitives that support shadow will get rendered. For opaque objects see {@link #setRenderOpaque(boolean)}
*/
@Override
public boolean setMaterial(Material material, JOGLTextureManager textureManager) {
if (!renderOpaque && material.getTransparency() == Transparency.FALSE) {
return false;
}
if (material.getShadow() == Shadow.FALSE) {
return false;
}
return super.setMaterial(material, textureManager);
}
/**
* Sets whether to render opaque objects or not. Useful if the shadow map is only needed for transparent objects.
*/
public void setRenderOpaque(boolean renderOpaque) {
this.renderOpaque = renderOpaque;
}
/**
* Returns the PMVMatrix that was used to render the shadow map.
* Should be called at least after {@link #useShader()}
*/
public PMVMatrix getPMVMatrix() {
return pmvMat;
}
/**
* Returns the handle of the texture containing the rendered shadow map.
*/
public int getShadowMapHandle() {
return depthBufferHandle;
}
/**
* Prepares rendering of the shadow map. This changes the current framebuffer and viewport.
* {@link #disableShader()} should be called after the rendering is complete to bind the default framebuffer again
* and restore the original viewport.
*/
@Override
public void useShader() {
super.useShader();
prepareShadowMapGeneration();
}
/**
* Completes the rendering of the shadow map. The default framebuffer and viewport get restored.
*/
@Override
public void disableShader() {
// bind default framebuffer
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);
// reset viewport
gl.glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
super.disableShader();
}
}