
This adds "high level camera driven rendering" to Bevy. The goal is to give users more control over what gets rendered (and where) without needing to deal with render logic. This will make scenarios like "render to texture", "multiple windows", "split screen", "2d on 3d", "3d on 2d", "pass layering", and more significantly easier. Here is an [example of a 2d render sandwiched between two 3d renders (each from a different perspective)](https://gist.github.com/cart/4fe56874b2e53bc5594a182fc76f4915):  Users can now spawn a camera, point it at a RenderTarget (a texture or a window), and it will "just work". Rendering to a second window is as simple as spawning a second camera and assigning it to a specific window id: ```rust // main camera (main window) commands.spawn_bundle(Camera2dBundle::default()); // second camera (other window) commands.spawn_bundle(Camera2dBundle { camera: Camera { target: RenderTarget::Window(window_id), ..default() }, ..default() }); ``` Rendering to a texture is as simple as pointing the camera at a texture: ```rust commands.spawn_bundle(Camera2dBundle { camera: Camera { target: RenderTarget::Texture(image_handle), ..default() }, ..default() }); ``` Cameras now have a "render priority", which controls the order they are drawn in. If you want to use a camera's output texture as a texture in the main pass, just set the priority to a number lower than the main pass camera (which defaults to `0`). ```rust // main pass camera with a default priority of 0 commands.spawn_bundle(Camera2dBundle::default()); commands.spawn_bundle(Camera2dBundle { camera: Camera { target: RenderTarget::Texture(image_handle.clone()), priority: -1, ..default() }, ..default() }); commands.spawn_bundle(SpriteBundle { texture: image_handle, ..default() }) ``` Priority can also be used to layer to cameras on top of each other for the same RenderTarget. This is what "2d on top of 3d" looks like in the new system: ```rust commands.spawn_bundle(Camera3dBundle::default()); commands.spawn_bundle(Camera2dBundle { camera: Camera { // this will render 2d entities "on top" of the default 3d camera's render priority: 1, ..default() }, ..default() }); ``` There is no longer the concept of a global "active camera". Resources like `ActiveCamera<Camera2d>` and `ActiveCamera<Camera3d>` have been replaced with the camera-specific `Camera::is_active` field. This does put the onus on users to manage which cameras should be active. Cameras are now assigned a single render graph as an "entry point", which is configured on each camera entity using the new `CameraRenderGraph` component. The old `PerspectiveCameraBundle` and `OrthographicCameraBundle` (generic on camera marker components like Camera2d and Camera3d) have been replaced by `Camera3dBundle` and `Camera2dBundle`, which set 3d and 2d default values for the `CameraRenderGraph` and projections. ```rust // old 3d perspective camera commands.spawn_bundle(PerspectiveCameraBundle::default()) // new 3d perspective camera commands.spawn_bundle(Camera3dBundle::default()) ``` ```rust // old 2d orthographic camera commands.spawn_bundle(OrthographicCameraBundle::new_2d()) // new 2d orthographic camera commands.spawn_bundle(Camera2dBundle::default()) ``` ```rust // old 3d orthographic camera commands.spawn_bundle(OrthographicCameraBundle::new_3d()) // new 3d orthographic camera commands.spawn_bundle(Camera3dBundle { projection: OrthographicProjection { scale: 3.0, scaling_mode: ScalingMode::FixedVertical, ..default() }.into(), ..default() }) ``` Note that `Camera3dBundle` now uses a new `Projection` enum instead of hard coding the projection into the type. There are a number of motivators for this change: the render graph is now a part of the bundle, the way "generic bundles" work in the rust type system prevents nice `..default()` syntax, and changing projections at runtime is much easier with an enum (ex for editor scenarios). I'm open to discussing this choice, but I'm relatively certain we will all come to the same conclusion here. Camera2dBundle and Camera3dBundle are much clearer than being generic on marker components / using non-default constructors. If you want to run a custom render graph on a camera, just set the `CameraRenderGraph` component: ```rust commands.spawn_bundle(Camera3dBundle { camera_render_graph: CameraRenderGraph::new(some_render_graph_name), ..default() }) ``` Just note that if the graph requires data from specific components to work (such as `Camera3d` config, which is provided in the `Camera3dBundle`), make sure the relevant components have been added. Speaking of using components to configure graphs / passes, there are a number of new configuration options: ```rust commands.spawn_bundle(Camera3dBundle { camera_3d: Camera3d { // overrides the default global clear color clear_color: ClearColorConfig::Custom(Color::RED), ..default() }, ..default() }) commands.spawn_bundle(Camera3dBundle { camera_3d: Camera3d { // disables clearing clear_color: ClearColorConfig::None, ..default() }, ..default() }) ``` Expect to see more of the "graph configuration Components on Cameras" pattern in the future. By popular demand, UI no longer requires a dedicated camera. `UiCameraBundle` has been removed. `Camera2dBundle` and `Camera3dBundle` now both default to rendering UI as part of their own render graphs. To disable UI rendering for a camera, disable it using the CameraUi component: ```rust commands .spawn_bundle(Camera3dBundle::default()) .insert(CameraUi { is_enabled: false, ..default() }) ``` ## Other Changes * The separate clear pass has been removed. We should revisit this for things like sky rendering, but I think this PR should "keep it simple" until we're ready to properly support that (for code complexity and performance reasons). We can come up with the right design for a modular clear pass in a followup pr. * I reorganized bevy_core_pipeline into Core2dPlugin and Core3dPlugin (and core_2d / core_3d modules). Everything is pretty much the same as before, just logically separate. I've moved relevant types (like Camera2d, Camera3d, Camera3dBundle, Camera2dBundle) into their relevant modules, which is what motivated this reorganization. * I adapted the `scene_viewer` example (which relied on the ActiveCameras behavior) to the new system. I also refactored bits and pieces to be a bit simpler. * All of the examples have been ported to the new camera approach. `render_to_texture` and `multiple_windows` are now _much_ simpler. I removed `two_passes` because it is less relevant with the new approach. If someone wants to add a new "layered custom pass with CameraRenderGraph" example, that might fill a similar niche. But I don't feel much pressure to add that in this pr. * Cameras now have `target_logical_size` and `target_physical_size` fields, which makes finding the size of a camera's render target _much_ simpler. As a result, the `Assets<Image>` and `Windows` parameters were removed from `Camera::world_to_screen`, making that operation much more ergonomic. * Render order ambiguities between cameras with the same target and the same priority now produce a warning. This accomplishes two goals: 1. Now that there is no "global" active camera, by default spawning two cameras will result in two renders (one covering the other). This would be a silent performance killer that would be hard to detect after the fact. By detecting ambiguities, we can provide a helpful warning when this occurs. 2. Render order ambiguities could result in unexpected / unpredictable render results. Resolving them makes sense. ## Follow Up Work * Per-Camera viewports, which will make it possible to render to a smaller area inside of a RenderTarget (great for something like splitscreen) * Camera-specific MSAA config (should use the same "overriding" pattern used for ClearColor) * Graph Based Camera Ordering: priorities are simple, but they make complicated ordering constraints harder to express. We should consider adopting a "graph based" camera ordering model with "before" and "after" relationships to other cameras (or build it "on top" of the priority system). * Consider allowing graphs to run subgraphs from any nest level (aka a global namespace for graphs). Right now the 2d and 3d graphs each need their own UI subgraph, which feels "fine" in the short term. But being able to share subgraphs between other subgraphs seems valuable. * Consider splitting `bevy_core_pipeline` into `bevy_core_2d` and `bevy_core_3d` packages. Theres a shared "clear color" dependency here, which would need a new home.
368 lines
11 KiB
Rust
368 lines
11 KiB
Rust
use crate::{
|
|
render_graph::{
|
|
Edge, InputSlotError, OutputSlotError, RenderGraphContext, RenderGraphError,
|
|
RunSubGraphError, SlotInfo, SlotInfos, SlotType, SlotValue,
|
|
},
|
|
renderer::RenderContext,
|
|
};
|
|
use bevy_ecs::world::World;
|
|
use bevy_utils::Uuid;
|
|
use downcast_rs::{impl_downcast, Downcast};
|
|
use std::{borrow::Cow, fmt::Debug};
|
|
use thiserror::Error;
|
|
|
|
/// A [`Node`] identifier.
|
|
/// It automatically generates its own random uuid.
|
|
///
|
|
/// This id is used to reference the node internally (edges, etc).
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
|
pub struct NodeId(Uuid);
|
|
|
|
impl NodeId {
|
|
#[allow(clippy::new_without_default)]
|
|
pub fn new() -> Self {
|
|
NodeId(Uuid::new_v4())
|
|
}
|
|
|
|
pub fn uuid(&self) -> &Uuid {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
/// A render node that can be added to a [`RenderGraph`](super::RenderGraph).
|
|
///
|
|
/// Nodes are the fundamental part of the graph and used to extend its functionality, by
|
|
/// generating draw calls and/or running subgraphs.
|
|
/// They are added via the `render_graph::add_node(my_node)` method.
|
|
///
|
|
/// To determine their position in the graph and ensure that all required dependencies (inputs)
|
|
/// are already executed, [`Edges`](Edge) are used.
|
|
///
|
|
/// A node can produce outputs used as dependencies by other nodes.
|
|
/// Those inputs and outputs are called slots and are the default way of passing render data
|
|
/// inside the graph. For more information see [`SlotType`](super::SlotType).
|
|
pub trait Node: Downcast + Send + Sync + 'static {
|
|
/// Specifies the required input slots for this node.
|
|
/// They will then be available during the run method inside the [`RenderGraphContext`].
|
|
fn input(&self) -> Vec<SlotInfo> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Specifies the produced output slots for this node.
|
|
/// They can then be passed one inside [`RenderGraphContext`] during the run method.
|
|
fn output(&self) -> Vec<SlotInfo> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Updates internal node state using the current render [`World`] prior to the run method.
|
|
fn update(&mut self, _world: &mut World) {}
|
|
|
|
/// Runs the graph node logic, issues draw calls, updates the output slots and
|
|
/// optionally queues up subgraphs for execution. The graph data, input and output values are
|
|
/// passed via the [`RenderGraphContext`].
|
|
fn run(
|
|
&self,
|
|
graph: &mut RenderGraphContext,
|
|
render_context: &mut RenderContext,
|
|
world: &World,
|
|
) -> Result<(), NodeRunError>;
|
|
}
|
|
|
|
impl_downcast!(Node);
|
|
|
|
#[derive(Error, Debug, Eq, PartialEq)]
|
|
pub enum NodeRunError {
|
|
#[error("encountered an input slot error")]
|
|
InputSlotError(#[from] InputSlotError),
|
|
#[error("encountered an output slot error")]
|
|
OutputSlotError(#[from] OutputSlotError),
|
|
#[error("encountered an error when running a sub-graph")]
|
|
RunSubGraphError(#[from] RunSubGraphError),
|
|
}
|
|
|
|
/// A collection of input and output [`Edges`](Edge) for a [`Node`].
|
|
#[derive(Debug)]
|
|
pub struct Edges {
|
|
id: NodeId,
|
|
input_edges: Vec<Edge>,
|
|
output_edges: Vec<Edge>,
|
|
}
|
|
|
|
impl Edges {
|
|
/// Returns all "input edges" (edges going "in") for this node .
|
|
#[inline]
|
|
pub fn input_edges(&self) -> &[Edge] {
|
|
&self.input_edges
|
|
}
|
|
|
|
/// Returns all "output edges" (edges going "out") for this node .
|
|
#[inline]
|
|
pub fn output_edges(&self) -> &[Edge] {
|
|
&self.output_edges
|
|
}
|
|
|
|
/// Returns this node's id.
|
|
#[inline]
|
|
pub fn id(&self) -> NodeId {
|
|
self.id
|
|
}
|
|
|
|
/// Adds an edge to the `input_edges` if it does not already exist.
|
|
pub(crate) fn add_input_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
|
if self.has_input_edge(&edge) {
|
|
return Err(RenderGraphError::EdgeAlreadyExists(edge));
|
|
}
|
|
self.input_edges.push(edge);
|
|
Ok(())
|
|
}
|
|
|
|
/// Removes an edge from the `input_edges` if it exists.
|
|
pub(crate) fn remove_input_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
|
if let Some((index, _)) = self
|
|
.input_edges
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_i, e)| **e == edge)
|
|
{
|
|
self.input_edges.swap_remove(index);
|
|
Ok(())
|
|
} else {
|
|
Err(RenderGraphError::EdgeDoesNotExist(edge))
|
|
}
|
|
}
|
|
|
|
/// Adds an edge to the `output_edges` if it does not already exist.
|
|
pub(crate) fn add_output_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
|
if self.has_output_edge(&edge) {
|
|
return Err(RenderGraphError::EdgeAlreadyExists(edge));
|
|
}
|
|
self.output_edges.push(edge);
|
|
Ok(())
|
|
}
|
|
|
|
/// Removes an edge from the `output_edges` if it exists.
|
|
pub(crate) fn remove_output_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> {
|
|
if let Some((index, _)) = self
|
|
.output_edges
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_i, e)| **e == edge)
|
|
{
|
|
self.output_edges.swap_remove(index);
|
|
Ok(())
|
|
} else {
|
|
Err(RenderGraphError::EdgeDoesNotExist(edge))
|
|
}
|
|
}
|
|
|
|
/// Checks whether the input edge already exists.
|
|
pub fn has_input_edge(&self, edge: &Edge) -> bool {
|
|
self.input_edges.contains(edge)
|
|
}
|
|
|
|
/// Checks whether the output edge already exists.
|
|
pub fn has_output_edge(&self, edge: &Edge) -> bool {
|
|
self.output_edges.contains(edge)
|
|
}
|
|
|
|
/// Searches the `input_edges` for a [`Edge::SlotEdge`],
|
|
/// which `input_index` matches the `index`;
|
|
pub fn get_input_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> {
|
|
self.input_edges
|
|
.iter()
|
|
.find(|e| {
|
|
if let Edge::SlotEdge { input_index, .. } = e {
|
|
*input_index == index
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
.ok_or(RenderGraphError::UnconnectedNodeInputSlot {
|
|
input_slot: index,
|
|
node: self.id,
|
|
})
|
|
}
|
|
|
|
/// Searches the `output_edges` for a [`Edge::SlotEdge`],
|
|
/// which `output_index` matches the `index`;
|
|
pub fn get_output_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> {
|
|
self.output_edges
|
|
.iter()
|
|
.find(|e| {
|
|
if let Edge::SlotEdge { output_index, .. } = e {
|
|
*output_index == index
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
.ok_or(RenderGraphError::UnconnectedNodeOutputSlot {
|
|
output_slot: index,
|
|
node: self.id,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The internal representation of a [`Node`], with all data required
|
|
/// by the [`RenderGraph`](super::RenderGraph).
|
|
///
|
|
/// The `input_slots` and `output_slots` are provided by the `node`.
|
|
pub struct NodeState {
|
|
pub id: NodeId,
|
|
pub name: Option<Cow<'static, str>>,
|
|
/// The name of the type that implements [`Node`].
|
|
pub type_name: &'static str,
|
|
pub node: Box<dyn Node>,
|
|
pub input_slots: SlotInfos,
|
|
pub output_slots: SlotInfos,
|
|
pub edges: Edges,
|
|
}
|
|
|
|
impl Debug for NodeState {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "{:?} ({:?})", self.id, self.name)
|
|
}
|
|
}
|
|
|
|
impl NodeState {
|
|
/// Creates an [`NodeState`] without edges, but the `input_slots` and `output_slots`
|
|
/// are provided by the `node`.
|
|
pub fn new<T>(id: NodeId, node: T) -> Self
|
|
where
|
|
T: Node,
|
|
{
|
|
NodeState {
|
|
id,
|
|
name: None,
|
|
input_slots: node.input().into(),
|
|
output_slots: node.output().into(),
|
|
node: Box::new(node),
|
|
type_name: std::any::type_name::<T>(),
|
|
edges: Edges {
|
|
id,
|
|
input_edges: Vec::new(),
|
|
output_edges: Vec::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Retrieves the [`Node`].
|
|
pub fn node<T>(&self) -> Result<&T, RenderGraphError>
|
|
where
|
|
T: Node,
|
|
{
|
|
self.node
|
|
.downcast_ref::<T>()
|
|
.ok_or(RenderGraphError::WrongNodeType)
|
|
}
|
|
|
|
/// Retrieves the [`Node`] mutably.
|
|
pub fn node_mut<T>(&mut self) -> Result<&mut T, RenderGraphError>
|
|
where
|
|
T: Node,
|
|
{
|
|
self.node
|
|
.downcast_mut::<T>()
|
|
.ok_or(RenderGraphError::WrongNodeType)
|
|
}
|
|
|
|
/// Validates that each input slot corresponds to an input edge.
|
|
pub fn validate_input_slots(&self) -> Result<(), RenderGraphError> {
|
|
for i in 0..self.input_slots.len() {
|
|
self.edges.get_input_slot_edge(i)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Validates that each output slot corresponds to an output edge.
|
|
pub fn validate_output_slots(&self) -> Result<(), RenderGraphError> {
|
|
for i in 0..self.output_slots.len() {
|
|
self.edges.get_output_slot_edge(i)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A [`NodeLabel`] is used to reference a [`NodeState`] by either its name or [`NodeId`]
|
|
/// inside the [`RenderGraph`](super::RenderGraph).
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum NodeLabel {
|
|
Id(NodeId),
|
|
Name(Cow<'static, str>),
|
|
}
|
|
|
|
impl From<&NodeLabel> for NodeLabel {
|
|
fn from(value: &NodeLabel) -> Self {
|
|
value.clone()
|
|
}
|
|
}
|
|
|
|
impl From<String> for NodeLabel {
|
|
fn from(value: String) -> Self {
|
|
NodeLabel::Name(value.into())
|
|
}
|
|
}
|
|
|
|
impl From<&'static str> for NodeLabel {
|
|
fn from(value: &'static str) -> Self {
|
|
NodeLabel::Name(value.into())
|
|
}
|
|
}
|
|
|
|
impl From<NodeId> for NodeLabel {
|
|
fn from(value: NodeId) -> Self {
|
|
NodeLabel::Id(value)
|
|
}
|
|
}
|
|
|
|
/// A [`Node`] without any inputs, outputs and subgraphs, which does nothing when run.
|
|
/// Used (as a label) to bundle multiple dependencies into one inside
|
|
/// the [`RenderGraph`](super::RenderGraph).
|
|
pub struct EmptyNode;
|
|
|
|
impl Node for EmptyNode {
|
|
fn run(
|
|
&self,
|
|
_graph: &mut RenderGraphContext,
|
|
_render_context: &mut RenderContext,
|
|
_world: &World,
|
|
) -> Result<(), NodeRunError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A [`RenderGraph`](super::RenderGraph) [`Node`] that takes a view entity as input and runs the configured graph name once.
|
|
/// This makes it easier to insert sub-graph runs into a graph.
|
|
pub struct RunGraphOnViewNode {
|
|
graph_name: Cow<'static, str>,
|
|
}
|
|
|
|
impl RunGraphOnViewNode {
|
|
pub const IN_VIEW: &'static str = "view";
|
|
pub fn new<T: Into<Cow<'static, str>>>(graph_name: T) -> Self {
|
|
Self {
|
|
graph_name: graph_name.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Node for RunGraphOnViewNode {
|
|
fn input(&self) -> Vec<SlotInfo> {
|
|
vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)]
|
|
}
|
|
fn run(
|
|
&self,
|
|
graph: &mut RenderGraphContext,
|
|
_render_context: &mut RenderContext,
|
|
_world: &World,
|
|
) -> Result<(), NodeRunError> {
|
|
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
|
|
graph.run_sub_graph(
|
|
self.graph_name.clone(),
|
|
vec![SlotValue::Entity(view_entity)],
|
|
)?;
|
|
Ok(())
|
|
}
|
|
}
|