
# 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
|
|
}
|
|
}
|