Add client rendering and collision optimizations with parity tests

This commit is contained in:
ms2k 2026-10-20 11:04:56 +09:00
commit 5c04905561
46 changed files with 3167 additions and 0 deletions

View file

@ -1,2 +1,31 @@
version = '0.1.0'
base.archivesName = 'saltatory-client'
repositories {
maven {
url = 'https://maven.createmod.net'
content {
includeGroup 'net.createmod.ponder'
includeGroup 'dev.engine-room.flywheel'
}
}
maven {
url = 'https://api.modrinth.com/maven'
content { includeGroup 'maven.modrinth' }
}
}
configurations { testImplementation.extendsFrom compileOnly }
dependencies {
compileOnly('net.createmod.ponder:ponder-neoforge:1.0.82+mc1.21.1') { transitive = false }
compileOnly('maven.modrinth:iris:t3ruzodq') { transitive = false }
compileOnly('dev.engine-room.flywheel:flywheel-neoforge-1.21.1:1.0.6') { transitive = false }
testImplementation 'org.ow2.asm:asm-tree:9.8'
testImplementation 'org.ow2.asm:asm-commons:9.8'
}
tasks.named('test') {
workingDir layout.buildDirectory.dir('test-work')
doFirst { workingDir.mkdirs() }
}

View file

@ -0,0 +1,7 @@
package dev.quorate.client;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.fml.common.Mod;
@Mod(value = "saltatory_client", dist = Dist.CLIENT)
public final class ClientMod {}

View file

@ -0,0 +1,49 @@
package dev.quorate.client.mixin;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import dev.quorate.client.render.WorkQueue;
import dev.quorate.client.render.WorkTarget;
import net.irisshaders.iris.vertices.MojangBufferAccessor;
import org.spongepowered.asm.mixin.Mixin;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ByteBufferBuilder.class)
public abstract class BufferTasks implements WorkTarget {
@Unique private WorkQueue work;
@Override
public WorkQueue workQueue() {
if (work == null) work = new WorkQueue();
return work;
}
@Inject(method = "build", at = @At("HEAD"))
private void publish(CallbackInfoReturnable<ByteBufferBuilder.Result> info) {
if (work != null) work.drain(((MojangBufferAccessor) this).getPointer(), true);
}
@Inject(
method = {"clear", "discard", "freeResult"},
at = @At("HEAD"))
private void compact(CallbackInfo info) {
if (work != null) work.drain(((MojangBufferAccessor) this).getPointer(), true);
}
@WrapMethod(method = "close")
private void release(Operation<Void> original) {
try {
if (work != null) work.drain(0, false);
} finally {
original.call();
}
}
}

View file

@ -0,0 +1,30 @@
package dev.quorate.client.mixin;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.quorate.client.render.ColorDraw;
import dev.quorate.client.render.TangentSkip;
import net.createmod.catnip.render.ShadeSeparatingSuperByteBuffer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(value = ShadeSeparatingSuperByteBuffer.class, remap = false)
public abstract class CreateAttrs {
@WrapMethod(method = "renderInto")
private void render(PoseStack pose, VertexConsumer consumer, Operation<Void> original) {
if (TangentSkip.enabled()) {
TangentSkip.scoped(() -> emit(pose, consumer, original));
} else original.call(pose, consumer);
}
@Unique
private void emit(PoseStack pose, VertexConsumer consumer, Operation<Void> original) {
if (!ColorDraw.render((ShadeSeparatingSuperByteBuffer) (Object) this, pose, consumer))
original.call(pose, consumer);
}
}

View file

@ -0,0 +1,44 @@
package dev.quorate.client.mixin;
import net.minecraft.core.Cursor3D;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(value = Cursor3D.class, remap = false)
public abstract class CursorStep {
@Shadow @Final private int width;
@Shadow @Final private int height;
@Shadow @Final private int end;
@Shadow private int index;
@Shadow private int x;
@Shadow private int y;
@Shadow private int z;
@Overwrite
public boolean advance() {
if (index == end) return false;
if (width > 0 && height > 0 && end > 0) {
if (index == 0) {
x = 0;
y = 0;
z = 0;
} else if (++x == width) {
x = 0;
if (++y == height) {
y = 0;
z++;
}
}
} else {
x = index % width;
int row = index / width;
y = row % height;
z = row / height;
}
index++;
return true;
}
}

View file

@ -0,0 +1,37 @@
package dev.quorate.client.mixin;
import dev.quorate.client.render.MeshPlan;
import dev.quorate.client.render.PlaneBound;
import dev.quorate.client.render.PlaneTarget;
import net.createmod.catnip.render.TemplateMesh;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
@Mixin(value = TemplateMesh.class, remap = false)
public abstract class MeshCull implements PlaneTarget {
@Shadow private int[] data;
@Unique private MeshPlan plan;
@Unique
private MeshPlan current() {
var value = plan;
if (value == null || !value.matches(data)) {
value = new MeshPlan(data);
plan = value;
}
return value;
}
@Override
public PlaneBound.Model plane() {
return current().model();
}
@Override
public TemplateMesh snapshot() {
return current().snapshot();
}
}

View file

@ -0,0 +1,13 @@
package dev.quorate.client.mixin;
import net.irisshaders.iris.layer.OuterWrappedRenderType;
import net.minecraft.client.renderer.RenderStateShard;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(value = OuterWrappedRenderType.class, remap = false)
public interface OuterState {
@Accessor("extra")
RenderStateShard extra();
}

View file

@ -0,0 +1,20 @@
package dev.quorate.client.mixin;
import it.unimi.dsi.fastutil.doubles.DoubleList;
import net.minecraft.core.Direction.Axis;
import net.minecraft.world.phys.shapes.DiscreteVoxelShape;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(value = VoxelShape.class, remap = false)
public interface ShapeAccess {
@Accessor("shape")
DiscreteVoxelShape grid();
@Invoker("getCoords")
DoubleList coords(Axis axis);
}

View file

