[1.18.X] Add events for collecting static and dynamic level geometry
As things stand right now, the rendering hooks provided by Forge are quite minimal, which limits the options natively available. This pull request aims to provide the two most useful level-related geometry collection events.
Implementation
Two events are provided: GatherGeometryEvent.ChunkSectionStatic and GatherGeometryEvent.LevelDynamic.
The former is fired every time a chunk section (16x16x16) is baked before any block geometry is added. This event provides a MultiBufferSource, a section origin position, a RenderChunkRegion which provides access to the blocks, and the visibility graph used to determine chunk section occlusion.
The latter is fired every frame after Entity rendering, but ahead of BlockEntity rendering. This event also provides a MultiBufferSource, as well as the active level renderer, pose stack, camera and partial tick time.
The static chunk geometry event requires a multi-buffer source to be provided by Forge, as the backing buffer container is a ChunkBufferBuilderPack. This buffer source ensures that the requested render type can be used for static rendering (see #8331) and initializes the layer's buffer. After firing the event, the layers containing geometry are marked as such so the chunk section knows whether to render or not.
Lastly, a test mod is provided (mod id geometry_gathering_test) which statically renders an obsidian block with full brightness at (0, 80, 0) in every chunk, and dynamically renders a stone block 5 meters above the player's head.
Demo
Here are some images of the obsidian and stone blocks previously mentioned:


Performance implications
The chunk section geometry-gathering event is fired for as many vertical sections as a chunk has (dependent on world height), for every chunk that needs to be re-rendered. Due to being fired before any blocks have been populated, the time iterating the visited render types should be minimal, and the overall performance impact should not be noticeable, even when re-rendering the entire level.
The level geometry-gathering event is fired once per frame for the current level, and has the same performance implications as the current RenderLevelLastEvent - that is, negligible.
Maintainability
These events shouldn't require any ongoing maintenance.
Hot
I have played a bit with this PR, specifically the dynamic geometry event. In general it seems pretty solid to me.
The only thing I noticed is that while solid and cutout render types work perfectly fine, the whole thing falls apart with certain combinations of translucent types and graphics modes. Do note that I have only tested the translucent types that use DefaultVertexFormat.BLOCK because I can render a block with those.
The results were as follows:
RenderType.translucent():- Fast, Fancy: hides other translucent types (Stained Glass, Water, block outline, etc.) behind it
- Fabulous: doesn't render at all
RenderType.translucentMovingBlock():- Fast, Fancy: hides other translucent types (Stained Glass, Water, block outline, etc.) behind it
- Fabulous: works fine
RenderType.translucentNoCrumbling():- Fast, Fancy: works fine
- Fabulous: doesn't render at all
RenderType.beaconBeam(TextureAtlas.LOCATION_BLOCKS, true):- Fast, Fancy, Fabulous: draws behind other translucent types (Stained Glass, Water, block outline, etc.)
RenderType.crumbling(TextureAtlas.LOCATION_BLOCKS):- Fast, Fancy, Fabulous: works fine
RenderType.tripwire():- Fast, Fancy: hides other translucent types (Stained Glass, Water, block outline, etc.) behind it
- Fabulous: works fine
I am not sure what the best solution for this would be.
Leaving it this way would make this new event similarly akward to use as the current RenderLevelLastEvent.
Trying to fire the event again at a later point closer to translucent rendering and adding a flag to it to distinguish between solid and translucent didn't get me any better results either.
This is the code snippet I added to the test mod to test this:
private static final Supplier<BlockState> TRANSLUCENT_STATE = Suppliers.memoize(() ->
Blocks.GREEN_STAINED_GLASS.defaultBlockState()
);
private static final List<Supplier<RenderType>> TYPES = List.of(
Suppliers.memoize(() -> RenderType.translucent()),
Suppliers.memoize(() -> RenderType.translucentMovingBlock()),
Suppliers.memoize(() -> RenderType.translucentNoCrumbling()),
Suppliers.memoize(() -> RenderType.beaconBeam(TextureAtlas.LOCATION_BLOCKS, true)),
Suppliers.memoize(() -> RenderType.crumbling(TextureAtlas.LOCATION_BLOCKS)),
Suppliers.memoize(() -> RenderType.tripwire())
);
@SubscribeEvent
public static void onGatherDynamicLevelGeometryTranslucent(final GatherGeometryEvent.LevelDynamic event)
{
if (!ENABLED) return;
HitResult mouseOver = Minecraft.getInstance().hitResult;
if (mouseOver == null || mouseOver.getType() != HitResult.Type.BLOCK) { return; }
BlockHitResult target = (BlockHitResult) mouseOver;
BlockPos renderPos = target.getBlockPos().relative(target.getDirection());
Vec3 camera = Minecraft.getInstance().gameRenderer.getMainCamera().getPosition();
Vec3 offset = Vec3.atLowerCornerOf(renderPos).subtract(camera);
PoseStack poseStack = event.getPoseStack();
poseStack.pushPose();
poseStack.translate(offset.x, offset.y, offset.z);
int slot = Minecraft.getInstance().player.getInventory().selected;
RenderType renderType = TYPES.get(slot % TYPES.size()).get();
BlockRenderDispatcher blockRenderer = Minecraft.getInstance().getBlockRenderer();
blockRenderer.getModelRenderer().renderModel(
poseStack.last(),
event.getBufferSource().getBuffer(renderType),
TRANSLUCENT_STATE.get(),
blockRenderer.getBlockModel(TRANSLUCENT_STATE.get()),
1f, 1f, 1f,
0x0E00E0,
OverlayTexture.NO_OVERLAY,
EmptyModelData.INSTANCE
);
poseStack.popPose();
}
The event for static geometry is (in most cases) fired off-thread AFAICT, this should probably be noted in the JavaDoc. This also means that any data used for rendering in this event needs to be stored in a thread-safe manner. For my own use-case (IE wires) this is not a major issue since only a very small part of the data structure needs to be thread-safe, but in general some equivalent of BlockEntity#getModelData (i.e. a way to gather data on-thread) might be nice to have.
(Note: BE#getModelData has JavaDoc claiming that it may be called off-thread, but according to the designer of the IModelData system this is intentionally not the case.)
The ChunkRenderDispatcher.RenderChunk.RebuildTask where the static geometry event is fired extends ChunkRenderDispatcher.RenderChunk.ChunkCompileTask, which in turn has a copy of all IModelData from the chunk the task is working on. Allowing access to that data would be as simple as passing this.modelData to the event and adding a GatherGeometryEvent.ChunkSectionStatic#getModelData(BlockPos) method.
IMO just giving access to the block model data is not enough, it's not unreasonable to want to render things into a chunk (section) without any blocks controlled by your mod. E.g. IE wires can easily pass through sections that don't contain any connectors (or other IE blocks for that matter).
Agreed with malte, most use cases I can think of won't be tied into blocks at all. Also, I am not sure whether supplying the IModelData in the event would be a good Idea. The IModelData was added to be used for block models, if it's provided here, many people would likely just use this event, because it's easier.
So you essentially want something similar to the existing ModelDataManager but not bound to a block in the world and instead to a chunk or chunk section, correct?
My (not very thought-through) approach would have been to fire the event on-thread and only have it gather a list of ISectionRenderers (or similar). This would be a functional interface with a render method that takes the current fields of the event. The ISectionRenderers would then be invoked off-thread, ideally doing most of the work there. The usage would be something like
var data = gatherDataForChunk(event.getOrigin());
event.addRenderer((buffers, [...]) -> render(buffers, data));
But something more structured like what you suggested should also work.
I'm not 100% sure what the purpose of the dynamic level geometry is
The dynamic level geometry is intended to replace RenderLevelLastEvent because that event is cumbersome to use and is also completely broken when used for translucent rendering while Fabulous mode is active.
I'll be re-evaluating the implementation as per everyone's comments and port it to 1.19 soon™. I'll close this PR and open a new one once it's ready. I could keep the same one, but it's going to be redone from scratch due to the all changes introduced in this area during the client cleanup/refactor, so I may as well keep them separate and just reference this PR in the other one.