Render Block Baked Model-1.12

In this tutorial we're going to explain how you can statically render a conduit like system. This needs a baked model since we have to be able to calculate geometry dynamically. However this example does not use tile entities or TESR (TileEntitySpecialRenderer) which means the result will be static and only change when new blocks are added or removed. Here is how it looks like:

There are a few things we need to do before this can work. First a baked model cannot access the world in any way because it can possibly be called in a thread. So you need a way to transfer all the rendering of the needed information from the block to the baked model. This works by using ExtendedBlockState and unlisted properties. These are properties that are not translated to metadata so you are not limited to the 4 bits of metadata that is reserved for blocks. It is also not directly stored with the block so you have to calculate it when the chunk renderer needs it. In this example we need six booleans to indicate if there is another block of this type on a given side. First we define a property so that we can store booleans. Note that you can make properties of any possible type. We could also have chosen to make a single property that contains six booleans (instead of six properties with each containing a boolean).

Then we use this in our block. Note that initItemModel() has to be called from within ClientProxy.init() (as opposed to the initModel() which is called from preInit() as usual). The methods createBlockState() and getExtendedState() are used to communicate the unlisted properties to our baked model. We override createBlockState() so that we can make an ExtendedBlockState instead of the normal blockstate. The getExtendedState() method is where we actually calculate the properties based on the presence of adjacent blocks. This will be used by our baked model at the time static geometry is rendered in the chunk.

