[Question]: How Should I Manually Render Framed Blocks For My Mod?
19PHOBOSS98 opened this issue ยท 22 comments
Question
Hello! I'm deving a mod and I want it to work with yours on release. I'm not sure if this counts as a "Mod Support" since I'm not really asking you to change anything in your mod for my sake ("mod integration"). I just wanted to know how camouflaged FramedBlocks are rendered so that I can render it properly in my mod (passively that is, It's not a FramedBlocks addon).
I've setup a local copy of FramedBlocks but I'm having a hard time tracing how they are rendered. I tried looking for how camoStack
and camoState
are used but my IDE can't find what's using them.
Right, my mod. It renders fake structures from a projector block... that's it :)
For now, I've set it up to take the block right below it and render a fake version of it 5 blocks above the projector:
But for some reason I can't render it with its camouflage:
I also tried using renderBlockAsEntity(...)
but I still couldn't get it to work right:
I tried looking at the create mod and how it renders contraptions with framed blocks and even tried asking the devs themselves about it but ultimately they told me to ask you personally about how framed blocks get rendered.
Am I missing something? a rendering method that I'm not familiar with maybe? Help would be really appreciated :)
oh, thanks for the insight. I never could fully understand how they do it until now. Tho, why not just get the modelData from the blockEntity instead?
I just updated the thing yesterday. What I do is "build" it all on the main level
from an nbt structure file:
Tho what I do is just get the model data from the blockEntity instead of using ModelDataManager
. For full blocks that don't have blockEntities I just render them normally using blockRenderManager.renderBlock(...)
Is there a special block out there that I should watch out for?
Tho what I do is just get the model data from the blockEntity instead of using ModelDataManager.
That will work fine. The main idea behind ModelDataManager
is to cache the data instead of retrieving it on the chunk render threads, which is not relevant for your use case. For 99% of blocks, I doubt there will be a significant performance issue from bypassing it.
oh ok cool!... tho I'm having a bit of trouble with tinted framed blocks (the dirt/leaves framed slope). referencing the block bellow the projector doesn't seem to work anymore to have it render the right color. Switching the enchanting table bellow with a dirt-framed-slop block doesn't do it either... I didn't look into it any further the first time, but could you guys explain how framed blocks actually choose their tint colors?
I would highly recommend not trying to hack this into the main level, that's really just begging for severe issues.
The tinting is done by getting the relevant camo directly from the BlockEntity
(which, to be fair, isn't great but unavoidable in 1.18 and a pain in the ass to do cleaner in any version) and then requesting the tint of the camo block: https://github.com/XFactHD/FramedBlocks/blob/1.18.x/src/main/java/xfacthd/framedblocks/client/util/FramedBlockColor.java, that's why it only works if a block with the exact camo or a camo that has identical tinting behavior is at the target position. This is another case where using a fake level would have benefits.
I would highly recommend not trying to hack this into the main level, that's really just begging for severe issues.
oh ok, tho I really wouldn't want to go thru Create's forest of a codebase to figure out how to spawn in a fake level. You guys have any other recommendations? maybe something a bit more simpler to understand?
Unfortunately not, no. In theory a custom implementation of BlockAndTintGetter
(BlockRenderView
in Yarn) would be sufficient since that's all the render methods expect and it would be very easy to implement but in practice that's also going to fail catastrophically since BEs require a full level, their implementations will expect it to be present when you start calling stuff on them and giving the BEs the actual level is most definitely going to end catastrophically as well. In other words, there is really no good way around a fake level if you want to fix these issues. But yes, I agree that the Create codebase is very messy and hard to navigate.
Lol, yeah. I guess I'll have to dredge thru it then. Thanks anyways... I'll try and find a way to implement a fake level somewhere else. Mind if I keep this issue open until then. I wouldn't want to end this here if anyone wants to know how
A first glance suggests you might just need to retrieve the ModelData
for that position using ModelDataManager
and pass it to the appropriate block rendering method. Without model data the block won't know how to camouflage itself, as far as I know.
Hello again. I tried this but I keep getting this error where it's telling me the index is out of bounds:
crash-2024-01-01_00.32.10-client.txt
I tried tracing it myself but it seems like my IDE cant for some reason. It keeps pointing at a line that's not there over at WorldSlice.java
. I think it has something to do with Rubidium. I'll try disabling it, brb.
Right, I've taken a look at the ChunkBuilder
class (forge) and I tried copying how it uses ModelData
:
I've enabled Rubidium back on and it stopped crashing the game but it still doesn't render the camo:
I meant to reply to this yesterday but didn't get to actually look into it sufficiently. I'll get back to you tomorrow to get you the necessary details.
Happy new year to you as well.
sorry for the late reply. Thanks! I'll see what I can do. Happy New Year as well!
Okay, looking over what you have done, that is almost exactly what you should have to do to make this work. The only missing piece is that the
BlockRenderDispatcher#renderBatched()
call should be wrapped in a loop that sets up render types to allow the model to return the correct quads for each render type and then reset it afterwards:for (RenderType renderType : RenderType.chunkBufferLayers()) { ForgeHooksClient.setRenderType(renderType); blockRenderDispatcher.render(..., modelData); } ForgeHooksClient.setRenderType(null);However, in practice this unfortunately won't get you far either: Due to how model data was implemented in 1.18 and earlier, I had to add a very dirty workaround which meant that the data is never in the
ModelDataManager
and instead is retrieved by the model itself ingetModelData()
(this is part of the reason why you got that crash). In 1.19 this system got overhauled, theModelDataManager
was changed to be attached to theLevel
instead of being global, which means the data of my blocks will be in the manager in 1.19 and later. In 1.19 the render type stuff also got significantly simpler and cleaner and now just looks like this:for (RenderType renderType : model.getRenderTypes(blockState, RANDOM, modelData)) { blockRenderDispatcher.render(..., modelData, renderType); }The TL;DR of it is that in 1.18 and earlier there is nothing either of us can do to make this work properly due to the model data being poorly designed in those versions.
The question still stands tho, what exactly am I suppose to watch out for whenever I attempt to mess with the ModelDataManager in 1.18?
I meant to reply to this yesterday but didn't get to actually look into it sufficiently. I'll get back to you tomorrow to get you the necessary details.
Happy new year to you as well.
anything yet?
Okay, looking over what you have done, that is almost exactly what you should have to do to make this work. The only missing piece is that the BlockRenderDispatcher#renderBatched()
call should be wrapped in a loop that sets up render types to allow the model to return the correct quads for each render type and then reset it afterwards:
for (RenderType renderType : RenderType.chunkBufferLayers()) {
ForgeHooksClient.setRenderType(renderType);
blockRenderDispatcher.render(..., modelData);
}
ForgeHooksClient.setRenderType(null);
However, in practice this unfortunately won't get you far either: Due to how model data was implemented in 1.18 and earlier, I had to add a very dirty workaround which meant that the data is never in the ModelDataManager
and instead is retrieved by the model itself in getModelData()
(this is part of the reason why you got that crash).
In 1.19 this system got overhauled, the ModelDataManager
was changed to be attached to the Level
instead of being global, which means the data of my blocks will be in the manager in 1.19 and later. In 1.19 the render type stuff also got significantly simpler and cleaner and now just looks like this:
for (RenderType renderType : model.getRenderTypes(blockState, RANDOM, modelData)) {
blockRenderDispatcher.render(..., modelData, renderType);
}
The TL;DR of it is that in 1.18 and earlier there is nothing either of us can do to make this work properly due to the model data being poorly designed in those versions.
slr. I've implemented the renderType for loop as you suggested:
and everything seems to be working fine now:
Except for grass blocks and leaves being grayed out:
Is this what you meant? would moving to 1.19 solve this "graying out" issue?
oh wait, I think I remember something about color providers doing this, let me check...
Anyways, thank you for all the help guys. It means a lot. I'll be attributing your help when my mod releases
The question still stands tho, what exactly am I suppose to watch out for whenever I attempt to mess with the ModelDataManager in 1.18?
The issue with the ModelDataManager
in 1.18 and earlier is that it will throw stones at you if you try to refresh or retrieve data for a position in a Level
that is not the main client level. This is an issue for mods like Create which uses fake levels as part of their contraption system, Immersive Portals which renders parts of a different level through portals and also mods like yours if you represent the structures as a fake level (which is the "easiest" way to circumvent issues like the undesired face culling shown on one of your screenshots).
I think I got most of it down.
You can extend the World
class and just use it as a container to keep a list of blocks to render. I used a custom Structure
class to load in structure nbt
files into my custom World
class.
The vanilla Structure
class's method to rotate and mirror entities (like paintings and frames) surprisingly still needs work:
https://bugs.mojang.com/browse/MC-102223
Luckily, a lot of mods already solved this issue: Litematica, Create, Schematica just to name a few. I highly recommend looking at their code as reference.
For now, I've settled with saving the blocks into a Long2ObjectOpenHashMap
in my custom World
class. I use BlockPos.asLong()
as the keys to query the hashmap.
Block, BlockEntity and Entity rendering seems straight forward enough. I looked at the WorldRenderer.render(...)
method and just copied over the methods I needed.
For animated BlockEntities I had to make a separate list to save each of the BlockEntityTickers
.
For my mod I have a MirageProjector
block that has a tick
method so I just loop thru the list of tickers in there and manually tick()
each of them.
Same thing happens for rendering each block and blockEntity. I loop thru the entire list in the MirageProjector's BlockEntityRenderer
.
This approach is resource intensive. It brought my game down to ~30 fps from ~113 fps when I loaded this in:
https://youtu.be/F8NW9hn_Gqs?si=9TY_MXuqIwJIdFdD
TBH, I should be using the Chunk
class to save the blocks instead of a hashmap. I'm not really sure if Minecraft's rendering pipeline would allow me to use multiple threads to render chunks in parallel. I still have to look into it...
Oh right... Liquids... I still don't get how to render them. It seems like I have to tick them in a separate temporary thread to get them to "flow" before actually rendering them but I haven't looked into how that would work in my mod...
That's the gist of it all, if anyone would be interested. The rest is just ironing out exception throws.
Thanks again guys for your help!