Skip to content

Commit

Permalink
Separate light and face normals to fix some problems
Browse files Browse the repository at this point in the history
Face normals are the "correct" normals, and are always calculated
using the inverse transpose of the position matrix.

Light normals are what the game typically deals with, and they
are calculated using the normal matrix.

Any code wanting to know the true surface normal of a primitive
should use the face normal, not the light normal.
  • Loading branch information
jellysquid3 committed Dec 26, 2023
1 parent ebd10c8 commit bc69ca9
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 53 deletions.
32 changes: 26 additions & 6 deletions src/api/java/net/caffeinemc/mods/sodium/api/math/MatrixHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,32 @@ public static int transformNormal(Matrix3f matrix, Direction direction) {
*/
public static void transformNormal(Vector3f result, Matrix3f matrix, Direction direction) {
switch (direction) {
case DOWN -> result.set(-matrix.m10, -matrix.m11, -matrix.m12);
case UP -> result.set( matrix.m10, matrix.m11, matrix.m12);
case NORTH -> result.set(-matrix.m20, -matrix.m21, -matrix.m22);
case SOUTH -> result.set( matrix.m20, matrix.m21, matrix.m22);
case WEST -> result.set(-matrix.m00, -matrix.m01, -matrix.m02);
case EAST -> result.set( matrix.m00, matrix.m01, matrix.m02);
case DOWN -> result.set(-matrix.m10(), -matrix.m11(), -matrix.m12());
case UP -> result.set( matrix.m10(), matrix.m11(), matrix.m12());
case NORTH -> result.set(-matrix.m20(), -matrix.m21(), -matrix.m22());
case SOUTH -> result.set( matrix.m20(), matrix.m21(), matrix.m22());
case WEST -> result.set(-matrix.m00(), -matrix.m01(), -matrix.m02());
case EAST -> result.set( matrix.m00(), matrix.m01(), matrix.m02());
};
}

/**
* Writes the transformed normal vector for a given unit vector (direction) into {@param result}. This is
* significantly faster than transforming the vector directly (i.e. with {@link Matrix3f#transform(Vector3f)}),
* as it can simply extract the values from the provided matrix (rather than transforming the vertices.)
*
* @param matrix The transformation matrix
* @param direction The unit vector (direction) to use
* @return A transformed normal in packed format
*/
public static void transformNormal(Vector3f result, Matrix4f matrix, Direction direction) {
switch (direction) {
case DOWN -> result.set(-matrix.m10(), -matrix.m11(), -matrix.m12());
case UP -> result.set( matrix.m10(), matrix.m11(), matrix.m12());
case NORTH -> result.set(-matrix.m20(), -matrix.m21(), -matrix.m22());
case SOUTH -> result.set( matrix.m20(), matrix.m21(), matrix.m22());
case WEST -> result.set(-matrix.m00(), -matrix.m01(), -matrix.m02());
case EAST -> result.set( matrix.m00(), matrix.m01(), matrix.m02());
};
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package me.jellysquid.mods.sodium.client.render.immediate.model;

import net.caffeinemc.mods.sodium.api.math.MatrixHelper;
import net.caffeinemc.mods.sodium.api.util.NormI8;
import net.caffeinemc.mods.sodium.api.vertex.buffer.VertexBufferWriter;
import net.caffeinemc.mods.sodium.api.vertex.format.common.ModelVertex;
import net.minecraft.client.model.ModelPart;
Expand Down Expand Up @@ -57,16 +56,20 @@ public class EntityRenderer {
private static final Vector2f[][] VERTEX_TEXTURES = new Vector2f[NUM_CUBE_FACES][NUM_FACE_VERTICES];
private static final Vector2f[][] VERTEX_TEXTURES_MIRRORED = new Vector2f[NUM_CUBE_FACES][NUM_FACE_VERTICES];

private static final Vector3f[] CUBE_NORMALS = new Vector3f[NUM_CUBE_FACES];
private static final Vector3f[] CUBE_NORMALS_MIRRORED = new Vector3f[NUM_CUBE_FACES];
private static final int[] LIGHT_NORMALS = new int[NUM_CUBE_FACES];
private static final int[] LIGHT_NORMALS_MIRRORED = new int[NUM_CUBE_FACES];

private static final Vector3f[] FACE_NORMALS = new Vector3f[NUM_CUBE_FACES];

private static final Matrix4f FACE_NORMAL_MATRIX = new Matrix4f();

static {
for (int cornerIndex = 0; cornerIndex < NUM_CUBE_VERTICES; cornerIndex++) {
CUBE_CORNERS[cornerIndex] = new Vector3f();
}

for (int quadIndex = 0; quadIndex < NUM_CUBE_FACES; quadIndex++) {
CUBE_NORMALS[quadIndex] = new Vector3f();
FACE_NORMALS[quadIndex] = new Vector3f();

for (int vertexIndex = 0; vertexIndex < NUM_FACE_VERTICES; vertexIndex++) {
VERTEX_TEXTURES[quadIndex][vertexIndex] = new Vector2f();
Expand All @@ -81,13 +84,14 @@ public class EntityRenderer {
}
}

// When mirroring is used, the normals for EAST and WEST are swapped.
CUBE_NORMALS_MIRRORED[FACE_NEG_Y] = CUBE_NORMALS[FACE_NEG_Y];
CUBE_NORMALS_MIRRORED[FACE_POS_Y] = CUBE_NORMALS[FACE_POS_Y];
CUBE_NORMALS_MIRRORED[FACE_NEG_Z] = CUBE_NORMALS[FACE_NEG_Z];
CUBE_NORMALS_MIRRORED[FACE_POS_Z] = CUBE_NORMALS[FACE_POS_Z];
CUBE_NORMALS_MIRRORED[FACE_POS_X] = CUBE_NORMALS[FACE_NEG_X]; // mirrored
CUBE_NORMALS_MIRRORED[FACE_NEG_X] = CUBE_NORMALS[FACE_POS_X]; // mirrored
// This is a land mine. Do not touch this code.
// When mirroring is used, the normals for EAST and WEST are swapped. Why only these normals? Who knows.
LIGHT_NORMALS_MIRRORED[FACE_NEG_Y] = LIGHT_NORMALS[FACE_NEG_Y];
LIGHT_NORMALS_MIRRORED[FACE_POS_Y] = LIGHT_NORMALS[FACE_POS_Y];
LIGHT_NORMALS_MIRRORED[FACE_NEG_Z] = LIGHT_NORMALS[FACE_NEG_Z];
LIGHT_NORMALS_MIRRORED[FACE_POS_Z] = LIGHT_NORMALS[FACE_POS_Z];
LIGHT_NORMALS_MIRRORED[FACE_POS_X] = LIGHT_NORMALS[FACE_NEG_X]; // mirrored
LIGHT_NORMALS_MIRRORED[FACE_NEG_X] = LIGHT_NORMALS[FACE_POS_X]; // mirrored
}

public static void render(MatrixStack matrixStack, VertexBufferWriter writer, ModelPart part, int light, int overlay, int color) {
Expand Down Expand Up @@ -129,16 +133,16 @@ private static void renderCuboids(MatrixStack.Entry matrices, VertexBufferWriter
for (ModelCuboid cuboid : cuboids) {
prepareVertices(matrices, cuboid);

var vertexCount = renderCuboid(cuboid, color, overlay, light);
var vertexCount = renderCuboid(matrices, cuboid, color, overlay, light);

try (MemoryStack stack = MemoryStack.stackPush()) {
writer.push(stack, SCRATCH_BUFFER, vertexCount, ModelVertex.FORMAT);
}
}
}

private static int renderCuboid(ModelCuboid cuboid, int color, int overlay, int light) {
final int faces = cuboid.faces & getVisibleFaces();
private static int renderCuboid(MatrixStack.Entry matrices, ModelCuboid cuboid, int color, int overlay, int light) {
final int faces = cuboid.faces & getVisibleFaces(matrices);

if (faces == 0) {
// No faces are visible, so zero vertices will be produced.
Expand All @@ -147,29 +151,27 @@ private static int renderCuboid(ModelCuboid cuboid, int color, int overlay, int

final var positions = cuboid.mirror ? VERTEX_POSITIONS_MIRRORED : VERTEX_POSITIONS;
final var textures = cuboid.mirror ? VERTEX_TEXTURES_MIRRORED : VERTEX_TEXTURES;
final var normals = cuboid.mirror ? CUBE_NORMALS_MIRRORED : CUBE_NORMALS;
final var normals = cuboid.mirror ? LIGHT_NORMALS_MIRRORED : LIGHT_NORMALS;

var vertexCount = 0;

long ptr = SCRATCH_BUFFER;

for (int quadIndex = 0; quadIndex < NUM_CUBE_FACES; quadIndex++) {
if ((faces & (1 << quadIndex)) != 0) {
if ((faces & (1 << quadIndex)) == 0) {
continue;
}

var normal = NormI8.pack(normals[quadIndex]);

emitVertex(ptr, positions[quadIndex][0], color, textures[quadIndex][0], overlay, light, normal);
emitVertex(ptr, positions[quadIndex][0], color, textures[quadIndex][0], overlay, light, normals[quadIndex]);
ptr += ModelVertex.STRIDE;

emitVertex(ptr, positions[quadIndex][1], color, textures[quadIndex][1], overlay, light, normal);
emitVertex(ptr, positions[quadIndex][1], color, textures[quadIndex][1], overlay, light, normals[quadIndex]);
ptr += ModelVertex.STRIDE;

emitVertex(ptr, positions[quadIndex][2], color, textures[quadIndex][2], overlay, light, normal);
emitVertex(ptr, positions[quadIndex][2], color, textures[quadIndex][2], overlay, light, normals[quadIndex]);
ptr += ModelVertex.STRIDE;

emitVertex(ptr, positions[quadIndex][3], color, textures[quadIndex][3], overlay, light, normal);
emitVertex(ptr, positions[quadIndex][3], color, textures[quadIndex][3], overlay, light, normals[quadIndex]);
ptr += ModelVertex.STRIDE;

vertexCount += 4;
Expand All @@ -183,13 +185,16 @@ private static void emitVertex(long ptr, Vector3f pos, int color, Vector2f tex,
}


private static int getVisibleFaces() {
final var min = CUBE_CORNERS[VERTEX_X1_Y1_Z1];
final var max = CUBE_CORNERS[VERTEX_X2_Y2_Z2];

int faces = 0;
private static int getVisibleFaces(MatrixStack.Entry matrices) {
// For orthogonal matrices (used in inventory rendering), this trick does not seem to work. Trying to debug
// how the orthogonal matrix is created made me immediately want to light my computer on fire, so we're instead
// just going to ignore the problem.
if ((matrices.getPositionMatrix().properties() & Matrix4f.PROPERTY_ORTHONORMAL) == 0) {
// All faces are visible.
return 0b111111;
}

// If the dot product between any vertex of a primitive and the normal is positive, then the primitive
// If the dot product between any vertex of a primitive and the normal is negative, then the primitive
// is front facing, and considered "visible".
//
// Since we only need to perform a dot product against *one* vertex of the primitive, we simply use the
Expand All @@ -202,32 +207,35 @@ private static int getVisibleFaces() {
// However, Minecraft is not a reasonable game, and cube normals are defined arbitrarily. Worse yet, the faces
// -X and +X also have their normals flipped when the cuboid is using "mirrored" texturing. So we have to
// completely ignore the normals Minecraft *normally* uses for rendering (which affects lighting) and use the
// "true" normals here. This is why you don't see (cuboid.mirror ? CUBE_NORMALS_MIRRORED : CUBE_NORMALS) used.
//
// The vertex used for each face in the comparisons below will depend on whether the indices for CUBE_NORMAL[face]
// contain either the minimum or maximum vertex.
if (min.dot(CUBE_NORMALS[FACE_NEG_Y]) > 0.0f) {
// "true" normals here.

final var min = CUBE_CORNERS[VERTEX_X1_Y1_Z1];
final var max = CUBE_CORNERS[VERTEX_X2_Y2_Z2];

int faces = 0;

if (min.dot(FACE_NORMALS[FACE_NEG_Y]) < 0.0f) {
faces |= 1 << FACE_NEG_Y;
}

if (max.dot(CUBE_NORMALS[FACE_POS_Y]) > 0.0f) {
if (max.dot(FACE_NORMALS[FACE_POS_Y]) < 0.0f) {
faces |= 1 << FACE_POS_Y;
}

if (min.dot(CUBE_NORMALS[FACE_NEG_Z]) > 0.0f) {
if (min.dot(FACE_NORMALS[FACE_NEG_Z]) < 0.0f) {
faces |= 1 << FACE_NEG_Z;
}

if (min.dot(CUBE_NORMALS[FACE_POS_X]) > 0.0f) {
faces |= 1 << FACE_POS_X;
if (max.dot(FACE_NORMALS[FACE_POS_Z]) < 0.0f) {
faces |= 1 << FACE_POS_Z;
}

if (max.dot(CUBE_NORMALS[FACE_NEG_X]) > 0.0f) {
faces |= 1 << FACE_NEG_X;
if (min.dot(FACE_NORMALS[FACE_POS_X]) < 0.0f) {
faces |= 1 << FACE_POS_X;
}

if (max.dot(CUBE_NORMALS[FACE_POS_Z]) > 0.0f) {
faces |= 1 << FACE_POS_Z;
if (max.dot(FACE_NORMALS[FACE_NEG_X]) < 0.0f) {
faces |= 1 << FACE_NEG_X;
}

return faces;
Expand All @@ -252,14 +260,35 @@ private static void prepareVertices(MatrixStack.Entry matrices, ModelCuboid cubo
}

private static void prepareNormals(MatrixStack.Entry matrices) {
MatrixHelper.transformNormal(CUBE_NORMALS[FACE_NEG_Y], matrices.getNormalMatrix(), Direction.DOWN);
MatrixHelper.transformNormal(CUBE_NORMALS[FACE_POS_Y], matrices.getNormalMatrix(), Direction.UP);
MatrixHelper.transformNormal(CUBE_NORMALS[FACE_NEG_Z], matrices.getNormalMatrix(), Direction.NORTH);
MatrixHelper.transformNormal(CUBE_NORMALS[FACE_POS_Z], matrices.getNormalMatrix(), Direction.SOUTH);
MatrixHelper.transformNormal(CUBE_NORMALS[FACE_POS_X], matrices.getNormalMatrix(), Direction.WEST);
MatrixHelper.transformNormal(CUBE_NORMALS[FACE_NEG_X], matrices.getNormalMatrix(), Direction.EAST);
LIGHT_NORMALS[FACE_NEG_Y] = MatrixHelper.transformNormal(matrices.getNormalMatrix(), Direction.DOWN);
LIGHT_NORMALS[FACE_POS_Y] = MatrixHelper.transformNormal(matrices.getNormalMatrix(), Direction.UP);
LIGHT_NORMALS[FACE_NEG_Z] = MatrixHelper.transformNormal(matrices.getNormalMatrix(), Direction.NORTH);
LIGHT_NORMALS[FACE_POS_Z] = MatrixHelper.transformNormal(matrices.getNormalMatrix(), Direction.SOUTH);
LIGHT_NORMALS[FACE_POS_X] = MatrixHelper.transformNormal(matrices.getNormalMatrix(), Direction.WEST);
LIGHT_NORMALS[FACE_NEG_X] = MatrixHelper.transformNormal(matrices.getNormalMatrix(), Direction.EAST);

// When mirroring is used, the light normals for EAST and WEST are swapped.
LIGHT_NORMALS_MIRRORED[FACE_NEG_Y] = LIGHT_NORMALS[FACE_NEG_Y];
LIGHT_NORMALS_MIRRORED[FACE_POS_Y] = LIGHT_NORMALS[FACE_POS_Y];
LIGHT_NORMALS_MIRRORED[FACE_NEG_Z] = LIGHT_NORMALS[FACE_NEG_Z];
LIGHT_NORMALS_MIRRORED[FACE_POS_Z] = LIGHT_NORMALS[FACE_POS_Z];
LIGHT_NORMALS_MIRRORED[FACE_POS_X] = LIGHT_NORMALS[FACE_NEG_X]; // mirrored
LIGHT_NORMALS_MIRRORED[FACE_NEG_X] = LIGHT_NORMALS[FACE_POS_X]; // mirrored

// The normal matrix given to us is not useful, because Minecraft modifies it in all kinds of arbitrary ways
// to adjust lighting. We need *actual* surface normals, not whatever Minecraft is treating it as.
matrices.getPositionMatrix()
.normal(FACE_NORMAL_MATRIX);

MatrixHelper.transformNormal(FACE_NORMALS[FACE_NEG_Y], FACE_NORMAL_MATRIX, Direction.DOWN);
MatrixHelper.transformNormal(FACE_NORMALS[FACE_POS_Y], FACE_NORMAL_MATRIX, Direction.UP);
MatrixHelper.transformNormal(FACE_NORMALS[FACE_NEG_Z], FACE_NORMAL_MATRIX, Direction.NORTH);
MatrixHelper.transformNormal(FACE_NORMALS[FACE_POS_Z], FACE_NORMAL_MATRIX, Direction.SOUTH);
MatrixHelper.transformNormal(FACE_NORMALS[FACE_NEG_X], FACE_NORMAL_MATRIX, Direction.EAST);
MatrixHelper.transformNormal(FACE_NORMALS[FACE_POS_X], FACE_NORMAL_MATRIX, Direction.WEST);
}


private static void buildVertexPosition(Vector3f vector, float x, float y, float z, Matrix4f matrix) {
vector.x = MatrixHelper.transformPositionX(matrix, x, y, z);
vector.y = MatrixHelper.transformPositionY(matrix, x, y, z);
Expand Down

0 comments on commit bc69ca9

Please sign in to comment.