 4b1f6f4ebb
			
		
	
	
		4b1f6f4ebb
		
	
	
	
	
		
			
			# Objective the bevy pbr shader doesn't handle at all normal maps if a mesh doesn't have backed tangents. This is a pitfall (that I fell into) and needs to be documented. # Solution Document the behavior. (Also document a few other `StandardMaterial` fields) ## Changelog * Add documentation to `emissive`, `normal_map_texture` and `occlusion_texture` fields of `StandardMaterial`.
		
			
				
	
	
		
			332 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			332 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use crate::{AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_SHADER_HANDLE};
 | |
| use bevy_asset::Handle;
 | |
| use bevy_math::Vec4;
 | |
| use bevy_reflect::TypeUuid;
 | |
| use bevy_render::{
 | |
|     color::Color, mesh::MeshVertexBufferLayout, render_asset::RenderAssets, render_resource::*,
 | |
|     texture::Image,
 | |
| };
 | |
| 
 | |
| /// A material with "standard" properties used in PBR lighting
 | |
| /// Standard property values with pictures here
 | |
| /// <https://google.github.io/filament/Material%20Properties.pdf>.
 | |
| ///
 | |
| /// May be created directly from a [`Color`] or an [`Image`].
 | |
| #[derive(AsBindGroup, Debug, Clone, TypeUuid)]
 | |
| #[uuid = "7494888b-c082-457b-aacf-517228cc0c22"]
 | |
| #[bind_group_data(StandardMaterialKey)]
 | |
| #[uniform(0, StandardMaterialUniform)]
 | |
| pub struct StandardMaterial {
 | |
|     /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything
 | |
|     /// in between. If used together with a base_color_texture, this is factored into the final
 | |
|     /// base color as `base_color * base_color_texture_value`
 | |
|     pub base_color: Color,
 | |
|     #[texture(1)]
 | |
|     #[sampler(2)]
 | |
|     pub base_color_texture: Option<Handle<Image>>,
 | |
| 
 | |
|     // 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
 | |
|     /// Color the material "emits" to the camera.
 | |
|     ///
 | |
|     /// This is typically used for monitor screens or LED lights.
 | |
|     /// Anything that can be visible even in darkness.
 | |
|     ///
 | |
|     /// The emissive color is added to what would otherwise be the material's visible color.
 | |
|     /// This means that for a light emissive value, in darkness,
 | |
|     /// you will mostly see the emissive component.
 | |
|     ///
 | |
|     /// The default emissive color is black, which doesn't add anything to the material color.
 | |
|     ///
 | |
|     /// Note that **an emissive material won't light up surrounding areas like a light source**,
 | |
|     /// it just adds a value to the color seen on screen.
 | |
|     pub emissive: Color,
 | |
| 
 | |
|     #[texture(3)]
 | |
|     #[sampler(4)]
 | |
|     pub emissive_texture: Option<Handle<Image>>,
 | |
|     /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader
 | |
|     /// Defaults to minimum of 0.089
 | |
|     /// If used together with a roughness/metallic texture, this is factored into the final base
 | |
|     /// color as `roughness * roughness_texture_value`
 | |
|     pub perceptual_roughness: f32,
 | |
|     /// From [0.0, 1.0], dielectric to pure metallic
 | |
|     /// If used together with a roughness/metallic texture, this is factored into the final base
 | |
|     /// color as `metallic * metallic_texture_value`
 | |
|     pub metallic: f32,
 | |
| 
 | |
|     #[texture(5)]
 | |
|     #[sampler(6)]
 | |
|     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,
 | |
| 
 | |
|     /// Used to fake the lighting of bumps and dents on a material.
 | |
|     ///
 | |
|     /// A typical usage would be faking cobblestones on a flat plane mesh in 3D.
 | |
|     ///
 | |
|     /// # Notes
 | |
|     ///
 | |
|     ///
 | |
|     /// Normal mapping with `StandardMaterial` and the core bevy PBR shaders requires:
 | |
|     /// - A normal map texture
 | |
|     /// - Vertex UVs
 | |
|     /// - Vertex tangents
 | |
|     /// - Vertex normals
 | |
|     ///
 | |
|     /// Tangents do not have to be stored in your model,
 | |
|     /// they can be generated using the [`Mesh::generate_tangents`] method.
 | |
|     /// If your material has a normal map, but still renders as a flat surface,
 | |
|     /// make sure your meshes have their tangents set.
 | |
|     ///
 | |
|     /// [`Mesh::generate_tangents`]: bevy_render::mesh::Mesh::generate_tangents
 | |
|     #[texture(9)]
 | |
|     #[sampler(10)]
 | |
|     pub normal_map_texture: Option<Handle<Image>>,
 | |
| 
 | |
|     /// Normal map textures authored for DirectX have their y-component flipped. Set this to flip
 | |
|     /// it to right-handed conventions.
 | |
|     pub flip_normal_map_y: bool,
 | |
| 
 | |
|     /// Specifies the level of exposure to ambient light.
 | |
|     ///
 | |
|     /// This is usually generated and stored automatically ("baked") by 3D-modelling software.
 | |
|     ///
 | |
|     /// Typically, steep concave parts of a model (such as the armpit of a shirt) are darker,
 | |
|     /// because they have little exposed to light.
 | |
|     /// An occlusion map specifies those parts of the model that light doesn't reach well.
 | |
|     ///
 | |
|     /// The material will be less lit in places where this texture is dark.
 | |
|     /// This is similar to ambient occlusion, but built into the model.
 | |
|     #[texture(7)]
 | |
|     #[sampler(8)]
 | |
|     pub occlusion_texture: Option<Handle<Image>>,
 | |
| 
 | |
|     /// Support two-sided lighting by automatically flipping the normals for "back" faces
 | |
|     /// within the PBR lighting shader.
 | |
|     /// Defaults to false.
 | |
|     /// This does not automatically configure backface culling, which can be done via
 | |
|     /// `cull_mode`.
 | |
|     pub double_sided: bool,
 | |
|     /// Whether to cull the "front", "back" or neither side of a mesh
 | |
|     /// defaults to `Face::Back`
 | |
|     pub cull_mode: Option<Face>,
 | |
|     pub unlit: bool,
 | |
|     pub alpha_mode: AlphaMode,
 | |
|     pub depth_bias: f32,
 | |
| }
 | |
| 
 | |
| impl Default for StandardMaterial {
 | |
|     fn default() -> Self {
 | |
|         StandardMaterial {
 | |
|             base_color: Color::rgb(1.0, 1.0, 1.0),
 | |
|             base_color_texture: None,
 | |
|             emissive: Color::BLACK,
 | |
|             emissive_texture: None,
 | |
|             // This is the minimum the roughness is clamped to in shader code
 | |
|             // See <https://google.github.io/filament/Filament.html#materialsystem/parameterization/>
 | |
|             // It's the minimum floating point value that won't be rounded down to 0 in the
 | |
|             // calculations used. Although technically for 32-bit floats, 0.045 could be
 | |
|             // used.
 | |
|             perceptual_roughness: 0.089,
 | |
|             // Few materials are purely dielectric or metallic
 | |
|             // This is just a default for mostly-dielectric
 | |
|             metallic: 0.01,
 | |
|             metallic_roughness_texture: None,
 | |
|             // Minimum real-world reflectance is 2%, most materials between 2-5%
 | |
|             // Expressed in a linear scale and equivalent to 4% reflectance see
 | |
|             // <https://google.github.io/filament/Material%20Properties.pdf>
 | |
|             reflectance: 0.5,
 | |
|             occlusion_texture: None,
 | |
|             normal_map_texture: None,
 | |
|             flip_normal_map_y: false,
 | |
|             double_sided: false,
 | |
|             cull_mode: Some(Face::Back),
 | |
|             unlit: false,
 | |
|             alpha_mode: AlphaMode::Opaque,
 | |
|             depth_bias: 0.0,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl From<Color> for StandardMaterial {
 | |
|     fn from(color: Color) -> Self {
 | |
|         StandardMaterial {
 | |
|             base_color: color,
 | |
|             alpha_mode: if color.a() < 1.0 {
 | |
|                 AlphaMode::Blend
 | |
|             } else {
 | |
|                 AlphaMode::Opaque
 | |
|             },
 | |
|             ..Default::default()
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl From<Handle<Image>> for StandardMaterial {
 | |
|     fn from(texture: Handle<Image>) -> Self {
 | |
|         StandardMaterial {
 | |
|             base_color_texture: Some(texture),
 | |
|             ..Default::default()
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| // NOTE: These must match the bit flags in bevy_pbr/src/render/pbr_types.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 ALPHA_MODE_OPAQUE          = (1 << 6);
 | |
|         const ALPHA_MODE_MASK            = (1 << 7);
 | |
|         const ALPHA_MODE_BLEND           = (1 << 8);
 | |
|         const TWO_COMPONENT_NORMAL_MAP   = (1 << 9);
 | |
|         const FLIP_NORMAL_MAP_Y          = (1 << 10);
 | |
|         const NONE                       = 0;
 | |
|         const UNINITIALIZED              = 0xFFFF;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// The GPU representation of the uniform data of a [`StandardMaterial`].
 | |
| #[derive(Clone, Default, ShaderType)]
 | |
| pub struct StandardMaterialUniform {
 | |
|     /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything
 | |
|     /// in between.
 | |
|     pub base_color: Vec4,
 | |
|     // Use a color for user friendliness even though we technically don't use the alpha channel
 | |
|     // Might be used in the future for exposure correction in HDR
 | |
|     pub emissive: Vec4,
 | |
|     /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader
 | |
|     /// Defaults to minimum of 0.089
 | |
|     pub roughness: f32,
 | |
|     /// 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,
 | |
|     pub flags: u32,
 | |
|     /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque,
 | |
|     /// and any below means fully transparent.
 | |
|     pub alpha_cutoff: f32,
 | |
| }
 | |
| 
 | |
| impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
 | |
|     fn as_bind_group_shader_type(&self, images: &RenderAssets<Image>) -> StandardMaterialUniform {
 | |
|         let mut flags = StandardMaterialFlags::NONE;
 | |
|         if self.base_color_texture.is_some() {
 | |
|             flags |= StandardMaterialFlags::BASE_COLOR_TEXTURE;
 | |
|         }
 | |
|         if self.emissive_texture.is_some() {
 | |
|             flags |= StandardMaterialFlags::EMISSIVE_TEXTURE;
 | |
|         }
 | |
|         if self.metallic_roughness_texture.is_some() {
 | |
|             flags |= StandardMaterialFlags::METALLIC_ROUGHNESS_TEXTURE;
 | |
|         }
 | |
|         if self.occlusion_texture.is_some() {
 | |
|             flags |= StandardMaterialFlags::OCCLUSION_TEXTURE;
 | |
|         }
 | |
|         if self.double_sided {
 | |
|             flags |= StandardMaterialFlags::DOUBLE_SIDED;
 | |
|         }
 | |
|         if self.unlit {
 | |
|             flags |= StandardMaterialFlags::UNLIT;
 | |
|         }
 | |
|         let has_normal_map = self.normal_map_texture.is_some();
 | |
|         if has_normal_map {
 | |
|             if let Some(texture) = images.get(self.normal_map_texture.as_ref().unwrap()) {
 | |
|                 match texture.texture_format {
 | |
|                     // All 2-component unorm formats
 | |
|                     TextureFormat::Rg8Unorm
 | |
|                     | TextureFormat::Rg16Unorm
 | |
|                     | TextureFormat::Bc5RgUnorm
 | |
|                     | TextureFormat::EacRg11Unorm => {
 | |
|                         flags |= StandardMaterialFlags::TWO_COMPONENT_NORMAL_MAP;
 | |
|                     }
 | |
|                     _ => {}
 | |
|                 }
 | |
|             }
 | |
|             if self.flip_normal_map_y {
 | |
|                 flags |= StandardMaterialFlags::FLIP_NORMAL_MAP_Y;
 | |
|             }
 | |
|         }
 | |
|         // NOTE: 0.5 is from the glTF default - do we want this?
 | |
|         let mut alpha_cutoff = 0.5;
 | |
|         match self.alpha_mode {
 | |
|             AlphaMode::Opaque => flags |= StandardMaterialFlags::ALPHA_MODE_OPAQUE,
 | |
|             AlphaMode::Mask(c) => {
 | |
|                 alpha_cutoff = c;
 | |
|                 flags |= StandardMaterialFlags::ALPHA_MODE_MASK;
 | |
|             }
 | |
|             AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND,
 | |
|         };
 | |
| 
 | |
|         StandardMaterialUniform {
 | |
|             base_color: self.base_color.as_linear_rgba_f32().into(),
 | |
|             emissive: self.emissive.into(),
 | |
|             roughness: self.perceptual_roughness,
 | |
|             metallic: self.metallic,
 | |
|             reflectance: self.reflectance,
 | |
|             flags: flags.bits(),
 | |
|             alpha_cutoff,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(Clone, PartialEq, Eq, Hash)]
 | |
| pub struct StandardMaterialKey {
 | |
|     normal_map: bool,
 | |
|     cull_mode: Option<Face>,
 | |
| }
 | |
| 
 | |
| impl From<&StandardMaterial> for StandardMaterialKey {
 | |
|     fn from(material: &StandardMaterial) -> Self {
 | |
|         StandardMaterialKey {
 | |
|             normal_map: material.normal_map_texture.is_some(),
 | |
|             cull_mode: material.cull_mode,
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl Material for StandardMaterial {
 | |
|     fn specialize(
 | |
|         _pipeline: &MaterialPipeline<Self>,
 | |
|         descriptor: &mut RenderPipelineDescriptor,
 | |
|         _layout: &MeshVertexBufferLayout,
 | |
|         key: MaterialPipelineKey<Self>,
 | |
|     ) -> Result<(), SpecializedMeshPipelineError> {
 | |
|         if key.bind_group_data.normal_map {
 | |
|             descriptor
 | |
|                 .fragment
 | |
|                 .as_mut()
 | |
|                 .unwrap()
 | |
|                 .shader_defs
 | |
|                 .push(String::from("STANDARDMATERIAL_NORMAL_MAP"));
 | |
|         }
 | |
|         descriptor.primitive.cull_mode = key.bind_group_data.cull_mode;
 | |
|         if let Some(label) = &mut descriptor.label {
 | |
|             *label = format!("pbr_{}", *label).into();
 | |
|         }
 | |
|         Ok(())
 | |
|     }
 | |
| 
 | |
|     fn fragment_shader() -> ShaderRef {
 | |
|         PBR_SHADER_HANDLE.typed().into()
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     fn alpha_mode(&self) -> AlphaMode {
 | |
|         self.alpha_mode
 | |
|     }
 | |
| 
 | |
|     #[inline]
 | |
|     fn depth_bias(&self) -> f32 {
 | |
|         self.depth_bias
 | |
|     }
 | |
| }
 |