/*!
[`ParallaxMaterial`]: ParallaxMaterial
*/
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![warn(clippy::pedantic, clippy::nursery)]
#[cfg(feature = "inspector-def")]
mod inspector_def;
use bevy::{
asset::load_internal_asset,
pbr::{MaterialPipeline, MaterialPipelineKey, StandardMaterialUniform},
prelude::*,
reflect::TypeUuid,
render::{
mesh::MeshVertexBufferLayout,
render_asset::RenderAssets,
render_resource::{
AsBindGroup, AsBindGroupShaderType, Face, RenderPipelineDescriptor, ShaderRef,
ShaderType, SpecializedMeshPipelineError,
},
},
};
/// The shader handle for `"parallax_map.wgsl"`.
#[allow(clippy::unreadable_literal)]
const PARALLAX_MAPPING_SHADER_HANDLE: HandleUntyped =
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9592100656503623734);
impl From<&'_ ParallaxMaterial> for StandardMaterial {
fn from(mat: &'_ ParallaxMaterial) -> Self {
let opt_clone_weak = |opt: &Option<_>| opt.as_ref().map(Handle::clone_weak);
Self {
base_color: mat.base_color,
base_color_texture: opt_clone_weak(&mat.base_color_texture),
emissive: mat.emissive,
emissive_texture: opt_clone_weak(&mat.emissive_texture),
perceptual_roughness: mat.perceptual_roughness,
metallic: mat.metallic,
metallic_roughness_texture: opt_clone_weak(&mat.metallic_roughness_texture),
reflectance: mat.reflectance,
normal_map_texture: Some(mat.normal_map_texture.clone_weak()),
flip_normal_map_y: mat.flip_normal_map_y,
occlusion_texture: opt_clone_weak(&mat.occlusion_texture),
double_sided: mat.double_sided,
cull_mode: mat.cull_mode,
unlit: mat.unlit,
alpha_mode: mat.alpha_mode,
depth_bias: mat.depth_bias,
}
}
}
/// The pipeline key for [`ParallaxMaterial`], this just copies the
/// [`StandardMaterialKey`] bevy impl.
///
/// [`StandardMaterialKey`]: bevy::pbr::StandardMaterialKey
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct ParallaxMaterialKey {
relief_mapping: bool,
cull_mode: Option<Face>,
}
impl From<&'_ ParallaxMaterial> for ParallaxMaterialKey {
fn from(material: &ParallaxMaterial) -> Self {
Self {
relief_mapping: material.algorithm == ParallaxAlgo::ReliefMapping,
cull_mode: material.cull_mode,
}
}
}
/// The GPU representation of the uniform data of a [`ParallaxMaterial`].
#[derive(Clone, Default, ShaderType)]
pub struct ParallaxMaterialUniform {
/// 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 shader flags.
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 height map.
pub height_depth: f32,
/// In how many layers to split the height maps for Steep parallax mapping.
///
/// If your `height_depth` is >0.1 and you are seeing jaggy edges,
/// increase this value. However, this incures a performance cost.
pub max_height_layers: f32,
}
impl AsBindGroupShaderType<ParallaxMaterialUniform> for ParallaxMaterial {
fn as_bind_group_shader_type(&self, images: &RenderAssets<Image>) -> ParallaxMaterialUniform {
let standard_material: StandardMaterial = self.into();
let standard_uniform: StandardMaterialUniform =
standard_material.as_bind_group_shader_type(images);
ParallaxMaterialUniform {
base_color: standard_uniform.base_color,
emissive: standard_uniform.emissive,
roughness: standard_uniform.roughness,
metallic: standard_uniform.metallic,
reflectance: standard_uniform.reflectance,
flags: standard_uniform.flags,
alpha_cutoff: standard_uniform.alpha_cutoff,
height_depth: self.height_depth,
max_height_layers: self.max_height_layers,
}
}
}
/// A shameless clone of bevy's [default PBR material] with an additional field:
/// `height_map`.
///
/// `height_map` is a greyscale image representing the height of the object at a given
/// pixel. Works like the original [`StandardMaterial`] otherwise.
///
/// **WARNING**: this material _assumes_ the mesh has tangents set. If your mesh doesn't
/// have tangents, bad unspecified things will happen.
///
/// [default PBR material]: StandardMaterial
#[derive(AsBindGroup, Debug, Clone, TypeUuid)]
#[uuid = "5bc9c7a3-fb25-4202-b91f-bc4c7d300d82"]
#[bind_group_data(ParallaxMaterialKey)]
#[uniform(0, ParallaxMaterialUniform)]
pub struct ParallaxMaterial {
/// 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,
/// The "albedo" of the material, when `Some`, this will be the texture applied to the mesh.
#[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,
/// Same as emissive, but based off a texture
#[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,
/// A texture representing both `metallic` and `preceptual_roughness`.
///
/// The blue channel is the `metallic` and green is `roughness` (we don't
/// talk about the red channel)
#[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: 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>,
/// Whether to shade this material.
///
/// Normals, occlusion textures, roughness, metallic, reflectance and
/// emissive are ignored if this is set to `true`.
pub unlit: bool,
/// How to interpret the alpha channel of the `base_color_texture`.
///
/// By default, it's `Opaque`, therefore completely ignored.
/// Note that currently bevy handles poorly semi-transparent textures. You
/// are likely to encounter the following bugs:
///
/// - When two `AlphaMode::Blend` material occupy the same pixel, only one
/// material's color will show.
/// - If a different mesh is both "in front" and "behind" a non-opaque material,
/// bevy won't know which material to display in front, which might result in
/// flickering.
pub alpha_mode: AlphaMode,
/// Re-arange depth of material, useful to avoid z-fighting.
pub depth_bias: f32,
/// The height map used for parallax mapping.
///
/// Black is the tallest, white deepest.
///
/// To improve performance, set your `height_map`'s [`Image::sampler_descriptor`]
/// filter mode to `FilterMode::Nearest`, as [this paper] indicates, it improves
/// perfs a bit.
///
/// [this paper]: https://www.diva-portal.org/smash/get/diva2:831762/FULLTEXT01.pdf
#[texture(11)]
#[sampler(12)]
pub height_map: Handle<Image>,
/// How deep the offset introduced by the height map should be.
///
/// Default is 0.1, anything over that value may look very awkward.
/// Lower value look less "deep."
pub height_depth: f32,
/// Whether to use a more accurate and more expensive algorithm.
///
/// We recommend that all objects use the same [`ParallaxAlgo`], to avoid
/// duplicating and running two shaders.
pub algorithm: ParallaxAlgo,
/// In how many layers to split the height maps for Steep Parallax Mapping.
///
/// If your `height_depth` is `>0.1` and you are seeing jaggy edges,
/// increase this value. However, this incures a performance cost.
///
/// Default is 16.0.
///
/// **This must never be less than `2.0`.**
pub max_height_layers: f32,
}
/// The algorithm to use beyond the initial Steep parallax mapping
/// to compute the displacement of a pixel.
///
/// See the shader code for implementation details and explanation
/// of the methods used.
#[cfg_attr(feature = "inspector-def", derive(bevy_inspector_egui::Inspectable))]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum ParallaxAlgo {
/// A simple linear interpolation, consists of a single texture sample.
#[default]
ParallaxOcclusionMapping,
/// An iterative discovery of up to 5 iteration of the best displacement
/// value. Each iteration incures a texture sample.
ReliefMapping,
}
impl Default for ParallaxMaterial {
fn default() -> Self {
Self {
base_color: Color::rgb(1.0, 1.0, 1.0),
base_color_texture: None,
emissive: Color::BLACK,
emissive_texture: None,
perceptual_roughness: 0.089,
metallic: 0.01,
metallic_roughness_texture: None,
reflectance: 0.5,
occlusion_texture: None,
normal_map_texture: default(),
flip_normal_map_y: false,
double_sided: false,
cull_mode: Some(Face::Back),
unlit: false,
alpha_mode: AlphaMode::Opaque,
depth_bias: 0.0,
height_map: default(),
height_depth: 0.1,
max_height_layers: 16.0,
algorithm: default(),
}
}
}
impl Material for ParallaxMaterial {
fn specialize(
_pipeline: &MaterialPipeline<Self>,
descriptor: &mut RenderPipelineDescriptor,
_layout: &MeshVertexBufferLayout,
key: MaterialPipelineKey<Self>,
) -> Result<(), SpecializedMeshPipelineError> {
let defs = &mut descriptor.fragment.as_mut().unwrap().shader_defs;
if key.bind_group_data.relief_mapping {
defs.push(String::from("RELIEF_MAPPING"));
}
descriptor.primitive.cull_mode = key.bind_group_data.cull_mode;
if let Some(label) = &mut descriptor.label {
*label = format!("parallax_{}", *label).into();
}
Ok(())
}
#[cfg(not(feature = "debug"))]
fn fragment_shader() -> ShaderRef {
PARALLAX_MAPPING_SHADER_HANDLE.typed::<Shader>().into()
}
#[cfg(feature = "debug")]
fn fragment_shader() -> ShaderRef {
"parallax_map.wgsl".into()
}
#[inline]
fn alpha_mode(&self) -> AlphaMode {
self.alpha_mode
}
#[inline]
fn depth_bias(&self) -> f32 {
self.depth_bias
}
}
/// Add this plugin to your app to use [`ParallaxMaterial`].
pub struct ParallaxMaterialPlugin;
impl Plugin for ParallaxMaterialPlugin {
fn build(&self, app: &mut App) {
#[cfg(not(feature = "debug"))]
load_internal_asset!(
app,
PARALLAX_MAPPING_SHADER_HANDLE,
"parallax_map.wgsl",
Shader::from_wgsl
);
app.add_plugin(MaterialPlugin::<ParallaxMaterial>::default());
#[cfg(feature = "inspector-def")]
{
use bevy_inspector_egui::RegisterInspectable;
app.register_inspectable::<ParallaxMaterial>()
.register_inspectable::<Handle<ParallaxMaterial>>();
}
}
}