From 2069bfc253bd418e2e380bdbc83f128d5c35934d Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 23 Apr 2026 04:35:25 -0600 Subject: [PATCH 1/2] Implement neoforge outline rendering --- .../LevelRendererMixin.java | 44 +------ .../CameraMixin.java | 8 +- .../block_outline_render/SubLevelCamera.java | 103 ++++++++++++++++ .../LevelRendererMixin.java | 72 +++++++++++ .../main/resources/sable-fabric.mixins.json | 1 + .../LevelRendererMixin.java | 113 ++++++++++++++++++ .../main/resources/sable-neoforge.mixins.json | 1 + 7 files changed, 296 insertions(+), 46 deletions(-) create mode 100644 common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java create mode 100644 fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java create mode 100644 neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/block_decal_render/LevelRendererMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/block_decal_render/LevelRendererMixin.java index 2c063db..43b1333 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/block_decal_render/LevelRendererMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/block_decal_render/LevelRendererMixin.java @@ -2,27 +2,20 @@ import com.llamalad7.mixinextras.sugar.Local; import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.blaze3d.vertex.VertexConsumer; import dev.ryanhcode.sable.Sable; import dev.ryanhcode.sable.companion.math.Pose3dc; import dev.ryanhcode.sable.sublevel.ClientSubLevel; import net.minecraft.client.Camera; import net.minecraft.client.DeltaTracker; -import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.LightTexture; import net.minecraft.core.BlockPos; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; -import net.minecraft.world.phys.shapes.CollisionContext; -import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; import org.joml.Quaternionf; -import org.joml.Vector3d; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -33,51 +26,18 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** - * Changes the distance block damage is rendered from, transforms block damage rendering for sublevels, and renders block hover outlines for sublevels. + * Changes the distance block damage is rendered from, and transforms block damage rendering for sublevels. */ @Mixin(LevelRenderer.class) public abstract class LevelRendererMixin { + // Storage vectors to avoid repeated allocation - private final @Unique Vector3d sable$localTranslationStorage = new Vector3d(); - private final @Unique Vector3d sable$globalTranslationStorage = new Vector3d(); private final @Unique Quaternionf sable$orientationStorage = new Quaternionf(); @Shadow @Nullable private ClientLevel level; - @Shadow - protected static void renderShape(final PoseStack arg, final VertexConsumer arg2, final VoxelShape arg3, final double d, final double e, final double f, final float g, final float h, final float i, final float j) { - } - - @Inject(method = "renderHitOutline", at = @At("HEAD"), cancellable = true) - private void sable$preRenderHitOutline(final PoseStack ps, final VertexConsumer pConsumer, final Entity pEntity, final double pCamX, final double pCamY, final double pCamZ, final BlockPos blockPos, final BlockState blockState, final CallbackInfo ci) { - final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(this.level, blockPos); - - if (subLevel == null) { - return; - } - - ps.pushPose(); - - final Pose3dc pose = subLevel.renderPose(); - - final Vec3 cameraPos = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); - - final Vector3d globalTranslation = pose.position().sub(cameraPos.x, cameraPos.y, cameraPos.z, this.sable$globalTranslationStorage); - final Vector3d localTranslation = this.sable$localTranslationStorage.set(blockPos.getX(), blockPos.getY(), blockPos.getZ()).sub(pose.rotationPoint()); - - // apply transforms - ps.translate(globalTranslation.x, globalTranslation.y, globalTranslation.z); - ps.mulPose(this.sable$orientationStorage.set(pose.orientation())); - ps.translate(localTranslation.x, localTranslation.y, localTranslation.z); - - renderShape(ps, pConsumer, blockState.getShape(this.level, blockPos, CollisionContext.of(pEntity)), 0, 0, 0, 0.0F, 0.0F, 0.0F, 0.4F); - - ps.popPose(); - ci.cancel(); - } - @Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;last()Lcom/mojang/blaze3d/vertex/PoseStack$Pose;", shift = At.Shift.BEFORE)) private void sable$preRenderBlockDamage(final DeltaTracker deltaTracker, final boolean bl, final Camera camera, final GameRenderer gameRenderer, final LightTexture lightTexture, final Matrix4f matrix4f, final Matrix4f matrix4f2, final CallbackInfo ci, @Local(ordinal = 0) final PoseStack ps, @Local(ordinal = 0) final BlockPos pos) { diff --git a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_sublevel_collision/CameraMixin.java b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_sublevel_collision/CameraMixin.java index 86c9cfe..fda99da 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_sublevel_collision/CameraMixin.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixin/entity/entity_sublevel_collision/CameraMixin.java @@ -32,9 +32,9 @@ public class CameraMixin { @WrapOperation(method = "setup", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Camera;setPosition(DDD)V")) private void sable$setPosition(final Camera instance, - final double d, - final double e, - final double f, + final double x, + final double y, + final double z, final Operation original, @Local(argsOnly = true) final Entity entity, @Local(argsOnly = true) final float partialTicks) { @@ -58,7 +58,7 @@ public class CameraMixin { return; } - original.call(instance, d, e, f); + original.call(instance, x, y, z); } diff --git a/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java new file mode 100644 index 0000000..5e48597 --- /dev/null +++ b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java @@ -0,0 +1,103 @@ +package dev.ryanhcode.sable.mixinhelpers.block_outline_render; + +import dev.ryanhcode.sable.companion.math.Pose3dc; +import net.minecraft.client.Camera; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.material.FogType; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.joml.Quaterniond; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +@ApiStatus.Internal +public class SubLevelCamera extends Camera { + + private Camera renderCamera; + private final Quaterniond inverseOrientation = new Quaterniond(); + private final Quaternionf inverseOrientationf = new Quaternionf(); + private final Vector3f rotationYXZ = new Vector3f(); + + private final BlockPos.MutableBlockPos blockPosition = new BlockPos.MutableBlockPos(); + private Vec3 pos = Vec3.ZERO; + + public void transform(final Camera renderCamera, final Pose3dc pose) { + this.renderCamera = renderCamera; + + final Vec3 pos = pose.transformPositionInverse(renderCamera.getPosition()); + + final Quaternionf rotation = this.rotation(); + renderCamera.rotation().mul(this.inverseOrientationf.set(pose.orientation().invert(this.inverseOrientation)), rotation); + + this.blockPosition.set(pos.x, pos.y, pos.z); + this.pos = pos; + + rotation.getEulerAnglesYXZ(this.rotationYXZ); + + this.getLookVector().set(0.0F, 0.0F, -1.0F).rotate(rotation); + this.getUpVector().set(0.0F, 1.0F, 0.0F).rotate(rotation); + this.getLeftVector().set(-1.0F, 0.0F, 0.0F).rotate(rotation); + } + + public void clear() { + this.renderCamera = null; + this.pos = Vec3.ZERO; + } + + @Override + public @NotNull Vec3 getPosition() { + return this.pos; + } + + @Override + public @NotNull BlockPos getBlockPosition() { + return this.blockPosition; + } + + @Override + public float getXRot() { + return (float) (180.0 / Math.PI * -this.rotationYXZ.x); + } + + @Override + public float getYRot() { + return (float) (180.0 / Math.PI * -this.rotationYXZ.y + 180.0); + } + + @Override + public @NotNull Entity getEntity() { + return this.renderCamera.getEntity(); + } + + @Override + public boolean isInitialized() { + return this.renderCamera.isInitialized(); + } + + @Override + public boolean isDetached() { + return this.renderCamera.isDetached(); + } + + @Override + public @NotNull NearPlane getNearPlane() { + return this.renderCamera.getNearPlane(); + } + + @Override + public @NotNull FogType getFluidInCamera() { + return this.renderCamera.getFluidInCamera(); + } + + @Override + public void reset() { + this.renderCamera.reset(); + } + + @Override + public float getPartialTickTime() { + return this.renderCamera.getPartialTickTime(); + } +} diff --git a/fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java b/fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java new file mode 100644 index 0000000..23ccbe0 --- /dev/null +++ b/fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java @@ -0,0 +1,72 @@ +package dev.ryanhcode.sable.fabric.mixin.block_outline_render; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.companion.math.Pose3dc; +import dev.ryanhcode.sable.sublevel.ClientSubLevel; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Transforms block hover outlines for sublevels. + */ +@Mixin(LevelRenderer.class) +public abstract class LevelRendererMixin { + // Storage vectors to avoid repeated allocation + private final @Unique Vector3d sable$localTranslationStorage = new Vector3d(); + private final @Unique Vector3d sable$globalTranslationStorage = new Vector3d(); + private final @Unique Quaternionf sable$orientationStorage = new Quaternionf(); + + @Shadow + @Nullable + private ClientLevel level; + + @Shadow + protected static void renderShape(final PoseStack arg, final VertexConsumer arg2, final VoxelShape arg3, final double d, final double e, final double f, final float g, final float h, final float i, final float j) { + } + + @Inject(method = "renderHitOutline", at = @At("HEAD"), cancellable = true) + private void sable$preRenderHitOutline(final PoseStack ps, final VertexConsumer pConsumer, final Entity pEntity, final double pCamX, final double pCamY, final double pCamZ, final BlockPos blockPos, final BlockState blockState, final CallbackInfo ci) { + final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(this.level, blockPos); + + if (subLevel == null) { + return; + } + + ps.pushPose(); + + final Pose3dc pose = subLevel.renderPose(); + + final Vec3 cameraPos = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); + + final Vector3d globalTranslation = pose.position().sub(cameraPos.x, cameraPos.y, cameraPos.z, this.sable$globalTranslationStorage); + final Vector3d localTranslation = this.sable$localTranslationStorage.set(blockPos.getX(), blockPos.getY(), blockPos.getZ()).sub(pose.rotationPoint()); + + // apply transforms + ps.translate(globalTranslation.x, globalTranslation.y, globalTranslation.z); + ps.mulPose(this.sable$orientationStorage.set(pose.orientation())); + ps.translate(localTranslation.x, localTranslation.y, localTranslation.z); + + renderShape(ps, pConsumer, blockState.getShape(this.level, blockPos, CollisionContext.of(pEntity)), 0, 0, 0, 0.0F, 0.0F, 0.0F, 0.4F); + + ps.popPose(); + ci.cancel(); + } +} diff --git a/fabric/src/main/resources/sable-fabric.mixins.json b/fabric/src/main/resources/sable-fabric.mixins.json index a240d4e..5b12c1b 100644 --- a/fabric/src/main/resources/sable-fabric.mixins.json +++ b/fabric/src/main/resources/sable-fabric.mixins.json @@ -5,6 +5,7 @@ "minVersion": "0.8", "plugin": "dev.ryanhcode.sable.plugin.SableMixinPlugin", "client": [ + "block_outline_render.LevelRendererMixin", "camera_rotation.CameraMixin", "compatibility.sodiumextras.EmbyToolsMixin", "dynamic_directional_shading.SectionCompilerMixin", diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java new file mode 100644 index 0000000..4e6447c --- /dev/null +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java @@ -0,0 +1,113 @@ +package dev.ryanhcode.sable.neoforge.mixin.block_outline_render; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.ryanhcode.sable.Sable; +import dev.ryanhcode.sable.companion.math.Pose3dc; +import dev.ryanhcode.sable.mixinhelpers.block_outline_render.SubLevelCamera; +import dev.ryanhcode.sable.sublevel.ClientSubLevel; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import org.joml.Quaterniondc; +import org.joml.Quaternionf; +import org.joml.Vector3dc; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Transforms block hover outlines for sublevels. + */ +@Mixin(value = LevelRenderer.class, priority = 2000) // This makes sure it applies after normal mixins +public abstract class LevelRendererMixin { + + // Storage vectors to avoid repeated allocation + private final @Unique Quaternionf sable$orientationStorage = new Quaternionf(); + private final @Unique SubLevelCamera sable$sublevelCamera = new SubLevelCamera(); + + @Shadow + @Nullable + private ClientLevel level; + + @WrapOperation(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/neoforged/neoforge/client/ClientHooks;onDrawHighlight(Lnet/minecraft/client/renderer/LevelRenderer;Lnet/minecraft/client/Camera;Lnet/minecraft/world/phys/HitResult;Lnet/minecraft/client/DeltaTracker;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;)Z")) + private boolean sable$preRenderHitOutline(final LevelRenderer context, final Camera camera, final HitResult target, final DeltaTracker deltaTracker, final PoseStack poseStack, final MultiBufferSource bufferSource, final Operation original, @Share("drawn") final LocalBooleanRef drawnRef) { + if (!(target instanceof final BlockHitResult blockTarget)) { + return original.call(context, camera, target, deltaTracker, poseStack, bufferSource); + } + + final BlockPos blockPos = blockTarget.getBlockPos(); + final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(this.level, blockPos); + + if (subLevel == null) { + return original.call(context, camera, target, deltaTracker, poseStack, bufferSource); + } + + poseStack.pushPose(); + + final Pose3dc pose = subLevel.renderPose(); + + this.sable$sublevelCamera.transform(camera, pose); + final Vec3 cameraPosition = this.sable$sublevelCamera.getPosition(); + final Vec3 realCameraPosition = camera.getPosition(); + + final Vector3dc position = pose.position(); + final Vector3dc rotationPoint = pose.rotationPoint(); + final Quaterniondc orientation = pose.orientation(); + final Vector3dc scale = pose.scale(); + + poseStack.translate( + (float) (position.x() - realCameraPosition.x), + (float) (position.y() - realCameraPosition.y), + (float) (position.z() - realCameraPosition.z) + ); + poseStack.mulPose(this.sable$orientationStorage.set(orientation)); + poseStack.translate( + (float) -(rotationPoint.x() - cameraPosition.x), + (float) -(rotationPoint.y() - cameraPosition.y), + (float) -(rotationPoint.z() - cameraPosition.z) + ); + poseStack.scale((float) scale.x(), (float) scale.y(), (float) scale.z()); + + drawnRef.set(true); + return original.call(context, this.sable$sublevelCamera, target, deltaTracker, poseStack, bufferSource); + } + + @Inject(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/debug/DebugRenderer;render(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource$BufferSource;DDD)V")) + public void sable$poseRenderHitOutline(final CallbackInfo ci, @Local final PoseStack poseStack, @Share("drawn") final LocalBooleanRef drawnRef) { + if (drawnRef.get()) { + poseStack.popPose(); + this.sable$sublevelCamera.clear(); + } + } + + @ModifyArg(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;renderHitOutline(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"), index = 3) + public double modifyX(final double original, @Share("drawn") final LocalBooleanRef drawnRef) { + return drawnRef.get() ? this.sable$sublevelCamera.getPosition().x : original; + } + + @ModifyArg(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;renderHitOutline(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"), index = 4) + public double modifyY(final double original, @Share("drawn") final LocalBooleanRef drawnRef) { + return drawnRef.get() ? this.sable$sublevelCamera.getPosition().y : original; + } + + @ModifyArg(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;renderHitOutline(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V"), index = 5) + public double modifyZ(final double original, @Share("drawn") final LocalBooleanRef drawnRef) { + return drawnRef.get() ? this.sable$sublevelCamera.getPosition().z : original; + } +} diff --git a/neoforge/src/main/resources/sable-neoforge.mixins.json b/neoforge/src/main/resources/sable-neoforge.mixins.json index 076304d..8aa40ed 100644 --- a/neoforge/src/main/resources/sable-neoforge.mixins.json +++ b/neoforge/src/main/resources/sable-neoforge.mixins.json @@ -9,6 +9,7 @@ "plugin": "dev.ryanhcode.sable.plugin.SableMixinPlugin", "client": [ "block_entity_visible.LevelRendererMixin", + "block_outline_render.LevelRendererMixin", "camera_rotation.CameraMixin", "compatibility.create.behaviour_compatibility.harvester_block_entity.HarvesterRendererMixin", "compatibility.create.belt.BeltRendererMixin", From b8e3c10e3419e9b196355be99633658adbcb9e5c Mon Sep 17 00:00:00 2001 From: Ocelot Date: Thu, 23 Apr 2026 05:36:46 -0600 Subject: [PATCH 2/2] Implement fabric outline rendering --- .../block_outline_render/SubLevelCamera.java | 40 +++++++--- .../LevelRendererMixin.java | 76 +++++++++++++------ .../LevelRendererMixin.java | 3 +- 3 files changed, 85 insertions(+), 34 deletions(-) diff --git a/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java index 5e48597..c3ebd6b 100644 --- a/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java +++ b/common/src/main/java/dev/ryanhcode/sable/mixinhelpers/block_outline_render/SubLevelCamera.java @@ -8,6 +8,7 @@ import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.joml.Quaterniond; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -23,22 +24,37 @@ public class SubLevelCamera extends Camera { private final BlockPos.MutableBlockPos blockPosition = new BlockPos.MutableBlockPos(); private Vec3 pos = Vec3.ZERO; - public void transform(final Camera renderCamera, final Pose3dc pose) { + public void setCamera(final Camera renderCamera) { this.renderCamera = renderCamera; + } + + public void setPose(@Nullable final Pose3dc pose) { + if (pose != null) { + final Vec3 pos = pose.transformPositionInverse(this.renderCamera.getPosition()); + + final Quaternionf rotation = this.rotation(); + this.renderCamera.rotation().mul(this.inverseOrientationf.set(pose.orientation().invert(this.inverseOrientation)), rotation); - final Vec3 pos = pose.transformPositionInverse(renderCamera.getPosition()); + this.blockPosition.set(pos.x, pos.y, pos.z); + this.pos = pos; - final Quaternionf rotation = this.rotation(); - renderCamera.rotation().mul(this.inverseOrientationf.set(pose.orientation().invert(this.inverseOrientation)), rotation); + rotation.getEulerAnglesYXZ(this.rotationYXZ); - this.blockPosition.set(pos.x, pos.y, pos.z); - this.pos = pos; + this.getLookVector().set(0.0F, 0.0F, -1.0F).rotate(rotation); + this.getUpVector().set(0.0F, 1.0F, 0.0F).rotate(rotation); + this.getLeftVector().set(-1.0F, 0.0F, 0.0F).rotate(rotation); + } else { + this.pos = this.renderCamera.getPosition(); + this.blockPosition.set(this.pos.x, this.pos.y, this.pos.z); + this.rotationYXZ.set(this.renderCamera.getXRot(), this.renderCamera.getYRot(), 0); - rotation.getEulerAnglesYXZ(this.rotationYXZ); + final Quaternionf rotation = this.rotation(); + rotation.set(this.renderCamera.rotation()); - this.getLookVector().set(0.0F, 0.0F, -1.0F).rotate(rotation); - this.getUpVector().set(0.0F, 1.0F, 0.0F).rotate(rotation); - this.getLeftVector().set(-1.0F, 0.0F, 0.0F).rotate(rotation); + this.getLookVector().set(0.0F, 0.0F, -1.0F).rotate(rotation); + this.getUpVector().set(0.0F, 1.0F, 0.0F).rotate(rotation); + this.getLeftVector().set(-1.0F, 0.0F, 0.0F).rotate(rotation); + } } public void clear() { @@ -100,4 +116,8 @@ public void reset() { public float getPartialTickTime() { return this.renderCamera.getPartialTickTime(); } + + public Camera getRenderCamera() { + return this.renderCamera; + } } diff --git a/fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java b/fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java index 23ccbe0..9ac8dee 100644 --- a/fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java +++ b/fabric/src/main/java/dev/ryanhcode/sable/fabric/mixin/block_outline_render/LevelRendererMixin.java @@ -1,22 +1,27 @@ package dev.ryanhcode.sable.fabric.mixin.block_outline_render; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; import dev.ryanhcode.sable.Sable; import dev.ryanhcode.sable.companion.math.Pose3dc; +import dev.ryanhcode.sable.mixinhelpers.block_outline_render.SubLevelCamera; import dev.ryanhcode.sable.sublevel.ClientSubLevel; -import net.minecraft.client.Minecraft; +import net.minecraft.client.Camera; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.core.BlockPos; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; -import net.minecraft.world.phys.shapes.CollisionContext; -import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; +import org.joml.Quaterniondc; import org.joml.Quaternionf; -import org.joml.Vector3d; +import org.joml.Vector3dc; +import org.spongepowered.asm.mixin.Debug; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -27,46 +32,71 @@ /** * Transforms block hover outlines for sublevels. */ -@Mixin(LevelRenderer.class) +@Debug(export = true) +@Mixin(value = LevelRenderer.class, priority = 400) +// Make sure this applies first so the camera can be modified public abstract class LevelRendererMixin { + // Storage vectors to avoid repeated allocation - private final @Unique Vector3d sable$localTranslationStorage = new Vector3d(); - private final @Unique Vector3d sable$globalTranslationStorage = new Vector3d(); private final @Unique Quaternionf sable$orientationStorage = new Quaternionf(); + private final @Unique SubLevelCamera sable$sublevelCamera = new SubLevelCamera(); @Shadow @Nullable private ClientLevel level; - @Shadow - protected static void renderShape(final PoseStack arg, final VertexConsumer arg2, final VoxelShape arg3, final double d, final double e, final double f, final float g, final float h, final float i, final float j) { + @Inject(method = "renderLevel", at = @At("HEAD")) + public void modifyCamera(final CallbackInfo ci, @Local(argsOnly = true) final LocalRef cameraRef) { + this.sable$sublevelCamera.setCamera(cameraRef.get()); + this.sable$sublevelCamera.setPose(null); + cameraRef.set(this.sable$sublevelCamera); } - @Inject(method = "renderHitOutline", at = @At("HEAD"), cancellable = true) - private void sable$preRenderHitOutline(final PoseStack ps, final VertexConsumer pConsumer, final Entity pEntity, final double pCamX, final double pCamY, final double pCamZ, final BlockPos blockPos, final BlockState blockState, final CallbackInfo ci) { - final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(this.level, blockPos); + @Inject(method = "renderLevel", at = @At("TAIL")) + public void clearCamera(final CallbackInfo ci, @Local(argsOnly = true) final LocalRef cameraRef) { + // This is important to make sure events fired after this mixin still have access to the camera + cameraRef.set(this.sable$sublevelCamera.getRenderCamera()); + this.sable$sublevelCamera.clear(); + } + + @WrapOperation(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/LevelRenderer;renderHitOutline(Lcom/mojang/blaze3d/vertex/PoseStack;Lcom/mojang/blaze3d/vertex/VertexConsumer;Lnet/minecraft/world/entity/Entity;DDDLnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V")) + private void sable$preRenderHitOutline(final LevelRenderer instance, final PoseStack poseStack, final VertexConsumer consumer, final Entity entity, final double camX, final double camY, final double camZ, final BlockPos pos, final BlockState state, final Operation original, @Local(argsOnly = true) final Camera camera) { + final ClientSubLevel subLevel = (ClientSubLevel) Sable.HELPER.getContaining(this.level, pos); if (subLevel == null) { + original.call(instance, poseStack, consumer, entity, camX, camY, camZ, pos, state); return; } - ps.pushPose(); + poseStack.pushPose(); final Pose3dc pose = subLevel.renderPose(); - final Vec3 cameraPos = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition(); + this.sable$sublevelCamera.setPose(pose); + final Vec3 cameraPosition = this.sable$sublevelCamera.getPosition(); + + final Vector3dc position = pose.position(); + final Vector3dc rotationPoint = pose.rotationPoint(); + final Quaterniondc orientation = pose.orientation(); + final Vector3dc scale = pose.scale(); - final Vector3d globalTranslation = pose.position().sub(cameraPos.x, cameraPos.y, cameraPos.z, this.sable$globalTranslationStorage); - final Vector3d localTranslation = this.sable$localTranslationStorage.set(blockPos.getX(), blockPos.getY(), blockPos.getZ()).sub(pose.rotationPoint()); + poseStack.translate( + (float) (position.x() - camX), + (float) (position.y() - camY), + (float) (position.z() - camZ) + ); + poseStack.mulPose(this.sable$orientationStorage.set(orientation)); + poseStack.translate( + (float) -(rotationPoint.x() - cameraPosition.x), + (float) -(rotationPoint.y() - cameraPosition.y), + (float) -(rotationPoint.z() - cameraPosition.z) + ); + poseStack.scale((float) scale.x(), (float) scale.y(), (float) scale.z()); - // apply transforms - ps.translate(globalTranslation.x, globalTranslation.y, globalTranslation.z); - ps.mulPose(this.sable$orientationStorage.set(pose.orientation())); - ps.translate(localTranslation.x, localTranslation.y, localTranslation.z); + original.call(instance, poseStack, consumer, entity, cameraPosition.x, cameraPosition.y, cameraPosition.z, pos, state); - renderShape(ps, pConsumer, blockState.getShape(this.level, blockPos, CollisionContext.of(pEntity)), 0, 0, 0, 0.0F, 0.0F, 0.0F, 0.4F); + poseStack.popPose(); - ps.popPose(); - ci.cancel(); + this.sable$sublevelCamera.setPose(null); } } diff --git a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java index 4e6447c..c98dcd2 100644 --- a/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java +++ b/neoforge/src/main/java/dev/ryanhcode/sable/neoforge/mixin/block_outline_render/LevelRendererMixin.java @@ -62,7 +62,8 @@ public abstract class LevelRendererMixin { final Pose3dc pose = subLevel.renderPose(); - this.sable$sublevelCamera.transform(camera, pose); + this.sable$sublevelCamera.setCamera(camera); + this.sable$sublevelCamera.setPose(pose); final Vec3 cameraPosition = this.sable$sublevelCamera.getPosition(); final Vec3 realCameraPosition = camera.getPosition();