package com.xfestudio.mydimension.client.builder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexBuffer;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
/** Renders cached projection models, rift veils, and shader-safe solid outlines. */
public final class BuilderPreviewRenderer {
private static final int MINIMUM_PREVIEW_DISTANCE = 160;
private static final int MAXIMUM_PREVIEW_DISTANCE = 512;
private static final int FULL_MODEL_DISTANCE = 128;
private static final int EXTRA_RENDER_SECTIONS = 4;
private static final int SECTION_UPLOADS_PER_FRAME = 3;
private static final long SECTION_UPLOAD_BUDGET_NANOS = 2_500_000L;
private static final int CELLS_PREPARED_PER_TICK = 2048;
private static final int CELLS_PREPARED_PER_FRAME = 768;
private static final double NORMAL_OUTLINE_WIDTH = 0.012D;
private static final double FOCUSED_OUTLINE_WIDTH = 0.048D;
private static final BuilderPreviewSectionMeshCache SECTION_CACHE =
new BuilderPreviewSectionMeshCache();
private static final BuilderFocusedOutlineCache FOCUSED_OUTLINE_CACHE =
new BuilderFocusedOutlineCache();
private static final FocusOutlineAnimation DYNAMIC_FOCUS_ANIMATION =
new FocusOutlineAnimation();
private BuilderPreviewRenderer() {
}
public static void tick(Minecraft minecraft) {
BuilderPreviewState.Snapshot snapshot = BuilderPreviewState.get().snapshot();
if (!BuilderClientServices.isHoldingRealmwright(minecraft)
|| snapshot == null || minecraft.level == null
|| !minecraft.level.dimension().equals(snapshot.dimension())) {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
return;
}
SECTION_CACHE.advance(snapshot, CELLS_PREPARED_PER_TICK);
}
public static void clearCache() {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
}
public static void render(RenderLevelStageEvent event) {
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) return;
RenderSystem.assertOnRenderThread();
Minecraft minecraft = Minecraft.getInstance();
if (!BuilderClientServices.isHoldingRealmwright(minecraft) || minecraft.level == null) {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
return;
}
BuilderClientEvents.updateControlTargetForRender(minecraft);
BuilderPreviewState previewState = BuilderPreviewState.get();
BuilderPreviewState.Snapshot snapshot = previewState.snapshot();
if (snapshot != null && !minecraft.level.dimension().equals(snapshot.dimension())) {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
snapshot = null;
}
BuilderPreviewVisibility visibility = BuilderPreviewVisibility.forModifiers(
Screen.hasControlDown(), Screen.hasAltDown(),
snapshot != null && snapshot.blueprintPreview());
previewState.updateHoveredTarget(minecraft, event.getPartialTick(),
Math.max(1, BuilderClientServices.snapshot().reach()), visibility);
BuilderPreviewState.Candidate candidate = previewState.controlCandidate();
if (candidate != null && !minecraft.level.dimension().equals(candidate.dimension())) {
candidate = null;
}
if (snapshot == null && candidate == null
&& (!visibility.showsAnchors()
|| BuilderAnchorPreviewTracker.positions(minecraft.level).isEmpty())) return;
SECTION_CACHE.advance(snapshot, CELLS_PREPARED_PER_FRAME);
Vec3 camera = event.getCamera().getPosition();
int previewDistance = previewDistance(minecraft);
double previewDistanceSqr = (double) previewDistance * previewDistance;
long uploadDeadline = System.nanoTime() + SECTION_UPLOAD_BUDGET_NANOS;
int stagedUploads = SECTION_CACHE.preparePending(minecraft, camera,
(double) FULL_MODEL_DISTANCE * FULL_MODEL_DISTANCE,
SECTION_UPLOADS_PER_FRAME, uploadDeadline);
BuilderPreviewState.Snapshot renderedSnapshot = SECTION_CACHE.renderSnapshot(snapshot);
BuilderPreviewSectionMeshCache.ModelResidency modelResidency =
SECTION_CACHE.modelResidency(camera,
(double) FULL_MODEL_DISTANCE * FULL_MODEL_DISTANCE);
boolean showCachedCells = renderedSnapshot != null
&& visibility.showsCachedCells(renderedSnapshot.blueprintPreview());
List<BuilderPreviewSectionMeshCache.SectionMesh> visibleSections = !showCachedCells
? List.of() : SECTION_CACHE.visibleSections(
event.getFrustum(), camera, previewDistanceSqr);
SECTION_CACHE.prepareVisibleSections(minecraft, modelResidency, camera,
Math.max(0, SECTION_UPLOADS_PER_FRAME - stagedUploads), uploadDeadline);
Set<BuilderPreviewSectionMeshCache.SectionKey> drawableGhostSections =
modelResidency.drawableGhostSections();
BuilderPreviewState.Focus focus = BuilderPreviewState.get().focus();
if (focus != null && !visibility.showsFocus(focus.kind())) {
focus = null;
}
if (focus != null && visibility.showsCachedCells(snapshot != null
&& snapshot.blueprintPreview())
&& focus.kind() != BuilderPreviewState.FocusKind.CANDIDATE
&& !SECTION_CACHE.represents(snapshot)) {
// The hit-test indexes already point at the newest immutable state, whereas section
// VBOs deliberately keep rendering the previous complete generation during upload.
// Do not draw a thick outline for an object that is not visible in that generation.
BuilderPreviewState.get().clearHover();
focus = null;
}
BuilderPreviewState.MissingGroup focusedMissingGroup = focus != null
&& focus.kind() == BuilderPreviewState.FocusKind.MISSING
? focus.missingGroup() : null;
long animationTime = System.nanoTime();
if (visibility == BuilderPreviewVisibility.NORMAL) {
FOCUSED_OUTLINE_CACHE.synchronize(focusedMissingGroup, animationTime);
}
DYNAMIC_FOCUS_ANIMATION.update(focus != null
&& focus.kind() != BuilderPreviewState.FocusKind.MISSING ? focus : null,
animationTime);
if (visibility == BuilderPreviewVisibility.NORMAL) {
FOCUSED_OUTLINE_CACHE.prepare(event.getFrustum(), camera, previewDistanceSqr);
}
PoseStack poseStack = event.getPoseStack();
poseStack.pushPose();
poseStack.translate(-camera.x, -camera.y, -camera.z);
try {
if (showCachedCells) {
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.ghostModel(),
section -> section.ghostBuffers(drawableGhostSections),
true, 1.0F);
drawSpecialGhostSections(poseStack, event, visibleSections,
drawableGhostSections);
BuilderPreviewRenderTypes.updateWaveUniforms();
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.projectionWave(),
BuilderPreviewSectionMeshCache.SectionMesh::waveBuffers, true, 1.0F);
// Draw the faint pass only where world geometry occludes the frame. Keeping it
// off visible pixels avoids two coplanar outline passes under temporal shaders.
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.outlineXray(),
section -> section.outlineBuffer() == null
? List.of() : List.of(section.outlineBuffer()), false, 0.16F);
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.outline(),
section -> section.outlineBuffer() == null
? List.of() : List.of(section.outlineBuffer()), false, 1.0F);
}
BuilderPreviewState.Snapshot dynamicSnapshot =
visibility == BuilderPreviewVisibility.BLUEPRINT_SELECTION_ONLY
? snapshot : renderedSnapshot;
renderDynamicOutlines(minecraft, poseStack, dynamicSnapshot, candidate,
camera, previewDistanceSqr, BuilderPreviewRenderTypes.outlineXray(), 0.16F,
visibility);
renderDynamicOutlines(minecraft, poseStack, dynamicSnapshot, candidate,
camera, previewDistanceSqr, BuilderPreviewRenderTypes.outline(), 1.0F,
visibility);
if (visibility == BuilderPreviewVisibility.NORMAL) {
FOCUSED_OUTLINE_CACHE.draw(poseStack, event.getProjectionMatrix(),
BuilderPreviewRenderTypes.outlineXray(), 0.16F);
FOCUSED_OUTLINE_CACHE.draw(poseStack, event.getProjectionMatrix(),
BuilderPreviewRenderTypes.outline(), 1.0F);
}
} finally {
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
VertexBuffer.unbind();
poseStack.popPose();
}
}
/**
* Draws captured block-entity projection geometry with the exact RenderType requested by its
* renderer. This preserves non-block atlases such as the chest sheet while the explicit blend
* state keeps the projection tint translucent even for entity-cutout render types.
*/
private static void drawSpecialGhostSections(
PoseStack poseStack,
RenderLevelStageEvent event,
List<BuilderPreviewSectionMeshCache.SectionMesh> sections,
Set<BuilderPreviewSectionMeshCache.SectionKey> drawableSections) {
if (sections.isEmpty()) return;
try {
for (int index = sections.size() - 1; index >= 0; index--) {
BuilderPreviewSectionMeshCache.SectionMesh section = sections.get(index);
List<BuilderPreviewSectionMeshCache.SpecialGhostBuffer> buffers =
section.specialGhostBuffers(drawableSections);
if (buffers.isEmpty()) continue;
poseStack.pushPose();
poseStack.translate(section.originX(), section.originY(), section.originZ());
try {
for (BuilderPreviewSectionMeshCache.SpecialGhostBuffer special : buffers) {
VertexBuffer buffer = special.buffer();
if (buffer == null || buffer.isInvalid()) continue;
RenderType renderType = special.renderType();
renderType.setupRenderState();
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
try {
ShaderInstance shader = RenderSystem.getShader();
if (shader == null) continue;
buffer.bind();
buffer.drawWithShader(poseStack.last().pose(),
event.getProjectionMatrix(), shader);
} finally {
VertexBuffer.unbind();
renderType.clearRenderState();
}
}
} finally {
poseStack.popPose();
}
}
} finally {
VertexBuffer.unbind();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
RenderSystem.disableBlend();
RenderSystem.defaultBlendFunc();
}
}
private static void drawCachedSections(PoseStack poseStack, RenderLevelStageEvent event,
List<BuilderPreviewSectionMeshCache.SectionMesh> sections,
RenderType renderType,
Function<BuilderPreviewSectionMeshCache.SectionMesh,
List<VertexBuffer>> bufferGetter,
boolean farToNear, float alpha) {
if (sections.isEmpty()) return;
renderType.setupRenderState();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, alpha);
try {
ShaderInstance shader = RenderSystem.getShader();
if (shader == null) return;
int start = farToNear ? sections.size() - 1 : 0;
int end = farToNear ? -1 : sections.size();
int step = farToNear ? -1 : 1;
for (int index = start; index != end; index += step) {
BuilderPreviewSectionMeshCache.SectionMesh section = sections.get(index);
poseStack.pushPose();
poseStack.translate(section.originX(), section.originY(), section.originZ());
for (VertexBuffer buffer : bufferGetter.apply(section)) {
if (buffer == null || buffer.isInvalid()) continue;
buffer.bind();
buffer.drawWithShader(poseStack.last().pose(), event.getProjectionMatrix(), shader);
}
poseStack.popPose();
}
} finally {
VertexBuffer.unbind();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
renderType.clearRenderState();
}
}
private static void renderDynamicOutlines(Minecraft minecraft, PoseStack poseStack,
@Nullable BuilderPreviewState.Snapshot snapshot,
@Nullable BuilderPreviewState.Candidate candidate,
Vec3 camera, double maximumDistanceSqr,
RenderType renderType, float alphaScale,
BuilderPreviewVisibility visibility) {
MultiBufferSource.BufferSource buffers = minecraft.renderBuffers().bufferSource();
VertexConsumer quads = buffers.getBuffer(renderType);
PoseStack.Pose pose = poseStack.last();
if (visibility.showsAnchors()) {
for (BlockPos anchor : BuilderAnchorPreviewTracker.positions(minecraft.level)) {
AABB bounds = new AABB(anchor).inflate(0.008D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.ANCHOR, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
}
BuilderPreviewState.Selection selection = snapshot == null
|| !visibility.showsSelection(snapshot.blueprintPreview())
? null : snapshot.selection();
if (selection != null && selection.active()) {
if (selection.first() != null) {
AABB bounds = new AABB(selection.first()).inflate(0.006D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.BUILD, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
if (selection.second() != null) {
AABB bounds = new AABB(selection.second()).inflate(0.006D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.DEMOLISH, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
AABB bounds = selection.bounds();
if (bounds != null && withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds.inflate(0.004D),
BuilderPreviewState.Kind.BLUEPRINT, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
// Follow the same fully published generation as the cached cell VBOs. Reading the newest
// state here would move the outer frame before its replacement geometry is ready.
AABB blueprintBounds = visibility.showsDeploymentFrame()
? blueprintBounds(snapshot) : null;
if (blueprintBounds != null && withinDistance(blueprintBounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, blueprintBounds.inflate(0.004D),
BuilderPreviewState.Kind.BLUEPRINT, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
if (candidate != null) {
AABB bounds = new AABB(candidate.pos()).inflate(0.006D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.BLUEPRINT, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
BuilderPreviewState.Focus focus = DYNAMIC_FOCUS_ANIMATION.renderedFocus();
float focusAmount = DYNAMIC_FOCUS_ANIMATION.smoothAmount();
if (focus != null && visibility.showsFocus(focus.kind())
&& focusAmount > 0.001F
&& withinDistance(focus.bounds(), camera, maximumDistanceSqr)) {
BuilderPreviewState.Kind color = focus.kind() == BuilderPreviewState.FocusKind.MISSING
? BuilderPreviewState.Kind.MISSING : BuilderPreviewState.Kind.BLUEPRINT;
double animatedWidth = Mth.lerp(focusAmount,
NORMAL_OUTLINE_WIDTH, FOCUSED_OUTLINE_WIDTH);
BuilderPreviewGeometry.emitBox(pose, quads, focus.bounds(),
color, animatedWidth, alphaScale * focusAmount, false);
}
buffers.endBatch(renderType);
}
private static boolean withinDistance(AABB bounds, Vec3 point, double maximumDistanceSqr) {
double x = Mth.clamp(point.x, bounds.minX, bounds.maxX) - point.x;
double y = Mth.clamp(point.y, bounds.minY, bounds.maxY) - point.y;
double z = Mth.clamp(point.z, bounds.minZ, bounds.maxZ) - point.z;
return x * x + y * y + z * z <= maximumDistanceSqr;
}
@Nullable
private static AABB blueprintBounds(@Nullable BuilderPreviewState.Snapshot snapshot) {
if (snapshot == null || !snapshot.blueprintPreview() || snapshot.cells().isEmpty()) return null;
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (BuilderPreviewState.Cell cell : snapshot.cells()) {
// Retained missing work never belongs to the deployment focus/frame volume.
if (cell.kind() == BuilderPreviewState.Kind.MISSING) continue;
minX = Math.min(minX, cell.pos().getX());
minY = Math.min(minY, cell.pos().getY());
minZ = Math.min(minZ, cell.pos().getZ());
maxX = Math.max(maxX, cell.pos().getX());
maxY = Math.max(maxY, cell.pos().getY());
maxZ = Math.max(maxZ, cell.pos().getZ());
}
return minX == Integer.MAX_VALUE ? null
: new AABB(minX, minY, minZ, maxX + 1.0D, maxY + 1.0D, maxZ + 1.0D);
}
private static int previewDistance(Minecraft minecraft) {
int renderSections = minecraft.options.getEffectiveRenderDistance() + EXTRA_RENDER_SECTIONS;
return Mth.clamp(renderSections * 16,
MINIMUM_PREVIEW_DISTANCE, MAXIMUM_PREVIEW_DISTANCE);
}
static double focusedOutlineWidth() {
return FOCUSED_OUTLINE_WIDTH;
}
/** Smoothly interpolates the handful of dynamic selection/deployment focus boxes. */
private static final class FocusOutlineAnimation {
private static final float DURATION_SECONDS = 0.16F;
@Nullable private BuilderPreviewState.Focus renderedFocus;
private boolean targetFocused;
private float amount;
private long lastNanos;
private void update(@Nullable BuilderPreviewState.Focus target, long nowNanos) {
if (target != null && !sameTarget(renderedFocus, target)) {
renderedFocus = target;
amount = 0.0F;
}
targetFocused = target != null;
if (lastNanos == 0L) {
lastNanos = nowNanos;
return;
}
float elapsed = Math.min(0.05F,
Math.max(0.0F, (nowNanos - lastNanos) / 1_000_000_000.0F));
lastNanos = nowNanos;
float step = elapsed / DURATION_SECONDS;
amount = targetFocused
? Math.min(1.0F, amount + step)
: Math.max(0.0F, amount - step);
if (!targetFocused && amount <= 0.0F) renderedFocus = null;
}
private void clear() {
renderedFocus = null;
targetFocused = false;
amount = 0.0F;
lastNanos = 0L;
}
@Nullable
private BuilderPreviewState.Focus renderedFocus() {
return renderedFocus;
}
private float smoothAmount() {
return amount * amount * (3.0F - 2.0F * amount);
}
private static boolean sameTarget(@Nullable BuilderPreviewState.Focus first,
BuilderPreviewState.Focus second) {
return first != null && first.kind() == second.kind()
&& first.bounds().equals(second.bounds());
}
}
}
package com.xfestudio.mydimension.client.builder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexBuffer;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.client.event.RenderLevelStageEvent;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
/** Renders cached projection models, rift veils, and shader-safe solid outlines. */
public final class BuilderPreviewRenderer {
private static final int MINIMUM_PREVIEW_DISTANCE = 160;
private static final int MAXIMUM_PREVIEW_DISTANCE = 512;
private static final int FULL_MODEL_DISTANCE = 128;
private static final int EXTRA_RENDER_SECTIONS = 4;
private static final int SECTION_UPLOADS_PER_FRAME = 3;
private static final long SECTION_UPLOAD_BUDGET_NANOS = 2_500_000L;
private static final int CELLS_PREPARED_PER_TICK = 2048;
private static final int CELLS_PREPARED_PER_FRAME = 768;
private static final double NORMAL_OUTLINE_WIDTH = 0.012D;
private static final double FOCUSED_OUTLINE_WIDTH = 0.048D;
private static final BuilderPreviewSectionMeshCache SECTION_CACHE =
new BuilderPreviewSectionMeshCache();
private static final BuilderFocusedOutlineCache FOCUSED_OUTLINE_CACHE =
new BuilderFocusedOutlineCache();
private static final FocusOutlineAnimation DYNAMIC_FOCUS_ANIMATION =
new FocusOutlineAnimation();
private BuilderPreviewRenderer() {
}
public static void tick(Minecraft minecraft) {
BuilderPreviewState.Snapshot snapshot = BuilderPreviewState.get().snapshot();
if (!BuilderClientServices.isHoldingRealmwright(minecraft)
|| snapshot == null || minecraft.level == null
|| !minecraft.level.dimension().equals(snapshot.dimension())) {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
return;
}
SECTION_CACHE.advance(snapshot, CELLS_PREPARED_PER_TICK);
}
public static void clearCache() {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
}
public static void render(RenderLevelStageEvent event) {
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_PARTICLES) return;
RenderSystem.assertOnRenderThread();
Minecraft minecraft = Minecraft.getInstance();
if (!BuilderClientServices.isHoldingRealmwright(minecraft) || minecraft.level == null) {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
return;
}
BuilderClientEvents.updateControlTargetForRender(minecraft);
BuilderPreviewState previewState = BuilderPreviewState.get();
BuilderPreviewState.Snapshot snapshot = previewState.snapshot();
if (snapshot != null && !minecraft.level.dimension().equals(snapshot.dimension())) {
SECTION_CACHE.clear();
FOCUSED_OUTLINE_CACHE.clear();
DYNAMIC_FOCUS_ANIMATION.clear();
snapshot = null;
}
BuilderPreviewVisibility visibility = BuilderPreviewVisibility.forModifiers(
Screen.hasControlDown(), Screen.hasAltDown(),
snapshot != null && snapshot.blueprintPreview());
previewState.updateHoveredTarget(minecraft, event.getPartialTick(),
Math.max(1, BuilderClientServices.snapshot().reach()), visibility);
BuilderPreviewState.Candidate candidate = previewState.controlCandidate();
if (candidate != null && !minecraft.level.dimension().equals(candidate.dimension())) {
candidate = null;
}
if (snapshot == null && candidate == null
&& (!visibility.showsAnchors()
|| BuilderAnchorPreviewTracker.positions(minecraft.level).isEmpty())) return;
SECTION_CACHE.advance(snapshot, CELLS_PREPARED_PER_FRAME);
Vec3 camera = event.getCamera().getPosition();
int previewDistance = previewDistance(minecraft);
double previewDistanceSqr = (double) previewDistance * previewDistance;
long uploadDeadline = System.nanoTime() + SECTION_UPLOAD_BUDGET_NANOS;
int stagedUploads = SECTION_CACHE.preparePending(minecraft, camera,
(double) FULL_MODEL_DISTANCE * FULL_MODEL_DISTANCE,
SECTION_UPLOADS_PER_FRAME, uploadDeadline);
BuilderPreviewState.Snapshot renderedSnapshot = SECTION_CACHE.renderSnapshot(snapshot);
BuilderPreviewSectionMeshCache.ModelResidency modelResidency =
SECTION_CACHE.modelResidency(camera,
(double) FULL_MODEL_DISTANCE * FULL_MODEL_DISTANCE);
boolean showCachedCells = renderedSnapshot != null
&& visibility.showsCachedCells(renderedSnapshot.blueprintPreview());
List<BuilderPreviewSectionMeshCache.SectionMesh> visibleSections = !showCachedCells
? List.of() : SECTION_CACHE.visibleSections(
event.getFrustum(), camera, previewDistanceSqr);
SECTION_CACHE.prepareVisibleSections(minecraft, modelResidency, camera,
Math.max(0, SECTION_UPLOADS_PER_FRAME - stagedUploads), uploadDeadline);
Set<BuilderPreviewSectionMeshCache.SectionKey> drawableGhostSections =
modelResidency.drawableGhostSections();
BuilderPreviewState.Focus focus = BuilderPreviewState.get().focus();
if (focus != null && !visibility.showsFocus(focus.kind())) {
focus = null;
}
if (focus != null && visibility.showsCachedCells(snapshot != null
&& snapshot.blueprintPreview())
&& focus.kind() != BuilderPreviewState.FocusKind.CANDIDATE
&& !SECTION_CACHE.represents(snapshot)) {
// The hit-test indexes already point at the newest immutable state, whereas section
// VBOs deliberately keep rendering the previous complete generation during upload.
// Do not draw a thick outline for an object that is not visible in that generation.
BuilderPreviewState.get().clearHover();
focus = null;
}
BuilderPreviewState.MissingGroup focusedMissingGroup = focus != null
&& focus.kind() == BuilderPreviewState.FocusKind.MISSING
? focus.missingGroup() : null;
long animationTime = System.nanoTime();
if (visibility == BuilderPreviewVisibility.NORMAL) {
FOCUSED_OUTLINE_CACHE.synchronize(focusedMissingGroup, animationTime);
}
DYNAMIC_FOCUS_ANIMATION.update(focus != null
&& focus.kind() != BuilderPreviewState.FocusKind.MISSING ? focus : null,
animationTime);
if (visibility == BuilderPreviewVisibility.NORMAL) {
FOCUSED_OUTLINE_CACHE.prepare(event.getFrustum(), camera, previewDistanceSqr);
}
PoseStack poseStack = event.getPoseStack();
poseStack.pushPose();
poseStack.translate(-camera.x, -camera.y, -camera.z);
try {
if (showCachedCells) {
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.ghostModel(),
section -> section.ghostBuffers(drawableGhostSections),
true, 1.0F);
drawSpecialGhostSections(poseStack, event, visibleSections,
drawableGhostSections);
BuilderPreviewRenderTypes.updateWaveUniforms();
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.projectionWave(),
BuilderPreviewSectionMeshCache.SectionMesh::waveBuffers, true, 1.0F);
// Draw the faint pass only where world geometry occludes the frame. Keeping it
// off visible pixels avoids two coplanar outline passes under temporal shaders.
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.outlineXray(),
section -> section.outlineBuffer() == null
? List.of() : List.of(section.outlineBuffer()), false, 0.16F);
drawCachedSections(poseStack, event, visibleSections,
BuilderPreviewRenderTypes.outline(),
section -> section.outlineBuffer() == null
? List.of() : List.of(section.outlineBuffer()), false, 1.0F);
}
BuilderPreviewState.Snapshot dynamicSnapshot =
visibility == BuilderPreviewVisibility.BLUEPRINT_SELECTION_ONLY
? snapshot : renderedSnapshot;
renderDynamicOutlines(minecraft, poseStack, dynamicSnapshot, candidate,
camera, previewDistanceSqr, BuilderPreviewRenderTypes.outlineXray(), 0.16F,
visibility);
renderDynamicOutlines(minecraft, poseStack, dynamicSnapshot, candidate,
camera, previewDistanceSqr, BuilderPreviewRenderTypes.outline(), 1.0F,
visibility);
if (visibility == BuilderPreviewVisibility.NORMAL) {
FOCUSED_OUTLINE_CACHE.draw(poseStack, event.getProjectionMatrix(),
BuilderPreviewRenderTypes.outlineXray(), 0.16F);
FOCUSED_OUTLINE_CACHE.draw(poseStack, event.getProjectionMatrix(),
BuilderPreviewRenderTypes.outline(), 1.0F);
}
} finally {
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
VertexBuffer.unbind();
poseStack.popPose();
}
}
/**
* Draws captured block-entity projection geometry with the exact RenderType requested by its
* renderer. This preserves non-block atlases such as the chest sheet while the explicit blend
* state keeps the projection tint translucent even for entity-cutout render types.
*/
private static void drawSpecialGhostSections(
PoseStack poseStack,
RenderLevelStageEvent event,
List<BuilderPreviewSectionMeshCache.SectionMesh> sections,
Set<BuilderPreviewSectionMeshCache.SectionKey> drawableSections) {
if (sections.isEmpty()) return;
try {
for (int index = sections.size() - 1; index >= 0; index--) {
BuilderPreviewSectionMeshCache.SectionMesh section = sections.get(index);
List<BuilderPreviewSectionMeshCache.SpecialGhostBuffer> buffers =
section.specialGhostBuffers(drawableSections);
if (buffers.isEmpty()) continue;
poseStack.pushPose();
poseStack.translate(section.originX(), section.originY(), section.originZ());
try {
for (BuilderPreviewSectionMeshCache.SpecialGhostBuffer special : buffers) {
VertexBuffer buffer = special.buffer();
if (buffer == null || buffer.isInvalid()) continue;
RenderType renderType = special.renderType();
renderType.setupRenderState();
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
try {
ShaderInstance shader = RenderSystem.getShader();
if (shader == null) continue;
buffer.bind();
buffer.drawWithShader(poseStack.last().pose(),
event.getProjectionMatrix(), shader);
} finally {
VertexBuffer.unbind();
renderType.clearRenderState();
}
}
} finally {
poseStack.popPose();
}
}
} finally {
VertexBuffer.unbind();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
RenderSystem.disableBlend();
RenderSystem.defaultBlendFunc();
}
}
private static void drawCachedSections(PoseStack poseStack, RenderLevelStageEvent event,
List<BuilderPreviewSectionMeshCache.SectionMesh> sections,
RenderType renderType,
Function<BuilderPreviewSectionMeshCache.SectionMesh,
List<VertexBuffer>> bufferGetter,
boolean farToNear, float alpha) {
if (sections.isEmpty()) return;
renderType.setupRenderState();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, alpha);
try {
ShaderInstance shader = RenderSystem.getShader();
if (shader == null) return;
int start = farToNear ? sections.size() - 1 : 0;
int end = farToNear ? -1 : sections.size();
int step = farToNear ? -1 : 1;
for (int index = start; index != end; index += step) {
BuilderPreviewSectionMeshCache.SectionMesh section = sections.get(index);
poseStack.pushPose();
poseStack.translate(section.originX(), section.originY(), section.originZ());
for (VertexBuffer buffer : bufferGetter.apply(section)) {
if (buffer == null || buffer.isInvalid()) continue;
buffer.bind();
buffer.drawWithShader(poseStack.last().pose(), event.getProjectionMatrix(), shader);
}
poseStack.popPose();
}
} finally {
VertexBuffer.unbind();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
renderType.clearRenderState();
}
}
private static void renderDynamicOutlines(Minecraft minecraft, PoseStack poseStack,
@Nullable BuilderPreviewState.Snapshot snapshot,
@Nullable BuilderPreviewState.Candidate candidate,
Vec3 camera, double maximumDistanceSqr,
RenderType renderType, float alphaScale,
BuilderPreviewVisibility visibility) {
MultiBufferSource.BufferSource buffers = minecraft.renderBuffers().bufferSource();
VertexConsumer quads = buffers.getBuffer(renderType);
PoseStack.Pose pose = poseStack.last();
if (visibility.showsAnchors()) {
for (BlockPos anchor : BuilderAnchorPreviewTracker.positions(minecraft.level)) {
AABB bounds = new AABB(anchor).inflate(0.008D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.ANCHOR, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
}
BuilderPreviewState.Selection selection = snapshot == null
|| !visibility.showsSelection(snapshot.blueprintPreview())
? null : snapshot.selection();
if (selection != null && selection.active()) {
if (selection.first() != null) {
AABB bounds = new AABB(selection.first()).inflate(0.006D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.BUILD, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
if (selection.second() != null) {
AABB bounds = new AABB(selection.second()).inflate(0.006D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.DEMOLISH, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
AABB bounds = selection.bounds();
if (bounds != null && withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds.inflate(0.004D),
BuilderPreviewState.Kind.BLUEPRINT, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
// Follow the same fully published generation as the cached cell VBOs. Reading the newest
// state here would move the outer frame before its replacement geometry is ready.
AABB blueprintBounds = visibility.showsDeploymentFrame()
? blueprintBounds(snapshot) : null;
if (blueprintBounds != null && withinDistance(blueprintBounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, blueprintBounds.inflate(0.004D),
BuilderPreviewState.Kind.BLUEPRINT, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
if (candidate != null) {
AABB bounds = new AABB(candidate.pos()).inflate(0.006D);
if (withinDistance(bounds, camera, maximumDistanceSqr)) {
BuilderPreviewGeometry.emitBox(pose, quads, bounds,
BuilderPreviewState.Kind.BLUEPRINT, NORMAL_OUTLINE_WIDTH,
alphaScale, false);
}
}
BuilderPreviewState.Focus focus = DYNAMIC_FOCUS_ANIMATION.renderedFocus();
float focusAmount = DYNAMIC_FOCUS_ANIMATION.smoothAmount();
if (focus != null && visibility.showsFocus(focus.kind())
&& focusAmount > 0.001F
&& withinDistance(focus.bounds(), camera, maximumDistanceSqr)) {
BuilderPreviewState.Kind color = focus.kind() == BuilderPreviewState.FocusKind.MISSING
? BuilderPreviewState.Kind.MISSING : BuilderPreviewState.Kind.BLUEPRINT;
double animatedWidth = Mth.lerp(focusAmount,
NORMAL_OUTLINE_WIDTH, FOCUSED_OUTLINE_WIDTH);
BuilderPreviewGeometry.emitBox(pose, quads, focus.bounds(),
color, animatedWidth, alphaScale * focusAmount, false);
}
buffers.endBatch(renderType);
}
private static boolean withinDistance(AABB bounds, Vec3 point, double maximumDistanceSqr) {
double x = Mth.clamp(point.x, bounds.minX, bounds.maxX) - point.x;
double y = Mth.clamp(point.y, bounds.minY, bounds.maxY) - point.y;
double z = Mth.clamp(point.z, bounds.minZ, bounds.maxZ) - point.z;
return x * x + y * y + z * z <= maximumDistanceSqr;
}
@Nullable
private static AABB blueprintBounds(@Nullable BuilderPreviewState.Snapshot snapshot) {
if (snapshot == null || !snapshot.blueprintPreview() || snapshot.cells().isEmpty()) return null;
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (BuilderPreviewState.Cell cell : snapshot.cells()) {
// Retained missing work never belongs to the deployment focus/frame volume.
if (cell.kind() == BuilderPreviewState.Kind.MISSING) continue;
minX = Math.min(minX, cell.pos().getX());
minY = Math.min(minY, cell.pos().getY());
minZ = Math.min(minZ, cell.pos().getZ());
maxX = Math.max(maxX, cell.pos().getX());
maxY = Math.max(maxY, cell.pos().getY());
maxZ = Math.max(maxZ, cell.pos().getZ());
}
return minX == Integer.MAX_VALUE ? null
: new AABB(minX, minY, minZ, maxX + 1.0D, maxY + 1.0D, maxZ + 1.0D);
}
private static int previewDistance(Minecraft minecraft) {
int renderSections = minecraft.options.getEffectiveRenderDistance() + EXTRA_RENDER_SECTIONS;
return Mth.clamp(renderSections * 16,
MINIMUM_PREVIEW_DISTANCE, MAXIMUM_PREVIEW_DISTANCE);
}
static double focusedOutlineWidth() {
return FOCUSED_OUTLINE_WIDTH;
}
/** Smoothly interpolates the handful of dynamic selection/deployment focus boxes. */
private static final class FocusOutlineAnimation {
private static final float DURATION_SECONDS = 0.16F;
@Nullable private BuilderPreviewState.Focus renderedFocus;
private boolean targetFocused;
private float amount;
private long lastNanos;
private void update(@Nullable BuilderPreviewState.Focus target, long nowNanos) {
if (target != null && !sameTarget(renderedFocus, target)) {
renderedFocus = target;
amount = 0.0F;
}
targetFocused = target != null;
if (lastNanos == 0L) {
lastNanos = nowNanos;
return;
}
float elapsed = Math.min(0.05F,
Math.max(0.0F, (nowNanos - lastNanos) / 1_000_000_000.0F));
lastNanos = nowNanos;
float step = elapsed / DURATION_SECONDS;
amount = targetFocused
? Math.min(1.0F, amount + step)
: Math.max(0.0F, amount - step);
if (!targetFocused && amount <= 0.0F) renderedFocus = null;
}
private void clear() {
renderedFocus = null;
targetFocused = false;
amount = 0.0F;
lastNanos = 0L;
}
@Nullable
private BuilderPreviewState.Focus renderedFocus() {
return renderedFocus;
}
private float smoothAmount() {
return amount * amount * (3.0F - 2.0F * amount);
}
private static boolean sameTarget(@Nullable BuilderPreviewState.Focus first,
BuilderPreviewState.Focus second) {
return first != null && first.kind() == second.kind()
&& first.bounds().equals(second.bounds());
}
}
}