From b1a91a823f0882b852426b171bcefbfa336f9538 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Mon, 28 Jun 2021 01:10:23 +0200 Subject: [PATCH] bevy_pbr2: Add support for most of the StandardMaterial textures (#4) * bevy_pbr2: Add support for most of the StandardMaterial textures Normal maps are not included here as they require tangents in a vertex attribute. * bevy_pbr2: Ensure RenderCommandQueue is ready for PbrShaders init * texture_pipelined: Add a light to the scene so we can see stuff * WIP bevy_pbr2: back to front sorting hack * bevy_pbr2: Uniform control flow for texture sampling in pbr.frag From 'fintelia' on the Bevy Render Rework Round 2 discussion: "My understanding is that GPUs these days never use the "execute both branches and select the result" strategy. Rather, what they do is evaluate the branch condition on all threads of a warp, and jump over it if all of them evaluate to false. If even a single thread needs to execute the if statement body, however, then the remaining threads are paused until that is completed." * bevy_pbr2: Simplify texture and sampler names The StandardMaterial_ prefix is no longer needed * bevy_pbr2: Match default 'AmbientColor' of current bevy_pbr for now * bevy_pbr2: Convert from non-linear to linear sRGB for the color uniform * bevy_pbr2: Add pbr_pipelined example * Fix view vector in pbr frag to work in ortho * bevy_pbr2: Use a 90 degree y fov and light range projection for lights * bevy_pbr2: Add AmbientLight resource * bevy_pbr2: Convert PointLight color to linear sRGB for use in fragment shader * bevy_pbr2: pbr.frag: Rename PointLight.projection to view_projection The uniform contains the view_projection matrix so this was incorrect. * bevy_pbr2: PointLight is an OmniLight as it has a radius * bevy_pbr2: Factoring out duplicated code * bevy_pbr2: Implement RenderAsset for StandardMaterial * Remove unnecessary texture and sampler clones * fix comment formatting * remove redundant Buffer:from * Don't extract meshes when their material textures aren't ready * make missing textures in the queue step an error Co-authored-by: Aevyrie Co-authored-by: Carter Anderson --- Cargo.toml | 8 + crates/bevy_ecs/src/event.rs | 2 +- crates/bevy_ecs/src/system/function_system.rs | 5 +- crates/bevy_ecs/src/system/query.rs | 7 +- crates/bevy_ecs/src/system/system_param.rs | 3 +- .../src/hierarchy/child_builder.rs | 2 +- examples/3d/3d_scene_pipelined.rs | 14 +- examples/3d/pbr_pipelined.rs | 86 +++++ examples/3d/texture_pipelined.rs | 101 ++++++ pipelined/bevy_pbr2/Cargo.toml | 4 +- pipelined/bevy_pbr2/src/bundle.rs | 6 +- pipelined/bevy_pbr2/src/lib.rs | 5 +- pipelined/bevy_pbr2/src/light.rs | 25 +- pipelined/bevy_pbr2/src/material.rs | 210 ++++++----- pipelined/bevy_pbr2/src/render/light.rs | 35 +- pipelined/bevy_pbr2/src/render/mod.rs | 334 ++++++++++++++++-- pipelined/bevy_pbr2/src/render/pbr.frag | 214 ++++++++--- pipelined/bevy_pbr2/src/render/pbr.vert | 7 +- pipelined/bevy_render2/src/mesh/mesh/mod.rs | 24 +- .../bevy_render2/src/mesh/shape/capsule.rs | 4 +- .../src/render_resource/buffer.rs | 9 +- .../bevy_render2/src/render_resource/mod.rs | 15 +- .../src/render_resource/texture.rs | 2 +- pipelined/bevy_render2/src/shader/mod.rs | 2 +- pipelined/bevy_render2/src/shader/shader.rs | 3 +- pipelined/bevy_render2/src/texture/mod.rs | 9 +- 26 files changed, 891 insertions(+), 245 deletions(-) create mode 100644 examples/3d/pbr_pipelined.rs create mode 100644 examples/3d/texture_pipelined.rs diff --git a/Cargo.toml b/Cargo.toml index 2a722c8347..7d3cd21e86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,6 +166,10 @@ path = "examples/3d/parenting.rs" name = "pbr" path = "examples/3d/pbr.rs" +[[example]] +name = "pbr_pipelined" +path = "examples/3d/pbr_pipelined.rs" + [[example]] name = "render_to_texture" path = "examples/3d/render_to_texture.rs" @@ -178,6 +182,10 @@ path = "examples/3d/spawner.rs" name = "texture" path = "examples/3d/texture.rs" +[[example]] +name = "texture_pipelined" +path = "examples/3d/texture_pipelined.rs" + [[example]] name = "update_gltf_scene" path = "examples/3d/update_gltf_scene.rs" diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 88abad4c39..c815703be6 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -160,7 +160,7 @@ pub struct EventReader<'s, 'w, T: Component> { #[derive(SystemParam)] pub struct EventWriter<'s, 'w, T: Component> { events: ResMut<'w, Events>, - // TODO: this isn't ideal ... maybe the SystemParam derive can be smarter about world and state lifetimes? + // TODO: this isn't ideal ... maybe the SystemParam derive can be smarter about world and state lifetimes? #[system_param(ignore)] marker: PhantomData<&'s usize>, } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 54ce65d624..b1b0670e10 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -87,7 +87,10 @@ impl SystemState { /// Retrieve the [`SystemParam`] values. This can only be called when all parameters are read-only. #[inline] - pub fn get<'s, 'w>(&'s mut self, world: &'w World) -> >::Item + pub fn get<'s, 'w>( + &'s mut self, + world: &'w World, + ) -> >::Item where Param::Fetch: ReadOnlySystemParamFetch, { diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 14c4f3e8e6..5317aa3c37 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -372,7 +372,7 @@ where pub fn get_mut( &mut self, entity: Entity, - ) -> Result<>::Item, QueryEntityError> { + ) -> Result<>::Item, QueryEntityError> { // SAFE: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { @@ -406,7 +406,10 @@ where /// entity does not have the given component type or if the given component type does not match /// this query. #[inline] - pub fn get_component(&self, entity: Entity) -> Result<&'w T, QueryComponentError> { + pub fn get_component( + &self, + entity: Entity, + ) -> Result<&'w T, QueryComponentError> { let world = self.world; let entity_ref = world .get_entity(entity) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 18a6ec0f1a..b56cda59cf 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -146,7 +146,8 @@ where fn default_config() {} } -impl<'s,'w, Q: WorldQuery + 'static, F: WorldQuery + 'static> SystemParamFetch<'s, 'w> for QueryState +impl<'s, 'w, Q: WorldQuery + 'static, F: WorldQuery + 'static> SystemParamFetch<'s, 'w> + for QueryState where F::Fetch: FilterFetch, { diff --git a/crates/bevy_transform/src/hierarchy/child_builder.rs b/crates/bevy_transform/src/hierarchy/child_builder.rs index 2ec1833390..ae5cf8911f 100644 --- a/crates/bevy_transform/src/hierarchy/child_builder.rs +++ b/crates/bevy_transform/src/hierarchy/child_builder.rs @@ -72,7 +72,7 @@ impl Command for PushChildren { } impl<'s, 'w, 'a> ChildBuilder<'s, 'w, 'a> { - pub fn spawn_bundle(&mut self, bundle: impl Bundle) -> EntityCommands<'s,'w, '_> { + pub fn spawn_bundle(&mut self, bundle: impl Bundle) -> EntityCommands<'s, 'w, '_> { let e = self.commands.spawn_bundle(bundle); self.push_children.children.push(e.id()); e diff --git a/examples/3d/3d_scene_pipelined.rs b/examples/3d/3d_scene_pipelined.rs index 31733543fd..adc31e56e7 100644 --- a/examples/3d/3d_scene_pipelined.rs +++ b/examples/3d/3d_scene_pipelined.rs @@ -4,7 +4,7 @@ use bevy::{ ecs::prelude::*, input::Input, math::Vec3, - pbr2::{PbrBundle, PointLightBundle, StandardMaterial}, + pbr2::{OmniLightBundle, PbrBundle, StandardMaterial}, prelude::{App, Assets, KeyCode, Transform}, render2::{ camera::PerspectiveCameraBundle, @@ -36,8 +36,8 @@ fn setup( commands.spawn_bundle(PbrBundle { mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })), material: materials.add(StandardMaterial { - color: Color::INDIGO, - roughness: 1.0, + base_color: Color::INDIGO, + perceptual_roughness: 1.0, ..Default::default() }), ..Default::default() @@ -47,8 +47,8 @@ fn setup( .spawn_bundle(PbrBundle { mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), material: materials.add(StandardMaterial { - color: Color::PINK, - roughness: 0.0, + base_color: Color::PINK, + perceptual_roughness: 0.0, metallic: 1.0, reflectance: 1.0, ..Default::default() @@ -65,7 +65,7 @@ fn setup( ..Default::default() })), material: materials.add(StandardMaterial { - color: Color::LIME_GREEN, + base_color: Color::LIME_GREEN, ..Default::default() }), transform: Transform::from_xyz(1.5, 1.0, 1.5), @@ -73,7 +73,7 @@ fn setup( }) .insert(Movable); // light - commands.spawn_bundle(PointLightBundle { + commands.spawn_bundle(OmniLightBundle { transform: Transform::from_xyz(5.0, 8.0, 2.0), ..Default::default() }); diff --git a/examples/3d/pbr_pipelined.rs b/examples/3d/pbr_pipelined.rs new file mode 100644 index 0000000000..2aa9601629 --- /dev/null +++ b/examples/3d/pbr_pipelined.rs @@ -0,0 +1,86 @@ +use bevy::{ + ecs::prelude::*, + math::Vec3, + pbr2::{OmniLight, OmniLightBundle, PbrBundle, StandardMaterial}, + prelude::{App, Assets, Transform}, + render2::{ + camera::{OrthographicCameraBundle, OrthographicProjection}, + color::Color, + mesh::{shape, Mesh}, + }, + PipelinedDefaultPlugins, +}; + +/// This example shows how to configure Physically Based Rendering (PBR) parameters. +fn main() { + App::new() + .add_plugins(PipelinedDefaultPlugins) + .add_startup_system(setup.system()) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // add entities to the world + for y in -2..=2 { + for x in -5..=5 { + let x01 = (x + 5) as f32 / 10.0; + let y01 = (y + 2) as f32 / 4.0; + // sphere + commands.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Icosphere { + radius: 0.45, + subdivisions: 32, + })), + material: materials.add(StandardMaterial { + base_color: Color::hex("ffd891").unwrap(), + // vary key PBR parameters on a grid of spheres to show the effect + metallic: y01, + perceptual_roughness: x01, + ..Default::default() + }), + transform: Transform::from_xyz(x as f32, y as f32 + 0.5, 0.0), + ..Default::default() + }); + } + } + // unlit sphere + commands.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Icosphere { + radius: 0.45, + subdivisions: 32, + })), + material: materials.add(StandardMaterial { + base_color: Color::hex("ffd891").unwrap(), + // vary key PBR parameters on a grid of spheres to show the effect + unlit: true, + ..Default::default() + }), + transform: Transform::from_xyz(-5.0, -2.5, 0.0), + ..Default::default() + }); + // light + commands.spawn_bundle(OmniLightBundle { + transform: Transform::from_translation(Vec3::new(50.0, 50.0, 50.0)), + omni_light: OmniLight { + intensity: 50000., + range: 100., + ..Default::default() + }, + ..Default::default() + }); + // camera + commands.spawn_bundle(OrthographicCameraBundle { + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 8.0)) + .looking_at(Vec3::default(), Vec3::Y), + orthographic_projection: OrthographicProjection { + scale: 0.01, + ..Default::default() + }, + ..OrthographicCameraBundle::new_3d() + }); +} diff --git a/examples/3d/texture_pipelined.rs b/examples/3d/texture_pipelined.rs new file mode 100644 index 0000000000..a5f07f16bb --- /dev/null +++ b/examples/3d/texture_pipelined.rs @@ -0,0 +1,101 @@ +use bevy::{ + ecs::prelude::*, + math::{Quat, Vec2, Vec3}, + pbr2::{PbrBundle, StandardMaterial}, + prelude::{App, AssetServer, Assets, Transform}, + render2::{ + camera::PerspectiveCameraBundle, + color::Color, + mesh::{shape, Mesh}, + }, + PipelinedDefaultPlugins, +}; + +/// This example shows various ways to configure texture materials in 3D +fn main() { + App::new() + .add_plugins(PipelinedDefaultPlugins) + .add_startup_system(setup.system()) + .run(); +} + +/// sets up a scene with textured entities +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // load a texture and retrieve its aspect ratio + let texture_handle = asset_server.load("branding/bevy_logo_dark_big.png"); + let aspect = 0.25; + + // create a new quad mesh. this is what we will apply the texture to + let quad_width = 8.0; + let quad_handle = meshes.add(Mesh::from(shape::Quad::new(Vec2::new( + quad_width, + quad_width * aspect, + )))); + + // this material renders the texture normally + let material_handle = materials.add(StandardMaterial { + base_color_texture: Some(texture_handle.clone()), + unlit: true, + ..Default::default() + }); + + // this material modulates the texture to make it red (and slightly transparent) + let red_material_handle = materials.add(StandardMaterial { + base_color: Color::rgba(1.0, 0.0, 0.0, 0.5), + base_color_texture: Some(texture_handle.clone()), + unlit: true, + ..Default::default() + }); + + // and lets make this one blue! (and also slightly transparent) + let blue_material_handle = materials.add(StandardMaterial { + base_color: Color::rgba(0.0, 0.0, 1.0, 0.5), + base_color_texture: Some(texture_handle), + unlit: true, + ..Default::default() + }); + + // textured quad - normal + commands.spawn_bundle(PbrBundle { + mesh: quad_handle.clone(), + material: material_handle, + transform: Transform { + translation: Vec3::new(0.0, 0.0, 1.5), + rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0), + ..Default::default() + }, + ..Default::default() + }); + // textured quad - modulated + commands.spawn_bundle(PbrBundle { + mesh: quad_handle.clone(), + material: red_material_handle, + transform: Transform { + translation: Vec3::new(0.0, 0.0, 0.0), + rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0), + ..Default::default() + }, + ..Default::default() + }); + // textured quad - modulated + commands.spawn_bundle(PbrBundle { + mesh: quad_handle, + material: blue_material_handle, + transform: Transform { + translation: Vec3::new(0.0, 0.0, -1.5), + rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0), + ..Default::default() + }, + ..Default::default() + }); + // camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(3.0, 5.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), + ..Default::default() + }); +} diff --git a/pipelined/bevy_pbr2/Cargo.toml b/pipelined/bevy_pbr2/Cargo.toml index 596afee895..e59352a998 100644 --- a/pipelined/bevy_pbr2/Cargo.toml +++ b/pipelined/bevy_pbr2/Cargo.toml @@ -25,6 +25,8 @@ bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" } bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" } # other +bitflags = "1.2" # direct dependency required for derive macro bytemuck = { version = "1", features = ["derive"] } -crevice = { path = "../../crates/crevice" } \ No newline at end of file +crevice = { path = "../../crates/crevice" } +wgpu = "0.8" diff --git a/pipelined/bevy_pbr2/src/bundle.rs b/pipelined/bevy_pbr2/src/bundle.rs index 933384edd6..dd9043d1db 100644 --- a/pipelined/bevy_pbr2/src/bundle.rs +++ b/pipelined/bevy_pbr2/src/bundle.rs @@ -1,4 +1,4 @@ -use crate::{PointLight, StandardMaterial}; +use crate::{OmniLight, StandardMaterial}; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; use bevy_render2::mesh::Mesh; @@ -25,8 +25,8 @@ impl Default for PbrBundle { /// A component bundle for "light" entities #[derive(Debug, Bundle, Default)] -pub struct PointLightBundle { - pub point_light: PointLight, +pub struct OmniLightBundle { + pub omni_light: OmniLight, pub transform: Transform, pub global_transform: GlobalTransform, } diff --git a/pipelined/bevy_pbr2/src/lib.rs b/pipelined/bevy_pbr2/src/lib.rs index cd006ec708..f82bca92d7 100644 --- a/pipelined/bevy_pbr2/src/lib.rs +++ b/pipelined/bevy_pbr2/src/lib.rs @@ -28,7 +28,8 @@ pub struct PbrPlugin; impl Plugin for PbrPlugin { fn build(&self, app: &mut App) { - app.add_plugin(StandardMaterialPlugin); + app.add_plugin(StandardMaterialPlugin) + .init_resource::(); let render_app = app.sub_app_mut(0); render_app @@ -46,6 +47,8 @@ impl Plugin for PbrPlugin { RenderStage::PhaseSort, sort_phase_system::.system(), ) + // FIXME: Hack to ensure RenderCommandQueue is initialized when PbrShaders is being initialized + // .init_resource::() .init_resource::() .init_resource::() .init_resource::() diff --git a/pipelined/bevy_pbr2/src/light.rs b/pipelined/bevy_pbr2/src/light.rs index 0f6404deec..24e9709ae1 100644 --- a/pipelined/bevy_pbr2/src/light.rs +++ b/pipelined/bevy_pbr2/src/light.rs @@ -2,19 +2,19 @@ use bevy_ecs::reflect::ReflectComponent; use bevy_reflect::Reflect; use bevy_render2::color::Color; -/// A point light +/// An omnidirectional light #[derive(Debug, Clone, Copy, Reflect)] #[reflect(Component)] -pub struct PointLight { +pub struct OmniLight { pub color: Color, pub intensity: f32, pub range: f32, pub radius: f32, } -impl Default for PointLight { +impl Default for OmniLight { fn default() -> Self { - PointLight { + OmniLight { color: Color::rgb(1.0, 1.0, 1.0), intensity: 200.0, range: 20.0, @@ -22,3 +22,20 @@ impl Default for PointLight { } } } + +// Ambient light color. +#[derive(Debug)] +pub struct AmbientLight { + pub color: Color, + /// Color is premultiplied by brightness before being passed to the shader + pub brightness: f32, +} + +impl Default for AmbientLight { + fn default() -> Self { + Self { + color: Color::rgb(1.0, 1.0, 1.0), + brightness: 0.05, + } + } +} diff --git a/pipelined/bevy_pbr2/src/material.rs b/pipelined/bevy_pbr2/src/material.rs index 3a33f3571b..0434f65717 100644 --- a/pipelined/bevy_pbr2/src/material.rs +++ b/pipelined/bevy_pbr2/src/material.rs @@ -1,20 +1,29 @@ -use bevy_app::{App, CoreStage, EventReader, Plugin}; -use bevy_asset::{AddAsset, AssetEvent, Assets}; -use bevy_ecs::prelude::*; +use bevy_app::{App, Plugin}; +use bevy_asset::{AddAsset, Handle}; use bevy_math::Vec4; use bevy_reflect::TypeUuid; use bevy_render2::{ color::Color, + render_asset::{RenderAsset, RenderAssetPlugin}, render_resource::{Buffer, BufferInitDescriptor, BufferUsage}, - renderer::RenderDevice, + renderer::{RenderDevice, RenderQueue}, + texture::Image, }; -use bevy_utils::HashSet; use crevice::std140::{AsStd140, Std140}; -// TODO: this shouldn't live in the StandardMaterial type -#[derive(Debug, Clone)] -pub struct StandardMaterialGpuData { - pub buffer: Buffer, +// NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.frag! +bitflags::bitflags! { + #[repr(transparent)] + struct StandardMaterialFlags: u32 { + const BASE_COLOR_TEXTURE = (1 << 0); + const EMISSIVE_TEXTURE = (1 << 1); + const METALLIC_ROUGHNESS_TEXTURE = (1 << 2); + const OCCLUSION_TEXTURE = (1 << 3); + const DOUBLE_SIDED = (1 << 4); + const UNLIT = (1 << 5); + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } } /// A material with "standard" properties used in PBR lighting @@ -23,46 +32,55 @@ pub struct StandardMaterialGpuData { #[uuid = "7494888b-c082-457b-aacf-517228cc0c22"] pub struct StandardMaterial { /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything - /// in between. - pub color: Color, - /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader - /// Defaults to minimum of 0.089 - pub roughness: f32, - /// From [0.0, 1.0], dielectric to pure metallic - pub metallic: f32, - /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] - /// defaults to 0.5 which is mapped to 4% reflectance in the shader - pub reflectance: f32, + /// in between. If used together with a base_color_texture, this is factored into the final + /// base color as `base_color * base_color_texture_value` + pub base_color: Color, + pub base_color_texture: Option>, // Use a color for user friendliness even though we technically don't use the alpha channel // Might be used in the future for exposure correction in HDR pub emissive: Color, - pub gpu_data: Option, -} - -impl StandardMaterial { - pub fn gpu_data(&self) -> Option<&StandardMaterialGpuData> { - self.gpu_data.as_ref() - } + pub emissive_texture: Option>, + /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader + /// Defaults to minimum of 0.089 + /// If used together with a roughness/metallic texture, this is factored into the final base + /// color as `roughness * roughness_texture_value` + pub perceptual_roughness: f32, + /// From [0.0, 1.0], dielectric to pure metallic + /// If used together with a roughness/metallic texture, this is factored into the final base + /// color as `metallic * metallic_texture_value` + pub metallic: f32, + pub metallic_roughness_texture: Option>, + /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] + /// defaults to 0.5 which is mapped to 4% reflectance in the shader + pub reflectance: f32, + pub occlusion_texture: Option>, + pub double_sided: bool, + pub unlit: bool, } impl Default for StandardMaterial { fn default() -> Self { StandardMaterial { - color: Color::rgb(1.0, 1.0, 1.0), + base_color: Color::rgb(1.0, 1.0, 1.0), + base_color_texture: None, + emissive: Color::BLACK, + emissive_texture: None, // This is the minimum the roughness is clamped to in shader code // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/ // It's the minimum floating point value that won't be rounded down to 0 in the // calculations used. Although technically for 32-bit floats, 0.045 could be // used. - roughness: 0.089, + perceptual_roughness: 0.089, // Few materials are purely dielectric or metallic // This is just a default for mostly-dielectric metallic: 0.01, + metallic_roughness_texture: None, // Minimum real-world reflectance is 2%, most materials between 2-5% // Expressed in a linear scale and equivalent to 4% reflectance see https://google.github.io/filament/Material%20Properties.pdf reflectance: 0.5, - emissive: Color::BLACK, - gpu_data: None, + occlusion_texture: None, + double_sided: false, + unlit: false, } } } @@ -70,7 +88,16 @@ impl Default for StandardMaterial { impl From for StandardMaterial { fn from(color: Color) -> Self { StandardMaterial { - color, + base_color: color, + ..Default::default() + } + } +} + +impl From> for StandardMaterial { + fn from(texture: Handle) -> Self { + StandardMaterial { + base_color_texture: Some(texture), ..Default::default() } } @@ -80,7 +107,10 @@ impl From for StandardMaterial { pub struct StandardMaterialUniformData { /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// in between. - pub color: Vec4, + pub base_color: Vec4, + // Use a color for user friendliness even though we technically don't use the alpha channel + // Might be used in the future for exposure correction in HDR + pub emissive: Vec4, /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader /// Defaults to minimum of 0.089 pub roughness: f32, @@ -89,71 +119,81 @@ pub struct StandardMaterialUniformData { /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] /// defaults to 0.5 which is mapped to 4% reflectance in the shader pub reflectance: f32, - // Use a color for user friendliness even though we technically don't use the alpha channel - // Might be used in the future for exposure correction in HDR - pub emissive: Vec4, + pub flags: u32, } pub struct StandardMaterialPlugin; impl Plugin for StandardMaterialPlugin { fn build(&self, app: &mut App) { - app.add_asset::().add_system_to_stage( - CoreStage::PostUpdate, - standard_material_resource_system.system(), - ); + app.add_plugin(RenderAssetPlugin::::default()) + .add_asset::(); } } -pub fn standard_material_resource_system( - render_device: Res, - mut materials: ResMut>, - mut material_events: EventReader>, -) { - let mut changed_materials = HashSet::default(); - for event in material_events.iter() { - match event { - AssetEvent::Created { ref handle } => { - changed_materials.insert(handle.clone_weak()); - } - AssetEvent::Modified { ref handle } => { - changed_materials.insert(handle.clone_weak()); - // TODO: uncomment this to support mutated materials - // remove_current_material_resources(render_resource_context, handle, &mut materials); - } - AssetEvent::Removed { ref handle } => { - // if material was modified and removed in the same update, ignore the modification - // events are ordered so future modification events are ok - changed_materials.remove(handle); - } - } +#[derive(Debug, Clone)] +pub struct GpuStandardMaterial { + pub buffer: Buffer, + // FIXME: image handles feel unnecessary here but the extracted asset is discarded + pub base_color_texture: Option>, + pub emissive_texture: Option>, + pub metallic_roughness_texture: Option>, + pub occlusion_texture: Option>, +} + +impl RenderAsset for StandardMaterial { + type ExtractedAsset = StandardMaterial; + type PreparedAsset = GpuStandardMaterial; + + fn extract_asset(&self) -> Self::ExtractedAsset { + self.clone() } - // update changed material data - for changed_material_handle in changed_materials.iter() { - if let Some(material) = materials.get_mut(changed_material_handle) { - // TODO: this avoids creating new materials each frame because storing gpu data in the material flags it as - // modified. this prevents hot reloading and therefore can't be used in an actual impl. - if material.gpu_data.is_some() { - continue; - } + fn prepare_asset( + material: Self::ExtractedAsset, + render_device: &RenderDevice, + _render_queue: &RenderQueue, + ) -> Self::PreparedAsset { + let mut flags = StandardMaterialFlags::NONE; + if material.base_color_texture.is_some() { + flags |= StandardMaterialFlags::BASE_COLOR_TEXTURE; + } + if material.emissive_texture.is_some() { + flags |= StandardMaterialFlags::EMISSIVE_TEXTURE; + } + if material.metallic_roughness_texture.is_some() { + flags |= StandardMaterialFlags::METALLIC_ROUGHNESS_TEXTURE; + } + if material.occlusion_texture.is_some() { + flags |= StandardMaterialFlags::OCCLUSION_TEXTURE; + } + if material.double_sided { + flags |= StandardMaterialFlags::DOUBLE_SIDED; + } + if material.unlit { + flags |= StandardMaterialFlags::UNLIT; + } + let value = StandardMaterialUniformData { + base_color: material.base_color.as_rgba_linear().into(), + emissive: material.emissive.into(), + roughness: material.perceptual_roughness, + metallic: material.metallic, + reflectance: material.reflectance, + flags: flags.bits, + }; + let value_std140 = value.as_std140(); - let value = StandardMaterialUniformData { - color: material.color.into(), - roughness: material.roughness, - metallic: material.metallic, - reflectance: material.reflectance, - emissive: material.emissive.into(), - }; - let value_std140 = value.as_std140(); - - let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { - label: None, - usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST, - contents: value_std140.as_bytes(), - }); - - material.gpu_data = Some(StandardMaterialGpuData { buffer }); + let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: None, + usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST, + contents: value_std140.as_bytes(), + }); + GpuStandardMaterial { + buffer, + base_color_texture: material.base_color_texture, + emissive_texture: material.emissive_texture, + metallic_roughness_texture: material.metallic_roughness_texture, + occlusion_texture: material.occlusion_texture, } } } diff --git a/pipelined/bevy_pbr2/src/render/light.rs b/pipelined/bevy_pbr2/src/render/light.rs index 45d36941ad..d9f9326c68 100644 --- a/pipelined/bevy_pbr2/src/render/light.rs +++ b/pipelined/bevy_pbr2/src/render/light.rs @@ -1,4 +1,4 @@ -use crate::{ExtractedMeshes, MeshMeta, PbrShaders, PointLight}; +use crate::{AmbientLight, ExtractedMeshes, MeshMeta, OmniLight, PbrShaders}; use bevy_ecs::{prelude::*, system::SystemState}; use bevy_math::{Mat4, Vec3, Vec4}; use bevy_render2::{ @@ -17,6 +17,11 @@ use bevy_transform::components::GlobalTransform; use crevice::std140::AsStd140; use std::num::NonZeroU32; +pub struct ExtractedAmbientLight { + color: Color, + brightness: f32, +} + pub struct ExtractedPointLight { color: Color, intensity: f32, @@ -38,16 +43,17 @@ pub struct GpuLight { #[repr(C)] #[derive(Copy, Clone, Debug, AsStd140)] pub struct GpuLights { + ambient_color: Vec4, len: u32, - lights: [GpuLight; MAX_POINT_LIGHTS], + lights: [GpuLight; MAX_OMNI_LIGHTS], } -// NOTE: this must be kept in sync MAX_POINT_LIGHTS in pbr.frag -pub const MAX_POINT_LIGHTS: usize = 10; +// NOTE: this must be kept in sync MAX_OMNI_LIGHTS in pbr.frag +pub const MAX_OMNI_LIGHTS: usize = 10; pub const SHADOW_SIZE: Extent3d = Extent3d { width: 1024, height: 1024, - depth_or_array_layers: MAX_POINT_LIGHTS as u32, + depth_or_array_layers: MAX_OMNI_LIGHTS as u32, }; pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; @@ -167,8 +173,13 @@ impl FromWorld for ShadowShaders { // TODO: ultimately these could be filtered down to lights relevant to actual views pub fn extract_lights( mut commands: Commands, - lights: Query<(Entity, &PointLight, &GlobalTransform)>, + ambient_light: Res, + lights: Query<(Entity, &OmniLight, &GlobalTransform)>, ) { + commands.insert_resource(ExtractedAmbientLight { + color: ambient_light.color, + brightness: ambient_light.brightness, + }); for (entity, light, transform) in lights.iter() { commands.get_or_spawn(entity).insert(ExtractedPointLight { color: light.color, @@ -203,6 +214,7 @@ pub fn prepare_lights( render_device: Res, mut light_meta: ResMut, views: Query>>, + ambient_light: Res, lights: Query<&ExtractedPointLight>, ) { // PERF: view.iter().count() could be views.iter().len() if we implemented ExactSizeIterator for archetype-only filters @@ -210,6 +222,7 @@ pub fn prepare_lights( .view_gpu_lights .reserve_and_clear(views.iter().count(), &render_device); + let ambient_color = ambient_light.color.as_rgba_linear() * ambient_light.brightness; // set up light data for each view for entity in views.iter() { let light_depth_texture = texture_cache.get( @@ -227,12 +240,13 @@ pub fn prepare_lights( let mut view_lights = Vec::new(); let mut gpu_lights = GpuLights { + ambient_color: ambient_color.into(), len: lights.iter().len() as u32, - lights: [GpuLight::default(); MAX_POINT_LIGHTS], + lights: [GpuLight::default(); MAX_OMNI_LIGHTS], }; // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query - for (i, light) in lights.iter().enumerate().take(MAX_POINT_LIGHTS) { + for (i, light) in lights.iter().enumerate().take(MAX_OMNI_LIGHTS) { let depth_texture_view = light_depth_texture .texture @@ -250,12 +264,13 @@ pub fn prepare_lights( let view_transform = GlobalTransform::from_translation(light.transform.translation) .looking_at(Vec3::default(), Vec3::Y); // TODO: configure light projection based on light configuration - let projection = Mat4::perspective_rh(1.0472, 1.0, 1.0, 20.0); + let projection = + Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, light.range); gpu_lights.lights[i] = GpuLight { // premultiply color by intensity // we don't use the alpha at all, so no reason to multiply only [0..3] - color: (light.color * light.intensity).into(), + color: (light.color.as_rgba_linear() * light.intensity).into(), radius: light.radius.into(), position: light.transform.translation.into(), range: 1.0 / (light.range * light.range), diff --git a/pipelined/bevy_pbr2/src/render/mod.rs b/pipelined/bevy_pbr2/src/render/mod.rs index 9879b01826..a974e3b157 100644 --- a/pipelined/bevy_pbr2/src/render/mod.rs +++ b/pipelined/bevy_pbr2/src/render/mod.rs @@ -11,15 +11,19 @@ use bevy_render2::{ render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::{Draw, DrawFunctions, Drawable, RenderPhase, TrackedRenderPass}, render_resource::*, - renderer::{RenderContext, RenderDevice}, + renderer::{RenderContext, RenderDevice, RenderQueue}, shader::Shader, - texture::BevyDefault, - view::{ViewMeta, ViewUniform, ViewUniformOffset}, + texture::{BevyDefault, GpuImage, Image, TextureFormatPixelInfo}, + view::{ExtractedView, ViewMeta, ViewUniform, ViewUniformOffset}, }; use bevy_transform::components::GlobalTransform; use bevy_utils::slab::{FrameSlabMap, FrameSlabMapKey}; use crevice::std140::AsStd140; use std::borrow::Cow; +use wgpu::{ + Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, TextureDimension, TextureFormat, + TextureViewDescriptor, +}; use crate::{StandardMaterial, StandardMaterialUniformData}; @@ -29,6 +33,8 @@ pub struct PbrShaders { view_layout: BindGroupLayout, material_layout: BindGroupLayout, mesh_layout: BindGroupLayout, + // This dummy white texture is to be used in place of optional StandardMaterial textures + dummy_white_gpu_image: GpuImage, } // TODO: this pattern for initializing the shaders / pipeline isn't ideal. this should be handled by the asset system @@ -122,18 +128,104 @@ impl FromWorld for PbrShaders { }); let material_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStage::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: BufferSize::new( - StandardMaterialUniformData::std140_size_static() as u64, - ), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: BufferSize::new( + StandardMaterialUniformData::std140_size_static() as u64, + ), + }, + count: None, }, - count: None, - }], + // Base Color Texture + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, + // Base Color Texture Sampler + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Sampler { + comparison: false, + filtering: true, + }, + count: None, + }, + // Emissive Texture + BindGroupLayoutEntry { + binding: 3, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, + // Emissive Texture Sampler + BindGroupLayoutEntry { + binding: 4, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Sampler { + comparison: false, + filtering: true, + }, + count: None, + }, + // Metallic Roughness Texture + BindGroupLayoutEntry { + binding: 5, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, + // Metallic Roughness Texture Sampler + BindGroupLayoutEntry { + binding: 6, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Sampler { + comparison: false, + filtering: true, + }, + count: None, + }, + // Occlusion Texture + BindGroupLayoutEntry { + binding: 7, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Texture { + multisampled: false, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }, + // Occlusion Texture Sampler + BindGroupLayoutEntry { + binding: 8, + visibility: ShaderStage::FRAGMENT, + ty: BindingType::Sampler { + comparison: false, + filtering: true, + }, + count: None, + }, + ], label: None, }); @@ -222,12 +314,53 @@ impl FromWorld for PbrShaders { }, }); + // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures + let dummy_white_gpu_image = { + let image = Image::new_fill( + Extent3d::default(), + TextureDimension::D2, + &[255u8; 4], + TextureFormat::bevy_default(), + ); + let texture = render_device.create_texture(&image.texture_descriptor); + let sampler = render_device.create_sampler(&image.sampler_descriptor); + + let format_size = image.texture_descriptor.format.pixel_size(); + let render_queue = world.get_resource_mut::().unwrap(); + render_queue.write_texture( + ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: Origin3d::ZERO, + }, + &image.data, + ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new( + image.texture_descriptor.size.width * format_size as u32, + ) + .unwrap(), + ), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + GpuImage { + texture, + texture_view, + sampler, + } + }; PbrShaders { pipeline, view_layout, material_layout, mesh_layout, vertex_shader_module, + dummy_white_gpu_image, } } } @@ -235,8 +368,8 @@ impl FromWorld for PbrShaders { struct ExtractedMesh { transform: Mat4, mesh: Handle, - material_buffer: Buffer, transform_binding_offset: u32, + material_handle: Handle, } pub struct ExtractedMeshes { @@ -247,6 +380,7 @@ pub fn extract_meshes( mut commands: Commands, meshes: Res>, materials: Res>, + images: Res>, query: Query<(&GlobalTransform, &Handle, &Handle)>, ) { let mut extracted_meshes = Vec::new(); @@ -254,15 +388,37 @@ pub fn extract_meshes( if !meshes.contains(mesh_handle) { continue; } + if let Some(material) = materials.get(material_handle) { - if let Some(material_gpu_data) = &material.gpu_data() { - extracted_meshes.push(ExtractedMesh { - transform: transform.compute_matrix(), - mesh: mesh_handle.clone_weak(), - material_buffer: material_gpu_data.buffer.clone(), - transform_binding_offset: 0, - }); + if let Some(ref image) = material.base_color_texture { + if !images.contains(image) { + continue; + } } + + if let Some(ref image) = material.emissive_texture { + if !images.contains(image) { + continue; + } + } + if let Some(ref image) = material.metallic_roughness_texture { + if !images.contains(image) { + continue; + } + } + if let Some(ref image) = material.occlusion_texture { + if !images.contains(image) { + continue; + } + } + extracted_meshes.push(ExtractedMesh { + transform: transform.compute_matrix(), + mesh: mesh_handle.clone_weak(), + transform_binding_offset: 0, + material_handle: material_handle.clone_weak(), + }); + } else { + continue; } } @@ -306,6 +462,23 @@ pub struct MeshViewBindGroups { view: BindGroup, } +fn image_handle_to_view_sampler<'a>( + pbr_shaders: &'a PbrShaders, + gpu_images: &'a RenderAssets, + image_option: &Option>, +) -> (&'a TextureView, &'a Sampler) { + image_option.as_ref().map_or( + ( + &pbr_shaders.dummy_white_gpu_image.texture_view, + &pbr_shaders.dummy_white_gpu_image.sampler, + ), + |image_handle| { + let gpu_image = gpu_images.get(image_handle).expect("only materials with valid textures should be drawn"); + (&gpu_image.texture_view, &gpu_image.sampler) + }, + ) +} + pub fn queue_meshes( mut commands: Commands, draw_functions: Res, @@ -316,7 +489,14 @@ pub fn queue_meshes( mut light_meta: ResMut, view_meta: Res, mut extracted_meshes: ResMut, - mut views: Query<(Entity, &ViewLights, &mut RenderPhase)>, + gpu_images: Res>, + render_materials: Res>, + mut views: Query<( + Entity, + &ExtractedView, + &ViewLights, + &mut RenderPhase, + )>, mut view_light_shadow_phases: Query<&mut RenderPhase>, ) { let mesh_meta = mesh_meta.into_inner(); @@ -346,7 +526,7 @@ pub fn queue_meshes( layout: &pbr_shaders.mesh_layout, }) }); - for (entity, view_lights, mut transparent_phase) in views.iter_mut() { + for (entity, view, view_lights, mut transparent_phase) in views.iter_mut() { // TODO: cache this? let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { entries: &[ @@ -379,30 +559,107 @@ pub fn queue_meshes( mesh_meta.mesh_draw_info.clear(); mesh_meta.material_bind_groups.next_frame(); + let view_matrix = view.transform.compute_matrix(); + let view_row_2 = view_matrix.row(2); for (i, mesh) in extracted_meshes.meshes.iter_mut().enumerate() { - let material_bind_group_key = mesh_meta.material_bind_groups.get_or_insert_with( - mesh.material_buffer.id(), - || { - render_device.create_bind_group(&BindGroupDescriptor { - entries: &[BindGroupEntry { - binding: 0, - resource: mesh.material_buffer.as_entire_binding(), - }], - label: None, - layout: &pbr_shaders.material_layout, - }) - }, - ); + let gpu_material = &render_materials + .get(&mesh.material_handle) + .expect("Failed to get StandardMaterial PreparedAsset"); + let material_bind_group_key = + mesh_meta + .material_bind_groups + .get_or_insert_with(gpu_material.buffer.id(), || { + let (base_color_texture_view, base_color_sampler) = + image_handle_to_view_sampler( + &pbr_shaders, + &gpu_images, + &gpu_material.base_color_texture, + ); + + let (emissive_texture_view, emissive_sampler) = + image_handle_to_view_sampler( + &pbr_shaders, + &gpu_images, + &gpu_material.emissive_texture, + ); + + let (metallic_roughness_texture_view, metallic_roughness_sampler) = + image_handle_to_view_sampler( + &pbr_shaders, + &gpu_images, + &gpu_material.metallic_roughness_texture, + ); + let (occlusion_texture_view, occlusion_sampler) = + image_handle_to_view_sampler( + &pbr_shaders, + &gpu_images, + &gpu_material.occlusion_texture, + ); + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: gpu_material.buffer.as_entire_binding(), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView( + &base_color_texture_view, + ), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::Sampler(&base_color_sampler), + }, + BindGroupEntry { + binding: 3, + resource: BindingResource::TextureView(&emissive_texture_view), + }, + BindGroupEntry { + binding: 4, + resource: BindingResource::Sampler(&emissive_sampler), + }, + BindGroupEntry { + binding: 5, + resource: BindingResource::TextureView( + &metallic_roughness_texture_view, + ), + }, + BindGroupEntry { + binding: 6, + resource: BindingResource::Sampler(&metallic_roughness_sampler), + }, + BindGroupEntry { + binding: 7, + resource: BindingResource::TextureView(&occlusion_texture_view), + }, + BindGroupEntry { + binding: 8, + resource: BindingResource::Sampler(&occlusion_sampler), + }, + ], + label: None, + layout: &pbr_shaders.material_layout, + }) + }); mesh_meta.mesh_draw_info.push(MeshDrawInfo { material_bind_group_key, }); + // NOTE: row 2 of the view matrix dotted with column 3 of the model matrix + // gives the z component of translation of the mesh in view space + let mesh_z = view_row_2.dot(mesh.transform.col(3)); + // FIXME: Switch from usize to u64 for portability and use sort key encoding + // similar to https://realtimecollisiondetection.net/blog/?p=86 as appropriate + // FIXME: What is the best way to map from view space z to a number of bits of unsigned integer? + let sort_key = (((mesh_z * 1000.0) as usize) << 10) + | (material_bind_group_key.index() & ((1 << 10) - 1)); // TODO: currently there is only "transparent phase". this should pick transparent vs opaque according to the mesh material transparent_phase.add(Drawable { draw_function: draw_pbr, draw_key: i, - sort_key: material_bind_group_key.index(), // TODO: sort back-to-front, sorting by material for now + sort_key, }); } @@ -499,6 +756,7 @@ impl Draw for DrawPbr { let mesh_draw_info = &mesh_meta.mesh_draw_info[draw_key]; pass.set_bind_group( 2, + // &mesh_meta.material_bind_groups[sort_key & ((1 << 10) - 1)], &mesh_meta.material_bind_groups[mesh_draw_info.material_bind_group_key], &[], ); diff --git a/pipelined/bevy_pbr2/src/render/pbr.frag b/pipelined/bevy_pbr2/src/render/pbr.frag index 8550958d70..8a2a8d1938 100644 --- a/pipelined/bevy_pbr2/src/render/pbr.frag +++ b/pipelined/bevy_pbr2/src/render/pbr.frag @@ -1,22 +1,74 @@ #version 450 +// From the Filament design doc +// https://google.github.io/filament/Filament.html#table_symbols +// Symbol Definition +// v View unit vector +// l Incident light unit vector +// n Surface normal unit vector +// h Half unit vector between l and v +// f BRDF +// f_d Diffuse component of a BRDF +// f_r Specular component of a BRDF +// α Roughness, remapped from using input perceptualRoughness +// σ Diffuse reflectance +// Ω Spherical domain +// f0 Reflectance at normal incidence +// f90 Reflectance at grazing angle +// χ+(a) Heaviside function (1 if a>0 and 0 otherwise) +// nior Index of refraction (IOR) of an interface +// ⟨n⋅l⟩ Dot product clamped to [0..1] +// ⟨a⟩ Saturated value (clamped to [0..1]) + +// The Bidirectional Reflectance Distribution Function (BRDF) describes the surface response of a standard material +// and consists of two components, the diffuse component (f_d) and the specular component (f_r): +// f(v,l) = f_d(v,l) + f_r(v,l) +// +// The form of the microfacet model is the same for diffuse and specular +// f_r(v,l) = f_d(v,l) = 1 / { |n⋅v||n⋅l| } ∫_Ω D(m,α) G(v,l,m) f_m(v,l,m) (v⋅m) (l⋅m) dm +// +// In which: +// D, also called the Normal Distribution Function (NDF) models the distribution of the microfacets +// G models the visibility (or occlusion or shadow-masking) of the microfacets +// f_m is the microfacet BRDF and differs between specular and diffuse components +// +// The above integration needs to be approximated. + layout(location = 0) in vec4 v_WorldPosition; layout(location = 1) in vec3 v_WorldNormal; layout(location = 2) in vec2 v_Uv; layout(location = 0) out vec4 o_Target; -struct PointLight { +struct OmniLight { vec4 color; float range; float radius; vec3 position; - mat4 projection; + mat4 view_projection; }; -// NOTE: this must be kept in sync with lights::MAX_LIGHTS +// NOTE: this must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs // TODO: this can be removed if we move to storage buffers for light arrays -const int MAX_POINT_LIGHTS = 10; +const int MAX_OMNI_LIGHTS = 10; + +struct StandardMaterial_t { + vec4 base_color; + vec4 emissive; + float perceptual_roughness; + float metallic; + float reflectance; + // 'flags' is a bit field indicating various option. uint is 32 bits so we have up to 32 options. + uint flags; +}; + +// NOTE: These must match those defined in bevy_pbr2/src/material.rs +const uint FLAGS_BASE_COLOR_TEXTURE_BIT = (1 << 0); +const uint FLAGS_EMISSIVE_TEXTURE_BIT = (1 << 1); +const uint FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT = (1 << 2); +const uint FLAGS_OCCLUSION_TEXTURE_BIT = (1 << 3); +const uint FLAGS_DOUBLE_SIDED_BIT = (1 << 4); +const uint FLAGS_UNLIT_BIT = (1 << 5); // View bindings - set 0 layout(set = 0, binding = 0) uniform View { @@ -24,24 +76,30 @@ layout(set = 0, binding = 0) uniform View { vec3 ViewWorldPosition; }; layout(std140, set = 0, binding = 1) uniform Lights { + vec4 AmbientColor; uint NumLights; - PointLight PointLights[MAX_POINT_LIGHTS]; + OmniLight OmniLights[MAX_OMNI_LIGHTS]; }; layout(set = 0, binding = 2) uniform texture2DArray t_Shadow; layout(set = 0, binding = 3) uniform samplerShadow s_Shadow; // Material bindings - set 2 -struct StandardMaterial_t { - vec4 color; - float roughness; - float metallic; - float reflectance; - vec4 emissive; -}; layout(set = 2, binding = 0) uniform StandardMaterial { StandardMaterial_t Material; }; +layout(set = 2, binding = 1) uniform texture2D base_color_texture; +layout(set = 2, binding = 2) uniform sampler base_color_sampler; + +layout(set = 2, binding = 3) uniform texture2D emissive_texture; +layout(set = 2, binding = 4) uniform sampler emissive_sampler; + +layout(set = 2, binding = 5) uniform texture2D metallic_roughness_texture; +layout(set = 2, binding = 6) uniform sampler metallic_roughness_sampler; + +layout(set = 2, binding = 7) uniform texture2D occlusion_texture; +layout(set = 2, binding = 8) uniform sampler occlusion_sampler; + # define saturate(x) clamp(x, 0.0, 1.0) const float PI = 3.141592653589793; @@ -200,7 +258,7 @@ vec3 reinhard_extended_luminance(vec3 color, float max_white_l) { return change_luminance(color, l_new); } -vec3 point_light(PointLight light, float roughness, float NdotV, vec3 N, vec3 V, vec3 R, vec3 F0, vec3 diffuseColor) { +vec3 omni_light(OmniLight light, float roughness, float NdotV, vec3 N, vec3 V, vec3 R, vec3 F0, vec3 diffuseColor) { vec3 light_to_frag = light.position.xyz - v_WorldPosition.xyz; float distance_square = dot(light_to_frag, light_to_frag); float rangeAttenuation = @@ -265,47 +323,101 @@ float fetch_shadow(int light_id, vec4 homogeneous_coords) { } void main() { - vec4 color = Material.color; - float metallic = Material.metallic; - float reflectance = Material.reflectance; - float perceptual_roughness = Material.roughness; - vec3 emissive = Material.emissive.xyz; - vec3 ambient_color = vec3(0.1, 0.1, 0.1); - float occlusion = 1.0; - - float roughness = perceptualRoughnessToRoughness(perceptual_roughness); - vec3 N = normalize(v_WorldNormal); - vec3 V = normalize(ViewWorldPosition.xyz - v_WorldPosition.xyz); - vec3 R = reflect(-V, N); - // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" - float NdotV = max(dot(N, V), 1e-4); - - // Remapping [0,1] reflectance to F0 - // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping - vec3 F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + color.rgb * metallic; - - // Diffuse strength inversely related to metallicity - vec3 diffuse_color = color.rgb * (1.0 - metallic); - - vec3 output_color = vec3(0.0); - for (int i = 0; i < int(NumLights); ++i) { - PointLight light = PointLights[i]; - vec3 light_contrib = point_light(light, roughness, NdotV, N, V, R, F0, diffuse_color); - float shadow = fetch_shadow(i, light.projection * v_WorldPosition); - output_color += light_contrib * shadow; + vec4 output_color = Material.base_color; + if ((Material.flags & FLAGS_BASE_COLOR_TEXTURE_BIT) != 0) { + output_color *= texture(sampler2D(base_color_texture, base_color_sampler), v_Uv); } - vec3 diffuse_ambient = EnvBRDFApprox(diffuse_color, 1.0, NdotV); - vec3 specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV); + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if ((Material.flags & FLAGS_UNLIT_BIT) == 0) { + // TODO use .a for exposure compensation in HDR + vec4 emissive = Material.emissive; + if ((Material.flags & FLAGS_EMISSIVE_TEXTURE_BIT) != 0) { + emissive.rgb *= texture(sampler2D(emissive_texture, emissive_sampler), v_Uv).rgb; + } - output_color += (diffuse_ambient + specular_ambient) * ambient_color * occlusion; - output_color += emissive * color.a; + // calculate non-linear roughness from linear perceptualRoughness + float metallic = Material.metallic; + float perceptual_roughness = Material.perceptual_roughness; + if ((Material.flags & FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0) { + vec4 metallic_roughness = texture(sampler2D(metallic_roughness_texture, metallic_roughness_sampler), v_Uv); + // Sampling from GLTF standard channels for now + metallic *= metallic_roughness.b; + perceptual_roughness *= metallic_roughness.g; + } + float roughness = perceptualRoughnessToRoughness(perceptual_roughness); - // tone_mapping - output_color = reinhard_luminance(output_color); - // Gamma correction. - // Not needed with sRGB buffer - // output_color = pow(output_color, vec3(1.0 / 2.2)); + float occlusion = 1.0; + if ((Material.flags & FLAGS_OCCLUSION_TEXTURE_BIT) != 0) { + occlusion = texture(sampler2D(occlusion_texture, occlusion_sampler), v_Uv).r; + } - o_Target = vec4(output_color, 1.0); + vec3 N = normalize(v_WorldNormal); + + // FIXME: Normal maps need an additional vertex attribute and vertex stage output/fragment stage input + // Just use a separate shader for lit with normal maps? + // # ifdef STANDARDMATERIAL_NORMAL_MAP + // vec3 T = normalize(v_WorldTangent.xyz); + // vec3 B = cross(N, T) * v_WorldTangent.w; + // # endif + + if ((Material.flags & FLAGS_DOUBLE_SIDED_BIT) != 0) { + N = gl_FrontFacing ? N : -N; + // # ifdef STANDARDMATERIAL_NORMAL_MAP + // T = gl_FrontFacing ? T : -T; + // B = gl_FrontFacing ? B : -B; + // # endif + } + + // # ifdef STANDARDMATERIAL_NORMAL_MAP + // mat3 TBN = mat3(T, B, N); + // N = TBN * normalize(texture(sampler2D(normal_map, normal_map_sampler), v_Uv).rgb * 2.0 - 1.0); + // # endif + + vec3 V; + if (ViewProj[3][3] != 1.0) { // If the projection is not orthographic + // Only valid for a perpective projection + V = normalize(ViewWorldPosition.xyz - v_WorldPosition.xyz); + } else { + // Ortho view vec + V = normalize(vec3(-ViewProj[0][2], -ViewProj[1][2], -ViewProj[2][2])); + } + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + float NdotV = max(dot(N, V), 1e-4); + + // Remapping [0,1] reflectance to F0 + // See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping + float reflectance = Material.reflectance; + vec3 F0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + output_color.rgb * metallic; + + // Diffuse strength inversely related to metallicity + vec3 diffuse_color = output_color.rgb * (1.0 - metallic); + + vec3 R = reflect(-V, N); + + // accumulate color + vec3 light_accum = vec3(0.0); + for (int i = 0; i < int(NumLights); ++i) { + OmniLight light = OmniLights[i]; + vec3 light_contrib = omni_light(light, roughness, NdotV, N, V, R, F0, diffuse_color); + float shadow = fetch_shadow(i, light.view_projection * v_WorldPosition); + light_accum += light_contrib * shadow; + } + + vec3 diffuse_ambient = EnvBRDFApprox(diffuse_color, 1.0, NdotV); + vec3 specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV); + + output_color.rgb = light_accum; + output_color.rgb += (diffuse_ambient + specular_ambient) * AmbientColor.rgb * occlusion; + output_color.rgb += emissive.rgb * output_color.a; + + // tone_mapping + output_color.rgb = reinhard_luminance(output_color.rgb); + // Gamma correction. + // Not needed with sRGB buffer + // output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2)); + } + + o_Target = output_color; } diff --git a/pipelined/bevy_pbr2/src/render/pbr.vert b/pipelined/bevy_pbr2/src/render/pbr.vert index 68544bd669..acc4a0bd87 100644 --- a/pipelined/bevy_pbr2/src/render/pbr.vert +++ b/pipelined/bevy_pbr2/src/render/pbr.vert @@ -19,7 +19,10 @@ layout(set = 1, binding = 0) uniform MeshTransform { void main() { v_Uv = Vertex_Uv; - v_WorldPosition = Model * vec4(Vertex_Position, 1.0); + vec4 world_position = Model * vec4(Vertex_Position, 1.0); + v_WorldPosition = world_position; + // FIXME: The inverse transpose of the model matrix should be used to correctly handle scaling + // of normals v_WorldNormal = mat3(Model) * Vertex_Normal; - gl_Position = ViewProj * v_WorldPosition; + gl_Position = ViewProj * world_position; } diff --git a/pipelined/bevy_render2/src/mesh/mesh/mod.rs b/pipelined/bevy_render2/src/mesh/mesh/mod.rs index 2e8fd131da..9f1946b668 100644 --- a/pipelined/bevy_render2/src/mesh/mesh/mod.rs +++ b/pipelined/bevy_render2/src/mesh/mesh/mod.rs @@ -544,22 +544,18 @@ impl RenderAsset for Mesh { _render_queue: &RenderQueue, ) -> Self::PreparedAsset { let vertex_buffer_data = mesh.get_vertex_buffer_data(); - let vertex_buffer = Buffer::from(render_device.create_buffer_with_data( - &BufferInitDescriptor { - usage: BufferUsage::VERTEX, - label: None, - contents: &vertex_buffer_data, - }, - )); + let vertex_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + usage: BufferUsage::VERTEX, + label: None, + contents: &vertex_buffer_data, + }); let index_info = mesh.get_index_buffer_bytes().map(|data| GpuIndexInfo { - buffer: Buffer::from(render_device.create_buffer_with_data( - &BufferInitDescriptor { - usage: BufferUsage::INDEX, - contents: &data, - label: None, - }, - )), + buffer: render_device.create_buffer_with_data(&BufferInitDescriptor { + usage: BufferUsage::INDEX, + contents: &data, + label: None, + }), count: mesh.indices().unwrap().len() as u32, }); diff --git a/pipelined/bevy_render2/src/mesh/shape/capsule.rs b/pipelined/bevy_render2/src/mesh/shape/capsule.rs index 9dffcc8303..7b6d268a9a 100644 --- a/pipelined/bevy_render2/src/mesh/shape/capsule.rs +++ b/pipelined/bevy_render2/src/mesh/shape/capsule.rs @@ -1,6 +1,4 @@ -use crate::{ - mesh::{Indices, Mesh}, -}; +use crate::mesh::{Indices, Mesh}; use bevy_math::{Vec2, Vec3}; use wgpu::PrimitiveTopology; diff --git a/pipelined/bevy_render2/src/render_resource/buffer.rs b/pipelined/bevy_render2/src/render_resource/buffer.rs index 2afe0ab155..a902af5834 100644 --- a/pipelined/bevy_render2/src/render_resource/buffer.rs +++ b/pipelined/bevy_render2/src/render_resource/buffer.rs @@ -1,5 +1,8 @@ use bevy_utils::Uuid; -use std::{ops::{Bound, Deref, RangeBounds}, sync::Arc}; +use std::{ + ops::{Bound, Deref, RangeBounds}, + sync::Arc, +}; #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] pub struct BufferId(Uuid); @@ -41,7 +44,7 @@ impl From for Buffer { id: BufferId(Uuid::new_v4()), value: Arc::new(value), } - } + } } impl Deref for Buffer { @@ -79,4 +82,4 @@ impl<'a> Deref for BufferSlice<'a> { fn deref(&self) -> &Self::Target { &self.value } -} \ No newline at end of file +} diff --git a/pipelined/bevy_render2/src/render_resource/mod.rs b/pipelined/bevy_render2/src/render_resource/mod.rs index b8f3f8fa4c..f1a655c014 100644 --- a/pipelined/bevy_render2/src/render_resource/mod.rs +++ b/pipelined/bevy_render2/src/render_resource/mod.rs @@ -21,11 +21,12 @@ pub use wgpu::{ BindGroupLayoutEntry, BindingResource, BindingType, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferBindingType, BufferSize, BufferUsage, ColorTargetState, ColorWrite, CompareFunction, DepthBiasState, DepthStencilState, Extent3d, - Face, FilterMode, FragmentState, FrontFace, IndexFormat, InputStepMode, MultisampleState, - PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, - RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, SamplerDescriptor, - ShaderFlags, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStage, StencilFaceState, - StencilState, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, - TextureSampleType, TextureUsage, TextureViewDescriptor, TextureViewDimension, VertexAttribute, - VertexBufferLayout, VertexFormat, VertexState,RenderPassDepthStencilAttachment, Operations, LoadOp + Face, FilterMode, FragmentState, FrontFace, IndexFormat, InputStepMode, LoadOp, + MultisampleState, Operations, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, + PrimitiveTopology, RenderPassColorAttachment, RenderPassDepthStencilAttachment, + RenderPassDescriptor, RenderPipelineDescriptor, SamplerDescriptor, ShaderFlags, ShaderModule, + ShaderModuleDescriptor, ShaderSource, ShaderStage, StencilFaceState, StencilState, + TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, + TextureUsage, TextureViewDescriptor, TextureViewDimension, VertexAttribute, VertexBufferLayout, + VertexFormat, VertexState, }; diff --git a/pipelined/bevy_render2/src/render_resource/texture.rs b/pipelined/bevy_render2/src/render_resource/texture.rs index 5474b2122f..8c778598cf 100644 --- a/pipelined/bevy_render2/src/render_resource/texture.rs +++ b/pipelined/bevy_render2/src/render_resource/texture.rs @@ -154,4 +154,4 @@ impl Deref for SwapChainFrame { fn deref(&self) -> &Self::Target { &self.value } -} \ No newline at end of file +} diff --git a/pipelined/bevy_render2/src/shader/mod.rs b/pipelined/bevy_render2/src/shader/mod.rs index a4b8270bf8..276fe41897 100644 --- a/pipelined/bevy_render2/src/shader/mod.rs +++ b/pipelined/bevy_render2/src/shader/mod.rs @@ -1,4 +1,4 @@ #[allow(clippy::module_inception)] mod shader; -pub use shader::*; \ No newline at end of file +pub use shader::*; diff --git a/pipelined/bevy_render2/src/shader/shader.rs b/pipelined/bevy_render2/src/shader/shader.rs index 75216d45cd..ec4249827c 100644 --- a/pipelined/bevy_render2/src/shader/shader.rs +++ b/pipelined/bevy_render2/src/shader/shader.rs @@ -1,10 +1,9 @@ use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; use bevy_reflect::{TypeUuid, Uuid}; use bevy_utils::{tracing::error, BoxedFuture}; -use wgpu::ShaderStage; use std::marker::Copy; use thiserror::Error; - +use wgpu::ShaderStage; /// An error that occurs during shader handling. #[derive(Error, Debug)] diff --git a/pipelined/bevy_render2/src/texture/mod.rs b/pipelined/bevy_render2/src/texture/mod.rs index 7ea379a88e..243c9fdc58 100644 --- a/pipelined/bevy_render2/src/texture/mod.rs +++ b/pipelined/bevy_render2/src/texture/mod.rs @@ -7,18 +7,15 @@ mod texture_cache; pub(crate) mod image_texture_conversion; +pub use self::image::*; #[cfg(feature = "hdr")] pub use hdr_texture_loader::*; -pub use self::image::*; pub use image_texture_loader::*; pub use texture_cache::*; -use crate::{ - render_asset::RenderAssetPlugin, - RenderStage, -}; +use crate::{render_asset::RenderAssetPlugin, RenderStage}; use bevy_app::{App, Plugin}; -use bevy_asset::{AddAsset}; +use bevy_asset::AddAsset; use bevy_ecs::prelude::*; // TODO: replace Texture names with Image names?