package org.osm2world.core.target.jogl;
import static javax.media.opengl.GL.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT;
import static javax.media.opengl.GL.GL_REPEAT;
import static javax.media.opengl.GL.GL_TEXTURE_2D;
import static javax.media.opengl.GL.GL_TEXTURE_MAX_ANISOTROPY_EXT;
import static javax.media.opengl.GL.GL_TEXTURE_WRAP_S;
import static javax.media.opengl.GL.GL_TEXTURE_WRAP_T;
import static javax.media.opengl.GL2GL3.GL_CLAMP_TO_BORDER;
import static javax.media.opengl.GL2GL3.GL_TEXTURE_BORDER_COLOR;
import static org.osm2world.core.target.jogl.AbstractJOGLTarget.getFloatBuffer;
import java.awt.Color;
import java.nio.FloatBuffer;
import javax.media.opengl.GL;
import javax.media.opengl.GL3;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.target.common.TextureData;
import org.osm2world.core.target.common.TextureData.Wrap;
import org.osm2world.core.target.common.lighting.GlobalLightingParameters;
import org.osm2world.core.target.common.material.Material;
import org.osm2world.core.target.common.material.Material.Transparency;
import com.jogamp.opengl.math.FloatUtil;
import com.jogamp.opengl.util.PMVMatrix;
import com.jogamp.opengl.util.texture.Texture;
/**
* Complex shader with support for complex materials and various graphic effects:
* <ul>
* <li> Shadow Volumes
* <li> Shadow Maps
* <li> Screen Space Ambient Occlusion
* <li> Phong shading
* <li> Bumpmaps / Normalmaps
* </ul>
*/
public class DefaultShader extends AbstractPrimitiveShader {
/** maximum number of texture layers any material can use */
public static final int MAX_TEXTURE_LAYERS = 4;
/** globally controls anisotropic filtering for all textures */
public static final boolean ANISOTROPIC_FILTERING = true;
/**
* Parameters for SSAO
*/
private int kernelSize = 16;
private float[] kernel;
private float ssaoRadius = 1;
private int noiseTextureHandle;
private static final int NOISE_TEXTURE_WIDTH=4;
private static final int NOISE_TEXTURE_HEIGHT=4;
/**
* Threshold for the dot product of a SSAO kernel sample and the vertex
* normal. this prevents samples from being too near to the vertex plane and
* so prevents self shadowing
*/
private static final double SSAO_SAMPLE_THRESHOLD = 0.15;
private int projectionMatrixID;
private int modelViewMatrixID;
private int modelViewProjectionMatrixID;
private int normalMatrixID;
private int shadowMatrixID;
private int vertexPositionID;
private int vertexNormalID;
private int[] vertexTexCoordID = new int[MAX_TEXTURE_LAYERS];
private int vertexBumpMapCoordID;
private int vertexTangentID;
public DefaultShader(GL3 gl) {
super(gl, "/shaders/default");
// get indices of named attributes
vertexPositionID = gl.glGetAttribLocation(shaderProgram, "VertexPosition");
vertexNormalID = gl.glGetAttribLocation(shaderProgram, "VertexNormal");
for (int i=0; i<MAX_TEXTURE_LAYERS; i++)
vertexTexCoordID[i] = gl.glGetAttribLocation(shaderProgram, "VertexTexCoord"+i+"");
vertexBumpMapCoordID = gl.glGetAttribLocation(shaderProgram, "VertexBumpMapCoord");
vertexTangentID = gl.glGetAttribLocation(shaderProgram, "VertexTangent");
// get indices of uniform variables
projectionMatrixID = gl.glGetUniformLocation(shaderProgram, "ProjectionMatrix");
modelViewMatrixID = gl.glGetUniformLocation(shaderProgram, "ModelViewMatrix");
modelViewProjectionMatrixID = gl.glGetUniformLocation(shaderProgram, "ModelViewProjectionMatrix");
normalMatrixID = gl.glGetUniformLocation(shaderProgram, "NormalMatrix");
shadowMatrixID = gl.glGetUniformLocation(shaderProgram, "ShadowMatrix");
generateSamplingMatrix();
generateNoiseTexture();
this.prepareValidation();
this.validateShader();
}
/**
* Executes necessary preparation steps for shader validation.
* Sets default texture units for samplers. This is needed if there are
* different sampler types, as they need to point to different texture units.
*/
private void prepareValidation() {
this.useShader();
// set default texture units
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "ShadowMap"), 0);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "DepthMap"), 1);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "NoiseTex"), 2);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "BumpMap"), getGLTextureNumber(0));
for (int i=0; i<MAX_TEXTURE_LAYERS; i++)
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "Tex["+i+"]"), getGLTextureNumber(i));
this.disableShader();
}
@Override
public void loadDefaults() {
renderSemiTransparent = true;
renderOnlySemiTransparent = false;
// set default material values
gl.glUniform3f(gl.glGetUniformLocation(shaderProgram, "Material.Color"), 0, 0, 0);
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "Material.Ka"), 0);
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "Material.Kd"), 0);
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "Material.Ks"), 0);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "Material.Shininess"), 0);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "isShadowed"), 0);
// reset optional parts
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useShadowMap"), 0);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useSSAO"), 0);
}
/**
* Send uniform matrices "ProjectionMatrix, ModelViewMatrix and ModelViewProjectionMatrix" to vertex shader
* @param pmvMatrix
*/
public void setPMVMatrix(PMVMatrix pmvMatrix) {
gl.glUniformMatrix4fv(this.getProjectionMatrixID(), 1, false, pmvMatrix.glGetPMatrixf());
gl.glUniformMatrix4fv(this.getModelViewMatrixID(), 1, false, pmvMatrix.glGetMvMatrixf());
FloatBuffer pmvMat = FloatBuffer.allocate(16);
FloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);
gl.glUniformMatrix4fv(this.getModelViewProjectionMatrixID(), 1, false, pmvMat);
gl.glUniformMatrix4fv(this.getNormalMatrixID(), 1, false, pmvMatrix.glGetMvitMatrixf());
}
/**
* Prepares the shader to do lighting.
* @param lighting the global lighting to apply. Can be <code>null</code> to disable lighting.
*/
public void setGlobalLighting(GlobalLightingParameters lighting) {
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useLighting"), lighting != null ? 1 : 0);
if (lighting != null) {
gl.glUniform4f(gl.glGetUniformLocation(shaderProgram, "Light.Position"), (float)lighting.lightFromDirection.getX(),
(float)lighting.lightFromDirection.getY(), -(float)lighting.lightFromDirection.getZ(), 0f);
gl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, "Light.La"), 1, getFloatBuffer(lighting.globalAmbientColor));
gl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, "Light.Ld"), 1, getFloatBuffer(lighting.lightColorDiffuse));
gl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, "Light.Ls"), 1, getFloatBuffer(lighting.lightColorSpecular));
}
}
/**
* Sets whether to Render everything as in shadow.
*/
public void setShadowed(boolean isShadowed) {
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "isShadowed"), isShadowed ? 1 : 0);
}
@Override
public boolean setMaterial(Material material, JOGLTextureManager textureManager) {
if (!super.setMaterial(material, textureManager))
return false;
int numTexLayers = 0;
if (material.getTextureDataList() != null) {
numTexLayers = material.getTextureDataList().size();
}
/* set color / lighting */
if (numTexLayers == 0 || material.getTextureDataList().get(0).colorable) {
gl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, "Material.Color"), 1, getFloatBuffer(material.getColor()));
} else {
gl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, "Material.Color"), 1, getFloatBuffer(Color.WHITE));
}
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "Material.Ka"), material.getAmbientFactor());
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "Material.Kd"), material.getDiffuseFactor());
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "Material.Ks"), material.getSpecularFactor());
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "Material.Shininess"), material.getShininess());
/* set textures and associated parameters */
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useBumpMaps"), material.hasBumpMap() ? 1 : 0);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useAlphaTreshold"), material.getTransparency() == Transparency.BINARY ? 1 : 0);
if (material.getTransparency() == Transparency.FALSE) {
gl.glDisable(GL.GL_BLEND);
} else {
gl.glEnable(GL.GL_BLEND);
/* GL.GL_SRC_ALPHA and GL.GL_ONE_MINUS_SRC_ALPHA for color blending produces correct results for color, while
* GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA produces correct alpha blended results: the blendfunction is in fact equal to 1-(1-SRC_APLHA)*(1-DST_APLHA)
*/
gl.glBlendFuncSeparate(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA, GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);
if (material.getTransparency() == Transparency.BINARY) {
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "alphaTreshold"), 0.5f );
}
}
for (int i = 0; i < MAX_TEXTURE_LAYERS; i++) {
if (i < numTexLayers) {
gl.glActiveTexture(getGLTextureConstant(i));
TextureData textureData = material.getTextureDataList().get(i);
Texture texture = textureManager.getTextureForFile(textureData.file);
texture.bind(gl);
if (textureData.isBumpMap) {
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useTexture["+i+"]"), 0);
} else {
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useTexture["+i+"]"), 1);
/* enable anisotropic filtering (note: this could be a
* per-texture setting, but currently isn't) */
if (gl.isExtensionAvailable("GL_EXT_texture_filter_anisotropic")) {
if (ANISOTROPIC_FILTERING) {
float max[] = new float[1];
gl.glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, max, 0);
gl.glTexParameterf(GL_TEXTURE_2D,
GL_TEXTURE_MAX_ANISOTROPY_EXT,
max[0]);
} else {
gl.glTexParameterf(GL_TEXTURE_2D,
GL_TEXTURE_MAX_ANISOTROPY_EXT,
1.0f);
}
}
}
/* wrapping behavior */
int wrap = 0;
switch (textureData.wrap) {
case CLAMP: System.out.println("Warning: CLAMP is no longer supported. Using CLAMP_TO_BORDER instead."); wrap = GL_CLAMP_TO_BORDER; break;
case REPEAT: wrap = GL_REPEAT; break;
case CLAMP_TO_BORDER: wrap = GL_CLAMP_TO_BORDER; break;
}
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap);
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap);
if (textureData.wrap == Wrap.CLAMP_TO_BORDER) {
/* TODO: make the RGB configurable - for some reason,
* it shows up in lowzoom even if fully transparent */
gl.glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR,
getFloatBuffer(new Color(1f, 1f, 1f, 0f)));
}
int loc;
if (textureData.isBumpMap) {
loc = gl.glGetUniformLocation(shaderProgram, "BumpMap");
if (loc < 0) {
//throw new RuntimeException("BumpMap not found in shader program.");
}
} else {
loc = gl.glGetUniformLocation(shaderProgram, "Tex["+i+"]");
if (loc < 0) {
//throw new RuntimeException("Tex["+i+"] not found in shader program.");
}
}
gl.glUniform1i(loc, getGLTextureNumber(i));
} else {
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useTexture["+i+"]"), 0);
}
}
return true;
}
static final int getGLTextureConstant(int textureNumber) {
switch (getGLTextureNumber(textureNumber)) {
//case 0: return GL.GL_TEXTURE0; // reserved for shadow map
//case 1: return GL.GL_TEXTURE1; // reserved for ssao depth map
//case 2: return GL.GL_TEXTURE2; // reserved for ssao noise texture
case 3: return GL.GL_TEXTURE3;
case 4: return GL.GL_TEXTURE4;
case 5: return GL.GL_TEXTURE5;
case 6: return GL.GL_TEXTURE6;
default: throw new Error("programming error: unhandled texture number");
}
}
static final int getGLTextureNumber(int textureNumber) {
return textureNumber + 3; // texture id 0-2 are reserved for shadow map, ssao depth map and ssao noise texture
}
/**
* Binds the specified texture as shadow map and uses it for rendering shadows.
*/
public void bindShadowMap(int shadowMapHandle) {
gl.glActiveTexture(GL.GL_TEXTURE0);
gl.glBindTexture(GL.GL_TEXTURE_2D, shadowMapHandle);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "ShadowMap"), 0);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useShadowMap"), 1);
}
/**
* Creates random sampling kernel data for SSAO.
*/
private void generateSamplingMatrix() {
kernel = new float[kernelSize*3];
for (int i = 0; i < kernelSize; ++i) {
float scale = (float)i / (float)kernelSize;
scale = lerp(0.2f, 1.0f, scale*scale);
VectorXYZ v = new VectorXYZ(
Math.random()*2-1,
Math.random()*2-1,
Math.random()
).normalize().mult(scale); //.mult(Math.random()*scale);
while (v.dot(new VectorXYZ(0, 0, 1)) < SSAO_SAMPLE_THRESHOLD) {
v = new VectorXYZ(
Math.random()*2-1,
Math.random()*2-1,
Math.random()
).normalize().mult(scale);
}
kernel[i*3] = (float)v.x;
kernel[i*3+1] = (float)v.y;
kernel[i*3+2] = (float)v.z;
}
}
/**
* Generates and binds the noise texture used for SSAO.
*/
private void generateNoiseTexture() {
float[] noise = new float[NOISE_TEXTURE_WIDTH*NOISE_TEXTURE_HEIGHT*3];
for (int i = 0; i < NOISE_TEXTURE_WIDTH*NOISE_TEXTURE_HEIGHT; i++)
{
noise[i*3] = (float)Math.random()*2-1;
noise[i*3+1] = (float)Math.random()*2-1;
noise[i*3+2] = 0;
}
int[] noiseTexture = new int[1];
gl.glGenTextures(1, noiseTexture, 0);
noiseTextureHandle = noiseTexture[0];
gl.glBindTexture(GL_TEXTURE_2D, noiseTextureHandle);
gl.glTexImage2D(GL_TEXTURE_2D, 0, GL.GL_RGB16F, NOISE_TEXTURE_WIDTH, NOISE_TEXTURE_HEIGHT, 0, GL.GL_RGB, GL.GL_FLOAT, FloatBuffer.wrap(noise));
gl.glTexParameteri(GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}
private float lerp(float a, float b, float f) {
return a + f * (b - a);
}
/**
* Binds a texture as depth map and uses it for SSAO. Also sets up the rest of the SSAO settings.
*/
public void enableSSAOwithDepthMap(int depthMapHandle) {
// bind depth map
gl.glActiveTexture(GL.GL_TEXTURE1);
gl.glBindTexture(GL.GL_TEXTURE_2D, depthMapHandle);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "DepthMap"), 1);
// send SSAO parameters
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "useSSAO"), 1);
int[] viewport = new int[4];
gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
int width = viewport[2], height = viewport[3];
gl.glUniform2f(gl.glGetUniformLocation(shaderProgram, "uNoiseScale"), (float)width/(float)NOISE_TEXTURE_WIDTH, (float)height/(float)NOISE_TEXTURE_HEIGHT);
// TODO: may be enough to only send once when initializing?
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "uKernelSize"), kernelSize);
gl.glUniform3fv(gl.glGetUniformLocation(shaderProgram, "uKernelOffsets"), kernelSize, kernel, 0);
gl.glUniform1f(gl.glGetUniformLocation(shaderProgram, "uRadius"), ssaoRadius);
gl.glActiveTexture(GL.GL_TEXTURE2);
gl.glBindTexture(GL.GL_TEXTURE_2D, noiseTextureHandle);
gl.glUniform1i(gl.glGetUniformLocation(shaderProgram, "NoiseTex"), 2);
}
/**
* Change the size of the SSAO kernel and recomputes it. If the size already
* matches nothing is done. This should be called before
* {@link #enableSSAOwithDepthMap(int)} to have any effect.
*/
public void setSSAOkernelSize(int kernelSize) {
if (this.kernelSize != kernelSize) {
this.kernelSize = kernelSize;
generateSamplingMatrix();
}
}
/**
* Change the radius used for SSAO. This should be called before
* {@link #enableSSAOwithDepthMap(int)} to have any effect.
*/
public void setSSAOradius(float radius) {
this.ssaoRadius = radius;
}
@Override
public int getVertexPositionID() {
return vertexPositionID;
}
@Override
public int getVertexNormalID() {
return vertexNormalID;
}
@Override
public int getVertexTexCoordID(int i) {
return vertexTexCoordID[i];
}
@Override
public int getVertexBumpMapCoordID() {
return vertexBumpMapCoordID;
}
public int getProjectionMatrixID() {
return projectionMatrixID;
}
public int getModelViewMatrixID() {
return modelViewMatrixID;
}
public int getModelViewProjectionMatrixID() {
return modelViewProjectionMatrixID;
}
public int getNormalMatrixID() {
return normalMatrixID;
}
public int getShadowMatrixID() {
return shadowMatrixID;
}
@Override
public int getVertexTangentID() {
return vertexTangentID;
}
/**
* Sets the PMVMatrix that was used to render the shadow map set with {@link #bindShadowMap(int)}.
* This is needed to correctly compare the shadow map depth values with the fragment depth value.
* @param pmvMatrix
*/
public void setShadowMatrix(PMVMatrix pmvMatrix) {
// S = B*MPV
// bias matrix
float[] b = {0.5f, 0, 0, 0,
0, 0.5f, 0, 0,
0, 0, 0.5f, 0f,
0.5f, 0.5f, 0.5f, 1.0f};
FloatBuffer bb = FloatBuffer.wrap(b);
// PMV of light source
FloatBuffer pmvMat = FloatBuffer.allocate(16);
FloatUtil.multMatrixf(pmvMatrix.glGetPMatrixf(), pmvMatrix.glGetMvMatrixf(), pmvMat);
FloatBuffer shadowMat = FloatBuffer.allocate(16);
FloatUtil.multMatrixf(bb, pmvMat, shadowMat);
gl.glUniformMatrix4fv(this.getShadowMatrixID(), 1, false, shadowMat);
}
}