Jump to content
Search In
  • More options...
Find results that contain...
Find results in...

[1.8.9] Rendering an .obj model


Bedrock_Miner
 Share

Recommended Posts

Hello everyone!

 

I wanted to create a pipe block, which can connect to adjacent pipes on every side. The connection element should be rendered with an .obj file, that is rotated to the correct direction for each connected side. The problem is, that I cannot get the obj models to work.

 

I've created a simple model file that does the same with vanilla model files (and actually works), but this is not customizable enough for me, I'd like to use .obj instead.

 

This is the model code I used with the vanilla model files:

 

package com.bedrockminer.ascore.client.render.block.models;

import static net.minecraftforge.client.model.TRSRTransformation.quatFromYXZDegrees;

import javax.vecmath.Vector3f;

import net.minecraft.client.renderer.block.model.ItemCameraTransforms.TransformType;
import net.minecraft.client.renderer.block.model.ModelBlock;
import net.minecraft.client.resources.model.ModelRotation;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.model.ITransformation;
import net.minecraftforge.client.model.TRSRTransformation;

@SuppressWarnings("deprecation")
public abstract class ModelPipeBase extends ModelBase {

protected ModelBlock pipe_part;
protected ModelBlock pipe_cap;

@Override
public void loadSubmodels() {
	this.pipe_part = this.loadSubmodel(new ResourceLocation("ascore:models/block/pressure_pipe_part.json"));
	this.pipe_cap = this.loadSubmodel(new ResourceLocation("ascore:models/block/pressure_pipe_cap.json"));
}

@Override
public void putQuads() {
	if (this.isItem()) {
		this.addSubmodel(this.pipe_part, ModelRotation.X0_Y0);
		this.addSubmodel(this.pipe_part, ModelRotation.X0_Y180);
		this.addSubmodel(this.pipe_cap, ModelRotation.X0_Y90);
		this.addSubmodel(this.pipe_cap, ModelRotation.X90_Y0);
		this.addSubmodel(this.pipe_cap, ModelRotation.X0_Y270);
		this.addSubmodel(this.pipe_cap, ModelRotation.X270_Y0);
	} else {
		for (EnumFacing direction: EnumFacing.values()) {
			ModelBlock m = this.pipe_cap;
			if (this.pipePart(direction))
				m = this.pipe_part;
			this.addSubmodel(m, new TRSRTransformation(direction));
		}
	}
}

public abstract boolean pipePart(EnumFacing side);

@Override
public ITransformation getTransformation(TransformType renderType) {
	switch (renderType) {
	case THIRD_PERSON:
		return new TRSRTransformation(new Vector3f(0, 0.09375f, -0.171875f), quatFromYXZDegrees(new Vector3f(10, -45, 170)), new Vector3f(0.375f, 0.375f, 0.375f), null);
	case FIXED:
		return new TRSRTransformation(new Vector3f(0, 0, -0.02f), quatFromYXZDegrees(new Vector3f(0, 90, 0)), new Vector3f(1.2f, 1.2f, 1.2f), null);
	default:
		return ModelRotation.X0_Y0;
	}
}

}

 

And this is the base class ModelBase I created for working with code-driven renderers:

 

package com.bedrockminer.ascore.client.render.block.models;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.LinkedList;
import java.util.List;

import javax.vecmath.Matrix4f;
import javax.vecmath.Vector2f;
import javax.vecmath.Vector3f;
import javax.vecmath.Vector4f;

import org.apache.commons.lang3.tuple.Pair;

import com.bedrockminer.ascore.block.base.BlockTileEntityProvider;
import com.bedrockminer.ascore.util.Log;

import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.BlockPart;
import net.minecraft.client.renderer.block.model.BlockPartFace;
import net.minecraft.client.renderer.block.model.FaceBakery;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms.TransformType;
import net.minecraft.client.renderer.block.model.ModelBlock;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexFormat;
import net.minecraft.client.resources.model.IBakedModel;
import net.minecraft.item.ItemStack;
import net.minecraft.util.BlockPos;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.model.IFlexibleBakedModel;
import net.minecraftforge.client.model.IPerspectiveAwareModel;
import net.minecraftforge.client.model.ISmartBlockModel;
import net.minecraftforge.client.model.ISmartItemModel;
import net.minecraftforge.client.model.ITransformation;
import net.minecraftforge.client.model.pipeline.LightUtil;
import net.minecraftforge.client.model.pipeline.UnpackedBakedQuad;