@ -0,0 +1,31 @@
package dev.quorate.client.mixin;
import dev.quorate.client.shape.BoxJoin;
import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(value = Shapes.class, remap = false)
public abstract class ShapeMasks {
@Inject(
method =
"joinIsNotEmpty(Lnet/minecraft/world/phys/shapes/VoxelShape;Lnet/minecraft/world/phys/shapes/VoxelShape;Lnet/minecraft/world/phys/shapes/BooleanOp;)Z",
at = @At("HEAD"),
cancellable = true)
private static void intersect(
VoxelShape first,
VoxelShape second,
BooleanOp operation,
CallbackInfoReturnable<Boolean> result) {
if (operation == BooleanOp.AND) {
var answer = BoxJoin.test(first, second);
if (answer != BoxJoin.Match.UNKNOWN) result.setReturnValue(answer == BoxJoin.Match.YES);
}
}
}

View file

@ -0,0 +1,100 @@
package dev.quorate.client.mixin;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.VertexFormat;
import dev.quorate.client.render.FaceCull;
import dev.quorate.client.render.FaceRead;
import dev.quorate.client.render.TangentSkip;
import dev.quorate.client.render.TangentTarget;
import net.irisshaders.iris.layer.BlockEntityRenderStateShard;
import net.irisshaders.iris.layer.OuterWrappedRenderType;
import net.irisshaders.iris.vertices.IrisVertexFormats;
import net.irisshaders.iris.vertices.MojangBufferAccessor;
import net.irisshaders.iris.vertices.NormalHelper;
import net.irisshaders.iris.vertices.views.QuadView;
import net.irisshaders.iris.vertices.views.TriView;
import net.minecraft.client.renderer.RenderType;
import org.joml.Vector3f;
import org.spongepowered.asm.mixin.Final;
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.Redirect;
// INVOKE injection into Iris's priority-1000 merged helper requires a higher priority.
@Mixin(value = BufferBuilder.class, priority = 1001, remap = false)
public abstract class TangentAttrs implements TangentTarget {
@Unique private boolean blockLayer;
@Unique private boolean backCull;
@Override
public void tangentType(RenderType type) {
backCull = FaceCull.culls(type);
blockLayer =
type instanceof OuterWrappedRenderType outer
&& ((OuterState) outer).extra() == BlockEntityRenderStateShard.INSTANCE
&& RenderType.chunkBufferLayers().contains(outer.unwrap());
}
@Override
public boolean cullsBackfaces() {
return backCull;
}
@Override
public boolean skipsTangents() {
return TangentSkip.active() && eligible();
}
@Unique
private boolean eligible() {
return format == IrisVertexFormats.ENTITY
|| format == IrisVertexFormats.TERRAIN && blockLayer;
}
@Shadow @Final private VertexFormat format;
@Shadow @Final private ByteBufferBuilder buffer;
@Shadow @Final private long[] vertexOffsets;
@Redirect(
method = "fillExtendedData",
at =
@At(
value = "INVOKE",
target =
"Lnet/irisshaders/iris/vertices/NormalHelper;computeFaceNormal(Lorg/joml/Vector3f;Lnet/irisshaders/iris/vertices/views/QuadView;)V"))
private void normal(Vector3f output, QuadView view) {
FaceRead.compute(output, ((MojangBufferAccessor) buffer).getPointer(), vertexOffsets);
}
@Redirect(
method = "fillExtendedData",
at =
@At(
value = "INVOKE",
target =
"Lnet/irisshaders/iris/vertices/NormalHelper;computeTangent(FFFLnet/irisshaders/iris/vertices/views/TriView;)I"))
private int quad(float x, float y, float z, TriView view) {
return eligible()
? TangentSkip.quad(x, y, z, view)
: NormalHelper.computeTangent(x, y, z, view);
}
@Redirect(
method = "fillExtendedData",
at =
@At(
value = "INVOKE",
target =
"Lnet/irisshaders/iris/vertices/NormalHelper;computeTangentSmooth(FFFLnet/irisshaders/iris/vertices/views/TriView;)I"))
private int smooth(float x, float y, float z, TriView view) {
return eligible()
? TangentSkip.smooth(x, y, z, view)
: NormalHelper.computeTangentSmooth(x, y, z, view);
}
}

View file

@ -0,0 +1,24 @@
package dev.quorate.client.mixin;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.quorate.client.render.TangentTarget;
import net.irisshaders.batchedentityrendering.impl.SegmentedBufferBuilder;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(
value = {MultiBufferSource.BufferSource.class, SegmentedBufferBuilder.class},
remap = false)
public abstract class TangentSource {
@Inject(method = "getBuffer", at = @At("RETURN"))
private void identify(RenderType type, CallbackInfoReturnable<VertexConsumer> result) {
if (result.getReturnValue() instanceof TangentTarget target) target.tangentType(type);
}
}

View file

@ -0,0 +1,68 @@
package dev.quorate.client.render;
import net.minecraft.util.Mth;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteOrder;
public final class BatchData {
private static final boolean LITTLE = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;
private BatchData() {}
private static byte normal(float value) {
return (byte) ((int) (Mth.clamp(value, -1f, 1f) * 127f) & 255);
}
private static void uv(long pointer, int value) {
if (LITTLE) MemoryUtil.memPutInt(pointer, value);
else {
MemoryUtil.memPutShort(pointer, (short) value);
MemoryUtil.memPutShort(pointer + 2, (short) (value >>> 16));
}
}
public static void write(
long pointer,
boolean entity,
float x,
float y,
float z,
int abgr,
float u,
float v,
int overlay,
int light,
float nx,
float ny,
float nz) {
attributes(pointer, entity, x, y, z, abgr, u, v, overlay, light);
int offset = entity ? 28 : 24;
MemoryUtil.memPutByte(pointer + offset + 4, normal(nx));
MemoryUtil.memPutByte(pointer + offset + 5, normal(ny));
MemoryUtil.memPutByte(pointer + offset + 6, normal(nz));
}
public static void attributes(
long pointer,
boolean entity,
float x,
float y,
float z,
int abgr,
float u,
float v,
int overlay,
int light) {
MemoryUtil.memPutFloat(pointer, x);
MemoryUtil.memPutFloat(pointer + 4, y);
MemoryUtil.memPutFloat(pointer + 8, z);
MemoryUtil.memPutInt(pointer + 12, LITTLE ? abgr : Integer.reverseBytes(abgr));
MemoryUtil.memPutFloat(pointer + 16, u);
MemoryUtil.memPutFloat(pointer + 20, v);
if (entity) uv(pointer + 24, overlay);
int offset = entity ? 28 : 24;
uv(pointer + offset, light);
}
}

View file

@ -0,0 +1,322 @@
package dev.quorate.client.render;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
import com.mojang.blaze3d.vertex.VertexFormat;
import com.mojang.blaze3d.vertex.VertexFormatElement;
import net.createmod.catnip.render.SuperByteBuffer;
import net.createmod.catnip.render.TemplateMesh;
import net.irisshaders.iris.shadows.ShadowRenderer;
import net.irisshaders.iris.uniforms.CapturedRenderingState;
import net.irisshaders.iris.vertices.ExtendedDataHelper;
import net.irisshaders.iris.vertices.ImmediateState;
import net.irisshaders.iris.vertices.IrisVertexFormats;
import net.irisshaders.iris.vertices.MojangBufferAccessor;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.system.MemoryUtil;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;
final class BatchDraw {
private record Layout(
boolean entity, int stride, int idOffset, int midOffset, QuadEnd.Format quad) {}
private static final class State {
private static final VarHandle BUILDING = field("building", boolean.class);
private static final VarHandle BUFFER = field("buffer", ByteBufferBuilder.class);
private static final VarHandle FORMAT = field("format", VertexFormat.class);
private static final VarHandle MODE = field("mode", VertexFormat.Mode.class);
private static final VarHandle POINTER = field("vertexPointer", long.class);
private static final VarHandle VERTICES = field("vertices", int.class);
private static final VarHandle ELEMENTS = field("elementsToFill", int.class);
private static final VarHandle COUNT = field("iris$vertexCount", int.class);
private static final VarHandle OFFSETS = field("vertexOffsets", long[].class);
private static final VarHandle SKIP = field("skipEndVertexOnce", boolean.class);
private static final VarHandle EXTENDING = field("extending", boolean.class);
private static final VarHandle X = field("currentLocalPosX", int.class);
private static final VarHandle Y = field("currentLocalPosY", int.class);
private static final VarHandle Z = field("currentLocalPosZ", int.class);
private static final VarHandle BLOCK = field("currentBlock", int.class);
private static final VarHandle TYPE = field("currentRenderType", byte.class);
private static final VarHandle NORMAL = field("normal", Vector3f.class);
private static final MethodHandle FILL;
private static final Layout ENTITY = layout(IrisVertexFormats.ENTITY, true);
private static final Layout TERRAIN = layout(IrisVertexFormats.TERRAIN, false);
private static final int BASE_MASK =
VertexFormatElement.COLOR.mask()
| VertexFormatElement.UV0.mask()
| VertexFormatElement.UV1.mask()
| VertexFormatElement.UV2.mask()
| VertexFormatElement.NORMAL.mask();
static {
try {
FILL =
MethodHandles.privateLookupIn(BufferBuilder.class, MethodHandles.lookup())
.findVirtual(
BufferBuilder.class,
"fillExtendedData",
MethodType.methodType(void.class, int.class));
} catch (ReflectiveOperationException failure) {
throw new ExceptionInInitializerError(failure);
}
}
private static VarHandle field(String name, Class<?> type) {
try {
return MethodHandles.privateLookupIn(BufferBuilder.class, MethodHandles.lookup())
.findVarHandle(BufferBuilder.class, name, type);
} catch (ReflectiveOperationException failure) {
throw new ExceptionInInitializerError(failure);
}
}
private static Layout layout(VertexFormat format, boolean entity) {
return new Layout(
entity,
format.getVertexSize(),
format.getOffset(
entity
? IrisVertexFormats.ENTITY_ID_ELEMENT
: IrisVertexFormats.ENTITY_ELEMENT),
entity ? -1 : format.getOffset(IrisVertexFormats.MID_BLOCK_ELEMENT),
new QuadEnd.Format(
format.getOffset(IrisVertexFormats.MID_TEXTURE_ELEMENT),
format.getOffset(VertexFormatElement.NORMAL),
format.getOffset(IrisVertexFormats.TANGENT_ELEMENT)));
}
}
private final BufferBuilder target;
private final ByteBufferBuilder storage;
private final Layout layout;
private final long[] offsets;
private final boolean completion;
private final boolean omitMid;
private final boolean strip;
private final Vector3f face;
private BatchDraw(BufferBuilder target) {
this.target = target;
storage = (ByteBufferBuilder) State.BUFFER.get(target);
layout =
State.FORMAT.get(target) == IrisVertexFormats.ENTITY ? State.ENTITY : State.TERRAIN;
offsets = (long[]) State.OFFSETS.get(target);
completion = ((TangentTarget) target).skipsTangents();
omitMid =
completion
&& !layout.entity()
&& ShaderInputs.current().skipMidBlock()
&& storage.getClass() == ByteBufferBuilder.class;
strip = omitMid && ShaderInputs.current().skipMovingFields() && !ShadowRenderer.ACTIVE;
face = completion ? (Vector3f) State.NORMAL.get(target) : null;
}
public static BatchDraw create(BufferBuilder target, int count) {
if (count < 8) return null;
var format = State.FORMAT.get(target);
if (State.MODE.get(target) != VertexFormat.Mode.QUADS
|| !(boolean) State.EXTENDING.get(target)
|| format != IrisVertexFormats.ENTITY && format != IrisVertexFormats.TERRAIN)
return null;
return new BatchDraw(target);
}
private static void endPrevious(
BufferBuilder target,
ByteBufferBuilder storage,
long[] offsets,
long pointer,
QuadEnd.Format quad,
Vector3f face,
boolean completion,
boolean strip) {
State.ELEMENTS.set(target, 0);
if ((boolean) State.SKIP.get(target)) {
State.SKIP.set(target, false);
return;
}
int count = (int) State.COUNT.get(target);
long base = ((MojangBufferAccessor) storage).getPointer();
offsets[count] = pointer - base;
State.COUNT.set(target, count + 1);
if (count + 1 == 4) {
try {
if (completion) {
State.COUNT.set(target, 0);
if (strip)
QuadEnd.normals(base, offsets, face, quad, ImmediateState.isRenderingLevel);
else QuadEnd.write(base, offsets, face, quad, ImmediateState.isRenderingLevel);
} else {
State.FILL.invokeExact(target, 4);
}
} catch (Throwable failure) {
throw new AssertionError(failure);
}
}
}
static void metadata(
BufferBuilder target,
long pointer,
boolean entity,
int idOffset,
int midOffset,
float x,
float y,
float z,
boolean omitMid) {
long id = pointer + idOffset;
if (entity) {
var state = CapturedRenderingState.INSTANCE;
MemoryUtil.memPutShort(id, (short) state.getCurrentRenderedEntity());
MemoryUtil.memPutShort(id + 2, (short) state.getCurrentRenderedBlockEntity());
MemoryUtil.memPutShort(id + 4, (short) state.getCurrentRenderedItem());
} else {
MemoryUtil.memPutShort(id, (short) (int) State.BLOCK.get(target));
MemoryUtil.memPutShort(id + 2, (byte) State.TYPE.get(target));
if (!omitMid) {
MemoryUtil.memPutInt(
pointer + midOffset,
ExtendedDataHelper.computeMidBlock(
x,
y,
z,
(int) State.X.get(target),
(int) State.Y.get(target),
(int) State.Z.get(target)));
MemoryUtil.memPutByte(pointer + midOffset + 3, (byte) -1);
}
}
}
private boolean stages(TemplateMesh template, int outputCount, boolean discard) {
return strip
&& discard
&& BatchWork.supports(template, outputCount, (int) State.VERTICES.get(target));
}
public void emit(
TemplateMesh template,
Matrix4f matrix,
Matrix3f normalMat,
Vector4f position,
Vector3f normal,
boolean customOverlay,
int overlay,
boolean customLight,
int light,
int count) {
var builder = target;
var buffer = storage;
var slots = offsets;
var quad = layout.quad();
var face = this.face;
boolean completion = this.completion;
boolean entity = layout.entity();
int stride = layout.stride();
int idOffset = layout.idOffset();
int midOffset = layout.midOffset();
long pointer = 0;
int tail = 0;
boolean discard =
completion
&& ImmediateState.isRenderingLevel
&& buffer.getClass() == ByteBufferBuilder.class;
var selected =
strip && ((TangentTarget) builder).cullsBackfaces()
? FaceCull.plan(template, matrix, count, (int) State.VERTICES.get(builder))
: null;
int outputCount = selected == null ? count : selected.count() * 4;
boolean parallel = stages(template, outputCount, discard);
boolean tailStart = false;
for (int output = 0; output < outputCount; output++) {
if (parallel && output == 4 && ImmediateState.isRenderingLevel) {
endPrevious(builder, buffer, slots, pointer, quad, face, completion, strip);
int middle = outputCount - 8;
long block = buffer.reserve((middle + 1) * stride);
pointer = block + (long) middle * stride;
State.VERTICES.set(builder, (int) State.VERTICES.get(builder) + middle + 1);
State.POINTER.set(builder, pointer);
var work =
new BatchWork(
((PlaneTarget) template).snapshot(),
matrix,
selected,
outputCount,
stride,
quad,
customLight,
light);
((WorkTarget) buffer)
.workQueue()
.submit(block - ((MojangBufferAccessor) buffer).getPointer(), work);
output = outputCount - 4;
tailStart = true;
}
int i = selected == null ? output : selected.starts()[output / 4] + output % 4;
position.set(template.x(i), template.y(i), template.z(i), 1f).mul(matrix);
// The final quad can be completed after the render state changes.
boolean keep = !discard || i >= count - 4;
if (keep) {
int packedNormal = template.normal(i);
float nx = (byte) (packedNormal & 255) / 127f;
float ny = (byte) (packedNormal >>> 8 & 255) / 127f;
float nz = (byte) (packedNormal >>> 16 & 255) / 127f;
normal.set(nx, ny, nz).mul(normalMat);
}
int color = template.color(i);
float u = template.u(i);
float v = template.v(i);
int vertexOverlay = customOverlay ? overlay : template.overlay(i);
int vertexLight = template.light(i);
if (customLight) vertexLight = SuperByteBuffer.maxLight(vertexLight, light);
float x = position.x;
float y = position.y;
float z = position.z;
if (i == 0) {
builder.addVertex(x, y, z);
pointer = (long) State.POINTER.get(builder);
tail = (int) State.ELEMENTS.get(builder) & ~State.BASE_MASK;
} else if (tailStart) {
tailStart = false;
} else {
if (!(boolean) State.BUILDING.get(builder))
throw new IllegalStateException("Not building!");
endPrevious(builder, buffer, slots, pointer, quad, face, completion, strip);
State.VERTICES.set(builder, (int) State.VERTICES.get(builder) + 1);
pointer = buffer.reserve(stride);
State.POINTER.set(builder, pointer);
if (!strip)
metadata(builder, pointer, entity, idOffset, midOffset, x, y, z, omitMid);
}
if (keep)
BatchData.write(
pointer,
entity,
x,
y,
z,
color,
u,
v,
vertexOverlay,
vertexLight,
normal.x,
normal.y,
normal.z);
else
BatchData.attributes(
pointer, entity, x, y, z, color, u, v, vertexOverlay, vertexLight);
State.ELEMENTS.set(builder, tail);
}
}
}

View file

@ -0,0 +1,78 @@
package dev.quorate.client.render;
import net.createmod.catnip.render.SuperByteBuffer;
import net.createmod.catnip.render.TemplateMesh;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
public final class BatchWork {
private final TemplateMesh mesh;
private final Matrix4f matrix;
private final int[] starts;
private final int quads;
private final int stride;
private final QuadEnd.Format format;
private final boolean customLight;
private final int light;
BatchWork(
TemplateMesh mesh,
Matrix4f matrix,
PlaneBound.Selection selected,
int count,
int stride,
QuadEnd.Format format,
boolean customLight,
int light) {
this.mesh = mesh;
this.matrix = new Matrix4f(matrix);
starts = selected == null ? null : selected.starts();
quads = count / 4 - 2;
this.stride = stride;
this.format = format;
this.customLight = customLight;
this.light = light;
}
static boolean supports(TemplateMesh template, int count, int prefix) {
return template.getClass() == TemplateMesh.class
&& prefix % 4 == 0
&& count > 8
&& count % 4 == 0;
}
int bytes() {
return quads * 4 * stride;
}
void write(long target) {
var position = new Vector4f();
var face = new Vector3f();
long[] offsets = {0, stride, 2L * stride, 3L * stride};
for (int quad = 0; quad < quads; quad++) {
int source = starts == null ? (quad + 1) * 4 : starts[quad + 1];
long pointer = target + (long) quad * 4 * stride;
for (int vertex = 0; vertex < 4; vertex++) {
int i = source + vertex;
position.set(mesh.x(i), mesh.y(i), mesh.z(i), 1f).mul(matrix);
int vertexLight = mesh.light(i);
if (customLight) vertexLight = SuperByteBuffer.maxLight(vertexLight, light);
BatchData.attributes(
pointer + (long) vertex * stride,
false,
position.x,
position.y,
position.z,
mesh.color(i),
mesh.u(i),
mesh.v(i),
0,
vertexLight);
}
for (int vertex = 0; vertex < 4; vertex++) offsets[vertex] = (long) vertex * stride;
QuadEnd.normals(pointer, offsets, face, format, true);
}
}
}

