/*******************************************************************************
* 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.assets.AssetManager;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.VertexAttributes;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight;
import com.badlogic.gdx.graphics.g3d.model.Node;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.profiling.GLProfiler;
import com.badlogic.gdx.math.RandomXS128;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.BoundingBox;
import com.badlogic.gdx.physics.bullet.collision.btBoxShape;
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.btDbvtBroadphase;
import com.badlogic.gdx.physics.bullet.collision.btDefaultCollisionConfiguration;
import com.badlogic.gdx.physics.bullet.dynamics.btDiscreteDynamicsWorld;
import com.badlogic.gdx.physics.bullet.dynamics.btSequentialImpulseConstraintSolver;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.StringBuilder;
/** @author jsjolund */
public class OcclusionCullingTest extends BaseBulletTest {
/** Types of culling to use in the test application */
private enum CullingPolicy {
/** Occlusion culling, renders only entities which are visible from the viewpoint of a camera. Objects which are hidden
* behind other objects (occluded) do not need to be rendered. */
OCCLUSION,
/** No culling, renders all objects. */
NONE,
/** Simple culling which loops through all entities in the world, and checks if the radius of the object bounding box is
* inside the camera frustum. Hidden surfaces are not taken into account. Bullet is not used. */
SIMPLE,
/** Same as {@link CullingPolicy#SIMPLE}, except instead of checking each object in the world, the process is accelerated by
* the Bullet broadphase bounding volume tree. */
KDOP;
private static CullingPolicy[] val = values();
public CullingPolicy next () {
return val[(this.ordinal() + 1) % val.length];
}
}
/** Collision objects with this collision flag can occlude other objects */
public final static short CF_OCCLUDER_OBJECT = 512;
/** Amount of occludee entities to spawn at program start */
private final static int STARTING_OCCLUDEE_AMOUNT = 300;
/** Number of occludee entities to spawn at key press */
private final static int KEY_SPAWN_OCCLUDEE_AMOUNT = 100;
/** Occlusion depth buffer image size */
private final static int[] OCL_BUFFER_EXTENTS = new int[] {128, 256, 512, 32, 64};
// Animated frustum camera settings
private final static float FRUSTUM_CAMERA_FAR = 50f;
private final static float FRUSTUM_CAMERA_FOV = 60f;
private final static float FRUSTUM_ANG_SPEED = 360f / 15f;
private final static float FRUSTUM_LIN_SPEED = -6f;
private final static float FRUSTUM_MOVE_RADIUS = 12;
// Occludee models and textures used in test
private final static String DEFAULT_TEX_PATH = "data/g3d/checkboard.png";
private final static String[] OCCLUDEE_PATHS_DYNAMIC = new String[] {"data/car.obj", "data/wheel.obj", "data/cube.obj",
"data/g3d/ship.obj", "data/g3d/shapes/sphere.g3dj", "data/g3d/shapes/torus.g3dj",};
private final static String[] OCCLUDEE_PATHS_STATIC = new String[OCCLUDEE_PATHS_DYNAMIC.length];
private final static float OCCLUDEE_MAX_EXTENT = 1.5f;
private final static Vector3 OCCLUDER_DIM = new Vector3(1f, 6f, 20f);
private final static Vector3 GROUND_DIM = new Vector3(120, 1, 120);
private final static int USE_FRUSTUM_CAM = 1;
private final static int PAUSE_FRUSTUM_CAM = 2;
private final static int SHOW_DEBUG_IMAGE = 4;
private final Vector3 frustumCamPos = new Vector3(0, 4, FRUSTUM_MOVE_RADIUS);
private float frustumCamAngleY;
private PerspectiveCamera frustumCam;
private ModelInstance frustumInstance;
private PerspectiveCamera overviewCam;
final Array<BulletEntity> visibleEntities = new Array<BulletEntity>();
// Program state variables
private CullingPolicy cullingPolicy = CullingPolicy.OCCLUSION;
private int bufferExtentIndex = 0;
private int state = 0;
// For occlusion culling
private OcclusionBuffer oclBuffer;
private OcclusionCuller occlusionCuller;
private btDbvtBroadphase broadphase;
private final RandomXS128 rng = new RandomXS128(0);
// For drawing occlusion buffer debug image
private ShapeRenderer shapeRenderer;
private SpriteBatch spriteBatch;
/** Adds an occluder entity of specified type
*
* @param type Type name
* @param rotationY Rotation on Y axis in degrees
* @param position The world position
* @return The added entity */
private BulletEntity addOccluder (String type, float rotationY, Vector3 position) {
BulletEntity e = world.add(type, 0, 0, 0);
e.body.setWorldTransform(e.transform.setToRotation(Vector3.Y, rotationY).setTranslation(position));
e.body.setCollisionFlags(e.body.getCollisionFlags() | CF_OCCLUDER_OBJECT);
e.setColor(Color.RED);
return e;
}
/** Adds an occludee entity of random type at a random place on the ground.
*
* @param dynamic If true, entity body will be dynamic (mass > 0)
* @return The added entity */
private BulletEntity addRandomOccludee (boolean dynamic) {
// Add occludee to world
BulletEntity entity = world.add(getRandomOccludeeType(dynamic), 0, 0, 0);
entity.setColor(Color.WHITE);
// Random rotation
float rotationY = rng.nextFloat() * 360f;
// Random ground position
Vector3 position = tmpV1;
int maxDstX = (int)(GROUND_DIM.x * 0.49f);
position.x = rng.nextInt(maxDstX) * ((rng.nextBoolean()) ? 1 : -1);
position.z = rng.nextInt(maxDstX) * ((rng.nextBoolean()) ? 1 : -1);
position.y = entity.boundingBox.getDimensions(tmpV2).y * 0.5f;
entity.modelInstance.transform.setToRotation(Vector3.Y, rotationY).setTranslation(position);
entity.body.setWorldTransform(entity.modelInstance.transform);
return entity;
}
@Override
public void create () {
Gdx.input.setOnscreenKeyboardVisible(true);
super.create();
GLProfiler.enable();
StringBuilder sb = new StringBuilder();
sb.append("Swipe for next test\n");
sb.append("Long press to toggle debug mode\n");
sb.append("Ctrl+drag to rotate\n");
sb.append("Scroll to zoom\n");
sb.append("Tap to spawn dynamic entity, press\n");
sb.append("'0' to spawn ").append(KEY_SPAWN_OCCLUDEE_AMOUNT).append(" static entities\n");
sb.append("'1' to set normal/disabled/occlusion-culling\n");
sb.append("'2' to change camera\n");
sb.append("'3' to toggle camera movement\n");
sb.append("'4' to cycle occlusion buffer sizes\n");
sb.append("'5' to toggle occlusion buffer image\n");
sb.append("'6' to toggle shadows\n");
instructions = sb.toString();
AssetManager assets = new AssetManager();
disposables.add(assets);
for (String modelName : OCCLUDEE_PATHS_DYNAMIC)
assets.load(modelName, Model.class);
assets.load(DEFAULT_TEX_PATH, Texture.class);
Camera shadowCamera = ((DirectionalShadowLight)light).getCamera();
shadowCamera.viewportWidth = shadowCamera.viewportHeight = 120;
// User controlled camera
overviewCam = camera;
overviewCam.position.set(overviewCam.direction).nor().scl(-100);
overviewCam.lookAt(Vector3.Zero);
overviewCam.far = camera.far *= 2;
overviewCam.update(true);
// Animated frustum camera model
frustumCam = new PerspectiveCamera(FRUSTUM_CAMERA_FOV, camera.viewportWidth, camera.viewportHeight);
frustumCam.far = FRUSTUM_CAMERA_FAR;
frustumCam.update(true);
final Model frustumModel = FrustumCullingTest.createFrustumModel(frustumCam.frustum.planePoints);
frustumModel.materials.first().set(new ColorAttribute(ColorAttribute.AmbientLight, Color.WHITE));
disposables.add(frustumModel);
frustumInstance = new ModelInstance(frustumModel);
spriteBatch = new SpriteBatch();
disposables.add(spriteBatch);
shapeRenderer = new ShapeRenderer();
disposables.add(shapeRenderer);
oclBuffer = new OcclusionBuffer(OCL_BUFFER_EXTENTS[0], OCL_BUFFER_EXTENTS[0]);
disposables.add(oclBuffer);
occlusionCuller = new OcclusionCuller() {
@Override
public boolean isOccluder (btCollisionObject object) {
return (object.getCollisionFlags() & CF_OCCLUDER_OBJECT) != 0;
}
@Override
public void onObjectVisible (btCollisionObject object) {
visibleEntities.add(world.entities.get(object.getUserValue()));
}
};
disposables.add(occlusionCuller);
// Add occluder walls
final Model occluderModel = modelBuilder.createBox(OCCLUDER_DIM.x, OCCLUDER_DIM.y, OCCLUDER_DIM.z,
new Material(ColorAttribute.createDiffuse(Color.WHITE)),
VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal);
disposables.add(occluderModel);
world.addConstructor("wall", new BulletConstructor(occluderModel, 0, new btBoxShape(tmpV1.set(OCCLUDER_DIM).scl(0.5f))));
float y = OCCLUDER_DIM.y * 0.5f;
addOccluder("wall", 0, tmpV1.set(20, y, 0));
addOccluder("wall", -60, tmpV1.set(10, y, 20));
addOccluder("wall", 60, tmpV1.set(10, y, -20));
addOccluder("wall", 0, tmpV1.set(-20, y, 0));
addOccluder("wall", 60, tmpV1.set(-10, y, 20));
addOccluder("wall", -60, tmpV1.set(-10, y, -20));
// Add ground
final Model groundModel = modelBuilder.createBox(GROUND_DIM.x, GROUND_DIM.y, GROUND_DIM.z,
new Material(ColorAttribute.createDiffuse(Color.WHITE)),
VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal);
btCollisionShape groundShape = new btBoxShape(tmpV1.set(GROUND_DIM).scl(0.5f));
world.addConstructor("big_ground", new BulletConstructor(groundModel, 0, groundShape));
BulletEntity e = world.add("big_ground", 0, -GROUND_DIM.y * 0.5f, 0f);
e.body.setFriction(1f);
e.setColor(Color.FOREST);
// Occludee entity constructors. Scale models uniformly and set a default diffuse texture.
BoundingBox bb = new BoundingBox();
assets.finishLoadingAsset(DEFAULT_TEX_PATH);
TextureAttribute defaultTexture = new TextureAttribute(TextureAttribute.Diffuse,
assets.get(DEFAULT_TEX_PATH, Texture.class));
for (int i = 0; i < OCCLUDEE_PATHS_DYNAMIC.length; i++) {
String modelPath = OCCLUDEE_PATHS_DYNAMIC[i];
OCCLUDEE_PATHS_STATIC[i] = "static" + modelPath;
assets.finishLoadingAsset(modelPath);
Model model = assets.get(modelPath, Model.class);
if (!model.materials.first().has(TextureAttribute.Diffuse)) model.materials.first().set(defaultTexture);
Vector3 dim = model.calculateBoundingBox(bb).getDimensions(tmpV1);
float scaleFactor = OCCLUDEE_MAX_EXTENT / Math.max(dim.x, Math.max(dim.y, dim.z));
for (Node node : model.nodes)
node.scale.scl(scaleFactor);
btCollisionShape shape = new btBoxShape(dim.scl(scaleFactor * 0.5f));
world.addConstructor(modelPath, new BulletConstructor(model, 1, shape));
world.addConstructor(OCCLUDEE_PATHS_STATIC[i], new BulletConstructor(model, 0, shape));
}
// Add occludees
for (int i = 0; i < STARTING_OCCLUDEE_AMOUNT; i++)
addRandomOccludee(false);
}
@Override
public BulletWorld createWorld () {
btDefaultCollisionConfiguration collisionConfig = new btDefaultCollisionConfiguration();
btCollisionDispatcher dispatcher = new btCollisionDispatcher(collisionConfig);
btSequentialImpulseConstraintSolver solver = new btSequentialImpulseConstraintSolver();
broadphase = new btDbvtBroadphase();
btDiscreteDynamicsWorld collisionWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfig);
return new BulletWorld(collisionConfig, dispatcher, broadphase, solver, collisionWorld);
}
@Override
public void dispose () {
Gdx.input.setOnscreenKeyboardVisible(false);
GLProfiler.disable();
visibleEntities.clear();
rng.setSeed(0);
state = 0;
bufferExtentIndex = 0;
cullingPolicy = CullingPolicy.OCCLUSION;
super.dispose();
}
/** Checks if entity is inside camera frustum.
*
* @param entity An entity
* @return True if entity is inside camera frustum */
private boolean entityInFrustum (BulletEntity entity) {
entity.modelInstance.transform.getTranslation(tmpV1);
return frustumCam.frustum.sphereInFrustum(tmpV1.add(entity.boundingBox.getCenter(tmpV2)), entity.boundingBoxRadius);
}
/** Get the type name of a random occludee entity.
*
* @param dynamic If true, the name of a dynamic entity will be returned (mass > 0)
* @return Name of a random entity type */
private String getRandomOccludeeType (boolean dynamic) {
int i = rng.nextInt(OCCLUDEE_PATHS_STATIC.length);
return (dynamic) ? OCCLUDEE_PATHS_DYNAMIC[i] : OCCLUDEE_PATHS_STATIC[i];
}
@Override
public boolean keyTyped (char character) {
oclBuffer.clear();
switch (character) {
case '0':
for (int i = 0; i < KEY_SPAWN_OCCLUDEE_AMOUNT; i++)
addRandomOccludee(false);
break;
case '1':
cullingPolicy = cullingPolicy.next();
break;
case '2':
state ^= USE_FRUSTUM_CAM;
camera = ((state & USE_FRUSTUM_CAM) == USE_FRUSTUM_CAM) ? frustumCam : overviewCam;
break;
case '3':
state ^= PAUSE_FRUSTUM_CAM;
break;
case '4':
oclBuffer.dispose();
bufferExtentIndex = (bufferExtentIndex + 1) % OCL_BUFFER_EXTENTS.length;
int extent = OCL_BUFFER_EXTENTS[bufferExtentIndex];
oclBuffer = new OcclusionBuffer(extent, extent);
break;
case '5':
state ^= SHOW_DEBUG_IMAGE;
break;
case '6':
shadows = !shadows;
// Clear the old shadows
visibleEntities.clear();
renderShadows();
break;
}
return true;
}
@Override
public void render () {
super.render();
if ((state & SHOW_DEBUG_IMAGE) == SHOW_DEBUG_IMAGE) renderOclDebugImage();
performance.append(", Culling: ").append(cullingPolicy.name());
performance.append(", Visible: ").append(visibleEntities.size).append("/").append(world.entities.size);
performance.append(", Buffer: ").append(OCL_BUFFER_EXTENTS[bufferExtentIndex]).append("px ");
performance.append(", GL Draw calls: ").append(GLProfiler.drawCalls);
GLProfiler.reset();
}
private void renderOclDebugImage () {
TextureRegion oclDebugTexture = oclBuffer.drawDebugTexture();
spriteBatch.begin();
spriteBatch.draw(oclDebugTexture, 0, 0);
spriteBatch.end();
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
shapeRenderer.setColor(Color.DARK_GRAY);
shapeRenderer.rect(0, 0, oclDebugTexture.getRegionWidth(), oclDebugTexture.getRegionHeight());
shapeRenderer.end();
}
private void renderShadows () {
((DirectionalShadowLight)light).begin(Vector3.Zero, camera.direction);
shadowBatch.begin(((DirectionalShadowLight)light).getCamera());
world.render(shadowBatch, null, visibleEntities);
shadowBatch.end();
((DirectionalShadowLight)light).end();
}
@Override
protected void renderWorld () {
visibleEntities.clear();
if (world.performanceCounter != null) world.performanceCounter.start();
if (cullingPolicy == CullingPolicy.NONE) {
visibleEntities.addAll(world.entities);
} else if (cullingPolicy == CullingPolicy.SIMPLE) {
for (BulletEntity entity : world.entities)
if (entityInFrustum(entity)) visibleEntities.add(entity);
} else if (cullingPolicy == CullingPolicy.OCCLUSION) {
oclBuffer.clear();
occlusionCuller.performOcclusionCulling(broadphase, oclBuffer, frustumCam);
} else if (cullingPolicy == CullingPolicy.KDOP) {
occlusionCuller.performKDOPCulling(broadphase, frustumCam);
}
if (world.performanceCounter != null) world.performanceCounter.stop();
if (shadows) renderShadows();
modelBatch.begin(camera);
world.render(modelBatch, environment, visibleEntities);
if ((state & USE_FRUSTUM_CAM) != USE_FRUSTUM_CAM) modelBatch.render(frustumInstance);
modelBatch.end();
}
@Override
public boolean tap (float x, float y, int count, int button) {
BulletEntity entity = shoot(getRandomOccludeeType(true), x, y, 30f);
entity.setColor(Color.WHITE);
return true;
}
@Override
public void update () {
super.update();
// Transform the frustum camera
if ((state & PAUSE_FRUSTUM_CAM) == PAUSE_FRUSTUM_CAM) return;
final float dt = Gdx.graphics.getDeltaTime();
frustumInstance.transform.idt().rotate(Vector3.Y, frustumCamAngleY = (frustumCamAngleY + dt * FRUSTUM_ANG_SPEED) % 360);
frustumCam.direction.set(0, 0, -1);
frustumCam.up.set(Vector3.Y);
frustumCam.position.set(Vector3.Zero);
frustumCam.rotate(frustumInstance.transform);
float frustumCamPosY = frustumCamPos.y;
frustumCamPos.add(tmpV1.set(Vector3.Y).crs(tmpV2.set(frustumCamPos).nor()).scl(dt * FRUSTUM_LIN_SPEED)).nor()
.scl(FRUSTUM_MOVE_RADIUS);
frustumCamPos.y = frustumCamPosY;
frustumCam.position.set(frustumCamPos);
frustumInstance.transform.setTranslation(frustumCamPos);
frustumCam.update();
}
}