/**
* A base class for code driven model implementations.
* This class provides methods for easy rendering and inclusion of "normal"
* file-driven submodels.
* @author _Bedrock_Miner_
*/
@SuppressWarnings("deprecation")
public abstract class ModelBase implements ISmartBlockModel, ISmartItemModel, IPerspectiveAwareModel {

private static FaceBakery face = new FaceBakery();
public static List<ModelBase> models = new LinkedList<ModelBase>();

private BlockPos renderPos;
private IBlockState state;
private ItemStack stack;
private boolean item;

protected List<BakedQuad> list;
private UnpackedBakedQuad.Builder builder;

private TextureAtlasSprite texture;
private EnumFacing orientation;

private Vector3f pos;
private Vector3f normal;
private Vector2f uv;
private Vector4f color;

/**
 * Instantiates a new ModelBase.
 */
public ModelBase() {
	this.loadSubmodels();
	models.add(this);
}

// Implementing the Interfaces

@Override
public List<BakedQuad> getFaceQuads(EnumFacing facing) {
	this.list = new LinkedList<BakedQuad>();
	this.putFaceQuads(facing);
	return this.list;
}

@Override
public List<BakedQuad> getGeneralQuads() {
	this.list = new LinkedList<BakedQuad>();
	this.putQuads();
	return this.list;
}

@Override
public boolean isAmbientOcclusion() {
	return true;
}

@Override
public boolean isGui3d() {
	return true;
}

@Override
public boolean isBuiltInRenderer() {
	return false;
}

@Override
public TextureAtlasSprite getParticleTexture() {
	return this.getParticleResource();
}

@Override
public ItemCameraTransforms getItemCameraTransforms() {
	return ItemCameraTransforms.DEFAULT;
}

@Override
public IBakedModel handleItemState(ItemStack stack) {
	this.stack = stack;
	this.state = null;
	this.renderPos = null;
	this.item = true;
	return this;
}

@Override
public IBakedModel handleBlockState(IBlockState state) {
	if (state.getBlock() instanceof BlockTileEntityProvider)
		this.renderPos = ((BlockTileEntityProvider)state.getBlock()).renderPosition;
	else
		this.renderPos = null;
	this.state = state;
	this.stack = null;
	this.item = false;
	return this;
}

@Override
public VertexFormat getFormat() {
	return this.getVertexFormat();
}

@Override
public Pair<? extends IFlexibleBakedModel, Matrix4f> handlePerspective(TransformType cameraTransformType) {
	return Pair.of(this, this.getTransformation(cameraTransformType).getMatrix());
}

// Overridable Methods

/**
 * Returns the TextureAtlasSprite that will be used for the particles of the
 * block. To get a sprite, you can use {@link #getSprite(ResourceLocation)}.
 *
 * @return the sprite to use for the particles.
 */
public abstract TextureAtlasSprite getParticleResource();

/**
 * Returns the vertex format to use while rendering. This is either
 * {@link DefaultVertexFormats#BLOCK} or
 * {@link DefaultVertexFormats#ITEM}.
 *
 * @return the vertex format to use.
 */
public VertexFormat getVertexFormat() {
	if (this.isItem())
		return DefaultVertexFormats.ITEM;
	else
		return DefaultVertexFormats.BLOCK;
}

/**
 * This is the main "rendering" method. Here, the faces are assembled using
 * and pushed to the internal face stack.
 */
public abstract void putQuads();

/**
 * Returns the transformation for the given render type.
 *
 * @param renderType the render type
 * @return the transformation
 */
public abstract ITransformation getTransformation(TransformType renderType);

/**
 * In this method you can render faces that are adjacent to a special side
 * of the block. If that side is blocked, the face won't be rendered.
 *
 * @param facing the face
 */
public void putFaceQuads(EnumFacing facing) {
}

/**
 * If you need file-driven submodels in your model, you can load them in
 * this method using {@link #loadSubmodel(ResourceLocation)}.
 */
public void loadSubmodels() {
}

// Getter Methods

/**
 * Returns whether the rendering is called for an item or a block.
 *
 * @return true if an item is rendered.
 */
public boolean isItem() {
	return this.item;
}

/**
 * Returns the item stack being rendered or null if a block is drawn.
 *
 * @return the item stack
 */
public ItemStack getItemStack() {
	return this.stack;
}

/**
 * Returns the blockstate of the block being rendered or null if an item is
 * drawn.
 *
 * @return the blockstate
 */
public IBlockState getBlockState() {
	return this.state;
}

/**
 * Returns the position of the block currently being rendered. If the block
 * is not an instance of BlockTileEntityProvider or if an item is being
 * rendered, this method returns null.
 *
 * @return the block position
 */
public BlockPos getRenderPos() {
	return this.renderPos;
}

// Utility Methods

/**
 * Returns a sprite based on the given resource location. Remember to
 * register "new" textures from the TextureStitchEvent.Pre first.
 *
 * @param resource the resource location
 * @return the sprite
 */
protected TextureAtlasSprite getSprite(ResourceLocation resource) {
	return Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(resource.toString());
}

/**
 * Loads a submodel from the given file.
 *
 * @param resource the file resource
 * @return the submodel
 */
protected ModelBlock loadSubmodel(ResourceLocation resource) {
	Reader r = null;
	try {
		r = new InputStreamReader(Minecraft.getMinecraft().getResourceManager().getResource(resource).getInputStream());
		ModelBlock model = ModelBlock.deserialize(r);
		model.name = resource.toString();
		return model;
	} catch (IOException e) {
		Log.fatal("IO Exception while loading model %s: %s", resource, e.getMessage());
		Log.printStackTrace(e);
	} finally {
		try {
			r.close();
		} catch (Exception e) {
			Log.fatal("Exception while closing stream", e.getMessage());
			Log.printStackTrace(e);
		}
	}
	return null;
}

/**
 * Includes the given submodel into the currently drawn block model.
 *
 * @param model the submodel
 * @param transf the transformations applied to the model
 */
protected void addSubmodel(ModelBlock model, ITransformation transf) {
	if (model == null)
		return;

	for (BlockPart part: model.getElements()) {
		for (EnumFacing side : part.mapFaces.keySet()) {
			BlockPartFace face = part.mapFaces.get(side);
                TextureAtlasSprite tex = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(new ResourceLocation(model.resolveTextureName(face.texture)).toString());
                BakedQuad quad = this.face.makeBakedQuad(part.positionFrom, part.positionTo, face, tex, side, transf, part.partRotation, false, part.shade);
                this.list.add(quad);
            }
	}
}

/**
 * Includes the given baked submodel into the currently drawn block model.
 *
 * @param model the submodel
 */
protected void addSubmodel(IBakedModel model) {
	for (EnumFacing side: EnumFacing.values())
		this.addQuads(model.getFaceQuads(side));

	this.addQuads(model.getGeneralQuads());
}

/**
 * Adds the given list of baked quads to the currently drawn model.
 *
 * @param quads the quads to add.
 */
protected void addQuads(List<BakedQuad> quads) {
	this.list.addAll(quads);
}

/**
 * Starts a new quadrilateral face.
 *
 * @param texture the texture sprite
 * @param orientation the (general) orientation of the face
 */
protected void newQuad(TextureAtlasSprite texture, EnumFacing orientation) {
	this.builder = new UnpackedBakedQuad.Builder(this.getVertexFormat());
	this.builder.setQuadOrientation(orientation);

	this.texture = texture;
	this.orientation = orientation;
}

/**
 * Finished a face and pushes it to the list.
 */
protected void emitQuad() {
	this.list.add(this.builder.build());
	this.normal = null;
}

/**
 * Finished the old face and starts a new one.
 *
 * @param texture the texture sprite
 * @param orientation the (general) orientation of the face
 */
protected void nextQuad(TextureAtlasSprite texture, EnumFacing orientation) {
	this.emitQuad();
	this.newQuad(texture, orientation);
}

/**
 * Marks this face as colored.
 */
protected void setColored() {
	this.builder.setQuadColored();
}

/**
 * Sets the face tint ID for this face. Default is -1 (no tint).
 *
 * @param tintid the tintid
 */
protected void setTintID(int tintid) {
	this.builder.setQuadTint(tintid);
}

/**
 * Sets the position for the current vertex.
 *
 * @param x
 * @param y
 * @param z
 * @return this object
 */
protected ModelBase pos(float x, float y, float z) {
	this.pos = new Vector3f(x, y, z);
	return this;
}

/**
 * Sets the normal for this vertex. This value can stay the same for
 * multiple vertices without calling the method again.
 *
 * @param x
 * @param y
 * @param z
 * @return this object
 */
protected ModelBase normal(float x, float y, float z) {
	this.normal = new Vector3f(x, y, z);
	return this;
}

/**
 * Sets the texture uv coordinates on the given sprite for the current
 * vertex.
 *
 * @param u
 * @param v
 * @return this object
 */
protected ModelBase uv(float u, float v) {
	this.uv = new Vector2f(u, v);
	return this;
}

/**
 * Sets the color for the current vertex.
 *
 * @param r
 * @param g
 * @param b
 * @param a
 * @return this object
 */
protected ModelBase color(float r, float g, float b, float a) {
	this.color = new Vector4f(r, g, b, a);
	return this;
}

/**
 * Sets the color for the current vertex.
 *
 * @param rgb
 * @return this object
 */
protected ModelBase color(int rgb) {
	float r = (rgb >> 16 & 255) / 255.0f;
	float g = (rgb >> 8 & 255) / 255.0f;
	float b = (rgb & 255) / 255.0f;
	return this.color(r, g, b, 1.0f);
}

/**
 * Ends this vertex and proceeds to the next one.
 */
protected void endVertex() {
	this.putVertexData();
	this.color = null;
}

private void putVertexData() {
	for (int e = 0; e < this.getVertexFormat().getElementCount(); e++) {
		switch (this.getVertexFormat().getElement(e).getUsage()) {

		case POSITION:
			this.builder.put(e, this.pos.x, this.pos.y, this.pos.z, 1);
			break;

		case COLOR:
			float d = this.normal == null ? LightUtil.diffuseLight(this.orientation) : LightUtil.diffuseLight(this.normal.x, this.normal.y, this.normal.z);
			if (this.isItem())
				d = 1.0f;
			if (this.color != null) {
				this.builder.put(e, d * this.color.x, d * this.color.y, d * this.color.z, this.color.w);
			} else {
				this.builder.put(e, d, d, d, 1);
			}
			break;

		case UV:
			int index = this.getVertexFormat().getElement(e).getIndex();
			if (index == 0) {
				this.builder.put(e, this.texture.getInterpolatedU(this.uv.x * 16), this.texture.getInterpolatedV((1 - this.uv.y) * 16), 0, 1);
			} else {
				this.builder.put(e, 0, 0, 0, 1);
			}
			break;

		case NORMAL:
			if (this.normal != null) {
				this.builder.put(e, this.normal.x, this.normal.y, this.normal.z, 0);
			} else {
				this.builder.put(e, this.orientation.getDirectionVec().getX(), this.orientation.getDirectionVec().getY(), this.orientation.getDirectionVec().getZ());
			}
			break;

		default:
			this.builder.put(e);
		}
	}
}
}

 

 

