Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ee28693
Fix variant block base texture never rendering in world
Ruling-0 Jul 9, 2026
1b56fb6
Fix variant ore block underlay missing in item form
Ruling-0 Jul 10, 2026
75cbfc6
Fix ore block underlay showing transparent instead of stone in world,…
Ruling-0 Jul 10, 2026
42b0ece
Confirm ShapeBlockItemRenderer already composites GUI slot and hotbar…
Ruling-0 Jul 10, 2026
00e23f1
Fix testOre variant base texture paths that resolved to nonexistent f…
Ruling-0 Jul 10, 2026
f9ce5dd
Fix item-form overlay z-fighting by scaling the overlay pass around t…
Ruling-0 Jul 10, 2026
332c721
Defeat Angelica's item-render display-list cache in ShapeBlockItemRen…
Ruling-0 Jul 11, 2026
a6df1a6
Pin ShapeBlockItemRenderer's off-1.0 brightness against future regres…
Ruling-0 Jul 11, 2026
ebac047
Replace composite ShapeBlock item rendering with an ISBRH
Ruling-0 Jul 11, 2026
7664d8f
Keep composite ShapeBlocks in the solid render pass like legacy ores
Ruling-0 Jul 11, 2026
ffd5c9b
Add a fluid-specific tint override and an existence-checked fluid ico…
Ruling-0 Jul 11, 2026
ff3db23
Add BLOCK_OVERLAY_TINT to let a block shape's overlay layer skip TINT
Ruling-0 Jul 12, 2026
acc8628
Add BLOCK_TINT to let a plain block shape's icon skip TINT
Ruling-0 Jul 12, 2026
cf293de
Add CELL_TINT to let a container's fill layer skip FLUID_TINT
Ruling-0 Jul 12, 2026
7e7b88c
Trim block rendering docs to contract statements
Ruling-0 Aug 13, 2026
a21ef21
Drive composite block rendering through explicit per-layer icons and …
Ruling-0 Aug 13, 2026
e0392f0
Fix tint docs and trim composite rendering tests
Ruling-0 Aug 13, 2026
06c0e74
Trim log occurence mentions
Ruling-0 Aug 13, 2026
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
3 changes: 3 additions & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ dependencies {

implementation('com.github.GTNewHorizons:Postea:1.2.5:dev')

// Only for the @ThreadSafeISBRH annotation; an unresolvable annotation class is ignored at runtime.
compileOnly('com.github.GTNewHorizons:Angelica:2.1.49:dev') { transitive = false }

// EndlessIDs required for >16 materials
runtimeOnly('com.falsepattern:chunkapi-mc1.7.10:0.8.3:dev')
runtimeOnly('com.falsepattern:endlessids-mc1.7.10:1.7.3:dev')
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/ruling_0/materiallib/ClientProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import net.minecraftforge.client.MinecraftForgeClient;
import net.minecraftforge.common.MinecraftForge;

import com.ruling_0.materiallib.api.ShapeBlock;
import com.ruling_0.materiallib.api.ShapeBlockRenderingHandler;
import com.ruling_0.materiallib.api.ShapeFluidIcons;
import com.ruling_0.materiallib.api.ShapeItem;
import com.ruling_0.materiallib.api.ShapeItemRenderer;
import com.ruling_0.materiallib.api.ShapeRegistry;

import cpw.mods.fml.client.registry.RenderingRegistry;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;

public class ClientProxy extends CommonProxy {
Expand All @@ -19,6 +22,13 @@ public void preInit(FMLPreInitializationEvent event) {
for (ShapeItem item : ShapeRegistry.instance().getItemShapes()) {
MinecraftForgeClient.registerItemRenderer(item, renderer);
}
ShapeBlockRenderingHandler blockRenderer = new ShapeBlockRenderingHandler();
RenderingRegistry.registerBlockHandler(blockRenderer);
for (ShapeBlock block : ShapeRegistry.instance().getBlockShapes()) {
if (block.hasBaseTexture()) {
block.setRenderType(blockRenderer.getRenderId());
}
}
MinecraftForge.EVENT_BUS.register(new ShapeFluidIcons());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ public BlockShapeBuilder variants(String... variants) {

/// Declares the untinted background texture drawn under `variant`'s tinted material icon (e.g. the stone
/// background of an ore), as a `domain:path` icon identifier (`"minecraft:stone"`) independent of any
/// material's texture set; the `blocks/` folder is implicit. `texture` is registered as a second render pass;
/// see [ShapeBlock#canRenderInPass]. Optional -- a variant with no base texture renders as a single tinted
/// layer. `variant` must be one of the names passed to [#variants].
/// material's texture set; the `blocks/` folder is implicit (see
/// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation]).
/// [ShapeBlockRenderingHandler] composites the two layers. Optional -- a variant with no base texture renders
/// as a single tinted layer. `variant` must be one of the names passed to [#variants].
public BlockShapeBuilder variantBase(String variant, String texture) {
Objects.requireNonNull(variant, "variant must not be null");
if (texture == null || texture.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public MaterialBuilder setTint(int tint) {
return setProperty(StandardProperties.TINT, tint);
}

/// Sets [StandardProperties#FLUID_TINT], the ARGB tint applied to a fluid shape's fill icon in place of
/// [StandardProperties#TINT].
public MaterialBuilder setFluidTint(int fluidTint) {
return setProperty(StandardProperties.FLUID_TINT, fluidTint);
}

/// Sets a property value. Rejects [StandardProperties#NAME] and [StandardProperties#TEXTURE_SET].
public <T> MaterialBuilder setProperty(Property<T> property, T value) {
StandardProperties.requireSettable(property, value);
Expand Down
95 changes: 67 additions & 28 deletions src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;

import net.minecraftforge.client.MinecraftForgeClient;
import com.gtnewhorizon.gtnhlib.util.ResourceUtil;
import com.ruling_0.materiallib.MaterialLib;

import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.relauncher.Side;
Expand All @@ -34,11 +35,11 @@
/// instances, and the block's item shows the same display name and advanced-tooltip attribution as an item shape.
///
/// A variant block built by [ShapeBlockVariants] additionally falls back from its own icon (`<shapeName>_<variant>`)
/// to the plain shape name, and may draw an untinted base texture (e.g. a stone background) in the solid render
/// pass, under the tinted material icon drawn in the alpha pass; see [#registerBlockIcons] and [#canRenderInPass].
/// Drops, hardness, resistance, and harvest level may be overridden per material and variant, and the harvest
/// tool class per shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla
/// default it replaces.
/// to the plain shape name, and may draw an untinted base texture (e.g. a stone background) under the tinted
/// material icon, composited by [ShapeBlockRenderingHandler]; see [#registerBlockIcons] and [#hasBaseTexture].
/// Drops, hardness, resistance, and harvest level may be overridden per material and variant, and the harvest tool
/// class per shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla default
/// it replaces.
public class ShapeBlock extends Block implements BackedShape {

private final String modid;
Expand All @@ -54,6 +55,8 @@ public class ShapeBlock extends Block implements BackedShape {
private final ServedMaterials served = new ServedMaterials();
private final ShapeIcons icons = new ShapeIcons(false);
private IIcon baseIcon;
private boolean warnedMissingBaseTexture;
private int renderType = 0;

/// Creates a block shape backed by a [net.minecraft.block.material.Material#iron] block. `oreDicts` are the
/// oredict prefixes, at least one; `displayNameFormat` is applied to the material name to build the display
Expand Down Expand Up @@ -164,36 +167,63 @@ public void registerBlockIcons(IIconRegister register) {
List<String> candidates = groupName != null ? List.of(name, groupName) : List.of(name);
icons.bind(register, served.get(), candidates, this::iconPathFor);
if (baseTexture != null) {
baseIcon = register.registerIcon(baseTexture);
baseIcon = registerBaseIcon(register);
}
}

/// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight
/// to them. The default implementation defers to this block's [BlockIconPather]. A subclass may override this
/// directly.
protected String iconPathFor(Material material) {
return iconPather != null ? iconPather.iconPath(this, material) : null;
/// Registers [#baseTexture] if it names an existing file, or the [ShapeIcons#EMPTY_ICON] placeholder if it
/// does not.
private IIcon registerBaseIcon(IIconRegister register) {
if (ResourceUtil.resourceExists(ResourceUtil.getCompleteBlockTextureResourceLocation(baseTexture))) {
return register.registerIcon(baseTexture);
}
if (!warnedMissingBaseTexture) {
warnedMissingBaseTexture = true;
MaterialLib.LOG.warn(
"Block shape {} variant {} has no base texture at {}; it will render the empty placeholder instead",
name,
variant,
baseTexture);
}
return register.registerIcon(ShapeIcons.EMPTY_ICON);
}

/// A block with no base texture renders as a single tinted layer. A block with a base texture
/// renders in two passes -- the untinted base in the solid pass 0, and the tinted material icon over it in
/// the alpha-blended pass 1 (pass 1 draws after pass 0, and the material texture's transparent pixels let
/// the base show through). The item form never sets an active render pass (pass -1) and shows the tinted
/// material icon; see [#canRenderInPass].
@Override
/// Whether this variant draws a base texture layer under the tinted material icon; see [#registerBlockIcons].
public boolean hasBaseTexture() {
return baseTexture != null;
}

/// Sets the render type [#getRenderType] reports: [ShapeBlockRenderingHandler]'s render ID for a
/// [#hasBaseTexture] composite, or the vanilla full-cube default (0).
@SideOnly(Side.CLIENT)
public int getRenderBlockPass() { return baseTexture != null ? 1 : 0; }
public void setRenderType(int renderType) { this.renderType = renderType; }

@Override
public int getRenderType() { return renderType; }

/// This variant's base texture icon, or null when it declares none.
@SideOnly(Side.CLIENT)
IIcon baseIcon() {
return baseIcon;
}

/// The material icon bound at the given metadata; see [ShapeIcons#get].
@SideOnly(Side.CLIENT)
public boolean canRenderInPass(int pass) {
return baseTexture == null ? pass == 0 : pass == 0 || pass == 1;
IIcon materialIcon(int meta) {
return icons.get(meta);
}

/// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight
/// to them. The default implementation defers to this block's [BlockIconPather]. A subclass may override this
/// directly.
protected String iconPathFor(Material material) {
return iconPather != null ? iconPather.iconPath(this, material) : null;
}

@Override
@SideOnly(Side.CLIENT)
public IIcon getIcon(int side, int meta) {
if (baseTexture != null && MinecraftForgeClient.getRenderPass() == 0) {
if (baseTexture != null) {
return baseIcon;
}
return icons.get(meta);
Expand All @@ -202,24 +232,33 @@ public IIcon getIcon(int side, int meta) {
@Override
@SideOnly(Side.CLIENT)
public int getRenderColor(int meta) {
if (baseTexture != null) {
return 0xFFFFFF;
}
return tintFor(meta);
}

@Override
@SideOnly(Side.CLIENT)
public int colorMultiplier(IBlockAccess world, int x, int y, int z) {
if (baseTexture != null && MinecraftForgeClient.getRenderPass() == 0) {
if (baseTexture != null) {
return 0xFFFFFF;
}
return tintFor(world.getBlockMetadata(x, y, z));
}

/// The RGB tint of the material at the given metadata, or white when the metadata maps to no live material.
/// Block render colors carry no alpha, so the material's ARGB [StandardProperties#TINT] is masked to its low
/// 24 bits.
private static int tintFor(int meta) {
/// The RGB tint of the material at the given metadata, or white when the metadata maps to no live material:
/// [StandardProperties#BLOCK_OVERLAY_TINT] for a [#hasBaseTexture] composite's overlay layer,
/// [StandardProperties#BLOCK_TINT] for a plain block, [StandardProperties#TINT] when the specific property is
/// unset. Block render colors carry no alpha, so the resolved ARGB value is masked to its low 24 bits.
@SideOnly(Side.CLIENT)
int tintFor(int meta) {
Material material = MaterialRegistry.instance().getMaterialByIndex(meta);
return material != null ? material.getProperty(StandardProperties.TINT) & 0xFFFFFF : 0xFFFFFF;
if (material == null) return 0xFFFFFF;
Integer override = material.getProperty(
baseTexture != null ? StandardProperties.BLOCK_OVERLAY_TINT : StandardProperties.BLOCK_TINT);
if (override != null) return override & 0xFFFFFF;
return material.getProperty(StandardProperties.TINT) & 0xFFFFFF;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.ruling_0.materiallib.api;

import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.IIcon;
import net.minecraft.world.IBlockAccess;

import com.gtnewhorizons.angelica.api.ThreadSafeISBRH;

import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler;
import cpw.mods.fml.client.registry.RenderingRegistry;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import org.lwjgl.opengl.GL11;

/// Renders a [ShapeBlock#hasBaseTexture] composite -- an untinted base texture under a tinted material icon -- in
/// world and in every item form (GUI slot, hotbar, held, and dropped). A block with no base texture keeps the
/// vanilla full-cube render type and never reaches this handler; see [ShapeBlock#setRenderType].
///
/// Each layer is drawn with its icon and color passed in explicitly, under a [RenderBlocks] override texture, so
/// the handler holds no state and one shared instance serves every thread of Angelica's off-thread chunk meshing.
/// [#renderInventoryBlock] draws both layers back-to-back into the same [Tessellator] batch; [#renderWorldBlock]
/// makes two standard-block draws with explicit colors. Submitting the coplanar quads with identical vertex data
/// lets the depth test resolve the tie in submission order instead of z-fighting.
///
/// The whole composite draws in the solid chunk pass (the vanilla render-pass defaults), where the alpha test cuts
/// out the overlay's transparent pixels; the overlay icons are cutout textures, not translucent ones, matching
/// legacy GT ore blocks.
@SideOnly(Side.CLIENT)
@ThreadSafeISBRH(perThread = false)
public final class ShapeBlockRenderingHandler implements ISimpleBlockRenderingHandler {

private static final int RENDER_ID = RenderingRegistry.getNextAvailableRenderId();

@Override
public int getRenderId() { return RENDER_ID; }

@Override
public boolean shouldRender3DInInventory(int modelId) {
return true;
}

@Override
public void renderInventoryBlock(Block block, int metadata, int modelId, RenderBlocks renderer) {
if (!(block instanceof ShapeBlock shape)) return;
block.setBlockBoundsForItemRender();
renderer.setRenderBoundsFromBlock(block);

GL11.glRotatef(90.0F, 0.0F, 1.0F, 0.0F);
GL11.glTranslatef(-0.5F, -0.5F, -0.5F);

Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
drawInventoryLayer(renderer, tessellator, shape, shape.baseIcon(), 0xFFFFFF);
drawInventoryLayer(renderer, tessellator, shape, shape.materialIcon(metadata), shape.tintFor(metadata));
tessellator.draw();

GL11.glTranslatef(0.5F, 0.5F, 0.5F);
}

private static void drawInventoryLayer(RenderBlocks renderer, Tessellator tessellator, ShapeBlock shape,
IIcon icon, int color) {
tessellator.setColorOpaque_F(
(color >> 16 & 255) / 255.0F,
(color >> 8 & 255) / 255.0F,
(color & 255) / 255.0F);
drawInventoryFace(renderer, tessellator, shape, 0, icon, 0.0F, -1.0F, 0.0F);
drawInventoryFace(renderer, tessellator, shape, 1, icon, 0.0F, 1.0F, 0.0F);
drawInventoryFace(renderer, tessellator, shape, 2, icon, 0.0F, 0.0F, -1.0F);
drawInventoryFace(renderer, tessellator, shape, 3, icon, 0.0F, 0.0F, 1.0F);
drawInventoryFace(renderer, tessellator, shape, 4, icon, -1.0F, 0.0F, 0.0F);
drawInventoryFace(renderer, tessellator, shape, 5, icon, 1.0F, 0.0F, 0.0F);
}

private static void drawInventoryFace(RenderBlocks renderer, Tessellator tessellator, ShapeBlock shape, int side,
IIcon icon, float nx, float ny, float nz) {
tessellator.setNormal(nx, ny, nz);
switch (side) {
case 0 -> renderer.renderFaceYNeg(shape, 0.0D, 0.0D, 0.0D, icon);
case 1 -> renderer.renderFaceYPos(shape, 0.0D, 0.0D, 0.0D, icon);
case 2 -> renderer.renderFaceZNeg(shape, 0.0D, 0.0D, 0.0D, icon);
case 3 -> renderer.renderFaceZPos(shape, 0.0D, 0.0D, 0.0D, icon);
case 4 -> renderer.renderFaceXNeg(shape, 0.0D, 0.0D, 0.0D, icon);
case 5 -> renderer.renderFaceXPos(shape, 0.0D, 0.0D, 0.0D, icon);
}
}

@Override
public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId,
RenderBlocks renderer) {
if (!(block instanceof ShapeBlock shape)) return false;
// The destroy-progress crack arrives through RenderBlocks.renderBlockUsingTexture, which sets its own
// override texture; a single draw stamps that texture once.
if (renderer.hasOverrideBlockTexture()) return renderer.renderStandardBlock(shape, x, y, z);
int meta = world.getBlockMetadata(x, y, z);
boolean rendered = drawWorldLayer(renderer, shape, x, y, z, shape.baseIcon(), 0xFFFFFF);
drawWorldLayer(renderer, shape, x, y, z, shape.materialIcon(meta), shape.tintFor(meta));
return rendered;
}

/// Draws one layer as a standard block of `icon` tinted `color`, mirroring [RenderBlocks#renderStandardBlock]'s
/// dispatch. That method is not called directly: it takes its color from [ShapeBlock#colorMultiplier], which is
/// white for every composite, in place of the per-layer color.
private static boolean drawWorldLayer(RenderBlocks renderer, ShapeBlock shape, int x, int y, int z, IIcon icon,
int color) {
renderer.setOverrideBlockTexture(icon);
try {
float red = (color >> 16 & 255) / 255.0F;
float green = (color >> 8 & 255) / 255.0F;
float blue = (color & 255) / 255.0F;
if (EntityRenderer.anaglyphEnable) {
float anaglyphRed = (red * 30.0F + green * 59.0F + blue * 11.0F) / 100.0F;
float anaglyphGreen = (red * 30.0F + green * 70.0F) / 100.0F;
float anaglyphBlue = (red * 30.0F + blue * 70.0F) / 100.0F;
red = anaglyphRed;
green = anaglyphGreen;
blue = anaglyphBlue;
}
if (Minecraft.isAmbientOcclusionEnabled() && shape.getLightValue() == 0) {
return renderer.partialRenderBounds ?
renderer.renderStandardBlockWithAmbientOcclusionPartial(shape, x, y, z, red, green, blue) :
renderer.renderStandardBlockWithAmbientOcclusion(shape, x, y, z, red, green, blue);
}
return renderer.renderStandardBlockWithColorMultiplier(shape, x, y, z, red, green, blue);
}
finally {
renderer.clearOverrideBlockTexture();
}
}
}
Loading
Loading