Add Custom Renderer API for Fluids (#1059)

This commit is contained in:
slprime authored and GitHub committed 2026-10-02 01:22:58 +03:00
1 parent a105a60a1a
commit c020ecbf1f
7 files changed
+151 -65

No files matched your search

@@ -361,7 +361,7 @@ public class PositionedStack implements Cloneable {
return;
}
FluidDrawer.drawFluid(
FluidDrawer.drawTank(
this.relx,
this.rely,
this.width,
@@ -12,6 +12,7 @@ import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.inventory.Slot;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraftforge.fluids.Fluid;
import org.lwjgl.input.Keyboard;
@@ -33,6 +34,7 @@ import codechicken.nei.SubsetWidget.SubsetTag;
import codechicken.nei.api.IRecipeFilter.IRecipeFilterProvider;
import codechicken.nei.api.ItemFilter.ItemFilterProvider;
import codechicken.nei.config.Option;
import codechicken.nei.item.FluidDrawer;
import codechicken.nei.recipe.CatalystInfo;
import codechicken.nei.recipe.GuiCraftingRecipe;
import codechicken.nei.recipe.GuiRecipe;
@@ -375,6 +377,16 @@ public class API {
StackInfo.stackStringifyHandlers.add(handler);
}
/**
* Replaces the default rendering of a fluid in NEI fluid display slots and fluid tanks.
*
* @param fluid the fluid to render
* @param renderer the renderer, or null to restore the default rendering
*/
public static void registerFluidRenderer(Fluid fluid, IFluidRenderer renderer) {
FluidDrawer.registerFluidRenderer(fluid, renderer);
}
/**
* Adds an association between an ingredient and what it can craft. (i.e. Furnace ItemStack -> Smelting and Fuel
* Recipes) Allows players to see what ingredient they need to craft in order to make recipes from a recipe
@@ -0,0 +1,17 @@
package codechicken.nei.api;
import net.minecraftforge.fluids.Fluid;
/**
* Custom renderer for a fluid, used by NEI fluid display slots and fluid tanks in recipes.
* <p>
* Register with {@link API#registerFluidRenderer(Fluid, IFluidRenderer)}.
*/
public interface IFluidRenderer {
/**
* Renders one 16x16 tile of the fluid. Partial tiles of a tank are clipped by NEI. The amount overlay is drawn by
* NEI.
*/
void renderFluid(Fluid fluid, int x, int y);
}
@@ -2,15 +2,14 @@ package codechicken.nei.item;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IIcon;
import net.minecraftforge.client.IItemRenderer;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry;
import org.lwjgl.opengl.GL11;
import codechicken.nei.item.FluidDrawer.FillDirection;
import codechicken.nei.util.ReadableNumberConverter;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -34,37 +33,13 @@ public class FluidDisplayRenderer implements IItemRenderer {
return;
}
renderIcon(item, fluidDisplay);
renderIcon(item);
renderAmountOverlay(item, fluidDisplay);
}
private void renderIcon(ItemStack item, ItemFluidDisplay fluidDisplay) {
final IIcon icon = fluidDisplay.getIconIndex(item);
if (icon == null) {
return;
}
final int color = fluidDisplay.getColorFromItemStack(item, 0);
final float red = (color >> 16 & 0xFF) / 255F;
final float green = (color >> 8 & 0xFF) / 255F;
final float blue = (color & 0xFF) / 255F;
Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.locationBlocksTexture);
GL11.glColor4f(red, green, blue, 1F);
GL11.glEnable(GL11.GL_BLEND);
OpenGlHelper
.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
final Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
tessellator.addVertexWithUV(0, 16, 0, icon.getMinU(), icon.getMaxV());
tessellator.addVertexWithUV(16, 16, 0, icon.getMaxU(), icon.getMaxV());
tessellator.addVertexWithUV(16, 0, 0, icon.getMaxU(), icon.getMinV());
tessellator.addVertexWithUV(0, 0, 0, icon.getMinU(), icon.getMinV());
tessellator.draw();
GL11.glColor4f(1F, 1F, 1F, 1F);
GL11.glDisable(GL11.GL_BLEND);
private void renderIcon(ItemStack item) {
final Fluid fluid = FluidRegistry.getFluid(item.getItemDamage());
FluidDrawer.drawFluidArea(fluid != null ? fluid : FluidRegistry.WATER, 0, 0, 16, 16, false, FillDirection.UP);
}
private void renderAmountOverlay(ItemStack item, ItemFluidDisplay fluidDisplay) {
@@ -1,14 +1,21 @@
package codechicken.nei.item;
import java.util.IdentityHashMap;
import java.util.Map;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.IIcon;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry;
import net.minecraftforge.fluids.FluidStack;
import org.lwjgl.opengl.GL11;
import codechicken.nei.api.IFluidRenderer;
import codechicken.nei.scroll.GuiHelper;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -27,28 +34,32 @@ public class FluidDrawer {
return this == LEFT || this == RIGHT;
}
public FillDirection resolve(FluidStack fluidStack) {
public FillDirection resolve(Fluid fluid) {
if (this != AUTO) return this;
return fluidStack.getFluid().isGaseous(fluidStack) ? DOWN : UP;
return fluid.isGaseous() ? DOWN : UP;
}
}
private static final Map<Fluid, IFluidRenderer> fluidRenderers = new IdentityHashMap<>();
private FluidDrawer() {}
public static void drawFluid(int x, int y, int width, int height, int capacity, FluidStack fluidStack,
boolean flowing, FillDirection direction) {
final Fluid fluid = fluidStack.getFluid();
IIcon icon = flowing ? fluid.getFlowingIcon() : null;
if (icon == null) {
icon = fluid.getIcon(fluidStack);
public static void registerFluidRenderer(Fluid fluid, IFluidRenderer renderer) {
if (renderer == null) {
fluidRenderers.remove(fluid);
} else {
fluidRenderers.put(fluid, renderer);
}
}
if (icon == null || fluidStack.amount <= 0) {
public static void drawTank(int x, int y, int width, int height, int capacity, FluidStack fluidStack,
boolean flowing, FillDirection direction) {
if (fluidStack.amount <= 0) {
return;
}
direction = direction.resolve(fluidStack);
final Fluid fluid = fluidStack.getFluid();
direction = direction.resolve(fluid);
final int tankSize = direction.isHorizontal() ? width : height;
final int tankCapacity = capacity > 0 ? capacity : fluidStack.amount;
@@ -59,38 +70,109 @@ public class FluidDrawer {
final int fillHeight = direction.isHorizontal() ? height : Math.min(fillSize, height);
final int fillX = direction == FillDirection.LEFT ? x + width - fillWidth : x;
final int fillY = direction == FillDirection.DOWN ? y : y + height - fillHeight;
GL11.glDisable(GL11.GL_LIGHTING);
drawFluidArea(fluid, fillX, fillY, fillWidth, fillHeight, flowing, direction);
GL11.glEnable(GL11.GL_LIGHTING);
}
/**
* Fills the area with fluid tiles. The tiles start from the side the fluid fills from, so a partial tile ends up at
* the fluid surface.
*/
public static void drawFluidArea(Fluid fluid, int x, int y, int width, int height, boolean flowing,
FillDirection direction) {
direction = direction.resolve(fluid);
final boolean anchorRight = direction == FillDirection.LEFT;
final boolean anchorBottom = direction != FillDirection.DOWN;
final IFluidRenderer renderer = fluidRenderers.get(fluid);
final int color = fluid.getColor(fluidStack);
if (renderer == null) {
drawDefaultFluid(fluid, x, y, width, height, flowing, anchorRight, anchorBottom);
} else {
drawCustomFluid(renderer, fluid, x, y, width, height, anchorRight, anchorBottom);
}
}
private static void drawCustomFluid(IFluidRenderer renderer, Fluid fluid, int x, int y, int width, int height,
boolean anchorRight, boolean anchorBottom) {
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_LIGHTING_BIT);
GL11.glDisable(GL11.GL_LIGHTING);
final boolean clip = width % 16 != 0 || height % 16 != 0;
if (clip) {
GuiHelper.pushScissorFrame(x, y, width, height);
}
try {
for (int tx = 0; tx < width; tx += 16) {
final int tileX = anchorRight ? x + width - tx - 16 : x + tx;
for (int ty = 0; ty < height; ty += 16) {
final int tileY = anchorBottom ? y + height - ty - 16 : y + ty;
renderer.renderFluid(fluid, tileX, tileY);
}
}
} finally {
if (clip) {
GuiHelper.popScissorFrame();
}
GL11.glPopAttrib();
GL11.glColor4f(1F, 1F, 1F, 1F);
}
}
private static void drawDefaultFluid(Fluid fluid, int x, int y, int width, int height, boolean flowing,
boolean anchorRight, boolean anchorBottom) {
IIcon icon = flowing ? fluid.getFlowingIcon() : null;
if (icon == null) {
icon = fluid.getStillIcon();
}
if (icon == null) {
icon = FluidRegistry.WATER.getStillIcon();
}
if (icon == null) {
return;
}
final int color = fluid.getColor();
final float red = (color >> 16 & 0xFF) / 255F;
final float green = (color >> 8 & 0xFF) / 255F;
final float blue = (color & 0xFF) / 255F;
Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.locationBlocksTexture);
GL11.glColor3f(red, green, blue);
GL11.glColor4f(red, green, blue, 1F);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDisable(GL11.GL_LIGHTING);
OpenGlHelper
.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);
final double iconMinU = icon.getMinU();
final double iconMaxU = icon.getMaxU();
final double iconMinV = icon.getMinV();
final double iconMaxV = icon.getMaxV();
final Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
for (int tx = 0; tx < fillWidth; tx += 16) {
final int tileWidth = Math.min(16, fillWidth - tx);
for (int tx = 0; tx < width; tx += 16) {
final int tileWidth = Math.min(16, width - tx);
// a cropped tile keeps the texture edge that stays seamless against the neighbouring tile
final double uSize = (icon.getMaxU() - icon.getMinU()) * tileWidth / 16D;
final double uMin = anchorRight ? icon.getMaxU() - uSize : icon.getMinU();
final double uMax = anchorRight ? icon.getMaxU() : icon.getMinU() + uSize;
final int leftX = anchorRight ? fillX + fillWidth - tx - tileWidth : fillX + tx;
final double uSize = (iconMaxU - iconMinU) * tileWidth / 16D;
final double uMin = anchorRight ? iconMaxU - uSize : iconMinU;
final double uMax = anchorRight ? iconMaxU : iconMinU + uSize;
final int leftX = anchorRight ? x + width - tx - tileWidth : x + tx;
final int rightX = leftX + tileWidth;
for (int ty = 0; ty < fillHeight; ty += 16) {
final int tileHeight = Math.min(16, fillHeight - ty);
final double vSize = (icon.getMaxV() - icon.getMinV()) * tileHeight / 16D;
final double vMin = anchorBottom ? icon.getMaxV() - vSize : icon.getMinV();
final double vMax = anchorBottom ? icon.getMaxV() : icon.getMinV() + vSize;
final int bottomY = anchorBottom ? fillY + fillHeight - ty : fillY + ty + tileHeight;
for (int ty = 0; ty < height; ty += 16) {
final int tileHeight = Math.min(16, height - ty);
final double vSize = (iconMaxV - iconMinV) * tileHeight / 16D;
final double vMin = anchorBottom ? iconMaxV - vSize : iconMinV;
final double vMax = anchorBottom ? iconMaxV : iconMinV + vSize;
final int bottomY = anchorBottom ? y + height - ty : y + ty + tileHeight;
final int topY = bottomY - tileHeight;
tessellator.addVertexWithUV(leftX, bottomY, 0, uMin, vMax);
@@ -102,7 +184,6 @@ public class FluidDrawer {
tessellator.draw();
GL11.glDisable(GL11.GL_BLEND);
GL11.glEnable(GL11.GL_LIGHTING);
GL11.glColor4f(1F, 1F, 1F, 1F);
}
}
@@ -73,7 +73,7 @@ public class ItemFluidDisplay extends Item implements IFluidContainerItem {
@Override
public FluidStack getFluid(ItemStack stack) {
if (stack == null || !(stack.getItem() instanceof ItemFluidDisplay) || !stack.hasTagCompound()) {
if (stack == null || !(stack.getItem() instanceof ItemFluidDisplay)) {
return null;
}
@@ -86,7 +86,7 @@ public class ItemFluidDisplay extends Item implements IFluidContainerItem {
final NBTTagCompound nbTag = stack.getTagCompound();
final FluidStack fluidStack = new FluidStack(
fluid,
(int) Math.min(nbTag.getLong("neiFluidDisplayAmount"), Integer.MAX_VALUE));
(int) Math.min(nbTag != null ? nbTag.getLong("neiFluidDisplayAmount") : 0, Integer.MAX_VALUE));
return fluidStack;
}
@@ -16,6 +16,7 @@ import cpw.mods.fml.relauncher.SideOnly;
public class GuiHelper {
private static final Stack<int[]> scissorFrameStack = new Stack<>();
private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer(16);
public static void useScissor(int x, int y, int width, int height, Runnable codeBlock) {
pushScissorFrame(x, y, width, height);
@@ -36,7 +37,7 @@ public class GuiHelper {
return currentTopFrame;
}
private static void pushScissorFrame(int x, int y, int width, int height) {
public static void pushScissorFrame(int x, int y, int width, int height) {
int[] matrix = prepareMatrix(x, y, width, height);
int[] parentScissor = peekFirstScissorOrFullScreen();
int parentX = parentScissor[0];
@@ -110,7 +111,7 @@ public class GuiHelper {
private static int[] prepareMatrix(int x, int y, int w, int h) {
// Get the current translation component to support the Gui being drawn at any position on the screen
final FloatBuffer matBuf = BufferUtils.createFloatBuffer(16);
final FloatBuffer matBuf = MATRIX_BUFFER;
GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, matBuf);
final float xScale = matBuf.get(0);
final float yScale = matBuf.get(5);