Working mtl obj rendering

This commit is contained in:
2026-03-03 02:51:06 +01:00
parent e3ad918755
commit 80684f7e07
4 changed files with 84 additions and 30 deletions
@@ -1,9 +1,11 @@
package org.saturnclient.cosmetics.hat; package org.saturnclient.cosmetics.hat;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import org.saturnclient.cosmetics.obj.ObjRenderer; import org.saturnclient.cosmetics.obj.ObjRenderer;
import de.javagl.obj.Mtl;
import de.javagl.obj.Obj; import de.javagl.obj.Obj;
import net.minecraft.client.render.VertexConsumerProvider; import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.feature.FeatureRenderer; import net.minecraft.client.render.entity.feature.FeatureRenderer;
@@ -15,12 +17,15 @@ import net.minecraft.util.Identifier;
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> { public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
static Identifier HAT = Identifier.of("saturnclient", "models/item/horns/horns.obj"); static Identifier HAT = Identifier.of("saturnclient", "models/item/horns/horns.obj");
static Identifier BLACK_MTL = Identifier.of("saturnclient", "models/item/black.mtl");
Obj obj; Obj obj;
Map<String, Mtl> mtl;
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) { public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
super(context); super(context);
try { try {
this.obj = ObjRenderer.loadObj(HAT); this.obj = ObjRenderer.loadObj(HAT);
this.mtl = ObjRenderer.loadMtl(BLACK_MTL);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -29,6 +34,6 @@ public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState,
@Override @Override
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
PlayerEntityRenderState state, float limbAngle, float limbDistance) { PlayerEntityRenderState state, float limbAngle, float limbDistance) {
ObjRenderer.renderObj(this.obj, matrices, vertexConsumers, light, 0); ObjRenderer.renderObj(this.obj, this.mtl, matrices, vertexConsumers, light, 0);
} }
} }
@@ -2,11 +2,10 @@ package org.saturnclient.cosmetics.obj;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.List;
import java.util.Map;
import de.javagl.obj.FloatTuple; import de.javagl.obj.*;
import de.javagl.obj.Obj;
import de.javagl.obj.ObjFace;
import de.javagl.obj.ObjReader;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.RenderLayer;
@@ -16,6 +15,7 @@ import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
public class ObjRenderer { public class ObjRenderer {
public static Obj loadObj(Identifier objId) throws IOException { public static Obj loadObj(Identifier objId) throws IOException {
try (InputStream is = MinecraftClient.getInstance().getResourceManager() try (InputStream is = MinecraftClient.getInstance().getResourceManager()
.getResource(objId).get().getInputStream()) { .getResource(objId).get().getInputStream()) {
@@ -23,42 +23,83 @@ public class ObjRenderer {
} }
} }
public static void renderObj(Obj obj, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, public static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException {
int overlay) { try (InputStream is = MinecraftClient.getInstance().getResourceManager()
VertexConsumer consumer = vertexConsumers.getBuffer( .getResource(mtlId).get().getInputStream()) {
RenderLayer.getEntityAlpha(Identifier.of("textures/misc/white.png"))); List<Mtl> mtlList = MtlReader.read(is);
Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>();
for (Mtl mtl : mtlList) {
mtlMap.put(mtl.getName(), mtl);
}
return mtlMap;
}
}
public static void renderObj(Obj obj, Map<String, Mtl> mtlMap, MatrixStack matrices,
VertexConsumerProvider vertexConsumers, int light, int overlay) {
MatrixStack.Entry entry = matrices.peek(); MatrixStack.Entry entry = matrices.peek();
int faces = obj.getNumFaces(); Map<String, Obj> materialGroups = ObjSplitting.splitByMaterialGroups(obj);
System.out.println("Faces " + faces + ", vertices " + obj.getNumVertices()); for (Map.Entry<String, Obj> group : materialGroups.entrySet()) {
String materialName = group.getKey();
Obj groupObj = group.getValue();
for (int f = 0; f < faces; f++) { Mtl mtl = mtlMap != null ? mtlMap.get(materialName) : null;
ObjFace face = obj.getFace(f);
int vertices = face.getNumVertices();
// Reverse winding order: OBJ is CCW, Minecraft expects CW // Resolve texture identifier using same logic as ObjUnbakedModelModel
for (int i = vertices - 1; i >= 0; i--) { Identifier texture = Identifier.of("minecraft:textures/misc/white.png");
writeVertex(obj, face, i, consumer, entry, light, overlay);
if (mtl != null) {
String mapKd = mtl.getMapKd();
if (mapKd != null) {
// "#key" references are not resolvable at runtime without a texture map,
// so we only support direct resource location strings here
if (!mapKd.startsWith("#")) {
texture = Identifier.of(mapKd);
}
}
}
// Diffuse color (Kd)
float r = 1f, g = 1f, b = 1f;
if (mtl != null) {
FloatTuple kd = mtl.getKd();
if (kd != null) {
r = kd.getX();
g = kd.getY();
b = kd.getZ();
}
}
VertexConsumer consumer = vertexConsumers.getBuffer(RenderLayer.getEntityAlpha(texture));
int faces = groupObj.getNumFaces();
final float fr = r, fg = g, fb = b;
for (int f = 0; f < faces; f++) {
ObjFace face = groupObj.getFace(f);
int verts = face.getNumVertices();
// Your original winding order logic, untouched
for (int i = verts - 1; i >= 0; i--) {
writeVertex(groupObj, face, i, consumer, entry, light, overlay, fr, fg, fb);
}
} }
} }
} }
/** Write a single vertex to the VertexConsumer using the OBJ FloatTuple */ private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer,
private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer, MatrixStack.Entry matrix, MatrixStack.Entry matrix, int light, int overlay,
int light, int overlay) { float r, float g, float b) {
FloatTuple pos = obj.getVertex(face.getVertexIndex(index)); FloatTuple pos = obj.getVertex(face.getVertexIndex(index));
float x = pos.getX(); float x = pos.getX();
float y = pos.getY(); float y = pos.getY();
float z = pos.getZ(); float z = pos.getZ();
// Default normal float nx = 0f, ny = 1f, nz = 0f;
float nx = 0f;
float ny = 1f;
float nz = 0f;
// Use OBJ normal if available
int normalIndex = face.getNormalIndex(index); int normalIndex = face.getNormalIndex(index);
if (normalIndex >= 0) { if (normalIndex >= 0) {
FloatTuple n = obj.getNormal(normalIndex); FloatTuple n = obj.getNormal(normalIndex);
@@ -67,9 +108,17 @@ public class ObjRenderer {
nz = n.getZ(); nz = n.getZ();
} }
float u = 0f, v = 0f;
int texIndex = face.getTexCoordIndex(index);
if (texIndex >= 0) {
FloatTuple tc = obj.getTexCoord(texIndex);
u = tc.getX();
v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left
}
consumer.vertex(matrix, x, y, z) consumer.vertex(matrix, x, y, z)
.color(255, 255, 255, 255) .color(r, g, b, 1f)
.texture(0f, 0f) .texture(u, v)
.overlay(overlay) .overlay(overlay)
.light(light) .light(light)
.normal(matrix, nx, ny, nz); .normal(matrix, nx, ny, nz);
@@ -10,4 +10,4 @@ Ke 0.000000 0.000000 0.000000
Ni 1.450000 Ni 1.450000
d 1.000000 d 1.000000
illum 2 illum 2
map_Kd saturnclient:item/halo_black map_Kd saturnclient:textures/item/halo_black.png
@@ -10,4 +10,4 @@ Ke 0.000000 0.000000 0.000000
Ni 1.450000 Ni 1.450000
d 1.000000 d 1.000000
illum 2 illum 2
map_Kd saturnclient:item/halo map_Kd saturnclient:textures/item/halo.png