From 50f619eac31279cab658b4a5a233acb1269e5310 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Mon, 29 Nov 2021 18:54:46 -0800 Subject: [PATCH 1/2] feat: add support for skinned Mesh Squashed commit of the following: commit 5fd46e2de40d5754b57011365973133e75564a11 Author: Michael Pollind Date: Mon Nov 29 17:54:39 2021 -0800 chore: update to skinned mesh commit 06f21899e501db4db18aafac982003647dc7087d Author: Michael Pollind Date: Mon Nov 29 12:11:06 2021 -0800 feat: add skinned mesh Material commit 63d817aa8e7fbfe11c7324574fc97d4457bc059d Author: Michael Pollind Date: Sun Nov 28 23:11:04 2021 -0800 feat: replace skeletalMesh with skinnedMesh commit 67bd3bf8a8c93307cb780adb493d79016d03dafc Author: Michael Pollind Date: Sun Nov 28 21:02:39 2021 -0800 feat: implement skinned mesh --- .../engine/HeadlessEnvironment.java | 4 + .../subsystem/headless/HeadlessGraphics.java | 3 + .../headless/assets/HeadlessSkinnedMesh.java | 101 +++++ .../subsystem/lwjgl/LwjglGraphicsManager.java | 6 + .../assets/animation/MeshAnimation.java | 4 +- .../assets/animation/MeshAnimationImpl.java | 4 +- .../rendering/assets/mesh/SkinnedMesh.java | 60 +++ .../assets/mesh/SkinnedMeshData.java | 33 ++ .../assets/mesh/StandardSkinnedMeshData.java | 169 +++++++ .../assets/mesh/resource/VertexResource.java | 8 +- .../mesh/resource/VertexResourceBuilder.java | 40 +- .../rendering/gltf/GLTFSkinnedMeshFormat.java | 105 +++++ .../rendering/logic/SkinnedMeshComponent.java | 81 ++++ .../rendering/logic/SkinnedMeshRenderer.java | 415 ++++++++++++++++++ .../rendering/opengl/OpenGLMeshBase.java | 12 +- .../rendering/opengl/OpenGLSkinnedMesh.java | 204 +++++++++ .../shaders/skinnedGenericMeshMaterial.info | 8 + .../skinnedGenericMeshMaterial_frag.glsl | 34 ++ .../skinnedGenericMeshMaterial_vert.glsl | 31 ++ 19 files changed, 1305 insertions(+), 17 deletions(-) create mode 100644 engine/src/main/java/org/terasology/engine/core/subsystem/headless/assets/HeadlessSkinnedMesh.java create mode 100644 engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMesh.java create mode 100644 engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMeshData.java create mode 100644 engine/src/main/java/org/terasology/engine/rendering/assets/mesh/StandardSkinnedMeshData.java create mode 100644 engine/src/main/java/org/terasology/engine/rendering/gltf/GLTFSkinnedMeshFormat.java create mode 100644 engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java create mode 100644 engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java create mode 100644 engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLSkinnedMesh.java create mode 100644 engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial.info create mode 100644 engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_frag.glsl create mode 100644 engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_vert.glsl diff --git a/engine-tests/src/main/java/org/terasology/engine/HeadlessEnvironment.java b/engine-tests/src/main/java/org/terasology/engine/HeadlessEnvironment.java index 75ebf4c7c81..bf8a40907ca 100644 --- a/engine-tests/src/main/java/org/terasology/engine/HeadlessEnvironment.java +++ b/engine-tests/src/main/java/org/terasology/engine/HeadlessEnvironment.java @@ -24,6 +24,7 @@ import org.terasology.engine.core.subsystem.headless.assets.HeadlessMesh; import org.terasology.engine.core.subsystem.headless.assets.HeadlessShader; import org.terasology.engine.core.subsystem.headless.assets.HeadlessSkeletalMesh; +import org.terasology.engine.core.subsystem.headless.assets.HeadlessSkinnedMesh; import org.terasology.engine.core.subsystem.headless.assets.HeadlessTexture; import org.terasology.engine.entitySystem.entity.internal.EngineEntityManager; import org.terasology.engine.entitySystem.prefab.Prefab; @@ -48,6 +49,7 @@ import org.terasology.engine.rendering.assets.font.FontImpl; import org.terasology.engine.rendering.assets.material.Material; import org.terasology.engine.rendering.assets.mesh.Mesh; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; import org.terasology.engine.rendering.assets.shader.Shader; import org.terasology.engine.rendering.assets.skeletalmesh.SkeletalMesh; import org.terasology.engine.rendering.assets.texture.PNGTextureFormat; @@ -230,6 +232,8 @@ protected AssetManager setupAssetManager() { HeadlessMesh::new, "mesh"); assetTypeManager.createAssetType(SkeletalMesh.class, HeadlessSkeletalMesh::new, "skeletalMesh"); + assetTypeManager.createAssetType(SkinnedMesh.class, + HeadlessSkinnedMesh::new, "skeletalMesh"); assetTypeManager.createAssetType(MeshAnimation.class, MeshAnimationImpl::new, "animations"); diff --git a/engine/src/main/java/org/terasology/engine/core/subsystem/headless/HeadlessGraphics.java b/engine/src/main/java/org/terasology/engine/core/subsystem/headless/HeadlessGraphics.java index 83aaf7d0692..99826d93ae1 100644 --- a/engine/src/main/java/org/terasology/engine/core/subsystem/headless/HeadlessGraphics.java +++ b/engine/src/main/java/org/terasology/engine/core/subsystem/headless/HeadlessGraphics.java @@ -10,6 +10,7 @@ import org.terasology.engine.core.subsystem.headless.assets.HeadlessMesh; import org.terasology.engine.core.subsystem.headless.assets.HeadlessShader; import org.terasology.engine.core.subsystem.headless.assets.HeadlessSkeletalMesh; +import org.terasology.engine.core.subsystem.headless.assets.HeadlessSkinnedMesh; import org.terasology.engine.core.subsystem.headless.assets.HeadlessTexture; import org.terasology.engine.core.subsystem.headless.device.HeadlessDisplayDevice; import org.terasology.engine.core.subsystem.headless.renderer.HeadlessCanvasRenderer; @@ -23,6 +24,7 @@ import org.terasology.engine.rendering.assets.font.FontImpl; import org.terasology.engine.rendering.assets.material.Material; import org.terasology.engine.rendering.assets.mesh.Mesh; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; import org.terasology.engine.rendering.assets.shader.Shader; import org.terasology.engine.rendering.assets.skeletalmesh.SkeletalMesh; import org.terasology.engine.rendering.assets.texture.PNGTextureFormat; @@ -55,6 +57,7 @@ public void registerCoreAssetTypes(ModuleAwareAssetTypeManager assetTypeManager) assetTypeManager.createAssetType(Material.class, HeadlessMaterial::new, "materials"); assetTypeManager.createAssetType(Mesh.class, HeadlessMesh::new, "mesh"); assetTypeManager.createAssetType(SkeletalMesh.class, HeadlessSkeletalMesh::new, "skeletalMesh"); + assetTypeManager.createAssetType(SkinnedMesh.class, HeadlessSkinnedMesh::new, "skeletalMesh"); assetTypeManager.createAssetType(MeshAnimation.class, MeshAnimationImpl::new, "animations"); assetTypeManager.createAssetType(Atlas.class, Atlas::new, "atlas"); assetTypeManager.createAssetType(Subtexture.class, Subtexture::new); diff --git a/engine/src/main/java/org/terasology/engine/core/subsystem/headless/assets/HeadlessSkinnedMesh.java b/engine/src/main/java/org/terasology/engine/core/subsystem/headless/assets/HeadlessSkinnedMesh.java new file mode 100644 index 00000000000..5244e557f00 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/core/subsystem/headless/assets/HeadlessSkinnedMesh.java @@ -0,0 +1,101 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.core.subsystem.headless.assets; + +import com.google.common.collect.Maps; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; +import org.terasology.engine.rendering.assets.mesh.SkinnedMeshData; +import org.terasology.engine.rendering.assets.mesh.resource.VertexAttributeBinding; +import org.terasology.engine.rendering.assets.mesh.resource.VertexByteAttributeBinding; +import org.terasology.engine.rendering.assets.skeletalmesh.Bone; +import org.terasology.gestalt.assets.AssetType; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.joml.geom.AABBf; +import org.terasology.joml.geom.AABBfc; + +import java.util.List; +import java.util.Map; + +public class HeadlessSkinnedMesh extends SkinnedMesh { + + private List bones; + private Map boneLookup = Maps.newHashMap(); + private VertexAttributeBinding positions; + private VertexByteAttributeBinding boneIndex0; + private VertexByteAttributeBinding boneIndex1; + private VertexByteAttributeBinding boneIndex2; + private VertexByteAttributeBinding boneIndex3; + private AABBf bounds; + + public HeadlessSkinnedMesh(ResourceUrn urn, AssetType assetType, SkinnedMeshData skinnedMesh) { + super(urn, assetType); + reload(skinnedMesh); + } + + @Override + protected void doReload(SkinnedMeshData data) { + this.boneLookup.clear(); + this.positions = data.positions(); + this.boneIndex0 = data.boneIndex0(); + this.boneIndex1 = data.boneIndex0(); + this.boneIndex2 = data.boneIndex0(); + this.boneIndex3 = data.boneIndex0(); + this.bones.forEach(b -> { + boneLookup.put(b.getName(), b); + }); + getBound(bounds); + } + + @Override + public AABBfc getAABB() { + return bounds; + } + + @Override + public VertexAttributeBinding vertices() { + return positions; + } + + @Override + public int elementCount() { + return this.positions.elements(); + } + + @Override + public VertexByteAttributeBinding boneIndex0() { + return boneIndex0; + } + + @Override + public VertexByteAttributeBinding boneIndex1() { + return boneIndex1; + } + + @Override + public VertexByteAttributeBinding boneIndex2() { + return boneIndex2; + } + + @Override + public VertexByteAttributeBinding boneIndex3() { + return boneIndex3; + } + + @Override + public List bones() { + return bones; + } + + @Override + public Bone getBone(String boneName) { + return boneLookup.get(boneName); + } + + @Override + public void render() { + + } + +} diff --git a/engine/src/main/java/org/terasology/engine/core/subsystem/lwjgl/LwjglGraphicsManager.java b/engine/src/main/java/org/terasology/engine/core/subsystem/lwjgl/LwjglGraphicsManager.java index b71b200f9fb..06eb0fe372b 100644 --- a/engine/src/main/java/org/terasology/engine/core/subsystem/lwjgl/LwjglGraphicsManager.java +++ b/engine/src/main/java/org/terasology/engine/core/subsystem/lwjgl/LwjglGraphicsManager.java @@ -18,6 +18,7 @@ import org.terasology.engine.rendering.assets.font.FontImpl; import org.terasology.engine.rendering.assets.material.Material; import org.terasology.engine.rendering.assets.mesh.Mesh; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; import org.terasology.engine.rendering.assets.shader.Shader; import org.terasology.engine.rendering.assets.skeletalmesh.SkeletalMesh; import org.terasology.engine.rendering.assets.texture.PNGTextureFormat; @@ -28,6 +29,7 @@ import org.terasology.engine.rendering.opengl.GLSLShader; import org.terasology.engine.rendering.opengl.OpenGLMesh; import org.terasology.engine.rendering.opengl.OpenGLSkeletalMesh; +import org.terasology.engine.rendering.opengl.OpenGLSkinnedMesh; import org.terasology.engine.rendering.opengl.OpenGLTexture; import org.terasology.gestalt.assets.AssetType; import org.terasology.gestalt.assets.module.ModuleAssetScanner; @@ -90,6 +92,10 @@ public void registerCoreAssetTypes(ModuleAwareAssetTypeManager assetTypeManager) (urn, assetType, data) -> OpenGLSkeletalMesh.create(urn, assetType, data, this), "skeletalMesh"); + assetTypeManager.createAssetType(SkinnedMesh.class, + (urn, assetType, data) -> + OpenGLSkinnedMesh.create(urn, assetType, data, this), + "skeletalMesh"); assetTypeManager.createAssetType(MeshAnimation.class, MeshAnimationImpl::new, "animations", "skeletalMesh"); assetTypeManager.createAssetType(Atlas.class, Atlas::new, "atlas"); diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java b/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java index 5bbdfe7f65f..1dc7757d05d 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java @@ -3,10 +3,10 @@ package org.terasology.engine.rendering.assets.animation; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; import org.terasology.gestalt.assets.Asset; import org.terasology.gestalt.assets.AssetType; import org.terasology.gestalt.assets.ResourceUrn; -import org.terasology.engine.rendering.assets.skeletalmesh.SkeletalMesh; import org.terasology.joml.geom.AABBf; public abstract class MeshAnimation extends Asset { @@ -15,7 +15,7 @@ protected MeshAnimation(ResourceUrn urn, AssetType assetTy super(urn, assetType); } - public abstract boolean isValidAnimationFor(SkeletalMesh mesh); + public abstract boolean isValidAnimationFor(SkinnedMesh mesh); public abstract int getBoneCount(); diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimationImpl.java b/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimationImpl.java index 9783c994a7b..c7bb9df3028 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimationImpl.java +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimationImpl.java @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.rendering.assets.animation; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; import org.terasology.gestalt.assets.Asset; import org.terasology.gestalt.assets.AssetType; import org.terasology.gestalt.assets.ResourceUrn; import org.terasology.engine.rendering.assets.skeletalmesh.Bone; -import org.terasology.engine.rendering.assets.skeletalmesh.SkeletalMesh; import org.terasology.joml.geom.AABBf; import java.util.Optional; @@ -21,7 +21,7 @@ public MeshAnimationImpl(ResourceUrn urn, AssetType assetT } @Override - public boolean isValidAnimationFor(SkeletalMesh mesh) { + public boolean isValidAnimationFor(SkinnedMesh mesh) { for (int i = 0; i < data.getBoneNames().size(); ++i) { Bone bone = mesh.getBone(data.getBoneNames().get(i)); boolean hasParent = data.getBoneParent().get(i) != MeshAnimationData.NO_PARENT; diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMesh.java b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMesh.java new file mode 100644 index 00000000000..c6fe9f12f69 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMesh.java @@ -0,0 +1,60 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.assets.mesh; + +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.terasology.engine.rendering.assets.mesh.resource.VertexAttributeBinding; +import org.terasology.engine.rendering.assets.mesh.resource.VertexByteAttributeBinding; +import org.terasology.engine.rendering.assets.skeletalmesh.Bone; +import org.terasology.gestalt.assets.Asset; +import org.terasology.gestalt.assets.AssetType; +import org.terasology.gestalt.assets.DisposableResource; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.joml.geom.AABBf; +import org.terasology.joml.geom.AABBfc; + +import java.util.List; + +public abstract class SkinnedMesh extends Asset { + + protected SkinnedMesh(ResourceUrn urn, AssetType assetType) { + super(urn, assetType); + } + + protected SkinnedMesh(ResourceUrn urn, AssetType assetType, DisposableResource resource) { + super(urn, assetType, resource); + } + + public abstract AABBfc getAABB(); + + protected AABBf getBound(AABBf dest) { + VertexAttributeBinding vertices = this.vertices(); + if (elementCount() == 0) { + dest.set(Float.POSITIVE_INFINITY, + Float.POSITIVE_INFINITY, + Float.POSITIVE_INFINITY, + Float.NEGATIVE_INFINITY, + Float.NEGATIVE_INFINITY, + Float.NEGATIVE_INFINITY); + return dest; + } + Vector3f pos = new Vector3f(); + for (int x = 0; x < elementCount(); x++) { + dest.union(vertices.get(x, pos)); + } + return dest; + } + + public abstract VertexAttributeBinding vertices(); + public abstract int elementCount(); + + public abstract VertexByteAttributeBinding boneIndex0(); + public abstract VertexByteAttributeBinding boneIndex1(); + public abstract VertexByteAttributeBinding boneIndex2(); + public abstract VertexByteAttributeBinding boneIndex3(); + public abstract List bones(); + public abstract Bone getBone(String name); + public abstract void render(); +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMeshData.java b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMeshData.java new file mode 100644 index 00000000000..dbba089f35e --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/SkinnedMeshData.java @@ -0,0 +1,33 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.assets.mesh; + +import org.joml.Vector4f; +import org.joml.Vector4fc; +import org.terasology.engine.rendering.assets.mesh.resource.AllocationType; +import org.terasology.engine.rendering.assets.mesh.resource.DrawingMode; +import org.terasology.engine.rendering.assets.mesh.resource.VertexAttributeBinding; +import org.terasology.engine.rendering.assets.mesh.resource.VertexByteAttributeBinding; +import org.terasology.engine.rendering.assets.skeletalmesh.Bone; + +import java.util.List; + +public abstract class SkinnedMeshData extends MeshData { + + public SkinnedMeshData() { + this(DrawingMode.TRIANGLES, AllocationType.STATIC); + } + + public SkinnedMeshData(DrawingMode mode, AllocationType allocationType) { + super(mode, allocationType); + } + + public abstract VertexByteAttributeBinding boneIndex0(); + public abstract VertexByteAttributeBinding boneIndex1(); + public abstract VertexByteAttributeBinding boneIndex2(); + public abstract VertexByteAttributeBinding boneIndex3(); + public abstract VertexAttributeBinding weight(); + public abstract List bones(); + +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/StandardSkinnedMeshData.java b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/StandardSkinnedMeshData.java new file mode 100644 index 00000000000..0d3fade629b --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/StandardSkinnedMeshData.java @@ -0,0 +1,169 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.assets.mesh; + +import org.joml.Vector2f; +import org.joml.Vector2fc; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.joml.Vector4f; +import org.joml.Vector4fc; +import org.terasology.engine.rendering.assets.mesh.resource.AllocationType; +import org.terasology.engine.rendering.assets.mesh.resource.DrawingMode; +import org.terasology.engine.rendering.assets.mesh.resource.GLAttributes; +import org.terasology.engine.rendering.assets.mesh.resource.IndexResource; +import org.terasology.engine.rendering.assets.mesh.resource.VertexAttributeBinding; +import org.terasology.engine.rendering.assets.mesh.resource.VertexByteAttributeBinding; +import org.terasology.engine.rendering.assets.mesh.resource.VertexResource; +import org.terasology.engine.rendering.assets.mesh.resource.VertexResourceBuilder; +import org.terasology.engine.rendering.assets.skeletalmesh.Bone; + +import java.util.List; + +public class StandardSkinnedMeshData extends SkinnedMeshData { + + public static final int VERTEX_INDEX = 0; + public static final int NORMAL_INDEX = 1; + public static final int UV0_INDEX = 2; + public static final int UV1_INDEX = 3; + + public static final int BONE_IDX0_INDEX = 4; + public static final int BONE_IDX1_INDEX = 5; + public static final int BONE_IDX2_INDEX = 6; + public static final int BONE_IDX3_INDEX = 7; + public static final int BONE_WEIGHT_INDEX = 8; + + public final VertexResource positionBuffer; + public final VertexAttributeBinding position; + + public final VertexResource normalBuffer; + public final VertexAttributeBinding normal; + + public final VertexResource uv0Buffer; + public final VertexAttributeBinding uv0; + + public final VertexResource uv1Buffer; + public final VertexAttributeBinding uv1; + + public final VertexResource boneIndexBuffer0; + public final VertexByteAttributeBinding boneIndex0; + + public final VertexResource boneIndexBuffer1; + public final VertexByteAttributeBinding boneIndex1; + + public final VertexResource boneIndexBuffer2; + public final VertexByteAttributeBinding boneIndex2; + + public final VertexResource boneIndexBuffer3; + public final VertexByteAttributeBinding boneIndex3; + + public final VertexResource weightBuffer; + public final VertexAttributeBinding weight; + + public final IndexResource indices; + + private final List bones; + + public StandardSkinnedMeshData(List bones) { + this(DrawingMode.TRIANGLES, AllocationType.STATIC, bones); + } + + public StandardSkinnedMeshData(DrawingMode mode, AllocationType allocationType, List bones) { + super(mode, allocationType); + this.bones = bones; + + VertexResourceBuilder builder = new VertexResourceBuilder(); + position = builder.add(VERTEX_INDEX, GLAttributes.VECTOR_3_F_VERTEX_ATTRIBUTE); + positionBuffer = builder.build(); + + builder = new VertexResourceBuilder(); + normal = builder.add(NORMAL_INDEX, GLAttributes.VECTOR_3_F_VERTEX_ATTRIBUTE); + normalBuffer = builder.build(); + + builder = new VertexResourceBuilder(); + uv0 = builder.add(UV0_INDEX, GLAttributes.VECTOR_2_F_VERTEX_ATTRIBUTE); + uv0Buffer = builder.build(); + + builder = new VertexResourceBuilder(); + uv1 = builder.add(UV1_INDEX, GLAttributes.VECTOR_2_F_VERTEX_ATTRIBUTE); + uv1Buffer = builder.build(); + + builder = new VertexResourceBuilder(); + boneIndex0 = builder.add(BONE_IDX0_INDEX, GLAttributes.BYTE_1_VERTEX_ATTRIBUTE, VertexResource.FEATURE_INTEGER); + boneIndexBuffer0 = builder.build(); + + builder = new VertexResourceBuilder(); + boneIndex1 = builder.add(BONE_IDX1_INDEX, GLAttributes.BYTE_1_VERTEX_ATTRIBUTE, VertexResource.FEATURE_INTEGER); + boneIndexBuffer1 = builder.build(); + + builder = new VertexResourceBuilder(); + boneIndex2 = builder.add(BONE_IDX2_INDEX, GLAttributes.BYTE_1_VERTEX_ATTRIBUTE, VertexResource.FEATURE_INTEGER); + boneIndexBuffer2 = builder.build(); + + builder = new VertexResourceBuilder(); + boneIndex3 = builder.add(BONE_IDX3_INDEX, GLAttributes.BYTE_1_VERTEX_ATTRIBUTE, VertexResource.FEATURE_INTEGER); + boneIndexBuffer3 = builder.build(); + + builder = new VertexResourceBuilder(); + weight = builder.add(BONE_WEIGHT_INDEX, GLAttributes.VECTOR_4_F_VERTEX_ATTRIBUTE); + weightBuffer = builder.build(); + + this.indices = new IndexResource(); + } + + @Override + public VertexByteAttributeBinding boneIndex0() { + return boneIndex0; + } + + @Override + public VertexByteAttributeBinding boneIndex1() { + return boneIndex1; + } + + @Override + public VertexByteAttributeBinding boneIndex2() { + return boneIndex2; + } + + @Override + public VertexByteAttributeBinding boneIndex3() { + return boneIndex3; + } + + @Override + public VertexAttributeBinding weight() { + return weight; + } + + @Override + public List bones() { + return this.bones; + } + + @Override + public VertexAttributeBinding positions() { + return position; + } + + @Override + public VertexResource[] vertexResources() { + return new VertexResource[] { + positionBuffer, + normalBuffer, + uv0Buffer, + uv1Buffer, + boneIndexBuffer0, + boneIndexBuffer1, + boneIndexBuffer2, + boneIndexBuffer3, + weightBuffer + }; + } + + @Override + public IndexResource indexResource() { + return indices; + } +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResource.java b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResource.java index 799071a1df5..695c6caf82a 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResource.java +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResource.java @@ -7,6 +7,9 @@ * A resource that represents vertex data */ public class VertexResource extends BufferedResource { + public static final int FEATURE_NORMALIZED = 0x1; + public static final int FEATURE_INTEGER = 0x2; + private int inStride = 0; private VertexDefinition[] attributes; @@ -102,14 +105,17 @@ public boolean isEmpty() { * describes the metadata and placement into the buffer based off the stride. */ public static class VertexDefinition { + public final int location; public final BaseVertexAttribute attribute; public final int offset; + public final int features; - public VertexDefinition(int location, int offset, BaseVertexAttribute attribute) { + public VertexDefinition(int location, int offset, BaseVertexAttribute attribute, int features) { this.location = location; this.attribute = attribute; this.offset = offset; + this.features = features; } } } diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResourceBuilder.java b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResourceBuilder.java index 5be9864e04b..c08a81a9e56 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResourceBuilder.java +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/mesh/resource/VertexResourceBuilder.java @@ -25,41 +25,61 @@ public VertexResourceBuilder() { * @param the target object type * @param a class implementing the target object type */ - public VertexAttributeBinding add(int location, VertexAttribute attribute) { + public VertexAttributeBinding add(int location, VertexAttribute attribute, int feature) { VertexAttributeBinding result = new VertexAttributeBinding(resource, inStride, attribute); - this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute)); + this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute, feature)); inStride += attribute.mapping.size * attribute.count; return result; } - public VertexIntegerAttributeBinding add(int location, VertexIntegerAttribute attribute) { + public VertexAttributeBinding add(int location, VertexAttribute attribute) { + return add(location, attribute, 0); + } + + public VertexIntegerAttributeBinding add(int location, VertexIntegerAttribute attribute, int feature) { VertexIntegerAttributeBinding result = new VertexIntegerAttributeBinding(resource, inStride, attribute); - this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute)); + this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute, feature)); inStride += attribute.mapping.size * attribute.count; return result; } - public VertexFloatAttributeBinding add(int location, VertexFloatAttribute attribute) { + public VertexIntegerAttributeBinding add(int location, VertexIntegerAttribute attribute) { + return add(location, attribute, 0); + } + + public VertexFloatAttributeBinding add(int location, VertexFloatAttribute attribute, int features) { VertexFloatAttributeBinding result = new VertexFloatAttributeBinding(resource, inStride, attribute); - this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute)); + this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute, features)); inStride += attribute.mapping.size * attribute.count; return result; } - public VertexByteAttributeBinding add(int location, VertexByteAttribute attribute) { + public VertexFloatAttributeBinding add(int location, VertexFloatAttribute attribute) { + return add(location, attribute, 0); + } + + public VertexByteAttributeBinding add(int location, VertexByteAttribute attribute, int features) { VertexByteAttributeBinding result = new VertexByteAttributeBinding(resource, inStride, attribute); - this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute)); + this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute, features)); inStride += attribute.mapping.size * attribute.count; return result; } - public VertexShortAttributeBinding add(int location, VertexShortAttribute attribute) { + public VertexByteAttributeBinding add(int location, VertexByteAttribute attribute) { + return add(location, attribute, 0); + } + + public VertexShortAttributeBinding add(int location, VertexShortAttribute attribute, int features) { VertexShortAttributeBinding result = new VertexShortAttributeBinding(resource, inStride, attribute); - this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute)); + this.definitions.add(new VertexResource.VertexDefinition(location, inStride, attribute, features)); inStride += attribute.mapping.size * attribute.count; return result; } + public VertexShortAttributeBinding add(int location, VertexShortAttribute attribute) { + return add(location, attribute, 0); + } + public VertexResource build() { resource.setDefinitions(definitions.toArray(new VertexResource.VertexDefinition[]{})); resource.allocate(0, inStride); diff --git a/engine/src/main/java/org/terasology/engine/rendering/gltf/GLTFSkinnedMeshFormat.java b/engine/src/main/java/org/terasology/engine/rendering/gltf/GLTFSkinnedMeshFormat.java new file mode 100644 index 00000000000..bf0ac0fc55b --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/gltf/GLTFSkinnedMeshFormat.java @@ -0,0 +1,105 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.engine.rendering.gltf; + +import gnu.trove.list.TFloatList; +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import org.joml.Vector2f; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.rendering.assets.mesh.SkinnedMeshData; +import org.terasology.engine.rendering.assets.mesh.StandardSkinnedMeshData; +import org.terasology.engine.rendering.gltf.model.GLTF; +import org.terasology.engine.rendering.gltf.model.GLTFAccessor; +import org.terasology.engine.rendering.gltf.model.GLTFBufferView; +import org.terasology.engine.rendering.gltf.model.GLTFMesh; +import org.terasology.engine.rendering.gltf.model.GLTFPrimitive; +import org.terasology.engine.rendering.gltf.model.GLTFSkin; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.gestalt.assets.format.AssetDataFile; +import org.terasology.gestalt.assets.management.AssetManager; +import org.terasology.gestalt.assets.module.annotations.RegisterAssetFileFormat; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.List; + +@RegisterAssetFileFormat +public class GLTFSkinnedMeshFormat extends GLTFCommonFormat { + + private Logger logger = LoggerFactory.getLogger(GLTFMeshFormat.class); + + public GLTFSkinnedMeshFormat(AssetManager assetManager) { + super(assetManager, "gltf"); + } + + @Override + public SkinnedMeshData load(ResourceUrn urn, List inputs) throws IOException { + try (Reader in = new InputStreamReader(inputs.get(0).openStream())) { + GLTF gltf = gson.fromJson(in, GLTF.class); + + checkVersionSupported(urn, gltf); + checkMeshPresent(urn, gltf); + + GLTFSkin skin = gltf.getSkins().get(0); + GLTFMesh gltfMesh = gltf.getMeshes().get(0); + + checkPrimitivePresent(urn, gltfMesh); + GLTFPrimitive gltfPrimitive = gltfMesh.getPrimitives().get(0); + + List loadedBuffers = loadBinaryBuffers(urn, gltf); + + StandardSkinnedMeshData skinnedMeshData = new StandardSkinnedMeshData(loadBones(gltf, skin, loadedBuffers)); + + List positions = loadVector3fList(MeshAttributeSemantic.Position, gltfPrimitive, gltf, loadedBuffers); + List normals = loadVector3fList(MeshAttributeSemantic.Normal, gltfPrimitive, gltf, loadedBuffers); + TIntList joints = readIntBuffer(MeshAttributeSemantic.Joints_0, gltfPrimitive, gltf, loadedBuffers); + TFloatList weights = readFloatBuffer(MeshAttributeSemantic.Weights_0, gltfPrimitive, gltf, loadedBuffers); + List uvs = loadVector2fList(MeshAttributeSemantic.Texcoord_0, gltfPrimitive, gltf, loadedBuffers); + + Vector4f tempWeight = new Vector4f(); + for (int i = 0; i < positions.size(); i++) { + skinnedMeshData.position.put(positions.get(i)); + skinnedMeshData.normal.put(normals.get(i)); + + skinnedMeshData.boneIndex0.put((byte) joints.get(4 * i + 0)); + skinnedMeshData.boneIndex1.put((byte) joints.get(4 * i + 1)); + skinnedMeshData.boneIndex2.put((byte) joints.get(4 * i + 2)); + skinnedMeshData.boneIndex3.put((byte) joints.get(4 * i + 3)); + tempWeight.set( + weights.get(4 * i + 0), + weights.get(4 * i + 1), + weights.get(4 * i + 2), + weights.get(4 * i + 3) + ); + skinnedMeshData.weight.put(tempWeight); + skinnedMeshData.uv0.put(uvs.get(i)); + } + + GLTFAccessor indicesAccessor = getIndicesAccessor(gltfPrimitive, gltf, urn); + if (indicesAccessor.getBufferView() == null) { + throw new IOException("Missing buffer view for indices accessor in " + urn); + } + GLTFBufferView indicesBuffer = gltf.getBufferViews().get(indicesAccessor.getBufferView()); + checkIndicesBuffer(indicesBuffer); + TIntList indicies = new TIntArrayList(); + readBuffer(loadedBuffers.get(indicesBuffer.getBuffer()), indicesAccessor, indicesBuffer, indicies); + + indicies.forEach(value -> { + skinnedMeshData.indices.put(value); + return true; + }); + + if (gltf.getSkins().isEmpty()) { + throw new IOException("Skeletal mesh '" + urn + "' missing skin"); + } + + return skinnedMeshData; + } + } + +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java new file mode 100644 index 00000000000..74daaf84d0a --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java @@ -0,0 +1,81 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.logic; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.joml.Vector3f; +import org.terasology.engine.entitySystem.Owns; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.network.Replicate; +import org.terasology.engine.rendering.assets.animation.MeshAnimation; +import org.terasology.engine.rendering.assets.material.Material; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; +import org.terasology.engine.world.block.ForceBlockActive; +import org.terasology.nui.Color; +import org.terasology.nui.properties.Range; + +import java.util.List; +import java.util.Map; + +@ForceBlockActive +public class SkinnedMeshComponent implements VisualComponent { + @Replicate + public SkinnedMesh mesh; + + @Replicate + public Material material; + + /** + * Should not be set manually. Stores the data of the selected animation variation. + */ + @Replicate + public MeshAnimation animation; + + /** + * If true, an animation from {@link #animationPool} will be played when the current animation is done. + */ + @Replicate + public boolean loop; + + /** + * When the current animation is done and loop is true then a random animation will be picked from this pool of + * animations. + */ + @Replicate + public List animationPool = Lists.newArrayList(); + + public float animationRate = 1.0f; + @Range(min = -2.5f, max = 2.5f) + public float heightOffset; + + @Owns + public Map boneEntities; + public EntityRef rootBone = EntityRef.NULL; + public float animationTime; + + @Replicate + public Vector3f scale = new Vector3f(1, 1, 1); + @Replicate + public Vector3f translate = new Vector3f(); + + @Replicate + public Color color = Color.WHITE; + + @Override + public void copyFrom(SkinnedMeshComponent other) { + this.mesh = other.mesh; + this.material = other.material; + this.animation = other.animation; + this.loop = other.loop; + this.animationPool = Lists.newArrayList(other.animationPool); + this.animationRate = other.animationRate; + this.boneEntities = Maps.newHashMap(other.boneEntities); + this.rootBone = other.rootBone; + this.animationTime = other.animationTime; + this.scale = new Vector3f(other.scale); + this.translate = new Vector3f(other.translate); + this.color = new Color(other.color); + } +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java new file mode 100644 index 00000000000..63285407bcf --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java @@ -0,0 +1,415 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.logic; + +import com.google.common.collect.Maps; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL33; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.config.Config; +import org.terasology.engine.entitySystem.entity.EntityManager; +import org.terasology.engine.entitySystem.entity.EntityRef; +import org.terasology.engine.entitySystem.entity.lifecycleEvents.OnActivatedComponent; +import org.terasology.engine.entitySystem.event.ReceiveEvent; +import org.terasology.engine.entitySystem.systems.BaseComponentSystem; +import org.terasology.engine.entitySystem.systems.RegisterMode; +import org.terasology.engine.entitySystem.systems.RegisterSystem; +import org.terasology.engine.entitySystem.systems.RenderSystem; +import org.terasology.engine.entitySystem.systems.UpdateSubscriberSystem; +import org.terasology.engine.logic.location.Location; +import org.terasology.engine.logic.location.LocationComponent; +import org.terasology.engine.registry.In; +import org.terasology.engine.rendering.assets.animation.MeshAnimation; +import org.terasology.engine.rendering.assets.animation.MeshAnimationFrame; +import org.terasology.engine.rendering.assets.material.Material; +import org.terasology.engine.rendering.assets.mesh.Mesh; +import org.terasology.engine.rendering.assets.mesh.StandardMeshData; +import org.terasology.engine.rendering.assets.mesh.resource.AllocationType; +import org.terasology.engine.rendering.assets.mesh.resource.DrawingMode; +import org.terasology.engine.rendering.assets.skeletalmesh.Bone; +import org.terasology.engine.rendering.world.WorldRenderer; +import org.terasology.engine.utilities.Assets; +import org.terasology.gestalt.assets.management.AssetManager; +import org.terasology.joml.geom.AABBf; +import org.terasology.joml.geom.AABBfc; +import org.terasology.nui.Color; + +import java.nio.FloatBuffer; +import java.util.List; +import java.util.Random; + +/** + * Renders the skeletal mesh of entities when the debug setting "renderSkeletons" is active. + *

+ * The entities must have a {@link SkinnedMeshComponent} and a {@link LocationComponent}. + * + * @see BoundingBoxRenderer another debug renderer for bounding boxes defined by shape components + */ +@RegisterSystem(RegisterMode.CLIENT) +public class SkinnedMeshRenderer extends BaseComponentSystem implements RenderSystem, UpdateSubscriberSystem { + + private static final Logger logger = LoggerFactory.getLogger(SkinnedMeshRenderer.class); + + @In + private EntityManager entityManager; + + @In + private WorldRenderer worldRenderer; + + @In + private AssetManager assetManager; + + @In + private Config config; + + private StandardMeshData meshData = new StandardMeshData(DrawingMode.LINES, AllocationType.STREAM); + private Mesh mesh; + private Material material; + + private Random random = new Random(); + + @Override + public void initialise() { + super.initialise(); + mesh = Assets.generateAsset(meshData, Mesh.class); + material = assetManager.getAsset("engine:white", Material.class).get(); + } + + @ReceiveEvent(components = {SkinnedMeshComponent.class, LocationComponent.class}) + public void newSkeleton(OnActivatedComponent event, EntityRef entity) { + SkinnedMeshComponent skeleton = entity.getComponent(SkinnedMeshComponent.class); + if (skeleton.mesh == null) { + return; + } + + if (skeleton.boneEntities == null) { + skeleton.boneEntities = Maps.newHashMap(); + for (Bone bone : skeleton.mesh.bones()) { + LocationComponent loc = new LocationComponent(); + EntityRef boneEntity = entityManager.create(loc); + skeleton.boneEntities.put(bone.getName(), boneEntity); + } + } + + for (Bone bone : skeleton.mesh.bones()) { + EntityRef boneEntity = skeleton.boneEntities.get(bone.getName()); + EntityRef parent = (bone.getParent() != null) ? skeleton.boneEntities.get(bone.getParent().getName()) : entity; + Location.attachChild(parent, boneEntity); + } + for (Bone bone : skeleton.mesh.bones()) { + EntityRef boneEntity = skeleton.boneEntities.get(bone.getName()); + LocationComponent loc = boneEntity.getComponent(LocationComponent.class); + loc.setLocalPosition(bone.getLocalPosition()); + loc.setLocalRotation(bone.getLocalRotation()); + loc.setLocalScale(bone.getLocalScale().x); + boneEntity.saveComponent(loc); + if (bone.getParent() == null) { + skeleton.rootBone = boneEntity; + } + } + entity.saveComponent(skeleton); + } + + @Override + public void update(float delta) { + for (EntityRef entity : entityManager.getEntitiesWith(SkinnedMeshComponent.class, LocationComponent.class)) { + updateSkeletalMeshOfEntity(entity, delta); + } + } + + private void updateSkeletalMeshOfEntity(EntityRef entity, float delta) { + SkinnedMeshComponent skeletalMeshComp = entity.getComponent(SkinnedMeshComponent.class); + if (skeletalMeshComp.mesh == null) { + return; + } + + 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 if (skeletalMeshComp.animationPool != null && !skeletalMeshComp.animationPool.isEmpty()) { + newAnimation = randomAnimationData(skeletalMeshComp, random); + } else { + newAnimation = skeletalMeshComp.animation; + } + + 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(SkinnedMeshComponent skeletalMeshComp) { + return skeletalMeshComp.animation.getTimePerFrame() * (skeletalMeshComp.animation.getFrameCount() - 1); + } + + private static MeshAnimation randomAnimationData(SkinnedMeshComponent skeletalMeshComp, Random random) { + List animationPool = skeletalMeshComp.animationPool; + if (animationPool == null) { + return null; + } + if (animationPool.isEmpty()) { + return null; + } + return animationPool.get(random.nextInt(animationPool.size())); + } + + private void updateSkeleton(SkinnedMeshComponent skeletalMeshComp, MeshAnimationFrame frameA, MeshAnimationFrame frameB, + float interpolationVal) { + for (int i = 0; i < skeletalMeshComp.animation.getBoneCount(); ++i) { + String boneName = skeletalMeshComp.animation.getBoneName(i); + EntityRef boneEntity = skeletalMeshComp.boneEntities.get(boneName); + if (boneEntity == null) { + continue; + } + LocationComponent boneLoc = boneEntity.getComponent(LocationComponent.class); + if (boneLoc != null) { + Vector3f newPos = frameA.getPosition(i).lerp(frameB.getPosition(i), interpolationVal, new Vector3f()); + boneLoc.setLocalPosition(newPos); + Quaternionf newRot = frameA.getRotation(i).slerp(frameB.getRotation(i), interpolationVal, new Quaternionf()); + newRot.normalize(); + boneLoc.setLocalRotation(newRot); + boneLoc.setLocalScale(frameA.getBoneScale(i).lerp(frameB.getBoneScale(i), interpolationVal, new Vector3f()).x); + boneEntity.saveComponent(boneLoc); + } + } + } + + @Override + public void renderOpaque() { + Vector3fc cameraPosition = worldRenderer.getActiveCamera().getPosition(); + + Quaternionf worldRot = new Quaternionf(); + Vector3f worldPos = new Vector3f(); + Quaternionf inverseWorldRot = new Quaternionf(); + + FloatBuffer tempMatrixBuffer44 = BufferUtils.createFloatBuffer(16); + FloatBuffer tempMatrixBuffer33 = BufferUtils.createFloatBuffer(12); + + for (EntityRef entity : entityManager.getEntitiesWith(SkinnedMeshComponent.class, LocationComponent.class)) { + + SkinnedMeshComponent skeletalMesh = entity.getComponent(SkinnedMeshComponent.class); + if (skeletalMesh.mesh == null + || skeletalMesh.material == null + || skeletalMesh.boneEntities == null + || !skeletalMesh.material.isRenderable()) { + continue; + } + AABBfc aabb; + MeshAnimation animation = skeletalMesh.animation; + if (animation != null) { + aabb = animation.getAabb(); + } else { + aabb = skeletalMesh.mesh.getAABB(); + } + LocationComponent location = entity.getComponent(LocationComponent.class); + location.getWorldRotation(worldRot); + worldRot.invert(inverseWorldRot); + location.getWorldPosition(worldPos); + float worldScale = location.getWorldScale(); + + aabb = aabb.transform(new Matrix4f().translationRotateScale(worldPos, worldRot, worldScale), new AABBf()); + + //Scale bounding box for skeletalMesh. + Vector3f scale = skeletalMesh.scale; + + Vector3f aabbCenter = aabb.center(new Vector3f()); + Vector3f scaledExtents = aabb.extent(new Vector3f()).mul(scale.x(), scale.y(), scale.z()); + aabb = new AABBf(aabbCenter, aabbCenter).expand(scaledExtents); + + 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.setFloat3("colorOffset", skeletalMesh.color.rf(), + skeletalMesh.color.gf(), skeletalMesh.color.bf(), true); + + skeletalMesh.material.setMatrix4("projectionMatrix", worldRenderer.getActiveCamera().getProjectionMatrix()); + skeletalMesh.material.bindTextures(); + + Vector3f worldPositionCameraSpace = new Vector3f(); + worldPos.sub(cameraPosition, worldPositionCameraSpace); + + worldPositionCameraSpace.y += skeletalMesh.heightOffset; + Matrix4f matrixCameraSpace = new Matrix4f().translationRotateScale(worldPositionCameraSpace, worldRot, worldScale); + + Matrix4f modelViewMatrix = worldRenderer.getActiveCamera().getViewMatrix().mul(matrixCameraSpace, new Matrix4f()); + modelViewMatrix.get(tempMatrixBuffer44); + skeletalMesh.material.setMatrix4("modelViewMatrix", tempMatrixBuffer44, true); + + modelViewMatrix.normal(new Matrix3f()).get(tempMatrixBuffer33); + skeletalMesh.material.setMatrix3("normalMatrix", tempMatrixBuffer33, true); + + skeletalMesh.material.setFloat("sunlight", worldRenderer.getMainLightIntensityAt(worldPos), true); + skeletalMesh.material.setFloat("blockLight", worldRenderer.getBlockLightIntensityAt(worldPos), true); + + Matrix4f[] boneTransforms = new Matrix4f[skeletalMesh.mesh.bones().size()]; + for (Bone bone : skeletalMesh.mesh.bones()) { + EntityRef boneEntity = skeletalMesh.boneEntities.get(bone.getName()); + if (boneEntity == null) { + boneEntity = EntityRef.NULL; + } + LocationComponent boneLocation = boneEntity.getComponent(LocationComponent.class); + if (boneLocation != null) { + Matrix4f boneTransform = new Matrix4f(); + boneLocation.getRelativeTransform(boneTransform, entity); + boneTransform.mul(bone.getInverseBindMatrix()); + boneTransforms[bone.getIndex()] = boneTransform; + } else { + logger.warn("Unable to resolve bone \"{}\"", bone.getName()); + boneTransforms[bone.getIndex()] = new Matrix4f(); + } + } + for (int i = 0; i < boneTransforms.length; i++) { + skeletalMesh.material.setMatrix4("boneTransforms[" + i + "]", boneTransforms[i], true); + } + skeletalMesh.mesh.render(); + } + } + + @Override + public void renderOverlay() { + if (config.getRendering().getDebug().isRenderSkeletons()) { + + meshData.reallocate(0, 0); + meshData.indices.rewind(); + meshData.position.rewind(); + meshData.color0.rewind(); + + Vector3f cameraPosition = worldRenderer.getActiveCamera().getPosition(); + + Matrix4f relMat = new Matrix4f(); + Matrix4f relFinal = new Matrix4f(); + Matrix4f entityTransform = new Matrix4f(); + + Matrix4f result = new Matrix4f(); + Vector3f currentPos = new Vector3f(); + + int index = 0; + for (EntityRef entity : entityManager.getEntitiesWith(SkinnedMeshComponent.class, LocationComponent.class)) { + SkinnedMeshComponent skeletalMesh = entity.getComponent(SkinnedMeshComponent.class); + LocationComponent locationComponent = entity.getComponent(LocationComponent.class); + if (skeletalMesh.boneEntities == null) { + continue; + } + + Vector3f location = locationComponent.getWorldPosition(new Vector3f()); + Quaternionf rotation = locationComponent.getWorldRotation(new Quaternionf()); + entityTransform.translationRotateScale(location, rotation, 1.0f); // transformation of the entity + + // position is referenced around (0,0,0) (worldposition - cameraposition) + Vector3f worldPositionCameraSpace = cameraPosition.negate(new Vector3f()); + + // same heightOffset is applied to worldPositionCameraSpace from #renderOpaque() + // TODO: resolve repeated logic for transformation applied to bones + worldPositionCameraSpace.y += skeletalMesh.heightOffset; + + Matrix4f matrixCameraSpace = new Matrix4f().translationRotateScale(worldPositionCameraSpace, new Quaternionf(), 1.0f); + Matrix4f modelViewMatrix = new Matrix4f(worldRenderer.getActiveCamera().getViewMatrix()).mul(matrixCameraSpace); + material.setMatrix4("projectionMatrix", worldRenderer.getActiveCamera().getProjectionMatrix()); + material.setMatrix4("modelViewMatrix", modelViewMatrix, true); + + for (Bone bone : skeletalMesh.mesh.bones()) { + Bone parentBone = bone.getParent(); + EntityRef boneEntity = skeletalMesh.boneEntities.get(bone.getName()); + if (parentBone == null) { + continue; + } + + // TODO: the position of the bone is de-coupled from the actual translation/scale + EntityRef boneParentEntity = skeletalMesh.boneEntities.get(parentBone.getName()); + LocationComponent locCompA = boneEntity.getComponent(LocationComponent.class); + LocationComponent locCompB = boneParentEntity.getComponent(LocationComponent.class); + + // need to calculate the relative transformation from the entity to the start of the bone + locCompA.getRelativeTransform(relMat.identity(), entity); + // entityTransform * (scale, translation) * relativeMat * [x,y,z,1] + result.set(entityTransform) + .mul(relFinal.identity() + .scale(skeletalMesh.scale) + .translate(skeletalMesh.translate) + .mul(relMat)) + .transformPosition(currentPos.zero()); // get the position of the start of the bone + meshData.position.put(currentPos); // the start of the bone + + // need to calculate the relative transformation from the entity to the connecting bone + locCompB.getRelativeTransform(relMat.identity(), entity); + // entityTransform * (scale, translation) * relativeMat * [x,y,z,1] + result.set(entityTransform) + .mul(relFinal + .identity() + .scale(skeletalMesh.scale) + .translate(skeletalMesh.translate) + .mul(relMat)) + .transformPosition(currentPos.zero()); // get the position to the connecting bone + meshData.position.put(currentPos); // the end of the bone + + meshData.color0.put(Color.white); + meshData.color0.put(Color.white); + + meshData.indices.putAll(new int[]{ + index, index + 1 + }); + + index += 2; + } + + } + + GL33.glDepthFunc(GL33.GL_ALWAYS); + material.enable(); + mesh.reload(meshData); + mesh.render(); + GL33.glDepthFunc(GL33.GL_LEQUAL); + } + } +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLMeshBase.java b/engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLMeshBase.java index 97585ad50d0..3871d991e4b 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLMeshBase.java +++ b/engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLMeshBase.java @@ -57,8 +57,16 @@ default VBOContext buildVBO(int vbo, AllocationType allocation, VertexResource[] }); for (VertexResource.VertexDefinition attribute : resource.definitions()) { GL30.glEnableVertexAttribArray(attribute.location); - GL30.glVertexAttribPointer(attribute.location, attribute.attribute.count, - attribute.attribute.mapping.glType, false, resource.inStride(), offset + attribute.offset); + boolean normalized = (attribute.features & VertexResource.FEATURE_NORMALIZED) > 0; + if ((attribute.features & VertexResource.FEATURE_INTEGER) > 0) { + GL30.glVertexAttribIPointer(attribute.location, attribute.attribute.count, + attribute.attribute.mapping.glType, resource.inStride(), offset + attribute.offset); + } else { + GL30.glVertexAttribPointer(attribute.location, attribute.attribute.count, + attribute.attribute.mapping.glType, normalized, resource.inStride(), offset + attribute.offset); + } + + } offset += resource.inSize(); } diff --git a/engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLSkinnedMesh.java b/engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLSkinnedMesh.java new file mode 100644 index 00000000000..3f2c403da99 --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/opengl/OpenGLSkinnedMesh.java @@ -0,0 +1,204 @@ +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.opengl; + +import com.google.common.collect.Maps; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.lwjgl.opengl.GL30; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.core.GameThread; +import org.terasology.engine.core.subsystem.lwjgl.LwjglGraphicsProcessing; +import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; +import org.terasology.engine.rendering.assets.mesh.SkinnedMeshData; +import org.terasology.engine.rendering.assets.mesh.resource.AllocationType; +import org.terasology.engine.rendering.assets.mesh.resource.DrawingMode; +import org.terasology.engine.rendering.assets.mesh.resource.IndexResource; +import org.terasology.engine.rendering.assets.mesh.resource.VertexAttributeBinding; +import org.terasology.engine.rendering.assets.mesh.resource.VertexByteAttributeBinding; +import org.terasology.engine.rendering.assets.skeletalmesh.Bone; +import org.terasology.gestalt.assets.AssetType; +import org.terasology.gestalt.assets.DisposableResource; +import org.terasology.gestalt.assets.ResourceUrn; +import org.terasology.joml.geom.AABBf; +import org.terasology.joml.geom.AABBfc; + +import java.util.List; +import java.util.Map; + +import static org.lwjgl.opengl.GL11.GL_UNSIGNED_INT; + +public class OpenGLSkinnedMesh extends SkinnedMesh implements OpenGLMeshBase { + private static final Logger logger = LoggerFactory.getLogger(OpenGLSkinnedMesh.class); + private OpenGLSkinnedMesh.DisposalAction disposalAction; + private DrawingMode drawMode; + private AllocationType allocationType; + private VBOContext state = null; + private AABBf aabb = new AABBf(); + + private VertexAttributeBinding positions; + private VertexByteAttributeBinding boneIndex0; + private VertexByteAttributeBinding boneIndex1; + private VertexByteAttributeBinding boneIndex2; + private VertexByteAttributeBinding boneIndex3; + private Map boneLookup = Maps.newHashMap(); + + private List bones; + + private int indexCount; + + protected OpenGLSkinnedMesh(ResourceUrn urn, AssetType assetType, SkinnedMeshData data, + OpenGLSkinnedMesh.DisposalAction disposalAction, LwjglGraphicsProcessing graphicsProcessing) { + super(urn, assetType, disposalAction); + this.disposalAction = disposalAction; + graphicsProcessing.asynchToDisplayThread(() -> { + reload(data); + }); + } + + public static OpenGLSkinnedMesh create(ResourceUrn urn, AssetType assetType, SkinnedMeshData data, + LwjglGraphicsProcessing graphicsProcessing) { + return new OpenGLSkinnedMesh(urn, assetType, data, new OpenGLSkinnedMesh.DisposalAction(urn), graphicsProcessing); + } + + @Override + protected void doReload(SkinnedMeshData newData) { + try { + GameThread.synch(() -> buildMesh(newData)); + } catch (InterruptedException e) { + logger.error("Failed to reload {}", getUrn(), e); + } + } + + @Override + public VertexByteAttributeBinding boneIndex0() { + return boneIndex0; + } + + @Override + public VertexByteAttributeBinding boneIndex1() { + return boneIndex1; + } + + @Override + public VertexByteAttributeBinding boneIndex2() { + return boneIndex2; + } + + @Override + public VertexByteAttributeBinding boneIndex3() { + return boneIndex3; + } + + @Override + public AABBfc getAABB() { + return aabb; + } + + @Override + public VertexAttributeBinding vertices() { + return positions; + } + + @Override + public int elementCount() { + return positions.elements(); + } + + @Override + public List bones() { + return bones; + } + + @Override + public Bone getBone(String name) { + return boneLookup.get(name); + } + + @Override + public void render() { + if (!isDisposed()) { + updateState(state); + GL30.glBindVertexArray(disposalAction.vao); + if (this.indexCount == 0) { + GL30.glDrawArrays(drawMode.glCall, 0, positions.elements()); + } else { + GL30.glDrawElements(drawMode.glCall, this.indexCount, GL_UNSIGNED_INT, 0); + } + GL30.glBindVertexArray(0); + } else { + logger.error("Attempted to render disposed mesh: {}", getUrn()); + } + } + + private void buildMesh(SkinnedMeshData newData) { + if (this.disposalAction.vao == 0) { + this.disposalAction.vao = GL30.glGenVertexArrays(); + this.disposalAction.vbo = GL30.glGenBuffers(); + this.disposalAction.ebo = GL30.glGenBuffers(); + } + allocationType = newData.allocationType(); + drawMode = newData.getMode(); + + GL30.glBindVertexArray(this.disposalAction.vao); + this.boneLookup.clear(); + this.positions = newData.positions(); + this.boneIndex0 = newData.boneIndex0(); + this.boneIndex1 = newData.boneIndex0(); + this.boneIndex2 = newData.boneIndex0(); + this.boneIndex3 = newData.boneIndex0(); + this.bones = newData.bones(); + this.bones.forEach(b -> { + boneLookup.put(b.getName(), b); + }); + this.state = buildVBO(this.disposalAction.vbo, allocationType, newData.vertexResources()); + + IndexResource indexResource = newData.indexResource(); + this.indexCount = indexResource.indices(); + GL30.glBindBuffer(GL30.GL_ELEMENT_ARRAY_BUFFER, this.disposalAction.ebo); + indexResource.writeBuffer((buffer) -> GL30.glBufferData(GL30.GL_ELEMENT_ARRAY_BUFFER, buffer, GL30.GL_STATIC_DRAW)); + + GL30.glBindVertexArray(0); + getBound(aabb); + } + + private static class DisposalAction implements DisposableResource { + private final ResourceUrn urn; + + private int vao = 0; + private int vbo = 0; + private int ebo = 0; + + private DisposalAction(ResourceUrn urn) { + this.urn = urn; + } + + public void dispose() { + if (vao != 0) { + GL30.glDeleteVertexArrays(vao); + } + if (vbo != 0) { + GL30.glDeleteBuffers(vbo); + } + if (ebo != 0) { + GL30.glDeleteBuffers(ebo); + } + vao = 0; + vbo = 0; + ebo = 0; + } + + @Override + public void close() { + try { + GameThread.synch(() -> { + dispose(); + }); + } catch (InterruptedException e) { + logger.error("Failed to dispose {}", urn, e); + } + } + } +} diff --git a/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial.info b/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial.info new file mode 100644 index 00000000000..d25448de720 --- /dev/null +++ b/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial.info @@ -0,0 +1,8 @@ +{ + "params": [ + { + "name": "diffuse", + "type": "sampler2D" + } + ] +} \ No newline at end of file diff --git a/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_frag.glsl b/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_frag.glsl new file mode 100644 index 00000000000..137d6d5bba6 --- /dev/null +++ b/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_frag.glsl @@ -0,0 +1,34 @@ +#version 330 core +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +uniform sampler2D diffuse; + +uniform float blockLight = 1.0; +uniform float sunlight = 1.0; + +uniform vec3 colorOffset; +uniform bool textured; + +in vec3 v_normal; +in vec2 v_uv0; + +layout(location = 0) out vec4 outColor; +layout(location = 1) out vec4 outNormal; +layout(location = 2) out vec4 outLight; + +void main(){ + vec4 color; + + if (textured) { + color = texture(diffuse, v_uv0); + color.rgb *= colorOffset.rgb; + outColor.rgba = color; + } else { + color = vec4(colorOffset.rgb, 1.0); + outColor.rgba = color; + } + + outNormal.rgba = vec4(v_normal.x / 2.0 + 0.5, v_normal.y / 2.0 + 0.5, v_normal.z / 2.0 + 0.5, 0.0); + outLight.rgba = vec4(blockLight, sunlight, 0.0, 0.0); +} diff --git a/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_vert.glsl b/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_vert.glsl new file mode 100644 index 00000000000..0719cd23785 --- /dev/null +++ b/engine/src/main/resources/org/terasology/engine/assets/shaders/skinnedGenericMeshMaterial_vert.glsl @@ -0,0 +1,31 @@ +#version 330 core +// Copyright 2021 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +layout (location = 0) in vec3 in_vert; +layout (location = 1) in vec3 in_normal; +layout (location = 2) in vec2 in_uv0; + +layout (location = 4) in int in_bone[4]; +layout (location = 8) in vec4 in_weight; + +uniform mat3 normalMatrix; +uniform mat4 modelViewMatrix; +uniform mat4 projectionMatrix; + +out vec3 v_normal; +out vec2 v_uv0; + +uniform mat4 boneTransforms[254]; + +void main() { + mat4 skinMat = boneTransforms[int(in_bone[0])] * in_weight.x; + skinMat += boneTransforms[int(in_bone[1])] * in_weight.y; + skinMat += boneTransforms[int(in_bone[2])] * in_weight.z; + skinMat += boneTransforms[int(in_bone[3])] * in_weight.w; + + gl_Position = (projectionMatrix * modelViewMatrix * skinMat) * vec4(in_vert, 1.0); + v_normal = normalMatrix * in_normal * mat3(skinMat); + v_uv0 = in_uv0; + +} From 6b60fbe794008d80836bf816cfb46c71c0b3fef4 Mon Sep 17 00:00:00 2001 From: Michael Pollind Date: Sat, 4 Dec 2021 23:53:37 -0800 Subject: [PATCH 2/2] chore: simplify skinned mesh component --- .../assets/animation/MeshAnimation.java | 4 + .../rendering/logic/SkinnedMeshComponent.java | 42 ++--- .../rendering/logic/SkinnedMeshRenderer.java | 147 ++++++------------ 3 files changed, 59 insertions(+), 134 deletions(-) diff --git a/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java b/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java index 1dc7757d05d..67407ec01b9 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java +++ b/engine/src/main/java/org/terasology/engine/rendering/assets/animation/MeshAnimation.java @@ -28,4 +28,8 @@ protected MeshAnimation(ResourceUrn urn, AssetType assetTy public abstract float getTimePerFrame(); public abstract AABBf getAabb(); + + public float getDuration() { + return getTimePerFrame() * (getFrameCount() - 1); + } } diff --git a/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java index 74daaf84d0a..84c5093b32b 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java +++ b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshComponent.java @@ -3,8 +3,8 @@ package org.terasology.engine.rendering.logic; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import org.joml.Quaternionf; import org.joml.Vector3f; import org.terasology.engine.entitySystem.Owns; import org.terasology.engine.entitySystem.entity.EntityRef; @@ -14,9 +14,7 @@ import org.terasology.engine.rendering.assets.mesh.SkinnedMesh; import org.terasology.engine.world.block.ForceBlockActive; import org.terasology.nui.Color; -import org.terasology.nui.properties.Range; -import java.util.List; import java.util.Map; @ForceBlockActive @@ -27,55 +25,35 @@ public class SkinnedMeshComponent implements VisualComponent animationPool = Lists.newArrayList(); - - public float animationRate = 1.0f; - @Range(min = -2.5f, max = 2.5f) - public float heightOffset; + public float currentTime; @Owns public Map boneEntities; public EntityRef rootBone = EntityRef.NULL; - public float animationTime; @Replicate - public Vector3f scale = new Vector3f(1, 1, 1); + public float localScale = 1.0f; + @Replicate + public Vector3f localOffset = new Vector3f(); @Replicate - public Vector3f translate = new Vector3f(); + public Quaternionf localRotation = new Quaternionf(); @Replicate - public Color color = Color.WHITE; + public Color color = new Color(Color.white); @Override public void copyFrom(SkinnedMeshComponent other) { this.mesh = other.mesh; this.material = other.material; this.animation = other.animation; - this.loop = other.loop; - this.animationPool = Lists.newArrayList(other.animationPool); - this.animationRate = other.animationRate; this.boneEntities = Maps.newHashMap(other.boneEntities); this.rootBone = other.rootBone; - this.animationTime = other.animationTime; - this.scale = new Vector3f(other.scale); - this.translate = new Vector3f(other.translate); + this.localScale = other.localScale; + this.localOffset.set(other.localOffset); + this.localRotation.set(other.localRotation); this.color = new Color(other.color); } } diff --git a/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java index 63285407bcf..26983fba166 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java +++ b/engine/src/main/java/org/terasology/engine/rendering/logic/SkinnedMeshRenderer.java @@ -17,7 +17,6 @@ import org.terasology.engine.entitySystem.entity.EntityManager; import org.terasology.engine.entitySystem.entity.EntityRef; import org.terasology.engine.entitySystem.entity.lifecycleEvents.OnActivatedComponent; -import org.terasology.engine.entitySystem.event.ReceiveEvent; import org.terasology.engine.entitySystem.systems.BaseComponentSystem; import org.terasology.engine.entitySystem.systems.RegisterMode; import org.terasology.engine.entitySystem.systems.RegisterSystem; @@ -37,12 +36,12 @@ import org.terasology.engine.rendering.world.WorldRenderer; import org.terasology.engine.utilities.Assets; import org.terasology.gestalt.assets.management.AssetManager; +import org.terasology.gestalt.entitysystem.event.ReceiveEvent; import org.terasology.joml.geom.AABBf; import org.terasology.joml.geom.AABBfc; import org.terasology.nui.Color; import java.nio.FloatBuffer; -import java.util.List; import java.util.Random; /** @@ -130,10 +129,6 @@ private void updateSkeletalMeshOfEntity(EntityRef entity, float delta) { return; } - if (skeletalMeshComp.animation == null && skeletalMeshComp.animationPool != null) { - skeletalMeshComp.animation = randomAnimationData(skeletalMeshComp, random); - } - if (skeletalMeshComp.animation == null) { return; } @@ -141,83 +136,61 @@ private void updateSkeletalMeshOfEntity(EntityRef entity, float delta) { 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 if (skeletalMeshComp.animationPool != null && !skeletalMeshComp.animationPool.isEmpty()) { - newAnimation = randomAnimationData(skeletalMeshComp, random); - } else { - newAnimation = skeletalMeshComp.animation; - } - 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; + if (skeletalMeshComp.rootBone != null) { + LocationComponent locationComponent = skeletalMeshComp.rootBone.getComponent(LocationComponent.class); + if (locationComponent != null) { + locationComponent.setLocalPosition(skeletalMeshComp.localOffset); + locationComponent.setLocalScale(skeletalMeshComp.localScale); + locationComponent.setLocalRotation(skeletalMeshComp.localRotation); } - skeletalMeshComp.animationTime -= animationDuration; - if (skeletalMeshComp.animationTime < 0) { - // In case the float calculation wasn't exact: - skeletalMeshComp.animationTime = 0; + } + + float animationDuration = skeletalMeshComp.animation.getDuration(); + if (skeletalMeshComp.currentTime >= animationDuration) { + skeletalMeshComp.currentTime -= animationDuration; + if (skeletalMeshComp.currentTime < 0) { + skeletalMeshComp.currentTime = 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; + + float framePos = skeletalMeshComp.currentTime / skeletalMeshComp.animation.getTimePerFrame(); + int currentFrame = (int) framePos; + int nextFrame = currentFrame + 1; + if (nextFrame >= skeletalMeshComp.animation.getFrameCount()) { + nextFrame = 0; } - MeshAnimationFrame frameA = skeletalMeshComp.animation.getFrame(frameAId); - MeshAnimationFrame frameB = skeletalMeshComp.animation.getFrame(frameBId); - updateSkeleton(skeletalMeshComp, frameA, frameB, framePos - frameAId); + float frameDelta = framePos - currentFrame; + MeshAnimationFrame animatedFrame1 = skeletalMeshComp.animation.getFrame(currentFrame); + MeshAnimationFrame animatedFrame2 = skeletalMeshComp.animation.getFrame(nextFrame); + updateFrame(skeletalMeshComp, animatedFrame1, animatedFrame2, frameDelta); entity.saveComponent(skeletalMeshComp); } - private float getDurationOfAnimation(SkinnedMeshComponent skeletalMeshComp) { - return skeletalMeshComp.animation.getTimePerFrame() * (skeletalMeshComp.animation.getFrameCount() - 1); - } + private void updateFrame(SkinnedMeshComponent skeletalMeshComp, MeshAnimationFrame frameA, MeshAnimationFrame frameB, + float interpolationVal) { - private static MeshAnimation randomAnimationData(SkinnedMeshComponent skeletalMeshComp, Random random) { - List animationPool = skeletalMeshComp.animationPool; - if (animationPool == null) { - return null; - } - if (animationPool.isEmpty()) { - return null; - } - return animationPool.get(random.nextInt(animationPool.size())); - } - private void updateSkeleton(SkinnedMeshComponent skeletalMeshComp, MeshAnimationFrame frameA, MeshAnimationFrame frameB, - float interpolationVal) { for (int i = 0; i < skeletalMeshComp.animation.getBoneCount(); ++i) { String boneName = skeletalMeshComp.animation.getBoneName(i); EntityRef boneEntity = skeletalMeshComp.boneEntities.get(boneName); if (boneEntity == null) { continue; } + LocationComponent boneLoc = boneEntity.getComponent(LocationComponent.class); - if (boneLoc != null) { - Vector3f newPos = frameA.getPosition(i).lerp(frameB.getPosition(i), interpolationVal, new Vector3f()); - boneLoc.setLocalPosition(newPos); - Quaternionf newRot = frameA.getRotation(i).slerp(frameB.getRotation(i), interpolationVal, new Quaternionf()); - newRot.normalize(); - boneLoc.setLocalRotation(newRot); - boneLoc.setLocalScale(frameA.getBoneScale(i).lerp(frameB.getBoneScale(i), interpolationVal, new Vector3f()).x); - boneEntity.saveComponent(boneLoc); + if (boneLoc == null) { + continue; } + + Vector3f newPos = frameA.getPosition(i).lerp(frameB.getPosition(i), interpolationVal, new Vector3f()); + boneLoc.setLocalPosition(newPos); + Quaternionf newRot = frameA.getRotation(i).slerp(frameB.getRotation(i), interpolationVal, new Quaternionf()); + newRot.normalize(); + boneLoc.setLocalRotation(newRot); + boneLoc.setLocalScale(frameA.getBoneScale(i).lerp(frameB.getBoneScale(i), interpolationVal, new Vector3f()).x); + boneEntity.saveComponent(boneLoc); + } } @@ -257,10 +230,10 @@ public void renderOpaque() { aabb = aabb.transform(new Matrix4f().translationRotateScale(worldPos, worldRot, worldScale), new AABBf()); //Scale bounding box for skeletalMesh. - Vector3f scale = skeletalMesh.scale; + float scale = skeletalMesh.localScale; Vector3f aabbCenter = aabb.center(new Vector3f()); - Vector3f scaledExtents = aabb.extent(new Vector3f()).mul(scale.x(), scale.y(), scale.z()); + Vector3f scaledExtents = aabb.extent(new Vector3f()).mul(scale, scale, scale); aabb = new AABBf(aabbCenter, aabbCenter).expand(scaledExtents); if (!worldRenderer.getActiveCamera().hasInSight(aabb)) { @@ -279,7 +252,6 @@ public void renderOpaque() { Vector3f worldPositionCameraSpace = new Vector3f(); worldPos.sub(cameraPosition, worldPositionCameraSpace); - worldPositionCameraSpace.y += skeletalMesh.heightOffset; Matrix4f matrixCameraSpace = new Matrix4f().translationRotateScale(worldPositionCameraSpace, worldRot, worldScale); Matrix4f modelViewMatrix = worldRenderer.getActiveCamera().getViewMatrix().mul(matrixCameraSpace, new Matrix4f()); @@ -292,25 +264,21 @@ public void renderOpaque() { skeletalMesh.material.setFloat("sunlight", worldRenderer.getMainLightIntensityAt(worldPos), true); skeletalMesh.material.setFloat("blockLight", worldRenderer.getBlockLightIntensityAt(worldPos), true); - Matrix4f[] boneTransforms = new Matrix4f[skeletalMesh.mesh.bones().size()]; + Matrix4f boneTransform = new Matrix4f(); for (Bone bone : skeletalMesh.mesh.bones()) { EntityRef boneEntity = skeletalMesh.boneEntities.get(bone.getName()); if (boneEntity == null) { boneEntity = EntityRef.NULL; } LocationComponent boneLocation = boneEntity.getComponent(LocationComponent.class); + boneTransform.identity(); if (boneLocation != null) { - Matrix4f boneTransform = new Matrix4f(); boneLocation.getRelativeTransform(boneTransform, entity); boneTransform.mul(bone.getInverseBindMatrix()); - boneTransforms[bone.getIndex()] = boneTransform; } else { logger.warn("Unable to resolve bone \"{}\"", bone.getName()); - boneTransforms[bone.getIndex()] = new Matrix4f(); } - } - for (int i = 0; i < boneTransforms.length; i++) { - skeletalMesh.material.setMatrix4("boneTransforms[" + i + "]", boneTransforms[i], true); + skeletalMesh.material.setMatrix4("boneTransforms[" + bone.getIndex() + "]", boneTransform, true); } skeletalMesh.mesh.render(); } @@ -327,11 +295,7 @@ public void renderOverlay() { Vector3f cameraPosition = worldRenderer.getActiveCamera().getPosition(); - Matrix4f relMat = new Matrix4f(); - Matrix4f relFinal = new Matrix4f(); Matrix4f entityTransform = new Matrix4f(); - - Matrix4f result = new Matrix4f(); Vector3f currentPos = new Vector3f(); int index = 0; @@ -349,10 +313,6 @@ public void renderOverlay() { // position is referenced around (0,0,0) (worldposition - cameraposition) Vector3f worldPositionCameraSpace = cameraPosition.negate(new Vector3f()); - // same heightOffset is applied to worldPositionCameraSpace from #renderOpaque() - // TODO: resolve repeated logic for transformation applied to bones - worldPositionCameraSpace.y += skeletalMesh.heightOffset; - Matrix4f matrixCameraSpace = new Matrix4f().translationRotateScale(worldPositionCameraSpace, new Quaternionf(), 1.0f); Matrix4f modelViewMatrix = new Matrix4f(worldRenderer.getActiveCamera().getViewMatrix()).mul(matrixCameraSpace); material.setMatrix4("projectionMatrix", worldRenderer.getActiveCamera().getProjectionMatrix()); @@ -370,27 +330,10 @@ public void renderOverlay() { LocationComponent locCompA = boneEntity.getComponent(LocationComponent.class); LocationComponent locCompB = boneParentEntity.getComponent(LocationComponent.class); - // need to calculate the relative transformation from the entity to the start of the bone - locCompA.getRelativeTransform(relMat.identity(), entity); - // entityTransform * (scale, translation) * relativeMat * [x,y,z,1] - result.set(entityTransform) - .mul(relFinal.identity() - .scale(skeletalMesh.scale) - .translate(skeletalMesh.translate) - .mul(relMat)) - .transformPosition(currentPos.zero()); // get the position of the start of the bone + locCompA.getWorldPosition(currentPos); meshData.position.put(currentPos); // the start of the bone - // need to calculate the relative transformation from the entity to the connecting bone - locCompB.getRelativeTransform(relMat.identity(), entity); - // entityTransform * (scale, translation) * relativeMat * [x,y,z,1] - result.set(entityTransform) - .mul(relFinal - .identity() - .scale(skeletalMesh.scale) - .translate(skeletalMesh.translate) - .mul(relMat)) - .transformPosition(currentPos.zero()); // get the position to the connecting bone + locCompB.getWorldPosition(currentPos); meshData.position.put(currentPos); // the end of the bone meshData.color0.put(Color.white);