Skip to content

Conversation

@momoluna444
Copy link

Hello everyone,

This is my first pull request. I've been experimenting with this for a while, and now it's time to share it.

Objective

To simplify the effort required to create and add a custom mesh pipeline.

This pull request currently only simplifies the work needed for the specialization and queue stages. I haven't yet found a suitable way to abstract the other stages. I think that's enough to close #21127.

As a reward, this enables a form of "multi-material" support by allowing custom MeshPasses to be added to a material.


Solution

I've abstracted MaterialPlugins into the more general MeshPassPlugin<P>, which customizes the behavior of a pass via the MeshPass generic parameter.

Now, a MeshPass can be defined like this:

pub struct MainPass;

impl MeshPass for MainPass {
    type ViewKeySource = Self;
    type Specializer = MaterialPipelineSpecializer;
    type PhaseItems = (Opaque3d, AlphaMask3d, Transmissive3d, Transparent3d);
    type RenderCommand = DrawMaterial;
}

Where Specializer and PhaseItem are expected to implement the following traits:

impl PipelineSpecializer for MaterialPipelineSpecializer {
    type Pipeline = MaterialPipeline;
    
    fn create_key(context: &SpecializerKeyContext) -> Self::Key {...}
    
    fn new(pipeline: &Self::Pipeline, material: &PreparedMaterial, pass_id: PassId) -> Self {...}
}

impl PhaseItemExt for Opaque3d {
    type RenderPhase = BinnedRenderPhase<Self>;
    type RenderPhases = ViewBinnedRenderPhases<Self>;
    type PhasePlugin = BinnedRenderPhasePlugin<Self, MeshPipeline>;
    const PHASE_TYPES: RenderPhaseType = RenderPhaseType::Opaque;
    
    fn queue(render_phase: &mut Self::RenderPhase, context: &PhaseContext) {...}
}

And a material can utilize it as follows:

impl Material for CustomMaterial {
    fn shaders() -> PassShaders {
        let mut pass_shaders = PassShaders::default();
        pass_shaders.insert(MainPass::id(), ShaderSet{...});
        ...
        pass_shaders
    }
}

Beyond this, some additional work is still required for usage. Please refer to main_pass.rs and prepass/mod.rs for details.

Implementation Challenges

To achieve this, I explored multiple approaches:

  • Abstracting the specialize and queue systems to be per-phase item: This meant using systems like specialize<Pass, PhaseItem> and queue<Pass, PhaseItem>. Each system would only handle one phase item, and we would add multiple system based on MeshPass::PhaseItems.

    • Pros: The implementation was simple, and systems for each phase item could run in parallel.
    • Cons: The system for every phase item iterated over all visible_entities, and I couldn't find a simple or efficient way to filter them down.
  • Switching to a per-pass system approach: To convert the MeshPass::PhaseItems tuple into parameters for the specialize and queue systems and to directly call the trait methods through the tuple within the systems, I ended up writing a lot of complicated and verbose macros. This eventually led to an issue with HRTB (Higher-Rank Trait Bounds), which propagated from the internal systems to the user's implementation, making the whole approach completely unusable.

Current Approach:

The current solution uses a fixed number of Option<Res<RenderPhasesN<P>>> for the specialize and queue system parameters, using DummyPhaseN to fill any unused slots in the PMeshPass::PhaseItems tuple. This approach is much simpler to implement, and the resulting boilerplate code is considered acceptable.

Pending Issues

Previously, when iterating over visible_entity in specialize_prepass_material_meshes, we had this:

if !material.properties.prepass_enabled && !material.properties.shadows_enabled {
    // If the material was previously specialized for prepass, remove it
    view_specialized_material_pipeline_cache.remove(visible_entity);
    continue;
}

Now that we support adding custom MeshPasses, and users can even disable the MainPass, this filtering needs to be extended into a general implementation. Also, since users can add two materials with completely different passes, when iterating over visible_entity, it would be best to filter for the relevant passes.

Both issues fundamentally boil down to the same problem: We need a way within the systems to determine if an entity is valid for the current pass.

Potential Solutions:

  1. Add a field to MaterialProperties: For example, passes_enabled: SmallVec[PassId]. We would also need to add a corresponding method in the Material trait.
  2. Use Material purely for defining the material: Then, use an additional PassMarker component to indicate whether the corresponding pass is enabled. This could potentially be combined with VisibilityClass.

Additionally, if you have any good suggestions regarding the API naming or parameter design, please let me know!

Testing

In this commit, MainPass, Prepass, and DeferredPass have been switched to the new MeshPass implementation and have been tested in the shader_prepass, motion_blur, and deferred_rendering examples.

@github-actions
Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile alice-i-cecile added A-Rendering Drawing game state to the screen C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Nov 18, 2025
@tychedelia tychedelia added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label Nov 19, 2025
@tychedelia
Copy link
Member

Okay, I haven't had a chance to look deeply at the code but a few thoughts:

  1. I'm very intrigued! Abstracting specialization and queue is a big win in itself even if it doesn't solve everything.
  2. This isn't meant as a critique especially without first reviewing the code, but I've been wanting to move the renderer away from type level constructs and towards being more ECS driven, basically to lean on dynamic dispatch where possible. This code is particularly hot and it may be the case that traits are a clear win.
  3. I'd love an example to help review the code to better understand what this looks like for users. This can be totally silly just showing the APIs.
  4. No need to do this right now but we'll obv need to perf test. Let me know if you need help here. We're particularly interested in perf on realistic scenes like https://github.com/DGriffin91/bevy_caldera_scene.

I'm particularly encouraged by more experimentation here as this is an area of the renderer that really needs to be more modular.

@momoluna444
Copy link
Author

momoluna444 commented Nov 19, 2025

I totally understand you.

While adding the example, I discovered a problem:
If a single Material contains multiple MeshPasses that share the same PhaseItem, the entity will fail to render. I suddenly realized that trying to add the same entity to the BinnedRenderPhase multiple times with different pipelines is actually invalid behavior, and BinnedRenderPhase cannot distinguish this situation from the case where an entity’s pipeline changes over time.

Adding MeshPasses that share a PhaseItem across different entities’ materials is fine, but this use case is very limited. If a user wants to add multiple MeshPasses to the same material, they must use completely different PhaseItems. Whether they implement them from scratch or use a newtype, they ultimately need to provide a corresponding render graph node.

Update:
After thinking about this problem, the solution is to detect and prevent repeated use of PhaseItem, then provide #[derive(Binned)] and #[derive(Sorted)] to make it easy for users to quickly create newtypes. I am still not entirely sure about the render graph node part and need to do some experimentation.

@github-actions
Copy link
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

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

Labels

A-Rendering Drawing game state to the screen C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Need a simpler way to add custom prepasses (light camera & main camera)

3 participants