Skip to content

Commit

Permalink
Downgrade quad materials by scanning their textures (#2666)
Browse files Browse the repository at this point in the history
  • Loading branch information
douira authored Aug 25, 2024
1 parent c91efa8 commit d8966e8
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public ChunkModelBuilder get(Material material) {
return this.builders.get(material.pass);
}

public ChunkModelBuilder get(TerrainRenderPass pass) {
return this.builders.get(pass);
}

/**
* Creates immutable baked chunk meshes from all non-empty scratch buffers. This is used after all blocks
* have been rendered to pass the finished meshes over to the graphics card. This function can be called multiple
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline;

import net.caffeinemc.mods.sodium.api.util.ColorABGR;
import net.caffeinemc.mods.sodium.api.util.ColorARGB;
import net.caffeinemc.mods.sodium.client.model.color.ColorProvider;
import net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry;
Expand All @@ -9,8 +10,12 @@
import net.caffeinemc.mods.sodium.client.model.quad.properties.ModelQuadOrientation;
import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers;
import net.caffeinemc.mods.sodium.client.render.chunk.compile.buffers.ChunkModelBuilder;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.DefaultMaterials;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.parameters.AlphaCutoffParameter;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.parameters.MaterialParameters;
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.TranslucentGeometryCollector;
import net.caffeinemc.mods.sodium.client.render.chunk.vertex.builder.ChunkMeshBufferBuilder;
import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder;
Expand All @@ -28,6 +33,7 @@
import net.fabricmc.fabric.api.util.TriState;
import net.minecraft.client.renderer.ItemBlockRenderTypes;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
Expand Down Expand Up @@ -128,11 +134,9 @@ protected void processQuad(MutableQuadViewImpl quad) {
material = DefaultMaterials.forRenderLayer(blendMode.blockRenderLayer == null ? type : blendMode.blockRenderLayer);
}

ChunkModelBuilder builder = this.buffers.get(material);

this.colorizeQuad(quad, colorIndex);
this.shadeQuad(quad, lightMode, emissive, shadeMode);
this.bufferQuad(quad, this.quadLightData.br, material, builder);
this.bufferQuad(quad, this.quadLightData.br, material);
}

private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) {
Expand All @@ -150,12 +154,15 @@ private void colorizeQuad(MutableQuadViewImpl quad, int colorIndex) {
}
}

private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material material, ChunkModelBuilder modelBuilder) {
private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material material) {
// TODO: Find a way to reimplement quad reorientation
ModelQuadOrientation orientation = ModelQuadOrientation.NORMAL;
ChunkVertexEncoder.Vertex[] vertices = this.vertices;
Vector3f offset = this.posOffset;

float uSum = 0.0f;
float vSum = 0.0f;

for (int dstIndex = 0; dstIndex < 4; dstIndex++) {
int srcIndex = orientation.getVertexIndex(dstIndex);

Expand All @@ -167,21 +174,93 @@ private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material
// FRAPI uses ARGB color format; convert to ABGR.
out.color = ColorARGB.toABGR(quad.color(srcIndex));
out.ao = brightnesses[srcIndex];
out.u = quad.u(srcIndex);
out.v = quad.v(srcIndex);

uSum += out.u = quad.u(srcIndex);
vSum += out.v = quad.v(srcIndex);

out.light = quad.lightmap(srcIndex);
}

var atlasSprite = SpriteFinderCache.forBlockAtlas().find(uSum / 4.0f, vSum / 4.0f);
var materialBits = material.bits();
ModelQuadFacing normalFace = quad.normalFace();

if (material.isTranslucent() && this.collector != null) {
// attempt render pass downgrade if possible
var pass = material.pass;
var downgradedPass = attemptPassDowngrade(quad, atlasSprite, pass);
if (downgradedPass != null) {
pass = downgradedPass;
}

// collect all translucent quads into the translucency sorting system if enabled
if (pass.isTranslucent() && this.collector != null) {
this.collector.appendQuad(quad.getFaceNormal(), vertices, normalFace);
}

ChunkMeshBufferBuilder vertexBuffer = modelBuilder.getVertexBuffer(normalFace);
vertexBuffer.push(vertices, material);
// if there was a downgrade from translucent to cutout, the material bits' alpha cutoff needs to be updated
if (downgradedPass != null && material == DefaultMaterials.TRANSLUCENT && pass == DefaultTerrainRenderPasses.CUTOUT) {
// ONE_TENTH and HALF are functionally the same so it doesn't matter which one we take here
materialBits = MaterialParameters.pack(AlphaCutoffParameter.ONE_TENTH, material.mipped);
}

ChunkModelBuilder builder = this.buffers.get(pass);
ChunkMeshBufferBuilder vertexBuffer = builder.getVertexBuffer(normalFace);
vertexBuffer.push(vertices, materialBits);

builder.addSprite(atlasSprite);
}

private boolean validateQuadUVs(TextureAtlasSprite atlasSprite) {
// sanity check that the quad's UVs are within the sprite's bounds
var spriteUMin = atlasSprite.getU0();
var spriteUMax = atlasSprite.getU1();
var spriteVMin = atlasSprite.getV0();
var spriteVMax = atlasSprite.getV1();

for (int i = 0; i < 4; i++) {
var u = this.vertices[i].u;
var v = this.vertices[i].v;
if (u < spriteUMin || u > spriteUMax || v < spriteVMin || v > spriteVMax) {
return false;
}
}

return true;
}

private TerrainRenderPass attemptPassDowngrade(MutableQuadViewImpl quad, TextureAtlasSprite sprite, TerrainRenderPass pass) {
boolean attemptDowngrade = true;
boolean hasNonOpaqueVertex = false;

for (int i = 0; i < 4; i++) {
hasNonOpaqueVertex |= ColorABGR.unpackAlpha(this.vertices[i].color) != 0xFF;
}

// don't do downgrade if some vertex is not fully opaque
if (pass.isTranslucent() && hasNonOpaqueVertex) {
attemptDowngrade = false;
}

if (attemptDowngrade) {
attemptDowngrade = validateQuadUVs(sprite);
}

if (attemptDowngrade) {
return getDowngradedPass(sprite, pass);
}

return null;
}

modelBuilder.addSprite(SpriteFinderCache.forBlockAtlas().find(quad.getTexU(0), quad.getTexV(0)));
private static TerrainRenderPass getDowngradedPass(TextureAtlasSprite sprite, TerrainRenderPass pass) {
if (sprite.contents() instanceof SpriteContentsExtension contents) {
if (pass == DefaultTerrainRenderPasses.TRANSLUCENT && !contents.sodium$hasTranslucentPixels()) {
pass = DefaultTerrainRenderPasses.CUTOUT;
}
if (pass == DefaultTerrainRenderPasses.CUTOUT && !contents.sodium$hasTransparentPixels()) {
pass = DefaultTerrainRenderPasses.SOLID;
}
}
return pass;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline;

public interface SpriteContentsExtension {
boolean sodium$hasTransparentPixels();

boolean sodium$hasTranslucentPixels();
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,18 @@ public ChunkMeshBufferBuilder(ChunkVertexType vertexType, int initialCapacity) {
}

public void push(ChunkVertexEncoder.Vertex[] vertices, Material material) {
this.push(vertices, material.bits());
}

public void push(ChunkVertexEncoder.Vertex[] vertices, int materialBits) {
var vertexCount = vertices.length;

if (this.count + vertexCount >= this.capacity) {
this.grow(this.stride * vertexCount);
}

this.encoder.write(MemoryUtil.memAddress(this.buffer, this.count * this.stride),
material, vertices, this.sectionIndex);
materialBits, vertices, this.sectionIndex);

this.count += vertexCount;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package net.caffeinemc.mods.sodium.client.render.chunk.vertex.format;

import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material;

public interface ChunkVertexEncoder {
long write(long ptr, Material material, Vertex[] vertices, int sectionIndex);
long write(long ptr, int materialBits, Vertex[] vertices, int sectionIndex);

class Vertex {
public float x;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public GlVertexFormat getVertexFormat() {

@Override
public ChunkVertexEncoder getEncoder() {
return (ptr, material, vertices, section) -> {
return (ptr, materialBits, vertices, section) -> {
// Calculate the center point of the texture region which is mapped to the quad
float texCentroidU = 0.0f;
float texCentroidV = 0.0f;
Expand Down Expand Up @@ -61,7 +61,7 @@ public ChunkVertexEncoder getEncoder() {
MemoryUtil.memPutInt(ptr + 4L, packPositionLo(x, y, z));
MemoryUtil.memPutInt(ptr + 8L, ColorHelper.multiplyRGB(vertex.color, vertex.ao));
MemoryUtil.memPutInt(ptr + 12L, packTexture(u, v));
MemoryUtil.memPutInt(ptr + 16L, packLightAndData(light, material.bits(), section));
MemoryUtil.memPutInt(ptr + 16L, packLightAndData(light, materialBits, section));

ptr += STRIDE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,17 @@
*/
package net.caffeinemc.mods.sodium.mixin.features.textures.mipmaps;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.platform.NativeImage;
import net.caffeinemc.mods.sodium.client.util.NativeImageHelper;
import net.caffeinemc.mods.sodium.client.util.color.ColorSRGB;
import net.minecraft.client.renderer.texture.SpriteContents;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.FastColor;
import org.lwjgl.system.MemoryUtil;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;

@Mixin(SpriteContents.class)
public class SpriteContentsMixin {
Expand All @@ -30,20 +26,11 @@ public class SpriteContentsMixin {
@Final
private NativeImage originalImage;

// While Fabric allows us to @Inject into the constructor here, that's just a specific detail of FabricMC's mixin
// fork. Upstream Mixin doesn't allow arbitrary @Inject usage in constructor. However, we can use @ModifyVariable
// just fine, in a way that hopefully doesn't conflict with other mods.
//
// By doing this, we can work with upstream Mixin as well, as is used on Forge. While we don't officially
// support Forge, since this works well on Fabric too, it's fine to ensure that the diff between Fabric and Forge
// can remain minimal. Being less dependent on specific details of Fabric is good, since it means we can be more
// cross-platform.
@Redirect(method = "<init>", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD))
private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, ResourceLocation identifier) {
// We're injecting after the "info" field has been set, so this is safe even though we're in a constructor.
@WrapOperation(method = "<init>", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD))
private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, Operation<Void> original) {
sodium$fillInTransparentPixelColors(nativeImage);

this.originalImage = nativeImage;
original.call(instance, nativeImage);
}

/**
Expand All @@ -68,7 +55,7 @@ public class SpriteContentsMixin {
float totalWeight = 0.0f;

for (int pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
long pPixel = ppPixel + (pixelIndex * 4);
long pPixel = ppPixel + (pixelIndex * 4L);

int color = MemoryUtil.memGetInt(pPixel);
int alpha = FastColor.ABGR32.alpha(color);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package net.caffeinemc.mods.sodium.mixin.features.textures.scan;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.blaze3d.platform.NativeImage;
import net.caffeinemc.mods.sodium.api.util.ColorABGR;
import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.SpriteContentsExtension;
import net.caffeinemc.mods.sodium.client.util.NativeImageHelper;
import net.minecraft.client.renderer.texture.SpriteContents;
import org.lwjgl.system.MemoryUtil;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;

/**
* This mixin scans a {@link SpriteContents} for transparent and translucent pixels. This information is later used during mesh generation to reassign the render pass to either cutout if the sprite has no translucent pixels or solid if it doesn't even have any transparent pixels.
*
* @author douira
*/
@Mixin(SpriteContents.class)
public class SpriteContentsMixin implements SpriteContentsExtension {
@Mutable
@Shadow
@Final
private NativeImage originalImage;

@Unique
public boolean sodium$hasTransparentPixels = false;

@Unique
public boolean sodium$hasTranslucentPixels = false;

/*
* Uses a WrapOperation here since Inject doesn't work on 1.20.1 forge.
*/
@WrapOperation(method = "<init>", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD))
private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, Operation<Void> original) {
scanSpriteContents(nativeImage);

original.call(instance, nativeImage);
}

@Unique
private void scanSpriteContents(NativeImage nativeImage) {
final long ppPixel = NativeImageHelper.getPointerRGBA(nativeImage);
final int pixelCount = nativeImage.getHeight() * nativeImage.getWidth();

for (int pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
int color = MemoryUtil.memGetInt(ppPixel + (pixelIndex * 4L));
int alpha = ColorABGR.unpackAlpha(color);

// 25 is used as the threshold since the alpha cutoff is 0.1
if (alpha <= 25) { // 0.1 * 255
this.sodium$hasTransparentPixels = true;
} else if (alpha < 255) {
this.sodium$hasTranslucentPixels = true;
}
}

// the image contains transparency also if there are translucent pixels,
// since translucent pixels prevent a downgrade to the opaque render pass just as transparent pixels do
this.sodium$hasTransparentPixels |= this.sodium$hasTranslucentPixels;
}

@Override
public boolean sodium$hasTransparentPixels() {
return this.sodium$hasTransparentPixels;
}

@Override
public boolean sodium$hasTranslucentPixels() {
return this.sodium$hasTranslucentPixels;
}
}
1 change: 1 addition & 0 deletions common/src/main/resources/sodium.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"features.textures.animations.upload.SpriteContentsInterpolationMixin",
"features.textures.mipmaps.MipmapGeneratorMixin",
"features.textures.mipmaps.SpriteContentsMixin",
"features.textures.scan.SpriteContentsMixin",
"features.world.biome.BiomeMixin",
"workarounds.context_creation.WindowMixin",
"workarounds.event_loop.RenderSystemMixin"
Expand Down

0 comments on commit d8966e8

Please sign in to comment.