bevy/crates/bevy_pbr/src/pbr_material.rs
Jakob Hellermann f867319336 add ReflectAsset and ReflectHandle (#5923)
# Objective
![image](https://user-images.githubusercontent.com/22177966/189350194-639a0211-e984-4f73-ae62-0ede44891eb9.png)

^ enable this

Concretely, I need to
- list all handle ids for an asset type
- fetch the asset as `dyn Reflect`, given a `HandleUntyped`
- when encountering a `Handle<T>`, find out what asset type that handle refers to (`T`'s type id) and turn the handle into a `HandleUntyped`

## Solution

- add `ReflectAsset` type containing function pointers for working with assets
```rust
pub struct ReflectAsset {
    type_uuid: Uuid,
    assets_resource_type_id: TypeId, // TypeId of the `Assets<T>` resource

    get: fn(&World, HandleUntyped) -> Option<&dyn Reflect>,
    get_mut: fn(&mut World, HandleUntyped) -> Option<&mut dyn Reflect>,
    get_unchecked_mut: unsafe fn(&World, HandleUntyped) -> Option<&mut dyn Reflect>,
    add: fn(&mut World, &dyn Reflect) -> HandleUntyped,
    set: fn(&mut World, HandleUntyped, &dyn Reflect) -> HandleUntyped,
    len: fn(&World) -> usize,
    ids: for<'w> fn(&'w World) -> Box<dyn Iterator<Item = HandleId> + 'w>,
    remove: fn(&mut World, HandleUntyped) -> Option<Box<dyn Reflect>>,
}
```
- add `ReflectHandle` type relating the handle back to the asset type and providing a way to create a `HandleUntyped`
```rust
pub struct ReflectHandle {
    type_uuid: Uuid,
    asset_type_id: TypeId,
    downcast_handle_untyped: fn(&dyn Any) -> Option<HandleUntyped>,
}
```
- add the corresponding `FromType` impls
- add a function `app.register_asset_reflect` which is supposed to be called after `.add_asset` and registers `ReflectAsset` and `ReflectHandle` in the type registry
---

## Changelog

- add `ReflectAsset` and `ReflectHandle` types, which allow code to use reflection to manipulate arbitrary assets without knowing their types at compile time
2022-10-28 20:42:33 +00:00

445 lines
17 KiB
Rust

use crate::{AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_SHADER_HANDLE};
use bevy_asset::Handle;
use bevy_math::Vec4;
use bevy_reflect::{std_traits::ReflectDefault, FromReflect, 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, Reflect, FromReflect, 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<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,
/// 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<Handle<Image>>,
/// Linear perceptual roughness, clamped to `[0.089, 1.0]` in the shader.
///
/// Defaults to minimum of `0.089`.
///
/// 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`.
pub perceptual_roughness: f32,
/// How "metallic" the material appears, within `[0.0, 1.0]`,
/// going from dielectric to pure metallic.
///
/// Defaults to `0.01`.
///
/// The closer to `1` the value, the more the material will
/// reflect light like a metal such as steel or gold.
///
/// 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<Handle<Image>>,
/// 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<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.
/// 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.
/// Conversly, 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<Face>,
/// 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,
/// How to apply the alpha channel of the `base_color_texture`.
///
/// See [`AlphaMode`] for details. Defaults to [`AlphaMode::Opaque`].
pub alpha_mode: AlphaMode,
/// Re-arrange render ordering.
///
/// 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` only affects render ordering. This means that for opaque materials,
/// `depth_bias` will only have any effect if two materials are overlapping,
/// which only serves as a [z-fighting] resolver.
///
/// `depth_bias` can however reorder [`AlphaMode::Blend`] materials.
/// This is useful if your transparent materials are not rendering
/// in the expected order.
///
/// [z-fighting]: https://en.wikipedia.org/wiki/Z-fighting
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! {
/// 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 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,
/// 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,
}
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
}
}