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 <aevyrie@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Robert Swain 2021-06-28 01:10:23 +02:00 committed by Carter Anderson
parent 25de2d1819
commit b1a91a823f
26 changed files with 891 additions and 245 deletions

View file

@ -166,6 +166,10 @@ path = "examples/3d/parenting.rs"
name = "pbr" name = "pbr"
path = "examples/3d/pbr.rs" path = "examples/3d/pbr.rs"
[[example]]
name = "pbr_pipelined"
path = "examples/3d/pbr_pipelined.rs"
[[example]] [[example]]
name = "render_to_texture" name = "render_to_texture"
path = "examples/3d/render_to_texture.rs" path = "examples/3d/render_to_texture.rs"
@ -178,6 +182,10 @@ path = "examples/3d/spawner.rs"
name = "texture" name = "texture"
path = "examples/3d/texture.rs" path = "examples/3d/texture.rs"
[[example]]
name = "texture_pipelined"
path = "examples/3d/texture_pipelined.rs"
[[example]] [[example]]
name = "update_gltf_scene" name = "update_gltf_scene"
path = "examples/3d/update_gltf_scene.rs" path = "examples/3d/update_gltf_scene.rs"

View file

@ -87,7 +87,10 @@ impl<Param: SystemParam> SystemState<Param> {
/// Retrieve the [`SystemParam`] values. This can only be called when all parameters are read-only. /// Retrieve the [`SystemParam`] values. This can only be called when all parameters are read-only.
#[inline] #[inline]
pub fn get<'s, 'w>(&'s mut self, world: &'w World) -> <Param::Fetch as SystemParamFetch<'s, 'w>>::Item pub fn get<'s, 'w>(
&'s mut self,
world: &'w World,
) -> <Param::Fetch as SystemParamFetch<'s, 'w>>::Item
where where
Param::Fetch: ReadOnlySystemParamFetch, Param::Fetch: ReadOnlySystemParamFetch,
{ {

View file

@ -372,7 +372,7 @@ where
pub fn get_mut( pub fn get_mut(
&mut self, &mut self,
entity: Entity, entity: Entity,
) -> Result<<Q::Fetch as Fetch<'w,'s>>::Item, QueryEntityError> { ) -> Result<<Q::Fetch as Fetch<'w, 's>>::Item, QueryEntityError> {
// SAFE: system runs without conflicts with other systems. // SAFE: system runs without conflicts with other systems.
// same-system queries have runtime borrow checks when they conflict // same-system queries have runtime borrow checks when they conflict
unsafe { unsafe {
@ -406,7 +406,10 @@ where
/// entity does not have the given component type or if the given component type does not match /// entity does not have the given component type or if the given component type does not match
/// this query. /// this query.
#[inline] #[inline]
pub fn get_component<T: Component>(&self, entity: Entity) -> Result<&'w T, QueryComponentError> { pub fn get_component<T: Component>(
&self,
entity: Entity,
) -> Result<&'w T, QueryComponentError> {
let world = self.world; let world = self.world;
let entity_ref = world let entity_ref = world
.get_entity(entity) .get_entity(entity)

View file

@ -146,7 +146,8 @@ where
fn default_config() {} fn default_config() {}
} }
impl<'s,'w, Q: WorldQuery + 'static, F: WorldQuery + 'static> SystemParamFetch<'s, 'w> for QueryState<Q, F> impl<'s, 'w, Q: WorldQuery + 'static, F: WorldQuery + 'static> SystemParamFetch<'s, 'w>
for QueryState<Q, F>
where where
F::Fetch: FilterFetch, F::Fetch: FilterFetch,
{ {

View file

@ -72,7 +72,7 @@ impl Command for PushChildren {
} }
impl<'s, 'w, 'a> ChildBuilder<'s, 'w, 'a> { 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); let e = self.commands.spawn_bundle(bundle);
self.push_children.children.push(e.id()); self.push_children.children.push(e.id());
e e

View file

@ -4,7 +4,7 @@ use bevy::{
ecs::prelude::*, ecs::prelude::*,
input::Input, input::Input,
math::Vec3, math::Vec3,
pbr2::{PbrBundle, PointLightBundle, StandardMaterial}, pbr2::{OmniLightBundle, PbrBundle, StandardMaterial},
prelude::{App, Assets, KeyCode, Transform}, prelude::{App, Assets, KeyCode, Transform},
render2::{ render2::{
camera::PerspectiveCameraBundle, camera::PerspectiveCameraBundle,
@ -36,8 +36,8 @@ fn setup(
commands.spawn_bundle(PbrBundle { commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })), mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })),
material: materials.add(StandardMaterial { material: materials.add(StandardMaterial {
color: Color::INDIGO, base_color: Color::INDIGO,
roughness: 1.0, perceptual_roughness: 1.0,
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@ -47,8 +47,8 @@ fn setup(
.spawn_bundle(PbrBundle { .spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(StandardMaterial { material: materials.add(StandardMaterial {
color: Color::PINK, base_color: Color::PINK,
roughness: 0.0, perceptual_roughness: 0.0,
metallic: 1.0, metallic: 1.0,
reflectance: 1.0, reflectance: 1.0,
..Default::default() ..Default::default()
@ -65,7 +65,7 @@ fn setup(
..Default::default() ..Default::default()
})), })),
material: materials.add(StandardMaterial { material: materials.add(StandardMaterial {
color: Color::LIME_GREEN, base_color: Color::LIME_GREEN,
..Default::default() ..Default::default()
}), }),
transform: Transform::from_xyz(1.5, 1.0, 1.5), transform: Transform::from_xyz(1.5, 1.0, 1.5),
@ -73,7 +73,7 @@ fn setup(
}) })
.insert(Movable); .insert(Movable);
// light // light
commands.spawn_bundle(PointLightBundle { commands.spawn_bundle(OmniLightBundle {
transform: Transform::from_xyz(5.0, 8.0, 2.0), transform: Transform::from_xyz(5.0, 8.0, 2.0),
..Default::default() ..Default::default()
}); });

View file

@ -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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// 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()
});
}

View file

@ -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<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// 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()
});
}

View file

@ -25,6 +25,8 @@ bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" }
bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" } bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" }
# other # other
bitflags = "1.2"
# direct dependency required for derive macro # direct dependency required for derive macro
bytemuck = { version = "1", features = ["derive"] } bytemuck = { version = "1", features = ["derive"] }
crevice = { path = "../../crates/crevice" } crevice = { path = "../../crates/crevice" }
wgpu = "0.8"

View file

@ -1,4 +1,4 @@
use crate::{PointLight, StandardMaterial}; use crate::{OmniLight, StandardMaterial};
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::Bundle;
use bevy_render2::mesh::Mesh; use bevy_render2::mesh::Mesh;
@ -25,8 +25,8 @@ impl Default for PbrBundle {
/// A component bundle for "light" entities /// A component bundle for "light" entities
#[derive(Debug, Bundle, Default)] #[derive(Debug, Bundle, Default)]
pub struct PointLightBundle { pub struct OmniLightBundle {
pub point_light: PointLight, pub omni_light: OmniLight,
pub transform: Transform, pub transform: Transform,
pub global_transform: GlobalTransform, pub global_transform: GlobalTransform,
} }

View file

@ -28,7 +28,8 @@ pub struct PbrPlugin;
impl Plugin for PbrPlugin { impl Plugin for PbrPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugin(StandardMaterialPlugin); app.add_plugin(StandardMaterialPlugin)
.init_resource::<AmbientLight>();
let render_app = app.sub_app_mut(0); let render_app = app.sub_app_mut(0);
render_app render_app
@ -46,6 +47,8 @@ impl Plugin for PbrPlugin {
RenderStage::PhaseSort, RenderStage::PhaseSort,
sort_phase_system::<ShadowPhase>.system(), sort_phase_system::<ShadowPhase>.system(),
) )
// FIXME: Hack to ensure RenderCommandQueue is initialized when PbrShaders is being initialized
// .init_resource::<RenderCommandQueue>()
.init_resource::<PbrShaders>() .init_resource::<PbrShaders>()
.init_resource::<ShadowShaders>() .init_resource::<ShadowShaders>()
.init_resource::<MeshMeta>() .init_resource::<MeshMeta>()

View file

@ -2,19 +2,19 @@ use bevy_ecs::reflect::ReflectComponent;
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use bevy_render2::color::Color; use bevy_render2::color::Color;
/// A point light /// An omnidirectional light
#[derive(Debug, Clone, Copy, Reflect)] #[derive(Debug, Clone, Copy, Reflect)]
#[reflect(Component)] #[reflect(Component)]
pub struct PointLight { pub struct OmniLight {
pub color: Color, pub color: Color,
pub intensity: f32, pub intensity: f32,
pub range: f32, pub range: f32,
pub radius: f32, pub radius: f32,
} }
impl Default for PointLight { impl Default for OmniLight {
fn default() -> Self { fn default() -> Self {
PointLight { OmniLight {
color: Color::rgb(1.0, 1.0, 1.0), color: Color::rgb(1.0, 1.0, 1.0),
intensity: 200.0, intensity: 200.0,
range: 20.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,
}
}
}

View file

@ -1,20 +1,29 @@
use bevy_app::{App, CoreStage, EventReader, Plugin}; use bevy_app::{App, Plugin};
use bevy_asset::{AddAsset, AssetEvent, Assets}; use bevy_asset::{AddAsset, Handle};
use bevy_ecs::prelude::*;
use bevy_math::Vec4; use bevy_math::Vec4;
use bevy_reflect::TypeUuid; use bevy_reflect::TypeUuid;
use bevy_render2::{ use bevy_render2::{
color::Color, color::Color,
render_asset::{RenderAsset, RenderAssetPlugin},
render_resource::{Buffer, BufferInitDescriptor, BufferUsage}, render_resource::{Buffer, BufferInitDescriptor, BufferUsage},
renderer::RenderDevice, renderer::{RenderDevice, RenderQueue},
texture::Image,
}; };
use bevy_utils::HashSet;
use crevice::std140::{AsStd140, Std140}; use crevice::std140::{AsStd140, Std140};
// TODO: this shouldn't live in the StandardMaterial type // NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.frag!
#[derive(Debug, Clone)] bitflags::bitflags! {
pub struct StandardMaterialGpuData { #[repr(transparent)]
pub buffer: Buffer, 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 /// A material with "standard" properties used in PBR lighting
@ -23,46 +32,55 @@ pub struct StandardMaterialGpuData {
#[uuid = "7494888b-c082-457b-aacf-517228cc0c22"] #[uuid = "7494888b-c082-457b-aacf-517228cc0c22"]
pub struct StandardMaterial { pub struct StandardMaterial {
/// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything
/// in between. /// in between. If used together with a base_color_texture, this is factored into the final
pub color: Color, /// base color as `base_color * base_color_texture_value`
/// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader pub base_color: Color,
/// Defaults to minimum of 0.089 pub base_color_texture: Option<Handle<Image>>,
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,
// Use a color for user friendliness even though we technically don't use the alpha channel // 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 // Might be used in the future for exposure correction in HDR
pub emissive: Color, pub emissive: Color,
pub gpu_data: Option<StandardMaterialGpuData>, pub emissive_texture: Option<Handle<Image>>,
} /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader
/// Defaults to minimum of 0.089
impl StandardMaterial { /// If used together with a roughness/metallic texture, this is factored into the final base
pub fn gpu_data(&self) -> Option<&StandardMaterialGpuData> { /// color as `roughness * roughness_texture_value`
self.gpu_data.as_ref() 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<Handle<Image>>,
/// 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<Handle<Image>>,
pub double_sided: bool,
pub unlit: bool,
} }
impl Default for StandardMaterial { impl Default for StandardMaterial {
fn default() -> Self { fn default() -> Self {
StandardMaterial { 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 // This is the minimum the roughness is clamped to in shader code
// See https://google.github.io/filament/Filament.html#materialsystem/parameterization/ // 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 // 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 // calculations used. Although technically for 32-bit floats, 0.045 could be
// used. // used.
roughness: 0.089, perceptual_roughness: 0.089,
// Few materials are purely dielectric or metallic // Few materials are purely dielectric or metallic
// This is just a default for mostly-dielectric // This is just a default for mostly-dielectric
metallic: 0.01, metallic: 0.01,
metallic_roughness_texture: None,
// Minimum real-world reflectance is 2%, most materials between 2-5% // 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 // Expressed in a linear scale and equivalent to 4% reflectance see https://google.github.io/filament/Material%20Properties.pdf
reflectance: 0.5, reflectance: 0.5,
emissive: Color::BLACK, occlusion_texture: None,
gpu_data: None, double_sided: false,
unlit: false,
} }
} }
} }
@ -70,7 +88,16 @@ impl Default for StandardMaterial {
impl From<Color> for StandardMaterial { impl From<Color> for StandardMaterial {
fn from(color: Color) -> Self { fn from(color: Color) -> Self {
StandardMaterial { StandardMaterial {
color, base_color: color,
..Default::default()
}
}
}
impl From<Handle<Image>> for StandardMaterial {
fn from(texture: Handle<Image>) -> Self {
StandardMaterial {
base_color_texture: Some(texture),
..Default::default() ..Default::default()
} }
} }
@ -80,7 +107,10 @@ impl From<Color> for StandardMaterial {
pub struct StandardMaterialUniformData { pub struct StandardMaterialUniformData {
/// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything
/// in between. /// 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 /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader
/// Defaults to minimum of 0.089 /// Defaults to minimum of 0.089
pub roughness: f32, pub roughness: f32,
@ -89,71 +119,81 @@ pub struct StandardMaterialUniformData {
/// Specular intensity for non-metals on a linear scale of [0.0, 1.0] /// 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 /// defaults to 0.5 which is mapped to 4% reflectance in the shader
pub reflectance: f32, pub reflectance: f32,
// Use a color for user friendliness even though we technically don't use the alpha channel pub flags: u32,
// Might be used in the future for exposure correction in HDR
pub emissive: Vec4,
} }
pub struct StandardMaterialPlugin; pub struct StandardMaterialPlugin;
impl Plugin for StandardMaterialPlugin { impl Plugin for StandardMaterialPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_asset::<StandardMaterial>().add_system_to_stage( app.add_plugin(RenderAssetPlugin::<StandardMaterial>::default())
CoreStage::PostUpdate, .add_asset::<StandardMaterial>();
standard_material_resource_system.system(),
);
} }
} }
pub fn standard_material_resource_system( #[derive(Debug, Clone)]
render_device: Res<RenderDevice>, pub struct GpuStandardMaterial {
mut materials: ResMut<Assets<StandardMaterial>>, pub buffer: Buffer,
mut material_events: EventReader<AssetEvent<StandardMaterial>>, // FIXME: image handles feel unnecessary here but the extracted asset is discarded
) { pub base_color_texture: Option<Handle<Image>>,
let mut changed_materials = HashSet::default(); pub emissive_texture: Option<Handle<Image>>,
for event in material_events.iter() { pub metallic_roughness_texture: Option<Handle<Image>>,
match event { pub occlusion_texture: Option<Handle<Image>>,
AssetEvent::Created { ref handle } => { }
changed_materials.insert(handle.clone_weak());
} impl RenderAsset for StandardMaterial {
AssetEvent::Modified { ref handle } => { type ExtractedAsset = StandardMaterial;
changed_materials.insert(handle.clone_weak()); type PreparedAsset = GpuStandardMaterial;
// TODO: uncomment this to support mutated materials
// remove_current_material_resources(render_resource_context, handle, &mut materials); fn extract_asset(&self) -> Self::ExtractedAsset {
} self.clone()
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);
}
}
} }
// update changed material data fn prepare_asset(
for changed_material_handle in changed_materials.iter() { material: Self::ExtractedAsset,
if let Some(material) = materials.get_mut(changed_material_handle) { render_device: &RenderDevice,
// TODO: this avoids creating new materials each frame because storing gpu data in the material flags it as _render_queue: &RenderQueue,
// modified. this prevents hot reloading and therefore can't be used in an actual impl. ) -> Self::PreparedAsset {
if material.gpu_data.is_some() { let mut flags = StandardMaterialFlags::NONE;
continue; 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 { let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
color: material.color.into(), label: None,
roughness: material.roughness, usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
metallic: material.metallic, contents: value_std140.as_bytes(),
reflectance: material.reflectance, });
emissive: material.emissive.into(), GpuStandardMaterial {
}; buffer,
let value_std140 = value.as_std140(); base_color_texture: material.base_color_texture,
emissive_texture: material.emissive_texture,
let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { metallic_roughness_texture: material.metallic_roughness_texture,
label: None, occlusion_texture: material.occlusion_texture,
usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
contents: value_std140.as_bytes(),
});
material.gpu_data = Some(StandardMaterialGpuData { buffer });
} }
} }
} }

View file

@ -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_ecs::{prelude::*, system::SystemState};
use bevy_math::{Mat4, Vec3, Vec4}; use bevy_math::{Mat4, Vec3, Vec4};
use bevy_render2::{ use bevy_render2::{
@ -17,6 +17,11 @@ use bevy_transform::components::GlobalTransform;
use crevice::std140::AsStd140; use crevice::std140::AsStd140;
use std::num::NonZeroU32; use std::num::NonZeroU32;
pub struct ExtractedAmbientLight {
color: Color,
brightness: f32,
}
pub struct ExtractedPointLight { pub struct ExtractedPointLight {
color: Color, color: Color,
intensity: f32, intensity: f32,
@ -38,16 +43,17 @@ pub struct GpuLight {
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, Debug, AsStd140)] #[derive(Copy, Clone, Debug, AsStd140)]
pub struct GpuLights { pub struct GpuLights {
ambient_color: Vec4,
len: u32, 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 // NOTE: this must be kept in sync MAX_OMNI_LIGHTS in pbr.frag
pub const MAX_POINT_LIGHTS: usize = 10; pub const MAX_OMNI_LIGHTS: usize = 10;
pub const SHADOW_SIZE: Extent3d = Extent3d { pub const SHADOW_SIZE: Extent3d = Extent3d {
width: 1024, width: 1024,
height: 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; 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 // TODO: ultimately these could be filtered down to lights relevant to actual views
pub fn extract_lights( pub fn extract_lights(
mut commands: Commands, mut commands: Commands,
lights: Query<(Entity, &PointLight, &GlobalTransform)>, ambient_light: Res<AmbientLight>,
lights: Query<(Entity, &OmniLight, &GlobalTransform)>,
) { ) {
commands.insert_resource(ExtractedAmbientLight {
color: ambient_light.color,
brightness: ambient_light.brightness,
});
for (entity, light, transform) in lights.iter() { for (entity, light, transform) in lights.iter() {
commands.get_or_spawn(entity).insert(ExtractedPointLight { commands.get_or_spawn(entity).insert(ExtractedPointLight {
color: light.color, color: light.color,
@ -203,6 +214,7 @@ pub fn prepare_lights(
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
mut light_meta: ResMut<LightMeta>, mut light_meta: ResMut<LightMeta>,
views: Query<Entity, With<RenderPhase<Transparent3dPhase>>>, views: Query<Entity, With<RenderPhase<Transparent3dPhase>>>,
ambient_light: Res<ExtractedAmbientLight>,
lights: Query<&ExtractedPointLight>, lights: Query<&ExtractedPointLight>,
) { ) {
// PERF: view.iter().count() could be views.iter().len() if we implemented ExactSizeIterator for archetype-only filters // 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 .view_gpu_lights
.reserve_and_clear(views.iter().count(), &render_device); .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 // set up light data for each view
for entity in views.iter() { for entity in views.iter() {
let light_depth_texture = texture_cache.get( let light_depth_texture = texture_cache.get(
@ -227,12 +240,13 @@ pub fn prepare_lights(
let mut view_lights = Vec::new(); let mut view_lights = Vec::new();
let mut gpu_lights = GpuLights { let mut gpu_lights = GpuLights {
ambient_color: ambient_color.into(),
len: lights.iter().len() as u32, 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 // 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 = let depth_texture_view =
light_depth_texture light_depth_texture
.texture .texture
@ -250,12 +264,13 @@ pub fn prepare_lights(
let view_transform = GlobalTransform::from_translation(light.transform.translation) let view_transform = GlobalTransform::from_translation(light.transform.translation)
.looking_at(Vec3::default(), Vec3::Y); .looking_at(Vec3::default(), Vec3::Y);
// TODO: configure light projection based on light configuration // 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 { gpu_lights.lights[i] = GpuLight {
// premultiply color by intensity // premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3] // 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(), radius: light.radius.into(),
position: light.transform.translation.into(), position: light.transform.translation.into(),
range: 1.0 / (light.range * light.range), range: 1.0 / (light.range * light.range),

View file

@ -11,15 +11,19 @@ use bevy_render2::{
render_graph::{Node, NodeRunError, RenderGraphContext}, render_graph::{Node, NodeRunError, RenderGraphContext},
render_phase::{Draw, DrawFunctions, Drawable, RenderPhase, TrackedRenderPass}, render_phase::{Draw, DrawFunctions, Drawable, RenderPhase, TrackedRenderPass},
render_resource::*, render_resource::*,
renderer::{RenderContext, RenderDevice}, renderer::{RenderContext, RenderDevice, RenderQueue},
shader::Shader, shader::Shader,
texture::BevyDefault, texture::{BevyDefault, GpuImage, Image, TextureFormatPixelInfo},
view::{ViewMeta, ViewUniform, ViewUniformOffset}, view::{ExtractedView, ViewMeta, ViewUniform, ViewUniformOffset},
}; };
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;
use bevy_utils::slab::{FrameSlabMap, FrameSlabMapKey}; use bevy_utils::slab::{FrameSlabMap, FrameSlabMapKey};
use crevice::std140::AsStd140; use crevice::std140::AsStd140;
use std::borrow::Cow; use std::borrow::Cow;
use wgpu::{
Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, TextureDimension, TextureFormat,
TextureViewDescriptor,
};
use crate::{StandardMaterial, StandardMaterialUniformData}; use crate::{StandardMaterial, StandardMaterialUniformData};
@ -29,6 +33,8 @@ pub struct PbrShaders {
view_layout: BindGroupLayout, view_layout: BindGroupLayout,
material_layout: BindGroupLayout, material_layout: BindGroupLayout,
mesh_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 // 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 { let material_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
entries: &[BindGroupLayoutEntry { entries: &[
binding: 0, BindGroupLayoutEntry {
visibility: ShaderStage::FRAGMENT, binding: 0,
ty: BindingType::Buffer { visibility: ShaderStage::FRAGMENT,
ty: BufferBindingType::Uniform, ty: BindingType::Buffer {
has_dynamic_offset: false, ty: BufferBindingType::Uniform,
min_binding_size: BufferSize::new( has_dynamic_offset: false,
StandardMaterialUniformData::std140_size_static() as u64, 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, 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::<RenderQueue>().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 { PbrShaders {
pipeline, pipeline,
view_layout, view_layout,
material_layout, material_layout,
mesh_layout, mesh_layout,
vertex_shader_module, vertex_shader_module,
dummy_white_gpu_image,
} }
} }
} }
@ -235,8 +368,8 @@ impl FromWorld for PbrShaders {
struct ExtractedMesh { struct ExtractedMesh {
transform: Mat4, transform: Mat4,
mesh: Handle<Mesh>, mesh: Handle<Mesh>,
material_buffer: Buffer,
transform_binding_offset: u32, transform_binding_offset: u32,
material_handle: Handle<StandardMaterial>,
} }
pub struct ExtractedMeshes { pub struct ExtractedMeshes {
@ -247,6 +380,7 @@ pub fn extract_meshes(
mut commands: Commands, mut commands: Commands,
meshes: Res<Assets<Mesh>>, meshes: Res<Assets<Mesh>>,
materials: Res<Assets<StandardMaterial>>, materials: Res<Assets<StandardMaterial>>,
images: Res<Assets<Image>>,
query: Query<(&GlobalTransform, &Handle<Mesh>, &Handle<StandardMaterial>)>, query: Query<(&GlobalTransform, &Handle<Mesh>, &Handle<StandardMaterial>)>,
) { ) {
let mut extracted_meshes = Vec::new(); let mut extracted_meshes = Vec::new();
@ -254,15 +388,37 @@ pub fn extract_meshes(
if !meshes.contains(mesh_handle) { if !meshes.contains(mesh_handle) {
continue; continue;
} }
if let Some(material) = materials.get(material_handle) { if let Some(material) = materials.get(material_handle) {
if let Some(material_gpu_data) = &material.gpu_data() { if let Some(ref image) = material.base_color_texture {
extracted_meshes.push(ExtractedMesh { if !images.contains(image) {
transform: transform.compute_matrix(), continue;
mesh: mesh_handle.clone_weak(), }
material_buffer: material_gpu_data.buffer.clone(),
transform_binding_offset: 0,
});
} }
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, view: BindGroup,
} }
fn image_handle_to_view_sampler<'a>(
pbr_shaders: &'a PbrShaders,
gpu_images: &'a RenderAssets<Image>,
image_option: &Option<Handle<Image>>,
) -> (&'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( pub fn queue_meshes(
mut commands: Commands, mut commands: Commands,
draw_functions: Res<DrawFunctions>, draw_functions: Res<DrawFunctions>,
@ -316,7 +489,14 @@ pub fn queue_meshes(
mut light_meta: ResMut<LightMeta>, mut light_meta: ResMut<LightMeta>,
view_meta: Res<ViewMeta>, view_meta: Res<ViewMeta>,
mut extracted_meshes: ResMut<ExtractedMeshes>, mut extracted_meshes: ResMut<ExtractedMeshes>,
mut views: Query<(Entity, &ViewLights, &mut RenderPhase<Transparent3dPhase>)>, gpu_images: Res<RenderAssets<Image>>,
render_materials: Res<RenderAssets<StandardMaterial>>,
mut views: Query<(
Entity,
&ExtractedView,
&ViewLights,
&mut RenderPhase<Transparent3dPhase>,
)>,
mut view_light_shadow_phases: Query<&mut RenderPhase<ShadowPhase>>, mut view_light_shadow_phases: Query<&mut RenderPhase<ShadowPhase>>,
) { ) {
let mesh_meta = mesh_meta.into_inner(); let mesh_meta = mesh_meta.into_inner();
@ -346,7 +526,7 @@ pub fn queue_meshes(
layout: &pbr_shaders.mesh_layout, 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? // TODO: cache this?
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[ entries: &[
@ -379,30 +559,107 @@ pub fn queue_meshes(
mesh_meta.mesh_draw_info.clear(); mesh_meta.mesh_draw_info.clear();
mesh_meta.material_bind_groups.next_frame(); 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() { for (i, mesh) in extracted_meshes.meshes.iter_mut().enumerate() {
let material_bind_group_key = mesh_meta.material_bind_groups.get_or_insert_with( let gpu_material = &render_materials
mesh.material_buffer.id(), .get(&mesh.material_handle)
|| { .expect("Failed to get StandardMaterial PreparedAsset");
render_device.create_bind_group(&BindGroupDescriptor { let material_bind_group_key =
entries: &[BindGroupEntry { mesh_meta
binding: 0, .material_bind_groups
resource: mesh.material_buffer.as_entire_binding(), .get_or_insert_with(gpu_material.buffer.id(), || {
}], let (base_color_texture_view, base_color_sampler) =
label: None, image_handle_to_view_sampler(
layout: &pbr_shaders.material_layout, &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 { mesh_meta.mesh_draw_info.push(MeshDrawInfo {
material_bind_group_key, 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 // TODO: currently there is only "transparent phase". this should pick transparent vs opaque according to the mesh material
transparent_phase.add(Drawable { transparent_phase.add(Drawable {
draw_function: draw_pbr, draw_function: draw_pbr,
draw_key: i, 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]; let mesh_draw_info = &mesh_meta.mesh_draw_info[draw_key];
pass.set_bind_group( pass.set_bind_group(
2, 2,
// &mesh_meta.material_bind_groups[sort_key & ((1 << 10) - 1)],
&mesh_meta.material_bind_groups[mesh_draw_info.material_bind_group_key], &mesh_meta.material_bind_groups[mesh_draw_info.material_bind_group_key],
&[], &[],
); );

View file

@ -1,22 +1,74 @@
#version 450 #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 = 0) in vec4 v_WorldPosition;
layout(location = 1) in vec3 v_WorldNormal; layout(location = 1) in vec3 v_WorldNormal;
layout(location = 2) in vec2 v_Uv; layout(location = 2) in vec2 v_Uv;
layout(location = 0) out vec4 o_Target; layout(location = 0) out vec4 o_Target;
struct PointLight { struct OmniLight {
vec4 color; vec4 color;
float range; float range;
float radius; float radius;
vec3 position; 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 // 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 // View bindings - set 0
layout(set = 0, binding = 0) uniform View { layout(set = 0, binding = 0) uniform View {
@ -24,24 +76,30 @@ layout(set = 0, binding = 0) uniform View {
vec3 ViewWorldPosition; vec3 ViewWorldPosition;
}; };
layout(std140, set = 0, binding = 1) uniform Lights { layout(std140, set = 0, binding = 1) uniform Lights {
vec4 AmbientColor;
uint NumLights; 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 = 2) uniform texture2DArray t_Shadow;
layout(set = 0, binding = 3) uniform samplerShadow s_Shadow; layout(set = 0, binding = 3) uniform samplerShadow s_Shadow;
// Material bindings - set 2 // Material bindings - set 2
struct StandardMaterial_t {
vec4 color;
float roughness;
float metallic;
float reflectance;
vec4 emissive;
};
layout(set = 2, binding = 0) uniform StandardMaterial { layout(set = 2, binding = 0) uniform StandardMaterial {
StandardMaterial_t Material; 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) # define saturate(x) clamp(x, 0.0, 1.0)
const float PI = 3.141592653589793; 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); 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; vec3 light_to_frag = light.position.xyz - v_WorldPosition.xyz;
float distance_square = dot(light_to_frag, light_to_frag); float distance_square = dot(light_to_frag, light_to_frag);
float rangeAttenuation = float rangeAttenuation =
@ -265,47 +323,101 @@ float fetch_shadow(int light_id, vec4 homogeneous_coords) {
} }
void main() { void main() {
vec4 color = Material.color; vec4 output_color = Material.base_color;
float metallic = Material.metallic; if ((Material.flags & FLAGS_BASE_COLOR_TEXTURE_BIT) != 0) {
float reflectance = Material.reflectance; output_color *= texture(sampler2D(base_color_texture, base_color_sampler), v_Uv);
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;
} }
vec3 diffuse_ambient = EnvBRDFApprox(diffuse_color, 1.0, NdotV); // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit
vec3 specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV); 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; // calculate non-linear roughness from linear perceptualRoughness
output_color += emissive * color.a; 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 float occlusion = 1.0;
output_color = reinhard_luminance(output_color); if ((Material.flags & FLAGS_OCCLUSION_TEXTURE_BIT) != 0) {
// Gamma correction. occlusion = texture(sampler2D(occlusion_texture, occlusion_sampler), v_Uv).r;
// Not needed with sRGB buffer }
// output_color = pow(output_color, vec3(1.0 / 2.2));
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;
} }

View file

@ -19,7 +19,10 @@ layout(set = 1, binding = 0) uniform MeshTransform {
void main() { void main() {
v_Uv = Vertex_Uv; 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; v_WorldNormal = mat3(Model) * Vertex_Normal;
gl_Position = ViewProj * v_WorldPosition; gl_Position = ViewProj * world_position;
} }

View file

@ -544,22 +544,18 @@ impl RenderAsset for Mesh {
_render_queue: &RenderQueue, _render_queue: &RenderQueue,
) -> Self::PreparedAsset { ) -> Self::PreparedAsset {
let vertex_buffer_data = mesh.get_vertex_buffer_data(); let vertex_buffer_data = mesh.get_vertex_buffer_data();
let vertex_buffer = Buffer::from(render_device.create_buffer_with_data( let vertex_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
&BufferInitDescriptor { usage: BufferUsage::VERTEX,
usage: BufferUsage::VERTEX, label: None,
label: None, contents: &vertex_buffer_data,
contents: &vertex_buffer_data, });
},
));
let index_info = mesh.get_index_buffer_bytes().map(|data| GpuIndexInfo { let index_info = mesh.get_index_buffer_bytes().map(|data| GpuIndexInfo {
buffer: Buffer::from(render_device.create_buffer_with_data( buffer: render_device.create_buffer_with_data(&BufferInitDescriptor {
&BufferInitDescriptor { usage: BufferUsage::INDEX,
usage: BufferUsage::INDEX, contents: &data,
contents: &data, label: None,
label: None, }),
},
)),
count: mesh.indices().unwrap().len() as u32, count: mesh.indices().unwrap().len() as u32,
}); });

View file

@ -1,6 +1,4 @@
use crate::{ use crate::mesh::{Indices, Mesh};
mesh::{Indices, Mesh},
};
use bevy_math::{Vec2, Vec3}; use bevy_math::{Vec2, Vec3};
use wgpu::PrimitiveTopology; use wgpu::PrimitiveTopology;

View file

@ -1,5 +1,8 @@
use bevy_utils::Uuid; 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)] #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)]
pub struct BufferId(Uuid); pub struct BufferId(Uuid);

View file

@ -21,11 +21,12 @@ pub use wgpu::{
BindGroupLayoutEntry, BindingResource, BindingType, BlendComponent, BlendFactor, BindGroupLayoutEntry, BindingResource, BindingType, BlendComponent, BlendFactor,
BlendOperation, BlendState, BufferAddress, BufferBindingType, BufferSize, BufferUsage, BlendOperation, BlendState, BufferAddress, BufferBindingType, BufferSize, BufferUsage,
ColorTargetState, ColorWrite, CompareFunction, DepthBiasState, DepthStencilState, Extent3d, ColorTargetState, ColorWrite, CompareFunction, DepthBiasState, DepthStencilState, Extent3d,
Face, FilterMode, FragmentState, FrontFace, IndexFormat, InputStepMode, MultisampleState, Face, FilterMode, FragmentState, FrontFace, IndexFormat, InputStepMode, LoadOp,
PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, MultisampleState, Operations, PipelineLayoutDescriptor, PolygonMode, PrimitiveState,
RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, SamplerDescriptor, PrimitiveTopology, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
ShaderFlags, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStage, StencilFaceState, RenderPassDescriptor, RenderPipelineDescriptor, SamplerDescriptor, ShaderFlags, ShaderModule,
StencilState, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, ShaderModuleDescriptor, ShaderSource, ShaderStage, StencilFaceState, StencilState,
TextureSampleType, TextureUsage, TextureViewDescriptor, TextureViewDimension, VertexAttribute, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType,
VertexBufferLayout, VertexFormat, VertexState,RenderPassDepthStencilAttachment, Operations, LoadOp TextureUsage, TextureViewDescriptor, TextureViewDimension, VertexAttribute, VertexBufferLayout,
VertexFormat, VertexState,
}; };

View file

@ -1,10 +1,9 @@
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
use bevy_reflect::{TypeUuid, Uuid}; use bevy_reflect::{TypeUuid, Uuid};
use bevy_utils::{tracing::error, BoxedFuture}; use bevy_utils::{tracing::error, BoxedFuture};
use wgpu::ShaderStage;
use std::marker::Copy; use std::marker::Copy;
use thiserror::Error; use thiserror::Error;
use wgpu::ShaderStage;
/// An error that occurs during shader handling. /// An error that occurs during shader handling.
#[derive(Error, Debug)] #[derive(Error, Debug)]

View file

@ -7,18 +7,15 @@ mod texture_cache;
pub(crate) mod image_texture_conversion; pub(crate) mod image_texture_conversion;
pub use self::image::*;
#[cfg(feature = "hdr")] #[cfg(feature = "hdr")]
pub use hdr_texture_loader::*; pub use hdr_texture_loader::*;
pub use self::image::*;
pub use image_texture_loader::*; pub use image_texture_loader::*;
pub use texture_cache::*; pub use texture_cache::*;
use crate::{ use crate::{render_asset::RenderAssetPlugin, RenderStage};
render_asset::RenderAssetPlugin,
RenderStage,
};
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_asset::{AddAsset}; use bevy_asset::AddAsset;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
// TODO: replace Texture names with Image names? // TODO: replace Texture names with Image names?