com.badlogic.gdx.tests.bullet.OcclusionBuffer.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.tests.bullet.OcclusionBuffer.java

Source

/*******************************************************************************
 * 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);
    }

}