[1.18.X] Add events for collecting static and dynamic level geometry#8337
[1.18.X] Add events for collecting static and dynamic level geometry#8337amadornes wants to merge 4 commits intoMinecraftForge:1.18.xfrom
Conversation
|
Hot |
|
I have played a bit with this PR, specifically the dynamic geometry event. In general it seems pretty solid to me.
I am not sure what the best solution for this would be. 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 |
|
The |
|
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. |
|
So you essentially want something similar to the existing |
|
My (not very thought-through) approach would have been to fire the event on-thread and only have it gather a list of var data = gatherDataForChunk(event.getOrigin());
event.addRenderer((buffers, [...]) -> render(buffers, data));But something more structured like what you suggested should also work. |
There was a problem hiding this comment.
I'm not 100% sure what the purpose of the dynamic level geometry is, but the rest looks sane to me, and is a feature I have wanted to see for a long time.
(Sorry for not reviewing sooner, it's rare for me these days to have both time and brain energy, but not be too busy worrying about other personal projects)
Note: this review does not take into account all the concerns from comments, only the code itself.
The dynamic level geometry is intended to replace |
sciwhiz12
left a comment
There was a problem hiding this comment.
Looks fine to me, pending some testing after the requested changes are made.
| @@ -0,0 +1,119 @@ | |||
| package net.minecraftforge.client.event; | |||
There was a problem hiding this comment.
Missing license header, please run the updateLicenses/licenseFormat task.
| public BlockPos.MutableBlockPos getOrigin() | ||
| { | ||
| return origin; | ||
| } |
There was a problem hiding this comment.
What origin within the chunk section does this block position represent? Is this intended for consumers to modify, given that we explicitly declare the return type to be MutableBlockPos? If not, could this be changed into BlockPos instead and noted that callers must call immutable() on this object if they intend on storing the position outside the bounds of the event?
| public float getPartialTicks() | ||
| { | ||
| return partialTicks; | ||
| } |
There was a problem hiding this comment.
I prefer partialTick myself, since this is a fraction of a tick -- a partial tick.
| /* | ||
| * Minecraft Forge | ||
| * Copyright (c) 2016-2021. | ||
| * | ||
| * This library is free software; you can redistribute it and/or | ||
| * modify it under the terms of the GNU Lesser General Public | ||
| * License as published by the Free Software Foundation version 2.1 | ||
| * of the License. | ||
| * | ||
| * This library is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| * Lesser General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Lesser General Public | ||
| * License along with this library; if not, write to the Free Software | ||
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
| */ |
There was a problem hiding this comment.
Outdated license header, please update this PR to the latest commit of the base branch and run the updateLicenses/licenseFormat task.
| import java.util.function.Supplier; | ||
|
|
||
| @Mod(GeometryGatheringTest.MOD_ID) | ||
| public class GeometryGatheringTest |
There was a problem hiding this comment.
Please provide a javadoc for this class which explains what feature this tests, how to test it, and what the expected results are from said test.
| @Nullable | ||
| public RenderChunkRegion getRegion() | ||
| { | ||
| return region; | ||
| } |
There was a problem hiding this comment.
Why is this @Nullable? Looking at the call site for this event, it seems that this will never be null, as the line directly before this event is fired is a null check on the variable passed to this.
| /** | ||
| * Fired on the {@link net.minecraftforge.common.MinecraftForge#EVENT_BUS} | ||
| * on {@link net.minecraftforge.api.distmarker.Dist#CLIENT} when geometry | ||
| * is being collected from its respective sources. | ||
| * | ||
| * @see ChunkSectionStatic | ||
| * @see LevelDynamic | ||
| */ |
There was a problem hiding this comment.
For the sake of completeness, please include where this event and subclasses are fired in their javadocs.
| MinecraftForge.EVENT_BUS.post(new GatherGeometryEvent.ChunkSectionStatic( | ||
| t -> { | ||
| var buf = chunkBufferBuilderPack.builder(t); | ||
| Preconditions.checkNotNull(buf, "The requested render type is not configured for static rendering: " + t); |
There was a problem hiding this comment.
As a point of technicality, the more appropriate precondition here is checkArgument (buf != null), as it would be inappropriate for a method to fire NullPointerException when the render type given to it is not null but an invalid/illegal argument.
There was a problem hiding this comment.
I disagree. From the NullPointerException Javadocs:
Applications should throw instances of this class to indicate other illegal uses of the null object
Additionally, the JDK built-in null validation method Objects.requireNonNull, which "is designed primarily for doing parameter validation in methods and constructors" also throws NullPointerException
There was a problem hiding this comment.
Respectfully, I disagree with your disagreement. If the value being validated by checkNotNull is t, then it would be appropriate to throw NullPointerException, as the validation checks that the caller's provided value is not a null object.
However, the value under validation is buf, which is the result of ChunkBufferBuilderPack#builder with the t parameter passed in as an argument. This is why NullPointerException is inappropriate here: a caller which provides a non-null argument should not be expecting a NullPointerException thrown when their argument is somehow invalid (as they'd expect IllegalArgumentException).
The details of how the t parameter is determined to be invalid is an implementation detail1 -- in this case, checking if the return value of ChunkBufferBuilderPack#builder for the given render type is null, which indicates that the render type is not configured for static rendering and therefore invalid. NullPointerException leaks this implementation detail to the caller.
Footnotes
-
It is an implementation detail because the way by which the validity of the argument can change without affecting the contract of the method, such as invoking a helper method in another class which returns a
boolean(and the way by which that method checks the validity of the argument is an implementation detail itself). ↩
There was a problem hiding this comment.
I accept your disagreement 😄
| * Fired when the client is gathering static geometry and | ||
| * occlusion data for a 16x16x16 section of a chunk. | ||
| */ | ||
| public static class ChunkSectionStatic extends GatherGeometryEvent |
There was a problem hiding this comment.
Perhaps it would be good to overrider getBufferSource here and specify in its javadocs that only render types configured for static rendering (as defined currently in RenderType#chunkBufferLayers() and in the future by #8337) may be used with the buffer source, and render types not configured for such will cause an exception to be thrown.
| public net.minecraft.client.renderer.chunk.ChunkRenderDispatcher$CompiledChunk f_112749_ # hasBlocks | ||
| public net.minecraft.client.renderer.chunk.ChunkRenderDispatcher$CompiledChunk f_112750_ # hasLayer | ||
| public net.minecraft.client.renderer.chunk.ChunkRenderDispatcher$CompiledChunk f_112751_ # isCompletelyEmpty |
There was a problem hiding this comment.
Please run the checkATs task to automatically move these lines to their proper sorted position.
|
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. |
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.ChunkSectionStaticandGatherGeometryEvent.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, aRenderChunkRegionwhich provides access to the blocks, and the visibility graph used to determine chunk section occlusion.The latter is fired every frame after
Entityrendering, but ahead ofBlockEntityrendering. This event also provides aMultiBufferSource, 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.