publicclassBakedModelBlockextendsBlock{// Properties that indicate if there is the same block in a certain direction.publicstaticfinalUnlistedPropertyBlockAvailableNORTH=newUnlistedPropertyBlockAvailable("north");publicstaticfinalUnlistedPropertyBlockAvailableSOUTH=newUnlistedPropertyBlockAvailable("south");publicstaticfinalUnlistedPropertyBlockAvailableWEST=newUnlistedPropertyBlockAvailable("west");publicstaticfinalUnlistedPropertyBlockAvailableEAST=newUnlistedPropertyBlockAvailable("east");publicstaticfinalUnlistedPropertyBlockAvailableUP=newUnlistedPropertyBlockAvailable("up");publicstaticfinalUnlistedPropertyBlockAvailableDOWN=newUnlistedPropertyBlockAvailable("down");publicBakedModelBlock(){super(Material.ROCK);setUnlocalizedName(ModTut.MODID+".bakedmodelblock");setRegistryName("bakedmodelblock");}@SideOnly(Side.CLIENT)publicvoidinitModel(){// To make sure that our baked model model is chosen for all states we use this custom state mapper:StateMapperBaseignoreState=newStateMapperBase(){@OverrideprotectedModelResourceLocationgetModelResourceLocation(IBlockStateiBlockState){returnExampleBakedModel.BAKED_MODEL;}};ModelLoader.setCustomStateMapper(this,ignoreState);}@SideOnly(Side.CLIENT)publicvoidinitItemModel(){// For our item model we want to use a normal json model. This has to be called in// ClientProxy.postInit (not preInit) so that's why it is a separate method.ItemitemBlock=Item.REGISTRY.getObject(newResourceLocation(ModTut.MODID,"bakedmodelblock"));ModelResourceLocationitemModelResourceLocation=newModelResourceLocation(getRegistryName(),"inventory");finalintDEFAULT_ITEM_SUBTYPE=0;Minecraft.getMinecraft().getRenderItem().getItemModelMesher().register(itemBlock,DEFAULT_ITEM_SUBTYPE,itemModelResourceLocation);}@OverridepublicbooleanisBlockNormalCube(IBlockStateblockState){returnfalse;}@OverridepublicbooleanisOpaqueCube(IBlockStateblockState){returnfalse;}@OverrideprotectedBlockStatecreateBlockState(){IProperty[]listedProperties=newIProperty[0];// no listed propertiesIUnlistedProperty[]unlistedProperties=newIUnlistedProperty[]{NORTH,SOUTH,WEST,EAST,UP,DOWN};returnnewExtendedBlockState(this,listedProperties,unlistedProperties);}@OverridepublicIBlockStategetExtendedState(IBlockStatestate,IBlockAccessworld,BlockPospos){IExtendedBlockStateextendedBlockState=(IExtendedBlockState)state;booleannorth=isSameBlock(world,pos.north());booleansouth=isSameBlock(world,pos.south());booleanwest=isSameBlock(world,pos.west());booleaneast=isSameBlock(world,pos.east());booleanup=isSameBlock(world,pos.up());booleandown=isSameBlock(world,pos.down());returnextendedBlockState.withProperty(NORTH,north).withProperty(SOUTH,south).withProperty(WEST,west).withProperty(EAST,east).withProperty(UP,up).withProperty(DOWN,down);}privatebooleanisSameBlock(IBlockAccessworld,BlockPospos){returnworld.getBlockState(pos).getBlock()==ModBlocks.bakedModelBlock;}}

To get our baked model working we need three things. First we need a model (implementation of IModel). That doesn't do much more then act as a way to create our baked model (IBakedModel implementation) which does the actual work. And finally we need a custom model loader (implementation of ICustomModelLoader) so that we can load our new model from within json.

First we make our model. This is the class that is responsible for making our baked model:

Now we have to define our baked model. This is a bit more complex but the important thing here is that this baked model calculates the quads (faces of our model) based on the input state which is coming from our block. To be able to support multiple formats with this (since we don't know how our model will be baked) we use a general putVertex() here that can convert a vertex to the appropriate format automatically:

publicclassExampleBakedModelimplementsIBakedModel{publicstaticfinalModelResourceLocationBAKED_MODEL=newModelResourceLocation(ModTut.MODID+":bakedmodelblock");privateTextureAtlasSpritesprite;privateVertexFormatformat;publicExampleBakedModel(IModelStatestate,VertexFormatformat,Function<ResourceLocation,TextureAtlasSprite>bakedTextureGetter){this.format=format;sprite=bakedTextureGetter.apply(newResourceLocation(ModTut.MODID,"blocks/isbmtexture"));}privatevoidputVertex(UnpackedBakedQuad.Builderbuilder,Vec3dnormal,doublex,doubley,doublez,floatu,floatv){for(inte=0;e<format.getElementCount();e++){switch(format.getElement(e).getUsage()){casePOSITION:builder.put(e,(float)x,(float)y,(float)z,1.0f);break;caseCOLOR:builder.put(e,1.0f,1.0f,1.0f,1.0f);break;caseUV:if(format.getElement(e).getIndex()==0){u=sprite.getInterpolatedU(u);v=sprite.getInterpolatedV(v);builder.put(e,u,v,0f,1f);break;}caseNORMAL:builder.put(e,(float)normal.x,(float)normal.y,(float)normal.z,0f);break;default:builder.put(e);break;}}}privateBakedQuadcreateQuad(Vec3dv1,Vec3dv2,Vec3dv3,Vec3dv4,TextureAtlasSpritesprite){Vec3dnormal=v3.subtract(v2).crossProduct(v1.subtract(v2)).normalize();UnpackedBakedQuad.Builderbuilder=newUnpackedBakedQuad.Builder(format);builder.setTexture(sprite);putVertex(builder,normal,v1.x,v1.y,v1.z,0,0);putVertex(builder,normal,v2.x,v2.y,v2.z,0,16);putVertex(builder,normal,v3.x,v3.y,v3.z,16,16);putVertex(builder,normal,v4.x,v4.y,v4.z,16,0);returnbuilder.build();}@OverridepublicList<BakedQuad>getQuads(IBlockStatestate,EnumFacingside,longrand){if(side!=null){returnCollections.emptyList();}IExtendedBlockStateextendedBlockState=(IExtendedBlockState)state;Booleannorth=extendedBlockState.getValue(BakedModelBlock.NORTH);Booleansouth=extendedBlockState.getValue(BakedModelBlock.SOUTH);Booleanwest=extendedBlockState.getValue(BakedModelBlock.WEST);Booleaneast=extendedBlockState.getValue(BakedModelBlock.EAST);Booleanup=extendedBlockState.getValue(BakedModelBlock.UP);Booleandown=extendedBlockState.getValue(BakedModelBlock.DOWN);List<BakedQuad>quads=newArrayList<>();doubleo=.4;// For each side we either cap it off if there is no similar block adjacent on that side// or else we extend so that we touch the adjacent block:if(up){quads.add(createQuad(newVec3d(1-o,1-o,o),newVec3d(1-o,1,o),newVec3d(1-o,1,1-o),newVec3d(1-o,1-o,1-o),sprite));quads.add(createQuad(newVec3d(o,1-o,1-o),newVec3d(o,1,1-o),newVec3d(o,1,o),newVec3d(o,1-o,o),sprite));quads.add(createQuad(newVec3d(o,1,o),newVec3d(1-o,1,o),newVec3d(1-o,1-o,o),newVec3d(o,1-o,o),sprite));quads.add(createQuad(newVec3d(o,1-o,1-o),newVec3d(1-o,1-o,1-o),newVec3d(1-o,1,1-o),newVec3d(o,1,1-o),sprite));}else{quads.add(createQuad(newVec3d(o,1-o,1-o),newVec3d(1-o,1-o,1-o),newVec3d(1-o,1-o,o),newVec3d(o,1-o,o),sprite));}if(down){quads.add(createQuad(newVec3d(1-o,0,o),newVec3d(1-o,o,o),newVec3d(1-o,o,1-o),newVec3d(1-o,0,1-o),sprite));quads.add(createQuad(newVec3d(o,0,1-o),newVec3d(o,o,1-o),newVec3d(o,o,o),newVec3d(o,0,o),sprite));quads.add(createQuad(newVec3d(o,o,o),newVec3d(1-o,o,o),newVec3d(1-o,0,o),newVec3d(o,0,o),sprite));quads.add(createQuad(newVec3d(o,0,1-o),newVec3d(1-o,0,1-o),newVec3d(1-o,o,1-o),newVec3d(o,o,1-o),sprite));}else{quads.add(createQuad(newVec3d(o,o,o),newVec3d(1-o,o,o),newVec3d(1-o,o,1-o),newVec3d(o,o,1-o),sprite));}if(east){quads.add(createQuad(newVec3d(1-o,1-o,1-o),newVec3d(1,1-o,1-o),newVec3d(1,1-o,o),newVec3d(1-o,1-o,o),sprite));quads.add(createQuad(newVec3d(1-o,o,o),newVec3d(1,o,o),newVec3d(1,o,1-o),newVec3d(1-o,o,1-o),sprite));quads.add(createQuad(newVec3d(1-o,1-o,o),newVec3d(1,1-o,o),newVec3d(1,o,o),newVec3d(1-o,o,o),sprite));quads.add(createQuad(newVec3d(1-o,o,1-o),newVec3d(1,o,1-o),newVec3d(1,1-o,1-o),newVec3d(1-o,1-o,1-o),sprite));}else{quads.add(createQuad(newVec3d(1-o,o,o),newVec3d(1-o,1-o,o),newVec3d(1-o,1-o,1-o),newVec3d(1-o,o,1-o),sprite));}if(west){quads.add(createQuad(newVec3d(0,1-o,1-o),newVec3d(o,1-o,1-o),newVec3d(o,1-o,o),newVec3d(0,1-o,o),sprite));quads.add(createQuad(newVec3d(0,o,o),newVec3d(o,o,o),newVec3d(o,o,1-o),newVec3d(0,o,1-o),sprite));quads.add(createQuad(newVec3d(0,1-o,o),newVec3d(o,1-o,o),newVec3d(o,o,o),newVec3d(0,o,o),sprite));quads.add(createQuad(newVec3d(0,o,1-o),newVec3d(o,o,1-o),newVec3d(o,1-o,1-o),newVec3d(0,1-o,1-o),sprite));}else{quads.add(createQuad(newVec3d(o,o,1-o),newVec3d(o,1-o,1-o),newVec3d(o,1-o,o),newVec3d(o,o,o),sprite));}if(north){quads.add(createQuad(newVec3d(o,1-o,o),newVec3d(1-o,1-o,o),newVec3d(1-o,1-o,0),newVec3d(o,1-o,0),sprite));quads.add(createQuad(newVec3d(o,o,0),newVec3d(1-o,o,0),newVec3d(1-o,o,o),newVec3d(o,o,o),sprite));quads.add(createQuad(newVec3d(1-o,o,0),newVec3d(1-o,1-o,0),newVec3d(1-o,1-o,o),newVec3d(1-o,o,o),sprite));quads.add(createQuad(newVec3d(o,o,o),newVec3d(o,1-o,o),newVec3d(o,1-o,0),newVec3d(o,o,0),sprite));}else{quads.add(createQuad(newVec3d(o,1-o,o),newVec3d(1-o,1-o,o),newVec3d(1-o,o,o),newVec3d(o,o,o),sprite));}if(south){quads.add(createQuad(newVec3d(o,1-o,1),newVec3d(1-o,1-o,1),newVec3d(1-o,1-o,1-o),newVec3d(o,1-o,1-o),sprite));quads.add(createQuad(newVec3d(o,o,1-o),newVec3d(1-o,o,1-o),newVec3d(1-o,o,1),newVec3d(o,o,1),sprite));quads.add(createQuad(newVec3d(1-o,o,1-o),newVec3d(1-o,1-o,1-o),newVec3d(1-o,1-o,1),newVec3d(1-o,o,1),sprite));quads.add(createQuad(newVec3d(o,o,1),newVec3d(o,1-o,1),newVec3d(o,1-o,1-o),newVec3d(o,o,1-o),sprite));}else{quads.add(createQuad(newVec3d(o,o,1-o),newVec3d(1-o,o,1-o),newVec3d(1-o,1-o,1-o),newVec3d(o,1-o,1-o),sprite));}returnquads;}@OverridepublicItemOverrideListgetOverrides(){returnnull;}@OverridepublicbooleanisAmbientOcclusion(){returnfalse;}@OverridepublicbooleanisGui3d(){returnfalse;}@OverridepublicbooleanisBuiltInRenderer(){returnfalse;}@OverridepublicTextureAtlasSpritegetParticleTexture(){returnsprite;}@OverridepublicItemCameraTransformsgetItemCameraTransforms(){returnItemCameraTransforms.DEFAULT;}}

In our ModBlocks class we need to define a new entry to initialize the item model for our baked model block. initItemModels() has to be called from ClientProxy.init():

Finally, even though we use a baked model we still need to define json's for the block states and models. For example, for our inventory model (what is shown in the inventory itself) and also a dummy block model that will get replaced with the baked model. First here is the blockstate (blockstates/bakedmodelblock.json):