From 417bb72733e2c2a4211d662123c387c74c5071b6 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:28:31 -0500 Subject: [PATCH 01/17] Added chest for testing and refactored model package --- .gitignore | 1 + README.md | 5 +- .../hytalemodels/HytaleModelLoader.java | 2 + .../hytalemodels/HytaleModelLoaderClient.java | 10 +- .../hytalemodels/blocks/HytaleChest.java | 39 + .../entity/AnimatedBlockEntityRenderer.java | 52 + .../entity/AnimatedChestBlockEntity.java | 139 +++ .../entity/AnimatedChestRenderState.java | 23 + .../entity/AnimatedHytaleBlockEntity.java | 27 + .../BlockyModel.java | 54 +- .../BlockyModelGeometry.java | 638 +++++----- .../BlockyModelLoader.java | 146 +-- .../BlockyModelParser.java | 666 +++++----- .../BlockyModelTokenizer.java | 66 +- .../QuadBuilder.java | 1070 ++++++++--------- .../TransformCalculator.java | 200 +-- .../hytalemodels/init/BlockEntityInit.java | 23 + .../litehed/hytalemodels/init/BlockInit.java | 2 + .../litehed/hytalemodels/init/ItemInit.java | 1 + .../blockstates/chest_small.json | 19 + .../hytalemodelloader/items/chest_small.json | 6 + .../models/block/chest_small.json | 9 + .../models/item/chest_small.json | 3 + .../templates/META-INF/neoforge.mods.toml | 2 +- 24 files changed, 1777 insertions(+), 1426 deletions(-) create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java rename src/main/java/com/litehed/hytalemodels/{modelstuff => blockymodel}/BlockyModel.java (91%) rename src/main/java/com/litehed/hytalemodels/{modelstuff => blockymodel}/BlockyModelGeometry.java (96%) rename src/main/java/com/litehed/hytalemodels/{modelstuff => blockymodel}/BlockyModelLoader.java (96%) rename src/main/java/com/litehed/hytalemodels/{modelstuff => blockymodel}/BlockyModelParser.java (96%) rename src/main/java/com/litehed/hytalemodels/{modelstuff => blockymodel}/BlockyModelTokenizer.java (92%) rename src/main/java/com/litehed/hytalemodels/{modelstuff => blockymodel}/QuadBuilder.java (97%) rename src/main/java/com/litehed/hytalemodels/{modelstuff => blockymodel}/TransformCalculator.java (96%) create mode 100644 src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java create mode 100644 src/main/resources/assets/hytalemodelloader/blockstates/chest_small.json create mode 100644 src/main/resources/assets/hytalemodelloader/items/chest_small.json create mode 100644 src/main/resources/assets/hytalemodelloader/models/block/chest_small.json create mode 100644 src/main/resources/assets/hytalemodelloader/models/item/chest_small.json diff --git a/.gitignore b/.gitignore index f3ca93a..b784848 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ run-data repo *.blockymodel *.png +*.blockyanim diff --git a/README.md b/README.md index 2fe327d..2321bcf 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,10 @@ Models are defined using `.blockymodel` files (custom binary/text format) and re ### v1.1.0 - [x] Check item and block scaling/translating using model json -- [ ] Make bounding boxes fit models -- [ ] Fix and clean up code - -### v1.2.0 - [ ] Add parser for animation support `.blockyanim` - [ ] Load animations in for blocks and items - [ ] Create animation system to actually play and time these animations +- [ ] Fix and clean up code ### v2.0.0 - [ ] Implement entity model loading diff --git a/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java b/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java index 5ece8ee..e472c89 100644 --- a/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java +++ b/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java @@ -1,5 +1,6 @@ package com.litehed.hytalemodels; +import com.litehed.hytalemodels.init.BlockEntityInit; import com.litehed.hytalemodels.init.BlockInit; import com.litehed.hytalemodels.init.ItemInit; import com.mojang.logging.LogUtils; @@ -18,6 +19,7 @@ public class HytaleModelLoader { public HytaleModelLoader(IEventBus modEventBus, ModContainer modContainer) { // Remember to comment these out for version releases BlockInit.BLOCKS.register(modEventBus); + BlockEntityInit.BLOCK_ENTITIES.register(modEventBus); ItemInit.ITEMS.register(modEventBus); // Register our mod's ModConfigSpec so that FML can create and load the config file for us diff --git a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java index 6648d2f..598f345 100644 --- a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java +++ b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java @@ -1,12 +1,15 @@ package com.litehed.hytalemodels; -import com.litehed.hytalemodels.modelstuff.BlockyModelLoader; +import com.litehed.hytalemodels.blocks.entity.AnimatedBlockEntityRenderer; +import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; +import com.litehed.hytalemodels.init.BlockEntityInit; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; import net.neoforged.neoforge.client.event.ModelEvent; import net.neoforged.neoforge.client.gui.ConfigurationScreen; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; @@ -32,4 +35,9 @@ public static void onRegisterReloadListeners(AddClientReloadListenersEvent event event.addListener(BlockyModelLoader.ID, BlockyModelLoader.INSTANCE); event.addDependency(BlockyModelLoader.ID, VanillaClientListeners.MODELS); } + + @SubscribeEvent + public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) { + event.registerBlockEntityRenderer(BlockEntityInit.CHEST_TEST_ENT.get(), AnimatedBlockEntityRenderer::new); + } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java new file mode 100644 index 0000000..178e8e4 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java @@ -0,0 +1,39 @@ +package com.litehed.hytalemodels.blocks; + +import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blocks.entity.AnimatedChestBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jspecify.annotations.Nullable; + +public class HytaleChest extends HytaleTestBlock implements EntityBlock { + public HytaleChest(Properties properties) { + super(properties); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new AnimatedChestBlockEntity(blockPos, blockState); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof AnimatedChestBlockEntity chest) { + if (chest.isOpen()) { + chest.closeChest(); + } else { + chest.openChest(); + } + } + } + return InteractionResult.SUCCESS; + } +} diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java new file mode 100644 index 0000000..df30d7e --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java @@ -0,0 +1,52 @@ +package com.litehed.hytalemodels.blocks.entity; + +import com.litehed.hytalemodels.HytaleModelLoader; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.model.geom.EntityModelSet; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.resources.model.MaterialSet; +import net.minecraft.resources.Identifier; +import net.minecraft.world.phys.Vec3; + +public class AnimatedBlockEntityRenderer implements BlockEntityRenderer { + + private final MaterialSet materials; + private final EntityModelSet entityModelSet; + + private static final Identifier CHEST_OPEN_ANIM = + Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "animations/chest_small/chest_open.blockyanim"); + + public AnimatedBlockEntityRenderer(BlockEntityRendererProvider.Context context) { + this.materials = context.materials(); + this.entityModelSet = context.entityModelSet(); + } + + @Override + public AnimatedChestRenderState createRenderState() { + return new AnimatedChestRenderState(); + } + + @Override + public void extractRenderState(AnimatedChestBlockEntity blockEntity, AnimatedChestRenderState renderState, float partialTick, Vec3 cameraPosition, ModelFeatureRenderer.CrumblingOverlay breakProgress) { + renderState.modelName = blockEntity.getModelName(); + renderState.isOpen = blockEntity.isOpen(); + renderState.animationTick = blockEntity.getAnimationTick(); + renderState.partialTick = partialTick; + + if (blockEntity.getLevel() != null) { + renderState.ageInTicks = blockEntity.getLevel().getGameTime() + partialTick; + } + } + + + @Override + public void submit(AnimatedChestRenderState renderState, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) { + if (renderState.modelName == null) { + return; + } + } +} diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java new file mode 100644 index 0000000..8315941 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java @@ -0,0 +1,139 @@ +package com.litehed.hytalemodels.blocks.entity; + +import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.init.BlockEntityInit; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.AnimationState; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +public class AnimatedChestBlockEntity extends AnimatedHytaleBlockEntity { + + // Track whether chest is open (for server-side state) + private boolean isOpen = false; + private final AnimationState openAnimationState = new AnimationState(); + private int animationTick = 0; + + // Animation name for opening + private static final String OPEN_ANIMATION = "chest_open"; + + // Animation name for closing (if we have one) + private static final String CLOSE_ANIMATION = "chest_close"; + + public AnimatedChestBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityInit.CHEST_TEST_ENT.get(), pos, state, "chest_small"); + } + + public void openChest() { + if (!this.isOpen) { + this.isOpen = true; + + // Start the animation on client side + if (this.level != null && this.level.isClientSide()) { + long gameTime = this.level.getGameTime(); + this.openAnimationState.start((int) gameTime); + HytaleModelLoader.LOGGER.info("Client: Starting open animation at game time: {}", gameTime); + } + + HytaleModelLoader.LOGGER.info("Chest opened at position: {}", this.worldPosition); + this.setChanged(); + + // Sync to clients + if (this.level != null && !this.level.isClientSide()) { + this.level.blockEntityChanged(this.getBlockPos()); + HytaleModelLoader.LOGGER.info("Server: Syncing open animation to clients"); + } + } + } + + public void closeChest() { + if (this.isOpen) { + this.isOpen = false; + + // Stop the animation on client side + if (this.level != null && this.level.isClientSide()) { + this.openAnimationState.stop(); + HytaleModelLoader.LOGGER.info("Client: Stopping open animation"); + } + + HytaleModelLoader.LOGGER.info("Chest closed at position: {}", this.worldPosition); + this.setChanged(); + + // Sync to clients + if (this.level != null && !this.level.isClientSide()) { + this.level.blockEntityChanged(this.getBlockPos()); + HytaleModelLoader.LOGGER.info("Server: Syncing close animation to clients"); + } + } + } + + + public boolean isOpen() { + return isOpen; + } + + public AnimationState getOpenAnimationState() { + return openAnimationState; + } + + public int getAnimationTick() { + return animationTick; + } + + public static void tick(Level level, BlockPos pos, BlockState state, AnimatedChestBlockEntity blockEntity) { + if (level.isClientSide()) { + // Update animation tick counter + if (blockEntity.isOpen) { + blockEntity.animationTick++; + } else { + // Optionally decay animation tick when closed + if (blockEntity.animationTick > 0) { + blockEntity.animationTick--; + } + } + } + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + input.read("ChestData", CompoundTag.CODEC).ifPresent(chestTag -> { + if (chestTag.contains("IsOpen")) { + boolean wasOpen = this.isOpen; + this.isOpen = chestTag.getBoolean("IsOpen").get(); + + // If state changed, update animation on client + if (this.level != null && this.level.isClientSide() && wasOpen != this.isOpen) { + if (this.isOpen) { + this.openAnimationState.start((int) this.level.getGameTime()); + } else { + this.openAnimationState.stop(); + } + } + } + if (chestTag.contains("AnimationTick")) { + this.animationTick = chestTag.getInt("AnimationTick").get(); + } + }); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + CompoundTag chestTag = new CompoundTag(); + chestTag.putBoolean("isOpen", isOpen); + chestTag.putInt("AnimationTick", animationTick); + output.store("ChestData", CompoundTag.CODEC, chestTag); + } + + @Override + public String toString() { + return "AnimatedChestBlockEntity{" + + "pos=" + this.worldPosition + + ", isOpen=" + isOpen + + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java new file mode 100644 index 0000000..a21b3ba --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java @@ -0,0 +1,23 @@ +package com.litehed.hytalemodels.blocks.entity; + +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; + +public class AnimatedChestRenderState extends BlockEntityRenderState { + + // Animation data + public String modelName; + public boolean isOpen; + public int animationTick; + + // Timing for animation + public float ageInTicks; + public float partialTick; + + public AnimatedChestRenderState() { + this.modelName = null; + this.isOpen = false; + this.animationTick = 0; + this.ageInTicks = 0; + this.partialTick = 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java new file mode 100644 index 0000000..0a41cc2 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java @@ -0,0 +1,27 @@ +package com.litehed.hytalemodels.blocks.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +public class AnimatedHytaleBlockEntity extends BlockEntity { + private final String modelName; + + public AnimatedHytaleBlockEntity(BlockEntityType type, BlockPos pos, BlockState state, String modelName) { + super(type, pos, state); + this.modelName = modelName; + } + + public String getModelName() { + return modelName; + } + + @Override + public String toString() { + return "AnimatedHytaleBlockEntity{" + + "modelName='" + modelName + '\'' + + ", pos=" + this.worldPosition + + "}"; + } +} diff --git a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModel.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModel.java similarity index 91% rename from src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModel.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModel.java index 024b868..72d9c06 100644 --- a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModel.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModel.java @@ -1,27 +1,27 @@ -package com.litehed.hytalemodels.modelstuff; - -import net.minecraft.client.resources.model.UnbakedGeometry; -import net.neoforged.neoforge.client.model.AbstractUnbakedModel; -import net.neoforged.neoforge.client.model.StandardModelParameters; -import org.jspecify.annotations.Nullable; - -public class BlockyModel extends AbstractUnbakedModel { - - private final BlockyModelGeometry geometry; - - /** - * Constructor for BlockyModel - * - * @param parameters the standard model parameters - * @param geometry the blocky model geometry - */ - protected BlockyModel(StandardModelParameters parameters, BlockyModelGeometry geometry) { - super(parameters); - this.geometry = geometry; - } - - @Override - public @Nullable UnbakedGeometry geometry() { - return geometry; - } -} +package com.litehed.hytalemodels.blockymodel; + +import net.minecraft.client.resources.model.UnbakedGeometry; +import net.neoforged.neoforge.client.model.AbstractUnbakedModel; +import net.neoforged.neoforge.client.model.StandardModelParameters; +import org.jspecify.annotations.Nullable; + +public class BlockyModel extends AbstractUnbakedModel { + + private final BlockyModelGeometry geometry; + + /** + * Constructor for BlockyModel + * + * @param parameters the standard model parameters + * @param geometry the blocky model geometry + */ + protected BlockyModel(StandardModelParameters parameters, BlockyModelGeometry geometry) { + super(parameters); + this.geometry = geometry; + } + + @Override + public @Nullable UnbakedGeometry geometry() { + return geometry; + } +} diff --git a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelGeometry.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java similarity index 96% rename from src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelGeometry.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java index d12095e..1a430f3 100644 --- a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelGeometry.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java @@ -1,319 +1,319 @@ -package com.litehed.hytalemodels.modelstuff; - -import com.google.common.collect.Lists; -import com.mojang.math.Transformation; -import net.minecraft.client.renderer.block.model.BakedQuad; -import net.minecraft.client.renderer.block.model.TextureSlots; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.client.resources.model.ModelBaker; -import net.minecraft.client.resources.model.ModelDebugName; -import net.minecraft.client.resources.model.ModelState; -import net.minecraft.client.resources.model.QuadCollection; -import net.minecraft.core.Direction; -import net.minecraft.resources.Identifier; -import net.minecraft.util.context.ContextMap; -import net.neoforged.neoforge.client.model.ExtendedUnbakedGeometry; -import net.neoforged.neoforge.client.model.NeoForgeModelProperties; -import org.apache.commons.lang3.tuple.Pair; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; - -import static com.litehed.hytalemodels.modelstuff.QuadBuilder.DEBUG_BORDERS; - -public class BlockyModelGeometry implements ExtendedUnbakedGeometry { - - private final List nodes; - private final Identifier modelLocation; - - public BlockyModelGeometry(Settings settings) { - this.nodes = Lists.newArrayList(); - this.modelLocation = settings.modelLocation(); - } - - public BlockyModelGeometry(List nodes, Identifier modelLocation) { - this.nodes = nodes; - this.modelLocation = modelLocation; - } - - /** - * Parses a BlockyModelGeometry from the given tokenizer and settings - * - * @param tokenizer the tokenizer to use for parsing - * @param settings the settings to use for parsing - * @return a new BlockyModelGeometry instance - */ - public static BlockyModelGeometry parse(BlockyModelTokenizer tokenizer, Settings settings) { - List nodes = BlockyModelParser.parseNodes(tokenizer.getRoot()); - return new BlockyModelGeometry(nodes, settings.modelLocation()); - } - - - /** - * Bakes the model into a QuadCollection for rendering - * - * @param textureSlots the texture slots for this model - * @param modelBaker the model baker instance - * @param modelState the model state (transformations) - * @param modelDebugName the debug name for this model - * @param contextMap the context map for additional properties - * @return the baked QuadCollection - */ - @Override - public QuadCollection bake(TextureSlots textureSlots, ModelBaker modelBaker, ModelState modelState, ModelDebugName modelDebugName, ContextMap contextMap) { - QuadCollection.Builder builder = new QuadCollection.Builder(); - Transformation rootTransform = contextMap.getOrDefault( - NeoForgeModelProperties.TRANSFORM, - Transformation.identity() - ); - - Transformation finalTransform = rootTransform.isIdentity() - ? modelState.transformation() - : modelState.transformation().compose(rootTransform); - - for (BlockyNode node : nodes) { - if (node.hasShape()) { - bakeNode(builder, node, textureSlots, modelBaker, finalTransform, modelDebugName); - } - } - - return builder.build(); - } - - /** - * Bakes a single BlockyNode into the QuadCollection builder - * - * @param builder the QuadCollection.Builder to add quads to - * @param node the BlockyNode to bake - * @param textureSlots the texture slots for this model - * @param modelBaker the model baker instance - * @param modelTransform the combined transformation for this model - * @param modelDebugName the debug name for this model - */ - private void bakeNode(QuadCollection.Builder builder, BlockyNode node, - TextureSlots textureSlots, ModelBaker modelBaker, - Transformation modelTransform, ModelDebugName modelDebugName) { - - BlockyShape shape = node.getShape(); - TextureAtlasSprite sprite = modelBaker.sprites() - .resolveSlot(textureSlots, "texture", modelDebugName); - - Vector3f worldPos = TransformCalculator.calculateWorldPosition(node); - Quaternionf worldRot = TransformCalculator.calculateWorldOrientation(node); - Transformation nodeTransform = TransformCalculator.createNodeTransform( - worldPos, shape.getOffset(), worldRot - ); - - // Translate after rotation - Transformation centerTranslate = new Transformation( - new Vector3f(0.5f, 0.5f, 0.5f), null, null, null - ); - - Transformation finalTransform; - if (modelTransform.isIdentity()) { - finalTransform = centerTranslate.compose(nodeTransform); - } else { - finalTransform = centerTranslate.compose(modelTransform).compose(nodeTransform); - } - - // Bounds - Vector3f halfSizes = TransformCalculator.calculateHalfSizes(shape.getSize()); - Vector3f min = new Vector3f(-halfSizes.x, -halfSizes.y, -halfSizes.z); - Vector3f max = new Vector3f(halfSizes.x, halfSizes.y, halfSizes.z); - - // Generate quads for each face - for (Direction direction : Direction.values()) { - if (!shape.hasTextureLayout(direction)) { - continue; - } - - FaceTextureLayout texLayout = shape.getTextureLayout(direction); - Pair quad = QuadBuilder.createQuad( - direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform - ); - - addQuadToBuilder(builder, quad); - - // Debug quads - if (DEBUG_BORDERS) { - List borderQuads = QuadBuilder.createBorderQuads( - direction, min, max, sprite, finalTransform - ); - for (BakedQuad borderQuad : borderQuads) { - builder.addUnculledFace(borderQuad); - } - } - - // Backface if double-sided - if (shape.isDoubleSided()) { - Pair backQuad = QuadBuilder.createReversedQuad( - direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform); - builder.addUnculledFace(backQuad.getLeft()); - } - } - } - - /** - * Adds a quad to the QuadCollection builder, handling culling - * - * @param builder the QuadCollection.Builder to add to - * @param quad the quad and its culling direction - */ - private void addQuadToBuilder(QuadCollection.Builder builder, Pair quad) { - if (quad.getRight() == null) { - builder.addUnculledFace(quad.getLeft()); - } else { - builder.addCulledFace(quad.getRight(), quad.getLeft()); - } - } - - public record Settings(Identifier modelLocation) { - public Identifier modelLocation() { - return this.modelLocation; - } - } - - public static final class BlockyNode { - private final String id; - private final String name; - private final Vector3f position; - private final Quaternionf orientation; - private final BlockyShape shape; - private final BlockyNode parent; - - public BlockyNode(String id, String name, Vector3f position, Quaternionf orientation, - BlockyShape shape, BlockyNode parent) { - this.id = id; - this.name = name; - // Defensive copies for mutable objects - this.position = new Vector3f(position); - this.orientation = new Quaternionf(orientation); - this.shape = shape; - this.parent = parent; - } - - // Getters only - no setters (immutable) - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public Vector3f getPosition() { - return new Vector3f(position); - } - - public Quaternionf getOrientation() { - return new Quaternionf(orientation); - } - - public BlockyShape getShape() { - return shape; - } - - public BlockyNode getParent() { - return parent; - } - - public boolean hasShape() { - return shape != null && shape.isVisible(); - } - - @Override - public String toString() { - return "BlockyNode{id='" + id + "', name='" + name + "'}"; - } - } - - public static final class BlockyShape { - private final boolean visible; - private final boolean doubleSided; - private final Vector3f offset; - private final Vector3f stretch; - private final Vector3f originalSize; - private final Vector3f size; - private final Map textureLayout; - - public BlockyShape(boolean visible, boolean doubleSided, Vector3f offset, - Vector3f stretch, Vector3f size, - Map textureLayout) { - this.visible = visible; - this.doubleSided = doubleSided; - this.offset = new Vector3f(offset); - - this.stretch = new Vector3f(stretch); - this.originalSize = new Vector3f(size); - this.size = new Vector3f( - size.x * Math.abs(stretch.x), - size.y * Math.abs(stretch.y), - size.z * Math.abs(stretch.z) - ); - - // Immutable map - this.textureLayout = Collections.unmodifiableMap( - new EnumMap<>(textureLayout) - ); - } - - public static BlockyShape invisible() { - return new BlockyShape( - false, false, - new Vector3f(0, 0, 0), - new Vector3f(1, 1, 1), - new Vector3f(16, 16, 16), - new EnumMap<>(Direction.class) - ); - } - - // Getters - public boolean isVisible() { - return visible; - } - - public boolean isDoubleSided() { - return doubleSided; - } - - public Vector3f getOffset() { - return new Vector3f(offset); - } - - public Vector3f getStretch() { - return stretch; - } - - public Vector3f getOriginalSize() { - return originalSize; - } - - public Vector3f getSize() { - return new Vector3f(size); - } - - public FaceTextureLayout getTextureLayout(Direction face) { - return textureLayout.get(face); - } - - public boolean hasTextureLayout(Direction face) { - return textureLayout.containsKey(face); - } - } - - public record FaceTextureLayout(int offsetX, int offsetY, boolean mirrorX, boolean mirrorY, int angle) { - public FaceTextureLayout { - if (angle != 0 && angle != 90 && angle != 180 && angle != 270) { - throw new IllegalArgumentException("Angle must be 0, 90, 180, or 270, got: " + angle); - } - } - - public static FaceTextureLayout defaultLayout() { - return new FaceTextureLayout(0, 0, false, false, 0); - } - } -} +package com.litehed.hytalemodels.blockymodel; + +import com.google.common.collect.Lists; +import com.mojang.math.Transformation; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.block.model.TextureSlots; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.ModelBaker; +import net.minecraft.client.resources.model.ModelDebugName; +import net.minecraft.client.resources.model.ModelState; +import net.minecraft.client.resources.model.QuadCollection; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.context.ContextMap; +import net.neoforged.neoforge.client.model.ExtendedUnbakedGeometry; +import net.neoforged.neoforge.client.model.NeoForgeModelProperties; +import org.apache.commons.lang3.tuple.Pair; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import static com.litehed.hytalemodels.blockymodel.QuadBuilder.DEBUG_BORDERS; + +public class BlockyModelGeometry implements ExtendedUnbakedGeometry { + + private final List nodes; + private final Identifier modelLocation; + + public BlockyModelGeometry(Settings settings) { + this.nodes = Lists.newArrayList(); + this.modelLocation = settings.modelLocation(); + } + + public BlockyModelGeometry(List nodes, Identifier modelLocation) { + this.nodes = nodes; + this.modelLocation = modelLocation; + } + + /** + * Parses a BlockyModelGeometry from the given tokenizer and settings + * + * @param tokenizer the tokenizer to use for parsing + * @param settings the settings to use for parsing + * @return a new BlockyModelGeometry instance + */ + public static BlockyModelGeometry parse(BlockyModelTokenizer tokenizer, Settings settings) { + List nodes = BlockyModelParser.parseNodes(tokenizer.getRoot()); + return new BlockyModelGeometry(nodes, settings.modelLocation()); + } + + + /** + * Bakes the model into a QuadCollection for rendering + * + * @param textureSlots the texture slots for this model + * @param modelBaker the model baker instance + * @param modelState the model state (transformations) + * @param modelDebugName the debug name for this model + * @param contextMap the context map for additional properties + * @return the baked QuadCollection + */ + @Override + public QuadCollection bake(TextureSlots textureSlots, ModelBaker modelBaker, ModelState modelState, ModelDebugName modelDebugName, ContextMap contextMap) { + QuadCollection.Builder builder = new QuadCollection.Builder(); + Transformation rootTransform = contextMap.getOrDefault( + NeoForgeModelProperties.TRANSFORM, + Transformation.identity() + ); + + Transformation finalTransform = rootTransform.isIdentity() + ? modelState.transformation() + : modelState.transformation().compose(rootTransform); + + for (BlockyNode node : nodes) { + if (node.hasShape()) { + bakeNode(builder, node, textureSlots, modelBaker, finalTransform, modelDebugName); + } + } + + return builder.build(); + } + + /** + * Bakes a single BlockyNode into the QuadCollection builder + * + * @param builder the QuadCollection.Builder to add quads to + * @param node the BlockyNode to bake + * @param textureSlots the texture slots for this model + * @param modelBaker the model baker instance + * @param modelTransform the combined transformation for this model + * @param modelDebugName the debug name for this model + */ + private void bakeNode(QuadCollection.Builder builder, BlockyNode node, + TextureSlots textureSlots, ModelBaker modelBaker, + Transformation modelTransform, ModelDebugName modelDebugName) { + + BlockyShape shape = node.getShape(); + TextureAtlasSprite sprite = modelBaker.sprites() + .resolveSlot(textureSlots, "texture", modelDebugName); + + Vector3f worldPos = TransformCalculator.calculateWorldPosition(node); + Quaternionf worldRot = TransformCalculator.calculateWorldOrientation(node); + Transformation nodeTransform = TransformCalculator.createNodeTransform( + worldPos, shape.getOffset(), worldRot + ); + + // Translate after rotation + Transformation centerTranslate = new Transformation( + new Vector3f(0.5f, 0.5f, 0.5f), null, null, null + ); + + Transformation finalTransform; + if (modelTransform.isIdentity()) { + finalTransform = centerTranslate.compose(nodeTransform); + } else { + finalTransform = centerTranslate.compose(modelTransform).compose(nodeTransform); + } + + // Bounds + Vector3f halfSizes = TransformCalculator.calculateHalfSizes(shape.getSize()); + Vector3f min = new Vector3f(-halfSizes.x, -halfSizes.y, -halfSizes.z); + Vector3f max = new Vector3f(halfSizes.x, halfSizes.y, halfSizes.z); + + // Generate quads for each face + for (Direction direction : Direction.values()) { + if (!shape.hasTextureLayout(direction)) { + continue; + } + + FaceTextureLayout texLayout = shape.getTextureLayout(direction); + Pair quad = QuadBuilder.createQuad( + direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform + ); + + addQuadToBuilder(builder, quad); + + // Debug quads + if (DEBUG_BORDERS) { + List borderQuads = QuadBuilder.createBorderQuads( + direction, min, max, sprite, finalTransform + ); + for (BakedQuad borderQuad : borderQuads) { + builder.addUnculledFace(borderQuad); + } + } + + // Backface if double-sided + if (shape.isDoubleSided()) { + Pair backQuad = QuadBuilder.createReversedQuad( + direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform); + builder.addUnculledFace(backQuad.getLeft()); + } + } + } + + /** + * Adds a quad to the QuadCollection builder, handling culling + * + * @param builder the QuadCollection.Builder to add to + * @param quad the quad and its culling direction + */ + private void addQuadToBuilder(QuadCollection.Builder builder, Pair quad) { + if (quad.getRight() == null) { + builder.addUnculledFace(quad.getLeft()); + } else { + builder.addCulledFace(quad.getRight(), quad.getLeft()); + } + } + + public record Settings(Identifier modelLocation) { + public Identifier modelLocation() { + return this.modelLocation; + } + } + + public static final class BlockyNode { + private final String id; + private final String name; + private final Vector3f position; + private final Quaternionf orientation; + private final BlockyShape shape; + private final BlockyNode parent; + + public BlockyNode(String id, String name, Vector3f position, Quaternionf orientation, + BlockyShape shape, BlockyNode parent) { + this.id = id; + this.name = name; + // Defensive copies for mutable objects + this.position = new Vector3f(position); + this.orientation = new Quaternionf(orientation); + this.shape = shape; + this.parent = parent; + } + + // Getters only - no setters (immutable) + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public Vector3f getPosition() { + return new Vector3f(position); + } + + public Quaternionf getOrientation() { + return new Quaternionf(orientation); + } + + public BlockyShape getShape() { + return shape; + } + + public BlockyNode getParent() { + return parent; + } + + public boolean hasShape() { + return shape != null && shape.isVisible(); + } + + @Override + public String toString() { + return "BlockyNode{id='" + id + "', name='" + name + "'}"; + } + } + + public static final class BlockyShape { + private final boolean visible; + private final boolean doubleSided; + private final Vector3f offset; + private final Vector3f stretch; + private final Vector3f originalSize; + private final Vector3f size; + private final Map textureLayout; + + public BlockyShape(boolean visible, boolean doubleSided, Vector3f offset, + Vector3f stretch, Vector3f size, + Map textureLayout) { + this.visible = visible; + this.doubleSided = doubleSided; + this.offset = new Vector3f(offset); + + this.stretch = new Vector3f(stretch); + this.originalSize = new Vector3f(size); + this.size = new Vector3f( + size.x * Math.abs(stretch.x), + size.y * Math.abs(stretch.y), + size.z * Math.abs(stretch.z) + ); + + // Immutable map + this.textureLayout = Collections.unmodifiableMap( + new EnumMap<>(textureLayout) + ); + } + + public static BlockyShape invisible() { + return new BlockyShape( + false, false, + new Vector3f(0, 0, 0), + new Vector3f(1, 1, 1), + new Vector3f(16, 16, 16), + new EnumMap<>(Direction.class) + ); + } + + // Getters + public boolean isVisible() { + return visible; + } + + public boolean isDoubleSided() { + return doubleSided; + } + + public Vector3f getOffset() { + return new Vector3f(offset); + } + + public Vector3f getStretch() { + return stretch; + } + + public Vector3f getOriginalSize() { + return originalSize; + } + + public Vector3f getSize() { + return new Vector3f(size); + } + + public FaceTextureLayout getTextureLayout(Direction face) { + return textureLayout.get(face); + } + + public boolean hasTextureLayout(Direction face) { + return textureLayout.containsKey(face); + } + } + + public record FaceTextureLayout(int offsetX, int offsetY, boolean mirrorX, boolean mirrorY, int angle) { + public FaceTextureLayout { + if (angle != 0 && angle != 90 && angle != 180 && angle != 270) { + throw new IllegalArgumentException("Angle must be 0, 90, 180, or 270, got: " + angle); + } + } + + public static FaceTextureLayout defaultLayout() { + return new FaceTextureLayout(0, 0, false, false, 0); + } + } +} diff --git a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelLoader.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelLoader.java similarity index 96% rename from src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelLoader.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelLoader.java index 564e7fe..4c1e4d4 100644 --- a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelLoader.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelLoader.java @@ -1,73 +1,73 @@ -package com.litehed.hytalemodels.modelstuff; - -import com.google.common.collect.Maps; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.litehed.hytalemodels.HytaleModelLoader; -import net.minecraft.client.Minecraft; -import net.minecraft.resources.Identifier; -import net.minecraft.server.packs.resources.Resource; -import net.minecraft.server.packs.resources.ResourceManager; -import net.minecraft.server.packs.resources.ResourceManagerReloadListener; -import net.neoforged.neoforge.client.model.StandardModelParameters; -import net.neoforged.neoforge.client.model.UnbakedModelLoader; - -import java.io.FileNotFoundException; -import java.util.Map; - -public class BlockyModelLoader implements UnbakedModelLoader, ResourceManagerReloadListener { - - public static final BlockyModelLoader INSTANCE = new BlockyModelLoader(); - public static final Identifier ID = Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "blockymodel_loader"); - private final Map geometryCache = Maps.newConcurrentMap(); - - // Important for cleaning up during resource reloads - @Override - public void onResourceManagerReload(ResourceManager resourceManager) { - geometryCache.clear(); - } - - /** - * Reads a BlockyModel file and returns a BlockyModel instance - * - * @param jsonObject the JsonObject representing the BlockyModel - * @param jsonDeserializationContext the context for deserializing JSON - * @return a BlockyModel instance - * @throws JsonParseException if the model is malformed or missing required fields - */ - @Override - public BlockyModel read(JsonObject jsonObject, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { - HytaleModelLoader.LOGGER.debug("[BlockyModelLoader] Reading model from file: {}", jsonObject); - - if (!jsonObject.has("model")) { - throw new JsonParseException("BlockyModel Loader requires a 'model' key that points to a valid BlockyModel file."); - } - - String modelLocation = jsonObject.get("model").getAsString(); - StandardModelParameters parameters = StandardModelParameters.parse(jsonObject, jsonDeserializationContext); - - var geometry = loadGeometry(new BlockyModelGeometry.Settings(Identifier.parse(modelLocation))); - return new BlockyModel(parameters, geometry); - } - - /** - * Loads and parses a BlockyModel file from the given location - * - * @param settings the settings containing the model location - * @return the parsed BlockyModelGeometry - */ - public BlockyModelGeometry loadGeometry(BlockyModelGeometry.Settings settings) { - return geometryCache.computeIfAbsent(settings, (data) -> { - ResourceManager manager = Minecraft.getInstance().getResourceManager(); - Resource resource = manager.getResource(settings.modelLocation()).orElseThrow(); - try (BlockyModelTokenizer tokenizer = new BlockyModelTokenizer(resource.open())) { - return BlockyModelGeometry.parse(tokenizer, data); - } catch (FileNotFoundException e) { - throw new RuntimeException("Could not find BlockyModel file", e); - } catch (Exception e) { - throw new RuntimeException("Could not read BlockyModel file", e); - } - }); - } -} +package com.litehed.hytalemodels.blockymodel; + +import com.google.common.collect.Maps; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.litehed.hytalemodels.HytaleModelLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import net.neoforged.neoforge.client.model.StandardModelParameters; +import net.neoforged.neoforge.client.model.UnbakedModelLoader; + +import java.io.FileNotFoundException; +import java.util.Map; + +public class BlockyModelLoader implements UnbakedModelLoader, ResourceManagerReloadListener { + + public static final BlockyModelLoader INSTANCE = new BlockyModelLoader(); + public static final Identifier ID = Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "blockymodel_loader"); + private final Map geometryCache = Maps.newConcurrentMap(); + + // Important for cleaning up during resource reloads + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + geometryCache.clear(); + } + + /** + * Reads a BlockyModel file and returns a BlockyModel instance + * + * @param jsonObject the JsonObject representing the BlockyModel + * @param jsonDeserializationContext the context for deserializing JSON + * @return a BlockyModel instance + * @throws JsonParseException if the model is malformed or missing required fields + */ + @Override + public BlockyModel read(JsonObject jsonObject, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + HytaleModelLoader.LOGGER.debug("[BlockyModelLoader] Reading model from file: {}", jsonObject); + + if (!jsonObject.has("model")) { + throw new JsonParseException("BlockyModel Loader requires a 'model' key that points to a valid BlockyModel file."); + } + + String modelLocation = jsonObject.get("model").getAsString(); + StandardModelParameters parameters = StandardModelParameters.parse(jsonObject, jsonDeserializationContext); + + var geometry = loadGeometry(new BlockyModelGeometry.Settings(Identifier.parse(modelLocation))); + return new BlockyModel(parameters, geometry); + } + + /** + * Loads and parses a BlockyModel file from the given location + * + * @param settings the settings containing the model location + * @return the parsed BlockyModelGeometry + */ + public BlockyModelGeometry loadGeometry(BlockyModelGeometry.Settings settings) { + return geometryCache.computeIfAbsent(settings, (data) -> { + ResourceManager manager = Minecraft.getInstance().getResourceManager(); + Resource resource = manager.getResource(settings.modelLocation()).orElseThrow(); + try (BlockyModelTokenizer tokenizer = new BlockyModelTokenizer(resource.open())) { + return BlockyModelGeometry.parse(tokenizer, data); + } catch (FileNotFoundException e) { + throw new RuntimeException("Could not find BlockyModel file", e); + } catch (Exception e) { + throw new RuntimeException("Could not read BlockyModel file", e); + } + }); + } +} diff --git a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelParser.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java similarity index 96% rename from src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelParser.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java index 5c293b3..2bc749f 100644 --- a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelParser.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java @@ -1,334 +1,334 @@ -package com.litehed.hytalemodels.modelstuff; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.litehed.hytalemodels.HytaleModelLoader; -import net.minecraft.core.Direction; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; - -public class BlockyModelParser { - - private static final float DEFAULT_SIZE = 16.0f; - private static final float DEFAULT_STRETCH = 1.0f; - private static final Vector3f DEFAULT_POSITION = new Vector3f(0, 0, 0); - private static final Quaternionf DEFAULT_ORIENTATION = new Quaternionf(); - - /** - * Parse the nodes from the root JsonObject - * - * @param root the root JsonObject of the BlockyModel - * @return a list of parsed BlockyNodes - * @throws JsonParseException if required fields are missing or invalid - */ - public static List parseNodes(JsonObject root) throws JsonParseException { - if (!root.has("nodes")) { - throw new JsonParseException("BlockyModel file must contain a 'nodes' array"); - } - - List nodes = new ArrayList<>(); - JsonArray nodesArray = root.getAsJsonArray("nodes"); - - for (JsonElement nodeElement : nodesArray) { - parseNode(nodeElement.getAsJsonObject(), null, nodes); - } - - return nodes; - } - - /** - * Parse a single node and its children recursively - * - * @param nodeObj the JsonObject representing the node - * @param parent the parent BlockyNode, or null if root - * @param allNodes the list to add parsed nodes to - * @throws JsonParseException if required fields are missing or invalid - */ - private static void parseNode(JsonObject nodeObj, BlockyModelGeometry.BlockyNode parent, List allNodes) { - validateRequiredFields(nodeObj); - - BlockyModelGeometry.BlockyNode node = new BlockyModelGeometry.BlockyNode( - parseString(nodeObj, "id"), - parseString(nodeObj, "name"), - parsePosition(nodeObj), - parseOrientation(nodeObj), - parseShape(nodeObj), - parent - ); - - allNodes.add(node); - - // Parse children recursively - if (nodeObj.has("children")) { - JsonArray children = nodeObj.getAsJsonArray("children"); - for (JsonElement childElement : children) { - parseNode(childElement.getAsJsonObject(), node, allNodes); - } - } - } - - /** - * Validate that required fields are present in the node JsonObject - * - * @param nodeObj the JsonObject representing the node - * @throws JsonParseException if required fields are missing - */ - private static void validateRequiredFields(JsonObject nodeObj) throws JsonParseException { - if (!nodeObj.has("name")) { - throw new JsonParseException("Node missing required field: 'name'"); - } - if (!nodeObj.has("id")) { - throw new JsonParseException("Node missing required field: 'id'"); - } - } - - /** - * Parse a string field from a JsonObject - * - * @param obj the JsonObject - * @param key the key of the string field - * @return the string value - */ - private static String parseString(JsonObject obj, String key) { - return obj.get(key).getAsString(); - } - - /** - * Parse the position vector from a node JsonObject - * - * @param nodeObj the JsonObject representing the node - * @return the parsed position vector - */ - private static Vector3f parsePosition(JsonObject nodeObj) { - if (!nodeObj.has("position")) { - return new Vector3f(DEFAULT_POSITION); - } - - JsonObject pos = nodeObj.getAsJsonObject("position"); - return new Vector3f( - getFloatOrDefault(pos, "x", 0), - getFloatOrDefault(pos, "y", 0), - getFloatOrDefault(pos, "z", 0) - ); - } - - /** - * Parse the orientation quaternion from a node JsonObject - * - * @param nodeObj the JsonObject representing the node - * @return the parsed orientation quaternion - */ - private static Quaternionf parseOrientation(JsonObject nodeObj) { - if (!nodeObj.has("orientation")) { - return new Quaternionf(DEFAULT_ORIENTATION); - } - - JsonObject orient = nodeObj.getAsJsonObject("orientation"); - return new Quaternionf( - getFloatOrDefault(orient, "x", 0), - getFloatOrDefault(orient, "y", 0), - getFloatOrDefault(orient, "z", 0), - getFloatOrDefault(orient, "w", 1) - ); - } - - /** - * Parse the shape from a node - * - * @param nodeObj the JsonObject representing the node - * @return the parsed BlockyShape, or null if none - */ - private static BlockyModelGeometry.BlockyShape parseShape(JsonObject nodeObj) { - if (!nodeObj.has("shape")) { - return null; - } - - JsonObject shapeObj = nodeObj.getAsJsonObject("shape"); - - boolean visible = getBooleanOrDefault(shapeObj, "visible", true); - if (!visible) { - return BlockyModelGeometry.BlockyShape.invisible(); - } - - return new BlockyModelGeometry.BlockyShape( - visible, - getBooleanOrDefault(shapeObj, "doubleSided", false), - parseOffset(shapeObj), - parseStretch(shapeObj), - parseSize(shapeObj), - parseTextureLayout(shapeObj) - ); - } - - /** - * Parse the offset vector from a shape JsonObject - * - * @param shapeObj the JsonObject representing the shape - * @return the parsed offset vector - */ - private static Vector3f parseOffset(JsonObject shapeObj) { - if (!shapeObj.has("offset")) { - return new Vector3f(0, 0, 0); - } - - JsonObject offset = shapeObj.getAsJsonObject("offset"); - return new Vector3f( - getFloatOrDefault(offset, "x", 0), - getFloatOrDefault(offset, "y", 0), - getFloatOrDefault(offset, "z", 0) - ); - } - - /** - * Parse the stretch vector from a shape JsonObject - * - * @param shapeObj the JsonObject representing the shape - * @return the parsed stretch vector - */ - private static Vector3f parseStretch(JsonObject shapeObj) { - if (!shapeObj.has("stretch")) { - return new Vector3f(1, 1, 1); - } - - JsonObject stretch = shapeObj.getAsJsonObject("stretch"); - return new Vector3f( - getFloatOrDefault(stretch, "x", DEFAULT_STRETCH), - getFloatOrDefault(stretch, "y", DEFAULT_STRETCH), - getFloatOrDefault(stretch, "z", DEFAULT_STRETCH) - ); - } - - /** - * Parse the size vector from a shape JsonObject - * - * @param shapeObj the JsonObject representing the shape - * @return the parsed size vector - */ - private static Vector3f parseSize(JsonObject shapeObj) { - if (!shapeObj.has("settings")) { - return new Vector3f(DEFAULT_SIZE, DEFAULT_SIZE, DEFAULT_SIZE); - } - - JsonObject settings = shapeObj.getAsJsonObject("settings"); - if (!settings.has("size")) { - return new Vector3f(DEFAULT_SIZE, DEFAULT_SIZE, DEFAULT_SIZE); - } - - JsonObject size = settings.getAsJsonObject("size"); - return new Vector3f( - getFloatOrDefault(size, "x", DEFAULT_SIZE), - getFloatOrDefault(size, "y", DEFAULT_SIZE), - getFloatOrDefault(size, "z", DEFAULT_SIZE) - ); - } - - /** - * Parse the texture layout from a shape JsonObject - * - * @param shapeObj the JsonObject representing the shape - * @return the parsed texture layout map - */ - private static Map parseTextureLayout(JsonObject shapeObj) { - Map layoutMap = new EnumMap<>(Direction.class); - - if (!shapeObj.has("textureLayout")) { - return layoutMap; - } - - JsonObject texLayout = shapeObj.getAsJsonObject("textureLayout"); - for (String dirName : texLayout.keySet()) { - Direction dir = parseDirectionName(dirName); - if (dir != null) { - layoutMap.put(dir, parseFaceLayout(texLayout.getAsJsonObject(dirName))); - } - } - - return layoutMap; - } - - /** - * Parse a single face texture layout from a JsonObject - * - * @param faceLayout the JsonObject representing the face layout - * @return the parsed FaceTextureLayout - */ - private static BlockyModelGeometry.FaceTextureLayout parseFaceLayout(JsonObject faceLayout) { - int offsetX = 0, offsetY = 0; - boolean mirrorX = false, mirrorY = false; - int angle = 0; - - if (faceLayout.has("offset")) { - JsonObject offset = faceLayout.getAsJsonObject("offset"); - offsetX = getIntOrDefault(offset, "x", 0); - offsetY = getIntOrDefault(offset, "y", 0); - } - - if (faceLayout.has("mirror")) { - JsonObject mirror = faceLayout.getAsJsonObject("mirror"); - mirrorX = getBooleanOrDefault(mirror, "x", false); - mirrorY = getBooleanOrDefault(mirror, "y", false); - } - - if (faceLayout.has("angle")) { - angle = faceLayout.get("angle").getAsInt(); - validateAngle(angle); - } - - return new BlockyModelGeometry.FaceTextureLayout(offsetX, offsetY, mirrorX, mirrorY, angle); - } - - /** - * Parse a direction name string to a Direction enum - * - * @param name the direction name string - * @return the corresponding Direction enum, or null if unknown - */ - private static Direction parseDirectionName(String name) { - return switch (name.toLowerCase()) { - case "front" -> Direction.SOUTH; - case "back" -> Direction.NORTH; - case "left" -> Direction.WEST; - case "right" -> Direction.EAST; - case "top" -> Direction.UP; - case "bottom" -> Direction.DOWN; - case "north", "south", "west", "east", "up", "down" -> Direction.valueOf(name.toUpperCase()); - default -> { - HytaleModelLoader.LOGGER.warn("Unknown direction name: {}, skipping", name); - yield null; - } - }; - } - - /** - * Validate that the angle is one of the allowed values - * - * @param angle the angle to validate - * @throws JsonParseException if the angle is invalid - */ - private static void validateAngle(int angle) { - if (angle != 0 && angle != 90 && angle != 180 && angle != 270) { - throw new JsonParseException("Invalid angle: " + angle + ". Must be 0, 90, 180, or 270"); - } - } - - // Utility methods to get values with defaults - - private static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) { - return obj.has(key) ? obj.get(key).getAsFloat() : defaultValue; - } - - private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) { - return obj.has(key) ? obj.get(key).getAsInt() : defaultValue; - } - - private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) { - return obj.has(key) ? obj.get(key).getAsBoolean() : defaultValue; - } +package com.litehed.hytalemodels.blockymodel; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.litehed.hytalemodels.HytaleModelLoader; +import net.minecraft.core.Direction; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class BlockyModelParser { + + private static final float DEFAULT_SIZE = 16.0f; + private static final float DEFAULT_STRETCH = 1.0f; + private static final Vector3f DEFAULT_POSITION = new Vector3f(0, 0, 0); + private static final Quaternionf DEFAULT_ORIENTATION = new Quaternionf(); + + /** + * Parse the nodes from the root JsonObject + * + * @param root the root JsonObject of the BlockyModel + * @return a list of parsed BlockyNodes + * @throws JsonParseException if required fields are missing or invalid + */ + public static List parseNodes(JsonObject root) throws JsonParseException { + if (!root.has("nodes")) { + throw new JsonParseException("BlockyModel file must contain a 'nodes' array"); + } + + List nodes = new ArrayList<>(); + JsonArray nodesArray = root.getAsJsonArray("nodes"); + + for (JsonElement nodeElement : nodesArray) { + parseNode(nodeElement.getAsJsonObject(), null, nodes); + } + + return nodes; + } + + /** + * Parse a single node and its children recursively + * + * @param nodeObj the JsonObject representing the node + * @param parent the parent BlockyNode, or null if root + * @param allNodes the list to add parsed nodes to + * @throws JsonParseException if required fields are missing or invalid + */ + private static void parseNode(JsonObject nodeObj, BlockyModelGeometry.BlockyNode parent, List allNodes) { + validateRequiredFields(nodeObj); + + BlockyModelGeometry.BlockyNode node = new BlockyModelGeometry.BlockyNode( + parseString(nodeObj, "id"), + parseString(nodeObj, "name"), + parsePosition(nodeObj), + parseOrientation(nodeObj), + parseShape(nodeObj), + parent + ); + + allNodes.add(node); + + // Parse children recursively + if (nodeObj.has("children")) { + JsonArray children = nodeObj.getAsJsonArray("children"); + for (JsonElement childElement : children) { + parseNode(childElement.getAsJsonObject(), node, allNodes); + } + } + } + + /** + * Validate that required fields are present in the node JsonObject + * + * @param nodeObj the JsonObject representing the node + * @throws JsonParseException if required fields are missing + */ + private static void validateRequiredFields(JsonObject nodeObj) throws JsonParseException { + if (!nodeObj.has("name")) { + throw new JsonParseException("Node missing required field: 'name'"); + } + if (!nodeObj.has("id")) { + throw new JsonParseException("Node missing required field: 'id'"); + } + } + + /** + * Parse a string field from a JsonObject + * + * @param obj the JsonObject + * @param key the key of the string field + * @return the string value + */ + private static String parseString(JsonObject obj, String key) { + return obj.get(key).getAsString(); + } + + /** + * Parse the position vector from a node JsonObject + * + * @param nodeObj the JsonObject representing the node + * @return the parsed position vector + */ + private static Vector3f parsePosition(JsonObject nodeObj) { + if (!nodeObj.has("position")) { + return new Vector3f(DEFAULT_POSITION); + } + + JsonObject pos = nodeObj.getAsJsonObject("position"); + return new Vector3f( + getFloatOrDefault(pos, "x", 0), + getFloatOrDefault(pos, "y", 0), + getFloatOrDefault(pos, "z", 0) + ); + } + + /** + * Parse the orientation quaternion from a node JsonObject + * + * @param nodeObj the JsonObject representing the node + * @return the parsed orientation quaternion + */ + private static Quaternionf parseOrientation(JsonObject nodeObj) { + if (!nodeObj.has("orientation")) { + return new Quaternionf(DEFAULT_ORIENTATION); + } + + JsonObject orient = nodeObj.getAsJsonObject("orientation"); + return new Quaternionf( + getFloatOrDefault(orient, "x", 0), + getFloatOrDefault(orient, "y", 0), + getFloatOrDefault(orient, "z", 0), + getFloatOrDefault(orient, "w", 1) + ); + } + + /** + * Parse the shape from a node + * + * @param nodeObj the JsonObject representing the node + * @return the parsed BlockyShape, or null if none + */ + private static BlockyModelGeometry.BlockyShape parseShape(JsonObject nodeObj) { + if (!nodeObj.has("shape")) { + return null; + } + + JsonObject shapeObj = nodeObj.getAsJsonObject("shape"); + + boolean visible = getBooleanOrDefault(shapeObj, "visible", true); + if (!visible) { + return BlockyModelGeometry.BlockyShape.invisible(); + } + + return new BlockyModelGeometry.BlockyShape( + visible, + getBooleanOrDefault(shapeObj, "doubleSided", false), + parseOffset(shapeObj), + parseStretch(shapeObj), + parseSize(shapeObj), + parseTextureLayout(shapeObj) + ); + } + + /** + * Parse the offset vector from a shape JsonObject + * + * @param shapeObj the JsonObject representing the shape + * @return the parsed offset vector + */ + private static Vector3f parseOffset(JsonObject shapeObj) { + if (!shapeObj.has("offset")) { + return new Vector3f(0, 0, 0); + } + + JsonObject offset = shapeObj.getAsJsonObject("offset"); + return new Vector3f( + getFloatOrDefault(offset, "x", 0), + getFloatOrDefault(offset, "y", 0), + getFloatOrDefault(offset, "z", 0) + ); + } + + /** + * Parse the stretch vector from a shape JsonObject + * + * @param shapeObj the JsonObject representing the shape + * @return the parsed stretch vector + */ + private static Vector3f parseStretch(JsonObject shapeObj) { + if (!shapeObj.has("stretch")) { + return new Vector3f(1, 1, 1); + } + + JsonObject stretch = shapeObj.getAsJsonObject("stretch"); + return new Vector3f( + getFloatOrDefault(stretch, "x", DEFAULT_STRETCH), + getFloatOrDefault(stretch, "y", DEFAULT_STRETCH), + getFloatOrDefault(stretch, "z", DEFAULT_STRETCH) + ); + } + + /** + * Parse the size vector from a shape JsonObject + * + * @param shapeObj the JsonObject representing the shape + * @return the parsed size vector + */ + private static Vector3f parseSize(JsonObject shapeObj) { + if (!shapeObj.has("settings")) { + return new Vector3f(DEFAULT_SIZE, DEFAULT_SIZE, DEFAULT_SIZE); + } + + JsonObject settings = shapeObj.getAsJsonObject("settings"); + if (!settings.has("size")) { + return new Vector3f(DEFAULT_SIZE, DEFAULT_SIZE, DEFAULT_SIZE); + } + + JsonObject size = settings.getAsJsonObject("size"); + return new Vector3f( + getFloatOrDefault(size, "x", DEFAULT_SIZE), + getFloatOrDefault(size, "y", DEFAULT_SIZE), + getFloatOrDefault(size, "z", DEFAULT_SIZE) + ); + } + + /** + * Parse the texture layout from a shape JsonObject + * + * @param shapeObj the JsonObject representing the shape + * @return the parsed texture layout map + */ + private static Map parseTextureLayout(JsonObject shapeObj) { + Map layoutMap = new EnumMap<>(Direction.class); + + if (!shapeObj.has("textureLayout")) { + return layoutMap; + } + + JsonObject texLayout = shapeObj.getAsJsonObject("textureLayout"); + for (String dirName : texLayout.keySet()) { + Direction dir = parseDirectionName(dirName); + if (dir != null) { + layoutMap.put(dir, parseFaceLayout(texLayout.getAsJsonObject(dirName))); + } + } + + return layoutMap; + } + + /** + * Parse a single face texture layout from a JsonObject + * + * @param faceLayout the JsonObject representing the face layout + * @return the parsed FaceTextureLayout + */ + private static BlockyModelGeometry.FaceTextureLayout parseFaceLayout(JsonObject faceLayout) { + int offsetX = 0, offsetY = 0; + boolean mirrorX = false, mirrorY = false; + int angle = 0; + + if (faceLayout.has("offset")) { + JsonObject offset = faceLayout.getAsJsonObject("offset"); + offsetX = getIntOrDefault(offset, "x", 0); + offsetY = getIntOrDefault(offset, "y", 0); + } + + if (faceLayout.has("mirror")) { + JsonObject mirror = faceLayout.getAsJsonObject("mirror"); + mirrorX = getBooleanOrDefault(mirror, "x", false); + mirrorY = getBooleanOrDefault(mirror, "y", false); + } + + if (faceLayout.has("angle")) { + angle = faceLayout.get("angle").getAsInt(); + validateAngle(angle); + } + + return new BlockyModelGeometry.FaceTextureLayout(offsetX, offsetY, mirrorX, mirrorY, angle); + } + + /** + * Parse a direction name string to a Direction enum + * + * @param name the direction name string + * @return the corresponding Direction enum, or null if unknown + */ + private static Direction parseDirectionName(String name) { + return switch (name.toLowerCase()) { + case "front" -> Direction.SOUTH; + case "back" -> Direction.NORTH; + case "left" -> Direction.WEST; + case "right" -> Direction.EAST; + case "top" -> Direction.UP; + case "bottom" -> Direction.DOWN; + case "north", "south", "west", "east", "up", "down" -> Direction.valueOf(name.toUpperCase()); + default -> { + HytaleModelLoader.LOGGER.warn("Unknown direction name: {}, skipping", name); + yield null; + } + }; + } + + /** + * Validate that the angle is one of the allowed values + * + * @param angle the angle to validate + * @throws JsonParseException if the angle is invalid + */ + private static void validateAngle(int angle) { + if (angle != 0 && angle != 90 && angle != 180 && angle != 270) { + throw new JsonParseException("Invalid angle: " + angle + ". Must be 0, 90, 180, or 270"); + } + } + + // Utility methods to get values with defaults + + private static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) { + return obj.has(key) ? obj.get(key).getAsFloat() : defaultValue; + } + + private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) { + return obj.has(key) ? obj.get(key).getAsInt() : defaultValue; + } + + private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) { + return obj.has(key) ? obj.get(key).getAsBoolean() : defaultValue; + } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelTokenizer.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelTokenizer.java similarity index 92% rename from src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelTokenizer.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelTokenizer.java index cd26482..6f300a7 100644 --- a/src/main/java/com/litehed/hytalemodels/modelstuff/BlockyModelTokenizer.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelTokenizer.java @@ -1,33 +1,33 @@ -package com.litehed.hytalemodels.modelstuff; - -import com.google.common.base.Charsets; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; - -public class BlockyModelTokenizer implements AutoCloseable { - private final BufferedReader lineReader; - private final JsonObject root; - - /** - * Creates a new BlockyModelTokenizer that reads from the given InputStream - * - * @param inputStream The InputStream to read from - */ - public BlockyModelTokenizer(InputStream inputStream) { - this.lineReader = new BufferedReader(new InputStreamReader(inputStream, Charsets.UTF_8)); - this.root = JsonParser.parseReader(lineReader).getAsJsonObject(); - } - - public JsonObject getRoot() { - return root; - } - - @Override - public void close() throws Exception { - this.lineReader.close(); - } -} +package com.litehed.hytalemodels.blockymodel; + +import com.google.common.base.Charsets; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +public class BlockyModelTokenizer implements AutoCloseable { + private final BufferedReader lineReader; + private final JsonObject root; + + /** + * Creates a new BlockyModelTokenizer that reads from the given InputStream + * + * @param inputStream The InputStream to read from + */ + public BlockyModelTokenizer(InputStream inputStream) { + this.lineReader = new BufferedReader(new InputStreamReader(inputStream, Charsets.UTF_8)); + this.root = JsonParser.parseReader(lineReader).getAsJsonObject(); + } + + public JsonObject getRoot() { + return root; + } + + @Override + public void close() throws Exception { + this.lineReader.close(); + } +} diff --git a/src/main/java/com/litehed/hytalemodels/modelstuff/QuadBuilder.java b/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java similarity index 97% rename from src/main/java/com/litehed/hytalemodels/modelstuff/QuadBuilder.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java index 4d66837..e8af16b 100644 --- a/src/main/java/com/litehed/hytalemodels/modelstuff/QuadBuilder.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java @@ -1,536 +1,536 @@ -package com.litehed.hytalemodels.modelstuff; - -import com.mojang.math.Transformation; -import net.minecraft.client.renderer.block.model.BakedQuad; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.core.Direction; -import net.neoforged.neoforge.client.model.pipeline.QuadBakingVertexConsumer; -import org.apache.commons.lang3.tuple.Pair; -import org.joml.Vector3f; -import org.joml.Vector4f; - -import java.util.ArrayList; -import java.util.List; - -public class QuadBuilder { - - private static final int TINT_INDEX_NONE = -1; // No tinting -1 - private static final float[] COLOR_WHITE = {1.0f, 1.0f, 1.0f, 1.0f}; - - public static final boolean DEBUG_BORDERS = false; - private static final float BORDER_THICKNESS = 0.002f; - - /** - * Create a single quad for a given face - * - * @param face the Direction of the face - * @param min the minimum coordinates of the face - * @param max the maximum coordinates of the face - * @param sprite the TextureAtlasSprite to use for the quad - * @param texLayout the texture layout for this face - * @param size the size of the block in each axis (x, y, z) - * @param transform the transformation to apply to the quad - * @return a Pair containing the BakedQuad and its cull face Direction - */ - public static Pair createQuad( - Direction face, - Vector3f min, - Vector3f max, - TextureAtlasSprite sprite, - BlockyModelGeometry.FaceTextureLayout texLayout, - Vector3f size, - Transformation transform) { - - QuadBakingVertexConsumer baker = setupQuadBaker(face, sprite); - - float[][] uvCoords = calculateUVCoordinates(face, texLayout, size, sprite); - Vector3f[] vertices = getFaceVertices(face, min, max); - - bakeVertices(baker, vertices, uvCoords, face, transform); - - Direction cullFace = calculateCullFace(face, min, max); - return Pair.of(baker.bakeQuad(), cullFace); - } - - - /** - * Create a reversed quad for backfaces - * - * @param face the Direction of the face - * @param min the minimum coordinates of the face - * @param max the maximum coordinates of the face - * @param sprite the TextureAtlasSprite to use for the quad - * @param texLayout the texture layout for this face - * @param size the size of the block in each axis (x, y, z) - * @param transform the transformation to apply to the quad - * @return a Pair containing the BakedQuad and null cull face - */ - public static Pair createReversedQuad( - Direction face, - Vector3f min, - Vector3f max, - TextureAtlasSprite sprite, - BlockyModelGeometry.FaceTextureLayout texLayout, - Vector3f size, - Transformation transform) { - - QuadBakingVertexConsumer baker = setupQuadBaker(face, sprite); - - float[][] uvCoords = calculateUVCoordinates(face, texLayout, size, sprite); - Vector3f[] vertices = getFaceVertices(face, min, max); - - // Bake in reverse order with flipped normals - bakeVerticesReversed(baker, vertices, uvCoords, face, transform); - // No cull, it breaks the backface - return Pair.of(baker.bakeQuad(), null); - } - - /** - * Set up the quad baker with common parameters - * - * @param face the Direction of the face - * @param sprite the TextureAtlasSprite to use for the quad - * @return a QuadBakingVertexConsumer instance - */ - private static QuadBakingVertexConsumer setupQuadBaker(Direction face, TextureAtlasSprite sprite) { - QuadBakingVertexConsumer baker = new QuadBakingVertexConsumer(); - baker.setSprite(sprite); - baker.setDirection(face); - baker.setTintIndex(TINT_INDEX_NONE); - baker.setShade(true); - return baker; - } - - /** - * Create border quads for debugging (4 thin strips around the edge) - * - * @param face the Direction of the face - * @param min the minimum coordinates of the face - * @param max the maximum coordinates of the face - * @param sprite the TextureAtlasSprite to use for the border quads - * @param transform the transformation to apply to each border quad - * @return a list of BakedQuad instances representing the border strips - */ - public static List createBorderQuads( - Direction face, - Vector3f min, - Vector3f max, - TextureAtlasSprite sprite, - Transformation transform) { - - List borderQuads = new ArrayList<>(); - Vector3f[] vertices = getFaceVertices(face, min, max); - - float[] color = getDebugColor(face); - float[][] dummyUVs = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; - float borderWidth = 0.05f; - - for (int i = 0; i < 4; i++) { - QuadBakingVertexConsumer baker = setupQuadBaker(face, sprite); - Vector3f[] borderVerts = createBorderStrip(vertices, i, borderWidth); - bakeBorderVertices(baker, borderVerts, dummyUVs, face, transform, color); - borderQuads.add(baker.bakeQuad()); - } - - return borderQuads; - } - - /** - * Create a border strip along one edge of the quad - * - * @param vertices Original quad vertices - * @param edge Edge index (0=bottom, 1=right, 2=top, 3=left) - * @param width Width of the border strip - * @return Vertices of the border strip quad - */ - private static Vector3f[] createBorderStrip(Vector3f[] vertices, int edge, float width) { - Vector3f[] strip = new Vector3f[4]; - - int v1 = (edge + 1) % 4; - - Vector3f start = new Vector3f(vertices[edge]); - Vector3f end = new Vector3f(vertices[v1]); - - // Calculate inward direction - Vector3f center = new Vector3f(vertices[0]).add(vertices[1]).add(vertices[2]).add(vertices[3]).mul(0.25f); - Vector3f toCenter = new Vector3f(start).add(end).mul(0.5f); - toCenter.sub(center); - toCenter.normalize().mul(width); - - strip[0] = new Vector3f(start); - strip[1] = new Vector3f(end); - strip[2] = new Vector3f(end).add(toCenter); - strip[3] = new Vector3f(start).add(toCenter); - - return strip; - } - - - /** - * Calculate UV coordinates for a face - * - * @param face the Direction of the face - * @param layout the texture layout for the face - * @param size the size of the face - * @param sprite the TextureAtlasSprite to use for UV calculation - * @return an array of UV coordinates for each vertex of the face - */ - private static float[][] calculateUVCoordinates(Direction face, BlockyModelGeometry.FaceTextureLayout layout, - Vector3f size, TextureAtlasSprite sprite) { - UVSize uvSize = getUVSizeForFace(face, size); - UVBounds bounds = calculateUVBounds(layout, uvSize, sprite); - return transformUVCoordinates(bounds, layout, uvSize, sprite); - } - - /** - * Get UV size based on face orientation - * - * @param face the Direction of the face - * @param size the size of the face - * @return the UVSize for the given face and rotation - */ - private static UVSize getUVSizeForFace(Direction face, Vector3f size) { - return switch (face) { - case UP, DOWN -> new UVSize(size.x, size.z); - case WEST, EAST -> new UVSize(size.z, size.y); - default -> new UVSize(size.x, size.y); - }; - } - - /** - * Calculate UV bounds using layout and sprite - * - * @param layout the texture layout for the face - * @param uvSize the UV size for the face - * @param sprite the TextureAtlasSprite to use for UV calculation - * @return the UV bounds for the given layout and sprite - */ - private static UVBounds calculateUVBounds(BlockyModelGeometry.FaceTextureLayout layout, UVSize uvSize, - TextureAtlasSprite sprite) { - int textureWidth = sprite.contents().width(); - int textureHeight = sprite.contents().height(); - - float uMin = layout.offsetX() / (float) textureWidth; - float vMin = layout.offsetY() / (float) textureHeight; - float uMax = (layout.offsetX() + (layout.mirrorX() ? -uvSize.u : uvSize.u)) / (float) textureWidth; - float vMax = (layout.offsetY() + (layout.mirrorY() ? -uvSize.v : uvSize.v)) / (float) textureHeight; - - if (layout.mirrorX()) { - float temp = uMin; - uMin = uMax; - uMax = temp; - } - if (layout.mirrorY()) { - float temp = vMin; - vMin = vMax; - vMax = temp; - } - - // Map to sprite atlas - uMin = sprite.getU0() + (sprite.getU1() - sprite.getU0()) * uMin; - vMin = sprite.getV0() + (sprite.getV1() - sprite.getV0()) * vMin; - uMax = sprite.getU0() + (sprite.getU1() - sprite.getU0()) * uMax; - vMax = sprite.getV0() + (sprite.getV1() - sprite.getV0()) * vMax; - return new UVBounds(uMin, vMin, uMax, vMax); - } - - /** - * Transform UV coordinates with rotation and mirroring - * - * @param bounds the UV bounds - * @param layout the texture layout containing rotation and mirror flags - * @param uvSize the size of the UV region - * @param sprite the TextureAtlasSprite for coordinate conversion - * @return transformed UV coordinates for all 4 vertices - */ - private static float[][] transformUVCoordinates( - UVBounds bounds, - BlockyModelGeometry.FaceTextureLayout layout, - UVSize uvSize, - TextureAtlasSprite sprite) { - - int textureWidth = sprite.contents().width(); - int textureHeight = sprite.contents().height(); - int angle = layout.angle(); - boolean mirrorX = layout.mirrorX(); - boolean mirrorY = layout.mirrorY(); - - float[][] uvs = new float[4][2]; - - if (angle != 0) { - float pivotUPx = layout.offsetX(); - float pivotVPx = layout.offsetY(); - - float uSize = mirrorX ? -uvSize.u : uvSize.u; - float vSize = mirrorY ? -uvSize.v : uvSize.v; - - float[][] cornersPx = new float[4][2]; - cornersPx[0] = new float[]{pivotUPx, pivotVPx}; // Top-left (pivot) - cornersPx[1] = new float[]{pivotUPx + uSize, pivotVPx}; // Top-right - cornersPx[2] = new float[]{pivotUPx + uSize, pivotVPx + vSize}; // Bottom-right - cornersPx[3] = new float[]{pivotUPx, pivotVPx + vSize}; // Bottom-left - - for (int i = 0; i < 4; i++) { - float[] rotated = rotatePointClockwise( - cornersPx[i][0], cornersPx[i][1], - pivotUPx, pivotVPx, - angle - ); - - uvs[i][0] = normalizeU(rotated[0], sprite, textureWidth); - uvs[i][1] = normalizeV(rotated[1], sprite, textureHeight); - } - } else { - uvs[0] = new float[]{bounds.uMin, bounds.vMax}; - uvs[1] = new float[]{bounds.uMax, bounds.vMax}; - uvs[2] = new float[]{bounds.uMax, bounds.vMin}; - uvs[3] = new float[]{bounds.uMin, bounds.vMin}; - - if (mirrorX) { - float[] temp = uvs[0]; - uvs[0] = uvs[1]; - uvs[1] = temp; - - temp = uvs[3]; - uvs[3] = uvs[2]; - uvs[2] = temp; - } - if (mirrorY) { - float[] temp = uvs[0]; - uvs[0] = uvs[3]; - uvs[3] = temp; - - temp = uvs[1]; - uvs[1] = uvs[2]; - uvs[2] = temp; - } - } - - return uvs; - } - - - /** - * Rotate a point (x, y) around a pivot (pivotX, pivotY) by a given angle in degrees - * - * @param x the x-coordinate of the point to rotate - * @param y the y-coordinate of the point to rotate - * @param pivotX the x-coordinate of the pivot point - * @param pivotY the y-coordinate of the pivot point - * @param angle the rotation angle in degrees (must be one of 0, 90, 180, 270) - * @return the new coordinates of the point after rotation as a float array [newX, newY] - */ - private static float[] rotatePointClockwise(float x, float y, float pivotX, float pivotY, int angle) { - float dx = x - pivotX; - float dy = y - pivotY; - - float newDx, newDy; - newDy = switch (angle) { - case 90 -> { - newDx = -dy; - yield dx; - } - case 180 -> { - newDx = -dx; - yield -dy; - } - case 270 -> { - newDx = dy; - yield -dx; - } - default -> { - newDx = dx; - yield dy; - } - }; - - return new float[]{pivotX + newDx, pivotY + newDy}; - } - - - // Helper funcs to get u and v normalized to proper coords - private static float normalizeU(float pixelU, TextureAtlasSprite sprite, int textureWidth) { - float relativeU = pixelU / textureWidth; - return sprite.getU0() + (sprite.getU1() - sprite.getU0()) * relativeU; - } - - private static float normalizeV(float pixelV, TextureAtlasSprite sprite, int textureHeight) { - float relativeV = pixelV / textureHeight; - return sprite.getV0() + (sprite.getV1() - sprite.getV0()) * relativeV; - } - - /** - * Get the 4 vertices for a given face - * - * @param face the Direction of the face - * @param min the minimum coordinates of the face - * @param max the maximum coordinates of the face - * @return an array of 4 Vector3f vertices - */ - private static Vector3f[] getFaceVertices(Direction face, Vector3f min, Vector3f max) { - float x0 = min.x, y0 = min.y, z0 = min.z; - float x1 = max.x, y1 = max.y, z1 = max.z; - - return switch (face) { - case DOWN -> new Vector3f[]{ - new Vector3f(x0, y0, z0), new Vector3f(x1, y0, z0), - new Vector3f(x1, y0, z1), new Vector3f(x0, y0, z1) - }; - case UP -> new Vector3f[]{ - new Vector3f(x0, y1, z1), new Vector3f(x1, y1, z1), - new Vector3f(x1, y1, z0), new Vector3f(x0, y1, z0) - }; - case NORTH -> new Vector3f[]{ - new Vector3f(x1, y0, z0), new Vector3f(x0, y0, z0), - new Vector3f(x0, y1, z0), new Vector3f(x1, y1, z0) - }; - case SOUTH -> new Vector3f[]{ - new Vector3f(x0, y0, z1), new Vector3f(x1, y0, z1), - new Vector3f(x1, y1, z1), new Vector3f(x0, y1, z1) - }; - case WEST -> new Vector3f[]{ - new Vector3f(x0, y0, z0), new Vector3f(x0, y0, z1), - new Vector3f(x0, y1, z1), new Vector3f(x0, y1, z0) - }; - case EAST -> new Vector3f[]{ - new Vector3f(x1, y0, z1), new Vector3f(x1, y0, z0), - new Vector3f(x1, y1, z0), new Vector3f(x1, y1, z1) - }; - }; - } - - /** - * Bake vertices for the quad - * - * @param baker the QuadBakingVertexConsumer - * @param vertices the vertices of the quad - * @param uvCoords the UV coordinates for the quad - * @param face the Direction of the face - * @param transform the transformation to apply - */ - private static void bakeVertices(QuadBakingVertexConsumer baker, Vector3f[] vertices, - float[][] uvCoords, Direction face, Transformation transform) { - Vector3f normal = new Vector3f(face.getStepX(), face.getStepY(), face.getStepZ()); - boolean hasTransform = !transform.isIdentity(); - - for (int i = 0; i < 4; i++) { - Vector4f pos = new Vector4f(vertices[i].x, vertices[i].y, vertices[i].z, 1.0f); - Vector3f norm = new Vector3f(normal); - - if (hasTransform) { - transform.transformPosition(pos); - transform.transformNormal(norm); - } - - baker.addVertex(pos.x(), pos.y(), pos.z()); - baker.setColor(COLOR_WHITE[0], COLOR_WHITE[1], COLOR_WHITE[2], COLOR_WHITE[3]); - baker.setUv(uvCoords[i][0], uvCoords[i][1]); - baker.setNormal(norm.x(), norm.y(), norm.z()); - } - } - - /** - * Bake vertices in reverse order for backfaces - * - * @param baker the QuadBakingVertexConsumer - * @param vertices the vertices of the quad - * @param uvCoords the UV coordinates for the quad - * @param face the Direction of the face - * @param transform the transformation to apply - */ - private static void bakeVerticesReversed(QuadBakingVertexConsumer baker, Vector3f[] vertices, - float[][] uvCoords, Direction face, Transformation transform) { - Vector3f normal = new Vector3f(-face.getStepX(), -face.getStepY(), -face.getStepZ()); - boolean hasTransform = !transform.isIdentity(); - - // Reverse order - for (int i = 3; i >= 0; i--) { - Vector4f pos = new Vector4f(vertices[i].x, vertices[i].y, vertices[i].z, 1.0f); - Vector3f norm = new Vector3f(normal); - - if (hasTransform) { - transform.transformPosition(pos); - transform.transformNormal(norm); - } - - baker.addVertex(pos.x(), pos.y(), pos.z()); - baker.setColor(COLOR_WHITE[0], COLOR_WHITE[1], COLOR_WHITE[2], COLOR_WHITE[3]); - baker.setUv(uvCoords[i][0], uvCoords[i][1]); - baker.setNormal(norm.x(), norm.y(), norm.z()); - } - } - - /** - * Bake border vertices for debugging - * - * @param baker the QuadBakingVertexConsumer - * @param vertices the vertices of the border strip - * @param uvCoords the UV coordinates for the border strip - * @param face the Direction of the face - * @param transform the transformation to apply - * @param color the color to use for debugging - */ - private static void bakeBorderVertices(QuadBakingVertexConsumer baker, Vector3f[] vertices, - float[][] uvCoords, Direction face, Transformation transform, float[] color) { - Vector3f normal = new Vector3f(face.getStepX(), face.getStepY(), face.getStepZ()); - boolean hasTransform = !transform.isIdentity(); - - // Offset vertices slightly outward to prevent z-fighting - Vector3f offset = new Vector3f(normal).mul(BORDER_THICKNESS); - - for (int i = 0; i < 4; i++) { - Vector3f offsetVert = new Vector3f(vertices[i]).add(offset); - Vector4f pos = new Vector4f(offsetVert.x, offsetVert.y, offsetVert.z, 1.0f); - Vector3f norm = new Vector3f(normal); - - if (hasTransform) { - transform.transformPosition(pos); - transform.transformNormal(norm); - } - - baker.addVertex(pos.x(), pos.y(), pos.z()); - baker.setColor(color[0], color[1], color[2], color[3]); - baker.setUv(uvCoords[i][0], uvCoords[i][1]); - baker.setNormal(norm.x(), norm.y(), norm.z()); - } - } - - /** - * Get debug color for vertex based on index and face - * Creates a colored border effect to visualize quad boundaries - * - * @param face the Direction of the face - * @return the RGBA color array - */ - private static float[] getDebugColor(Direction face) { - // Assign different colors to different faces - return switch (face) { - case UP -> new float[]{1.0f, 0.0f, 0.0f, 1.0f}; // Red - case DOWN -> new float[]{0.0f, 1.0f, 0.0f, 1.0f}; // Green - case NORTH -> new float[]{0.0f, 0.0f, 1.0f, 1.0f}; // Blue - case SOUTH -> new float[]{1.0f, 1.0f, 0.0f, 1.0f}; // Yellow - case WEST -> new float[]{1.0f, 0.0f, 1.0f, 1.0f}; // Magenta - case EAST -> new float[]{0.0f, 1.0f, 1.0f, 1.0f}; // Cyan - }; - } - - /** - * Calculate cull face based on quad orientation - * - * @param face the Direction of the face - * @param min the minimum coordinates of the face - * @param max the maximum coordinates of the face - * @return the cull face Direction, or null if none - */ - private static Direction calculateCullFace(Direction face, Vector3f min, Vector3f max) { - // Currently disabled - return null; - } - - // Helper records for clean data passing - private record UVSize(float u, float v) { - } - - private record UVBounds(float uMin, float vMin, float uMax, float vMax) { - } +package com.litehed.hytalemodels.blockymodel; + +import com.mojang.math.Transformation; +import net.minecraft.client.renderer.block.model.BakedQuad; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.Direction; +import net.neoforged.neoforge.client.model.pipeline.QuadBakingVertexConsumer; +import org.apache.commons.lang3.tuple.Pair; +import org.joml.Vector3f; +import org.joml.Vector4f; + +import java.util.ArrayList; +import java.util.List; + +public class QuadBuilder { + + private static final int TINT_INDEX_NONE = -1; // No tinting -1 + private static final float[] COLOR_WHITE = {1.0f, 1.0f, 1.0f, 1.0f}; + + public static final boolean DEBUG_BORDERS = false; + private static final float BORDER_THICKNESS = 0.002f; + + /** + * Create a single quad for a given face + * + * @param face the Direction of the face + * @param min the minimum coordinates of the face + * @param max the maximum coordinates of the face + * @param sprite the TextureAtlasSprite to use for the quad + * @param texLayout the texture layout for this face + * @param size the size of the block in each axis (x, y, z) + * @param transform the transformation to apply to the quad + * @return a Pair containing the BakedQuad and its cull face Direction + */ + public static Pair createQuad( + Direction face, + Vector3f min, + Vector3f max, + TextureAtlasSprite sprite, + BlockyModelGeometry.FaceTextureLayout texLayout, + Vector3f size, + Transformation transform) { + + QuadBakingVertexConsumer baker = setupQuadBaker(face, sprite); + + float[][] uvCoords = calculateUVCoordinates(face, texLayout, size, sprite); + Vector3f[] vertices = getFaceVertices(face, min, max); + + bakeVertices(baker, vertices, uvCoords, face, transform); + + Direction cullFace = calculateCullFace(face, min, max); + return Pair.of(baker.bakeQuad(), cullFace); + } + + + /** + * Create a reversed quad for backfaces + * + * @param face the Direction of the face + * @param min the minimum coordinates of the face + * @param max the maximum coordinates of the face + * @param sprite the TextureAtlasSprite to use for the quad + * @param texLayout the texture layout for this face + * @param size the size of the block in each axis (x, y, z) + * @param transform the transformation to apply to the quad + * @return a Pair containing the BakedQuad and null cull face + */ + public static Pair createReversedQuad( + Direction face, + Vector3f min, + Vector3f max, + TextureAtlasSprite sprite, + BlockyModelGeometry.FaceTextureLayout texLayout, + Vector3f size, + Transformation transform) { + + QuadBakingVertexConsumer baker = setupQuadBaker(face, sprite); + + float[][] uvCoords = calculateUVCoordinates(face, texLayout, size, sprite); + Vector3f[] vertices = getFaceVertices(face, min, max); + + // Bake in reverse order with flipped normals + bakeVerticesReversed(baker, vertices, uvCoords, face, transform); + // No cull, it breaks the backface + return Pair.of(baker.bakeQuad(), null); + } + + /** + * Set up the quad baker with common parameters + * + * @param face the Direction of the face + * @param sprite the TextureAtlasSprite to use for the quad + * @return a QuadBakingVertexConsumer instance + */ + private static QuadBakingVertexConsumer setupQuadBaker(Direction face, TextureAtlasSprite sprite) { + QuadBakingVertexConsumer baker = new QuadBakingVertexConsumer(); + baker.setSprite(sprite); + baker.setDirection(face); + baker.setTintIndex(TINT_INDEX_NONE); + baker.setShade(true); + return baker; + } + + /** + * Create border quads for debugging (4 thin strips around the edge) + * + * @param face the Direction of the face + * @param min the minimum coordinates of the face + * @param max the maximum coordinates of the face + * @param sprite the TextureAtlasSprite to use for the border quads + * @param transform the transformation to apply to each border quad + * @return a list of BakedQuad instances representing the border strips + */ + public static List createBorderQuads( + Direction face, + Vector3f min, + Vector3f max, + TextureAtlasSprite sprite, + Transformation transform) { + + List borderQuads = new ArrayList<>(); + Vector3f[] vertices = getFaceVertices(face, min, max); + + float[] color = getDebugColor(face); + float[][] dummyUVs = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; + float borderWidth = 0.05f; + + for (int i = 0; i < 4; i++) { + QuadBakingVertexConsumer baker = setupQuadBaker(face, sprite); + Vector3f[] borderVerts = createBorderStrip(vertices, i, borderWidth); + bakeBorderVertices(baker, borderVerts, dummyUVs, face, transform, color); + borderQuads.add(baker.bakeQuad()); + } + + return borderQuads; + } + + /** + * Create a border strip along one edge of the quad + * + * @param vertices Original quad vertices + * @param edge Edge index (0=bottom, 1=right, 2=top, 3=left) + * @param width Width of the border strip + * @return Vertices of the border strip quad + */ + private static Vector3f[] createBorderStrip(Vector3f[] vertices, int edge, float width) { + Vector3f[] strip = new Vector3f[4]; + + int v1 = (edge + 1) % 4; + + Vector3f start = new Vector3f(vertices[edge]); + Vector3f end = new Vector3f(vertices[v1]); + + // Calculate inward direction + Vector3f center = new Vector3f(vertices[0]).add(vertices[1]).add(vertices[2]).add(vertices[3]).mul(0.25f); + Vector3f toCenter = new Vector3f(start).add(end).mul(0.5f); + toCenter.sub(center); + toCenter.normalize().mul(width); + + strip[0] = new Vector3f(start); + strip[1] = new Vector3f(end); + strip[2] = new Vector3f(end).add(toCenter); + strip[3] = new Vector3f(start).add(toCenter); + + return strip; + } + + + /** + * Calculate UV coordinates for a face + * + * @param face the Direction of the face + * @param layout the texture layout for the face + * @param size the size of the face + * @param sprite the TextureAtlasSprite to use for UV calculation + * @return an array of UV coordinates for each vertex of the face + */ + private static float[][] calculateUVCoordinates(Direction face, BlockyModelGeometry.FaceTextureLayout layout, + Vector3f size, TextureAtlasSprite sprite) { + UVSize uvSize = getUVSizeForFace(face, size); + UVBounds bounds = calculateUVBounds(layout, uvSize, sprite); + return transformUVCoordinates(bounds, layout, uvSize, sprite); + } + + /** + * Get UV size based on face orientation + * + * @param face the Direction of the face + * @param size the size of the face + * @return the UVSize for the given face and rotation + */ + private static UVSize getUVSizeForFace(Direction face, Vector3f size) { + return switch (face) { + case UP, DOWN -> new UVSize(size.x, size.z); + case WEST, EAST -> new UVSize(size.z, size.y); + default -> new UVSize(size.x, size.y); + }; + } + + /** + * Calculate UV bounds using layout and sprite + * + * @param layout the texture layout for the face + * @param uvSize the UV size for the face + * @param sprite the TextureAtlasSprite to use for UV calculation + * @return the UV bounds for the given layout and sprite + */ + private static UVBounds calculateUVBounds(BlockyModelGeometry.FaceTextureLayout layout, UVSize uvSize, + TextureAtlasSprite sprite) { + int textureWidth = sprite.contents().width(); + int textureHeight = sprite.contents().height(); + + float uMin = layout.offsetX() / (float) textureWidth; + float vMin = layout.offsetY() / (float) textureHeight; + float uMax = (layout.offsetX() + (layout.mirrorX() ? -uvSize.u : uvSize.u)) / (float) textureWidth; + float vMax = (layout.offsetY() + (layout.mirrorY() ? -uvSize.v : uvSize.v)) / (float) textureHeight; + + if (layout.mirrorX()) { + float temp = uMin; + uMin = uMax; + uMax = temp; + } + if (layout.mirrorY()) { + float temp = vMin; + vMin = vMax; + vMax = temp; + } + + // Map to sprite atlas + uMin = sprite.getU0() + (sprite.getU1() - sprite.getU0()) * uMin; + vMin = sprite.getV0() + (sprite.getV1() - sprite.getV0()) * vMin; + uMax = sprite.getU0() + (sprite.getU1() - sprite.getU0()) * uMax; + vMax = sprite.getV0() + (sprite.getV1() - sprite.getV0()) * vMax; + return new UVBounds(uMin, vMin, uMax, vMax); + } + + /** + * Transform UV coordinates with rotation and mirroring + * + * @param bounds the UV bounds + * @param layout the texture layout containing rotation and mirror flags + * @param uvSize the size of the UV region + * @param sprite the TextureAtlasSprite for coordinate conversion + * @return transformed UV coordinates for all 4 vertices + */ + private static float[][] transformUVCoordinates( + UVBounds bounds, + BlockyModelGeometry.FaceTextureLayout layout, + UVSize uvSize, + TextureAtlasSprite sprite) { + + int textureWidth = sprite.contents().width(); + int textureHeight = sprite.contents().height(); + int angle = layout.angle(); + boolean mirrorX = layout.mirrorX(); + boolean mirrorY = layout.mirrorY(); + + float[][] uvs = new float[4][2]; + + if (angle != 0) { + float pivotUPx = layout.offsetX(); + float pivotVPx = layout.offsetY(); + + float uSize = mirrorX ? -uvSize.u : uvSize.u; + float vSize = mirrorY ? -uvSize.v : uvSize.v; + + float[][] cornersPx = new float[4][2]; + cornersPx[0] = new float[]{pivotUPx, pivotVPx}; // Top-left (pivot) + cornersPx[1] = new float[]{pivotUPx + uSize, pivotVPx}; // Top-right + cornersPx[2] = new float[]{pivotUPx + uSize, pivotVPx + vSize}; // Bottom-right + cornersPx[3] = new float[]{pivotUPx, pivotVPx + vSize}; // Bottom-left + + for (int i = 0; i < 4; i++) { + float[] rotated = rotatePointClockwise( + cornersPx[i][0], cornersPx[i][1], + pivotUPx, pivotVPx, + angle + ); + + uvs[i][0] = normalizeU(rotated[0], sprite, textureWidth); + uvs[i][1] = normalizeV(rotated[1], sprite, textureHeight); + } + } else { + uvs[0] = new float[]{bounds.uMin, bounds.vMax}; + uvs[1] = new float[]{bounds.uMax, bounds.vMax}; + uvs[2] = new float[]{bounds.uMax, bounds.vMin}; + uvs[3] = new float[]{bounds.uMin, bounds.vMin}; + + if (mirrorX) { + float[] temp = uvs[0]; + uvs[0] = uvs[1]; + uvs[1] = temp; + + temp = uvs[3]; + uvs[3] = uvs[2]; + uvs[2] = temp; + } + if (mirrorY) { + float[] temp = uvs[0]; + uvs[0] = uvs[3]; + uvs[3] = temp; + + temp = uvs[1]; + uvs[1] = uvs[2]; + uvs[2] = temp; + } + } + + return uvs; + } + + + /** + * Rotate a point (x, y) around a pivot (pivotX, pivotY) by a given angle in degrees + * + * @param x the x-coordinate of the point to rotate + * @param y the y-coordinate of the point to rotate + * @param pivotX the x-coordinate of the pivot point + * @param pivotY the y-coordinate of the pivot point + * @param angle the rotation angle in degrees (must be one of 0, 90, 180, 270) + * @return the new coordinates of the point after rotation as a float array [newX, newY] + */ + private static float[] rotatePointClockwise(float x, float y, float pivotX, float pivotY, int angle) { + float dx = x - pivotX; + float dy = y - pivotY; + + float newDx, newDy; + newDy = switch (angle) { + case 90 -> { + newDx = -dy; + yield dx; + } + case 180 -> { + newDx = -dx; + yield -dy; + } + case 270 -> { + newDx = dy; + yield -dx; + } + default -> { + newDx = dx; + yield dy; + } + }; + + return new float[]{pivotX + newDx, pivotY + newDy}; + } + + + // Helper funcs to get u and v normalized to proper coords + private static float normalizeU(float pixelU, TextureAtlasSprite sprite, int textureWidth) { + float relativeU = pixelU / textureWidth; + return sprite.getU0() + (sprite.getU1() - sprite.getU0()) * relativeU; + } + + private static float normalizeV(float pixelV, TextureAtlasSprite sprite, int textureHeight) { + float relativeV = pixelV / textureHeight; + return sprite.getV0() + (sprite.getV1() - sprite.getV0()) * relativeV; + } + + /** + * Get the 4 vertices for a given face + * + * @param face the Direction of the face + * @param min the minimum coordinates of the face + * @param max the maximum coordinates of the face + * @return an array of 4 Vector3f vertices + */ + private static Vector3f[] getFaceVertices(Direction face, Vector3f min, Vector3f max) { + float x0 = min.x, y0 = min.y, z0 = min.z; + float x1 = max.x, y1 = max.y, z1 = max.z; + + return switch (face) { + case DOWN -> new Vector3f[]{ + new Vector3f(x0, y0, z0), new Vector3f(x1, y0, z0), + new Vector3f(x1, y0, z1), new Vector3f(x0, y0, z1) + }; + case UP -> new Vector3f[]{ + new Vector3f(x0, y1, z1), new Vector3f(x1, y1, z1), + new Vector3f(x1, y1, z0), new Vector3f(x0, y1, z0) + }; + case NORTH -> new Vector3f[]{ + new Vector3f(x1, y0, z0), new Vector3f(x0, y0, z0), + new Vector3f(x0, y1, z0), new Vector3f(x1, y1, z0) + }; + case SOUTH -> new Vector3f[]{ + new Vector3f(x0, y0, z1), new Vector3f(x1, y0, z1), + new Vector3f(x1, y1, z1), new Vector3f(x0, y1, z1) + }; + case WEST -> new Vector3f[]{ + new Vector3f(x0, y0, z0), new Vector3f(x0, y0, z1), + new Vector3f(x0, y1, z1), new Vector3f(x0, y1, z0) + }; + case EAST -> new Vector3f[]{ + new Vector3f(x1, y0, z1), new Vector3f(x1, y0, z0), + new Vector3f(x1, y1, z0), new Vector3f(x1, y1, z1) + }; + }; + } + + /** + * Bake vertices for the quad + * + * @param baker the QuadBakingVertexConsumer + * @param vertices the vertices of the quad + * @param uvCoords the UV coordinates for the quad + * @param face the Direction of the face + * @param transform the transformation to apply + */ + private static void bakeVertices(QuadBakingVertexConsumer baker, Vector3f[] vertices, + float[][] uvCoords, Direction face, Transformation transform) { + Vector3f normal = new Vector3f(face.getStepX(), face.getStepY(), face.getStepZ()); + boolean hasTransform = !transform.isIdentity(); + + for (int i = 0; i < 4; i++) { + Vector4f pos = new Vector4f(vertices[i].x, vertices[i].y, vertices[i].z, 1.0f); + Vector3f norm = new Vector3f(normal); + + if (hasTransform) { + transform.transformPosition(pos); + transform.transformNormal(norm); + } + + baker.addVertex(pos.x(), pos.y(), pos.z()); + baker.setColor(COLOR_WHITE[0], COLOR_WHITE[1], COLOR_WHITE[2], COLOR_WHITE[3]); + baker.setUv(uvCoords[i][0], uvCoords[i][1]); + baker.setNormal(norm.x(), norm.y(), norm.z()); + } + } + + /** + * Bake vertices in reverse order for backfaces + * + * @param baker the QuadBakingVertexConsumer + * @param vertices the vertices of the quad + * @param uvCoords the UV coordinates for the quad + * @param face the Direction of the face + * @param transform the transformation to apply + */ + private static void bakeVerticesReversed(QuadBakingVertexConsumer baker, Vector3f[] vertices, + float[][] uvCoords, Direction face, Transformation transform) { + Vector3f normal = new Vector3f(-face.getStepX(), -face.getStepY(), -face.getStepZ()); + boolean hasTransform = !transform.isIdentity(); + + // Reverse order + for (int i = 3; i >= 0; i--) { + Vector4f pos = new Vector4f(vertices[i].x, vertices[i].y, vertices[i].z, 1.0f); + Vector3f norm = new Vector3f(normal); + + if (hasTransform) { + transform.transformPosition(pos); + transform.transformNormal(norm); + } + + baker.addVertex(pos.x(), pos.y(), pos.z()); + baker.setColor(COLOR_WHITE[0], COLOR_WHITE[1], COLOR_WHITE[2], COLOR_WHITE[3]); + baker.setUv(uvCoords[i][0], uvCoords[i][1]); + baker.setNormal(norm.x(), norm.y(), norm.z()); + } + } + + /** + * Bake border vertices for debugging + * + * @param baker the QuadBakingVertexConsumer + * @param vertices the vertices of the border strip + * @param uvCoords the UV coordinates for the border strip + * @param face the Direction of the face + * @param transform the transformation to apply + * @param color the color to use for debugging + */ + private static void bakeBorderVertices(QuadBakingVertexConsumer baker, Vector3f[] vertices, + float[][] uvCoords, Direction face, Transformation transform, float[] color) { + Vector3f normal = new Vector3f(face.getStepX(), face.getStepY(), face.getStepZ()); + boolean hasTransform = !transform.isIdentity(); + + // Offset vertices slightly outward to prevent z-fighting + Vector3f offset = new Vector3f(normal).mul(BORDER_THICKNESS); + + for (int i = 0; i < 4; i++) { + Vector3f offsetVert = new Vector3f(vertices[i]).add(offset); + Vector4f pos = new Vector4f(offsetVert.x, offsetVert.y, offsetVert.z, 1.0f); + Vector3f norm = new Vector3f(normal); + + if (hasTransform) { + transform.transformPosition(pos); + transform.transformNormal(norm); + } + + baker.addVertex(pos.x(), pos.y(), pos.z()); + baker.setColor(color[0], color[1], color[2], color[3]); + baker.setUv(uvCoords[i][0], uvCoords[i][1]); + baker.setNormal(norm.x(), norm.y(), norm.z()); + } + } + + /** + * Get debug color for vertex based on index and face + * Creates a colored border effect to visualize quad boundaries + * + * @param face the Direction of the face + * @return the RGBA color array + */ + private static float[] getDebugColor(Direction face) { + // Assign different colors to different faces + return switch (face) { + case UP -> new float[]{1.0f, 0.0f, 0.0f, 1.0f}; // Red + case DOWN -> new float[]{0.0f, 1.0f, 0.0f, 1.0f}; // Green + case NORTH -> new float[]{0.0f, 0.0f, 1.0f, 1.0f}; // Blue + case SOUTH -> new float[]{1.0f, 1.0f, 0.0f, 1.0f}; // Yellow + case WEST -> new float[]{1.0f, 0.0f, 1.0f, 1.0f}; // Magenta + case EAST -> new float[]{0.0f, 1.0f, 1.0f, 1.0f}; // Cyan + }; + } + + /** + * Calculate cull face based on quad orientation + * + * @param face the Direction of the face + * @param min the minimum coordinates of the face + * @param max the maximum coordinates of the face + * @return the cull face Direction, or null if none + */ + private static Direction calculateCullFace(Direction face, Vector3f min, Vector3f max) { + // Currently disabled + return null; + } + + // Helper records for clean data passing + private record UVSize(float u, float v) { + } + + private record UVBounds(float uMin, float vMin, float uMax, float vMax) { + } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/modelstuff/TransformCalculator.java b/src/main/java/com/litehed/hytalemodels/blockymodel/TransformCalculator.java similarity index 96% rename from src/main/java/com/litehed/hytalemodels/modelstuff/TransformCalculator.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/TransformCalculator.java index 7bbf2c3..3a57667 100644 --- a/src/main/java/com/litehed/hytalemodels/modelstuff/TransformCalculator.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/TransformCalculator.java @@ -1,101 +1,101 @@ -package com.litehed.hytalemodels.modelstuff; - -import com.mojang.math.Transformation; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -public class TransformCalculator { - - private static final float POSITION_SCALE = 32.0f; // Convert from model units to block units - private static final float POSITION_OFFSET_Y = 16.0f; // Y-axis offset - - /** - * Calculate the world-space position of a node - * Recursively adds parent positions and applies parent orientations - * - * @param node the node to calculate the world position for - * @return the world-space position of the node - */ - public static Vector3f calculateWorldPosition(BlockyModelGeometry.BlockyNode node) { - Vector3f worldPos = new Vector3f(node.getPosition()); - - BlockyModelGeometry.BlockyNode current = node.getParent(); - while (current != null) { - Quaternionf parentOrientation = new Quaternionf(current.getOrientation()); - parentOrientation.transform(worldPos); - - worldPos.add(current.getPosition()); - if (current.hasShape()) { - Vector3f parentOffset = new Vector3f(current.getShape().getOffset()); - parentOrientation.transform(parentOffset); - worldPos.add(parentOffset); - } - - current = current.getParent(); - } - - return worldPos; - } - - /** - * Calculate the world-space orientation of a node - * Recursively combines parent orientations - * - * @param node the node to calculate the world orientation for - * @return the world-space orientation of the node - */ - public static Quaternionf calculateWorldOrientation(BlockyModelGeometry.BlockyNode node) { - Quaternionf worldRot = new Quaternionf(node.getOrientation()); - - BlockyModelGeometry.BlockyNode current = node.getParent(); - while (current != null) { - // Compose rotations: parent * child - worldRot = new Quaternionf(current.getOrientation()).mul(worldRot); - current = current.getParent(); - } - - return worldRot; - } - - /** - * Create a Transformation for a node given its world position, shape offset, and world orientation - * - * @param worldPosition the world-space position of the node - * @param shapeOffset the offset of the shape relative to the node's position - * @param worldOrientation the world-space orientation of the node - * @return a Transformation object representing the node's transform in world space - */ - public static Transformation createNodeTransform(Vector3f worldPosition, Vector3f shapeOffset, - Quaternionf worldOrientation) { - - Vector3f rotatedOffset = new Vector3f(shapeOffset); - worldOrientation.transform(rotatedOffset); - - // Calculate center position in block coordinates - float centerX = (worldPosition.x + rotatedOffset.x) / POSITION_SCALE; - float centerY = (worldPosition.y + rotatedOffset.y - POSITION_OFFSET_Y) / POSITION_SCALE; - float centerZ = (worldPosition.z + rotatedOffset.z) / POSITION_SCALE; - - return new Transformation( - new Vector3f(centerX, centerY, centerZ), // Translation - worldOrientation, // Rotation - new Vector3f(1), // Scale (no scaling) - null // No additional rotation - ); - } - - /** - * Calculate half-sizes for a shape given its full size - * - * @param size the full size of the shape - * @return the half-sizes of the shape - */ - public static Vector3f calculateHalfSizes(Vector3f size) { - return new Vector3f( - (size.x / 2) / POSITION_SCALE, - (size.y / 2) / POSITION_SCALE, - (size.z / 2) / POSITION_SCALE - ); - } - +package com.litehed.hytalemodels.blockymodel; + +import com.mojang.math.Transformation; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +public class TransformCalculator { + + private static final float POSITION_SCALE = 32.0f; // Convert from model units to block units + private static final float POSITION_OFFSET_Y = 16.0f; // Y-axis offset + + /** + * Calculate the world-space position of a node + * Recursively adds parent positions and applies parent orientations + * + * @param node the node to calculate the world position for + * @return the world-space position of the node + */ + public static Vector3f calculateWorldPosition(BlockyModelGeometry.BlockyNode node) { + Vector3f worldPos = new Vector3f(node.getPosition()); + + BlockyModelGeometry.BlockyNode current = node.getParent(); + while (current != null) { + Quaternionf parentOrientation = new Quaternionf(current.getOrientation()); + parentOrientation.transform(worldPos); + + worldPos.add(current.getPosition()); + if (current.hasShape()) { + Vector3f parentOffset = new Vector3f(current.getShape().getOffset()); + parentOrientation.transform(parentOffset); + worldPos.add(parentOffset); + } + + current = current.getParent(); + } + + return worldPos; + } + + /** + * Calculate the world-space orientation of a node + * Recursively combines parent orientations + * + * @param node the node to calculate the world orientation for + * @return the world-space orientation of the node + */ + public static Quaternionf calculateWorldOrientation(BlockyModelGeometry.BlockyNode node) { + Quaternionf worldRot = new Quaternionf(node.getOrientation()); + + BlockyModelGeometry.BlockyNode current = node.getParent(); + while (current != null) { + // Compose rotations: parent * child + worldRot = new Quaternionf(current.getOrientation()).mul(worldRot); + current = current.getParent(); + } + + return worldRot; + } + + /** + * Create a Transformation for a node given its world position, shape offset, and world orientation + * + * @param worldPosition the world-space position of the node + * @param shapeOffset the offset of the shape relative to the node's position + * @param worldOrientation the world-space orientation of the node + * @return a Transformation object representing the node's transform in world space + */ + public static Transformation createNodeTransform(Vector3f worldPosition, Vector3f shapeOffset, + Quaternionf worldOrientation) { + + Vector3f rotatedOffset = new Vector3f(shapeOffset); + worldOrientation.transform(rotatedOffset); + + // Calculate center position in block coordinates + float centerX = (worldPosition.x + rotatedOffset.x) / POSITION_SCALE; + float centerY = (worldPosition.y + rotatedOffset.y - POSITION_OFFSET_Y) / POSITION_SCALE; + float centerZ = (worldPosition.z + rotatedOffset.z) / POSITION_SCALE; + + return new Transformation( + new Vector3f(centerX, centerY, centerZ), // Translation + worldOrientation, // Rotation + new Vector3f(1), // Scale (no scaling) + null // No additional rotation + ); + } + + /** + * Calculate half-sizes for a shape given its full size + * + * @param size the full size of the shape + * @return the half-sizes of the shape + */ + public static Vector3f calculateHalfSizes(Vector3f size) { + return new Vector3f( + (size.x / 2) / POSITION_SCALE, + (size.y / 2) / POSITION_SCALE, + (size.z / 2) / POSITION_SCALE + ); + } + } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java b/src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java new file mode 100644 index 0000000..df2e6a5 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java @@ -0,0 +1,23 @@ +package com.litehed.hytalemodels.init; + +import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blocks.entity.AnimatedChestBlockEntity; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.neoforged.neoforge.registries.DeferredRegister; + +import java.util.function.Supplier; + +public class BlockEntityInit { + + public static final DeferredRegister> BLOCK_ENTITIES = + DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, HytaleModelLoader.MODID); + + public static final Supplier> CHEST_TEST_ENT = BLOCK_ENTITIES.register( + "hytale_chest", () -> new BlockEntityType<>( + AnimatedChestBlockEntity::new, + false, + BlockInit.SMALL_CHEST.get() + ) + ); +} diff --git a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java index dab1ccf..ae6344a 100644 --- a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java @@ -1,6 +1,7 @@ package com.litehed.hytalemodels.init; import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blocks.HytaleChest; import com.litehed.hytalemodels.blocks.HytaleTestBlock; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockBehaviour; @@ -16,6 +17,7 @@ public class BlockInit { public static final DeferredBlock BED = BLOCKS.registerSimpleBlock("bed", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock COFFIN = BLOCKS.registerSimpleBlock("coffin", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock SLOPE = BLOCKS.registerBlock("slope", HytaleTestBlock::new); + public static final DeferredBlock SMALL_CHEST = BLOCKS.registerBlock("chest_small", HytaleChest::new); public static final DeferredBlock CHAIR = BLOCKS.registerSimpleBlock("chair", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock TABLE = BLOCKS.registerSimpleBlock("table", () -> BlockBehaviour.Properties.of().noOcclusion()); } diff --git a/src/main/java/com/litehed/hytalemodels/init/ItemInit.java b/src/main/java/com/litehed/hytalemodels/init/ItemInit.java index e9044e0..dcabd6a 100644 --- a/src/main/java/com/litehed/hytalemodels/init/ItemInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/ItemInit.java @@ -14,6 +14,7 @@ public class ItemInit { public static final DeferredItem BED = ITEMS.registerSimpleBlockItem("bed", BlockInit.BED); public static final DeferredItem COFFIN = ITEMS.registerSimpleBlockItem("coffin", BlockInit.COFFIN); public static final DeferredItem SLOPE = ITEMS.registerSimpleBlockItem("slope", BlockInit.SLOPE); + public static final DeferredItem SMALL_CHEST = ITEMS.registerSimpleBlockItem("chest_small", BlockInit.SMALL_CHEST); public static final DeferredItem CHAIR = ITEMS.registerSimpleBlockItem("chair", BlockInit.CHAIR); public static final DeferredItem TABLE = ITEMS.registerSimpleBlockItem("table", BlockInit.TABLE); diff --git a/src/main/resources/assets/hytalemodelloader/blockstates/chest_small.json b/src/main/resources/assets/hytalemodelloader/blockstates/chest_small.json new file mode 100644 index 0000000..8d1524d --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/blockstates/chest_small.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "hytalemodelloader:block/chest_small", + "y": 180 + }, + "facing=east": { + "model": "hytalemodelloader:block/chest_small", + "y": 270 + }, + "facing=south": { + "model": "hytalemodelloader:block/chest_small" + }, + "facing=west": { + "model": "hytalemodelloader:block/chest_small", + "y": 90 + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/items/chest_small.json b/src/main/resources/assets/hytalemodelloader/items/chest_small.json new file mode 100644 index 0000000..7a83a3d --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/items/chest_small.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "hytalemodelloader:item/chest_small" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/block/chest_small.json b/src/main/resources/assets/hytalemodelloader/models/block/chest_small.json new file mode 100644 index 0000000..bfb62af --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/block/chest_small.json @@ -0,0 +1,9 @@ +{ + "loader": "hytalemodelloader:blockymodel_loader", + "model": "hytalemodelloader:models/chest_small.blockymodel", + "render_type": "minecraft:cutout", + "textures": { + "texture": "hytalemodelloader:block/chest_small_texture", + "particle": "hytalemodelloader:block/chest_small_texture" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/item/chest_small.json b/src/main/resources/assets/hytalemodelloader/models/item/chest_small.json new file mode 100644 index 0000000..4d5cea5 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/item/chest_small.json @@ -0,0 +1,3 @@ +{ + "parent": "hytalemodelloader:block/chest_small" +} \ No newline at end of file diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml index bb98394..c5e16be 100644 --- a/src/main/templates/META-INF/neoforge.mods.toml +++ b/src/main/templates/META-INF/neoforge.mods.toml @@ -40,7 +40,7 @@ authors = "litehed" # The description text for the mod (multi line!) (#mandatory) description = ''' -Hytale Model Loader enables importing .blockymodel models from Hytale into Minecraft. Simply reference this loader in your model file and point to the blockymodel location to utilize this mod. Animation and entity support will come in the future. +Hytale Model Loader enables importing .blockymodel models and their respective .blockyanim animations from Hytale into Minecraft. Simply reference this loader in your model file and point to the blockymodel location to utilize this mod. Entity support will come in the future. ''' # The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. From f5dc9b463360c042f931b5c9d6480dc565d02a01 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:35:41 -0500 Subject: [PATCH 02/17] Stitched code together to get basic anims working --- .../hytalemodels/HytaleModelLoaderClient.java | 4 +- .../hytalemodels/blocks/HytaleChest.java | 7 +- .../entity/AnimatedBlockEntityRenderer.java | 13 +- .../entity/DirectQuadAnimatedRenderer.java | 316 ++++++++++++++++++ .../blockymodel/BlockyModelGeometry.java | 4 + .../animations/AnimatedUVCalculator.java | 80 +++++ 6 files changed, 411 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/DirectQuadAnimatedRenderer.java create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java diff --git a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java index 598f345..8967790 100644 --- a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java +++ b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java @@ -1,6 +1,6 @@ package com.litehed.hytalemodels; -import com.litehed.hytalemodels.blocks.entity.AnimatedBlockEntityRenderer; +import com.litehed.hytalemodels.blocks.entity.DirectQuadAnimatedRenderer; import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; import com.litehed.hytalemodels.init.BlockEntityInit; import net.neoforged.api.distmarker.Dist; @@ -38,6 +38,6 @@ public static void onRegisterReloadListeners(AddClientReloadListenersEvent event @SubscribeEvent public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) { - event.registerBlockEntityRenderer(BlockEntityInit.CHEST_TEST_ENT.get(), AnimatedBlockEntityRenderer::new); + event.registerBlockEntityRenderer(BlockEntityInit.CHEST_TEST_ENT.get(), DirectQuadAnimatedRenderer::new); } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java index 178e8e4..853f9ab 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java @@ -1,12 +1,12 @@ package com.litehed.hytalemodels.blocks; -import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blocks.entity.AnimatedChestBlockEntity; import net.minecraft.core.BlockPos; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; @@ -36,4 +36,9 @@ protected InteractionResult useWithoutItem(BlockState state, Level level, BlockP } return InteractionResult.SUCCESS; } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java index df30d7e..f9d6a58 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java @@ -2,27 +2,20 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.model.geom.EntityModelSet; import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.client.renderer.feature.ModelFeatureRenderer; import net.minecraft.client.renderer.state.CameraRenderState; import net.minecraft.client.resources.model.MaterialSet; -import net.minecraft.resources.Identifier; import net.minecraft.world.phys.Vec3; public class AnimatedBlockEntityRenderer implements BlockEntityRenderer { private final MaterialSet materials; - private final EntityModelSet entityModelSet; - - private static final Identifier CHEST_OPEN_ANIM = - Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "animations/chest_small/chest_open.blockyanim"); public AnimatedBlockEntityRenderer(BlockEntityRendererProvider.Context context) { this.materials = context.materials(); - this.entityModelSet = context.entityModelSet(); } @Override @@ -32,6 +25,8 @@ public AnimatedChestRenderState createRenderState() { @Override public void extractRenderState(AnimatedChestBlockEntity blockEntity, AnimatedChestRenderState renderState, float partialTick, Vec3 cameraPosition, ModelFeatureRenderer.CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(blockEntity, renderState, partialTick, cameraPosition, breakProgress); + renderState.modelName = blockEntity.getModelName(); renderState.isOpen = blockEntity.isOpen(); renderState.animationTick = blockEntity.getAnimationTick(); @@ -45,8 +40,6 @@ public void extractRenderState(AnimatedChestBlockEntity blockEntity, AnimatedChe @Override public void submit(AnimatedChestRenderState renderState, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) { - if (renderState.modelName == null) { - return; - } + HytaleModelLoader.LOGGER.debug("MEOW"); } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/DirectQuadAnimatedRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/DirectQuadAnimatedRenderer.java new file mode 100644 index 0000000..e53f7e2 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/DirectQuadAnimatedRenderer.java @@ -0,0 +1,316 @@ +package com.litehed.hytalemodels.blocks.entity; + +import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; +import com.litehed.hytalemodels.blockymodel.TransformCalculator; +import com.litehed.hytalemodels.blockymodel.animations.AnimatedUVCalculator; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.CameraRenderState; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.Material; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DirectQuadAnimatedRenderer implements BlockEntityRenderer { + + private final Map geometryCache = new HashMap<>(); + + private static final Material CHEST_TEXTURE_MATERIAL = + new Material(TextureAtlas.LOCATION_BLOCKS, + Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "block/chest_small_texture")); + + // Model location + private static final Identifier CHEST_MODEL = + Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "models/chest_small.blockymodel"); + + public DirectQuadAnimatedRenderer(BlockEntityRendererProvider.Context context) { + } + + @Override + public AnimatedChestRenderState createRenderState() { + return new AnimatedChestRenderState(); + } + + @Override + public void extractRenderState(AnimatedChestBlockEntity blockEntity, AnimatedChestRenderState renderState, + float partialTick, Vec3 cameraPosition, + ModelFeatureRenderer.CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(blockEntity, renderState, partialTick, cameraPosition, breakProgress); + renderState.modelName = blockEntity.getModelName(); + renderState.isOpen = blockEntity.isOpen(); + renderState.animationTick = blockEntity.getAnimationTick(); + renderState.partialTick = partialTick; + + if (blockEntity.getLevel() != null) { + renderState.ageInTicks = blockEntity.getLevel().getGameTime() + partialTick; + } + } + + @Override + public void submit(AnimatedChestRenderState renderState, PoseStack poseStack, + SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) { + + + if (renderState.modelName == null) { + return; + } + + BlockyModelGeometry geometry = getOrLoadGeometry(CHEST_MODEL); + if (geometry == null) { + return; + } + + TextureAtlasSprite sprite = Minecraft.getInstance() + .getAtlasManager().get(CHEST_TEXTURE_MATERIAL); + + poseStack.pushPose(); + + poseStack.translate(0.5, 0.5, 0.5); + + Map nodeTransforms = calculateNodeTransforms(renderState, geometry); + List nodes = geometry.getNodes(); + for (BlockyModelGeometry.BlockyNode node : nodes) { + if (node.hasShape()) { + renderNode(poseStack, submitNodeCollector, node, sprite, nodeTransforms, renderState); + + } + } + + poseStack.popPose(); + } + + // Test anims + private Map calculateNodeTransforms(AnimatedChestRenderState renderState, + BlockyModelGeometry geometry) { + Map transforms = new HashMap<>(); + + float time = renderState.ageInTicks * 0.1f; + float yOffset = (float) Math.sin(time) * 3.0f; + NodeTransform lidTransform = new NodeTransform( + new Vector3f(0, yOffset, 0), + new Quaternionf(), + new Vector3f(1, 1, 1) + ); + + transforms.put("Lid", lidTransform); + + return transforms; + } + + private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, + BlockyModelGeometry.BlockyNode node, TextureAtlasSprite sprite, + Map nodeTransforms, AnimatedChestRenderState renderState) { + + poseStack.pushPose(); + + applyNodeTransform(poseStack, node, nodeTransforms); + + BlockyModelGeometry.BlockyShape shape = node.getShape(); + + Vector3f halfSizes = TransformCalculator.calculateHalfSizes(shape.getSize()); + Vector3f min = new Vector3f(-halfSizes.x, -halfSizes.y, -halfSizes.z); + Vector3f max = new Vector3f(halfSizes.x, halfSizes.y, halfSizes.z); + + for (Direction direction : Direction.values()) { + if (!shape.hasTextureLayout(direction)) { + continue; + } + + BlockyModelGeometry.FaceTextureLayout texLayout = shape.getTextureLayout(direction); + + RenderType renderType = RenderTypes.cutoutMovingBlock(); + + collector.submitCustomGeometry(poseStack, renderType, (pose, buffer) -> + renderQuad(buffer, pose, direction, min, max, sprite, texLayout, shape.getOriginalSize(), renderState)); + + // Render backface if double-sided + if (shape.isDoubleSided()) { + collector.submitCustomGeometry(poseStack, renderType, (pose, buffer) -> + renderQuadReversed(buffer, pose, direction, min, max, sprite, texLayout, shape.getOriginalSize(), renderState)); + } + } + + poseStack.popPose(); + } + + + private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyNode node, + Map animTransforms) { + Vector3f worldPos = TransformCalculator.calculateWorldPosition(node); + Quaternionf worldRot = TransformCalculator.calculateWorldOrientation(node); + + Vector3f shapeOffset = node.getShape().getOffset(); + Vector3f rotatedOffset = new Vector3f(shapeOffset); + worldRot.transform(rotatedOffset); + + float centerX = (worldPos.x + rotatedOffset.x) / 32.0f; + float centerY = ((worldPos.y + rotatedOffset.y) - 16.0f) / 32.0f; + float centerZ = (worldPos.z + rotatedOffset.z) / 32.0f; + + poseStack.translate(centerX, centerY, centerZ); + + poseStack.mulPose(worldRot); + + NodeTransform animTransform = animTransforms.get(node.getName()); + if (animTransform != null) { + poseStack.translate( + animTransform.position.x / 16.0f, + animTransform.position.y / 16.0f, + animTransform.position.z / 16.0f + ); + poseStack.mulPose(animTransform.rotation); + poseStack.scale(animTransform.scale.x, animTransform.scale.y, animTransform.scale.z); + } + } + + private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction direction, + Vector3f min, Vector3f max, TextureAtlasSprite sprite, + BlockyModelGeometry.FaceTextureLayout texLayout, Vector3f originalSize, + AnimatedChestRenderState renderState) { + + Matrix4f poseMatrix = pose.pose(); + Matrix3f normalMatrix = pose.normal(); + + Vector3f n = new Vector3f(direction.getStepX(), direction.getStepY(), direction.getStepZ()); + normalMatrix.transform(n); + + float[][] uvCoords = AnimatedUVCalculator.calculateUVs(direction, texLayout, originalSize, sprite); + + Vector3f[] vertices = getQuadBuilderVertices(direction, min, max); + + for (int i = 0; i < 4; i++) { + vertex(buffer, poseMatrix, n, vertices[i].x, vertices[i].y, vertices[i].z, + uvCoords[i][0], uvCoords[i][1], renderState.lightCoords); + } + } + + private void renderQuadReversed(VertexConsumer buffer, PoseStack.Pose pose, Direction direction, + Vector3f min, Vector3f max, TextureAtlasSprite sprite, + BlockyModelGeometry.FaceTextureLayout texLayout, Vector3f originalSize, + AnimatedChestRenderState renderState) { + + Matrix4f poseMatrix = pose.pose(); + Matrix3f normalMatrix = pose.normal(); + + Vector3f n = new Vector3f(-direction.getStepX(), -direction.getStepY(), -direction.getStepZ()); + normalMatrix.transform(n); + + float[][] uvCoords = AnimatedUVCalculator.calculateUVs(direction, texLayout, originalSize, sprite); + + Vector3f[] vertices = getQuadBuilderVertices(direction, min, max); + + for (int i = 3; i >= 0; i--) { + vertex(buffer, poseMatrix, n, vertices[i].x, vertices[i].y, vertices[i].z, + uvCoords[i][0], uvCoords[i][1], renderState.lightCoords); + } + } + + private Vector3f[] getQuadBuilderVertices(Direction face, Vector3f min, Vector3f max) { + float x0 = min.x, y0 = min.y, z0 = min.z; + float x1 = max.x, y1 = max.y, z1 = max.z; + + return switch (face) { + case DOWN -> new Vector3f[]{ + new Vector3f(x0, y0, z0), new Vector3f(x1, y0, z0), + new Vector3f(x1, y0, z1), new Vector3f(x0, y0, z1) + }; + case UP -> new Vector3f[]{ + new Vector3f(x0, y1, z1), new Vector3f(x1, y1, z1), + new Vector3f(x1, y1, z0), new Vector3f(x0, y1, z0) + }; + case NORTH -> new Vector3f[]{ + new Vector3f(x1, y0, z0), new Vector3f(x0, y0, z0), + new Vector3f(x0, y1, z0), new Vector3f(x1, y1, z0) + }; + case SOUTH -> new Vector3f[]{ + new Vector3f(x0, y0, z1), new Vector3f(x1, y0, z1), + new Vector3f(x1, y1, z1), new Vector3f(x0, y1, z1) + }; + case WEST -> new Vector3f[]{ + new Vector3f(x0, y0, z0), new Vector3f(x0, y0, z1), + new Vector3f(x0, y1, z1), new Vector3f(x0, y1, z0) + }; + case EAST -> new Vector3f[]{ + new Vector3f(x1, y0, z1), new Vector3f(x1, y0, z0), + new Vector3f(x1, y1, z0), new Vector3f(x1, y1, z1) + }; + }; + } + + + private void vertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, + float x, float y, float z, float u, float v, int lightCoords) { + int blockLight = lightCoords & 0xFFFF; + int skyLight = (lightCoords >> 16) & 0xFFFF; + + buffer.addVertex(pose, x, y, z) + .setColor(255, 255, 255, 255) + .setUv(u, v) + .setUv2(blockLight, skyLight) + .setNormal(normal.x, normal.y, normal.z); + } + + + private BlockyModelGeometry getOrLoadGeometry(Identifier modelLocation) { + if (geometryCache.containsKey(modelLocation.toString())) { + return geometryCache.get(modelLocation.toString()); + } + + try { + BlockyModelGeometry geometry = BlockyModelLoader.INSTANCE.loadGeometry( + new BlockyModelGeometry.Settings(modelLocation) + ); + geometryCache.put(modelLocation.toString(), geometry); + return geometry; + } catch (Exception e) { + return null; + } + } + + + private static class NodeTransform { + final Vector3f position; + final Quaternionf rotation; + final Vector3f scale; + + NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { + this.position = position; + this.rotation = rotation; + this.scale = scale; + } + } +} + + + + + + + + + + + + + + + diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java index 1a430f3..bca960f 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java @@ -171,6 +171,10 @@ private void addQuadToBuilder(QuadCollection.Builder builder, Pair getNodes() { + return nodes; + } + public record Settings(Identifier modelLocation) { public Identifier modelLocation() { return this.modelLocation; diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java new file mode 100644 index 0000000..aaf6db2 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java @@ -0,0 +1,80 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.QuadBuilder; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.Direction; +import org.joml.Vector3f; + +import java.lang.reflect.Method; + +public class AnimatedUVCalculator { + + private static Method calculateUVCoordinatesMethod; + + // Static block to initialize the reflection method + static { + try { + calculateUVCoordinatesMethod = QuadBuilder.class.getDeclaredMethod( + "calculateUVCoordinates", + Direction.class, + BlockyModelGeometry.FaceTextureLayout.class, + Vector3f.class, + TextureAtlasSprite.class + ); + calculateUVCoordinatesMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + HytaleModelLoader.LOGGER.error("Failed to find QuadBuilder.calculateUVCoordinates method: {}", e.getMessage()); + } + } + + /** + * Calculate UV coordinates for a face using QuadBuilder's logic + * Returns a 2D array where each element is [u, v] for each of the 4 vertices + * + * @param direction the face direction + * @param texLayout the texture layout for this face + * @param originalSize the original size of the shape + * @param sprite the texture sprite + * @return 2D array of UV coordinates [[u0,v0], [u1,v1], [u2,v2], [u3,v3]] + */ + public static float[][] calculateUVs(Direction direction, BlockyModelGeometry.FaceTextureLayout texLayout, + Vector3f originalSize, TextureAtlasSprite sprite) { + if (calculateUVCoordinatesMethod != null) { + try { + return (float[][]) calculateUVCoordinatesMethod.invoke( + null, direction, texLayout, originalSize, sprite + ); + } catch (Exception e) { + HytaleModelLoader.LOGGER.error("Failed to invoke QuadBuilder.calculateUVCoordinates: {}", e.getMessage()); + } + } + + return calculateUVsFallback(direction, texLayout, originalSize, sprite); + } + + /** + * Fallback method to calculate UVs if reflection fails. This is a simplified version and may not match QuadBuilder's logic exactly. + * + * @param direction the face direction + * @param texLayout the texture layout for this face + * @param originalSize the original size of the shape + * @param sprite the texture sprite + * @return 2D array of UV coordinates [[u0,v0], [u1,v1], [u2,v2], [u3,v3]] + */ + private static float[][] calculateUVsFallback(Direction direction, BlockyModelGeometry.FaceTextureLayout texLayout, + Vector3f originalSize, TextureAtlasSprite sprite) { + float u0 = sprite.getU0(); + float u1 = sprite.getU1(); + float v0 = sprite.getV0(); + float v1 = sprite.getV1(); + + return new float[][]{ + {u0, v0}, + {u1, v0}, + {u1, v1}, + {u0, v1} + }; + } +} \ No newline at end of file From a091502edb1e06ed4f6325cd4b3e4514442e1bde Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:56:16 -0500 Subject: [PATCH 03/17] Refactored and made adding animated blocks easier --- README.md | 4 +- .../hytalemodels/HytaleModelLoaderClient.java | 4 +- .../entity/AnimatedBlockEntityRenderer.java | 45 ---- .../entity/AnimatedChestBlockEntity.java | 130 ++++++----- .../entity/AnimatedChestRenderState.java | 20 +- .../blocks/entity/AnimatedChestRenderer.java | 44 ++++ .../entity/AnimatedHytaleBlockEntity.java | 27 --- .../blocks/entity/HytaleBlockEntity.java | 57 +++++ ...er.java => HytaleBlockEntityRenderer.java} | 211 +++++++----------- .../blocks/entity/HytaleRenderState.java | 10 + .../blocks/entity/NodeTransform.java | 44 ++++ .../hytalemodels/blockymodel/QuadBuilder.java | 2 +- .../animations/AnimatedUVCalculator.java | 80 ------- 13 files changed, 318 insertions(+), 360 deletions(-) delete mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java delete mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntity.java rename src/main/java/com/litehed/hytalemodels/blocks/entity/{DirectQuadAnimatedRenderer.java => HytaleBlockEntityRenderer.java} (57%) create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java delete mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java diff --git a/README.md b/README.md index 2321bcf..529a697 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,11 @@ Models are defined using `.blockymodel` files (custom binary/text format) and re ### v1.1.0 - [x] Check item and block scaling/translating using model json +- [x] Implement custom BlockEntities for animation support since baked models cannot - [ ] Add parser for animation support `.blockyanim` -- [ ] Load animations in for blocks and items +- [ ] Load animations in for blocks - [ ] Create animation system to actually play and time these animations +- [ ] Add wiki to show how to use different parts of the mod - [ ] Fix and clean up code ### v2.0.0 diff --git a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java index 8967790..9a335fc 100644 --- a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java +++ b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java @@ -1,6 +1,6 @@ package com.litehed.hytalemodels; -import com.litehed.hytalemodels.blocks.entity.DirectQuadAnimatedRenderer; +import com.litehed.hytalemodels.blocks.entity.AnimatedChestRenderer; import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; import com.litehed.hytalemodels.init.BlockEntityInit; import net.neoforged.api.distmarker.Dist; @@ -38,6 +38,6 @@ public static void onRegisterReloadListeners(AddClientReloadListenersEvent event @SubscribeEvent public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) { - event.registerBlockEntityRenderer(BlockEntityInit.CHEST_TEST_ENT.get(), DirectQuadAnimatedRenderer::new); + event.registerBlockEntityRenderer(BlockEntityInit.CHEST_TEST_ENT.get(), AnimatedChestRenderer::new); } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java deleted file mode 100644 index f9d6a58..0000000 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedBlockEntityRenderer.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.litehed.hytalemodels.blocks.entity; - -import com.litehed.hytalemodels.HytaleModelLoader; -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.renderer.SubmitNodeCollector; -import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; -import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; -import net.minecraft.client.renderer.feature.ModelFeatureRenderer; -import net.minecraft.client.renderer.state.CameraRenderState; -import net.minecraft.client.resources.model.MaterialSet; -import net.minecraft.world.phys.Vec3; - -public class AnimatedBlockEntityRenderer implements BlockEntityRenderer { - - private final MaterialSet materials; - - public AnimatedBlockEntityRenderer(BlockEntityRendererProvider.Context context) { - this.materials = context.materials(); - } - - @Override - public AnimatedChestRenderState createRenderState() { - return new AnimatedChestRenderState(); - } - - @Override - public void extractRenderState(AnimatedChestBlockEntity blockEntity, AnimatedChestRenderState renderState, float partialTick, Vec3 cameraPosition, ModelFeatureRenderer.CrumblingOverlay breakProgress) { - BlockEntityRenderer.super.extractRenderState(blockEntity, renderState, partialTick, cameraPosition, breakProgress); - - renderState.modelName = blockEntity.getModelName(); - renderState.isOpen = blockEntity.isOpen(); - renderState.animationTick = blockEntity.getAnimationTick(); - renderState.partialTick = partialTick; - - if (blockEntity.getLevel() != null) { - renderState.ageInTicks = blockEntity.getLevel().getGameTime() + partialTick; - } - } - - - @Override - public void submit(AnimatedChestRenderState renderState, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) { - HytaleModelLoader.LOGGER.debug("MEOW"); - } -} diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java index 8315941..b848ae2 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java @@ -10,66 +10,65 @@ import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; -public class AnimatedChestBlockEntity extends AnimatedHytaleBlockEntity { +public class AnimatedChestBlockEntity extends HytaleBlockEntity { + + private static final String NBT_KEY = "ChestData"; + private static final String NBT_IS_OPEN = "IsOpen"; + private static final String NBT_ANIM_TICK = "AnimationTick"; - // Track whether chest is open (for server-side state) private boolean isOpen = false; private final AnimationState openAnimationState = new AnimationState(); private int animationTick = 0; - // Animation name for opening - private static final String OPEN_ANIMATION = "chest_open"; - - // Animation name for closing (if we have one) - private static final String CLOSE_ANIMATION = "chest_close"; - public AnimatedChestBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityInit.CHEST_TEST_ENT.get(), pos, state, "chest_small"); } + /** + * Opens the chest and starts the opening animation. + */ public void openChest() { - if (!this.isOpen) { - this.isOpen = true; - - // Start the animation on client side - if (this.level != null && this.level.isClientSide()) { - long gameTime = this.level.getGameTime(); - this.openAnimationState.start((int) gameTime); - HytaleModelLoader.LOGGER.info("Client: Starting open animation at game time: {}", gameTime); - } + if (!isOpen) { + isOpen = true; - HytaleModelLoader.LOGGER.info("Chest opened at position: {}", this.worldPosition); - this.setChanged(); - - // Sync to clients - if (this.level != null && !this.level.isClientSide()) { - this.level.blockEntityChanged(this.getBlockPos()); - HytaleModelLoader.LOGGER.info("Server: Syncing open animation to clients"); + if (level != null && level.isClientSide()) { + long gameTime = level.getGameTime(); + openAnimationState.start((int) gameTime); + HytaleModelLoader.LOGGER.debug("Client: Starting chest open animation at {}", gameTime); } + + setChanged(); + syncToClients(); } } + /** + * Closes the chest and stops the opening animation. + */ public void closeChest() { - if (this.isOpen) { - this.isOpen = false; + if (isOpen) { + isOpen = false; - // Stop the animation on client side - if (this.level != null && this.level.isClientSide()) { - this.openAnimationState.stop(); - HytaleModelLoader.LOGGER.info("Client: Stopping open animation"); + if (level != null && level.isClientSide()) { + openAnimationState.stop(); + HytaleModelLoader.LOGGER.debug("Client: Stopping chest open animation"); } - HytaleModelLoader.LOGGER.info("Chest closed at position: {}", this.worldPosition); - this.setChanged(); - - // Sync to clients - if (this.level != null && !this.level.isClientSide()) { - this.level.blockEntityChanged(this.getBlockPos()); - HytaleModelLoader.LOGGER.info("Server: Syncing close animation to clients"); - } + setChanged(); + syncToClients(); } } + /** + * Toggles the chest open/closed state. + */ + public void toggleChest() { + if (isOpen) { + closeChest(); + } else { + openChest(); + } + } public boolean isOpen() { return isOpen; @@ -79,17 +78,19 @@ public AnimationState getOpenAnimationState() { return openAnimationState; } + @Override public int getAnimationTick() { return animationTick; } + /** + * Server/client tick for animation updates. + */ public static void tick(Level level, BlockPos pos, BlockState state, AnimatedChestBlockEntity blockEntity) { if (level.isClientSide()) { - // Update animation tick counter if (blockEntity.isOpen) { blockEntity.animationTick++; } else { - // Optionally decay animation tick when closed if (blockEntity.animationTick > 0) { blockEntity.animationTick--; } @@ -98,42 +99,51 @@ public static void tick(Level level, BlockPos pos, BlockState state, AnimatedChe } @Override - protected void loadAdditional(ValueInput input) { - super.loadAdditional(input); - input.read("ChestData", CompoundTag.CODEC).ifPresent(chestTag -> { - if (chestTag.contains("IsOpen")) { - boolean wasOpen = this.isOpen; - this.isOpen = chestTag.getBoolean("IsOpen").get(); - - // If state changed, update animation on client - if (this.level != null && this.level.isClientSide() && wasOpen != this.isOpen) { - if (this.isOpen) { - this.openAnimationState.start((int) this.level.getGameTime()); + protected void loadAnimationData(ValueInput input) { + input.read(NBT_KEY, CompoundTag.CODEC).ifPresent(chestTag -> { + if (chestTag.contains(NBT_IS_OPEN)) { + boolean wasOpen = isOpen; + isOpen = chestTag.getBoolean(NBT_IS_OPEN).get(); + + // Update animation state on client + if (level != null && level.isClientSide() && wasOpen != isOpen) { + if (isOpen) { + openAnimationState.start((int) level.getGameTime()); } else { - this.openAnimationState.stop(); + openAnimationState.stop(); } } } - if (chestTag.contains("AnimationTick")) { - this.animationTick = chestTag.getInt("AnimationTick").get(); + + if (chestTag.contains(NBT_ANIM_TICK)) { + animationTick = chestTag.getInt(NBT_ANIM_TICK).get(); } }); } @Override - protected void saveAdditional(ValueOutput output) { - super.saveAdditional(output); + protected void saveAnimationData(ValueOutput output) { CompoundTag chestTag = new CompoundTag(); - chestTag.putBoolean("isOpen", isOpen); - chestTag.putInt("AnimationTick", animationTick); - output.store("ChestData", CompoundTag.CODEC, chestTag); + chestTag.putBoolean(NBT_IS_OPEN, isOpen); + chestTag.putInt(NBT_ANIM_TICK, animationTick); + output.store(NBT_KEY, CompoundTag.CODEC, chestTag); + } + + /** + * Sync the block entity state to clients. + */ + private void syncToClients() { + if (level != null && !level.isClientSide()) { + level.blockEntityChanged(getBlockPos()); + } } @Override public String toString() { return "AnimatedChestBlockEntity{" + - "pos=" + this.worldPosition + + "pos=" + worldPosition + ", isOpen=" + isOpen + + ", animationTick=" + animationTick + "}"; } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java index a21b3ba..ba1b083 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java @@ -1,23 +1,5 @@ package com.litehed.hytalemodels.blocks.entity; -import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; - -public class AnimatedChestRenderState extends BlockEntityRenderState { - - // Animation data - public String modelName; +public class AnimatedChestRenderState extends HytaleRenderState { public boolean isOpen; - public int animationTick; - - // Timing for animation - public float ageInTicks; - public float partialTick; - - public AnimatedChestRenderState() { - this.modelName = null; - this.isOpen = false; - this.animationTick = 0; - this.ageInTicks = 0; - this.partialTick = 0; - } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java new file mode 100644 index 0000000..656f3e8 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -0,0 +1,44 @@ +package com.litehed.hytalemodels.blocks.entity; + +import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import org.joml.Vector3f; + +import java.util.HashMap; +import java.util.Map; + +public class AnimatedChestRenderer extends HytaleBlockEntityRenderer { + + private static final float ANIMATION_SPEED = 0.1f; + private static final float MAX_LID_OFFSET = 3.0f; + + public AnimatedChestRenderer(BlockEntityRendererProvider.Context context) { + super(context); + } + + @Override + public AnimatedChestRenderState createRenderState() { + return new AnimatedChestRenderState(); + } + + @Override + protected void extractAdditionalRenderState(AnimatedChestBlockEntity blockEntity, + AnimatedChestRenderState renderState, + float partialTick) { + renderState.isOpen = blockEntity.isOpen(); + } + + @Override + protected Map calculateAnimationTransforms(AnimatedChestRenderState renderState, + BlockyModelGeometry geometry) { + Map transforms = new HashMap<>(); + + float time = renderState.ageInTicks * ANIMATION_SPEED; + float yOffset = (float) Math.sin(time) * MAX_LID_OFFSET; + + transforms.put("Lid", NodeTransform.translation(new Vector3f(0, yOffset, 0))); + + + return transforms; + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java deleted file mode 100644 index 0a41cc2..0000000 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedHytaleBlockEntity.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.litehed.hytalemodels.blocks.entity; - -import net.minecraft.core.BlockPos; -import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.entity.BlockEntityType; -import net.minecraft.world.level.block.state.BlockState; - -public class AnimatedHytaleBlockEntity extends BlockEntity { - private final String modelName; - - public AnimatedHytaleBlockEntity(BlockEntityType type, BlockPos pos, BlockState state, String modelName) { - super(type, pos, state); - this.modelName = modelName; - } - - public String getModelName() { - return modelName; - } - - @Override - public String toString() { - return "AnimatedHytaleBlockEntity{" + - "modelName='" + modelName + '\'' + - ", pos=" + this.worldPosition + - "}"; - } -} diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntity.java new file mode 100644 index 0000000..ed27651 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntity.java @@ -0,0 +1,57 @@ +package com.litehed.hytalemodels.blocks.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +public abstract class HytaleBlockEntity extends BlockEntity { + + private final String modelName; + + + public HytaleBlockEntity(BlockEntityType type, BlockPos pos, BlockState state, String modelName) { + super(type, pos, state); + this.modelName = modelName; + } + + + public String getModelName() { + return modelName; + } + + public int getAnimationTick() { + return 0; + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + loadAnimationData(input); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + saveAnimationData(output); + } + + + protected void loadAnimationData(ValueInput input) { + // Default implementation does nothing + } + + protected void saveAnimationData(ValueOutput output) { + // Default implementation does nothing + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "modelName='" + modelName + '\'' + + ", pos=" + worldPosition + + "}"; + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/DirectQuadAnimatedRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java similarity index 57% rename from src/main/java/com/litehed/hytalemodels/blocks/entity/DirectQuadAnimatedRenderer.java rename to src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index e53f7e2..2f7ddfb 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/DirectQuadAnimatedRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -3,8 +3,8 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; +import com.litehed.hytalemodels.blockymodel.QuadBuilder; import com.litehed.hytalemodels.blockymodel.TransformCalculator; -import com.litehed.hytalemodels.blockymodel.animations.AnimatedUVCalculator; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import net.minecraft.client.Minecraft; @@ -30,95 +30,87 @@ import java.util.List; import java.util.Map; -public class DirectQuadAnimatedRenderer implements BlockEntityRenderer { +public abstract class HytaleBlockEntityRenderer + implements BlockEntityRenderer { private final Map geometryCache = new HashMap<>(); - private static final Material CHEST_TEXTURE_MATERIAL = - new Material(TextureAtlas.LOCATION_BLOCKS, - Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "block/chest_small_texture")); - - // Model location - private static final Identifier CHEST_MODEL = - Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "models/chest_small.blockymodel"); - - public DirectQuadAnimatedRenderer(BlockEntityRendererProvider.Context context) { - } - - @Override - public AnimatedChestRenderState createRenderState() { - return new AnimatedChestRenderState(); + public HytaleBlockEntityRenderer(BlockEntityRendererProvider.Context context) { } @Override - public void extractRenderState(AnimatedChestBlockEntity blockEntity, AnimatedChestRenderState renderState, - float partialTick, Vec3 cameraPosition, - ModelFeatureRenderer.CrumblingOverlay breakProgress) { + public void extractRenderState(T blockEntity, S renderState, float partialTick, + Vec3 cameraPosition, ModelFeatureRenderer.CrumblingOverlay breakProgress) { BlockEntityRenderer.super.extractRenderState(blockEntity, renderState, partialTick, cameraPosition, breakProgress); + renderState.modelName = blockEntity.getModelName(); - renderState.isOpen = blockEntity.isOpen(); renderState.animationTick = blockEntity.getAnimationTick(); renderState.partialTick = partialTick; if (blockEntity.getLevel() != null) { renderState.ageInTicks = blockEntity.getLevel().getGameTime() + partialTick; } + + extractAdditionalRenderState(blockEntity, renderState, partialTick); } - @Override - public void submit(AnimatedChestRenderState renderState, PoseStack poseStack, - SubmitNodeCollector submitNodeCollector, CameraRenderState cameraRenderState) { + protected void extractAdditionalRenderState(T blockEntity, S renderState, float partialTick) { + } + @Override + public void submit(S renderState, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, + CameraRenderState cameraRenderState) { if (renderState.modelName == null) { return; } - BlockyModelGeometry geometry = getOrLoadGeometry(CHEST_MODEL); + Identifier modelLocation = getModelLocation(renderState.modelName); + BlockyModelGeometry geometry = getOrLoadGeometry(modelLocation); if (geometry == null) { + HytaleModelLoader.LOGGER.warn("Failed to load geometry for model: {}", modelLocation); return; } + Material textureMaterial = getTextureMaterial(renderState.modelName); TextureAtlasSprite sprite = Minecraft.getInstance() - .getAtlasManager().get(CHEST_TEXTURE_MATERIAL); + .getAtlasManager().get(textureMaterial); poseStack.pushPose(); + // Center the model in the block poseStack.translate(0.5, 0.5, 0.5); - Map nodeTransforms = calculateNodeTransforms(renderState, geometry); + // Calculate transforms for all nodes + Map nodeTransforms = calculateAnimationTransforms(renderState, geometry); + + // Render each node with its shape List nodes = geometry.getNodes(); for (BlockyModelGeometry.BlockyNode node : nodes) { if (node.hasShape()) { renderNode(poseStack, submitNodeCollector, node, sprite, nodeTransforms, renderState); - } } poseStack.popPose(); } - // Test anims - private Map calculateNodeTransforms(AnimatedChestRenderState renderState, - BlockyModelGeometry geometry) { - Map transforms = new HashMap<>(); - - float time = renderState.ageInTicks * 0.1f; - float yOffset = (float) Math.sin(time) * 3.0f; - NodeTransform lidTransform = new NodeTransform( - new Vector3f(0, yOffset, 0), - new Quaternionf(), - new Vector3f(1, 1, 1) - ); - transforms.put("Lid", lidTransform); + protected abstract Map calculateAnimationTransforms(S renderState, BlockyModelGeometry geometry); - return transforms; + + protected Identifier getModelLocation(String modelName) { + return Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "models/" + modelName + ".blockymodel"); + } + + protected Material getTextureMaterial(String modelName) { + return new Material(TextureAtlas.LOCATION_BLOCKS, + Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "block/" + modelName + "_texture")); } private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, BlockyModelGeometry.BlockyNode node, TextureAtlasSprite sprite, - Map nodeTransforms, AnimatedChestRenderState renderState) { + Map nodeTransforms, S renderState) { poseStack.pushPose(); @@ -130,6 +122,8 @@ private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, Vector3f min = new Vector3f(-halfSizes.x, -halfSizes.y, -halfSizes.z); Vector3f max = new Vector3f(halfSizes.x, halfSizes.y, halfSizes.z); + RenderType renderType = getRenderType(); + for (Direction direction : Direction.values()) { if (!shape.hasTextureLayout(direction)) { continue; @@ -137,21 +131,24 @@ private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, BlockyModelGeometry.FaceTextureLayout texLayout = shape.getTextureLayout(direction); - RenderType renderType = RenderTypes.cutoutMovingBlock(); - collector.submitCustomGeometry(poseStack, renderType, (pose, buffer) -> - renderQuad(buffer, pose, direction, min, max, sprite, texLayout, shape.getOriginalSize(), renderState)); + renderQuad(buffer, pose, direction, min, max, sprite, texLayout, + shape.getOriginalSize(), renderState, false)); // Render backface if double-sided if (shape.isDoubleSided()) { collector.submitCustomGeometry(poseStack, renderType, (pose, buffer) -> - renderQuadReversed(buffer, pose, direction, min, max, sprite, texLayout, shape.getOriginalSize(), renderState)); + renderQuad(buffer, pose, direction, min, max, sprite, texLayout, + shape.getOriginalSize(), renderState, true)); } } poseStack.popPose(); } + protected RenderType getRenderType() { + return RenderTypes.cutoutMovingBlock(); + } private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyNode node, Map animTransforms) { @@ -167,64 +164,67 @@ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyN float centerZ = (worldPos.z + rotatedOffset.z) / 32.0f; poseStack.translate(centerX, centerY, centerZ); - poseStack.mulPose(worldRot); NodeTransform animTransform = animTransforms.get(node.getName()); if (animTransform != null) { + Vector3f animPos = animTransform.position(); poseStack.translate( - animTransform.position.x / 16.0f, - animTransform.position.y / 16.0f, - animTransform.position.z / 16.0f + animPos.x / 16.0f, + animPos.y / 16.0f, + animPos.z / 16.0f ); - poseStack.mulPose(animTransform.rotation); - poseStack.scale(animTransform.scale.x, animTransform.scale.y, animTransform.scale.z); + poseStack.mulPose(animTransform.rotation()); + + Vector3f animScale = animTransform.scale(); + poseStack.scale(animScale.x, animScale.y, animScale.z); } } private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction direction, Vector3f min, Vector3f max, TextureAtlasSprite sprite, BlockyModelGeometry.FaceTextureLayout texLayout, Vector3f originalSize, - AnimatedChestRenderState renderState) { + S renderState, boolean reversed) { Matrix4f poseMatrix = pose.pose(); Matrix3f normalMatrix = pose.normal(); - Vector3f n = new Vector3f(direction.getStepX(), direction.getStepY(), direction.getStepZ()); - normalMatrix.transform(n); + int normalMult = reversed ? -1 : 1; + Vector3f normal = new Vector3f( + direction.getStepX() * normalMult, + direction.getStepY() * normalMult, + direction.getStepZ() * normalMult + ); + normalMatrix.transform(normal); - float[][] uvCoords = AnimatedUVCalculator.calculateUVs(direction, texLayout, originalSize, sprite); + float[][] uvCoords = QuadBuilder.calculateUVCoordinates(direction, texLayout, originalSize, sprite); - Vector3f[] vertices = getQuadBuilderVertices(direction, min, max); + Vector3f[] vertices = getQuadVertices(direction, min, max); - for (int i = 0; i < 4; i++) { - vertex(buffer, poseMatrix, n, vertices[i].x, vertices[i].y, vertices[i].z, - uvCoords[i][0], uvCoords[i][1], renderState.lightCoords); + if (reversed) { + for (int i = 3; i >= 0; i--) { + addVertex(buffer, poseMatrix, normal, vertices[i], uvCoords[i], renderState.lightCoords); + } + } else { + for (int i = 0; i < 4; i++) { + addVertex(buffer, poseMatrix, normal, vertices[i], uvCoords[i], renderState.lightCoords); + } } } - private void renderQuadReversed(VertexConsumer buffer, PoseStack.Pose pose, Direction direction, - Vector3f min, Vector3f max, TextureAtlasSprite sprite, - BlockyModelGeometry.FaceTextureLayout texLayout, Vector3f originalSize, - AnimatedChestRenderState renderState) { - - Matrix4f poseMatrix = pose.pose(); - Matrix3f normalMatrix = pose.normal(); - - Vector3f n = new Vector3f(-direction.getStepX(), -direction.getStepY(), -direction.getStepZ()); - normalMatrix.transform(n); - - float[][] uvCoords = AnimatedUVCalculator.calculateUVs(direction, texLayout, originalSize, sprite); - - Vector3f[] vertices = getQuadBuilderVertices(direction, min, max); + private void addVertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, + Vector3f vertex, float[] uv, int lightCoords) { + int blockLight = lightCoords & 0xFFFF; + int skyLight = (lightCoords >> 16) & 0xFFFF; - for (int i = 3; i >= 0; i--) { - vertex(buffer, poseMatrix, n, vertices[i].x, vertices[i].y, vertices[i].z, - uvCoords[i][0], uvCoords[i][1], renderState.lightCoords); - } + buffer.addVertex(pose, vertex.x, vertex.y, vertex.z) + .setColor(255, 255, 255, 255) + .setUv(uv[0], uv[1]) + .setUv2(blockLight, skyLight) + .setNormal(normal.x, normal.y, normal.z); } - private Vector3f[] getQuadBuilderVertices(Direction face, Vector3f min, Vector3f max) { + private Vector3f[] getQuadVertices(Direction face, Vector3f min, Vector3f max) { float x0 = min.x, y0 = min.y, z0 = min.z; float x1 = max.x, y1 = max.y, z1 = max.z; @@ -256,61 +256,22 @@ private Vector3f[] getQuadBuilderVertices(Direction face, Vector3f min, Vector3f }; } - - private void vertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, - float x, float y, float z, float u, float v, int lightCoords) { - int blockLight = lightCoords & 0xFFFF; - int skyLight = (lightCoords >> 16) & 0xFFFF; - - buffer.addVertex(pose, x, y, z) - .setColor(255, 255, 255, 255) - .setUv(u, v) - .setUv2(blockLight, skyLight) - .setNormal(normal.x, normal.y, normal.z); - } - - private BlockyModelGeometry getOrLoadGeometry(Identifier modelLocation) { - if (geometryCache.containsKey(modelLocation.toString())) { - return geometryCache.get(modelLocation.toString()); + String key = modelLocation.toString(); + + if (geometryCache.containsKey(key)) { + return geometryCache.get(key); } try { BlockyModelGeometry geometry = BlockyModelLoader.INSTANCE.loadGeometry( new BlockyModelGeometry.Settings(modelLocation) ); - geometryCache.put(modelLocation.toString(), geometry); + geometryCache.put(key, geometry); return geometry; } catch (Exception e) { + HytaleModelLoader.LOGGER.error("Failed to load geometry for {}: {}", modelLocation, e.getMessage()); return null; } } - - - private static class NodeTransform { - final Vector3f position; - final Quaternionf rotation; - final Vector3f scale; - - NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { - this.position = position; - this.rotation = rotation; - this.scale = scale; - } - } -} - - - - - - - - - - - - - - - +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java new file mode 100644 index 0000000..e63d7ea --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java @@ -0,0 +1,10 @@ +package com.litehed.hytalemodels.blocks.entity; + +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; + +public class HytaleRenderState extends BlockEntityRenderState { + public String modelName; + public int animationTick; + public float ageInTicks; + public float partialTick; +} diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java new file mode 100644 index 0000000..f37b042 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java @@ -0,0 +1,44 @@ +package com.litehed.hytalemodels.blocks.entity; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + +public record NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { + + public NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { + this.position = new Vector3f(position); + this.rotation = new Quaternionf(rotation); + this.scale = new Vector3f(scale); + } + + public static NodeTransform translation(Vector3f position) { + return new NodeTransform(position, new Quaternionf(), new Vector3f(1, 1, 1)); + } + + public static NodeTransform rotation(Quaternionf rotation) { + return new NodeTransform(new Vector3f(), rotation, new Vector3f(1, 1, 1)); + } + + public static NodeTransform scale(Vector3f scale) { + return new NodeTransform(new Vector3f(), new Quaternionf(), scale); + } + + public static NodeTransform identity() { + return new NodeTransform(new Vector3f(), new Quaternionf(), new Vector3f(1, 1, 1)); + } + + @Override + public Vector3f position() { + return new Vector3f(position); + } + + @Override + public Quaternionf rotation() { + return new Quaternionf(rotation); + } + + @Override + public Vector3f scale() { + return new Vector3f(scale); + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java b/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java index e8af16b..c1c01c6 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java @@ -175,7 +175,7 @@ private static Vector3f[] createBorderStrip(Vector3f[] vertices, int edge, float * @param sprite the TextureAtlasSprite to use for UV calculation * @return an array of UV coordinates for each vertex of the face */ - private static float[][] calculateUVCoordinates(Direction face, BlockyModelGeometry.FaceTextureLayout layout, + public static float[][] calculateUVCoordinates(Direction face, BlockyModelGeometry.FaceTextureLayout layout, Vector3f size, TextureAtlasSprite sprite) { UVSize uvSize = getUVSizeForFace(face, size); UVBounds bounds = calculateUVBounds(layout, uvSize, sprite); diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java deleted file mode 100644 index aaf6db2..0000000 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimatedUVCalculator.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.litehed.hytalemodels.blockymodel.animations; - -import com.litehed.hytalemodels.HytaleModelLoader; -import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; -import com.litehed.hytalemodels.blockymodel.QuadBuilder; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.core.Direction; -import org.joml.Vector3f; - -import java.lang.reflect.Method; - -public class AnimatedUVCalculator { - - private static Method calculateUVCoordinatesMethod; - - // Static block to initialize the reflection method - static { - try { - calculateUVCoordinatesMethod = QuadBuilder.class.getDeclaredMethod( - "calculateUVCoordinates", - Direction.class, - BlockyModelGeometry.FaceTextureLayout.class, - Vector3f.class, - TextureAtlasSprite.class - ); - calculateUVCoordinatesMethod.setAccessible(true); - } catch (NoSuchMethodException e) { - HytaleModelLoader.LOGGER.error("Failed to find QuadBuilder.calculateUVCoordinates method: {}", e.getMessage()); - } - } - - /** - * Calculate UV coordinates for a face using QuadBuilder's logic - * Returns a 2D array where each element is [u, v] for each of the 4 vertices - * - * @param direction the face direction - * @param texLayout the texture layout for this face - * @param originalSize the original size of the shape - * @param sprite the texture sprite - * @return 2D array of UV coordinates [[u0,v0], [u1,v1], [u2,v2], [u3,v3]] - */ - public static float[][] calculateUVs(Direction direction, BlockyModelGeometry.FaceTextureLayout texLayout, - Vector3f originalSize, TextureAtlasSprite sprite) { - if (calculateUVCoordinatesMethod != null) { - try { - return (float[][]) calculateUVCoordinatesMethod.invoke( - null, direction, texLayout, originalSize, sprite - ); - } catch (Exception e) { - HytaleModelLoader.LOGGER.error("Failed to invoke QuadBuilder.calculateUVCoordinates: {}", e.getMessage()); - } - } - - return calculateUVsFallback(direction, texLayout, originalSize, sprite); - } - - /** - * Fallback method to calculate UVs if reflection fails. This is a simplified version and may not match QuadBuilder's logic exactly. - * - * @param direction the face direction - * @param texLayout the texture layout for this face - * @param originalSize the original size of the shape - * @param sprite the texture sprite - * @return 2D array of UV coordinates [[u0,v0], [u1,v1], [u2,v2], [u3,v3]] - */ - private static float[][] calculateUVsFallback(Direction direction, BlockyModelGeometry.FaceTextureLayout texLayout, - Vector3f originalSize, TextureAtlasSprite sprite) { - float u0 = sprite.getU0(); - float u1 = sprite.getU1(); - float v0 = sprite.getV0(); - float v1 = sprite.getV1(); - - return new float[][]{ - {u0, v0}, - {u1, v0}, - {u1, v1}, - {u0, v1} - }; - } -} \ No newline at end of file From 13de4fc24e4c118076d960f5a7658b174a4c3d2c Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:08:13 -0500 Subject: [PATCH 04/17] Fixed negative stretching and started on pivots --- .../blocks/entity/AnimatedChestRenderer.java | 39 ++++++-- .../entity/HytaleBlockEntityRenderer.java | 38 +------- .../blockymodel/BlockyModelGeometry.java | 42 +++++++-- .../hytalemodels/blockymodel/QuadBuilder.java | 2 +- .../AnimationTransformCalculator.java | 88 +++++++++++++++++++ 5 files changed, 161 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index 656f3e8..8f51a34 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -1,7 +1,9 @@ package com.litehed.hytalemodels.blocks.entity; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.animations.AnimationTransformCalculator; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.HashMap; @@ -12,6 +14,8 @@ public class AnimatedChestRenderer extends HytaleBlockEntityRenderer calculateAnimationTransforms(AnimatedChestRenderState renderState, BlockyModelGeometry geometry) { Map transforms = new HashMap<>(); +// +// BlockyModelGeometry.BlockyNode lidNode = findNodeByName(geometry, "Lid"); +// if (lidNode == null) { +// return transforms; +// } +// +// Vector3f pivot = AnimationTransformCalculator.getPivotInBlockCoords(lidNode); +// +// float time = renderState.ageInTicks * ANIMATION_SPEED; +// float angle = (float) Math.sin(time) * MAX_LID_ANGLE; +// +// Quaternionf rotation = new Quaternionf().rotateX((float) Math.toRadians(-angle)); +// +// transforms.put("Lid", NodeTransform.rotation(rotation)); - float time = renderState.ageInTicks * ANIMATION_SPEED; - float yOffset = (float) Math.sin(time) * MAX_LID_OFFSET; - - transforms.put("Lid", NodeTransform.translation(new Vector3f(0, yOffset, 0))); + return transforms; + } + private BlockyModelGeometry.BlockyNode findNodeByName(BlockyModelGeometry geometry, String name) { + for (BlockyModelGeometry.BlockyNode node : geometry.getNodes()) { + BlockyModelGeometry.BlockyNode found = findNodeByNameRecursive(node, name); + if (found != null) { + return found; + } + } + return null; + } - return transforms; + private BlockyModelGeometry.BlockyNode findNodeByNameRecursive(BlockyModelGeometry.BlockyNode node, String name) { + if (node.getName().equals(name)) { + return node; + } + return null; } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index 2f7ddfb..c65265f 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -130,16 +130,17 @@ private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, } BlockyModelGeometry.FaceTextureLayout texLayout = shape.getTextureLayout(direction); + boolean shouldReverse = shape.needsWindingReversal(); collector.submitCustomGeometry(poseStack, renderType, (pose, buffer) -> renderQuad(buffer, pose, direction, min, max, sprite, texLayout, - shape.getOriginalSize(), renderState, false)); + shape.getOriginalSize(), renderState, shouldReverse)); // Render backface if double-sided if (shape.isDoubleSided()) { collector.submitCustomGeometry(poseStack, renderType, (pose, buffer) -> renderQuad(buffer, pose, direction, min, max, sprite, texLayout, - shape.getOriginalSize(), renderState, true)); + shape.getOriginalSize(), renderState, !shouldReverse)); } } @@ -199,7 +200,7 @@ private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction di float[][] uvCoords = QuadBuilder.calculateUVCoordinates(direction, texLayout, originalSize, sprite); - Vector3f[] vertices = getQuadVertices(direction, min, max); + Vector3f[] vertices = QuadBuilder.getFaceVertices(direction, min, max); if (reversed) { for (int i = 3; i >= 0; i--) { @@ -224,37 +225,6 @@ private void addVertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, .setNormal(normal.x, normal.y, normal.z); } - private Vector3f[] getQuadVertices(Direction face, Vector3f min, Vector3f max) { - float x0 = min.x, y0 = min.y, z0 = min.z; - float x1 = max.x, y1 = max.y, z1 = max.z; - - return switch (face) { - case DOWN -> new Vector3f[]{ - new Vector3f(x0, y0, z0), new Vector3f(x1, y0, z0), - new Vector3f(x1, y0, z1), new Vector3f(x0, y0, z1) - }; - case UP -> new Vector3f[]{ - new Vector3f(x0, y1, z1), new Vector3f(x1, y1, z1), - new Vector3f(x1, y1, z0), new Vector3f(x0, y1, z0) - }; - case NORTH -> new Vector3f[]{ - new Vector3f(x1, y0, z0), new Vector3f(x0, y0, z0), - new Vector3f(x0, y1, z0), new Vector3f(x1, y1, z0) - }; - case SOUTH -> new Vector3f[]{ - new Vector3f(x0, y0, z1), new Vector3f(x1, y0, z1), - new Vector3f(x1, y1, z1), new Vector3f(x0, y1, z1) - }; - case WEST -> new Vector3f[]{ - new Vector3f(x0, y0, z0), new Vector3f(x0, y0, z1), - new Vector3f(x0, y1, z1), new Vector3f(x0, y1, z0) - }; - case EAST -> new Vector3f[]{ - new Vector3f(x1, y0, z1), new Vector3f(x1, y0, z0), - new Vector3f(x1, y1, z0), new Vector3f(x1, y1, z1) - }; - }; - } private BlockyModelGeometry getOrLoadGeometry(Identifier modelLocation) { String key = modelLocation.toString(); diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java index bca960f..34f03b9 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java @@ -132,9 +132,17 @@ private void bakeNode(QuadCollection.Builder builder, BlockyNode node, } FaceTextureLayout texLayout = shape.getTextureLayout(direction); - Pair quad = QuadBuilder.createQuad( - direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform - ); + + boolean shouldReverse = shape.needsWindingReversal(); + + Pair quad; + if (shouldReverse) { + quad = QuadBuilder.createReversedQuad( + direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform); + } else { + quad = QuadBuilder.createQuad( + direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform); + } addQuadToBuilder(builder, quad); @@ -150,8 +158,15 @@ private void bakeNode(QuadCollection.Builder builder, BlockyNode node, // Backface if double-sided if (shape.isDoubleSided()) { - Pair backQuad = QuadBuilder.createReversedQuad( - direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform); + Pair backQuad; + if (shouldReverse) { + // If winding is reversed, backface should be normal + backQuad = QuadBuilder.createQuad( + direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform); + } else { + backQuad = QuadBuilder.createReversedQuad( + direction, min, max, sprite, texLayout, shape.getOriginalSize(), finalTransform); + } builder.addUnculledFace(backQuad.getLeft()); } } @@ -240,6 +255,7 @@ public static final class BlockyShape { private final boolean doubleSided; private final Vector3f offset; private final Vector3f stretch; + private final boolean needsWindingReversal; private final Vector3f originalSize; private final Vector3f size; private final Map textureLayout; @@ -254,11 +270,17 @@ public BlockyShape(boolean visible, boolean doubleSided, Vector3f offset, this.stretch = new Vector3f(stretch); this.originalSize = new Vector3f(size); this.size = new Vector3f( - size.x * Math.abs(stretch.x), - size.y * Math.abs(stretch.y), - size.z * Math.abs(stretch.z) + size.x * stretch.x, + size.y * stretch.y, + size.z * stretch.z ); + int negativeCount = 0; + if (stretch.x < 0) negativeCount++; + if (stretch.y < 0) negativeCount++; + if (stretch.z < 0) negativeCount++; + this.needsWindingReversal = (negativeCount % 2) == 1; + // Immutable map this.textureLayout = Collections.unmodifiableMap( new EnumMap<>(textureLayout) @@ -292,6 +314,10 @@ public Vector3f getStretch() { return stretch; } + public boolean needsWindingReversal() { + return needsWindingReversal; + } + public Vector3f getOriginalSize() { return originalSize; } diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java b/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java index c1c01c6..8eb120d 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/QuadBuilder.java @@ -367,7 +367,7 @@ private static float normalizeV(float pixelV, TextureAtlasSprite sprite, int tex * @param max the maximum coordinates of the face * @return an array of 4 Vector3f vertices */ - private static Vector3f[] getFaceVertices(Direction face, Vector3f min, Vector3f max) { + public static Vector3f[] getFaceVertices(Direction face, Vector3f min, Vector3f max) { float x0 = min.x, y0 = min.y, z0 = min.z; float x1 = max.x, y1 = max.y, z1 = max.z; diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java new file mode 100644 index 0000000..91dbfa2 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java @@ -0,0 +1,88 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.TransformCalculator; +import com.mojang.math.Transformation; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +public class AnimationTransformCalculator { + private static final float POSITION_SCALE = 32.0f; // Convert from model units to block units + private static final float POSITION_OFFSET_Y = 16.0f; // Y-axis offset + + public static Transformation applyAnimationRotation( + BlockyModelGeometry.BlockyNode node, + Transformation baseTransform, + Quaternionf animationRotation) { + + Vector3f pivotWorld = TransformCalculator.calculateWorldPosition(node); + + float pivotX = pivotWorld.x / POSITION_SCALE; + float pivotY = (pivotWorld.y - POSITION_OFFSET_Y) / POSITION_SCALE; + float pivotZ = pivotWorld.z / POSITION_SCALE; + Vector3f pivotBlock = new Vector3f(pivotX, pivotY, pivotZ); + + Quaternionf worldOrientation = TransformCalculator.calculateWorldOrientation(node); + + Quaternionf combinedRotation = new Quaternionf(worldOrientation).mul(animationRotation); + + Transformation translateToPivot = new Transformation( + pivotBlock, + null, + null, + null + ); + + Transformation applyRotation = new Transformation( + null, + combinedRotation, + null, + null + ); + + Transformation translateBack = new Transformation( + new Vector3f(-pivotBlock.x, -pivotBlock.y, -pivotBlock.z), + null, + null, + null + ); + + // translateBack * applyRotation * translateToPivot * baseTransform + return translateBack.compose(applyRotation).compose(translateToPivot).compose(baseTransform); + } + + public static Transformation createPivotedAnimationTransform( + BlockyModelGeometry.BlockyNode node, + Quaternionf animationRotation, + Vector3f animationTranslation) { + + Vector3f pivotWorld = node.getPosition(); + + Vector3f pivotBlock = new Vector3f( + pivotWorld.x / POSITION_SCALE, + (pivotWorld.y - POSITION_OFFSET_Y) / POSITION_SCALE, + pivotWorld.z / POSITION_SCALE + ); + + Vector3f finalTranslation = pivotBlock; + if (animationTranslation != null) { + finalTranslation = new Vector3f(pivotBlock).add(animationTranslation); + } + + return new Transformation( + finalTranslation, + animationRotation, + null, + null + ); + } + + public static Vector3f getPivotInBlockCoords(BlockyModelGeometry.BlockyNode node) { + Vector3f pivotWorld = node.getPosition(); + return new Vector3f( + pivotWorld.x / POSITION_SCALE, + (pivotWorld.y - POSITION_OFFSET_Y) / POSITION_SCALE, + pivotWorld.z / POSITION_SCALE + ); + } +} From 1849b90e8899860ebcc79feae9368084ddaf7104 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:22:10 -0500 Subject: [PATCH 05/17] Children now rotate around parents pivot --- .../blocks/entity/AnimatedChestRenderer.java | 55 ++++++------ .../entity/HytaleBlockEntityRenderer.java | 53 +++++++---- .../blocks/entity/NodeTransform.java | 20 +++++ .../blockymodel/BlockyModelGeometry.java | 16 +++- .../blockymodel/BlockyModelParser.java | 4 + .../AnimationTransformCalculator.java | 88 ------------------- 6 files changed, 103 insertions(+), 133 deletions(-) delete mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index 8f51a34..0776b1b 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -1,17 +1,16 @@ package com.litehed.hytalemodels.blocks.entity; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; -import com.litehed.hytalemodels.blockymodel.animations.AnimationTransformCalculator; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import org.joml.Quaternionf; -import org.joml.Vector3f; import java.util.HashMap; +import java.util.List; import java.util.Map; public class AnimatedChestRenderer extends HytaleBlockEntityRenderer { - private static final float ANIMATION_SPEED = 0.1f; + private static final float ANIMATION_SPEED = 0.05f; private static final float MAX_LID_OFFSET = 3.0f; private static final float MAX_LID_ANGLE = 45; @@ -36,38 +35,42 @@ protected void extractAdditionalRenderState(AnimatedChestBlockEntity blockEntity protected Map calculateAnimationTransforms(AnimatedChestRenderState renderState, BlockyModelGeometry geometry) { Map transforms = new HashMap<>(); -// -// BlockyModelGeometry.BlockyNode lidNode = findNodeByName(geometry, "Lid"); -// if (lidNode == null) { -// return transforms; -// } -// -// Vector3f pivot = AnimationTransformCalculator.getPivotInBlockCoords(lidNode); -// -// float time = renderState.ageInTicks * ANIMATION_SPEED; -// float angle = (float) Math.sin(time) * MAX_LID_ANGLE; -// -// Quaternionf rotation = new Quaternionf().rotateX((float) Math.toRadians(-angle)); -// -// transforms.put("Lid", NodeTransform.rotation(rotation)); + float time = renderState.ageInTicks * ANIMATION_SPEED; + float angle = (float) Math.sin(time) * MAX_LID_ANGLE; + Quaternionf rotation = new Quaternionf().rotateX((float) Math.toRadians(-Math.abs(angle))); + transforms.put("Lid", NodeTransform.rotation(rotation)); + + BlockyModelGeometry.BlockyNode lidNode = findNodeByName(geometry, "Lid"); + if (lidNode != null) { + applyTransformToDescendants(lidNode, rotation, transforms); + } return transforms; } + private void applyTransformToDescendants(BlockyModelGeometry.BlockyNode node, + Quaternionf rotation, + Map transforms) { + for (BlockyModelGeometry.BlockyNode child : node.getChildren()) { + transforms.put(child.getName(), NodeTransform.rotation(rotation)); + applyTransformToDescendants(child, rotation, transforms); + } + } + private BlockyModelGeometry.BlockyNode findNodeByName(BlockyModelGeometry geometry, String name) { - for (BlockyModelGeometry.BlockyNode node : geometry.getNodes()) { - BlockyModelGeometry.BlockyNode found = findNodeByNameRecursive(node, name); + return findNodeByNameRecursive(geometry.getNodes(), name); + } + + private BlockyModelGeometry.BlockyNode findNodeByNameRecursive(List nodes, String name) { + for (BlockyModelGeometry.BlockyNode node : nodes) { + if (node.getName().equals(name)) { + return node; + } + BlockyModelGeometry.BlockyNode found = findNodeByNameRecursive(node.getChildren(), name); if (found != null) { return found; } } return null; } - - private BlockyModelGeometry.BlockyNode findNodeByNameRecursive(BlockyModelGeometry.BlockyNode node, String name) { - if (node.getName().equals(name)) { - return node; - } - return null; - } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index c65265f..d8d509b 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -78,13 +78,10 @@ public void submit(S renderState, PoseStack poseStack, SubmitNodeCollector submi poseStack.pushPose(); - // Center the model in the block poseStack.translate(0.5, 0.5, 0.5); - // Calculate transforms for all nodes Map nodeTransforms = calculateAnimationTransforms(renderState, geometry); - // Render each node with its shape List nodes = geometry.getNodes(); for (BlockyModelGeometry.BlockyNode node : nodes) { if (node.hasShape()) { @@ -111,7 +108,6 @@ protected Material getTextureMaterial(String modelName) { private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, BlockyModelGeometry.BlockyNode node, TextureAtlasSprite sprite, Map nodeTransforms, S renderState) { - poseStack.pushPose(); applyNodeTransform(poseStack, node, nodeTransforms); @@ -152,7 +148,7 @@ protected RenderType getRenderType() { } private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyNode node, - Map animTransforms) { + Map effectiveTransforms) { Vector3f worldPos = TransformCalculator.calculateWorldPosition(node); Quaternionf worldRot = TransformCalculator.calculateWorldOrientation(node); @@ -164,21 +160,48 @@ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyN float centerY = ((worldPos.y + rotatedOffset.y) - 16.0f) / 32.0f; float centerZ = (worldPos.z + rotatedOffset.z) / 32.0f; - poseStack.translate(centerX, centerY, centerZ); - poseStack.mulPose(worldRot); + NodeTransform effectiveTransform = effectiveTransforms.get(node.getName()); + + if (effectiveTransform != null && !effectiveTransform.equals(NodeTransform.identity())) { + Vector3f pivotBlock; + BlockyModelGeometry.BlockyNode parent = node.getParent(); + + if (parent != null && effectiveTransforms.containsKey(parent.getName())) { + // Child node + Vector3f parentWorldPos = TransformCalculator.calculateWorldPosition(parent); + float pivotX = parentWorldPos.x / 32.0f; + float pivotY = (parentWorldPos.y - 16.0f) / 32.0f; + float pivotZ = parentWorldPos.z / 32.0f; + pivotBlock = new Vector3f(pivotX, pivotY, pivotZ); + } else { + // Parent node or standalone node + float pivotX = worldPos.x / 32.0f; + float pivotY = (worldPos.y - 16.0f) / 32.0f; + float pivotZ = worldPos.z / 32.0f; + pivotBlock = new Vector3f(pivotX, pivotY, pivotZ); + } + + poseStack.translate(pivotBlock.x, pivotBlock.y, pivotBlock.z); + + poseStack.mulPose(effectiveTransform.rotation()); + + Vector3f animPos = effectiveTransform.position(); + poseStack.translate(animPos.x, animPos.y, animPos.z); - NodeTransform animTransform = animTransforms.get(node.getName()); - if (animTransform != null) { - Vector3f animPos = animTransform.position(); poseStack.translate( - animPos.x / 16.0f, - animPos.y / 16.0f, - animPos.z / 16.0f + centerX - pivotBlock.x, + centerY - pivotBlock.y, + centerZ - pivotBlock.z ); - poseStack.mulPose(animTransform.rotation()); - Vector3f animScale = animTransform.scale(); + poseStack.mulPose(worldRot); + + Vector3f animScale = effectiveTransform.scale(); poseStack.scale(animScale.x, animScale.y, animScale.z); + } else { + // No animation + poseStack.translate(centerX, centerY, centerZ); + poseStack.mulPose(worldRot); } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java index f37b042..84ace20 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java @@ -3,6 +3,8 @@ import org.joml.Quaternionf; import org.joml.Vector3f; +import static com.mojang.math.Constants.EPSILON; + public record NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { public NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { @@ -41,4 +43,22 @@ public Quaternionf rotation() { public Vector3f scale() { return new Vector3f(scale); } + + public boolean isIdentity() { + return position.lengthSquared() < EPSILON && + rotation.equals(new Quaternionf(), EPSILON) && + Math.abs(scale.x - 1.0f) < EPSILON && + Math.abs(scale.y - 1.0f) < EPSILON && + Math.abs(scale.z - 1.0f) < EPSILON; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof NodeTransform other)) return false; + + return position.equals(other.position, EPSILON) && + rotation.equals(other.rotation, EPSILON) && + scale.equals(other.scale, EPSILON); + } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java index 34f03b9..ac44ea8 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java @@ -18,10 +18,7 @@ import org.joml.Quaternionf; import org.joml.Vector3f; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.litehed.hytalemodels.blockymodel.QuadBuilder.DEBUG_BORDERS; @@ -203,6 +200,7 @@ public static final class BlockyNode { private final Quaternionf orientation; private final BlockyShape shape; private final BlockyNode parent; + private final List children; public BlockyNode(String id, String name, Vector3f position, Quaternionf orientation, BlockyShape shape, BlockyNode parent) { @@ -213,6 +211,7 @@ public BlockyNode(String id, String name, Vector3f position, Quaternionf orienta this.orientation = new Quaternionf(orientation); this.shape = shape; this.parent = parent; + this.children = new ArrayList<>(); } // Getters only - no setters (immutable) @@ -240,6 +239,15 @@ public BlockyNode getParent() { return parent; } + public List getChildren() { + return Collections.unmodifiableList(children); + } + + void addChild(BlockyNode child) { + this.children.add(child); + } + + public boolean hasShape() { return shape != null && shape.isVisible(); } diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java index 2bc749f..cf4328b 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java @@ -65,6 +65,10 @@ private static void parseNode(JsonObject nodeObj, BlockyModelGeometry.BlockyNode allNodes.add(node); + if (parent != null) { + parent.addChild(node); + } + // Parse children recursively if (nodeObj.has("children")) { JsonArray children = nodeObj.getAsJsonArray("children"); diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java deleted file mode 100644 index 91dbfa2..0000000 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/AnimationTransformCalculator.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.litehed.hytalemodels.blockymodel.animations; - -import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; -import com.litehed.hytalemodels.blockymodel.TransformCalculator; -import com.mojang.math.Transformation; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -public class AnimationTransformCalculator { - private static final float POSITION_SCALE = 32.0f; // Convert from model units to block units - private static final float POSITION_OFFSET_Y = 16.0f; // Y-axis offset - - public static Transformation applyAnimationRotation( - BlockyModelGeometry.BlockyNode node, - Transformation baseTransform, - Quaternionf animationRotation) { - - Vector3f pivotWorld = TransformCalculator.calculateWorldPosition(node); - - float pivotX = pivotWorld.x / POSITION_SCALE; - float pivotY = (pivotWorld.y - POSITION_OFFSET_Y) / POSITION_SCALE; - float pivotZ = pivotWorld.z / POSITION_SCALE; - Vector3f pivotBlock = new Vector3f(pivotX, pivotY, pivotZ); - - Quaternionf worldOrientation = TransformCalculator.calculateWorldOrientation(node); - - Quaternionf combinedRotation = new Quaternionf(worldOrientation).mul(animationRotation); - - Transformation translateToPivot = new Transformation( - pivotBlock, - null, - null, - null - ); - - Transformation applyRotation = new Transformation( - null, - combinedRotation, - null, - null - ); - - Transformation translateBack = new Transformation( - new Vector3f(-pivotBlock.x, -pivotBlock.y, -pivotBlock.z), - null, - null, - null - ); - - // translateBack * applyRotation * translateToPivot * baseTransform - return translateBack.compose(applyRotation).compose(translateToPivot).compose(baseTransform); - } - - public static Transformation createPivotedAnimationTransform( - BlockyModelGeometry.BlockyNode node, - Quaternionf animationRotation, - Vector3f animationTranslation) { - - Vector3f pivotWorld = node.getPosition(); - - Vector3f pivotBlock = new Vector3f( - pivotWorld.x / POSITION_SCALE, - (pivotWorld.y - POSITION_OFFSET_Y) / POSITION_SCALE, - pivotWorld.z / POSITION_SCALE - ); - - Vector3f finalTranslation = pivotBlock; - if (animationTranslation != null) { - finalTranslation = new Vector3f(pivotBlock).add(animationTranslation); - } - - return new Transformation( - finalTranslation, - animationRotation, - null, - null - ); - } - - public static Vector3f getPivotInBlockCoords(BlockyModelGeometry.BlockyNode node) { - Vector3f pivotWorld = node.getPosition(); - return new Vector3f( - pivotWorld.x / POSITION_SCALE, - (pivotWorld.y - POSITION_OFFSET_Y) / POSITION_SCALE, - pivotWorld.z / POSITION_SCALE - ); - } -} From 672023bf89f4191139ded069a14329f1aa4acc1f Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:28:07 -0500 Subject: [PATCH 06/17] Made it so other nodes by same name are not added Why would you make the animation file use the name over the id if multiple nodes have the same name >:( --- .../blocks/entity/AnimatedChestRenderer.java | 12 ++++++------ .../blocks/entity/HytaleBlockEntityRenderer.java | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index 0776b1b..f90e25e 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -42,18 +42,18 @@ protected Map calculateAnimationTransforms(AnimatedChestR BlockyModelGeometry.BlockyNode lidNode = findNodeByName(geometry, "Lid"); if (lidNode != null) { - applyTransformToDescendants(lidNode, rotation, transforms); + applyTransformToDescendantsById(lidNode, rotation, transforms); } return transforms; } - private void applyTransformToDescendants(BlockyModelGeometry.BlockyNode node, - Quaternionf rotation, - Map transforms) { + private void applyTransformToDescendantsById(BlockyModelGeometry.BlockyNode node, + Quaternionf rotation, + Map transforms) { for (BlockyModelGeometry.BlockyNode child : node.getChildren()) { - transforms.put(child.getName(), NodeTransform.rotation(rotation)); - applyTransformToDescendants(child, rotation, transforms); + transforms.put(child.getId(), NodeTransform.rotation(rotation)); + applyTransformToDescendantsById(child, rotation, transforms); } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index d8d509b..6008c65 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -160,13 +160,16 @@ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyN float centerY = ((worldPos.y + rotatedOffset.y) - 16.0f) / 32.0f; float centerZ = (worldPos.z + rotatedOffset.z) / 32.0f; - NodeTransform effectiveTransform = effectiveTransforms.get(node.getName()); + NodeTransform effectiveTransform = effectiveTransforms.get(node.getId()); + if (effectiveTransform == null) { + effectiveTransform = effectiveTransforms.get(node.getName()); + } if (effectiveTransform != null && !effectiveTransform.equals(NodeTransform.identity())) { Vector3f pivotBlock; BlockyModelGeometry.BlockyNode parent = node.getParent(); - if (parent != null && effectiveTransforms.containsKey(parent.getName())) { + if (parent != null && (effectiveTransforms.containsKey(parent.getId()) || effectiveTransforms.containsKey(parent.getName()))) { // Child node Vector3f parentWorldPos = TransformCalculator.calculateWorldPosition(parent); float pivotX = parentWorldPos.x / 32.0f; From ada6c0460bdb5904047a2a2f0ad1a9906cbd13ca Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:33:39 -0500 Subject: [PATCH 07/17] Finished basic impl of keyframe system --- ...aleTestBlock.java => HytaleBlockBase.java} | 4 +- .../hytalemodels/blocks/HytaleChest.java | 18 +- .../entity/AnimatedChestBlockEntity.java | 48 +-- .../blocks/entity/AnimatedChestRenderer.java | 78 +++-- .../entity/HytaleBlockEntityRenderer.java | 107 ++++-- .../blockymodel/BlockyModelGeometry.java | 2 +- .../blockymodel/BlockyModelLoader.java | 2 +- .../blockymodel/BlockyModelParser.java | 16 +- ...delTokenizer.java => BlockyTokenizer.java} | 6 +- .../hytalemodels/blockymodel/ParserUtil.java | 17 + .../animations/BlockyAnimParser.java | 182 ++++++++++ .../animations/BlockyAnimationDefinition.java | 74 ++++ .../animations/BlockyAnimationLoader.java | 53 +++ .../animations/BlockyAnimationPlayer.java | 319 ++++++++++++++++++ .../animations/BlockyKeyframe.java | 84 +++++ .../animations/NodeAnimationTrack.java | 66 ++++ .../litehed/hytalemodels/init/BlockInit.java | 4 +- 17 files changed, 957 insertions(+), 123 deletions(-) rename src/main/java/com/litehed/hytalemodels/blocks/{HytaleTestBlock.java => HytaleBlockBase.java} (93%) rename src/main/java/com/litehed/hytalemodels/blockymodel/{BlockyModelTokenizer.java => BlockyTokenizer.java} (78%) create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationDefinition.java create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java create mode 100644 src/main/java/com/litehed/hytalemodels/blockymodel/animations/NodeAnimationTrack.java diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleTestBlock.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleBlockBase.java similarity index 93% rename from src/main/java/com/litehed/hytalemodels/blocks/HytaleTestBlock.java rename to src/main/java/com/litehed/hytalemodels/blocks/HytaleBlockBase.java index 280ad7c..87304de 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/HytaleTestBlock.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleBlockBase.java @@ -10,12 +10,12 @@ import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.EnumProperty; -public class HytaleTestBlock extends Block { +public class HytaleBlockBase extends Block { public static final EnumProperty FACING = HorizontalDirectionalBlock.FACING; - public HytaleTestBlock(Properties properties) { + public HytaleBlockBase(Properties properties) { super(properties.noOcclusion()); this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH)); } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java index 853f9ab..775b545 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java @@ -8,11 +8,13 @@ import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; import org.jspecify.annotations.Nullable; -public class HytaleChest extends HytaleTestBlock implements EntityBlock { +public class HytaleChest extends HytaleBlockBase implements EntityBlock { public HytaleChest(Properties properties) { super(properties); } @@ -24,7 +26,7 @@ public HytaleChest(Properties properties) { @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { - if (!level.isClientSide()) { + if (level.isClientSide()) { BlockEntity entity = level.getBlockEntity(pos); if (entity instanceof AnimatedChestBlockEntity chest) { if (chest.isOpen()) { @@ -37,6 +39,18 @@ protected InteractionResult useWithoutItem(BlockState state, Level level, BlockP return InteractionResult.SUCCESS; } + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (level.isClientSide()) { + return (lvl, pos, blockState, blockEntity) -> { + if (blockEntity instanceof AnimatedChestBlockEntity chest) { + AnimatedChestBlockEntity.tick(lvl, pos, blockState, chest); + } + }; + } + return null; + } + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.INVISIBLE; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java index b848ae2..c88e6f3 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java @@ -4,7 +4,6 @@ import com.litehed.hytalemodels.init.BlockEntityInit; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.entity.AnimationState; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; @@ -14,11 +13,10 @@ public class AnimatedChestBlockEntity extends HytaleBlockEntity { private static final String NBT_KEY = "ChestData"; private static final String NBT_IS_OPEN = "IsOpen"; - private static final String NBT_ANIM_TICK = "AnimationTick"; private boolean isOpen = false; - private final AnimationState openAnimationState = new AnimationState(); - private int animationTick = 0; + private int openTick = 0; + private int closeTick = 0; public AnimatedChestBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityInit.CHEST_TEST_ENT.get(), pos, state, "chest_small"); @@ -30,10 +28,10 @@ public AnimatedChestBlockEntity(BlockPos pos, BlockState state) { public void openChest() { if (!isOpen) { isOpen = true; + openTick = 0; if (level != null && level.isClientSide()) { long gameTime = level.getGameTime(); - openAnimationState.start((int) gameTime); HytaleModelLoader.LOGGER.debug("Client: Starting chest open animation at {}", gameTime); } @@ -48,9 +46,8 @@ public void openChest() { public void closeChest() { if (isOpen) { isOpen = false; - + closeTick = 0; if (level != null && level.isClientSide()) { - openAnimationState.stop(); HytaleModelLoader.LOGGER.debug("Client: Stopping chest open animation"); } @@ -59,9 +56,6 @@ public void closeChest() { } } - /** - * Toggles the chest open/closed state. - */ public void toggleChest() { if (isOpen) { closeChest(); @@ -70,17 +64,18 @@ public void toggleChest() { } } + /** + * Toggles the chest open/closed state. + */ + public boolean isOpen() { return isOpen; } - public AnimationState getOpenAnimationState() { - return openAnimationState; - } @Override public int getAnimationTick() { - return animationTick; + return isOpen ? openTick : closeTick; } /** @@ -89,11 +84,9 @@ public int getAnimationTick() { public static void tick(Level level, BlockPos pos, BlockState state, AnimatedChestBlockEntity blockEntity) { if (level.isClientSide()) { if (blockEntity.isOpen) { - blockEntity.animationTick++; + blockEntity.openTick++; } else { - if (blockEntity.animationTick > 0) { - blockEntity.animationTick--; - } + blockEntity.closeTick++; } } } @@ -105,19 +98,13 @@ protected void loadAnimationData(ValueInput input) { boolean wasOpen = isOpen; isOpen = chestTag.getBoolean(NBT_IS_OPEN).get(); - // Update animation state on client - if (level != null && level.isClientSide() && wasOpen != isOpen) { - if (isOpen) { - openAnimationState.start((int) level.getGameTime()); - } else { - openAnimationState.stop(); + if (level != null && level.isClientSide()) { + if (wasOpen != isOpen && isOpen) { + openTick = 0; + HytaleModelLoader.LOGGER.debug("CLIENT: Chest opened, reset animationTick to 0"); } } } - - if (chestTag.contains(NBT_ANIM_TICK)) { - animationTick = chestTag.getInt(NBT_ANIM_TICK).get(); - } }); } @@ -125,7 +112,6 @@ protected void loadAnimationData(ValueInput input) { protected void saveAnimationData(ValueOutput output) { CompoundTag chestTag = new CompoundTag(); chestTag.putBoolean(NBT_IS_OPEN, isOpen); - chestTag.putInt(NBT_ANIM_TICK, animationTick); output.store(NBT_KEY, CompoundTag.CODEC, chestTag); } @@ -142,8 +128,6 @@ private void syncToClients() { public String toString() { return "AnimatedChestBlockEntity{" + "pos=" + worldPosition + - ", isOpen=" + isOpen + - ", animationTick=" + animationTick + - "}"; + ", isOpen=" + isOpen + "}"; } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index f90e25e..db6a549 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -1,16 +1,19 @@ package com.litehed.hytalemodels.blocks.entity; +import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationDefinition; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationLoader; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; -import org.joml.Quaternionf; +import net.minecraft.resources.Identifier; import java.util.HashMap; -import java.util.List; import java.util.Map; public class AnimatedChestRenderer extends HytaleBlockEntityRenderer { - private static final float ANIMATION_SPEED = 0.05f; + private static final float ANIMATION_SPEED = 0.01f; private static final float MAX_LID_OFFSET = 3.0f; private static final float MAX_LID_ANGLE = 45; @@ -31,46 +34,47 @@ protected void extractAdditionalRenderState(AnimatedChestBlockEntity blockEntity renderState.isOpen = blockEntity.isOpen(); } - @Override - protected Map calculateAnimationTransforms(AnimatedChestRenderState renderState, - BlockyModelGeometry geometry) { - Map transforms = new HashMap<>(); - float time = renderState.ageInTicks * ANIMATION_SPEED; - float angle = (float) Math.sin(time) * MAX_LID_ANGLE; - Quaternionf rotation = new Quaternionf().rotateX((float) Math.toRadians(-Math.abs(angle))); - transforms.put("Lid", NodeTransform.rotation(rotation)); - - BlockyModelGeometry.BlockyNode lidNode = findNodeByName(geometry, "Lid"); - if (lidNode != null) { - applyTransformToDescendantsById(lidNode, rotation, transforms); + private Identifier getAnimationFile(AnimatedChestRenderState renderState) { + if (renderState.isOpen) { + return Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "animations/chest_small/chest_open.blockyanim"); } - - return transforms; + return Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "animations/chest_small/chest_close.blockyanim"); } - private void applyTransformToDescendantsById(BlockyModelGeometry.BlockyNode node, - Quaternionf rotation, - Map transforms) { - for (BlockyModelGeometry.BlockyNode child : node.getChildren()) { - transforms.put(child.getId(), NodeTransform.rotation(rotation)); - applyTransformToDescendantsById(child, rotation, transforms); - } - } + @Override + protected Map calculateAnimationTransforms(AnimatedChestRenderState renderState, + BlockyModelGeometry geometry) { + // For use of basic animations like base minecraft system +// Map transforms = new HashMap<>(); +// float time = renderState.ageInTicks * ANIMATION_SPEED; +// float angle = (float) Math.sin(time) * MAX_LID_ANGLE; +// Quaternionf rotation = new Quaternionf().rotateX((float) Math.toRadians(-Math.abs(angle))); +// transforms.put("Lid", NodeTransform.rotation(rotation)); +// +// BlockyModelGeometry.BlockyNode lidNode = findNodeByName(geometry, "Lid"); +// if (lidNode != null) { +// applyTransformToDescendantsById(lidNode, rotation, transforms); +// } +// +// return transforms; - private BlockyModelGeometry.BlockyNode findNodeByName(BlockyModelGeometry geometry, String name) { - return findNodeByNameRecursive(geometry.getNodes(), name); - } + // For use of an animation file + Identifier animationFile = getAnimationFile(renderState); + try { + BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(animationFile); - private BlockyModelGeometry.BlockyNode findNodeByNameRecursive(List nodes, String name) { - for (BlockyModelGeometry.BlockyNode node : nodes) { - if (node.getName().equals(name)) { - return node; - } - BlockyModelGeometry.BlockyNode found = findNodeByNameRecursive(node.getChildren(), name); - if (found != null) { - return found; + if (definition == null) { + HytaleModelLoader.LOGGER.warn("Animation definition is null for: {}", animationFile); + return new HashMap<>(); } + HytaleModelLoader.LOGGER.debug("Animation loaded - duration: {}, ageInTicks: {}", + definition.getDuration(), renderState.ageInTicks); + + BlockyAnimationPlayer player = new BlockyAnimationPlayer(definition); + return player.calculateTransforms(renderState.ageInTicks); + } catch (Exception e) { + HytaleModelLoader.LOGGER.error("Error playing animation: {}", e.getMessage()); + return new HashMap<>(); } - return null; } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index 6008c65..7e9b94a 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -46,10 +46,7 @@ public void extractRenderState(T blockEntity, S renderState, float partialTick, renderState.modelName = blockEntity.getModelName(); renderState.animationTick = blockEntity.getAnimationTick(); renderState.partialTick = partialTick; - - if (blockEntity.getLevel() != null) { - renderState.ageInTicks = blockEntity.getLevel().getGameTime() + partialTick; - } + renderState.ageInTicks = (blockEntity.getAnimationTick() + partialTick) * 4; extractAdditionalRenderState(blockEntity, renderState, partialTick); } @@ -165,41 +162,52 @@ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyN effectiveTransform = effectiveTransforms.get(node.getName()); } - if (effectiveTransform != null && !effectiveTransform.equals(NodeTransform.identity())) { - Vector3f pivotBlock; - BlockyModelGeometry.BlockyNode parent = node.getParent(); + NodeTransform parentAnimTransform = getParentAnimationTransform(node, effectiveTransforms); + + boolean hasOwnAnimation = effectiveTransform != null && !effectiveTransform.equals(NodeTransform.identity()); + boolean parentHasAnimation = parentAnimTransform != null && !parentAnimTransform.equals(NodeTransform.identity()); - if (parent != null && (effectiveTransforms.containsKey(parent.getId()) || effectiveTransforms.containsKey(parent.getName()))) { - // Child node + if (hasOwnAnimation || parentHasAnimation) { + Vector3f parentPivot = null; + BlockyModelGeometry.BlockyNode parent = node.getParent(); + if (parent != null && (effectiveTransforms.containsKey(parent.getId()) || + effectiveTransforms.containsKey(parent.getName()))) { Vector3f parentWorldPos = TransformCalculator.calculateWorldPosition(parent); - float pivotX = parentWorldPos.x / 32.0f; - float pivotY = (parentWorldPos.y - 16.0f) / 32.0f; - float pivotZ = parentWorldPos.z / 32.0f; - pivotBlock = new Vector3f(pivotX, pivotY, pivotZ); - } else { - // Parent node or standalone node - float pivotX = worldPos.x / 32.0f; - float pivotY = (worldPos.y - 16.0f) / 32.0f; - float pivotZ = worldPos.z / 32.0f; - pivotBlock = new Vector3f(pivotX, pivotY, pivotZ); + parentPivot = new Vector3f( + parentWorldPos.x / 32.0f, + (parentWorldPos.y - 16.0f) / 32.0f, + parentWorldPos.z / 32.0f + ); } - poseStack.translate(pivotBlock.x, pivotBlock.y, pivotBlock.z); + float childPivotX = worldPos.x / 32.0f; + float childPivotY = (worldPos.y - 16.0f) / 32.0f; + float childPivotZ = worldPos.z / 32.0f; - poseStack.mulPose(effectiveTransform.rotation()); + if (parentHasAnimation && parentPivot != null) { + poseStack.translate(parentPivot.x, parentPivot.y, parentPivot.z); - Vector3f animPos = effectiveTransform.position(); - poseStack.translate(animPos.x, animPos.y, animPos.z); + poseStack.mulPose(parentAnimTransform.rotation()); + Vector3f parentPos = parentAnimTransform.position(); + poseStack.translate(parentPos.x, parentPos.y, parentPos.z); - poseStack.translate( - centerX - pivotBlock.x, - centerY - pivotBlock.y, - centerZ - pivotBlock.z - ); + poseStack.translate(-parentPivot.x, -parentPivot.y, -parentPivot.z); + } + if (hasOwnAnimation) { + poseStack.translate(childPivotX, childPivotY, childPivotZ); + + poseStack.mulPose(effectiveTransform.rotation()); + Vector3f childAnimPos = effectiveTransform.position(); + poseStack.translate(childAnimPos.x, childAnimPos.y, childAnimPos.z); + + poseStack.translate(-childPivotX, -childPivotY, -childPivotZ); + } + + poseStack.translate(centerX, centerY, centerZ); poseStack.mulPose(worldRot); - Vector3f animScale = effectiveTransform.scale(); + Vector3f animScale = effectiveTransform != null ? effectiveTransform.scale() : new Vector3f(1, 1, 1); poseStack.scale(animScale.x, animScale.y, animScale.z); } else { // No animation @@ -208,6 +216,25 @@ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyN } } + private NodeTransform getParentAnimationTransform(BlockyModelGeometry.BlockyNode node, + Map effectiveTransforms) { + BlockyModelGeometry.BlockyNode current = node.getParent(); + + while (current != null) { + NodeTransform transform = effectiveTransforms.get(current.getId()); + if (transform == null) { + transform = effectiveTransforms.get(current.getName()); + } + if (transform != null && !transform.equals(NodeTransform.identity())) { + return transform; + } + current = current.getParent(); + } + + return null; + } + + private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction direction, Vector3f min, Vector3f max, TextureAtlasSprite sprite, BlockyModelGeometry.FaceTextureLayout texLayout, Vector3f originalSize, @@ -251,6 +278,28 @@ private void addVertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, .setNormal(normal.x, normal.y, normal.z); } + protected BlockyModelGeometry.BlockyNode findNodeByName(BlockyModelGeometry geometry, String name) { + return findNodeByNameRecursive(geometry.getNodes(), name); + } + + private BlockyModelGeometry.BlockyNode findNodeByNameRecursive( + List nodes, String name) { + for (BlockyModelGeometry.BlockyNode node : nodes) { + if (node.getName().equals(name)) return node; + BlockyModelGeometry.BlockyNode found = findNodeByNameRecursive(node.getChildren(), name); + if (found != null) return found; + } + return null; + } + + protected void applyTransformToDescendants(BlockyModelGeometry.BlockyNode node, + Quaternionf rotation, + Map transforms) { + for (BlockyModelGeometry.BlockyNode child : node.getChildren()) { + transforms.put(child.getId(), NodeTransform.rotation(rotation)); + applyTransformToDescendants(child, rotation, transforms); + } + } private BlockyModelGeometry getOrLoadGeometry(Identifier modelLocation) { String key = modelLocation.toString(); diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java index ac44ea8..63d2bb1 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelGeometry.java @@ -44,7 +44,7 @@ public BlockyModelGeometry(List nodes, Identifier modelLocation) { * @param settings the settings to use for parsing * @return a new BlockyModelGeometry instance */ - public static BlockyModelGeometry parse(BlockyModelTokenizer tokenizer, Settings settings) { + public static BlockyModelGeometry parse(BlockyTokenizer tokenizer, Settings settings) { List nodes = BlockyModelParser.parseNodes(tokenizer.getRoot()); return new BlockyModelGeometry(nodes, settings.modelLocation()); } diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelLoader.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelLoader.java index 4c1e4d4..331febd 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelLoader.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelLoader.java @@ -61,7 +61,7 @@ public BlockyModelGeometry loadGeometry(BlockyModelGeometry.Settings settings) { return geometryCache.computeIfAbsent(settings, (data) -> { ResourceManager manager = Minecraft.getInstance().getResourceManager(); Resource resource = manager.getResource(settings.modelLocation()).orElseThrow(); - try (BlockyModelTokenizer tokenizer = new BlockyModelTokenizer(resource.open())) { + try (BlockyTokenizer tokenizer = new BlockyTokenizer(resource.open())) { return BlockyModelGeometry.parse(tokenizer, data); } catch (FileNotFoundException e) { throw new RuntimeException("Could not find BlockyModel file", e); diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java index cf4328b..2bd4398 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Map; +import static com.litehed.hytalemodels.blockymodel.ParserUtil.*; + public class BlockyModelParser { private static final float DEFAULT_SIZE = 16.0f; @@ -321,18 +323,4 @@ private static void validateAngle(int angle) { throw new JsonParseException("Invalid angle: " + angle + ". Must be 0, 90, 180, or 270"); } } - - // Utility methods to get values with defaults - - private static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) { - return obj.has(key) ? obj.get(key).getAsFloat() : defaultValue; - } - - private static int getIntOrDefault(JsonObject obj, String key, int defaultValue) { - return obj.has(key) ? obj.get(key).getAsInt() : defaultValue; - } - - private static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) { - return obj.has(key) ? obj.get(key).getAsBoolean() : defaultValue; - } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelTokenizer.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyTokenizer.java similarity index 78% rename from src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelTokenizer.java rename to src/main/java/com/litehed/hytalemodels/blockymodel/BlockyTokenizer.java index 6f300a7..7db2d4b 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelTokenizer.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyTokenizer.java @@ -8,16 +8,16 @@ import java.io.InputStream; import java.io.InputStreamReader; -public class BlockyModelTokenizer implements AutoCloseable { +public class BlockyTokenizer implements AutoCloseable { private final BufferedReader lineReader; private final JsonObject root; /** - * Creates a new BlockyModelTokenizer that reads from the given InputStream + * Creates a new BlockyTokenizer that reads from the given InputStream * * @param inputStream The InputStream to read from */ - public BlockyModelTokenizer(InputStream inputStream) { + public BlockyTokenizer(InputStream inputStream) { this.lineReader = new BufferedReader(new InputStreamReader(inputStream, Charsets.UTF_8)); this.root = JsonParser.parseReader(lineReader).getAsJsonObject(); } diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java b/src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java new file mode 100644 index 0000000..c99ba22 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java @@ -0,0 +1,17 @@ +package com.litehed.hytalemodels.blockymodel; + +import com.google.gson.JsonObject; + +public class ParserUtil { + public static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) { + return obj.has(key) ? obj.get(key).getAsFloat() : defaultValue; + } + + public static int getIntOrDefault(JsonObject obj, String key, int defaultValue) { + return obj.has(key) ? obj.get(key).getAsInt() : defaultValue; + } + + public static boolean getBooleanOrDefault(JsonObject obj, String key, boolean defaultValue) { + return obj.has(key) ? obj.get(key).getAsBoolean() : defaultValue; + } +} diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java new file mode 100644 index 0000000..aa78283 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java @@ -0,0 +1,182 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.litehed.hytalemodels.HytaleModelLoader; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.litehed.hytalemodels.blockymodel.ParserUtil.*; + +public class BlockyAnimParser { + + public static BlockyAnimationDefinition parse(JsonObject root) throws JsonParseException { + validateRequiredFields(root); + + int formatVersion = getIntOrDefault(root, "formatVersion", 1); + float duration = root.get("duration").getAsFloat(); + boolean holdLastKeyframe = getBooleanOrDefault(root, "holdLastKeyframe", false); + + Map> nodeAnimations = parseNodeAnimations(root); + + return new BlockyAnimationDefinition.Builder() + .duration(duration) + .holdLastKeyframe(holdLastKeyframe) + .addNodeAnimations(nodeAnimations) + .build(); + } + + private static void validateRequiredFields(JsonObject root) throws JsonParseException { + if (!root.has("duration")) { + throw new JsonParseException("Animation file missing required field: 'duration'"); + } + if (!root.has("nodeAnimations")) { + throw new JsonParseException("Animation file missing required field: 'nodeAnimations'"); + } + } + + private static Map> parseNodeAnimations(JsonObject root) { + Map> result = new HashMap<>(); + + JsonObject nodeAnimationsObj = root.getAsJsonObject("nodeAnimations"); + for (String nodeId : nodeAnimationsObj.keySet()) { + List tracks = parseNodeAnimationTracks(nodeId, nodeAnimationsObj.getAsJsonObject(nodeId)); + if (!tracks.isEmpty()) { + result.put(nodeId, tracks); + } + } + + return result; + } + + private static List parseNodeAnimationTracks(String nodeId, JsonObject nodeObj) { + List tracks = new ArrayList<>(); + + for (String trackKey : nodeObj.keySet()) { + NodeAnimationTrack.AnimationTrackType trackType = NodeAnimationTrack.AnimationTrackType.fromJsonKey(trackKey); + if (trackType == null) { + HytaleModelLoader.LOGGER.warn("Unknown animation track type: {}, skipping", trackKey); + continue; + } + + JsonArray keyframesArray = nodeObj.getAsJsonArray(trackKey); + if (keyframesArray.isEmpty()) { + continue; // Skip empty tracks + } + + List keyframes = parseKeyframes(trackType, keyframesArray); + if (!keyframes.isEmpty()) { + tracks.add(new NodeAnimationTrack(nodeId, trackType, keyframes)); + } + } + + return tracks; + } + + private static List parseKeyframes( + NodeAnimationTrack.AnimationTrackType trackType, + JsonArray keyframesArray) { + + List keyframes = new ArrayList<>(); + + for (JsonElement element : keyframesArray) { + JsonObject keyframeObj = element.getAsJsonObject(); + float time = keyframeObj.get("time").getAsFloat(); + BlockyKeyframe.InterpolationType interpolationType = parseInterpolationType(keyframeObj); + + BlockyKeyframe keyframe = switch (trackType) { + case POSITION -> parsePositionKeyframe(time, keyframeObj, interpolationType); + case ORIENTATION -> parseOrientationKeyframe(time, keyframeObj, interpolationType); + case SHAPE_VISIBLE -> parseVisibilityKeyframe(time, keyframeObj, interpolationType); + case SHAPE_STRETCH -> parseScaleKeyframe(time, keyframeObj, interpolationType); + case SHAPE_UV_OFFSET -> null; + }; + + if (keyframe != null) { + keyframes.add(keyframe); + } + } + + return keyframes; + } + + private static BlockyKeyframe.PositionKeyframe parsePositionKeyframe( + float time, + JsonObject keyframeObj, + BlockyKeyframe.InterpolationType interpolationType) { + + JsonObject deltaObj = keyframeObj.getAsJsonObject("delta"); + Vector3f delta = new Vector3f( + getFloatOrDefault(deltaObj, "x", 0), + getFloatOrDefault(deltaObj, "y", 0), + getFloatOrDefault(deltaObj, "z", 0) + ); + + return new BlockyKeyframe.PositionKeyframe(time, delta, interpolationType); + } + + private static BlockyKeyframe.OrientationKeyframe parseOrientationKeyframe( + float time, + JsonObject keyframeObj, + BlockyKeyframe.InterpolationType interpolationType) { + + JsonObject deltaObj = keyframeObj.getAsJsonObject("delta"); + Quaternionf delta = new Quaternionf( + getFloatOrDefault(deltaObj, "x", 0), + getFloatOrDefault(deltaObj, "y", 0), + getFloatOrDefault(deltaObj, "z", 0), + getFloatOrDefault(deltaObj, "w", 1) + ); + + return new BlockyKeyframe.OrientationKeyframe(time, delta, interpolationType); + } + + + private static BlockyKeyframe.VisibilityKeyframe parseVisibilityKeyframe( + float time, + JsonObject keyframeObj, + BlockyKeyframe.InterpolationType interpolationType) { + + boolean visible = keyframeObj.get("delta").getAsBoolean(); + return new BlockyKeyframe.VisibilityKeyframe(time, visible, interpolationType); + } + + private static BlockyKeyframe.ScaleKeyframe parseScaleKeyframe( + float time, + JsonObject keyframeObj, + BlockyKeyframe.InterpolationType interpolationType) { + + JsonObject deltaObj = keyframeObj.getAsJsonObject("delta"); + Vector3f delta = new Vector3f( + getFloatOrDefault(deltaObj, "x", 0), + getFloatOrDefault(deltaObj, "y", 0), + getFloatOrDefault(deltaObj, "z", 0) + ); + + return new BlockyKeyframe.ScaleKeyframe(time, delta, interpolationType); + } + + private static BlockyKeyframe.InterpolationType parseInterpolationType(JsonObject keyframeObj) { + if (!keyframeObj.has("interpolationType")) { + return BlockyKeyframe.InterpolationType.SMOOTH; + } + + String typeStr = keyframeObj.get("interpolationType").getAsString().toLowerCase(); + return switch (typeStr) { + case "linear" -> BlockyKeyframe.InterpolationType.LINEAR; + case "smooth" -> BlockyKeyframe.InterpolationType.SMOOTH; + case "catmullrom" -> BlockyKeyframe.InterpolationType.CATMULLROM; + default -> { + HytaleModelLoader.LOGGER.warn("Unknown interpolation type: {}, defaulting to SMOOTH", typeStr); + yield BlockyKeyframe.InterpolationType.SMOOTH; + } + }; + } +} diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationDefinition.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationDefinition.java new file mode 100644 index 0000000..bcedd48 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationDefinition.java @@ -0,0 +1,74 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BlockyAnimationDefinition { + + private final float duration; // in ticks/frames + private final boolean holdLastKeyframe; + private final Map> nodeAnimations; + + public BlockyAnimationDefinition( + float duration, + boolean holdLastKeyframe, + Map> nodeAnimations) { + this.duration = duration; + this.holdLastKeyframe = holdLastKeyframe; + this.nodeAnimations = Map.copyOf(nodeAnimations); + } + + public float getDuration() { + return duration; + } + + public boolean isHoldLastKeyframe() { + return holdLastKeyframe; + } + + public Map> getNodeAnimations() { + return nodeAnimations; + } + + public List getNodeTracks(String nodeId) { + return nodeAnimations.getOrDefault(nodeId, List.of()); + } + + public NodeAnimationTrack getNodeTrack(String nodeId, NodeAnimationTrack.AnimationTrackType trackType) { + return getNodeTracks(nodeId).stream() + .filter(track -> track.getTrackType() == trackType) + .findFirst() + .orElse(null); + } + + public static final class Builder { + private float duration = 0; + private boolean holdLastKeyframe = false; + private final Map> nodeAnimations = new HashMap<>(); + + public Builder duration(float duration) { + this.duration = duration; + return this; + } + + public Builder holdLastKeyframe(boolean hold) { + this.holdLastKeyframe = hold; + return this; + } + + public Builder addNodeAnimations(String nodeId, List tracks) { + this.nodeAnimations.put(nodeId, List.copyOf(tracks)); + return this; + } + + public Builder addNodeAnimations(Map> animations) { + this.nodeAnimations.putAll(animations); + return this; + } + + public BlockyAnimationDefinition build() { + return new BlockyAnimationDefinition(duration, holdLastKeyframe, nodeAnimations); + } + } +} diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java new file mode 100644 index 0000000..5941a57 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java @@ -0,0 +1,53 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import com.google.common.collect.Maps; +import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blockymodel.BlockyTokenizer; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; + +import java.util.Map; + +public class BlockyAnimationLoader implements ResourceManagerReloadListener { + + public static final BlockyAnimationLoader INSTANCE = new BlockyAnimationLoader(); + + private final Map animationCache = Maps.newConcurrentMap(); + + public BlockyAnimationDefinition getAnimation(Identifier animationId) { + return animationCache.get(animationId); + } + + public BlockyAnimationDefinition loadAnimation(Identifier animationId) { + return animationCache.computeIfAbsent(animationId, (id) -> { + try { + ResourceManager manager = Minecraft.getInstance().getResourceManager(); + Resource resource = manager.getResource(id).orElse(null); + + if (resource == null) { + HytaleModelLoader.LOGGER.warn("Could not find animation file: {}", id); + return null; + } + + try (BlockyTokenizer tokenizer = new BlockyTokenizer(resource.open())) { + return BlockyAnimParser.parse(tokenizer.getRoot()); + } + } catch (Exception e) { + HytaleModelLoader.LOGGER.error("Failed to load animation: {}", animationId, e); + return null; + } + }); + } + + @Override + public void onResourceManagerReload(ResourceManager resourceManager) { + animationCache.clear(); + } + + public void clearAnimation(Identifier animationId) { + animationCache.remove(animationId); + } +} diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java new file mode 100644 index 0000000..417d1d8 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java @@ -0,0 +1,319 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import com.litehed.hytalemodels.blocks.entity.NodeTransform; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BlockyAnimationPlayer { + private final BlockyAnimationDefinition definition; + private final Map positionInterpolators = new HashMap<>(); + private final Map orientationInterpolators = new HashMap<>(); + private final Map visibilityInterpolators = new HashMap<>(); + private final Map scaleInterpolators = new HashMap<>(); + + public BlockyAnimationPlayer(BlockyAnimationDefinition definition) { + this.definition = definition; + initializeInterpolators(); + } + + private void initializeInterpolators() { + for (Map.Entry> entry : definition.getNodeAnimations().entrySet()) { + String nodeId = entry.getKey(); + for (NodeAnimationTrack track : entry.getValue()) { + switch (track.getTrackType()) { + case POSITION -> { + List keyframes = track.getKeyframes().stream() + .filter(kf -> kf instanceof BlockyKeyframe.PositionKeyframe) + .map(kf -> (BlockyKeyframe.PositionKeyframe) kf) + .toList(); + if (!keyframes.isEmpty()) { + positionInterpolators.put(nodeId, new PositionInterpolator(keyframes)); + } + } + case ORIENTATION -> { + List keyframes = track.getKeyframes().stream() + .filter(kf -> kf instanceof BlockyKeyframe.OrientationKeyframe) + .map(kf -> (BlockyKeyframe.OrientationKeyframe) kf) + .toList(); + if (!keyframes.isEmpty()) { + orientationInterpolators.put(nodeId, new OrientationInterpolator(keyframes)); + } + } + case SHAPE_VISIBLE -> { + List keyframes = track.getKeyframes().stream() + .filter(kf -> kf instanceof BlockyKeyframe.VisibilityKeyframe) + .map(kf -> (BlockyKeyframe.VisibilityKeyframe) kf) + .toList(); + if (!keyframes.isEmpty()) { + visibilityInterpolators.put(nodeId, new VisibilityInterpolator(keyframes)); + } + } + case SHAPE_STRETCH -> { + List keyframes = track.getKeyframes().stream() + .filter(kf -> kf instanceof BlockyKeyframe.ScaleKeyframe) + .map(kf -> (BlockyKeyframe.ScaleKeyframe) kf) + .toList(); + if (!keyframes.isEmpty()) { + scaleInterpolators.put(nodeId, new ScaleInterpolator(keyframes)); + } + } + default -> { + } // Other track types ignored for now + } + } + } + } + + public Map calculateTransforms(float timeInTicks) { + Map transforms = new HashMap<>(); + float elapsedTime = calculateElapsedTime(timeInTicks); + + // Position updates + for (Map.Entry entry : positionInterpolators.entrySet()) { + Vector3f position = entry.getValue().interpolate(elapsedTime); + if (position != null) { + transforms.put(entry.getKey(), NodeTransform.translation(position)); + } + } + + // Orientation updates - need to merge with position updates + for (Map.Entry entry : orientationInterpolators.entrySet()) { + Quaternionf rotation = entry.getValue().interpolate(elapsedTime); + if (rotation != null) { + String nodeId = entry.getKey(); + NodeTransform existing = transforms.get(nodeId); + if (existing != null && !existing.position().equals(new Vector3f())) { + // Merge position and rotation + transforms.put(nodeId, new NodeTransform(existing.position(), rotation, existing.scale())); + } else { + transforms.put(nodeId, NodeTransform.rotation(rotation)); + } + } + } + return transforms; + } + + private float calculateElapsedTime(float timeInTicks) { + if (definition.isHoldLastKeyframe()) { + return Math.min(timeInTicks, definition.getDuration()); + } else { + return timeInTicks % definition.getDuration(); + } + } + + public float getAnimationDuration() { + return definition.getDuration(); + } + + public boolean isHoldLastKeyframe() { + return definition.isHoldLastKeyframe(); + } + + private static final class PositionInterpolator { + private final List keyframes; + + PositionInterpolator(List keyframes) { + this.keyframes = new ArrayList<>(keyframes); + } + + Vector3f interpolate(float elapsedTime) { + if (keyframes.isEmpty()) return null; + + // Find surrounding keyframes + int nextIdx = 0; + while (nextIdx < keyframes.size() && keyframes.get(nextIdx).getTime() <= elapsedTime) { + nextIdx++; + } + + int prevIdx = Math.max(0, nextIdx - 1); + if (nextIdx >= keyframes.size()) { + nextIdx = keyframes.size() - 1; + } + + if (prevIdx == nextIdx) { + return new Vector3f(keyframes.get(prevIdx).getDelta()); + } + + BlockyKeyframe.PositionKeyframe prevKeyframe = keyframes.get(prevIdx); + BlockyKeyframe.PositionKeyframe nextKeyframe = keyframes.get(nextIdx); + + float timeDiff = nextKeyframe.getTime() - prevKeyframe.getTime(); + float alpha = (elapsedTime - prevKeyframe.getTime()) / timeDiff; + alpha = Math.max(0, Math.min(1, alpha)); // Clamp to [0, 1] + + // Apply interpolation based on type + Vector3f result = interpolatePosition( + prevKeyframe, nextKeyframe, alpha, prevKeyframe.getInterpolationType()); + + return result; + } + + private Vector3f interpolatePosition( + BlockyKeyframe.PositionKeyframe prev, + BlockyKeyframe.PositionKeyframe next, + float alpha, + BlockyKeyframe.InterpolationType interpolationType) { + + Vector3f prevDelta = prev.getDelta(); + Vector3f nextDelta = next.getDelta(); + + return switch (interpolationType) { + case LINEAR -> new Vector3f(prevDelta).lerp(nextDelta, alpha); + case SMOOTH -> smoothstep(prevDelta, nextDelta, alpha); + case CATMULLROM -> catmullromInterpolate(prevDelta, nextDelta, alpha); + }; + } + + private Vector3f smoothstep(Vector3f from, Vector3f to, float t) { + // Smoothstep: 3t^2 - 2t^3 + float smoothT = t * t * (3 - 2 * t); + return new Vector3f(from).lerp(to, smoothT); + } + + private Vector3f catmullromInterpolate(Vector3f from, Vector3f to, float t) { + // Simple Catmull-Rom approximation (simplified version) + // For full implementation, would need access to adjacent keyframes + float t2 = t * t; + float t3 = t2 * t; + + float mt = 1.0f - t; + float mt2 = mt * mt; + float mt3 = mt2 * mt; + + float coeff0 = -0.5f * mt3 + mt2 - 0.5f * mt; + float coeff1 = 1.5f * t3 - 2.5f * t2 + 1.0f; + float coeff2 = -1.5f * t3 + 2.0f * t2 + 0.5f * t; + float coeff3 = 0.5f * t3 - 0.5f * t2; + + return new Vector3f( + from.x * coeff1 + to.x * coeff2, + from.y * coeff1 + to.y * coeff2, + from.z * coeff1 + to.z * coeff2 + ); + } + } + + /** + * Interpolates orientation keyframes over time + */ + private static final class OrientationInterpolator { + private final List keyframes; + + OrientationInterpolator(List keyframes) { + this.keyframes = new ArrayList<>(keyframes); + } + + Quaternionf interpolate(float elapsedTime) { + if (keyframes.isEmpty()) return null; + + // Find surrounding keyframes + int nextIdx = 0; + while (nextIdx < keyframes.size() && keyframes.get(nextIdx).getTime() <= elapsedTime) { + nextIdx++; + } + + int prevIdx = Math.max(0, nextIdx - 1); + if (nextIdx >= keyframes.size()) { + nextIdx = keyframes.size() - 1; + } + + if (prevIdx == nextIdx) { + return new Quaternionf(keyframes.get(prevIdx).getDelta()); + } + + BlockyKeyframe.OrientationKeyframe prevKeyframe = keyframes.get(prevIdx); + BlockyKeyframe.OrientationKeyframe nextKeyframe = keyframes.get(nextIdx); + + float timeDiff = nextKeyframe.getTime() - prevKeyframe.getTime(); + float alpha = (elapsedTime - prevKeyframe.getTime()) / timeDiff; + alpha = Math.max(0, Math.min(1, alpha)); // Clamp to [0, 1] + + return new Quaternionf(prevKeyframe.getDelta()) + .slerp(nextKeyframe.getDelta(), alpha); + } + } + + /** + * Interpolates visibility keyframes over time + */ + private static final class VisibilityInterpolator { + private final List keyframes; + + VisibilityInterpolator(List keyframes) { + this.keyframes = new ArrayList<>(keyframes); + } + + boolean isVisible(float elapsedTime) { + if (keyframes.isEmpty()) return true; + + // Find the most recent keyframe at or before elapsedTime + BlockyKeyframe.VisibilityKeyframe lastKeyframe = keyframes.getFirst(); + for (BlockyKeyframe.VisibilityKeyframe kf : keyframes) { + if (kf.getTime() <= elapsedTime) { + lastKeyframe = kf; + } else { + break; + } + } + + return lastKeyframe.isVisible(); + } + } + + private static final class ScaleInterpolator { + private final List keyframes; + + ScaleInterpolator(List keyframes) { + this.keyframes = new ArrayList<>(keyframes); + } + + private Vector3f interpolateScale( + BlockyKeyframe.PositionKeyframe prev, + BlockyKeyframe.PositionKeyframe next, + float alpha, + BlockyKeyframe.InterpolationType interpolationType) { + Vector3f prevDelta = prev.getDelta(); + Vector3f nextDelta = next.getDelta(); + + return switch (interpolationType) { + case LINEAR -> new Vector3f(prevDelta).lerp(nextDelta, alpha); + case SMOOTH -> smoothstep(prevDelta, nextDelta, alpha); + case CATMULLROM -> catmullromInterpolate(prevDelta, nextDelta, alpha); + }; + } + + private Vector3f smoothstep(Vector3f from, Vector3f to, float t) { + // Smoothstep: 3t^2 - 2t^3 + float smoothT = t * t * (3 - 2 * t); + return new Vector3f(from).lerp(to, smoothT); + } + + private Vector3f catmullromInterpolate(Vector3f from, Vector3f to, float t) { + // Simple Catmull-Rom approximation (simplified version) + // For full implementation, would need access to adjacent keyframes + float t2 = t * t; + float t3 = t2 * t; + + float mt = 1.0f - t; + float mt2 = mt * mt; + float mt3 = mt2 * mt; + + float coeff0 = -0.5f * mt3 + mt2 - 0.5f * mt; + float coeff1 = 1.5f * t3 - 2.5f * t2 + 1.0f; + float coeff2 = -1.5f * t3 + 2.0f * t2 + 0.5f * t; + float coeff3 = 0.5f * t3 - 0.5f * t2; + + return new Vector3f( + from.x * coeff1 + to.x * coeff2, + from.y * coeff1 + to.y * coeff2, + from.z * coeff1 + to.z * coeff2 + ); + } + + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java new file mode 100644 index 0000000..f9414b7 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java @@ -0,0 +1,84 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + +public class BlockyKeyframe { + + protected final float time; + protected final InterpolationType interpolationType; + + protected BlockyKeyframe(float time, InterpolationType interpolationType) { + this.time = time; + this.interpolationType = interpolationType; + } + + public float getTime() { + return time; + } + + public InterpolationType getInterpolationType() { + return interpolationType; + } + + public static final class PositionKeyframe extends BlockyKeyframe { + private final Vector3f delta; + + public PositionKeyframe(float time, Vector3f delta, InterpolationType interpolationType) { + super(time, interpolationType); + this.delta = new Vector3f(delta); + } + + public Vector3f getDelta() { + return new Vector3f(delta); + } + } + + public static final class OrientationKeyframe extends BlockyKeyframe { + private final Quaternionf delta; + + public OrientationKeyframe(float time, Quaternionf delta, InterpolationType interpolationType) { + super(time, interpolationType); + this.delta = new Quaternionf(delta); + } + + public Quaternionf getDelta() { + return new Quaternionf(delta); + } + } + + public static final class ScaleKeyframe extends BlockyKeyframe { + private final Vector3f delta; + + public ScaleKeyframe(float time, Vector3f delta, InterpolationType interpolationType) { + super(time, interpolationType); + this.delta = new Vector3f(delta); + } + + public Vector3f getDelta() { + return new Vector3f(delta); + } + } + + public static final class VisibilityKeyframe extends BlockyKeyframe { + private final boolean visible; + + public VisibilityKeyframe(float time, boolean visible, InterpolationType interpolationType) { + super(time, interpolationType); + this.visible = visible; + } + + public boolean isVisible() { + return visible; + } + } + + /** + * Interpolation type enum for animation curves + */ + public enum InterpolationType { + LINEAR, + SMOOTH, + CATMULLROM + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/NodeAnimationTrack.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/NodeAnimationTrack.java new file mode 100644 index 0000000..88d8f18 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/NodeAnimationTrack.java @@ -0,0 +1,66 @@ +package com.litehed.hytalemodels.blockymodel.animations; + +import java.util.List; + +public final class NodeAnimationTrack { + + private final String nodeId; + private final AnimationTrackType trackType; + private final List keyframes; + + public NodeAnimationTrack(String nodeId, AnimationTrackType trackType, List keyframes) { + this.nodeId = nodeId; + this.trackType = trackType; + this.keyframes = List.copyOf(keyframes); // Immutable copy + } + + public String getNodeId() { + return nodeId; + } + + public AnimationTrackType getTrackType() { + return trackType; + } + + public List getKeyframes() { + return keyframes; + } + + /** + * Enum representing different types of animation tracks + */ + public enum AnimationTrackType { + POSITION("position"), + ORIENTATION("orientation"), + SHAPE_STRETCH("shapeStretch"), + SHAPE_VISIBLE("shapeVisible"), + SHAPE_UV_OFFSET("shapeUvOffset"); + + private final String jsonKey; + + AnimationTrackType(String jsonKey) { + this.jsonKey = jsonKey; + } + + public String getJsonKey() { + return jsonKey; + } + + public static AnimationTrackType fromJsonKey(String key) { + return switch (key) { + case "position" -> POSITION; + case "orientation" -> ORIENTATION; + case "shapeStretch" -> SHAPE_STRETCH; + case "shapeVisible" -> SHAPE_VISIBLE; + case "shapeUvOffset" -> SHAPE_UV_OFFSET; + default -> null; + }; + } + } +} + + + + + + diff --git a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java index ae6344a..36d93ce 100644 --- a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java @@ -2,7 +2,7 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blocks.HytaleChest; -import com.litehed.hytalemodels.blocks.HytaleTestBlock; +import com.litehed.hytalemodels.blocks.HytaleBlockBase; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockBehaviour; import net.neoforged.neoforge.registries.DeferredBlock; @@ -16,7 +16,7 @@ public class BlockInit { public static final DeferredBlock CRYSTAL_BIG = BLOCKS.registerSimpleBlock("crystal_big", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock BED = BLOCKS.registerSimpleBlock("bed", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock COFFIN = BLOCKS.registerSimpleBlock("coffin", () -> BlockBehaviour.Properties.of().noOcclusion()); - public static final DeferredBlock SLOPE = BLOCKS.registerBlock("slope", HytaleTestBlock::new); + public static final DeferredBlock SLOPE = BLOCKS.registerBlock("slope", HytaleBlockBase::new); public static final DeferredBlock SMALL_CHEST = BLOCKS.registerBlock("chest_small", HytaleChest::new); public static final DeferredBlock CHAIR = BLOCKS.registerSimpleBlock("chair", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock TABLE = BLOCKS.registerSimpleBlock("table", () -> BlockBehaviour.Properties.of().noOcclusion()); From 310f349c87acf4985447f73bd9a766851f531a57 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:25:41 -0500 Subject: [PATCH 08/17] Cleaned and optimized animation files --- .../hytalemodels/blocks/HytaleChest.java | 14 +- .../entity/AnimatedChestBlockEntity.java | 58 +-- .../blocks/entity/AnimatedChestRenderer.java | 39 +- .../entity/HytaleBlockEntityRenderer.java | 5 + .../blocks/entity/HytaleRenderState.java | 2 +- .../blocks/entity/NodeTransform.java | 38 +- .../animations/BlockyAnimParser.java | 66 ++-- .../animations/BlockyAnimationLoader.java | 49 +-- .../animations/BlockyAnimationPlayer.java | 374 +++++++----------- .../animations/BlockyKeyframe.java | 3 +- 10 files changed, 269 insertions(+), 379 deletions(-) diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java index 775b545..b572933 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java @@ -41,14 +41,12 @@ protected InteractionResult useWithoutItem(BlockState state, Level level, BlockP @Override public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { - if (level.isClientSide()) { - return (lvl, pos, blockState, blockEntity) -> { - if (blockEntity instanceof AnimatedChestBlockEntity chest) { - AnimatedChestBlockEntity.tick(lvl, pos, blockState, chest); - } - }; - } - return null; + if (!level.isClientSide()) return null; + return (lvl, pos, blockState, blockEntity) -> { + if (blockEntity instanceof AnimatedChestBlockEntity chest) { + AnimatedChestBlockEntity.clientTick(lvl, pos, blockState, chest); + } + }; } @Override diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java index c88e6f3..16549ab 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java @@ -1,6 +1,5 @@ package com.litehed.hytalemodels.blocks.entity; -import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.init.BlockEntityInit; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; @@ -15,8 +14,8 @@ public class AnimatedChestBlockEntity extends HytaleBlockEntity { private static final String NBT_IS_OPEN = "IsOpen"; private boolean isOpen = false; - private int openTick = 0; - private int closeTick = 0; + private int animationTick = 0; + private boolean lastKnownOpen = false; public AnimatedChestBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityInit.CHEST_TEST_ENT.get(), pos, state, "chest_small"); @@ -27,14 +26,8 @@ public AnimatedChestBlockEntity(BlockPos pos, BlockState state) { */ public void openChest() { if (!isOpen) { + animationTick = 0; isOpen = true; - openTick = 0; - - if (level != null && level.isClientSide()) { - long gameTime = level.getGameTime(); - HytaleModelLoader.LOGGER.debug("Client: Starting chest open animation at {}", gameTime); - } - setChanged(); syncToClients(); } @@ -45,25 +38,13 @@ public void openChest() { */ public void closeChest() { if (isOpen) { + animationTick = 0; isOpen = false; - closeTick = 0; - if (level != null && level.isClientSide()) { - HytaleModelLoader.LOGGER.debug("Client: Stopping chest open animation"); - } - setChanged(); syncToClients(); } } - public void toggleChest() { - if (isOpen) { - closeChest(); - } else { - openChest(); - } - } - /** * Toggles the chest open/closed state. */ @@ -75,36 +56,27 @@ public boolean isOpen() { @Override public int getAnimationTick() { - return isOpen ? openTick : closeTick; + return animationTick; } /** * Server/client tick for animation updates. */ - public static void tick(Level level, BlockPos pos, BlockState state, AnimatedChestBlockEntity blockEntity) { - if (level.isClientSide()) { - if (blockEntity.isOpen) { - blockEntity.openTick++; - } else { - blockEntity.closeTick++; - } - } + public static void clientTick(Level level, BlockPos pos, BlockState state, + AnimatedChestBlockEntity be) { + be.animationTick++; } @Override protected void loadAnimationData(ValueInput input) { - input.read(NBT_KEY, CompoundTag.CODEC).ifPresent(chestTag -> { - if (chestTag.contains(NBT_IS_OPEN)) { - boolean wasOpen = isOpen; - isOpen = chestTag.getBoolean(NBT_IS_OPEN).get(); - - if (level != null && level.isClientSide()) { - if (wasOpen != isOpen && isOpen) { - openTick = 0; - HytaleModelLoader.LOGGER.debug("CLIENT: Chest opened, reset animationTick to 0"); - } - } + input.read(NBT_KEY, CompoundTag.CODEC).ifPresent(tag -> { + boolean newOpen = tag.getBoolean(NBT_IS_OPEN).orElse(false); + // If the state changed (server pushed an update), reset the animation tick + if (newOpen != lastKnownOpen) { + animationTick = 0; + lastKnownOpen = newOpen; } + isOpen = newOpen; }); } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index db6a549..aa8f405 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -8,7 +8,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.resources.Identifier; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; public class AnimatedChestRenderer extends HytaleBlockEntityRenderer { @@ -18,6 +18,14 @@ public class AnimatedChestRenderer extends HytaleBlockEntityRenderer calculateAnimationTransforms(AnimatedChestRenderState renderState, BlockyModelGeometry geometry) { @@ -59,22 +60,14 @@ protected Map calculateAnimationTransforms(AnimatedChestR // return transforms; // For use of an animation file - Identifier animationFile = getAnimationFile(renderState); - try { - BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(animationFile); - - if (definition == null) { - HytaleModelLoader.LOGGER.warn("Animation definition is null for: {}", animationFile); - return new HashMap<>(); - } - HytaleModelLoader.LOGGER.debug("Animation loaded - duration: {}, ageInTicks: {}", - definition.getDuration(), renderState.ageInTicks); + Identifier animId = renderState.isOpen ? ANIM_OPEN : ANIM_CLOSE; - BlockyAnimationPlayer player = new BlockyAnimationPlayer(definition); - return player.calculateTransforms(renderState.ageInTicks); - } catch (Exception e) { - HytaleModelLoader.LOGGER.error("Error playing animation: {}", e.getMessage()); - return new HashMap<>(); + BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(animId); + if (definition == null) { + HytaleModelLoader.LOGGER.warn("Animation definition not found: {}", animId); + return Collections.emptyMap(); } + + return new BlockyAnimationPlayer(definition).calculateTransforms(renderState.ageInTicks); } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index 7e9b94a..9b6c9bf 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -105,6 +105,11 @@ protected Material getTextureMaterial(String modelName) { private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, BlockyModelGeometry.BlockyNode node, TextureAtlasSprite sprite, Map nodeTransforms, S renderState) { + + NodeTransform transform = nodeTransforms.get(node.getId()); + if (transform == null) transform = nodeTransforms.get(node.getName()); + if (transform != null && !transform.visible()) return; + poseStack.pushPose(); applyNodeTransform(poseStack, node, nodeTransforms); diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java index e63d7ea..981e56d 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java @@ -5,6 +5,6 @@ public class HytaleRenderState extends BlockEntityRenderState { public String modelName; public int animationTick; - public float ageInTicks; + public float ageInTicks; // Smoothed animation time in ticks sped 4x to work with blockyanim public float partialTick; } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java index 84ace20..d25c2a3 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java @@ -5,28 +5,33 @@ import static com.mojang.math.Constants.EPSILON; -public record NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { +public record NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale, boolean visible) { - public NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale) { + public NodeTransform(Vector3f position, Quaternionf rotation, Vector3f scale, boolean visible) { this.position = new Vector3f(position); this.rotation = new Quaternionf(rotation); this.scale = new Vector3f(scale); + this.visible = visible; } public static NodeTransform translation(Vector3f position) { - return new NodeTransform(position, new Quaternionf(), new Vector3f(1, 1, 1)); + return new NodeTransform(position, new Quaternionf(), new Vector3f(1, 1, 1), true); } public static NodeTransform rotation(Quaternionf rotation) { - return new NodeTransform(new Vector3f(), rotation, new Vector3f(1, 1, 1)); + return new NodeTransform(new Vector3f(), rotation, new Vector3f(1, 1, 1), true); } public static NodeTransform scale(Vector3f scale) { - return new NodeTransform(new Vector3f(), new Quaternionf(), scale); + return new NodeTransform(new Vector3f(), new Quaternionf(), scale, true); + } + + public static NodeTransform visibility(boolean visible) { + return new NodeTransform(new Vector3f(), new Quaternionf(), new Vector3f(1, 1, 1), visible); } public static NodeTransform identity() { - return new NodeTransform(new Vector3f(), new Quaternionf(), new Vector3f(1, 1, 1)); + return new NodeTransform(new Vector3f(), new Quaternionf(), new Vector3f(1, 1, 1), true); } @Override @@ -44,21 +49,20 @@ public Vector3f scale() { return new Vector3f(scale); } + public NodeTransform merge(NodeTransform other) { + Vector3f mergedPos = new Vector3f(this.position).add(other.position); + Quaternionf mergedRot = new Quaternionf(this.rotation).mul(other.rotation); + Vector3f mergedScale = new Vector3f(this.scale).mul(other.scale); + boolean mergedVis = this.visible && other.visible; + return new NodeTransform(mergedPos, mergedRot, mergedScale, mergedVis); + } + public boolean isIdentity() { return position.lengthSquared() < EPSILON && rotation.equals(new Quaternionf(), EPSILON) && Math.abs(scale.x - 1.0f) < EPSILON && Math.abs(scale.y - 1.0f) < EPSILON && - Math.abs(scale.z - 1.0f) < EPSILON; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (!(obj instanceof NodeTransform other)) return false; - - return position.equals(other.position, EPSILON) && - rotation.equals(other.rotation, EPSILON) && - scale.equals(other.scale, EPSILON); + Math.abs(scale.z - 1.0f) < EPSILON && + visible; } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java index aa78283..05dc7d1 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimParser.java @@ -13,23 +13,22 @@ import java.util.List; import java.util.Map; -import static com.litehed.hytalemodels.blockymodel.ParserUtil.*; +import static com.litehed.hytalemodels.blockymodel.ParserUtil.getBooleanOrDefault; +import static com.litehed.hytalemodels.blockymodel.ParserUtil.getFloatOrDefault; -public class BlockyAnimParser { +public final class BlockyAnimParser { public static BlockyAnimationDefinition parse(JsonObject root) throws JsonParseException { validateRequiredFields(root); - int formatVersion = getIntOrDefault(root, "formatVersion", 1); +// int formatVersion = getIntOrDefault(root, "formatVersion", 1); idk if this will have a use, commented for now float duration = root.get("duration").getAsFloat(); boolean holdLastKeyframe = getBooleanOrDefault(root, "holdLastKeyframe", false); - Map> nodeAnimations = parseNodeAnimations(root); - return new BlockyAnimationDefinition.Builder() .duration(duration) .holdLastKeyframe(holdLastKeyframe) - .addNodeAnimations(nodeAnimations) + .addNodeAnimations(parseNodeAnimations(root)) .build(); } @@ -67,9 +66,7 @@ private static List parseNodeAnimationTracks(String nodeId, } JsonArray keyframesArray = nodeObj.getAsJsonArray(trackKey); - if (keyframesArray.isEmpty()) { - continue; // Skip empty tracks - } + if (keyframesArray.isEmpty()) continue; // Skip empty tracks List keyframes = parseKeyframes(trackType, keyframesArray); if (!keyframes.isEmpty()) { @@ -94,8 +91,8 @@ private static List parseKeyframes( BlockyKeyframe keyframe = switch (trackType) { case POSITION -> parsePositionKeyframe(time, keyframeObj, interpolationType); case ORIENTATION -> parseOrientationKeyframe(time, keyframeObj, interpolationType); - case SHAPE_VISIBLE -> parseVisibilityKeyframe(time, keyframeObj, interpolationType); case SHAPE_STRETCH -> parseScaleKeyframe(time, keyframeObj, interpolationType); + case SHAPE_VISIBLE -> parseVisibilityKeyframe(time, keyframeObj, interpolationType); case SHAPE_UV_OFFSET -> null; }; @@ -108,18 +105,10 @@ private static List parseKeyframes( } private static BlockyKeyframe.PositionKeyframe parsePositionKeyframe( - float time, - JsonObject keyframeObj, - BlockyKeyframe.InterpolationType interpolationType) { + float time, JsonObject kfObj, BlockyKeyframe.InterpolationType interp) { - JsonObject deltaObj = keyframeObj.getAsJsonObject("delta"); - Vector3f delta = new Vector3f( - getFloatOrDefault(deltaObj, "x", 0), - getFloatOrDefault(deltaObj, "y", 0), - getFloatOrDefault(deltaObj, "z", 0) - ); - - return new BlockyKeyframe.PositionKeyframe(time, delta, interpolationType); + Vector3f delta = parseVector3f(kfObj.getAsJsonObject("delta")); + return new BlockyKeyframe.PositionKeyframe(time, delta, interp); } private static BlockyKeyframe.OrientationKeyframe parseOrientationKeyframe( @@ -138,31 +127,29 @@ private static BlockyKeyframe.OrientationKeyframe parseOrientationKeyframe( return new BlockyKeyframe.OrientationKeyframe(time, delta, interpolationType); } + private static BlockyKeyframe.ScaleKeyframe parseScaleKeyframe( + float time, JsonObject kfObj, BlockyKeyframe.InterpolationType interp) { + + Vector3f delta = parseVector3f(kfObj.getAsJsonObject("delta")); + return new BlockyKeyframe.ScaleKeyframe(time, delta, interp); + } private static BlockyKeyframe.VisibilityKeyframe parseVisibilityKeyframe( - float time, - JsonObject keyframeObj, - BlockyKeyframe.InterpolationType interpolationType) { + float time, JsonObject kfObj, BlockyKeyframe.InterpolationType interp) { - boolean visible = keyframeObj.get("delta").getAsBoolean(); - return new BlockyKeyframe.VisibilityKeyframe(time, visible, interpolationType); + boolean visible = kfObj.get("delta").getAsBoolean(); + return new BlockyKeyframe.VisibilityKeyframe(time, visible, interp); } - private static BlockyKeyframe.ScaleKeyframe parseScaleKeyframe( - float time, - JsonObject keyframeObj, - BlockyKeyframe.InterpolationType interpolationType) { - - JsonObject deltaObj = keyframeObj.getAsJsonObject("delta"); - Vector3f delta = new Vector3f( - getFloatOrDefault(deltaObj, "x", 0), - getFloatOrDefault(deltaObj, "y", 0), - getFloatOrDefault(deltaObj, "z", 0) + private static Vector3f parseVector3f(JsonObject obj) { + return new Vector3f( + getFloatOrDefault(obj, "x", 0f), + getFloatOrDefault(obj, "y", 0f), + getFloatOrDefault(obj, "z", 0f) ); - - return new BlockyKeyframe.ScaleKeyframe(time, delta, interpolationType); } + private static BlockyKeyframe.InterpolationType parseInterpolationType(JsonObject keyframeObj) { if (!keyframeObj.has("interpolationType")) { return BlockyKeyframe.InterpolationType.SMOOTH; @@ -172,9 +159,8 @@ private static BlockyKeyframe.InterpolationType parseInterpolationType(JsonObjec return switch (typeStr) { case "linear" -> BlockyKeyframe.InterpolationType.LINEAR; case "smooth" -> BlockyKeyframe.InterpolationType.SMOOTH; - case "catmullrom" -> BlockyKeyframe.InterpolationType.CATMULLROM; default -> { - HytaleModelLoader.LOGGER.warn("Unknown interpolation type: {}, defaulting to SMOOTH", typeStr); + HytaleModelLoader.LOGGER.warn("Unknown interpolation type: '{}', defaulting to SMOOTH", typeStr); yield BlockyKeyframe.InterpolationType.SMOOTH; } }; diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java index 5941a57..db312ad 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationLoader.java @@ -10,44 +10,45 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import java.util.Map; +import java.util.Optional; -public class BlockyAnimationLoader implements ResourceManagerReloadListener { +public final class BlockyAnimationLoader implements ResourceManagerReloadListener { public static final BlockyAnimationLoader INSTANCE = new BlockyAnimationLoader(); private final Map animationCache = Maps.newConcurrentMap(); + public BlockyAnimationDefinition loadAnimation(Identifier animationId) { + return animationCache.computeIfAbsent(animationId, this::parseAnimation); + } + public BlockyAnimationDefinition getAnimation(Identifier animationId) { return animationCache.get(animationId); } - public BlockyAnimationDefinition loadAnimation(Identifier animationId) { - return animationCache.computeIfAbsent(animationId, (id) -> { - try { - ResourceManager manager = Minecraft.getInstance().getResourceManager(); - Resource resource = manager.getResource(id).orElse(null); - - if (resource == null) { - HytaleModelLoader.LOGGER.warn("Could not find animation file: {}", id); - return null; - } - - try (BlockyTokenizer tokenizer = new BlockyTokenizer(resource.open())) { - return BlockyAnimParser.parse(tokenizer.getRoot()); - } - } catch (Exception e) { - HytaleModelLoader.LOGGER.error("Failed to load animation: {}", animationId, e); - return null; - } - }); + public void clearAnimation(Identifier animationId) { + animationCache.remove(animationId); + } + + private BlockyAnimationDefinition parseAnimation(Identifier id) { + ResourceManager manager = Minecraft.getInstance().getResourceManager(); + Optional resource = manager.getResource(id); + + if (resource.isEmpty()) { + HytaleModelLoader.LOGGER.warn("Animation resource not found: {}", id); + return null; + } + + try (BlockyTokenizer tokenizer = new BlockyTokenizer(resource.get().open())) { + return BlockyAnimParser.parse(tokenizer.getRoot()); + } catch (Exception e) { + HytaleModelLoader.LOGGER.error("Failed to parse animation '{}': {}", id, e.getMessage(), e); + return null; + } } @Override public void onResourceManagerReload(ResourceManager resourceManager) { animationCache.clear(); } - - public void clearAnimation(Identifier animationId) { - animationCache.remove(animationId); - } } diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java index 417d1d8..f5ffc45 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java @@ -10,57 +10,99 @@ import java.util.Map; public class BlockyAnimationPlayer { + + private static final float POSITION_SCALE = 1f / 32f; + private final BlockyAnimationDefinition definition; private final Map positionInterpolators = new HashMap<>(); private final Map orientationInterpolators = new HashMap<>(); - private final Map visibilityInterpolators = new HashMap<>(); private final Map scaleInterpolators = new HashMap<>(); + private final Map visibilityInterpolators = new HashMap<>(); public BlockyAnimationPlayer(BlockyAnimationDefinition definition) { this.definition = definition; initializeInterpolators(); } + public float getAnimationDuration() { + return definition.getDuration(); + } + + public boolean isHoldLastKeyframe() { + return definition.isHoldLastKeyframe(); + } + + private float resolveTime(float timeInTicks) { + float duration = definition.getDuration(); + return definition.isHoldLastKeyframe() + ? Math.min(timeInTicks, duration) + : timeInTicks % duration; + } + + private static void mergeIntoMap(Map map, + String nodeId, + NodeTransform incoming) { + map.merge(nodeId, incoming, NodeTransform::merge); + } + + public Map calculateTransforms(float timeInTicks) { + float t = resolveTime(timeInTicks); + Map transforms = new HashMap<>(); + + positionInterpolators.forEach((nodeId, interp) -> { + Vector3f pos = interp.interpolate(t); + if (pos != null) { + Vector3f scaled = pos.mul(POSITION_SCALE, new Vector3f()); + mergeIntoMap(transforms, nodeId, NodeTransform.translation(scaled)); + } + }); + + orientationInterpolators.forEach((nodeId, interp) -> { + Quaternionf rot = interp.interpolate(t); + if (rot != null) { + mergeIntoMap(transforms, nodeId, NodeTransform.rotation(rot)); + } + }); + + scaleInterpolators.forEach((nodeId, interp) -> { + Vector3f scale = interp.interpolate(t); + if (scale != null) { + mergeIntoMap(transforms, nodeId, NodeTransform.scale(scale)); + } + }); + + visibilityInterpolators.forEach((nodeId, interp) -> { + boolean visible = interp.isVisible(t); + mergeIntoMap(transforms, nodeId, NodeTransform.visibility(visible)); + }); + + return transforms; + } + private void initializeInterpolators() { for (Map.Entry> entry : definition.getNodeAnimations().entrySet()) { String nodeId = entry.getKey(); for (NodeAnimationTrack track : entry.getValue()) { switch (track.getTrackType()) { case POSITION -> { - List keyframes = track.getKeyframes().stream() - .filter(kf -> kf instanceof BlockyKeyframe.PositionKeyframe) - .map(kf -> (BlockyKeyframe.PositionKeyframe) kf) - .toList(); - if (!keyframes.isEmpty()) { - positionInterpolators.put(nodeId, new PositionInterpolator(keyframes)); - } + List kfs = castKeyframes( + track.getKeyframes(), BlockyKeyframe.PositionKeyframe.class); + if (!kfs.isEmpty()) positionInterpolators.put(nodeId, new PositionInterpolator(kfs)); } case ORIENTATION -> { - List keyframes = track.getKeyframes().stream() - .filter(kf -> kf instanceof BlockyKeyframe.OrientationKeyframe) - .map(kf -> (BlockyKeyframe.OrientationKeyframe) kf) - .toList(); - if (!keyframes.isEmpty()) { - orientationInterpolators.put(nodeId, new OrientationInterpolator(keyframes)); - } - } - case SHAPE_VISIBLE -> { - List keyframes = track.getKeyframes().stream() - .filter(kf -> kf instanceof BlockyKeyframe.VisibilityKeyframe) - .map(kf -> (BlockyKeyframe.VisibilityKeyframe) kf) - .toList(); - if (!keyframes.isEmpty()) { - visibilityInterpolators.put(nodeId, new VisibilityInterpolator(keyframes)); - } + List kfs = castKeyframes( + track.getKeyframes(), BlockyKeyframe.OrientationKeyframe.class); + if (!kfs.isEmpty()) orientationInterpolators.put(nodeId, new OrientationInterpolator(kfs)); } case SHAPE_STRETCH -> { - List keyframes = track.getKeyframes().stream() - .filter(kf -> kf instanceof BlockyKeyframe.ScaleKeyframe) - .map(kf -> (BlockyKeyframe.ScaleKeyframe) kf) - .toList(); - if (!keyframes.isEmpty()) { - scaleInterpolators.put(nodeId, new ScaleInterpolator(keyframes)); - } + List kfs = castKeyframes( + track.getKeyframes(), BlockyKeyframe.ScaleKeyframe.class); + if (!kfs.isEmpty()) scaleInterpolators.put(nodeId, new ScaleInterpolator(kfs)); + } + case SHAPE_VISIBLE -> { + List kfs = castKeyframes( + track.getKeyframes(), BlockyKeyframe.VisibilityKeyframe.class); + if (!kfs.isEmpty()) visibilityInterpolators.put(nodeId, new VisibilityInterpolator(kfs)); } default -> { } // Other track types ignored for now @@ -69,49 +111,13 @@ private void initializeInterpolators() { } } - public Map calculateTransforms(float timeInTicks) { - Map transforms = new HashMap<>(); - float elapsedTime = calculateElapsedTime(timeInTicks); - - // Position updates - for (Map.Entry entry : positionInterpolators.entrySet()) { - Vector3f position = entry.getValue().interpolate(elapsedTime); - if (position != null) { - transforms.put(entry.getKey(), NodeTransform.translation(position)); - } - } - - // Orientation updates - need to merge with position updates - for (Map.Entry entry : orientationInterpolators.entrySet()) { - Quaternionf rotation = entry.getValue().interpolate(elapsedTime); - if (rotation != null) { - String nodeId = entry.getKey(); - NodeTransform existing = transforms.get(nodeId); - if (existing != null && !existing.position().equals(new Vector3f())) { - // Merge position and rotation - transforms.put(nodeId, new NodeTransform(existing.position(), rotation, existing.scale())); - } else { - transforms.put(nodeId, NodeTransform.rotation(rotation)); - } - } - } - return transforms; - } - - private float calculateElapsedTime(float timeInTicks) { - if (definition.isHoldLastKeyframe()) { - return Math.min(timeInTicks, definition.getDuration()); - } else { - return timeInTicks % definition.getDuration(); + private static List castKeyframes( + List raw, Class type) { + List result = new ArrayList<>(raw.size()); + for (BlockyKeyframe kf : raw) { + if (type.isInstance(kf)) result.add((K) kf); } - } - - public float getAnimationDuration() { - return definition.getDuration(); - } - - public boolean isHoldLastKeyframe() { - return definition.isHoldLastKeyframe(); + return result; } private static final class PositionInterpolator { @@ -121,126 +127,77 @@ private static final class PositionInterpolator { this.keyframes = new ArrayList<>(keyframes); } - Vector3f interpolate(float elapsedTime) { + Vector3f interpolate(float t) { if (keyframes.isEmpty()) return null; + if (keyframes.size() == 1) return keyframes.getFirst().getDelta(); - // Find surrounding keyframes - int nextIdx = 0; - while (nextIdx < keyframes.size() && keyframes.get(nextIdx).getTime() <= elapsedTime) { - nextIdx++; - } + int prev = prevIndex(keyframes, t); + int next = Math.min(prev + 1, keyframes.size() - 1); - int prevIdx = Math.max(0, nextIdx - 1); - if (nextIdx >= keyframes.size()) { - nextIdx = keyframes.size() - 1; - } + if (prev == next) return new Vector3f(keyframes.get(prev).getDelta()); - if (prevIdx == nextIdx) { - return new Vector3f(keyframes.get(prevIdx).getDelta()); - } + BlockyKeyframe.PositionKeyframe a = keyframes.get(prev); + BlockyKeyframe.PositionKeyframe b = keyframes.get(next); + float alpha = clampAlpha(t, a.getTime(), b.getTime()); - BlockyKeyframe.PositionKeyframe prevKeyframe = keyframes.get(prevIdx); - BlockyKeyframe.PositionKeyframe nextKeyframe = keyframes.get(nextIdx); + return interpolateVec(a.getDelta(), b.getDelta(), alpha, a.getInterpolationType()); + } + } - float timeDiff = nextKeyframe.getTime() - prevKeyframe.getTime(); - float alpha = (elapsedTime - prevKeyframe.getTime()) / timeDiff; - alpha = Math.max(0, Math.min(1, alpha)); // Clamp to [0, 1] - // Apply interpolation based on type - Vector3f result = interpolatePosition( - prevKeyframe, nextKeyframe, alpha, prevKeyframe.getInterpolationType()); + private static final class OrientationInterpolator { + private final List keyframes; - return result; + OrientationInterpolator(List keyframes) { + this.keyframes = new ArrayList<>(keyframes); } - private Vector3f interpolatePosition( - BlockyKeyframe.PositionKeyframe prev, - BlockyKeyframe.PositionKeyframe next, - float alpha, - BlockyKeyframe.InterpolationType interpolationType) { + Quaternionf interpolate(float t) { + if (keyframes.isEmpty()) return null; + if (keyframes.size() == 1) return keyframes.getFirst().getDelta(); + + int prev = prevIndex(keyframes, t); + int next = Math.min(prev + 1, keyframes.size() - 1); - Vector3f prevDelta = prev.getDelta(); - Vector3f nextDelta = next.getDelta(); + if (prev == next) return new Quaternionf(keyframes.get(prev).getDelta()); - return switch (interpolationType) { - case LINEAR -> new Vector3f(prevDelta).lerp(nextDelta, alpha); - case SMOOTH -> smoothstep(prevDelta, nextDelta, alpha); - case CATMULLROM -> catmullromInterpolate(prevDelta, nextDelta, alpha); - }; - } + BlockyKeyframe.OrientationKeyframe a = keyframes.get(prev); + BlockyKeyframe.OrientationKeyframe b = keyframes.get(next); + float alpha = clampAlpha(t, a.getTime(), b.getTime()); - private Vector3f smoothstep(Vector3f from, Vector3f to, float t) { - // Smoothstep: 3t^2 - 2t^3 - float smoothT = t * t * (3 - 2 * t); - return new Vector3f(from).lerp(to, smoothT); - } + float slerpAlpha = (a.getInterpolationType() == BlockyKeyframe.InterpolationType.LINEAR) + ? alpha + : smoothstep(alpha); - private Vector3f catmullromInterpolate(Vector3f from, Vector3f to, float t) { - // Simple Catmull-Rom approximation (simplified version) - // For full implementation, would need access to adjacent keyframes - float t2 = t * t; - float t3 = t2 * t; - - float mt = 1.0f - t; - float mt2 = mt * mt; - float mt3 = mt2 * mt; - - float coeff0 = -0.5f * mt3 + mt2 - 0.5f * mt; - float coeff1 = 1.5f * t3 - 2.5f * t2 + 1.0f; - float coeff2 = -1.5f * t3 + 2.0f * t2 + 0.5f * t; - float coeff3 = 0.5f * t3 - 0.5f * t2; - - return new Vector3f( - from.x * coeff1 + to.x * coeff2, - from.y * coeff1 + to.y * coeff2, - from.z * coeff1 + to.z * coeff2 - ); + return new Quaternionf(a.getDelta()).slerp(b.getDelta(), slerpAlpha); } } - /** - * Interpolates orientation keyframes over time - */ - private static final class OrientationInterpolator { - private final List keyframes; + private static final class ScaleInterpolator { + private final List keyframes; - OrientationInterpolator(List keyframes) { + ScaleInterpolator(List keyframes) { this.keyframes = new ArrayList<>(keyframes); } - Quaternionf interpolate(float elapsedTime) { + Vector3f interpolate(float t) { if (keyframes.isEmpty()) return null; + if (keyframes.size() == 1) return keyframes.getFirst().getDelta(); - // Find surrounding keyframes - int nextIdx = 0; - while (nextIdx < keyframes.size() && keyframes.get(nextIdx).getTime() <= elapsedTime) { - nextIdx++; - } - - int prevIdx = Math.max(0, nextIdx - 1); - if (nextIdx >= keyframes.size()) { - nextIdx = keyframes.size() - 1; - } + int prev = prevIndex(keyframes, t); + int next = Math.min(prev + 1, keyframes.size() - 1); - if (prevIdx == nextIdx) { - return new Quaternionf(keyframes.get(prevIdx).getDelta()); - } - - BlockyKeyframe.OrientationKeyframe prevKeyframe = keyframes.get(prevIdx); - BlockyKeyframe.OrientationKeyframe nextKeyframe = keyframes.get(nextIdx); + if (prev == next) return new Vector3f(keyframes.get(prev).getDelta()); - float timeDiff = nextKeyframe.getTime() - prevKeyframe.getTime(); - float alpha = (elapsedTime - prevKeyframe.getTime()) / timeDiff; - alpha = Math.max(0, Math.min(1, alpha)); // Clamp to [0, 1] + BlockyKeyframe.ScaleKeyframe a = keyframes.get(prev); + BlockyKeyframe.ScaleKeyframe b = keyframes.get(next); + float alpha = clampAlpha(t, a.getTime(), b.getTime()); - return new Quaternionf(prevKeyframe.getDelta()) - .slerp(nextKeyframe.getDelta(), alpha); + return interpolateVec(a.getDelta(), b.getDelta(), alpha, a.getInterpolationType()); } } - /** - * Interpolates visibility keyframes over time - */ + private static final class VisibilityInterpolator { private final List keyframes; @@ -248,72 +205,47 @@ private static final class VisibilityInterpolator { this.keyframes = new ArrayList<>(keyframes); } - boolean isVisible(float elapsedTime) { + boolean isVisible(float t) { if (keyframes.isEmpty()) return true; + return keyframes.get(prevIndex(keyframes, t)).isVisible(); + } + } - // Find the most recent keyframe at or before elapsedTime - BlockyKeyframe.VisibilityKeyframe lastKeyframe = keyframes.getFirst(); - for (BlockyKeyframe.VisibilityKeyframe kf : keyframes) { - if (kf.getTime() <= elapsedTime) { - lastKeyframe = kf; - } else { - break; - } - } - return lastKeyframe.isVisible(); - } + private static float clampAlpha(float t, float tStart, float tEnd) { + float span = tEnd - tStart; + if (span <= 0f) return 0f; + return Math.clamp((t - tStart) / span, 0f, 1f); } - private static final class ScaleInterpolator { - private final List keyframes; + private static float smoothstep(float t) { + return t * t * (3 - 2 * t); + } - ScaleInterpolator(List keyframes) { - this.keyframes = new ArrayList<>(keyframes); - } + private static Vector3f lerpVec(Vector3f from, Vector3f to, float t) { + return new Vector3f(from).lerp(to, t); + } - private Vector3f interpolateScale( - BlockyKeyframe.PositionKeyframe prev, - BlockyKeyframe.PositionKeyframe next, - float alpha, - BlockyKeyframe.InterpolationType interpolationType) { - Vector3f prevDelta = prev.getDelta(); - Vector3f nextDelta = next.getDelta(); - - return switch (interpolationType) { - case LINEAR -> new Vector3f(prevDelta).lerp(nextDelta, alpha); - case SMOOTH -> smoothstep(prevDelta, nextDelta, alpha); - case CATMULLROM -> catmullromInterpolate(prevDelta, nextDelta, alpha); - }; - } + private static Vector3f smoothstepVec(Vector3f from, Vector3f to, float t) { + return lerpVec(from, to, smoothstep(t)); + } - private Vector3f smoothstep(Vector3f from, Vector3f to, float t) { - // Smoothstep: 3t^2 - 2t^3 - float smoothT = t * t * (3 - 2 * t); - return new Vector3f(from).lerp(to, smoothT); - } + private static Vector3f interpolateVec(Vector3f from, Vector3f to, float alpha, + BlockyKeyframe.InterpolationType type) { + return switch (type) { + case LINEAR -> lerpVec(from, to, alpha); + case SMOOTH -> smoothstepVec(from, to, alpha); + }; + } - private Vector3f catmullromInterpolate(Vector3f from, Vector3f to, float t) { - // Simple Catmull-Rom approximation (simplified version) - // For full implementation, would need access to adjacent keyframes - float t2 = t * t; - float t3 = t2 * t; - - float mt = 1.0f - t; - float mt2 = mt * mt; - float mt3 = mt2 * mt; - - float coeff0 = -0.5f * mt3 + mt2 - 0.5f * mt; - float coeff1 = 1.5f * t3 - 2.5f * t2 + 1.0f; - float coeff2 = -1.5f * t3 + 2.0f * t2 + 0.5f * t; - float coeff3 = 0.5f * t3 - 0.5f * t2; - - return new Vector3f( - from.x * coeff1 + to.x * coeff2, - from.y * coeff1 + to.y * coeff2, - from.z * coeff1 + to.z * coeff2 - ); + private static int prevIndex(List keyframes, float time) { + int lo = 0, hi = keyframes.size() - 1; + while (lo < hi) { + int mid = (lo + hi + 1) >>> 1; + if (keyframes.get(mid).getTime() <= time) lo = mid; + else hi = mid - 1; } - + return lo; } + } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java index f9414b7..11c1a5c 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyKeyframe.java @@ -78,7 +78,6 @@ public boolean isVisible() { */ public enum InterpolationType { LINEAR, - SMOOTH, - CATMULLROM + SMOOTH } } \ No newline at end of file From 4209fcf4faedc7eefe169cd960932812a296b226 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:06:50 -0500 Subject: [PATCH 09/17] Added another animated block for testing --- .../hytalemodels/HytaleModelLoaderClient.java | 2 + .../hytalemodels/blocks/HytaleCoffin.java | 60 +++++++++++++++++++ .../blocks/entity/AnimatedChestRenderer.java | 30 ++++------ .../blocks/entity/CoffinBlockEntity.java | 54 +++++++++++++++++ .../blocks/entity/CoffinRenderer.java | 53 ++++++++++++++++ .../hytalemodels/init/BlockEntityInit.java | 9 +++ .../litehed/hytalemodels/init/BlockInit.java | 5 +- 7 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java create mode 100644 src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java diff --git a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java index 9a335fc..e8fa175 100644 --- a/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java +++ b/src/main/java/com/litehed/hytalemodels/HytaleModelLoaderClient.java @@ -1,6 +1,7 @@ package com.litehed.hytalemodels; import com.litehed.hytalemodels.blocks.entity.AnimatedChestRenderer; +import com.litehed.hytalemodels.blocks.entity.CoffinRenderer; import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; import com.litehed.hytalemodels.init.BlockEntityInit; import net.neoforged.api.distmarker.Dist; @@ -39,5 +40,6 @@ public static void onRegisterReloadListeners(AddClientReloadListenersEvent event @SubscribeEvent public static void registerEntityRenderers(EntityRenderersEvent.RegisterRenderers event) { event.registerBlockEntityRenderer(BlockEntityInit.CHEST_TEST_ENT.get(), AnimatedChestRenderer::new); + event.registerBlockEntityRenderer(BlockEntityInit.COFFIN_ENT.get(), CoffinRenderer::new); } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java new file mode 100644 index 0000000..ba7a8a7 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java @@ -0,0 +1,60 @@ +package com.litehed.hytalemodels.blocks; + +import com.litehed.hytalemodels.blocks.entity.CoffinBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import javax.annotation.Nullable; + +public class HytaleCoffin extends HytaleBlockBase implements EntityBlock { + + public HytaleCoffin(Properties properties) { + super(properties); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CoffinBlockEntity(pos, state); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, BlockHitResult hitResult) { + if (level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof CoffinBlockEntity coffin) { + if (coffin.isOpen()) { + coffin.closeCoffin(); + } else { + coffin.openCoffin(); + } + } + } + return InteractionResult.SUCCESS; + } + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, + BlockEntityType type) { + if (!level.isClientSide()) return null; + return (lvl, pos, blockState, blockEntity) -> { + if (blockEntity instanceof CoffinBlockEntity coffin) { + CoffinBlockEntity.clientTick(lvl, pos, blockState, coffin); + } + }; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.INVISIBLE; + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index aa8f405..f07f887 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -8,7 +8,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.resources.Identifier; -import java.util.Collections; +import java.util.HashMap; import java.util.Map; public class AnimatedChestRenderer extends HytaleBlockEntityRenderer { @@ -45,29 +45,19 @@ protected void extractAdditionalRenderState(AnimatedChestBlockEntity blockEntity @Override protected Map calculateAnimationTransforms(AnimatedChestRenderState renderState, BlockyModelGeometry geometry) { - // For use of basic animations like base minecraft system -// Map transforms = new HashMap<>(); -// float time = renderState.ageInTicks * ANIMATION_SPEED; -// float angle = (float) Math.sin(time) * MAX_LID_ANGLE; -// Quaternionf rotation = new Quaternionf().rotateX((float) Math.toRadians(-Math.abs(angle))); -// transforms.put("Lid", NodeTransform.rotation(rotation)); -// -// BlockyModelGeometry.BlockyNode lidNode = findNodeByName(geometry, "Lid"); -// if (lidNode != null) { -// applyTransformToDescendantsById(lidNode, rotation, transforms); -// } -// -// return transforms; + Map transforms = new HashMap<>(); - // For use of an animation file Identifier animId = renderState.isOpen ? ANIM_OPEN : ANIM_CLOSE; - BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(animId); - if (definition == null) { - HytaleModelLoader.LOGGER.warn("Animation definition not found: {}", animId); - return Collections.emptyMap(); + if (definition != null) { + transforms.putAll(new BlockyAnimationPlayer(definition).calculateTransforms(renderState.ageInTicks)); } - return new BlockyAnimationPlayer(definition).calculateTransforms(renderState.ageInTicks); + // Procedural System +// float angle = (float) Math.sin(renderState.ageInTicks * ANIMATION_SPEED) * MAX_LID_ANGLE; +// Quaternionf rotation = new Quaternionf().rotateX((float) Math.toRadians(-Math.abs(angle))); +// transforms.put("Lid", NodeTransform.rotation(rotation)); + + return transforms; } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java new file mode 100644 index 0000000..83dc9cf --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java @@ -0,0 +1,54 @@ +package com.litehed.hytalemodels.blocks.entity; + +import com.litehed.hytalemodels.init.BlockEntityInit; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +public class CoffinBlockEntity extends HytaleBlockEntity { + private boolean isOpen = false; + private int animationTick = 0; + + public CoffinBlockEntity(BlockPos pos, BlockState state) { + super(BlockEntityInit.COFFIN_ENT.get(), pos, state, "coffin"); + } + + public void openCoffin() { + if (!isOpen) { + animationTick = 0; + isOpen = true; + setChanged(); + syncToClients(); + } + } + + public void closeCoffin() { + if (isOpen) { + animationTick = 0; + isOpen = false; + setChanged(); + syncToClients(); + } + } + + public boolean isOpen() { + return isOpen; + } + + @Override + public int getAnimationTick() { + return animationTick; + } + + public static void clientTick(Level level, BlockPos pos, BlockState state, + CoffinBlockEntity be) { + be.animationTick++; + } + + private void syncToClients() { + if (level != null && !level.isClientSide()) { + level.blockEntityChanged(getBlockPos()); + } + } + +} diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java new file mode 100644 index 0000000..93960b7 --- /dev/null +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java @@ -0,0 +1,53 @@ +package com.litehed.hytalemodels.blocks.entity; + +import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationDefinition; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationLoader; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.resources.Identifier; + +import java.util.HashMap; +import java.util.Map; + +public class CoffinRenderer extends HytaleBlockEntityRenderer { + + private static final Identifier ANIM_OPEN = + Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, + "animations/coffin/coffin_open.blockyanim"); + + private static final Identifier ANIM_CLOSE = + Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, + "animations/coffin/coffin_close.blockyanim"); + + public CoffinRenderer(BlockEntityRendererProvider.Context context) { + super(context); + } + + @Override + public AnimatedChestRenderState createRenderState() { + return new AnimatedChestRenderState(); + } + + @Override + protected void extractAdditionalRenderState(CoffinBlockEntity blockEntity, + AnimatedChestRenderState renderState, + float partialTick) { + renderState.isOpen = blockEntity.isOpen(); + } + + @Override + protected Map calculateAnimationTransforms(AnimatedChestRenderState renderState, + BlockyModelGeometry geometry) { + Map transforms = new HashMap<>(); + + Identifier animId = renderState.isOpen ? ANIM_OPEN : ANIM_CLOSE; + BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(animId); + if (definition != null) { + transforms.putAll(new BlockyAnimationPlayer(definition).calculateTransforms(renderState.ageInTicks)); + } + + return transforms; + } +} \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java b/src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java index df2e6a5..004faa6 100644 --- a/src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/BlockEntityInit.java @@ -2,6 +2,7 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blocks.entity.AnimatedChestBlockEntity; +import com.litehed.hytalemodels.blocks.entity.CoffinBlockEntity; import net.minecraft.core.registries.Registries; import net.minecraft.world.level.block.entity.BlockEntityType; import net.neoforged.neoforge.registries.DeferredRegister; @@ -20,4 +21,12 @@ public class BlockEntityInit { BlockInit.SMALL_CHEST.get() ) ); + + public static final Supplier> COFFIN_ENT = BLOCK_ENTITIES.register( + "coffin", () -> new BlockEntityType<>( + CoffinBlockEntity::new, + false, + BlockInit.COFFIN.get() + ) + ); } diff --git a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java index 36d93ce..8ac076f 100644 --- a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java @@ -1,8 +1,9 @@ package com.litehed.hytalemodels.init; import com.litehed.hytalemodels.HytaleModelLoader; -import com.litehed.hytalemodels.blocks.HytaleChest; import com.litehed.hytalemodels.blocks.HytaleBlockBase; +import com.litehed.hytalemodels.blocks.HytaleChest; +import com.litehed.hytalemodels.blocks.HytaleCoffin; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockBehaviour; import net.neoforged.neoforge.registries.DeferredBlock; @@ -15,7 +16,7 @@ public class BlockInit { public static final DeferredBlock POT = BLOCKS.registerSimpleBlock("pot", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock CRYSTAL_BIG = BLOCKS.registerSimpleBlock("crystal_big", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock BED = BLOCKS.registerSimpleBlock("bed", () -> BlockBehaviour.Properties.of().noOcclusion()); - public static final DeferredBlock COFFIN = BLOCKS.registerSimpleBlock("coffin", () -> BlockBehaviour.Properties.of().noOcclusion()); + public static final DeferredBlock COFFIN = BLOCKS.registerBlock("coffin", HytaleCoffin::new); public static final DeferredBlock SLOPE = BLOCKS.registerBlock("slope", HytaleBlockBase::new); public static final DeferredBlock SMALL_CHEST = BLOCKS.registerBlock("chest_small", HytaleChest::new); public static final DeferredBlock CHAIR = BLOCKS.registerSimpleBlock("chair", () -> BlockBehaviour.Properties.of().noOcclusion()); From f92786a0a8dace60ca933a6a55e21659a79f88f0 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:23:21 -0500 Subject: [PATCH 10/17] Added documentation --- .../entity/HytaleBlockEntityRenderer.java | 89 ++++++++++++++++++- .../blocks/entity/HytaleRenderState.java | 4 +- .../blocks/entity/NodeTransform.java | 10 +++ .../blockymodel/BlockyTokenizer.java | 1 + .../hytalemodels/blockymodel/ParserUtil.java | 1 + .../animations/BlockyAnimationPlayer.java | 67 ++++++++++++++ 6 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index 9b6c9bf..10ea8c3 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -52,6 +52,12 @@ public void extractRenderState(T blockEntity, S renderState, float partialTick, } + /** + * Allows subclasses to extract additional data from the block entity and put it into the render state + * @param blockEntity the block entity being rendered + * @param renderState the render state being built for this block entity + * @param partialTick the current partial tick + */ protected void extractAdditionalRenderState(T blockEntity, S renderState, float partialTick) { } @@ -90,18 +96,43 @@ public void submit(S renderState, PoseStack poseStack, SubmitNodeCollector submi } + /** + * Calculates the transforms for all nodes in the model + * @param renderState the current render state containing animation tick and other relevant data + * @param geometry the geometry of the model being rendered + * @return a map of node IDs/names to their corresponding transforms for the current frame + */ protected abstract Map calculateAnimationTransforms(S renderState, BlockyModelGeometry geometry); + /** + * Gets the model location for a given model name + * @param modelName the name of the model + * @return the identifier pointing to the model file for this model name + */ protected Identifier getModelLocation(String modelName) { return Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "models/" + modelName + ".blockymodel"); } + /** + * Gets the texture material for a given model name + * @param modelName the name of the model + * @return the material pointing to the texture for this model name + */ protected Material getTextureMaterial(String modelName) { return new Material(TextureAtlas.LOCATION_BLOCKS, Identifier.fromNamespaceAndPath(HytaleModelLoader.MODID, "block/" + modelName + "_texture")); } + /** + * Renders a single node of the model, including applying any relevant transforms and rendering the quads for its shape + * @param poseStack the current pose stack for rendering + * @param collector the submit node collector to submit geometry to + * @param node the node being rendered + * @param sprite the texture sprite to use for rendering this node + * @param nodeTransforms the map of node transforms calculated for the current frame for animations + * @param renderState the current render state + */ private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, BlockyModelGeometry.BlockyNode node, TextureAtlasSprite sprite, Map nodeTransforms, S renderState) { @@ -149,6 +180,12 @@ protected RenderType getRenderType() { return RenderTypes.cutoutMovingBlock(); } + /** + * Applies the necessary transformations to the pose stack for rendering a given node + * @param poseStack the current pose stack for rendering + * @param node the node being rendered + * @param effectiveTransforms the map of effective transforms for all nodes + */ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyNode node, Map effectiveTransforms) { Vector3f worldPos = TransformCalculator.calculateWorldPosition(node); @@ -221,6 +258,12 @@ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyN } } + /** + * Recursively checks parent nodes for animation transforms to apply to the current node (Child nodes inherit animations) + * @param node the node being rendered + * @param effectiveTransforms the map of effective transforms for all nodes + * @return the first parent animation transform found, else null + */ private NodeTransform getParentAnimationTransform(BlockyModelGeometry.BlockyNode node, Map effectiveTransforms) { BlockyModelGeometry.BlockyNode current = node.getParent(); @@ -239,7 +282,19 @@ private NodeTransform getParentAnimationTransform(BlockyModelGeometry.BlockyNode return null; } - + /** + * Renders a single quad for a given face of the shape, applying the appropriate texture coordinates and normal based on the face direction + * @param buffer the vertex consumer buffer to submit vertices to + * @param pose the current pose for rendering, containing the transformation matrix and normal matrix + * @param direction the face direction this quad is being rendered for + * @param min the minimum corner of the quad in model space + * @param max the maximum corner of the quad in model space + * @param sprite the texture sprite to use for calculating UV coordinates + * @param texLayout the texture layout information for this face, containing UV offsets and sizes + * @param originalSize the original size of the shape, used for calculating UV coordinates when set to stretch + * @param renderState the current render state + * @param reversed whether to reverse the vertex order for this quad + */ private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction direction, Vector3f min, Vector3f max, TextureAtlasSprite sprite, BlockyModelGeometry.FaceTextureLayout texLayout, Vector3f originalSize, @@ -271,6 +326,15 @@ private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction di } } + /** + * Adds a single vertex to the buffer with the given position, normal, UV coordinates, and light information + * @param buffer the vertex consumer buffer to submit the vertex to + * @param pose the current pose transformation matrix to apply to the vertex position + * @param normal the normal vector for this vertex, used for lighting calculations + * @param vertex the position of the vertex in model space + * @param uv the UV coordinates for this vertex, calculated based on the texture layout and sprite + * @param lightCoords the combined block and sky light coordinates for this vertex, used for lighting calculations + */ private void addVertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, Vector3f vertex, float[] uv, int lightCoords) { int blockLight = lightCoords & 0xFFFF; @@ -283,10 +347,22 @@ private void addVertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, .setNormal(normal.x, normal.y, normal.z); } + /** + * Finds a node in the model geometry by its name, searching recursively through all nodes and their children + * @param geometry the model geometry to search through + * @param name the name of the node to find + * @return the node with the given name, else null + */ protected BlockyModelGeometry.BlockyNode findNodeByName(BlockyModelGeometry geometry, String name) { return findNodeByNameRecursive(geometry.getNodes(), name); } + /** + * Helper method to recursively search for a node by name through a list of nodes and their children + * @param nodes the list of nodes to search through + * @param name the name of the node to find + * @return the node with the given name, else null + */ private BlockyModelGeometry.BlockyNode findNodeByNameRecursive( List nodes, String name) { for (BlockyModelGeometry.BlockyNode node : nodes) { @@ -297,6 +373,12 @@ private BlockyModelGeometry.BlockyNode findNodeByNameRecursive( return null; } + /** + * Recursively applies a given rotation transform to a node and all of its descendants, storing the results in the provided transforms map + * @param node the node to apply the transform to, along with all of its children + * @param rotation the rotation to apply to this node and its descendants + * @param transforms the map to store the resulting transforms for each node, keyed by node ID or name + */ protected void applyTransformToDescendants(BlockyModelGeometry.BlockyNode node, Quaternionf rotation, Map transforms) { @@ -306,6 +388,11 @@ protected void applyTransformToDescendants(BlockyModelGeometry.BlockyNode node, } } + /** + * Gets the geometry for a given model location, either from the cache or by loading it from the model file if not already cached + * @param modelLocation the identifier pointing to the model file for this model + * @return the geometry for this model, else null + */ private BlockyModelGeometry getOrLoadGeometry(Identifier modelLocation) { String key = modelLocation.toString(); diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java index 981e56d..7de4e03 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java @@ -4,7 +4,7 @@ public class HytaleRenderState extends BlockEntityRenderState { public String modelName; - public int animationTick; + public int animationTick; // Animation tick + public float partialTick; // Partial tick for smooth animation public float ageInTicks; // Smoothed animation time in ticks sped 4x to work with blockyanim - public float partialTick; } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java index d25c2a3..79beada 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java @@ -49,6 +49,12 @@ public Vector3f scale() { return new Vector3f(scale); } + /** + * Merges this NodeTransform with another NodeTransform + * + * @param other The other NodeTransform to merge with + * @return The combined node transform + */ public NodeTransform merge(NodeTransform other) { Vector3f mergedPos = new Vector3f(this.position).add(other.position); Quaternionf mergedRot = new Quaternionf(this.rotation).mul(other.rotation); @@ -57,6 +63,10 @@ public NodeTransform merge(NodeTransform other) { return new NodeTransform(mergedPos, mergedRot, mergedScale, mergedVis); } + /** + * Checks if this NodeTransform is the identity transform + * @return true if this NodeTransform is the identity transform, false otherwise + */ public boolean isIdentity() { return position.lengthSquared() < EPSILON && rotation.equals(new Quaternionf(), EPSILON) && diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyTokenizer.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyTokenizer.java index 7db2d4b..7b98870 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyTokenizer.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyTokenizer.java @@ -8,6 +8,7 @@ import java.io.InputStream; import java.io.InputStreamReader; +// Tokenizer for reading BlockyModel JSON files. It reads the entire file into memory and parses it as a JsonObject. public class BlockyTokenizer implements AutoCloseable { private final BufferedReader lineReader; private final JsonObject root; diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java b/src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java index c99ba22..27a7387 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/ParserUtil.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; +// Utility class for parsing JSON objects with default values public class ParserUtil { public static float getFloatOrDefault(JsonObject obj, String key, float defaultValue) { return obj.has(key) ? obj.get(key).getAsFloat() : defaultValue; diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java index f5ffc45..6e3fb4d 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java @@ -32,6 +32,11 @@ public boolean isHoldLastKeyframe() { return definition.isHoldLastKeyframe(); } + /** + * Resolves the input time in ticks to a valid time within the animation's duration + * @param timeInTicks The input time in ticks + * @return A time value that is either clamped to the animation's duration (if holdLastKeyframe is true) else wrapped + */ private float resolveTime(float timeInTicks) { float duration = definition.getDuration(); return definition.isHoldLastKeyframe() @@ -39,12 +44,23 @@ private float resolveTime(float timeInTicks) { : timeInTicks % duration; } + /** + * Merges a new NodeTransform into the map for a given nodeId + * @param map The map to merge into + * @param nodeId The ID of the node whose transform is being merged + * @param incoming The new NodeTransform to merge + */ private static void mergeIntoMap(Map map, String nodeId, NodeTransform incoming) { map.merge(nodeId, incoming, NodeTransform::merge); } + /** + * Calculates the current transforms for all nodes based on the input time in ticks + * @param timeInTicks The current time in ticks for which to calculate the transforms + * @return A map of node IDs to their corresponding NodeTransforms at the given time + */ public Map calculateTransforms(float timeInTicks) { float t = resolveTime(timeInTicks); Map transforms = new HashMap<>(); @@ -79,6 +95,9 @@ public Map calculateTransforms(float timeInTicks) { return transforms; } + /** + * Initializes the interpolators for each node and track type based on the animation definition + */ private void initializeInterpolators() { for (Map.Entry> entry : definition.getNodeAnimations().entrySet()) { String nodeId = entry.getKey(); @@ -111,6 +130,13 @@ private void initializeInterpolators() { } } + /** + * Utility method to filter and cast a list of BlockyKeyframes to a specific subtype, based on the provided class type + * @param The specific subtype of BlockyKeyframe to filter and cast to + * @param raw The original list of BlockyKeyframes to filter and cast + * @param type The Class object representing the specific subtype of BlockyKeyframe to filter and cast to + * @return A new list containing only the keyframes from the original list that are instances of the specified type, cast to that type + */ private static List castKeyframes( List raw, Class type) { List result = new ArrayList<>(raw.size()); @@ -212,24 +238,58 @@ boolean isVisible(float t) { } + /** + * Utility method to clamp the interpolation alpha value between 0 and 1 based on the input time and the keyframe times + * @param t The current time for which to calculate the alpha + * @param tStart The time of the previous keyframe + * @param tEnd The time of the next keyframe + * @return The clamped alpha value between 0 and 1 + */ private static float clampAlpha(float t, float tStart, float tEnd) { float span = tEnd - tStart; if (span <= 0f) return 0f; return Math.clamp((t - tStart) / span, 0f, 1f); } + /** + * Utility method to apply a smoothstep function to the input alpha value for smoother interpolation + * @param t The input alpha value between 0 and 1 + * @return The output alpha value after applying the smoothstep function, also between 0 and 1 + */ private static float smoothstep(float t) { return t * t * (3 - 2 * t); } + /** + * Utility method to linearly interpolate between two Vector3f values based on the input alpha + * @param from The starting Vector3f value + * @param to The ending Vector3f value + * @param t The interpolation alpha value between 0 and 1 + * @return A new Vector3f that is the result of linearly interpolating between 'from' and 'to' based on 't' + */ private static Vector3f lerpVec(Vector3f from, Vector3f to, float t) { return new Vector3f(from).lerp(to, t); } + /** + * Utility method to interpolate between two Vector3f values using either linear or smooth interpolation based on the specified type + * @param from The starting Vector3f value + * @param to The ending Vector3f value + * @param t The interpolation alpha value between 0 and 1 + * @return A new Vector3f that is the result of interpolating between 'from' and 'to' based on 't' + */ private static Vector3f smoothstepVec(Vector3f from, Vector3f to, float t) { return lerpVec(from, to, smoothstep(t)); } + /** + * Utility method to interpolate between two Vector3f values based on the input alpha and interpolation type (linear or smooth) + * @param from The starting Vector3f value + * @param to The ending Vector3f value + * @param alpha The interpolation alpha value between 0 and 1 + * @param type The interpolation type (LINEAR or SMOOTH) + * @return A new Vector3f that is the result of interpolating between 'from' and 'to' based on 'alpha' + */ private static Vector3f interpolateVec(Vector3f from, Vector3f to, float alpha, BlockyKeyframe.InterpolationType type) { return switch (type) { @@ -238,6 +298,13 @@ private static Vector3f interpolateVec(Vector3f from, Vector3f to, float alpha, }; } + /** + * Utility method to find the index of the previous keyframe in a sorted list of keyframes based on the input time + * @param The specific subtype of BlockyKeyframe contained in the list + * @param keyframes A list of keyframes sorted by their time value in ascending order + * @param time The input time for which to find the previous keyframe index + * @return The index of the keyframe in the list that is the greatest keyframe time less than or equal to the input time + */ private static int prevIndex(List keyframes, float time) { int lo = 0, hi = keyframes.size() - 1; while (lo < hi) { From fc85ba5ca756a0ff1b5b2cbb9072f52d169029f4 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:32:00 -0500 Subject: [PATCH 11/17] Updated Readme --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 529a697..f8e3e5d 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,14 @@ Models are defined using `.blockymodel` files (custom binary/text format) and re ### v1.1.0 - [x] Check item and block scaling/translating using model json - [x] Implement custom BlockEntities for animation support since baked models cannot -- [ ] Add parser for animation support `.blockyanim` -- [ ] Load animations in for blocks -- [ ] Create animation system to actually play and time these animations -- [ ] Add wiki to show how to use different parts of the mod +- [x] Add parser for animation support `.blockyanim` +- [x] Load animations in for blocks +- [x] Create animation system to actually play and time these animations +- [x] Add wiki to show how to use different parts of the mod - [ ] Fix and clean up code ### v2.0.0 - [ ] Implement entity model loading -- [ ] Create in-game model preview/editing tool - [ ] Support for custom render layers and transparency blending - [ ] Clean code and docs for v2 release From bbe3624960751a27ead520f72c7c5238b9d150d7 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Fri, 20 Feb 2026 22:13:49 -0500 Subject: [PATCH 12/17] Made animated blocks rotatable --- .../entity/HytaleBlockEntityRenderer.java | 96 +++++++++++++------ .../blocks/entity/HytaleRenderState.java | 2 + 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index 10ea8c3..05ac49a 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -1,6 +1,7 @@ package com.litehed.hytalemodels.blocks.entity; import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.blocks.HytaleBlockBase; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; import com.litehed.hytalemodels.blockymodel.QuadBuilder; @@ -20,6 +21,7 @@ import net.minecraft.client.resources.model.Material; import net.minecraft.core.Direction; import net.minecraft.resources.Identifier; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import org.joml.Matrix3f; import org.joml.Matrix4f; @@ -48,12 +50,20 @@ public void extractRenderState(T blockEntity, S renderState, float partialTick, renderState.partialTick = partialTick; renderState.ageInTicks = (blockEntity.getAnimationTick() + partialTick) * 4; + if (blockEntity.getLevel() != null) { + BlockState blockState = blockEntity.getLevel().getBlockState(blockEntity.getBlockPos()); + if (blockState.hasProperty(HytaleBlockBase.FACING)) { + renderState.facing = blockState.getValue(HytaleBlockBase.FACING); + } + } + extractAdditionalRenderState(blockEntity, renderState, partialTick); } /** * Allows subclasses to extract additional data from the block entity and put it into the render state + * * @param blockEntity the block entity being rendered * @param renderState the render state being built for this block entity * @param partialTick the current partial tick @@ -83,6 +93,11 @@ public void submit(S renderState, PoseStack poseStack, SubmitNodeCollector submi poseStack.translate(0.5, 0.5, 0.5); + float yRotDegrees = getFacingYRotation(renderState.facing); + if (yRotDegrees != 0f) { + poseStack.mulPose(new Quaternionf().rotationY((float) Math.toRadians(yRotDegrees))); + } + Map nodeTransforms = calculateAnimationTransforms(renderState, geometry); List nodes = geometry.getNodes(); @@ -95,11 +110,27 @@ public void submit(S renderState, PoseStack poseStack, SubmitNodeCollector submi poseStack.popPose(); } + /** + * Gets the rotation degree based on direction blockstate + * + * @param facing The Direction the block is being placed in + * @return Degrees to rotate block along the Y + */ + protected float getFacingYRotation(Direction facing) { + return switch (facing) { + case NORTH -> 180f; + case EAST -> 90f; + case WEST -> 270f; + default -> 0f; + }; + } + /** * Calculates the transforms for all nodes in the model + * * @param renderState the current render state containing animation tick and other relevant data - * @param geometry the geometry of the model being rendered + * @param geometry the geometry of the model being rendered * @return a map of node IDs/names to their corresponding transforms for the current frame */ protected abstract Map calculateAnimationTransforms(S renderState, BlockyModelGeometry geometry); @@ -107,6 +138,7 @@ public void submit(S renderState, PoseStack poseStack, SubmitNodeCollector submi /** * Gets the model location for a given model name + * * @param modelName the name of the model * @return the identifier pointing to the model file for this model name */ @@ -116,6 +148,7 @@ protected Identifier getModelLocation(String modelName) { /** * Gets the texture material for a given model name + * * @param modelName the name of the model * @return the material pointing to the texture for this model name */ @@ -126,12 +159,13 @@ protected Material getTextureMaterial(String modelName) { /** * Renders a single node of the model, including applying any relevant transforms and rendering the quads for its shape - * @param poseStack the current pose stack for rendering - * @param collector the submit node collector to submit geometry to - * @param node the node being rendered - * @param sprite the texture sprite to use for rendering this node + * + * @param poseStack the current pose stack for rendering + * @param collector the submit node collector to submit geometry to + * @param node the node being rendered + * @param sprite the texture sprite to use for rendering this node * @param nodeTransforms the map of node transforms calculated for the current frame for animations - * @param renderState the current render state + * @param renderState the current render state */ private void renderNode(PoseStack poseStack, SubmitNodeCollector collector, BlockyModelGeometry.BlockyNode node, TextureAtlasSprite sprite, @@ -182,8 +216,9 @@ protected RenderType getRenderType() { /** * Applies the necessary transformations to the pose stack for rendering a given node - * @param poseStack the current pose stack for rendering - * @param node the node being rendered + * + * @param poseStack the current pose stack for rendering + * @param node the node being rendered * @param effectiveTransforms the map of effective transforms for all nodes */ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyNode node, @@ -260,7 +295,8 @@ private void applyNodeTransform(PoseStack poseStack, BlockyModelGeometry.BlockyN /** * Recursively checks parent nodes for animation transforms to apply to the current node (Child nodes inherit animations) - * @param node the node being rendered + * + * @param node the node being rendered * @param effectiveTransforms the map of effective transforms for all nodes * @return the first parent animation transform found, else null */ @@ -284,16 +320,17 @@ private NodeTransform getParentAnimationTransform(BlockyModelGeometry.BlockyNode /** * Renders a single quad for a given face of the shape, applying the appropriate texture coordinates and normal based on the face direction - * @param buffer the vertex consumer buffer to submit vertices to - * @param pose the current pose for rendering, containing the transformation matrix and normal matrix - * @param direction the face direction this quad is being rendered for - * @param min the minimum corner of the quad in model space - * @param max the maximum corner of the quad in model space - * @param sprite the texture sprite to use for calculating UV coordinates - * @param texLayout the texture layout information for this face, containing UV offsets and sizes + * + * @param buffer the vertex consumer buffer to submit vertices to + * @param pose the current pose for rendering, containing the transformation matrix and normal matrix + * @param direction the face direction this quad is being rendered for + * @param min the minimum corner of the quad in model space + * @param max the maximum corner of the quad in model space + * @param sprite the texture sprite to use for calculating UV coordinates + * @param texLayout the texture layout information for this face, containing UV offsets and sizes * @param originalSize the original size of the shape, used for calculating UV coordinates when set to stretch - * @param renderState the current render state - * @param reversed whether to reverse the vertex order for this quad + * @param renderState the current render state + * @param reversed whether to reverse the vertex order for this quad */ private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction direction, Vector3f min, Vector3f max, TextureAtlasSprite sprite, @@ -328,11 +365,12 @@ private void renderQuad(VertexConsumer buffer, PoseStack.Pose pose, Direction di /** * Adds a single vertex to the buffer with the given position, normal, UV coordinates, and light information - * @param buffer the vertex consumer buffer to submit the vertex to - * @param pose the current pose transformation matrix to apply to the vertex position - * @param normal the normal vector for this vertex, used for lighting calculations - * @param vertex the position of the vertex in model space - * @param uv the UV coordinates for this vertex, calculated based on the texture layout and sprite + * + * @param buffer the vertex consumer buffer to submit the vertex to + * @param pose the current pose transformation matrix to apply to the vertex position + * @param normal the normal vector for this vertex, used for lighting calculations + * @param vertex the position of the vertex in model space + * @param uv the UV coordinates for this vertex, calculated based on the texture layout and sprite * @param lightCoords the combined block and sky light coordinates for this vertex, used for lighting calculations */ private void addVertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, @@ -349,8 +387,9 @@ private void addVertex(VertexConsumer buffer, Matrix4f pose, Vector3f normal, /** * Finds a node in the model geometry by its name, searching recursively through all nodes and their children + * * @param geometry the model geometry to search through - * @param name the name of the node to find + * @param name the name of the node to find * @return the node with the given name, else null */ protected BlockyModelGeometry.BlockyNode findNodeByName(BlockyModelGeometry geometry, String name) { @@ -359,8 +398,9 @@ protected BlockyModelGeometry.BlockyNode findNodeByName(BlockyModelGeometry geom /** * Helper method to recursively search for a node by name through a list of nodes and their children + * * @param nodes the list of nodes to search through - * @param name the name of the node to find + * @param name the name of the node to find * @return the node with the given name, else null */ private BlockyModelGeometry.BlockyNode findNodeByNameRecursive( @@ -375,8 +415,9 @@ private BlockyModelGeometry.BlockyNode findNodeByNameRecursive( /** * Recursively applies a given rotation transform to a node and all of its descendants, storing the results in the provided transforms map - * @param node the node to apply the transform to, along with all of its children - * @param rotation the rotation to apply to this node and its descendants + * + * @param node the node to apply the transform to, along with all of its children + * @param rotation the rotation to apply to this node and its descendants * @param transforms the map to store the resulting transforms for each node, keyed by node ID or name */ protected void applyTransformToDescendants(BlockyModelGeometry.BlockyNode node, @@ -390,6 +431,7 @@ protected void applyTransformToDescendants(BlockyModelGeometry.BlockyNode node, /** * Gets the geometry for a given model location, either from the cache or by loading it from the model file if not already cached + * * @param modelLocation the identifier pointing to the model file for this model * @return the geometry for this model, else null */ diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java index 7de4e03..6aa5a7b 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java @@ -1,10 +1,12 @@ package com.litehed.hytalemodels.blocks.entity; import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.core.Direction; public class HytaleRenderState extends BlockEntityRenderState { public String modelName; public int animationTick; // Animation tick public float partialTick; // Partial tick for smooth animation public float ageInTicks; // Smoothed animation time in ticks sped 4x to work with blockyanim + public Direction facing; // Block facing direction for rotation } From bdc40d0351aa9408595aacc22faf3b2e4aef5cfc Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:22:24 -0500 Subject: [PATCH 13/17] No longer creates definition and player every time --- .../blocks/entity/AnimatedChestRenderer.java | 9 ++------- .../blocks/entity/CoffinRenderer.java | 9 ++------- .../entity/HytaleBlockEntityRenderer.java | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index f07f887..607e1b6 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -2,9 +2,6 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; -import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationDefinition; -import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationLoader; -import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.resources.Identifier; @@ -48,10 +45,8 @@ protected Map calculateAnimationTransforms(AnimatedChestR Map transforms = new HashMap<>(); Identifier animId = renderState.isOpen ? ANIM_OPEN : ANIM_CLOSE; - BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(animId); - if (definition != null) { - transforms.putAll(new BlockyAnimationPlayer(definition).calculateTransforms(renderState.ageInTicks)); - } + getOrCreatePlayer(animId).calculateTransforms(renderState.ageInTicks).putAll(transforms); + // Procedural System // float angle = (float) Math.sin(renderState.ageInTicks * ANIMATION_SPEED) * MAX_LID_ANGLE; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java index 93960b7..0198aa4 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java @@ -2,9 +2,6 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; -import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationDefinition; -import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationLoader; -import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.resources.Identifier; @@ -43,11 +40,9 @@ protected Map calculateAnimationTransforms(AnimatedChestR Map transforms = new HashMap<>(); Identifier animId = renderState.isOpen ? ANIM_OPEN : ANIM_CLOSE; - BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(animId); - if (definition != null) { - transforms.putAll(new BlockyAnimationPlayer(definition).calculateTransforms(renderState.ageInTicks)); - } + getOrCreatePlayer(animId).calculateTransforms(renderState.ageInTicks).putAll(transforms); + return transforms; } } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java index 05ac49a..524adf7 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java @@ -6,6 +6,9 @@ import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; import com.litehed.hytalemodels.blockymodel.QuadBuilder; import com.litehed.hytalemodels.blockymodel.TransformCalculator; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationDefinition; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationLoader; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import net.minecraft.client.Minecraft; @@ -36,6 +39,7 @@ public abstract class HytaleBlockEntityRenderer { private final Map geometryCache = new HashMap<>(); + private final Map playerCache = new HashMap<>(); public HytaleBlockEntityRenderer(BlockEntityRendererProvider.Context context) { } @@ -453,4 +457,19 @@ private BlockyModelGeometry getOrLoadGeometry(Identifier modelLocation) { return null; } } + + /** + * Returns a cached BlockyAnimationPlayer for the given animation ID, creating and caching one + * on first access. Returns null if the animation definition cannot be loaded. + * + * @param animId the identifier pointing to the animation file + * @return the cached player for this animation, else null + */ + protected BlockyAnimationPlayer getOrCreatePlayer(Identifier animId) { + return playerCache.computeIfAbsent(animId, id -> { + BlockyAnimationDefinition definition = BlockyAnimationLoader.INSTANCE.loadAnimation(id); + return definition != null ? new BlockyAnimationPlayer(definition) : null; + }); + } + } \ No newline at end of file From 16d5cb41e8006fbe94e11ed67f2ec9a81fac8cbc Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:33:32 -0500 Subject: [PATCH 14/17] Oops --- .../hytalemodels/blocks/entity/AnimatedChestRenderer.java | 6 +++++- .../hytalemodels/blocks/entity/CoffinRenderer.java | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index 607e1b6..46cd743 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -2,6 +2,7 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.resources.Identifier; @@ -45,7 +46,10 @@ protected Map calculateAnimationTransforms(AnimatedChestR Map transforms = new HashMap<>(); Identifier animId = renderState.isOpen ? ANIM_OPEN : ANIM_CLOSE; - getOrCreatePlayer(animId).calculateTransforms(renderState.ageInTicks).putAll(transforms); + BlockyAnimationPlayer player = getOrCreatePlayer(animId); + if (player != null) { + transforms.putAll(player.calculateTransforms(renderState.ageInTicks)); + } // Procedural System diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java index 0198aa4..1096b01 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java @@ -2,6 +2,7 @@ import com.litehed.hytalemodels.HytaleModelLoader; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; +import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.resources.Identifier; @@ -41,8 +42,11 @@ protected Map calculateAnimationTransforms(AnimatedChestR Identifier animId = renderState.isOpen ? ANIM_OPEN : ANIM_CLOSE; - getOrCreatePlayer(animId).calculateTransforms(renderState.ageInTicks).putAll(transforms); - + BlockyAnimationPlayer player = getOrCreatePlayer(animId); + if (player != null) { + transforms.putAll(player.calculateTransforms(renderState.ageInTicks)); + } + return transforms; } } \ No newline at end of file From 9f51cb0a551265ec7fd184458d93867aae5d67f2 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:59:32 -0500 Subject: [PATCH 15/17] Fixed issue with flat nodes --- .../blockymodel/BlockyModelParser.java | 2 +- .../litehed/hytalemodels/init/BlockInit.java | 3 + .../litehed/hytalemodels/init/ItemInit.java | 3 + .../blockstates/ash_leaves.json | 19 ++++ .../blockstates/slope_hay_hollow.json | 19 ++++ .../hytalemodelloader/items/ash_leaves.json | 6 ++ .../hytalemodelloader/items/onyxium.json | 6 ++ .../items/slope_hay_hollow.json | 6 ++ .../models/block/ash_leaves.json | 9 ++ .../models/block/slope_hay_hollow.json | 9 ++ .../hytalemodelloader/models/gen_model.py | 101 ++++++++++++++++++ .../models/item/ash_leaves.json | 3 + .../models/item/onyxium.json | 9 ++ .../models/item/slope_hay_hollow.json | 3 + 14 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/assets/hytalemodelloader/blockstates/ash_leaves.json create mode 100644 src/main/resources/assets/hytalemodelloader/blockstates/slope_hay_hollow.json create mode 100644 src/main/resources/assets/hytalemodelloader/items/ash_leaves.json create mode 100644 src/main/resources/assets/hytalemodelloader/items/onyxium.json create mode 100644 src/main/resources/assets/hytalemodelloader/items/slope_hay_hollow.json create mode 100644 src/main/resources/assets/hytalemodelloader/models/block/ash_leaves.json create mode 100644 src/main/resources/assets/hytalemodelloader/models/block/slope_hay_hollow.json create mode 100644 src/main/resources/assets/hytalemodelloader/models/gen_model.py create mode 100644 src/main/resources/assets/hytalemodelloader/models/item/ash_leaves.json create mode 100644 src/main/resources/assets/hytalemodelloader/models/item/onyxium.json create mode 100644 src/main/resources/assets/hytalemodelloader/models/item/slope_hay_hollow.json diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java index 2bd4398..869cc66 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/BlockyModelParser.java @@ -231,7 +231,7 @@ private static Vector3f parseSize(JsonObject shapeObj) { return new Vector3f( getFloatOrDefault(size, "x", DEFAULT_SIZE), getFloatOrDefault(size, "y", DEFAULT_SIZE), - getFloatOrDefault(size, "z", DEFAULT_SIZE) + getFloatOrDefault(size, "z", 0) ); } diff --git a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java index 8ac076f..60074b4 100644 --- a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java @@ -21,4 +21,7 @@ public class BlockInit { public static final DeferredBlock SMALL_CHEST = BLOCKS.registerBlock("chest_small", HytaleChest::new); public static final DeferredBlock CHAIR = BLOCKS.registerSimpleBlock("chair", () -> BlockBehaviour.Properties.of().noOcclusion()); public static final DeferredBlock TABLE = BLOCKS.registerSimpleBlock("table", () -> BlockBehaviour.Properties.of().noOcclusion()); + public static final DeferredBlock ASH_LEAVES = BLOCKS.registerBlock("ash_leaves", HytaleBlockBase::new); + public static final DeferredBlock SLOPE_HOLLOW = BLOCKS.registerBlock("slope_hay_hollow", HytaleBlockBase::new); + } diff --git a/src/main/java/com/litehed/hytalemodels/init/ItemInit.java b/src/main/java/com/litehed/hytalemodels/init/ItemInit.java index dcabd6a..a72cbe6 100644 --- a/src/main/java/com/litehed/hytalemodels/init/ItemInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/ItemInit.java @@ -17,6 +17,9 @@ public class ItemInit { public static final DeferredItem SMALL_CHEST = ITEMS.registerSimpleBlockItem("chest_small", BlockInit.SMALL_CHEST); public static final DeferredItem CHAIR = ITEMS.registerSimpleBlockItem("chair", BlockInit.CHAIR); public static final DeferredItem TABLE = ITEMS.registerSimpleBlockItem("table", BlockInit.TABLE); + public static final DeferredItem ASH_LEAVES = ITEMS.registerSimpleBlockItem("ash_leaves", BlockInit.ASH_LEAVES); + public static final DeferredItem SLOPE_HOLLOW = ITEMS.registerSimpleBlockItem("slope_hay_hollow", BlockInit.SLOPE_HOLLOW); public static final DeferredItem ADAMANTITE_PICK = ITEMS.registerSimpleItem("adamantite"); + public static final DeferredItem ONYXIUM = ITEMS.registerSimpleItem("onyxium"); } diff --git a/src/main/resources/assets/hytalemodelloader/blockstates/ash_leaves.json b/src/main/resources/assets/hytalemodelloader/blockstates/ash_leaves.json new file mode 100644 index 0000000..8f4e31e --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/blockstates/ash_leaves.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "hytalemodelloader:block/ash_leaves", + "y": 180 + }, + "facing=east": { + "model": "hytalemodelloader:block/ash_leaves", + "y": 270 + }, + "facing=south": { + "model": "hytalemodelloader:block/ash_leaves" + }, + "facing=west": { + "model": "hytalemodelloader:block/ash_leaves", + "y": 90 + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/blockstates/slope_hay_hollow.json b/src/main/resources/assets/hytalemodelloader/blockstates/slope_hay_hollow.json new file mode 100644 index 0000000..3e7f6c2 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/blockstates/slope_hay_hollow.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=north": { + "model": "hytalemodelloader:block/slope_hay_hollow", + "y": 180 + }, + "facing=east": { + "model": "hytalemodelloader:block/slope_hay_hollow", + "y": 270 + }, + "facing=south": { + "model": "hytalemodelloader:block/slope_hay_hollow" + }, + "facing=west": { + "model": "hytalemodelloader:block/slope_hay_hollow", + "y": 90 + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/items/ash_leaves.json b/src/main/resources/assets/hytalemodelloader/items/ash_leaves.json new file mode 100644 index 0000000..0557af9 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/items/ash_leaves.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "hytalemodelloader:item/ash_leaves" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/items/onyxium.json b/src/main/resources/assets/hytalemodelloader/items/onyxium.json new file mode 100644 index 0000000..8e7cf7b --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/items/onyxium.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "hytalemodelloader:item/onyxium" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/items/slope_hay_hollow.json b/src/main/resources/assets/hytalemodelloader/items/slope_hay_hollow.json new file mode 100644 index 0000000..fa40f43 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/items/slope_hay_hollow.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "hytalemodelloader:item/slope_hay_hollow" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/block/ash_leaves.json b/src/main/resources/assets/hytalemodelloader/models/block/ash_leaves.json new file mode 100644 index 0000000..8300e8c --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/block/ash_leaves.json @@ -0,0 +1,9 @@ +{ + "loader": "hytalemodelloader:blockymodel_loader", + "model": "hytalemodelloader:models/ash_leaves.blockymodel", + "render_type": "minecraft:cutout", + "textures": { + "texture": "hytalemodelloader:block/ash_leaves", + "particle": "hytalemodelloader:block/ash_leaves" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/block/slope_hay_hollow.json b/src/main/resources/assets/hytalemodelloader/models/block/slope_hay_hollow.json new file mode 100644 index 0000000..1289ee3 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/block/slope_hay_hollow.json @@ -0,0 +1,9 @@ +{ + "loader": "hytalemodelloader:blockymodel_loader", + "model": "hytalemodelloader:models/slope_hay_hollow.blockymodel", + "render_type": "minecraft:cutout", + "textures": { + "texture": "hytalemodelloader:block/slope_hay_hollow", + "particle": "hytalemodelloader:block/slope_hay_hollow" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/gen_model.py b/src/main/resources/assets/hytalemodelloader/models/gen_model.py new file mode 100644 index 0000000..09230d7 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/gen_model.py @@ -0,0 +1,101 @@ +import os +import json + +def ensure_folder(path): + if not os.path.exists(path): + os.makedirs(path) + +def write_json(path, data): + with open(path, "w") as f: + json.dump(data, f, indent=2) + +def generate_from_blockymodel(filepath, model_type): + filename = os.path.basename(filepath) + name = filename.replace(".blockymodel", "") + + # Folder structure + block_folder = "./block" + item_folder = "./item" + items_folder = "../items" + blockstates_folder = "../blockstates" + + ensure_folder(block_folder) + ensure_folder(item_folder) + ensure_folder(items_folder) + + if model_type == "block": + ensure_folder(blockstates_folder) + + # BLOCK MODEL + if model_type == "block": + block_json = { + "loader": "hytalemodelloader:blockymodel_loader", + "model": f"hytalemodelloader:models/{name}.blockymodel", + "render_type": "minecraft:cutout", + "textures": { + "texture": f"hytalemodelloader:block/{name}", + "particle": f"hytalemodelloader:block/{name}" + } + } + write_json(f"{block_folder}/{name}.json", block_json) + + # Item JSON (block item) + item_json = { + "parent": f"hytalemodelloader:block/{name}" + } + write_json(f"{item_folder}/{name}.json", item_json) + + # Items folder JSON + items_json = { + "model": { + "type": "minecraft:model", + "model": f"hytalemodelloader:item/{name}" + } + } + write_json(f"{items_folder}/{name}.json", items_json) + + # Blockstates JSON + blockstates_json = { + "variants": { + "facing=north": {"model": f"hytalemodelloader:block/{name}", "y": 180}, + "facing=east": {"model": f"hytalemodelloader:block/{name}", "y": 270}, + "facing=south": {"model": f"hytalemodelloader:block/{name}"}, + "facing=west": {"model": f"hytalemodelloader:block/{name}", "y": 90} + } + } + write_json(f"{blockstates_folder}/{name}.json", blockstates_json) + + print(f"Block model generated for {name}") + + # ITEM MODEL + elif model_type == "item": + item_json = { + "parent": "neoforge:item/default", + "loader": "hytalemodelloader:blockymodel_loader", + "model": f"hytalemodelloader:models/{name}.blockymodel", + "textures": { + "texture": f"hytalemodelloader:item/{name}_texture", + "particle": f"hytalemodelloader:item/{name}_texture" + } + } + write_json(f"{item_folder}/{name}.json", item_json) + + items_json = { + "model": { + "type": "minecraft:model", + "model": f"hytalemodelloader:item/{name}" + } + } + write_json(f"{items_folder}/{name}.json", items_json) + + print(f"Item model generated for {name}") + + else: + print("Invalid type. Must be 'block' or 'item'.") + + +if __name__ == "__main__": + filepath = input("Enter the .blockymodel file path: ").strip() + model_type = input("Is this a block or item? ").strip().lower() + + generate_from_blockymodel(filepath, model_type) \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/item/ash_leaves.json b/src/main/resources/assets/hytalemodelloader/models/item/ash_leaves.json new file mode 100644 index 0000000..00ba395 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/item/ash_leaves.json @@ -0,0 +1,3 @@ +{ + "parent": "hytalemodelloader:block/ash_leaves" +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/item/onyxium.json b/src/main/resources/assets/hytalemodelloader/models/item/onyxium.json new file mode 100644 index 0000000..def3f4e --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/item/onyxium.json @@ -0,0 +1,9 @@ +{ + "parent": "neoforge:item/default", + "loader": "hytalemodelloader:blockymodel_loader", + "model": "hytalemodelloader:models/onyxium.blockymodel", + "textures": { + "texture": "hytalemodelloader:item/onyxium_texture", + "particle": "hytalemodelloader:item/onyxium_texture" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/hytalemodelloader/models/item/slope_hay_hollow.json b/src/main/resources/assets/hytalemodelloader/models/item/slope_hay_hollow.json new file mode 100644 index 0000000..7c6b508 --- /dev/null +++ b/src/main/resources/assets/hytalemodelloader/models/item/slope_hay_hollow.json @@ -0,0 +1,3 @@ +{ + "parent": "hytalemodelloader:block/slope_hay_hollow" +} \ No newline at end of file From 758a755ba094c58c78eee77953d397d199c4d7a5 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:54:14 -0500 Subject: [PATCH 16/17] Updated some readme and doc stuff --- README.md | 19 ++++++++++++++----- gradle.properties | 7 ++----- .../templates/META-INF/neoforge.mods.toml | 3 ++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f8e3e5d..6702e41 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ - Hytale Model Loader ======= -A mod for Minecraft Neoforge 1.21.11 that allows the player to import and use Hytale models in game. It converts Hytale's BlockyModel file format into a renderable version inside of Minecraft. Currently, these can be rendered as item or block models with entity models coming in a future version. This is usable with both resource packs and mods. +A mod for Minecraft Neoforge 1.21.11 that allows the player to import and use Hytale models in game. It converts +Hytale's BlockyModel and BLockyAnim file formats into renderable versions inside of Minecraft. Currently, these can be +rendered as item or block models with entity models coming in a future version. This is usable with both resource packs +and mods. ### Creating a Model -Models are defined using `.blockymodel` files (custom binary/text format) and referenced in `.json` model files just like standard Minecraft models. To create the model simply download this mod and when creating a model file make sure to add the loader and model location as shown below. +Refer to the wiki to learn how to create and animate your models. #### Example: `pot.json` @@ -23,15 +25,16 @@ Models are defined using `.blockymodel` files (custom binary/text format) and re ``` **Key fields:** + - `loader` – References the BlockyModelLoader **This is most important** - `model` – Path to your `.blockymodel` file (This file is best in the models folder but put it wherever) - `render_type` – Standard Minecraft render type this needs to be changed depending on model transparency - `textures` – Texture references used by your model - ## TODO ### v1.0.0 + - [x] Add model parser `.blockymodel` - [x] Implement custom item/block loader - [x] Add block rotation support @@ -39,22 +42,28 @@ Models are defined using `.blockymodel` files (custom binary/text format) and re - [x] Add UV rotation and mirroring support ### v1.1.0 + - [x] Check item and block scaling/translating using model json - [x] Implement custom BlockEntities for animation support since baked models cannot - [x] Add parser for animation support `.blockyanim` - [x] Load animations in for blocks - [x] Create animation system to actually play and time these animations - [x] Add wiki to show how to use different parts of the mod -- [ ] Fix and clean up code +- [x] Fix and clean up code ### v2.0.0 + - [ ] Implement entity model loading - [ ] Support for custom render layers and transparency blending - [ ] Clean code and docs for v2 release ### v2.1.0 + - [ ] Add animation support for entities +### v2.2.0 + +- [ ] Add player model swapping ## Contributing diff --git a/gradle.properties b/gradle.properties index b36419c..4ef24b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,6 @@ org.gradle.daemon=true org.gradle.parallel=true org.gradle.caching=true org.gradle.configuration-cache=true - #read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment # you can also find the latest versions at: https://parchmentmc.org/docs/getting-started parchment_minecraft_version=1.21.11 @@ -19,18 +18,16 @@ minecraft_version=1.21.11 minecraft_version_range=[1.21.11] # The Neo version must agree with the Minecraft version to get a valid artifact neo_version=21.11.37-beta - ## Mod Properties - # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} # Must match the String constant located in the main mod class annotated with @Mod. mod_id=hytalemodelloader # The human-readable display name for the mod. mod_name=HytaleModelLoader # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. -mod_license=All Rights Reserved +mod_license=MIT # The mod version. See https://semver.org/ -mod_version=1.0.0 +mod_version=1.1.0 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml index c5e16be..c5e4be6 100644 --- a/src/main/templates/META-INF/neoforge.mods.toml +++ b/src/main/templates/META-INF/neoforge.mods.toml @@ -27,7 +27,7 @@ displayName = "${mod_name}" #mandatory #updateJSONURL="https://change.me.example.invalid/updates.json" #optional # A URL for the "homepage" for this mod, displayed in the mod UI -#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional +displayURL="https://github.com/litehed/HytaleModelLoader/wiki" #optional # A file name (in the root of the mod JAR) containing a logo for display #logoFile="examplemod.png" #optional @@ -41,6 +41,7 @@ authors = "litehed" # The description text for the mod (multi line!) (#mandatory) description = ''' Hytale Model Loader enables importing .blockymodel models and their respective .blockyanim animations from Hytale into Minecraft. Simply reference this loader in your model file and point to the blockymodel location to utilize this mod. Entity support will come in the future. +For more advanced usages check the wiki. ''' # The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. From 043adec4a3d0160abe20c1dfcb7bc7dd136eb8e6 Mon Sep 17 00:00:00 2001 From: Ethan Leitner <57595571+litehed@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:25:01 -0500 Subject: [PATCH 17/17] Updated mod to be easier to interact with --- .../java/com/litehed/hytalemodels/Config.java | 8 ---- .../hytalemodels/HytaleModelLoader.java | 4 -- .../{blocks/entity => api}/NodeTransform.java | 2 +- .../block}/HytaleBlockBase.java | 4 +- .../block}/entity/HytaleBlockEntity.java | 40 ++++++++++++++++--- .../entity/HytaleBlockEntityRenderer.java | 21 +++++++--- .../block}/entity/HytaleRenderState.java | 9 ++++- .../hytalemodels/blocks/HytaleChest.java | 1 + .../hytalemodels/blocks/HytaleCoffin.java | 1 + .../entity/AnimatedChestBlockEntity.java | 1 + .../entity/AnimatedChestRenderState.java | 2 + .../blocks/entity/AnimatedChestRenderer.java | 2 + .../blocks/entity/CoffinBlockEntity.java | 1 + .../blocks/entity/CoffinRenderer.java | 2 + .../animations/BlockyAnimationPlayer.java | 2 +- .../litehed/hytalemodels/init/BlockInit.java | 2 +- 16 files changed, 74 insertions(+), 28 deletions(-) delete mode 100644 src/main/java/com/litehed/hytalemodels/Config.java rename src/main/java/com/litehed/hytalemodels/{blocks/entity => api}/NodeTransform.java (98%) rename src/main/java/com/litehed/hytalemodels/{blocks => api/block}/HytaleBlockBase.java (95%) rename src/main/java/com/litehed/hytalemodels/{blocks => api/block}/entity/HytaleBlockEntity.java (50%) rename src/main/java/com/litehed/hytalemodels/{blocks => api/block}/entity/HytaleBlockEntityRenderer.java (96%) rename src/main/java/com/litehed/hytalemodels/{blocks => api/block}/entity/HytaleRenderState.java (51%) diff --git a/src/main/java/com/litehed/hytalemodels/Config.java b/src/main/java/com/litehed/hytalemodels/Config.java deleted file mode 100644 index 3385b69..0000000 --- a/src/main/java/com/litehed/hytalemodels/Config.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.litehed.hytalemodels; - -import net.neoforged.neoforge.common.ModConfigSpec; - -public class Config { - private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder(); - static final ModConfigSpec SPEC = BUILDER.build(); -} diff --git a/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java b/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java index e472c89..be8c8e2 100644 --- a/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java +++ b/src/main/java/com/litehed/hytalemodels/HytaleModelLoader.java @@ -7,7 +7,6 @@ import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; -import net.neoforged.fml.config.ModConfig; import org.slf4j.Logger; // The value here should match an entry in the META-INF/neoforge.mods.toml file @@ -21,8 +20,5 @@ public HytaleModelLoader(IEventBus modEventBus, ModContainer modContainer) { BlockInit.BLOCKS.register(modEventBus); BlockEntityInit.BLOCK_ENTITIES.register(modEventBus); ItemInit.ITEMS.register(modEventBus); - - // Register our mod's ModConfigSpec so that FML can create and load the config file for us - modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC); } } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java b/src/main/java/com/litehed/hytalemodels/api/NodeTransform.java similarity index 98% rename from src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java rename to src/main/java/com/litehed/hytalemodels/api/NodeTransform.java index 79beada..4dbe5ce 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/NodeTransform.java +++ b/src/main/java/com/litehed/hytalemodels/api/NodeTransform.java @@ -1,4 +1,4 @@ -package com.litehed.hytalemodels.blocks.entity; +package com.litehed.hytalemodels.api; import org.joml.Quaternionf; import org.joml.Vector3f; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleBlockBase.java b/src/main/java/com/litehed/hytalemodels/api/block/HytaleBlockBase.java similarity index 95% rename from src/main/java/com/litehed/hytalemodels/blocks/HytaleBlockBase.java rename to src/main/java/com/litehed/hytalemodels/api/block/HytaleBlockBase.java index 87304de..7972d47 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/HytaleBlockBase.java +++ b/src/main/java/com/litehed/hytalemodels/api/block/HytaleBlockBase.java @@ -1,4 +1,4 @@ -package com.litehed.hytalemodels.blocks; +package com.litehed.hytalemodels.api.block; import net.minecraft.core.Direction; import net.minecraft.world.item.context.BlockPlaceContext; @@ -31,10 +31,12 @@ public BlockState getStateForPlacement(BlockPlaceContext context) { return super.getStateForPlacement(context).setValue(FACING, context.getHorizontalDirection().getOpposite()); } + @Override public BlockState rotate(BlockState state, Rotation rot) { return state.setValue(FACING, rot.rotate(state.getValue(FACING))); } + @Override public BlockState mirror(BlockState state, Mirror mirrorIn) { return state.rotate(mirrorIn.getRotation(state.getValue(FACING))); } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntity.java b/src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleBlockEntity.java similarity index 50% rename from src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntity.java rename to src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleBlockEntity.java index ed27651..adbb65f 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntity.java +++ b/src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleBlockEntity.java @@ -1,4 +1,4 @@ -package com.litehed.hytalemodels.blocks.entity; +package com.litehed.hytalemodels.api.block.entity; import net.minecraft.core.BlockPos; import net.minecraft.world.level.block.entity.BlockEntity; @@ -15,16 +15,30 @@ public abstract class HytaleBlockEntity extends BlockEntity { public HytaleBlockEntity(BlockEntityType type, BlockPos pos, BlockState state, String modelName) { super(type, pos, state); this.modelName = modelName; - } + } + /** + * Returns the name of the Blocky model used to render this block entity. + * This is used to locate the {@code .blockymodel} file and the associated texture. + * + * @return the model name; never null or blank + */ public String getModelName() { return modelName; } - public int getAnimationTick() { - return 0; - } + /** + * Returns the current animation tick for this block entity. + * + *

This value is used alongside the partial tick to compute smooth animation playback. + * Internally, it is multiplied by 4 before being passed to the animation system to match + * Blockyanims expected animation speed. Increment this each game tick in your static + * {@code tick()} method for continuous animations. + * + * @return the current animation tick; must be a non-negative integer + */ + public abstract int getAnimationTick(); @Override protected void loadAdditional(ValueInput input) { @@ -39,10 +53,26 @@ protected void saveAdditional(ValueOutput output) { } + /** + * Override this method to load custom animation or state data from NBT/storage on world load. + * Called automatically during {@link #loadAdditional}. + * + *

Default implementation does nothing. + * + * @param input the value input to read from + */ protected void loadAnimationData(ValueInput input) { // Default implementation does nothing } + /** + * Override this method to save custom animation or state data to NBT/storage for persistence. + * Called automatically during {@link #saveAdditional}. + * + *

Default implementation does nothing. + * + * @param output the value output to write to + */ protected void saveAnimationData(ValueOutput output) { // Default implementation does nothing } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java b/src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleBlockEntityRenderer.java similarity index 96% rename from src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java rename to src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleBlockEntityRenderer.java index 524adf7..0193955 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleBlockEntityRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleBlockEntityRenderer.java @@ -1,7 +1,8 @@ -package com.litehed.hytalemodels.blocks.entity; +package com.litehed.hytalemodels.api.block.entity; import com.litehed.hytalemodels.HytaleModelLoader; -import com.litehed.hytalemodels.blocks.HytaleBlockBase; +import com.litehed.hytalemodels.api.NodeTransform; +import com.litehed.hytalemodels.api.block.HytaleBlockBase; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; import com.litehed.hytalemodels.blockymodel.BlockyModelLoader; import com.litehed.hytalemodels.blockymodel.QuadBuilder; @@ -115,10 +116,20 @@ public void submit(S renderState, PoseStack poseStack, SubmitNodeCollector submi } /** - * Gets the rotation degree based on direction blockstate + * Returns the Y-axis rotation in degrees that should be applied to the model based on the + * block's facing direction. * - * @param facing The Direction the block is being placed in - * @return Degrees to rotate block along the Y + *

Override to customize facing rotation behaviour. Default mapping: + *

    + *
  • NORTH -> 180
  • + *
  • EAST -> 90
  • + *
  • WEST -> 270
  • + *
  • SOUTH -> 0
  • + *
+ * + * @param facing the direction the block is facing; may be {@code null} if the block has no + * facing property, in which case 0° is returned + * @return the rotation in degrees around the Y axis */ protected float getFacingYRotation(Direction facing) { return switch (facing) { diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java b/src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleRenderState.java similarity index 51% rename from src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java rename to src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleRenderState.java index 6aa5a7b..c9eb2a2 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/HytaleRenderState.java +++ b/src/main/java/com/litehed/hytalemodels/api/block/entity/HytaleRenderState.java @@ -1,4 +1,4 @@ -package com.litehed.hytalemodels.blocks.entity; +package com.litehed.hytalemodels.api.block.entity; import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; import net.minecraft.core.Direction; @@ -7,6 +7,11 @@ public class HytaleRenderState extends BlockEntityRenderState { public String modelName; public int animationTick; // Animation tick public float partialTick; // Partial tick for smooth animation - public float ageInTicks; // Smoothed animation time in ticks sped 4x to work with blockyanim + /** + * The smoothed animation time in ticks, pre-multiplied by 4 to match blockyanim internal + * animation speed expectations. Computed as {@code (animationTick + partialTick) * 4}. + * Pass this value directly to {@code BlockyAnimationPlayer} when sampling keyframes. + */ + public float ageInTicks; public Direction facing; // Block facing direction for rotation } diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java index b572933..16d4b42 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleChest.java @@ -1,5 +1,6 @@ package com.litehed.hytalemodels.blocks; +import com.litehed.hytalemodels.api.block.HytaleBlockBase; import com.litehed.hytalemodels.blocks.entity.AnimatedChestBlockEntity; import net.minecraft.core.BlockPos; import net.minecraft.world.InteractionResult; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java b/src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java index ba7a8a7..2bad1b9 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/HytaleCoffin.java @@ -1,5 +1,6 @@ package com.litehed.hytalemodels.blocks; +import com.litehed.hytalemodels.api.block.HytaleBlockBase; import com.litehed.hytalemodels.blocks.entity.CoffinBlockEntity; import net.minecraft.core.BlockPos; import net.minecraft.world.InteractionResult; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java index 16549ab..b834408 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestBlockEntity.java @@ -1,5 +1,6 @@ package com.litehed.hytalemodels.blocks.entity; +import com.litehed.hytalemodels.api.block.entity.HytaleBlockEntity; import com.litehed.hytalemodels.init.BlockEntityInit; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java index ba1b083..48b4a86 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderState.java @@ -1,5 +1,7 @@ package com.litehed.hytalemodels.blocks.entity; +import com.litehed.hytalemodels.api.block.entity.HytaleRenderState; + public class AnimatedChestRenderState extends HytaleRenderState { public boolean isOpen; } \ No newline at end of file diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java index 46cd743..3b4025e 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/AnimatedChestRenderer.java @@ -1,6 +1,8 @@ package com.litehed.hytalemodels.blocks.entity; import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.api.block.entity.HytaleBlockEntityRenderer; +import com.litehed.hytalemodels.api.NodeTransform; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java index 83dc9cf..118ed6b 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinBlockEntity.java @@ -1,5 +1,6 @@ package com.litehed.hytalemodels.blocks.entity; +import com.litehed.hytalemodels.api.block.entity.HytaleBlockEntity; import com.litehed.hytalemodels.init.BlockEntityInit; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; diff --git a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java index 1096b01..8709255 100644 --- a/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java +++ b/src/main/java/com/litehed/hytalemodels/blocks/entity/CoffinRenderer.java @@ -1,6 +1,8 @@ package com.litehed.hytalemodels.blocks.entity; import com.litehed.hytalemodels.HytaleModelLoader; +import com.litehed.hytalemodels.api.block.entity.HytaleBlockEntityRenderer; +import com.litehed.hytalemodels.api.NodeTransform; import com.litehed.hytalemodels.blockymodel.BlockyModelGeometry; import com.litehed.hytalemodels.blockymodel.animations.BlockyAnimationPlayer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; diff --git a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java index 6e3fb4d..61fbe3b 100644 --- a/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java +++ b/src/main/java/com/litehed/hytalemodels/blockymodel/animations/BlockyAnimationPlayer.java @@ -1,6 +1,6 @@ package com.litehed.hytalemodels.blockymodel.animations; -import com.litehed.hytalemodels.blocks.entity.NodeTransform; +import com.litehed.hytalemodels.api.NodeTransform; import org.joml.Quaternionf; import org.joml.Vector3f; diff --git a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java index 60074b4..672bf54 100644 --- a/src/main/java/com/litehed/hytalemodels/init/BlockInit.java +++ b/src/main/java/com/litehed/hytalemodels/init/BlockInit.java @@ -1,7 +1,7 @@ package com.litehed.hytalemodels.init; import com.litehed.hytalemodels.HytaleModelLoader; -import com.litehed.hytalemodels.blocks.HytaleBlockBase; +import com.litehed.hytalemodels.api.block.HytaleBlockBase; import com.litehed.hytalemodels.blocks.HytaleChest; import com.litehed.hytalemodels.blocks.HytaleCoffin; import net.minecraft.world.level.block.Block;