mirror of
https://github.com/bevyengine/bevy
synced 2024-11-26 14:40:19 +00:00
Merge branch 'main' into transmission
This commit is contained in:
commit
50faf11062
55 changed files with 2823 additions and 1380 deletions
2
.github/workflows/daily.yml
vendored
2
.github/workflows/daily.yml
vendored
|
@ -99,6 +99,8 @@ jobs:
|
|||
os_version: "12.0"
|
||||
- device: "Samsung Galaxy S23"
|
||||
os_version: "13.0"
|
||||
- device: "Google Pixel 8"
|
||||
os_version: "14.0"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
|
20
Cargo.toml
20
Cargo.toml
|
@ -1411,6 +1411,16 @@ description = "Illustrates creating custom system parameters with `SystemParam`"
|
|||
category = "ECS (Entity Component System)"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "time"
|
||||
path = "examples/ecs/time.rs"
|
||||
|
||||
[package.metadata.example.time]
|
||||
name = "Time handling"
|
||||
description = "Explains how Time is handled in ECS"
|
||||
category = "ECS (Entity Component System)"
|
||||
wasm = false
|
||||
|
||||
[[example]]
|
||||
name = "timers"
|
||||
path = "examples/ecs/timers.rs"
|
||||
|
@ -1723,6 +1733,16 @@ description = "A shader and a material that uses it"
|
|||
category = "Shaders"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "extended_material"
|
||||
path = "examples/shader/extended_material.rs"
|
||||
|
||||
[package.metadata.example.extended_material]
|
||||
name = "Extended Material"
|
||||
description = "A custom shader that builds on the standard material"
|
||||
category = "Shaders"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "shader_prepass"
|
||||
path = "examples/shader/shader_prepass.rs"
|
||||
|
|
|
@ -46,5 +46,5 @@ fn fragment(
|
|||
);
|
||||
pbr_input.V = fns::calculate_view(mesh.world_position, pbr_input.is_orthographic);
|
||||
|
||||
return tone_mapping(fns::pbr(pbr_input), view.color_grading);
|
||||
return tone_mapping(fns::apply_pbr_lighting(pbr_input), view.color_grading);
|
||||
}
|
||||
|
|
53
assets/shaders/extended_material.wgsl
Normal file
53
assets/shaders/extended_material.wgsl
Normal file
|
@ -0,0 +1,53 @@
|
|||
#import bevy_pbr::pbr_fragment pbr_input_from_standard_material
|
||||
#import bevy_pbr::pbr_functions alpha_discard
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
|
||||
#import bevy_pbr::pbr_deferred_functions deferred_output
|
||||
#else
|
||||
#import bevy_pbr::forward_io VertexOutput, FragmentOutput
|
||||
#import bevy_pbr::pbr_functions apply_pbr_lighting, main_pass_post_lighting_processing
|
||||
#endif
|
||||
|
||||
struct MyExtendedMaterial {
|
||||
quantize_steps: u32,
|
||||
}
|
||||
|
||||
@group(1) @binding(100)
|
||||
var<uniform> my_extended_material: MyExtendedMaterial;
|
||||
|
||||
@fragment
|
||||
fn fragment(
|
||||
in: VertexOutput,
|
||||
@builtin(front_facing) is_front: bool,
|
||||
) -> FragmentOutput {
|
||||
// generate a PbrInput struct from the StandardMaterial bindings
|
||||
var pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
|
||||
// we can optionally modify the input before lighting and alpha_discard is applied
|
||||
pbr_input.material.base_color.b = pbr_input.material.base_color.r;
|
||||
|
||||
// alpha discard
|
||||
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
// in deferred mode we can't modify anything after that, as lighting is run in a separate fullscreen shader.
|
||||
let out = deferred_output(in, pbr_input);
|
||||
#else
|
||||
var out: FragmentOutput;
|
||||
// apply lighting
|
||||
out.color = apply_pbr_lighting(pbr_input);
|
||||
|
||||
// we can optionally modify the lit color before post-processing is applied
|
||||
out.color = vec4<f32>(vec4<u32>(out.color * f32(my_extended_material.quantize_steps))) / f32(my_extended_material.quantize_steps);
|
||||
|
||||
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
|
||||
// note this does not include fullscreen postprocessing effects like bloom.
|
||||
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
||||
|
||||
// we can optionally modify the final result here
|
||||
out.color = out.color * 2.0;
|
||||
#endif
|
||||
|
||||
return out;
|
||||
}
|
|
@ -241,20 +241,25 @@ impl AssetSourceBuilder {
|
|||
/// Returns a builder containing the "platform default source" for the given `path` and `processed_path`.
|
||||
/// For most platforms, this will use [`FileAssetReader`](crate::io::file::FileAssetReader) / [`FileAssetWriter`](crate::io::file::FileAssetWriter),
|
||||
/// but some platforms (such as Android) have their own default readers / writers / watchers.
|
||||
pub fn platform_default(path: &str, processed_path: &str) -> Self {
|
||||
Self::default()
|
||||
pub fn platform_default(path: &str, processed_path: Option<&str>) -> Self {
|
||||
let default = Self::default()
|
||||
.with_reader(AssetSource::get_default_reader(path.to_string()))
|
||||
.with_writer(AssetSource::get_default_writer(path.to_string()))
|
||||
.with_watcher(AssetSource::get_default_watcher(
|
||||
path.to_string(),
|
||||
Duration::from_millis(300),
|
||||
))
|
||||
));
|
||||
if let Some(processed_path) = processed_path {
|
||||
default
|
||||
.with_processed_reader(AssetSource::get_default_reader(processed_path.to_string()))
|
||||
.with_processed_writer(AssetSource::get_default_writer(processed_path.to_string()))
|
||||
.with_processed_watcher(AssetSource::get_default_watcher(
|
||||
processed_path.to_string(),
|
||||
Duration::from_millis(300),
|
||||
))
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -315,7 +320,7 @@ impl AssetSourceBuilders {
|
|||
}
|
||||
|
||||
/// Initializes the default [`AssetSourceBuilder`] if it has not already been set.
|
||||
pub fn init_default_source(&mut self, path: &str, processed_path: &str) {
|
||||
pub fn init_default_source(&mut self, path: &str, processed_path: Option<&str>) {
|
||||
self.default
|
||||
.get_or_insert_with(|| AssetSourceBuilder::platform_default(path, processed_path));
|
||||
}
|
||||
|
|
|
@ -122,7 +122,11 @@ impl Plugin for AssetPlugin {
|
|||
let mut sources = app
|
||||
.world
|
||||
.get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
|
||||
sources.init_default_source(&self.file_path, &self.processed_file_path);
|
||||
sources.init_default_source(
|
||||
&self.file_path,
|
||||
(!matches!(self.mode, AssetMode::Unprocessed))
|
||||
.then_some(self.processed_file_path.as_str()),
|
||||
);
|
||||
embedded.register_source(&mut sources);
|
||||
}
|
||||
{
|
||||
|
|
|
@ -203,6 +203,12 @@ impl<'a> AssetPath<'a> {
|
|||
self.label.as_deref()
|
||||
}
|
||||
|
||||
/// Gets the "sub-asset label".
|
||||
#[inline]
|
||||
pub fn label_cow(&self) -> Option<CowArc<'a, str>> {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
/// Gets the path to the asset in the "virtual filesystem".
|
||||
#[inline]
|
||||
pub fn path(&self) -> &Path {
|
||||
|
|
|
@ -69,6 +69,9 @@ pub(crate) struct AssetInfos {
|
|||
/// Tracks assets that depend on the "key" asset path inside their asset loaders ("loader dependencies")
|
||||
/// This should only be set when watching for changes to avoid unnecessary work.
|
||||
pub(crate) loader_dependants: HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
|
||||
/// Tracks living labeled assets for a given source asset.
|
||||
/// This should only be set when watching for changes to avoid unnecessary work.
|
||||
pub(crate) living_labeled_assets: HashMap<AssetPath<'static>, HashSet<String>>,
|
||||
pub(crate) handle_providers: HashMap<TypeId, AssetHandleProvider>,
|
||||
pub(crate) dependency_loaded_event_sender: HashMap<TypeId, fn(&mut World, UntypedAssetId)>,
|
||||
}
|
||||
|
@ -88,6 +91,8 @@ impl AssetInfos {
|
|||
Self::create_handle_internal(
|
||||
&mut self.infos,
|
||||
&self.handle_providers,
|
||||
&mut self.living_labeled_assets,
|
||||
self.watching_for_changes,
|
||||
TypeId::of::<A>(),
|
||||
None,
|
||||
None,
|
||||
|
@ -107,6 +112,8 @@ impl AssetInfos {
|
|||
Self::create_handle_internal(
|
||||
&mut self.infos,
|
||||
&self.handle_providers,
|
||||
&mut self.living_labeled_assets,
|
||||
self.watching_for_changes,
|
||||
type_id,
|
||||
None,
|
||||
None,
|
||||
|
@ -116,9 +123,12 @@ impl AssetInfos {
|
|||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_handle_internal(
|
||||
infos: &mut HashMap<UntypedAssetId, AssetInfo>,
|
||||
handle_providers: &HashMap<TypeId, AssetHandleProvider>,
|
||||
living_labeled_assets: &mut HashMap<AssetPath<'static>, HashSet<String>>,
|
||||
watching_for_changes: bool,
|
||||
type_id: TypeId,
|
||||
path: Option<AssetPath<'static>>,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
|
@ -128,6 +138,16 @@ impl AssetInfos {
|
|||
.get(&type_id)
|
||||
.ok_or(MissingHandleProviderError(type_id))?;
|
||||
|
||||
if watching_for_changes {
|
||||
if let Some(path) = &path {
|
||||
let mut without_label = path.to_owned();
|
||||
if let Some(label) = without_label.take_label() {
|
||||
let labels = living_labeled_assets.entry(without_label).or_default();
|
||||
labels.insert(label.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let handle = provider.reserve_handle_internal(true, path.clone(), meta_transform);
|
||||
let mut info = AssetInfo::new(Arc::downgrade(&handle), path);
|
||||
if loading {
|
||||
|
@ -136,6 +156,7 @@ impl AssetInfos {
|
|||
info.rec_dep_load_state = RecursiveDependencyLoadState::Loading;
|
||||
}
|
||||
infos.insert(handle.id, info);
|
||||
|
||||
Ok(UntypedHandle::Strong(handle))
|
||||
}
|
||||
|
||||
|
@ -226,6 +247,8 @@ impl AssetInfos {
|
|||
let handle = Self::create_handle_internal(
|
||||
&mut self.infos,
|
||||
&self.handle_providers,
|
||||
&mut self.living_labeled_assets,
|
||||
self.watching_for_changes,
|
||||
type_id,
|
||||
Some(path),
|
||||
meta_transform,
|
||||
|
@ -256,7 +279,7 @@ impl AssetInfos {
|
|||
Some(UntypedHandle::Strong(strong_handle))
|
||||
}
|
||||
|
||||
/// Returns `true` if this path has
|
||||
/// Returns `true` if the asset this path points to is still alive
|
||||
pub(crate) fn is_path_alive<'a>(&self, path: impl Into<AssetPath<'a>>) -> bool {
|
||||
let path = path.into();
|
||||
if let Some(id) = self.path_to_id.get(&path) {
|
||||
|
@ -267,12 +290,26 @@ impl AssetInfos {
|
|||
false
|
||||
}
|
||||
|
||||
/// Returns `true` if the asset at this path should be reloaded
|
||||
pub(crate) fn should_reload(&self, path: &AssetPath) -> bool {
|
||||
if self.is_path_alive(path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(living) = self.living_labeled_assets.get(path) {
|
||||
!living.is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Returns `true` if the asset should be removed from the collection
|
||||
pub(crate) fn process_handle_drop(&mut self, id: UntypedAssetId) -> bool {
|
||||
Self::process_handle_drop_internal(
|
||||
&mut self.infos,
|
||||
&mut self.path_to_id,
|
||||
&mut self.loader_dependants,
|
||||
&mut self.living_labeled_assets,
|
||||
self.watching_for_changes,
|
||||
id,
|
||||
)
|
||||
|
@ -521,6 +558,7 @@ impl AssetInfos {
|
|||
infos: &mut HashMap<UntypedAssetId, AssetInfo>,
|
||||
path_to_id: &mut HashMap<AssetPath<'static>, UntypedAssetId>,
|
||||
loader_dependants: &mut HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
|
||||
living_labeled_assets: &mut HashMap<AssetPath<'static>, HashSet<String>>,
|
||||
watching_for_changes: bool,
|
||||
id: UntypedAssetId,
|
||||
) -> bool {
|
||||
|
@ -540,6 +578,18 @@ impl AssetInfos {
|
|||
dependants.remove(&path);
|
||||
}
|
||||
}
|
||||
if let Some(label) = path.label() {
|
||||
let mut without_label = path.to_owned();
|
||||
without_label.remove_label();
|
||||
if let Entry::Occupied(mut entry) =
|
||||
living_labeled_assets.entry(without_label)
|
||||
{
|
||||
entry.get_mut().remove(label);
|
||||
if entry.get().is_empty() {
|
||||
entry.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
path_to_id.remove(&path);
|
||||
}
|
||||
|
@ -566,6 +616,7 @@ impl AssetInfos {
|
|||
&mut self.infos,
|
||||
&mut self.path_to_id,
|
||||
&mut self.loader_dependants,
|
||||
&mut self.living_labeled_assets,
|
||||
self.watching_for_changes,
|
||||
id.untyped(provider.type_id),
|
||||
);
|
||||
|
|
|
@ -257,7 +257,7 @@ impl AssetServer {
|
|||
path: impl Into<AssetPath<'a>>,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Handle<A> {
|
||||
let mut path = path.into().into_owned();
|
||||
let path = path.into().into_owned();
|
||||
let (handle, should_load) = self.data.infos.write().get_or_create_path_handle::<A>(
|
||||
path.clone(),
|
||||
HandleLoadingMode::Request,
|
||||
|
@ -265,13 +265,10 @@ impl AssetServer {
|
|||
);
|
||||
|
||||
if should_load {
|
||||
let mut owned_handle = Some(handle.clone().untyped());
|
||||
let owned_handle = Some(handle.clone().untyped());
|
||||
let server = self.clone();
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
if path.take_label().is_some() {
|
||||
owned_handle = None;
|
||||
}
|
||||
if let Err(err) = server.load_internal(owned_handle, path, false, None).await {
|
||||
error!("{}", err);
|
||||
}
|
||||
|
@ -291,6 +288,10 @@ impl AssetServer {
|
|||
self.load_internal(None, path, false, None).await
|
||||
}
|
||||
|
||||
/// Performs an async asset load.
|
||||
///
|
||||
/// `input_handle` must only be [`Some`] if `should_load` was true when retrieving `input_handle`. This is an optimization to
|
||||
/// avoid looking up `should_load` twice, but it means you _must_ be sure a load is necessary when calling this function with [`Some`].
|
||||
async fn load_internal<'a>(
|
||||
&self,
|
||||
input_handle: Option<UntypedHandle>,
|
||||
|
@ -298,7 +299,7 @@ impl AssetServer {
|
|||
force: bool,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Result<UntypedHandle, AssetLoadError> {
|
||||
let mut path = path.into_owned();
|
||||
let path = path.into_owned();
|
||||
let path_clone = path.clone();
|
||||
let (mut meta, loader, mut reader) = self
|
||||
.get_meta_loader_and_reader(&path_clone)
|
||||
|
@ -312,18 +313,8 @@ impl AssetServer {
|
|||
e
|
||||
})?;
|
||||
|
||||
let has_label = path.label().is_some();
|
||||
|
||||
let (handle, should_load) = match input_handle {
|
||||
Some(handle) => {
|
||||
if !has_label && handle.type_id() != loader.asset_type_id() {
|
||||
return Err(AssetLoadError::RequestedHandleTypeMismatch {
|
||||
path: path.into_owned(),
|
||||
requested: handle.type_id(),
|
||||
actual_asset_name: loader.asset_type_name(),
|
||||
loader_name: loader.type_name(),
|
||||
});
|
||||
}
|
||||
// if a handle was passed in, the "should load" check was already done
|
||||
(handle, true)
|
||||
}
|
||||
|
@ -339,37 +330,51 @@ impl AssetServer {
|
|||
}
|
||||
};
|
||||
|
||||
if path.label().is_none() && handle.type_id() != loader.asset_type_id() {
|
||||
return Err(AssetLoadError::RequestedHandleTypeMismatch {
|
||||
path: path.into_owned(),
|
||||
requested: handle.type_id(),
|
||||
actual_asset_name: loader.asset_type_name(),
|
||||
loader_name: loader.type_name(),
|
||||
});
|
||||
}
|
||||
|
||||
if !should_load && !force {
|
||||
return Ok(handle);
|
||||
}
|
||||
let base_asset_id = if has_label {
|
||||
path.remove_label();
|
||||
// If the path has a label, the current id does not match the asset root type.
|
||||
// We need to get the actual asset id
|
||||
|
||||
let (base_handle, base_path) = if path.label().is_some() {
|
||||
let mut infos = self.data.infos.write();
|
||||
let (actual_handle, _) = infos.get_or_create_path_handle_untyped(
|
||||
path.clone(),
|
||||
let base_path = path.without_label().into_owned();
|
||||
let (base_handle, _) = infos.get_or_create_path_handle_untyped(
|
||||
base_path.clone(),
|
||||
loader.asset_type_id(),
|
||||
loader.asset_type_name(),
|
||||
// ignore current load state ... we kicked off this sub asset load because it needed to be loaded but
|
||||
// does not currently exist
|
||||
HandleLoadingMode::Force,
|
||||
None,
|
||||
);
|
||||
actual_handle.id()
|
||||
(base_handle, base_path)
|
||||
} else {
|
||||
handle.id()
|
||||
(handle.clone(), path.clone())
|
||||
};
|
||||
|
||||
if let Some(meta_transform) = handle.meta_transform() {
|
||||
if let Some(meta_transform) = base_handle.meta_transform() {
|
||||
(*meta_transform)(&mut *meta);
|
||||
}
|
||||
|
||||
match self
|
||||
.load_with_meta_loader_and_reader(&path, meta, &*loader, &mut *reader, true, false)
|
||||
.load_with_meta_loader_and_reader(&base_path, meta, &*loader, &mut *reader, true, false)
|
||||
.await
|
||||
{
|
||||
Ok(mut loaded_asset) => {
|
||||
if let Some(label) = path.label_cow() {
|
||||
if !loaded_asset.labeled_assets.contains_key(&label) {
|
||||
return Err(AssetLoadError::MissingLabel {
|
||||
base_path,
|
||||
label: label.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (_, labeled_asset) in loaded_asset.labeled_assets.drain() {
|
||||
self.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id: labeled_asset.handle.id(),
|
||||
|
@ -377,13 +382,15 @@ impl AssetServer {
|
|||
});
|
||||
}
|
||||
self.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id: base_asset_id,
|
||||
id: base_handle.id(),
|
||||
loaded_asset,
|
||||
});
|
||||
Ok(handle)
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_asset_event(InternalAssetEvent::Failed { id: base_asset_id });
|
||||
self.send_asset_event(InternalAssetEvent::Failed {
|
||||
id: base_handle.id(),
|
||||
});
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
@ -395,7 +402,7 @@ impl AssetServer {
|
|||
let path = path.into().into_owned();
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
if server.data.infos.read().is_path_alive(&path) {
|
||||
if server.data.infos.read().should_reload(&path) {
|
||||
info!("Reloading {path} because it has changed");
|
||||
if let Err(err) = server.load_internal(None, path, true, None).await {
|
||||
error!("{}", err);
|
||||
|
@ -935,6 +942,11 @@ pub enum AssetLoadError {
|
|||
loader_name: &'static str,
|
||||
error: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
#[error("The file at '{base_path}' does not contain the labeled asset '{label}'.")]
|
||||
MissingLabel {
|
||||
base_path: AssetPath<'static>,
|
||||
label: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// An error that occurs when an [`AssetLoader`] is not registered for a given extension.
|
||||
|
|
|
@ -154,7 +154,10 @@ impl Plugin for FrameCountPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_frame_count(mut frame_count: ResMut<FrameCount>) {
|
||||
/// A system used to increment [`FrameCount`] with wrapping addition.
|
||||
///
|
||||
/// See [`FrameCount`] for more details.
|
||||
pub fn update_frame_count(mut frame_count: ResMut<FrameCount>) {
|
||||
frame_count.0 = frame_count.0.wrapping_add(1);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic};
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_core::FrameCount;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_time::Time;
|
||||
use bevy_time::{Real, Time};
|
||||
|
||||
/// Adds "frame time" diagnostic to an App, specifically "frame time", "fps" and "frame count"
|
||||
#[derive(Default)]
|
||||
|
@ -30,12 +30,12 @@ impl FrameTimeDiagnosticsPlugin {
|
|||
|
||||
pub fn diagnostic_system(
|
||||
mut diagnostics: Diagnostics,
|
||||
time: Res<Time>,
|
||||
time: Res<Time<Real>>,
|
||||
frame_count: Res<FrameCount>,
|
||||
) {
|
||||
diagnostics.add_measurement(Self::FRAME_COUNT, || frame_count.0 as f64);
|
||||
|
||||
let delta_seconds = time.raw_delta_seconds_f64();
|
||||
let delta_seconds = time.delta_seconds_f64();
|
||||
if delta_seconds == 0.0 {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use super::{Diagnostic, DiagnosticId, DiagnosticsStore};
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_log::{debug, info};
|
||||
use bevy_time::{Time, Timer, TimerMode};
|
||||
use bevy_time::{Real, Time, Timer, TimerMode};
|
||||
use bevy_utils::Duration;
|
||||
|
||||
/// An App Plugin that logs diagnostics to the console
|
||||
|
@ -82,10 +82,10 @@ impl LogDiagnosticsPlugin {
|
|||
|
||||
fn log_diagnostics_system(
|
||||
mut state: ResMut<LogDiagnosticsState>,
|
||||
time: Res<Time>,
|
||||
time: Res<Time<Real>>,
|
||||
diagnostics: Res<DiagnosticsStore>,
|
||||
) {
|
||||
if state.timer.tick(time.raw_delta()).finished() {
|
||||
if state.timer.tick(time.delta()).finished() {
|
||||
if let Some(ref filter) = state.filter {
|
||||
for diagnostic in filter.iter().flat_map(|id| {
|
||||
diagnostics
|
||||
|
@ -107,10 +107,10 @@ impl LogDiagnosticsPlugin {
|
|||
|
||||
fn log_diagnostics_debug_system(
|
||||
mut state: ResMut<LogDiagnosticsState>,
|
||||
time: Res<Time>,
|
||||
time: Res<Time<Real>>,
|
||||
diagnostics: Res<DiagnosticsStore>,
|
||||
) {
|
||||
if state.timer.tick(time.raw_delta()).finished() {
|
||||
if state.timer.tick(time.delta()).finished() {
|
||||
if let Some(ref filter) = state.filter {
|
||||
for diagnostic in filter.iter().flat_map(|id| {
|
||||
diagnostics
|
||||
|
|
|
@ -5,7 +5,7 @@ use bevy_ecs::{
|
|||
};
|
||||
use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
|
||||
use bevy_log::{debug, warn};
|
||||
use bevy_time::Time;
|
||||
use bevy_time::{Real, Time};
|
||||
use bevy_utils::{Duration, HashMap};
|
||||
use gilrs::{
|
||||
ff::{self, BaseEffect, BaseEffectType, Repeat, Replay},
|
||||
|
@ -120,12 +120,12 @@ fn handle_rumble_request(
|
|||
Ok(())
|
||||
}
|
||||
pub(crate) fn play_gilrs_rumble(
|
||||
time: Res<Time>,
|
||||
time: Res<Time<Real>>,
|
||||
mut gilrs: NonSendMut<Gilrs>,
|
||||
mut requests: EventReader<GamepadRumbleRequest>,
|
||||
mut running_rumbles: NonSendMut<RunningRumbleEffects>,
|
||||
) {
|
||||
let current_time = time.raw_elapsed();
|
||||
let current_time = time.elapsed();
|
||||
// Remove outdated rumble effects.
|
||||
for rumbles in running_rumbles.rumbles.values_mut() {
|
||||
// `ff::Effect` uses RAII, dropping = deactivating
|
||||
|
|
|
@ -50,7 +50,9 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
|||
#ifdef WEBGL2
|
||||
frag_coord.z = deferred_types::unpack_unorm3x4_plus_unorm_20_(deferred_data.b).w;
|
||||
#else
|
||||
#ifdef DEPTH_PREPASS
|
||||
frag_coord.z = bevy_pbr::prepass_utils::prepass_depth(in.position, 0u);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
var pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, deferred_data);
|
||||
|
@ -65,28 +67,12 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
|
|||
pbr_input.occlusion = min(pbr_input.occlusion, ssao_multibounce);
|
||||
#endif // SCREEN_SPACE_AMBIENT_OCCLUSION
|
||||
|
||||
output_color = pbr_functions::pbr(pbr_input);
|
||||
output_color = pbr_functions::apply_pbr_lighting(pbr_input);
|
||||
} else {
|
||||
output_color = pbr_input.material.base_color;
|
||||
}
|
||||
|
||||
// fog
|
||||
if (fog.mode != FOG_MODE_OFF && (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) {
|
||||
output_color = pbr_functions::apply_fog(fog, output_color, pbr_input.world_position.xyz, view.world_position.xyz);
|
||||
}
|
||||
|
||||
#ifdef TONEMAP_IN_SHADER
|
||||
output_color = tone_mapping(output_color, view.color_grading);
|
||||
#ifdef DEBAND_DITHER
|
||||
var output_rgb = output_color.rgb;
|
||||
output_rgb = powsafe(output_rgb, 1.0 / 2.2);
|
||||
output_rgb = output_rgb + screen_space_dither(frag_coord.xy);
|
||||
// This conversion back to linear space is required because our output texture format is
|
||||
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
|
||||
output_rgb = powsafe(output_rgb, 2.2);
|
||||
output_color = vec4(output_rgb, output_color.a);
|
||||
#endif
|
||||
#endif
|
||||
output_color = pbr_functions::main_pass_post_lighting_processing(pbr_input, output_color);
|
||||
|
||||
return output_color;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use bevy_core_pipeline::{
|
|||
copy_lighting_id::DeferredLightingIdDepthTexture, DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT,
|
||||
},
|
||||
prelude::{Camera3d, ClearColor},
|
||||
prepass::DeferredPrepass,
|
||||
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
|
||||
tonemapping::{DebandDither, Tonemapping},
|
||||
};
|
||||
use bevy_ecs::{prelude::*, query::QueryItem};
|
||||
|
@ -258,6 +258,9 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
|
|||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||
let mut shader_defs = Vec::new();
|
||||
|
||||
// Let the shader code know that it's running in a deferred pipeline.
|
||||
shader_defs.push("DEFERRED_LIGHTING_PIPELINE".into());
|
||||
|
||||
#[cfg(all(feature = "webgl", target_arch = "wasm32"))]
|
||||
shader_defs.push("WEBGL2".into());
|
||||
|
||||
|
@ -298,6 +301,21 @@ impl SpecializedRenderPipeline for DeferredLightingLayout {
|
|||
shader_defs.push("ENVIRONMENT_MAP".into());
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::NORMAL_PREPASS) {
|
||||
shader_defs.push("NORMAL_PREPASS".into());
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::DEPTH_PREPASS) {
|
||||
shader_defs.push("DEPTH_PREPASS".into());
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
|
||||
shader_defs.push("MOTION_VECTOR_PREPASS".into());
|
||||
}
|
||||
|
||||
// Always true, since we're in the deferred lighting pipeline
|
||||
shader_defs.push("DEFERRED_PREPASS".into());
|
||||
|
||||
let shadow_filter_method =
|
||||
key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS);
|
||||
if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 {
|
||||
|
@ -408,14 +426,44 @@ pub fn prepare_deferred_lighting_pipelines(
|
|||
Option<&EnvironmentMapLight>,
|
||||
Option<&ShadowFilteringMethod>,
|
||||
Option<&ScreenSpaceAmbientOcclusionSettings>,
|
||||
(
|
||||
Has<NormalPrepass>,
|
||||
Has<DepthPrepass>,
|
||||
Has<MotionVectorPrepass>,
|
||||
),
|
||||
),
|
||||
With<DeferredPrepass>,
|
||||
>,
|
||||
images: Res<RenderAssets<Image>>,
|
||||
) {
|
||||
for (entity, view, tonemapping, dither, environment_map, shadow_filter_method, ssao) in &views {
|
||||
for (
|
||||
entity,
|
||||
view,
|
||||
tonemapping,
|
||||
dither,
|
||||
environment_map,
|
||||
shadow_filter_method,
|
||||
ssao,
|
||||
(normal_prepass, depth_prepass, motion_vector_prepass),
|
||||
) in &views
|
||||
{
|
||||
let mut view_key = MeshPipelineKey::from_hdr(view.hdr);
|
||||
|
||||
if normal_prepass {
|
||||
view_key |= MeshPipelineKey::NORMAL_PREPASS;
|
||||
}
|
||||
|
||||
if depth_prepass {
|
||||
view_key |= MeshPipelineKey::DEPTH_PREPASS;
|
||||
}
|
||||
|
||||
if motion_vector_prepass {
|
||||
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
|
||||
}
|
||||
|
||||
// Always true, since we're in the deferred lighting pipeline
|
||||
view_key |= MeshPipelineKey::DEFERRED_PREPASS;
|
||||
|
||||
if !view.hdr {
|
||||
if let Some(tonemapping) = tonemapping {
|
||||
view_key |= MeshPipelineKey::TONEMAP_IN_SHADER;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#define_import_path bevy_pbr::pbr_deferred_functions
|
||||
|
||||
#import bevy_pbr::pbr_types PbrInput, standard_material_new, STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT
|
||||
#import bevy_pbr::pbr_deferred_types as deferred_types
|
||||
#import bevy_pbr::pbr_functions as pbr_functions
|
||||
|
@ -6,6 +7,11 @@
|
|||
#import bevy_pbr::mesh_view_bindings as view_bindings
|
||||
#import bevy_pbr::mesh_view_bindings view
|
||||
#import bevy_pbr::utils octahedral_encode, octahedral_decode
|
||||
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
|
||||
|
||||
#ifdef MOTION_VECTOR_PREPASS
|
||||
#import bevy_pbr::pbr_prepass_functions calculate_motion_vector
|
||||
#endif
|
||||
|
||||
// ---------------------------
|
||||
// from https://github.com/DGriffin91/bevy_coordinate_systems/blob/main/src/transformations.wgsl
|
||||
|
@ -126,4 +132,23 @@ fn pbr_input_from_deferred_gbuffer(frag_coord: vec4<f32>, gbuffer: vec4<u32>) ->
|
|||
return pbr;
|
||||
}
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
fn deferred_output(in: VertexOutput, pbr_input: PbrInput) -> FragmentOutput {
|
||||
var out: FragmentOutput;
|
||||
|
||||
// gbuffer
|
||||
out.deferred = deferred_gbuffer_from_pbr_input(pbr_input);
|
||||
// lighting pass id (used to determine which lighting shader to run for the fragment)
|
||||
out.deferred_lighting_pass_id = pbr_input.material.deferred_lighting_pass_id;
|
||||
// normal if required
|
||||
#ifdef NORMAL_PREPASS
|
||||
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
|
||||
#endif
|
||||
// motion vectors if required
|
||||
#ifdef MOTION_VECTOR_PREPASS
|
||||
out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position);
|
||||
#endif
|
||||
|
||||
return out;
|
||||
}
|
||||
#endif
|
||||
|
|
257
crates/bevy_pbr/src/extended_material.rs
Normal file
257
crates/bevy_pbr/src/extended_material.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
use bevy_asset::{Asset, Handle};
|
||||
use bevy_reflect::TypePath;
|
||||
use bevy_render::{
|
||||
mesh::MeshVertexBufferLayout,
|
||||
render_asset::RenderAssets,
|
||||
render_resource::{
|
||||
AsBindGroup, AsBindGroupError, BindGroupLayout, RenderPipelineDescriptor, Shader,
|
||||
ShaderRef, SpecializedMeshPipelineError, UnpreparedBindGroup,
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
texture::{FallbackImage, Image},
|
||||
};
|
||||
|
||||
use crate::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshPipelineKey};
|
||||
|
||||
pub struct MaterialExtensionPipeline {
|
||||
pub mesh_pipeline: MeshPipeline,
|
||||
pub material_layout: BindGroupLayout,
|
||||
pub vertex_shader: Option<Handle<Shader>>,
|
||||
pub fragment_shader: Option<Handle<Shader>>,
|
||||
}
|
||||
|
||||
pub struct MaterialExtensionKey<E: MaterialExtension> {
|
||||
pub mesh_key: MeshPipelineKey,
|
||||
pub bind_group_data: E::Data,
|
||||
}
|
||||
|
||||
/// A subset of the `Material` trait for defining extensions to a base `Material`, such as the builtin `StandardMaterial`.
|
||||
/// A user type implementing the trait should be used as the `E` generic param in an `ExtendedMaterial` struct.
|
||||
pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized {
|
||||
/// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the base material mesh vertex shader
|
||||
/// will be used.
|
||||
fn vertex_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
}
|
||||
|
||||
/// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the base material mesh fragment shader
|
||||
/// will be used.
|
||||
#[allow(unused_variables)]
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
}
|
||||
|
||||
/// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader
|
||||
/// will be used.
|
||||
fn prepass_vertex_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
}
|
||||
|
||||
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material prepass fragment shader
|
||||
/// will be used.
|
||||
#[allow(unused_variables)]
|
||||
fn prepass_fragment_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
}
|
||||
|
||||
/// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the base material deferred vertex shader
|
||||
/// will be used.
|
||||
fn deferred_vertex_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
}
|
||||
|
||||
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material deferred fragment shader
|
||||
/// will be used.
|
||||
#[allow(unused_variables)]
|
||||
fn deferred_fragment_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
}
|
||||
|
||||
/// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's
|
||||
/// [`MaterialPipelineKey`] and [`MeshVertexBufferLayout`] as input.
|
||||
/// Specialization for the base material is applied before this function is called.
|
||||
#[allow(unused_variables)]
|
||||
#[inline]
|
||||
fn specialize(
|
||||
pipeline: &MaterialExtensionPipeline,
|
||||
descriptor: &mut RenderPipelineDescriptor,
|
||||
layout: &MeshVertexBufferLayout,
|
||||
key: MaterialExtensionKey<Self>,
|
||||
) -> Result<(), SpecializedMeshPipelineError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A material that extends a base [`Material`] with additional shaders and data.
|
||||
///
|
||||
/// The data from both materials will be combined and made available to the shader
|
||||
/// so that shader functions built for the base material (and referencing the base material
|
||||
/// bindings) will work as expected, and custom alterations based on custom data can also be used.
|
||||
///
|
||||
/// If the extension `E` returns a non-default result from `vertex_shader()` it will be used in place of the base
|
||||
/// material's vertex shader.
|
||||
///
|
||||
/// If the extension `E` returns a non-default result from `fragment_shader()` it will be used in place of the base
|
||||
/// fragment shader.
|
||||
///
|
||||
/// When used with `StandardMaterial` as the base, all the standard material fields are
|
||||
/// present, so the `pbr_fragment` shader functions can be called from the extension shader (see
|
||||
/// the `extended_material` example).
|
||||
#[derive(Asset, Clone, TypePath)]
|
||||
pub struct ExtendedMaterial<B: Material, E: MaterialExtension> {
|
||||
pub base: B,
|
||||
pub extension: E,
|
||||
}
|
||||
|
||||
impl<B: Material, E: MaterialExtension> AsBindGroup for ExtendedMaterial<B, E> {
|
||||
type Data = (<B as AsBindGroup>::Data, <E as AsBindGroup>::Data);
|
||||
|
||||
fn unprepared_bind_group(
|
||||
&self,
|
||||
layout: &BindGroupLayout,
|
||||
render_device: &RenderDevice,
|
||||
images: &RenderAssets<Image>,
|
||||
fallback_image: &FallbackImage,
|
||||
) -> Result<bevy_render::render_resource::UnpreparedBindGroup<Self::Data>, AsBindGroupError>
|
||||
{
|
||||
// add together the bindings of the base material and the user material
|
||||
let UnpreparedBindGroup {
|
||||
mut bindings,
|
||||
data: base_data,
|
||||
} = B::unprepared_bind_group(&self.base, layout, render_device, images, fallback_image)?;
|
||||
let extended_bindgroup = E::unprepared_bind_group(
|
||||
&self.extension,
|
||||
layout,
|
||||
render_device,
|
||||
images,
|
||||
fallback_image,
|
||||
)?;
|
||||
|
||||
bindings.extend(extended_bindgroup.bindings);
|
||||
|
||||
Ok(UnpreparedBindGroup {
|
||||
bindings,
|
||||
data: (base_data, extended_bindgroup.data),
|
||||
})
|
||||
}
|
||||
|
||||
fn bind_group_layout_entries(
|
||||
render_device: &RenderDevice,
|
||||
) -> Vec<bevy_render::render_resource::BindGroupLayoutEntry>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// add together the bindings of the standard material and the user material
|
||||
let mut entries = B::bind_group_layout_entries(render_device);
|
||||
entries.extend(E::bind_group_layout_entries(render_device));
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Material, E: MaterialExtension> Material for ExtendedMaterial<B, E> {
|
||||
fn vertex_shader() -> bevy_render::render_resource::ShaderRef {
|
||||
match E::vertex_shader() {
|
||||
ShaderRef::Default => B::vertex_shader(),
|
||||
specified => specified,
|
||||
}
|
||||
}
|
||||
|
||||
fn fragment_shader() -> bevy_render::render_resource::ShaderRef {
|
||||
match E::fragment_shader() {
|
||||
ShaderRef::Default => B::fragment_shader(),
|
||||
specified => specified,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepass_vertex_shader() -> bevy_render::render_resource::ShaderRef {
|
||||
match E::prepass_vertex_shader() {
|
||||
ShaderRef::Default => B::prepass_vertex_shader(),
|
||||
specified => specified,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepass_fragment_shader() -> bevy_render::render_resource::ShaderRef {
|
||||
match E::prepass_fragment_shader() {
|
||||
ShaderRef::Default => B::prepass_fragment_shader(),
|
||||
specified => specified,
|
||||
}
|
||||
}
|
||||
|
||||
fn deferred_vertex_shader() -> bevy_render::render_resource::ShaderRef {
|
||||
match E::deferred_vertex_shader() {
|
||||
ShaderRef::Default => B::deferred_vertex_shader(),
|
||||
specified => specified,
|
||||
}
|
||||
}
|
||||
|
||||
fn deferred_fragment_shader() -> bevy_render::render_resource::ShaderRef {
|
||||
match E::deferred_fragment_shader() {
|
||||
ShaderRef::Default => B::deferred_fragment_shader(),
|
||||
specified => specified,
|
||||
}
|
||||
}
|
||||
|
||||
fn alpha_mode(&self) -> crate::AlphaMode {
|
||||
B::alpha_mode(&self.base)
|
||||
}
|
||||
|
||||
fn depth_bias(&self) -> f32 {
|
||||
B::depth_bias(&self.base)
|
||||
}
|
||||
|
||||
fn opaque_render_method(&self) -> crate::OpaqueRendererMethod {
|
||||
B::opaque_render_method(&self.base)
|
||||
}
|
||||
|
||||
fn specialize(
|
||||
pipeline: &MaterialPipeline<Self>,
|
||||
descriptor: &mut RenderPipelineDescriptor,
|
||||
layout: &MeshVertexBufferLayout,
|
||||
key: MaterialPipelineKey<Self>,
|
||||
) -> Result<(), SpecializedMeshPipelineError> {
|
||||
// Call the base material's specialize function
|
||||
let MaterialPipeline::<Self> {
|
||||
mesh_pipeline,
|
||||
material_layout,
|
||||
vertex_shader,
|
||||
fragment_shader,
|
||||
..
|
||||
} = pipeline.clone();
|
||||
let base_pipeline = MaterialPipeline::<B> {
|
||||
mesh_pipeline,
|
||||
material_layout,
|
||||
vertex_shader,
|
||||
fragment_shader,
|
||||
marker: Default::default(),
|
||||
};
|
||||
let base_key = MaterialPipelineKey::<B> {
|
||||
mesh_key: key.mesh_key,
|
||||
bind_group_data: key.bind_group_data.0,
|
||||
};
|
||||
B::specialize(&base_pipeline, descriptor, layout, base_key)?;
|
||||
|
||||
// Call the extended material's specialize function afterwards
|
||||
let MaterialPipeline::<Self> {
|
||||
mesh_pipeline,
|
||||
material_layout,
|
||||
vertex_shader,
|
||||
fragment_shader,
|
||||
..
|
||||
} = pipeline.clone();
|
||||
|
||||
E::specialize(
|
||||
&MaterialExtensionPipeline {
|
||||
mesh_pipeline,
|
||||
material_layout,
|
||||
vertex_shader,
|
||||
fragment_shader,
|
||||
},
|
||||
descriptor,
|
||||
layout,
|
||||
MaterialExtensionKey {
|
||||
mesh_key: key.mesh_key,
|
||||
bind_group_data: key.bind_group_data.1,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ mod alpha;
|
|||
mod bundle;
|
||||
pub mod deferred;
|
||||
mod environment_map;
|
||||
mod extended_material;
|
||||
mod fog;
|
||||
mod light;
|
||||
mod material;
|
||||
|
@ -18,6 +19,7 @@ mod ssao;
|
|||
pub use alpha::*;
|
||||
pub use bundle::*;
|
||||
pub use environment_map::EnvironmentMapLight;
|
||||
pub use extended_material::*;
|
||||
pub use fog::*;
|
||||
pub use light::*;
|
||||
pub use material::*;
|
||||
|
@ -73,6 +75,7 @@ pub const CLUSTERED_FORWARD_HANDLE: Handle<Shader> = Handle::weak_from_u128(1668
|
|||
pub const PBR_LIGHTING_HANDLE: Handle<Shader> = Handle::weak_from_u128(14170772752254856967);
|
||||
pub const SHADOWS_HANDLE: Handle<Shader> = Handle::weak_from_u128(11350275143789590502);
|
||||
pub const SHADOW_SAMPLING_HANDLE: Handle<Shader> = Handle::weak_from_u128(3145627513789590502);
|
||||
pub const PBR_FRAGMENT_HANDLE: Handle<Shader> = Handle::weak_from_u128(2295049283805286543);
|
||||
pub const PBR_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4805239651767701046);
|
||||
pub const PBR_PREPASS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(9407115064344201137);
|
||||
pub const PBR_FUNCTIONS_HANDLE: Handle<Shader> = Handle::weak_from_u128(16550102964439850292);
|
||||
|
@ -172,6 +175,12 @@ impl Plugin for PbrPlugin {
|
|||
"render/pbr_ambient.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
load_internal_asset!(
|
||||
app,
|
||||
PBR_FRAGMENT_HANDLE,
|
||||
"render/pbr_fragment.wgsl",
|
||||
Shader::from_wgsl
|
||||
);
|
||||
load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl);
|
||||
load_internal_asset!(
|
||||
app,
|
||||
|
|
|
@ -10,7 +10,7 @@ use bevy_core_pipeline::{
|
|||
AlphaMask3d, Camera3d, Opaque3d, ScreenSpaceTransmissionQuality, Transmissive3d,
|
||||
Transparent3d,
|
||||
},
|
||||
prepass::{DeferredPrepass, NormalPrepass},
|
||||
prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass},
|
||||
tonemapping::{DebandDither, Tonemapping},
|
||||
};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
|
@ -148,12 +148,18 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized {
|
|||
|
||||
/// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader
|
||||
/// will be used.
|
||||
///
|
||||
/// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps
|
||||
/// required for shadow mapping.
|
||||
fn prepass_vertex_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
}
|
||||
|
||||
/// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader
|
||||
/// will be used.
|
||||
///
|
||||
/// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps
|
||||
/// required for shadow mapping.
|
||||
#[allow(unused_variables)]
|
||||
fn prepass_fragment_shader() -> ShaderRef {
|
||||
ShaderRef::Default
|
||||
|
@ -304,7 +310,7 @@ pub struct MaterialPipeline<M: Material> {
|
|||
pub material_layout: BindGroupLayout,
|
||||
pub vertex_shader: Option<Handle<Shader>>,
|
||||
pub fragment_shader: Option<Handle<Shader>>,
|
||||
marker: PhantomData<M>,
|
||||
pub marker: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M: Material> Clone for MaterialPipeline<M> {
|
||||
|
@ -477,10 +483,14 @@ pub fn queue_material_meshes<M: Material>(
|
|||
Option<&EnvironmentMapLight>,
|
||||
Option<&ShadowFilteringMethod>,
|
||||
Option<&ScreenSpaceAmbientOcclusionSettings>,
|
||||
Option<&NormalPrepass>,
|
||||
Option<&TemporalJitter>,
|
||||
(
|
||||
Has<NormalPrepass>,
|
||||
Has<DepthPrepass>,
|
||||
Has<MotionVectorPrepass>,
|
||||
Has<DeferredPrepass>,
|
||||
),
|
||||
Option<&Camera3d>,
|
||||
Option<&DeferredPrepass>,
|
||||
Option<&TemporalJitter>,
|
||||
&mut RenderPhase<Opaque3d>,
|
||||
&mut RenderPhase<AlphaMask3d>,
|
||||
&mut RenderPhase<Transmissive3d>,
|
||||
|
@ -497,10 +507,9 @@ pub fn queue_material_meshes<M: Material>(
|
|||
environment_map,
|
||||
shadow_filter_method,
|
||||
ssao,
|
||||
normal_prepass,
|
||||
temporal_jitter,
|
||||
(normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass),
|
||||
camera_3d,
|
||||
deferred_prepass,
|
||||
temporal_jitter,
|
||||
mut opaque_phase,
|
||||
mut alpha_mask_phase,
|
||||
mut transmissive_phase,
|
||||
|
@ -515,7 +524,19 @@ pub fn queue_material_meshes<M: Material>(
|
|||
let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples())
|
||||
| MeshPipelineKey::from_hdr(view.hdr);
|
||||
|
||||
if deferred_prepass.is_some() {
|
||||
if normal_prepass {
|
||||
view_key |= MeshPipelineKey::NORMAL_PREPASS;
|
||||
}
|
||||
|
||||
if depth_prepass {
|
||||
view_key |= MeshPipelineKey::DEPTH_PREPASS;
|
||||
}
|
||||
|
||||
if motion_vector_prepass {
|
||||
view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS;
|
||||
}
|
||||
|
||||
if deferred_prepass {
|
||||
view_key |= MeshPipelineKey::DEFERRED_PREPASS;
|
||||
}
|
||||
|
||||
|
@ -581,14 +602,6 @@ pub fn queue_material_meshes<M: Material>(
|
|||
|
||||
let mut mesh_key = view_key;
|
||||
|
||||
if normal_prepass.is_some()
|
||||
&& mesh_key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS)
|
||||
== MeshPipelineKey::BLEND_OPAQUE
|
||||
&& !material.properties.reads_view_transmission_texture
|
||||
{
|
||||
mesh_key |= MeshPipelineKey::NORMAL_PREPASS;
|
||||
}
|
||||
|
||||
mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology);
|
||||
|
||||
if mesh.morph_targets.is_some() {
|
||||
|
@ -596,10 +609,6 @@ pub fn queue_material_meshes<M: Material>(
|
|||
}
|
||||
mesh_key |= alpha_mode_pipeline_key(material.properties.alpha_mode);
|
||||
|
||||
if deferred_prepass.is_some() && !forward {
|
||||
mesh_key |= MeshPipelineKey::DEFERRED_PREPASS;
|
||||
}
|
||||
|
||||
let pipeline_id = pipelines.specialize(
|
||||
&pipeline_cache,
|
||||
&material_pipeline,
|
||||
|
@ -744,7 +753,7 @@ pub struct MaterialProperties {
|
|||
|
||||
/// Data prepared for a [`Material`] instance.
|
||||
pub struct PreparedMaterial<T: Material> {
|
||||
pub bindings: Vec<OwnedBindingResource>,
|
||||
pub bindings: Vec<(u32, OwnedBindingResource)>,
|
||||
pub bind_group: BindGroup,
|
||||
pub key: T::Data,
|
||||
pub properties: MaterialProperties,
|
||||
|
|
|
@ -378,6 +378,11 @@ where
|
|||
let mut shader_defs = Vec::new();
|
||||
let mut vertex_attributes = Vec::new();
|
||||
|
||||
// Let the shader code know that it's running in a prepass pipeline.
|
||||
// (PBR code will use this to detect that it's running in deferred mode,
|
||||
// since that's the only time it gets called from a prepass pipeline.)
|
||||
shader_defs.push("PREPASS_PIPELINE".into());
|
||||
|
||||
// NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material.
|
||||
// The main limitation right now is that bind group order is hardcoded in shaders.
|
||||
bind_group_layouts.insert(1, self.material_layout.clone());
|
||||
|
@ -886,18 +891,26 @@ pub fn queue_prepass_material_meshes<M: Material>(
|
|||
render_mesh_instances: Res<RenderMeshInstances>,
|
||||
render_materials: Res<RenderMaterials<M>>,
|
||||
render_material_instances: Res<RenderMaterialInstances<M>>,
|
||||
mut views: Query<(
|
||||
mut views: Query<
|
||||
(
|
||||
&ExtractedView,
|
||||
&VisibleEntities,
|
||||
&mut RenderPhase<Opaque3dPrepass>,
|
||||
&mut RenderPhase<AlphaMask3dPrepass>,
|
||||
Option<&mut RenderPhase<Opaque3dPrepass>>,
|
||||
Option<&mut RenderPhase<AlphaMask3dPrepass>>,
|
||||
Option<&mut RenderPhase<Opaque3dDeferred>>,
|
||||
Option<&mut RenderPhase<AlphaMask3dDeferred>>,
|
||||
Option<&DepthPrepass>,
|
||||
Option<&NormalPrepass>,
|
||||
Option<&MotionVectorPrepass>,
|
||||
Option<&DeferredPrepass>,
|
||||
),
|
||||
Or<(
|
||||
With<RenderPhase<Opaque3dPrepass>>,
|
||||
With<RenderPhase<AlphaMask3dPrepass>>,
|
||||
With<RenderPhase<Opaque3dDeferred>>,
|
||||
With<RenderPhase<AlphaMask3dDeferred>>,
|
||||
)>,
|
||||
>,
|
||||
) where
|
||||
M::Data: PartialEq + Eq + Hash + Clone,
|
||||
{
|
||||
|
@ -1028,7 +1041,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
|
|||
dynamic_offset: None,
|
||||
});
|
||||
} else {
|
||||
opaque_phase.add(Opaque3dPrepass {
|
||||
opaque_phase.as_mut().unwrap().add(Opaque3dPrepass {
|
||||
entity: *visible_entity,
|
||||
draw_function: opaque_draw_prepass,
|
||||
pipeline_id,
|
||||
|
@ -1052,7 +1065,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
|
|||
dynamic_offset: None,
|
||||
});
|
||||
} else {
|
||||
alpha_mask_phase.add(AlphaMask3dPrepass {
|
||||
alpha_mask_phase.as_mut().unwrap().add(AlphaMask3dPrepass {
|
||||
entity: *visible_entity,
|
||||
draw_function: alpha_mask_draw_prepass,
|
||||
pipeline_id,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#import bevy_pbr::mesh_view_bindings as view_bindings
|
||||
|
||||
#ifndef DEPTH_PREPASS
|
||||
#ifdef DEPTH_PREPASS
|
||||
#ifndef WEBGL2
|
||||
fn prepass_depth(frag_coord: vec4<f32>, sample_index: u32) -> f32 {
|
||||
#ifdef MULTISAMPLED
|
||||
|
@ -14,7 +14,7 @@ fn prepass_depth(frag_coord: vec4<f32>, sample_index: u32) -> f32 {
|
|||
#endif // WEBGL2
|
||||
#endif // DEPTH_PREPASS
|
||||
|
||||
#ifndef NORMAL_PREPASS
|
||||
#ifdef NORMAL_PREPASS
|
||||
fn prepass_normal(frag_coord: vec4<f32>, sample_index: u32) -> vec3<f32> {
|
||||
#ifdef MULTISAMPLED
|
||||
let normal_sample = textureLoad(view_bindings::normal_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
|
||||
|
@ -25,7 +25,7 @@ fn prepass_normal(frag_coord: vec4<f32>, sample_index: u32) -> vec3<f32> {
|
|||
}
|
||||
#endif // NORMAL_PREPASS
|
||||
|
||||
#ifndef MOTION_VECTOR_PREPASS
|
||||
#ifdef MOTION_VECTOR_PREPASS
|
||||
fn prepass_motion_vector(frag_coord: vec4<f32>, sample_index: u32) -> vec2<f32> {
|
||||
#ifdef MULTISAMPLED
|
||||
let motion_vector_sample = textureLoad(view_bindings::motion_vector_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
|
||||
|
|
|
@ -811,6 +811,9 @@ impl SpecializedMeshPipeline for MeshPipeline {
|
|||
let mut shader_defs = Vec::new();
|
||||
let mut vertex_attributes = Vec::new();
|
||||
|
||||
// Let the shader code know that it's running in a mesh pipeline.
|
||||
shader_defs.push("MESH_PIPELINE".into());
|
||||
|
||||
shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into());
|
||||
|
||||
if layout.contains(Mesh::ATTRIBUTE_POSITION) {
|
||||
|
@ -913,6 +916,22 @@ impl SpecializedMeshPipeline for MeshPipeline {
|
|||
is_opaque = true;
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::NORMAL_PREPASS) {
|
||||
shader_defs.push("NORMAL_PREPASS".into());
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::DEPTH_PREPASS) {
|
||||
shader_defs.push("DEPTH_PREPASS".into());
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) {
|
||||
shader_defs.push("MOTION_VECTOR_PREPASS".into());
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::DEFERRED_PREPASS) {
|
||||
shader_defs.push("DEFERRED_PREPASS".into());
|
||||
}
|
||||
|
||||
if key.contains(MeshPipelineKey::NORMAL_PREPASS) && key.msaa_samples() == 1 && is_opaque {
|
||||
shader_defs.push("LOAD_PREPASS_NORMALS".into());
|
||||
}
|
||||
|
|
|
@ -1,256 +1,43 @@
|
|||
#define_import_path bevy_pbr::fragment
|
||||
#import bevy_pbr::pbr_functions alpha_discard
|
||||
#import bevy_pbr::pbr_fragment pbr_input_from_standard_material
|
||||
|
||||
#import bevy_pbr::pbr_functions as pbr_functions
|
||||
#import bevy_pbr::pbr_bindings as pbr_bindings
|
||||
#import bevy_pbr::pbr_types as pbr_types
|
||||
|
||||
#import bevy_pbr::mesh_bindings mesh
|
||||
#import bevy_pbr::mesh_view_bindings view, fog, screen_space_ambient_occlusion_texture
|
||||
#import bevy_pbr::mesh_view_types FOG_MODE_OFF
|
||||
#import bevy_core_pipeline::tonemapping screen_space_dither, powsafe, tone_mapping
|
||||
#import bevy_pbr::parallax_mapping parallaxed_uv
|
||||
|
||||
#import bevy_pbr::prepass_utils
|
||||
|
||||
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
|
||||
#import bevy_pbr::gtao_utils gtao_multibounce
|
||||
#endif
|
||||
|
||||
#ifdef DEFERRED_PREPASS
|
||||
#import bevy_pbr::pbr_deferred_functions deferred_gbuffer_from_pbr_input
|
||||
#import bevy_pbr::pbr_prepass_functions calculate_motion_vector
|
||||
#ifdef PREPASS_PIPELINE
|
||||
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
|
||||
#else // DEFERRED_PREPASS
|
||||
#import bevy_pbr::pbr_deferred_functions deferred_output
|
||||
#else
|
||||
#import bevy_pbr::forward_io VertexOutput, FragmentOutput
|
||||
#endif // DEFERRED_PREPASS
|
||||
|
||||
#ifdef MOTION_VECTOR_PREPASS
|
||||
@group(0) @binding(2)
|
||||
var<uniform> previous_view_proj: mat4x4<f32>;
|
||||
#endif // MOTION_VECTOR_PREPASS
|
||||
#import bevy_pbr::pbr_functions apply_pbr_lighting, main_pass_post_lighting_processing
|
||||
#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_UNLIT_BIT
|
||||
#endif
|
||||
|
||||
@fragment
|
||||
fn fragment(
|
||||
in: VertexOutput,
|
||||
@builtin(front_facing) is_front: bool,
|
||||
) -> FragmentOutput {
|
||||
var out: FragmentOutput;
|
||||
// generate a PbrInput struct from the StandardMaterial bindings
|
||||
var pbr_input = pbr_input_from_standard_material(in, is_front);
|
||||
|
||||
// calculate unlit color
|
||||
// ---------------------
|
||||
var unlit_color: vec4<f32> = pbr_bindings::material.base_color;
|
||||
// alpha discard
|
||||
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
|
||||
|
||||
let is_orthographic = view.projection[3].w == 1.0;
|
||||
let V = pbr_functions::calculate_view(in.world_position, is_orthographic);
|
||||
#ifdef VERTEX_UVS
|
||||
var uv = in.uv;
|
||||
#ifdef VERTEX_TANGENTS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) {
|
||||
let N = in.world_normal;
|
||||
let T = in.world_tangent.xyz;
|
||||
let B = in.world_tangent.w * cross(N, T);
|
||||
// Transform V from fragment to camera in world space to tangent space.
|
||||
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
|
||||
uv = parallaxed_uv(
|
||||
pbr_bindings::material.parallax_depth_scale,
|
||||
pbr_bindings::material.max_parallax_layer_count,
|
||||
pbr_bindings::material.max_relief_mapping_search_steps,
|
||||
uv,
|
||||
// Flip the direction of Vt to go toward the surface to make the
|
||||
// parallax mapping algorithm easier to understand and reason
|
||||
// about.
|
||||
-Vt,
|
||||
);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef VERTEX_COLORS
|
||||
unlit_color = unlit_color * in.color;
|
||||
#endif
|
||||
#ifdef VERTEX_UVS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
|
||||
unlit_color = unlit_color * textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias);
|
||||
}
|
||||
#endif
|
||||
|
||||
// gather pbr lighting data
|
||||
// ------------------
|
||||
var pbr_input: pbr_types::PbrInput;
|
||||
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
|
||||
// Prepare a 'processed' StandardMaterial by sampling all textures to resolve
|
||||
// the material members
|
||||
|
||||
pbr_input.material.reflectance = pbr_bindings::material.reflectance;
|
||||
pbr_input.material.ior = pbr_bindings::material.ior;
|
||||
pbr_input.material.attenuation_color = pbr_bindings::material.attenuation_color;
|
||||
pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance;
|
||||
pbr_input.material.flags = pbr_bindings::material.flags;
|
||||
pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff;
|
||||
pbr_input.frag_coord = in.position;
|
||||
pbr_input.world_position = in.world_position;
|
||||
pbr_input.is_orthographic = is_orthographic;
|
||||
pbr_input.flags = mesh[in.instance_index].flags;
|
||||
|
||||
// emmissive
|
||||
// TODO use .a for exposure compensation in HDR
|
||||
var emissive: vec4<f32> = pbr_bindings::material.emissive;
|
||||
#ifdef VERTEX_UVS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
|
||||
emissive = vec4<f32>(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0);
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.emissive = emissive;
|
||||
|
||||
// metallic and perceptual roughness
|
||||
var metallic: f32 = pbr_bindings::material.metallic;
|
||||
var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness;
|
||||
#ifdef VERTEX_UVS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
|
||||
let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias);
|
||||
// Sampling from GLTF standard channels for now
|
||||
metallic = metallic * metallic_roughness.b;
|
||||
perceptual_roughness = perceptual_roughness * metallic_roughness.g;
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.metallic = metallic;
|
||||
pbr_input.material.perceptual_roughness = perceptual_roughness;
|
||||
|
||||
var specular_transmission: f32 = pbr_bindings::material.specular_transmission;
|
||||
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) {
|
||||
specular_transmission *= textureSample(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv).r;
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.specular_transmission = specular_transmission;
|
||||
|
||||
var thickness: f32 = pbr_bindings::material.thickness;
|
||||
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) {
|
||||
thickness *= textureSample(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv).g;
|
||||
}
|
||||
#endif
|
||||
// scale the thickness by the average length of basis vectors for the transform matrix
|
||||
// this is a rough way to approximate the average “scale” applied to the mesh as a single scalar
|
||||
thickness *= (length(mesh[in.instance_index].model[0].xyz) + length(mesh[in.instance_index].model[1].xyz) + length(mesh[in.instance_index].model[2].xyz)) / 3.0;
|
||||
pbr_input.material.thickness = thickness;
|
||||
|
||||
var diffuse_transmission = pbr_bindings::material.diffuse_transmission;
|
||||
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) {
|
||||
diffuse_transmission *= textureSample(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv).a;
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.diffuse_transmission = diffuse_transmission;
|
||||
|
||||
// occlusion
|
||||
// TODO: Split into diffuse/specular occlusion?
|
||||
var occlusion: vec3<f32> = vec3(1.0);
|
||||
#ifdef VERTEX_UVS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
|
||||
occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r);
|
||||
}
|
||||
#endif
|
||||
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
|
||||
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.position.xy), 0i).r;
|
||||
let ssao_multibounce = gtao_multibounce(ssao, unlit_color.rgb);
|
||||
occlusion = min(occlusion, ssao_multibounce);
|
||||
#endif
|
||||
pbr_input.occlusion = occlusion;
|
||||
|
||||
// world normal
|
||||
pbr_input.world_normal = pbr_functions::prepare_world_normal(
|
||||
in.world_normal,
|
||||
(pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u,
|
||||
is_front,
|
||||
);
|
||||
|
||||
// N (normal vector)
|
||||
#ifdef LOAD_PREPASS_NORMALS
|
||||
pbr_input.N = bevy_pbr::prepass_utils::prepass_normal(in.position, 0u);
|
||||
#ifdef PREPASS_PIPELINE
|
||||
// write the gbuffer, lighting pass id, and optionally normal and motion_vector textures
|
||||
let out = deferred_output(in, pbr_input);
|
||||
#else
|
||||
pbr_input.N = pbr_functions::apply_normal_mapping(
|
||||
pbr_bindings::material.flags,
|
||||
pbr_input.world_normal,
|
||||
#ifdef VERTEX_TANGENTS
|
||||
#ifdef STANDARDMATERIAL_NORMAL_MAP
|
||||
in.world_tangent,
|
||||
#endif
|
||||
#endif
|
||||
#ifdef VERTEX_UVS
|
||||
uv,
|
||||
#endif
|
||||
view.mip_bias,
|
||||
);
|
||||
#endif
|
||||
|
||||
// V (view vector)
|
||||
pbr_input.V = V;
|
||||
|
||||
} else { // if UNLIT_BIT != 0
|
||||
#ifdef DEFERRED_PREPASS
|
||||
// in deferred mode, we need to fill some of the pbr input data even for unlit materials
|
||||
// to pass through the gbuffer to the deferred lighting shader
|
||||
pbr_input = pbr_types::pbr_input_new();
|
||||
pbr_input.flags = mesh[in.instance_index].flags;
|
||||
pbr_input.material.flags = pbr_bindings::material.flags;
|
||||
pbr_input.world_position = in.world_position;
|
||||
#endif
|
||||
}
|
||||
|
||||
// apply alpha discard
|
||||
// -------------------
|
||||
// note even though this is based on the unlit color, it must be done after all texture samples for uniform control flow
|
||||
unlit_color = pbr_functions::alpha_discard(pbr_bindings::material, unlit_color);
|
||||
pbr_input.material.base_color = unlit_color;
|
||||
|
||||
// generate output
|
||||
// ---------------
|
||||
#ifdef DEFERRED_PREPASS
|
||||
// write the gbuffer
|
||||
out.deferred = deferred_gbuffer_from_pbr_input(pbr_input);
|
||||
out.deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id;
|
||||
#ifdef NORMAL_PREPASS
|
||||
out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0);
|
||||
#endif
|
||||
#ifdef MOTION_VECTOR_PREPASS
|
||||
out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position);
|
||||
#endif // MOTION_VECTOR_PREPASS
|
||||
|
||||
#else // DEFERRED_PREPASS
|
||||
|
||||
// in forward mode, we calculate the lit color immediately, and then apply some post-lighting effects here.
|
||||
// in deferred mode the lit color and these effects will be calculated in the deferred lighting shader
|
||||
var output_color = unlit_color;
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
|
||||
output_color = pbr_functions::pbr(pbr_input);
|
||||
var out: FragmentOutput;
|
||||
if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u {
|
||||
out.color = apply_pbr_lighting(pbr_input);
|
||||
} else {
|
||||
out.color = pbr_input.material.base_color;
|
||||
}
|
||||
|
||||
if (fog.mode != FOG_MODE_OFF && (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) {
|
||||
output_color = pbr_functions::apply_fog(fog, output_color, in.world_position.xyz, view.world_position.xyz);
|
||||
}
|
||||
|
||||
#ifdef TONEMAP_IN_SHADER
|
||||
output_color = tone_mapping(output_color, view.color_grading);
|
||||
#ifdef DEBAND_DITHER
|
||||
var output_rgb = output_color.rgb;
|
||||
output_rgb = powsafe(output_rgb, 1.0 / 2.2);
|
||||
output_rgb = output_rgb + screen_space_dither(in.position.xy);
|
||||
// This conversion back to linear space is required because our output texture format is
|
||||
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
|
||||
output_rgb = powsafe(output_rgb, 2.2);
|
||||
output_color = vec4(output_rgb, output_color.a);
|
||||
// apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr)
|
||||
// note this does not include fullscreen postprocessing effects like bloom.
|
||||
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
|
||||
#endif
|
||||
#endif
|
||||
#ifdef PREMULTIPLY_ALPHA
|
||||
output_color = pbr_functions::premultiply_alpha(pbr_bindings::material.flags, output_color);
|
||||
#endif
|
||||
|
||||
// write the final pixel color
|
||||
out.color = output_color;
|
||||
|
||||
#endif //DEFERRED_PREPASS
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
192
crates/bevy_pbr/src/render/pbr_fragment.wgsl
Normal file
192
crates/bevy_pbr/src/render/pbr_fragment.wgsl
Normal file
|
@ -0,0 +1,192 @@
|
|||
#define_import_path bevy_pbr::pbr_fragment
|
||||
|
||||
#import bevy_pbr::pbr_functions as pbr_functions
|
||||
#import bevy_pbr::pbr_bindings as pbr_bindings
|
||||
#import bevy_pbr::pbr_types as pbr_types
|
||||
#import bevy_pbr::prepass_utils
|
||||
|
||||
#import bevy_pbr::mesh_bindings mesh
|
||||
#import bevy_pbr::mesh_view_bindings view, screen_space_ambient_occlusion_texture
|
||||
#import bevy_pbr::parallax_mapping parallaxed_uv
|
||||
|
||||
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
|
||||
#import bevy_pbr::gtao_utils gtao_multibounce
|
||||
#endif
|
||||
|
||||
#ifdef PREPASS_PIPELINE
|
||||
#import bevy_pbr::prepass_io VertexOutput
|
||||
#else
|
||||
#import bevy_pbr::forward_io VertexOutput
|
||||
#endif
|
||||
|
||||
// prepare a basic PbrInput from the vertex stage output, mesh binding and view binding
|
||||
fn pbr_input_from_vertex_output(
|
||||
in: VertexOutput,
|
||||
is_front: bool,
|
||||
double_sided: bool,
|
||||
) -> pbr_types::PbrInput {
|
||||
var pbr_input: pbr_types::PbrInput = pbr_types::pbr_input_new();
|
||||
|
||||
pbr_input.flags = mesh[in.instance_index].flags;
|
||||
pbr_input.is_orthographic = view.projection[3].w == 1.0;
|
||||
pbr_input.V = pbr_functions::calculate_view(in.world_position, pbr_input.is_orthographic);
|
||||
pbr_input.frag_coord = in.position;
|
||||
pbr_input.world_position = in.world_position;
|
||||
|
||||
#ifdef VERTEX_COLORS
|
||||
pbr_input.material.base_color = in.color;
|
||||
#endif
|
||||
|
||||
pbr_input.world_normal = pbr_functions::prepare_world_normal(
|
||||
in.world_normal,
|
||||
double_sided,
|
||||
is_front,
|
||||
);
|
||||
|
||||
#ifdef LOAD_PREPASS_NORMALS
|
||||
pbr_input.N = bevy_pbr::prepass_utils::prepass_normal(in.position, 0u);
|
||||
#else
|
||||
pbr_input.N = normalize(pbr_input.world_normal);
|
||||
#endif
|
||||
|
||||
return pbr_input;
|
||||
}
|
||||
|
||||
// Prepare a full PbrInput by sampling all textures to resolve
|
||||
// the material members
|
||||
fn pbr_input_from_standard_material(
|
||||
in: VertexOutput,
|
||||
is_front: bool,
|
||||
) -> pbr_types::PbrInput {
|
||||
let double_sided = (pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u;
|
||||
|
||||
var pbr_input: pbr_types::PbrInput = pbr_input_from_vertex_output(in, is_front, double_sided);
|
||||
pbr_input.material.flags = pbr_bindings::material.flags;
|
||||
pbr_input.material.base_color *= pbr_bindings::material.base_color;
|
||||
pbr_input.material.deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id;
|
||||
|
||||
#ifdef VERTEX_UVS
|
||||
var uv = in.uv;
|
||||
|
||||
#ifdef VERTEX_TANGENTS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) {
|
||||
let V = pbr_input.V;
|
||||
let N = in.world_normal;
|
||||
let T = in.world_tangent.xyz;
|
||||
let B = in.world_tangent.w * cross(N, T);
|
||||
// Transform V from fragment to camera in world space to tangent space.
|
||||
let Vt = vec3(dot(V, T), dot(V, B), dot(V, N));
|
||||
uv = parallaxed_uv(
|
||||
pbr_bindings::material.parallax_depth_scale,
|
||||
pbr_bindings::material.max_parallax_layer_count,
|
||||
pbr_bindings::material.max_relief_mapping_search_steps,
|
||||
uv,
|
||||
// Flip the direction of Vt to go toward the surface to make the
|
||||
// parallax mapping algorithm easier to understand and reason
|
||||
// about.
|
||||
-Vt,
|
||||
);
|
||||
}
|
||||
#endif // VERTEX_TANGENTS
|
||||
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) {
|
||||
pbr_input.material.base_color *= textureSampleBias(pbr_bindings::base_color_texture, pbr_bindings::base_color_sampler, uv, view.mip_bias);
|
||||
}
|
||||
#endif // VERTEX_UVS
|
||||
|
||||
pbr_input.material.flags = pbr_bindings::material.flags;
|
||||
|
||||
// NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) {
|
||||
pbr_input.material.reflectance = pbr_bindings::material.reflectance;
|
||||
pbr_input.material.ior = pbr_bindings::material.ior;
|
||||
pbr_input.material.attenuation_color = pbr_bindings::material.attenuation_color;
|
||||
pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance;
|
||||
pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff;
|
||||
|
||||
// emissive
|
||||
// TODO use .a for exposure compensation in HDR
|
||||
var emissive: vec4<f32> = pbr_bindings::material.emissive;
|
||||
#ifdef VERTEX_UVS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) {
|
||||
emissive = vec4<f32>(emissive.rgb * textureSampleBias(pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, view.mip_bias).rgb, 1.0);
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.emissive = emissive;
|
||||
|
||||
// metallic and perceptual roughness
|
||||
var metallic: f32 = pbr_bindings::material.metallic;
|
||||
var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness;
|
||||
#ifdef VERTEX_UVS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) {
|
||||
let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias);
|
||||
// Sampling from GLTF standard channels for now
|
||||
metallic *= metallic_roughness.b;
|
||||
perceptual_roughness *= metallic_roughness.g;
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.metallic = metallic;
|
||||
pbr_input.material.perceptual_roughness = perceptual_roughness;
|
||||
|
||||
var specular_transmission: f32 = pbr_bindings::material.specular_transmission;
|
||||
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) {
|
||||
specular_transmission *= textureSample(pbr_bindings::specular_transmission_texture, pbr_bindings::specular_transmission_sampler, uv).r;
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.specular_transmission = specular_transmission;
|
||||
|
||||
var thickness: f32 = pbr_bindings::material.thickness;
|
||||
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) {
|
||||
thickness *= textureSample(pbr_bindings::thickness_texture, pbr_bindings::thickness_sampler, uv).g;
|
||||
}
|
||||
#endif
|
||||
// scale the thickness by the average length of basis vectors for the transform matrix
|
||||
// this is a rough way to approximate the average “scale” applied to the mesh as a single scalar
|
||||
thickness *= (length(mesh[in.instance_index].model[0].xyz) + length(mesh[in.instance_index].model[1].xyz) + length(mesh[in.instance_index].model[2].xyz)) / 3.0;
|
||||
pbr_input.material.thickness = thickness;
|
||||
|
||||
var diffuse_transmission = pbr_bindings::material.diffuse_transmission;
|
||||
#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) {
|
||||
diffuse_transmission *= textureSample(pbr_bindings::diffuse_transmission_texture, pbr_bindings::diffuse_transmission_sampler, uv).a;
|
||||
}
|
||||
#endif
|
||||
pbr_input.material.diffuse_transmission = diffuse_transmission;
|
||||
|
||||
// occlusion
|
||||
// TODO: Split into diffuse/specular occlusion?
|
||||
var occlusion: vec3<f32> = vec3(1.0);
|
||||
#ifdef VERTEX_UVS
|
||||
if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) {
|
||||
occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r);
|
||||
}
|
||||
#endif
|
||||
#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION
|
||||
let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2<i32>(in.position.xy), 0i).r;
|
||||
let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb);
|
||||
occlusion = min(occlusion, ssao_multibounce);
|
||||
#endif
|
||||
pbr_input.occlusion = occlusion;
|
||||
|
||||
// N (normal vector)
|
||||
#ifndef LOAD_PREPASS_NORMALS
|
||||
pbr_input.N = pbr_functions::apply_normal_mapping(
|
||||
pbr_bindings::material.flags,
|
||||
pbr_input.world_normal,
|
||||
#ifdef VERTEX_TANGENTS
|
||||
#ifdef STANDARDMATERIAL_NORMAL_MAP
|
||||
in.world_tangent,
|
||||
#endif
|
||||
#endif
|
||||
#ifdef VERTEX_UVS
|
||||
uv,
|
||||
#endif
|
||||
view.mip_bias,
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
return pbr_input;
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
#ifdef ENVIRONMENT_MAP
|
||||
#import bevy_pbr::environment_map
|
||||
#endif
|
||||
#import bevy_core_pipeline::tonemapping screen_space_dither, powsafe, tone_mapping
|
||||
|
||||
#import bevy_pbr::utils E
|
||||
#import bevy_pbr::mesh_types MESH_FLAGS_SHADOW_RECEIVER_BIT, MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT
|
||||
|
@ -135,7 +136,7 @@ fn calculate_view(
|
|||
}
|
||||
|
||||
#ifndef PREPASS_FRAGMENT
|
||||
fn pbr(
|
||||
fn apply_pbr_lighting(
|
||||
in: pbr_types::PbrInput,
|
||||
) -> vec4<f32> {
|
||||
var output_color: vec4<f32> = in.material.base_color;
|
||||
|
@ -377,7 +378,6 @@ fn pbr(
|
|||
}
|
||||
#endif // PREPASS_FRAGMENT
|
||||
|
||||
#ifndef PREPASS_FRAGMENT
|
||||
fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4<f32>, fragment_world_position: vec3<f32>, view_world_position: vec3<f32>) -> vec4<f32> {
|
||||
let view_to_world = fragment_world_position.xyz - view_world_position.xyz;
|
||||
|
||||
|
@ -415,7 +415,6 @@ fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4<f32>, fragment_
|
|||
return input_color;
|
||||
}
|
||||
}
|
||||
#endif // PREPASS_FRAGMENT
|
||||
|
||||
#ifdef PREMULTIPLY_ALPHA
|
||||
fn premultiply_alpha(standard_material_flags: u32, color: vec4<f32>) -> vec4<f32> {
|
||||
|
@ -468,3 +467,34 @@ fn premultiply_alpha(standard_material_flags: u32, color: vec4<f32>) -> vec4<f32
|
|||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// fog, alpha premultiply
|
||||
// for non-hdr cameras, tonemapping and debanding
|
||||
fn main_pass_post_lighting_processing(
|
||||
pbr_input: pbr_types::PbrInput,
|
||||
input_color: vec4<f32>,
|
||||
) -> vec4<f32> {
|
||||
var output_color = input_color;
|
||||
|
||||
// fog
|
||||
if (view_bindings::fog.mode != mesh_view_types::FOG_MODE_OFF && (pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) {
|
||||
output_color = apply_fog(view_bindings::fog, output_color, pbr_input.world_position.xyz, view_bindings::view.world_position.xyz);
|
||||
}
|
||||
|
||||
#ifdef TONEMAP_IN_SHADER
|
||||
output_color = tone_mapping(output_color, view_bindings::view.color_grading);
|
||||
#ifdef DEBAND_DITHER
|
||||
var output_rgb = output_color.rgb;
|
||||
output_rgb = powsafe(output_rgb, 1.0 / 2.2);
|
||||
output_rgb += screen_space_dither(pbr_input.frag_coord.xy);
|
||||
// This conversion back to linear space is required because our output texture format is
|
||||
// SRGB; the GPU will assume our output is linear and will apply an SRGB conversion.
|
||||
output_rgb = powsafe(output_rgb, 2.2);
|
||||
output_color = vec4(output_rgb, output_color.a);
|
||||
#endif
|
||||
#endif
|
||||
#ifdef PREMULTIPLY_ALPHA
|
||||
output_color = premultiply_alpha(pbr_input.material.flags, output_color);
|
||||
#endif
|
||||
return output_color;
|
||||
}
|
||||
|
|
|
@ -413,7 +413,7 @@ fn fetch_transmissive_background(offset_position: vec2<f32>, frag_coord: vec3<f3
|
|||
// Calculate final offset position, with blur and spiral offset
|
||||
let modified_offset_position = offset_position + rotated_spiral_offset * blur_intensity * (1.0 - f32(pixel_checkboard) * 0.1);
|
||||
|
||||
#ifndef DEPTH_PREPASS
|
||||
#ifdef DEPTH_PREPASS
|
||||
#ifndef WEBGL2
|
||||
// Use depth prepass data to reject values that are in front of the current fragment
|
||||
if prepass_utils::prepass_depth(vec4<f32>(modified_offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z {
|
||||
|
|
|
@ -8,7 +8,7 @@ pub use type_data::*;
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{self as bevy_reflect, DynamicTupleStruct};
|
||||
use crate::{self as bevy_reflect, DynamicTupleStruct, Struct};
|
||||
use crate::{
|
||||
serde::{ReflectSerializer, UntypedReflectDeserializer},
|
||||
type_registry::TypeRegistry,
|
||||
|
@ -94,8 +94,10 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "cannot get type info for bevy_reflect::DynamicStruct")]
|
||||
fn unproxied_dynamic_should_not_serialize() {
|
||||
#[should_panic(
|
||||
expected = "cannot serialize dynamic value without represented type: bevy_reflect::DynamicStruct"
|
||||
)]
|
||||
fn should_not_serialize_unproxied_dynamic() {
|
||||
let registry = TypeRegistry::default();
|
||||
|
||||
let mut value = DynamicStruct::default();
|
||||
|
@ -104,4 +106,36 @@ mod tests {
|
|||
let serializer = ReflectSerializer::new(&value, ®istry);
|
||||
ron::ser::to_string(&serializer).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_roundtrip_proxied_dynamic() {
|
||||
#[derive(Reflect)]
|
||||
struct TestStruct {
|
||||
a: i32,
|
||||
b: i32,
|
||||
}
|
||||
|
||||
let mut registry = TypeRegistry::default();
|
||||
registry.register::<TestStruct>();
|
||||
|
||||
let value: DynamicStruct = TestStruct { a: 123, b: 456 }.clone_dynamic();
|
||||
|
||||
let serializer = ReflectSerializer::new(&value, ®istry);
|
||||
|
||||
let expected = r#"{"bevy_reflect::serde::tests::TestStruct":(a:123,b:456)}"#;
|
||||
let result = ron::ser::to_string(&serializer).unwrap();
|
||||
assert_eq!(expected, result);
|
||||
|
||||
let mut deserializer = ron::de::Deserializer::from_str(&result).unwrap();
|
||||
let reflect_deserializer = UntypedReflectDeserializer::new(®istry);
|
||||
|
||||
let expected = value.clone_value();
|
||||
let result = reflect_deserializer
|
||||
.deserialize(&mut deserializer)
|
||||
.unwrap()
|
||||
.take::<DynamicStruct>()
|
||||
.unwrap();
|
||||
|
||||
assert!(expected.reflect_partial_eq(&result).unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,22 @@ impl<'a> Serialize for ReflectSerializer<'a> {
|
|||
{
|
||||
let mut state = serializer.serialize_map(Some(1))?;
|
||||
state.serialize_entry(
|
||||
self.value.reflect_type_path(),
|
||||
self.value
|
||||
.get_represented_type_info()
|
||||
.ok_or_else(|| {
|
||||
if self.value.is_dynamic() {
|
||||
Error::custom(format_args!(
|
||||
"cannot serialize dynamic value without represented type: {}",
|
||||
self.value.reflect_type_path()
|
||||
))
|
||||
} else {
|
||||
Error::custom(format_args!(
|
||||
"cannot get type info for {}",
|
||||
self.value.reflect_type_path()
|
||||
))
|
||||
}
|
||||
})?
|
||||
.type_path(),
|
||||
&TypedReflectSerializer::new(self.value, self.registry),
|
||||
)?;
|
||||
state.end()
|
||||
|
|
|
@ -183,6 +183,10 @@ impl fmt::Debug for TypePathTable {
|
|||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TypePathVtable")
|
||||
.field("type_path", &self.type_path)
|
||||
.field("short_type_path", &(self.short_type_path)())
|
||||
.field("type_ident", &(self.type_ident)())
|
||||
.field("crate_name", &(self.crate_name)())
|
||||
.field("module_path", &(self.module_path)())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
|
||||
let mut binding_states: Vec<BindingState> = Vec::new();
|
||||
let mut binding_impls = Vec::new();
|
||||
let mut bind_group_entries = Vec::new();
|
||||
let mut binding_layouts = Vec::new();
|
||||
let mut attr_prepared_data_ident = None;
|
||||
|
||||
|
@ -63,6 +62,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new());
|
||||
let converted: #converted_shader_type = self.as_bind_group_shader_type(images);
|
||||
buffer.write(&converted).unwrap();
|
||||
(
|
||||
#binding_index,
|
||||
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
|
||||
&#render_path::render_resource::BufferInitDescriptor {
|
||||
label: None,
|
||||
|
@ -70,6 +71,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
contents: buffer.as_ref(),
|
||||
},
|
||||
))
|
||||
)
|
||||
}});
|
||||
|
||||
binding_layouts.push(quote!{
|
||||
|
@ -85,14 +87,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
}
|
||||
});
|
||||
|
||||
let binding_vec_index = bind_group_entries.len();
|
||||
bind_group_entries.push(quote! {
|
||||
#render_path::render_resource::BindGroupEntry {
|
||||
binding: #binding_index,
|
||||
resource: bindings[#binding_vec_index].get_binding(),
|
||||
}
|
||||
});
|
||||
|
||||
let required_len = binding_index as usize + 1;
|
||||
if required_len > binding_states.len() {
|
||||
binding_states.resize(required_len, BindingState::Free);
|
||||
|
@ -164,13 +158,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
_ => {
|
||||
// only populate bind group entries for non-uniforms
|
||||
// uniform entries are deferred until the end
|
||||
let binding_vec_index = bind_group_entries.len();
|
||||
bind_group_entries.push(quote! {
|
||||
#render_path::render_resource::BindGroupEntry {
|
||||
binding: #binding_index,
|
||||
resource: bindings[#binding_vec_index].get_binding(),
|
||||
}
|
||||
});
|
||||
BindingState::Occupied {
|
||||
binding_type,
|
||||
ident: field_name,
|
||||
|
@ -230,15 +217,20 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
|
||||
if buffer {
|
||||
binding_impls.push(quote! {
|
||||
(
|
||||
#binding_index,
|
||||
#render_path::render_resource::OwnedBindingResource::Buffer({
|
||||
self.#field_name.clone()
|
||||
})
|
||||
)
|
||||
});
|
||||
} else {
|
||||
binding_impls.push(quote! {{
|
||||
use #render_path::render_resource::AsBindGroupShaderType;
|
||||
let mut buffer = #render_path::render_resource::encase::StorageBuffer::new(Vec::new());
|
||||
buffer.write(&self.#field_name).unwrap();
|
||||
(
|
||||
#binding_index,
|
||||
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
|
||||
&#render_path::render_resource::BufferInitDescriptor {
|
||||
label: None,
|
||||
|
@ -246,6 +238,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
contents: buffer.as_ref(),
|
||||
},
|
||||
))
|
||||
)
|
||||
}});
|
||||
}
|
||||
|
||||
|
@ -276,6 +269,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
let fallback_image = get_fallback_image(&render_path, *dimension);
|
||||
|
||||
binding_impls.push(quote! {
|
||||
(
|
||||
#binding_index,
|
||||
#render_path::render_resource::OwnedBindingResource::TextureView({
|
||||
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
|
||||
if let Some(handle) = handle {
|
||||
|
@ -284,6 +279,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
#fallback_image.texture_view.clone()
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
binding_layouts.push(quote! {
|
||||
|
@ -315,6 +311,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
let fallback_image = get_fallback_image(&render_path, *dimension);
|
||||
|
||||
binding_impls.push(quote! {
|
||||
(
|
||||
#binding_index,
|
||||
#render_path::render_resource::OwnedBindingResource::Sampler({
|
||||
let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into();
|
||||
if let Some(handle) = handle {
|
||||
|
@ -323,6 +321,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
#fallback_image.sampler.clone()
|
||||
}
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
binding_layouts.push(quote!{
|
||||
|
@ -340,17 +339,12 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
|
||||
// Produce impls for fields with uniform bindings
|
||||
let struct_name = &ast.ident;
|
||||
let struct_name_literal = struct_name.to_string();
|
||||
let struct_name_literal = struct_name_literal.as_str();
|
||||
let mut field_struct_impls = Vec::new();
|
||||
for (binding_index, binding_state) in binding_states.iter().enumerate() {
|
||||
let binding_index = binding_index as u32;
|
||||
if let BindingState::OccupiedMergeableUniform { uniform_fields } = binding_state {
|
||||
let binding_vec_index = bind_group_entries.len();
|
||||
bind_group_entries.push(quote! {
|
||||
#render_path::render_resource::BindGroupEntry {
|
||||
binding: #binding_index,
|
||||
resource: bindings[#binding_vec_index].get_binding(),
|
||||
}
|
||||
});
|
||||
// single field uniform bindings for a given index can use a straightforward binding
|
||||
if uniform_fields.len() == 1 {
|
||||
let field = &uniform_fields[0];
|
||||
|
@ -359,6 +353,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
binding_impls.push(quote! {{
|
||||
let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new());
|
||||
buffer.write(&self.#field_name).unwrap();
|
||||
(
|
||||
#binding_index,
|
||||
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
|
||||
&#render_path::render_resource::BufferInitDescriptor {
|
||||
label: None,
|
||||
|
@ -366,6 +362,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
contents: buffer.as_ref(),
|
||||
},
|
||||
))
|
||||
)
|
||||
}});
|
||||
|
||||
binding_layouts.push(quote!{
|
||||
|
@ -402,6 +399,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
buffer.write(&#uniform_struct_name {
|
||||
#(#field_name: &self.#field_name,)*
|
||||
}).unwrap();
|
||||
(
|
||||
#binding_index,
|
||||
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
|
||||
&#render_path::render_resource::BufferInitDescriptor {
|
||||
label: None,
|
||||
|
@ -409,6 +408,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
contents: buffer.as_ref(),
|
||||
},
|
||||
))
|
||||
)
|
||||
}});
|
||||
|
||||
binding_layouts.push(quote!{
|
||||
|
@ -443,36 +443,28 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
|
|||
|
||||
impl #impl_generics #render_path::render_resource::AsBindGroup for #struct_name #ty_generics #where_clause {
|
||||
type Data = #prepared_data;
|
||||
fn as_bind_group(
|
||||
|
||||
fn label() -> Option<&'static str> {
|
||||
Some(#struct_name_literal)
|
||||
}
|
||||
|
||||
fn unprepared_bind_group(
|
||||
&self,
|
||||
layout: &#render_path::render_resource::BindGroupLayout,
|
||||
render_device: &#render_path::renderer::RenderDevice,
|
||||
images: &#render_path::render_asset::RenderAssets<#render_path::texture::Image>,
|
||||
fallback_image: &#render_path::texture::FallbackImage,
|
||||
) -> Result<#render_path::render_resource::PreparedBindGroup<Self::Data>, #render_path::render_resource::AsBindGroupError> {
|
||||
) -> Result<#render_path::render_resource::UnpreparedBindGroup<Self::Data>, #render_path::render_resource::AsBindGroupError> {
|
||||
let bindings = vec![#(#binding_impls,)*];
|
||||
|
||||
let bind_group = {
|
||||
let descriptor = #render_path::render_resource::BindGroupDescriptor {
|
||||
entries: &[#(#bind_group_entries,)*],
|
||||
label: None,
|
||||
layout: &layout,
|
||||
};
|
||||
render_device.create_bind_group(&descriptor)
|
||||
};
|
||||
|
||||
Ok(#render_path::render_resource::PreparedBindGroup {
|
||||
Ok(#render_path::render_resource::UnpreparedBindGroup {
|
||||
bindings,
|
||||
bind_group,
|
||||
data: #get_prepared_data,
|
||||
})
|
||||
}
|
||||
|
||||
fn bind_group_layout(render_device: &#render_path::renderer::RenderDevice) -> #render_path::render_resource::BindGroupLayout {
|
||||
render_device.create_bind_group_layout(&#render_path::render_resource::BindGroupLayoutDescriptor {
|
||||
entries: &[#(#binding_layouts,)*],
|
||||
label: None,
|
||||
})
|
||||
fn bind_group_layout_entries(render_device: &#render_path::renderer::RenderDevice) -> Vec<#render_path::render_resource::BindGroupLayoutEntry> {
|
||||
vec![#(#binding_layouts,)*]
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
|
|
@ -39,7 +39,7 @@ fn extract_frame_count(mut commands: Commands, frame_count: Extract<Res<FrameCou
|
|||
}
|
||||
|
||||
fn extract_time(mut commands: Commands, time: Extract<Res<Time>>) {
|
||||
commands.insert_resource(time.clone());
|
||||
commands.insert_resource(**time);
|
||||
}
|
||||
|
||||
/// Contains global values useful when writing shaders.
|
||||
|
|
|
@ -92,19 +92,19 @@ pub enum RenderSet {
|
|||
/// Queue drawable entities as phase items in [`RenderPhase`](crate::render_phase::RenderPhase)s
|
||||
/// ready for sorting
|
||||
Queue,
|
||||
/// A sub-set within Queue where mesh entity queue systems are executed. Ensures `prepare_assets::<Mesh>` is completed.
|
||||
/// A sub-set within [`Queue`](RenderSet::Queue) where mesh entity queue systems are executed. Ensures `prepare_assets::<Mesh>` is completed.
|
||||
QueueMeshes,
|
||||
// TODO: This could probably be moved in favor of a system ordering abstraction in Render or Queue
|
||||
// TODO: This could probably be moved in favor of a system ordering abstraction in `Render` or `Queue`
|
||||
/// Sort the [`RenderPhases`](render_phase::RenderPhase) here.
|
||||
PhaseSort,
|
||||
/// Prepare render resources from extracted data for the GPU based on their sorted order.
|
||||
/// Create [`BindGroups`](crate::render_resource::BindGroup) that depend on those data.
|
||||
Prepare,
|
||||
/// A sub-set within Prepare for initializing buffers, textures and uniforms for use in bind groups.
|
||||
/// A sub-set within [`Prepare`](RenderSet::Prepare) for initializing buffers, textures and uniforms for use in bind groups.
|
||||
PrepareResources,
|
||||
/// The copy of [`apply_deferred`] that runs between [`PrepareResources`](RenderSet::PrepareResources) and ['PrepareBindGroups'](RenderSet::PrepareBindGroups).
|
||||
PrepareResourcesFlush,
|
||||
/// A sub-set within Prepare for constructing bind groups, or other data that relies on render resources prepared in [`PrepareResources`](RenderSet::PrepareResources).
|
||||
/// A sub-set within [`Prepare`](RenderSet::Prepare) for constructing bind groups, or other data that relies on render resources prepared in [`PrepareResources`](RenderSet::PrepareResources).
|
||||
PrepareBindGroups,
|
||||
/// The copy of [`apply_deferred`] that runs immediately after [`Prepare`](RenderSet::Prepare).
|
||||
PrepareFlush,
|
||||
|
@ -127,7 +127,7 @@ impl Render {
|
|||
/// Sets up the base structure of the rendering [`Schedule`].
|
||||
///
|
||||
/// The sets defined in this enum are configured to run in order,
|
||||
/// and a copy of [`apply_deferred`] is inserted into each `*Flush` set.
|
||||
/// and a copy of [`apply_deferred`] is inserted into each [`*Flush` set](RenderSet).
|
||||
pub fn base_schedule() -> Schedule {
|
||||
use RenderSet::*;
|
||||
|
||||
|
|
|
@ -9,7 +9,10 @@ use crate::{
|
|||
pub use bevy_render_macros::AsBindGroup;
|
||||
use encase::ShaderType;
|
||||
use std::ops::Deref;
|
||||
use wgpu::BindingResource;
|
||||
use wgpu::{
|
||||
BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry,
|
||||
BindingResource,
|
||||
};
|
||||
|
||||
define_atomic_id!(BindGroupId);
|
||||
render_resource_wrapper!(ErasedBindGroup, wgpu::BindGroup);
|
||||
|
@ -262,6 +265,11 @@ pub trait AsBindGroup {
|
|||
/// Data that will be stored alongside the "prepared" bind group.
|
||||
type Data: Send + Sync;
|
||||
|
||||
/// label
|
||||
fn label() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Creates a bind group for `self` matching the layout defined in [`AsBindGroup::bind_group_layout`].
|
||||
fn as_bind_group(
|
||||
&self,
|
||||
|
@ -269,10 +277,56 @@ pub trait AsBindGroup {
|
|||
render_device: &RenderDevice,
|
||||
images: &RenderAssets<Image>,
|
||||
fallback_image: &FallbackImage,
|
||||
) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError>;
|
||||
) -> Result<PreparedBindGroup<Self::Data>, AsBindGroupError> {
|
||||
let UnpreparedBindGroup { bindings, data } =
|
||||
Self::unprepared_bind_group(self, layout, render_device, images, fallback_image)?;
|
||||
|
||||
let entries = bindings
|
||||
.iter()
|
||||
.map(|(index, binding)| BindGroupEntry {
|
||||
binding: *index,
|
||||
resource: binding.get_binding(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bind_group = render_device.create_bind_group(&BindGroupDescriptor {
|
||||
label: Self::label(),
|
||||
layout,
|
||||
entries: &entries,
|
||||
});
|
||||
|
||||
Ok(PreparedBindGroup {
|
||||
bindings,
|
||||
bind_group,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a vec of (binding index, `OwnedBindingResource`).
|
||||
/// In cases where `OwnedBindingResource` is not available (as for bindless texture arrays currently),
|
||||
/// an implementor may define `as_bind_group` directly. This may prevent certain features
|
||||
/// from working correctly.
|
||||
fn unprepared_bind_group(
|
||||
&self,
|
||||
layout: &BindGroupLayout,
|
||||
render_device: &RenderDevice,
|
||||
images: &RenderAssets<Image>,
|
||||
fallback_image: &FallbackImage,
|
||||
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError>;
|
||||
|
||||
/// Creates the bind group layout matching all bind groups returned by [`AsBindGroup::as_bind_group`]
|
||||
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
label: Self::label(),
|
||||
entries: &Self::bind_group_layout_entries(render_device),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a vec of bind group layout entries
|
||||
fn bind_group_layout_entries(render_device: &RenderDevice) -> Vec<BindGroupLayoutEntry>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
@ -285,14 +339,21 @@ pub enum AsBindGroupError {
|
|||
|
||||
/// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`].
|
||||
pub struct PreparedBindGroup<T> {
|
||||
pub bindings: Vec<OwnedBindingResource>,
|
||||
pub bindings: Vec<(u32, OwnedBindingResource)>,
|
||||
pub bind_group: BindGroup,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
/// a map containing `OwnedBindingResource`s, keyed by the target binding index
|
||||
pub struct UnpreparedBindGroup<T> {
|
||||
pub bindings: Vec<(u32, OwnedBindingResource)>,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
/// An owned binding resource of any type (ex: a [`Buffer`], [`TextureView`], etc).
|
||||
/// This is used by types like [`PreparedBindGroup`] to hold a single list of all
|
||||
/// render resources used by bindings.
|
||||
#[derive(Debug)]
|
||||
pub enum OwnedBindingResource {
|
||||
Buffer(Buffer),
|
||||
TextureView(TextureView),
|
||||
|
|
|
@ -344,7 +344,9 @@ pub fn prepare_windows(
|
|||
.enumerate_adapters(wgpu::Backends::VULKAN)
|
||||
.any(|adapter| {
|
||||
let name = adapter.get_info().name;
|
||||
name.starts_with("AMD") || name.starts_with("Intel")
|
||||
name.starts_with("Radeon")
|
||||
|| name.starts_with("AMD")
|
||||
|| name.starts_with("Intel")
|
||||
})
|
||||
};
|
||||
|
||||
|
|
|
@ -29,10 +29,10 @@ impl FromWorld for SceneLoader {
|
|||
#[non_exhaustive]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SceneLoaderError {
|
||||
/// An [IO](std::io) Error
|
||||
#[error("Could load shader: {0}")]
|
||||
/// An [IO Error](std::io::Error)
|
||||
#[error("Error while trying to read the scene file: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// A [RON](ron) Error
|
||||
/// A [RON Error](ron::error::SpannedError)
|
||||
#[error("Could not parse RON: {0}")]
|
||||
RonSpannedError(#[from] ron::error::SpannedError),
|
||||
}
|
||||
|
|
|
@ -462,7 +462,7 @@ pub struct Material2dBindGroupId(Option<BindGroupId>);
|
|||
|
||||
/// Data prepared for a [`Material2d`] instance.
|
||||
pub struct PreparedMaterial2d<T: Material2d> {
|
||||
pub bindings: Vec<OwnedBindingResource>,
|
||||
pub bindings: Vec<(u32, OwnedBindingResource)>,
|
||||
pub bind_group: BindGroup,
|
||||
pub key: T::Data,
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
use crate::{fixed_timestep::FixedTime, Time, Timer, TimerMode};
|
||||
use crate::{Time, Timer, TimerMode};
|
||||
use bevy_ecs::system::Res;
|
||||
use bevy_utils::Duration;
|
||||
|
||||
/// Run condition that is active on a regular time interval, using [`Time`] to advance
|
||||
/// the timer.
|
||||
///
|
||||
/// If used for a fixed timestep system, use [`on_fixed_timer`] instead.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, Update};
|
||||
/// # use bevy_ecs::schedule::IntoSystemConfigs;
|
||||
|
@ -40,40 +38,6 @@ pub fn on_timer(duration: Duration) -> impl FnMut(Res<Time>) -> bool + Clone {
|
|||
}
|
||||
}
|
||||
|
||||
/// Run condition that is active on a regular time interval, using [`FixedTime`] to
|
||||
/// advance the timer.
|
||||
///
|
||||
/// If used for a non-fixed timestep system, use [`on_timer`] instead.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, FixedUpdate};
|
||||
/// # use bevy_ecs::schedule::IntoSystemConfigs;
|
||||
/// # use bevy_utils::Duration;
|
||||
/// # use bevy_time::common_conditions::on_fixed_timer;
|
||||
/// fn main() {
|
||||
/// App::new()
|
||||
/// .add_plugins(DefaultPlugins)
|
||||
/// .add_systems(FixedUpdate,
|
||||
/// tick.run_if(on_fixed_timer(Duration::from_secs(1))),
|
||||
/// )
|
||||
/// .run();
|
||||
/// }
|
||||
/// fn tick() {
|
||||
/// // ran once a second
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note that this run condition may not behave as expected if `duration` is smaller
|
||||
/// than the fixed timestep period, since the timer may complete multiple times in
|
||||
/// one fixed update.
|
||||
pub fn on_fixed_timer(duration: Duration) -> impl FnMut(Res<FixedTime>) -> bool + Clone {
|
||||
let mut timer = Timer::new(duration, TimerMode::Repeating);
|
||||
move |time: Res<FixedTime>| {
|
||||
timer.tick(time.period);
|
||||
timer.just_finished()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -85,9 +49,7 @@ mod tests {
|
|||
#[test]
|
||||
fn distributive_run_if_compiles() {
|
||||
Schedule::default().add_systems(
|
||||
(test_system, test_system)
|
||||
.distributive_run_if(on_timer(Duration::new(1, 0)))
|
||||
.distributive_run_if(on_fixed_timer(Duration::new(1, 0))),
|
||||
(test_system, test_system).distributive_run_if(on_timer(Duration::new(1, 0))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
343
crates/bevy_time/src/fixed.rs
Normal file
343
crates/bevy_time/src/fixed.rs
Normal file
|
@ -0,0 +1,343 @@
|
|||
use bevy_ecs::world::World;
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_utils::Duration;
|
||||
|
||||
use crate::{time::Time, virt::Virtual, FixedUpdate};
|
||||
|
||||
/// The fixed timestep game clock following virtual time.
|
||||
///
|
||||
/// A specialization of the [`Time`] structure. **For method documentation, see
|
||||
/// [`Time<Fixed>#impl-Time<Fixed>`].**
|
||||
///
|
||||
/// It is automatically inserted as a resource by
|
||||
/// [`TimePlugin`](crate::TimePlugin) and updated based on
|
||||
/// [`Time<Virtual>`](Virtual). The fixed clock is automatically set as the
|
||||
/// generic [`Time`] resource during [`FixedUpdate`] schedule processing.
|
||||
///
|
||||
/// The fixed timestep clock advances in fixed-size increments, which is
|
||||
/// extremely useful for writing logic (like physics) that should have
|
||||
/// consistent behavior, regardless of framerate.
|
||||
///
|
||||
/// The default [`timestep()`](Time::timestep) is 64 hertz, or 15625
|
||||
/// microseconds. This value was chosen because using 60 hertz has the potential
|
||||
/// for a pathological interaction with the monitor refresh rate where the game
|
||||
/// alternates between running two fixed timesteps and zero fixed timesteps per
|
||||
/// frame (for example when running two fixed timesteps takes longer than a
|
||||
/// frame). Additionally, the value is a power of two which losslessly converts
|
||||
/// into [`f32`] and [`f64`].
|
||||
///
|
||||
/// To run a system on a fixed timestep, add it to the [`FixedUpdate`] schedule.
|
||||
/// This schedule is run a number of times between
|
||||
/// [`PreUpdate`](bevy_app::PreUpdate) and [`Update`](bevy_app::Update)
|
||||
/// according to the accumulated [`overstep()`](Time::overstep) time divided by
|
||||
/// the [`timestep()`](Time::timestep). This means the schedule may run 0, 1 or
|
||||
/// more times during a single update (which typically corresponds to a rendered
|
||||
/// frame).
|
||||
///
|
||||
/// `Time<Fixed>` and the generic [`Time`] resource will report a
|
||||
/// [`delta()`](Time::delta) equal to [`timestep()`](Time::timestep) and always
|
||||
/// grow [`elapsed()`](Time::elapsed) by one [`timestep()`](Time::timestep) per
|
||||
/// iteration.
|
||||
///
|
||||
/// The fixed timestep clock follows the [`Time<Virtual>`](Virtual) clock, which
|
||||
/// means it is affected by [`pause()`](Time::pause),
|
||||
/// [`set_relative_speed()`](Time::set_relative_speed) and
|
||||
/// [`set_max_delta()`](Time::set_max_delta) from virtual time. If the virtual
|
||||
/// clock is paused, the [`FixedUpdate`] schedule will not run. It is guaranteed
|
||||
/// that the [`elapsed()`](Time::elapsed) time in `Time<Fixed>` is always
|
||||
/// between the previous `elapsed()` and the current `elapsed()` value in
|
||||
/// `Time<Virtual>`, so the values are compatible.
|
||||
///
|
||||
/// Changing the timestep size while the game is running should not normally be
|
||||
/// done, as having a regular interval is the point of this schedule, but it may
|
||||
/// be necessary for effects like "bullet-time" if the normal granularity of the
|
||||
/// fixed timestep is too big for the slowed down time. In this case,
|
||||
/// [`set_timestep()`](Time::set_timestep) and be called to set a new value. The
|
||||
/// new value will be used immediately for the next run of the [`FixedUpdate`]
|
||||
/// schedule, meaning that it will affect the [`delta()`](Time::delta) value for
|
||||
/// the very next [`FixedUpdate`], even if it is still during the same frame.
|
||||
/// Any [`overstep()`](Time::overstep) present in the accumulator will be
|
||||
/// processed according to the new [`timestep()`](Time::timestep) value.
|
||||
#[derive(Debug, Copy, Clone, Reflect)]
|
||||
pub struct Fixed {
|
||||
timestep: Duration,
|
||||
overstep: Duration,
|
||||
}
|
||||
|
||||
impl Time<Fixed> {
|
||||
/// Corresponds to 64 Hz.
|
||||
const DEFAULT_TIMESTEP: Duration = Duration::from_micros(15625);
|
||||
|
||||
/// Return new fixed time clock with given timestep as [`Duration`]
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `timestep` is zero.
|
||||
pub fn from_duration(timestep: Duration) -> Self {
|
||||
let mut ret = Self::default();
|
||||
ret.set_timestep(timestep);
|
||||
ret
|
||||
}
|
||||
|
||||
/// Return new fixed time clock with given timestep seconds as `f64`
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `seconds` is zero, negative or not finite.
|
||||
pub fn from_seconds(seconds: f64) -> Self {
|
||||
let mut ret = Self::default();
|
||||
ret.set_timestep_seconds(seconds);
|
||||
ret
|
||||
}
|
||||
|
||||
/// Return new fixed time clock with given timestep frequency in Hertz (1/seconds)
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `hz` is zero, negative or not finite.
|
||||
pub fn from_hz(hz: f64) -> Self {
|
||||
let mut ret = Self::default();
|
||||
ret.set_timestep_hz(hz);
|
||||
ret
|
||||
}
|
||||
|
||||
/// Returns the amount of virtual time that must pass before the fixed
|
||||
/// timestep schedule is run again.
|
||||
#[inline]
|
||||
pub fn timestep(&self) -> Duration {
|
||||
self.context().timestep
|
||||
}
|
||||
|
||||
/// Sets the amount of virtual time that must pass before the fixed timestep
|
||||
/// schedule is run again, as [`Duration`].
|
||||
///
|
||||
/// Takes effect immediately on the next run of the schedule, respecting
|
||||
/// what is currently in [`Self::overstep`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `timestep` is zero.
|
||||
#[inline]
|
||||
pub fn set_timestep(&mut self, timestep: Duration) {
|
||||
assert_ne!(
|
||||
timestep,
|
||||
Duration::ZERO,
|
||||
"attempted to set fixed timestep to zero"
|
||||
);
|
||||
self.context_mut().timestep = timestep;
|
||||
}
|
||||
|
||||
/// Sets the amount of virtual time that must pass before the fixed timestep
|
||||
/// schedule is run again, as seconds.
|
||||
///
|
||||
/// Timestep is stored as a [`Duration`], which has fixed nanosecond
|
||||
/// resolution and will be converted from the floating point number.
|
||||
///
|
||||
/// Takes effect immediately on the next run of the schedule, respecting
|
||||
/// what is currently in [`Self::overstep`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `seconds` is zero, negative or not finite.
|
||||
#[inline]
|
||||
pub fn set_timestep_seconds(&mut self, seconds: f64) {
|
||||
assert!(
|
||||
seconds.is_sign_positive(),
|
||||
"seconds less than or equal to zero"
|
||||
);
|
||||
assert!(seconds.is_finite(), "seconds is infinite");
|
||||
self.set_timestep(Duration::from_secs_f64(seconds));
|
||||
}
|
||||
|
||||
/// Sets the amount of virtual time that must pass before the fixed timestep
|
||||
/// schedule is run again, as frequency.
|
||||
///
|
||||
/// The timestep value is set to `1 / hz`, converted to a [`Duration`] which
|
||||
/// has fixed nanosecond resolution.
|
||||
///
|
||||
/// Takes effect immediately on the next run of the schedule, respecting
|
||||
/// what is currently in [`Self::overstep`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `hz` is zero, negative or not finite.
|
||||
#[inline]
|
||||
pub fn set_timestep_hz(&mut self, hz: f64) {
|
||||
assert!(hz.is_sign_positive(), "Hz less than or equal to zero");
|
||||
assert!(hz.is_finite(), "Hz is infinite");
|
||||
self.set_timestep_seconds(1.0 / hz);
|
||||
}
|
||||
|
||||
/// Returns the amount of overstep time accumulated toward new steps, as
|
||||
/// [`Duration`].
|
||||
#[inline]
|
||||
pub fn overstep(&self) -> Duration {
|
||||
self.context().overstep
|
||||
}
|
||||
|
||||
/// Returns the amount of overstep time accumulated toward new steps, as an
|
||||
/// [`f32`] fraction of the timestep.
|
||||
#[inline]
|
||||
pub fn overstep_percentage(&self) -> f32 {
|
||||
self.context().overstep.as_secs_f32() / self.context().timestep.as_secs_f32()
|
||||
}
|
||||
|
||||
/// Returns the amount of overstep time accumulated toward new steps, as an
|
||||
/// [`f64`] fraction of the timestep.
|
||||
#[inline]
|
||||
pub fn overstep_percentage_f64(&self) -> f64 {
|
||||
self.context().overstep.as_secs_f64() / self.context().timestep.as_secs_f64()
|
||||
}
|
||||
|
||||
fn accumulate(&mut self, delta: Duration) {
|
||||
self.context_mut().overstep += delta;
|
||||
}
|
||||
|
||||
fn expend(&mut self) -> bool {
|
||||
let timestep = self.timestep();
|
||||
if let Some(new_value) = self.context_mut().overstep.checked_sub(timestep) {
|
||||
// reduce accumulated and increase elapsed by period
|
||||
self.context_mut().overstep = new_value;
|
||||
self.advance_by(timestep);
|
||||
true
|
||||
} else {
|
||||
// no more periods left in accumulated
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Fixed {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timestep: Time::<Fixed>::DEFAULT_TIMESTEP,
|
||||
overstep: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs [`FixedUpdate`] zero or more times based on delta of
|
||||
/// [`Time<Virtual>`](Virtual) and [`Time::overstep`]
|
||||
pub fn run_fixed_update_schedule(world: &mut World) {
|
||||
let delta = world.resource::<Time<Virtual>>().delta();
|
||||
world.resource_mut::<Time<Fixed>>().accumulate(delta);
|
||||
|
||||
// Run the schedule until we run out of accumulated time
|
||||
let _ = world.try_schedule_scope(FixedUpdate, |world, schedule| {
|
||||
while world.resource_mut::<Time<Fixed>>().expend() {
|
||||
*world.resource_mut::<Time>() = world.resource::<Time<Fixed>>().as_generic();
|
||||
schedule.run(world);
|
||||
}
|
||||
});
|
||||
|
||||
*world.resource_mut::<Time>() = world.resource::<Time<Virtual>>().as_generic();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_set_timestep() {
|
||||
let mut time = Time::<Fixed>::default();
|
||||
|
||||
assert_eq!(time.timestep(), Time::<Fixed>::DEFAULT_TIMESTEP);
|
||||
|
||||
time.set_timestep(Duration::from_millis(500));
|
||||
assert_eq!(time.timestep(), Duration::from_millis(500));
|
||||
|
||||
time.set_timestep_seconds(0.25);
|
||||
assert_eq!(time.timestep(), Duration::from_millis(250));
|
||||
|
||||
time.set_timestep_hz(8.0);
|
||||
assert_eq!(time.timestep(), Duration::from_millis(125));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expend() {
|
||||
let mut time = Time::<Fixed>::from_seconds(2.0);
|
||||
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
|
||||
time.accumulate(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
assert_eq!(time.overstep(), Duration::from_secs(1));
|
||||
assert_eq!(time.overstep_percentage(), 0.5);
|
||||
assert_eq!(time.overstep_percentage_f64(), 0.5);
|
||||
|
||||
assert!(!time.expend()); // false
|
||||
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
assert_eq!(time.overstep(), Duration::from_secs(1));
|
||||
assert_eq!(time.overstep_percentage(), 0.5);
|
||||
assert_eq!(time.overstep_percentage_f64(), 0.5);
|
||||
|
||||
time.accumulate(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
assert_eq!(time.overstep(), Duration::from_secs(2));
|
||||
assert_eq!(time.overstep_percentage(), 1.0);
|
||||
assert_eq!(time.overstep_percentage_f64(), 1.0);
|
||||
|
||||
assert!(time.expend()); // true
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_secs(2));
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
||||
assert_eq!(time.overstep(), Duration::ZERO);
|
||||
assert_eq!(time.overstep_percentage(), 0.0);
|
||||
assert_eq!(time.overstep_percentage_f64(), 0.0);
|
||||
|
||||
assert!(!time.expend()); // false
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_secs(2));
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
||||
assert_eq!(time.overstep(), Duration::ZERO);
|
||||
assert_eq!(time.overstep_percentage(), 0.0);
|
||||
assert_eq!(time.overstep_percentage_f64(), 0.0);
|
||||
|
||||
time.accumulate(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_secs(2));
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
||||
assert_eq!(time.overstep(), Duration::from_secs(1));
|
||||
assert_eq!(time.overstep_percentage(), 0.5);
|
||||
assert_eq!(time.overstep_percentage_f64(), 0.5);
|
||||
|
||||
assert!(!time.expend()); // false
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_secs(2));
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
||||
assert_eq!(time.overstep(), Duration::from_secs(1));
|
||||
assert_eq!(time.overstep_percentage(), 0.5);
|
||||
assert_eq!(time.overstep_percentage_f64(), 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expend_multiple() {
|
||||
let mut time = Time::<Fixed>::from_seconds(2.0);
|
||||
|
||||
time.accumulate(Duration::from_secs(7));
|
||||
assert_eq!(time.overstep(), Duration::from_secs(7));
|
||||
|
||||
assert!(time.expend()); // true
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
||||
assert_eq!(time.overstep(), Duration::from_secs(5));
|
||||
|
||||
assert!(time.expend()); // true
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(4));
|
||||
assert_eq!(time.overstep(), Duration::from_secs(3));
|
||||
|
||||
assert!(time.expend()); // true
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(6));
|
||||
assert_eq!(time.overstep(), Duration::from_secs(1));
|
||||
|
||||
assert!(!time.expend()); // false
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(6));
|
||||
assert_eq!(time.overstep(), Duration::from_secs(1));
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
//! Tools to run systems at a regular interval.
|
||||
//! This can be extremely useful for steady, frame-rate independent gameplay logic and physics.
|
||||
//!
|
||||
//! To run a system on a fixed timestep, add it to the [`FixedUpdate`] [`Schedule`](bevy_ecs::schedule::Schedule).
|
||||
//! This schedule is run in [`RunFixedUpdateLoop`](bevy_app::RunFixedUpdateLoop) near the start of each frame,
|
||||
//! via the [`run_fixed_update_schedule`] exclusive system.
|
||||
//!
|
||||
//! This schedule will be run a number of times each frame,
|
||||
//! equal to the accumulated divided by the period resource, rounded down,
|
||||
//! as tracked in the [`FixedTime`] resource.
|
||||
//! Unused time will be carried over.
|
||||
//!
|
||||
//! This does not guarantee that the time elapsed between executions is exact,
|
||||
//! and systems in this schedule can run 0, 1 or more times on any given frame.
|
||||
//!
|
||||
//! For example, a system with a fixed timestep run criteria of 120 times per second will run
|
||||
//! two times during a ~16.667ms frame, once during a ~8.333ms frame, and once every two frames
|
||||
//! with ~4.167ms frames. However, the same criteria may not result in exactly 8.333ms passing
|
||||
//! between each execution.
|
||||
//!
|
||||
//! When using fixed time steps, it is advised not to rely on [`Time::delta`] or any of it's
|
||||
//! variants for game simulation, but rather use the value of [`FixedTime`] instead.
|
||||
|
||||
use crate::Time;
|
||||
use bevy_app::FixedUpdate;
|
||||
use bevy_ecs::{system::Resource, world::World};
|
||||
use bevy_utils::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
/// The amount of time that must pass before the fixed timestep schedule is run again.
|
||||
///
|
||||
/// For more information, see the [module-level documentation](self).
|
||||
///
|
||||
/// When using bevy's default configuration, this will be updated using the [`Time`]
|
||||
/// resource. To customize how `Time` is updated each frame, see [`TimeUpdateStrategy`].
|
||||
///
|
||||
/// [`TimeUpdateStrategy`]: crate::TimeUpdateStrategy
|
||||
#[derive(Resource, Debug)]
|
||||
pub struct FixedTime {
|
||||
accumulated: Duration,
|
||||
/// The amount of time spanned by each fixed update.
|
||||
/// Defaults to 1/60th of a second.
|
||||
///
|
||||
/// To configure this value, simply mutate or overwrite this field.
|
||||
pub period: Duration,
|
||||
}
|
||||
|
||||
impl FixedTime {
|
||||
/// Creates a new [`FixedTime`] struct with a specified period.
|
||||
pub fn new(period: Duration) -> Self {
|
||||
FixedTime {
|
||||
accumulated: Duration::ZERO,
|
||||
period,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`FixedTime`] struct with a period specified in seconds.
|
||||
pub fn new_from_secs(period: f32) -> Self {
|
||||
FixedTime {
|
||||
accumulated: Duration::ZERO,
|
||||
period: Duration::from_secs_f32(period),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds to this instance's accumulated time. `delta_time` should be the amount of in-game time
|
||||
/// that has passed since `tick` was last called.
|
||||
///
|
||||
/// Note that if you are using the default configuration of bevy, this will be called for you.
|
||||
pub fn tick(&mut self, delta_time: Duration) {
|
||||
self.accumulated += delta_time;
|
||||
}
|
||||
|
||||
/// Returns the current amount of accumulated time.
|
||||
///
|
||||
/// Approximately, this represents how far behind the fixed update schedule is from the main schedule.
|
||||
pub fn accumulated(&self) -> Duration {
|
||||
self.accumulated
|
||||
}
|
||||
|
||||
/// Attempts to advance by a single period. This will return [`FixedUpdateError`] if there is not enough
|
||||
/// accumulated time -- in other words, if advancing time would put the fixed update schedule
|
||||
/// ahead of the main schedule.
|
||||
///
|
||||
/// Note that if you are using the default configuration of bevy, this will be called for you.
|
||||
pub fn expend(&mut self) -> Result<(), FixedUpdateError> {
|
||||
if let Some(new_value) = self.accumulated.checked_sub(self.period) {
|
||||
self.accumulated = new_value;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(FixedUpdateError::NotEnoughTime {
|
||||
accumulated: self.accumulated,
|
||||
period: self.period,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FixedTime {
|
||||
fn default() -> Self {
|
||||
FixedTime {
|
||||
accumulated: Duration::ZERO,
|
||||
period: Duration::from_secs_f32(1. / 60.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned when working with [`FixedTime`].
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FixedUpdateError {
|
||||
/// There is not enough accumulated time to advance the fixed update schedule.
|
||||
#[error("At least one period worth of time must be accumulated.")]
|
||||
NotEnoughTime {
|
||||
/// The amount of time available to advance the fixed update schedule.
|
||||
accumulated: Duration,
|
||||
/// The length of one fixed update.
|
||||
period: Duration,
|
||||
},
|
||||
}
|
||||
|
||||
/// Ticks the [`FixedTime`] resource then runs the [`FixedUpdate`].
|
||||
///
|
||||
/// For more information, see the [module-level documentation](self).
|
||||
pub fn run_fixed_update_schedule(world: &mut World) {
|
||||
// Tick the time
|
||||
let delta_time = world.resource::<Time>().delta();
|
||||
let mut fixed_time = world.resource_mut::<FixedTime>();
|
||||
fixed_time.tick(delta_time);
|
||||
|
||||
// Run the schedule until we run out of accumulated time
|
||||
let _ = world.try_schedule_scope(FixedUpdate, |world, schedule| {
|
||||
while world.resource_mut::<FixedTime>().expend().is_ok() {
|
||||
schedule.run(world);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fixed_time_starts_at_zero() {
|
||||
let new_time = FixedTime::new_from_secs(42.);
|
||||
assert_eq!(new_time.accumulated(), Duration::ZERO);
|
||||
|
||||
let default_time = FixedTime::default();
|
||||
assert_eq!(default_time.accumulated(), Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_time_ticks_up() {
|
||||
let mut fixed_time = FixedTime::default();
|
||||
fixed_time.tick(Duration::from_secs(1));
|
||||
assert_eq!(fixed_time.accumulated(), Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enough_accumulated_time_is_required() {
|
||||
let mut fixed_time = FixedTime::new(Duration::from_secs(2));
|
||||
fixed_time.tick(Duration::from_secs(1));
|
||||
assert!(fixed_time.expend().is_err());
|
||||
assert_eq!(fixed_time.accumulated(), Duration::from_secs(1));
|
||||
|
||||
fixed_time.tick(Duration::from_secs(1));
|
||||
assert!(fixed_time.expend().is_ok());
|
||||
assert_eq!(fixed_time.accumulated(), Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeatedly_expending_time() {
|
||||
let mut fixed_time = FixedTime::new(Duration::from_secs(1));
|
||||
fixed_time.tick(Duration::from_secs_f32(3.2));
|
||||
assert!(fixed_time.expend().is_ok());
|
||||
assert!(fixed_time.expend().is_ok());
|
||||
assert!(fixed_time.expend().is_ok());
|
||||
assert!(fixed_time.expend().is_err());
|
||||
}
|
||||
}
|
|
@ -4,16 +4,20 @@
|
|||
|
||||
/// Common run conditions
|
||||
pub mod common_conditions;
|
||||
pub mod fixed_timestep;
|
||||
mod fixed;
|
||||
mod real;
|
||||
mod stopwatch;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod time;
|
||||
mod timer;
|
||||
mod virt;
|
||||
|
||||
use fixed_timestep::FixedTime;
|
||||
pub use fixed::*;
|
||||
pub use real::*;
|
||||
pub use stopwatch::*;
|
||||
pub use time::*;
|
||||
pub use timer::*;
|
||||
pub use virt::*;
|
||||
|
||||
use bevy_ecs::system::{Res, ResMut};
|
||||
use bevy_utils::{tracing::warn, Duration, Instant};
|
||||
|
@ -23,14 +27,12 @@ use crossbeam_channel::{Receiver, Sender};
|
|||
pub mod prelude {
|
||||
//! The Bevy Time Prelude.
|
||||
#[doc(hidden)]
|
||||
pub use crate::{fixed_timestep::FixedTime, Time, Timer, TimerMode};
|
||||
pub use crate::{Fixed, Real, Time, Timer, TimerMode, Virtual};
|
||||
}
|
||||
|
||||
use bevy_app::{prelude::*, RunFixedUpdateLoop};
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::fixed_timestep::run_fixed_update_schedule;
|
||||
|
||||
/// Adds time functionality to Apps.
|
||||
#[derive(Default)]
|
||||
pub struct TimePlugin;
|
||||
|
@ -43,12 +45,20 @@ pub struct TimeSystem;
|
|||
impl Plugin for TimePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<Time>()
|
||||
.init_resource::<Time<Real>>()
|
||||
.init_resource::<Time<Virtual>>()
|
||||
.init_resource::<Time<Fixed>>()
|
||||
.init_resource::<TimeUpdateStrategy>()
|
||||
.register_type::<Timer>()
|
||||
.register_type::<Time>()
|
||||
.register_type::<Time<Real>>()
|
||||
.register_type::<Time<Virtual>>()
|
||||
.register_type::<Time<Fixed>>()
|
||||
.register_type::<Timer>()
|
||||
.register_type::<Stopwatch>()
|
||||
.init_resource::<FixedTime>()
|
||||
.add_systems(First, time_system.in_set(TimeSystem))
|
||||
.add_systems(
|
||||
First,
|
||||
(time_system, virtual_time_system.after(time_system)).in_set(TimeSystem),
|
||||
)
|
||||
.add_systems(RunFixedUpdateLoop, run_fixed_update_schedule);
|
||||
|
||||
#[cfg(feature = "bevy_ci_testing")]
|
||||
|
@ -68,8 +78,8 @@ impl Plugin for TimePlugin {
|
|||
|
||||
/// Configuration resource used to determine how the time system should run.
|
||||
///
|
||||
/// For most cases, [`TimeUpdateStrategy::Automatic`] is fine. When writing tests, dealing with networking, or similar
|
||||
/// you may prefer to set the next [`Time`] value manually.
|
||||
/// For most cases, [`TimeUpdateStrategy::Automatic`] is fine. When writing tests, dealing with
|
||||
/// networking or similar, you may prefer to set the next [`Time`] value manually.
|
||||
#[derive(Resource, Default)]
|
||||
pub enum TimeUpdateStrategy {
|
||||
/// [`Time`] will be automatically updated each frame using an [`Instant`] sent from the render world via a [`TimeSender`].
|
||||
|
@ -101,10 +111,10 @@ pub fn create_time_channels() -> (TimeSender, TimeReceiver) {
|
|||
(TimeSender(s), TimeReceiver(r))
|
||||
}
|
||||
|
||||
/// The system used to update the [`Time`] used by app logic. If there is a render world the time is sent from
|
||||
/// there to this system through channels. Otherwise the time is updated in this system.
|
||||
/// The system used to update the [`Time`] used by app logic. If there is a render world the time is
|
||||
/// sent from there to this system through channels. Otherwise the time is updated in this system.
|
||||
fn time_system(
|
||||
mut time: ResMut<Time>,
|
||||
mut time: ResMut<Time<Real>>,
|
||||
update_strategy: Res<TimeUpdateStrategy>,
|
||||
time_recv: Option<Res<TimeReceiver>>,
|
||||
mut has_received_time: Local<bool>,
|
||||
|
@ -127,9 +137,6 @@ fn time_system(
|
|||
match update_strategy.as_ref() {
|
||||
TimeUpdateStrategy::Automatic => time.update_with_instant(new_time),
|
||||
TimeUpdateStrategy::ManualInstant(instant) => time.update_with_instant(*instant),
|
||||
TimeUpdateStrategy::ManualDuration(duration) => {
|
||||
let last_update = time.last_update().unwrap_or_else(|| time.startup());
|
||||
time.update_with_instant(last_update + *duration);
|
||||
}
|
||||
TimeUpdateStrategy::ManualDuration(duration) => time.update_with_duration(*duration),
|
||||
}
|
||||
}
|
||||
|
|
220
crates/bevy_time/src/real.rs
Normal file
220
crates/bevy_time/src/real.rs
Normal file
|
@ -0,0 +1,220 @@
|
|||
use bevy_reflect::Reflect;
|
||||
use bevy_utils::{Duration, Instant};
|
||||
|
||||
use crate::time::Time;
|
||||
|
||||
/// Real time clock representing elapsed wall clock time.
|
||||
///
|
||||
/// A specialization of the [`Time`] structure. **For method documentation, see
|
||||
/// [`Time<Real>#impl-Time<Real>`].**
|
||||
///
|
||||
/// It is automatically inserted as a resource by
|
||||
/// [`TimePlugin`](crate::TimePlugin) and updated with time instants according
|
||||
/// to [`TimeUpdateStrategy`](crate::TimeUpdateStrategy).
|
||||
///
|
||||
/// The [`delta()`](Time::delta) and [`elapsed()`](Time::elapsed) values of this
|
||||
/// clock should be used for anything which deals specifically with real time
|
||||
/// (wall clock time). It will not be affected by relative game speed
|
||||
/// adjustments, pausing or other adjustments.
|
||||
///
|
||||
/// The clock does not count time from [`startup()`](Time::startup) to
|
||||
/// [`first_update()`](Time::first_update()) into elapsed, but instead will
|
||||
/// start counting time from the first update call. [`delta()`](Time::delta) and
|
||||
/// [`elapsed()`](Time::elapsed) will report zero on the first update as there
|
||||
/// is no previous update instant. This means that a [`delta()`](Time::delta) of
|
||||
/// zero must be handled without errors in application logic, as it may
|
||||
/// theoretically also happen at other times.
|
||||
///
|
||||
/// [`Instant`](std::time::Instant)s for [`startup()`](Time::startup),
|
||||
/// [`first_update()`](Time::first_update) and
|
||||
/// [`last_update()`](Time::last_update) are recorded and accessible.
|
||||
#[derive(Debug, Copy, Clone, Reflect)]
|
||||
pub struct Real {
|
||||
startup: Instant,
|
||||
first_update: Option<Instant>,
|
||||
last_update: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Default for Real {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
startup: Instant::now(),
|
||||
first_update: None,
|
||||
last_update: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Time<Real> {
|
||||
/// Constructs a new `Time<Real>` instance with a specific startup
|
||||
/// [`Instant`](std::time::Instant).
|
||||
pub fn new(startup: Instant) -> Self {
|
||||
Self::new_with(Real {
|
||||
startup,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates the internal time measurements.
|
||||
///
|
||||
/// Calling this method as part of your app will most likely result in
|
||||
/// inaccurate timekeeping, as the [`Time`] resource is ordinarily managed
|
||||
/// by the [`TimePlugin`](crate::TimePlugin).
|
||||
pub fn update(&mut self) {
|
||||
let instant = Instant::now();
|
||||
self.update_with_instant(instant);
|
||||
}
|
||||
|
||||
/// Updates time with a specified [`Duration`].
|
||||
///
|
||||
/// This method is provided for use in tests.
|
||||
///
|
||||
/// Calling this method as part of your app will most likely result in
|
||||
/// inaccurate timekeeping, as the [`Time`] resource is ordinarily managed
|
||||
/// by the [`TimePlugin`](crate::TimePlugin).
|
||||
pub fn update_with_duration(&mut self, duration: Duration) {
|
||||
let last_update = self.context().last_update.unwrap_or(self.context().startup);
|
||||
self.update_with_instant(last_update + duration);
|
||||
}
|
||||
|
||||
/// Updates time with a specified [`Instant`](std::time::Instant).
|
||||
///
|
||||
/// This method is provided for use in tests.
|
||||
///
|
||||
/// Calling this method as part of your app will most likely result in inaccurate timekeeping,
|
||||
/// as the [`Time`] resource is ordinarily managed by the [`TimePlugin`](crate::TimePlugin).
|
||||
pub fn update_with_instant(&mut self, instant: Instant) {
|
||||
let Some(last_update) = self.context().last_update else {
|
||||
let context = self.context_mut();
|
||||
context.first_update = Some(instant);
|
||||
context.last_update = Some(instant);
|
||||
return;
|
||||
};
|
||||
let delta = instant - last_update;
|
||||
self.advance_by(delta);
|
||||
self.context_mut().last_update = Some(instant);
|
||||
}
|
||||
|
||||
/// Returns the [`Instant`](std::time::Instant) the clock was created.
|
||||
///
|
||||
/// This usually represents when the app was started.
|
||||
#[inline]
|
||||
pub fn startup(&self) -> Instant {
|
||||
self.context().startup
|
||||
}
|
||||
|
||||
/// Returns the [`Instant`](std::time::Instant) when [`Self::update`] was first called, if it
|
||||
/// exists.
|
||||
///
|
||||
/// This usually represents when the first app update started.
|
||||
#[inline]
|
||||
pub fn first_update(&self) -> Option<Instant> {
|
||||
self.context().first_update
|
||||
}
|
||||
|
||||
/// Returns the [`Instant`](std::time::Instant) when [`Self::update`] was last called, if it
|
||||
/// exists.
|
||||
///
|
||||
/// This usually represents when the current app update started.
|
||||
#[inline]
|
||||
pub fn last_update(&self) -> Option<Instant> {
|
||||
self.context().last_update
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_update() {
|
||||
let startup = Instant::now();
|
||||
let mut time = Time::<Real>::new(startup);
|
||||
|
||||
assert_eq!(time.startup(), startup);
|
||||
assert_eq!(time.first_update(), None);
|
||||
assert_eq!(time.last_update(), None);
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
|
||||
time.update();
|
||||
|
||||
assert_ne!(time.first_update(), None);
|
||||
assert_ne!(time.last_update(), None);
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
|
||||
time.update();
|
||||
|
||||
assert_ne!(time.first_update(), None);
|
||||
assert_ne!(time.last_update(), None);
|
||||
assert_ne!(time.last_update(), time.first_update());
|
||||
assert_ne!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), time.delta());
|
||||
|
||||
let prev_elapsed = time.elapsed();
|
||||
time.update();
|
||||
|
||||
assert_ne!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), prev_elapsed + time.delta());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_with_instant() {
|
||||
let startup = Instant::now();
|
||||
let mut time = Time::<Real>::new(startup);
|
||||
|
||||
let first_update = Instant::now();
|
||||
time.update_with_instant(first_update);
|
||||
|
||||
assert_eq!(time.startup(), startup);
|
||||
assert_eq!(time.first_update(), Some(first_update));
|
||||
assert_eq!(time.last_update(), Some(first_update));
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
|
||||
let second_update = Instant::now();
|
||||
time.update_with_instant(second_update);
|
||||
|
||||
assert_eq!(time.first_update(), Some(first_update));
|
||||
assert_eq!(time.last_update(), Some(second_update));
|
||||
assert_eq!(time.delta(), second_update - first_update);
|
||||
assert_eq!(time.elapsed(), second_update - first_update);
|
||||
|
||||
let third_update = Instant::now();
|
||||
time.update_with_instant(third_update);
|
||||
|
||||
assert_eq!(time.first_update(), Some(first_update));
|
||||
assert_eq!(time.last_update(), Some(third_update));
|
||||
assert_eq!(time.delta(), third_update - second_update);
|
||||
assert_eq!(time.elapsed(), third_update - first_update);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_with_duration() {
|
||||
let startup = Instant::now();
|
||||
let mut time = Time::<Real>::new(startup);
|
||||
|
||||
time.update_with_duration(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(time.startup(), startup);
|
||||
assert_eq!(time.first_update(), Some(startup + Duration::from_secs(1)));
|
||||
assert_eq!(time.last_update(), Some(startup + Duration::from_secs(1)));
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
|
||||
time.update_with_duration(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(time.first_update(), Some(startup + Duration::from_secs(1)));
|
||||
assert_eq!(time.last_update(), Some(startup + Duration::from_secs(2)));
|
||||
assert_eq!(time.delta(), Duration::from_secs(1));
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(1));
|
||||
|
||||
time.update_with_duration(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(time.first_update(), Some(startup + Duration::from_secs(1)));
|
||||
assert_eq!(time.last_update(), Some(startup + Duration::from_secs(3)));
|
||||
assert_eq!(time.delta(), Duration::from_secs(1));
|
||||
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
use bevy_reflect::prelude::*;
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_reflect::{prelude::*, Reflect};
|
||||
use bevy_utils::Duration;
|
||||
|
||||
/// A Stopwatch is a struct that track elapsed time when started.
|
||||
|
|
File diff suppressed because it is too large
Load diff
437
crates/bevy_time/src/virt.rs
Normal file
437
crates/bevy_time/src/virt.rs
Normal file
|
@ -0,0 +1,437 @@
|
|||
use bevy_ecs::system::{Res, ResMut};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_utils::{tracing::debug, Duration};
|
||||
|
||||
use crate::{real::Real, time::Time};
|
||||
|
||||
/// The virtual game clock representing game time.
|
||||
///
|
||||
/// A specialization of the [`Time`] structure. **For method documentation, see
|
||||
/// [`Time<Virtual>#impl-Time<Virtual>`].**
|
||||
///
|
||||
/// Normally used as `Time<Virtual>`. It is automatically inserted as a resource
|
||||
/// by [`TimePlugin`](crate::TimePlugin) and updated based on
|
||||
/// [`Time<Real>`](Real). The virtual clock is automatically set as the default
|
||||
/// generic [`Time`] resource for the update.
|
||||
///
|
||||
/// The virtual clock differs from real time clock in that it can be paused, sped up
|
||||
/// and slowed down. It also limits how much it can advance in a single update
|
||||
/// in order to prevent unexpected behavior in cases where updates do not happen
|
||||
/// at regular intervals (e.g. coming back after the program was suspended a long time).
|
||||
///
|
||||
/// The virtual clock can be paused by calling [`pause()`](Time::pause) and
|
||||
/// unpaused by calling [`unpause()`](Time::unpause). When the game clock is
|
||||
/// paused [`delta()`](Time::delta) will be zero on each update, and
|
||||
/// [`elapsed()`](Time::elapsed) will not grow.
|
||||
/// [`effective_speed()`](Time::effective_speed) will return `0.0`. Calling
|
||||
/// [`pause()`](Time::pause) will not affect value the [`delta()`](Time::delta)
|
||||
/// value for the update currently being processed.
|
||||
///
|
||||
/// The speed of the virtual clock can be changed by calling
|
||||
/// [`set_relative_speed()`](Time::set_relative_speed). A value of `2.0` means
|
||||
/// that virtual clock should advance twice as fast as real time, meaning that
|
||||
/// [`delta()`](Time::delta) values will be double of what
|
||||
/// [`Time<Real>::delta()`](Time::delta) reports and
|
||||
/// [`elapsed()`](Time::elapsed) will go twice as fast as
|
||||
/// [`Time<Real>::elapsed()`](Time::elapsed). Calling
|
||||
/// [`set_relative_speed()`](Time::set_relative_speed) will not affect the
|
||||
/// [`delta()`](Time::delta) value for the update currently being processed.
|
||||
///
|
||||
/// The maximum amount of delta time that can be added by a single update can be
|
||||
/// set by [`set_max_delta()`](Time::set_max_delta). This value serves a dual
|
||||
/// purpose in the virtual clock.
|
||||
///
|
||||
/// If the game temporarily freezes due to any reason, such as disk access, a
|
||||
/// blocking system call, or operating system level suspend, reporting the full
|
||||
/// elapsed delta time is likely to cause bugs in game logic. Usually if a
|
||||
/// laptop is suspended for an hour, it doesn't make sense to try to simulate
|
||||
/// the game logic for the elapsed hour when resuming. Instead it is better to
|
||||
/// lose the extra time and pretend a shorter duration of time passed. Setting
|
||||
/// [`max_delta()`](Time::max_delta) to a relatively short time means that the
|
||||
/// impact on game logic will be minimal.
|
||||
///
|
||||
/// If the game lags for some reason, meaning that it will take a longer time to
|
||||
/// compute a frame than the real time that passes during the computation, then
|
||||
/// we would fall behind in processing virtual time. If this situation persists,
|
||||
/// and computing a frame takes longer depending on how much virtual time has
|
||||
/// passed, the game would enter a "death spiral" where computing each frame
|
||||
/// takes longer and longer and the game will appear to freeze. By limiting the
|
||||
/// maximum time that can be added at once, we also limit the amount of virtual
|
||||
/// time the game needs to compute for each frame. This means that the game will
|
||||
/// run slow, and it will run slower than real time, but it will not freeze and
|
||||
/// it will recover as soon as computation becomes fast again.
|
||||
///
|
||||
/// You should set [`max_delta()`](Time::max_delta) to a value that is
|
||||
/// approximately the minimum FPS your game should have even if heavily lagged
|
||||
/// for a moment. The actual FPS when lagged will be somewhat lower than this,
|
||||
/// depending on how much more time it takes to compute a frame compared to real
|
||||
/// time. You should also consider how stable your FPS is, as the limit will
|
||||
/// also dictate how big of an FPS drop you can accept without losing time and
|
||||
/// falling behind real time.
|
||||
#[derive(Debug, Copy, Clone, Reflect)]
|
||||
pub struct Virtual {
|
||||
max_delta: Duration,
|
||||
paused: bool,
|
||||
relative_speed: f64,
|
||||
effective_speed: f64,
|
||||
}
|
||||
|
||||
impl Time<Virtual> {
|
||||
/// The default amount of time that can added in a single update.
|
||||
///
|
||||
/// Equal to 250 milliseconds.
|
||||
const DEFAULT_MAX_DELTA: Duration = Duration::from_millis(250);
|
||||
|
||||
/// Create new virtual clock with given maximum delta step [`Duration`]
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `max_delta` is zero.
|
||||
pub fn from_max_delta(max_delta: Duration) -> Self {
|
||||
let mut ret = Self::default();
|
||||
ret.set_max_delta(max_delta);
|
||||
ret
|
||||
}
|
||||
|
||||
/// Returns the maximum amount of time that can be added to this clock by a
|
||||
/// single update, as [`Duration`].
|
||||
///
|
||||
/// This is the maximum value [`Self::delta()`] will return and also to
|
||||
/// maximum time [`Self::elapsed()`] will be increased by in a single
|
||||
/// update.
|
||||
///
|
||||
/// This ensures that even if no updates happen for an extended amount of time,
|
||||
/// the clock will not have a sudden, huge advance all at once. This also indirectly
|
||||
/// limits the maximum number of fixed update steps that can run in a single update.
|
||||
///
|
||||
/// The default value is 250 milliseconds.
|
||||
#[inline]
|
||||
pub fn max_delta(&self) -> Duration {
|
||||
self.context().max_delta
|
||||
}
|
||||
|
||||
/// Sets the maximum amount of time that can be added to this clock by a
|
||||
/// single update, as [`Duration`].
|
||||
///
|
||||
/// This is the maximum value [`Self::delta()`] will return and also to
|
||||
/// maximum time [`Self::elapsed()`] will be increased by in a single
|
||||
/// update.
|
||||
///
|
||||
/// This is used to ensure that even if the game freezes for a few seconds,
|
||||
/// or is suspended for hours or even days, the virtual clock doesn't
|
||||
/// suddenly jump forward for that full amount, which would likely cause
|
||||
/// gameplay bugs or having to suddenly simulate all the intervening time.
|
||||
///
|
||||
/// If no updates happen for an extended amount of time, this limit prevents
|
||||
/// having a sudden, huge advance all at once. This also indirectly limits
|
||||
/// the maximum number of fixed update steps that can run in a single
|
||||
/// update.
|
||||
///
|
||||
/// The default value is 250 milliseconds. If you want to disable this
|
||||
/// feature, set the value to [`Duration::MAX`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `max_delta` is zero.
|
||||
#[inline]
|
||||
pub fn set_max_delta(&mut self, max_delta: Duration) {
|
||||
assert_ne!(max_delta, Duration::ZERO, "tried to set max delta to zero");
|
||||
self.context_mut().max_delta = max_delta;
|
||||
}
|
||||
|
||||
/// Returns the speed the clock advances relative to your system clock, as [`f32`].
|
||||
/// This is known as "time scaling" or "time dilation" in other engines.
|
||||
#[inline]
|
||||
pub fn relative_speed(&self) -> f32 {
|
||||
self.relative_speed_f64() as f32
|
||||
}
|
||||
|
||||
/// Returns the speed the clock advances relative to your system clock, as [`f64`].
|
||||
/// This is known as "time scaling" or "time dilation" in other engines.
|
||||
#[inline]
|
||||
pub fn relative_speed_f64(&self) -> f64 {
|
||||
self.context().relative_speed
|
||||
}
|
||||
|
||||
/// Returns the speed the clock advanced relative to your system clock in
|
||||
/// this update, as [`f32`].
|
||||
///
|
||||
/// Returns `0.0` if the game was paused or what the `relative_speed` value
|
||||
/// was at the start of this update.
|
||||
#[inline]
|
||||
pub fn effective_speed(&self) -> f32 {
|
||||
self.context().effective_speed as f32
|
||||
}
|
||||
|
||||
/// Returns the speed the clock advanced relative to your system clock in
|
||||
/// this update, as [`f64`].
|
||||
///
|
||||
/// Returns `0.0` if the game was paused or what the `relative_speed` value
|
||||
/// was at the start of this update.
|
||||
#[inline]
|
||||
pub fn effective_speed_f64(&self) -> f64 {
|
||||
self.context().effective_speed
|
||||
}
|
||||
|
||||
/// Sets the speed the clock advances relative to your system clock, given as an [`f32`].
|
||||
///
|
||||
/// For example, setting this to `2.0` will make the clock advance twice as fast as your system
|
||||
/// clock.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `ratio` is negative or not finite.
|
||||
#[inline]
|
||||
pub fn set_relative_speed(&mut self, ratio: f32) {
|
||||
self.set_relative_speed_f64(ratio as f64);
|
||||
}
|
||||
|
||||
/// Sets the speed the clock advances relative to your system clock, given as an [`f64`].
|
||||
///
|
||||
/// For example, setting this to `2.0` will make the clock advance twice as fast as your system
|
||||
/// clock.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `ratio` is negative or not finite.
|
||||
#[inline]
|
||||
pub fn set_relative_speed_f64(&mut self, ratio: f64) {
|
||||
assert!(ratio.is_finite(), "tried to go infinitely fast");
|
||||
assert!(ratio >= 0.0, "tried to go back in time");
|
||||
self.context_mut().relative_speed = ratio;
|
||||
}
|
||||
|
||||
/// Stops the clock, preventing it from advancing until resumed.
|
||||
#[inline]
|
||||
pub fn pause(&mut self) {
|
||||
self.context_mut().paused = true;
|
||||
}
|
||||
|
||||
/// Resumes the clock if paused.
|
||||
#[inline]
|
||||
pub fn unpause(&mut self) {
|
||||
self.context_mut().paused = false;
|
||||
}
|
||||
|
||||
/// Returns `true` if the clock is currently paused.
|
||||
#[inline]
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.context().paused
|
||||
}
|
||||
|
||||
/// Returns `true` if the clock was paused at the start of this update.
|
||||
#[inline]
|
||||
pub fn was_paused(&self) -> bool {
|
||||
self.context().effective_speed == 0.0
|
||||
}
|
||||
|
||||
/// Updates the elapsed duration of `self` by `raw_delta`, up to the `max_delta`.
|
||||
fn advance_with_raw_delta(&mut self, raw_delta: Duration) {
|
||||
let max_delta = self.context().max_delta;
|
||||
let clamped_delta = if raw_delta > max_delta {
|
||||
debug!(
|
||||
"delta time larger than maximum delta, clamping delta to {:?} and skipping {:?}",
|
||||
max_delta,
|
||||
raw_delta - max_delta
|
||||
);
|
||||
max_delta
|
||||
} else {
|
||||
raw_delta
|
||||
};
|
||||
let effective_speed = if self.context().paused {
|
||||
0.0
|
||||
} else {
|
||||
self.context().relative_speed
|
||||
};
|
||||
let delta = if effective_speed != 1.0 {
|
||||
clamped_delta.mul_f64(effective_speed)
|
||||
} else {
|
||||
// avoid rounding when at normal speed
|
||||
clamped_delta
|
||||
};
|
||||
self.context_mut().effective_speed = effective_speed;
|
||||
self.advance_by(delta);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Virtual {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_delta: Time::<Virtual>::DEFAULT_MAX_DELTA,
|
||||
paused: false,
|
||||
relative_speed: 1.0,
|
||||
effective_speed: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances [`Time<Virtual>`] and [`Time`] based on the elapsed [`Time<Real>`].
|
||||
///
|
||||
/// The virtual time will be advanced up to the provided [`Time::max_delta`].
|
||||
pub fn virtual_time_system(
|
||||
mut current: ResMut<Time>,
|
||||
mut virt: ResMut<Time<Virtual>>,
|
||||
real: Res<Time<Real>>,
|
||||
) {
|
||||
let raw_delta = real.delta();
|
||||
virt.advance_with_raw_delta(raw_delta);
|
||||
*current = virt.as_generic();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
let time = Time::<Virtual>::default();
|
||||
|
||||
assert!(!time.is_paused()); // false
|
||||
assert_eq!(time.relative_speed(), 1.0);
|
||||
assert_eq!(time.max_delta(), Time::<Virtual>::DEFAULT_MAX_DELTA);
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_advance() {
|
||||
let mut time = Time::<Virtual>::default();
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(125));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(125));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(125));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(125));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(125));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(125));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(125));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(375));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(125));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(125));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_speed() {
|
||||
let mut time = Time::<Virtual>::default();
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(250));
|
||||
|
||||
assert_eq!(time.relative_speed(), 1.0);
|
||||
assert_eq!(time.effective_speed(), 1.0);
|
||||
assert_eq!(time.delta(), Duration::from_millis(250));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
||||
|
||||
time.set_relative_speed_f64(2.0);
|
||||
|
||||
assert_eq!(time.relative_speed(), 2.0);
|
||||
assert_eq!(time.effective_speed(), 1.0);
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(250));
|
||||
|
||||
assert_eq!(time.relative_speed(), 2.0);
|
||||
assert_eq!(time.effective_speed(), 2.0);
|
||||
assert_eq!(time.delta(), Duration::from_millis(500));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(750));
|
||||
|
||||
time.set_relative_speed_f64(0.5);
|
||||
|
||||
assert_eq!(time.relative_speed(), 0.5);
|
||||
assert_eq!(time.effective_speed(), 2.0);
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(250));
|
||||
|
||||
assert_eq!(time.relative_speed(), 0.5);
|
||||
assert_eq!(time.effective_speed(), 0.5);
|
||||
assert_eq!(time.delta(), Duration::from_millis(125));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(875));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pause() {
|
||||
let mut time = Time::<Virtual>::default();
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(250));
|
||||
|
||||
assert!(!time.is_paused()); // false
|
||||
assert!(!time.was_paused()); // false
|
||||
assert_eq!(time.relative_speed(), 1.0);
|
||||
assert_eq!(time.effective_speed(), 1.0);
|
||||
assert_eq!(time.delta(), Duration::from_millis(250));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
||||
|
||||
time.pause();
|
||||
|
||||
assert!(time.is_paused()); // true
|
||||
assert!(!time.was_paused()); // false
|
||||
assert_eq!(time.relative_speed(), 1.0);
|
||||
assert_eq!(time.effective_speed(), 1.0);
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(250));
|
||||
|
||||
assert!(time.is_paused()); // true
|
||||
assert!(time.was_paused()); // true
|
||||
assert_eq!(time.relative_speed(), 1.0);
|
||||
assert_eq!(time.effective_speed(), 0.0);
|
||||
assert_eq!(time.delta(), Duration::ZERO);
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
||||
|
||||
time.unpause();
|
||||
|
||||
assert!(!time.is_paused()); // false
|
||||
assert!(time.was_paused()); // true
|
||||
assert_eq!(time.relative_speed(), 1.0);
|
||||
assert_eq!(time.effective_speed(), 0.0);
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(250));
|
||||
|
||||
assert!(!time.is_paused()); // false
|
||||
assert!(!time.was_paused()); // false
|
||||
assert_eq!(time.relative_speed(), 1.0);
|
||||
assert_eq!(time.effective_speed(), 1.0);
|
||||
assert_eq!(time.delta(), Duration::from_millis(250));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_delta() {
|
||||
let mut time = Time::<Virtual>::default();
|
||||
time.set_max_delta(Duration::from_millis(500));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(250));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(250));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(250));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(500));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(500));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(750));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(750));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(500));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(1250));
|
||||
|
||||
time.set_max_delta(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(time.max_delta(), Duration::from_secs(1));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(750));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(750));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(2000));
|
||||
|
||||
time.advance_with_raw_delta(Duration::from_millis(1250));
|
||||
|
||||
assert_eq!(time.delta(), Duration::from_millis(1000));
|
||||
assert_eq!(time.elapsed(), Duration::from_millis(3000));
|
||||
}
|
||||
}
|
33
deny.toml
33
deny.toml
|
@ -28,33 +28,14 @@ exceptions = [
|
|||
default = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "deny"
|
||||
multiple-versions = "warn"
|
||||
wildcards = "deny"
|
||||
highlight = "all"
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
{ name = "ahash", version = "0.7" },
|
||||
{ name = "bitflags", version = "1.3" },
|
||||
{ name = "core-foundation-sys", version = "0.6" },
|
||||
{ name = "jni", version = "0.19" },
|
||||
{ name = "hashbrown", version = "0.12"},
|
||||
{ name = "libloading", version = "0.7"},
|
||||
{ name = "miniz_oxide", version = "0.6"},
|
||||
{ name = "nix", version = "0.24" },
|
||||
{ name = "redox_syscall", version = "0.2" },
|
||||
{ name = "regex-syntax", version = "0.6"},
|
||||
{ name = "syn", version = "1.0"},
|
||||
{ name = "windows", version = "0.44"},
|
||||
{ name = "windows", version = "0.46"},
|
||||
{ name = "windows-sys", version = "0.45" },
|
||||
{ name = "windows-targets", version = "0.42"},
|
||||
{ name = "windows_aarch64_gnullvm", version = "0.42"},
|
||||
{ name = "windows_aarch64_msvc", version = "0.42"},
|
||||
{ name = "windows_i686_gnu", version = "0.42"},
|
||||
{ name = "windows_i686_msvc", version = "0.42"},
|
||||
{ name = "windows_x86_64_gnu", version = "0.42"},
|
||||
{ name = "windows_x86_64_gnullvm", version = "0.42"},
|
||||
{ name = "windows_x86_64_msvc", version = "0.42"},
|
||||
# Certain crates that we don't want multiple versions of in the dependency tree
|
||||
deny = [
|
||||
{ name = "ahash", deny-multiple-versions = true },
|
||||
{ name = "android-activity", deny-multiple-versions = true },
|
||||
{ name = "glam", deny-multiple-versions = true },
|
||||
{ name = "raw-window-handle", deny-multiple-versions = true },
|
||||
]
|
||||
|
||||
[sources]
|
||||
|
|
|
@ -21,7 +21,7 @@ You also need to select a `tracing` backend using one of the following cargo fea
|
|||
|
||||
When your app is bottlenecked by the GPU, you may encounter frames that have multiple prepare-set systems all taking an unusually long time to complete, and all finishing at about the same time.
|
||||
|
||||
Improvements are planned to resolve this issue, you can find more details in the doc comment for [`prepare_windows`](../crates/bevy_render/src/view/window/mod.rs).
|
||||
Improvements are planned to resolve this issue, you can find more details in the docs for [`prepare_windows`](https://docs.rs/bevy/latest/bevy/render/view/fn.prepare_windows.html).
|
||||
|
||||
![prepare_windows span bug](https://github.com/bevyengine/bevy/assets/2771466/15c0819b-0e07-4665-aa1e-579caa24fece)
|
||||
|
||||
|
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
use bevy::prelude::*;
|
||||
|
||||
const TIME_STEP: f32 = 1.0 / 60.0;
|
||||
const BOUNDS: Vec2 = Vec2::new(1200.0, 640.0);
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.insert_resource(FixedTime::new_from_secs(TIME_STEP))
|
||||
.insert_resource(Time::<Fixed>::from_hz(60.0))
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
|
@ -117,6 +116,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
|
||||
/// Demonstrates applying rotation and movement based on keyboard input.
|
||||
fn player_movement_system(
|
||||
time: Res<Time>,
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut query: Query<(&Player, &mut Transform)>,
|
||||
) {
|
||||
|
@ -138,12 +138,14 @@ fn player_movement_system(
|
|||
}
|
||||
|
||||
// update the ship rotation around the Z axis (perpendicular to the 2D plane of the screen)
|
||||
transform.rotate_z(rotation_factor * ship.rotation_speed * TIME_STEP);
|
||||
transform.rotate_z(rotation_factor * ship.rotation_speed * time.delta_seconds());
|
||||
|
||||
// get the ship's forward vector by applying the current rotation to the ships initial facing vector
|
||||
// get the ship's forward vector by applying the current rotation to the ships initial facing
|
||||
// vector
|
||||
let movement_direction = transform.rotation * Vec3::Y;
|
||||
// get the distance the ship will move based on direction, the ship's movement speed and delta time
|
||||
let movement_distance = movement_factor * ship.movement_speed * TIME_STEP;
|
||||
// get the distance the ship will move based on direction, the ship's movement speed and delta
|
||||
// time
|
||||
let movement_distance = movement_factor * ship.movement_speed * time.delta_seconds();
|
||||
// create the change in translation using the new movement direction and distance
|
||||
let translation_delta = movement_direction * movement_distance;
|
||||
// update the ship translation with our new translation delta
|
||||
|
@ -182,8 +184,8 @@ fn snap_to_player_system(
|
|||
/// if not, which way to rotate to face the player. The dot product on two unit length vectors
|
||||
/// will return a value between -1.0 and +1.0 which tells us the following about the two vectors:
|
||||
///
|
||||
/// * If the result is 1.0 the vectors are pointing in the same direction, the angle between them
|
||||
/// is 0 degrees.
|
||||
/// * If the result is 1.0 the vectors are pointing in the same direction, the angle between them is
|
||||
/// 0 degrees.
|
||||
/// * If the result is 0.0 the vectors are perpendicular, the angle between them is 90 degrees.
|
||||
/// * If the result is -1.0 the vectors are parallel but pointing in opposite directions, the angle
|
||||
/// between them is 180 degrees.
|
||||
|
@ -198,6 +200,7 @@ fn snap_to_player_system(
|
|||
/// floating point precision loss, so it pays to clamp your dot product value before calling
|
||||
/// `acos`.
|
||||
fn rotate_to_player_system(
|
||||
time: Res<Time>,
|
||||
mut query: Query<(&RotateToPlayer, &mut Transform), Without<Player>>,
|
||||
player_query: Query<&Transform, With<Player>>,
|
||||
) {
|
||||
|
@ -242,7 +245,8 @@ fn rotate_to_player_system(
|
|||
let max_angle = forward_dot_player.clamp(-1.0, 1.0).acos(); // clamp acos for safety
|
||||
|
||||
// calculate angle of rotation with limit
|
||||
let rotation_angle = rotation_sign * (config.rotation_speed * TIME_STEP).min(max_angle);
|
||||
let rotation_angle =
|
||||
rotation_sign * (config.rotation_speed * time.delta_seconds()).min(max_angle);
|
||||
|
||||
// rotate the enemy to face the player
|
||||
enemy_transform.rotate_z(rotation_angle);
|
||||
|
|
|
@ -234,6 +234,7 @@ Example | Description
|
|||
[System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state
|
||||
[System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam`
|
||||
[System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully
|
||||
[Time handling](../examples/ecs/time.rs) | Explains how Time is handled in ECS
|
||||
[Timers](../examples/ecs/timers.rs) | Illustrates ticking `Timer` resources inside systems and handling their state
|
||||
|
||||
## Games
|
||||
|
@ -292,6 +293,7 @@ Example | Description
|
|||
[Array Texture](../examples/shader/array_texture.rs) | A shader that shows how to reuse the core bevy PBR shading functionality in a custom material that obtains the base color from an array texture.
|
||||
[Compute - Game of Life](../examples/shader/compute_shader_game_of_life.rs) | A compute shader that simulates Conway's Game of Life
|
||||
[Custom Vertex Attribute](../examples/shader/custom_vertex_attribute.rs) | A shader that reads a mesh's custom vertex attribute
|
||||
[Extended Material](../examples/shader/extended_material.rs) | A custom shader that builds on the standard material
|
||||
[Instancing](../examples/shader/shader_instancing.rs) | A shader that renders a mesh multiple times in one draw call
|
||||
[Material](../examples/shader/shader_material.rs) | A shader and a material that uses it
|
||||
[Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
use bevy::prelude::*;
|
||||
|
||||
const FIXED_TIMESTEP: f32 = 0.5;
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
|
@ -11,28 +10,31 @@ fn main() {
|
|||
// add our system to the fixed timestep schedule
|
||||
.add_systems(FixedUpdate, fixed_update)
|
||||
// configure our fixed timestep schedule to run twice a second
|
||||
.insert_resource(FixedTime::new_from_secs(FIXED_TIMESTEP))
|
||||
.insert_resource(Time::<Fixed>::from_seconds(0.5))
|
||||
.run();
|
||||
}
|
||||
|
||||
fn frame_update(mut last_time: Local<f32>, time: Res<Time>) {
|
||||
// Default `Time` is `Time<Virtual>` here
|
||||
info!(
|
||||
"time since last frame_update: {}",
|
||||
time.raw_elapsed_seconds() - *last_time
|
||||
time.elapsed_seconds() - *last_time
|
||||
);
|
||||
*last_time = time.raw_elapsed_seconds();
|
||||
*last_time = time.elapsed_seconds();
|
||||
}
|
||||
|
||||
fn fixed_update(mut last_time: Local<f32>, time: Res<Time>, fixed_time: Res<FixedTime>) {
|
||||
fn fixed_update(mut last_time: Local<f32>, time: Res<Time>, fixed_time: Res<Time<Fixed>>) {
|
||||
// Default `Time`is `Time<Fixed>` here
|
||||
info!(
|
||||
"time since last fixed_update: {}\n",
|
||||
time.raw_elapsed_seconds() - *last_time
|
||||
time.elapsed_seconds() - *last_time
|
||||
);
|
||||
|
||||
info!("fixed timestep: {}\n", FIXED_TIMESTEP);
|
||||
info!("fixed timestep: {}\n", time.delta_seconds());
|
||||
// If we want to see the overstep, we need to access `Time<Fixed>` specifically
|
||||
info!(
|
||||
"time accrued toward next fixed_update: {}\n",
|
||||
fixed_time.accumulated().as_secs_f32()
|
||||
fixed_time.overstep().as_secs_f32()
|
||||
);
|
||||
*last_time = time.raw_elapsed_seconds();
|
||||
*last_time = time.elapsed_seconds();
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
use bevy::{pbr::AmbientLight, prelude::*};
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
|
||||
const DELTA_TIME: f32 = 0.01;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
|
@ -13,7 +11,6 @@ fn main() {
|
|||
..default()
|
||||
})
|
||||
.insert_resource(ClearColor(Color::BLACK))
|
||||
.insert_resource(FixedTime::new_from_secs(DELTA_TIME))
|
||||
.add_systems(Startup, generate_bodies)
|
||||
.add_systems(FixedUpdate, (interact_bodies, integrate))
|
||||
.add_systems(Update, look_at_star)
|
||||
|
@ -41,6 +38,7 @@ struct BodyBundle {
|
|||
}
|
||||
|
||||
fn generate_bodies(
|
||||
time: Res<Time>,
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
|
@ -96,7 +94,7 @@ fn generate_bodies(
|
|||
rng.gen_range(vel_range.clone()),
|
||||
rng.gen_range(vel_range.clone()),
|
||||
rng.gen_range(vel_range.clone()),
|
||||
) * DELTA_TIME,
|
||||
) * time.delta_seconds(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -160,8 +158,8 @@ fn interact_bodies(mut query: Query<(&Mass, &GlobalTransform, &mut Acceleration)
|
|||
}
|
||||
}
|
||||
|
||||
fn integrate(mut query: Query<(&mut Acceleration, &mut Transform, &mut LastPos)>) {
|
||||
let dt_sq = DELTA_TIME * DELTA_TIME;
|
||||
fn integrate(time: Res<Time>, mut query: Query<(&mut Acceleration, &mut Transform, &mut LastPos)>) {
|
||||
let dt_sq = time.delta_seconds() * time.delta_seconds();
|
||||
for (mut acceleration, mut transform, mut last_pos) in &mut query {
|
||||
// verlet integration
|
||||
// x(t+dt) = 2x(t) - x(t-dt) + a(t)dt^2 + O(dt^4)
|
||||
|
|
115
examples/ecs/time.rs
Normal file
115
examples/ecs/time.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
use std::io::{self, BufRead};
|
||||
use std::time::Duration;
|
||||
|
||||
fn banner() {
|
||||
println!("This example is meant to intuitively demonstrate how Time works in Bevy.");
|
||||
println!();
|
||||
println!("Time will be printed in three different schedules in the app:");
|
||||
println!("- PreUpdate: real time is printed");
|
||||
println!("- FixedUpdate: fixed time step time is printed, may be run zero or multiple times");
|
||||
println!("- Update: virtual game time is printed");
|
||||
println!();
|
||||
println!("Max delta time is set to 5 seconds. Fixed timestep is set to 1 second.");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn help() {
|
||||
println!("The app reads commands line-by-line from standard input.");
|
||||
println!();
|
||||
println!("Commands:");
|
||||
println!(" empty line: Run app.update() once on the Bevy App");
|
||||
println!(" q: Quit the app.");
|
||||
println!(" f: Set speed to fast, 2x");
|
||||
println!(" n: Set speed to normal, 1x");
|
||||
println!(" s: Set speed to slow, 0.5x");
|
||||
println!(" p: Pause");
|
||||
println!(" u: Unpause");
|
||||
}
|
||||
|
||||
fn runner(mut app: App) {
|
||||
banner();
|
||||
help();
|
||||
let stdin = io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
if let Err(err) = line {
|
||||
println!("read err: {:#}", err);
|
||||
break;
|
||||
}
|
||||
match line.unwrap().as_str() {
|
||||
"" => {
|
||||
app.update();
|
||||
}
|
||||
"f" => {
|
||||
println!("FAST: setting relative speed to 2x");
|
||||
app.world
|
||||
.resource_mut::<Time<Virtual>>()
|
||||
.set_relative_speed(2.0);
|
||||
}
|
||||
"n" => {
|
||||
println!("NORMAL: setting relative speed to 1x");
|
||||
app.world
|
||||
.resource_mut::<Time<Virtual>>()
|
||||
.set_relative_speed(1.0);
|
||||
}
|
||||
"s" => {
|
||||
println!("SLOW: setting relative speed to 0.5x");
|
||||
app.world
|
||||
.resource_mut::<Time<Virtual>>()
|
||||
.set_relative_speed(0.5);
|
||||
}
|
||||
"p" => {
|
||||
println!("PAUSE: pausing virtual clock");
|
||||
app.world.resource_mut::<Time<Virtual>>().pause();
|
||||
}
|
||||
"u" => {
|
||||
println!("UNPAUSE: resuming virtual clock");
|
||||
app.world.resource_mut::<Time<Virtual>>().unpause();
|
||||
}
|
||||
"q" => {
|
||||
println!("QUITTING!");
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
help();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_real_time(time: Res<Time<Real>>) {
|
||||
println!(
|
||||
"PreUpdate: this is real time clock, delta is {:?} and elapsed is {:?}",
|
||||
time.delta(),
|
||||
time.elapsed()
|
||||
);
|
||||
}
|
||||
|
||||
fn print_fixed_time(time: Res<Time>) {
|
||||
println!(
|
||||
"FixedUpdate: this is generic time clock inside fixed, delta is {:?} and elapsed is {:?}",
|
||||
time.delta(),
|
||||
time.elapsed()
|
||||
);
|
||||
}
|
||||
|
||||
fn print_time(time: Res<Time>) {
|
||||
println!(
|
||||
"Update: this is generic time clock, delta is {:?} and elapsed is {:?}",
|
||||
time.delta(),
|
||||
time.elapsed()
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(MinimalPlugins)
|
||||
.insert_resource(Time::<Virtual>::from_max_delta(Duration::from_secs(5)))
|
||||
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs(1)))
|
||||
.add_systems(PreUpdate, print_real_time)
|
||||
.add_systems(FixedUpdate, print_fixed_time)
|
||||
.add_systems(Update, print_time)
|
||||
.set_runner(runner)
|
||||
.run();
|
||||
}
|
|
@ -53,20 +53,19 @@ fn main() {
|
|||
.insert_resource(Scoreboard { score: 0 })
|
||||
.insert_resource(ClearColor(BACKGROUND_COLOR))
|
||||
.add_event::<CollisionEvent>()
|
||||
// Configure how frequently our gameplay systems are run
|
||||
.insert_resource(FixedTime::new_from_secs(1.0 / 60.0))
|
||||
.add_systems(Startup, setup)
|
||||
// Add our gameplay simulation systems to the fixed timestep schedule
|
||||
// which runs at 64 Hz by default
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
(
|
||||
apply_velocity,
|
||||
move_paddle,
|
||||
check_for_collisions,
|
||||
apply_velocity.before(check_for_collisions),
|
||||
move_paddle
|
||||
.before(check_for_collisions)
|
||||
.after(apply_velocity),
|
||||
play_collision_sound.after(check_for_collisions),
|
||||
),
|
||||
play_collision_sound,
|
||||
)
|
||||
// `chain`ing systems together runs them in order
|
||||
.chain(),
|
||||
)
|
||||
.add_systems(Update, (update_scoreboard, bevy::window::close_on_esc))
|
||||
.run();
|
||||
|
@ -306,7 +305,7 @@ fn setup(
|
|||
fn move_paddle(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut query: Query<&mut Transform, With<Paddle>>,
|
||||
time_step: Res<FixedTime>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let mut paddle_transform = query.single_mut();
|
||||
let mut direction = 0.0;
|
||||
|
@ -321,7 +320,7 @@ fn move_paddle(
|
|||
|
||||
// Calculate the new horizontal paddle position based on player input
|
||||
let new_paddle_position =
|
||||
paddle_transform.translation.x + direction * PADDLE_SPEED * time_step.period.as_secs_f32();
|
||||
paddle_transform.translation.x + direction * PADDLE_SPEED * time.delta_seconds();
|
||||
|
||||
// Update the paddle position,
|
||||
// making sure it doesn't cause the paddle to leave the arena
|
||||
|
@ -331,10 +330,10 @@ fn move_paddle(
|
|||
paddle_transform.translation.x = new_paddle_position.clamp(left_bound, right_bound);
|
||||
}
|
||||
|
||||
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time_step: Res<FixedTime>) {
|
||||
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) {
|
||||
for (mut transform, velocity) in &mut query {
|
||||
transform.translation.x += velocity.x * time_step.period.as_secs_f32();
|
||||
transform.translation.y += velocity.y * time_step.period.as_secs_f32();
|
||||
transform.translation.x += velocity.x * time.delta_seconds();
|
||||
transform.translation.y += velocity.y * time.delta_seconds();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
92
examples/shader/extended_material.rs
Normal file
92
examples/shader/extended_material.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
//! Demonstrates using a custom extension to the `StandardMaterial` to modify the results of the builtin pbr shader.
|
||||
|
||||
use bevy::reflect::TypePath;
|
||||
use bevy::{
|
||||
pbr::{ExtendedMaterial, MaterialExtension, OpaqueRendererMethod},
|
||||
prelude::*,
|
||||
render::render_resource::*,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugins(MaterialPlugin::<
|
||||
ExtendedMaterial<StandardMaterial, MyExtension>,
|
||||
>::default())
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, rotate_things)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, MyExtension>>>,
|
||||
) {
|
||||
// sphere
|
||||
commands.spawn(MaterialMeshBundle {
|
||||
mesh: meshes.add(
|
||||
Mesh::try_from(shape::Icosphere {
|
||||
radius: 1.0,
|
||||
subdivisions: 5,
|
||||
})
|
||||
.unwrap(),
|
||||
),
|
||||
transform: Transform::from_xyz(0.0, 0.5, 0.0),
|
||||
material: materials.add(ExtendedMaterial {
|
||||
base: StandardMaterial {
|
||||
base_color: Color::RED,
|
||||
// can be used in forward or deferred mode.
|
||||
opaque_render_method: OpaqueRendererMethod::Auto,
|
||||
// in deferred mode, only the PbrInput can be modified (uvs, color and other material properties),
|
||||
// in forward mode, the output can also be modified after lighting is applied.
|
||||
// see the fragment shader `extended_material.wgsl` for more info.
|
||||
// Note: to run in deferred mode, you must also add a `DeferredPrepass` component to the camera and either
|
||||
// change the above to `OpaqueRendererMethod::Deferred` or add the `DefaultOpaqueRendererMethod` resource.
|
||||
..Default::default()
|
||||
},
|
||||
extension: MyExtension { quantize_steps: 3 },
|
||||
}),
|
||||
..default()
|
||||
});
|
||||
|
||||
// light
|
||||
commands.spawn((PointLightBundle::default(), Rotate));
|
||||
|
||||
// camera
|
||||
commands.spawn(Camera3dBundle {
|
||||
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||
..default()
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct Rotate;
|
||||
|
||||
fn rotate_things(mut q: Query<&mut Transform, With<Rotate>>, time: Res<Time>) {
|
||||
for mut t in q.iter_mut() {
|
||||
t.translation = Vec3::new(
|
||||
time.elapsed_seconds().sin(),
|
||||
0.5,
|
||||
time.elapsed_seconds().cos(),
|
||||
) * 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Asset, AsBindGroup, TypePath, Debug, Clone)]
|
||||
struct MyExtension {
|
||||
// We need to ensure that the bindings of the base material and the extension do not conflict,
|
||||
// so we start from binding slot 100, leaving slots 0-99 for the base material.
|
||||
#[uniform(100)]
|
||||
quantize_steps: u32,
|
||||
}
|
||||
|
||||
impl MaterialExtension for MyExtension {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"shaders/extended_material.wgsl".into()
|
||||
}
|
||||
|
||||
fn deferred_fragment_shader() -> ShaderRef {
|
||||
"shaders/extended_material.wgsl".into()
|
||||
}
|
||||
}
|
|
@ -141,13 +141,23 @@ impl AsBindGroup for BindlessMaterial {
|
|||
})
|
||||
}
|
||||
|
||||
fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout
|
||||
fn unprepared_bind_group(
|
||||
&self,
|
||||
_: &BindGroupLayout,
|
||||
_: &RenderDevice,
|
||||
_: &RenderAssets<Image>,
|
||||
_: &FallbackImage,
|
||||
) -> Result<UnpreparedBindGroup<Self::Data>, AsBindGroupError> {
|
||||
// we implement as_bind_group directly because
|
||||
panic!("bindless texture arrays can't be owned")
|
||||
// or rather, they can be owned, but then you can't make a `&'a [&'a TextureView]` from a vec of them in get_binding().
|
||||
}
|
||||
|
||||
fn bind_group_layout_entries(_: &RenderDevice) -> Vec<BindGroupLayoutEntry>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||
label: "bindless_material_layout".into(),
|
||||
entries: &[
|
||||
vec![
|
||||
// @group(1) @binding(0) var textures: binding_array<texture_2d<f32>>;
|
||||
BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
|
@ -169,8 +179,7 @@ impl AsBindGroup for BindlessMaterial {
|
|||
// One may need to pay attention to the limit of sampler binding amount on some platforms.
|
||||
// count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
|
||||
},
|
||||
],
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use bevy::{
|
|||
prelude::*,
|
||||
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
|
||||
sprite::{MaterialMesh2dBundle, Mesh2dHandle},
|
||||
utils::Duration,
|
||||
window::{PresentMode, WindowResolution},
|
||||
};
|
||||
use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
|
||||
|
@ -123,7 +124,9 @@ fn main() {
|
|||
counter_system,
|
||||
),
|
||||
)
|
||||
.insert_resource(FixedTime::new_from_secs(FIXED_TIMESTEP))
|
||||
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
|
||||
FIXED_TIMESTEP,
|
||||
)))
|
||||
.run();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue