Skip to content

[1.18.X] Add events for collecting static and dynamic level geometry#8337

Closed
amadornes wants to merge 4 commits intoMinecraftForge:1.18.xfrom
amadornes:feature_gather_chunk_geometry
Closed

[1.18.X] Add events for collecting static and dynamic level geometry#8337
amadornes wants to merge 4 commits intoMinecraftForge:1.18.xfrom
amadornes:feature_gather_chunk_geometry

Conversation

@amadornes
Copy link
Copy Markdown
Contributor

@amadornes amadornes commented Dec 31, 2021

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:

obsidian blocks

stone block

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.

@KitsuneAlex
Copy link
Copy Markdown

Hot

@sciwhiz12 sciwhiz12 added 1.18 Feature This request implements a new feature. New Event This request implements a new event. Rendering This request deals with rendering. These must be treated with care, and tend to progress slowly. Triage This request requires the active attention of the Triage Team. Requires labelling or reviews. labels Dec 31, 2021
@XFactHD
Copy link
Copy Markdown
Contributor

XFactHD commented Jan 2, 2022

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();
}

@malte0811
Copy link
Copy Markdown
Contributor

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.)

@XFactHD
Copy link
Copy Markdown
Contributor

XFactHD commented Jan 4, 2022

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.

@malte0811
Copy link
Copy Markdown
Contributor

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).

@JTK222
Copy link
Copy Markdown
Contributor

JTK222 commented Jan 4, 2022

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.

@XFactHD
Copy link
Copy Markdown
Contributor

XFactHD commented Jan 4, 2022

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?

@malte0811
Copy link
Copy Markdown
Contributor

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.

@sciwhiz12 sciwhiz12 requested a review from gigaherz February 25, 2022 12:10
Copy link
Copy Markdown
Contributor

@gigaherz gigaherz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@XFactHD
Copy link
Copy Markdown
Contributor

XFactHD commented Apr 10, 2022

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.

Copy link
Copy Markdown
Contributor

@sciwhiz12 sciwhiz12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine to me, pending some testing after the requested changes are made.

@@ -0,0 +1,119 @@
package net.minecraftforge.client.event;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing license header, please run the updateLicenses/licenseFormat task.

Comment on lines +61 to +64
public BlockPos.MutableBlockPos getOrigin()
{
return origin;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines +109 to +112
public float getPartialTicks()
{
return partialTicks;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer partialTick myself, since this is a fraction of a tick -- a partial tick.

Comment on lines +1 to +18
/*
* 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
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +55 to +59
@Nullable
public RenderChunkRegion getRegion()
{
return region;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +14 to +21
/**
* 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
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. 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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +130 to +132
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please run the checkATs task to automatically move these lines to their proper sorted position.

@sciwhiz12 sciwhiz12 removed the Triage This request requires the active attention of the Triage Team. Requires labelling or reviews. label May 13, 2022
@sciwhiz12 sciwhiz12 added Needs Update This request requires an update from the author to confirm whether the request is relevant. Needs Rebase This PR requires a rebase to implement upstream changes (usually to fix merge conflicts). labels May 26, 2022
@amadornes
Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.18 Feature This request implements a new feature. Needs Rebase This PR requires a rebase to implement upstream changes (usually to fix merge conflicts). Needs Update This request requires an update from the author to confirm whether the request is relevant. New Event This request implements a new event. Rendering This request deals with rendering. These must be treated with care, and tend to progress slowly.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants