1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.tests.bullet; 18 19 import com.badlogic.gdx.Gdx; 20 import com.badlogic.gdx.graphics.Color; 21 import com.badlogic.gdx.graphics.GL20; 22 import com.badlogic.gdx.graphics.PerspectiveCamera; 23 import com.badlogic.gdx.graphics.VertexAttribute; 24 import com.badlogic.gdx.graphics.VertexAttributes.Usage; 25 import com.badlogic.gdx.graphics.g3d.Material; 26 import com.badlogic.gdx.graphics.g3d.Model; 27 import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute; 28 import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder; 29 import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder; 30 import com.badlogic.gdx.math.Matrix4; 31 import com.badlogic.gdx.math.Vector3; 32 import com.badlogic.gdx.physics.bullet.collision.btBroadphasePairArray; 33 import com.badlogic.gdx.physics.bullet.collision.btCollisionDispatcher; 34 import com.badlogic.gdx.physics.bullet.collision.btCollisionObject; 35 import com.badlogic.gdx.physics.bullet.collision.btCollisionShape; 36 import com.badlogic.gdx.physics.bullet.collision.btCollisionWorld; 37 import com.badlogic.gdx.physics.bullet.collision.btCompoundShape; 38 import com.badlogic.gdx.physics.bullet.collision.btConvexHullShape; 39 import com.badlogic.gdx.physics.bullet.collision.btDbvtBroadphase; 40 import com.badlogic.gdx.physics.bullet.collision.btDefaultCollisionConfiguration; 41 import com.badlogic.gdx.physics.bullet.collision.btPairCachingGhostObject; 42 import com.badlogic.gdx.physics.bullet.collision.btPersistentManifoldArray; 43 import com.badlogic.gdx.utils.Array; 44 45 /** @author Xoppa */ 46 public class FrustumCullingTest extends BaseBulletTest { 47 /** Only show entities inside the frustum */ 48 final static int CULL_FRUSTUM = 1; 49 /** Transform the render cam with the frustum */ 50 final static int FRUSTUM_CAM = 2; 51 52 final static boolean USE_BULLET_FRUSTUM_CULLING = true; 53 54 int state = 0; // 0 = No culling, look from above 55 56 final static int BOXCOUNT = 200; 57 58 final static float BOX_X_MIN = -25; 59 final static float BOX_Y_MIN = -25; 60 final static float BOX_Z_MIN = -25; 61 62 final static float BOX_X_MAX = 25; 63 final static float BOX_Y_MAX = 25; 64 final static float BOX_Z_MAX = 25; 65 66 final static float SPEED_X = 360f / 7f; 67 final static float SPEED_Y = 360f / 19f; 68 final static float SPEED_Z = 360f / 13f; 69 70 final static Vector3 tmpV = new Vector3(); 71 final static Matrix4 tmpM = new Matrix4(); 72 73 final static int ptrs[] = new int[512]; 74 final static Array<btCollisionObject> visibleObjects = new Array<btCollisionObject>(); 75 createFrustumObject(final Vector3... points)76 public static btPairCachingGhostObject createFrustumObject (final Vector3... points) { 77 final btPairCachingGhostObject result = new TestPairCachingGhostObject(); 78 final boolean USE_COMPOUND = true; 79 // Using a compound shape is not necessary, but it's good practice to create shapes around the center. 80 if (USE_COMPOUND) { 81 final Vector3 centerNear = new Vector3(points[2]).sub(points[0]).scl(0.5f).add(points[0]); 82 final Vector3 centerFar = new Vector3(points[6]).sub(points[4]).scl(0.5f).add(points[4]); 83 final Vector3 center = new Vector3(centerFar).sub(centerNear).scl(0.5f).add(centerNear); 84 final btConvexHullShape hullShape = new btConvexHullShape(); 85 for (int i = 0; i < points.length; i++) 86 hullShape.addPoint(tmpV.set(points[i]).sub(center)); 87 final btCompoundShape shape = new btCompoundShape(); 88 shape.addChildShape(tmpM.setToTranslation(center), hullShape); 89 result.setCollisionShape(shape); 90 } else { 91 final btConvexHullShape shape = new btConvexHullShape(); 92 for (int i = 0; i < points.length; i++) 93 shape.addPoint(points[i]); 94 result.setCollisionShape(shape); 95 } 96 result.setCollisionFlags(btCollisionObject.CollisionFlags.CF_NO_CONTACT_RESPONSE); 97 return result; 98 } 99 getEntitiesCollidingWithObject(final BulletWorld world, final btCollisionObject object, final Array<BulletEntity> out, final btPersistentManifoldArray tmpArr)100 public static Array<BulletEntity> getEntitiesCollidingWithObject (final BulletWorld world, final btCollisionObject object, 101 final Array<BulletEntity> out, final btPersistentManifoldArray tmpArr) { 102 // Fetch the array of contacts 103 btBroadphasePairArray arr = world.broadphase.getOverlappingPairCache().getOverlappingPairArray(); 104 // Get the user values (which are indices in the entities array) of all objects colliding with the object 105 final int n = arr.getCollisionObjectsValue(ptrs, object); 106 // Fill the array of entities 107 out.clear(); 108 for (int i = 0; i < n; i++) 109 out.add(world.entities.get(ptrs[i])); 110 return out; 111 } 112 createFrustumModel(final Vector3... p)113 public static Model createFrustumModel (final Vector3... p) { 114 ModelBuilder builder = new ModelBuilder(); 115 builder.begin(); 116 MeshPartBuilder mpb = builder.part("", GL20.GL_LINES, Usage.Position | Usage.Normal, new Material(new ColorAttribute(ColorAttribute.Diffuse, Color.WHITE))); 117 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, 118 p[2].y, p[2].z, 0, 0, 1, p[3].x, p[3].y, p[3].z, 0, 0, 119 1, // near 120 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, 121 p[7].z, 0, 0, -1); 122 mpb.index((short)0, (short)1, (short)1, (short)2, (short)2, (short)3, (short)3, (short)0); 123 mpb.index((short)4, (short)5, (short)5, (short)6, (short)6, (short)7, (short)7, (short)4); 124 mpb.index((short)0, (short)4, (short)1, (short)5, (short)2, (short)6, (short)3, (short)7); 125 return builder.end(); 126 } 127 128 private float angleX, angleY, angleZ; 129 private btPairCachingGhostObject frustumObject; 130 private BulletEntity frustumEntity; 131 private final Array<BulletEntity> visibleEntities = new Array<BulletEntity>(); 132 private btPersistentManifoldArray tempManifoldArr; 133 private PerspectiveCamera frustumCam; 134 private PerspectiveCamera overviewCam; 135 136 @Override create()137 public void create () { 138 super.create(); 139 140 instructions = "Tap to toggle view\nLong press to toggle debug mode\nSwipe for next test\nCtrl+drag to rotate\nScroll to zoom"; 141 142 tempManifoldArr = new btPersistentManifoldArray(); 143 144 world.addConstructor("collisionBox", new BulletConstructor(world.getConstructor("box").model)); 145 146 // Create the entities 147 final float dX = BOX_X_MAX - BOX_X_MIN; 148 final float dY = BOX_Y_MAX - BOX_Y_MIN; 149 final float dZ = BOX_Z_MAX - BOX_Z_MIN; 150 for (int i = 0; i < BOXCOUNT; i++) 151 world.add("collisionBox", BOX_X_MIN + dX * (float)Math.random(), BOX_Y_MIN + dY * (float)Math.random(), 152 BOX_Z_MIN + dZ * (float)Math.random()).setColor(Color.GRAY); 153 154 frustumCam = new PerspectiveCamera(camera.fieldOfView, camera.viewportWidth, camera.viewportHeight); 155 frustumCam.far = Vector3.len(BOX_X_MAX, BOX_Y_MAX, BOX_Z_MAX); 156 frustumCam.update(); 157 158 overviewCam = camera; 159 overviewCam.position.set(BOX_X_MAX, BOX_Y_MAX, BOX_Z_MAX); 160 overviewCam.lookAt(Vector3.Zero); 161 overviewCam.far = 150f; 162 overviewCam.update(); 163 164 final Model frustumModel = createFrustumModel(frustumCam.frustum.planePoints); 165 disposables.add(frustumModel); 166 frustumObject = createFrustumObject(frustumCam.frustum.planePoints); 167 world.add(frustumEntity = new BulletEntity(frustumModel, frustumObject, 0, 0, 0)); 168 frustumEntity.setColor(Color.BLUE); 169 } 170 171 @Override createWorld()172 public BulletWorld createWorld () { 173 // No need to use dynamics for this test 174 btDbvtBroadphase broadphase = new btDbvtBroadphase(); 175 btDefaultCollisionConfiguration collisionConfig = new btDefaultCollisionConfiguration(); 176 btCollisionDispatcher dispatcher = new btCollisionDispatcher(collisionConfig); 177 btCollisionWorld collisionWorld = new btCollisionWorld(dispatcher, broadphase, collisionConfig); 178 return new BulletWorld(collisionConfig, dispatcher, broadphase, null, collisionWorld); 179 } 180 181 @Override update()182 public void update () { 183 super.update(); 184 // Not using dynamics, so update the collision world manually 185 if (USE_BULLET_FRUSTUM_CULLING) { 186 if (world.performanceCounter != null) world.performanceCounter.start(); 187 world.collisionWorld.performDiscreteCollisionDetection(); 188 if (world.performanceCounter != null) world.performanceCounter.stop(); 189 } 190 } 191 192 @Override render()193 public void render () { 194 final float dt = Gdx.graphics.getDeltaTime(); 195 frustumEntity.transform.idt(); 196 frustumEntity.transform.rotate(Vector3.X, angleX = (angleX + dt * SPEED_X) % 360); 197 frustumEntity.transform.rotate(Vector3.Y, angleY = (angleY + dt * SPEED_Y) % 360); 198 frustumEntity.transform.rotate(Vector3.Z, angleZ = (angleZ + dt * SPEED_Z) % 360); 199 200 // Transform the ghost object 201 frustumEntity.body.setWorldTransform(frustumEntity.transform); 202 // Transform the frustum cam 203 frustumCam.direction.set(0, 0, -1); 204 frustumCam.up.set(0, 1, 0); 205 frustumCam.position.set(0, 0, 0); 206 frustumCam.rotate(frustumEntity.transform); 207 frustumCam.update(); 208 209 super.render(); 210 211 performance.append(" visible: ").append(visibleEntities.size); 212 } 213 214 @Override renderWorld()215 protected void renderWorld () { 216 if (world.performanceCounter != null) world.performanceCounter.start(); 217 if (USE_BULLET_FRUSTUM_CULLING) 218 getEntitiesCollidingWithObject(world, frustumObject, visibleEntities, tempManifoldArr); 219 else { 220 visibleEntities.clear(); 221 for (int i = 0; i < world.entities.size; i++) { 222 final BulletEntity e = world.entities.get(i); 223 if (e == frustumEntity) continue; 224 e.modelInstance.transform.getTranslation(tmpV); 225 if (frustumCam.frustum.sphereInFrustum(tmpV, 1)) visibleEntities.add(e); 226 } 227 } 228 if (world.performanceCounter != null) world.performanceCounter.stop(); 229 230 for (int i = 0; i < visibleEntities.size; i++) 231 visibleEntities.get(i).setColor(Color.RED); 232 233 modelBatch.begin(camera); 234 if ((state & CULL_FRUSTUM) == CULL_FRUSTUM) { 235 world.render(modelBatch, environment, visibleEntities); 236 world.render(modelBatch, environment, frustumEntity); 237 } else 238 world.render(modelBatch, environment); 239 modelBatch.end(); 240 241 for (int i = 0; i < visibleEntities.size; i++) 242 visibleEntities.get(i).setColor(Color.GRAY); 243 } 244 245 @Override beginRender(boolean lighting)246 protected void beginRender (boolean lighting) { 247 super.beginRender(false); 248 } 249 250 @Override dispose()251 public void dispose () { 252 frustumObject = null; 253 254 super.dispose(); 255 256 if (tempManifoldArr != null) tempManifoldArr.dispose(); 257 tempManifoldArr = null; 258 } 259 260 @Override tap(float x, float y, int count, int button)261 public boolean tap (float x, float y, int count, int button) { 262 state = (state + 1) % 3; 263 if ((state & FRUSTUM_CAM) == FRUSTUM_CAM) 264 camera = frustumCam; 265 else 266 camera = overviewCam; 267 return true; 268 } 269 270 // Simple helper class to keep a reference to the collision shape 271 public static class TestPairCachingGhostObject extends btPairCachingGhostObject { 272 public btCollisionShape shape; 273 274 @Override setCollisionShape(btCollisionShape collisionShape)275 public void setCollisionShape (btCollisionShape collisionShape) { 276 shape = collisionShape; 277 super.setCollisionShape(collisionShape); 278 } 279 280 @Override dispose()281 public void dispose () { 282 super.dispose(); 283 if (shape != null) shape.dispose(); 284 shape = null; 285 } 286 } 287 } 288