/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.badlogic.gdx.tests.bullet; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.math.*; import com.badlogic.gdx.utils.BufferUtils; import com.badlogic.gdx.utils.Disposable; import java.nio.FloatBuffer; /** Software rasterizer used for depth rendering and testing of bounding box triangles. Stores depth values inside a * {@link FloatBuffer}. CPU rendering is used in order to avoid the frequent GPU to CPU synchronization which would be needed if * hardware rendering were to be used for occlusion culling queries. * <p> * Based on the algorithm from the Bullet CDTestFramework, BulletSAPCompleteBoxPruningTest.cpp, written by Erwin Coumans. * * @author jsjolund */ public class OcclusionBuffer implements Disposable { static class GridPoint3 extends com.badlogic.gdx.math.GridPoint3 { public GridPoint3 add (GridPoint3 other) { return (GridPoint3)set(x + other.x, y + other.y, z + other.z); } } /** Determines actions and return values for triangle rasterization policies. */ private enum Policy { DRAW, QUERY; /** Evaluate the positions of the vertices depending on policy. * * @param vertices Vertices in camera space * @return True if in query mode and any of the vertices are behind camera frustum near plane. */ boolean evaluate (Quaternion[] vertices) { switch (this) { case DRAW: return false; case QUERY: // If we are querying and any of the vertices are behind the camera, return true. // This means a bounding box will not be considered occluded when any of its vertices // are behind the camera frustum near plane. for (Quaternion vertex : vertices) { if (vertex.z + vertex.w <= 0) return true; } return false; } return false; } /** Compare the current value in the depth buffer with a new value. If draw policy is used, write the new value if it is * larger than current depth (closer to camera). If query is used, return true if new depth not occluded by old depth. * * @param depthBuffer The depth buffer * @param bufferIndex Index in buffer at which to compare depth * @param newDepth New value to compare with * @return True if in query mode and new value closer to the camera, false otherwise */ boolean process (FloatBuffer depthBuffer, int bufferIndex, float newDepth) { float oldDepth = depthBuffer.get(bufferIndex); switch (this) { case DRAW: if (newDepth > oldDepth) depthBuffer.put(bufferIndex, newDepth); return false; case QUERY: return (newDepth >= oldDepth); } return false; } } static class Quaternion extends com.badlogic.gdx.math.Quaternion { /** Left-multiplies the quaternion by the given matrix. * @param matrix The matrix * @return This vector for chaining */ public Quaternion mul (final Matrix4 matrix) { final float[] val = matrix.val; return this.set(x * val[Matrix4.M00] + y * val[Matrix4.M01] + z * val[Matrix4.M02] + w * val[Matrix4.M03], x * val[Matrix4.M10] + y * val[Matrix4.M11] + z * val[Matrix4.M12] + w * val[Matrix4.M13], x * val[Matrix4.M20] + y * val[Matrix4.M21] + z * val[Matrix4.M22] + w * val[Matrix4.M23], x * val[Matrix4.M30] + y * val[Matrix4.M31] + z * val[Matrix4.M32] + w * val[Matrix4.M33]); } /** Multiply the x,y,z,w components of the passed in quaternion with the scalar and add them to the components of this * quaternion */ public Quaternion mulAdd (final Quaternion quaternion, float scalar) { this.x += quaternion.x * scalar; this.y += quaternion.y * scalar; this.z += quaternion.z * scalar; this.w += quaternion.w * scalar; return this; } @Override public Quaternion set (float x, float y, float z, float w) { return (Quaternion)super.set(x, y, z, w); } public Quaternion set (Quaternion quaternion) { return (Quaternion)super.set(quaternion); } /** Subtract the x,y,z,w components of the passed in quaternion to the ones of this quaternion */ public Quaternion sub (float qx, float qy, float qz, float qw) { this.x -= qx; this.y -= qy; this.z -= qz; this.w -= qw; return this; } /** Subtract the x,y,z,w components of the passed in quaternion to the ones of this quaternion */ public Quaternion sub (Quaternion quaternion) { this.x -= quaternion.x; this.y -= quaternion.y; this.z -= quaternion.z; this.w -= quaternion.w; return this; } } /** Face winding order of {@link #setAABBVertices} */ private static final int[] WINDING = {1, 0, 3, 2, 4, 5, 6, 7, 4, 7, 3, 0, 6, 5, 1, 2, 7, 6, 2, 3, 5, 4, 0, 1}; /** Depth buffer */ private final FloatBuffer buffer; /** Half extents of depth buffer in pixels */ private final Vector2 bufferHalfExt; /** Half extents of depth buffer offset by half a pixel */ private final Vector2 bufferOffset; /** Width of depth buffer image in pixels */ public final int bufferWidth; /** Height of depth buffer image in pixels */ public final int bufferHeight; // Temporary storage private final Vector3[] box = new Vector3[8]; private final Quaternion[] tmpVertices = new Quaternion[8]; private final Quaternion[] clippedQuad = new Quaternion[8]; private final Quaternion[] quad = new Quaternion[4]; private final Quaternion tmpQ1 = new Quaternion(); private final Quaternion tmpQ2 = new Quaternion(); private final Vector3 tmpV1 = new Vector3(); private final Vector3 tmpV2 = new Vector3(); private final GridPoint3 triX = new GridPoint3(); private final GridPoint3 triY = new GridPoint3(); private final GridPoint3 triDX = new GridPoint3(); private final GridPoint3 triDY = new GridPoint3(); private final GridPoint3 cursor = new GridPoint3(); // Debug drawing private Pixmap debugPixmap; private Texture debugTexture; private TextureRegion debugTextureRegion; private Matrix4 projectionMatrix = new Matrix4(); /** Creates a new {@link OcclusionBuffer} * * @param width Width of the buffer image * @param height Height of the buffer image */ public OcclusionBuffer (int width, int height) { bufferWidth = width; bufferHeight = height; bufferHalfExt = new Vector2(width * 0.5f, height * 0.5f); bufferOffset = new Vector2(bufferHalfExt.x + 0.5f, bufferHalfExt.y + 0.5f); buffer = BufferUtils.newFloatBuffer(width * height); for (int i = 0; i < 8; i++) { box[i] = new Vector3(); tmpVertices[i] = new Quaternion(); clippedQuad[i] = new Quaternion(); } for (int i = 0; i < 4; i++) quad[i] = new Quaternion(); } /** Clears the depth buffer by setting the depth to -1. */ public void clear () { buffer.clear(); while (buffer.position() < buffer.capacity()) buffer.put(-1); } /** Clip a polygon with camera near plane if necessary. * * @param verticesIn Input * @param verticesOut Output * @return Number of vertices needed to draw the (clipped) face */ private int clipQuad (Quaternion[] verticesIn, Quaternion[] verticesOut) { int numVerts = verticesIn.length; int numVertsBehind = 0; float[] s = new float[4]; for (int i = 0; i < numVerts; i++) { s[i] = verticesIn[i].z + verticesIn[i].w; if (s[i] < 0) numVertsBehind++; } if (numVertsBehind == numVerts) { // All vertices outside frustum return 0; } else if (numVertsBehind > 0) { // Some vertices are behind the camera, so perform clipping. int newNumVerts = 0; for (int i = numVerts - 1, j = 0; j < numVerts; i = j++) { Quaternion a = tmpQ1.set(verticesIn[i]); Quaternion b = tmpQ2.set(verticesIn[j]); float t = s[i] / (a.w + a.z - b.w - b.z); if ((t > 0) && (t < 1)) verticesOut[newNumVerts++].set(a).mulAdd(b.sub(a), t); if (s[j] > 0) verticesOut[newNumVerts++].set(verticesIn[j]); } return newNumVerts; } else { // No clipping needed. for (int i = 0; i < numVerts; i++) verticesOut[i].set(verticesIn[i]); return numVerts; } } @Override public void dispose () { if (debugPixmap != null) { debugPixmap.dispose(); debugTexture.dispose(); debugPixmap = null; debugTexture = null; } } /** Renders an AABB (axis aligned bounding box) to the depth buffer. * * @param center Center of AABB in world coordinates * @param halfExt Half extents of AABB */ public void drawAABB (Vector3 center, Vector3 halfExt) { setAABBVertices(center, halfExt, box); drawBox(box, Policy.DRAW); } /** Renders a bounding box to the depth buffer. Does not need to be axis aligned, but will use the translation, rotation and * scale from the matrix parameter. * * @param worldTransform World transform of the box to render. * @param halfExt Half extents of the box. */ public void drawBB (Matrix4 worldTransform, Vector3 halfExt) { Vector3 center = tmpV1.setZero(); setAABBVertices(center, halfExt, box); worldTransform.getTranslation(center); for (Vector3 vertex : box) { vertex.rot(worldTransform); vertex.add(center); } drawBox(box, Policy.DRAW); } /** Draws a bounding box to the depth buffer, or queries the depth buffer at the pixels the box occupies, depending on policy. * * @param vertices Vertices of box * @param policy Rasterization policy to use * @return True if query policy is used, and any part of the box passes a depth test. False otherwise. */ private boolean drawBox (Vector3[] vertices, Policy policy) { for (int i = 0; i < 8; i++) { // Multiply the world coordinates by the camera combined matrix, but do not divide by w component yet. Vector3 v = vertices[i]; tmpVertices[i].set(v.x, v.y, v.z, 1).mul(projectionMatrix); } if (policy.evaluate(tmpVertices)) return true; // Loop over each box quad in the predefined winding order. for (int i = 0; i < WINDING.length;) { quad[0].set(tmpVertices[WINDING[i++]]); quad[1].set(tmpVertices[WINDING[i++]]); quad[2].set(tmpVertices[WINDING[i++]]); quad[3].set(tmpVertices[WINDING[i++]]); // Clip the quad with near frustum plane if needed int numVertices = clipQuad(quad, clippedQuad); // Divide by w to project vertices to camera space for (int j = 0; j < numVertices; j++) { Quaternion q = clippedQuad[j]; q.z = 1 / q.w; vertices[j].set(q.x * q.z, q.y * q.z, q.z); } // Perform draw/query for (int j = 2; j < numVertices; j++) { // If we are querying and depth test passes, there is no need to continue the rasterization, // since part of the AABB must be visible then. if (drawTriangle(vertices[0], vertices[j - 1], vertices[j], policy)) return true; } } return false; } /** Draw the depth buffer to a texture. Slow, should only be used for debugging purposes. * * @return Region of debug texture */ public TextureRegion drawDebugTexture () { if (debugPixmap == null) { debugPixmap = new Pixmap(bufferWidth, bufferHeight, Pixmap.Format.RGBA8888); debugTexture = new Texture(debugPixmap); debugTextureRegion = new TextureRegion(debugTexture); debugTextureRegion.flip(false, true); } debugPixmap.setColor(Color.BLACK); debugPixmap.fill(); // Find min/max depth values in buffer float minDepth = Float.POSITIVE_INFINITY; float maxDepth = Float.NEGATIVE_INFINITY; buffer.clear(); while (buffer.position() < buffer.capacity()) { float depth = MathUtils.clamp(buffer.get(), 0, Float.POSITIVE_INFINITY); minDepth = Math.min(depth, minDepth); maxDepth = Math.max(depth, maxDepth); } float extent = 1 / (maxDepth - minDepth); buffer.clear(); // Draw to pixmap for (int x = 0; x < bufferWidth; x++) { for (int y = 0; y < bufferHeight; y++) { float depth = MathUtils.clamp(buffer.get(x + y * bufferWidth), 0, Float.POSITIVE_INFINITY); float c = depth * extent; debugPixmap.drawPixel(x, y, Color.rgba8888(c, c, c, 1)); } } debugTexture.draw(debugPixmap, 0, 0); return debugTextureRegion; } /** Rasterizes a triangle with linearly interpolated depth values. * <p> * If used with {@link Policy#DRAW} the triangle will be drawn to the depth buffer wherever it passes a depth test. * <p> * If {@link Policy#QUERY} is used, the depth values in the triangle will be compared with existing depth buffer values. If any * pixel passes a depth test the rasterization will be aborted and the method will return true. * * @param a Triangle vertex in camera space * @param b Triangle vertex in camera space * @param c Triangle vertex in camera space * @param policy Draw or query policy * @return With query policy, true if any pixel in the triangle passes a depth test. False otherwise. */ private boolean drawTriangle (Vector3 a, Vector3 b, Vector3 c, Policy policy) { // Check if triangle faces away from the camera (back-face culling). if (((tmpV1.set(b).sub(a)).crs(tmpV2.set(c).sub(a))).z <= 0) return false; // Triangle coordinates and size. // Note that x, y, z in e.g. triX corresponds to x components of vertices a, b, c, // which means triX.x is the x coordinate of a. triX.set((int)(a.x * bufferHalfExt.x + bufferOffset.x), (int)(b.x * bufferHalfExt.x + bufferOffset.x), (int)(c.x * bufferHalfExt.x + bufferOffset.x)); triY.set((int)(a.y * bufferHalfExt.y + bufferOffset.y), (int)(b.y * bufferHalfExt.y + bufferOffset.y), (int)(c.y * bufferHalfExt.y + bufferOffset.y)); // X/Y extents int xMin = Math.max(0, Math.min(triX.x, Math.min(triX.y, triX.z))); int xMax = Math.min(bufferWidth, 1 + Math.max(triX.x, Math.max(triX.y, triX.z))); int yMin = Math.max(0, Math.min(triY.x, Math.min(triY.y, triY.z))); int yMax = Math.min(bufferWidth, 1 + Math.max(triY.x, Math.max(triY.y, triY.z))); int width = xMax - xMin; int height = yMax - yMin; if (width * height <= 0) return false; // Cursor triDX.set(triY.x - triY.y, triY.y - triY.z, triY.z - triY.x); triDY.set(triX.y - triX.x - triDX.x * width, triX.z - triX.y - triDX.y * width, triX.x - triX.z - triDX.z * width); cursor.set(yMin * (triX.y - triX.x) + xMin * (triY.x - triY.y) + triX.x * triY.y - triX.y * triY.x, yMin * (triX.z - triX.y) + xMin * (triY.y - triY.z) + triX.y * triY.z - triX.z * triY.y, yMin * (triX.x - triX.z) + xMin * (triY.z - triY.x) + triX.z * triY.x - triX.x * triY.z); // Depth interpolation float ia = 1f / (float)(triX.x * triY.y - triX.y * triY.x + triX.z * triY.x - triX.x * triY.z + triX.y * triY.z - triX.z * triY.y); float dzx = ia * (triY.x * (c.z - b.z) + triY.y * (a.z - c.z) + triY.z * (b.z - a.z)); float dzy = ia * (triX.x * (b.z - c.z) + triX.y * (c.z - a.z) + triX.z * (a.z - b.z)) - (dzx * width); float drawDepth = ia * (a.z * cursor.y + b.z * cursor.z + c.z * cursor.x); int bufferRow = (yMin * bufferHeight); // Loop over pixels and process the triangle pixel depth versus the existing value in buffer. for (int iy = yMin; iy < yMax; iy++) { for (int ix = xMin; ix < xMax; ix++) { int bufferIndex = bufferRow + ix; if (cursor.x >= 0 && cursor.y >= 0 && cursor.z >= 0 && policy.process(buffer, bufferIndex, drawDepth)) return true; cursor.add(triDX); drawDepth += dzx; } cursor.add(triDY); drawDepth += dzy; bufferRow += bufferWidth; } return false; } /** Queries the depth buffer as to whether an AABB (axis aligned bounding box) is completely occluded by a previously rendered * object. If any part of the AABB is visible (not occluded), the method returns true. * * @param center Center of AABB in world coordinates * @param halfExt Half extents of AABB * @return True if any part of the AABB is visible, false otherwise. */ public boolean queryAABB (Vector3 center, Vector3 halfExt) { setAABBVertices(center, halfExt, box); return drawBox(box, Policy.QUERY); } /** Calculates the eight vertices of an AABB. * * @param center Center point * @param halfExt Half extents * @param vertices Vertices output */ private static void setAABBVertices (Vector3 center, Vector3 halfExt, Vector3[] vertices) { vertices[0].set(center.x - halfExt.x, center.y - halfExt.y, center.z - halfExt.z); vertices[1].set(center.x + halfExt.x, center.y - halfExt.y, center.z - halfExt.z); vertices[2].set(center.x + halfExt.x, center.y + halfExt.y, center.z - halfExt.z); vertices[3].set(center.x - halfExt.x, center.y + halfExt.y, center.z - halfExt.z); vertices[4].set(center.x - halfExt.x, center.y - halfExt.y, center.z + halfExt.z); vertices[5].set(center.x + halfExt.x, center.y - halfExt.y, center.z + halfExt.z); vertices[6].set(center.x + halfExt.x, center.y + halfExt.y, center.z + halfExt.z); vertices[7].set(center.x - halfExt.x, center.y + halfExt.y, center.z + halfExt.z); } /** Sets the projection matrix to be used for rendering. Usually this will be set to Camera.combined. * @param matrix */ public void setProjectionMatrix (Matrix4 matrix) { projectionMatrix.set(matrix); } }