package com.simibubi.create.foundation.ponder.element; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.function.Consumer; import com.jozufozu.flywheel.core.model.ModelUtil; import com.jozufozu.flywheel.core.model.ShadeSeparatedBufferedData; import com.jozufozu.flywheel.core.model.ShadeSeparatingVertexConsumer; import com.jozufozu.flywheel.util.transform.TransformStack; import com.mojang.blaze3d.vertex.BufferBuilder; import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.SheetedDecalTextureGenerator; import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.blaze3d.vertex.VertexFormat; import com.simibubi.create.CreateClient; import com.simibubi.create.foundation.outliner.AABBOutline; import com.simibubi.create.foundation.ponder.PonderScene; import com.simibubi.create.foundation.ponder.PonderWorld; import com.simibubi.create.foundation.ponder.Selection; import com.simibubi.create.foundation.render.BlockEntityRenderHelper; import com.simibubi.create.foundation.render.SuperByteBuffer; import com.simibubi.create.foundation.render.SuperByteBufferCache; import com.simibubi.create.foundation.render.SuperRenderTypeBuffer; import com.simibubi.create.foundation.utility.AnimationTickHolder; import com.simibubi.create.foundation.utility.Pair; import com.simibubi.create.foundation.utility.VecHelper; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.ItemBlockRenderTypes; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.block.BlockRenderDispatcher; import net.minecraft.client.renderer.block.ModelBlockRenderer; import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction.Axis; import net.minecraft.util.Mth; import net.minecraft.world.level.ClipContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; 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.state.BlockState; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraftforge.client.ForgeHooksClient; import net.minecraftforge.client.model.data.EmptyModelData; public class WorldSectionElement extends AnimatedSceneElement { public static final SuperByteBufferCache.Compartment> DOC_WORLD_SECTION = new SuperByteBufferCache.Compartment<>(); private static final ThreadLocal THREAD_LOCAL_OBJECTS = ThreadLocal.withInitial(ThreadLocalObjects::new); List renderedBlockEntities; List>> tickableBlockEntities; Selection section; boolean redraw; Vec3 prevAnimatedOffset = Vec3.ZERO; Vec3 animatedOffset = Vec3.ZERO; Vec3 prevAnimatedRotation = Vec3.ZERO; Vec3 animatedRotation = Vec3.ZERO; Vec3 centerOfRotation = Vec3.ZERO; Vec3 stabilizationAnchor = null; BlockPos selectedBlock; public WorldSectionElement() {} public WorldSectionElement(Selection section) { this.section = section.copy(); centerOfRotation = section.getCenter(); } public void mergeOnto(WorldSectionElement other) { setVisible(false); if (other.isEmpty()) other.set(section); else other.add(section); } public void set(Selection selection) { applyNewSelection(selection.copy()); } public void add(Selection toAdd) { applyNewSelection(this.section.add(toAdd)); } public void erase(Selection toErase) { applyNewSelection(this.section.substract(toErase)); } private void applyNewSelection(Selection selection) { this.section = selection; queueRedraw(); } public void setCenterOfRotation(Vec3 center) { centerOfRotation = center; } public void stabilizeRotation(Vec3 anchor) { stabilizationAnchor = anchor; } @Override public void reset(PonderScene scene) { super.reset(scene); resetAnimatedTransform(); resetSelectedBlock(); } public void selectBlock(BlockPos pos) { selectedBlock = pos; } public void resetSelectedBlock() { selectedBlock = null; } public void resetAnimatedTransform() { prevAnimatedOffset = Vec3.ZERO; animatedOffset = Vec3.ZERO; prevAnimatedRotation = Vec3.ZERO; animatedRotation = Vec3.ZERO; } public void queueRedraw() { redraw = true; } public boolean isEmpty() { return section == null; } public void setEmpty() { section = null; } public void setAnimatedRotation(Vec3 eulerAngles, boolean force) { this.animatedRotation = eulerAngles; if (force) prevAnimatedRotation = animatedRotation; } public Vec3 getAnimatedRotation() { return animatedRotation; } public void setAnimatedOffset(Vec3 offset, boolean force) { this.animatedOffset = offset; if (force) prevAnimatedOffset = animatedOffset; } public Vec3 getAnimatedOffset() { return animatedOffset; } @Override public boolean isVisible() { return super.isVisible() && !isEmpty(); } class WorldSectionRayTraceResult { Vec3 actualHitVec; BlockPos worldPos; } public Pair rayTrace(PonderWorld world, Vec3 source, Vec3 target) { world.setMask(this.section); Vec3 transformedTarget = reverseTransformVec(target); BlockHitResult rayTraceBlocks = world.clip(new ClipContext(reverseTransformVec(source), transformedTarget, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, null)); world.clearMask(); if (rayTraceBlocks == null) return null; if (rayTraceBlocks.getLocation() == null) return null; double t = rayTraceBlocks.getLocation() .subtract(transformedTarget) .lengthSqr() / source.subtract(target) .lengthSqr(); Vec3 actualHit = VecHelper.lerp((float) t, target, source); return Pair.of(actualHit, rayTraceBlocks); } private Vec3 reverseTransformVec(Vec3 in) { float pt = AnimationTickHolder.getPartialTicks(); in = in.subtract(VecHelper.lerp(pt, prevAnimatedOffset, animatedOffset)); if (!animatedRotation.equals(Vec3.ZERO) || !prevAnimatedRotation.equals(Vec3.ZERO)) { if (centerOfRotation == null) centerOfRotation = section.getCenter(); double rotX = Mth.lerp(pt, prevAnimatedRotation.x, animatedRotation.x); double rotZ = Mth.lerp(pt, prevAnimatedRotation.z, animatedRotation.z); double rotY = Mth.lerp(pt, prevAnimatedRotation.y, animatedRotation.y); in = in.subtract(centerOfRotation); in = VecHelper.rotate(in, -rotX, Axis.X); in = VecHelper.rotate(in, -rotZ, Axis.Z); in = VecHelper.rotate(in, -rotY, Axis.Y); in = in.add(centerOfRotation); if (stabilizationAnchor != null) { in = in.subtract(stabilizationAnchor); in = VecHelper.rotate(in, rotX, Axis.X); in = VecHelper.rotate(in, rotZ, Axis.Z); in = VecHelper.rotate(in, rotY, Axis.Y); in = in.add(stabilizationAnchor); } } return in; } public void transformMS(PoseStack ms, float pt) { TransformStack.cast(ms) .translate(VecHelper.lerp(pt, prevAnimatedOffset, animatedOffset)); if (!animatedRotation.equals(Vec3.ZERO) || !prevAnimatedRotation.equals(Vec3.ZERO)) { if (centerOfRotation == null) centerOfRotation = section.getCenter(); double rotX = Mth.lerp(pt, prevAnimatedRotation.x, animatedRotation.x); double rotZ = Mth.lerp(pt, prevAnimatedRotation.z, animatedRotation.z); double rotY = Mth.lerp(pt, prevAnimatedRotation.y, animatedRotation.y); TransformStack.cast(ms) .translate(centerOfRotation) .rotateX(rotX) .rotateZ(rotZ) .rotateY(rotY) .translateBack(centerOfRotation); if (stabilizationAnchor != null) { TransformStack.cast(ms) .translate(stabilizationAnchor) .rotateX(-rotX) .rotateZ(-rotZ) .rotateY(-rotY) .translateBack(stabilizationAnchor); } } } public void tick(PonderScene scene) { prevAnimatedOffset = animatedOffset; prevAnimatedRotation = animatedRotation; if (!isVisible()) return; loadBEsIfMissing(scene.getWorld()); renderedBlockEntities.removeIf(be -> scene.getWorld() .getBlockEntity(be.getBlockPos()) != be); tickableBlockEntities.removeIf(be -> scene.getWorld() .getBlockEntity(be.getFirst() .getBlockPos()) != be.getFirst()); tickableBlockEntities.forEach(be -> be.getSecond() .accept(scene.getWorld())); } @Override public void whileSkipping(PonderScene scene) { if (redraw) { renderedBlockEntities = null; tickableBlockEntities = null; } redraw = false; } protected void loadBEsIfMissing(PonderWorld world) { if (renderedBlockEntities != null) return; tickableBlockEntities = new ArrayList<>(); renderedBlockEntities = new ArrayList<>(); section.forEach(pos -> { BlockEntity blockEntity = world.getBlockEntity(pos); BlockState blockState = world.getBlockState(pos); Block block = blockState.getBlock(); if (blockEntity == null) return; if (!(block instanceof EntityBlock)) return; blockEntity.setBlockState(world.getBlockState(pos)); BlockEntityTicker ticker = ((EntityBlock) block).getTicker(world, blockState, blockEntity.getType()); if (ticker != null) addTicker(blockEntity, ticker); renderedBlockEntities.add(blockEntity); }); } @SuppressWarnings("unchecked") private void addTicker(T blockEntity, BlockEntityTicker ticker) { tickableBlockEntities.add(Pair.of(blockEntity, w -> ((BlockEntityTicker) ticker).tick(w, blockEntity.getBlockPos(), blockEntity.getBlockState(), blockEntity))); } @Override public void renderFirst(PonderWorld world, MultiBufferSource buffer, PoseStack ms, float fade, float pt) { int light = -1; if (fade != 1) light = (int) (Mth.lerp(fade, 5, 14)); if (redraw) { renderedBlockEntities = null; tickableBlockEntities = null; } ms.pushPose(); transformMS(ms, pt); world.pushFakeLight(light); renderBlockEntities(world, ms, buffer, pt); world.popLight(); Map blockBreakingProgressions = world.getBlockBreakingProgressions(); PoseStack overlayMS = null; for (Entry entry : blockBreakingProgressions.entrySet()) { BlockPos pos = entry.getKey(); if (!section.test(pos)) continue; if (overlayMS == null) { overlayMS = new PoseStack(); overlayMS.last().pose().load(ms.last().pose()); overlayMS.last().normal().load(ms.last().normal()); float scaleFactor = world.scene.getScaleFactor(); float f = (float) Math.pow(30 * scaleFactor, -1.2); overlayMS.scale(f, f, f); } VertexConsumer builder = new SheetedDecalTextureGenerator( buffer.getBuffer(ModelBakery.DESTROY_TYPES.get(entry.getValue())), overlayMS.last() .pose(), overlayMS.last() .normal()); ms.pushPose(); ms.translate(pos.getX(), pos.getY(), pos.getZ()); ModelUtil.VANILLA_RENDERER .renderBreakingTexture(world.getBlockState(pos), pos, world, ms, builder, EmptyModelData.INSTANCE); ms.popPose(); } ms.popPose(); } @Override protected void renderLayer(PonderWorld world, MultiBufferSource buffer, RenderType type, PoseStack ms, float fade, float pt) { SuperByteBufferCache bufferCache = CreateClient.BUFFER_CACHE; int code = hashCode() ^ world.hashCode(); Pair key = Pair.of(code, RenderType.chunkBufferLayers() .indexOf(type)); if (redraw) bufferCache.invalidate(DOC_WORLD_SECTION, key); SuperByteBuffer contraptionBuffer = bufferCache.get(DOC_WORLD_SECTION, key, () -> buildStructureBuffer(world, type)); if (contraptionBuffer.isEmpty()) return; transformMS(contraptionBuffer.getTransforms(), pt); int light = lightCoordsFromFade(fade); contraptionBuffer .light(light) .renderInto(ms, buffer.getBuffer(type)); } @Override protected void renderLast(PonderWorld world, MultiBufferSource buffer, PoseStack ms, float fade, float pt) { redraw = false; if (selectedBlock == null) return; BlockState blockState = world.getBlockState(selectedBlock); if (blockState.isAir()) return; VoxelShape shape = blockState.getShape(world, selectedBlock, CollisionContext.of(Minecraft.getInstance().player)); if (shape.isEmpty()) return; ms.pushPose(); transformMS(ms, pt); ms.translate(selectedBlock.getX(), selectedBlock.getY(), selectedBlock.getZ()); AABBOutline aabbOutline = new AABBOutline(shape.bounds()); aabbOutline.getParams() .lineWidth(1 / 64f) .colored(0xefefef) .disableLineNormals(); aabbOutline.render(ms, (SuperRenderTypeBuffer) buffer, Vec3.ZERO, pt); ms.popPose(); } private void renderBlockEntities(PonderWorld world, PoseStack ms, MultiBufferSource buffer, float pt) { loadBEsIfMissing(world); BlockEntityRenderHelper.renderBlockEntities(world, renderedBlockEntities, ms, buffer, pt); } private SuperByteBuffer buildStructureBuffer(PonderWorld world, RenderType layer) { BlockRenderDispatcher dispatcher = ModelUtil.VANILLA_RENDERER; ThreadLocalObjects objects = THREAD_LOCAL_OBJECTS.get(); PoseStack poseStack = objects.poseStack; Random random = objects.random; ShadeSeparatingVertexConsumer shadeSeparatingWrapper = objects.shadeSeparatingWrapper; BufferBuilder shadedBuilder = objects.shadedBuilder; BufferBuilder unshadedBuilder = objects.unshadedBuilder; shadedBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); unshadedBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.BLOCK); shadeSeparatingWrapper.prepare(shadedBuilder, unshadedBuilder); world.setMask(this.section); ForgeHooksClient.setRenderType(layer); ModelBlockRenderer.enableCaching(); section.forEach(pos -> { BlockState state = world.getBlockState(pos); FluidState fluidState = world.getFluidState(pos); poseStack.pushPose(); poseStack.translate(pos.getX(), pos.getY(), pos.getZ()); if (state.getRenderShape() == RenderShape.MODEL && ItemBlockRenderTypes.canRenderInLayer(state, layer)) { BlockEntity blockEntity = world.getBlockEntity(pos); dispatcher.renderBatched(state, pos, world, poseStack, shadeSeparatingWrapper, true, random, blockEntity != null ? blockEntity.getModelData() : EmptyModelData.INSTANCE); } if (!fluidState.isEmpty() && ItemBlockRenderTypes.canRenderInLayer(fluidState, layer)) dispatcher.renderLiquid(pos, world, shadedBuilder, state, fluidState); poseStack.popPose(); }); ModelBlockRenderer.clearCache(); ForgeHooksClient.setRenderType(null); world.clearMask(); shadeSeparatingWrapper.clear(); ShadeSeparatedBufferedData bufferedData = ModelUtil.endAndCombine(shadedBuilder, unshadedBuilder); SuperByteBuffer sbb = new SuperByteBuffer(bufferedData); bufferedData.release(); return sbb; } private static class ThreadLocalObjects { public final PoseStack poseStack = new PoseStack(); public final Random random = new Random(); public final ShadeSeparatingVertexConsumer shadeSeparatingWrapper = new ShadeSeparatingVertexConsumer(); public final BufferBuilder shadedBuilder = new BufferBuilder(512); public final BufferBuilder unshadedBuilder = new BufferBuilder(512); } }