Support for normal maps including from glTF models (#2741)

# Objective

- Support tangent vertex attributes, and normal maps
- Support loading these from glTF models

## Solution

- Make two pipelines in both the shadow and pbr passes, one for without normal maps, one for with normal maps
- Select the correct pipeline to bind based on the presence of the normal map texture
- Share the vertex attribute layout between shadow and pbr passes
- Refactored pbr.wgsl to share a bunch of common code between the normal map and non-normal map entry points. I tried to do this in a way that will allow custom shader reuse.

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Robert Swain 2021-11-04 21:47:57 +00:00
parent 85487707ef
commit 09706cdb2a
7 changed files with 313 additions and 110 deletions

View file

@ -128,12 +128,12 @@ async fn load_gltf<'a, 'b>(
mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute); mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute);
} }
// if let Some(vertex_attribute) = reader if let Some(vertex_attribute) = reader
// .read_tangents() .read_tangents()
// .map(|v| VertexAttributeValues::Float32x4(v.collect())) .map(|v| VertexAttributeValues::Float32x4(v.collect()))
// { {
// mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute);
// } }
if let Some(vertex_attribute) = reader if let Some(vertex_attribute) = reader
.read_tex_coords(0) .read_tex_coords(0)
@ -382,15 +382,16 @@ fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<
None None
}; };
// let normal_map: Option<Handle<Texture>> = if let Some(normal_texture) = material.normal_texture() { let normal_map_texture: Option<Handle<Image>> =
// // TODO: handle normal_texture.scale if let Some(normal_texture) = material.normal_texture() {
// // TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords) // TODO: handle normal_texture.scale
// let label = texture_label(&normal_texture.texture()); // TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords)
// let path = AssetPath::new_ref(load_context.path(), Some(&label)); let label = texture_label(&normal_texture.texture());
// Some(load_context.get_handle(path)) let path = AssetPath::new_ref(load_context.path(), Some(&label));
// } else { Some(load_context.get_handle(path))
// None } else {
// }; None
};
let metallic_roughness_texture = if let Some(info) = pbr.metallic_roughness_texture() { let metallic_roughness_texture = if let Some(info) = pbr.metallic_roughness_texture() {
// TODO: handle info.tex_coord() (the *set* index for the right texcoords) // TODO: handle info.tex_coord() (the *set* index for the right texcoords)
@ -430,7 +431,7 @@ fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<
perceptual_roughness: pbr.roughness_factor(), perceptual_roughness: pbr.roughness_factor(),
metallic: pbr.metallic_factor(), metallic: pbr.metallic_factor(),
metallic_roughness_texture, metallic_roughness_texture,
// normal_map, normal_map_texture,
double_sided: material.double_sided(), double_sided: material.double_sided(),
occlusion_texture, occlusion_texture,
emissive: Color::rgba(emissive[0], emissive[1], emissive[2], 1.0), emissive: Color::rgba(emissive[0], emissive[1], emissive[2], 1.0),

View file

@ -70,7 +70,8 @@ impl Plugin for PbrPlugin {
.init_resource::<ShadowPipeline>() .init_resource::<ShadowPipeline>()
.init_resource::<DrawFunctions<Shadow>>() .init_resource::<DrawFunctions<Shadow>>()
.init_resource::<LightMeta>() .init_resource::<LightMeta>()
.init_resource::<SpecializedPipelines<PbrPipeline>>(); .init_resource::<SpecializedPipelines<PbrPipeline>>()
.init_resource::<SpecializedPipelines<ShadowPipeline>>();
let draw_shadow_mesh = DrawShadowMesh::new(&mut render_app.world); let draw_shadow_mesh = DrawShadowMesh::new(&mut render_app.world);
let shadow_pass_node = ShadowPassNode::new(&mut render_app.world); let shadow_pass_node = ShadowPassNode::new(&mut render_app.world);

View file

@ -1,4 +1,4 @@
use crate::PbrPipeline; use crate::{PbrPipeline, StandardMaterialFlags};
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_asset::{AddAsset, Handle}; use bevy_asset::{AddAsset, Handle};
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
@ -16,21 +16,6 @@ use bevy_render2::{
use crevice::std140::{AsStd140, Std140}; use crevice::std140::{AsStd140, Std140};
use wgpu::{BindGroupDescriptor, BindGroupEntry, BindingResource}; use wgpu::{BindGroupDescriptor, BindGroupEntry, BindingResource};
// 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 /// A material with "standard" properties used in PBR lighting
/// Standard property values with pictures here https://google.github.io/filament/Material%20Properties.pdf /// Standard property values with pictures here https://google.github.io/filament/Material%20Properties.pdf
#[derive(Debug, Clone, TypeUuid)] #[derive(Debug, Clone, TypeUuid)]
@ -58,6 +43,7 @@ pub struct StandardMaterial {
/// 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,
pub normal_map_texture: Option<Handle<Image>>,
pub occlusion_texture: Option<Handle<Image>>, pub occlusion_texture: Option<Handle<Image>>,
pub double_sided: bool, pub double_sided: bool,
pub unlit: bool, pub unlit: bool,
@ -84,6 +70,7 @@ impl Default for StandardMaterial {
// 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,
occlusion_texture: None, occlusion_texture: None,
normal_map_texture: None,
double_sided: false, double_sided: false,
unlit: false, unlit: false,
} }
@ -140,6 +127,7 @@ impl Plugin for StandardMaterialPlugin {
pub struct GpuStandardMaterial { pub struct GpuStandardMaterial {
pub buffer: Buffer, pub buffer: Buffer,
pub bind_group: BindGroup, pub bind_group: BindGroup,
pub flags: StandardMaterialFlags,
} }
impl RenderAsset for StandardMaterial { impl RenderAsset for StandardMaterial {
@ -185,6 +173,13 @@ impl RenderAsset for StandardMaterial {
} else { } else {
return Err(PrepareAssetError::RetryNextUpdate(material)); return Err(PrepareAssetError::RetryNextUpdate(material));
}; };
let (normal_map_texture_view, normal_map_sampler) = if let Some(result) =
image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.normal_map_texture)
{
result
} else {
return Err(PrepareAssetError::RetryNextUpdate(material));
};
let (occlusion_texture_view, occlusion_sampler) = if let Some(result) = let (occlusion_texture_view, occlusion_sampler) = if let Some(result) =
image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.occlusion_texture) image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.occlusion_texture)
{ {
@ -211,13 +206,16 @@ impl RenderAsset for StandardMaterial {
if material.unlit { if material.unlit {
flags |= StandardMaterialFlags::UNLIT; flags |= StandardMaterialFlags::UNLIT;
} }
if material.normal_map_texture.is_some() {
flags |= StandardMaterialFlags::NORMAL_MAP_TEXTURE;
}
let value = StandardMaterialUniformData { let value = StandardMaterialUniformData {
base_color: material.base_color.as_rgba_linear().into(), base_color: material.base_color.as_rgba_linear().into(),
emissive: material.emissive.into(), emissive: material.emissive.into(),
roughness: material.perceptual_roughness, roughness: material.perceptual_roughness,
metallic: material.metallic, metallic: material.metallic,
reflectance: material.reflectance, reflectance: material.reflectance,
flags: flags.bits, flags: flags.bits(),
}; };
let value_std140 = value.as_std140(); let value_std140 = value.as_std140();
@ -264,12 +262,24 @@ impl RenderAsset for StandardMaterial {
binding: 8, binding: 8,
resource: BindingResource::Sampler(occlusion_sampler), resource: BindingResource::Sampler(occlusion_sampler),
}, },
BindGroupEntry {
binding: 9,
resource: BindingResource::TextureView(normal_map_texture_view),
},
BindGroupEntry {
binding: 10,
resource: BindingResource::Sampler(normal_map_sampler),
},
], ],
label: Some("pbr_standard_material_bind_group"), label: Some("pbr_standard_material_bind_group"),
layout: &pbr_pipeline.material_layout, layout: &pbr_pipeline.material_layout,
}); });
Ok(GpuStandardMaterial { buffer, bind_group }) Ok(GpuStandardMaterial {
buffer,
bind_group,
flags,
})
} }
} }