View file

@ -0,0 +1,139 @@
package dev.quorate.client.render;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.createmod.catnip.render.ShadeSeparatingSuperByteBuffer;
import net.createmod.catnip.render.SuperByteBuffer;
import net.createmod.catnip.render.TemplateMesh;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public final class ColorDraw {
private static final VarHandle TEMPLATE = field("template", TemplateMesh.class);
private static final VarHandle R = field("r", float.class);
private static final VarHandle G = field("g", float.class);
private static final VarHandle B = field("b", float.class);
private static final VarHandle A = field("a", float.class);
private static final VarHandle SHIFT =
field("spriteShiftFunc", SuperByteBuffer.SpriteShiftFunc.class);
private static final VarHandle LEVEL = field("useLevelLight", boolean.class);
private static final VarHandle HAS_OVERLAY = field("hasCustomOverlay", boolean.class);
private static final VarHandle OVERLAY = field("overlay", int.class);
private static final VarHandle HAS_LIGHT = field("hasCustomLight", boolean.class);
private static final VarHandle LIGHT = field("packedLight", int.class);
private static final VarHandle MODEL = field("modelMat", Matrix4f.class);
private static final VarHandle NORMAL_MAT = field("normalMat", Matrix3f.class);
private static final VarHandle POSITION = field("pos", Vector4f.class);
private static final VarHandle NORMAL = field("normal", Vector3f.class);
private ColorDraw() {}
private static VarHandle field(String name, Class<?> type) {
try {
return MethodHandles.privateLookupIn(
ShadeSeparatingSuperByteBuffer.class, MethodHandles.lookup())
.findVarHandle(ShadeSeparatingSuperByteBuffer.class, name, type);
} catch (ReflectiveOperationException failure) {
throw new ExceptionInInitializerError(failure);
}
}
public static boolean render(
ShadeSeparatingSuperByteBuffer source, PoseStack pose, VertexConsumer consumer) {
if (source.getClass() != ShadeSeparatingSuperByteBuffer.class
|| consumer.getClass() != BufferBuilder.class
|| TEMPLATE.get(source).getClass() != TemplateMesh.class
|| (float) R.get(source) != 1f
|| (float) G.get(source) != 1f
|| (float) B.get(source) != 1f
|| (float) A.get(source) != 1f
|| SHIFT.get(source) != null
|| (boolean) LEVEL.get(source)) return false;
if (source.isEmpty()) return true;
var template = (TemplateMesh) TEMPLATE.get(source);
var transforms = source.getTransforms().last();
var matrix = ((Matrix4f) MODEL.get(source)).set(pose.last().pose()).mul(transforms.pose());
var normalMat =
((Matrix3f) NORMAL_MAT.get(source))
.set(pose.last().normal())
.mul(transforms.normal());
var position = (Vector4f) POSITION.get(source);
var normal = (Vector3f) NORMAL.get(source);
boolean customOverlay = (boolean) HAS_OVERLAY.get(source);
int overlay = (int) OVERLAY.get(source);
boolean customLight = (boolean) HAS_LIGHT.get(source);
int light = (int) LIGHT.get(source);
int count = template.vertexCount();
var batch = BatchDraw.create((BufferBuilder) consumer, count);
if (batch == null) {
emit(
template,
matrix,
normalMat,
position,
normal,
customOverlay,
overlay,
customLight,
light,
consumer,
count);
} else {
batch.emit(
template,
matrix,
normalMat,
position,
normal,
customOverlay,
overlay,
customLight,
light,
count);
}
source.reset();
return true;
}
private static void emit(
TemplateMesh template,
Matrix4f matrix,
Matrix3f normalMat,
Vector4f position,
Vector3f normal,
boolean customOverlay,
int overlay,
boolean customLight,
int light,
VertexConsumer consumer,
int count) {
for (int i = 0; i < count; i++) {
position.set(template.x(i), template.y(i), template.z(i), 1f).mul(matrix);
int packedNormal = template.normal(i);
float nx = (byte) (packedNormal & 255) / 127f;
float ny = (byte) (packedNormal >>> 8 & 255) / 127f;
float nz = (byte) (packedNormal >>> 16 & 255) / 127f;
normal.set(nx, ny, nz).mul(normalMat);
int color = template.color(i);
float u = template.u(i);
float v = template.v(i);
int vertexOverlay = customOverlay ? overlay : template.overlay(i);
int vertexLight = template.light(i);
if (customLight) vertexLight = SuperByteBuffer.maxLight(vertexLight, light);
consumer.addVertex(position.x, position.y, position.z)
.setColor(color & 255, color >>> 8 & 255, color >>> 16 & 255, color >>> 24)
.setUv(u, v)
.setOverlay(vertexOverlay)
.setLight(vertexLight)
.setNormal(normal.x, normal.y, normal.z);
}
}
}

