/******************************************************************************* * 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.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.PerspectiveCamera; import com.badlogic.gdx.graphics.VertexAttribute; import com.badlogic.gdx.graphics.VertexAttributes.Usage; import com.badlogic.gdx.graphics.g3d.Material; import com.badlogic.gdx.graphics.g3d.Model; import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute; import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder; import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.physics.bullet.collision.btBroadphasePairArray; import com.badlogic.gdx.physics.bullet.collision.btCollisionDispatcher; import com.badlogic.gdx.physics.bullet.collision.btCollisionObject; import com.badlogic.gdx.physics.bullet.collision.btCollisionShape; import com.badlogic.gdx.physics.bullet.collision.btCollisionWorld; import com.badlogic.gdx.physics.bullet.collision.btCompoundShape; import com.badlogic.gdx.physics.bullet.collision.btConvexHullShape; import com.badlogic.gdx.physics.bullet.collision.btDbvtBroadphase; import com.badlogic.gdx.physics.bullet.collision.btDefaultCollisionConfiguration; import com.badlogic.gdx.physics.bullet.collision.btPairCachingGhostObject; import com.badlogic.gdx.physics.bullet.collision.btPersistentManifoldArray; import com.badlogic.gdx.utils.Array; /** @author Xoppa */ public class FrustumCullingTest extends BaseBulletTest { /** Only show entities inside the frustum */ final static int CULL_FRUSTUM = 1; /** Transform the render cam with the frustum */ final static int FRUSTUM_CAM = 2; final static boolean USE_BULLET_FRUSTUM_CULLING = true; int state = 0; // 0 = No culling, look from above final static int BOXCOUNT = 200; final static float BOX_X_MIN = -25; final static float BOX_Y_MIN = -25; final static float BOX_Z_MIN = -25; final static float BOX_X_MAX = 25; final static float BOX_Y_MAX = 25; final static float BOX_Z_MAX = 25; final static float SPEED_X = 360f / 7f; final static float SPEED_Y = 360f / 19f; final static float SPEED_Z = 360f / 13f; final static Vector3 tmpV = new Vector3(); final static Matrix4 tmpM = new Matrix4(); final static int ptrs[] = new int[512]; final static Array<btCollisionObject> visibleObjects = new Array<btCollisionObject>(); public static btPairCachingGhostObject createFrustumObject (final Vector3... points) { final btPairCachingGhostObject result = new TestPairCachingGhostObject(); final boolean USE_COMPOUND = true; // Using a compound shape is not necessary, but it's good practice to create shapes around the center. if (USE_COMPOUND) { final Vector3 centerNear = new Vector3(points[2]).sub(points[0]).scl(0.5f).add(points[0]); final Vector3 centerFar = new Vector3(points[6]).sub(points[4]).scl(0.5f).add(points[4]); final Vector3 center = new Vector3(centerFar).sub(centerNear).scl(0.5f).add(centerNear); final btConvexHullShape hullShape = new btConvexHullShape(); for (int i = 0; i < points.length; i++) hullShape.addPoint(tmpV.set(points[i]).sub(center)); final btCompoundShape shape = new btCompoundShape(); shape.addChildShape(tmpM.setToTranslation(center), hullShape); result.setCollisionShape(shape); } else { final btConvexHullShape shape = new btConvexHullShape(); for (int i = 0; i < points.length; i++) shape.addPoint(points[i]); result.setCollisionShape(shape); } result.setCollisionFlags(btCollisionObject.CollisionFlags.CF_NO_CONTACT_RESPONSE); return result; } public static Array<BulletEntity> getEntitiesCollidingWithObject (final BulletWorld world, final btCollisionObject object, final Array<BulletEntity> out, final btPersistentManifoldArray tmpArr) { // Fetch the array of contacts btBroadphasePairArray arr = world.broadphase.getOverlappingPairCache().getOverlappingPairArray(); // Get the user values (which are indices in the entities array) of all objects colliding with the object final int n = arr.getCollisionObjectsValue(ptrs, object); // Fill the array of entities out.clear(); for (int i = 0; i < n; i++) out.add(world.entities.get(ptrs[i])); return out; } public static Model createFrustumModel (final Vector3... p) { ModelBuilder builder = new ModelBuilder(); builder.begin(); MeshPartBuilder mpb = builder.part("", GL20.GL_LINES, Usage.Position | Usage.Normal, new Material(new ColorAttribute(ColorAttribute.Diffuse, Color.WHITE))); mpb.vertex(p[0].x, p[0].y, p[0].z, 0, 0, 1, p[1].x, p[1].y, p[1].z, 0, 0, 1, p[2].x, p[2].y, p[2].z, 0, 0, 1, p[3].x, p[3].y, p[3].z, 0, 0, 1, // near p[4].x, p[4].y, p[4].z, 0, 0, -1, p[5].x, p[5].y, p[5].z, 0, 0, -1, p[6].x, p[6].y, p[6].z, 0, 0, -1, p[7].x, p[7].y, p[7].z, 0, 0, -1); mpb.index((short)0, (short)1, (short)1, (short)2, (short)2, (short)3, (short)3, (short)0); mpb.index((short)4, (short)5, (short)5, (short)6, (short)6, (short)7, (short)7, (short)4); mpb.index((short)0, (short)4, (short)1, (short)5, (short)2, (short)6, (short)3, (short)7); return builder.end(); } private float angleX, angleY, angleZ; private btPairCachingGhostObject frustumObject; private BulletEntity frustumEntity; private final Array<BulletEntity> visibleEntities = new Array<BulletEntity>(); private btPersistentManifoldArray tempManifoldArr; private PerspectiveCamera frustumCam; private PerspectiveCamera overviewCam; @Override public void create () { super.create(); instructions = "Tap to toggle view\nLong press to toggle debug mode\nSwipe for next test\nCtrl+drag to rotate\nScroll to zoom"; tempManifoldArr = new btPersistentManifoldArray(); world.addConstructor("collisionBox", new BulletConstructor(world.getConstructor("box").model)); // Create the entities final float dX = BOX_X_MAX - BOX_X_MIN; final float dY = BOX_Y_MAX - BOX_Y_MIN; final float dZ = BOX_Z_MAX - BOX_Z_MIN; for (int i = 0; i < BOXCOUNT; i++) world.add("collisionBox", BOX_X_MIN + dX * (float)Math.random(), BOX_Y_MIN + dY * (float)Math.random(), BOX_Z_MIN + dZ * (float)Math.random()).setColor(Color.GRAY); frustumCam = new PerspectiveCamera(camera.fieldOfView, camera.viewportWidth, camera.viewportHeight); frustumCam.far = Vector3.len(BOX_X_MAX, BOX_Y_MAX, BOX_Z_MAX); frustumCam.update(); overviewCam = camera; overviewCam.position.set(BOX_X_MAX, BOX_Y_MAX, BOX_Z_MAX); overviewCam.lookAt(Vector3.Zero); overviewCam.far = 150f; overviewCam.update(); final Model frustumModel = createFrustumModel(frustumCam.frustum.planePoints); disposables.add(frustumModel); frustumObject = createFrustumObject(frustumCam.frustum.planePoints); world.add(frustumEntity = new BulletEntity(frustumModel, frustumObject, 0, 0, 0)); frustumEntity.setColor(Color.BLUE); } @Override public BulletWorld createWorld () { // No need to use dynamics for this test btDbvtBroadphase broadphase = new btDbvtBroadphase(); btDefaultCollisionConfiguration collisionConfig = new btDefaultCollisionConfiguration(); btCollisionDispatcher dispatcher = new btCollisionDispatcher(collisionConfig); btCollisionWorld collisionWorld = new btCollisionWorld(dispatcher, broadphase, collisionConfig); return new BulletWorld(collisionConfig, dispatcher, broadphase, null, collisionWorld); } @Override public void update () { super.update(); // Not using dynamics, so update the collision world manually if (USE_BULLET_FRUSTUM_CULLING) { if (world.performanceCounter != null) world.performanceCounter.start(); world.collisionWorld.performDiscreteCollisionDetection(); if (world.performanceCounter != null) world.performanceCounter.stop(); } } @Override public void render () { final float dt = Gdx.graphics.getDeltaTime(); frustumEntity.transform.idt(); frustumEntity.transform.rotate(Vector3.X, angleX = (angleX + dt * SPEED_X) % 360); frustumEntity.transform.rotate(Vector3.Y, angleY = (angleY + dt * SPEED_Y) % 360); frustumEntity.transform.rotate(Vector3.Z, angleZ = (angleZ + dt * SPEED_Z) % 360); // Transform the ghost object frustumEntity.body.setWorldTransform(frustumEntity.transform); // Transform the frustum cam frustumCam.direction.set(0, 0, -1); frustumCam.up.set(0, 1, 0); frustumCam.position.set(0, 0, 0); frustumCam.rotate(frustumEntity.transform); frustumCam.update(); super.render(); performance.append(" visible: ").append(visibleEntities.size); } @Override protected void renderWorld () { if (world.performanceCounter != null) world.performanceCounter.start(); if (USE_BULLET_FRUSTUM_CULLING) getEntitiesCollidingWithObject(world, frustumObject, visibleEntities, tempManifoldArr); else { visibleEntities.clear(); for (int i = 0; i < world.entities.size; i++) { final BulletEntity e = world.entities.get(i); if (e == frustumEntity) continue; e.modelInstance.transform.getTranslation(tmpV); if (frustumCam.frustum.sphereInFrustum(tmpV, 1)) visibleEntities.add(e); } } if (world.performanceCounter != null) world.performanceCounter.stop(); for (int i = 0; i < visibleEntities.size; i++) visibleEntities.get(i).setColor(Color.RED); modelBatch.begin(camera); if ((state & CULL_FRUSTUM) == CULL_FRUSTUM) { world.render(modelBatch, environment, visibleEntities); world.render(modelBatch, environment, frustumEntity); } else world.render(modelBatch, environment); modelBatch.end(); for (int i = 0; i < visibleEntities.size; i++) visibleEntities.get(i).setColor(Color.GRAY); } @Override protected void beginRender (boolean lighting) { super.beginRender(false); } @Override public void dispose () { frustumObject = null; super.dispose(); if (tempManifoldArr != null) tempManifoldArr.dispose(); tempManifoldArr = null; } @Override public boolean tap (float x, float y, int count, int button) { state = (state + 1) % 3; if ((state & FRUSTUM_CAM) == FRUSTUM_CAM) camera = frustumCam; else camera = overviewCam; return true; } // Simple helper class to keep a reference to the collision shape public static class TestPairCachingGhostObject extends btPairCachingGhostObject { public btCollisionShape shape; @Override public void setCollisionShape (btCollisionShape collisionShape) { shape = collisionShape; super.setCollisionShape(collisionShape); } @Override public void dispose () { super.dispose(); if (shape != null) shape.dispose(); shape = null; } } }