Skip to content

Rendering Plug-In and Enhanced/Dynamic Model APIs #55

Closed
grondag wants to merge 34 commits intoFabricMC:masterfrom
grondag:master
Closed

Rendering Plug-In and Enhanced/Dynamic Model APIs #55
grondag wants to merge 34 commits intoFabricMC:masterfrom
grondag:master

Conversation

@grondag
Copy link
Copy Markdown
Contributor

@grondag grondag commented Jan 2, 2019

This PR defines standards for implementing a RenderPlugin and standard vertex formats and interfaces to be used by enhanced and/or dynamic models to provide vertex data to such plugins. The intention is to allow cross-compatibility across the widest possible range of rendering needs and implementation approaches with minimum viable patches and logic in the Fabric API itself.

It includes a reliable and performant hook for an enhanced and/or dynamic model to retrieve extended world state in response to chunk rebuilds.

It establishes minimal interfaces and functional expectations for a rendering plug in and creates a lightweight registration system for such a plug-in to make itself accessible. The scope of the specification includes block rendering, item rendering and fast block entity rendering.

This PR does not make any changes to vertex lighting or buffering. All such hooks are delegated to plug-in implementations.

This PR is feature-complete and I do not intend to make any changes except in response to defects or reviewer input. While is it under review I will work on a barebones reference implementation for RenderPlugin.

Update: I have a working proof-of-concept plug-in implementation that handles block rendering with accurate lighting and comparable performance. The intent was not to create something elegant or optimal, just demonstrate viability. That code is here.

As it stands, I am going to set this PR aside and submit a different PR with an alternate approach in response to feedback. Scope will be similar. Nothing is yet settled, so feedback on this PR is still welcome.

@grondag grondag changed the title Alternative Enhanced Rendering Approach Alternative Enhanced Model Approach Jan 2, 2019
@shadowfacts
Copy link
Copy Markdown

Not a fan of the cutesy "tailor" names. Naming projects like that is one thing, but the code should be kept readable/easily understandable, IMO.

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 3, 2019

Yeah, it's a straw man. EnhancedBakedQuad was too unwieldy for me. Any suggestions? FancyBakedQuad? FabricBakedQuad?

@shadowfacts
Copy link
Copy Markdown

I think EnhancedBakedQuad is fine, but FabricBakedQuad is also good.

@RedstoneParadox
Copy link
Copy Markdown
Contributor

Maybe use Dynamic as the prefix, since the purpose of this seems to be to provide more dynamic models.

@asiekierka
Copy link
Copy Markdown
Contributor

I don't like the FabricBakedQuad getter being separate from the BakedQuad getter: I think FabricBakedQuads should gracefully fall back to regular quads (by, say, having a null int[] field containing the original vertex data populated on demand), and then - now that the RenderData gathering is similar - I think we can just treat this PR as an extension of #45, no?

@asiekierka
Copy link
Copy Markdown
Contributor

On the other hand, checking every BakedQuad for it being a Fabric one or not could get pricey...

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 4, 2019

Trying to avoid the per-quad check, yes, and also the fabric getter is all sides at once. But mostly, the fabric getter needs additional parameters, which forces it to be different.

The vertex format standards will require the first 28 elements of vertex data to match the standard Minecraft block format - with a few situational exceptions. This ensures the fallback getQuads is trivial to implement when it matters and requires no copying or transformation of the vertex data. I hope to provide a default implementation of that interface method.

That said, I'll consider ways to make the two more harmonious - maybe adding back the side parameter, which would certainly help with the default implementation be more efficient.

**edit: I don't see any way to provide a default implementation of vanilla getQuads that doesn't constrain implementations in bad ways. A key benefit to single pass is the model doesn't have to store model state between sides/layers. The vanilla get quads method forces a pass per side, plus general quads. And I also can't know what model state assumptions to use in the default case.

I think standardizing the vertex format is the most impactful thing, because having to transform or repack vertices would kill performance. If I can show in the reference implementation that implementing the vanilla getQuads is a low bar, I hope that is sufficient.**

This is indeed ending up very close to #45 in approach, which gives me confidence we're on the right track, because I came to the same place by a somewhat different route. I still don't like that Block Entities have to know about their models but I also see no good way around it.

There are some small but significant bits still to be added and documented, but this PR by itself will not make any changes to model baking, lighting or rendering. It will simply define standards and create minimal hooks for those things to be done in an interoperable way. To use any extended features (emissive, for example) will require an implementation of the extended model/quad interfaces and an active rendering plugin.

Should be ready for thorough going-over in a couple days, but then I'd like to create minimal reference implementations, separate from the PR, and demonstrate some common use cases and run it through the profiler to be sure it holds up.

We then might want to cherry-pick some of that reference implementation into a separate PR to make things easier for mod authors and provide some basic "out-of-box" capabilities, but that's to be seen.

Lastly this doesn't fully address block entity rendering and it doesn't address item rendering at all. I'd like to come back to those after this is wrapped up.

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 4, 2019

One other thing regarding getFabricBakedQuads. Because we're requesting a List instance, the interface contract needs to guarantee to the implementer the list reference won't be retained. This allows implementers to re-use list instances, which will be important to reduce allocation overhead for mods doing dynamic mesh generation/baking at render time.

I would prefer to enforce this guarantee in code vs spec, but am trying not to do anything too exotic.

@asiekierka
Copy link
Copy Markdown
Contributor

What I'm saying is - I'd like the "dynamic baked model" and "advanced-quad baked model" patches to be separate in the sense of not having to do one to get the other, as well as greater/easier cooperation for cross-mod-loader compat in the future, possibly. Much easier to feed in a RenderDataType object than it is to support a whole new quad type.

Also, keep in mind int[] being of size 28 is not always a guarantee - for instance, OptiFine's shader patches will cause it to be of size 56.

All of this is why I'd ideally like #55, if it ends up merged, to build on #45, and - by extension - for #45 to be merged first.

I'm still iffy about this adding features to the engine versus being hooks/interop - I'm fearing setting a precedent in which Fabric's API module stops being that. But I understand the needs. Perhaps it would be better to create another module under the "official Fabric" umbrella which provides advanced rendering complete with basic implementations?

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 5, 2019

With the changes just pushed, this is now far less invasive that #45. I could not extend #45 without removing substantial parts of it.

As it stands, this no longer makes any changes to vertex lighting or buffering. There are no patches to BlockModelRenderer. Instead, it simply defines standards for implementing a RenderPlugin and for enhanced models to provide vertex data to the plugin. Existing baked models and quads will support any render plug via purely additive extensions.

After going back and forth over all the ways to implement hook for model lighting and buffering, I believe the best and most performant will involve replacing large swaths of BlockModelRenderer, WorldRenderer, etc. That obviously has no place in the Fabric API, and if we instead try to insert more conservative hooks we are probably wasting our time and possibly making life more complicated for future plugin authors who will then need to reproduce or work around those hooks in their own patches.

Regarding the vertex formats: the defined formats are for model vertex data being sent to a rendering plug in. This is the key to interoperability for render plugin and models. It also has the nice side effect of insulating model authors from future changes to the rendering pipeline by Mojang, because model vertex formats become independent.

The first-28 convention is essential for compatibility when a rendering plug-in is absent, and it also provides performance and compatibility benefits for plug in implementors. While this spec will offer guidance on how vertex buffers ought to be populated for benefit of shader compatibility across render plugins, it does not constrain it. A render plug-in author can use whatever vertex formats are appropriate for their implementation.

A bare-bones reference implementation of a render plug-in may be useful as a Fabric-adjacent project, so that mod authors can be guaranteed at least some minimal level of features. I intend to create one to validate the approach, so we could evaluate that when I have something to show. A little down the road it may also be useful to have a project devoted to developing standards for render-side vertex formats, shader-libraries, etc. for people who want to implement a render plug in or employ custom shaders.