View file

@ -0,0 +1,84 @@
package dev.quorate.client.render;
import net.createmod.catnip.render.TemplateMesh;
import net.irisshaders.iris.layer.OuterWrappedRenderType;
import net.irisshaders.iris.uniforms.CapturedRenderingState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.RenderStateShard;
import net.minecraft.client.renderer.RenderType;
import org.joml.Matrix4f;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public final class FaceCull {
private FaceCull() {}
private static final class Types {
private static final Class<?> COMPOSITE;
private static final VarHandle STATE;
private static final VarHandle CULL;
private static final VarHandle ENABLED;
static {
try {
COMPOSITE =
Class.forName(
"net.minecraft.client.renderer.RenderType$CompositeRenderType");
STATE =
MethodHandles.privateLookupIn(COMPOSITE, MethodHandles.lookup())
.findVarHandle(COMPOSITE, "state", RenderType.CompositeState.class);
CULL =
MethodHandles.privateLookupIn(
RenderType.CompositeState.class, MethodHandles.lookup())
.findVarHandle(
RenderType.CompositeState.class,
"cullState",
RenderStateShard.CullStateShard.class);
ENABLED =
MethodHandles.privateLookupIn(
RenderStateShard.BooleanStateShard.class,
MethodHandles.lookup())
.findVarHandle(
RenderStateShard.BooleanStateShard.class,
"enabled",
boolean.class);
} catch (ReflectiveOperationException failure) {
throw new ExceptionInInitializerError(failure);
}
}
}
public static boolean culls(RenderType type) {
while (type instanceof OuterWrappedRenderType outer) type = outer.unwrap();
return Types.COMPOSITE.isInstance(type)
&& (boolean) Types.ENABLED.get(Types.CULL.get(Types.STATE.get(type)));
}
public static PlaneBound.Selection plan(
TemplateMesh template, Matrix4f matrix, int count, int prefix) {
if (!ShaderInputs.current().cull()
|| count < 1024
|| count % 4 != 0
|| prefix % 4 != 0
|| count != template.vertexCount()
|| (matrix.properties() & Matrix4f.PROPERTY_AFFINE) == 0) return null;
var model = ((PlaneTarget) template).plane();
var captured = CapturedRenderingState.INSTANCE;
var window = Minecraft.getInstance().getWindow();
var view =
FastBound.view(
model,
matrix,
captured.getGbufferModelView(),
captured.getGbufferProjection(),
window.getWidth(),
window.getHeight(),
ShaderInputs.current().pixelUnit(),
ShaderInputs.TAA_JITTER);
if (!view.active()) return null;
var selected = PlaneBound.select(model, view);
return selected.count() * 4 == count ? null : selected;
}
}

View file

@ -0,0 +1,31 @@
package dev.quorate.client.render;
import net.irisshaders.iris.vertices.NormalHelper;
import org.joml.Vector3f;
import org.lwjgl.system.MemoryUtil;
public final class FaceRead {
private FaceRead() {}
public static void compute(Vector3f output, long pointer, long[] offsets) {
long a = pointer + offsets[0];
long b = pointer + offsets[1];
long c = pointer + offsets[2];
long d = pointer + offsets[3];
NormalHelper.computeFaceNormalManual(
output,
MemoryUtil.memGetFloat(a),
MemoryUtil.memGetFloat(a + 4),
MemoryUtil.memGetFloat(a + 8),
MemoryUtil.memGetFloat(b),
MemoryUtil.memGetFloat(b + 4),
MemoryUtil.memGetFloat(b + 8),
MemoryUtil.memGetFloat(c),
MemoryUtil.memGetFloat(c + 4),
MemoryUtil.memGetFloat(c + 8),
MemoryUtil.memGetFloat(d),
MemoryUtil.memGetFloat(d + 4),
MemoryUtil.memGetFloat(d + 8));
}
}

View file

