Renamed + Updated to Bevy 0.14 + Added 0BSD Option

This commit is contained in:
Silas Bartha 2024-07-24 11:46:40 -04:00
parent 5200536964
commit d7e1903369
Signed by: soaos
GPG Key ID: 9BD3DCC0D56A09B2
8 changed files with 94 additions and 79 deletions

View File

@ -1,11 +1,13 @@
[package] [package]
name = "bevy_framebuffer_extract" name = "bevy_headless_render"
version = "0.1.2" version = "0.1.0"
edition = "2021" edition = "2021"
description = "A plugin for the bevy engine which enables headless rendering to an image for use in the main world."
license = "0BSD OR MIT OR Apache-2.0"
[dependencies] [dependencies]
oneshot = "0.1.6" oneshot = "0.1.6"
pollster = "0.3.0" pollster = "0.3.0"
[dependencies.bevy] [dependencies.bevy]
version = "0.13" version = "0.14"

5
LICENSE-0BSD Normal file
View File

@ -0,0 +1,5 @@
Copyright (C) 2024 by Silas Bartha silas@exvacuum.dev
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -1,26 +1,33 @@
# bevy_framebuffer_extract # bevy_headless_render
[![Crates](https://img.shields.io/crates/v/bevy_headless_render)](https://crates.io/crates/bevy_headless_render)
![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg) ![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)
![Tag](https://img.shields.io/github/v/tag/exvacuum/bevy_framebuffer_extract) ![Tag](https://img.shields.io/github/v/tag/exvacuum/bevy_headless_render)
![Build](https://img.shields.io/github/actions/workflow/status/exvacuum/bevy_framebuffer_extract/rust.yml) ![Build](https://img.shields.io/github/actions/workflow/status/exvacuum/bevy_headless_render/rust.yml)
[![Docs](https://img.shields.io/website?url=https%3A%2F%2Fexvacuum.github.io%2Fbevy_framebuffer_extract%2F&label=docs)](https://exvacuum.github.io/bevy_framebuffer_extract) [![Docs](https://img.shields.io/website?url=https%3A%2F%2Fexvacuum.github.io%2Fbevy_headless_render%2F&label=docs)](https://exvacuum.github.io/bevy_headless_render)
A plugin for the [Bevy](https://bevyengine.org) engine which allows for exporting framebuffer data from a camera. A plugin for the [Bevy](https://bevyengine.org) engine which allows for headless rendering.
Currently it only supports cameras which render to a render texture. Every frame will be copied from `HeadlessRenderSource` render textures into `HeadlessRenderDestination` images each frame.
## Compatibility ## Compatibility
| Crate Version | Bevy Version | | Crate Version | Bevy Version |
|--- |--- | |--- |--- |
| 0.1 | 0.13 | | 0.1 | 0.14 |
## Installation ## Installation
### crates.io
```toml
[dependencies]
bevy_headless_render = "0.2"
```
### Using git URL in Cargo.toml ### Using git URL in Cargo.toml
```toml ```toml
[dependencies.bevy_framebuffer_extract] [dependencies.bevy_headless_render]
git = "https://github.com/exvacuum/bevy_framebuffer_extract.git" git = "https://github.com/exvacuum/bevy_headless_render.git"
``` ```
## Usage ## Usage
@ -28,13 +35,13 @@ git = "https://github.com/exvacuum/bevy_framebuffer_extract.git"
In `main.rs`: In `main.rs`:
```rs ```rs
use bevy::prelude::*; use bevy::prelude::*;
use bevy_framebuffer_extract; use bevy_headless_render;
fn main() { fn main() {
App::new() App::new()
.add_plugins(( .add_plugins((
DefaultPlugins, DefaultPlugins,
bevy_framebuffer_extract::FramebufferExtractPlugin, bevy_headless_render::HeadlessRenderPlugin,
)) ))
.run(); .run();
} }
@ -76,11 +83,16 @@ commands.spawn((
}, },
..Default::default() ..Default::default()
}, },
bevy_framebuffer_extract::ExtractFramebufferBundle { bevy_headless_render::HeadlessRenderBundle {
source: framebuffer_extract_sources.add(FramebufferExtractSource(image_handle.clone())), // ResMut<Assets<FramebufferExtractSource>> source: headless_render_sources.add(HeadlessRenderSource(image_handle.clone())), // ResMut<Assets<HeadlessRenderSource>>
destination: FramebufferExtractDestination::default(), destination: HeadlessRenderDestination::default(),
}, },
)); ));
``` ```
The FramebufferExtractDestination component will contain the extracted image which can be used or saved for whatever you need. The HeadlessRenderDestination component will contain the extracted image which can be used or saved for whatever you need.
## License
This crate is licensed under your choice of 0BSD, Apache-2.0, or MIT license.

View File

@ -2,18 +2,18 @@ use std::sync::{Arc, Mutex};
use bevy::{ecs::query::QueryItem, prelude::*, render::extract_component::ExtractComponent}; use bevy::{ecs::query::QueryItem, prelude::*, render::extract_component::ExtractComponent};
use crate::render_assets::FramebufferExtractSource; use crate::render_assets::HeadlessRenderSource;
/// Framebuffer extraction destination. Contains the image which the framebuffer is extracted to. /// Headless render destination. Contains the image which the rendered frame is copied to.
#[derive(Component, Default, Clone)] #[derive(Component, Default, Clone)]
pub struct FramebufferExtractDestination(pub Arc<Mutex<Image>>); pub struct HeadlessRenderDestination(pub Arc<Mutex<Image>>);
impl ExtractComponent for FramebufferExtractDestination { impl ExtractComponent for HeadlessRenderDestination {
type QueryData = (&'static Self, &'static Handle<FramebufferExtractSource>); type QueryData = (&'static Self, &'static Handle<HeadlessRenderSource>);
type QueryFilter = (); type QueryFilter = ();
type Out = (Self, Handle<FramebufferExtractSource>); type Out = (Self, Handle<HeadlessRenderSource>);
fn extract_component( fn extract_component(
(destination, source_handle): QueryItem<'_, Self::QueryData>, (destination, source_handle): QueryItem<'_, Self::QueryData>,
@ -22,11 +22,11 @@ impl ExtractComponent for FramebufferExtractDestination {
} }
} }
/// Bundle containing both a source and destination for framebuffer extraction. /// Bundle containing both a source and destination for headless rendering.
#[derive(Bundle)] #[derive(Bundle)]
pub struct ExtractFramebufferBundle { pub struct HeadlessRenderBundle {
/// Source /// Source
pub source: Handle<FramebufferExtractSource>, pub source: Handle<HeadlessRenderSource>,
/// Destination /// Destination
pub dest: FramebufferExtractDestination, pub dest: HeadlessRenderDestination,
} }

View File

@ -1,7 +1,6 @@
#![warn(missing_docs)] #![warn(missing_docs)]
//! Plugin for the Bevy game engine which provides the ability to extract the frambuffer image after rendering //! Plugin for the Bevy game engine which provides the ability to render to an image headlessly.
//! to use for whatever you want.
use bevy::{ use bevy::{
prelude::*, prelude::*,
@ -10,9 +9,9 @@ use bevy::{
render_asset::RenderAssetPlugin, render_graph::RenderGraph, Render, RenderApp, RenderSet, render_asset::RenderAssetPlugin, render_graph::RenderGraph, Render, RenderApp, RenderSet,
}, },
}; };
use components::FramebufferExtractDestination; use components::HeadlessRenderDestination;
use nodes::{FramebufferExtractLabel, FramebufferExtractNode}; use nodes::{HeadlessRenderCopyLabel, HeadlessRenderCopyNode};
use render_assets::FramebufferExtractSource; use render_assets::{HeadlessRenderSource, GpuHeadlessRenderSource};
/// Components used by this plugin. /// Components used by this plugin.
pub mod components; pub mod components;
@ -22,28 +21,28 @@ pub mod render_assets;
mod nodes; mod nodes;
mod systems; mod systems;
/// Plugin which handles framebuffer extraction. /// Plugin which handles headless rendering.
pub struct FramebufferExtractPlugin; pub struct HeadlessRenderPlugin;
impl Plugin for FramebufferExtractPlugin { impl Plugin for HeadlessRenderPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.register_type::<FramebufferExtractSource>() app.register_type::<HeadlessRenderSource>()
.init_asset::<FramebufferExtractSource>() .init_asset::<HeadlessRenderSource>()
.register_asset_reflect::<FramebufferExtractSource>() .register_asset_reflect::<HeadlessRenderSource>()
.add_plugins(( .add_plugins((
RenderAssetPlugin::<FramebufferExtractSource>::default(), RenderAssetPlugin::<GpuHeadlessRenderSource>::default(),
ExtractComponentPlugin::<FramebufferExtractDestination>::default(), ExtractComponentPlugin::<HeadlessRenderDestination>::default(),
)); ));
let render_app = app.sub_app_mut(RenderApp); let render_app = app.sub_app_mut(RenderApp);
render_app.add_systems( render_app.add_systems(
Render, Render,
systems::extract_framebuffers systems::copy_buffers
.after(RenderSet::Render) .after(RenderSet::Render)
.before(RenderSet::Cleanup), .before(RenderSet::Cleanup),
); );
let mut graph = render_app.world.resource_mut::<RenderGraph>(); let mut graph = render_app.world_mut().resource_mut::<RenderGraph>();
graph.add_node(FramebufferExtractLabel, FramebufferExtractNode); graph.add_node(HeadlessRenderCopyLabel, HeadlessRenderCopyNode);
graph.add_node_edge(CameraDriverLabel, FramebufferExtractLabel); graph.add_node_edge(CameraDriverLabel, HeadlessRenderCopyLabel);
} }
} }

View File

@ -4,19 +4,19 @@ use bevy::{
render_asset::RenderAssets, render_asset::RenderAssets,
render_graph::{Node, NodeRunError, RenderGraphContext, RenderLabel}, render_graph::{Node, NodeRunError, RenderGraphContext, RenderLabel},
render_resource::{ImageCopyBuffer, ImageDataLayout}, render_resource::{ImageCopyBuffer, ImageDataLayout},
renderer::RenderContext, renderer::RenderContext, texture::GpuImage,
}, },
}; };
use crate::render_assets::FramebufferExtractSource; use crate::render_assets::GpuHeadlessRenderSource;
#[derive(RenderLabel, Clone, PartialEq, Eq, Debug, Hash)] #[derive(RenderLabel, Clone, PartialEq, Eq, Debug, Hash)]
pub struct FramebufferExtractLabel; pub struct HeadlessRenderCopyLabel;
#[derive(Default)] #[derive(Default)]
pub struct FramebufferExtractNode; pub struct HeadlessRenderCopyNode;
impl Node for FramebufferExtractNode { impl Node for HeadlessRenderCopyNode {
fn run( fn run(
&self, &self,
_graph: &mut RenderGraphContext, _graph: &mut RenderGraphContext,
@ -24,12 +24,12 @@ impl Node for FramebufferExtractNode {
world: &World, world: &World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
for (_, source) in world for (_, source) in world
.resource::<RenderAssets<FramebufferExtractSource>>() .resource::<RenderAssets<GpuHeadlessRenderSource>>()
.iter() .iter()
{ {
let Some(gpu_image) = world let Some(gpu_image) = world
.resource::<RenderAssets<Image>>() .resource::<RenderAssets<GpuImage>>()
.get(&source.source_handle) .get(source.source_handle.id())
else { else {
return Ok(()); return Ok(());
}; };

View File

@ -2,14 +2,14 @@ use bevy::{
ecs::system::{lifetimeless::SRes, SystemParamItem}, ecs::system::{lifetimeless::SRes, SystemParamItem},
prelude::*, prelude::*,
render::{ render::{
render_asset::{PrepareAssetError, RenderAsset, RenderAssetUsages, RenderAssets}, render_asset::{PrepareAssetError, RenderAsset, RenderAssets},
render_resource::{Buffer, BufferDescriptor, BufferUsages, Extent3d, TextureFormat}, render_resource::{Buffer, BufferDescriptor, BufferUsages, Extent3d, TextureFormat},
renderer::RenderDevice, renderer::RenderDevice, texture::GpuImage,
}, },
}; };
/// Render-world version of FramebufferExtractSource /// Render-world version of HeadlessRenderSource
pub struct GpuFramebufferExtractSource { pub struct GpuHeadlessRenderSource {
pub(crate) buffer: Buffer, pub(crate) buffer: Buffer,
pub(crate) source_handle: Handle<Image>, pub(crate) source_handle: Handle<Image>,
pub(crate) source_size: Extent3d, pub(crate) source_size: Extent3d,
@ -18,26 +18,22 @@ pub struct GpuFramebufferExtractSource {
pub(crate) format: TextureFormat, pub(crate) format: TextureFormat,
} }
/// Framebuffer extraction source. Contains a handle to the render texture which will be extracted /// Headless render source. Contains a handle to the render texture which will be copied
/// from. /// from.
#[derive(Asset, Reflect, Clone, Default)] #[derive(Asset, Reflect, Clone, Default)]
pub struct FramebufferExtractSource(pub Handle<Image>); pub struct HeadlessRenderSource(pub Handle<Image>);
impl RenderAsset for FramebufferExtractSource { impl RenderAsset for GpuHeadlessRenderSource {
type PreparedAsset = GpuFramebufferExtractSource; type SourceAsset = HeadlessRenderSource;
type Param = (SRes<RenderDevice>, SRes<RenderAssets<Image>>); type Param = (SRes<RenderDevice>, SRes<RenderAssets<GpuImage>>);
fn asset_usage(&self) -> RenderAssetUsages {
RenderAssetUsages::default()
}
fn prepare_asset( fn prepare_asset(
self, source_asset: Self::SourceAsset,
(device, images): &mut SystemParamItem<Self::Param>, (device, images): &mut SystemParamItem<Self::Param>,
) -> Result<Self::PreparedAsset, PrepareAssetError<Self>> { ) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
let Some(gpu_image) = images.get(&self.0) else { let Some(gpu_image) = images.get(source_asset.0.id()) else {
warn!("Failed to get GPU image"); warn!("Failed to get GPU image");
return Err(PrepareAssetError::RetryNextUpdate(self)); return Err(PrepareAssetError::RetryNextUpdate(source_asset));
}; };
let size = gpu_image.texture.size(); let size = gpu_image.texture.size();
@ -47,18 +43,19 @@ impl RenderAsset for FramebufferExtractSource {
let padded_bytes_per_row = let padded_bytes_per_row =
RenderDevice::align_copy_bytes_per_row(bytes_per_row as usize) as u32; RenderDevice::align_copy_bytes_per_row(bytes_per_row as usize) as u32;
Ok(GpuFramebufferExtractSource { Ok(GpuHeadlessRenderSource {
buffer: device.create_buffer(&BufferDescriptor { buffer: device.create_buffer(&BufferDescriptor {
label: Some("framebuffer_extract_buffer"), label: Some("framebuffer_extract_buffer"),
size: (size.height * padded_bytes_per_row) as u64, size: (size.height * padded_bytes_per_row) as u64,
usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
mapped_at_creation: false, mapped_at_creation: false,
}), }),
source_handle: self.0.clone(), source_handle: source_asset.0.clone(),
source_size: size, source_size: size,
bytes_per_row, bytes_per_row,
padded_bytes_per_row, padded_bytes_per_row,
format, format,
}) })
} }
} }

View File

@ -9,17 +9,17 @@ use bevy::{
use pollster::FutureExt; use pollster::FutureExt;
use crate::{components::FramebufferExtractDestination, render_assets::FramebufferExtractSource}; use crate::{components::HeadlessRenderDestination, render_assets::{HeadlessRenderSource, GpuHeadlessRenderSource}};
pub fn extract_framebuffers( pub fn copy_buffers(
mut extract_bundles: Query<( mut headless_render_query: Query<(
&Handle<FramebufferExtractSource>, &Handle<HeadlessRenderSource>,
&mut FramebufferExtractDestination, &mut HeadlessRenderDestination,
)>, )>,
sources: Res<RenderAssets<FramebufferExtractSource>>, sources: Res<RenderAssets<GpuHeadlessRenderSource>>,
device: Res<RenderDevice>, device: Res<RenderDevice>,
) { ) {
for (source_handle, destination_handle) in extract_bundles.iter_mut() { for (source_handle, destination_handle) in headless_render_query.iter_mut() {
let Some(gpu_source) = sources.get(source_handle) else { let Some(gpu_source) = sources.get(source_handle) else {
continue; continue;
}; };