bevy_mod_outline 0.13.0

A mesh outlining plugin for Bevy.
Documentation
use std::ops::Range;

use bevy::prelude::*;
use bevy::render::camera::ExtractedCamera;
use bevy::render::mesh::allocator::MeshAllocator;
use bevy::render::render_phase::{DrawFunctions, PhaseItemExtraIndex, ViewSortedRenderPhases};
use bevy::render::view::ExtractedView;
use bevy::render::{
    render_phase::SortedRenderPhase,
    render_resource::{
        LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
    },
    renderer::RenderContext,
    texture::CachedTexture,
};

use crate::culling::RenderOutlineEntities;
use crate::node::OutlineSortingInfo;
use crate::queue::{
    DirtyOutlineSpecialisations, OutlineCache, OutlineCacheEntry, PendingOutlineQueues,
};
use crate::uniforms::RenderOutlineInstances;
use crate::view_uniforms::OutlineQueueStatus;

use super::node::FloodOutline;
use super::{DrawMode, DrawOutline, OutlineViewUniform, FLOOD_OPS};

pub(crate) fn prepare_flood_phases(
    query: Query<&ExtractedView, With<OutlineViewUniform>>,
    mut flood_phases: ResMut<ViewSortedRenderPhases<FloodOutline>>,
) {
    for view in query.iter() {
        flood_phases.prepare_for_new_frame(view.retained_view_entity);
    }
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn queue_flood_meshes(
    flood_draw_functions: Res<DrawFunctions<FloodOutline>>,
    mesh_allocator: Res<MeshAllocator>,
    outline_cache: Res<OutlineCache>,
    render_outlines: Res<RenderOutlineInstances>,
    render_visible: Res<RenderOutlineEntities>,
    pending_queues: ResMut<PendingOutlineQueues>,
    specialisations: Res<DirtyOutlineSpecialisations>,
    mut flood_phases: ResMut<ViewSortedRenderPhases<FloodOutline>>,
    mut views: Query<(&ExtractedView, &mut OutlineQueueStatus)>,
) {
    let draw_flood = flood_draw_functions.read().get_id::<DrawOutline>().unwrap();

    for (view, mut queue_status) in views.iter_mut() {
        let Some(flood_phase) = flood_phases.get_mut(&view.retained_view_entity) else {
            continue;
        };

        let outline_view_cache = outline_cache
            .view_map
            .get(&view.retained_view_entity)
            .unwrap();

        let Some(render_view_visible) = render_visible.views.get(&view.retained_view_entity) else {
            continue;
        };

        let Some(view_pending_queues) = pending_queues.get(&view.retained_view_entity) else {
            continue;
        };

        for &main_entity in
            specialisations.iter_to_dequeue(view.retained_view_entity, &render_view_visible.0)
        {
            flood_phase.remove(Entity::PLACEHOLDER, main_entity);
        }

        for (_, &main_entity) in specialisations.iter_to_queue(
            view.retained_view_entity,
            &render_view_visible.0,
            &view_pending_queues.prev_frame,
        ) {
            let Some(outline) = render_outlines.get(&main_entity) else {
                continue;
            };

            if outline.draw_mode != DrawMode::JumpFlood {
                continue;
            }

            let mesh_slabs = mesh_allocator.mesh_slabs(&outline.mesh_id);
            let index_slab = mesh_slabs.and_then(|s| s.index_slab_id);

            let Some(OutlineCacheEntry {
                stencil_pipeline_id: _,
                volume_pipeline_id,
            }) = outline_view_cache.entity_map.get(&main_entity)
            else {
                continue;
            };

            let sorting_info = OutlineSortingInfo {
                world_plane_origin: outline.instance_data.world_plane_origin,
                world_plane_offset: outline.instance_data.world_plane_offset,
            };
            flood_phase.add_retained(FloodOutline {
                sorting_info,
                distance: 0.0,
                entity: Entity::PLACEHOLDER,
                main_entity,
                pipeline: *volume_pipeline_id,
                draw_function: draw_flood,
                batch_range: 0..0,
                extra_index: PhaseItemExtraIndex::None,
                indexed: index_slab.is_some(),
                volume_offset: outline.instance_data.volume_offset,
                volume_colour: outline.instance_data.volume_colour,
            });
        }

        if !flood_phase.items.is_empty() {
            queue_status.has_volume = true;
        }
    }
}

pub(crate) struct FloodInitPass<'w> {
    world: &'w World,
    view_entity: Entity,
    flood_phase: &'w SortedRenderPhase<FloodOutline>,
    camera: &'w ExtractedCamera,
}

impl<'w> FloodInitPass<'w> {
    pub fn new(
        world: &'w World,
        view_entity: Entity,
        flood_phase: &'w SortedRenderPhase<FloodOutline>,
        camera: &'w ExtractedCamera,
    ) -> Self {
        Self {
            world,
            view_entity,
            flood_phase,
            camera,
        }
    }

    pub fn execute_direct(
        &mut self,
        render_context: &mut RenderContext<'_, '_>,
        range: Range<usize>,
        output: &CachedTexture,
    ) {
        let color_attachment = RenderPassColorAttachment {
            view: &output.default_view,
            depth_slice: None,
            resolve_target: None,
            ops: FLOOD_OPS,
        };

        self.run(render_context, range, color_attachment);
    }

    pub fn execute_coverage(
        &mut self,
        render_context: &mut RenderContext<'_, '_>,
        range: Range<usize>,
        coverage_msaa: &CachedTexture,
        resolve_target: &CachedTexture,
    ) {
        let color_attachment = RenderPassColorAttachment {
            view: &coverage_msaa.default_view,
            depth_slice: None,
            resolve_target: Some(&resolve_target.default_view),
            ops: Operations {
                load: LoadOp::Clear(wgpu_types::Color::TRANSPARENT),
                store: StoreOp::Store,
            },
        };

        self.run(render_context, range, color_attachment);
    }

    fn run(
        &mut self,
        render_context: &mut RenderContext<'_, '_>,
        range: Range<usize>,
        color_attachment: RenderPassColorAttachment<'_>,
    ) {
        let mut init_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor {
            label: Some("outline_flood_init"),
            color_attachments: &[Some(color_attachment)],
            depth_stencil_attachment: None,
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });

        if let Some(viewport) = self.camera.viewport.as_ref() {
            init_pass.set_camera_viewport(viewport);
        }

        if let Err(err) =
            self.flood_phase
                .render_range(&mut init_pass, self.world, self.view_entity, range)
        {
            error!("Error encountered while rendering the outline flood init phase {err:?}");
        }
    }
}