@ -0,0 +1,216 @@
package dev.quorate.client.render;
import org.joml.Matrix4fc;
public final class FastBound {
// Four-term double FMAs need four rounding steps; cofactors need five per signed triple
// product.
private static final double DOT = gamma(4, 0x1p-53);
private static final double MINOR = gamma(5, 0x1p-53);
// CPU float products use seven steps; GLSL's one-ULP allowance doubles that for each of two
// transforms.
private static final double CPU = gamma(7, 0x1p-24);
private static final double GPU = gamma(28, 0x1p-24);
// Shader jitter allows five division steps and two each for multiplication and addition.
private static final double JITTER = gamma(9, 0x1p-24);
private record Matrix(double[] values, double[] errors) {}
private FastBound() {}
private static double add(double a, double b) {
return Math.nextUp(a + b);
}
private static double mul(double a, double b) {
return Math.nextUp(a * b);
}
private static double gamma(int count, double unit) {
double value = count * unit;
return Math.nextUp(value / (1 - value));
}
private static double[] values(Matrix4fc source) {
var result = new double[16];
for (int row = 0; row < 4; row++)
for (int column = 0; column < 4; column++)
result[row * 4 + column] = source.get(column, row);
return result;
}
private static Matrix product(Matrix a, double[] b) {
var values = new double[16];
var errors = new double[16];
for (int row = 0; row < 4; row++)
for (int col = 0; col < 4; col++) {
int at = row * 4;
values[at + col] =
Math.fma(
a.values()[at],
b[col],
Math.fma(
a.values()[at + 1],
b[4 + col],
Math.fma(
a.values()[at + 2],
b[8 + col],
a.values()[at + 3] * b[12 + col])));
double norm = 0;
double carried = 0;
for (int k = 0; k < 4; k++) {
norm = add(norm, mul(Math.abs(a.values()[at + k]), Math.abs(b[k * 4 + col])));
carried = add(carried, mul(a.errors()[at + k], Math.abs(b[k * 4 + col])));
}
errors[at + col] = add(carried, mul(DOT, norm));
}
return new Matrix(values, errors);
}
private static double[] absolute(double[] matrix, double[] vector) {
var result = new double[4];
for (int row = 0; row < 4; row++) {
double sum = 0;
for (int col = 0; col < 4; col++)
sum = add(sum, mul(Math.abs(matrix[row * 4 + col]), vector[col]));
result[row] = sum;
}
return result;
}
private static double minor(double[] m, int i, int j, int k) {
return m[i] * (m[4 + j] * m[12 + k] - m[4 + k] * m[12 + j])
- m[j] * (m[4 + i] * m[12 + k] - m[4 + k] * m[12 + i])
+ m[k] * (m[4 + i] * m[12 + j] - m[4 + j] * m[12 + i]);
}
private static double basisError(Matrix matrix) {
double maximum = 0;
double error = 0;
for (int row : new int[] {0, 1, 3})
for (int col = 0; col < 4; col++) {
maximum = Math.max(maximum, Math.abs(matrix.values()[row * 4 + col]));
error = Math.max(error, matrix.errors()[row * 4 + col]);
}
double square = mul(maximum, maximum);
double errorSquare = mul(error, error);
double linear = mul(3, mul(error, square));
double quadratic = mul(3, mul(errorSquare, maximum));
double cubic = mul(errorSquare, error);
double changed = mul(6, add(add(linear, quadratic), cubic));
double arithmetic = mul(MINOR, mul(6, mul(square, maximum)));
return add(changed, arithmetic);
}
public static PlaneBound.View view(
PlaneBound.Model model,
Matrix4fc local,
Matrix4fc camera,
Matrix4fc projection,
int width,
int height,
double pixelUnit,
double jitterMaximum) {
var p = values(projection);
var v = values(camera);
var m = values(local);
var matrix = product(product(new Matrix(p, new double[16]), v), m);
var a = matrix.values();
var ae = matrix.errors();
var coefficients =
new double[] {
minor(a, 1, 2, 3), -minor(a, 0, 2, 3), minor(a, 0, 1, 3), -minor(a, 0, 1, 2)
};
var basis =
new PlaneCull.Basis(
coefficients[0], coefficients[1], coefficients[2], coefficients[3]);
double coefficientError = basisError(matrix);
double scoreError = 0;
double scoreNorm = 0;
for (int i = 0; i < 4; i++) {
double magnitude = Math.abs(coefficients[i]);
scoreError =
add(scoreError, mul(model.planeError()[i], add(magnitude, coefficientError)));
scoreError = add(scoreError, mul(model.planeAbs()[i], coefficientError));
scoreNorm = add(scoreNorm, mul(model.planeAbs()[i], magnitude));
}
scoreError = add(scoreError, mul(DOT, scoreNorm));
var cpuNorm = absolute(m, model.magnitude());
var cpuError = new double[4];
var cpuTotal = new double[4];
for (int row = 0; row < 4; row++) {
cpuError[row] = mul(CPU, cpuNorm[row]);
cpuTotal[row] = add(cpuNorm[row], cpuError[row]);
}
var carried = absolute(p, absolute(v, cpuError));
var gpuNorm = absolute(p, absolute(v, cpuTotal));
var error = new double[4];
var maximum = new double[4];
var gain = new double[4];
double minimumW = 0;
for (int row = 0; row < 4; row++) {
error[row] = add(carried[row], mul(GPU, gpuNorm[row]));
double low = 0;
double high = 0;
double matrixError = 0;
for (int column = 0; column < 4; column++) {
int at = row * 4 + column;
double first = a[at] * model.low()[column];
double last = a[at] * model.high()[column];
low = Math.nextDown(low + Math.nextDown(Math.min(first, last)));
high = Math.nextUp(high + Math.nextUp(Math.max(first, last)));
matrixError = add(matrixError, mul(ae[at], model.magnitude()[column]));
if (column != 3 || model.low()[3] != model.high()[3])
gain[row] = Math.max(gain[row], add(Math.abs(a[at]), ae[at]));
}
low = Math.nextDown(low - matrixError);
high = Math.nextUp(high + matrixError);
maximum[row] = Math.max(Math.abs(low), Math.abs(high));
if (row == 3) minimumW = low;
}
for (int axis = 0; axis < 2; axis++) {
double size = axis == 0 ? width : height;
if (jitterMaximum != 0) {
double jitter = Math.nextUp(jitterMaximum / size);
double amount = mul(maximum[3], jitter);
error[axis] =
add(
add(error[axis], mul(error[3], jitter)),
mul(add(maximum[axis], amount), JITTER));
maximum[axis] = add(maximum[axis], amount);
gain[axis] = add(gain[axis], mul(gain[3], jitter));
}
double raster = Math.nextUp(mul(2, pixelUnit) / size);
error[axis] = add(error[axis], mul(add(maximum[3], error[3]), raster));
}
double x = maximum[0];
double y = maximum[1];
double w = maximum[3];
double ex = error[0];
double ey = error[1];
double ew = error[3];
double rx = gain[0];
double ry = gain[1];
double rw = gain[3];
double linear =
mul(
3,
add(
add(
mul(ex, add(mul(y, rw), mul(w, ry))),
mul(ey, add(mul(x, rw), mul(w, rx)))),
mul(ew, add(mul(x, ry), mul(y, rx)))));
double constant =
mul(
6,
add(
add(mul(mul(ex, ey), w), mul(mul(ex, ew), y)),
add(mul(mul(ey, ew), x), mul(mul(ex, ey), ew))));
constant = add(constant, scoreError);
return new PlaneBound.View(
basis,
linear,
constant,
minimumW > error[3] && Double.isFinite(linear) && Double.isFinite(constant));
}
}

