Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
package com.tiedup.remake.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import com.tiedup.remake.blocks.entity.CellCoreBlockEntity;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderStateShard;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.SlabBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.SlabType;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* Block entity renderer for CellCore.
|
||||
*
|
||||
* Renders a small pulsing cyan diamond indicator on the interior face
|
||||
* of the Cell Core block. This helps players identify which side of the
|
||||
* block faces into the cell, and confirms the block is a Cell Core.
|
||||
*
|
||||
* Extends RenderStateShard to access protected render state fields
|
||||
* (standard Forge pattern for custom RenderTypes).
|
||||
*/
|
||||
public class CellCoreRenderer
|
||||
extends RenderStateShard
|
||||
implements BlockEntityRenderer<CellCoreBlockEntity>
|
||||
{
|
||||
|
||||
private static final float DIAMOND_SIZE = 0.15f;
|
||||
private static final float FACE_OFFSET = 0.001f;
|
||||
|
||||
// Cyan indicator color (#44FFFF)
|
||||
private static final float COLOR_R = 0.267f;
|
||||
private static final float COLOR_G = 1.0f;
|
||||
private static final float COLOR_B = 1.0f;
|
||||
|
||||
/**
|
||||
* Custom RenderType: POSITION_COLOR quads with translucent blending, no texture.
|
||||
* Avoids block atlas UV issues that made the diamond invisible.
|
||||
*/
|
||||
private static final RenderType INDICATOR = RenderType.create(
|
||||
"tiedup_indicator",
|
||||
DefaultVertexFormat.POSITION_COLOR,
|
||||
VertexFormat.Mode.QUADS,
|
||||
256,
|
||||
false,
|
||||
true,
|
||||
RenderType.CompositeState.builder()
|
||||
.setShaderState(
|
||||
new ShaderStateShard(GameRenderer::getPositionColorShader)
|
||||
)
|
||||
.setTransparencyState(TRANSLUCENT_TRANSPARENCY)
|
||||
.setCullState(NO_CULL)
|
||||
.setDepthTestState(LEQUAL_DEPTH_TEST)
|
||||
.setWriteMaskState(COLOR_DEPTH_WRITE)
|
||||
.createCompositeState(false)
|
||||
);
|
||||
|
||||
public CellCoreRenderer(BlockEntityRendererProvider.Context context) {
|
||||
super("tiedup_cell_core_renderer", () -> {}, () -> {});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(
|
||||
CellCoreBlockEntity blockEntity,
|
||||
float partialTick,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource bufferSource,
|
||||
int packedLight,
|
||||
int packedOverlay
|
||||
) {
|
||||
Direction interiorFace = blockEntity.getInteriorFace();
|
||||
if (interiorFace == null) return;
|
||||
if (blockEntity.getLevel() == null) return;
|
||||
|
||||
// Calculate pulsing alpha synced to game time (pauses when game pauses)
|
||||
float time = blockEntity.getLevel().getGameTime() + partialTick;
|
||||
float alpha = 0.4f + 0.2f * (float) Math.sin(time * 0.15);
|
||||
|
||||
// Compute vertical center: adapt to slab shape if disguised as a slab
|
||||
float centerY = 0.5f;
|
||||
BlockState disguise = blockEntity.getDisguiseState();
|
||||
if (disguise == null && blockEntity.getLevel() != null) {
|
||||
// Auto-detect: check resolved model data (same logic as CellCoreBlockEntity)
|
||||
disguise = blockEntity
|
||||
.getModelData()
|
||||
.get(
|
||||
com.tiedup.remake.client.model.CellCoreBakedModel.DISGUISE_PROPERTY
|
||||
);
|
||||
}
|
||||
if (disguise != null && disguise.getBlock() instanceof SlabBlock) {
|
||||
SlabType slabType = disguise.getValue(SlabBlock.TYPE);
|
||||
if (slabType == SlabType.BOTTOM) {
|
||||
centerY = 0.25f; // lower half
|
||||
} else if (slabType == SlabType.TOP) {
|
||||
centerY = 0.75f; // upper half
|
||||
}
|
||||
// DOUBLE = 0.5f (full block)
|
||||
}
|
||||
|
||||
poseStack.pushPose();
|
||||
|
||||
VertexConsumer consumer = bufferSource.getBuffer(INDICATOR);
|
||||
Matrix4f pose = poseStack.last().pose();
|
||||
|
||||
renderDiamond(consumer, pose, interiorFace, alpha, centerY);
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private void renderDiamond(
|
||||
VertexConsumer consumer,
|
||||
Matrix4f pose,
|
||||
Direction face,
|
||||
float alpha,
|
||||
float centerY
|
||||
) {
|
||||
float[][] verts = getDiamondVertices(face, 0.5f, centerY, 0.5f);
|
||||
|
||||
for (float[] v : verts) {
|
||||
consumer
|
||||
.vertex(pose, v[0], v[1], v[2])
|
||||
.color(COLOR_R, COLOR_G, COLOR_B, alpha)
|
||||
.endVertex();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 4 vertices of a diamond shape on the given face.
|
||||
* The diamond is centered on the face and offset slightly outward to avoid z-fighting.
|
||||
*/
|
||||
private float[][] getDiamondVertices(
|
||||
Direction face,
|
||||
float cx,
|
||||
float cy,
|
||||
float cz
|
||||
) {
|
||||
float s = DIAMOND_SIZE;
|
||||
float o = FACE_OFFSET;
|
||||
|
||||
return switch (face) {
|
||||
case NORTH -> new float[][] {
|
||||
{ cx, cy + s, 0.0f - o }, // top
|
||||
{ cx + s, cy, 0.0f - o }, // right
|
||||
{ cx, cy - s, 0.0f - o }, // bottom
|
||||
{ cx - s, cy, 0.0f - o }, // left
|
||||
};
|
||||
case SOUTH -> new float[][] {
|
||||
{ cx, cy + s, 1.0f + o },
|
||||
{ cx - s, cy, 1.0f + o },
|
||||
{ cx, cy - s, 1.0f + o },
|
||||
{ cx + s, cy, 1.0f + o },
|
||||
};
|
||||
case WEST -> new float[][] {
|
||||
{ 0.0f - o, cy + s, cz },
|
||||
{ 0.0f - o, cy, cz - s },
|
||||
{ 0.0f - o, cy - s, cz },
|
||||
{ 0.0f - o, cy, cz + s },
|
||||
};
|
||||
case EAST -> new float[][] {
|
||||
{ 1.0f + o, cy + s, cz },
|
||||
{ 1.0f + o, cy, cz + s },
|
||||
{ 1.0f + o, cy - s, cz },
|
||||
{ 1.0f + o, cy, cz - s },
|
||||
};
|
||||
case DOWN -> {
|
||||
// Bottom face: y=0.0 for full blocks & bottom slabs, y=0.5 for top slabs only
|
||||
float downY = (cy >= 0.75f) ? 0.5f - o : 0.0f - o;
|
||||
yield new float[][] {
|
||||
{ cx, downY, cz + s },
|
||||
{ cx + s, downY, cz },
|
||||
{ cx, downY, cz - s },
|
||||
{ cx - s, downY, cz },
|
||||
};
|
||||
}
|
||||
case UP -> {
|
||||
// Top face: y=1.0 for full blocks & top slabs, y=0.5 for bottom slabs only
|
||||
float upY = (cy <= 0.25f) ? 0.5f + o : 1.0f + o;
|
||||
yield new float[][] {
|
||||
{ cx, upY, cz - s },
|
||||
{ cx + s, upY, cz },
|
||||
{ cx, upY, cz + s },
|
||||
{ cx - s, upY, cz },
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package com.tiedup.remake.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.*;
|
||||
import com.tiedup.remake.cells.CellDataV2;
|
||||
import com.tiedup.remake.cells.MarkerType;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* Utility class for rendering cell outlines in the world.
|
||||
*
|
||||
* Simplified renderer - only filled blocks, no wireframe outlines.
|
||||
* Each marker type gets a semi-transparent colored block overlay.
|
||||
* Spawn point has a pulsating effect.
|
||||
*
|
||||
* Features:
|
||||
* - Depth test enabled (blocks hidden behind world geometry)
|
||||
* - Simple filled block rendering per marker type
|
||||
* - Pulsating spawn point indicator
|
||||
*/
|
||||
public class CellOutlineRenderer {
|
||||
|
||||
/** Enable depth test (true = blocks hidden behind world blocks) */
|
||||
public static boolean DEPTH_TEST_ENABLED = true;
|
||||
|
||||
// Simple colors per type (RGBA)
|
||||
private static final float[] COLOR_WALL = { 0.3f, 0.5f, 1.0f, 1.0f }; // Blue
|
||||
private static final float[] COLOR_ANCHOR = { 1.0f, 0.2f, 0.2f, 1.0f }; // Red
|
||||
private static final float[] COLOR_BED = { 0.9f, 0.4f, 0.9f, 1.0f }; // Violet
|
||||
private static final float[] COLOR_DOOR = { 0.2f, 0.9f, 0.9f, 1.0f }; // Cyan
|
||||
private static final float[] COLOR_ENTRANCE = { 0.2f, 1.0f, 0.4f, 1.0f }; // Green
|
||||
private static final float[] COLOR_PATROL = { 1.0f, 1.0f, 0.2f, 1.0f }; // Yellow
|
||||
private static final float[] COLOR_LOOT = { 1.0f, 0.7f, 0.0f, 1.0f }; // Gold
|
||||
private static final float[] COLOR_SPAWNER = { 0.8f, 0.1f, 0.1f, 1.0f }; // Dark red
|
||||
private static final float[] COLOR_TRADER_SPAWN = {
|
||||
1.0f,
|
||||
0.84f,
|
||||
0.0f,
|
||||
1.0f,
|
||||
}; // Gold
|
||||
private static final float[] COLOR_MAID_SPAWN = {
|
||||
1.0f,
|
||||
0.41f,
|
||||
0.71f,
|
||||
1.0f,
|
||||
}; // Hot pink
|
||||
private static final float[] COLOR_MERCHANT_SPAWN = {
|
||||
0.2f,
|
||||
0.9f,
|
||||
0.9f,
|
||||
1.0f,
|
||||
}; // Cyan
|
||||
private static final float[] COLOR_DELIVERY = { 1.0f, 0.8f, 0.2f, 1.0f }; // Orange/Yellow
|
||||
private static final float[] COLOR_SPAWN = { 1.0f, 0.0f, 1.0f, 1.0f }; // Magenta
|
||||
private static final float[] COLOR_WAYPOINT = { 0.0f, 1.0f, 0.5f, 1.0f }; // Bright green
|
||||
|
||||
/**
|
||||
* Render filled blocks for all positions in a cell.
|
||||
* Renders V2 cell data: walls, anchors, beds, doors, and spawn/core positions.
|
||||
*
|
||||
* @param poseStack The pose stack from the render event
|
||||
* @param cell The cell data to render
|
||||
* @param camera The camera for view offset calculation
|
||||
*/
|
||||
public static void renderCellOutlines(
|
||||
PoseStack poseStack,
|
||||
CellDataV2 cell,
|
||||
Camera camera
|
||||
) {
|
||||
if (cell == null) return;
|
||||
|
||||
Vec3 cameraPos = camera.getPosition();
|
||||
|
||||
// Setup rendering state
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.defaultBlendFunc();
|
||||
RenderSystem.enableDepthTest();
|
||||
RenderSystem.depthMask(false);
|
||||
RenderSystem.setShader(GameRenderer::getPositionColorShader);
|
||||
|
||||
// 1. Render spawn point or core pos (pulsating magenta)
|
||||
BlockPos spawnPoint =
|
||||
cell.getSpawnPoint() != null
|
||||
? cell.getSpawnPoint()
|
||||
: cell.getCorePos();
|
||||
float pulse =
|
||||
0.4f + 0.2f * (float) Math.sin(System.currentTimeMillis() / 300.0);
|
||||
float[] spawnColor = {
|
||||
COLOR_SPAWN[0],
|
||||
COLOR_SPAWN[1],
|
||||
COLOR_SPAWN[2],
|
||||
pulse,
|
||||
};
|
||||
renderFilledBlock(poseStack, spawnPoint, cameraPos, spawnColor);
|
||||
|
||||
// 2. Render wall blocks
|
||||
renderPositionCollection(
|
||||
poseStack,
|
||||
cell.getWallBlocks(),
|
||||
cameraPos,
|
||||
COLOR_WALL
|
||||
);
|
||||
|
||||
// 3. Render anchors
|
||||
renderPositionCollection(
|
||||
poseStack,
|
||||
cell.getAnchors(),
|
||||
cameraPos,
|
||||
COLOR_ANCHOR
|
||||
);
|
||||
|
||||
// 4. Render beds
|
||||
renderPositionCollection(
|
||||
poseStack,
|
||||
cell.getBeds(),
|
||||
cameraPos,
|
||||
COLOR_BED
|
||||
);
|
||||
|
||||
// 5. Render doors
|
||||
renderPositionCollection(
|
||||
poseStack,
|
||||
cell.getDoors(),
|
||||
cameraPos,
|
||||
COLOR_DOOR
|
||||
);
|
||||
|
||||
// 6. Render delivery point if present
|
||||
BlockPos deliveryPoint = cell.getDeliveryPoint();
|
||||
if (deliveryPoint != null) {
|
||||
float[] deliveryFillColor = {
|
||||
COLOR_DELIVERY[0],
|
||||
COLOR_DELIVERY[1],
|
||||
COLOR_DELIVERY[2],
|
||||
0.35f,
|
||||
};
|
||||
renderFilledBlock(
|
||||
poseStack,
|
||||
deliveryPoint,
|
||||
cameraPos,
|
||||
deliveryFillColor
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Render path waypoints with pulsating effect and numbers
|
||||
java.util.List<BlockPos> waypoints = cell.getPathWaypoints();
|
||||
if (!waypoints.isEmpty()) {
|
||||
float waypointPulse =
|
||||
0.5f +
|
||||
0.3f * (float) Math.sin(System.currentTimeMillis() / 200.0);
|
||||
float[] waypointColor = {
|
||||
COLOR_WAYPOINT[0],
|
||||
COLOR_WAYPOINT[1],
|
||||
COLOR_WAYPOINT[2],
|
||||
waypointPulse,
|
||||
};
|
||||
|
||||
for (int i = 0; i < waypoints.size(); i++) {
|
||||
BlockPos wp = waypoints.get(i);
|
||||
renderFilledBlock(poseStack, wp, cameraPos, waypointColor);
|
||||
// Render number above waypoint
|
||||
renderWaypointNumber(poseStack, wp, cameraPos, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore rendering state
|
||||
RenderSystem.depthMask(true);
|
||||
RenderSystem.disableBlend();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a collection of positions with the given base color at 0.35 alpha.
|
||||
*/
|
||||
private static void renderPositionCollection(
|
||||
PoseStack poseStack,
|
||||
Collection<BlockPos> positions,
|
||||
Vec3 cameraPos,
|
||||
float[] baseColor
|
||||
) {
|
||||
if (positions == null || positions.isEmpty()) return;
|
||||
|
||||
float[] fillColor = { baseColor[0], baseColor[1], baseColor[2], 0.35f };
|
||||
|
||||
for (BlockPos pos : positions) {
|
||||
renderFilledBlock(poseStack, pos, cameraPos, fillColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a filled block (semi-transparent cube).
|
||||
*/
|
||||
public static void renderFilledBlock(
|
||||
PoseStack poseStack,
|
||||
BlockPos pos,
|
||||
Vec3 cameraPos,
|
||||
float[] color
|
||||
) {
|
||||
poseStack.pushPose();
|
||||
|
||||
double x = pos.getX() - cameraPos.x;
|
||||
double y = pos.getY() - cameraPos.y;
|
||||
double z = pos.getZ() - cameraPos.z;
|
||||
|
||||
poseStack.translate(x, y, z);
|
||||
|
||||
Matrix4f matrix = poseStack.last().pose();
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder buffer = tesselator.getBuilder();
|
||||
|
||||
buffer.begin(
|
||||
VertexFormat.Mode.QUADS,
|
||||
DefaultVertexFormat.POSITION_COLOR
|
||||
);
|
||||
|
||||
float r = color[0];
|
||||
float g = color[1];
|
||||
float b = color[2];
|
||||
float a = color[3];
|
||||
|
||||
float min = 0.0f;
|
||||
float max = 1.0f;
|
||||
|
||||
// Bottom face
|
||||
vertex(buffer, matrix, min, min, min, r, g, b, a);
|
||||
vertex(buffer, matrix, max, min, min, r, g, b, a);
|
||||
vertex(buffer, matrix, max, min, max, r, g, b, a);
|
||||
vertex(buffer, matrix, min, min, max, r, g, b, a);
|
||||
|
||||
// Top face
|
||||
vertex(buffer, matrix, min, max, min, r, g, b, a);
|
||||
vertex(buffer, matrix, min, max, max, r, g, b, a);
|
||||
vertex(buffer, matrix, max, max, max, r, g, b, a);
|
||||
vertex(buffer, matrix, max, max, min, r, g, b, a);
|
||||
|
||||
// North face
|
||||
vertex(buffer, matrix, min, min, min, r, g, b, a);
|
||||
vertex(buffer, matrix, min, max, min, r, g, b, a);
|
||||
vertex(buffer, matrix, max, max, min, r, g, b, a);
|
||||
vertex(buffer, matrix, max, min, min, r, g, b, a);
|
||||
|
||||
// South face
|
||||
vertex(buffer, matrix, min, min, max, r, g, b, a);
|
||||
vertex(buffer, matrix, max, min, max, r, g, b, a);
|
||||
vertex(buffer, matrix, max, max, max, r, g, b, a);
|
||||
vertex(buffer, matrix, min, max, max, r, g, b, a);
|
||||
|
||||
// West face
|
||||
vertex(buffer, matrix, min, min, min, r, g, b, a);
|
||||
vertex(buffer, matrix, min, min, max, r, g, b, a);
|
||||
vertex(buffer, matrix, min, max, max, r, g, b, a);
|
||||
vertex(buffer, matrix, min, max, min, r, g, b, a);
|
||||
|
||||
// East face
|
||||
vertex(buffer, matrix, max, min, min, r, g, b, a);
|
||||
vertex(buffer, matrix, max, max, min, r, g, b, a);
|
||||
vertex(buffer, matrix, max, max, max, r, g, b, a);
|
||||
vertex(buffer, matrix, max, min, max, r, g, b, a);
|
||||
|
||||
tesselator.end();
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
private static void vertex(
|
||||
BufferBuilder buffer,
|
||||
Matrix4f matrix,
|
||||
float x,
|
||||
float y,
|
||||
float z,
|
||||
float r,
|
||||
float g,
|
||||
float b,
|
||||
float a
|
||||
) {
|
||||
buffer.vertex(matrix, x, y, z).color(r, g, b, a).endVertex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a waypoint number floating above the block.
|
||||
*/
|
||||
public static void renderWaypointNumber(
|
||||
PoseStack poseStack,
|
||||
BlockPos pos,
|
||||
Vec3 cameraPos,
|
||||
int number
|
||||
) {
|
||||
poseStack.pushPose();
|
||||
|
||||
double x = pos.getX() + 0.5 - cameraPos.x;
|
||||
double y = pos.getY() + 1.5 - cameraPos.y;
|
||||
double z = pos.getZ() + 0.5 - cameraPos.z;
|
||||
|
||||
poseStack.translate(x, y, z);
|
||||
|
||||
// Billboard effect - face the camera
|
||||
net.minecraft.client.Minecraft mc =
|
||||
net.minecraft.client.Minecraft.getInstance();
|
||||
poseStack.mulPose(mc.getEntityRenderDispatcher().cameraOrientation());
|
||||
poseStack.scale(-0.05f, -0.05f, 0.05f);
|
||||
|
||||
// Render the number
|
||||
String text = String.valueOf(number);
|
||||
net.minecraft.client.gui.Font font = mc.font;
|
||||
float textWidth = font.width(text);
|
||||
|
||||
// Background
|
||||
net.minecraft.client.renderer.MultiBufferSource.BufferSource buffer = mc
|
||||
.renderBuffers()
|
||||
.bufferSource();
|
||||
|
||||
font.drawInBatch(
|
||||
text,
|
||||
-textWidth / 2,
|
||||
0,
|
||||
0x00FF80, // Bright green
|
||||
false,
|
||||
poseStack.last().pose(),
|
||||
buffer,
|
||||
net.minecraft.client.gui.Font.DisplayMode.NORMAL,
|
||||
0x80000000, // Semi-transparent black background
|
||||
15728880
|
||||
);
|
||||
buffer.endBatch();
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color for a marker type.
|
||||
*/
|
||||
public static float[] getColorForType(MarkerType type) {
|
||||
return switch (type) {
|
||||
case WALL -> COLOR_WALL;
|
||||
case ANCHOR -> COLOR_ANCHOR;
|
||||
case BED -> COLOR_BED;
|
||||
case DOOR -> COLOR_DOOR;
|
||||
case DELIVERY -> COLOR_DELIVERY;
|
||||
case ENTRANCE -> COLOR_ENTRANCE;
|
||||
case PATROL -> COLOR_PATROL;
|
||||
case LOOT -> COLOR_LOOT;
|
||||
case SPAWNER -> COLOR_SPAWNER;
|
||||
case TRADER_SPAWN -> COLOR_TRADER_SPAWN;
|
||||
case MAID_SPAWN -> COLOR_MAID_SPAWN;
|
||||
case MERCHANT_SPAWN -> COLOR_MERCHANT_SPAWN;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the spawn point color.
|
||||
*/
|
||||
public static float[] getSpawnColor() {
|
||||
return COLOR_SPAWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package com.tiedup.remake.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import com.tiedup.remake.client.animation.render.RenderConstants;
|
||||
import com.tiedup.remake.client.model.DamselModel;
|
||||
import com.tiedup.remake.compat.wildfire.WildfireCompat;
|
||||
import com.tiedup.remake.compat.wildfire.render.WildfireDamselLayer;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import net.minecraft.client.model.HumanoidArmorModel;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.entity.HumanoidMobRenderer;
|
||||
import net.minecraft.client.renderer.entity.layers.HumanoidArmorLayer;
|
||||
import net.minecraft.client.renderer.entity.layers.ItemInHandLayer;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Renderer for AbstractTiedUpNpc and all subtypes (Kidnapper, Elite, Archer, Merchant, Shiny).
|
||||
*
|
||||
* <p>Uses ISkinnedEntity interface for polymorphic texture lookup.
|
||||
* Each entity subclass overrides getSkinTexture() to return the appropriate texture.
|
||||
*
|
||||
* <p><b>Issue #19 fix:</b> Replaced 6+ instanceof checks with single interface call.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class DamselRenderer
|
||||
extends HumanoidMobRenderer<AbstractTiedUpNpc, DamselModel>
|
||||
{
|
||||
|
||||
/**
|
||||
* Normal arms model (4px wide - Steve model).
|
||||
*/
|
||||
private final DamselModel normalModel;
|
||||
|
||||
/**
|
||||
* Slim arms model (3px wide - Alex model).
|
||||
*/
|
||||
private final DamselModel slimModel;
|
||||
|
||||
/**
|
||||
* Create renderer.
|
||||
*
|
||||
* Phase 19: Uses vanilla ModelLayers.PLAYER for full layer support (jacket, sleeves, pants).
|
||||
*/
|
||||
public DamselRenderer(EntityRendererProvider.Context context) {
|
||||
super(
|
||||
context,
|
||||
new DamselModel(context.bakeLayer(ModelLayers.PLAYER), false),
|
||||
0.5f // Shadow radius
|
||||
);
|
||||
// Store both models for runtime swapping
|
||||
this.normalModel = this.getModel();
|
||||
this.slimModel = new DamselModel(
|
||||
context.bakeLayer(ModelLayers.PLAYER_SLIM),
|
||||
true
|
||||
);
|
||||
|
||||
// Add armor render layer (renders equipped armor)
|
||||
this.addLayer(
|
||||
new HumanoidArmorLayer<>(
|
||||
this,
|
||||
new HumanoidArmorModel<>(
|
||||
context.bakeLayer(ModelLayers.PLAYER_INNER_ARMOR)
|
||||
),
|
||||
new HumanoidArmorModel<>(
|
||||
context.bakeLayer(ModelLayers.PLAYER_OUTER_ARMOR)
|
||||
),
|
||||
context.getModelManager()
|
||||
)
|
||||
);
|
||||
|
||||
// Add item in hand layer (renders held items)
|
||||
this.addLayer(
|
||||
new ItemInHandLayer<>(this, context.getItemInHandRenderer())
|
||||
);
|
||||
|
||||
// Add Wildfire breast layer BEFORE bondage (so bondage renders on top of breasts)
|
||||
if (WildfireCompat.isLoaded()) {
|
||||
this.addLayer(
|
||||
new WildfireDamselLayer<>(this, context.getModelSet())
|
||||
);
|
||||
}
|
||||
|
||||
// Add V2 bondage render layer (GLB-based V2 equipment rendering)
|
||||
this.addLayer(new com.tiedup.remake.v2.bondage.client.V2BondageRenderLayer<>(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the entity.
|
||||
* Uses entity's hasSlimArms() for model selection.
|
||||
*
|
||||
* Phase 19: Wearer layer hiding is now handled in DamselModel.setupAnim()
|
||||
* to ensure it happens after visibility resets.
|
||||
*
|
||||
* DOG pose: X rotation is applied in setupRotations() AFTER Y rotation,
|
||||
* so the "belly down" direction follows entity facing.
|
||||
* Head compensation is applied in DamselModel.setupAnim().
|
||||
* Body rotation smoothing is handled in AbstractTiedUpNpc.tick().
|
||||
*/
|
||||
@Override
|
||||
public void render(
|
||||
AbstractTiedUpNpc entity,
|
||||
float entityYaw,
|
||||
float partialTicks,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight
|
||||
) {
|
||||
// Use entity's hasSlimArms() - each entity type overrides this appropriately
|
||||
boolean useSlim = entity.hasSlimArms();
|
||||
|
||||
// Swap to appropriate model
|
||||
this.model = useSlim ? this.slimModel : this.normalModel;
|
||||
|
||||
// Apply vertical offset for sitting/kneeling/dog poses
|
||||
// This ensures the model AND all layers (gag, blindfold, etc.) move together
|
||||
float verticalOffset = getVerticalOffset(entity);
|
||||
boolean pushedPose = false;
|
||||
if (verticalOffset != 0) {
|
||||
poseStack.pushPose();
|
||||
pushedPose = true;
|
||||
// Convert from model units (16 = 1 block) to render units
|
||||
poseStack.translate(0, verticalOffset / 16.0, 0);
|
||||
}
|
||||
|
||||
// Call parent render
|
||||
// Note: Wearer layer hiding happens in DamselModel.setupAnim()
|
||||
super.render(
|
||||
entity,
|
||||
entityYaw,
|
||||
partialTicks,
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight
|
||||
);
|
||||
|
||||
if (pushedPose) {
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vertical offset for sitting/kneeling/dog poses.
|
||||
* Returns offset in model units (16 units = 1 block).
|
||||
*
|
||||
* @param entity The entity to check
|
||||
* @return Vertical offset (negative = down)
|
||||
*/
|
||||
private float getVerticalOffset(AbstractTiedUpNpc entity) {
|
||||
if (entity.isSitting()) {
|
||||
return RenderConstants.DAMSEL_SIT_OFFSET;
|
||||
} else if (entity.isKneeling()) {
|
||||
return RenderConstants.DAMSEL_KNEEL_OFFSET;
|
||||
} else if (entity.isDogPose()) {
|
||||
return RenderConstants.DAMSEL_DOG_OFFSET;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the texture location based on entity type.
|
||||
*
|
||||
* <p>Issue #19 fix: Uses ISkinnedEntity interface instead of instanceof cascade.
|
||||
* Each entity subclass implements getSkinTexture() to return appropriate texture.
|
||||
*/
|
||||
@Override
|
||||
public ResourceLocation getTextureLocation(AbstractTiedUpNpc entity) {
|
||||
// ISkinnedEntity provides polymorphic skin texture lookup
|
||||
// Each entity type (Damsel, Kidnapper, Elite, Archer, Merchant, Shiny)
|
||||
// overrides getSkinTexture() to return the correct texture
|
||||
return entity.getSkinTexture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply scale transformation.
|
||||
*/
|
||||
@Override
|
||||
protected void scale(
|
||||
AbstractTiedUpNpc entity,
|
||||
PoseStack poseStack,
|
||||
float partialTick
|
||||
) {
|
||||
poseStack.scale(0.9375f, 0.9375f, 0.9375f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup rotations for the entity.
|
||||
*
|
||||
* DOG pose: After Y rotation is applied by parent, add X rotation
|
||||
* to make the body horizontal. This is applied in entity-local space,
|
||||
* so the "belly down" direction follows the entity's facing.
|
||||
*/
|
||||
@Override
|
||||
protected void setupRotations(
|
||||
AbstractTiedUpNpc entity,
|
||||
PoseStack poseStack,
|
||||
float ageInTicks,
|
||||
float rotationYaw,
|
||||
float partialTicks
|
||||
) {
|
||||
// Call parent to apply Y rotation (body facing)
|
||||
super.setupRotations(
|
||||
entity,
|
||||
poseStack,
|
||||
ageInTicks,
|
||||
rotationYaw,
|
||||
partialTicks
|
||||
);
|
||||
|
||||
// DOG pose: Apply X rotation to make body horizontal
|
||||
// This happens AFTER Y rotation, so it's in entity-local space
|
||||
if (entity.isDogPose()) {
|
||||
// Rotate -90° on X axis around the model's pivot point
|
||||
// Pivot at waist height (12 model units = 0.75 blocks up from feet)
|
||||
poseStack.translate(0, 0.75, 0);
|
||||
poseStack.mulPose(Axis.XP.rotationDegrees(-90));
|
||||
poseStack.translate(0, -0.75, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.tiedup.remake.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import com.tiedup.remake.blocks.ModBlocks;
|
||||
import com.tiedup.remake.entities.EntityKidnapBomb;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.BlockRenderDispatcher;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.entity.TntMinecartRenderer;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
/**
|
||||
* Renderer for EntityKidnapBomb.
|
||||
*
|
||||
* Phase 16: Blocks
|
||||
*
|
||||
* Renders the primed kidnap bomb using our custom block texture.
|
||||
*/
|
||||
public class KidnapBombRenderer extends EntityRenderer<EntityKidnapBomb> {
|
||||
|
||||
private final BlockRenderDispatcher blockRenderer;
|
||||
|
||||
public KidnapBombRenderer(EntityRendererProvider.Context context) {
|
||||
super(context);
|
||||
this.shadowRadius = 0.5F;
|
||||
this.blockRenderer = context.getBlockRenderDispatcher();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(
|
||||
EntityKidnapBomb entity,
|
||||
float entityYaw,
|
||||
float partialTicks,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight
|
||||
) {
|
||||
poseStack.pushPose();
|
||||
poseStack.translate(0.0F, 0.5F, 0.0F);
|
||||
|
||||
int fuse = entity.getFuse();
|
||||
if ((float) fuse - partialTicks + 1.0F < 10.0F) {
|
||||
float scale = 1.0F - ((float) fuse - partialTicks + 1.0F) / 10.0F;
|
||||
scale = Mth.clamp(scale, 0.0F, 1.0F);
|
||||
scale *= scale;
|
||||
scale *= scale;
|
||||
float expand = 1.0F + scale * 0.3F;
|
||||
poseStack.scale(expand, expand, expand);
|
||||
}
|
||||
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(-90.0F));
|
||||
poseStack.translate(-0.5F, -0.5F, 0.5F);
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(90.0F));
|
||||
|
||||
// Render our custom block instead of vanilla TNT
|
||||
TntMinecartRenderer.renderWhiteSolidBlock(
|
||||
this.blockRenderer,
|
||||
ModBlocks.KIDNAP_BOMB.get().defaultBlockState(),
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight,
|
||||
(fuse / 5) % 2 == 0
|
||||
);
|
||||
|
||||
poseStack.popPose();
|
||||
super.render(
|
||||
entity,
|
||||
entityYaw,
|
||||
partialTicks,
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public ResourceLocation getTextureLocation(EntityKidnapBomb entity) {
|
||||
return TextureAtlas.LOCATION_BLOCKS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.tiedup.remake.client.renderer;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Axis;
|
||||
import com.tiedup.remake.entities.NpcFishingBobber;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* Renderer for NpcFishingBobber.
|
||||
*
|
||||
* Renders a textured quad using the vanilla fishing hook texture.
|
||||
* Billboard-style: always faces the camera.
|
||||
*/
|
||||
public class NpcFishingBobberRenderer extends EntityRenderer<NpcFishingBobber> {
|
||||
|
||||
private static final ResourceLocation TEXTURE = new ResourceLocation(
|
||||
"textures/entity/fishing_hook.png"
|
||||
);
|
||||
private static final RenderType RENDER_TYPE = RenderType.entityCutout(
|
||||
TEXTURE
|
||||
);
|
||||
|
||||
public NpcFishingBobberRenderer(EntityRendererProvider.Context ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(
|
||||
NpcFishingBobber entity,
|
||||
float yaw,
|
||||
float partialTick,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource bufferSource,
|
||||
int packedLight
|
||||
) {
|
||||
poseStack.pushPose();
|
||||
poseStack.scale(0.5F, 0.5F, 0.5F);
|
||||
poseStack.mulPose(this.entityRenderDispatcher.cameraOrientation());
|
||||
poseStack.mulPose(Axis.YP.rotationDegrees(180.0F));
|
||||
|
||||
VertexConsumer vertexConsumer = bufferSource.getBuffer(RENDER_TYPE);
|
||||
PoseStack.Pose pose = poseStack.last();
|
||||
Matrix4f matrix4f = pose.pose();
|
||||
Matrix3f matrix3f = pose.normal();
|
||||
|
||||
vertex(vertexConsumer, matrix4f, matrix3f, packedLight, 0.0F, 0, 0, 1);
|
||||
vertex(vertexConsumer, matrix4f, matrix3f, packedLight, 1.0F, 0, 1, 1);
|
||||
vertex(vertexConsumer, matrix4f, matrix3f, packedLight, 1.0F, 1, 1, 0);
|
||||
vertex(vertexConsumer, matrix4f, matrix3f, packedLight, 0.0F, 1, 0, 0);
|
||||
|
||||
poseStack.popPose();
|
||||
super.render(
|
||||
entity,
|
||||
yaw,
|
||||
partialTick,
|
||||
poseStack,
|
||||
bufferSource,
|
||||
packedLight
|
||||
);
|
||||
}
|
||||
|
||||
private static void vertex(
|
||||
VertexConsumer consumer,
|
||||
Matrix4f matrix4f,
|
||||
Matrix3f matrix3f,
|
||||
int packedLight,
|
||||
float x,
|
||||
int y,
|
||||
int u,
|
||||
int v
|
||||
) {
|
||||
consumer
|
||||
.vertex(matrix4f, x - 0.5F, y - 0.5F, 0.0F)
|
||||
.color(255, 255, 255, 255)
|
||||
.uv((float) u, (float) v)
|
||||
.overlayCoords(OverlayTexture.NO_OVERLAY)
|
||||
.uv2(packedLight)
|
||||
.normal(matrix3f, 0.0F, 1.0F, 0.0F)
|
||||
.endVertex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getTextureLocation(NpcFishingBobber entity) {
|
||||
return TEXTURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.tiedup.remake.client.renderer;
|
||||
|
||||
import com.tiedup.remake.entities.EntityRopeArrow;
|
||||
import net.minecraft.client.renderer.entity.ArrowRenderer;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Renderer for EntityRopeArrow.
|
||||
* Phase 15: Uses vanilla arrow texture for simplicity.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class RopeArrowRenderer extends ArrowRenderer<EntityRopeArrow> {
|
||||
|
||||
/** Texture for the rope arrow (uses vanilla arrow texture) */
|
||||
private static final ResourceLocation TEXTURE =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
"minecraft",
|
||||
"textures/entity/projectiles/arrow.png"
|
||||
);
|
||||
|
||||
public RopeArrowRenderer(EntityRendererProvider.Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getTextureLocation(EntityRopeArrow entity) {
|
||||
return TEXTURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.tiedup.remake.client.renderer.layers;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import net.minecraft.client.model.PlayerModel;
|
||||
import net.minecraft.client.model.geom.EntityModelSet;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Cache for PlayerModel instances used for clothes rendering.
|
||||
*
|
||||
* <p>Clothes textures are standard Minecraft skins (64x64) and need to be
|
||||
* rendered using PlayerModel with correct UV mappings, NOT the bondage item models.
|
||||
*
|
||||
* <p>Two models are cached:
|
||||
* <ul>
|
||||
* <li>Normal (Steve) - 4px wide arms</li>
|
||||
* <li>Slim (Alex) - 3px wide arms</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Initialize via {@link #init(EntityModelSet)} during AddLayers event.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class ClothesModelCache {
|
||||
|
||||
private static PlayerModel<AbstractClientPlayer> normalModel;
|
||||
private static PlayerModel<AbstractClientPlayer> slimModel;
|
||||
private static boolean initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize the model cache.
|
||||
* Must be called during EntityRenderersEvent.AddLayers.
|
||||
*
|
||||
* @param modelSet The entity model set from the event
|
||||
*/
|
||||
public static void init(EntityModelSet modelSet) {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
normalModel = new PlayerModel<>(
|
||||
modelSet.bakeLayer(ModelLayers.PLAYER),
|
||||
false
|
||||
);
|
||||
slimModel = new PlayerModel<>(
|
||||
modelSet.bakeLayer(ModelLayers.PLAYER_SLIM),
|
||||
true
|
||||
);
|
||||
initialized = true;
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[ClothesModelCache] Initialized normal and slim player models for clothes rendering"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate player model for clothes rendering.
|
||||
*
|
||||
* @param slim true for slim (Alex) arms, false for normal (Steve) arms
|
||||
* @return The cached PlayerModel
|
||||
*/
|
||||
public static PlayerModel<AbstractClientPlayer> getModel(boolean slim) {
|
||||
if (!initialized) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[ClothesModelCache] Not initialized! Returning null."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return slim ? slimModel : normalModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache is initialized.
|
||||
*/
|
||||
public static boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
package com.tiedup.remake.client.renderer.layers;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.tiedup.remake.client.state.ClothesClientCache;
|
||||
import com.tiedup.remake.client.texture.DynamicTextureManager;
|
||||
import com.tiedup.remake.items.clothes.ClothesProperties;
|
||||
import com.tiedup.remake.items.clothes.GenericClothes;
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.client.model.HumanoidModel;
|
||||
import net.minecraft.client.model.PlayerModel;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Helper class for rendering clothes with dynamic textures.
|
||||
*
|
||||
* <p>IMPORTANT: Clothes are rendered using a PlayerModel (not bondage item model)
|
||||
* because clothes textures are standard Minecraft skins (64x64) with player UV mappings.
|
||||
*
|
||||
* <p>Handles:
|
||||
* <ul>
|
||||
* <li>Dynamic texture URL rendering</li>
|
||||
* <li>Full-skin mode (covers entire player model)</li>
|
||||
* <li>Small arms mode (Alex/slim arms)</li>
|
||||
* <li>Layer visibility control</li>
|
||||
* <li>Steve vs Alex model selection</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>This is called from BondageItemRenderLayer when rendering CLOTHES slot.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class ClothesRenderHelper {
|
||||
|
||||
/**
|
||||
* Attempt to render clothes with dynamic texture.
|
||||
* Uses PlayerModel (not bondage model) because clothes use skin UV mappings.
|
||||
*
|
||||
* @param clothes The clothes ItemStack
|
||||
* @param entity The entity wearing the clothes
|
||||
* @param poseStack The pose stack
|
||||
* @param buffer The render buffer
|
||||
* @param packedLight The packed light value
|
||||
* @param packedOverlay The packed overlay value (for hit flash effect)
|
||||
* @param parentModel The parent PlayerModel to copy pose from
|
||||
* @return true if dynamic texture was rendered, false otherwise
|
||||
*/
|
||||
public static boolean tryRenderDynamic(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay,
|
||||
PlayerModel<?> parentModel
|
||||
) {
|
||||
if (
|
||||
clothes.isEmpty() ||
|
||||
!(clothes.getItem() instanceof GenericClothes gc)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if ClothesModelCache is initialized
|
||||
if (!ClothesModelCache.isInitialized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get properties from equipped clothes or remote player cache
|
||||
ClothesProperties props = getClothesProperties(clothes, entity);
|
||||
if (props == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String dynamicUrl = props.dynamicTextureUrl();
|
||||
if (dynamicUrl == null || dynamicUrl.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get texture from manager
|
||||
DynamicTextureManager texManager = DynamicTextureManager.getInstance();
|
||||
ResourceLocation texLocation = texManager.getTextureLocation(
|
||||
dynamicUrl,
|
||||
props.fullSkin()
|
||||
);
|
||||
|
||||
if (texLocation == null) {
|
||||
// Texture not loaded yet (downloading)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine if we should use slim (Alex) arms
|
||||
boolean useSlim = shouldUseSlimArms(clothes, entity, props);
|
||||
|
||||
// Get the appropriate PlayerModel from cache
|
||||
PlayerModel<AbstractClientPlayer> clothesModel =
|
||||
ClothesModelCache.getModel(useSlim);
|
||||
if (clothesModel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy pose from parent model to clothes model
|
||||
copyPose(parentModel, clothesModel);
|
||||
|
||||
// Apply layer visibility settings
|
||||
applyLayerVisibility(
|
||||
clothesModel,
|
||||
props.visibleLayers(),
|
||||
props.keepHead()
|
||||
);
|
||||
|
||||
// Render the clothes model with dynamic texture
|
||||
// Use entityTranslucent for layered rendering (clothes on top of player)
|
||||
VertexConsumer vertexConsumer = buffer.getBuffer(
|
||||
RenderType.entityTranslucent(texLocation)
|
||||
);
|
||||
clothesModel.renderToBuffer(
|
||||
poseStack,
|
||||
vertexConsumer,
|
||||
packedLight,
|
||||
packedOverlay,
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
|
||||
// Restore visibility (model is shared, need to reset for next render)
|
||||
restoreLayerVisibility(clothesModel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render clothes for NPCs (Damsel, etc.)
|
||||
* Uses PlayerModel from cache, copies pose from parent HumanoidModel.
|
||||
*
|
||||
* @param clothes The clothes ItemStack
|
||||
* @param entity The NPC entity wearing clothes
|
||||
* @param poseStack The pose stack
|
||||
* @param buffer The render buffer
|
||||
* @param packedLight The packed light value
|
||||
* @param packedOverlay The packed overlay value (for hit flash effect)
|
||||
* @param parentModel The parent HumanoidModel to copy pose from
|
||||
* @param hasSlimArms Whether the NPC uses slim arms
|
||||
* @return true if rendered successfully
|
||||
*/
|
||||
public static boolean tryRenderDynamicForNPC(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay,
|
||||
HumanoidModel<?> parentModel,
|
||||
boolean hasSlimArms
|
||||
) {
|
||||
if (
|
||||
clothes.isEmpty() || !(clothes.getItem() instanceof GenericClothes)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ClothesModelCache.isInitialized()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get properties directly from item (NPCs don't use remote cache)
|
||||
ClothesProperties props = ClothesProperties.fromStack(clothes);
|
||||
if (props == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String dynamicUrl = props.dynamicTextureUrl();
|
||||
if (dynamicUrl == null || dynamicUrl.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get texture from manager
|
||||
ResourceLocation texLocation =
|
||||
DynamicTextureManager.getInstance().getTextureLocation(
|
||||
dynamicUrl,
|
||||
props.fullSkin()
|
||||
);
|
||||
if (texLocation == null) {
|
||||
return false; // Still downloading
|
||||
}
|
||||
|
||||
// Use slim if: clothes force it OR NPC has slim arms
|
||||
boolean useSlim = props.smallArms() || hasSlimArms;
|
||||
|
||||
// Get PlayerModel from cache (same cache as players)
|
||||
PlayerModel<AbstractClientPlayer> clothesModel =
|
||||
ClothesModelCache.getModel(useSlim);
|
||||
if (clothesModel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy pose from HumanoidModel (NPC) to PlayerModel (clothes)
|
||||
copyPoseFromHumanoid(parentModel, clothesModel);
|
||||
|
||||
// Apply layer visibility
|
||||
applyLayerVisibility(
|
||||
clothesModel,
|
||||
props.visibleLayers(),
|
||||
props.keepHead()
|
||||
);
|
||||
|
||||
// Render
|
||||
VertexConsumer vertexConsumer = buffer.getBuffer(
|
||||
RenderType.entityTranslucent(texLocation)
|
||||
);
|
||||
clothesModel.renderToBuffer(
|
||||
poseStack,
|
||||
vertexConsumer,
|
||||
packedLight,
|
||||
packedOverlay,
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
|
||||
// Restore visibility
|
||||
restoreLayerVisibility(clothesModel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy pose from HumanoidModel (NPC) to PlayerModel (clothes).
|
||||
* PlayerModel has extra parts (jacket, sleeves, pants) that HumanoidModel lacks,
|
||||
* so we copy those from the corresponding base parts.
|
||||
*/
|
||||
private static void copyPoseFromHumanoid(
|
||||
HumanoidModel<?> source,
|
||||
PlayerModel<?> dest
|
||||
) {
|
||||
// Base parts (exist in both models)
|
||||
dest.head.copyFrom(source.head);
|
||||
dest.hat.copyFrom(source.hat);
|
||||
dest.body.copyFrom(source.body);
|
||||
dest.rightArm.copyFrom(source.rightArm);
|
||||
dest.leftArm.copyFrom(source.leftArm);
|
||||
dest.rightLeg.copyFrom(source.rightLeg);
|
||||
dest.leftLeg.copyFrom(source.leftLeg);
|
||||
|
||||
// PlayerModel-only parts: copy from corresponding base parts
|
||||
dest.jacket.copyFrom(source.body);
|
||||
dest.leftSleeve.copyFrom(source.leftArm);
|
||||
dest.rightSleeve.copyFrom(source.rightArm);
|
||||
dest.leftPants.copyFrom(source.leftLeg);
|
||||
dest.rightPants.copyFrom(source.rightLeg);
|
||||
|
||||
// Animation flags
|
||||
dest.crouching = source.crouching;
|
||||
dest.riding = source.riding;
|
||||
dest.young = source.young;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy pose from source PlayerModel to destination PlayerModel.
|
||||
* This ensures the clothes model matches the player's current pose/animation.
|
||||
*/
|
||||
private static void copyPose(PlayerModel<?> source, PlayerModel<?> dest) {
|
||||
// Main body parts
|
||||
dest.head.copyFrom(source.head);
|
||||
dest.hat.copyFrom(source.hat);
|
||||
dest.body.copyFrom(source.body);
|
||||
dest.rightArm.copyFrom(source.rightArm);
|
||||
dest.leftArm.copyFrom(source.leftArm);
|
||||
dest.rightLeg.copyFrom(source.rightLeg);
|
||||
dest.leftLeg.copyFrom(source.leftLeg);
|
||||
|
||||
// Outer layer parts (jacket, sleeves, pants)
|
||||
dest.jacket.copyFrom(source.jacket);
|
||||
dest.leftSleeve.copyFrom(source.leftSleeve);
|
||||
dest.rightSleeve.copyFrom(source.rightSleeve);
|
||||
dest.leftPants.copyFrom(source.leftPants);
|
||||
dest.rightPants.copyFrom(source.rightPants);
|
||||
|
||||
// Copy other properties
|
||||
dest.crouching = source.crouching;
|
||||
dest.young = source.young;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if slim (Alex) arms should be used.
|
||||
* Priority: 1) Clothes force small arms, 2) Player's actual model type
|
||||
*/
|
||||
private static boolean shouldUseSlimArms(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity,
|
||||
ClothesProperties props
|
||||
) {
|
||||
// 1. Check if clothes item forces small arms
|
||||
if (props.smallArms()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Check local item setting
|
||||
if (clothes.getItem() instanceof GenericClothes gc) {
|
||||
if (gc.shouldForceSmallArms(clothes)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Follow player's actual model type
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
String modelName = player.getModelName();
|
||||
return "slim".equals(modelName);
|
||||
}
|
||||
|
||||
return false; // Default: normal (Steve) arms
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clothes properties, checking both equipped item and remote player cache.
|
||||
*/
|
||||
@Nullable
|
||||
private static ClothesProperties getClothesProperties(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity
|
||||
) {
|
||||
// First try to get from the equipped item directly
|
||||
ClothesProperties localProps = ClothesProperties.fromStack(clothes);
|
||||
|
||||
// If entity is a player, also check remote cache (for other players' clothes)
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
UUID playerUUID = player.getUUID();
|
||||
ClothesClientCache.CachedClothesData cached =
|
||||
ClothesClientCache.getPlayerClothes(playerUUID);
|
||||
|
||||
if (cached != null && cached.hasDynamicTexture()) {
|
||||
// Use cached data (synced from server)
|
||||
return new ClothesProperties(
|
||||
cached.dynamicUrl(),
|
||||
cached.fullSkin(),
|
||||
cached.smallArms(),
|
||||
cached.keepHead(),
|
||||
cached.visibleLayers()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return localProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply layer visibility from clothes properties.
|
||||
* Controls which parts of the outer layer (jacket, sleeves, pants) are visible.
|
||||
*
|
||||
* @param model The clothes PlayerModel
|
||||
* @param visible Which layers are enabled on the clothes
|
||||
* @param keepHead If true, hide the clothes head/hat (wearer's head shows instead)
|
||||
*/
|
||||
private static void applyLayerVisibility(
|
||||
PlayerModel<?> model,
|
||||
EnumSet<ClothesProperties.LayerPart> visible,
|
||||
boolean keepHead
|
||||
) {
|
||||
// Main body parts visibility
|
||||
// If keepHead is true, hide clothes head so wearer's head shows through
|
||||
model.head.visible = !keepHead;
|
||||
model.body.visible = true;
|
||||
model.rightArm.visible = true;
|
||||
model.leftArm.visible = true;
|
||||
model.rightLeg.visible = true;
|
||||
model.leftLeg.visible = true;
|
||||
|
||||
// Outer layer parts controlled by settings
|
||||
// If keepHead is true, hide clothes hat so wearer's hat shows through
|
||||
model.hat.visible =
|
||||
!keepHead && visible.contains(ClothesProperties.LayerPart.HEAD);
|
||||
model.jacket.visible = visible.contains(
|
||||
ClothesProperties.LayerPart.BODY
|
||||
);
|
||||
model.leftSleeve.visible = visible.contains(
|
||||
ClothesProperties.LayerPart.LEFT_ARM
|
||||
);
|
||||
model.rightSleeve.visible = visible.contains(
|
||||
ClothesProperties.LayerPart.RIGHT_ARM
|
||||
);
|
||||
model.leftPants.visible = visible.contains(
|
||||
ClothesProperties.LayerPart.LEFT_LEG
|
||||
);
|
||||
model.rightPants.visible = visible.contains(
|
||||
ClothesProperties.LayerPart.RIGHT_LEG
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all layer visibility to default (all visible).
|
||||
* Important because the model is cached and shared.
|
||||
*/
|
||||
private static void restoreLayerVisibility(PlayerModel<?> model) {
|
||||
model.head.visible = true;
|
||||
model.hat.visible = true;
|
||||
model.body.visible = true;
|
||||
model.jacket.visible = true;
|
||||
model.rightArm.visible = true;
|
||||
model.leftArm.visible = true;
|
||||
model.rightSleeve.visible = true;
|
||||
model.leftSleeve.visible = true;
|
||||
model.rightLeg.visible = true;
|
||||
model.leftLeg.visible = true;
|
||||
model.rightPants.visible = true;
|
||||
model.leftPants.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if clothes should force small arms rendering.
|
||||
* Public method for BondageItemRenderLayer.
|
||||
*/
|
||||
public static boolean shouldUseSmallArms(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity
|
||||
) {
|
||||
if (
|
||||
clothes.isEmpty() ||
|
||||
!(clothes.getItem() instanceof GenericClothes gc)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check local item
|
||||
if (gc.shouldForceSmallArms(clothes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check remote cache
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
return ClothesClientCache.isSmallArmsForced(player.getUUID());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if clothes have a dynamic texture (for deciding render path).
|
||||
*/
|
||||
public static boolean hasDynamicTexture(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity
|
||||
) {
|
||||
if (
|
||||
clothes.isEmpty() ||
|
||||
!(clothes.getItem() instanceof GenericClothes gc)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check local item
|
||||
String localUrl = gc.getDynamicTextureUrl(clothes);
|
||||
if (localUrl != null && !localUrl.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check remote cache
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
String cachedUrl = ClothesClientCache.getPlayerDynamicUrl(
|
||||
player.getUUID()
|
||||
);
|
||||
return cachedUrl != null && !cachedUrl.isEmpty();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if full-skin mode is enabled for clothes.
|
||||
*/
|
||||
public static boolean isFullSkinMode(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity
|
||||
) {
|
||||
if (
|
||||
clothes.isEmpty() ||
|
||||
!(clothes.getItem() instanceof GenericClothes gc)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check local item
|
||||
if (gc.isFullSkinEnabled(clothes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check remote cache
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
return ClothesClientCache.isFullSkinEnabled(player.getUUID());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== Wearer Layer Hiding ====================
|
||||
|
||||
/**
|
||||
* Hide wearer's outer layers when clothes are equipped.
|
||||
* Called BEFORE rendering the base model.
|
||||
*
|
||||
* <p>Logic: When clothes are equipped, hide ALL wearer's outer layers.
|
||||
* The clothes will render their own layers on top.
|
||||
* Exception: If keepHead is enabled, the head/hat layers remain visible.
|
||||
*
|
||||
* @param model The wearer's PlayerModel
|
||||
* @param props Clothes properties (used to confirm clothes are valid)
|
||||
* @return Original visibility state for restoration (6 booleans)
|
||||
*/
|
||||
public static boolean[] hideWearerLayers(
|
||||
PlayerModel<?> model,
|
||||
ClothesProperties props
|
||||
) {
|
||||
if (props == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Save original state
|
||||
boolean[] original = {
|
||||
model.hat.visible,
|
||||
model.jacket.visible,
|
||||
model.leftSleeve.visible,
|
||||
model.rightSleeve.visible,
|
||||
model.leftPants.visible,
|
||||
model.rightPants.visible,
|
||||
};
|
||||
|
||||
// When wearing clothes, hide wearer's outer layers
|
||||
// Exception: if keepHead is true, don't hide head/hat
|
||||
if (!props.keepHead()) {
|
||||
model.hat.visible = false;
|
||||
}
|
||||
model.jacket.visible = false;
|
||||
model.leftSleeve.visible = false;
|
||||
model.rightSleeve.visible = false;
|
||||
model.leftPants.visible = false;
|
||||
model.rightPants.visible = false;
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore wearer's layer visibility after rendering.
|
||||
*
|
||||
* @param model The wearer's PlayerModel
|
||||
* @param original Original visibility state from hideWearerLayers()
|
||||
*/
|
||||
public static void restoreWearerLayers(
|
||||
PlayerModel<?> model,
|
||||
boolean[] original
|
||||
) {
|
||||
if (original == null || original.length != 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.hat.visible = original[0];
|
||||
model.jacket.visible = original[1];
|
||||
model.leftSleeve.visible = original[2];
|
||||
model.rightSleeve.visible = original[3];
|
||||
model.leftPants.visible = original[4];
|
||||
model.rightPants.visible = original[5];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clothes properties for wearer layer hiding.
|
||||
* Works for both players (with remote cache) and NPCs (direct from item).
|
||||
*
|
||||
* @param clothes The clothes ItemStack
|
||||
* @param entity The entity wearing clothes
|
||||
* @return ClothesProperties or null if not available
|
||||
*/
|
||||
public static ClothesProperties getPropsForLayerHiding(
|
||||
ItemStack clothes,
|
||||
LivingEntity entity
|
||||
) {
|
||||
if (
|
||||
clothes.isEmpty() || !(clothes.getItem() instanceof GenericClothes)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For players, check remote cache first
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
ClothesClientCache.CachedClothesData cached =
|
||||
ClothesClientCache.getPlayerClothes(player.getUUID());
|
||||
if (cached != null) {
|
||||
return new ClothesProperties(
|
||||
cached.dynamicUrl(),
|
||||
cached.fullSkin(),
|
||||
cached.smallArms(),
|
||||
cached.keepHead(),
|
||||
cached.visibleLayers()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to direct item properties
|
||||
return ClothesProperties.fromStack(clothes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.tiedup.remake.client.renderer.models;
|
||||
|
||||
import net.minecraft.client.model.geom.ModelLayerLocation;
|
||||
import net.minecraft.client.model.geom.PartPose;
|
||||
import net.minecraft.client.model.geom.builders.CubeDeformation;
|
||||
import net.minecraft.client.model.geom.builders.CubeListBuilder;
|
||||
import net.minecraft.client.model.geom.builders.LayerDefinition;
|
||||
import net.minecraft.client.model.geom.builders.MeshDefinition;
|
||||
import net.minecraft.client.model.geom.builders.PartDefinition;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public class BondageLayerDefinitions {
|
||||
|
||||
public static final ModelLayerLocation BONDAGE_LAYER =
|
||||
new ModelLayerLocation(
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "bondage_layer"),
|
||||
"main"
|
||||
);
|
||||
|
||||
public static final ModelLayerLocation BONDAGE_LAYER_SLIM =
|
||||
new ModelLayerLocation(
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
"bondage_layer_slim"
|
||||
),
|
||||
"main"
|
||||
);
|
||||
|
||||
/**
|
||||
* Create bondage layer for normal arms (4px wide - Steve model).
|
||||
*/
|
||||
public static LayerDefinition createBodyLayer() {
|
||||
return createBondageLayer(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create bondage layer for slim arms (3px wide - Alex model).
|
||||
*/
|
||||
public static LayerDefinition createSlimBodyLayer() {
|
||||
return createBondageLayer(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create bondage layer with specified arm width.
|
||||
*
|
||||
* @param armWidth Arm width in pixels (4 for Steve, 3 for Alex)
|
||||
*/
|
||||
private static LayerDefinition createBondageLayer(int armWidth) {
|
||||
MeshDefinition meshdefinition = new MeshDefinition();
|
||||
PartDefinition partdefinition = meshdefinition.getRoot();
|
||||
|
||||
// Inflation for tight fit on body
|
||||
CubeDeformation deformation = new CubeDeformation(0.35F);
|
||||
|
||||
// Head at standard position
|
||||
partdefinition.addOrReplaceChild(
|
||||
"head",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(0, 0)
|
||||
.addBox(-4.0F, -8.0F, -4.0F, 8.0F, 8.0F, 8.0F, deformation),
|
||||
PartPose.offset(0.0F, 0.0F, 0.0F)
|
||||
);
|
||||
|
||||
partdefinition.addOrReplaceChild(
|
||||
"hat",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(32, 0)
|
||||
.addBox(
|
||||
-4.0F,
|
||||
-8.0F,
|
||||
-4.0F,
|
||||
8.0F,
|
||||
8.0F,
|
||||
8.0F,
|
||||
deformation.extend(0.1F)
|
||||
),
|
||||
PartPose.offset(0.0F, 0.0F, 0.0F)
|
||||
);
|
||||
|
||||
partdefinition.addOrReplaceChild(
|
||||
"body",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(16, 16)
|
||||
.addBox(-4.0F, 0.0F, -2.0F, 8.0F, 12.0F, 4.0F, deformation),
|
||||
PartPose.offset(0.0F, 0.0F, 0.0F)
|
||||
);
|
||||
|
||||
// Arms - width varies based on model type
|
||||
float armWidthF = (float) armWidth;
|
||||
float rightArmX = -(armWidthF - 1.0F); // -3 for 4px, -2 for 3px
|
||||
|
||||
partdefinition.addOrReplaceChild(
|
||||
"right_arm",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(40, 16)
|
||||
.addBox(
|
||||
rightArmX,
|
||||
-2.0F,
|
||||
-2.0F,
|
||||
armWidthF,
|
||||
12.0F,
|
||||
4.0F,
|
||||
deformation
|
||||
),
|
||||
PartPose.offset(-5.0F, 2.0F, 0.0F)
|
||||
);
|
||||
|
||||
partdefinition.addOrReplaceChild(
|
||||
"left_arm",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(40, 16)
|
||||
.mirror()
|
||||
.addBox(
|
||||
-1.0F,
|
||||
-2.0F,
|
||||
-2.0F,
|
||||
armWidthF,
|
||||
12.0F,
|
||||
4.0F,
|
||||
deformation
|
||||
),
|
||||
PartPose.offset(5.0F, 2.0F, 0.0F)
|
||||
);
|
||||
|
||||
partdefinition.addOrReplaceChild(
|
||||
"right_leg",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(0, 16)
|
||||
.addBox(-2.0F, 0.0F, -2.0F, 4.0F, 12.0F, 4.0F, deformation),
|
||||
PartPose.offset(-1.9F, 12.0F, 0.0F)
|
||||
);
|
||||
|
||||
partdefinition.addOrReplaceChild(
|
||||
"left_leg",
|
||||
CubeListBuilder.create()
|
||||
.texOffs(0, 16)
|
||||
.mirror()
|
||||
.addBox(-2.0F, 0.0F, -2.0F, 4.0F, 12.0F, 4.0F, deformation),
|
||||
PartPose.offset(1.9F, 12.0F, 0.0F)
|
||||
);
|
||||
|
||||
return LayerDefinition.create(meshdefinition, 64, 32);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.tiedup.remake.client.renderer.obj;
|
||||
|
||||
/**
|
||||
* Immutable record representing a triangular face in an OBJ model.
|
||||
* Contains exactly 3 vertices forming a triangle.
|
||||
*/
|
||||
public record ObjFace(ObjVertex v0, ObjVertex v1, ObjVertex v2) {
|
||||
/**
|
||||
* Get the vertices as an array for iteration.
|
||||
*/
|
||||
public ObjVertex[] vertices() {
|
||||
return new ObjVertex[] { v0, v1, v2 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the face normal from vertices (cross product of edges).
|
||||
* Useful when the OBJ file doesn't provide normals.
|
||||
*/
|
||||
public float[] calculateNormal() {
|
||||
// Edge vectors
|
||||
float e1x = v1.x() - v0.x();
|
||||
float e1y = v1.y() - v0.y();
|
||||
float e1z = v1.z() - v0.z();
|
||||
|
||||
float e2x = v2.x() - v0.x();
|
||||
float e2y = v2.y() - v0.y();
|
||||
float e2z = v2.z() - v0.z();
|
||||
|
||||
// Cross product
|
||||
float nx = e1y * e2z - e1z * e2y;
|
||||
float ny = e1z * e2x - e1x * e2z;
|
||||
float nz = e1x * e2y - e1y * e2x;
|
||||
|
||||
// Normalize
|
||||
float len = (float) Math.sqrt(nx * nx + ny * ny + nz * nz);
|
||||
if (len > 0.0001f) {
|
||||
nx /= len;
|
||||
ny /= len;
|
||||
nz /= len;
|
||||
}
|
||||
|
||||
return new float[] { nx, ny, nz };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.tiedup.remake.client.renderer.obj;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Immutable record representing a material from an MTL file.
|
||||
* Contains the material name, diffuse color (Kd), and optional diffuse texture (map_Kd).
|
||||
*/
|
||||
public record ObjMaterial(
|
||||
String name,
|
||||
float r,
|
||||
float g,
|
||||
float b,
|
||||
@Nullable String texturePath
|
||||
) {
|
||||
/**
|
||||
* Default white material used when no material is specified.
|
||||
*/
|
||||
public static final ObjMaterial DEFAULT = new ObjMaterial(
|
||||
"default",
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f,
|
||||
null
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a material with just color (no texture).
|
||||
*/
|
||||
public ObjMaterial(String name, float r, float g, float b) {
|
||||
this(name, r, g, b, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a material with grayscale color.
|
||||
*/
|
||||
public static ObjMaterial ofGrayscale(String name, float value) {
|
||||
return new ObjMaterial(name, value, value, value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this material has a diffuse texture.
|
||||
*/
|
||||
public boolean hasTexture() {
|
||||
return texturePath != null && !texturePath.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the color as a packed ARGB int (with full alpha).
|
||||
*/
|
||||
public int toARGB() {
|
||||
int ri = (int) (r * 255) & 0xFF;
|
||||
int gi = (int) (g * 255) & 0xFF;
|
||||
int bi = (int) (b * 255) & 0xFF;
|
||||
return 0xFF000000 | (ri << 16) | (gi << 8) | bi;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package com.tiedup.remake.client.renderer.obj;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Container for a loaded OBJ model.
|
||||
* Holds faces grouped by material and material definitions.
|
||||
* Immutable after construction.
|
||||
*/
|
||||
public class ObjModel {
|
||||
|
||||
private final Map<String, List<ObjFace>> facesByMaterial;
|
||||
private final Map<String, ObjMaterial> materials;
|
||||
private final int totalFaces;
|
||||
private final int totalVertices;
|
||||
|
||||
/** Base path for resolving relative texture paths (e.g., "tiedup:textures/models/obj/") */
|
||||
@Nullable
|
||||
private final String textureBasePath;
|
||||
|
||||
// Bounding box (AABB)
|
||||
private final float minX, minY, minZ;
|
||||
private final float maxX, maxY, maxZ;
|
||||
|
||||
private ObjModel(Builder builder) {
|
||||
this.facesByMaterial = Collections.unmodifiableMap(
|
||||
new HashMap<>(builder.facesByMaterial)
|
||||
);
|
||||
this.materials = Collections.unmodifiableMap(
|
||||
new HashMap<>(builder.materials)
|
||||
);
|
||||
this.textureBasePath = builder.textureBasePath;
|
||||
|
||||
// Calculate totals
|
||||
int faces = 0;
|
||||
for (List<ObjFace> faceList : this.facesByMaterial.values()) {
|
||||
faces += faceList.size();
|
||||
}
|
||||
this.totalFaces = faces;
|
||||
this.totalVertices = faces * 3;
|
||||
|
||||
// Calculate AABB
|
||||
float minX = Float.MAX_VALUE,
|
||||
minY = Float.MAX_VALUE,
|
||||
minZ = Float.MAX_VALUE;
|
||||
float maxX = -Float.MAX_VALUE,
|
||||
maxY = -Float.MAX_VALUE,
|
||||
maxZ = -Float.MAX_VALUE;
|
||||
|
||||
for (List<ObjFace> faceList : this.facesByMaterial.values()) {
|
||||
for (ObjFace face : faceList) {
|
||||
for (ObjVertex v : face.vertices()) {
|
||||
minX = Math.min(minX, v.x());
|
||||
minY = Math.min(minY, v.y());
|
||||
minZ = Math.min(minZ, v.z());
|
||||
maxX = Math.max(maxX, v.x());
|
||||
maxY = Math.max(maxY, v.y());
|
||||
maxZ = Math.max(maxZ, v.z());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.minX = minX == Float.MAX_VALUE ? 0 : minX;
|
||||
this.minY = minY == Float.MAX_VALUE ? 0 : minY;
|
||||
this.minZ = minZ == Float.MAX_VALUE ? 0 : minZ;
|
||||
this.maxX = maxX == -Float.MAX_VALUE ? 0 : maxX;
|
||||
this.maxY = maxY == -Float.MAX_VALUE ? 0 : maxY;
|
||||
this.maxZ = maxZ == -Float.MAX_VALUE ? 0 : maxZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all faces grouped by material name.
|
||||
*/
|
||||
public Map<String, List<ObjFace>> getFacesByMaterial() {
|
||||
return facesByMaterial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a material by name.
|
||||
* Returns default white material if not found.
|
||||
*/
|
||||
public ObjMaterial getMaterial(String name) {
|
||||
return materials.getOrDefault(name, ObjMaterial.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all materials.
|
||||
*/
|
||||
public Map<String, ObjMaterial> getMaterials() {
|
||||
return materials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of triangles in the model.
|
||||
*/
|
||||
public int getTotalFaces() {
|
||||
return totalFaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of vertices (faces * 3).
|
||||
*/
|
||||
public int getTotalVertices() {
|
||||
return totalVertices;
|
||||
}
|
||||
|
||||
// AABB getters
|
||||
public float getMinX() {
|
||||
return minX;
|
||||
}
|
||||
|
||||
public float getMinY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
public float getMinZ() {
|
||||
return minZ;
|
||||
}
|
||||
|
||||
public float getMaxX() {
|
||||
return maxX;
|
||||
}
|
||||
|
||||
public float getMaxY() {
|
||||
return maxY;
|
||||
}
|
||||
|
||||
public float getMaxZ() {
|
||||
return maxZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the center point of the model's bounding box.
|
||||
*/
|
||||
public float[] getCenter() {
|
||||
return new float[] {
|
||||
(minX + maxX) / 2f,
|
||||
(minY + maxY) / 2f,
|
||||
(minZ + maxZ) / 2f,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of the bounding box.
|
||||
*/
|
||||
public float[] getDimensions() {
|
||||
return new float[] { maxX - minX, maxY - minY, maxZ - minZ };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a texture filename to a full ResourceLocation.
|
||||
* Uses the textureBasePath set during loading.
|
||||
*
|
||||
* @param filename The texture filename (e.g., "ball_gag.png")
|
||||
* @return Full ResourceLocation, or null if no base path is set
|
||||
*/
|
||||
@Nullable
|
||||
public ResourceLocation resolveTexture(String filename) {
|
||||
if (textureBasePath == null || filename == null) {
|
||||
return null;
|
||||
}
|
||||
return ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
textureBasePath + filename
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a texture filename with a color suffix inserted before the extension.
|
||||
* Example: "texture.png" + "red" -> "texture_red.png"
|
||||
*
|
||||
* @param filename The base texture filename (e.g., "texture.png")
|
||||
* @param colorSuffix The color suffix to insert (e.g., "red"), or null for no suffix
|
||||
* @return Full ResourceLocation with color suffix, or null if no base path is set
|
||||
*/
|
||||
@Nullable
|
||||
public ResourceLocation resolveTextureWithColorSuffix(
|
||||
String filename,
|
||||
@Nullable String colorSuffix
|
||||
) {
|
||||
if (textureBasePath == null || filename == null) {
|
||||
return null;
|
||||
}
|
||||
if (colorSuffix == null || colorSuffix.isEmpty()) {
|
||||
return resolveTexture(filename);
|
||||
}
|
||||
// Insert color suffix before extension: "texture.png" -> "texture_red.png"
|
||||
int dotIndex = filename.lastIndexOf('.');
|
||||
String newFilename;
|
||||
if (dotIndex > 0) {
|
||||
newFilename =
|
||||
filename.substring(0, dotIndex) +
|
||||
"_" +
|
||||
colorSuffix +
|
||||
filename.substring(dotIndex);
|
||||
} else {
|
||||
newFilename = filename + "_" + colorSuffix;
|
||||
}
|
||||
return ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
textureBasePath + newFilename
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the texture base path.
|
||||
*/
|
||||
@Nullable
|
||||
public String getTextureBasePath() {
|
||||
return textureBasePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for constructing ObjModel instances.
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private final Map<String, List<ObjFace>> facesByMaterial =
|
||||
new HashMap<>();
|
||||
private final Map<String, ObjMaterial> materials = new HashMap<>();
|
||||
private String textureBasePath;
|
||||
|
||||
public Builder addFaces(String materialName, List<ObjFace> faces) {
|
||||
if (faces != null && !faces.isEmpty()) {
|
||||
facesByMaterial.put(materialName, List.copyOf(faces));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addMaterial(ObjMaterial material) {
|
||||
if (material != null) {
|
||||
materials.put(material.name(), material);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTextureBasePath(String path) {
|
||||
this.textureBasePath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ObjModel build() {
|
||||
return new ObjModel(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
package com.tiedup.remake.client.renderer.obj;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Parses .obj and .mtl files from ResourceLocations.
|
||||
* Supports:
|
||||
* - Vertex positions (v)
|
||||
* - Texture coordinates (vt)
|
||||
* - Vertex normals (vn)
|
||||
* - Faces (f) with v/vt/vn format, triangles and quads
|
||||
* - Material library (mtllib)
|
||||
* - Material assignment (usemtl)
|
||||
* - MTL diffuse color (Kd)
|
||||
*/
|
||||
public class ObjModelLoader {
|
||||
|
||||
private ObjModelLoader() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an OBJ model from a ResourceLocation.
|
||||
*
|
||||
* @param location ResourceLocation pointing to the .obj file
|
||||
* @return Loaded ObjModel, or null if loading failed
|
||||
*/
|
||||
@Nullable
|
||||
public static ObjModel load(ResourceLocation location) {
|
||||
ResourceManager resourceManager =
|
||||
Minecraft.getInstance().getResourceManager();
|
||||
|
||||
try {
|
||||
Optional<Resource> resourceOpt = resourceManager.getResource(
|
||||
location
|
||||
);
|
||||
if (resourceOpt.isEmpty()) {
|
||||
TiedUpMod.LOGGER.warn("OBJ file not found: {}", location);
|
||||
return null;
|
||||
}
|
||||
|
||||
Resource resource = resourceOpt.get();
|
||||
|
||||
// Parse context
|
||||
List<float[]> positions = new ArrayList<>();
|
||||
List<float[]> texCoords = new ArrayList<>();
|
||||
List<float[]> normals = new ArrayList<>();
|
||||
Map<String, List<ObjFace>> facesByMaterial = new HashMap<>();
|
||||
Map<String, ObjMaterial> materials = new HashMap<>();
|
||||
|
||||
String currentMaterial = "default";
|
||||
String mtlLib = null;
|
||||
|
||||
// Parse OBJ file
|
||||
try (
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
resource.open(),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = line.split("\\s+");
|
||||
if (parts.length == 0) continue;
|
||||
|
||||
switch (parts[0]) {
|
||||
case "v" -> {
|
||||
// Vertex position: v x y z
|
||||
if (parts.length >= 4) {
|
||||
positions.add(
|
||||
new float[] {
|
||||
parseFloat(parts[1]),
|
||||
parseFloat(parts[2]),
|
||||
parseFloat(parts[3]),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
case "vt" -> {
|
||||
// Texture coordinate: vt u v
|
||||
if (parts.length >= 3) {
|
||||
texCoords.add(
|
||||
new float[] {
|
||||
parseFloat(parts[1]),
|
||||
parseFloat(parts[2]),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
case "vn" -> {
|
||||
// Vertex normal: vn x y z
|
||||
if (parts.length >= 4) {
|
||||
normals.add(
|
||||
new float[] {
|
||||
parseFloat(parts[1]),
|
||||
parseFloat(parts[2]),
|
||||
parseFloat(parts[3]),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
case "mtllib" -> {
|
||||
// Material library: mtllib filename.mtl
|
||||
if (parts.length >= 2) {
|
||||
mtlLib = parts[1];
|
||||
}
|
||||
}
|
||||
case "usemtl" -> {
|
||||
// Use material: usemtl MaterialName
|
||||
if (parts.length >= 2) {
|
||||
currentMaterial = parts[1];
|
||||
}
|
||||
}
|
||||
case "f" -> {
|
||||
// Face: f v/vt/vn v/vt/vn v/vt/vn [v/vt/vn]
|
||||
List<ObjVertex> faceVerts = new ArrayList<>();
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
ObjVertex vert = parseVertex(
|
||||
parts[i],
|
||||
positions,
|
||||
texCoords,
|
||||
normals
|
||||
);
|
||||
if (vert != null) {
|
||||
faceVerts.add(vert);
|
||||
}
|
||||
}
|
||||
|
||||
// Triangulate if needed (quad -> 2 triangles)
|
||||
List<ObjFace> faces = triangulate(faceVerts);
|
||||
facesByMaterial
|
||||
.computeIfAbsent(currentMaterial, k ->
|
||||
new ArrayList<>()
|
||||
)
|
||||
.addAll(faces);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load MTL file if referenced
|
||||
if (mtlLib != null) {
|
||||
ResourceLocation mtlLocation = resolveMtlPath(location, mtlLib);
|
||||
Map<String, ObjMaterial> loadedMaterials = loadMtl(mtlLocation);
|
||||
materials.putAll(loadedMaterials);
|
||||
}
|
||||
|
||||
// Build and return model
|
||||
ObjModel.Builder builder = ObjModel.builder();
|
||||
for (Map.Entry<
|
||||
String,
|
||||
List<ObjFace>
|
||||
> entry : facesByMaterial.entrySet()) {
|
||||
builder.addFaces(entry.getKey(), entry.getValue());
|
||||
}
|
||||
for (ObjMaterial mat : materials.values()) {
|
||||
builder.addMaterial(mat);
|
||||
}
|
||||
|
||||
// Set texture base path (textures are now in the same directory as the OBJ)
|
||||
String objPath = location.getPath();
|
||||
int lastSlash = objPath.lastIndexOf('/');
|
||||
String directory =
|
||||
lastSlash >= 0 ? objPath.substring(0, lastSlash + 1) : "";
|
||||
// Textures are in the same folder as the OBJ file
|
||||
String textureBasePath = directory;
|
||||
builder.setTextureBasePath(textureBasePath);
|
||||
|
||||
ObjModel model = builder.build();
|
||||
TiedUpMod.LOGGER.info(
|
||||
"Loaded OBJ model: {} ({} faces, {} materials)",
|
||||
location,
|
||||
model.getTotalFaces(),
|
||||
materials.size()
|
||||
);
|
||||
|
||||
return model;
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.error("Failed to load OBJ model: {}", location, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single vertex from face data.
|
||||
* Format: v/vt/vn or v//vn or v/vt or v
|
||||
*/
|
||||
@Nullable
|
||||
private static ObjVertex parseVertex(
|
||||
String data,
|
||||
List<float[]> positions,
|
||||
List<float[]> texCoords,
|
||||
List<float[]> normals
|
||||
) {
|
||||
String[] indices = data.split("/");
|
||||
if (indices.length == 0) return null;
|
||||
|
||||
// Position index (required)
|
||||
int posIdx = parseInt(indices[0]) - 1; // OBJ indices are 1-based
|
||||
if (posIdx < 0 || posIdx >= positions.size()) return null;
|
||||
float[] pos = positions.get(posIdx);
|
||||
|
||||
// Texture coordinate index (optional)
|
||||
float u = 0,
|
||||
v = 0;
|
||||
if (indices.length >= 2 && !indices[1].isEmpty()) {
|
||||
int texIdx = parseInt(indices[1]) - 1;
|
||||
if (texIdx >= 0 && texIdx < texCoords.size()) {
|
||||
float[] tex = texCoords.get(texIdx);
|
||||
u = tex[0];
|
||||
v = tex[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Normal index (optional)
|
||||
float nx = 0,
|
||||
ny = 1,
|
||||
nz = 0;
|
||||
if (indices.length >= 3 && !indices[2].isEmpty()) {
|
||||
int normIdx = parseInt(indices[2]) - 1;
|
||||
if (normIdx >= 0 && normIdx < normals.size()) {
|
||||
float[] norm = normals.get(normIdx);
|
||||
nx = norm[0];
|
||||
ny = norm[1];
|
||||
nz = norm[2];
|
||||
}
|
||||
}
|
||||
|
||||
return new ObjVertex(pos[0], pos[1], pos[2], u, v, nx, ny, nz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triangulate a face (convert quad to 2 triangles).
|
||||
*/
|
||||
private static List<ObjFace> triangulate(List<ObjVertex> vertices) {
|
||||
List<ObjFace> result = new ArrayList<>();
|
||||
|
||||
if (vertices.size() < 3) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Triangle
|
||||
if (vertices.size() == 3) {
|
||||
result.add(
|
||||
new ObjFace(vertices.get(0), vertices.get(1), vertices.get(2))
|
||||
);
|
||||
}
|
||||
// Quad -> 2 triangles (fan triangulation)
|
||||
else if (vertices.size() >= 4) {
|
||||
ObjVertex v0 = vertices.get(0);
|
||||
for (int i = 1; i < vertices.size() - 1; i++) {
|
||||
result.add(
|
||||
new ObjFace(v0, vertices.get(i), vertices.get(i + 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the MTL file path relative to the OBJ file.
|
||||
*/
|
||||
private static ResourceLocation resolveMtlPath(
|
||||
ResourceLocation objLocation,
|
||||
String mtlFileName
|
||||
) {
|
||||
String objPath = objLocation.getPath();
|
||||
int lastSlash = objPath.lastIndexOf('/');
|
||||
String directory =
|
||||
lastSlash >= 0 ? objPath.substring(0, lastSlash + 1) : "";
|
||||
return ResourceLocation.fromNamespaceAndPath(
|
||||
objLocation.getNamespace(),
|
||||
directory + mtlFileName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load materials from an MTL file.
|
||||
*/
|
||||
private static Map<String, ObjMaterial> loadMtl(ResourceLocation location) {
|
||||
Map<String, ObjMaterial> materials = new HashMap<>();
|
||||
ResourceManager resourceManager =
|
||||
Minecraft.getInstance().getResourceManager();
|
||||
|
||||
try {
|
||||
Optional<Resource> resourceOpt = resourceManager.getResource(
|
||||
location
|
||||
);
|
||||
if (resourceOpt.isEmpty()) {
|
||||
TiedUpMod.LOGGER.warn("MTL file not found: {}", location);
|
||||
return materials;
|
||||
}
|
||||
|
||||
Resource resource = resourceOpt.get();
|
||||
String currentName = null;
|
||||
float r = 1,
|
||||
g = 1,
|
||||
b = 1;
|
||||
String texturePath = null;
|
||||
|
||||
try (
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(
|
||||
resource.open(),
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = line.split("\\s+");
|
||||
if (parts.length == 0) continue;
|
||||
|
||||
switch (parts[0]) {
|
||||
case "newmtl" -> {
|
||||
// Save previous material
|
||||
if (currentName != null) {
|
||||
materials.put(
|
||||
currentName,
|
||||
new ObjMaterial(
|
||||
currentName,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
texturePath
|
||||
)
|
||||
);
|
||||
}
|
||||
// Start new material
|
||||
currentName =
|
||||
parts.length >= 2 ? parts[1] : "unnamed";
|
||||
r = 1;
|
||||
g = 1;
|
||||
b = 1; // Reset to default
|
||||
texturePath = null;
|
||||
}
|
||||
case "Kd" -> {
|
||||
// Diffuse color: Kd r g b
|
||||
if (parts.length >= 4) {
|
||||
r = parseFloat(parts[1]);
|
||||
g = parseFloat(parts[2]);
|
||||
b = parseFloat(parts[3]);
|
||||
}
|
||||
}
|
||||
case "map_Kd" -> {
|
||||
// Diffuse texture map: map_Kd filename.png
|
||||
if (parts.length >= 2) {
|
||||
texturePath = parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last material
|
||||
if (currentName != null) {
|
||||
materials.put(
|
||||
currentName,
|
||||
new ObjMaterial(currentName, r, g, b, texturePath)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"Loaded {} materials from MTL: {}",
|
||||
materials.size(),
|
||||
location
|
||||
);
|
||||
} catch (IOException e) {
|
||||
TiedUpMod.LOGGER.error("Failed to load MTL file: {}", location, e);
|
||||
}
|
||||
|
||||
return materials;
|
||||
}
|
||||
|
||||
private static float parseFloat(String s) {
|
||||
try {
|
||||
return Float.parseFloat(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseInt(String s) {
|
||||
try {
|
||||
return Integer.parseInt(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.tiedup.remake.client.renderer.obj;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Singleton registry/cache for loaded OBJ models.
|
||||
* Models are loaded on-demand and cached for reuse.
|
||||
* Client-side only.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class ObjModelRegistry {
|
||||
|
||||
private static final Map<ResourceLocation, ObjModel> CACHE =
|
||||
new HashMap<>();
|
||||
private static boolean initialized = false;
|
||||
|
||||
private ObjModelRegistry() {
|
||||
// Singleton utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the registry and preload known models.
|
||||
* Called during FMLClientSetupEvent.
|
||||
*/
|
||||
public static void init() {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info("Initializing ObjModelRegistry...");
|
||||
|
||||
// Preload known 3D models
|
||||
preloadModel("tiedup:models/obj/ball_gag/model.obj");
|
||||
preloadModel("tiedup:models/obj/choke_collar_leather/model.obj");
|
||||
|
||||
initialized = true;
|
||||
TiedUpMod.LOGGER.info(
|
||||
"ObjModelRegistry initialized with {} models",
|
||||
CACHE.size()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload a model into the cache.
|
||||
*
|
||||
* @param path Resource path (e.g., "tiedup:models/obj/ball_gag.obj")
|
||||
*/
|
||||
private static void preloadModel(String path) {
|
||||
ResourceLocation location = ResourceLocation.tryParse(path);
|
||||
if (location != null) {
|
||||
ObjModel model = ObjModelLoader.load(location);
|
||||
if (model != null) {
|
||||
CACHE.put(location, model);
|
||||
TiedUpMod.LOGGER.debug("Preloaded OBJ model: {}", path);
|
||||
} else {
|
||||
TiedUpMod.LOGGER.warn("Failed to preload OBJ model: {}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a model from the cache, loading it if not present.
|
||||
*
|
||||
* @param location ResourceLocation of the .obj file
|
||||
* @return The loaded model, or null if not found/failed to load
|
||||
*/
|
||||
@Nullable
|
||||
public static ObjModel get(ResourceLocation location) {
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CACHE.computeIfAbsent(location, ObjModelLoader::load);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a model by string path.
|
||||
*
|
||||
* @param path Resource path (e.g., "tiedup:models/obj/ball_gag.obj")
|
||||
* @return The loaded model, or null if not found/failed to load
|
||||
*/
|
||||
@Nullable
|
||||
public static ObjModel get(String path) {
|
||||
ResourceLocation location = ResourceLocation.tryParse(path);
|
||||
return location != null ? get(location) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model is loaded in the cache.
|
||||
*/
|
||||
public static boolean isLoaded(ResourceLocation location) {
|
||||
return CACHE.containsKey(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache.
|
||||
* Useful for resource reload events.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
CACHE.clear();
|
||||
initialized = false;
|
||||
TiedUpMod.LOGGER.info("ObjModelRegistry cache cleared");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of loaded models.
|
||||
*/
|
||||
public static int getCachedCount() {
|
||||
return CACHE.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package com.tiedup.remake.client.renderer.obj;
|
||||
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderStateShard;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* Renders OBJ models using Minecraft's rendering system.
|
||||
* Uses VertexConsumer for hardware-accelerated rendering.
|
||||
* Client-side only.
|
||||
*
|
||||
* <p><b>Important:</b> Uses TRIANGLES mode RenderType, not QUADS,
|
||||
* because OBJ models are triangulated during loading.
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public class ObjModelRenderer extends RenderStateShard {
|
||||
|
||||
/** White texture for vertex color rendering */
|
||||
public static final ResourceLocation WHITE_TEXTURE =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
"models/obj/shared/white.png"
|
||||
);
|
||||
|
||||
private ObjModelRenderer() {
|
||||
super("tiedup_obj_renderer", () -> {}, () -> {});
|
||||
// Utility class - extends RenderStateShard to access protected members
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TRIANGLES-mode RenderType for OBJ rendering.
|
||||
* Standard entityCutoutNoCull uses QUADS which causes spiky artifacts
|
||||
* when we submit triangles.
|
||||
*/
|
||||
private static RenderType createTriangleRenderType(
|
||||
ResourceLocation texture
|
||||
) {
|
||||
RenderType.CompositeState state = RenderType.CompositeState.builder()
|
||||
.setShaderState(RENDERTYPE_ENTITY_CUTOUT_NO_CULL_SHADER)
|
||||
.setTextureState(
|
||||
new RenderStateShard.TextureStateShard(texture, false, false)
|
||||
)
|
||||
.setTransparencyState(NO_TRANSPARENCY)
|
||||
.setCullState(NO_CULL)
|
||||
.setLightmapState(LIGHTMAP)
|
||||
.setOverlayState(OVERLAY)
|
||||
.createCompositeState(true);
|
||||
|
||||
return RenderType.create(
|
||||
"tiedup_obj_triangles",
|
||||
DefaultVertexFormat.NEW_ENTITY,
|
||||
VertexFormat.Mode.TRIANGLES, // Key fix: TRIANGLES not QUADS
|
||||
1536,
|
||||
true,
|
||||
false,
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an OBJ model with vertex colors from materials.
|
||||
*
|
||||
* @param model The OBJ model to render
|
||||
* @param poseStack The current pose stack (with transformations applied)
|
||||
* @param buffer The multi-buffer source
|
||||
* @param packedLight Packed light value
|
||||
* @param packedOverlay Packed overlay value (for hit flash etc.)
|
||||
*/
|
||||
public static void render(
|
||||
ObjModel model,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay
|
||||
) {
|
||||
render(
|
||||
model,
|
||||
poseStack,
|
||||
buffer,
|
||||
packedLight,
|
||||
packedOverlay,
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an OBJ model with textures from materials (map_Kd) or vertex colors as fallback.
|
||||
*
|
||||
* @param model The OBJ model to render
|
||||
* @param poseStack The current pose stack (with transformations applied)
|
||||
* @param buffer The multi-buffer source
|
||||
* @param packedLight Packed light value
|
||||
* @param packedOverlay Packed overlay value (for hit flash etc.)
|
||||
* @param tintR Red tint multiplier (0-1)
|
||||
* @param tintG Green tint multiplier (0-1)
|
||||
* @param tintB Blue tint multiplier (0-1)
|
||||
* @param alpha Alpha value (0-1)
|
||||
*/
|
||||
public static void render(
|
||||
ObjModel model,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay,
|
||||
float tintR,
|
||||
float tintG,
|
||||
float tintB,
|
||||
float alpha
|
||||
) {
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Matrix4f pose = poseStack.last().pose();
|
||||
Matrix3f normal = poseStack.last().normal();
|
||||
|
||||
// Render faces grouped by material
|
||||
for (Map.Entry<String, List<ObjFace>> entry : model
|
||||
.getFacesByMaterial()
|
||||
.entrySet()) {
|
||||
ObjMaterial material = model.getMaterial(entry.getKey());
|
||||
|
||||
// Determine texture and color based on material
|
||||
ResourceLocation texture;
|
||||
float r, g, b;
|
||||
|
||||
if (material.hasTexture()) {
|
||||
// Use texture from map_Kd - resolve to full path
|
||||
texture = model.resolveTexture(material.texturePath());
|
||||
if (texture == null) {
|
||||
texture = WHITE_TEXTURE;
|
||||
}
|
||||
// White color to let texture show through, with tint applied
|
||||
r = tintR;
|
||||
g = tintG;
|
||||
b = tintB;
|
||||
} else {
|
||||
// Use vertex color from Kd
|
||||
texture = WHITE_TEXTURE;
|
||||
r = material.r() * tintR;
|
||||
g = material.g() * tintG;
|
||||
b = material.b() * tintB;
|
||||
}
|
||||
|
||||
// Get buffer for this material's texture (TRIANGLES mode)
|
||||
VertexConsumer vertexConsumer = buffer.getBuffer(
|
||||
createTriangleRenderType(texture)
|
||||
);
|
||||
|
||||
for (ObjFace face : entry.getValue()) {
|
||||
ObjVertex[] verts = face.vertices();
|
||||
|
||||
// Use vertex normals from the OBJ file for smooth shading
|
||||
for (ObjVertex vertex : verts) {
|
||||
vertexConsumer
|
||||
.vertex(pose, vertex.x(), vertex.y(), vertex.z())
|
||||
.color(r, g, b, alpha)
|
||||
.uv(vertex.u(), 1.0f - vertex.v())
|
||||
.overlayCoords(packedOverlay)
|
||||
.uv2(packedLight)
|
||||
.normal(normal, vertex.nx(), vertex.ny(), vertex.nz())
|
||||
.endVertex();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an OBJ model with selective material tinting.
|
||||
* Only materials in the tintMaterials set will be tinted.
|
||||
*
|
||||
* @param model The OBJ model to render
|
||||
* @param poseStack The current pose stack
|
||||
* @param buffer The multi-buffer source
|
||||
* @param packedLight Packed light value
|
||||
* @param packedOverlay Packed overlay value
|
||||
* @param tintR Red tint (0-1)
|
||||
* @param tintG Green tint (0-1)
|
||||
* @param tintB Blue tint (0-1)
|
||||
* @param tintMaterials Set of material names to apply tint to (e.g., "Ball")
|
||||
*/
|
||||
public static void renderWithSelectiveTint(
|
||||
ObjModel model,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay,
|
||||
float tintR,
|
||||
float tintG,
|
||||
float tintB,
|
||||
java.util.Set<String> tintMaterials
|
||||
) {
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Matrix4f pose = poseStack.last().pose();
|
||||
Matrix3f normal = poseStack.last().normal();
|
||||
|
||||
for (Map.Entry<String, List<ObjFace>> entry : model
|
||||
.getFacesByMaterial()
|
||||
.entrySet()) {
|
||||
String materialName = entry.getKey();
|
||||
ObjMaterial material = model.getMaterial(materialName);
|
||||
|
||||
// Check if this material should be tinted
|
||||
boolean shouldTint = tintMaterials.contains(materialName);
|
||||
|
||||
ResourceLocation texture;
|
||||
float r, g, b;
|
||||
|
||||
if (material.hasTexture()) {
|
||||
texture = model.resolveTexture(material.texturePath());
|
||||
if (texture == null) {
|
||||
texture = WHITE_TEXTURE;
|
||||
}
|
||||
// Apply tint only to specified materials
|
||||
if (shouldTint) {
|
||||
r = tintR;
|
||||
g = tintG;
|
||||
b = tintB;
|
||||
} else {
|
||||
r = 1.0f;
|
||||
g = 1.0f;
|
||||
b = 1.0f;
|
||||
}
|
||||
} else {
|
||||
// Vertex color from Kd
|
||||
texture = WHITE_TEXTURE;
|
||||
if (shouldTint) {
|
||||
r = material.r() * tintR;
|
||||
g = material.g() * tintG;
|
||||
b = material.b() * tintB;
|
||||
} else {
|
||||
r = material.r();
|
||||
g = material.g();
|
||||
b = material.b();
|
||||
}
|
||||
}
|
||||
|
||||
VertexConsumer vertexConsumer = buffer.getBuffer(
|
||||
createTriangleRenderType(texture)
|
||||
);
|
||||
|
||||
for (ObjFace face : entry.getValue()) {
|
||||
for (ObjVertex vertex : face.vertices()) {
|
||||
vertexConsumer
|
||||
.vertex(pose, vertex.x(), vertex.y(), vertex.z())
|
||||
.color(r, g, b, 1.0f)
|
||||
.uv(vertex.u(), 1.0f - vertex.v())
|
||||
.overlayCoords(packedOverlay)
|
||||
.uv2(packedLight)
|
||||
.normal(normal, vertex.nx(), vertex.ny(), vertex.nz())
|
||||
.endVertex();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an OBJ model with color-suffixed textures.
|
||||
* When colorSuffix is provided, ALL textured materials use texture_COLOR.png.
|
||||
*
|
||||
* @param model The OBJ model to render
|
||||
* @param poseStack The current pose stack
|
||||
* @param buffer The multi-buffer source
|
||||
* @param packedLight Packed light value
|
||||
* @param packedOverlay Packed overlay value
|
||||
* @param colorSuffix The color suffix (e.g., "red", "blue"), or null for default texture
|
||||
*/
|
||||
public static void renderWithColoredTextures(
|
||||
ObjModel model,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay,
|
||||
String colorSuffix
|
||||
) {
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Matrix4f pose = poseStack.last().pose();
|
||||
Matrix3f normal = poseStack.last().normal();
|
||||
|
||||
for (Map.Entry<String, List<ObjFace>> entry : model
|
||||
.getFacesByMaterial()
|
||||
.entrySet()) {
|
||||
ObjMaterial material = model.getMaterial(entry.getKey());
|
||||
|
||||
ResourceLocation texture;
|
||||
|
||||
if (material.hasTexture()) {
|
||||
// Apply color suffix to texture path if provided
|
||||
if (colorSuffix != null && !colorSuffix.isEmpty()) {
|
||||
texture = model.resolveTextureWithColorSuffix(
|
||||
material.texturePath(),
|
||||
colorSuffix
|
||||
);
|
||||
} else {
|
||||
texture = model.resolveTexture(material.texturePath());
|
||||
}
|
||||
if (texture == null) {
|
||||
texture = WHITE_TEXTURE;
|
||||
}
|
||||
} else {
|
||||
// No texture - use vertex color from Kd
|
||||
texture = WHITE_TEXTURE;
|
||||
}
|
||||
|
||||
VertexConsumer vertexConsumer = buffer.getBuffer(
|
||||
createTriangleRenderType(texture)
|
||||
);
|
||||
|
||||
// Determine vertex color - white for textured, Kd for untextured
|
||||
float r, g, b;
|
||||
if (material.hasTexture()) {
|
||||
r = 1.0f;
|
||||
g = 1.0f;
|
||||
b = 1.0f;
|
||||
} else {
|
||||
r = material.r();
|
||||
g = material.g();
|
||||
b = material.b();
|
||||
}
|
||||
|
||||
for (ObjFace face : entry.getValue()) {
|
||||
for (ObjVertex vertex : face.vertices()) {
|
||||
vertexConsumer
|
||||
.vertex(pose, vertex.x(), vertex.y(), vertex.z())
|
||||
.color(r, g, b, 1.0f)
|
||||
.uv(vertex.u(), 1.0f - vertex.v())
|
||||
.overlayCoords(packedOverlay)
|
||||
.uv2(packedLight)
|
||||
.normal(normal, vertex.nx(), vertex.ny(), vertex.nz())
|
||||
.endVertex();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an OBJ model with a specific texture instead of vertex colors.
|
||||
*
|
||||
* @param model The OBJ model to render
|
||||
* @param poseStack The current pose stack (with transformations applied)
|
||||
* @param buffer The multi-buffer source
|
||||
* @param packedLight Packed light value
|
||||
* @param packedOverlay Packed overlay value
|
||||
* @param texture The texture to apply
|
||||
*/
|
||||
public static void renderTextured(
|
||||
ObjModel model,
|
||||
PoseStack poseStack,
|
||||
MultiBufferSource buffer,
|
||||
int packedLight,
|
||||
int packedOverlay,
|
||||
ResourceLocation texture
|
||||
) {
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Matrix4f pose = poseStack.last().pose();
|
||||
Matrix3f normal = poseStack.last().normal();
|
||||
|
||||
// Use custom TRIANGLES-mode RenderType
|
||||
VertexConsumer vertexConsumer = buffer.getBuffer(
|
||||
createTriangleRenderType(texture)
|
||||
);
|
||||
|
||||
// Render all faces with white color (let texture provide color)
|
||||
for (Map.Entry<String, List<ObjFace>> entry : model
|
||||
.getFacesByMaterial()
|
||||
.entrySet()) {
|
||||
for (ObjFace face : entry.getValue()) {
|
||||
ObjVertex[] verts = face.vertices();
|
||||
|
||||
for (ObjVertex vertex : verts) {
|
||||
vertexConsumer
|
||||
.vertex(pose, vertex.x(), vertex.y(), vertex.z())
|
||||
.color(1.0f, 1.0f, 1.0f, 1.0f)
|
||||
.uv(vertex.u(), 1.0f - vertex.v())
|
||||
.overlayCoords(packedOverlay)
|
||||
.uv2(packedLight)
|
||||
.normal(normal, vertex.nx(), vertex.ny(), vertex.nz())
|
||||
.endVertex();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.tiedup.remake.client.renderer.obj;
|
||||
|
||||
/**
|
||||
* Immutable record representing a single vertex in an OBJ model.
|
||||
* Contains position (x, y, z), texture coordinates (u, v), and normal (nx, ny, nz).
|
||||
*/
|
||||
public record ObjVertex(
|
||||
float x,
|
||||
float y,
|
||||
float z,
|
||||
float u,
|
||||
float v,
|
||||
float nx,
|
||||
float ny,
|
||||
float nz
|
||||
) {
|
||||
/**
|
||||
* Create a vertex with only position data.
|
||||
* UV and normal will be set to defaults.
|
||||
*/
|
||||
public static ObjVertex ofPosition(float x, float y, float z) {
|
||||
return new ObjVertex(x, y, z, 0f, 0f, 0f, 1f, 0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a vertex with position and UV.
|
||||
* Normal will be set to default (pointing up).
|
||||
*/
|
||||
public static ObjVertex ofPositionUV(
|
||||
float x,
|
||||
float y,
|
||||
float z,
|
||||
float u,
|
||||
float v
|
||||
) {
|
||||
return new ObjVertex(x, y, z, u, v, 0f, 1f, 0f);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user