Skip to content

[WIP] Dynamic baked model API#45

Closed
asiekierka wants to merge 6 commits intoFabricMC:masterfrom
asiekierka:dynamic-baking-render-cache
Closed

[WIP] Dynamic baked model API#45
asiekierka wants to merge 6 commits intoFabricMC:masterfrom
asiekierka:dynamic-baking-render-cache

Conversation

@asiekierka
Copy link
Copy Markdown
Contributor

See #43 for prior art.

There are many questions left to be answered; this is mostly putting out some code and a general idea of my approach to discuss.

Copy link
Copy Markdown
Contributor

@grondag grondag left a comment

Choose a reason for hiding this comment

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

You could simplify this a lot by removing RenderDataProvidingBlockEntity altogether. Blocks should know if they have a BlockEntity, and if that is where they derive/cache render data.


@Mixin(BlockModelRenderer.class)
public class MixinBlockModelRenderer {
// TODO NORELEASE: should this be split into two hooks to arrow overriding AO check?
Copy link
Copy Markdown
Contributor

@grondag grondag Dec 24, 2018

Choose a reason for hiding this comment

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

// TODO NORELEASE: should this be split into two hooks to arrow overriding AO check?
Yes, seems better to hook the other two methods - mainly to avoid reproducing all of tesselate(). You lose the specificity of the error message, but doesn't seem worth it.

ExtendedBlockView eView = (ExtendedBlockView) view;
List<BakedQuad> quads = new ArrayList<>();
BitSet bitSet = new BitSet(3);
boolean rendered = false;
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 baffles me why Mojang chose to instantiate a new BitSet here, of all places, just to hold a few bit flags. I can only assume their profiling showed escape analysis to be reliable here.

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.

It's Mojang; don't assume, but verify 🤔

BitSet bitSet = new BitSet(3);
boolean rendered = false;

for (Direction direction : Direction.values()) {
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.

Recommend creating a local reference to Direction.values() as Mojang did. In concurrent access, JVM will often create many clones of enum.values() that have to get garbage collected, even though it is effectively final. Would be better still to reference Directions.ALL but assume you are trying to be consistent with Mojang here.

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.

Really, in this method we can get away with anything as long as we pass on to tesselateQuadsSmooth later down the line. As such, Directions.ALL is fine.


public boolean fabric_tesselateFlatDynamic(RenderCacheView view, DynamicBakedModel model, BlockState state, BlockPos pos, BufferBuilder builder, boolean allSides, Random rand, long seed) {
ExtendedBlockView eView = (ExtendedBlockView) view;
List<BakedQuad> quads = new ArrayList<>();
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.

Unless we are going to also change tesselateQuads, better to use the list interface on DynamicModel here, vs. constructing a new list. Most or at least many dynamic models will have a list at hand.

I don't think reimplementing tesselateQuads makes sense for the core FabricAPI, as much as I want to do it, personally. This also implies the consumer interface isn't needed in the core API either. sigh

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.

This also implies the consumer interface isn't needed in the core API either.

Needed or not, I'm asking if it should be an option. I don't mind the API making some provisions to ensure optimization mods can work more smoothly, as long as it doesn't end up with us being the engine rewriters.

@asiekierka
Copy link
Copy Markdown
Contributor Author

asiekierka commented Dec 24, 2018

@grondag I'm not getting rid of the BlockEntity split without a good argument. Look at MixinClass853 - iterating over all BlockEntities in a chunk is probably faster than having every Block with a BlockEntity get it from the world view... For Class853, it's an array lookup + hash lookup per getBlockEntity. Not sure which approach is more likely to be faster.

@grondag
Copy link
Copy Markdown
Contributor

grondag commented Dec 24, 2018

@grondag I'm not getting rid of the BlockEntity split. Look at MixinClass853 - iterating over all BlockEntities in a chunk is probably faster than having every Block with a BlockEntity get it from the world view...

Hmm, I did see that. That's actually what prompted me to suggest removing it. You already iterate through all the blocks in the MixinClass853 constructor before checking the Block Entities.

Given that you've already queried every block in the chunk to ask if it has render data, and those blocks should already know if they rely on a BlockEntity for render data, why not delegate the BlockEntity lookup to the Block, vs doing it in a separate loop?

I suppose if you want to query the BlockEntities first, and then avoid checking blocks if you already have the render data, but then you'd have to add a check at each at position in block loop, and you'd still be doing two iterations.

@asiekierka
Copy link
Copy Markdown
Contributor Author

asiekierka commented Dec 24, 2018

I suppose if you want to query the BlockEntities first, and then avoid checking blocks if you already have the render data, but then you'd have to add a check at each at position in block loop, and you'd still be doing two iterations.

The difference: You can iterate BlockEntities by the Map's values(); by iterating Blocks and having them get their BlockEntities, you have to do an additional array+hash lookup every time.

I'd like to benchmark both approaches at some point, but I'm not sure how to best do it.

@grondag
Copy link
Copy Markdown
Contributor

grondag commented Dec 24, 2018

The difference: You can iterate BlockEntities by the Map's values(); by iterating Blocks and having them get their BlockEntities, you have to do a hash lookup every time.

This is true. Might be premature optimization but I won't tell Knuth if you don't.

If you think most mod authors are likely to provide render data from BlockEntity, might be worth doing the Block Entity loop first, and then skip the blocks calls when you have non-null render data in the array. Hard to say which way will be more prevalent.

@asiekierka
Copy link
Copy Markdown
Contributor Author

If you think most mod authors are likely to provide render data from BlockEntity, might be worth doing the Block Entity loop first, and then skip the blocks calls when you have non-null render data in the array. Hard to say which way will be more prevalent.

I absolutely think that, but the only case where you'd get non-BlockState data out of a Block is neighboring blocks in terrain...

...which, while rare in terms of mod author count, is probably going to have a lot of blocks involved.

@grondag
Copy link
Copy Markdown
Contributor

grondag commented Dec 24, 2018

I absolutely think that, but the only case where you'd get non-BlockState data out of a Block is neighboring blocks in terrain...

Actually, for dynamic terrain I implement a separate client-side cache mechanism to hold partial computations by block position. It listens for world updates and clears the cache as needed. It also does cache the final result to avoid recomputes. It's complex but avoids a lot of state lookups.

Blocks in that case will query the cache first (based on world and block pos) and then only recompute if necessary.

Earlier I used Block entities that were entirely transient to cache some of the lookups, but there's really no such thing as a lightweight Block Entity when it comes to network packets, sooo... specialized cache.

@asiekierka
Copy link
Copy Markdown
Contributor Author

Actually, for dynamic terrain I implement a separate client-side cache mechanism to hold partial computations by block position. It listens for world updates and clears the cache as needed. It also does cache the final result to avoid recomputes. It's complex but avoids a lot of state lookups.

Yes, you could pass this as a render cache object too. Nice, but still means we should probably allow Blocks and BlockEntities both to provide render data objects.

*/

package net.fabricmc.fabric.api.client.model;

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.

Suggest adding comments here to clarify intent of this. For example:

If your block relies on a Block Entity for render state, it is better to implement this method on your Block Entity instead of looking up the state from your Block. This allows Fabric to query your block entity in a more efficient way.

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.

Not here, but in the interface. And I'd still like to see benchmarks (and/or Player's opinion) to see if it's really warranted to optimize cache building like so, particularly as I believe we query a grand total of 48x256x48's worth of tile entities for an 18x18x18 right now?

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, I was on the wrong class. I tried to delete this one but you were too fast. :-)

Copy link
Copy Markdown
Contributor

@grondag grondag left a comment

Choose a reason for hiding this comment

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

...still means we should probably allow Blocks and BlockEntities both to provide render data objects.

I am sold on that now, to be clear.


for (WorldChunk[] chunkA : chunks) {
for (WorldChunk chunkB : chunkA) {
for (BlockEntity entity : chunkB.getBlockEntityMap().values()) {
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.

You should check the chunk coordinates here to see if they are within the region we are caching and continue if not. Then you won't need the check below.

Cache regions are always going to be on chunk boundaries, so probably best to compute the chunk X,Z min, max before the loop starts so that you have them handy. (Should just be the blockMin/Max >> 4)

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.

The chunk array passed to class_853 is already cropped to only contain the region we are caching, no?

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 would make sense for it be. If so, then you don't need the bounds checks on the entity block position. May still be worth doing a bounds check on the result of method_3691, but really anything out of bounds would seem to be a hard fault.

Was also thinking about your earlier comment (below.) Perhaps I misunderstood.

And I'd still like to see benchmarks (and/or Player's opinion) to see if it's really warranted to optimize cache building like so, particularly as I believe we query a grand total of 48x256x48's worth of tile entities for an 18x18x18 right now?

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.

Bah, I see it now. Render sections vs chunks.

Copy link
Copy Markdown
Contributor

@grondag grondag Dec 24, 2018

Choose a reason for hiding this comment

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

So, I see that you do still need to check the block entity position, but suggest moving the call to getRenderDataObject() after the bounds check. We don't know how expensive getRenderDataObject() will be but entity.getPos() is cheap, as are the bounds checks.

BlockPos entPos = entity.getPos();
if (entPos.getX() >= posFrom.getX() && entPos.getX() <= posTo.getX()
&& entPos.getY() >= posFrom.getY() && entPos.getY() <= posTo.getY()
&& entPos.getZ() >= posFrom.getZ() && entPos.getY() <= posTo.getZ()) {
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.

Suggested change
&& entPos.getZ() >= posFrom.getZ() && entPos.getY() <= posTo.getZ()) {
&& entPos.getZ() >= posFrom.getZ() && entPos.getZ() <= posTo.getZ()) {

@asiekierka
Copy link
Copy Markdown
Contributor Author

asiekierka commented Dec 24, 2018

Made some changes, in particular:

  • after a bit of discussion, we decided that the few mods which would benefit from an explicit Consumer are probably better off adding specific support for another render pipeline - besides, it's an addition, so we can re-look into it later;
  • Block's render data getter has been moved to the Model. Mojang assumes all light value information to be thread-safe; in addition, BlockStates, FluidStates and block entity render data is thread-safe - as such, we want to maximally promote performing heavy render data computation in the chunk render worker threads, off the main thread. (Not to mention, it removes an iteration from ChunkCache)

@grondag
Copy link
Copy Markdown
Contributor

grondag commented Dec 24, 2018

Block's render data getter has been moved to the Model. Mojang assumes all light value information to be thread-safe; in addition, BlockStates, FluidStates and block entity render data is thread-safe - as such, we want to maximally promote performing heavy render data computation in the chunk render worker threads, off the main thread.

When does the model do this, and where does it store the resulting data between calls to getQuads for each block face? We should avoid making it retrieve state on each call?

@asiekierka
Copy link
Copy Markdown
Contributor Author

asiekierka commented Dec 24, 2018

When does the model do this, and where does it store the resulting data between calls to getQuads for each block face? We should avoid making it retrieve state on each call?

It does it at the beginning of model tesselation, so once per block - this has not changed. This approach has two key advantages:

  • as non-block-entity data is handled mostly thread-safely, we can perform these computations multithreaded and save CPU time on the main thread,
  • we don't need to iterate through all Blocks, merely ones which have a dynamic model.

There is a disadvantage, however - it's now done once per rendering layer, so up to four times if your block renders on multiple layers (which is uncommon, but possible with a mixin - not by default, though!). Something to think about.

@grondag
Copy link
Copy Markdown
Contributor

grondag commented Dec 24, 2018

I see it now. Makes sense.

return fabric_tesselateFlatDynamic(renderData, view, model, state, pos, builder, allSides, rand, seed);
}

public boolean fabric_tesselateFlatDynamic(Object renderData, ExtendedBlockView view, DynamicBakedModel model, BlockState state, BlockPos pos, BufferBuilder builder, boolean allSides, Random rand, long seed) {
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.

You could simplify these patches by making renderData an attribute of RenderCacheView. Its a bit of a hack, but RenderCacheView is always per-thread, and the only consumer of the render data will be the model we are rendering.

This in turn means you don't need to pass an additional parameter to tesselateSmooth or tesselateFlat - those calls can remain unchanged. The only change in tesselate() would be check for a dynamic model, and populating the renderData attribute of the view.

In tesselateSmooth and tesselateFlat you'd only need a patch something like this:

@Redirect(method = "tesselateSmooth", at = @At(value = "INVOKE", 
            target = "Lnet/minecraft/client/render/model/BakedModel;getQuads(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/Direction;Ljava/util/Random;)Ljava/util/List;"))
    private BlockPos onGetQuads(BakedModel model, @Nullable BlockState state, @Nullable Direction direction, Random rand)
    {
        return model instanceof DynamicBakedModel && view instanceof RenderCacheView 
            ? ((DynamicBakedModel)model).getQuads(((RenderCacheView)view).renderData(), state, direction, rand)
            : model.getQuads(state, direction, rand);
    }

Perhaps it should be an Inject instead of Redirect, but hopefully this conveys the idea.

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.

We can't do an Inject instead of a Redirect there, and I definitely don't want to stop other mods from also redirecting that particular call.

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 understand the hesitation to use a redirect, philosophically, but would invite you to reconsider in this specific instance. The inject here is functionally a redirect of the entire method with a new implementation. It's still very invasive.

If mod authors want to implement finer-grained hooks that co-exist with the existing Fabric changes, they will need to reimplement the Fabric changes in their own method and inject with a higher priority. Worse, that becomes the "normal" way to do anything with this method.

If you instead do a couple very narrow redirects on getQuads, authors can still make changes around those and the Fabric changes overall are less invasive. You could also pass the BlockRenderLayer without a static ThreadLocal hack by attaching it to the RenderCacheView along with the RenderData.

Other use cases for redirecting that particular method seem unlikely. An alternate render pipeline implementation will likely bypass tesselate() entirely, and I think that's fine. Model interfaces can be extended for alternate render pipelines, but all models should still render with the vanilla pipeline. To this end, a model shouldn't know how it is being rendered to the screen. It should simply produce requested vertex data per defined interfaces.

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.

You could also pass the BlockRenderLayer without a static ThreadLocal hack by attaching it to the RenderCacheView along with the RenderData.

I can do that regardless.

If you instead do a couple very narrow redirects on getQuads, authors can still make changes around those and the Fabric changes overall are less invasive. You could also pass the BlockRenderLayer without a static ThreadLocal hack by attaching it to the RenderCacheView along with the RenderData.

Yes, but then we become the only party which can perform a narrow redirect on getQuads. The idea here is that if someone throws out, say, the vanilla processing for static block models - dynamic block models which require different provisions are unaffected and still use familiar logic. Besides, you can inject into methods added by a mixin, IIRC?

@Player3324
Copy link
Copy Markdown
Contributor

The latest rev looks ok to me besides missing Javadoc.

for DynamicBakedModel.getRenderData's javadoc:

  • please document that it is unspecified whether it may get called for every layer or only once - it being called for every layer is just an implementation artefact
  • add information about the existence/combined use of BlockEntity.getRenderData


public static BlockRenderLayer getRenderLayer(BlockState state) {
BlockRenderLayer layer = DYNAMIC_MODEL_RENDER_LAYER.get();
return layer != null ? layer : state.getBlock().getRenderLayer();
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 think I understand what you're doing here, but it sure is confusing. Single layer blocks will always get their nominal render layer and blocks with multiple layers will get their extra layers in separate iterations via the hook in ChunkRenderer. Will need to doc the heck out of this.

From the name, is this supposed to be an interface implementation but the interface API isn't here yet?

I don't like that we have to resort to side-band thread locals to convey this info. What is actually happening here is very simple - we just need route the vertex data to the buffer builder for that layer. Would be sooo much easier if the model could get or provide the render layer directly.

That said, I don't see a better way if you're striving for minimal patches, which I believe you are. But by that logic, does multi-layer rendering even belong in the core Fabric API at all? It's not a vanilla feature.

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.

Any other approach to passing this information is significantly more complex, IMO.

This class is in impl/ and other mods should NEVER call it. The layer is passed to DynamicBakedModel.

Whether or not multi-layer rendering belongs is an open question, but many mods off the top of my head rely on it: pretty much all multipart mods, many modules I made for Charset, I think Chisel relies on it, any mod with a forge:multi-layer model in 1.9+, etc... pretty much anything with both opaque and translucent components needs it for properly rendering the respective quads, IIRC.

* @return The additional render layer mask.
*/
int getExtraRenderLayerMask();
}
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.

This is clever but it also means the block has to know about its model. It's better if the model tells you what layers it can render in.

The model should knows about the block and be designed to consume attributes the block will always provide, but the block shouldn't know how that information is being used. The exception would be blocks that have a user-defined appearance or other behavior explicitly tied to rendering.

Example of how this would matter: a resource pack maker wants to provide an alternate model for a block that is normally only rendered in the SOLID layer. The alternate model also requires TRANSLUCENT. They want the solid layer to be emissive but the translucent layer isn't.

In that scenario they could define the whole model as translucent, but that will be inefficient for translucency sort, and they won't get the lighting they want. Z-fighting might also be a problem.

Copy link
Copy Markdown
Contributor Author

@asiekierka asiekierka Dec 26, 2018

Choose a reason for hiding this comment

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

This is clever but it also means the block has to know about its model. It's better if the model tells you what layers it can render in.

I've talked this over on IRC or elsewhere before, and the problem with that is, due to way Minecraft's rendering system is structured, it would be a noticeably more invasive (and slower!) patch to allow the model to decide. The only workaround you get to do here here is to make Blocks store information about render layers generated from models as they are processed - but that is still fairly invasive.

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 does require big changes in ChunkRenderer.method_3652. I will support it in rendering API but I can see why you wouldn't want that in Fabric.

@Player3324
Copy link
Copy Markdown
Contributor

The handling of RenderDataProvidingBlockEntity should be changed in this or a follow-up PR to maintain a separate list on the (render?)chunk that tracks implementing classes to avoid iterating all entities. Addition and removal happens in line with the chunk's regular be map.

@asiekierka
Copy link
Copy Markdown
Contributor Author

I think follow-up is sufficient for this.

@grondag
Copy link
Copy Markdown
Contributor

grondag commented Jan 6, 2019

FWIW I did investigate this for #55 and it doesn't appear to be a straightforward patch. When the cache view is being created it doesn't have a reference to the ChunkRenderer.

Would be logical to handle this in method_3652 when block entities are already being reconciled. Unfortunately that happens on the render threads so it doesn't work for our purpose.

I looked briefly at feasibility of handling in WorldChunk but wary of going there.

Deferring for now.

@Player3324
Copy link
Copy Markdown
Contributor

For extra flexibility and performance the following changes should be considered:

Materials

Replace the concept of render layers with render materials, where a material choice defines the required vertex format and how the quads are to be rendered.

All vertex formats have to keep the first 24 bytes identical to vanilla for easy analysis or transformation, i.e. position, color and texture. The vertex format is defined by and consistent across the material.

The material is also responsible for processing the models' vertex data. It receives the output of getQuads, processes it internally to apply light, tint or whatever it desires and normally outputs to the vanilla vertex data buffers or its own. Vanilla's layer materials delegate to the existing render methods with the appropriate buffer. The format of non-vanilla vertex data buffers is unspecified.

DynamicBakedModel.getQuads has to be called exclusively for the materials returned by DynamicBakedModel.getMaterials, which defaults to the material corresponding to Block.getRenderLayer.

Support for providing custom materials can be added at a later time. The 1:1 relationship between materials and layers makes it trivial to adjust the PR for vanilla materials only.

Examples:

vanilla translucent material: vertex format pos+color+uv+padding, render with alpha blending
emissive translucent material: vertex format pos+color+uv+emissivity, render with alpha blending and the block light map clamped

Allocation prevention

Allocations for the List and BakedQuad instances returned by getQuads in the non-static case can be avoided by caching the objects thread local. For this to work the documented has to specify that the returned list and quads are only valid until the next getQuads invocation on any model on the same thread.

Allowing caches

There needs to be a way to determine what getQuads does, with conservative defaults. Properties of interest are:

  • Are the returned list + quads constant? (e.g. not reused for other contents in a TL cache or similar, otherwise a cache has to copy)
  • Does the return value depend on renderData?
  • .. or state?
  • .. or random?

RenderDataType

The generic type variable is extra noise and technically incorrect. In Java the caller defines its value, not the callee.

Render Types (for later)

In addition to dynamic models, custom render types besides normal model, liquids and no-op can provide a cleaner and faster hook for rendering outside the model system. Their implementation is responsible for the entire processing chain between state+world+pos and the output vertex data buffers.

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

grondag commented Jan 13, 2019

PR is up, but want to respond directly to the points Player made, because it may not be immediately clear how the new PR addresses them...

Materials
Materials are in, and they do replace BlockRenderLayer. BlockRenderLayer remained useful as an attribute of materials to specify texture blending - I couldn't think of any other useful combinations of cutout/translucent/LOD and people already understand how those four work.

Materials do imply vertex format, but in the latest PR the implementation of material is unconstrained. The important interface is the Material Builder, which defined the standard feature set of what can be specified in a material: emissive, overlays and blending.

getQuads()
For static models, I see the utility of being able to control the order in which materials are piped to vertex buffer, and how. Ultimately this was resolved by delegating the static version of the model implementation itself.

For dynamic models, some more explicit method for vertex transfer must be exposed, but I don't want to expose vertex formats because we will never all agree on what they should be and if they aren't standard, we're back to analyzing formats on the fly.

To handle dynamic vertex data, I abstracted a subset of BufferBuilder functionality and again left the rest up to the implementation.

In the dynamic case, the model is not required to pre-sort by material. I trust the ModelRenderer implementers to handle material sorting efficiently more than I trust everyone else who might ever create a dynamic vertex producer.

Allocation / Caches
Agree with your comments here, but ends up not mattering in the current PR.

Render Types
Still a very clean hook but not needed for the current PR. Using it or not will up to the implementation. There are some trade-offs in that it creates multiple logic paths for models that should be rendered with the same lighting, which in turn depends on some world state that could be reused for both. Don't think we should try to constrain that choice.

@asiekierka asiekierka closed this Feb 9, 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.

3 participants