View file

@ -0,0 +1,40 @@
package dev.quorate.client.render;
import net.createmod.catnip.render.TemplateMesh;
import java.util.Arrays;
public final class MeshPlan {
private final int[] data;
private final PlaneBound.Model model;
private final TemplateMesh snapshot;
public MeshPlan(int[] source) {
data = source.clone();
snapshot = new TemplateMesh(data);
int count = data.length / 9;
var positions = new float[count * 4];
var indices = new int[count / 4 * 6];
for (int i = 0; i < count; i++) {
for (int axis = 0; axis < 3; axis++)
positions[i * 4 + axis] = Float.intBitsToFloat(data[i * 9 + axis]);
positions[i * 4 + 3] = 1;
}
int[] order = {0, 1, 2, 2, 3, 0};
for (int quad = 0; quad < count / 4; quad++)
for (int i = 0; i < 6; i++) indices[quad * 6 + i] = quad * 4 + order[i];
model = PlaneBound.build(positions, indices);
}
public boolean matches(int[] source) {
return Arrays.equals(data, source);
}
public TemplateMesh snapshot() {
return snapshot;
}
public PlaneBound.Model model() {
return model;
}
}

View file

@ -0,0 +1,132 @@
package dev.quorate.client.render;
import java.util.Arrays;
public final class PlaneBound {
public record Model(
PlaneCull.Planes planes,
double[] edges,
double[] low,
double[] high,
double[] magnitude,
double[] planeAbs,
double[] planeError) {}
public record View(PlaneCull.Basis basis, double linear, double constant, boolean active) {
public double limit(Model model, int face) {
return Math.nextUp(Math.fma(linear, model.edges()[face], constant));
}
}
public record Selection(int[] starts, int count) {}
record Span(double low, double high) {
static Span of(double value) {
return new Span(value, value);
}
Span add(Span other) {
return new Span(Math.nextDown(low + other.low), Math.nextUp(high + other.high));
}
Span sub(Span other) {
return new Span(Math.nextDown(low - other.high), Math.nextUp(high - other.low));
}
Span mul(Span other) {
double a = low * other.low;
double b = low * other.high;
double c = high * other.low;
double d = high * other.high;
return new Span(
Math.nextDown(Math.min(Math.min(a, b), Math.min(c, d))),
Math.nextUp(Math.max(Math.max(a, b), Math.max(c, d))));
}
Span scale(double value) {
return mul(of(value));
}
double absolute() {
return Math.max(Math.abs(low), Math.abs(high));
}
double error(double value) {
return Math.nextUp(Math.max(Math.abs(low - value), Math.abs(high - value)));
}
}
private PlaneBound() {}
private static Span minor(Span[][] data, int i, int j, int k) {
return data[0][i]
.mul(data[1][j].mul(data[2][k]).sub(data[1][k].mul(data[2][j])))
.sub(data[0][j].mul(data[1][i].mul(data[2][k]).sub(data[1][k].mul(data[2][i]))))
.add(data[0][k].mul(data[1][i].mul(data[2][j]).sub(data[1][j].mul(data[2][i]))));
}
private static Span[] cofactors(Span[][] rows) {
return new Span[] {
minor(rows, 1, 2, 3),
minor(rows, 0, 2, 3).scale(-1),
minor(rows, 0, 1, 3),
minor(rows, 0, 1, 2).scale(-1)
};
}
public static Model build(float[] vertices, int[] indices) {
var planes = PlaneCull.build(vertices, indices);
var low = new double[4];
var high = new double[4];
Arrays.fill(low, Double.POSITIVE_INFINITY);
Arrays.fill(high, Double.NEGATIVE_INFINITY);
for (int at = 0; at < vertices.length; at += 4)
for (int axis = 0; axis < 4; axis++) {
low[axis] = Math.min(low[axis], vertices[at + axis]);
high[axis] = Math.max(high[axis], vertices[at + axis]);
}
var magnitude = new double[4];
for (int axis = 0; axis < 4; axis++)
magnitude[axis] = Math.max(Math.abs(low[axis]), Math.abs(high[axis]));
var edges = new double[indices.length / 3];
var maximum = new double[4];
var error = new double[4];
double[][] coefficients = {planes.x(), planes.y(), planes.z(), planes.w()};
for (int face = 0; face < edges.length; face++) {
var rows = new Span[3][4];
for (int p = 0; p < 3; p++)
for (int axis = 0; axis < 4; axis++)
rows[p][axis] = Span.of(vertices[indices[face * 3 + p] * 4 + axis]);
var bounds = cofactors(rows);
for (int axis = 0; axis < 4; axis++) {
maximum[axis] = Math.max(maximum[axis], Math.abs(coefficients[axis][face]));
error[axis] = Math.max(error[axis], bounds[axis].error(coefficients[axis][face]));
}
for (int p = 0; p < 3; p++) {
var sum = Span.of(0);
for (int axis = 0; axis < 4; axis++)
sum = sum.add(Span.of(rows[p][axis].sub(rows[(p + 1) % 3][axis]).absolute()));
edges[face] = Math.max(edges[face], sum.high());
}
}
return new Model(planes, edges, low, high, magnitude, maximum, error);
}
public static boolean back(Model model, int face, View view) {
return view.active()
&& (model.planes().repeated()[face]
|| PlaneCull.score(model.planes(), face, view.basis())
< -view.limit(model, face));
}
public static Selection select(Model model, View view) {
var starts = new int[model.edges().length / 2];
int count = 0;
for (int face = 0; face < model.edges().length; face += 2)
if (face == 0
|| face + 2 == model.edges().length
|| !back(model, face, view)
|| !back(model, face + 1, view)) starts[count++] = face * 2;
return new Selection(starts, count);
}
}

Some files were not shown because too many files have changed in this diff Show more