Open santorac opened 2 years ago
For either the primary method proposed or the alternative, the main idea of moving all PSO state away from runtime mutable material state to a statically configured location strikes me as a good idea.
Configuring that state in the shader file seems pretty convenient, and makes me think variants should be considered in the same way. Previously, there were restrictions that prevented this consolidation (as I understand it), but with intermediate assets, perhaps we should consolidate both variants and material PSO state under the singular shader asset and simplify the process of shader creation in general
Summary:
The material system should generally not allow individual materials to change render states such as BlendState, DepthStencilState, or similar. It also should not allow individual materials to override a shader's draw list tag, because of its impact on render states.
What is the relevance of this feature?
Currently, materials can change any render state at any time, through both .material file settings and through dynamic changes at runtime. This will be problematic if O3DE is to support game consoles in the future, as some game consoles do not allow Pipeline State Objects (PSOs) to be compiled at runtime, and render states are part of the input structure for compiling PSOs. Game engines typically need to know ahead of time what pipeline states will be needed, compile them offline, and ship them with the game. If any material could potentially change render states at any time, it will be difficult (or impossible) for the build system to discover the full set of PSOs that will be needed. If we don't prevent materials from changing render states, then users may develop libraries of material types and materials that rely on this feature, making it harder for O3DE to eventually support game consoles. We should add new limitations soon, before much user content is built on the current system.
The details of how a future offline PSO compiler would work is outside the scope of this RFC. Right now we are mainly concerned about simplifying the design of the material system to mitigate future complications.
Feature design description:
First, it's important to recognize that some render states must be set according to the needs of the overall renderer, rather than the needs of individual materials or material types. Even if we restrict materials to disallow setting render states, the system render states will need to be configured dynamically. There are two render states that are currently set outside the material system (see Scene::ConfigurePipelineState https://github.com/o3de/o3de/blob/2a734b4e02755e0315faa9866245c4d52291f90b/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp#L923): RenderAttachmentConfiguration and MultisampleState. These are set according to the configuration of the render pipeline. (We might be able to simplify this as well, given the proposed material pipeline abstraction https://github.com/o3de/sig-graphics-audio/issues/68 but that is out of scope here). We are not proposing any changes to how these render states are configured at this time, but it's important to be aware of them.
There are currently only two cases of materials controlling render state
Although we need to allow dynamic configuration of CullMode (as well as system controlled render states such RenderAttachmentConfiguration and MultisampleState), removing access to the other render states should help simplify the material system design, and mitigate the risk of future complications for PSO baking.
Finally, we should also remove the ability to override the draw list tag because of its relationship to render states. The draw list tag controls which pass is used to run a shader, and the pass dictates which RenderAttachmentConfiguration and MultisampleState is applied. So allowing materials to override the draw list tag will complicate the determination of which PSOs need to be compiled offline. Also, the opacity.mode setting described above is the only use case for this feature, and can be handled by simply switching to a different .shader asset.
Technical design description:
Change StandardPBR and EnhancedPBR to use separate shaders for forward and transparent passes.
Replace the CullMode overrides in StandardPBR and EnhancedPBR with a specialized double-sided mechanism.
Each material property in a material type file can be connected to certain outputs. This is controlled using a MaterialPropertyOutputType enum which can be set to either "ShaderInput" for ShaderResourceGroup fields, or "ShaderOption" for connection to shader options. For example, see https://github.com/o3de/o3de/blob/2a734b4e02755e0315faa9866245c4d52291f90b/Gems/Atom/Feature/Common/Assets/Materials/Types/MaterialInputs/BaseColorPropertyGroup.json#L12 . We will add a new special connection type that will allow the doubleSided material property to be connected to a special "DoubleSided" setting instead of being controlled by a lua functor. The setup will look like this:
Here are the specific changes we'll need to make:
Delete a bunch of code
(Optional) Optimize by removing redundant shader code compilation
At this point, we'll have three separate .shader files for the forward pass, for blended transparency, and for tinted transparency. However these three files could still be using the same .azsl source file. This would mean the shader asset builder is compiling the exact same shader code three times. Instead, we could improve the .shader file to allow it to reference another .shader file and provide its own blend states (or any settings not related to shader compilation). The shader system would then be able to reuse the compiled shader bytecode from the other shader asset instead of compiling it again. Probably the cleanest way to achieve this would be to separate the bytecode into a ShaderBytecodeAsset rather than storing it directly in a ShaderAsset. That way, multiple ShaderAssets could reference the same bytecode.
However, this improvement is optional, and maybe not even necessary if the shader code diverges anyway. Once we split the forward pass, blended transparency, and tinted transparency into separate .shader assets, we might find that it's cleaner to have separate .azsl files with code that's specific to each use case and the expected render targets.
What are the advantages of the feature?
What are the disadvantages of the feature?
How will this be implemented or integrated into the O3DE environment?
Are there any alternatives to this feature?
The system could do some static analysis of the material type to determine which render states could change to which values. That way, the (future) offline PSO compiler can compile all possible PSOs for each shader. But this would be difficult to implement, it's easier to provide structured data rather than conjure reflection data.
Or render state information could be reflected by the material type, just listing alternate render state combinations that the lua functors might achieve. This is similar to the proposed solution of having multiple .shader files that configure different render states; in both cases the material type author has to manually provide separate render state values for each possible configuration. The advantage here is that material types could reuse shader bytecode, avoiding redundant compilation, without any changes to the shader system. But the potential problem is that material type authors could create some complex unforeseen scenario where there are many combinations of different render states, so the list becomes unwieldy and difficult to align with what is implemented in the lua material functor.
Main alternative: The .shader file could define a named list of alternate render state configurations that are available. The material would not be allowed to set individual render states, but it would be allowed to select one of the shader asset's published configurations. For the transparency mode thing, the forward shader would have render state configurations for "Default", "BlendedTransparent", and "TintedTransparent". There would only be one .azsl file, the render states could be changed as a predefined unit, and the offline PSO compiler would have information about what PSOs are needed, to support any dynamic changes coming from the material. The shader code would only be compiled once, unlike the original proposal. We could also combine CullMode into this solution, so there would be an expansion of the configurations like "TintedTransparent_SingleSided" and "TintedTransparent_DoubleSided" ... or CullMode could still have special handling like described above, this would need more thought. The approach still has the disadvantage of requiring the draw list override feature, which impacts RenderAttachmentConfiguration and MultisampleState and therefore complicates the determination of which PSOs need to be compiled offline. We could do a hybrid approach where draw list override is disallowed, so the same shader can't be used for both forward and transparent, but at least the multiple render state configurations would allow the same shader asset to be used for BlendedTransparent and TintedTransparent.
We thought about moving the double-sided setting to be controlled through the MeshComponent rather than on a per-material basis. That would make CullMode be a "system render state" similar to RenderAttachmentConfiguration and MultisampleState, and use special code paths outside the material system. This would allow us to avoid having a special case in the material system for CullMode. But there are some problems with this idea. It would complicate the MeshComponent because it would need a double-sided setting for each mesh in the model. Also, users have come to expect double-side to be a per-material setting based on how other engines expose this.
How will users learn this feature?
The material system documentation will be updated to describe the "Special" "DoubleSided" connection that's available to material properties, and to reflect the changes to the lua material functor API.
Are there any open questions?