package ch.ethz.karto.map3d;
import com.jogamp.common.nio.Buffers;
import java.nio.Buffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
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
* Use ARB VBO functions to support drivers with OpenGL older than 1.5 with the
* VBO extension. Old Intel hardware claims to support VBO, but only supports the ARB
* variants ( http://www.gamedev.net/community/forums/topic.asp?topic_id=501651 ).
* @author jenny
*/
public class Map3DModelVBO extends Map3DModel {
/**
* vertex buffer names
*/
private int vertexBufs[] = null;
/**
* normal buffer names
*/
private int normalBufs[] = null;
/**
* texture buffer names for non-linear one-dimensional height texture
*/
private int textureBufs[] = null;
/**
* index buffer name
*/
private int indexBuffer[] = null;
/**
* height of a patch in rows.
*/
private int patchHeight;
private Map3DTexture texture;
protected Map3DModelVBO() {
}
@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 5MB
// large. The index buffer will be about twice that large.
int vboSize = 1024 * 1024 * 5; // VBO target size is 5MB
patchHeight = vboSize / 3 // 3 values in vertex buffer
/ 4 // bytes per vertex
/ getCols();
if (patchHeight < 2) {
throw new IllegalArgumentException("Grid has too many columns");
}
if (patchHeight > getRows()) {
patchHeight = getRows();
}
}
/**
* Copies one row of the grid to the vertex, normal and texture buffer.
* @param verticesBuffer The buffer for the vertices.
* @param normalsBuffer The buffer for the normals.
* @param textureBuffer The buffer for texture coordinates. Can be null.
* @param row The row to copy.
*/
private void copyRowToBuffer(FloatBuffer verticesBuffer,
FloatBuffer normalsBuffer, FloatBuffer textureBuffer,
int row) {
if (this.grid == null) {
return;
}
final int rows = this.grid.length;
final int cols = this.grid[0].length;
final int cols_1 = cols - 1;
final float s = 1.0f / (Math.max(cols, rows) - 1);
final float zScale = 1.0f / this.cellSize;
final int r1 = row;
final int r2 = Math.min(r1 + 1, rows - 1);
float[] row1 = this.grid[r1];
float[] row2 = this.grid[r2];
boolean use1DTextureBuffer = texture.hasTexture()
&& texture.is1D()
&& !texture1DMapper.isLinearHeightMapping()
&& textureBuffer != null;
for (int c0 = 0; c0 < cols; ++c0) {
final int c1 = Math.min(c0 + 1, cols_1);
final float v10 = zScale * (row1[c0] - this.minValue);
final float v20 = zScale * (row2[c0] - this.minValue);
final float v11 = zScale * (row1[c1] - this.minValue);
// compute normal with the point to the right and the point below
final float nx = v10 - v11;
final float ny = v10 - v20;
final float len_inv = (float) (1. / Math.sqrt(nx * nx + ny * ny + 1.0f));
normalsBuffer.put(nx * len_inv);
normalsBuffer.put(ny * len_inv);
normalsBuffer.put(len_inv);
final float x = c0 * s;
final float y = r1 * s;
verticesBuffer.put(x);
verticesBuffer.put(y);
verticesBuffer.put(v10 * s);
if (use1DTextureBuffer) {
final float t = texture1DMapper.get1DTextureCoordinate(c0, r1);
textureBuffer.put(t);
}
}
}
private boolean use1DTextureBuffer() {
return texture.hasTexture()
&& texture.is1D()
&& !texture1DMapper.isLinearHeightMapping();
}
/**
* Loads the vertex, normals and texture buffers to the GPU.
* @param gl
* @param vBuf vertex buffer
* @param nBuf normals buffer
* @param tBuf one-dimensional textureBuffer
* @param nBytes
* @param bufID
*/
private void loadToGPU(GL gl, Buffer vBuf, Buffer nBuf, Buffer tBuf,
int nVertices, int bufID) {
int nbytes = nVertices * 3 * 4;
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, vertexBufs[bufID]);
gl.glBufferData(GL2.GL_ARRAY_BUFFER, nbytes, vBuf, GL2.GL_STATIC_DRAW);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, normalBufs[bufID]);
gl.glBufferData(GL2.GL_ARRAY_BUFFER, nbytes, nBuf, GL2.GL_STATIC_DRAW);
if (use1DTextureBuffer()) {
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, textureBufs[bufID]);
nbytes = nVertices * 4;
gl.glBufferData(GL2.GL_ARRAY_BUFFER, nbytes, tBuf, GL2.GL_STATIC_DRAW);
}
}
/**
* 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;
}
@Override
public void loadModel(GL gl1, Map3DTexture texture) {
if (modelInitialized || grid == null) {
return;
}
GL2 gl = (GL2)gl1;
this.texture = texture;
texture.updateEnabledState(gl);
gl.glTranslatef(0, 0, ZOFFSET);
// create new vertex and normal buffers
final int vertexCount = getCols() * patchHeight;
FloatBuffer vBuf = Buffers.newDirectFloatBuffer(vertexCount * 3);
FloatBuffer nBuf = Buffers.newDirectFloatBuffer(vertexCount * 3);
FloatBuffer tBuf = null;
if (use1DTextureBuffer()) {
tBuf = Buffers.newDirectFloatBuffer(vertexCount);
}
// release old buffers to free space on GPU
releaseModel(gl);
// create buffer names
final int nPatches = nPatches();
final int nUnbrokenPatches = nUnbrokenPatches();
vertexBufs = new int[nPatches];
normalBufs = new int[nPatches];
gl.glGenBuffers(nPatches, vertexBufs, 0);
gl.glGenBuffers(nPatches, normalBufs, 0);
if (use1DTextureBuffer()) {
textureBufs = new int[nPatches];
gl.glGenBuffers(nPatches, textureBufs, 0);
} else {
textureBufs = null;
}
// load entire patches to the GPU
for (int i = 0; i < nUnbrokenPatches; i++) {
// fill the vertex, normal, and texture buffers
int lastRow = (patchHeight - 1) * (i + 1);
int firstRow = (patchHeight - 1) * i;
loadBuffer(gl, vBuf, nBuf, tBuf, firstRow, lastRow, i);
}
// load last partial patch to the GPU
if (hasBrokenPatch()) {
int firstRow = (patchHeight - 1) * nUnbrokenPatches;
final int bufID = normalBufs.length - 1;
loadBuffer(gl, vBuf, nBuf, tBuf, firstRow, getRows() - 1, bufID);
}
buildIndexBuffer(gl);
this.modelInitialized = true;
}
private void loadBuffer(GL gl,
FloatBuffer vBuf, FloatBuffer nBuf, FloatBuffer tBuf,
int firstRow, int lastRow, int bufID) {
vBuf.rewind();
nBuf.rewind();
if (tBuf != null) {
tBuf.rewind();
}
// fill the vertex, normal, and texture buffers
for (int r = firstRow; r <= lastRow; r++) {
copyRowToBuffer(vBuf, nBuf, tBuf, r);
}
vBuf.rewind();
nBuf.rewind();
if (tBuf != null) {
tBuf.rewind();
}
int nVertices = (lastRow - firstRow + 1) * getCols();
loadToGPU(gl, vBuf, nBuf, tBuf, nVertices, bufID);
}
@Override
public void releaseModel(GL gl1) {
// release data buffered on the GPU
GL2 gl = (GL2)gl1;
if (vertexBufs != null) {
gl.glDeleteBuffers(vertexBufs.length, vertexBufs, 0);
}
if (normalBufs != null) {
gl.glDeleteBuffers(normalBufs.length, normalBufs, 0);
}
if (indexBuffer != null) {
gl.glDeleteBuffers(1, indexBuffer, 0);
}
if (textureBufs != null) {
gl.glDeleteBuffers(textureBufs.length, textureBufs, 0);
}
}
@Override
public boolean canRun() {
// VBO are available with OpenGL 1.5 or earlier with an extension
// glTexGen is deprecatd with OpenGL 3.0 and not available in 3.1
// use the ARB calls, non-ARB calls are not defined prior to 1.5 and
// are not supported on Intel hardware, such as Intel 965
return Map3DGLCapabilities.hasVBO()
&& !Map3DGLCapabilities.hasOpenGLVersion(3, 1);
}
/**
* Draws the buffer identified by index i. First binds, then draws the buffers.
* @param gl
* @param i The index of the buffer to draw.
* @param indexBufSize The size of the index buffer
*/
private void drawBuffer(GL gl1, int i, int indexBufSize) {
GL2 gl = (GL2)gl1;
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, vertexBufs[i]);
gl.glVertexPointer(3, GL2.GL_FLOAT, 0, 0);
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, normalBufs[i]);
gl.glNormalPointer(GL2.GL_FLOAT, 0, 0);
if (use1DTextureBuffer()) {
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, textureBufs[i]);
gl.glTexCoordPointer(1, GL2.GL_FLOAT, 0, 0);
}
gl.glDrawElements(GL2.GL_TRIANGLE_STRIP, indexBufSize, GL2.GL_UNSIGNED_INT, 0);
}
@Override
public void draw(GL gl1, boolean shading, boolean fog) {
if (grid == null) {
return;
}
GL2 gl = (GL2)gl1;
gl.glEnableClientState(GL2.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL2.GL_NORMAL_ARRAY);
if (use1DTextureBuffer()) {
gl.glEnableClientState(GL2.GL_TEXTURE_COORD_ARRAY);
}
try {
if (texture.hasTexture() && !use1DTextureBuffer()) {
autoGenerateTextureCoordinates(gl);
}
// the index buffer only needs to be bound once
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer[0]);
for (int i = 0; i < vertexBufs.length - 1; i++) {
drawBuffer(gl, i, indexBufferSize());
}
// last patch is possibly smaller, only draw part of it
drawBuffer(gl, vertexBufs.length - 1, lastIndexBufferSize());
} finally {
// do this in case the next time drawing is not done with VBOs
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, 0);
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, 0);
gl.glDisableClientState(GL2.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL2.GL_NORMAL_ARRAY);
gl.glDisableClientState(GL2.GL_TEXTURE_COORD_ARRAY);
// do this in case the next time none or another type of texture is used.
gl.glDisable(GL2.GL_TEXTURE_GEN_S);
gl.glDisable(GL2.GL_TEXTURE_GEN_T);
}
}
/**
* 2D textures are automatically derived from the vertex coordinates by
* glTexGen. Vertex coordinates need to be scaled.
* @param gl
*/
private void autoGenerateTextureCoordinates(GL gl1) {
int cols = getCols();
int rows = getRows();
GL2 gl = (GL2)gl1;
if (texture.getDim() == 1) {
// map z value of vertices to s coordinate for 1D texture
gl.glEnable(GL2.GL_TEXTURE_GEN_S);
float zScale = 1.f / ((maxValue - minValue) / cellSize / (Math.max(cols, rows) - 1));
gl.glTexGeni(GL2.GL_S, GL2.GL_TEXTURE_GEN_MODE, GL2.GL_OBJECT_LINEAR);
gl.glTexGenfv(GL2.GL_S, GL2.GL_OBJECT_PLANE, new float[]{0, 0, zScale, 0}, 0);
} else {
// map x and y of vertices to s and t coordinates of 2D texture
gl.glEnable(GL2.GL_TEXTURE_GEN_S);
gl.glEnable(GL2.GL_TEXTURE_GEN_T);
float xScale = cols >= rows ? 1 : (float) rows / cols;
float yScale = rows >= cols ? 1 : (float) cols / rows;
gl.glTexGeni(GL2.GL_S, GL2.GL_TEXTURE_GEN_MODE, GL2.GL_OBJECT_LINEAR);
gl.glTexGenfv(GL2.GL_S, GL2.GL_OBJECT_PLANE, new float[]{xScale, 0, 0, 0}, 0);
gl.glTexGeni(GL2.GL_T, GL2.GL_TEXTURE_GEN_MODE, GL2.GL_OBJECT_LINEAR);
gl.glTexGenfv(GL2.GL_T, GL2.GL_OBJECT_PLANE, new float[]{0, yScale, 0, 0}, 0);
}
}
/**
* Allocates the index buffer and loads it to the GPU.
* An integer index buffer (and not shorts) must be used for large grids,
* otherwise rendering becomes extremely slow. The individual VBOs
* are too small for efficient rendering with shorts.
* The index buffer contains two indices per grid value for triangle strips
* (excep for the last row) and one index for each row (except for
* the topmost and the bottomost row).
* @param gl
*/
private void buildIndexBuffer(GL gl1) {
int cols = getCols();
GL2 gl = (GL2)gl1;
// 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
// http://www.gamedev.net/community/forums/topic.asp?topic_id=227553&whichpage=1
if (y + 2 < patchHeight - 1) {
indexBuf.put(((y + 1) * cols + cols));
}
}
// create name for index buffer and load it to the GPU
indexBuf.rewind();
indexBuffer = new int[1];
gl.glGenBuffers(1, indexBuffer, 0);
indexBuf.rewind();
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, indexBuffer[0]);
int bufBytes = indexBuf.capacity() * 4;
gl.glBufferData(GL2.GL_ELEMENT_ARRAY_BUFFER, bufBytes, indexBuf, GL2.GL_STATIC_DRAW);
}
@Override
public boolean canDisplay(GL gl, float[][] grid) {
return grid != null && grid.length >= 2 && grid[0].length >= 2;
}
}