Merge branch 'main' into transmission

This commit is contained in:
Marco Buono 2023-10-18 00:07:21 -03:00
commit 50faf11062
55 changed files with 2823 additions and 1380 deletions

View file

@ -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

View file

@ -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"

View file

@ -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);
}

View 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;
}

View file

@ -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),
))
.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),
))
));
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));
}

View file

@ -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);
}
{

View file

@ -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 {

View file

@ -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),
);

View file

@ -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.

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -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;

View file

@ -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

View 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,
},
)
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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<(
&ExtractedView,
&VisibleEntities,
&mut RenderPhase<Opaque3dPrepass>,
&mut RenderPhase<AlphaMask3dPrepass>,
Option<&mut RenderPhase<Opaque3dDeferred>>,
Option<&mut RenderPhase<AlphaMask3dDeferred>>,
Option<&DepthPrepass>,
Option<&NormalPrepass>,
Option<&MotionVectorPrepass>,
Option<&DeferredPrepass>,
)>,
mut views: Query<
(
&ExtractedView,
&VisibleEntities,
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,

View file

@ -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));

View file

@ -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());
}

View file

@ -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
#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
#import bevy_pbr::pbr_types STANDARD_MATERIAL_FLAGS_UNLIT_BIT
#endif
#ifdef DEFERRED_PREPASS
#import bevy_pbr::pbr_deferred_functions deferred_gbuffer_from_pbr_input
#import bevy_pbr::pbr_prepass_functions calculate_motion_vector
#import bevy_pbr::prepass_io VertexOutput, FragmentOutput
#else // DEFERRED_PREPASS
#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
@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;
}

View 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;
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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, &registry);
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, &registry);
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(&registry);
let expected = value.clone_value();
let result = reflect_deserializer
.deserialize(&mut deserializer)
.unwrap()
.take::<DynamicStruct>()
.unwrap();
assert!(expected.reflect_partial_eq(&result).unwrap());
}
}

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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,13 +62,16 @@ 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();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
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,22 +217,28 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
if buffer {
binding_impls.push(quote! {
#render_path::render_resource::OwnedBindingResource::Buffer({
self.#field_name.clone()
})
(
#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();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::STORAGE,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::STORAGE,
contents: buffer.as_ref(),
},
))
)
}});
}
@ -276,14 +269,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let fallback_image = get_fallback_image(&render_path, *dimension);
binding_impls.push(quote! {
#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 {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
}
})
(
#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 {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone()
} else {
#fallback_image.texture_view.clone()
}
})
)
});
binding_layouts.push(quote! {
@ -315,14 +311,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
let fallback_image = get_fallback_image(&render_path, *dimension);
binding_impls.push(quote! {
#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 {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone()
} else {
#fallback_image.sampler.clone()
}
})
(
#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 {
images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.sampler.clone()
} else {
#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,13 +353,16 @@ 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();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
)
}});
binding_layouts.push(quote!{
@ -402,13 +399,16 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result<TokenStream> {
buffer.write(&#uniform_struct_name {
#(#field_name: &self.#field_name,)*
}).unwrap();
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
contents: buffer.as_ref(),
},
))
(
#binding_index,
#render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(
&#render_path::render_resource::BufferInitDescriptor {
label: None,
usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::UNIFORM,
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,)*]
}
}
}))

View file

@ -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.

View file

@ -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::*;

View file

@ -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),

View file

@ -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")
})
};

View file

@ -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),
}

View file

@ -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,
}

View file

@ -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))),
);
}
}

View 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));
}
}

View file

@ -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());
}
}

View file

@ -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),
}
}

View 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));
}
}

View file

@ -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

View 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));
}
}

View file

@ -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]

View file

@ -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)

View file

@ -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);

View file

@ -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

View file

@ -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();
}

View file

@ -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
View 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();
}

View file

@ -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();
}
}

View 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()
}
}

View file

@ -141,36 +141,45 @@ 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: &[
// @group(1) @binding(0) var textures: binding_array<texture_2d<f32>>;
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
vec![
// @group(1) @binding(0) var textures: binding_array<texture_2d<f32>>;
BindGroupLayoutEntry {
binding: 0,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
multisampled: false,
},
// @group(1) @binding(1) var nearest_sampler: sampler;
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
// Note: as textures, multiple samplers can also be bound onto one binding slot.
// One may need to pay attention to the limit of sampler binding amount on some platforms.
// count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
],
})
count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
// @group(1) @binding(1) var nearest_sampler: sampler;
BindGroupLayoutEntry {
binding: 1,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler(SamplerBindingType::Filtering),
count: None,
// Note: as textures, multiple samplers can also be bound onto one binding slot.
// One may need to pay attention to the limit of sampler binding amount on some platforms.
// count: NonZeroU32::new(MAX_TEXTURE_COUNT as u32),
},
]
}
}

View file

@ -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();
}