/*
* Copyright 2012 Benjamin Glatzel <benjamin.glatzel@me.com>
*
* 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 org.terasology.rendering.physics;
import java.nio.FloatBuffer;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.vecmath.Matrix3f;
import javax.vecmath.Matrix4f;
import javax.vecmath.Vector3d;
import javax.vecmath.Vector3f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.spout.api.geo.cuboid.Chunk;
import org.spout.api.material.BlockMaterial;
import org.spout.api.material.MaterialRegistry;
import org.terasology.asset.AssetType;
import org.terasology.asset.AssetUri;
import org.terasology.components.CharacterMovementComponent;
import org.terasology.components.InventoryComponent;
import org.terasology.components.ItemComponent;
import org.terasology.components.world.LocationComponent;
import org.terasology.entityFactory.BlockItemFactory;
import org.terasology.entitySystem.EntityManager;
import org.terasology.entitySystem.EntityRef;
import org.terasology.entitySystem.PrefabManager;
import org.terasology.events.inventory.ReceiveItemEvent;
import org.terasology.game.CoreRegistry;
import org.terasology.logic.manager.AudioManager;
import org.terasology.rendering.interfaces.IGameObject;
import org.terasology.rendering.primitives.ChunkMesh;
import org.terasology.rendering.world.WorldRenderer;
import org.terasology.teraspout.TeraBlock;
import org.terasology.teraspout.TeraChunk;
import org.terasology.teraspout.TeraSpout;
import org.terasology.utilities.FastRandom;
import com.beust.jcommander.internal.Lists;
import com.bulletphysics.collision.broadphase.BroadphaseInterface;
import com.bulletphysics.collision.broadphase.DbvtBroadphase;
import com.bulletphysics.collision.dispatch.CollisionDispatcher;
import com.bulletphysics.collision.dispatch.CollisionObject;
import com.bulletphysics.collision.dispatch.DefaultCollisionConfiguration;
import com.bulletphysics.collision.shapes.BoxShape;
import com.bulletphysics.collision.shapes.BvhTriangleMeshShape;
import com.bulletphysics.collision.shapes.IndexedMesh;
import com.bulletphysics.collision.shapes.TriangleIndexVertexArray;
import com.bulletphysics.dynamics.DiscreteDynamicsWorld;
import com.bulletphysics.dynamics.RigidBody;
import com.bulletphysics.dynamics.RigidBodyConstructionInfo;
import com.bulletphysics.dynamics.constraintsolver.SequentialImpulseConstraintSolver;
import com.bulletphysics.linearmath.DefaultMotionState;
import com.bulletphysics.linearmath.Transform;
/**
* Renders blocks using the Bullet physics library.
*
* @author Benjamin Glatzel <benjamin.glatzel@me.com>
*/
public class BulletPhysicsRenderer implements IGameObject {
private class BlockRigidBody extends RigidBody implements Comparable<BlockRigidBody> {
private final short _type;
private final long _createdAt;
public boolean _temporary = false;
public boolean _picked = false;
public BlockRigidBody(RigidBodyConstructionInfo constructionInfo, short type) {
super(constructionInfo);
_type = type;
_createdAt = System.currentTimeMillis();
}
public float distanceToEntity(Vector3f pos) {
Transform t = new Transform();
getMotionState().getWorldTransform(t);
Matrix4f tMatrix = new Matrix4f();
t.getMatrix(tMatrix);
Vector3f blockPlayer = new Vector3f();
tMatrix.get(blockPlayer);
blockPlayer.sub(pos);
return blockPlayer.length();
}
public long calcAgeInMs() {
return System.currentTimeMillis() - _createdAt;
}
public short getType() {
return _type;
}
@Override
public int compareTo(BlockRigidBody blockRigidBody) {
if (blockRigidBody.calcAgeInMs() == calcAgeInMs()) {
return 0;
} else if (blockRigidBody.calcAgeInMs() > calcAgeInMs()) {
return 1;
} else {
return -1;
}
}
}
public enum BLOCK_SIZE {
FULL_SIZE,
HALF_SIZE,
QUARTER_SIZE
}
private static final int MAX_TEMP_BLOCKS = 128;
private Logger _logger = Logger.getLogger(getClass().getName());
private final LinkedList<RigidBody> _insertionQueue = new LinkedList<RigidBody>();
private final LinkedList<BlockRigidBody> _blocks = new LinkedList<BlockRigidBody>();
private HashSet<RigidBody> _chunks = new HashSet<RigidBody>();
private final BoxShape _blockShape = new BoxShape(new Vector3f(0.5f, 0.5f, 0.5f));
private final BoxShape _blockShapeHalf = new BoxShape(new Vector3f(0.25f, 0.25f, 0.25f));
private final BoxShape _blockShapeQuarter = new BoxShape(new Vector3f(0.125f, 0.125f, 0.125f));
private final CollisionDispatcher _dispatcher;
private final BroadphaseInterface _broadphase;
private final DefaultCollisionConfiguration _defaultCollisionConfiguration;
private final SequentialImpulseConstraintSolver _sequentialImpulseConstraintSolver;
private final DiscreteDynamicsWorld _discreteDynamicsWorld;
private final BlockItemFactory _blockItemFactory;
private FastRandom _random = new FastRandom();
private final WorldRenderer _parent;
public BulletPhysicsRenderer(WorldRenderer parent) {
_broadphase = new DbvtBroadphase();
_defaultCollisionConfiguration = new DefaultCollisionConfiguration();
_dispatcher = new CollisionDispatcher(_defaultCollisionConfiguration);
_sequentialImpulseConstraintSolver = new SequentialImpulseConstraintSolver();
_discreteDynamicsWorld = new DiscreteDynamicsWorld(_dispatcher, _broadphase, _sequentialImpulseConstraintSolver, _defaultCollisionConfiguration);
_discreteDynamicsWorld.setGravity(new Vector3f(0f, -10f, 0f));
_parent = parent;
_blockItemFactory = new BlockItemFactory(CoreRegistry.get(EntityManager.class), CoreRegistry.get(PrefabManager.class));
}
public BlockRigidBody[] addLootableBlocks(Vector3f position, TeraBlock block) {
BlockRigidBody result[] = new BlockRigidBody[8];
for (int i = 0; i < block.getLootAmount(); i++) {
// Position the smaller blocks
Vector3f offsetPossition = new Vector3f((float) _random.randomDouble() * 0.5f, (float) _random.randomDouble() * 0.5f, (float) _random.randomDouble() * 0.5f);
offsetPossition.add(position);
result[i] = addBlock(offsetPossition, block.getId(), new Vector3f(0.0f, 4000f, 0.0f), BLOCK_SIZE.QUARTER_SIZE, false);
}
return result;
}
public BlockRigidBody addTemporaryBlock(Vector3f position, short type, BLOCK_SIZE size) {
BlockRigidBody result = addBlock(position, type, size, true);
return result;
}
public BlockRigidBody addTemporaryBlock(Vector3f position, short type, Vector3f impulse, BLOCK_SIZE size) {
BlockRigidBody result = addBlock(position, type, impulse, size, true);
return result;
}
public BlockRigidBody addBlock(Vector3f position, short type, BLOCK_SIZE size, boolean temporary) {
return addBlock(position, type, new Vector3f(0f, 0f, 0f), size, temporary);
}
/**
* Adds a new physics block to be rendered as a rigid body. Translucent blocks are ignored.
*
* @param position The position
* @param type The block type
* @param impulse An impulse
* @param size The size of the block
* @return The created rigid body (if any)
*/
public synchronized BlockRigidBody addBlock(Vector3f position, short type, Vector3f impulse, BLOCK_SIZE size, boolean temporary) {
if (temporary && _blocks.size() > MAX_TEMP_BLOCKS)
removeTemporaryBlocks();
BoxShape shape = _blockShape;
TeraBlock block = TeraSpout.getInstance().getBlock((BlockMaterial) MaterialRegistry.get(type));
if (block.isTranslucent())
return null;
if (size == BLOCK_SIZE.HALF_SIZE)
shape = _blockShapeHalf;
else if (size == BLOCK_SIZE.QUARTER_SIZE)
shape = _blockShapeQuarter;
Matrix3f rot = new Matrix3f();
rot.setIdentity();
DefaultMotionState blockMotionState = new DefaultMotionState(new Transform(new Matrix4f(rot, position, 1.0f)));
Vector3f fallInertia = new Vector3f();
shape.calculateLocalInertia(block.getMass(), fallInertia);
RigidBodyConstructionInfo blockCI = new RigidBodyConstructionInfo(block.getMass(), blockMotionState, shape, fallInertia);
BlockRigidBody rigidBlock = new BlockRigidBody(blockCI, type);
rigidBlock.setRestitution(0.0f);
rigidBlock.setAngularFactor(0.5f);
rigidBlock.setFriction(0.5f);
rigidBlock._temporary = temporary;
// Apply impulse
rigidBlock.applyImpulse(impulse, new Vector3f(0.0f, 0.0f, 0.0f));
_insertionQueue.add(rigidBlock);
return rigidBlock;
}
public void updateChunks() {
// List<TeraChunk> chunks = CoreRegistry.get(WorldRenderer.class).getChunksInProximity();
List<TeraChunk> chunks = Lists.newArrayList(); // TODO
HashSet<RigidBody> newBodies = new HashSet<RigidBody>();
boolean updatedThisFrame = false;
for (int i = 0; i < 32 && i < chunks.size(); i++) {
final TeraChunk chunk = chunks.get(i);
if (chunk != null) {
if (!updatedThisFrame && updateRigidBody(chunk)) {
updatedThisFrame = true;
}
RigidBody c = chunk.getRigidBody();
if (c != null) {
newBodies.add(c);
if (!_chunks.contains(c)) {
_discreteDynamicsWorld.addRigidBody(c);
}
}
}
}
for (RigidBody body : _chunks) {
if (!newBodies.contains(body)) {
_discreteDynamicsWorld.removeRigidBody(body);
}
}
_chunks = newBodies;
}
private boolean updateRigidBody(TeraChunk chunk) {
if (chunk.getRigidBody() != null || chunk.getMesh() == null)
return false;
// TODO: Lock on chunk meshes
TriangleIndexVertexArray vertexArray = new TriangleIndexVertexArray();
int tris = 0;
ChunkMesh[] meshes = chunk.getMesh();
for (int k = 0; k < TeraChunk.VERTICAL_SEGMENTS; k++) {
ChunkMesh mesh = meshes[k];
if (mesh != null) {
IndexedMesh indexedMesh = mesh._indexedMesh;
if (indexedMesh != null) {
tris += mesh._indexedMesh.numTriangles;
vertexArray.addIndexedMesh(indexedMesh);
}
mesh._indexedMesh = null;
}
}
// TODO: Deal with this situation better
if (tris == 0) {
return false;
}
try {
BvhTriangleMeshShape shape = new BvhTriangleMeshShape(vertexArray, true);
Matrix3f rot = new Matrix3f();
rot.setIdentity();
DefaultMotionState blockMotionState = new DefaultMotionState(new Transform(new Matrix4f(rot, new Vector3f((float) chunk.getPos().x * Chunk.BLOCKS.SIZE, (float) chunk.getPos().y * Chunk.BLOCKS.SIZE, (float) chunk.getPos().z * Chunk.BLOCKS.SIZE), 1.0f)));
RigidBodyConstructionInfo blockConsInf = new RigidBodyConstructionInfo(0, blockMotionState, shape, new Vector3f());
RigidBody rigidBody = new RigidBody(blockConsInf);
chunk.setRigidBody(rigidBody);
} catch (Exception e) {
_logger.log(Level.WARNING, "Chunk failed to create rigid body.", e);
}
return true;
}
@Override
public void render() {
FloatBuffer mBuffer = BufferUtils.createFloatBuffer(16);
float[] mFloat = new float[16];
GL11.glPushMatrix();
Vector3d cameraPosition = _parent.getActiveCamera().getPosition();
GL11.glTranslated(-cameraPosition.x, -cameraPosition.y, -cameraPosition.z);
List<CollisionObject> collisionObjects = _discreteDynamicsWorld.getCollisionObjectArray();
for (CollisionObject co : collisionObjects) {
if (co.getClass().equals(BlockRigidBody.class)) {
BlockRigidBody br = (BlockRigidBody) co;
TeraBlock block = TeraSpout.getInstance().getBlock((BlockMaterial) MaterialRegistry.get(br.getType()));
Transform t = new Transform();
br.getMotionState().getWorldTransform(t);
t.getOpenGLMatrix(mFloat);
mBuffer.put(mFloat);
mBuffer.flip();
GL11.glPushMatrix();
GL11.glMultMatrix(mBuffer);
if (br.getCollisionShape() == _blockShapeHalf)
GL11.glScalef(0.5f, 0.5f, 0.5f);
else if (br.getCollisionShape() == _blockShapeQuarter)
GL11.glScalef(0.25f, 0.25f, 0.25f);
block.renderWithLightValue(_parent.getRenderingLightValueAt(t.origin));
GL11.glPopMatrix();
}
}
GL11.glPopMatrix();
}
@Override
public void update(float delta) {
addQueuedBodies();
try {
_discreteDynamicsWorld.stepSimulation(delta, 3);
} catch (Exception e) {
_logger.log(Level.WARNING, "Somehow Bullet Physics managed to throw an exception again. Go along: " + e.toString());
}
updateChunks();
removeTemporaryBlocks();
checkForLootedBlocks();
}
private synchronized void addQueuedBodies() {
while (!_insertionQueue.isEmpty()) {
RigidBody body = _insertionQueue.poll();
if (body instanceof BlockRigidBody)
_blocks.addFirst((BlockRigidBody) body);
_discreteDynamicsWorld.addRigidBody(body);
}
}
private void checkForLootedBlocks() {
for (int i = _blocks.size() - 1; i >= 0; i--) {
BlockRigidBody b = _blocks.get(i);
if (b._temporary) {
continue;
}
EntityRef closestCreature = EntityRef.NULL;
Vector3f closestPosition = new Vector3f();
float closestDist = Float.MAX_VALUE;
// TODO: We should have some other component for things that can pick up items? CreatureComponent? ItemMagnetComponent?
for (EntityRef creature : CoreRegistry.get(EntityManager.class).iteratorEntities(InventoryComponent.class, CharacterMovementComponent.class, LocationComponent.class)) {
Vector3f pos = creature.getComponent(LocationComponent.class).getWorldPosition();
float dist = b.distanceToEntity(pos);
if (dist < closestDist) {
closestDist = dist;
closestCreature = creature;
closestPosition.set(pos);
}
}
if (closestDist < 8 && !b._picked) {
b._picked = true;
}
// Block was marked as being picked
if (b._picked && closestDist < 32.0f) {
// Animate the movement in direction of the player
if (closestDist > 1.0) {
Transform t = new Transform();
b.getMotionState().getWorldTransform(t);
Matrix4f tMatrix = new Matrix4f();
t.getMatrix(tMatrix);
Vector3f blockPlayer = new Vector3f();
tMatrix.get(blockPlayer);
blockPlayer.sub(new Vector3f(closestPosition));
blockPlayer.normalize();
blockPlayer.scale(-16000f);
b.applyCentralImpulse(blockPlayer);
} else {
// TODO: Handle full inventories
// TODO: Loot blocks should be entities
// Block was looted (and reached the player)
TeraBlock block = TeraSpout.getInstance().getBlock((BlockMaterial) MaterialRegistry.get(b.getType()));
EntityRef blockItem = _blockItemFactory.newInstance(block.getBlockFamily());
closestCreature.send(new ReceiveItemEvent(blockItem));
ItemComponent itemComp = blockItem.getComponent(ItemComponent.class);
if (itemComp != null && !itemComp.container.exists()) {
blockItem.destroy();
}
AudioManager.play(new AssetUri(AssetType.SOUND, "engine:Loot"));
_blocks.remove(i);
_discreteDynamicsWorld.removeRigidBody(b);
}
}
}
}
private void removeTemporaryBlocks() {
if (_blocks.size() > 0) {
for (int i = _blocks.size() - 1; i >= 0; i--) {
if (!_blocks.get(i)._temporary) {
continue;
}
if (!_blocks.get(i).isActive() || _blocks.get(i).calcAgeInMs() > 10000 || _blocks.size() > MAX_TEMP_BLOCKS) {
_discreteDynamicsWorld.removeRigidBody(_blocks.get(i));
_blocks.remove(i);
}
}
}
}
}