View file

@ -101,8 +101,8 @@ pub const DIRECTIONAL_SHADOW_LAYERS: u32 = MAX_DIRECTIONAL_LIGHTS as u32;
pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float;
pub struct ShadowPipeline { pub struct ShadowPipeline {
pub pipeline: CachedPipelineId,
pub view_layout: BindGroupLayout, pub view_layout: BindGroupLayout,
pub mesh_layout: BindGroupLayout,
pub point_light_sampler: Sampler, pub point_light_sampler: Sampler,
pub directional_light_sampler: Sampler, pub directional_light_sampler: Sampler,
} }
@ -133,15 +133,81 @@ impl FromWorld for ShadowPipeline {
}); });
let pbr_pipeline = world.get_resource::<PbrPipeline>().unwrap(); let pbr_pipeline = world.get_resource::<PbrPipeline>().unwrap();
let descriptor = RenderPipelineDescriptor {
vertex: VertexState { ShadowPipeline {
shader: SHADOW_SHADER_HANDLE.typed::<Shader>(), view_layout,
entry_point: "vertex".into(), mesh_layout: pbr_pipeline.mesh_layout.clone(),
shader_defs: vec![], point_light_sampler: render_device.create_sampler(&SamplerDescriptor {
buffers: vec![VertexBufferLayout { address_mode_u: AddressMode::ClampToEdge,
array_stride: 32, address_mode_v: AddressMode::ClampToEdge,
step_mode: VertexStepMode::Vertex, address_mode_w: AddressMode::ClampToEdge,
attributes: vec![ mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
}),
directional_light_sampler: render_device.create_sampler(&SamplerDescriptor {
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
}),
}
}
}
bitflags::bitflags! {
#[repr(transparent)]
pub struct ShadowPipelineKey: u32 {
const NONE = 0;
const VERTEX_TANGENTS = (1 << 0);
}
}
impl SpecializedPipeline for ShadowPipeline {
type Key = ShadowPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
let (vertex_array_stride, vertex_attributes) =
if key.contains(ShadowPipelineKey::VERTEX_TANGENTS) {
(
48,
vec![
// Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically))
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 12,
shader_location: 0,
},
// Normal
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 0,
shader_location: 1,
},
// Uv (GOTCHA! uv is no longer third in the buffer due to how Mesh sorts attributes (alphabetically))
VertexAttribute {
format: VertexFormat::Float32x2,
offset: 40,
shader_location: 2,
},
// Tangent
VertexAttribute {
format: VertexFormat::Float32x4,
offset: 24,
shader_location: 3,
},
],
)
} else {
(
32,
vec![
// Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically)) // Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically))
VertexAttribute { VertexAttribute {
format: VertexFormat::Float32x3, format: VertexFormat::Float32x3,
@ -161,10 +227,21 @@ impl FromWorld for ShadowPipeline {
shader_location: 2, shader_location: 2,
}, },
], ],
)
};
RenderPipelineDescriptor {
vertex: VertexState {
shader: SHADOW_SHADER_HANDLE.typed::<Shader>(),
entry_point: "vertex".into(),
shader_defs: vec![],
buffers: vec![VertexBufferLayout {
array_stride: vertex_array_stride,
step_mode: VertexStepMode::Vertex,
attributes: vertex_attributes,
}], }],
}, },
fragment: None, fragment: None,
layout: Some(vec![view_layout.clone(), pbr_pipeline.mesh_layout.clone()]), layout: Some(vec![self.view_layout.clone(), self.mesh_layout.clone()]),
primitive: PrimitiveState { primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList, topology: PrimitiveTopology::TriangleList,
strip_index_format: None, strip_index_format: None,
@ -192,32 +269,6 @@ impl FromWorld for ShadowPipeline {
}), }),
multisample: MultisampleState::default(), multisample: MultisampleState::default(),
label: Some("shadow_pipeline".into()), label: Some("shadow_pipeline".into()),
};
let mut render_pipeline_cache = world.get_resource_mut::<RenderPipelineCache>().unwrap();
ShadowPipeline {
pipeline: render_pipeline_cache.queue(descriptor),
view_layout,
point_light_sampler: render_device.create_sampler(&SamplerDescriptor {
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
}),
directional_light_sampler: render_device.create_sampler(&SamplerDescriptor {
address_mode_u: AddressMode::ClampToEdge,
address_mode_v: AddressMode::ClampToEdge,
address_mode_w: AddressMode::ClampToEdge,
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
}),
} }
} }
} }
@ -626,10 +677,14 @@ pub fn queue_shadow_view_bind_group(
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn queue_shadows( pub fn queue_shadows(
shadow_draw_functions: Res<DrawFunctions<Shadow>>, shadow_draw_functions: Res<DrawFunctions<Shadow>>,
shadow_pipeline: Res<ShadowPipeline>, shadow_pipeline: Res<ShadowPipeline>,
casting_meshes: Query<Entity, (With<Handle<Mesh>>, Without<NotShadowCaster>)>, casting_meshes: Query<(Entity, &Handle<Mesh>), Without<NotShadowCaster>>,
render_meshes: Res<RenderAssets<Mesh>>,
mut pipelines: ResMut<SpecializedPipelines<ShadowPipeline>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
mut view_lights: Query<&ViewLights>, mut view_lights: Query<&ViewLights>,
mut view_light_shadow_phases: Query<&mut RenderPhase<Shadow>>, mut view_light_shadow_phases: Query<&mut RenderPhase<Shadow>>,
) { ) {
@ -642,10 +697,18 @@ pub fn queue_shadows(
for view_light_entity in view_lights.lights.iter().copied() { for view_light_entity in view_lights.lights.iter().copied() {
let mut shadow_phase = view_light_shadow_phases.get_mut(view_light_entity).unwrap(); let mut shadow_phase = view_light_shadow_phases.get_mut(view_light_entity).unwrap();
// TODO: this should only queue up meshes that are actually visible by each "light view" // TODO: this should only queue up meshes that are actually visible by each "light view"
for entity in casting_meshes.iter() { for (entity, mesh_handle) in casting_meshes.iter() {
let mut key = ShadowPipelineKey::empty();
if let Some(mesh) = render_meshes.get(mesh_handle) {
if mesh.has_tangents {
key |= ShadowPipelineKey::VERTEX_TANGENTS;
}
}
let pipeline_id = pipelines.specialize(&mut pipeline_cache, &shadow_pipeline, key);
shadow_phase.add(Shadow { shadow_phase.add(Shadow {
draw_function: draw_shadow_mesh, draw_function: draw_shadow_mesh,
pipeline: shadow_pipeline.pipeline, pipeline: pipeline_id,
entity, entity,
distance: 0.0, // TODO: sort back-to-front distance: 0.0, // TODO: sort back-to-front
}) })

View file

@ -47,6 +47,22 @@ bitflags::bitflags! {
} }
} }
// NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.wgsl!
bitflags::bitflags! {
#[repr(transparent)]
pub 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 NORMAL_MAP_TEXTURE = (1 << 6);
const NONE = 0;
const UNINITIALIZED = 0xFFFF;
}
}
pub fn extract_meshes( pub fn extract_meshes(
mut commands: Commands, mut commands: Commands,
mut previous_caster_len: Local<usize>, mut previous_caster_len: Local<usize>,
@ -300,6 +316,27 @@ impl FromWorld for PbrPipeline {
}, },
count: None, count: None,
}, },
// Normal Map Texture
BindGroupLayoutEntry {
binding: 9,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Texture {
multisampled: false,
sample_type: TextureSampleType::Float { filterable: true },
view_dimension: TextureViewDimension::D2,
},
count: None,
},
// Normal Map Texture Sampler
BindGroupLayoutEntry {
binding: 10,
visibility: ShaderStages::FRAGMENT,
ty: BindingType::Sampler {
comparison: false,
filtering: true,
},
count: None,
},
], ],
label: Some("pbr_material_layout"), label: Some("pbr_material_layout"),
}); });
@ -374,8 +411,10 @@ bitflags::bitflags! {
// NOTE: Apparently quadro drivers support up to 64x MSAA. // NOTE: Apparently quadro drivers support up to 64x MSAA.
/// MSAA uses the highest 6 bits for the MSAA sample count - 1 to support up to 64x MSAA. /// MSAA uses the highest 6 bits for the MSAA sample count - 1 to support up to 64x MSAA.
pub struct PbrPipelineKey: u32 { pub struct PbrPipelineKey: u32 {
const NONE = 0; const NONE = 0;
const MSAA_RESERVED_BITS = PbrPipelineKey::MSAA_MASK_BITS << PbrPipelineKey::MSAA_SHIFT_BITS; const VERTEX_TANGENTS = (1 << 0);
const STANDARDMATERIAL_NORMAL_MAP = (1 << 1);
const MSAA_RESERVED_BITS = PbrPipelineKey::MSAA_MASK_BITS << PbrPipelineKey::MSAA_SHIFT_BITS;
} }
} }
@ -397,15 +436,41 @@ impl SpecializedPipeline for PbrPipeline {
type Key = PbrPipelineKey; type Key = PbrPipelineKey;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor { let (vertex_array_stride, vertex_attributes) =
vertex: VertexState { if key.contains(PbrPipelineKey::VERTEX_TANGENTS) {
shader: PBR_SHADER_HANDLE.typed::<Shader>(), (
entry_point: "vertex".into(), 48,
shader_defs: vec![], vec![
buffers: vec![VertexBufferLayout { // Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically))
array_stride: 32, VertexAttribute {
step_mode: VertexStepMode::Vertex, format: VertexFormat::Float32x3,
attributes: vec![ offset: 12,
shader_location: 0,
},
// Normal
VertexAttribute {
format: VertexFormat::Float32x3,
offset: 0,
shader_location: 1,
},
// Uv (GOTCHA! uv is no longer third in the buffer due to how Mesh sorts attributes (alphabetically))
VertexAttribute {
format: VertexFormat::Float32x2,
offset: 40,
shader_location: 2,
},
// Tangent
VertexAttribute {
format: VertexFormat::Float32x4,
offset: 24,
shader_location: 3,
},
],
)
} else {
(
32,
vec![
// Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically)) // Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically))
VertexAttribute { VertexAttribute {
format: VertexFormat::Float32x3, format: VertexFormat::Float32x3,
@ -425,11 +490,29 @@ impl SpecializedPipeline for PbrPipeline {
shader_location: 2, shader_location: 2,
}, },
], ],
)
};
let mut shader_defs = Vec::new();
if key.contains(PbrPipelineKey::VERTEX_TANGENTS) {
shader_defs.push(String::from("VERTEX_TANGENTS"));
}
if key.contains(PbrPipelineKey::STANDARDMATERIAL_NORMAL_MAP) {
shader_defs.push(String::from("STANDARDMATERIAL_NORMAL_MAP"));
}
RenderPipelineDescriptor {
vertex: VertexState {
shader: PBR_SHADER_HANDLE.typed::<Shader>(),
entry_point: "vertex".into(),
shader_defs: shader_defs.clone(),
buffers: vec![VertexBufferLayout {
array_stride: vertex_array_stride,
step_mode: VertexStepMode::Vertex,
attributes: vertex_attributes,
}], }],
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
shader: PBR_SHADER_HANDLE.typed::<Shader>(), shader: PBR_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![], shader_defs,
entry_point: "fragment".into(), entry_point: "fragment".into(),
targets: vec![ColorTargetState { targets: vec![ColorTargetState {
format: TextureFormat::bevy_default(), format: TextureFormat::bevy_default(),
@ -528,11 +611,14 @@ pub fn queue_meshes(
light_meta: Res<LightMeta>, light_meta: Res<LightMeta>,
msaa: Res<Msaa>, msaa: Res<Msaa>,
view_uniforms: Res<ViewUniforms>, view_uniforms: Res<ViewUniforms>,
render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderAssets<StandardMaterial>>, render_materials: Res<RenderAssets<StandardMaterial>>,
standard_material_meshes: Query< standard_material_meshes: Query<(
(Entity, &Handle<StandardMaterial>, &MeshUniform), Entity,
With<Handle<Mesh>>, &Handle<StandardMaterial>,
>, &Handle<Mesh>,
&MeshUniform,
)>,
mut views: Query<( mut views: Query<(
Entity, Entity,
&ExtractedView, &ExtractedView,
@ -544,7 +630,6 @@ pub fn queue_meshes(
view_uniforms.uniforms.binding(), view_uniforms.uniforms.binding(),
light_meta.view_gpu_lights.binding(), light_meta.view_gpu_lights.binding(),
) { ) {
let msaa_key = PbrPipelineKey::from_msaa_samples(msaa.samples);
for (entity, view, view_lights, mut transparent_phase) in views.iter_mut() { for (entity, view, view_lights, mut transparent_phase) in views.iter_mut() {
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[ entries: &[
@ -595,13 +680,27 @@ pub fn queue_meshes(
let view_matrix = view.transform.compute_matrix(); let view_matrix = view.transform.compute_matrix();
let view_row_2 = view_matrix.row(2); let view_row_2 = view_matrix.row(2);
for (entity, material_handle, mesh_uniform) in standard_material_meshes.iter() { for (entity, material_handle, mesh_handle, mesh_uniform) in
if !render_materials.contains_key(material_handle) { standard_material_meshes.iter()
{
let mut key = PbrPipelineKey::from_msaa_samples(msaa.samples);
if let Some(material) = render_materials.get(material_handle) {
if material
.flags
.contains(StandardMaterialFlags::NORMAL_MAP_TEXTURE)
{
key |= PbrPipelineKey::STANDARDMATERIAL_NORMAL_MAP;
}
} else {
continue; continue;
} }
if let Some(mesh) = render_meshes.get(mesh_handle) {
if mesh.has_tangents {
key |= PbrPipelineKey::VERTEX_TANGENTS;
}
}
let pipeline_id = pipelines.specialize(&mut pipeline_cache, &pbr_pipeline, key);
let pipeline_id =
pipelines.specialize(&mut pipeline_cache, &pbr_pipeline, msaa_key);
// NOTE: row 2 of the view matrix dotted with column 3 of the model matrix // 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 // gives the z component of translation of the mesh in view space
let mesh_z = view_row_2.dot(mesh_uniform.transform.col(3)); let mesh_z = view_row_2.dot(mesh_uniform.transform.col(3));

View file

@ -26,6 +26,9 @@ struct Vertex {
[[location(0)]] position: vec3<f32>; [[location(0)]] position: vec3<f32>;
[[location(1)]] normal: vec3<f32>; [[location(1)]] normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>; [[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] tangent: vec4<f32>;
#endif
}; };
struct VertexOutput { struct VertexOutput {
@ -33,6 +36,9 @@ struct VertexOutput {
[[location(0)]] world_position: vec4<f32>; [[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>; [[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>; [[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
}; };
[[stage(vertex)]] [[stage(vertex)]]
@ -48,6 +54,16 @@ fn vertex(vertex: Vertex) -> VertexOutput {
mesh.inverse_transpose_model.y.xyz, mesh.inverse_transpose_model.y.xyz,
mesh.inverse_transpose_model.z.xyz mesh.inverse_transpose_model.z.xyz
) * vertex.normal; ) * vertex.normal;
#ifdef VERTEX_TANGENTS
out.world_tangent = vec4<f32>(
mat3x3<f32>(
mesh.model.x.xyz,
mesh.model.y.xyz,
mesh.model.z.xyz
) * vertex.tangent.xyz,
vertex.tangent.w
);
#endif
return out; return out;
} }
@ -164,6 +180,10 @@ var metallic_roughness_sampler: sampler;
var occlusion_texture: texture_2d<f32>; var occlusion_texture: texture_2d<f32>;
[[group(1), binding(8)]] [[group(1), binding(8)]]
var occlusion_sampler: sampler; var occlusion_sampler: sampler;
[[group(1), binding(9)]]
var normal_map_texture: texture_2d<f32>;
[[group(1), binding(10)]]
var normal_map_sampler: sampler;
let PI: f32 = 3.141592653589793; let PI: f32 = 3.141592653589793;
@ -475,6 +495,9 @@ struct FragmentInput {
[[location(0)]] world_position: vec4<f32>; [[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>; [[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>; [[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
}; };
[[stage(fragment)]] [[stage(fragment)]]
@ -510,27 +533,31 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
var N: vec3<f32> = normalize(in.world_normal); var N: vec3<f32> = normalize(in.world_normal);
// FIXME: Normal maps need an additional vertex attribute and vertex stage output/fragment stage input #ifdef VERTEX_TANGENTS
// Just use a separate shader for lit with normal maps? #ifdef STANDARDMATERIAL_NORMAL_MAP
// # ifdef STANDARDMATERIAL_NORMAL_MAP var T: vec3<f32> = normalize(in.world_tangent.xyz - N * dot(in.world_tangent.xyz, N));
// vec3 T = normalize(v_WorldTangent.xyz); var B: vec3<f32> = cross(N, T) * in.world_tangent.w;
// vec3 B = cross(N, T) * v_WorldTangent.w; #endif
// # endif #endif
if ((material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u) { if ((material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u) {
if (!in.is_front) { if (!in.is_front) {
N = -N; N = -N;
#ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP
T = -T;
B = -B;
#endif
#endif
} }
// # ifdef STANDARDMATERIAL_NORMAL_MAP
// T = gl_FrontFacing ? T : -T;
// B = gl_FrontFacing ? B : -B;
// # endif
} }
// # ifdef STANDARDMATERIAL_NORMAL_MAP #ifdef VERTEX_TANGENTS
// mat3 TBN = mat3(T, B, N); #ifdef STANDARDMATERIAL_NORMAL_MAP
// N = TBN * normalize(texture(sampler2D(normal_map, normal_map_sampler), v_Uv).rgb * 2.0 - 1.0); let TBN = mat3x3<f32>(T, B, N);
// # endif N = TBN * normalize(textureSample(normal_map_texture, normal_map_sampler, in.uv).rgb * 2.0 - 1.0);
#endif
#endif
var V: vec3<f32>; var V: vec3<f32>;
if (view.projection.w.w != 1.0) { // If the projection is not orthographic if (view.projection.w.w != 1.0) { // If the projection is not orthographic

View file

@ -532,6 +532,7 @@ impl From<&Indices> for IndexFormat {
pub struct GpuMesh { pub struct GpuMesh {
pub vertex_buffer: Buffer, pub vertex_buffer: Buffer,
pub index_info: Option<GpuIndexInfo>, pub index_info: Option<GpuIndexInfo>,
pub has_tangents: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -574,6 +575,7 @@ impl RenderAsset for Mesh {
Ok(GpuMesh { Ok(GpuMesh {
vertex_buffer, vertex_buffer,
index_info, index_info,
has_tangents: mesh.attributes.contains_key(Mesh::ATTRIBUTE_TANGENT),
}) })
} }
} }