From 0710bdf3f5a64e5fe1c725a30421b2c7523dca44 Mon Sep 17 00:00:00 2001 From: RainWarrior Date: Fri, 1 Jan 2016 18:15:48 +0300 Subject: [PATCH] Model animation system. Main things of interest: * IAnimationStateMachine - state machine for animations; can load from json. * AnimationTESR - automatic TESR for animated models. * AnimationModelBase - same for entities. * ITimeValue - time-varying value, used to control animation parameters from code. * TESRs can now be batched - look at TESR.renderTileEntityFast + TE.hasFastRenderer. * RegionRenderCache is not accessible to TESRs and other client-side logic - MinecraftForgeClient.getRegionRenderCache. --- .../client/renderer/RenderGlobal.java.patch | 26 +- .../renderer/chunk/RenderChunk.java.patch | 14 +- .../TileEntityRendererDispatcher.java.patch | 79 +++ .../TileEntitySpecialRenderer.java.patch | 9 + .../tileentity/TileEntity.java.patch | 11 +- .../client/ForgeHooksClient.java | 14 +- .../client/MinecraftForgeClient.java | 36 ++ .../client/model/ModelLoader.java | 84 ++- .../client/model/animation/Animation.java | 184 ++++++ .../model/animation/AnimationModelBase.java | 108 ++++ .../animation/AnimationStateMachine.java | 169 ++++++ .../client/model/animation/AnimationTESR.java | 79 +++ .../client/model/animation/Clips.java | 538 ++++++++++++++++++ .../client/model/animation/Event.java | 46 ++ .../client/model/animation/FastTESR.java | 49 ++ .../model/animation/IAnimatedModel.java | 13 + .../model/animation/IAnimationProvider.java | 12 + .../client/model/animation/IClip.java | 12 + .../client/model/animation/IEventHandler.java | 10 + .../client/model/animation/IJoint.java | 16 + .../client/model/animation/IJointClip.java | 11 + .../client/model/animation/ITimeValue.java | 12 + .../client/model/animation/JointClips.java | 36 ++ .../model/animation/ModelBlockAnimation.java | 509 +++++++++++++++++ .../client/model/animation/TimeValues.java | 370 ++++++++++++ .../client/model/b3d/B3DClip.java | 80 +++ .../client/model/b3d/B3DLoader.java | 68 ++- .../client/model/b3d/B3DModel.java | 30 + .../client/model/pipeline/LightUtil.java | 112 ++-- .../model/pipeline/VertexLighterFlat.java | 10 +- .../model/pipeline/VertexLighterSmoothAo.java | 21 +- .../model/pipeline/WorldRendererConsumer.java | 6 +- .../animation/IAnimationStateMachine.java | 36 ++ .../common/property/Properties.java | 18 + .../minecraftforge/common/util/JsonUtils.java | 73 +++ .../fml/client/FMLClientHandler.java | 3 + .../debug/ModelAnimationDebug.java | 341 +++++++---- .../armatures/block/engine_ring.json | 64 +++ .../asms/block/chest.json | 37 ++ .../asms/block/engine.json | 26 + .../asms/block/engine2.json | 23 + .../blockstates/test_animation_block.json | 28 +- .../models/block/engine_ring.json | 4 +- .../blockstates/CustomModelBlock.json | 10 +- 44 files changed, 3185 insertions(+), 252 deletions(-) create mode 100644 patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java.patch create mode 100644 patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer.java.patch create mode 100644 src/main/java/net/minecraftforge/client/model/animation/Animation.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/AnimationModelBase.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/AnimationStateMachine.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/AnimationTESR.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/Clips.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/Event.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/FastTESR.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/IAnimatedModel.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/IAnimationProvider.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/IClip.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/IEventHandler.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/IJoint.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/IJointClip.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/ITimeValue.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/JointClips.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/ModelBlockAnimation.java create mode 100644 src/main/java/net/minecraftforge/client/model/animation/TimeValues.java create mode 100644 src/main/java/net/minecraftforge/client/model/b3d/B3DClip.java create mode 100644 src/main/java/net/minecraftforge/common/model/animation/IAnimationStateMachine.java create mode 100644 src/main/java/net/minecraftforge/common/util/JsonUtils.java create mode 100644 src/test/resources/assets/forgedebugmodelanimation/armatures/block/engine_ring.json create mode 100644 src/test/resources/assets/forgedebugmodelanimation/asms/block/chest.json create mode 100644 src/test/resources/assets/forgedebugmodelanimation/asms/block/engine.json create mode 100644 src/test/resources/assets/forgedebugmodelanimation/asms/block/engine2.json diff --git a/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.java.patch b/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.java.patch index 395aa5466..cee9639e6 100644 --- a/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.java.patch +++ b/patches/minecraft/net/minecraft/client/renderer/RenderGlobal.java.patch @@ -56,7 +56,15 @@ flag2 = this.field_175010_j.func_178635_a(entity2, p_180446_2_, d0, d1, d2) || entity2.field_70153_n == this.field_72777_q.field_71439_g; if (!flag2) -@@ -662,6 +673,7 @@ +@@ -654,6 +665,7 @@ + this.field_72769_h.field_72984_F.func_76318_c("blockentities"); + RenderHelper.func_74519_b(); + ++ TileEntityRendererDispatcher.field_147556_a.preDrawBatch(); + for (RenderGlobal.ContainerLocalRenderInformation renderglobal$containerlocalrenderinformation1 : this.field_72755_R) + { + List list1 = renderglobal$containerlocalrenderinformation1.field_178036_a.func_178571_g().func_178485_b(); +@@ -662,6 +674,7 @@ { for (TileEntity tileentity2 : list1) { @@ -64,7 +72,7 @@ TileEntityRendererDispatcher.field_147556_a.func_180546_a(tileentity2, p_180446_3_, -1); } } -@@ -671,6 +683,7 @@ +@@ -671,9 +684,11 @@ { for (TileEntity tileentity : this.field_181024_n) { @@ -72,7 +80,11 @@ TileEntityRendererDispatcher.field_147556_a.func_180546_a(tileentity, p_180446_3_, -1); } } -@@ -700,7 +713,7 @@ ++ TileEntityRendererDispatcher.field_147556_a.drawBatch(pass); + + this.func_180443_s(); + +@@ -700,7 +715,7 @@ Block block = this.field_72769_h.func_180495_p(blockpos).func_177230_c(); @@ -81,7 +93,7 @@ { TileEntityRendererDispatcher.field_147556_a.func_180546_a(tileentity1, p_180446_3_, destroyblockprogress.func_73106_e()); } -@@ -1161,6 +1174,12 @@ +@@ -1161,6 +1176,12 @@ public void func_174976_a(float p_174976_1_, int p_174976_2_) { @@ -94,7 +106,7 @@ if (this.field_72777_q.field_71441_e.field_73011_w.func_177502_q() == 1) { this.func_180448_r(); -@@ -1378,6 +1397,12 @@ +@@ -1378,6 +1399,12 @@ public void func_180447_b(float p_180447_1_, int p_180447_2_) { @@ -107,7 +119,7 @@ if (this.field_72777_q.field_71441_e.field_73011_w.func_76569_d()) { if (this.field_72777_q.field_71474_y.func_181147_e() == 2) -@@ -1793,8 +1818,11 @@ +@@ -1793,8 +1820,11 @@ double d4 = (double)blockpos.func_177956_o() - d1; double d5 = (double)blockpos.func_177952_p() - d2; Block block = this.field_72769_h.func_180495_p(blockpos).func_177230_c(); @@ -120,7 +132,7 @@ { if (d3 * d3 + d4 * d4 + d5 * d5 > 1024.0D) { -@@ -1949,13 +1977,16 @@ +@@ -1949,13 +1979,16 @@ if (p_174961_1_ != null) { ItemRecord itemrecord = ItemRecord.func_150926_b(p_174961_1_); diff --git a/patches/minecraft/net/minecraft/client/renderer/chunk/RenderChunk.java.patch b/patches/minecraft/net/minecraft/client/renderer/chunk/RenderChunk.java.patch index f88c6b175..c7abba832 100644 --- a/patches/minecraft/net/minecraft/client/renderer/chunk/RenderChunk.java.patch +++ b/patches/minecraft/net/minecraft/client/renderer/chunk/RenderChunk.java.patch @@ -1,15 +1,17 @@ --- ../src-base/minecraft/net/minecraft/client/renderer/chunk/RenderChunk.java +++ ../src-work/minecraft/net/minecraft/client/renderer/chunk/RenderChunk.java -@@ -131,7 +131,7 @@ +@@ -131,7 +131,9 @@ return; } - iblockaccess = new RegionRenderCache(this.field_178588_d, blockpos.func_177982_a(-1, -1, -1), blockpos1.func_177982_a(1, 1, 1), 1); -+ iblockaccess = createRegionRenderCache(this.field_178588_d, blockpos.func_177982_a(-1, -1, -1), blockpos1.func_177982_a(1, 1, 1), 1); ++ RegionRenderCache cache = createRegionRenderCache(this.field_178588_d, blockpos.func_177982_a(-1, -1, -1), blockpos1.func_177982_a(1, 1, 1), 1); ++ net.minecraftforge.client.MinecraftForgeClient.onRebuildChunk(field_178588_d, field_178586_f, cache); ++ iblockaccess = cache; p_178581_4_.func_178543_a(compiledchunk); } finally -@@ -158,7 +158,7 @@ +@@ -158,7 +160,7 @@ lvt_10_1_.func_178606_a(blockpos$mutableblockpos); } @@ -18,7 +20,7 @@ { TileEntity tileentity = iblockaccess.func_175625_s(new BlockPos(blockpos$mutableblockpos)); TileEntitySpecialRenderer tileentityspecialrenderer = TileEntityRendererDispatcher.field_147556_a.func_147547_b(tileentity); -@@ -174,7 +174,9 @@ +@@ -174,7 +176,9 @@ } } @@ -29,7 +31,7 @@ int j = enumworldblocklayer1.ordinal(); if (block.func_149645_b() != -1) -@@ -189,6 +191,7 @@ +@@ -189,6 +193,7 @@ aboolean[j] |= blockrendererdispatcher.func_175018_a(iblockstate, blockpos$mutableblockpos, iblockaccess, worldrenderer); } @@ -37,7 +39,7 @@ } for (EnumWorldBlockLayer enumworldblocklayer : EnumWorldBlockLayer.values()) -@@ -385,6 +388,26 @@ +@@ -385,6 +390,26 @@ return this.field_178593_n; } diff --git a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java.patch b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java.patch new file mode 100644 index 000000000..92a84ea6b --- /dev/null +++ b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java.patch @@ -0,0 +1,79 @@ +--- ../src-base/minecraft/net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java ++++ ../src-work/minecraft/net/minecraft/client/renderer/tileentity/TileEntityRendererDispatcher.java +@@ -102,11 +102,14 @@ + { + if (p_180546_1_.func_145835_a(this.field_147560_j, this.field_147561_k, this.field_147558_l) < p_180546_1_.func_145833_n()) + { ++ if(!p_180546_1_.hasFastRenderer()) ++ { + int i = this.field_147550_f.func_175626_b(p_180546_1_.func_174877_v(), 0); + int j = i % 65536; + int k = i / 65536; + OpenGlHelper.func_77475_a(OpenGlHelper.field_77476_b, (float)j / 1.0F, (float)k / 1.0F); + GlStateManager.func_179131_c(1.0F, 1.0F, 1.0F, 1.0F); ++ } + BlockPos blockpos = p_180546_1_.func_174877_v(); + this.func_178469_a(p_180546_1_, (double)blockpos.func_177958_n() - field_147554_b, (double)blockpos.func_177956_o() - field_147555_c, (double)blockpos.func_177952_p() - field_147552_d, p_180546_2_, p_180546_3_); + } +@@ -125,6 +128,11 @@ + { + try + { ++ if(p_178469_1_.hasFastRenderer()) ++ { ++ tileentityspecialrenderer.renderTileEntityFast(p_178469_1_, p_178469_2_, p_178469_4_, p_178469_6_, p_178469_8_, p_178469_9_, batchBuffer.func_178180_c()); ++ } ++ else + tileentityspecialrenderer.func_180535_a(p_178469_1_, p_178469_2_, p_178469_4_, p_178469_6_, p_178469_8_, p_178469_9_); + } + catch (Throwable throwable) +@@ -146,4 +154,49 @@ + { + return this.field_147557_n; + } ++ ++ /* ======================================== FORGE START =====================================*/ ++ /** ++ * Buffer used for batched TESRs ++ */ ++ private net.minecraft.client.renderer.Tessellator batchBuffer = new net.minecraft.client.renderer.Tessellator(0x200000); ++ ++ /** ++ * Prepare for a batched TESR rendering. ++ * You probably shouldn't call this manually. ++ */ ++ public void preDrawBatch() ++ { ++ batchBuffer.func_178180_c().func_181668_a(org.lwjgl.opengl.GL11.GL_QUADS, net.minecraft.client.renderer.vertex.DefaultVertexFormats.field_176600_a); ++ } ++ ++ /** ++ * Render all TESRs batched so far. ++ * You probably shouldn't call this manually. ++ */ ++ public void drawBatch(int pass) ++ { ++ field_147553_e.func_110577_a(net.minecraft.client.renderer.texture.TextureMap.field_110575_b); ++ net.minecraft.client.renderer.RenderHelper.func_74518_a(); ++ GlStateManager.func_179112_b(org.lwjgl.opengl.GL11.GL_SRC_ALPHA, org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA); ++ GlStateManager.func_179147_l(); ++ GlStateManager.func_179129_p(); ++ ++ if (net.minecraft.client.Minecraft.func_71379_u()) ++ { ++ GlStateManager.func_179103_j(org.lwjgl.opengl.GL11.GL_SMOOTH); ++ } ++ else ++ { ++ GlStateManager.func_179103_j(org.lwjgl.opengl.GL11.GL_FLAT); ++ } ++ ++ if(pass > 0) ++ { ++ batchBuffer.func_178180_c().func_181674_a((float)field_147554_b, (float)field_147555_c, (float)field_147552_d); ++ } ++ batchBuffer.func_78381_a(); ++ ++ net.minecraft.client.renderer.RenderHelper.func_74519_b(); ++ } + } diff --git a/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer.java.patch b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer.java.patch new file mode 100644 index 000000000..2dcbbec25 --- /dev/null +++ b/patches/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer.java.patch @@ -0,0 +1,9 @@ +--- ../src-base/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer.java ++++ ../src-work/minecraft/net/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer.java +@@ -45,4 +45,6 @@ + { + return false; + } ++ ++ public void renderTileEntityFast(T te, double x, double y, double z, float partialTicks, int destroyStage, net.minecraft.client.renderer.WorldRenderer worldRenderer) {} + } diff --git a/patches/minecraft/net/minecraft/tileentity/TileEntity.java.patch b/patches/minecraft/net/minecraft/tileentity/TileEntity.java.patch index 595547873..538523ac7 100644 --- a/patches/minecraft/net/minecraft/tileentity/TileEntity.java.patch +++ b/patches/minecraft/net/minecraft/tileentity/TileEntity.java.patch @@ -65,7 +65,7 @@ public double func_145835_a(double p_145835_1_, double p_145835_3_, double p_145835_5_) { double d0 = (double)this.field_174879_c.func_177958_n() + 0.5D - p_145835_1_; -@@ -279,4 +293,176 @@ +@@ -279,4 +293,185 @@ func_145826_a(TileEntityFlowerPot.class, "FlowerPot"); func_145826_a(TileEntityBanner.class, "Banner"); } @@ -212,6 +212,15 @@ + // NOOP + } + ++ /** ++ * If the TileEntitySpecialRenderer associated with this TileEntity can be batched in with another renderers, and won't access the GL state. ++ * If TileEntity returns true, then TESR should have the same functionality as (and probably extend) the FastTESR class. ++ */ ++ public boolean hasFastRenderer() ++ { ++ return false; ++ } ++ + private net.minecraftforge.common.capabilities.CapabilityDispatcher capabilities; + public TileEntity() + { diff --git a/src/main/java/net/minecraftforge/client/ForgeHooksClient.java b/src/main/java/net/minecraftforge/client/ForgeHooksClient.java index 643913bbe..5bc322c17 100644 --- a/src/main/java/net/minecraftforge/client/ForgeHooksClient.java +++ b/src/main/java/net/minecraftforge/client/ForgeHooksClient.java @@ -412,28 +412,30 @@ public class ForgeHooksClient public static void preDraw(EnumUsage attrType, VertexFormat format, int element, int stride, ByteBuffer buffer) { VertexFormatElement attr = format.getElement(element); + int count = attr.getElementCount(); + int constant = attr.getType().getGlConstant(); buffer.position(format.func_181720_d(element)); switch(attrType) { case POSITION: - glVertexPointer(attr.getElementCount(), attr.getType().getGlConstant(), stride, buffer); + glVertexPointer(count, constant, stride, buffer); glEnableClientState(GL_VERTEX_ARRAY); break; case NORMAL: - if(attr.getElementCount() != 3) + if(count != 3) { throw new IllegalArgumentException("Normal attribute should have the size 3: " + attr); } - glNormalPointer(attr.getType().getGlConstant(), stride, buffer); + glNormalPointer(constant, stride, buffer); glEnableClientState(GL_NORMAL_ARRAY); break; case COLOR: - glColorPointer(attr.getElementCount(), attr.getType().getGlConstant(), stride, buffer); + glColorPointer(count, constant, stride, buffer); glEnableClientState(GL_COLOR_ARRAY); break; case UV: OpenGlHelper.setClientActiveTexture(OpenGlHelper.defaultTexUnit + attr.getIndex()); - glTexCoordPointer(attr.getElementCount(), attr.getType().getGlConstant(), stride, buffer); + glTexCoordPointer(count, constant, stride, buffer); glEnableClientState(GL_TEXTURE_COORD_ARRAY); OpenGlHelper.setClientActiveTexture(OpenGlHelper.defaultTexUnit); break; @@ -441,7 +443,7 @@ public class ForgeHooksClient break; case GENERIC: glEnableVertexAttribArray(attr.getIndex()); - glVertexAttribPointer(attr.getIndex(), attr.getElementCount(), attr.getType().getGlConstant(), false, stride, buffer); + glVertexAttribPointer(attr.getIndex(), count, constant, false, stride, buffer); default: FMLLog.severe("Unimplemented vanilla attribute upload: %s", attrType.getDisplayName()); } diff --git a/src/main/java/net/minecraftforge/client/MinecraftForgeClient.java b/src/main/java/net/minecraftforge/client/MinecraftForgeClient.java index a363d8ded..53801cb18 100644 --- a/src/main/java/net/minecraftforge/client/MinecraftForgeClient.java +++ b/src/main/java/net/minecraftforge/client/MinecraftForgeClient.java @@ -6,7 +6,18 @@ package net.minecraftforge.client; import java.util.BitSet; +import java.util.concurrent.TimeUnit; + +import net.minecraft.client.renderer.RegionRenderCache; +import net.minecraft.util.BlockPos; import net.minecraft.util.EnumWorldBlockLayer; +import net.minecraft.world.World; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; public class MinecraftForgeClient { @@ -56,4 +67,29 @@ public class MinecraftForgeClient stencilBits.set(bit); } } + + private static final LoadingCache, RegionRenderCache> regionCache = CacheBuilder.newBuilder() + .maximumSize(500) + .concurrencyLevel(5) + .expireAfterAccess(1, TimeUnit.SECONDS) + .build(new CacheLoader, RegionRenderCache>() + { + public RegionRenderCache load(Pair key) throws Exception + { + return new RegionRenderCache(key.getLeft(), key.getRight().add(-1, -1, -1), key.getRight().add(16, 16, 16), 1); + } + }); + + public static void onRebuildChunk(World world, BlockPos position, RegionRenderCache cache) + { + regionCache.put(Pair.of(world, position), cache); + } + + public static RegionRenderCache getRegionRenderCache(World world, BlockPos pos) + { + int x = pos.getX() & ~0xF; + int y = pos.getY() & ~0xF; + int z = pos.getZ() & ~0xF; + return regionCache.getUnchecked(Pair.of(world, new BlockPos(x, y, z))); + } } diff --git a/src/main/java/net/minecraftforge/client/model/ModelLoader.java b/src/main/java/net/minecraftforge/client/model/ModelLoader.java index bc88f7ba7..772a93fa9 100644 --- a/src/main/java/net/minecraftforge/client/model/ModelLoader.java +++ b/src/main/java/net/minecraftforge/client/model/ModelLoader.java @@ -23,6 +23,7 @@ import net.minecraft.client.renderer.ItemMeshDefinition; import net.minecraft.client.renderer.ItemModelMesher; import net.minecraft.client.renderer.block.model.BlockPart; import net.minecraft.client.renderer.block.model.BlockPartFace; +import net.minecraft.client.renderer.block.model.BlockPartRotation; import net.minecraft.client.renderer.block.model.ItemCameraTransforms; import net.minecraft.client.renderer.block.model.ItemCameraTransforms.TransformType; import net.minecraft.client.renderer.block.model.ItemModelGenerator; @@ -51,7 +52,13 @@ import net.minecraft.item.ItemStack; import net.minecraft.util.EnumFacing; import net.minecraft.util.IRegistry; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.model.animation.Animation; +import net.minecraftforge.client.model.animation.IAnimatedModel; +import net.minecraftforge.client.model.animation.IClip; +import net.minecraftforge.client.model.animation.ModelBlockAnimation; import net.minecraftforge.common.ForgeModContainer; +import net.minecraftforge.common.property.IExtendedBlockState; +import net.minecraftforge.common.property.Properties; import net.minecraftforge.fluids.Fluid; import net.minecraftforge.fluids.FluidContainerRegistry; import net.minecraftforge.fluids.FluidRegistry; @@ -129,14 +136,7 @@ public class ModelLoader extends ModelBakery } } }); - Function textureGetter = new Function() - { - public TextureAtlasSprite apply(ResourceLocation location) - { - return Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(location.toString()); - } - }; - IFlexibleBakedModel missingBaked = missingModel.bake(missingModel.getDefaultState(), DefaultVertexFormats.ITEM, textureGetter); + IFlexibleBakedModel missingBaked = missingModel.bake(missingModel.getDefaultState(), DefaultVertexFormats.ITEM, DefaultTextureGetter.instance); for (Entry e : stateModels.entrySet()) { if(e.getValue() == getMissingModel()) @@ -145,7 +145,7 @@ public class ModelLoader extends ModelBakery } else { - bakedRegistry.putObject(e.getKey(), e.getValue().bake(e.getValue().getDefaultState(), DefaultVertexFormats.ITEM, textureGetter)); + bakedRegistry.putObject(e.getKey(), e.getValue().bake(e.getValue().getDefaultState(), DefaultVertexFormats.ITEM, DefaultTextureGetter.instance)); } } return bakedRegistry; @@ -407,15 +407,17 @@ public class ModelLoader extends ModelBakery textures.addAll(model.getTextures()); } - private class VanillaModelWrapper implements IRetexturableModel + private class VanillaModelWrapper implements IRetexturableModel, IAnimatedModel { private final ResourceLocation location; private final ModelBlock model; + private final ModelBlockAnimation animation; - public VanillaModelWrapper(ResourceLocation location, ModelBlock model) + public VanillaModelWrapper(ResourceLocation location, ModelBlock model, ModelBlockAnimation animation) { this.location = location; this.model = model; + this.animation = animation; } public Collection getDependencies() @@ -508,7 +510,8 @@ public class ModelLoader extends ModelBakery List newTransforms = Lists.newArrayList(); for(int i = 0; i < model.getElements().size(); i++) { - newTransforms.add(null); + BlockPart part = model.getElements().get(i); + newTransforms.add(animation.getPartTransform(state, part, i)); } ItemCameraTransforms transforms = model.func_181682_g(); @@ -531,7 +534,7 @@ public class ModelLoader extends ModelBakery return bakeNormal(model, perState, state.apply(Optional.absent()).or(TRSRTransformation.identity()), newTransforms, format, bakedTextureGetter, uvlock); } - private IFlexibleBakedModel bakeNormal(ModelBlock model, IModelState perState, final TRSRTransformation modelState, List newTransforms, VertexFormat format, Function bakedTextureGetter, boolean uvLocked) + private IFlexibleBakedModel bakeNormal(ModelBlock model, IModelState perState, final TRSRTransformation modelState, List newTransforms, VertexFormat format, final Function bakedTextureGetter, boolean uvLocked) { TextureAtlasSprite particle = bakedTextureGetter.apply(new ResourceLocation(model.resolveTextureName("particle"))); SimpleBakedModel.Builder builder = (new SimpleBakedModel.Builder(model)).setTexture(particle); @@ -542,6 +545,9 @@ public class ModelLoader extends ModelBakery if(newTransforms.get(i) != null) { transformation = transformation.compose(newTransforms.get(i)); + BlockPartRotation rot = part.partRotation; + if(rot == null) rot = new BlockPartRotation(new org.lwjgl.util.vector.Vector3f(), EnumFacing.Axis.Y, 0, false); + part = new BlockPart(part.positionFrom, part.positionTo, part.mapFaces, rot, part.shade); } for(Map.Entry e : (Iterable>)part.mapFaces.entrySet()) { @@ -562,13 +568,25 @@ public class ModelLoader extends ModelBakery { public IBakedModel handleBlockState(IBlockState state) { - return VanillaModelWrapper.this.handleBlockState(parent, modelState, state); + return VanillaModelWrapper.this.handleBlockState(parent, bakedTextureGetter, modelState, state); } }; } - private IBakedModel handleBlockState(IFlexibleBakedModel model, TRSRTransformation modelState, IBlockState state) + private IBakedModel handleBlockState(IFlexibleBakedModel model, Function bakedTextureGetter, TRSRTransformation modelState, IBlockState state) { + if(state instanceof IExtendedBlockState) + { + IExtendedBlockState exState = (IExtendedBlockState)state; + if(exState.getUnlistedNames().contains(Properties.AnimationProperty)) + { + IModelState newState = exState.getValue(Properties.AnimationProperty); + if(newState != null) + { + return VanillaModelWrapper.this.bake(new ModelStateComposition(modelState, newState), model.getFormat(), bakedTextureGetter); + } + } + } return model; } @@ -630,7 +648,17 @@ public class ModelLoader extends ModelBakery } } - return new VanillaModelWrapper(location, neweModel); + return new VanillaModelWrapper(location, neweModel, animation); + } + + @Override + public Optional getClip(String name) + { + if(animation.getClips().containsKey(name)) + { + return Optional.fromNullable(animation.getClips().get(name)); + } + return Optional.absent(); } public IModelState getDefaultState() @@ -829,7 +857,14 @@ public class ModelLoader extends ModelBakery public IModel loadModel(ResourceLocation modelLocation) throws IOException { - return loader.new VanillaModelWrapper(modelLocation, loader.loadModel(modelLocation)); + String modelPath = modelLocation.getResourcePath(); + if(modelLocation.getResourcePath().startsWith("models/")) + { + modelPath = modelPath.substring("models/".length()); + } + ResourceLocation armatureLocation = new ResourceLocation(modelLocation.getResourceDomain(), "armatures/" + modelPath + ".json"); + ModelBlockAnimation animation = Animation.INSTANCE.loadVanillaAnimation(armatureLocation); + return loader.new VanillaModelWrapper(modelLocation, loader.loadModel(modelLocation), animation); } } @@ -997,4 +1032,19 @@ public class ModelLoader extends ModelBakery mesher.register(e.getKey().getLeft().get(), e.getKey().getRight(), e.getValue()); } } + + private static enum DefaultTextureGetter implements Function + { + instance; + + public TextureAtlasSprite apply(ResourceLocation location) + { + return Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(location.toString()); + } + } + + public static Function defaultTextureGetter() + { + return DefaultTextureGetter.instance; + } } diff --git a/src/main/java/net/minecraftforge/client/model/animation/Animation.java b/src/main/java/net/minecraftforge/client/model/animation/Animation.java new file mode 100644 index 000000000..27c6f187a --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/Animation.java @@ -0,0 +1,184 @@ +package net.minecraftforge.client.model.animation; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +import net.minecraft.client.resources.IResource; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.IResourceManagerReloadListener; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import net.minecraftforge.common.model.animation.IAnimationStateMachine; +import net.minecraftforge.common.util.JsonUtils; +import net.minecraftforge.fml.common.FMLLog; + +import org.apache.logging.log4j.Level; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; + +public enum Animation implements IResourceManagerReloadListener +{ + INSTANCE; + + /** + * Get the global world time for the current tick, in seconds. + */ + public static float getWorldTime(World world) + { + return getWorldTime(world, 0); + } + + /** + * Get the global world time for the current tick + partial tick progress, in seconds. + */ + public static float getWorldTime(World world, float tickProgress) + { + return (world.getTotalWorldTime() + tickProgress) / 20; + } + + /** + * Load a new instance if AnimationStateMachine at specified location, with specified custom parameters. + */ + public IAnimationStateMachine load(ResourceLocation location, ImmutableMap customParameters) + { + try + { + ClipResolver clipResolver = new ClipResolver(); + ParameterResolver parameterResolver = new ParameterResolver(customParameters); + Clips.CommonClipTypeAdapterFactory.INSTANCE.setClipResolver(clipResolver); + TimeValues.CommonTimeValueTypeAdapterFactory.INSTANCE.setValueResolver(parameterResolver); + IResource resource = manager.getResource(location); + AnimationStateMachine asm = asmGson.fromJson(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8), AnimationStateMachine.class); + clipResolver.asm = asm; + parameterResolver.asm = asm; + asm.initialize(); + //String json = asmGson.toJson(asm); + //System.out.println(location + ": " + json); + return asm; + } + catch(IOException e) + { + FMLLog.log(Level.ERROR, e, "Exception loading Animation State Machine %s, skipping", location); + return missing; + } + catch(JsonParseException e) + { + FMLLog.log(Level.ERROR, e, "Exception loading Animation State Machine %s, skipping", location); + return missing; + } + finally + { + Clips.CommonClipTypeAdapterFactory.INSTANCE.setClipResolver(null); + TimeValues.CommonTimeValueTypeAdapterFactory.INSTANCE.setValueResolver(null); + } + } + + /** + * Load armature associated with a vanilla model. + */ + public ModelBlockAnimation loadVanillaAnimation(ResourceLocation armatureLocation) + { + try + { + IResource resource = null; + try + { + resource = manager.getResource(armatureLocation); + } + catch(FileNotFoundException e) + { + // this is normal. FIXME: error reporting? + return defaultModelBlockAnimation; + } + ModelBlockAnimation mba = mbaGson.fromJson(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8), ModelBlockAnimation.class); + String json = mbaGson.toJson(mba); + return mba; + } + catch(IOException e) + { + FMLLog.log(Level.ERROR, e, "Exception loading vanilla model aniamtion %s, skipping", armatureLocation); + return defaultModelBlockAnimation; + } + catch(JsonParseException e) + { + FMLLog.log(Level.ERROR, e, "Exception loading vanilla model aniamtion %s, skipping", armatureLocation); + return defaultModelBlockAnimation; + } + } + + private IResourceManager manager; + + private final AnimationStateMachine missing = new AnimationStateMachine( + ImmutableMap.of(), + ImmutableMap.of("missingno", (IClip)Clips.IdentityClip.instance), + ImmutableList.of("missingno"), + ImmutableMap.of(), + "missingno"); + + { + missing.initialize(); + } + + private final Gson asmGson = new GsonBuilder() + .registerTypeAdapter(ImmutableList.class, JsonUtils.ImmutableListTypeAdapter.INSTANCE) + .registerTypeAdapter(ImmutableMap.class, JsonUtils.ImmutableMapTypeAdapter.INSTANCE) + .registerTypeAdapterFactory(Clips.CommonClipTypeAdapterFactory.INSTANCE) + //.registerTypeAdapterFactory(ClipProviders.CommonClipProviderTypeAdapterFactory.INSTANCE) + .registerTypeAdapterFactory(TimeValues.CommonTimeValueTypeAdapterFactory.INSTANCE) + .setPrettyPrinting() + .enableComplexMapKeySerialization() + .disableHtmlEscaping() + .create(); + + private static final class ClipResolver implements Function + { + private AnimationStateMachine asm; + + public IClip apply(String name) + { + return asm.getClips().get(name); + } + } + + private static final class ParameterResolver implements Function + { + private final ImmutableMap customParameters; + private AnimationStateMachine asm; + + public ParameterResolver(ImmutableMap customParameters) + { + this.customParameters = customParameters; + } + + public ITimeValue apply(String name) + { + if(asm.getParameters().containsKey(name)) + { + return asm.getParameters().get(name); + } + return customParameters.get(name); + } + } + + private final Gson mbaGson = new GsonBuilder() + .registerTypeAdapter(ImmutableList.class, JsonUtils.ImmutableListTypeAdapter.INSTANCE) + .registerTypeAdapter(ImmutableMap.class, JsonUtils.ImmutableMapTypeAdapter.INSTANCE) + .setPrettyPrinting() + .enableComplexMapKeySerialization() + .disableHtmlEscaping() + .create(); + + private final ModelBlockAnimation defaultModelBlockAnimation = new ModelBlockAnimation(ImmutableMap.>of(), ImmutableMap.of()); + + public void onResourceManagerReload(IResourceManager manager) + { + this.manager = manager; + } +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/AnimationModelBase.java b/src/main/java/net/minecraftforge/client/model/animation/AnimationModelBase.java new file mode 100644 index 000000000..a688aa0fd --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/AnimationModelBase.java @@ -0,0 +1,108 @@ +package net.minecraftforge.client.model.animation; + +import java.util.List; + +import net.minecraft.client.model.ModelBase; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.resources.model.IBakedModel; +import net.minecraft.entity.Entity; +import net.minecraft.init.Blocks; +import net.minecraft.util.BlockPos; +import net.minecraft.util.EnumFacing; +import net.minecraftforge.client.model.IModel; +import net.minecraftforge.client.model.IModelState; +import net.minecraftforge.client.model.ModelLoader; +import net.minecraftforge.client.model.pipeline.VertexLighterFlat; +import net.minecraftforge.client.model.pipeline.WorldRendererConsumer; + +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.opengl.GL11; + +/** + * ModelBase that works with the Forge model system and animations. + * Some quirks are still left, deprecated for the moment. + */ +@Deprecated +public class AnimationModelBase extends ModelBase implements IEventHandler +{ + private final VertexLighterFlat lighter; + private final IModel model; + + public AnimationModelBase(IModel model, VertexLighterFlat lighter) + { + this.model = model; + this.lighter = lighter; + } + + @SuppressWarnings("unchecked") + @Override + public void render(Entity entity, float limbSwing, float limbSwingSpeed, float timeAlive, float yawHead, float rotationPitch, float scale) + { + if(!(entity instanceof IAnimationProvider)) + { + throw new ClassCastException("AnimationModelBase expects IAnimationProvider"); + } + + Pair> pair = ((IAnimationProvider)entity).asm().apply(timeAlive / 20); + handleEvents((T)entity, timeAlive / 20, pair.getRight()); + IBakedModel bakedModel = model.bake(pair.getLeft(), DefaultVertexFormats.ITEM, ModelLoader.defaultTextureGetter()); + + BlockPos pos = new BlockPos(entity.posX, entity.posY + entity.height, entity.posZ); + + RenderHelper.disableStandardItemLighting(); + GlStateManager.pushMatrix(); + GlStateManager.rotate(180, 0, 0, 1); + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldRenderer = tessellator.getWorldRenderer(); + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.BLOCK); + worldRenderer.setTranslation(-0.5, -1.5, -0.5); + + lighter.setParent(new WorldRendererConsumer(worldRenderer)); + lighter.setWorld(entity.worldObj); + lighter.setBlock(Blocks.air); + lighter.setBlockPos(pos); + boolean empty = true; + List quads = bakedModel.getGeneralQuads(); + if(!quads.isEmpty()) + { + lighter.updateBlockInfo(); + empty = false; + for(BakedQuad quad : quads) + { + quad.pipe(lighter); + } + } + for(EnumFacing side : EnumFacing.values()) + { + quads = bakedModel.getFaceQuads(side); + if(!quads.isEmpty()) + { + if(empty) lighter.updateBlockInfo(); + empty = false; + for(BakedQuad quad : quads) + { + quad.pipe(lighter); + } + } + } + + // debug quad + /*worldRenderer.pos(0, 1, 0).color(0xFF, 0xFF, 0xFF, 0xFF).tex(0, 0).lightmap(240, 0).endVertex(); + worldRenderer.pos(0, 1, 1).color(0xFF, 0xFF, 0xFF, 0xFF).tex(0, 1).lightmap(240, 0).endVertex(); + worldRenderer.pos(1, 1, 1).color(0xFF, 0xFF, 0xFF, 0xFF).tex(1, 1).lightmap(240, 0).endVertex(); + worldRenderer.pos(1, 1, 0).color(0xFF, 0xFF, 0xFF, 0xFF).tex(1, 0).lightmap(240, 0).endVertex();*/ + + worldRenderer.setTranslation(0, 0, 0); + + tessellator.draw(); + GlStateManager.popMatrix(); + RenderHelper.enableStandardItemLighting(); + } + + public void handleEvents(T instance, float time, Iterable pastEvents) {} +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/AnimationStateMachine.java b/src/main/java/net/minecraftforge/client/model/animation/AnimationStateMachine.java new file mode 100644 index 000000000..d2a7c8f37 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/AnimationStateMachine.java @@ -0,0 +1,169 @@ +package net.minecraftforge.client.model.animation; + +import java.util.concurrent.TimeUnit; + +import net.minecraftforge.client.model.IModelState; +import net.minecraftforge.common.model.animation.IAnimationStateMachine; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; + +import com.google.common.base.Predicate; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; + +class AnimationStateMachine implements IAnimationStateMachine +{ + private final ImmutableMap parameters; + private final ImmutableMap clips; + private final ImmutableList states; + private final ImmutableMap transitions; + @SerializedName("start_state") + private final String startState; + + private transient boolean shouldHandleSpecialEvents; + private transient String currentStateName; + private transient IClip currentState; + private transient float lastPollTime; + + private static final LoadingCache, Pair>> clipCache = CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterWrite(100, TimeUnit.MILLISECONDS) + .build(new CacheLoader, Pair>>() + { + public Pair> load(Triple key) throws Exception + { + return Clips.apply(key.getLeft(), key.getMiddle(), key.getRight()); + } + }); + + public AnimationStateMachine(ImmutableMap parameters, ImmutableMap clips, ImmutableList states, ImmutableMap transitions, String startState) + { + this.parameters = parameters; + this.clips = clips; + this.states = states; + this.transitions = transitions; + this.startState = startState; + } + + /** + * Used during resolution of parameter references. + */ + ImmutableMap getParameters() + { + return parameters; + } + + /** + * Used during resolution of clip references. + */ + ImmutableMap getClips() + { + return clips; + } + + /** + * post-loading initialization hook. + */ + void initialize() + { + if(parameters == null) + { + throw new JsonParseException("Animation State Machine should contain \"parameters\" key."); + } + if(clips == null) + { + throw new JsonParseException("Animation State Machine should contain \"clips\" key."); + } + if(states == null) + { + throw new JsonParseException("Animation State Machine should contain \"states\" key."); + } + if(transitions == null) + { + throw new JsonParseException("Animation State Machine should contain \"transitions\" key."); + } + shouldHandleSpecialEvents = true; + lastPollTime = Float.NEGATIVE_INFINITY; + // setting the starting state + IClip state = clips.get(startState); + if(!clips.containsKey(startState) || !states.contains(startState)) + { + throw new IllegalStateException("unknown state: " + startState); + } + currentStateName = startState; + currentState = state; + } + + public Pair> apply(float time) + { + if(lastPollTime == Float.NEGATIVE_INFINITY) + { + lastPollTime = time; + } + Pair> pair = clipCache.getUnchecked(Triple.of(currentState, lastPollTime, time)); + lastPollTime = time; + boolean shouldFilter = false; + if(shouldHandleSpecialEvents) + { + for(Event event : ImmutableList.copyOf(pair.getRight()).reverse()) + { + if(event.event().startsWith("!")) + { + shouldFilter = true; + if(event.event().startsWith("!transition:")) + { + String newState = event.event().substring("!transition:".length()); + transition(newState); + } + else + { + System.out.println("Unknown special event \"" + event.event() + "\", ignoring"); + } + } + } + } + if(!shouldFilter) + { + return pair; + } + return Pair.of(pair.getLeft(), Iterables.filter(pair.getRight(), new Predicate() + { + public boolean apply(Event event) + { + return !event.event().startsWith("!"); + } + })); + } + + public void transition(String newState) + { + IClip nc = clips.get(newState); + if(!clips.containsKey(newState) || !states.contains(newState)) + { + throw new IllegalStateException("unknown state: " + newState); + } + if(!transitions.get(currentStateName).equals(newState)) + { + throw new IllegalArgumentException("no transition from current clip \"" + currentStateName + "\" to the clip \"" + newState + "\" found."); + } + currentStateName = newState; + currentState = nc; + } + + public String currentState() + { + return currentStateName; + } + + public void shouldHandleSpecialEvents(boolean value) + { + shouldHandleSpecialEvents = true; + } +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/AnimationTESR.java b/src/main/java/net/minecraftforge/client/model/animation/AnimationTESR.java new file mode 100644 index 000000000..56a6d8964 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/AnimationTESR.java @@ -0,0 +1,79 @@ +package net.minecraftforge.client.model.animation; + +import java.util.concurrent.TimeUnit; + +import net.minecraft.block.state.IBlockState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.BlockRendererDispatcher; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.resources.model.IBakedModel; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.BlockPos; +import net.minecraft.world.IBlockAccess; +import net.minecraftforge.client.MinecraftForgeClient; +import net.minecraftforge.client.model.IModelState; +import net.minecraftforge.client.model.ISmartBlockModel; +import net.minecraftforge.common.property.IExtendedBlockState; +import net.minecraftforge.common.property.Properties; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +/** + * Generic TileEntitySpecialRenderer that works with the Forge model system and animations. + */ +public class AnimationTESR extends FastTESR implements IEventHandler +{ + protected static BlockRendererDispatcher blockRenderer; + + protected static final LoadingCache, IBakedModel> modelCache = CacheBuilder.newBuilder().maximumSize(10).expireAfterWrite(100, TimeUnit.MILLISECONDS).build(new CacheLoader, IBakedModel>() + { + public IBakedModel load(Pair key) throws Exception + { + IBakedModel model = blockRenderer.getBlockModelShapes().getModelForState(key.getLeft().getClean()); + if(model instanceof ISmartBlockModel) + { + model = ((ISmartBlockModel)model).handleBlockState(key.getLeft().withProperty(Properties.AnimationProperty, key.getRight())); + } + return model; + } + }); + + protected static IBakedModel getModel(IExtendedBlockState state, IModelState modelState) + { + return modelCache.getUnchecked(Pair.of(state, modelState)); + } + + public void renderTileEntityFast(T te, double x, double y, double z, float partialTick, int breakStage, WorldRenderer renderer) + { + if(blockRenderer == null) blockRenderer = Minecraft.getMinecraft().getBlockRendererDispatcher(); + BlockPos pos = te.getPos(); + IBlockAccess world = MinecraftForgeClient.getRegionRenderCache(te.getWorld(), pos); + IBlockState state = world.getBlockState(pos); + if(state.getPropertyNames().contains(Properties.StaticProperty)) + { + state = state.withProperty(Properties.StaticProperty, false); + } + if(state instanceof IExtendedBlockState) + { + IExtendedBlockState exState = (IExtendedBlockState)state; + if(exState.getUnlistedNames().contains(Properties.AnimationProperty)) + { + float time = Animation.getWorldTime(getWorld(), partialTick); + Pair> pair = te.asm().apply(time); + handleEvents(te, time, pair.getRight()); + + IBakedModel model = getModel(exState, pair.getLeft()); + + renderer.setTranslation(x - pos.getX(), y - pos.getY(), z - pos.getZ()); + + blockRenderer.getBlockModelRenderer().renderModel(world, model, state, pos, renderer, false); + } + } + } + + public void handleEvents(T te, float time, Iterable pastEvents) {} +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/client/model/animation/Clips.java b/src/main/java/net/minecraftforge/client/model/animation/Clips.java new file mode 100644 index 000000000..c673ff60b --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/Clips.java @@ -0,0 +1,538 @@ +package net.minecraftforge.client.model.animation; + +import java.io.IOException; + +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.util.IStringSerializable; +import net.minecraft.util.MathHelper; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.model.IModel; +import net.minecraftforge.client.model.IModelPart; +import net.minecraftforge.client.model.IModelState; +import net.minecraftforge.client.model.ModelLoaderRegistry; +import net.minecraftforge.client.model.TRSRTransformation; +import net.minecraftforge.fml.common.FMLLog; + +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * Various implementations of IClip, and utility methods. + */ +public final class Clips +{ + /** + * Clip that does nothing. + */ + public static enum IdentityClip implements IClip, IStringSerializable + { + instance; + + public IJointClip apply(IJoint joint) + { + return JointClips.IdentityJointClip.instance; + } + + public Iterable pastEvents(float lastPollTime, float time) + { + return ImmutableSet.of(); + } + + public String getName() + { + return "identity"; + } + } + + /** + * Retrieves the clip from the model. + */ + public static IClip getModelClipNode(ResourceLocation modelLocation, String clipName) + { + IModel model; + try + { + model = ModelLoaderRegistry.getModel(modelLocation); + if(model instanceof IAnimatedModel) + { + Optional clip = ((IAnimatedModel)model).getClip(clipName); + if(clip.isPresent()) + { + return new ModelClip(clip.get(), modelLocation, clipName); + } + FMLLog.getLogger().error("Unable to find clip " + clipName + " in the model " + modelLocation); + } + } + catch (IOException e) + { + FMLLog.getLogger().error("Unable to load model" + modelLocation + " while loading clip " + clipName, e); + } + // FIXME: missing clip? + return new ModelClip(IdentityClip.instance, modelLocation, clipName); + } + + /** + * Wrapper for model clips; useful for debugging and serialization; + */ + public static final class ModelClip implements IClip + { + private final IClip childClip; + private final ResourceLocation modelLocation; + private final String clipName; + + public ModelClip(IClip childClip, ResourceLocation modelLocation, String clipName) + { + this.childClip = childClip; + this.modelLocation = modelLocation; + this.clipName = clipName; + } + + public IJointClip apply(IJoint joint) + { + return childClip.apply(joint); + } + + public Iterable pastEvents(float lastPollTime, float time) + { + return childClip.pastEvents(lastPollTime, time); + } + + @Override + public int hashCode() + { + return Objects.hashCode(modelLocation, clipName); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ModelClip other = (ModelClip) obj; + return Objects.equal(modelLocation, other.modelLocation) && Objects.equal(clipName, other.clipName); + } + } + + /** + * Clip with custom parameterization of the time. + */ + public static final class TimeClip implements IClip + { + private final IClip childClip; + private final ITimeValue time; + + public TimeClip(IClip childClip, ITimeValue time) + { + this.childClip = childClip; + this.time = time; + } + + public IJointClip apply(final IJoint joint) + { + return new IJointClip() + { + private final IJointClip parent = childClip.apply(joint); + public TRSRTransformation apply(float time) + { + return parent.apply(TimeClip.this.time.apply(time)); + } + }; + } + + public Iterable pastEvents(float lastPollTime, float time) + { + return childClip.pastEvents(this.time.apply(lastPollTime), this.time.apply(time)); + } + + @Override + public int hashCode() + { + return Objects.hashCode(childClip, time); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TimeClip other = (TimeClip) obj; + return Objects.equal(childClip, other.childClip) && Objects.equal(time, other.time); + } + } + + /** + * Spherical linear blend between 2 clips. + */ + public static final class SlerpClip implements IClip + { + private final IClip from; + private final IClip to; + private final ITimeValue input; + private final ITimeValue progress; + + public SlerpClip(IClip from, IClip to, ITimeValue input, ITimeValue progress) + { + this.from = from; + this.to = to; + this.input = input; + this.progress = progress; + } + + public IJointClip apply(IJoint joint) + { + IJointClip fromClip = from.apply(joint); + IJointClip toClip = to.apply(joint); + return blendClips(joint, fromClip, toClip, input, progress); + } + + public Iterable pastEvents(float lastPollTime, float time) + { + float clipLastPollTime = input.apply(lastPollTime); + float clipTime = input.apply(time); + return Iterables.mergeSorted(ImmutableSet.of( + from.pastEvents(clipLastPollTime, clipTime), + to.pastEvents(clipLastPollTime, clipTime) + ), Ordering.natural()); + } + + @Override + public int hashCode() + { + return Objects.hashCode(from, to, input, progress); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SlerpClip other = (SlerpClip) obj; + return Objects.equal(from, other.from) && + Objects.equal(to, other.to) && + Objects.equal(input, other.input) && + Objects.equal(progress, other.progress); + } + } + + /*public static class AdditiveLerpClip implements IClip + { + private final IClip base; + private final IClip add; + private final IParameter progress; + + public AdditiveLerpClip(IClip base, IClip add, IParameter progress) + { + this.base = base; + this.add = add; + this.progress = progress; + } + + public IJointClip apply(IJoint joint) + { + throw new NotImplementedException("AdditiveLerpClip.apply"); + } + }*/ + + private static IJointClip blendClips(final IJoint joint, final IJointClip fromClip, final IJointClip toClip, final ITimeValue input, final ITimeValue progress) + { + return new IJointClip() + { + public TRSRTransformation apply(float time) + { + float clipTime = input.apply(time); + return fromClip.apply(clipTime).slerp(toClip.apply(clipTime), MathHelper.clamp_float(progress.apply(time), 0, 1)); + } + }; + } + + /** + * IModelState wrapper for a Clip, sampled at specified time. + */ + public static Pair> apply(final IClip clip, final float lastPollTime, final float time) + { + return Pair.>of(new IModelState() + { + public Optional apply(Optional part) + { + if(!part.isPresent() || !(part.get() instanceof IJoint)) + { + return Optional.absent(); + } + IJoint joint = (IJoint)part.get(); + if(!joint.getParent().isPresent()) + { + return Optional.of(clip.apply(joint).apply(time).compose(joint.getInvBindPose())); + } + return Optional.of(clip.apply(joint.getParent().get()).apply(time).compose(clip.apply(joint).apply(time)).compose(joint.getInvBindPose())); + } + }, clip.pastEvents(lastPollTime, time)); + } + + /** + * Clip + Event, triggers when parameter becomes non-negative. + */ + public static final class TriggerClip implements IClip + { + private final IClip clip; + private final ITimeValue parameter; + private final String event; + + public TriggerClip(IClip clip, ITimeValue parameter, String event) + { + this.clip = clip; + this.parameter = parameter; + this.event = event; + } + + public IJointClip apply(IJoint joint) + { + return clip.apply(joint); + } + + public Iterable pastEvents(float lastPollTime, float time) + { + if(parameter.apply(lastPollTime) < 0 && parameter.apply(time) >= 0) + { + return Iterables.mergeSorted(ImmutableSet.of( + clip.pastEvents(lastPollTime, time), + ImmutableSet.of(new Event(event, 0)) + ), Ordering.natural()); + } + return clip.pastEvents(lastPollTime, time); + } + } + + /** + * Reference to another clip. + * Should only exist during debugging. + */ + public static final class ClipReference implements IClip, IStringSerializable + { + private final String clipName; + private final Function clipResolver; + private IClip clip; + + public ClipReference(String clipName, Function clipResolver) + { + this.clipName = clipName; + this.clipResolver = clipResolver; + } + + private void resolve() + { + if(clip == null) + { + if(clipResolver != null) + { + clip = clipResolver.apply(clipName); + } + if(clip == null) + { + throw new IllegalArgumentException("Couldn't resolve clip " + clipName); + } + } + } + + public IJointClip apply(final IJoint joint) + { + resolve(); + return clip.apply(joint); + } + + public Iterable pastEvents(float lastPollTime, float time) + { + resolve(); + return clip.pastEvents(lastPollTime, time); + } + + public String getName() + { + return clipName; + } + + @Override + public int hashCode() + { + resolve(); + return clip.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ClipReference other = (ClipReference) obj; + resolve(); + other.resolve(); + return Objects.equal(clip, other.clip); + } + } + + public static enum CommonClipTypeAdapterFactory implements TypeAdapterFactory + { + INSTANCE; + + private final ThreadLocal> clipResolver = new ThreadLocal>(); + + public void setClipResolver(Function clipResolver) + { + this.clipResolver.set(clipResolver); + } + + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken type) + { + if(type.getRawType() != IClip.class) + { + return null; + } + + final TypeAdapter parameterAdapter = gson.getAdapter(ITimeValue.class); + + return (TypeAdapter)new TypeAdapter() + { + public void write(JsonWriter out, IClip clip) throws IOException + { + // IdentityClip + ClipReference + if(clip instanceof IStringSerializable) + { + out.value("#" + ((IStringSerializable)clip).getName()); + return; + } + else if(clip instanceof TimeClip) + { + out.beginArray(); + out.value("apply"); + TimeClip timeClip = (TimeClip)clip; + write(out, timeClip.childClip); + parameterAdapter.write(out, timeClip.time); + out.endArray(); + return; + } + else if(clip instanceof SlerpClip) + { + out.beginArray(); + out.value("slerp"); + SlerpClip slerpClip = (SlerpClip)clip; + write(out, slerpClip.from); + write(out, slerpClip.to); + parameterAdapter.write(out, slerpClip.input); + parameterAdapter.write(out, slerpClip.progress); + out.endArray(); + return; + } + else if(clip instanceof TriggerClip) + { + out.beginArray(); + out.value("trigger_positive"); + TriggerClip triggerClip = (TriggerClip)clip; + write(out, triggerClip.clip); + parameterAdapter.write(out, triggerClip.parameter); + out.value(triggerClip.event); + out.endArray(); + return; + } + else if(clip instanceof ModelClip) + { + ModelClip modelClip = (ModelClip)clip; + out.value(modelClip.modelLocation + "@" + modelClip.clipName); + return; + } + // TODO custom clip writing? + throw new NotImplementedException("unknown Clip to json: " + clip); + } + + public IClip read(JsonReader in) throws IOException + { + switch(in.peek()) + { + case BEGIN_ARRAY: + in.beginArray(); + String type = in.nextString(); + IClip clip; + // TimeClip + if("apply".equals(type)) + { + clip = new TimeClip(read(in), parameterAdapter.read(in)); + } + else if("slerp".equals(type)) + { + clip = new SlerpClip(read(in), read(in), parameterAdapter.read(in), parameterAdapter.read(in)); + } + else if("trigger_positive".equals(type)) + { + clip = new TriggerClip(read(in), parameterAdapter.read(in), in.nextString()); + } + else + { + throw new IOException("Unknown Clip type \"" + type + "\""); + } + in.endArray(); + return clip; + case STRING: + String string = in.nextString(); + // IdentityClip + if(string.equals("#identity")) + { + return IdentityClip.instance; + } + // Clip reference + if(string.startsWith("#")) + { + return new ClipReference(string.substring(1), clipResolver.get()); + } + // ModelClip + else + { + int at = string.lastIndexOf('@'); + String location = string.substring(0, at); + String clipName = string.substring(at + 1, string.length()); + ResourceLocation model; + if(location.indexOf('#') != -1) + { + model = new ModelResourceLocation(location); + } + else + { + model = new ResourceLocation(location); + } + return getModelClipNode(model, clipName); + } + default: + throw new IOException("expected Clip, got " + in.peek()); + } + } + }; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/client/model/animation/Event.java b/src/main/java/net/minecraftforge/client/model/animation/Event.java new file mode 100644 index 000000000..96135d2d3 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/Event.java @@ -0,0 +1,46 @@ +package net.minecraftforge.client.model.animation; + +import com.google.common.base.Objects; + + +/** + * Event stored in the clip + */ +public final class Event implements Comparable +{ + private final String event; + private final float offset; + + public Event(String event, float offset) + { + this.event = event; + this.offset = offset; + } + + /** + * @return the name of the event. + */ + public String event() + { + return event; + } + + /** + * @return how long ago the event happened, relative to the next event / first query time + */ + public float offset() + { + return offset; + } + + public int compareTo(Event event) + { + return new Float(offset).compareTo(event.offset); + } + + @Override + public String toString() + { + return Objects.toStringHelper(getClass()).add("event", event).add("offset", offset).toString(); + } +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/FastTESR.java b/src/main/java/net/minecraftforge/client/model/animation/FastTESR.java new file mode 100644 index 000000000..ed3baad49 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/FastTESR.java @@ -0,0 +1,49 @@ +package net.minecraftforge.client.model.animation; + +import org.lwjgl.opengl.GL11; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.tileentity.TileEntity; + +public abstract class FastTESR extends TileEntitySpecialRenderer +{ + @Override + public final void renderTileEntityAt(T te, double x, double y, double z, float partialTicks, int destroyStage) + { + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldRenderer = tessellator.getWorldRenderer(); + this.bindTexture(TextureMap.locationBlocksTexture); + RenderHelper.disableStandardItemLighting(); + GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GlStateManager.enableBlend(); + GlStateManager.disableCull(); + + if (Minecraft.isAmbientOcclusionEnabled()) + { + GlStateManager.shadeModel(GL11.GL_SMOOTH); + } + else + { + GlStateManager.shadeModel(GL11.GL_FLAT); + } + + worldRenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.BLOCK); + + renderTileEntityFast(te, x, y, z, partialTicks, destroyStage, worldRenderer); + worldRenderer.setTranslation(0, 0, 0); + + tessellator.draw(); + + RenderHelper.enableStandardItemLighting(); + } + + @Override + public abstract void renderTileEntityFast(T te, double x, double y, double z, float partialTicks, int destroyStage, WorldRenderer worldRenderer); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/IAnimatedModel.java b/src/main/java/net/minecraftforge/client/model/animation/IAnimatedModel.java new file mode 100644 index 000000000..e4d6df8ee --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/IAnimatedModel.java @@ -0,0 +1,13 @@ +package net.minecraftforge.client.model.animation; + +import net.minecraftforge.client.model.IModel; + +import com.google.common.base.Optional; + +/** + * IModel that has animation data. + */ +public interface IAnimatedModel extends IModel +{ + Optional getClip(String name); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/IAnimationProvider.java b/src/main/java/net/minecraftforge/client/model/animation/IAnimationProvider.java new file mode 100644 index 000000000..94723d38f --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/IAnimationProvider.java @@ -0,0 +1,12 @@ +package net.minecraftforge.client.model.animation; + +import net.minecraftforge.common.model.animation.IAnimationStateMachine; + + +/** + * Something that can provide the Animation State Machine, for example and Entity or a Block + */ +public interface IAnimationProvider +{ + public IAnimationStateMachine asm(); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/IClip.java b/src/main/java/net/minecraftforge/client/model/animation/IClip.java new file mode 100644 index 000000000..714322a01 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/IClip.java @@ -0,0 +1,12 @@ +package net.minecraftforge.client.model.animation; + + +/** + * Clip for a rigged model. + */ +public interface IClip +{ + IJointClip apply(IJoint joint); + + Iterable pastEvents(float lastPollTime, float time); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/IEventHandler.java b/src/main/java/net/minecraftforge/client/model/animation/IEventHandler.java new file mode 100644 index 000000000..81c7f3892 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/IEventHandler.java @@ -0,0 +1,10 @@ +package net.minecraftforge.client.model.animation; + + +/** + * Handler for animation events; + */ +public interface IEventHandler +{ + void handleEvents(T instance, float time, Iterable pastEvents); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/IJoint.java b/src/main/java/net/minecraftforge/client/model/animation/IJoint.java new file mode 100644 index 000000000..288a0cc4e --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/IJoint.java @@ -0,0 +1,16 @@ +package net.minecraftforge.client.model.animation; + +import com.google.common.base.Optional; + +import net.minecraftforge.client.model.IModelPart; +import net.minecraftforge.client.model.TRSRTransformation; + +/** + * Model part that's a part of the hierarchical skeleton. + */ +public interface IJoint extends IModelPart +{ + TRSRTransformation getInvBindPose(); + + Optional getParent(); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/IJointClip.java b/src/main/java/net/minecraftforge/client/model/animation/IJointClip.java new file mode 100644 index 000000000..27edea0e5 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/IJointClip.java @@ -0,0 +1,11 @@ +package net.minecraftforge.client.model.animation; + +import net.minecraftforge.client.model.TRSRTransformation; + +/** + * Returns Local joint pose; animation clip for specific model part. + */ +public interface IJointClip +{ + TRSRTransformation apply(float time); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/ITimeValue.java b/src/main/java/net/minecraftforge/client/model/animation/ITimeValue.java new file mode 100644 index 000000000..88770b125 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/ITimeValue.java @@ -0,0 +1,12 @@ +package net.minecraftforge.client.model.animation; + +/** + * Time-varying value associated with the animation. + * Return value should be constant with the respect to the input and reasonable context (current render frame). + * Simplest example is the input time itself. + * Unity calls them Parameters, Unreal calls them Variables. + */ +public interface ITimeValue +{ + public float apply(float input); +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/JointClips.java b/src/main/java/net/minecraftforge/client/model/animation/JointClips.java new file mode 100644 index 000000000..945c54f46 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/JointClips.java @@ -0,0 +1,36 @@ +package net.minecraftforge.client.model.animation; + +import net.minecraftforge.client.model.TRSRTransformation; + +/** + * Various implementations of IJointClip. + */ +public final class JointClips +{ + public static enum IdentityJointClip implements IJointClip + { + instance; + + public TRSRTransformation apply(float time) + { + return TRSRTransformation.identity(); + } + } + + public static class NodeJointClip implements IJointClip + { + private final IJoint child; + private final IClip clip; + + public NodeJointClip(IJoint joint, IClip clip) + { + this.child = joint; + this.clip = clip; + } + + public TRSRTransformation apply(float time) + { + return clip.apply(child).apply(time); + } + } +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/ModelBlockAnimation.java b/src/main/java/net/minecraftforge/client/model/animation/ModelBlockAnimation.java new file mode 100644 index 000000000..ca07406e7 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/ModelBlockAnimation.java @@ -0,0 +1,509 @@ +package net.minecraftforge.client.model.animation; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.TreeMap; + +import javax.vecmath.AxisAngle4f; +import javax.vecmath.Matrix4f; +import javax.vecmath.Quat4f; +import javax.vecmath.Vector3f; + +import net.minecraft.client.renderer.block.model.BlockPart; +import net.minecraft.util.MathHelper; +import net.minecraftforge.client.model.IModelState; +import net.minecraftforge.client.model.TRSRTransformation; +import net.minecraftforge.client.model.animation.ModelBlockAnimation.Parameter.Interpolation; +import net.minecraftforge.client.model.animation.ModelBlockAnimation.Parameter.Type; +import net.minecraftforge.client.model.animation.ModelBlockAnimation.Parameter.Variable; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.collect.UnmodifiableIterator; +import com.google.gson.annotations.SerializedName; + +public class ModelBlockAnimation +{ + private final ImmutableMap> joints; + private final ImmutableMap clips; + private transient ImmutableMultimap jointIndexMap; + + public ModelBlockAnimation(ImmutableMap> joints, ImmutableMap clips) + { + this.joints = joints; + this.clips = clips; + } + + public ImmutableMap getClips() + { + return clips; + } + + public ImmutableCollection getJoint(int i) + { + if(jointIndexMap == null) + { + ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); + for(Map.Entry> info : joints.entrySet()) + { + ImmutableMap.Builder weightBuilder = ImmutableMap.builder(); + for(Map.Entry e : info.getValue().entrySet()) + { + weightBuilder.put(Integer.parseInt(e.getKey()), e.getValue()); + } + ImmutableMap weightMap = weightBuilder.build(); + for(Map.Entry e : weightMap.entrySet()) + { + builder.put(e.getKey(), new MBJointWeight(info.getKey(), weightMap)); + } + } + jointIndexMap = builder.build(); + } + return jointIndexMap.get(i); + } + + protected static class MBVariableClip + { + private final Variable variable; + @SuppressWarnings("unused") + private final Type type; + private final Interpolation interpolation; + private final float[] samples; + + public MBVariableClip(Variable variable, Type type, Interpolation interpolation, float[] samples) + { + this.variable = variable; + this.type = type; + this.interpolation = interpolation; + this.samples = samples; + } + } + + protected static class MBClip implements IClip + { + private final boolean loop; + @SerializedName("joint_clips") + private final ImmutableMap> jointClipsFlat; + private transient ImmutableMap jointClips; + @SerializedName("events") + private final ImmutableMap eventsRaw; + private transient TreeMap events; + + public MBClip(boolean loop, ImmutableMap> clips, ImmutableMap events) + { + this.loop = loop; + this.jointClipsFlat = clips; + this.eventsRaw = events; + } + + private void initialize() + { + if(jointClips == null) + { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for(Map.Entry> e : jointClipsFlat.entrySet()) + { + builder.put(e.getKey(), new MBJointClip(loop, e.getValue())); + } + jointClips = builder.build(); + events = Maps.newTreeMap(); + TreeMap times = Maps.newTreeMap(); + for(String time : eventsRaw.keySet()) + { + times.put(Float.parseFloat(time), time); + } + float lastTime = Float.POSITIVE_INFINITY; + if(loop) + { + lastTime = times.firstKey(); + } + for(Map.Entry entry : times.descendingMap().entrySet()) + { + float time = entry.getKey(); + float offset = lastTime - time; + if(loop) + { + offset = 1 - (1 - offset) % 1; + } + events.put(time, new Event(eventsRaw.get(entry.getValue()), offset)); + } + } + } + + @Override + public IJointClip apply(IJoint joint) + { + initialize(); + if(joint instanceof MBJoint) + { + MBJoint mbJoint = (MBJoint)joint; + //MBJointInfo = jointInfos. + MBJointClip clip = jointClips.get(mbJoint.getName()); + if(clip != null) return clip; + } + return JointClips.IdentityJointClip.instance; + } + + @Override + public Iterable pastEvents(final float lastPollTime, final float time) + { + initialize(); + return new Iterable() + { + public Iterator iterator() + { + return new UnmodifiableIterator() + { + private Float curKey; + private Event firstEvent; + private float stopTime; + { + if(lastPollTime >= time) + { + curKey = null; + } + else + { + float fractTime = time - (float)Math.floor(time); + float fractLastTime = lastPollTime - (float)Math.floor(lastPollTime); + // swap if not in order + if(fractLastTime > fractTime) + { + float tmp = fractTime; + fractTime = fractLastTime; + fractLastTime = tmp; + } + // need to wrap around, swap again + if(fractTime - fractLastTime > .5f) + { + float tmp = fractTime; + fractTime = fractLastTime; + fractLastTime = tmp; + } + + stopTime = fractLastTime; + + curKey = events.floorKey(fractTime); + if(curKey == null && loop && !events.isEmpty()) + { + curKey = events.lastKey(); + } + if(curKey != null) + { + float checkCurTime = curKey; + float checkStopTime = stopTime; + if(checkCurTime >= fractTime) checkCurTime--; + if(checkStopTime >= fractTime) checkStopTime--; + float offset = fractTime - checkCurTime; + Event event = events.get(curKey); + if(checkCurTime < checkStopTime) + { + curKey = null; + } + else if(offset != event.offset()) + { + firstEvent = new Event(event.event(), offset); + } + } + } + } + + public boolean hasNext() + { + return curKey != null; + } + + @Override + public Event next() + { + if(curKey == null) + { + throw new NoSuchElementException(); + } + Event event; + if(firstEvent == null) + { + event = events.get(curKey); + } + else + { + event = firstEvent; + firstEvent = null; + } + curKey = events.lowerKey(curKey); + if(curKey == null && loop) + { + curKey = events.lastKey(); + } + if(curKey != null) + { + float checkStopTime = stopTime; + while(curKey + events.get(curKey).offset() < checkStopTime) checkStopTime--; + while(curKey + events.get(curKey).offset() >= checkStopTime + 1) checkStopTime++; + if(curKey <= checkStopTime) + { + curKey = null; + } + } + return event; + } + }; + } + }; + } + + protected static class MBJointClip implements IJointClip + { + private final boolean loop; + private final ImmutableList variables; + + public MBJointClip(boolean loop, ImmutableList variables) + { + this.loop = loop; + this.variables = variables; + EnumSet hadVar = Sets.newEnumSet(Collections.emptyList(), Variable.class); + for(MBVariableClip var : variables) + { + if(hadVar.contains(var.variable)) + { + throw new IllegalArgumentException("duplicate variable: " + var); + } + hadVar.add(var.variable); + } + } + + public TRSRTransformation apply(float time) + { + time -= Math.floor(time); + Vector3f translation = new Vector3f(0, 0, 0); + Vector3f scale = new Vector3f(1, 1, 1); + AxisAngle4f rotation = new AxisAngle4f(0, 0, 0, 0); + for(MBVariableClip var : variables) + { + int length = loop ? var.samples.length : (var.samples.length - 1); + float timeScaled = time * length; + int s1 = MathHelper.clamp_int((int)Math.round(Math.floor(timeScaled)), 0, length - 1); + float progress = timeScaled - s1; + int s2 = s1 + 1; + if(s2 == length && loop) s2 = 0; + float value = 0; + switch(var.interpolation) + { + case LINEAR: + if(var.variable == Variable.ANGLE) + { + float v1 = var.samples[s1]; + float v2 = var.samples[s2]; + float diff = ((v2 - v1) % 360 + 540) % 360 - 180; + value = v1 + diff * progress; + } + else + { + value = var.samples[s1] * (1 - progress) + var.samples[s2] * progress; + } + break; + case NEAREST: + value = var.samples[progress < .5f ? s1 : s2]; + break; + } + switch(var.variable) + { + case X: + translation.x = value; + break; + case Y: + translation.y = value; + break; + case Z: + translation.z = value; + break; + case XROT: + rotation.x = value; + break; + case YROT: + rotation.y = value; + break; + case ZROT: + rotation.z = value; + break; + case ANGLE: + rotation.angle = (float)Math.toRadians(value); + break; + case SCALE: + scale.x = scale.y = scale.z = value; + break; + case XS: + scale.x = value; + break; + case YS: + scale.y = value; + break; + case ZS: + scale.z = value; + break; + } + } + Quat4f rot = new Quat4f(); + rot.set(rotation); + return TRSRTransformation.blockCenterToCorner(new TRSRTransformation(translation, rot, scale, null)); + } + } + } + + protected static class MBJoint implements IJoint + { + private final String name; + private final TRSRTransformation invBindPose; + + public MBJoint(String name, BlockPart part) + { + this.name = name; + if(part.partRotation != null) + { + float x = 0, y = 0, z = 0; + switch(part.partRotation.axis) + { + case X: + x = 1; + case Y: + y = 1; + case Z: + z = 1; + } + Quat4f rotation = new Quat4f(); + rotation.set(new AxisAngle4f(x, y, z, 0)); + Matrix4f m = new TRSRTransformation( + TRSRTransformation.toVecmath(part.partRotation.origin), + rotation, + null, + null).getMatrix(); + m.invert(); + invBindPose = new TRSRTransformation(m); + } + else + { + invBindPose = TRSRTransformation.identity(); + } + } + + public TRSRTransformation getInvBindPose() + { + return invBindPose; + } + + public Optional getParent() + { + return Optional.absent(); + } + + public String getName() + { + return name; + } + } + + protected static class MBJointWeight + { + private final String name; + private final ImmutableMap weights; + + public MBJointWeight(String name, ImmutableMap weights) + { + this.name = name; + this.weights = weights; + } + + public String getName() + { + return name; + } + + public ImmutableMap getWeights() + { + return weights; + } + } + + protected static class Parameter + { + public static enum Variable + { + @SerializedName("offset_x") + X, + @SerializedName("offset_y") + Y, + @SerializedName("offset_z") + Z, + @SerializedName("axis_x") + XROT, + @SerializedName("axis_y") + YROT, + @SerializedName("axis_z") + ZROT, + @SerializedName("angle") + ANGLE, + @SerializedName("scale") + SCALE, + @SerializedName("scale_x") + XS, + @SerializedName("scale_y") + YS, + @SerializedName("scale_z") + ZS; + } + + public static enum Type + { + @SerializedName("uniform") + UNIFORM; + } + + public static enum Interpolation + { + @SerializedName("linear") + LINEAR, + @SerializedName("nearest") + NEAREST; + } + } + + public TRSRTransformation getPartTransform(IModelState state, BlockPart part, int i) + { + ImmutableCollection infos = getJoint(i); + if(!infos.isEmpty()) + { + Matrix4f m = new Matrix4f(), tmp; + float weight = 0; + for(MBJointWeight info : infos) + { + if(info.getWeights().containsKey(i)) + { + ModelBlockAnimation.MBJoint joint = new ModelBlockAnimation.MBJoint(info.getName(), part); + Optional trOp = state.apply(Optional.of(joint)); + if(trOp.isPresent() && trOp.get() != TRSRTransformation.identity()) + { + float w = info.getWeights().get(i)[0]; + tmp = trOp.get().getMatrix(); + tmp.mul(w); + m.add(tmp); + weight += w; + } + } + } + if(weight > 1e-5) + { + m.mul(1f / weight); + return new TRSRTransformation(m); + } + } + return null; + } +} diff --git a/src/main/java/net/minecraftforge/client/model/animation/TimeValues.java b/src/main/java/net/minecraftforge/client/model/animation/TimeValues.java new file mode 100644 index 000000000..a61268578 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/animation/TimeValues.java @@ -0,0 +1,370 @@ +package net.minecraftforge.client.model.animation; + +import java.io.IOException; +import java.util.regex.Pattern; + +import net.minecraft.util.IStringSerializable; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Various implementations of ITimeValue. + */ +public final class TimeValues +{ + public static enum IdentityValue implements ITimeValue, IStringSerializable + { + instance; + + public float apply(float input) + { + return input; + } + + public String getName() + { + return "identity"; + } + } + + public static final class ConstValue implements ITimeValue + { + private final float output; + + public ConstValue(float output) + { + this.output = output; + } + + public float apply(float input) + { + return output; + } + + @Override + public int hashCode() + { + return Objects.hashCode(output); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ConstValue other = (ConstValue) obj; + return output == other.output; + } + } + + /** + * Simple value holder. + */ + public static final class VariableValue implements ITimeValue + { + private float output; + + public VariableValue(float initialValue) + { + this.output = initialValue; + } + + public void setValue(float newValue) + { + this.output = newValue; + } + + public float apply(float input) + { + return output; + } + + @Override + public int hashCode() + { + return Objects.hashCode(output); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + VariableValue other = (VariableValue) obj; + return output == other.output; + } + } + + public static final class SimpleExprValue implements ITimeValue + { + private static final Pattern opsPattern = Pattern.compile("[+\\-*/mMrRfF]+"); + + private final String operators; + private final ImmutableList args; + + public SimpleExprValue(String operators, ImmutableList args) + { + this.operators = operators; + this.args = args; + } + + public float apply(float input) + { + float ret = input; + for(int i = 0; i < operators.length(); i++) + { + float arg = args.get(i).apply(input); + switch(operators.charAt(i)) + { + case '+': ret += arg; break; + case '-': ret -= arg; break; + case '*': ret *= arg; break; + case '/': ret /= arg; break; + case 'm': ret = Math.min(ret, arg); break; + case 'M': ret = Math.max(ret, arg); break; + case 'r': ret = (float)Math.floor(ret / arg) * arg; break; + case 'R': ret = (float)Math.ceil(ret / arg) * arg; break; + case 'f': ret -= Math.floor(ret / arg) * arg; break; + case 'F': ret = (float)Math.ceil(ret / arg) * arg - ret; break; + } + } + return ret; + } + + @Override + public int hashCode() + { + return Objects.hashCode(operators, args); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SimpleExprValue other = (SimpleExprValue) obj; + return Objects.equal(operators, other.operators) && Objects.equal(args, other.args); + } + } + + public static final class CompositionValue implements ITimeValue + { + private final ITimeValue g; + private final ITimeValue f; + + public CompositionValue(ITimeValue g, ITimeValue f) + { + super(); + this.g = g; + this.f = f; + } + + public float apply(float input) + { + return g.apply(f.apply(input)); + } + + @Override + public int hashCode() + { + return Objects.hashCode(g, f); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CompositionValue other = (CompositionValue) obj; + return Objects.equal(g, other.g) && Objects.equal(f, other.f); + } + } + + public static final class ParameterValue implements ITimeValue, IStringSerializable + { + private final String parameterName; + private final Function valueResolver; + private ITimeValue parameter; + + public ParameterValue(String parameterName, Function valueResolver) + { + this.parameterName = parameterName; + this.valueResolver = valueResolver; + } + + public String getName() + { + return parameterName; + } + + private void resolve() + { + if(parameter == null) + { + if(valueResolver != null) + { + parameter = valueResolver.apply(parameterName); + } + if(parameter == null) + { + throw new IllegalArgumentException("Couldn't resolve parameter value " + parameterName); + } + } + } + + public float apply(float input) + { + resolve(); + return parameter.apply(input); + } + + @Override + public int hashCode() + { + resolve(); + return parameter.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ParameterValue other = (ParameterValue) obj; + resolve(); + other.resolve(); + return Objects.equal(parameter, other.parameter); + } + } + + public static enum CommonTimeValueTypeAdapterFactory implements TypeAdapterFactory + { + INSTANCE; + + private final ThreadLocal> valueResolver = new ThreadLocal>(); + + public void setValueResolver(Function valueResolver) + { + this.valueResolver.set(valueResolver); + } + + @SuppressWarnings("unchecked") + public TypeAdapter create(Gson gson, TypeToken type) + { + if(type.getRawType() != ITimeValue.class) + { + return null; + } + + return (TypeAdapter)new TypeAdapter() + { + public void write(JsonWriter out, ITimeValue parameter) throws IOException + { + if(parameter instanceof ConstValue) + { + out.value(((ConstValue)parameter).output); + } + else if(parameter instanceof SimpleExprValue) + { + SimpleExprValue p = (SimpleExprValue)parameter; + out.beginArray(); + out.value(p.operators); + for(ITimeValue v : p.args) + { + write(out, v); + } + out.endArray(); + } + else if(parameter instanceof CompositionValue) + { + CompositionValue p = (CompositionValue)parameter; + out.beginArray(); + out.value("compose"); + write(out, p.g); + write(out, p.f); + out.endArray(); + } + else if(parameter instanceof IStringSerializable) + { + out.value("#" + ((IStringSerializable)parameter).getName()); + } + } + + public ITimeValue read(JsonReader in) throws IOException + { + switch(in.peek()) + { + case NUMBER: + return new ConstValue((float)in.nextDouble()); + case BEGIN_ARRAY: + in.beginArray(); + String type = in.nextString(); + ITimeValue p; + if(SimpleExprValue.opsPattern.matcher(type).matches()) + { + ImmutableList.Builder builder = ImmutableList.builder(); + while(in.peek() != JsonToken.END_ARRAY) + { + builder.add(read(in)); + } + p = new SimpleExprValue(type, builder.build()); + } + else if("compose".equals(type)) + { + p = new CompositionValue(read(in), read(in)); + } + else + { + throw new IOException("Unknown TimeValue type \"" + type + "\""); + } + in.endArray(); + return p; + case STRING: + String string = in.nextString(); + if(string.equals("#identity")) + { + return IdentityValue.instance; + } + if(!string.startsWith("#")) + { + throw new IOException("Expected TimeValue reference, got \"" + string + "\""); + } + // User Parameter TimeValue + return new ParameterValue(string.substring(1), valueResolver.get()); + default: + throw new IOException("Expected TimeValue, got " + in.peek()); + } + } + }; + } + } +} diff --git a/src/main/java/net/minecraftforge/client/model/b3d/B3DClip.java b/src/main/java/net/minecraftforge/client/model/b3d/B3DClip.java new file mode 100644 index 000000000..91df31ae1 --- /dev/null +++ b/src/main/java/net/minecraftforge/client/model/b3d/B3DClip.java @@ -0,0 +1,80 @@ +package net.minecraftforge.client.model.b3d; + +import net.minecraftforge.client.model.TRSRTransformation; +import net.minecraftforge.client.model.animation.Event; +import net.minecraftforge.client.model.animation.IClip; +import net.minecraftforge.client.model.animation.IJoint; +import net.minecraftforge.client.model.animation.IJointClip; +import net.minecraftforge.client.model.animation.JointClips; +import net.minecraftforge.client.model.b3d.B3DLoader.NodeJoint; +import net.minecraftforge.client.model.b3d.B3DModel.Key; +import net.minecraftforge.client.model.b3d.B3DModel.Node; + +import com.google.common.collect.ImmutableSet; + +// FIXME: is this fast enough? +public enum B3DClip implements IClip +{ + instance; + + public IJointClip apply(final IJoint joint) + { + if(!(joint instanceof NodeJoint)) + { + return JointClips.IdentityJointClip.instance; + } + return new NodeClip(((NodeJoint)joint).getNode()); + } + + public Iterable pastEvents(float lastPollTime, float time) + { + return ImmutableSet.of(); + } + + protected static class NodeClip implements IJointClip + { + private final Node node; + + public NodeClip(Node node) + { + this.node = node; + } + + public TRSRTransformation apply(float time) + { + TRSRTransformation ret = TRSRTransformation.identity(); + if(node.getAnimation() == null) + { + return ret.compose(new TRSRTransformation(node.getPos(), node.getRot(), node.getScale(), null)); + } + int start = Math.max(1, (int)Math.round(Math.floor(time))); + int end = Math.min(start + 1, (int)Math.round(Math.ceil(time))); + float progress = time - (float)Math.floor(time); + Key keyStart = node.getAnimation().getKeys().get(start, node); + Key keyEnd = node.getAnimation().getKeys().get(end, node); + TRSRTransformation startTr = keyStart == null ? null : new TRSRTransformation(keyStart.getPos(), keyStart.getRot(),keyStart.getScale(), null); + TRSRTransformation endTr = keyEnd == null ? null : new TRSRTransformation(keyEnd.getPos(), keyEnd.getRot(),keyEnd.getScale(), null); + if(keyStart == null) + { + if(keyEnd == null) + { + ret = ret.compose(new TRSRTransformation(node.getPos(), node.getRot(), node.getScale(), null)); + } + // TODO animated TRSR for speed? + else + { + ret = ret.compose(endTr); + } + } + else if(progress < 1e-5 || keyEnd == null) + { + ret = ret.compose(startTr); + } + else + { + ret = ret.compose(startTr.slerp(endTr, progress)); + } + return ret; + } + } +} diff --git a/src/main/java/net/minecraftforge/client/model/b3d/B3DLoader.java b/src/main/java/net/minecraftforge/client/model/b3d/B3DLoader.java index f0fa418da..d80226a4d 100644 --- a/src/main/java/net/minecraftforge/client/model/b3d/B3DLoader.java +++ b/src/main/java/net/minecraftforge/client/model/b3d/B3DLoader.java @@ -38,7 +38,11 @@ import net.minecraftforge.client.model.ISmartBlockModel; import net.minecraftforge.client.model.ISmartItemModel; import net.minecraftforge.client.model.ModelLoader; import net.minecraftforge.client.model.ModelLoaderRegistry; +import net.minecraftforge.client.model.ModelStateComposition; import net.minecraftforge.client.model.TRSRTransformation; +import net.minecraftforge.client.model.animation.IClip; +import net.minecraftforge.client.model.animation.IAnimatedModel; +import net.minecraftforge.client.model.animation.IJoint; import net.minecraftforge.client.model.b3d.B3DModel.Animation; import net.minecraftforge.client.model.b3d.B3DModel.Face; import net.minecraftforge.client.model.b3d.B3DModel.IKind; @@ -51,6 +55,7 @@ import net.minecraftforge.client.model.pipeline.LightUtil; import net.minecraftforge.client.model.pipeline.UnpackedBakedQuad; import net.minecraftforge.common.property.IExtendedBlockState; import net.minecraftforge.common.property.IUnlistedProperty; +import net.minecraftforge.common.property.Properties; import net.minecraftforge.fml.common.FMLLog; import org.apache.commons.lang3.tuple.Pair; @@ -366,7 +371,7 @@ public class B3DLoader implements ICustomModelLoader } } - public static class NodeJoint implements IModelPart + public static class NodeJoint implements IJoint { private final Node node; @@ -553,11 +558,6 @@ public class B3DLoader implements ICustomModelLoader return new BakedWrapper(getNode(), state, format, meshes, builder.build()); } - public B3DState getDefaultState() - { - return new B3DState(getNode().getAnimation(), 1); - } - public ResourceLocation getLocation() { return location; @@ -619,8 +619,15 @@ public class B3DLoader implements ICustomModelLoader { return this; } + + @Override + public IModelState getDefaultState() + { + return new B3DState(getNode().getAnimation(), 1); + } } - public static class ModelWrapper implements IRetexturableModel, IModelCustomData + + public static class ModelWrapper implements IRetexturableModel, IModelCustomData, IAnimatedModel { private final ResourceLocation modelLocation; private final B3DModel model; @@ -701,12 +708,6 @@ public class B3DLoader implements ICustomModelLoader return new BakedWrapper(model.getRoot(), state, format, meshes, builder.build()); } - @Override - public IModelState getDefaultState() - { - return new B3DState(model.getRoot().getAnimation(), defaultKey, defaultKey, 0); - } - @Override public IModel retexture(ImmutableMap textures) { @@ -777,6 +778,20 @@ public class B3DLoader implements ICustomModelLoader } return this; } + + public Optional getClip(String name) + { + if(name.equals("main")) + { + return Optional.of(B3DClip.instance); + } + return Optional.absent(); + } + + public IModelState getDefaultState() + { + return new B3DState(model.getRoot().getAnimation(), defaultKey, defaultKey, 0); + } } private static class BakedWrapper implements IFlexibleBakedModel, ISmartBlockModel, ISmartItemModel, IPerspectiveAwareModel @@ -840,9 +855,19 @@ public class B3DLoader implements ICustomModelLoader Collection faces = mesh.bake(new Function, Matrix4f>() { private final TRSRTransformation global = state.apply(Optional.absent()).or(TRSRTransformation.identity()); + private final LoadingCache, TRSRTransformation> localCache = CacheBuilder.newBuilder() + .maximumSize(32) + .build(new CacheLoader, TRSRTransformation>() + { + public TRSRTransformation load(Node node) throws Exception + { + return state.apply(Optional.of(new NodeJoint(node))).or(TRSRTransformation.identity()); + } + }); + public Matrix4f apply(Node node) { - return global.compose(state.apply(Optional.of(new NodeJoint(node))).or(TRSRTransformation.identity())).getMatrix(); + return global.compose(localCache.getUnchecked(node)).getMatrix(); } }); for(Face f : faces) @@ -978,6 +1003,21 @@ public class B3DLoader implements ICustomModelLoader return new BakedWrapper(node, newState, format, meshes, textures); } } + else if(exState.getUnlistedNames().contains(Properties.AnimationProperty)) + { + // FIXME: should animation state handle the parent state, or should it remain here? + IModelState parent = this.state; + if(parent instanceof B3DState) + { + B3DState ps = (B3DState)parent; + parent = ps.getParent(); + } + IModelState newState = exState.getValue(Properties.AnimationProperty); + if(newState != null) + { + return new BakedWrapper(node, new ModelStateComposition(parent, newState), format, meshes, textures); + } + } } return this; } diff --git a/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java b/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java index 06c297099..3cc123107 100644 --- a/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java +++ b/src/main/java/net/minecraftforge/client/model/b3d/B3DModel.java @@ -31,6 +31,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.google.common.base.Function; +import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableList; @@ -43,6 +44,7 @@ import com.google.common.collect.Table; public class B3DModel { static final Logger logger = LogManager.getLogger(B3DModel.class); + private static final boolean printLoadedModels = "true".equals(System.getProperty("b3dloader.printLoadedModels")); private final List textures; private final List brushes; private final Node root; @@ -88,13 +90,27 @@ public class B3DModel } } + private String dump = ""; + private void dump(String str) + { + if(printLoadedModels) + { + dump += str + "\n"; + } + } + private B3DModel res; public B3DModel parse() throws IOException { if(res != null) return res; + dump = "\n"; readHeader(); res = bb3d(); + if(printLoadedModels) + { + logger.info(dump); + } return res; } @@ -192,6 +208,7 @@ public class B3DModel List textures = Collections.emptyList(); List brushes = Collections.emptyList(); Node root = null; + dump("BB3D(version = " + version + ") {"); while(buf.hasRemaining()) { readHeader(); @@ -200,6 +217,7 @@ public class B3DModel else if(isChunk("NODE")) root = node(); else skip(); } + dump("}"); popLimit(); return new B3DModel(textures, brushes, root, meshes.build()); } @@ -218,6 +236,7 @@ public class B3DModel float rot = buf.getFloat(); ret.add(new Texture(path, flags, blend, pos, scale, rot)); } + dump("TEXS([" + Joiner.on(", ").join(ret) + "])"); popLimit(); this.textures.addAll(ret); return ret; @@ -239,6 +258,7 @@ public class B3DModel for(int i = 0; i < n_texs; i++) textures.add(getTexture(buf.getInt())); ret.add(new Brush(name, color, shininess, blend, fx, textures)); } + dump("BRUS([" + Joiner.on(", ").join(ret) + "])"); popLimit(); this.brushes.addAll(ret); return ret; @@ -287,6 +307,7 @@ public class B3DModel } ret.add(new Vertex(v, n, color, tex_coords)); } + dump("VRTS([" + Joiner.on(", ").join(ret) + "])"); popLimit(); this.vertices.clear(); this.vertices.addAll(ret); @@ -302,6 +323,7 @@ public class B3DModel { ret.add(new Face(getVertex(buf.getInt()), getVertex(buf.getInt()), getVertex(buf.getInt()), getBrush(brush_id))); } + dump("TRIS([" + Joiner.on(", ").join(ret) + "])"); popLimit(); return ret; } @@ -311,6 +333,7 @@ public class B3DModel chunk("MESH"); int brush_id = buf.getInt(); readHeader(); + dump("MESH(brush = " + brush_id + ") {"); vrts(); List ret = new ArrayList(); while(buf.hasRemaining()) @@ -318,6 +341,7 @@ public class B3DModel readHeader(); ret.addAll(tris()); } + dump("}"); popLimit(); return Pair.of(getBrush(brush_id), ret); } @@ -330,6 +354,7 @@ public class B3DModel { ret.add(Pair.of(getVertex(buf.getInt()), buf.getFloat())); } + dump("BONE(...)"); popLimit(); return ret; } @@ -381,6 +406,7 @@ public class B3DModel animations.peek().put(frame, Optional.>absent(), key); ret.put(frame, key); } + dump("KEYS([(" + Joiner.on("), (").withKeyValueSeparator(" -> ").join(ret) + ")])"); popLimit(); return ret; } @@ -391,6 +417,7 @@ public class B3DModel int flags = buf.getInt(); int frames = buf.getInt(); float fps = buf.getFloat(); + dump("ANIM(" + flags + ", " + frames + ", " + fps + ")"); popLimit(); return Triple.of(flags, frames, fps); } @@ -408,6 +435,7 @@ public class B3DModel Vector3f pos = new Vector3f(buf.getFloat(), buf.getFloat(), buf.getFloat()); Vector3f scale = new Vector3f(buf.getFloat(), buf.getFloat(), buf.getFloat()); Quat4f rot = readQuat(); + dump("NODE(" + name + ", " + pos + ", " + scale + ", " + rot + ") {"); while(buf.hasRemaining()) { readHeader(); @@ -418,6 +446,7 @@ public class B3DModel else if(isChunk("ANIM")) animData = anim(); else skip(); } + dump("}"); popLimit(); Table>, Key> keyData = animations.pop(); Node node; @@ -965,6 +994,7 @@ public class B3DModel private Node parent; private final Brush brush; private final ImmutableList faces; + //private final ImmutableList bones; private Set> bones = new HashSet>(); diff --git a/src/main/java/net/minecraftforge/client/model/pipeline/LightUtil.java b/src/main/java/net/minecraftforge/client/model/pipeline/LightUtil.java index 4a5a8036f..9a23a1109 100644 --- a/src/main/java/net/minecraftforge/client/model/pipeline/LightUtil.java +++ b/src/main/java/net/minecraftforge/client/model/pipeline/LightUtil.java @@ -81,6 +81,8 @@ public class LightUtil } }); + private static final int itemCount = DefaultVertexFormats.ITEM.getElementCount(); + public static void putBakedQuad(IVertexConsumer consumer, BakedQuad quad) { consumer.setQuadOrientation(quad.getFace()); @@ -94,12 +96,14 @@ public class LightUtil } //int[] eMap = mapFormats(consumer.getVertexFormat(), DefaultVertexFormats.ITEM); float[] data = new float[4]; - int[] eMap = formatMaps.getUnchecked(consumer.getVertexFormat()); + VertexFormat format = consumer.getVertexFormat(); + int count = format.getElementCount(); + int[] eMap = formatMaps.getUnchecked(format); for(int v = 0; v < 4; v++) { - for(int e = 0; e < consumer.getVertexFormat().getElementCount(); e++) + for(int e = 0; e < count; e++) { - if(eMap[e] != DefaultVertexFormats.ITEM.getElementCount()) + if(eMap[e] != itemCount) { unpack(quad.getVertexData(), data, DefaultVertexFormats.ITEM, v, eMap[e]); consumer.put(e, data); @@ -114,13 +118,15 @@ public class LightUtil public static int[] mapFormats(VertexFormat from, VertexFormat to) { - int[] eMap = new int[from.getElementCount()]; + int fromCount = from.getElementCount(); + int toCount = to.getElementCount(); + int[] eMap = new int[fromCount]; - for(int e = 0; e < from.getElementCount(); e++) + for(int e = 0; e < fromCount; e++) { VertexFormatElement expected = from.getElement(e); int e2; - for(e2 = 0; e2 < to.getElementCount(); e2++) + for(e2 = 0; e2 < toCount; e2++) { VertexFormatElement current = to.getElement(e2); if(expected.getUsage() == current.getUsage() && expected.getIndex() == current.getIndex()) @@ -135,44 +141,50 @@ public class LightUtil public static void unpack(int[] from, float[] to, VertexFormat formatFrom, int v, int e) { + int length = 4 < to.length ? 4 : to.length; VertexFormatElement element = formatFrom.getElement(e); - int length = 4 <+ to.length ? 4 : to.length; + int vertexStart = v * formatFrom.getNextOffset() + formatFrom.func_181720_d(e); + int count = element.getElementCount(); + VertexFormatElement.EnumType type = element.getType(); + int size = type.getSize(); + int mask = (256 << (8 * (size - 1))) - 1; for(int i = 0; i < length; i++) { - if(i < element.getElementCount()) + if(i < count) { - int pos = v * formatFrom.getNextOffset() + formatFrom.func_181720_d(e) + element.getType().getSize() * i; + int pos = vertexStart + size * i; int index = pos >> 2; int offset = pos & 3; int bits = from[index]; bits = bits >>> (offset * 8); - if((pos + element.getType().getSize() - 1) / 4 != index) + if((pos + size - 1) / 4 != index) { bits |= from[index + 1] << ((4 - offset) * 8); } - int mask = (256 << (8 * (element.getType().getSize() - 1))) - 1; bits &= mask; - switch(element.getType()) + if(type == VertexFormatElement.EnumType.FLOAT) { - case FLOAT: - to[i] = Float.intBitsToFloat(bits); - break; - case UBYTE: - case USHORT: - to[i] = (float)bits / mask; - break; - case UINT: - to[i] = (float)((double)(bits & 0xFFFFFFFFL) / 0xFFFFFFFFL); - break; - case BYTE: - to[i] = ((float)(byte)bits) / mask * 2; - break; - case SHORT: - to[i] = ((float)(short)bits) / mask * 2; - break; - case INT: - to[i] = ((float)(bits & 0xFFFFFFFFL)) / 0xFFFFFFFFL * 2; - break; + to[i] = Float.intBitsToFloat(bits); + } + else if(type == VertexFormatElement.EnumType.UBYTE || type == VertexFormatElement.EnumType.USHORT) + { + to[i] = (float)bits / mask; + } + else if(type == VertexFormatElement.EnumType.UINT) + { + to[i] = (float)((double)(bits & 0xFFFFFFFFL) / 0xFFFFFFFFL); + } + else if(type == VertexFormatElement.EnumType.BYTE) + { + to[i] = ((float)(byte)bits) / mask * 2; + } + else if(type == VertexFormatElement.EnumType.SHORT) + { + to[i] = ((float)(short)bits) / mask * 2; + } + else if(type == VertexFormatElement.EnumType.INT) + { + to[i] = ((float)(bits & 0xFFFFFFFFL)) / 0xFFFFFFFFL * 2; } } else @@ -185,31 +197,35 @@ public class LightUtil public static void pack(float[] from, int[] to, VertexFormat formatTo, int v, int e) { VertexFormatElement element = formatTo.getElement(e); + int vertexStart = v * formatTo.getNextOffset() + formatTo.func_181720_d(e); + int count = element.getElementCount(); + VertexFormatElement.EnumType type = element.getType(); + int size = type.getSize(); + int mask = (256 << (8 * (size - 1))) - 1; for(int i = 0; i < 4; i++) { - if(i < element.getElementCount()) + if(i < count) { - int pos = v * formatTo.getNextOffset() + formatTo.func_181720_d(e) + element.getType().getSize() * i; + int pos = vertexStart + size * i; int index = pos >> 2; int offset = pos & 3; int bits = 0; - int mask = (256 << (8 * (element.getType().getSize() - 1))) - 1; float f = i < from.length ? from[i] : 0; - switch(element.getType()) + if(type == VertexFormatElement.EnumType.FLOAT) { - case FLOAT: - bits = Float.floatToRawIntBits(f); - break; - case UBYTE: - case USHORT: - case UINT: - bits = (int)(f * mask); - break; - case BYTE: - case SHORT: - case INT: - bits = (int)(f * mask / 2); - break; + bits = Float.floatToRawIntBits(f); + } + else if( + type == VertexFormatElement.EnumType.UBYTE || + type == VertexFormatElement.EnumType.USHORT || + type == VertexFormatElement.EnumType.UINT + ) + { + bits = (int)(f * mask); + } + else + { + bits = (int)(f * mask / 2); } to[index] &= ~(mask << (offset * 8)); to[index] |= (((bits & mask) << (offset * 8))); diff --git a/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java b/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java index 7e8fbe787..ca23d9fc0 100644 --- a/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java +++ b/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterFlat.java @@ -114,6 +114,9 @@ public class VertexLighterFlat extends QuadGatheringTransformer multiplier = blockInfo.getColorMultiplier(tint); } + VertexFormat format = parent.getVertexFormat(); + int count = format.getElementCount(); + for(int v = 0; v < 4; v++) { position[v][0] += blockInfo.getShx(); @@ -145,9 +148,10 @@ public class VertexLighterFlat extends QuadGatheringTransformer } // no need for remapping cause all we could've done is add 1 element to the end - for(int e = 0; e < parent.getVertexFormat().getElementCount(); e++) + for(int e = 0; e < count; e++) { - switch(parent.getVertexFormat().getElement(e).getUsage()) + VertexFormatElement element = format.getElement(e); + switch(element.getUsage()) { case POSITION: // position adding moved to WorldRendererConsumer due to x and z not fitting completely into a float @@ -166,7 +170,7 @@ public class VertexLighterFlat extends QuadGatheringTransformer case COLOR: parent.put(e, color[v]); break; - case UV: if(getVertexFormat().getElement(e).getIndex() == 1) + case UV: if(element.getIndex() == 1) { parent.put(e, lightmap[v]); break; diff --git a/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterSmoothAo.java b/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterSmoothAo.java index 497b8ac20..bc1d80ebb 100644 --- a/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterSmoothAo.java +++ b/src/main/java/net/minecraftforge/client/model/pipeline/VertexLighterSmoothAo.java @@ -40,25 +40,28 @@ public class VertexLighterSmoothAo extends VertexLighterFlat y *= s; z *= s; } - float ax = Math.abs(x); - float ay = Math.abs(y); - float az = Math.abs(z); + float ax = x > 0 ? x : -x; + float ay = y > 0 ? y : -y; + float az = z > 0 ? z : -z; float e1 = 1 + 1e-4f; if(ax > 2 - 1e-4f && ay <= e1 && az <= e1) { - x = MathHelper.clamp_float(x, -2 + 1e-4f, 2 - 1e-4f); + if(x > -2 + 1e-4f) x = -2 + 1e-4f; + if(x < 2 - 1e-4f) x = 2 - 1e-4f; } else if(ay > 2 - 1e-4f && az <= e1 && ax <= e1) { - y = MathHelper.clamp_float(y, -2 + 1e-4f, 2 - 1e-4f); + if(y > -2 + 1e-4f) y = -2 + 1e-4f; + if(y < 2 - 1e-4f) y = 2 - 1e-4f; } else if(az > 2 - 1e-4f && ax <= e1 && ay <= e1) { - z = MathHelper.clamp_float(z, -2 + 1e-4f, 2 - 1e-4f); + if(z > -2 + 1e-4f) z = -2 + 1e-4f; + if(z < 2 - 1e-4f) z = 2 - 1e-4f; } - ax = Math.abs(x); - ay = Math.abs(y); - az = Math.abs(z); + ax = x > 0 ? x : -x; + ay = y > 0 ? y : -y; + az = z > 0 ? z : -z; if(ax <= e1 && ay + az > 3f - 1e-4f) { float s = (3f - 1e-4f) / (ay + az); diff --git a/src/main/java/net/minecraftforge/client/model/pipeline/WorldRendererConsumer.java b/src/main/java/net/minecraftforge/client/model/pipeline/WorldRendererConsumer.java index 3d268bf85..9822afdac 100644 --- a/src/main/java/net/minecraftforge/client/model/pipeline/WorldRendererConsumer.java +++ b/src/main/java/net/minecraftforge/client/model/pipeline/WorldRendererConsumer.java @@ -1,7 +1,5 @@ package net.minecraftforge.client.model.pipeline; -import java.util.Arrays; - import net.minecraft.client.renderer.WorldRenderer; import net.minecraft.client.renderer.vertex.VertexFormat; import net.minecraft.client.renderer.vertex.VertexFormatElement.EnumUsage; @@ -46,7 +44,7 @@ public class WorldRendererConsumer implements IVertexConsumer { renderer.addVertexData(quadData); renderer.putPosition(offset.getX(), offset.getY(), offset.getZ()); - Arrays.fill(quadData, 0); + //Arrays.fill(quadData, 0); v = 0; } } @@ -60,4 +58,4 @@ public class WorldRendererConsumer implements IVertexConsumer public void setQuadTint(int tint) {} public void setQuadOrientation(EnumFacing orientation) {} public void setQuadColored() {} -} \ No newline at end of file +} diff --git a/src/main/java/net/minecraftforge/common/model/animation/IAnimationStateMachine.java b/src/main/java/net/minecraftforge/common/model/animation/IAnimationStateMachine.java new file mode 100644 index 000000000..53b640c21 --- /dev/null +++ b/src/main/java/net/minecraftforge/common/model/animation/IAnimationStateMachine.java @@ -0,0 +1,36 @@ +package net.minecraftforge.common.model.animation; + +import net.minecraftforge.client.model.IModelState; +import net.minecraftforge.client.model.animation.Event; + +import org.apache.commons.lang3.tuple.Pair; + +/** + * State machine representing the model animation. + */ +public interface IAnimationStateMachine +{ + /** + * Sample the state and events at the current time. + * Event iterable will contain all events that happened from the last invocation of this method, from most to least recent. + * Event offset is relative to the previous event, and for the first event it's relative to the current time. + */ + Pair> apply(float time); + + /** + * Transition to a new state. + */ + void transition(String newState); + + /** + * Get current state name. + */ + String currentState(); + + /** + * Set to true if the machine should handle special events that come from the clips (they start with '!'). + * Right now only implemented event is "!transition:". + * Default value is true. + */ + void shouldHandleSpecialEvents(boolean value); +} diff --git a/src/main/java/net/minecraftforge/common/property/Properties.java b/src/main/java/net/minecraftforge/common/property/Properties.java index 5063da273..243109cc0 100644 --- a/src/main/java/net/minecraftforge/common/property/Properties.java +++ b/src/main/java/net/minecraftforge/common/property/Properties.java @@ -1,9 +1,27 @@ package net.minecraftforge.common.property; import net.minecraft.block.properties.IProperty; +import net.minecraft.block.properties.PropertyBool; +import net.minecraftforge.client.model.IModelState; public class Properties { + /** + * Property indicating if the model should be rendered in the static renderer or in the TESR. AnimationTESR sets it to false. + */ + public static final PropertyBool StaticProperty = PropertyBool.create("static"); + + /** + * Property holding the IModelState used for animating the model in the TESR. + */ + public static final IUnlistedProperty AnimationProperty = new IUnlistedProperty() + { + public String getName() { return "forge_animation"; } + public boolean isValid(IModelState state) { return true; } + public Class getType() { return IModelState.class; } + public String valueToString(IModelState state) { return state.toString(); } + }; + public static > IUnlistedProperty toUnlisted(IProperty property) { return new PropertyAdapter(property); diff --git a/src/main/java/net/minecraftforge/common/util/JsonUtils.java b/src/main/java/net/minecraftforge/common/util/JsonUtils.java new file mode 100644 index 000000000..68d2d34ee --- /dev/null +++ b/src/main/java/net/minecraftforge/common/util/JsonUtils.java @@ -0,0 +1,73 @@ +package net.minecraftforge.common.util; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.TypeParameter; +import com.google.common.reflect.TypeToken; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +public class JsonUtils +{ + // http://stackoverflow.com/questions/7706772/deserializing-immutablelist-using-gson/21677349#21677349 + public enum ImmutableListTypeAdapter implements JsonDeserializer>, JsonSerializer> + { + INSTANCE; + + public ImmutableList deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException + { + final Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + final Type parameterizedType = listOf(typeArguments[0]).getType(); + final List list = context.deserialize(json, parameterizedType); + return ImmutableList.copyOf(list); + } + + public JsonElement serialize(ImmutableList src, Type type, JsonSerializationContext context) + { + final Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + final Type parameterizedType = listOf(typeArguments[0]).getType(); + return context.serialize(src, parameterizedType); + } + } + + @SuppressWarnings({ "serial", "unchecked" }) + private static TypeToken> listOf(final Type arg) + { + return new TypeToken>() {}.where(new TypeParameter() {}, (TypeToken) TypeToken.of(arg)); + } + + public enum ImmutableMapTypeAdapter implements JsonDeserializer>, JsonSerializer> + { + INSTANCE; + + public ImmutableMap deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException + { + final Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + final Type parameterizedType = mapOf(typeArguments[1]).getType(); + final Map map = context.deserialize(json, parameterizedType); + return ImmutableMap.copyOf(map); + } + + public JsonElement serialize(ImmutableMap src, Type type, JsonSerializationContext context) + { + final Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); + final Type parameterizedType = mapOf(typeArguments[1]).getType(); + return context.serialize(src, parameterizedType); + } + } + + @SuppressWarnings({ "serial", "unchecked" }) + private static TypeToken> mapOf(final Type arg) + { + return new TypeToken>() {}.where(new TypeParameter() {}, (TypeToken) TypeToken.of(arg)); + } +} diff --git a/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java b/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java index 993ded08c..3f24d4e62 100644 --- a/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java +++ b/src/main/java/net/minecraftforge/fml/client/FMLClientHandler.java @@ -68,6 +68,7 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.util.StringUtils; import net.minecraft.world.WorldSettings; import net.minecraft.world.storage.SaveFormatOld; +import net.minecraftforge.client.model.animation.Animation; import net.minecraftforge.fml.client.registry.RenderingRegistry; import net.minecraftforge.fml.common.DummyModContainer; import net.minecraftforge.fml.common.DuplicateModsFoundException; @@ -199,6 +200,8 @@ public class FMLClientHandler implements IFMLSidedHandler return; } + resourceManager.registerReloadListener(Animation.INSTANCE); + FMLCommonHandler.instance().beginLoading(this); try { diff --git a/src/test/java/net/minecraftforge/debug/ModelAnimationDebug.java b/src/test/java/net/minecraftforge/debug/ModelAnimationDebug.java index 79d762cc2..820ca941c 100644 --- a/src/test/java/net/minecraftforge/debug/ModelAnimationDebug.java +++ b/src/test/java/net/minecraftforge/debug/ModelAnimationDebug.java @@ -1,50 +1,64 @@ package net.minecraftforge.debug; +import java.io.IOException; + import net.minecraft.block.Block; import net.minecraft.block.BlockPistonBase; import net.minecraft.block.material.Material; import net.minecraft.block.properties.IProperty; -import net.minecraft.block.properties.PropertyBool; import net.minecraft.block.properties.PropertyDirection; import net.minecraft.block.state.IBlockState; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.BlockRendererDispatcher; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.client.renderer.Tessellator; -import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.client.renderer.entity.RenderLiving; +import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.texture.TextureMap; -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; -import net.minecraft.client.renderer.vertex.DefaultVertexFormats; -import net.minecraft.client.resources.model.IBakedModel; import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.creativetab.CreativeTabs; +import net.minecraft.entity.EntityLiving; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.Item; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.BlockPos; import net.minecraft.util.EnumFacing; -import net.minecraft.util.ITickable; +import net.minecraft.util.ResourceLocation; import net.minecraft.world.IBlockAccess; import net.minecraft.world.World; -import net.minecraftforge.client.model.ISmartBlockModel; +import net.minecraftforge.client.model.IModel; +import net.minecraftforge.client.model.IModelState; +import net.minecraftforge.client.model.IRetexturableModel; import net.minecraftforge.client.model.ModelLoader; +import net.minecraftforge.client.model.ModelLoaderRegistry; +import net.minecraftforge.client.model.MultiModel; +import net.minecraftforge.client.model.TRSRTransformation; +import net.minecraftforge.client.model.animation.Animation; +import net.minecraftforge.client.model.animation.AnimationModelBase; +import net.minecraftforge.client.model.animation.AnimationTESR; +import net.minecraftforge.client.model.animation.Event; +import net.minecraftforge.client.model.animation.IAnimationProvider; +import net.minecraftforge.client.model.animation.ITimeValue; +import net.minecraftforge.client.model.animation.TimeValues.VariableValue; import net.minecraftforge.client.model.b3d.B3DLoader; -import net.minecraftforge.client.model.b3d.B3DLoader.B3DFrameProperty; -import net.minecraftforge.client.model.b3d.B3DLoader.B3DState; +import net.minecraftforge.client.model.pipeline.VertexLighterSmoothAo; +import net.minecraftforge.common.model.animation.IAnimationStateMachine; import net.minecraftforge.common.property.ExtendedBlockState; -import net.minecraftforge.common.property.IExtendedBlockState; import net.minecraftforge.common.property.IUnlistedProperty; +import net.minecraftforge.common.property.Properties; import net.minecraftforge.fml.client.registry.ClientRegistry; +import net.minecraftforge.fml.client.registry.IRenderFactory; +import net.minecraftforge.fml.client.registry.RenderingRegistry; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.Mod.Instance; import net.minecraftforge.fml.common.SidedProxy; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +import net.minecraftforge.fml.common.registry.EntityRegistry; import net.minecraftforge.fml.common.registry.GameRegistry; -import org.lwjgl.opengl.GL11; +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.collect.ImmutableMap; @Mod(modid = ModelAnimationDebug.MODID, version = ModelAnimationDebug.VERSION) public class ModelAnimationDebug @@ -54,12 +68,14 @@ public class ModelAnimationDebug public static String blockName = "test_animation_block"; public static final PropertyDirection FACING = PropertyDirection.create("facing"); - public static final PropertyBool STATIC = PropertyBool.create("static");; + + @Instance(MODID) + public static ModelAnimationDebug instance; @SidedProxy public static CommonProxy proxy; - public static class CommonProxy + public static abstract class CommonProxy { public void preInit(FMLPreInitializationEvent event) { @@ -73,7 +89,7 @@ public class ModelAnimationDebug @Override public ExtendedBlockState createBlockState() { - return new ExtendedBlockState(this, new IProperty[]{ FACING, STATIC }, new IUnlistedProperty[]{ B3DFrameProperty.instance }); + return new ExtendedBlockState(this, new IProperty[]{ FACING, Properties.StaticProperty }, new IUnlistedProperty[]{ Properties.AnimationProperty }); } @Override @@ -105,12 +121,12 @@ public class ModelAnimationDebug @Override public TileEntity createTileEntity(World world, IBlockState state) { - return new Chest(state); + return new Chest(); } @Override public IBlockState getActualState(IBlockState state, IBlockAccess world, BlockPos pos) { - return state.withProperty(STATIC, true); + return state.withProperty(Properties.StaticProperty, true); } /*@Override @@ -131,18 +147,25 @@ public class ModelAnimationDebug TileEntity te = world.getTileEntity(pos); if(te instanceof Chest) { - ((Chest)te).click(); + ((Chest)te).click(player.isSneaking()); } } - return false; + return true; } }, blockName); + GameRegistry.registerTileEntity(Chest.class, MODID + ":" + "tile_" + blockName); } - public void init(FMLInitializationEvent event) {} + public abstract IAnimationStateMachine load(ResourceLocation location, ImmutableMap parameters); } - public static class ServerProxy extends CommonProxy {} + public static class ServerProxy extends CommonProxy + { + public IAnimationStateMachine load(ResourceLocation location, ImmutableMap parameters) + { + return null; + } + } public static class ClientProxy extends CommonProxy { @@ -152,142 +175,212 @@ public class ModelAnimationDebug super.preInit(event); B3DLoader.instance.addDomain(MODID); ModelLoader.setCustomModelResourceLocation(Item.getItemFromBlock(GameRegistry.findBlock(MODID, blockName)), 0, new ModelResourceLocation(MODID.toLowerCase() + ":" + blockName, "inventory")); - ClientRegistry.bindTileEntitySpecialRenderer(Chest.class, ChestRenderer.instance); + ClientRegistry.bindTileEntitySpecialRenderer(Chest.class, new AnimationTESR() + { + @Override + public void handleEvents(Chest chest, float time, Iterable pastEvents) + { + chest.handleEvents(time, pastEvents); + } + }); + String entityName = MODID + ":entity_chest"; + //EntityRegistry.registerGlobalEntityID(EntityChest.class, entityName, EntityRegistry.findGlobalUniqueEntityId()); + EntityRegistry.registerModEntity(EntityChest.class, entityName, 0, ModelAnimationDebug.instance, 64, 20, true, 0xFFAAAA00, 0xFFDDDD00); + RenderingRegistry.registerEntityRenderingHandler(EntityChest.class, new IRenderFactory() + { + public Render createRenderFor(RenderManager manager) + { + try + { + /*model = ModelLoaderRegistry.getModel(new ResourceLocation(ModelLoaderRegistryDebug.MODID, "block/chest.b3d")); + if(model instanceof IRetexturableModel) + { + model = ((IRetexturableModel)model).retexture(ImmutableMap.of("#chest", "entity/chest/normal")); + } + if(model instanceof IModelCustomData) + { + model = ((IModelCustomData)model).process(ImmutableMap.of("mesh", "[\"Base\", \"Lid\"]")); + }*/ + IModel base = ModelLoaderRegistry.getModel(new ResourceLocation(ModelAnimationDebug.MODID, "block/engine")); + IModel ring = ModelLoaderRegistry.getModel(new ResourceLocation(ModelAnimationDebug.MODID, "block/engine_ring")); + ImmutableMap textures = ImmutableMap.of( + "base", "blocks/stone", + "front", "blocks/log_oak", + "chamber", "blocks/redstone_block", + "trunk", "blocks/end_stone" + ); + if(base instanceof IRetexturableModel) + { + base = ((IRetexturableModel)base).retexture(textures); + } + if(ring instanceof IRetexturableModel) + { + ring = ((IRetexturableModel)ring).retexture(textures); + } + IModel model = new MultiModel( + new ResourceLocation(ModelAnimationDebug.MODID, "builtin/engine"), + ring, + TRSRTransformation.identity(), + ImmutableMap.of( + "base", Pair.of(base, TRSRTransformation.identity()) + ) + ); + return new RenderLiving(manager, new AnimationModelBase(model, new VertexLighterSmoothAo()) + { + @Override + public void handleEvents(EntityChest chest, float time, Iterable pastEvents) + { + chest.handleEvents(time, pastEvents); + } + }, 0.5f) + { + protected ResourceLocation getEntityTexture(EntityChest entity) + { + return TextureMap.locationBlocksTexture; + } + }; + } + catch(IOException e) + { + throw new RuntimeException(e); + } + } + }); } - @Override - public void init(FMLInitializationEvent event) + public IAnimationStateMachine load(ResourceLocation location, ImmutableMap parameters) { - super.init(event); + return Animation.INSTANCE.load(location, parameters); } + } @EventHandler public void preInit(FMLPreInitializationEvent event) { proxy.preInit(event); } - @EventHandler - public void init(FMLInitializationEvent event) { proxy.init(event); } - - private static class Chest extends TileEntity implements ITickable + public static class Chest extends TileEntity implements IAnimationProvider { - private final int minFrame = 1; - private final int maxFrame = 10; - private int tick = minFrame; - private boolean opening = false; - private boolean closing = false; + private final IAnimationStateMachine asm; + private final VariableValue cycleLength = new VariableValue(4); + private final VariableValue clickTime = new VariableValue(Float.NEGATIVE_INFINITY); + //private final VariableValue offset = new VariableValue(0); - public Chest(IBlockState state) { + public Chest() { + /*asm = proxy.load(new ResourceLocation(MODID.toLowerCase(), "asms/block/chest.json"), ImmutableMap.of( + "click_time", clickTime + ));*/ + asm = proxy.load(new ResourceLocation(MODID.toLowerCase(), "asms/block/engine.json"), ImmutableMap.of( + "cycle_length", cycleLength, + "click_time", clickTime + //"offset", offset + )); + } + + public void handleEvents(float time, Iterable pastEvents) + { + for(Event event : pastEvents) + { + System.out.println("Event: " + event.event() + " " + event.offset() + " " + getPos() + " " + time); + } + } + + @Override + public boolean hasFastRenderer() + { + return true; } /*public IExtendedBlockState getState(IExtendedBlockState state) { return state.withProperty(B3DFrameProperty.instance, curState); }*/ - public void click() + public void click(boolean sneaking) { - if(opening || tick == maxFrame) + if(asm != null) { - opening = false; - closing = true; - return; - } - if(closing || tick == minFrame) - { - closing = false; - opening = true; - return; - } - opening = true; - } - - @Override - public void update() - { - if(opening) - { - tick++; - if(tick >= maxFrame) + if(sneaking) { - tick = maxFrame; - opening = false; + cycleLength.setValue(6 - cycleLength.apply(0)); } - } - if(closing) - { - tick--; - if(tick <= minFrame) + /*else if(asm.currentState().equals("closed")) { - tick = minFrame; - closing = false; + clickTime.setValue(Animation.getWorldTime(getWorld())); + asm.transition("opening"); + } + else if(asm.currentState().equals("open")) + { + clickTime.setValue(Animation.getWorldTime(getWorld())); + asm.transition("closing"); + }*/ + else if(asm.currentState().equals("default")) + { + float time = Animation.getWorldTime(getWorld()); + clickTime.setValue(time); + //offset.setValue(time); + //asm.transition("moving"); + asm.transition("starting"); + } + else if(asm.currentState().equals("moving")) + { + clickTime.setValue(Animation.getWorldTime(getWorld())); + asm.transition("stopping"); } } } - public int getCurFrame() + public IAnimationStateMachine asm() { - return tick; - } - - public int getNextFrame() - { - if(opening) return Math.min(tick + 1, maxFrame); - if(closing) return Math.max(tick - 1, minFrame); - return tick; + return asm; } } - private static class ChestRenderer extends TileEntitySpecialRenderer + public static class EntityChest extends EntityLiving implements IAnimationProvider { - public static ChestRenderer instance = new ChestRenderer(); - private ChestRenderer() {} + private final IAnimationStateMachine asm; + private VariableValue cycleLength; - private BlockRendererDispatcher blockRenderer; - - public void renderTileEntityAt(Chest te, double x, double y, double z, float partialTick, int breakStage) + public EntityChest(World world) { - if(blockRenderer == null) blockRenderer = Minecraft.getMinecraft().getBlockRendererDispatcher(); - IBlockState state = te.getWorld().getBlockState(te.getPos()); - state = state.withProperty(STATIC, false); - IBakedModel model = this.blockRenderer.getBlockModelShapes().getModelForState(state); - if(state instanceof IExtendedBlockState) + super(world); + setSize(1, 1); + if(cycleLength == null) { - IExtendedBlockState exState = (IExtendedBlockState)state; - if(exState.getUnlistedNames().contains(B3DFrameProperty.instance)) + cycleLength = new VariableValue(getHealth() / 5); + } + asm = proxy.load(new ResourceLocation(MODID.toLowerCase(), "asms/block/engine.json"), ImmutableMap.of( + "cycle_length", cycleLength + )); + } + + public void handleEvents(float time, Iterable pastEvents) + { + // TODO Auto-generated method stub + } + + public IAnimationStateMachine asm() + { + return asm; + } + + @Override + public void onDataWatcherUpdate(int id) + { + super.onDataWatcherUpdate(id); + if(id == 6) // health + { + if(cycleLength == null) { - exState = exState.withProperty(B3DFrameProperty.instance, new B3DState(null, te.getCurFrame(), te.getNextFrame(), partialTick)); - if(model instanceof ISmartBlockModel) - { - model = ((ISmartBlockModel)model).handleBlockState(exState); - } + cycleLength = new VariableValue(0); } + cycleLength.setValue(getHealth() / 5); } + } - Tessellator tessellator = Tessellator.getInstance(); - WorldRenderer worldrenderer = tessellator.getWorldRenderer(); - this.bindTexture(TextureMap.locationBlocksTexture); - RenderHelper.disableStandardItemLighting(); - GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GlStateManager.enableBlend(); - GlStateManager.disableCull(); - - if (Minecraft.isAmbientOcclusionEnabled()) - { - GlStateManager.shadeModel(GL11.GL_SMOOTH); - } - else - { - GlStateManager.shadeModel(GL11.GL_FLAT); - } - - worldrenderer.begin(7, DefaultVertexFormats.BLOCK); - worldrenderer.setTranslation(x - te.getPos().getX(), y - te.getPos().getY(), z - te.getPos().getZ()); - - this.blockRenderer.getBlockModelRenderer().renderModel(te.getWorld(), model, state, te.getPos(), worldrenderer, false); - - worldrenderer.setTranslation(0, 0, 0); - tessellator.draw(); - - RenderHelper.enableStandardItemLighting(); + @Override + protected void applyEntityAttributes() + { + super.applyEntityAttributes(); + this.getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(60); } } } diff --git a/src/test/resources/assets/forgedebugmodelanimation/armatures/block/engine_ring.json b/src/test/resources/assets/forgedebugmodelanimation/armatures/block/engine_ring.json new file mode 100644 index 000000000..0c64b31d1 --- /dev/null +++ b/src/test/resources/assets/forgedebugmodelanimation/armatures/block/engine_ring.json @@ -0,0 +1,64 @@ +{ + "joints": { + "ring": { "0": [ 1.0 ] }, + "chamber": { "1": [ 1.0 ] }, + "trunk": { "2": [ 1.0 ] } + }, + "clips": { + "default": { + "loop": false, + "joint_clips": {}, + "events": {} + }, + "moving": { + "loop": true, + "joint_clips": { + "ring": [ + { + "variable": "offset_y", + "type": "uniform", + "interpolation": "linear", + "samples": [ 0, 0.08, 0.25, 0.42, 0.5, 0.42, 0.25, 0.08 ] + }, + { + "variable": "axis_y", + "type": "uniform", + "interpolation": "nearest", + "samples": [ 1 ] + }, + { + "variable": "angle", + "type": "uniform", + "interpolation": "linear", + "samples": [ + 0, 120, 240, + 0, 120, 240, + 0, 120, 240, + 0, 120, 240 + ] + } + ], + "chamber": [ + { + "variable": "scale", + "type": "uniform", + "interpolation": "nearest", + "samples": [ 0, 1 ] + } + ], + "trunk": [ + { + "variable": "scale", + "type": "uniform", + "interpolation": "nearest", + "samples": [ 1, 0 ] + } + ] + }, + "events": { + "0.5": "boop" + } + } + } +} + diff --git a/src/test/resources/assets/forgedebugmodelanimation/asms/block/chest.json b/src/test/resources/assets/forgedebugmodelanimation/asms/block/chest.json new file mode 100644 index 000000000..566357241 --- /dev/null +++ b/src/test/resources/assets/forgedebugmodelanimation/asms/block/chest.json @@ -0,0 +1,37 @@ +{ + "parameters": { + "end_anim": [ "compose", [ "+", 1 ] , "#click_time" ], + "trigger_anim": [ "-", "#end_anim" ], + "progress": [ "-", "#click_time" ] + }, + "clips": { + "model": "forgedebugmodelloaderregistry:block/chest.b3d@main", + "closed": [ "apply", "#model", 0 ], + "opening": [ + "trigger_positive", + [ "slerp", "#closed", "#open", "#identity", "#progress" ], + "#trigger_anim", + "!transition:open" + ], + "open": [ "apply", "#model", 10 ], + "closing": [ + "trigger_positive", + [ "slerp", "#open", "#closed", "#identity", "#progress" ], + "#trigger_anim", + "!transition:closed" + ] + }, + "states": [ + "closed", + "opening", + "open", + "closing" + ], + "transitions": { + "closed": "opening", + "opening": "open", + "open": "closing", + "closing": "closed" + }, + "start_state": "closed" +} diff --git a/src/test/resources/assets/forgedebugmodelanimation/asms/block/engine.json b/src/test/resources/assets/forgedebugmodelanimation/asms/block/engine.json new file mode 100644 index 000000000..8ee1f8cdd --- /dev/null +++ b/src/test/resources/assets/forgedebugmodelanimation/asms/block/engine.json @@ -0,0 +1,26 @@ +{ + "parameters": { + "world_to_cycle": [ "/", "#cycle_length" ], + "round_cycle": [ "compose", [ "R", "#cycle_length" ] , "#click_time" ], + "end_cycle": [ "-", "#round_cycle" ] + }, + "clips": { + "default": "forgedebugmodelanimation:block/engine_ring@default", + "starting": [ "trigger_positive", "#default", "#end_cycle", "!transition:moving" ], + "moving": [ "apply", "forgedebugmodelanimation:block/engine_ring@moving", "#world_to_cycle" ], + "stopping": [ "trigger_positive", "#moving", "#end_cycle", "!transition:default" ] + }, + "states": [ + "default", + "starting", + "moving", + "stopping" + ], + "transitions": { + "default": "starting", + "starting": "moving", + "moving": "stopping", + "stopping": "default" + }, + "start_state": "moving" +} diff --git a/src/test/resources/assets/forgedebugmodelanimation/asms/block/engine2.json b/src/test/resources/assets/forgedebugmodelanimation/asms/block/engine2.json new file mode 100644 index 000000000..9c48ae046 --- /dev/null +++ b/src/test/resources/assets/forgedebugmodelanimation/asms/block/engine2.json @@ -0,0 +1,23 @@ +{ + "parameters": { + "clip_time": [ "-/", "#offset", "#cycle_length" ], + "round_cycle": [ "compose", [ "-R+", "#offset", "#cycle_length", "#offset" ] , "#click_time" ], + "end_cycle": [ "-", "#round_cycle" ] + }, + "clips": { + "default": "forgedebugmodelanimation:block/engine_ring@default", + "moving": [ "apply", "forgedebugmodelanimation:block/engine_ring@moving", "#clip_time" ], + "stopping": [ "trigger_positive", "#moving", "#end_cycle", "!transition:default" ] + }, + "states": [ + "default", + "moving", + "stopping" + ], + "transitions": { + "default": "moving", + "moving": "stopping", + "stopping": "default" + }, + "start_state": "moving" +} diff --git a/src/test/resources/assets/forgedebugmodelanimation/blockstates/test_animation_block.json b/src/test/resources/assets/forgedebugmodelanimation/blockstates/test_animation_block.json index 2b46abd6c..6fc560225 100644 --- a/src/test/resources/assets/forgedebugmodelanimation/blockstates/test_animation_block.json +++ b/src/test/resources/assets/forgedebugmodelanimation/blockstates/test_animation_block.json @@ -2,12 +2,16 @@ "forge_marker": 1, "defaults": { "textures": { - "#chest": "entity/chest/normal" - }, + "#chest": "entity/chest/normal", + "base": "blocks/stone", + "front": "blocks/log_oak", + "chamber": "blocks/redstone_block", + "trunk": "blocks/end_stone" + }/*, "model": "forgedebugmodelloaderregistry:chest.b3d", "custom": { "mesh": ["Base", "Lid"] - } + }*/ }, "variants": { "normal": [{}], @@ -16,9 +20,13 @@ "rotation": { "y": 180 }, "thirdperson": { "rotation": [ { "z": 170 }, { "y": -45 }, { "z": 20 } ], - "translation": [ 0, 0.09375, -0.171875 ], - "scale": 0.375 + "translation": [ 0, 0.09375, -0.171875 ], + "scale": 0.375 } + }, + "model": "forgedebugmodelanimation:engine", + "submodel": { + "ring": { "model": "forgedebugmodelanimation:engine_ring" } } }], "facing": { @@ -31,14 +39,16 @@ }, "static": { "true": { - "custom": { + /*"custom": { "mesh": ["Base"] - } + }*/ + "model": "forgedebugmodelanimation:engine" }, "false": { - "custom": { + /*"custom": { "mesh": ["Lid"] - } + }*/ + "model": "forgedebugmodelanimation:engine_ring" } } } diff --git a/src/test/resources/assets/forgedebugmodelanimation/models/block/engine_ring.json b/src/test/resources/assets/forgedebugmodelanimation/models/block/engine_ring.json index b6de7a275..c18f2e091 100644 --- a/src/test/resources/assets/forgedebugmodelanimation/models/block/engine_ring.json +++ b/src/test/resources/assets/forgedebugmodelanimation/models/block/engine_ring.json @@ -13,8 +13,8 @@ } }, { - "from": [ 4, 0, 4 ], - "to": [ 12, 4, 12 ], + "from": [ 4, 8, 4 ], + "to": [ 12, 12, 12 ], "faces": { "north": { "uv": [ 4, 8, 12, 12 ], "texture":"#chamber" }, "south": { "uv": [ 4, 8, 12, 12 ], "texture":"#chamber" }, diff --git a/src/test/resources/assets/forgedebugmodelloaderregistry/blockstates/CustomModelBlock.json b/src/test/resources/assets/forgedebugmodelloaderregistry/blockstates/CustomModelBlock.json index d8e22b4a6..e4e82e072 100644 --- a/src/test/resources/assets/forgedebugmodelloaderregistry/blockstates/CustomModelBlock.json +++ b/src/test/resources/assets/forgedebugmodelloaderregistry/blockstates/CustomModelBlock.json @@ -6,7 +6,6 @@ "#chest": "entity/chest/normal" }, "model": "forgedebugmodelloaderregistry:chest.b3d", - "transform": "forge:default-block", "custom": { "mesh": ["Base", "Lid"] } @@ -14,7 +13,14 @@ "variants": { "normal": [{}], "inventory": [{ - "y": 180 + "transform": { + "rotation": { "y": 180 }, + "thirdperson": { + "rotation": [ { "z": 170 }, { "y": -45 }, { "z": 20 } ], + "translation": [ 0, 0.09375, -0.171875 ], + "scale": 0.375 + } + } }], "facing": { "down": {"x": 90},