/*******************************************************************************
* Copyright 2015 See AUTHORS file.
* <p/>
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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.mygdx.game.scene;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.assets.loaders.ModelLoader;
import com.badlogic.gdx.assets.loaders.TextureLoader;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
import com.badlogic.gdx.graphics.g3d.environment.BaseLight;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.environment.PointLight;
import com.badlogic.gdx.graphics.g3d.environment.SpotLight;
import com.badlogic.gdx.graphics.g3d.model.Node;
import com.badlogic.gdx.graphics.g3d.model.NodePart;
import com.badlogic.gdx.graphics.g3d.particles.ParticleEffect;
import com.badlogic.gdx.graphics.g3d.particles.ParticleEffectLoader;
import com.badlogic.gdx.graphics.g3d.particles.ParticleSystem;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.BoundingBox;
import com.badlogic.gdx.physics.bullet.collision.btCollisionShape;
import com.badlogic.gdx.physics.bullet.dynamics.btHingeConstraint;
import com.badlogic.gdx.utils.*;
import com.mygdx.game.GameEngine;
import com.mygdx.game.blender.BlenderAssetManager;
import com.mygdx.game.blender.objects.BlenderCamera;
import com.mygdx.game.blender.objects.BlenderEmpty;
import com.mygdx.game.blender.objects.BlenderLight;
import com.mygdx.game.blender.objects.BlenderModel;
import com.mygdx.game.objects.*;
import com.mygdx.game.pathfinding.NavMesh;
import com.mygdx.game.utilities.Constants;
import com.mygdx.game.utilities.Entity;
import com.mygdx.game.utilities.GhostCamera;
import com.mygdx.game.utilities.VertexColorTextureBlend;
import java.util.Comparator;
import static com.mygdx.game.utilities.Constants.V3_DOWN;
/**
* A GameScene contains models, lights, physics bodies, navigation mesh and particle effects.
* It is read from the json files generated by the Blender export script.
*
* @author jsjolund
*/
public class GameScene implements Disposable {
/**
* For now, this is used to order the MeshParts of the navmesh in layers
*/
private class NavMeshNodeSorter implements Comparator<NodePart> {
@Override
public int compare(NodePart a, NodePart b) {
return a.material.id.compareTo(b.material.id);
}
}
private final static String TAG = "GameScene";
public final BlenderAssetManager assets;
private final ArrayMap<String, Array<GameObject>> gameObjects = new ArrayMap<String, Array<GameObject>>();
private final ObjectMap<String, GameObjectBlueprint> sharedBlueprints;
public NavMesh navMesh;
public Entity navmeshBody;
public BoundingBox worldBounds = new BoundingBox();
public Array<BaseLight<?>> lights = new Array<BaseLight<?>>();
public Vector3 shadowCameraDirection = new Vector3(V3_DOWN);
private BlenderCamera sceneCamera;
private final Array<ParticleEffect> particleEffects = new Array<ParticleEffect>();
public GameScene(ModelLoader.ModelParameters modelParameters,
TextureLoader.TextureParameter textureParameter,
ParticleEffectLoader.ParticleEffectLoadParameter pfxParameter,
String pfxPath, String modelPath, String modelExt, ObjectMap<String, GameObjectBlueprint> sharedBlueprints) {
this.sharedBlueprints = sharedBlueprints;
this.assets = new BlenderAssetManager(modelParameters, textureParameter, pfxParameter,
pfxPath, modelPath, modelExt);
}
public void addGameObject(GameObject obj) {
if (!gameObjects.containsKey(obj.name)) {
gameObjects.put(obj.name, new Array<GameObject>());
}
gameObjects.get(obj.name).add(obj);
}
public void getGameModelById(String id, Array<GameObject> out) {
out.addAll(gameObjects.get(id));
}
private void setVColorBlendAttributes() {
Array<String> modelsIdsInScene = assets.getPlaceholderIdsByType(BlenderModel.class);
Array<BlenderModel> instancesWithId = new Array<BlenderModel>();
for (String id : modelsIdsInScene) {
instancesWithId.clear();
assets.getPlaceholders(id, BlenderModel.class, instancesWithId);
for (BlenderModel blenderModel : instancesWithId) {
// Maybe check if
// renderable.meshPart.mesh.getVertexAttribute(VertexAttributes.Usage.ColorUnpacked) != null
if (blenderModel.custom_properties.containsKey("v_color_material_blend")) {
Model model = assets.getAsset(id, Model.class);
String redMaterialName = blenderModel.custom_properties.get("v_color_material_red");
String greenMaterialName = blenderModel.custom_properties.get("v_color_material_green");
String blueMaterialName = blenderModel.custom_properties.get("v_color_material_blue");
TextureAttribute redTexAttr = (TextureAttribute)
model.getMaterial(redMaterialName).get(TextureAttribute.Diffuse);
TextureAttribute greenTexAttr = (TextureAttribute)
model.getMaterial(greenMaterialName).get(TextureAttribute.Diffuse);
TextureAttribute blueTexAttr = (TextureAttribute)
model.getMaterial(blueMaterialName).get(TextureAttribute.Diffuse);
VertexColorTextureBlend redAttribute =
new VertexColorTextureBlend(VertexColorTextureBlend.Red,
redTexAttr.textureDescription.texture);
VertexColorTextureBlend greenAttribute =
new VertexColorTextureBlend(VertexColorTextureBlend.Green,
greenTexAttr.textureDescription.texture);
VertexColorTextureBlend blueAttribute =
new VertexColorTextureBlend(VertexColorTextureBlend.Blue,
blueTexAttr.textureDescription.texture);
for (Node node : model.nodes) {
for (NodePart nodePart : node.parts) {
nodePart.material.set(redAttribute, greenAttribute, blueAttribute);
}
}
break;
}
}
}
}
public void spawnGameObjectsFromPlaceholders() {
Gdx.app.debug(TAG, "Spawning predefined game objects.");
// TODO: Clean this up
Array<GameObjectBlueprint> blueprints = new Array<GameObjectBlueprint>();
Array<String> modelsIdsInScene = assets.getPlaceholderIdsByType(BlenderModel.class);
Array<BlenderEmpty> emptiesWithId = new Array<BlenderEmpty>();
Array<BlenderModel> instancesWithId = new Array<BlenderModel>();
Vector3 tmpScale = new Vector3();
setVColorBlendAttributes();
for (String id : modelsIdsInScene) {
// TODO: handle this in a better way
if (id.equals("navmesh")) {
continue;
}
emptiesWithId.clear();
instancesWithId.clear();
Model model = assets.getAsset(id, Model.class);
assets.getPlaceholders(id, BlenderModel.class, instancesWithId);
assets.getPlaceholders(id, BlenderEmpty.class, emptiesWithId);
boolean shapeIsDefined = emptiesWithId.size > 0;
// Collision shape must be of correct size, but we do not want to create
// new btCollisionShapes if we can reuse old ones.
// Create an unscaled version of the shape to be shared if dimensions match.
BlenderModel firstInstance = instancesWithId.first();
tmpScale.set(firstInstance.scale);
firstInstance.scale.set(1, 1, 1);
GameObjectBlueprint refBp;
boolean refUsed = false;
refBp = (shapeIsDefined) ?
new GameObjectBlueprint(firstInstance, model, emptiesWithId.first()) :
new GameObjectBlueprint(firstInstance, model);
firstInstance.scale.set(tmpScale);
for (BlenderModel modelPlaceholder : instancesWithId) {
// If model is not scaled, reuse the collision shape
GameObjectBlueprint bp;
if (modelPlaceholder.scale.epsilonEquals(1, 1, 1, 0.00001f)) {
bp = new GameObjectBlueprint(modelPlaceholder, model,
refBp.shape, refBp.mass, refBp.shapeType);
refUsed = true;
} else {
bp = (shapeIsDefined) ?
new GameObjectBlueprint(modelPlaceholder, model, emptiesWithId.first()) :
new GameObjectBlueprint(modelPlaceholder, model);
}
blueprints.add(bp);
}
if (!refUsed) {
refBp.dispose();
}
}
Array<String> emptiesIdsInScene = assets.getPlaceholderIdsByType(BlenderEmpty.class);
for (String id : modelsIdsInScene) {
emptiesIdsInScene.removeValue(id, false);
}
for (String id : emptiesIdsInScene) {
emptiesWithId.clear();
assets.getPlaceholders(id, BlenderEmpty.class, emptiesWithId);
for (BlenderEmpty blenderEmpty : emptiesWithId) {
blueprints.add(new GameObjectBlueprint(blenderEmpty));
}
}
Array<BlenderLight> sceneLights = new Array<BlenderLight>();
assets.getAllPlaceholders(BlenderLight.class, sceneLights);
for (BlenderLight light : sceneLights) {
spawnLight(light);
}
Array<BlenderCamera> sceneCameras = new Array<BlenderCamera>();
assets.getAllPlaceholders(BlenderCamera.class, sceneCameras);
sceneCamera = sceneCameras.first();
Array<BlenderModel> navmeshObjects = new Array<BlenderModel>();
assets.getPlaceholders("navmesh", BlenderModel.class, navmeshObjects);
GameObjectBlueprint navmeshBp = new GameObjectBlueprint();
navmeshBp.name = "navmesh";
navmeshBp.model = assets.getAsset("navmesh", Model.class);
navmeshBp.position = navmeshObjects.first().position;
navmeshBp.rotation = navmeshObjects.first().rotation;
navmeshBp.scale = navmeshObjects.first().scale;
setNavmesh(navmeshBp);
for (GameObjectBlueprint blueprint : blueprints) {
spawnFromBlueprint(blueprint);
}
}
public HumanCharacter spawnHuman(String sharedBlueprintId, Vector3 initialPosition) {
return spawnHuman(sharedBlueprintId, initialPosition, Float.NaN);
}
public HumanCharacter spawnHuman(String sharedBlueprintId, Vector3 initialPosition, float initialOrientation) {
GameObjectBlueprint bp = sharedBlueprints.get(sharedBlueprintId);
HumanCharacter obj = new HumanCharacter(
bp.model, bp.name,
initialPosition, bp.rotation, bp.scale,
bp.shape, bp.mass,
bp.belongsToFlag, bp.collidesWithFlag,
bp.callback, bp.noDeactivate,
bp.ragdollEmpties, bp.armatureNodeId);
setSteerableOrientation(obj, initialOrientation);
obj.updateSteerableData(this);
addGameObject(obj);
return obj;
}
public DogCharacter spawnDog(String sharedBlueprintId, String dogName, Vector3 initialPosition) {
return spawnDog(sharedBlueprintId, dogName, initialPosition, Float.NaN);
}
public DogCharacter spawnDog(String sharedBlueprintId, String dogName, Vector3 initialPosition, float initialOrientation) {
GameObjectBlueprint bp = sharedBlueprints.get(sharedBlueprintId);
DogCharacter obj = new DogCharacter(
bp.model, bp.name,
initialPosition, bp.rotation, bp.scale,
bp.shape, bp.mass,
bp.belongsToFlag, bp.collidesWithFlag,
bp.callback, bp.noDeactivate);
setSteerableOrientation(obj, initialOrientation);
obj.updateSteerableData(this);
obj.dogName = dogName;
addGameObject(obj);
return obj;
}
public GameModelBody spawnGameModelBody(String sharedBlueprintId, Vector3 initialPosition) {
GameObjectBlueprint bp = sharedBlueprints.get(sharedBlueprintId);
return spawnGameModelBody(bp, initialPosition);
}
public Stick spawnStick(String sharedBlueprintId, Vector3 initialPosition) {
GameObjectBlueprint bp = sharedBlueprints.get(sharedBlueprintId);
Stick obj = new Stick(
bp.model, bp.name,
initialPosition, bp.rotation, bp.scale,
bp.shape, bp.mass,
bp.belongsToFlag, bp.collidesWithFlag,
bp.callback, bp.noDeactivate);
obj.visibleOnLayers.clear();
obj.visibleOnLayers.or(bp.visibleOnLayers);
addGameObject(obj);
return obj;
}
public GameModelBody spawnGameModelBody(GameObjectBlueprint bp, Vector3 initialPosition) {
GameModelBody obj = new GameModelBody(
bp.model, bp.name,
initialPosition, bp.rotation, bp.scale,
bp.shape, bp.mass,
bp.belongsToFlag, bp.collidesWithFlag,
bp.callback, bp.noDeactivate);
obj.visibleOnLayers.clear();
obj.visibleOnLayers.or(bp.visibleOnLayers);
if (bp.name.startsWith("door")) {
btHingeConstraint hinge = new btHingeConstraint(
obj.body, new Vector3(0, 0, -obj.halfExtents.z), Vector3.Y);
hinge.enableAngularMotor(true, 0, 0.1f);
obj.constraints.add(hinge);
}
addGameObject(obj);
return obj;
}
private static void setSteerableOrientation(SteerableBody entity, float orientation) {
if (Float.isNaN(orientation)) {
orientation = MathUtils.random(-Constants.PI, Constants.PI);
}
entity.setOrientation(orientation);
Gdx.app.debug(TAG, entity.getClass().getSimpleName() + ": Orientation in radians is " + entity.getOrientation() + " should be " + orientation);
}
public Billboard spawnSelectionBillboard(String sharedBlueprintId, Camera camera) {
GameObjectBlueprint bp = sharedBlueprints.get(sharedBlueprintId);
Billboard obj = new Billboard(bp.model, bp.name, camera, true, new Matrix4(), new Vector3());
addGameObject(obj);
return obj;
}
private GameObject spawnFromBlueprint(GameObjectBlueprint bp) {
if (bp.pfx != null) {
ParticleEffect originalEffect = assets.getAsset(bp.pfx, ParticleEffect.class);
// we cannot use the originalEffect, we must make a copy each time we create new particle effect
ParticleEffect effect = originalEffect.copy();
particleEffects.add(effect);
effect.translate(bp.position);
effect.rotate(Vector3.X, 180);
effect.init();
effect.start();
ParticleSystem particleSystem = ParticleSystem.get();
particleSystem.add(effect);
return null;
}
if (bp.model != null && bp.shape != null) {
return spawnGameModelBody(bp, bp.position);
} else if (bp.model == null && bp.shape != null) {
InvisibleBody obj = new InvisibleBody(bp.name, bp.shape, bp.mass,
bp.position, bp.rotation,
bp.belongsToFlag, bp.collidesWithFlag,
bp.callback, bp.noDeactivate);
addGameObject(obj);
return obj;
} else if (bp.model != null) {
GameModel obj = new GameModel(bp.model, bp.name, bp.position, bp.rotation, bp.scale);
obj.visibleOnLayers.clear();
obj.visibleOnLayers.or(bp.visibleOnLayers);
addGameObject(obj);
return obj;
} else {
throw new GdxRuntimeException("Could not read blueprint " + bp);
}
}
public void getGameObjects(Array<GameObject> out) {
for (Array<GameObject> objs : gameObjects.values()) {
out.addAll(objs);
}
}
public void dispose() {
ParticleSystem particleSystem = ParticleSystem.get();
for (ParticleEffect pfx : particleEffects) {
particleSystem.remove(pfx);
pfx.dispose();
}
particleEffects.clear();
navMesh.dispose();
for (Array<GameObject> objs : gameObjects.values()) {
for (GameObject obj : objs) {
obj.dispose();
}
}
gameObjects.clear();
assets.dispose();
}
/**
* Creates and adds the navmesh to this scene.
*/
private void setNavmesh(GameObjectBlueprint bp) {
// We need to set the node transforms before calculating the navmesh shape
GameModel gameModel = new GameModel(bp.model, bp.name, bp.position, bp.rotation, bp.scale);
Array<NodePart> nodes = gameModel.modelInstance.model.getNode("navmesh").parts;
// Sort the model meshParts array according to material name
nodes.sort(new NavMeshNodeSorter());
// The model transform must be applied to the meshparts for shape generation to work correctly.
gameModel.modelInstance.calculateTransforms();
Matrix4 transform = new Matrix4();
for (Node node : gameModel.modelInstance.nodes) {
transform.set(node.globalTransform).inv();
for (NodePart nodePart : node.parts) {
nodePart.meshPart.mesh.transform(transform);
}
}
navMesh = new NavMesh(gameModel.modelInstance.model);
btCollisionShape shape = navMesh.getShape();
navmeshBody = new InvisibleBody("navmesh",
shape, 0, gameModel.modelInstance.transform, GameEngine.NAVMESH_FLAG, GameEngine.NAVMESH_FLAG, false, false);
worldBounds.set(gameModel.boundingBox);
gameModel.dispose();
}
private void spawnLight(BlenderLight bLight) {
Vector3 direction = new Vector3(V3_DOWN);
direction.rotate(Vector3.X, bLight.rotation.x);
direction.rotate(Vector3.Z, bLight.rotation.z);
direction.rotate(Vector3.Y, bLight.rotation.y);
// TODO: Don't know how to map lamp intensity in blender to libgdx correctly
float intensity = bLight.lamp_energy;
float cutoffAngle = bLight.lamp_falloff;
float exponent = 1;
if (bLight.type.equals("PointLamp")) {
BaseLight<?> light = new PointLight().set(
bLight.lamp_color.r, bLight.lamp_color.g, bLight.lamp_color.b,
bLight.position, bLight.lamp_energy);
lights.add(light);
} else if (bLight.type.equals("SpotLamp")) {
BaseLight<?> light = new SpotLight().set(bLight.lamp_color, bLight.position,
direction, intensity, cutoffAngle, exponent);
lights.add(light);
} else if (bLight.type.equals("SunLamp")) {
BaseLight<?> light = new DirectionalLight().set(
bLight.lamp_color.r,
bLight.lamp_color.g,
bLight.lamp_color.b,
direction.x, direction.y, direction.z);
lights.add(light);
shadowCameraDirection.set(direction);
}
}
public void setToSceneCamera(GhostCamera camera) {
Vector3 direction = new Vector3(V3_DOWN);
direction.rotate(Vector3.X, sceneCamera.rotation.x);
direction.rotate(Vector3.Z, sceneCamera.rotation.z);
direction.rotate(Vector3.Y, sceneCamera.rotation.y);
direction.nor();
camera.fieldOfView = sceneCamera.fov;
camera.targetPosition.set(sceneCamera.position);
camera.targetDirection.set(direction);
camera.targetUp.set(Vector3.Y);
camera.snapToTarget();
camera.update();
}
}