package ch.ethz.karto.map3d; import com.jogamp.common.nio.Buffers; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; import javax.media.opengl.GL; import javax.media.opengl.GL2; /** * http://www.java-tips.org/other-api-tips/jogl/vertex-buffer-objects-nehe-tutorial-jogl-port-2.html * @author jenny */ public class Map3DModelVBOShader extends Map3DModel { protected String fragmentShaderPath = "/ch/ethz/karto/map3d/vbo.frag"; protected String vertexShaderPath = "/ch/ethz/karto/map3d/vbo.vert"; /** * number of textures used in fragment shader */ private static final int FRAG_SHADER_TEXTURE_COUNT = 3; /** * vertex buffer names */ private int vertexBuffer[] = null; /** * index buffer name */ private int indexBuffer[] = null; /** * height of a patch in rows. */ private int patchHeight; /** * vertex and fragment shader. */ private Map3DShader shaders; /** * A VBO buffer should be this large (but will be a bit smaller in reality) * In mega bytes. */ private static final int VBO_SIZE_MB = 5; /** * Floating point texture that holds the height values. */ private Map3DTextureFloat zTexture; private Map3DTexture texture; private static final int VERTEX_ATTRIB_ID = 0; private static final String VERTEX_ATTRIB_NAME = "v_vertex"; private Map3DTextureByte nonLinearHypsoTexture; private float shearX = 0; private float shearY = 0; private float shearBaseline = 1f; protected Map3DModelVBOShader() { } @Override public void setModel(float grid[][], float cellSize, Map3DTexture1DMapper t) { super.setModel(grid, cellSize, t); // compute patch height such that a single vertex buffer is about VBO_SIZE_MB // large. The index buffer will be about twice that large. int vboSize = 1024 * 1024 * VBO_SIZE_MB; // VBO target size in bytes patchHeight = vboSize / 2 // number of vertex coordinates per vertex / Buffers.SIZEOF_SHORT // number of bytes per vertex coordinate / getCols(); // number of vertices per row if (patchHeight < 2) { throw new IllegalArgumentException("Grid has too many columns"); } if (patchHeight > Map3DTexture.getMaxTextureSize()) { patchHeight = Map3DTexture.getMaxTextureSize(); } if (patchHeight > getRows()) { patchHeight = getRows(); } } @Override public void loadModel(GL gl1, Map3DTexture texture) { GL2 gl = (GL2)gl1; if (modelInitialized || grid == null || !canDisplay(gl, grid)) { return; } this.texture = texture; // first release old buffers to free memory on the GPU releaseModel(gl); gl.glTranslatef(0, 0, ZOFFSET); // load height texture zTexture = new Map3DTextureFloat(gl, gridTexture(), getCols(), getRows()); if (useNonHeightProportional1DTexture()) { ByteBuffer buf = nonLinearHypsoTextureBuffer(); nonLinearHypsoTexture = new Map3DTextureByte(gl, buf, getCols(), getRows()); } else { nonLinearHypsoTexture = null; } // load buffers and shader programs loadVertexBuffer(gl); loadIndexBuffer(gl); loadShaderProgram(gl); this.modelInitialized = true; } @Override public void releaseModel(GL gl) { if (vertexBuffer != null) { gl.glDeleteBuffers(vertexBuffer.length, vertexBuffer, 0); vertexBuffer = null; } if (indexBuffer != null) { gl.glDeleteBuffers(indexBuffer.length, indexBuffer, 0); indexBuffer = null; } releaseShaderProgram(gl); if (zTexture != null) { zTexture.release(gl); zTexture = null; } if (nonLinearHypsoTexture != null) { nonLinearHypsoTexture.release(gl); nonLinearHypsoTexture = null; } this.modelInitialized = false; } protected void releaseShaderProgram(GL gl) { if (shaders != null) { shaders.disableShaders(gl); shaders = null; } } @Override public boolean canRun() { // number of textures accessible in vertex shader int vTex = Map3DGLCapabilities.getMaxVertexTextureImageUnits(); // number of textures accessible in fragment shader int fTex = Map3DGLCapabilities.getMaxTextureImageUnits(); // OpenGL 2.0 is required for shader programs and glVertexAttribPointer return Map3DGLCapabilities.hasOpenGLVersion(2, 0) // GL_ARB_texture_float extension is required for float textures && Map3DGLCapabilities.hasFloatingPointTextures() // height values are read from texture in vertex shader && vTex >= 1 // three textures (1x1D and 2x2D) are declared in fragment shader && fTex >= FRAG_SHADER_TEXTURE_COUNT; } @Override public boolean canDisplay(GL gl, float[][] grid) { if (grid == null || grid.length < 2 || grid[0].length < 2) { return false; } // width of grid must be smaller than max texture size, as heights are // stored in a texture. if (grid[0].length > Map3DTexture.getMaxTextureSize()) { return false; } return true; } @Override public void draw(GL gl1, boolean shading, boolean fog) { if (grid == null) { return; } GL2 gl = (GL2)gl1; try { // set the vertex coordinates to use gl.glEnableVertexAttribArray(VERTEX_ATTRIB_ID); // set active texture unit to unit 0, bind it, and make it active // this is the texture with height values gl.glActiveTexture(GL2.GL_TEXTURE0); gl.glBindTexture(GL2.GL_TEXTURE_2D, zTexture.getTextureName()); // enable optional texture on texture unit 1 int textureType = 0; // 0: no texture; 1: 1D texture; 2: 2D texture if (texture.hasTexture()) { gl.glActiveTexture(GL2.GL_TEXTURE1); gl.glBindTexture(texture.getTexType(), texture.getTextureName()); textureType = texture.is1D() ? 1 : 2; } // enable optional nonLinearHypsoTexture on unit 2 if (useNonHeightProportional1DTexture()) { gl.glActiveTexture(GL2.GL_TEXTURE2); gl.glBindTexture(GL2.GL_TEXTURE_2D, nonLinearHypsoTexture.getTextureName()); textureType = -1; } shaders.setUniform(gl, "textureType", textureType); // enable optional fog shaders.setUniform(gl, "applyFog", fog); shaders.setUniform(gl, "applyShading", shading); // shearing shaders.setUniform(gl, "shearXY", this.shearX, this.shearY); shaders.setUniform(gl, "shearBaseline", this.shearBaseline); // the index and vertex buffers only need to be bound once gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer[0]); gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, vertexBuffer[0]); // use buffer with xy-vertex positions gl.glVertexAttribPointer(0, 2, GL2.GL_UNSIGNED_SHORT, false, 0, 0); int n = nPatches(); shaders.setUniform(gl, "rowOffset", 0f); // better use glTranslatef to shift vertices with fixed function vertex buffer? // FIXME assert (shaders.validateProgram(gl)); for (int i = 0; i < n - 1; i++) { gl.glDrawElements(GL2.GL_TRIANGLE_STRIP, indexBufferSize(), GL2.GL_UNSIGNED_INT, 0); shaders.setUniform(gl, "rowOffset", (float) (i + 1) * (patchHeight - 1)); } // last patch is possibly smaller, only draw part of it gl.glDrawElements(GL2.GL_TRIANGLE_STRIP, lastIndexBufferSize(), GL2.GL_UNSIGNED_INT, 0); } finally { gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0); gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0); gl.glDisableVertexAttribArray(0); } } /** * Call textureChanged() after the texture for this terrain model has changed. */ @Override public void textureChanged() { this.modelInitialized = false; // FIXME // should be changed such that the geometry is not reloaded when the texture changes. } private boolean useNonHeightProportional1DTexture() { boolean use1DTextureBuffer = texture != null && texture1DMapper != null && texture.hasTexture() && texture.is1D() && !texture1DMapper.isLinearHeightMapping(); return use1DTextureBuffer; } private void loadVertexBuffer(GL gl) { if (getCols() > Short.MAX_VALUE || patchHeight > Short.MAX_VALUE) { throw new IllegalStateException("grid too large for rendering"); } // create new vertex and normal buffers final int vertexCount = getCols() * patchHeight; ShortBuffer vBuf = Buffers.newDirectShortBuffer(vertexCount * 2); // create buffer name vertexBuffer = new int[1]; gl.glGenBuffers(1, vertexBuffer, 0); // fill the buffers final int cols = this.grid[0].length; for (short r = 0; r < patchHeight; r++) { for (short c = 0; c < cols; ++c) { vBuf.put(c); vBuf.put(r); } } vBuf.rewind(); int nbytes = patchHeight * getCols() * 2 * Buffers.SIZEOF_SHORT; gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, vertexBuffer[0]); gl.glBufferData(GL2.GL_ARRAY_BUFFER, nbytes, vBuf, GL2.GL_STATIC_DRAW); } /** * allocates the index buffer and loads it to the GPU. * An integer index buffer must be used for large grids - and not shorts - * otherwise rendering becomes extremely slow, because shorts are not * aligned to 4 bytes boundaries. * The index buffer contains two indices per grid value for triangle strips * (except for the last row) and one index for each row (except for * the topmost and the bottomost row). * @param gl */ private void loadIndexBuffer(GL gl) { int cols = getCols(); // create index buffer IntBuffer indexBuf = Buffers.newDirectIntBuffer(indexBufferSize()); for (int y = 0; y < patchHeight - 1; y += 2) { // even row from left to right for (int x = 0; x < cols; x++) { final int i = x + y * cols; indexBuf.put(i); indexBuf.put(i + cols); } // stop if this was the last row if (y + 1 >= patchHeight - 1) { break; } // add a degenerate triangle // http://www.gamedev.net/community/forums/topic.asp?topic_id=227553&whichpage=1� indexBuf.put((cols - 1) + y * cols + cols); // odd rows from right to left for (int x = cols - 1; x >= 0; x--) { final int i = x + (y + 1) * cols; indexBuf.put(i); indexBuf.put(i + cols); } // add a degenerate triangle if this is not the last row if (y + 2 < patchHeight - 1) { indexBuf.put((y + 1) * cols + cols); } } // create name for index buffer and load it to the GPU indexBuffer = new int[1]; gl.glGenBuffers(1, indexBuffer, 0); indexBuf.rewind(); gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer[0]); int bufBytes = indexBuf.capacity() * Buffers.SIZEOF_INT; gl.glBufferData(GL2.GL_ELEMENT_ARRAY_BUFFER, bufBytes, indexBuf, GL2.GL_STATIC_DRAW); } protected void loadShaderProgram(GL gl) { if (grid == null) { return; } shaders = new Map3DShader(); shaders.enableShaders(gl, vertexShaderPath, fragmentShaderPath, new String[]{VERTEX_ATTRIB_NAME}, new int[]{VERTEX_ATTRIB_ID}); int cols = getCols(); int rows = getRows(); float hTextureScale = cols >= rows ? 1 : (float) rows / cols; float vTextureScale = rows >= cols ? 1 : (float) cols / rows; shaders.setUniform(gl, "textureScale", hTextureScale, vTextureScale); // scale factor for converting between columns/rows to unity box float scaleGridToUnity = 1.0f / (Math.max(cols, rows) - 1); shaders.setUniform(gl, "scaleGridToUnity", scaleGridToUnity); float scaleZToUnity = 1f / ((maxValue - minValue) / cellSize); shaders.setUniform(gl, "scaleZToUnity", scaleZToUnity); // texture unit 0 is for height values shaders.setUniform(gl, "zTexture", 0); // texture unit 1 is the optional 2D image draped onto the terrain or // the optional 1D hypsometric color range // The third texture uniform must also be set (as uniforms are declared // in the shaders) but is not used. if (texture.is2D) { shaders.setUniform(gl, "imageTexture", 1); shaders.setUniform(gl, "hypsoLookUpTexture", 2); shaders.setUniform(gl, "hypsoTexture", 3); // not used } else { shaders.setUniform(gl, "hypsoTexture", 1); shaders.setUniform(gl, "hypsoLookUpTexture", 2); shaders.setUniform(gl, "imageTexture", 3); // not used } // shearing shaders.setUniform(gl, "shearXY", this.shearX, this.shearY); shaders.setUniform(gl, "shearBaseline", this.shearBaseline); } /** * Returns a buffer with all grid values, scaled to columns/rows grid units. * The minimum value of the grid is subtracted from values in the returned * grid, i.e. the smallest value is 0. * @return */ private FloatBuffer gridTexture() { int cols = getCols(); int rows = getRows(); FloatBuffer buffer = Buffers.newDirectFloatBuffer(cols * rows); for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { // FIXME no shifting to avoid inconsistent results with plan oblique rendering buffer.put((grid[r][c] /*- minValue*/) / cellSize); } } buffer.rewind(); return buffer; } private ByteBuffer nonLinearHypsoTextureBuffer() { int cols = getCols(); int rows = getRows(); ByteBuffer buffer = Buffers.newDirectByteBuffer(cols * rows); for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { int i = (int)(texture1DMapper.get1DTextureCoordinate(c, r) * 255); buffer.put((byte)(i)); } } buffer.rewind(); return buffer; } /** * Returns the number of patches, including the last one, which may by * smaller than the others. * @return The number of patches. */ private int nPatches() { int n = (getRows() - 1) / (patchHeight - 1); return n + ((getRows() - 1) % (patchHeight - 1) > 0 ? 1 : 0); } /** * Returns the number of entire patches. This number includes the last patch * if it is as large as the others. * @return The number of entire patches. */ private int nUnbrokenPatches() { return (getRows() - 1) / (patchHeight - 1); } /** * Returns whether the last patch is smaller than a normal patch. * @return True if the last patch has less rows than a normal patch. */ private boolean hasBrokenPatch() { return nPatches() > nUnbrokenPatches(); } /** * Returns the height of the last patch, which can be equal to patchHeight * or less. * @return The number of rows of the last patch. */ private int lastPatchHeight() { if (hasBrokenPatch()) { return getRows() - nUnbrokenPatches() * (patchHeight - 1); } else { return patchHeight; } } /** * Returns the number of indices used to construct a triangle strip from the * regular grids of vertices, normals and texture coordinates. * @return The number of indices in the indexBuf. */ private int indexBufferSize() { return getCols() * 2 * (patchHeight - 1) + patchHeight - 2; } /** * Returns the number of indices used to construct a triangle strip for the * last patch from the regular grids of vertices, normals and texture coordinates. * @return The number of indices for the last patch. */ private int lastIndexBufferSize() { int h = lastPatchHeight(); return getCols() * 2 * (h - 1) + h - 2; } public void setShearing(float shearX, float shearY) { this.shearX = shearX; this.shearY = shearY; } public void setShearBaseline(float z){ this.shearBaseline = z; } }