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);
}
// if let Some(vertex_attribute) = reader
// .read_tangents()
// .map(|v| VertexAttributeValues::Float32x4(v.collect()))
// {
// mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute);
// }
if let Some(vertex_attribute) = reader
.read_tangents()
.map(|v| VertexAttributeValues::Float32x4(v.collect()))
{
mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute);
}
if let Some(vertex_attribute) = reader
.read_tex_coords(0)
@ -382,15 +382,16 @@ fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<
None
};
// let normal_map: Option<Handle<Texture>> = if let Some(normal_texture) = material.normal_texture() {
// // TODO: handle normal_texture.scale
// // TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords)
// let label = texture_label(&normal_texture.texture());
// let path = AssetPath::new_ref(load_context.path(), Some(&label));
// Some(load_context.get_handle(path))
// } else {
// None
// };
let normal_map_texture: Option<Handle<Image>> =
if let Some(normal_texture) = material.normal_texture() {
// TODO: handle normal_texture.scale
// TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords)
let label = texture_label(&normal_texture.texture());
let path = AssetPath::new_ref(load_context.path(), Some(&label));
Some(load_context.get_handle(path))
} else {
None
};
let metallic_roughness_texture = if let Some(info) = pbr.metallic_roughness_texture() {
// 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(),
metallic: pbr.metallic_factor(),
metallic_roughness_texture,
// normal_map,
normal_map_texture,
double_sided: material.double_sided(),
occlusion_texture,
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::<DrawFunctions<Shadow>>()
.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 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_asset::{AddAsset, Handle};
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
@ -16,21 +16,6 @@ use bevy_render2::{
use crevice::std140::{AsStd140, Std140};
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
/// Standard property values with pictures here https://google.github.io/filament/Material%20Properties.pdf
#[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]
/// defaults to 0.5 which is mapped to 4% reflectance in the shader
pub reflectance: f32,
pub normal_map_texture: Option<Handle<Image>>,
pub occlusion_texture: Option<Handle<Image>>,
pub double_sided: 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
reflectance: 0.5,
occlusion_texture: None,
normal_map_texture: None,
double_sided: false,
unlit: false,
}
@ -140,6 +127,7 @@ impl Plugin for StandardMaterialPlugin {
pub struct GpuStandardMaterial {
pub buffer: Buffer,
pub bind_group: BindGroup,
pub flags: StandardMaterialFlags,
}
impl RenderAsset for StandardMaterial {
@ -185,6 +173,13 @@ impl RenderAsset for StandardMaterial {
} else {
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) =
image_handle_to_view_sampler(pbr_pipeline, gpu_images, &material.occlusion_texture)
{
@ -211,13 +206,16 @@ impl RenderAsset for StandardMaterial {
if material.unlit {
flags |= StandardMaterialFlags::UNLIT;
}
if material.normal_map_texture.is_some() {
flags |= StandardMaterialFlags::NORMAL_MAP_TEXTURE;
}
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,
flags: flags.bits(),
};
let value_std140 = value.as_std140();
@ -264,12 +262,24 @@ impl RenderAsset for StandardMaterial {
binding: 8,
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"),
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 struct ShadowPipeline {
pub pipeline: CachedPipelineId,
pub view_layout: BindGroupLayout,
pub mesh_layout: BindGroupLayout,
pub point_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 descriptor = RenderPipelineDescriptor {
vertex: VertexState {
shader: SHADOW_SHADER_HANDLE.typed::<Shader>(),
entry_point: "vertex".into(),
shader_defs: vec![],
buffers: vec![VertexBufferLayout {
array_stride: 32,
step_mode: VertexStepMode::Vertex,
attributes: vec![
ShadowPipeline {
view_layout,
mesh_layout: pbr_pipeline.mesh_layout.clone(),
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()
}),
}
}
}
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))
VertexAttribute {
format: VertexFormat::Float32x3,
@ -161,10 +227,21 @@ impl FromWorld for ShadowPipeline {
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,
layout: Some(vec![view_layout.clone(), pbr_pipeline.mesh_layout.clone()]),
layout: Some(vec![self.view_layout.clone(), self.mesh_layout.clone()]),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
@ -192,32 +269,6 @@ impl FromWorld for ShadowPipeline {
}),
multisample: MultisampleState::default(),
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(
shadow_draw_functions: Res<DrawFunctions<Shadow>>,
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_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() {
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"
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 {
draw_function: draw_shadow_mesh,
pipeline: shadow_pipeline.pipeline,
pipeline: pipeline_id,
entity,
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(
mut commands: Commands,
mut previous_caster_len: Local<usize>,
@ -300,6 +316,27 @@ impl FromWorld for PbrPipeline {
},
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"),
});
@ -375,6 +412,8 @@ bitflags::bitflags! {
/// MSAA uses the highest 6 bits for the MSAA sample count - 1 to support up to 64x MSAA.
pub struct PbrPipelineKey: u32 {
const NONE = 0;
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;
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
RenderPipelineDescriptor {
vertex: VertexState {
shader: PBR_SHADER_HANDLE.typed::<Shader>(),
entry_point: "vertex".into(),
shader_defs: vec![],
buffers: vec![VertexBufferLayout {
array_stride: 32,
step_mode: VertexStepMode::Vertex,
attributes: vec![
let (vertex_array_stride, vertex_attributes) =
if key.contains(PbrPipelineKey::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))
VertexAttribute {
format: VertexFormat::Float32x3,
@ -425,11 +490,29 @@ impl SpecializedPipeline for PbrPipeline {
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 {
shader: PBR_SHADER_HANDLE.typed::<Shader>(),
shader_defs: vec![],
shader_defs,
entry_point: "fragment".into(),
targets: vec![ColorTargetState {
format: TextureFormat::bevy_default(),
@ -528,11 +611,14 @@ pub fn queue_meshes(
light_meta: Res<LightMeta>,
msaa: Res<Msaa>,
view_uniforms: Res<ViewUniforms>,
render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderAssets<StandardMaterial>>,
standard_material_meshes: Query<
(Entity, &Handle<StandardMaterial>, &MeshUniform),
With<Handle<Mesh>>,
>,
standard_material_meshes: Query<(
Entity,
&Handle<StandardMaterial>,
&Handle<Mesh>,
&MeshUniform,
)>,
mut views: Query<(
Entity,
&ExtractedView,
@ -544,7 +630,6 @@ pub fn queue_meshes(
view_uniforms.uniforms.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() {
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
entries: &[
@ -595,13 +680,27 @@ pub fn queue_meshes(
let view_matrix = view.transform.compute_matrix();
let view_row_2 = view_matrix.row(2);
for (entity, material_handle, mesh_uniform) in standard_material_meshes.iter() {
if !render_materials.contains_key(material_handle) {
for (entity, material_handle, mesh_handle, mesh_uniform) in
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;
}
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
// gives the z component of translation of the mesh in view space
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(1)]] normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] tangent: vec4<f32>;
#endif
};
struct VertexOutput {
@ -33,6 +36,9 @@ struct VertexOutput {
[[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
};
[[stage(vertex)]]
@ -48,6 +54,16 @@ fn vertex(vertex: Vertex) -> VertexOutput {
mesh.inverse_transpose_model.y.xyz,
mesh.inverse_transpose_model.z.xyz
) * 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;
}
@ -164,6 +180,10 @@ var metallic_roughness_sampler: sampler;
var occlusion_texture: texture_2d<f32>;
[[group(1), binding(8)]]
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;
@ -475,6 +495,9 @@ struct FragmentInput {
[[location(0)]] world_position: vec4<f32>;
[[location(1)]] world_normal: vec3<f32>;
[[location(2)]] uv: vec2<f32>;
#ifdef VERTEX_TANGENTS
[[location(3)]] world_tangent: vec4<f32>;
#endif
};
[[stage(fragment)]]
@ -510,27 +533,31 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
var N: vec3<f32> = normalize(in.world_normal);
// 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
#ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP
var T: vec3<f32> = normalize(in.world_tangent.xyz - N * dot(in.world_tangent.xyz, N));
var B: vec3<f32> = cross(N, T) * in.world_tangent.w;
#endif
#endif
if ((material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u) {
if (!in.is_front) {
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
// mat3 TBN = mat3(T, B, N);
// N = TBN * normalize(texture(sampler2D(normal_map, normal_map_sampler), v_Uv).rgb * 2.0 - 1.0);
// # endif
#ifdef VERTEX_TANGENTS
#ifdef STANDARDMATERIAL_NORMAL_MAP
let TBN = mat3x3<f32>(T, B, N);
N = TBN * normalize(textureSample(normal_map_texture, normal_map_sampler, in.uv).rgb * 2.0 - 1.0);
#endif
#endif
var V: vec3<f32>;
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 vertex_buffer: Buffer,
pub index_info: Option<GpuIndexInfo>,
pub has_tangents: bool,
}
#[derive(Debug, Clone)]
@ -574,6 +575,7 @@ impl RenderAsset for Mesh {
Ok(GpuMesh {
vertex_buffer,
index_info,
has_tangents: mesh.attributes.contains_key(Mesh::ATTRIBUTE_TANGENT),
})
}
}