This code does work.

However, my second attempt with the .obj models did not work at all: The models are loaded, but they are not rendered in the game. Nothing there at all.

Here's the code of the loadSubmodels and putQuads method (everything else is the same).

 

 

	protected OBJModel model;

@Override
public void loadSubmodels() {
	try {
		this.model = new OBJModel.Parser(Minecraft.getMinecraft().getResourceManager().getResource(new ResourceLocation("ascore:models/obj/pressure_pipe_part.obj")), Minecraft.getMinecraft().getResourceManager()).parse();
	} catch (IOException e) {
		e.printStackTrace();
	}

	this.pipe_part = this.loadSubmodel(new ResourceLocation("ascore:models/block/pressure_pipe_part.json"));
	this.pipe_cap = this.loadSubmodel(new ResourceLocation("ascore:models/block/pressure_pipe_cap.json"));
}

@Override
public void putQuads() {
	// Item part should be changed once the block part works!
	if (this.isItem()) {
		this.addSubmodel(this.pipe_part, ModelRotation.X0_Y0);
		this.addSubmodel(this.pipe_part, ModelRotation.X0_Y180);
		this.addSubmodel(this.pipe_cap, ModelRotation.X0_Y90);
		this.addSubmodel(this.pipe_cap, ModelRotation.X90_Y0);
		this.addSubmodel(this.pipe_cap, ModelRotation.X0_Y270);
		this.addSubmodel(this.pipe_cap, ModelRotation.X270_Y0);
	} else {
// This part SHOULD work, but doesn't.
		for (EnumFacing direction : EnumFacing.values()) {
			if (this.pipePart(direction)) {
				IFlexibleBakedModel bkdmodel = this.model.bake(new TRSRTransformation(direction), this.getVertexFormat(), ModelLoader.defaultTextureGetter());
				this.addSubmodel(bkdmodel);
			}
		}
	}
}

 

 

