use crate::{ AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, ParallaxMappingMethod, PBR_PREPASS_SHADER_HANDLE, PBR_SHADER_HANDLE, }; use bevy_asset::Handle; use bevy_math::Vec4; use bevy_reflect::{std_traits::ReflectDefault, 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 /// . /// /// May be created directly from a [`Color`] or an [`Image`]. #[derive(AsBindGroup, Reflect, Debug, Clone, TypeUuid)] #[uuid = "7494888b-c082-457b-aacf-517228cc0c22"] #[bind_group_data(StandardMaterialKey)] #[uniform(0, StandardMaterialUniform)] #[reflect(Default, Debug)] pub struct StandardMaterial { /// The color of the surface of the material before lighting. /// /// 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` /// /// Defaults to [`Color::WHITE`]. pub base_color: Color, /// The texture component of the material's color before lighting. /// The actual pre-lighting color is `base_color * this_texture`. /// /// See [`base_color`] for details. /// /// You should set `base_color` to [`Color::WHITE`] (the default) /// if you want the texture to show as-is. /// /// Setting `base_color` to something else than white will tint /// the texture. For example, setting `base_color` to pure red will /// tint the texture red. /// /// [`base_color`]: StandardMaterial::base_color #[texture(1)] #[sampler(2)] pub base_color_texture: Option>, // Use a color for user friendliness even though we technically don't use the alpha channel // Might be used in the future for exposure correction in HDR /// 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, /// The emissive map, multiplies pixels with [`emissive`] /// to get the final "emitting" color of a surface. /// /// This color is multiplied by [`emissive`] to get the final emitted color. /// Meaning that you should set [`emissive`] to [`Color::WHITE`] /// if you want to use the full range of color of the emissive texture. /// /// [`emissive`]: StandardMaterial::emissive #[texture(3)] #[sampler(4)] pub emissive_texture: Option>, /// Linear perceptual roughness, clamped to `[0.089, 1.0]` in the shader. /// /// Defaults to `0.5`. /// /// Low values result in a "glossy" material with specular highlights, /// while values close to `1` result in rough materials. /// /// If used together with a roughness/metallic texture, this is factored into the final base /// color as `roughness * roughness_texture_value`. /// /// 0.089 is the minimum floating point value that won't be rounded down to 0 in the /// calculations used. // // Technically for 32-bit floats, 0.045 could be used. // See pub perceptual_roughness: f32, /// How "metallic" the material appears, within `[0.0, 1.0]`. /// /// This should be set to 0.0 for dielectric materials or 1.0 for metallic materials. /// For a hybrid surface such as corroded metal, you may need to use in-between values. /// /// Defaults to `0.00`, for dielectric. /// /// If used together with a roughness/metallic texture, this is factored into the final base /// color as `metallic * metallic_texture_value`. pub metallic: f32, /// Metallic and roughness maps, stored as a single texture. /// /// The blue channel contains metallic values, /// and the green channel contains the roughness values. /// Other channels are unused. /// /// Those values are multiplied by the scalar ones of the material, /// see [`metallic`] and [`perceptual_roughness`] for details. /// /// Note that with the default values of [`metallic`] and [`perceptual_roughness`], /// setting this texture has no effect. If you want to exclusively use the /// `metallic_roughness_texture` values for your material, make sure to set [`metallic`] /// and [`perceptual_roughness`] to `1.0`. /// /// [`metallic`]: StandardMaterial::metallic /// [`perceptual_roughness`]: StandardMaterial::perceptual_roughness #[texture(5)] #[sampler(6)] pub metallic_roughness_texture: Option>, /// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`. /// /// Use the value as a way to control the intensity of the /// specular highlight of the material, i.e. how reflective is the material, /// rather than the physical property "reflectance." /// /// Set to `0.0`, no specular highlight is visible, the highlight is strongest /// when `reflectance` is set to `1.0`. /// /// Defaults to `0.5` which is mapped to 4% reflectance in the shader. #[doc(alias = "specular_intensity")] 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>, /// 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 exposure 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>, /// 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. /// If set to `None`, the two sides of the mesh are visible. /// /// Defaults to `Some(Face::Back)`. /// In bevy, the order of declaration of a triangle's vertices /// in [`Mesh`] defines the triangle's front face. /// /// When a triangle is in a viewport, /// if its vertices appear counter-clockwise from the viewport's perspective, /// then the viewport is seeing the triangle's front face. /// Conversely, if the vertices appear clockwise, you are seeing the back face. /// /// In short, in bevy, front faces winds counter-clockwise. /// /// Your 3D editing software should manage all of that. /// /// [`Mesh`]: bevy_render::mesh::Mesh // TODO: include this in reflection somehow (maybe via remote types like serde https://serde.rs/remote-derive.html) #[reflect(ignore)] pub cull_mode: Option, /// Whether to apply only the base color to this material. /// /// Normals, occlusion textures, roughness, metallic, reflectance, emissive, /// shadows, alpha mode and ambient light are ignored if this is set to `true`. pub unlit: bool, /// Whether to enable fog for this material. pub fog_enabled: bool, /// How to apply the alpha channel of the `base_color_texture`. /// /// See [`AlphaMode`] for details. Defaults to [`AlphaMode::Opaque`]. pub alpha_mode: AlphaMode, /// Adjust rendered depth. /// /// A material with a positive depth bias will render closer to the /// camera while negative values cause the material to render behind /// other objects. This is independent of the viewport. /// /// `depth_bias` affects render ordering and depth write operations /// using the `wgpu::DepthBiasState::Constant` field. /// /// [z-fighting]: https://en.wikipedia.org/wiki/Z-fighting pub depth_bias: f32, /// The depth map used for [parallax mapping]. /// /// It is a greyscale image where white represents bottom and black the top. /// If this field is set, bevy will apply [parallax mapping]. /// Parallax mapping, unlike simple normal maps, will move the texture /// coordinate according to the current perspective, /// giving actual depth to the texture. /// /// The visual result is similar to a displacement map, /// but does not require additional geometry. /// /// Use the [`parallax_depth_scale`] field to control the depth of the parallax. /// /// ## Limitations /// /// - It will look weird on bent/non-planar surfaces. /// - The depth of the pixel does not reflect its visual position, resulting /// in artifacts for depth-dependent features such as fog or SSAO. /// - For the same reason, the the geometry silhouette will always be /// the one of the actual geometry, not the parallaxed version, resulting /// in awkward looks on intersecting parallaxed surfaces. /// /// ## Performance /// /// Parallax mapping requires multiple texture lookups, proportional to /// [`max_parallax_layer_count`], which might be costly. /// /// Use the [`parallax_mapping_method`] and [`max_parallax_layer_count`] fields /// to tweak the shader, trading graphical quality for performance. /// /// To improve performance, set your `depth_map`'s [`Image::sampler_descriptor`] /// filter mode to `FilterMode::Nearest`, as [this paper] indicates, it improves /// performance a bit. /// /// To reduce artifacts, avoid steep changes in depth, blurring the depth /// map helps with this. /// /// Larger depth maps haves a disproportionate performance impact. /// /// [this paper]: https://www.diva-portal.org/smash/get/diva2:831762/FULLTEXT01.pdf /// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping /// [`parallax_depth_scale`]: StandardMaterial::parallax_depth_scale /// [`parallax_mapping_method`]: StandardMaterial::parallax_mapping_method /// [`max_parallax_layer_count`]: StandardMaterial::max_parallax_layer_count #[texture(11)] #[sampler(12)] pub depth_map: Option>, /// How deep the offset introduced by the depth map should be. /// /// Default is `0.1`, anything over that value may look distorted. /// Lower values lessen the effect. /// /// The depth is relative to texture size. This means that if your texture /// occupies a surface of `1` world unit, and `parallax_depth_scale` is `0.1`, then /// the in-world depth will be of `0.1` world units. /// If the texture stretches for `10` world units, then the final depth /// will be of `1` world unit. pub parallax_depth_scale: f32, /// Which parallax mapping method to use. /// /// We recommend that all objects use the same [`ParallaxMappingMethod`], to avoid /// duplicating and running two shaders. pub parallax_mapping_method: ParallaxMappingMethod, /// In how many layers to split the depth maps for parallax mapping. /// /// If you are seeing jaggy edges, increase this value. /// However, this incurs a performance cost. /// /// Dependent on the situation, switching to [`ParallaxMappingMethod::Relief`] /// and keeping this value low might have better performance than increasing the /// layer count while using [`ParallaxMappingMethod::Occlusion`]. /// /// Default is `16.0`. pub max_parallax_layer_count: f32, } impl Default for StandardMaterial { fn default() -> Self { StandardMaterial { // White because it gets multiplied with texture values if someone uses // a texture. base_color: Color::rgb(1.0, 1.0, 1.0), base_color_texture: None, emissive: Color::BLACK, emissive_texture: None, // Matches Blender's default roughness. perceptual_roughness: 0.5, // Metallic should generally be set to 0.0 or 1.0. metallic: 0.0, 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 // 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, fog_enabled: true, alpha_mode: AlphaMode::Opaque, depth_bias: 0.0, depth_map: None, parallax_depth_scale: 0.1, max_parallax_layer_count: 16.0, parallax_mapping_method: ParallaxMappingMethod::Occlusion, } } } impl From 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> for StandardMaterial { fn from(texture: Handle) -> 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! { /// Bitflags info about the material a shader is currently rendering. /// This is accessible in the shader in the [`StandardMaterialUniform`] #[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 TWO_COMPONENT_NORMAL_MAP = (1 << 6); const FLIP_NORMAL_MAP_Y = (1 << 7); const FOG_ENABLED = (1 << 8); const DEPTH_MAP = (1 << 9); // Used for parallax mapping const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode` const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into const ALPHA_MODE_MASK = (1 << Self::ALPHA_MODE_SHIFT_BITS); // the bitmask, and can range from 0 to 7. const ALPHA_MODE_BLEND = (2 << Self::ALPHA_MODE_SHIFT_BITS); // const ALPHA_MODE_PREMULTIPLIED = (3 << Self::ALPHA_MODE_SHIFT_BITS); // const ALPHA_MODE_ADD = (4 << Self::ALPHA_MODE_SHIFT_BITS); // Right now only values 0–5 are used, which still gives const ALPHA_MODE_MULTIPLY = (5 << Self::ALPHA_MODE_SHIFT_BITS); // ← us "room" for two more modes without adding more bits const NONE = 0; const UNINITIALIZED = 0xFFFF; } } impl StandardMaterialFlags { const ALPHA_MODE_MASK_BITS: u32 = 0b111; const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones(); } /// 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, /// The [`StandardMaterialFlags`] accessible in the `wgsl` shader. 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, /// The depth of the [`StandardMaterial::depth_map`] to apply. pub parallax_depth_scale: f32, /// In how many layers to split the depth maps for Steep parallax mapping. /// /// If your `parallax_depth_scale` is >0.1 and you are seeing jaggy edges, /// increase this value. However, this incurs a performance cost. pub max_parallax_layer_count: f32, /// Using [`ParallaxMappingMethod::Relief`], how many additional /// steps to use at most to find the depth value. pub max_relief_mapping_search_steps: u32, } impl AsBindGroupShaderType for StandardMaterial { fn as_bind_group_shader_type(&self, images: &RenderAssets) -> 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; } if self.fog_enabled { flags |= StandardMaterialFlags::FOG_ENABLED; } if self.depth_map.is_some() { flags |= StandardMaterialFlags::DEPTH_MAP; } 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, AlphaMode::Premultiplied => flags |= StandardMaterialFlags::ALPHA_MODE_PREMULTIPLIED, AlphaMode::Add => flags |= StandardMaterialFlags::ALPHA_MODE_ADD, AlphaMode::Multiply => flags |= StandardMaterialFlags::ALPHA_MODE_MULTIPLY, }; StandardMaterialUniform { base_color: self.base_color.as_linear_rgba_f32().into(), emissive: self.emissive.as_linear_rgba_f32().into(), roughness: self.perceptual_roughness, metallic: self.metallic, reflectance: self.reflectance, flags: flags.bits(), alpha_cutoff, parallax_depth_scale: self.parallax_depth_scale, max_parallax_layer_count: self.max_parallax_layer_count, max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), } } } /// The pipeline key for [`StandardMaterial`]. #[derive(Clone, PartialEq, Eq, Hash)] pub struct StandardMaterialKey { normal_map: bool, cull_mode: Option, depth_bias: i32, relief_mapping: bool, } impl From<&StandardMaterial> for StandardMaterialKey { fn from(material: &StandardMaterial) -> Self { StandardMaterialKey { normal_map: material.normal_map_texture.is_some(), cull_mode: material.cull_mode, depth_bias: material.depth_bias as i32, relief_mapping: matches!( material.parallax_mapping_method, ParallaxMappingMethod::Relief { .. } ), } } } impl Material for StandardMaterial { fn specialize( _pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, _layout: &MeshVertexBufferLayout, key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { if let Some(fragment) = descriptor.fragment.as_mut() { let shader_defs = &mut fragment.shader_defs; if key.bind_group_data.normal_map { shader_defs.push("STANDARDMATERIAL_NORMAL_MAP".into()); } if key.bind_group_data.relief_mapping { shader_defs.push("RELIEF_MAPPING".into()); } } descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; if let Some(label) = &mut descriptor.label { *label = format!("pbr_{}", *label).into(); } if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() { depth_stencil.bias.constant = key.bind_group_data.depth_bias; } Ok(()) } fn prepass_fragment_shader() -> ShaderRef { PBR_PREPASS_SHADER_HANDLE.typed().into() } 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 } }