/*
* Copyright 2014 MovingBlocks
*
* 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.logic;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.utilities.Assets;
import org.terasology.config.Config;
import org.terasology.entitySystem.entity.EntityManager;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.entity.lifecycleEvents.OnActivatedComponent;
import org.terasology.entitySystem.event.ReceiveEvent;
import org.terasology.entitySystem.systems.BaseComponentSystem;
import org.terasology.entitySystem.systems.RegisterMode;
import org.terasology.entitySystem.systems.RegisterSystem;
import org.terasology.entitySystem.systems.RenderSystem;
import org.terasology.entitySystem.systems.UpdateSubscriberSystem;
import org.terasology.logic.location.Location;
import org.terasology.logic.location.LocationComponent;
import org.terasology.math.AABB;
import org.terasology.math.MatrixUtils;
import org.terasology.math.geom.BaseQuat4f;
import org.terasology.math.geom.BaseVector3f;
import org.terasology.math.geom.Matrix4f;
import org.terasology.math.geom.Quat4f;
import org.terasology.math.geom.Vector3f;
import org.terasology.registry.In;
import org.terasology.rendering.assets.animation.MeshAnimation;
import org.terasology.rendering.assets.animation.MeshAnimationFrame;
import org.terasology.rendering.assets.material.Material;
import org.terasology.rendering.assets.skeletalmesh.Bone;
import org.terasology.rendering.opengl.OpenGLSkeletalMesh;
import org.terasology.rendering.world.WorldRenderer;
import java.nio.FloatBuffer;
import java.util.List;
import java.util.Random;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.glBegin;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnd;
import static org.lwjgl.opengl.GL11.glLineWidth;
import static org.lwjgl.opengl.GL11.glPopMatrix;
import static org.lwjgl.opengl.GL11.glPushMatrix;
import static org.lwjgl.opengl.GL11.glVertex3f;
/**
*/
@RegisterSystem(RegisterMode.CLIENT)
public class SkeletonRenderer extends BaseComponentSystem implements RenderSystem, UpdateSubscriberSystem {
private static final Logger logger = LoggerFactory.getLogger(SkeletonRenderer.class);
@In
private EntityManager entityManager;
@In
private WorldRenderer worldRenderer;
@In
private Config config;
private Random random = new Random();
@ReceiveEvent(components = {SkeletalMeshComponent.class, LocationComponent.class})
public void newSkeleton(OnActivatedComponent event, EntityRef entity) {
SkeletalMeshComponent skeleton = entity.getComponent(SkeletalMeshComponent.class);
if (skeleton.mesh == null) {
return;
}
if (skeleton.boneEntities == null) {
skeleton.boneEntities = Maps.newHashMap();
for (Bone bone : skeleton.mesh.getBones()) {
LocationComponent loc = new LocationComponent();
EntityRef parent = (bone.getParent() != null) ? skeleton.boneEntities.get(bone.getParent().getName()) : entity;
EntityRef boneEntity = entityManager.create(loc);
Location.attachChild(parent, boneEntity);
loc.setLocalPosition(bone.getLocalPosition());
loc.setLocalRotation(bone.getLocalRotation());
if (bone.getParent() == null) {
skeleton.rootBone = boneEntity;
}
skeleton.boneEntities.put(bone.getName(), boneEntity);
}
entity.saveComponent(skeleton);
}
}
@Override
public void update(float delta) {
for (EntityRef entity : entityManager.getEntitiesWith(SkeletalMeshComponent.class, LocationComponent.class)) {
updateSkeletalMeshOfEntity(entity, delta);
}
}
private void updateSkeletalMeshOfEntity(EntityRef entity, float delta) {
SkeletalMeshComponent skeletalMeshComp = entity.getComponent(SkeletalMeshComponent.class);
if (skeletalMeshComp.animation == null && skeletalMeshComp.animationPool != null) {
skeletalMeshComp.animation = randomAnimationData(skeletalMeshComp, random);
}
if (skeletalMeshComp.animation == null) {
return;
}
if (skeletalMeshComp.animation.getFrameCount() < 1) {
return;
}
skeletalMeshComp.animationTime += delta * skeletalMeshComp.animationRate;
float animationDuration = getDurationOfAnimation(skeletalMeshComp);
while (skeletalMeshComp.animationTime >= animationDuration) {
MeshAnimation newAnimation;
if (!skeletalMeshComp.loop) {
newAnimation = null;
} else {
newAnimation = randomAnimationData(skeletalMeshComp, random);
}
if (newAnimation == null) {
MeshAnimation finishedAnimation = skeletalMeshComp.animation;
skeletalMeshComp.animationTime = animationDuration;
MeshAnimationFrame frame = skeletalMeshComp.animation.getFrame(skeletalMeshComp.animation.getFrameCount() - 1);
updateSkeleton(skeletalMeshComp, frame, frame, 1.0f);
// Set animation to null so that AnimEndEvent fires only once
skeletalMeshComp.animation = null;
entity.saveComponent(skeletalMeshComp);
entity.send(new AnimEndEvent(finishedAnimation));
return;
}
skeletalMeshComp.animationTime -= animationDuration;
if (skeletalMeshComp.animationTime < 0) {
// In case the float calculation wasn't exact:
skeletalMeshComp.animationTime = 0;
}
skeletalMeshComp.animation = newAnimation;
animationDuration = getDurationOfAnimation(skeletalMeshComp);
}
float framePos = skeletalMeshComp.animationTime / skeletalMeshComp.animation.getTimePerFrame();
int frameAId = (int) framePos;
int frameBId = frameAId + 1;
if (frameBId >= skeletalMeshComp.animation.getFrameCount()) {
// In case the float calcuation wasn't exact:
frameBId = skeletalMeshComp.animation.getFrameCount() - 1;
}
MeshAnimationFrame frameA = skeletalMeshComp.animation.getFrame(frameAId);
MeshAnimationFrame frameB = skeletalMeshComp.animation.getFrame(frameBId);
updateSkeleton(skeletalMeshComp, frameA, frameB, framePos - frameAId);
entity.saveComponent(skeletalMeshComp);
}
private float getDurationOfAnimation(SkeletalMeshComponent skeletalMeshComp) {
return skeletalMeshComp.animation.getTimePerFrame() * (skeletalMeshComp.animation.getFrameCount() - 1);
}
private static MeshAnimation randomAnimationData(SkeletalMeshComponent skeletalMeshComp, Random random) {
List<MeshAnimation> animationPool = skeletalMeshComp.animationPool;
if (animationPool == null) {
return null;
}
if (animationPool.isEmpty()) {
return null;
}
return animationPool.get(random.nextInt(animationPool.size()));
}
private void updateSkeleton(SkeletalMeshComponent skeletalMeshComp, MeshAnimationFrame frameA, MeshAnimationFrame frameB, float interpolationVal) {
for (int i = 0; i < skeletalMeshComp.animation.getBoneCount(); ++i) {
EntityRef boneEntity = skeletalMeshComp.boneEntities.get(skeletalMeshComp.animation.getBoneName(i));
if (boneEntity == null) {
continue;
}
LocationComponent boneLoc = boneEntity.getComponent(LocationComponent.class);
if (boneLoc != null) {
Vector3f newPos = BaseVector3f.lerp(frameA.getPosition(i), frameB.getPosition(i), interpolationVal);
boneLoc.setLocalPosition(newPos);
Quat4f newRot = BaseQuat4f.interpolate(frameA.getRotation(i), frameB.getRotation(i), interpolationVal);
newRot.normalize();
boneLoc.setLocalRotation(newRot);
boneEntity.saveComponent(boneLoc);
}
}
}
@Override
public void renderOpaque() {
Vector3f cameraPosition = worldRenderer.getActiveCamera().getPosition();
Quat4f worldRot = new Quat4f();
Vector3f worldPos = new Vector3f();
Quat4f inverseWorldRot = new Quat4f();
FloatBuffer tempMatrixBuffer44 = BufferUtils.createFloatBuffer(16);
FloatBuffer tempMatrixBuffer33 = BufferUtils.createFloatBuffer(12);
for (EntityRef entity : entityManager.getEntitiesWith(SkeletalMeshComponent.class, LocationComponent.class)) {
SkeletalMeshComponent skeletalMesh = entity.getComponent(SkeletalMeshComponent.class);
if (skeletalMesh.mesh == null || skeletalMesh.material == null || skeletalMesh.boneEntities == null || !skeletalMesh.material.isRenderable()) {
continue;
}
AABB aabb;
MeshAnimation animation = skeletalMesh.animation;
if (animation != null) {
aabb = animation.getAabb();
} else {
aabb = skeletalMesh.mesh.getStaticAabb();
}
LocationComponent location = entity.getComponent(LocationComponent.class);
location.getWorldRotation(worldRot);
inverseWorldRot.inverse(worldRot);
location.getWorldPosition(worldPos);
float worldScale = location.getWorldScale();
aabb = aabb.transform(worldRot, worldPos, worldScale);
if (!worldRenderer.getActiveCamera().hasInSight(aabb)) {
continue;
}
skeletalMesh.material.enable();
skeletalMesh.material.setFloat("sunlight", 1.0f, true);
skeletalMesh.material.setFloat("blockLight", 1.0f, true);
skeletalMesh.material.setMatrix4("projectionMatrix", worldRenderer.getActiveCamera().getProjectionMatrix());
skeletalMesh.material.bindTextures();
Vector3f worldPositionCameraSpace = new Vector3f();
worldPositionCameraSpace.sub(worldPos, cameraPosition);
worldPos.y -= skeletalMesh.heightOffset;
Matrix4f matrixCameraSpace = new Matrix4f(worldRot, worldPositionCameraSpace, worldScale);
Matrix4f modelViewMatrix = MatrixUtils.calcModelViewMatrix(worldRenderer.getActiveCamera().getViewMatrix(), matrixCameraSpace);
MatrixUtils.matrixToFloatBuffer(modelViewMatrix, tempMatrixBuffer44);
skeletalMesh.material.setMatrix4("worldViewMatrix", tempMatrixBuffer44, true);
MatrixUtils.matrixToFloatBuffer(MatrixUtils.calcNormalMatrix(modelViewMatrix), tempMatrixBuffer33);
skeletalMesh.material.setMatrix3("normalMatrix", tempMatrixBuffer33, true);
skeletalMesh.material.setFloat("sunlight", worldRenderer.getMainLightIntensityAt(worldPos), true);
skeletalMesh.material.setFloat("blockLight", worldRenderer.getBlockLightIntensityAt(worldPos), true);
List<Vector3f> bonePositions = Lists.newArrayListWithCapacity(skeletalMesh.mesh.getVertexCount());
List<Quat4f> boneRotations = Lists.newArrayListWithCapacity(skeletalMesh.mesh.getVertexCount());
for (Bone bone : skeletalMesh.mesh.getBones()) {
EntityRef boneEntity = skeletalMesh.boneEntities.get(bone.getName());
if (boneEntity == null) {
boneEntity = EntityRef.NULL;
}
LocationComponent boneLocation = boneEntity.getComponent(LocationComponent.class);
if (boneLocation != null) {
Vector3f pos = boneLocation.getWorldPosition();
pos.sub(worldPos);
inverseWorldRot.rotate(pos, pos);
bonePositions.add(pos);
Quat4f rot = new Quat4f(inverseWorldRot);
rot.mul(boneLocation.getWorldRotation());
boneRotations.add(rot);
} else {
logger.warn("Unable to resolve bone \"{}\"", bone.getName());
bonePositions.add(new Vector3f());
boneRotations.add(new Quat4f());
}
}
((OpenGLSkeletalMesh) skeletalMesh.mesh).setScaleTranslate(skeletalMesh.scale, skeletalMesh.translate);
((OpenGLSkeletalMesh) skeletalMesh.mesh).render(bonePositions, boneRotations);
}
}
@Override
public void renderAlphaBlend() {
}
@Override
public void renderOverlay() {
if (config.getRendering().getDebug().isRenderSkeletons()) {
glDisable(GL_DEPTH_TEST);
Vector3f cameraPosition = worldRenderer.getActiveCamera().getPosition();
Material material = Assets.getMaterial("engine:white").get();
material.setFloat("sunlight", 1.0f, true);
material.setFloat("blockLight", 1.0f, true);
material.setMatrix4("projectionMatrix", worldRenderer.getActiveCamera().getProjectionMatrix());
glLineWidth(2);
Vector3f worldPos = new Vector3f();
FloatBuffer tempMatrixBuffer44 = BufferUtils.createFloatBuffer(16);
FloatBuffer tempMatrixBuffer33 = BufferUtils.createFloatBuffer(12);
for (EntityRef entity : entityManager.getEntitiesWith(SkeletalMeshComponent.class, LocationComponent.class)) {
LocationComponent location = entity.getComponent(LocationComponent.class);
location.getWorldPosition(worldPos);
Vector3f worldPositionCameraSpace = new Vector3f();
worldPositionCameraSpace.sub(worldPos, cameraPosition);
float worldScale = location.getWorldScale();
Matrix4f matrixCameraSpace = new Matrix4f(new Quat4f(0, 0, 0, 1), worldPositionCameraSpace, worldScale);
Matrix4f modelViewMatrix = MatrixUtils.calcModelViewMatrix(worldRenderer.getActiveCamera().getViewMatrix(), matrixCameraSpace);
MatrixUtils.matrixToFloatBuffer(modelViewMatrix, tempMatrixBuffer44);
material.setMatrix4("worldViewMatrix", tempMatrixBuffer44, true);
MatrixUtils.matrixToFloatBuffer(MatrixUtils.calcNormalMatrix(modelViewMatrix), tempMatrixBuffer33);
material.setMatrix3("normalMatrix", tempMatrixBuffer33, true);
SkeletalMeshComponent skeletalMesh = entity.getComponent(SkeletalMeshComponent.class);
renderBone(skeletalMesh.rootBone, worldPos);
}
glEnable(GL_DEPTH_TEST);
}
}
@Override
public void renderFirstPerson() {
}
@Override
public void renderShadows() {
}
private void renderBoneOrientation(EntityRef boneEntity) {
LocationComponent loc = boneEntity.getComponent(LocationComponent.class);
if (loc == null) {
return;
}
glPushMatrix();
Vector3f worldPosA = loc.getWorldPosition();
Quat4f worldRot = loc.getWorldRotation();
Vector3f offset = new Vector3f(0, 0, 0.1f);
worldRot.rotate(offset, offset);
offset.add(worldPosA);
glBegin(GL11.GL_LINES);
glVertex3f(worldPosA.x, worldPosA.y, worldPosA.z);
glVertex3f(offset.x, offset.y, offset.z);
glEnd();
loc.getChildren().forEach(this::renderBoneOrientation);
glPopMatrix();
}
private void renderBone(EntityRef boneEntity, Vector3f centerPos) {
LocationComponent loc = boneEntity.getComponent(LocationComponent.class);
if (loc == null) {
return;
}
LocationComponent parentLoc = loc.getParent().getComponent(LocationComponent.class);
if (parentLoc != null) {
Vector3f worldPosA = loc.getWorldPosition();
worldPosA.sub(centerPos);
Vector3f worldPosB = parentLoc.getWorldPosition();
worldPosB.sub(centerPos);
glBegin(GL11.GL_LINES);
glVertex3f(worldPosA.x, worldPosA.y, worldPosA.z);
glVertex3f(worldPosB.x, worldPosB.y, worldPosB.z);
glEnd();
for (EntityRef child : loc.getChildren()) {
renderBone(child, centerPos);
}
}
}
}