This is close to done now. I want to add more docs explaining vertex formats and expectations for a render plug in, and at least stub in how BlockEntityRenderers and Item Rendering will work to make sure it hangs together.

class MixinBakedModelHelper {
/**
* This is here for use by {@link MixinBakedModel} which cannot define
* static fields without Mixin complaining. The implementation there
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.

It can, as long as they're private.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If I could make MixinBakedModel abstract that would certainly work, but believe it has to be an interface because BakedModel is.

Is there a sneaky way to declare private members in an interface?

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.

Yes! In later versions of Java :D.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Whoa, should I be targeting J9? Totally missed that if so. Would be nice.

So used to that other one... haven't even really looked at J9 much.

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 wish! That was TOTALLY NOT what I tried to imply. Nope. We have to support J8.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, well. So, leave it for now? Accept the potential allocation hit? Is there a less bad place to stash the ugliness?

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.

Yes.

api/ is public-facing classes and interfaces, impl/ is our internal details, mixin/ is MIXINS ONLY. As such, such a helper class should be thrown in impl/, with just a public static final field...

@grondag grondag changed the title Alternative Enhanced Model Approach Rendering Plug-In and Enhanced/Dynamic Model APIs Jan 6, 2019
@asiekierka
Copy link
Copy Markdown
Contributor

Yes, I mean - since Fabric only provides the standard vertex formats, there is no reason why it should be decisive over the flags.

This is slowly getting to a point where I'm pleased with it, but I need to get Player to look at the performance and assess that...

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 8, 2019

Edit: not actually rendering yet. My build setup was borked and I was too tired to notice.

Needs a little more bit twiddling to render properly. Still very pleased with how API works as an abstraction layer.

first plugin screenshot

Very basic plug-in rendering now. Flat lighting only, no extra features. Was straightforward to implement - models and rendering are nicely decoupled. Performance seems good, subjectively. Won't do benchmarks until smooth lighting works and have confirmed visual match.

Pressed for time so code isn't pretty but at least there isn't much of it. Can be seen here: link to source

@asiekierka
Copy link
Copy Markdown
Contributor

I think I can say that I like the direction in which this is going. Just waiting for some performance analysis/"roasts" from Player now.

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 8, 2019

FIXED: Made FabricBakedQuad.getRenderLayerFlags() unambiguous so plug-ins can distinguish
quads that use block render layer vs quads that specify their own render layer(s).

@asiekierka
Copy link
Copy Markdown
Contributor

We've discussed some things about this PR recently on IRC.

The way you structure dynamic models means you move side/layer decisions to the rendered model. This effectively means that the models have to be "smarter", by force - but it also prevents easy re-use of their data in other contexts (say, as parts of a bigger model); now, while I see no issue to having the layer be part of quad information (and it has advantages!), I have to create a fake BlockView to be sure that I have dumped all the data from the model. One solution could be to create an "inbetween" model interface: a marker interface which says: this model's getQuads() outputs FabricBakedQuads and opts into the newer pipelines, but does not use any dynamic generation. Maybe there's others.

Also, something I've noticed: FabricBakedQuad having methods named exactly as their vanilla counterparts without extending can cause issues with obfuscation, though I haven't tested this - have you?

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 9, 2019

The way you structure dynamic models means you move side/layer decisions to the rendered model. This effectively means that the models have to be "smarter", by force - but it also prevents easy re-use of their data in other contexts (say, as parts of a bigger model); now, while I see no issue to having the layer be part of quad information (and it has advantages!), I have to create a fake BlockView to be sure that I have dumped all the data from the model. One solution could be to create an "inbetween" model interface: a marker interface which says: this model's getQuads() outputs FabricBakedQuads and opts into the newer pipelines, but does not use any dynamic generation. Maybe there's others.

To make sure I understand, the concern is having a way to extract a complete model representation without a block view, because a call to getQuads doesn't necessarily return FabricBakedQuads, and a call to produceFabricBakedQuads() requires a BlockView if I want to get block quads.

That's a legitimate concern. I had been assuming consumers who wrap the model would do so in each context (block, item, FRBE) and so would already have the necessary parameters in hand, but maybe they won't. Models get used in other ways.

One thing I'd like to preserve is automatic and reliable forward compatibility for vanilla baked models and quads. It makes life easier for plug-in authors or really anyone who wants to consume the new interfaces. They only need to write one handler.

My initial thought is to split produceFabricBakedQuads() into three different methods:

// for block renders
produceBlockQuads(ModelBlockView blockView, BlockState state, BlockPos pos, Random random, long seed, Consumer<FabricBakedQuad> consumer);

// for fast-rendering block entities
produceBlockEntityQuads(@Nullable Object RenderData, BlockState state, BlockPos pos, Consumer<FabricBakedQuad> consumer);

// for generic block quads and item models
produceAllQuads(@Nullable Object modelState, Consumer<FabricBakedQuad> consumer);

This makes it more explicit what is being requested from the model without the need to test parameters. And a default implementation for vanilla models remains feasible. It also fixes a defect for FBER I hadn't spotted yet - it has no way to receive its render data. Default implementations remain viable.

I don't like that produceAllQuads() remains ambiguous - the vertex format depends on the type of model you are calling. If you have a block model you'll get block quads. If you have an item model (via getItemPropertyOverrides()) you'll get item quads. But it seems useful to remain consistent with how getQuads() currently works. And if we did add separate methods for block and item quads, are we then requiring models to output both on demand? And if not, why bother?

Consumers who get a dump of generic block quads via produceAllQuads() with a null modelState will get the "default" quads - there is nothing else for the model to go on. That's the same behavior as getQuads() today. The optional ModelState parameter is there for mod authors who may have use of it.

For example, a possible model implementation pattern is to implement quad output in produceAllQuads and then make the implementations of produceBlockQuads and produceBlockEntityQuads derive a model state which they then pass to produceAllQuads. But we would not be forcing that - it doesn't make sense for all models.

Did I understand the concern correctly? Thoughts?

Also, something I've noticed: FabricBakedQuad having methods named exactly as their vanilla counterparts without extending can cause issues with obfuscation, though I haven't tested this - have you?

Good catch. Obvious in retrospect but I didn't spot and have not tested. It does seem certain to cause problems, at least for the default implementation in BakedQuad. I recall Mixin has annotations to help finesse obfuscation edge cases but I haven't used them much. I'll look into this but my bias is towards simple and reliable - will probably be best to give the methods different names.

edit: voldemapping

@asiekierka
Copy link
Copy Markdown
Contributor

To make sure I understand, the concern is having a way to extract a complete model representation without a block view, because a call to getQuads doesn't necessarily return FabricBakedQuads, and a call to produceFabricBakedQuads() requires a BlockView if I want to get block quads.

Yes, but also a way to reproduce a complete model representation for, say, doing your own caching. What I mean is - with getQuads(), you can get the quads for the center, then the quads for each culling side, and you can make culling decisions yourself; with produceFabricBakedQuads(), you have to trust in the model every time.

What I'm really trying to get at here is that our model API as it stands is good for truly dynamic models, but is bad for the middle case: a static model wanting to make use of enhanced rendering features, and then dynamic models wanting to build on top of that. Take the SimpleLogic gate model, which is built from models of redstone torches translated accordingly. My solution was to, well, add some documented/supported measure of creating a static "dumb/List" model which can still benefit from Fabric's extensions. Another thing to see would be if, instead of Block.shouldDrawSide, we could expose some helper methods in the ModelBlockView (that could then be overridden!).

On a side-note, indexing BlockEntities by BlockStates (-> BakedModels) doesn't seem like a great idea performance-wise, but I'd have to check if both are easily available at rendering time.

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 9, 2019

...with getQuads(), you can get the quads for the center, then the quads for each culling side, and you can make culling decisions yourself; with produceFabricBakedQuads(), you have to trust in the model every time.

I understand. I can add methods that accept a Direction parameter to emulate how getQuads() works. I'd prefer to keep the Consumer<> pattern vs. forcing the model to instantiate lists, but that's negotiable.

I'd like to keep the existing all-at-once methods for use by plug-ins, but I will add default implementations for them based on the per-side versions - it's trivial to do.

One of my goals here is to make it easy for mod authors with high-performance needs to use a single int[] array as the internal model representation. And then let plug-ins fully exploit that when available. This has significant benefits for locality of reference and allocation overhead, but I agree many, probably most, won't need it.

Those who do want to optimize can simply override the all-at-once producer methods and satisfy the per-side version by sorting the quads in the array by face and then storing some offsets to know where each face starts. The offsets can even be packed into the same array. They will need to do something similar anyway to satisfy the BakedQuad.getQuads() method for compatibility.

Acceptable?

On a side-note, indexing BlockEntities by BlockStates (-> BakedModels) doesn't seem like a great idea performance-wise, but I'd have to check if both are easily available at rendering time.

Could be. I assumed but didn't require that plug-ins would pre-fetch and cache the models in some performant way before it ever got to that point. But the whole FastRenderingBlockEntity thing isn't fully "baked" yet (ha!). For example, I think per-frame, animated renderers may also need to get a fractional tick value sent to them. But I really won't know for sure until I run some experiments.

My main intention with FBRE was to show that some kind of fast block entity rendering scheme is in-scope and start getting feedback on it. If we arrive at a point where the rest is ready but the FRBE isn't we can just push it to a follow-on PR. IRRC it's only one interface at this point.

@asiekierka
Copy link
Copy Markdown
Contributor

asiekierka commented Jan 9, 2019

I'd like to keep the existing all-at-once methods for use by plug-ins, but I will add default implementations for them based on the per-side versions - it's trivial to do.

I'm saying have two interfaces: FabricBakedModel (preserves getQuads, but allows additional FabricBakedQuad features etc.) and DynamicFabricBakedModel (consumer pattern + renderdata support).

@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 9, 2019

I'm saying have two interfaces: FabricBakedModel (preserves getQuads) and DynamicFabricBakedModel (consumer pattern + renderdata support).

Ah, yes, that seems promising. Ends up being equivalent for the plug-in but easier to work with for mod authors who just want fancy renders on a static model. I'll have a revision up in the next couple days.

Separate but related topic:

Would you consider making emissive render for single-layer quads a minimum standard? That, and being able to specify multiple block render layers in the same model?

The render layer attribute is already there - I'd just need to document that plug-ins are required to honor it for single-layer quads.

For emissive, I'd handle by stating that plug-ins must honor light maps in the BLOCK format as minimums. Right now it says they may.

Block lightmaps won't work with item renders - those would require a plug-in specific extension. If we do want emissive item renders as part of a minimum standard, we'd need to add an extended vertex format that includes both normals and lightmap and also require plug-ins to recognize it. I'd be OK with that if you are.

I'll note here that adding a standard vertex format that includes both normals and lightmap, and requiring plug-ins to recognize it, has another big benefit for model authors who elect to use it: they can return the same vertex data for both item and block renders. They don't need to produce separate formats (except for vanilla getQuads). But it does take us to the edge, if not outside, of what I understand to be your intended scope for the core Fabric API.

These minimums put a little more burden on plug-in authors but they were probably going to implement those features anyway and it would give mod authors the assurance of some baseline capabilities that satisfy the most common use cases.

Thoughts?

@grondag grondag mentioned this pull request Jan 13, 2019
@grondag
Copy link
Copy Markdown
Contributor Author

grondag commented Jan 21, 2019

Looks like this will be fully superseded by #65, so closing out to prevent any confusion or waste of time for reviewers.

@grondag grondag closed this Jan 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants