Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Downgrade quad materials by scanning their textures #2666

Merged
merged 14 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand Up @@ -9,8 +9,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 +32,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 +133,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,11 +153,18 @@ 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) {
ModelQuadOrientation orientation = defaultLightMode == LightMode.FLAT ? ModelQuadOrientation.NORMAL : ModelQuadOrientation.orientByBrightness(brightnesses, quad);
ChunkVertexEncoder.Vertex[] vertices = this.vertices;
Vector3f offset = this.posOffset;

var pass = material.pass;
var attemptDowngrade = true;

float uSum = 0.0f;
float vSum = 0.0f;
int alphaSum = 0;

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

Expand All @@ -164,23 +174,81 @@ private void bufferQuad(MutableQuadViewImpl quad, float[] brightnesses, Material
out.z = quad.z(srcIndex) + offset.z;

// FRAPI uses ARGB color format; convert to ABGR.
out.color = ColorARGB.toABGR(quad.color(srcIndex));
var color = quad.color(srcIndex);
alphaSum += ColorARGB.unpackAlpha(color);
douira marked this conversation as resolved.
Show resolved Hide resolved

out.color = ColorARGB.toABGR(color);
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);
douira marked this conversation as resolved.
Show resolved Hide resolved

// don't do downgrade if some vertex is not fully opaque
if (pass.isTranslucent() && alphaSum < 4 * 255) {
douira marked this conversation as resolved.
Show resolved Hide resolved
attemptDowngrade = false;
}

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

if (attemptDowngrade) {
pass = getDowngradedPass(atlasSprite, pass);
}

var materialBits = material.bits();

ModelQuadFacing normalFace = quad.normalFace();

if (material.isTranslucent() && this.collector != null) {
if (pass.isTranslucent() && this.collector != null) {
this.collector.appendQuad(quad.getFaceNormal(), vertices, normalFace);
}

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

modelBuilder.addSprite(SpriteFinderCache.forBlockAtlas().find(quad.getTexU(0), quad.getTexV(0)));
if (attemptDowngrade && 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);
}

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 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 @@ -33,7 +33,7 @@ public GlVertexFormat<ChunkMeshAttribute> 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 @@ -62,7 +62,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,66 @@
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.client.render.chunk.compile.pipeline.SpriteContentsExtension;
import net.caffeinemc.mods.sodium.client.util.NativeImageHelper;
import net.minecraft.client.renderer.texture.SpriteContents;
import net.minecraft.util.FastColor;
import org.lwjgl.system.MemoryUtil;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;

@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;

@WrapOperation(method = "<init>", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/SpriteContents;originalImage:Lcom/mojang/blaze3d/platform/NativeImage;", opcode = Opcodes.PUTFIELD))
douira marked this conversation as resolved.
Show resolved Hide resolved
private void sodium$beforeGenerateMipLevels(SpriteContents instance, NativeImage nativeImage, Operation<Void> original) {
sodium$scanSpriteContents(nativeImage);

original.call(instance, nativeImage);
}

@Unique
private void sodium$scanSpriteContents(NativeImage nativeImage) {
douira marked this conversation as resolved.
Show resolved Hide resolved
final long ppPixel = NativeImageHelper.getPointerRGBA(nativeImage);
final int pixelCount = nativeImage.getHeight() * nativeImage.getWidth();

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

int color = MemoryUtil.memGetInt(pPixel);
int alpha = FastColor.ABGR32.alpha(color);

// track if this image has transparent or even translucent pixels
if (alpha <= 25) { // 0.1 * 255
this.sodium$hasTransparentPixels = true;
} else if (alpha < 255) {
this.sodium$hasTranslucentPixels = true;
}
}

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 @@ -82,6 +82,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
Loading