package com.xfestudio.mydimension.client.builder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexBuffer;
import com.mojang.blaze3d.vertex.VertexFormat;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Progressive VBO cache for the one focused missing-material connected group. */
final class BuilderFocusedOutlineCache {
private static final int SECTION_SHIFT = 4;
private static final int SECTION_SIZE = 1 << SECTION_SHIFT;
private static final int EDGES_PER_UPLOAD = 1024;
private static final int UPLOADS_PER_FRAME = 2;
private static final long UPLOAD_BUDGET_NANOS = 1_500_000L;
private static final float FOCUS_ANIMATION_SECONDS = 0.16F;
private static final Axis[] AXES = Axis.values();
@Nullable private BuilderPreviewState.MissingGroup source;
private final Map<SectionKey, FocusSection> sections = new LinkedHashMap<>();
private List<FocusSection> visible = List.of();
private boolean focusTarget;
private float focusAmount;
private long lastAnimationNanos;
/**
* Changes focus without destroying the previous VBO immediately. The maximum-width outline
* fades over the always-present normal outline, giving even a very large group a smooth grow and
* shrink transition without rebuilding all of its geometry every frame.
*/
void synchronize(@Nullable BuilderPreviewState.MissingGroup group, long nowNanos) {
if (group != null && source != group) {
if (sameGroup(source, group)) {
// Snapshot refreshes rebuild the immutable group object even when its cells did
// not change. Keep the finished VBOs instead of flashing back to a thin outline.
source = group;
} else {
clearGeometry();
source = group;
focusAmount = 0.0F;
populate(group);
}
}
focusTarget = group != null;
advanceAnimation(nowNanos);
if (!focusTarget && focusAmount <= 0.0F && source != null) {
clearGeometry();
source = null;
}
}
private static boolean sameGroup(@Nullable BuilderPreviewState.MissingGroup first,
BuilderPreviewState.MissingGroup second) {
return first != null && first.bounds().equals(second.bounds())
&& first.cells().equals(second.cells());
}
private void populate(BuilderPreviewState.MissingGroup group) {
if (group.cells().isEmpty()) return;
// Group only lightweight cell references here. Edge expansion is deferred until a
// section is actually visible, so moving the crosshair into a very large group cannot
// allocate hundreds of thousands of edge objects in one frame.
Map<SectionKey, List<BuilderPreviewState.Cell>> grouped = new LinkedHashMap<>();
for (BuilderPreviewState.Cell cell : group.cells()) {
SectionKey key = SectionKey.of(cell);
grouped.computeIfAbsent(key, ignored -> new ArrayList<>()).add(cell);
}
grouped.forEach((key, cells) -> sections.put(key,
new FocusSection(key, List.copyOf(cells))));
}
private void advanceAnimation(long nowNanos) {
if (lastAnimationNanos == 0L) {
lastAnimationNanos = nowNanos;
return;
}
float elapsed = Math.min(0.05F,
Math.max(0.0F, (nowNanos - lastAnimationNanos) / 1_000_000_000.0F));
lastAnimationNanos = nowNanos;
float step = elapsed / FOCUS_ANIMATION_SECONDS;
focusAmount = focusTarget
? Math.min(1.0F, focusAmount + step)
: Math.max(0.0F, focusAmount - step);
}
void prepare(Frustum frustum, Vec3 camera, double maximumDistanceSqr) {
List<FocusSection> current = new ArrayList<>();
for (FocusSection section : sections.values()) {
if (section.closestDistanceToSqr(camera) <= maximumDistanceSqr
&& frustum.isVisible(section.bounds.inflate(1.5D))) {
current.add(section);
}
}
current.sort(Comparator.comparingDouble(section -> section.distanceToSqr(camera)));
visible = List.copyOf(current);
if (!focusTarget) return;
int uploads = 0;
long deadline = System.nanoTime() + UPLOAD_BUDGET_NANOS;
for (FocusSection section : current) {
if (uploads >= UPLOADS_PER_FRAME) break;
if (uploads > 0 && System.nanoTime() >= deadline) break;
if (section.uploadNext()) uploads++;
}
}
void draw(PoseStack poseStack, Matrix4f projection, RenderType renderType, float alpha) {
float animatedAlpha = alpha * smoothFocusAmount();
if (visible.isEmpty() || animatedAlpha <= 0.001F) return;
renderType.setupRenderState();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, animatedAlpha);
try {
ShaderInstance shader = RenderSystem.getShader();
if (shader == null) return;
for (FocusSection section : visible) {
poseStack.pushPose();
poseStack.translate(section.originX, section.originY, section.originZ);
for (VertexBuffer buffer : section.buffers) {
if (buffer.isInvalid()) continue;
buffer.bind();
buffer.drawWithShader(poseStack.last().pose(), projection, shader);
}
poseStack.popPose();
}
} finally {
VertexBuffer.unbind();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
renderType.clearRenderState();
}
}
void clear() {
clearGeometry();
source = null;
focusTarget = false;
focusAmount = 0.0F;
lastAnimationNanos = 0L;
}
private void clearGeometry() {
visible = List.of();
sections.values().forEach(FocusSection::close);
sections.clear();
}
private float smoothFocusAmount() {
return focusAmount * focusAmount * (3.0F - 2.0F * focusAmount);
}
private static final class FocusSection {
private final int originX;
private final int originY;
private final int originZ;
private final AABB bounds;
private List<BuilderPreviewState.Cell> cells;
@Nullable private LongList edges;
private final List<VertexBuffer> buffers = new ArrayList<>();
private int cursor;
private FocusSection(SectionKey key, List<BuilderPreviewState.Cell> cells) {
originX = key.x() * SECTION_SIZE;
originY = key.y() * SECTION_SIZE;
originZ = key.z() * SECTION_SIZE;
bounds = new AABB(originX, originY, originZ,
originX + SECTION_SIZE, originY + SECTION_SIZE, originZ + SECTION_SIZE);
this.cells = cells;
}
private boolean uploadNext() {
ensureEdges();
LongList currentEdges = edges;
if (currentEdges == null) return false;
if (cursor >= currentEdges.size()) return false;
int end = Math.min(currentEdges.size(), cursor + EDGES_PER_UPLOAD);
BufferBuilder builder = new BufferBuilder(Math.max(1024, (end - cursor) * 272));
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
PoseStack.Pose identity = new PoseStack().last();
for (int index = cursor; index < end; index++) {
long packedEdge = currentEdges.getLong(index);
int x = edgeX(packedEdge);
int y = edgeY(packedEdge);
int z = edgeZ(packedEdge);
Axis axis = edgeAxis(packedEdge);
double x2 = x + (axis == Axis.X ? 1.0D : 0.0D);
double y2 = y + (axis == Axis.Y ? 1.0D : 0.0D);
double z2 = z + (axis == Axis.Z ? 1.0D : 0.0D);
BuilderPreviewGeometry.emitEdge(identity, builder,
x, y, z, x2, y2, z2,
BuilderPreviewState.Kind.MISSING,
BuilderPreviewRenderer.focusedOutlineWidth(), 1.0F);
}
cursor = end;
BufferBuilder.RenderedBuffer rendered = builder.endOrDiscardIfEmpty();
if (rendered != null) {
VertexBuffer buffer = new VertexBuffer(VertexBuffer.Usage.STATIC);
buffer.bind();
buffer.upload(rendered);
VertexBuffer.unbind();
buffers.add(buffer);
}
return true;
}
private void ensureEdges() {
if (edges != null) return;
LongOpenHashSet unique = new LongOpenHashSet(Math.max(16, cells.size() * 4));
for (BuilderPreviewState.Cell cell : cells) {
addCellEdges(unique, cell.pos().getX() - originX,
cell.pos().getY() - originY, cell.pos().getZ() - originZ);
}
edges = new LongArrayList(unique);
cells = List.of();
}
private double closestDistanceToSqr(Vec3 point) {
double dx = axisDistance(point.x, bounds.minX, bounds.maxX);
double dy = axisDistance(point.y, bounds.minY, bounds.maxY);
double dz = axisDistance(point.z, bounds.minZ, bounds.maxZ);
return dx * dx + dy * dy + dz * dz;
}
private double distanceToSqr(Vec3 point) {
double x = (bounds.minX + bounds.maxX) * 0.5D - point.x;
double y = (bounds.minY + bounds.maxY) * 0.5D - point.y;
double z = (bounds.minZ + bounds.maxZ) * 0.5D - point.z;
return x * x + y * y + z * z;
}
private void close() {
for (VertexBuffer buffer : buffers) {
if (buffer.isInvalid()) continue;
if (RenderSystem.isOnRenderThread()) buffer.close();
else RenderSystem.recordRenderCall(buffer::close);
}
buffers.clear();
}
}
private static void addCellEdges(LongOpenHashSet edges, int x, int y, int z) {
edges.add(packEdge(x, y, z, Axis.X));
edges.add(packEdge(x, y + 1, z, Axis.X));
edges.add(packEdge(x, y, z + 1, Axis.X));
edges.add(packEdge(x, y + 1, z + 1, Axis.X));
edges.add(packEdge(x, y, z, Axis.Y));
edges.add(packEdge(x + 1, y, z, Axis.Y));
edges.add(packEdge(x, y, z + 1, Axis.Y));
edges.add(packEdge(x + 1, y, z + 1, Axis.Y));
edges.add(packEdge(x, y, z, Axis.Z));
edges.add(packEdge(x + 1, y, z, Axis.Z));
edges.add(packEdge(x, y + 1, z, Axis.Z));
edges.add(packEdge(x + 1, y + 1, z, Axis.Z));
}
private static double axisDistance(double value, double minimum, double maximum) {
if (value < minimum) return minimum - value;
if (value > maximum) return value - maximum;
return 0.0D;
}
private static long packEdge(int x, int y, int z, Axis axis) {
return (long) x | (long) y << 5 | (long) z << 10 | (long) axis.ordinal() << 15;
}
private static int edgeX(long edge) { return (int) (edge & 31L); }
private static int edgeY(long edge) { return (int) (edge >> 5 & 31L); }
private static int edgeZ(long edge) { return (int) (edge >> 10 & 31L); }
private static Axis edgeAxis(long edge) { return AXES[(int) (edge >> 15 & 3L)]; }
private enum Axis { X, Y, Z }
private record SectionKey(int x, int y, int z) {
private static SectionKey of(BuilderPreviewState.Cell cell) {
return new SectionKey(cell.pos().getX() >> SECTION_SHIFT,
cell.pos().getY() >> SECTION_SHIFT,
cell.pos().getZ() >> SECTION_SHIFT);
}
}
}
package com.xfestudio.mydimension.client.builder;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexBuffer;
import com.mojang.blaze3d.vertex.VertexFormat;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.ShaderInstance;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/** Progressive VBO cache for the one focused missing-material connected group. */
final class BuilderFocusedOutlineCache {
private static final int SECTION_SHIFT = 4;
private static final int SECTION_SIZE = 1 << SECTION_SHIFT;
private static final int EDGES_PER_UPLOAD = 1024;
private static final int UPLOADS_PER_FRAME = 2;
private static final long UPLOAD_BUDGET_NANOS = 1_500_000L;
private static final float FOCUS_ANIMATION_SECONDS = 0.16F;
private static final Axis[] AXES = Axis.values();
@Nullable private BuilderPreviewState.MissingGroup source;
private final Map<SectionKey, FocusSection> sections = new LinkedHashMap<>();
private List<FocusSection> visible = List.of();
private boolean focusTarget;
private float focusAmount;
private long lastAnimationNanos;
/**
* Changes focus without destroying the previous VBO immediately. The maximum-width outline
* fades over the always-present normal outline, giving even a very large group a smooth grow and
* shrink transition without rebuilding all of its geometry every frame.
*/
void synchronize(@Nullable BuilderPreviewState.MissingGroup group, long nowNanos) {
if (group != null && source != group) {
if (sameGroup(source, group)) {
// Snapshot refreshes rebuild the immutable group object even when its cells did
// not change. Keep the finished VBOs instead of flashing back to a thin outline.
source = group;
} else {
clearGeometry();
source = group;
focusAmount = 0.0F;
populate(group);
}
}
focusTarget = group != null;
advanceAnimation(nowNanos);
if (!focusTarget && focusAmount <= 0.0F && source != null) {
clearGeometry();
source = null;
}
}
private static boolean sameGroup(@Nullable BuilderPreviewState.MissingGroup first,
BuilderPreviewState.MissingGroup second) {
return first != null && first.bounds().equals(second.bounds())
&& first.cells().equals(second.cells());
}
private void populate(BuilderPreviewState.MissingGroup group) {
if (group.cells().isEmpty()) return;
// Group only lightweight cell references here. Edge expansion is deferred until a
// section is actually visible, so moving the crosshair into a very large group cannot
// allocate hundreds of thousands of edge objects in one frame.
Map<SectionKey, List<BuilderPreviewState.Cell>> grouped = new LinkedHashMap<>();
for (BuilderPreviewState.Cell cell : group.cells()) {
SectionKey key = SectionKey.of(cell);
grouped.computeIfAbsent(key, ignored -> new ArrayList<>()).add(cell);
}
grouped.forEach((key, cells) -> sections.put(key,
new FocusSection(key, List.copyOf(cells))));
}
private void advanceAnimation(long nowNanos) {
if (lastAnimationNanos == 0L) {
lastAnimationNanos = nowNanos;
return;
}
float elapsed = Math.min(0.05F,
Math.max(0.0F, (nowNanos - lastAnimationNanos) / 1_000_000_000.0F));
lastAnimationNanos = nowNanos;
float step = elapsed / FOCUS_ANIMATION_SECONDS;
focusAmount = focusTarget
? Math.min(1.0F, focusAmount + step)
: Math.max(0.0F, focusAmount - step);
}
void prepare(Frustum frustum, Vec3 camera, double maximumDistanceSqr) {
List<FocusSection> current = new ArrayList<>();
for (FocusSection section : sections.values()) {
if (section.closestDistanceToSqr(camera) <= maximumDistanceSqr
&& frustum.isVisible(section.bounds.inflate(1.5D))) {
current.add(section);
}
}
current.sort(Comparator.comparingDouble(section -> section.distanceToSqr(camera)));
visible = List.copyOf(current);
if (!focusTarget) return;
int uploads = 0;
long deadline = System.nanoTime() + UPLOAD_BUDGET_NANOS;
for (FocusSection section : current) {
if (uploads >= UPLOADS_PER_FRAME) break;
if (uploads > 0 && System.nanoTime() >= deadline) break;
if (section.uploadNext()) uploads++;
}
}
void draw(PoseStack poseStack, Matrix4f projection, RenderType renderType, float alpha) {
float animatedAlpha = alpha * smoothFocusAmount();
if (visible.isEmpty() || animatedAlpha <= 0.001F) return;
renderType.setupRenderState();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, animatedAlpha);
try {
ShaderInstance shader = RenderSystem.getShader();
if (shader == null) return;
for (FocusSection section : visible) {
poseStack.pushPose();
poseStack.translate(section.originX, section.originY, section.originZ);
for (VertexBuffer buffer : section.buffers) {
if (buffer.isInvalid()) continue;
buffer.bind();
buffer.drawWithShader(poseStack.last().pose(), projection, shader);
}
poseStack.popPose();
}
} finally {
VertexBuffer.unbind();
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
renderType.clearRenderState();
}
}
void clear() {
clearGeometry();
source = null;
focusTarget = false;
focusAmount = 0.0F;
lastAnimationNanos = 0L;
}
private void clearGeometry() {
visible = List.of();
sections.values().forEach(FocusSection::close);
sections.clear();
}
private float smoothFocusAmount() {
return focusAmount * focusAmount * (3.0F - 2.0F * focusAmount);
}
private static final class FocusSection {
private final int originX;
private final int originY;
private final int originZ;
private final AABB bounds;
private List<BuilderPreviewState.Cell> cells;
@Nullable private LongList edges;
private final List<VertexBuffer> buffers = new ArrayList<>();
private int cursor;
private FocusSection(SectionKey key, List<BuilderPreviewState.Cell> cells) {
originX = key.x() * SECTION_SIZE;
originY = key.y() * SECTION_SIZE;
originZ = key.z() * SECTION_SIZE;
bounds = new AABB(originX, originY, originZ,
originX + SECTION_SIZE, originY + SECTION_SIZE, originZ + SECTION_SIZE);
this.cells = cells;
}
private boolean uploadNext() {
ensureEdges();
LongList currentEdges = edges;
if (currentEdges == null) return false;
if (cursor >= currentEdges.size()) return false;
int end = Math.min(currentEdges.size(), cursor + EDGES_PER_UPLOAD);
BufferBuilder builder = new BufferBuilder(Math.max(1024, (end - cursor) * 272));
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
PoseStack.Pose identity = new PoseStack().last();
for (int index = cursor; index < end; index++) {
long packedEdge = currentEdges.getLong(index);
int x = edgeX(packedEdge);
int y = edgeY(packedEdge);
int z = edgeZ(packedEdge);
Axis axis = edgeAxis(packedEdge);
double x2 = x + (axis == Axis.X ? 1.0D : 0.0D);
double y2 = y + (axis == Axis.Y ? 1.0D : 0.0D);
double z2 = z + (axis == Axis.Z ? 1.0D : 0.0D);
BuilderPreviewGeometry.emitEdge(identity, builder,
x, y, z, x2, y2, z2,
BuilderPreviewState.Kind.MISSING,
BuilderPreviewRenderer.focusedOutlineWidth(), 1.0F);
}
cursor = end;
BufferBuilder.RenderedBuffer rendered = builder.endOrDiscardIfEmpty();
if (rendered != null) {
VertexBuffer buffer = new VertexBuffer(VertexBuffer.Usage.STATIC);
buffer.bind();
buffer.upload(rendered);
VertexBuffer.unbind();
buffers.add(buffer);
}
return true;
}
private void ensureEdges() {
if (edges != null) return;
LongOpenHashSet unique = new LongOpenHashSet(Math.max(16, cells.size() * 4));
for (BuilderPreviewState.Cell cell : cells) {
addCellEdges(unique, cell.pos().getX() - originX,
cell.pos().getY() - originY, cell.pos().getZ() - originZ);
}
edges = new LongArrayList(unique);
cells = List.of();
}
private double closestDistanceToSqr(Vec3 point) {
double dx = axisDistance(point.x, bounds.minX, bounds.maxX);
double dy = axisDistance(point.y, bounds.minY, bounds.maxY);
double dz = axisDistance(point.z, bounds.minZ, bounds.maxZ);
return dx * dx + dy * dy + dz * dz;
}
private double distanceToSqr(Vec3 point) {
double x = (bounds.minX + bounds.maxX) * 0.5D - point.x;
double y = (bounds.minY + bounds.maxY) * 0.5D - point.y;
double z = (bounds.minZ + bounds.maxZ) * 0.5D - point.z;
return x * x + y * y + z * z;
}
private void close() {
for (VertexBuffer buffer : buffers) {
if (buffer.isInvalid()) continue;
if (RenderSystem.isOnRenderThread()) buffer.close();
else RenderSystem.recordRenderCall(buffer::close);
}
buffers.clear();
}
}
private static void addCellEdges(LongOpenHashSet edges, int x, int y, int z) {
edges.add(packEdge(x, y, z, Axis.X));
edges.add(packEdge(x, y + 1, z, Axis.X));
edges.add(packEdge(x, y, z + 1, Axis.X));
edges.add(packEdge(x, y + 1, z + 1, Axis.X));
edges.add(packEdge(x, y, z, Axis.Y));
edges.add(packEdge(x + 1, y, z, Axis.Y));
edges.add(packEdge(x, y, z + 1, Axis.Y));
edges.add(packEdge(x + 1, y, z + 1, Axis.Y));
edges.add(packEdge(x, y, z, Axis.Z));
edges.add(packEdge(x + 1, y, z, Axis.Z));
edges.add(packEdge(x, y + 1, z, Axis.Z));
edges.add(packEdge(x + 1, y + 1, z, Axis.Z));
}
private static double axisDistance(double value, double minimum, double maximum) {
if (value < minimum) return minimum - value;
if (value > maximum) return value - maximum;
return 0.0D;
}
private static long packEdge(int x, int y, int z, Axis axis) {
return (long) x | (long) y << 5 | (long) z << 10 | (long) axis.ordinal() << 15;
}
private static int edgeX(long edge) { return (int) (edge & 31L); }
private static int edgeY(long edge) { return (int) (edge >> 5 & 31L); }
private static int edgeZ(long edge) { return (int) (edge >> 10 & 31L); }
private static Axis edgeAxis(long edge) { return AXES[(int) (edge >> 15 & 3L)]; }
private enum Axis { X, Y, Z }
private record SectionKey(int x, int y, int z) {
private static SectionKey of(BuilderPreviewState.Cell cell) {
return new SectionKey(cell.pos().getX() >> SECTION_SHIFT,
cell.pos().getY() >> SECTION_SHIFT,
cell.pos().getZ() >> SECTION_SHIFT);
}
}
}