Skip to content

Commit

Permalink
Efficient CPU Translucency Sorting with BSP Trees and Heuristics (Caf…
Browse files Browse the repository at this point in the history
…feineMC#2016)

This PR implements efficient CPU translucency sorting. See CaffeineMC#2016 for a useful overview of the concepts.
Closes CaffeineMC#38
  • Loading branch information
douira authored Feb 15, 2024
1 parent 44e0489 commit 3378e3d
Show file tree
Hide file tree
Showing 81 changed files with 6,223 additions and 336 deletions.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ with some minor changes, as described below.
- If you are using more than three levels of indentation, you should likely consider restructuring your code.
- Branches which are only exceptionally or very rarely taken should remain concise. When this is not possible,
prefer breaking out to a new method (where it makes sense) as this helps the compiler better optimize the code.
- Use `this` to qualify member and field access, as it avoids some ambiguity in certain contexts.
- Use `this` to qualify method and field access, as it avoids some ambiguity in certain contexts.

We also provide these code styles as [EditorConfig](https://editorconfig.org/) files, which most Java IDEs will
automatically detect and make use of.
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
minecraft(group = "com.mojang", name = "minecraft", version = Constants.MINECRAFT_VERSION)
mappings(loom.officialMojangMappings())
modImplementation(group = "net.fabricmc", name = "fabric-loader", version = Constants.FABRIC_LOADER_VERSION)
include(implementation(group = "com.lodborg", name = "interval-tree", version = "1.0.0"))

fun addEmbeddedFabricModule(name: String) {
val module = fabricApi.module(name, Constants.FABRIC_API_VERSION)
Expand Down
40 changes: 38 additions & 2 deletions src/api/java/net/caffeinemc/mods/sodium/api/util/NormI8.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package net.caffeinemc.mods.sodium.api.util;

import net.minecraft.util.Mth;
import org.joml.Vector3f;
import org.joml.Vector3fc;

/**
* Provides some utilities for working with packed normal vectors. Each normal component provides 8 bits of
Expand All @@ -27,7 +27,7 @@ public class NormI8 {
*/
private static final float NORM = 1.0f / COMPONENT_RANGE;

public static int pack(Vector3f normal) {
public static int pack(Vector3fc normal) {
return pack(normal.x(), normal.y(), normal.z());
}

Expand Down Expand Up @@ -78,4 +78,40 @@ public static float unpackY(int norm) {
public static float unpackZ(int norm) {
return ((byte) ((norm >> Z_COMPONENT_OFFSET) & 0xFF)) * NORM;
}

/**
* Flips the direction of a packed normal by negating each component. (multiplication by -1)
* @param norm The packed normal
*/
public static int flipPacked(int norm) {
int normX = (((norm >> X_COMPONENT_OFFSET) & 0xFF) * -1) & 0xFF;
int normY = (((norm >> Y_COMPONENT_OFFSET) & 0xFF) * -1) & 0xFF;
int normZ = (((norm >> Z_COMPONENT_OFFSET) & 0xFF) * -1) & 0xFF;

return (normZ << Z_COMPONENT_OFFSET) | (normY << Y_COMPONENT_OFFSET) | (normX << X_COMPONENT_OFFSET);
}

/**
* Returns true if the two packed normals are opposite directions.
*
* TODO: this could possibly be faster by using normA == (~normB + 0x010101) but
* that has to special case when a component is zero since that wouldn't
* overflow correctly back to zero. (~0+1 == 0 but not if it's somewhere inside
* th int)
*
* @param normA The first packed normal
* @param normB The second packed normal
*/
public static boolean isOpposite(int normA, int normB) {
// use byte to automatically sign extend the components
byte normAX = (byte) (normA >> X_COMPONENT_OFFSET);
byte normAY = (byte) (normA >> Y_COMPONENT_OFFSET);
byte normAZ = (byte) (normA >> Z_COMPONENT_OFFSET);

byte normBX = (byte) (normB >> X_COMPONENT_OFFSET);
byte normBY = (byte) (normB >> Y_COMPONENT_OFFSET);
byte normBZ = (byte) (normB >> Z_COMPONENT_OFFSET);

return normAX == -normBX && normAY == -normBY && normAZ == -normBZ;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ public static OptionPage performance() {
.build())
.build());

groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(boolean.class, sodiumOpts)
.setName(Component.translatable("sodium.options.sort_behavior.name"))
.setTooltip(Component.translatable("sodium.options.sort_behavior.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> opts.performance.sortingEnabled = value, opts -> opts.performance.sortingEnabled)
.setImpact(OptionImpact.LOW)
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
.build())
.build());

return new OptionPage(Component.translatable("sodium.options.pages.performance"), ImmutableList.copyOf(groups));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.google.gson.annotations.SerializedName;
import net.caffeinemc.mods.sodium.client.gui.options.TextProvider;
import net.caffeinemc.mods.sodium.client.util.FileUtil;
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.GraphicsStatus;
import net.minecraft.network.chat.Component;
Expand Down Expand Up @@ -44,6 +45,12 @@ public static class PerformanceSettings {
public boolean useFogOcclusion = true;
public boolean useBlockFaceCulling = true;
public boolean useNoErrorGLContext = true;

public boolean sortingEnabled = true;

public SortBehavior getSortBehavior() {
return this.sortingEnabled ? SortBehavior.DYNAMIC_DEFER_NEARBY_ONE_FRAME : SortBehavior.OFF;
}
}

public static class AdvancedSettings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

public interface BakedQuadView extends ModelQuadView {
ModelQuadFacing getNormalFace();


int getNormal();

boolean hasShade();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.caffeinemc.mods.sodium.client.model.quad;

import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadFlags;
import net.caffeinemc.mods.sodium.api.util.NormI8;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.core.Direction;

Expand Down Expand Up @@ -61,4 +62,43 @@ public interface ModelQuadView {
default boolean hasColor() {
return this.getColorIndex() != -1;
}

default int calculateNormal() {
final float x0 = getX(0);
final float y0 = getY(0);
final float z0 = getZ(0);

final float x1 = getX(1);
final float y1 = getY(1);
final float z1 = getZ(1);

final float x2 = getX(2);
final float y2 = getY(2);
final float z2 = getZ(2);

final float x3 = getX(3);
final float y3 = getY(3);
final float z3 = getZ(3);

final float dx0 = x2 - x0;
final float dy0 = y2 - y0;
final float dz0 = z2 - z0;
final float dx1 = x3 - x1;
final float dy1 = y3 - y1;
final float dz1 = z3 - z1;

float normX = dy0 * dz1 - dz0 * dy1;
float normY = dz0 * dx1 - dx0 * dz1;
float normZ = dx0 * dy1 - dy0 * dx1;

// normalize by length for the packed normal
float length = (float) Math.sqrt(normX * normX + normY * normY + normZ * normZ);
if (length != 0.0 && length != 1.0) {
normX /= length;
normY /= length;
normZ /= length;
}

return NormI8.pack(normX, normY, normZ);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package net.caffeinemc.mods.sodium.client.model.quad.properties;

import net.caffeinemc.mods.sodium.client.util.DirectionUtil;
import net.caffeinemc.mods.sodium.api.util.NormI8;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import org.joml.Math;
import org.joml.Vector3f;
import org.joml.Vector3fc;

import java.util.Arrays;

public enum ModelQuadFacing {
POS_X,
Expand All @@ -14,10 +22,28 @@ public enum ModelQuadFacing {
public static final ModelQuadFacing[] VALUES = ModelQuadFacing.values();

public static final int COUNT = VALUES.length;
public static final int DIRECTIONS = VALUES.length - 1;

public static final int NONE = 0;
public static final int ALL = (1 << COUNT) - 1;

public static final Vector3fc[] ALIGNED_NORMALS = new Vector3fc[] {
new Vector3f(1, 0, 0),
new Vector3f(0, 1, 0),
new Vector3f(0, 0, 1),
new Vector3f(-1, 0, 0),
new Vector3f(0, -1, 0),
new Vector3f(0, 0, -1),
};

public static final int[] PACKED_ALIGNED_NORMALS = Arrays.stream(ALIGNED_NORMALS)
.mapToInt(NormI8::pack)
.toArray();

public static final int OPPOSING_X = 1 << ModelQuadFacing.POS_X.ordinal() | 1 << ModelQuadFacing.NEG_X.ordinal();
public static final int OPPOSING_Y = 1 << ModelQuadFacing.POS_Y.ordinal() | 1 << ModelQuadFacing.NEG_Y.ordinal();
public static final int OPPOSING_Z = 1 << ModelQuadFacing.POS_Z.ordinal() | 1 << ModelQuadFacing.NEG_Z.ordinal();

public static ModelQuadFacing fromDirection(Direction dir) {
return switch (dir) {
case DOWN -> NEG_Y;
Expand All @@ -40,4 +66,58 @@ public ModelQuadFacing getOpposite() {
default -> UNASSIGNED;
};
}

public int getSign() {
return switch (this) {
case POS_Y, POS_X, POS_Z -> 1;
case NEG_Y, NEG_X, NEG_Z -> -1;
default -> 0;
};
}

public int getAxis() {
return switch (this) {
case POS_X, NEG_X -> 0;
case POS_Y, NEG_Y -> 1;
case POS_Z, NEG_Z -> 2;
default -> -1;
};
}

public boolean isAligned() {
return this != UNASSIGNED;
}

public Vector3fc getAlignedNormal() {
if (!this.isAligned()) {
throw new IllegalStateException("Cannot get aligned normal for unassigned facing");
}
return ALIGNED_NORMALS[this.ordinal()];
}

public int getPackedAlignedNormal() {
if (!this.isAligned()) {
throw new IllegalStateException("Cannot get packed aligned normal for unassigned facing");
}
return PACKED_ALIGNED_NORMALS[this.ordinal()];
}

public static ModelQuadFacing fromNormal(float x, float y, float z) {
if (!(Math.isFinite(x) && Math.isFinite(y) && Math.isFinite(z))) {
return ModelQuadFacing.UNASSIGNED;
}

for (Direction face : DirectionUtil.ALL_DIRECTIONS) {
var step = face.step();
if (Mth.equal(Math.fma(x, step.x(), Math.fma(y, step.y(), z * step.z())), 1.0f)) {
return ModelQuadFacing.fromDirection(face);
}
}

return ModelQuadFacing.UNASSIGNED;
}

public static ModelQuadFacing fromPackedNormal(int normal) {
return fromNormal(NormI8.unpackX(normal), NormI8.unpackY(normal), NormI8.unpackZ(normal));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import net.caffeinemc.mods.sodium.client.render.chunk.map.ChunkTracker;
import net.caffeinemc.mods.sodium.client.render.chunk.map.ChunkTrackerHolder;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses;
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.CameraMovement;
import net.caffeinemc.mods.sodium.client.render.viewport.Viewport;
import net.caffeinemc.mods.sodium.client.util.NativeBuffer;
import net.caffeinemc.mods.sodium.client.world.LevelRendererExtension;
Expand All @@ -42,6 +43,8 @@
import java.util.Iterator;
import java.util.SortedSet;

import org.joml.Vector3d;

/**
* Provides an extension to vanilla's {@link LevelRenderer}.
*/
Expand All @@ -51,7 +54,7 @@ public class SodiumWorldRenderer {
private ClientLevel level;
private int renderDistance;

private double lastCameraX, lastCameraY, lastCameraZ;
private Vector3d lastCameraPos;
private double lastCameraPitch, lastCameraYaw;
private float lastFogDistance;

Expand Down Expand Up @@ -174,44 +177,50 @@ public void setupTerrain(Camera camera,
throw new IllegalStateException("Client instance has no active player entity");
}

Vec3 pos = camera.getPosition();
Vec3 posRaw = camera.getPosition();
Vector3d pos = new Vector3d(posRaw.x(), posRaw.y(), posRaw.z());
float pitch = camera.getXRot();
float yaw = camera.getYRot();
float fogDistance = RenderSystem.getShaderFogEnd();

boolean dirty = pos.x != this.lastCameraX || pos.y != this.lastCameraY || pos.z != this.lastCameraZ ||
pitch != this.lastCameraPitch || yaw != this.lastCameraYaw || fogDistance != this.lastFogDistance;

if (dirty) {
this.renderSectionManager.markGraphDirty();
if (this.lastCameraPos == null) {
this.lastCameraPos = new Vector3d(pos);
}
boolean cameraLocationChanged = !pos.equals(this.lastCameraPos);
boolean cameraAngleChanged = pitch != this.lastCameraPitch || yaw != this.lastCameraYaw || fogDistance != this.lastFogDistance;

this.lastCameraX = pos.x;
this.lastCameraY = pos.y;
this.lastCameraZ = pos.z;
this.lastCameraPitch = pitch;
this.lastCameraYaw = yaw;
this.lastFogDistance = fogDistance;

profiler.popPush("chunk_update");
if (cameraLocationChanged || cameraAngleChanged) {
this.renderSectionManager.markGraphDirty();
}

this.renderSectionManager.updateChunks(updateChunksImmediately);
this.lastFogDistance = fogDistance;

profiler.popPush("chunk_upload");
this.renderSectionManager.updateCameraState(pos, camera);

this.renderSectionManager.uploadChunks();
if (cameraLocationChanged) {
profiler.popPush("translucent_triggering");

this.renderSectionManager.processGFNIMovement(new CameraMovement(this.lastCameraPos, pos));
this.lastCameraPos = new Vector3d(pos);
}

if (this.renderSectionManager.needsUpdate()) {
profiler.popPush("chunk_render_lists");

this.renderSectionManager.update(camera, viewport, frame, spectator);
}

if (updateChunksImmediately) {
profiler.popPush("chunk_upload_immediately");
profiler.popPush("chunk_update");

this.renderSectionManager.uploadChunks();
}
this.renderSectionManager.cleanupAndFlip();
this.renderSectionManager.updateChunks(updateChunksImmediately);

profiler.popPush("chunk_upload");

this.renderSectionManager.uploadChunks();

profiler.popPush("chunk_render_tick");

Expand Down
Loading

0 comments on commit 3378e3d

Please sign in to comment.