The obj file is a simple cube at the moment to keep it simple.

 

OBJ:

# Blender v2.66 (sub 0) OBJ File: 'pressure_pipe_part'
# www.blender.org

mtllib Cube.mtl
o Cube
v 1.000000 0.000000 -1.000000
v 1.000000 0.000000 0.000000
v 0.000000 0.000000 0.000000
v 0.000000 0.000000 -1.000000
v 1.000000 1.000000 -1.000000
v 1.000000 1.000000 0.000000
v 0.000000 1.000000 0.000000
v 0.000000 1.000000 -1.000000
vt 0.000000 1.000000
vt 0.000000 0.000000
vt 1.000000 0.000000
vt 1.000000 1.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 0.000000 0.000000 1.000000
vn -1.000000 0.000000 0.000000
vn 0.000000 0.000000 -1.000000
usemtl Cube
f 1/1/1 2/2/1 3/3/1 4/4/1
f 5/1/2 8/2/2 7/3/2 6/4/2
f 1/1/3 5/2/3 6/3/3 2/4/3
f 2/1/4 6/2/4 7/3/4 3/4/4
f 3/1/5 7/2/5 8/3/5 4/4/5
f 5/1/6 1/2/6 4/3/6 8/4/6

MTL:

# Blender MTL File: 'pressure_pipe_part'
# Material Count: 1

newmtl Cube
Kd 0.000000 0.800000 0.000000
map_Kd blocks/dirt

 

 

If anyone has an idea why this does not work or how to fix it, please tell me!

If you need some more code, I can provide it as well.

Link to comment
Share on other sites

There is a lot simpler way of rendering OBJ models. If you register your mod with OBJLoader.instance.addDomain, you can simply use your .obj files instead of defining vanilla json model files. With the forge blockstate json syntax and submodels, I think you can get the versatility you need.

Link to comment
Share on other sites

Yes, this would work, however I have one problem with this:

The pipes I try to create would have way too many different blockstates!

There is one boolean state for each direction (2^6 states), the pipe can have different colors (*16 more states), the pipe can have different materials (even more) and maybe I want to allow different pipe types to connect to each other, which needs to be rendered differently.

 

Every single possible combination of those states would require a single blockstate which

a) slows down minecraft loading, because every single blockstate is evaluated and cached (Tried it, I got about 2048 states per pipe)

b) consumes tons of ram, especially if the models become more complicated

c) makes the blockstates json file a mess and difficult to read and write.

Therefore I decided not to use blockstates at all and to make a code-driven renderer which works fine generally. Only the obj models are not rendered, everything else is fine already. Therefore, I wont change to forge's blockstates file now.

Link to comment
Share on other sites

Well, I did some further investigation and found out what caused the problem, although I neither know why this is a problem, nor how to solve it. (Did I mention that I hate the Minecraft model system because it makes everything pretty user-unfriendly to debug?)

 

I created an even simpler .obj file consisting out of a single face and generated the same face by code (the latter works, the former doesn't) and compared the raw data I got.

This is the data stored in the UnpackedBakedQuad classes:

 

By code:
[[
  [0.0, 1.0, 0.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.40626952, 0.15623046, 0.0, 1.0], 
  [0.0, 0.0, 0.0, 1.0]
], [
  [1.0, 1.0, 0.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.43748048, 0.15623046, 0.0, 1.0], 
  [0.0, 0.0, 0.0, 1.0]
], [
  [1.0, 1.0, 1.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.43748048, 0.12501954, 0.0, 1.0], 
  [0.0, 0.0, 0.0, 1.0]
], [
  [0.0, 1.0, 1.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.40626952, 0.12501954, 0.0, 1.0], 
  [0.0, 0.0, 0.0, 1.0]
]]

By .obj file:
[[
  [0.0, 1.0, 0.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.40626952, 0.12501954, 0.0, 1.0], 
  [0.40626952, 0.12501954, 0.0, 1.0]
], [
  [1.0, 1.0, 0.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.43748048, 0.12501954, 0.0, 1.0], 
  [0.43748048, 0.12501954, 0.0, 1.0]
], [
  [1.0, 1.0, 1.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.43748048, 0.15623046, 0.0, 1.0], 
  [0.43748048, 0.15623046, 0.0, 1.0]
], [
  [0.0, 1.0, 1.0, 1.0], 
  [1.0000001, 1.0000001, 1.0000001, 1.0], 
  [0.40626952, 0.15623046, 0.0, 1.0], 
  [0.40626952, 0.15623046, 0.0, 1.0]
]]

 

 

The difference is, that the UV coordinates are repeated for the obj model. I changed the repeated values to 0 in eclipse debug mode, and it works.

I assume, the values are doubled because the BLOCK vertex format uses

[POS, COLOR, UV (double), UV (short)].

 

Now the questions:

1st and most important: Dear Minecraft, why the heck do you need UVs twice, why in different formats and WHY do I HAVE TO make the second ones all zero?

2nd: Why does the Forge model loader put the uvs in two times if one is enough? Or is this a problem only I have so no one else noticed?

3rd: How can I fix this from inside my code, not from debug mode? I don't want to recreate the whole ObjModel class to change a tiny bit in the final putVertexData method and I also don't really want to create a coremod for this.

Link to comment
Share on other sites

  • 1 month later...

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share



×
×
  • Create New...

Important Information

By using this site, you agree to our Privacy Policy.