Add PBR textures (#1632)

This PR adds normal maps on top of PBR #1554. Once that PR lands, the changes should look simpler.

Edit: Turned out to be so little extra work, I added metallic/roughness texture too. And occlusion and emissive.

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Jonas Matser 2021-03-26 21:00:34 +00:00
parent 0c374df712
commit 9a78addff0
10 changed files with 264 additions and 12 deletions

View file

@ -95,6 +95,12 @@ async fn load_gltf<'a, 'b>(
if let Some(texture) = material.occlusion_texture() {
linear_textures.insert(texture.texture().index());
}
if let Some(texture) = material
.pbr_metallic_roughness()
.metallic_roughness_texture()
{
linear_textures.insert(texture.texture().index());
}
}
let mut meshes = vec![];
@ -122,6 +128,13 @@ async fn load_gltf<'a, 'b>(
mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute);
}
if let Some(vertex_attribute) = reader
.read_tangents()
.map(|v| VertexAttributeValues::Float4(v.collect()))
{
mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute);
}
if let Some(vertex_attribute) = reader
.read_tex_coords(0)
.map(|v| VertexAttributeValues::Float2(v.into_f32().collect()))
@ -279,8 +292,52 @@ async fn load_gltf<'a, 'b>(
fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<StandardMaterial> {
let material_label = material_label(&material);
let pbr = material.pbr_metallic_roughness();
let texture_handle = if let Some(info) = pbr.base_color_texture() {
let color = pbr.base_color_factor();
let base_color_texture = if let Some(info) = pbr.base_color_texture() {
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
let label = texture_label(&info.texture());
let path = AssetPath::new_ref(load_context.path(), Some(&label));
Some(load_context.get_handle(path))
} else {
None
};
let normal_map = if let Some(normal_texture) = material.normal_texture() {
// TODO: handle normal_texture.scale
// TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords)
let label = texture_label(&normal_texture.texture());
let path = AssetPath::new_ref(load_context.path(), Some(&label));
Some(load_context.get_handle(path))
} else {
None
};
let metallic_roughness_texture = if let Some(info) = pbr.metallic_roughness_texture() {
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
let label = texture_label(&info.texture());
let path = AssetPath::new_ref(load_context.path(), Some(&label));
Some(load_context.get_handle(path))
} else {
None
};
let occlusion_texture = if let Some(occlusion_texture) = material.occlusion_texture() {
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
let label = texture_label(&occlusion_texture.texture());
let path = AssetPath::new_ref(load_context.path(), Some(&label));
Some(load_context.get_handle(path))
} else {
None
};
let emissive = material.emissive_factor();
let emissive_texture = if let Some(info) = material.emissive_texture() {
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
let label = texture_label(&info.texture());
let path = AssetPath::new_ref(load_context.path(), Some(&label));
Some(load_context.get_handle(path))
@ -288,14 +345,19 @@ fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<
None
};
let color = pbr.base_color_factor();
load_context.set_labeled_asset(
&material_label,
LoadedAsset::new(StandardMaterial {
base_color: Color::rgba(color[0], color[1], color[2], color[3]),
base_color_texture: texture_handle,
base_color_texture,
roughness: pbr.roughness_factor(),
metallic: pbr.metallic_factor(),
metallic_roughness_texture,
normal_map,
double_sided: material.double_sided(),
occlusion_texture,
emissive: Color::rgba(emissive[0], emissive[1], emissive[2], 1.0),
emissive_texture,
unlit: material.unlit(),
..Default::default()
}),

View file

@ -24,7 +24,21 @@ pub struct StandardMaterial {
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
#[shader_def]
pub metallic_roughness_texture: Option<Handle<Texture>>,
pub reflectance: f32,
#[shader_def]
pub normal_map: Option<Handle<Texture>>,
#[render_resources(ignore)]
#[shader_def]
pub double_sided: bool,
#[shader_def]
pub occlusion_texture: Option<Handle<Texture>>,
// 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: Color,
#[shader_def]
pub emissive_texture: Option<Handle<Texture>>,
#[render_resources(ignore)]
#[shader_def]
pub unlit: bool,
@ -45,7 +59,13 @@ impl Default for StandardMaterial {
metallic: 0.01,
// 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
metallic_roughness_texture: None,
reflectance: 0.5,
normal_map: None,
double_sided: false,
occlusion_texture: None,
emissive: Color::BLACK,
emissive_texture: None,
unlit: false,
}
}

View file

@ -48,6 +48,10 @@ layout(location = 0) in vec3 v_WorldPosition;
layout(location = 1) in vec3 v_WorldNormal;
layout(location = 2) in vec2 v_Uv;
#ifdef STANDARDMATERIAL_NORMAL_MAP
layout(location = 3) in vec4 v_WorldTangent;
#endif
layout(location = 0) out vec4 o_Target;
layout(set = 0, binding = 0) uniform CameraViewProj {
@ -83,10 +87,38 @@ layout(set = 3, binding = 4) uniform StandardMaterial_metallic {
float metallic;
};
layout(set = 3, binding = 5) uniform StandardMaterial_reflectance {
# ifdef STANDARDMATERIAL_METALLIC_ROUGHNESS_TEXTURE
layout(set = 3, binding = 5) uniform texture2D StandardMaterial_metallic_roughness_texture;
layout(set = 3,
binding = 6) uniform sampler StandardMaterial_metallic_roughness_texture_sampler;
# endif
layout(set = 3, binding = 7) uniform StandardMaterial_reflectance {
float reflectance;
};
# ifdef STANDARDMATERIAL_NORMAL_MAP
layout(set = 3, binding = 8) uniform texture2D StandardMaterial_normal_map;
layout(set = 3,
binding = 9) uniform sampler StandardMaterial_normal_map_sampler;
# endif
# if defined(STANDARDMATERIAL_OCCLUSION_TEXTURE)
layout(set = 3, binding = 10) uniform texture2D StandardMaterial_occlusion_texture;
layout(set = 3,
binding = 11) uniform sampler StandardMaterial_occlusion_texture_sampler;
# endif
layout(set = 3, binding = 12) uniform StandardMaterial_emissive {
vec4 emissive;
};
# if defined(STANDARDMATERIAL_EMISSIVE_TEXTURE)
layout(set = 3, binding = 13) uniform texture2D StandardMaterial_emissive_texture;
layout(set = 3,
binding = 14) uniform sampler StandardMaterial_emissive_texture_sampler;
# endif
# define saturate(x) clamp(x, 0.0, 1.0)
const float PI = 3.141592653589793;
@ -258,10 +290,46 @@ void main() {
#ifndef STANDARDMATERIAL_UNLIT
// calculate non-linear roughness from linear perceptualRoughness
# ifdef STANDARDMATERIAL_METALLIC_ROUGHNESS_TEXTURE
vec4 metallic_roughness = texture(sampler2D(StandardMaterial_metallic_roughness_texture, StandardMaterial_metallic_roughness_texture_sampler), v_Uv);
// Sampling from GLTF standard channels for now
float metallic = metallic * metallic_roughness.b;
float perceptual_roughness = perceptual_roughness * metallic_roughness.g;
# endif
float roughness = perceptualRoughnessToRoughness(perceptual_roughness);
vec3 N = normalize(v_WorldNormal);
# ifdef STANDARDMATERIAL_NORMAL_MAP
vec3 T = normalize(v_WorldTangent.xyz);
vec3 B = cross(N, T) * v_WorldTangent.w;
# endif
# ifdef STANDARDMATERIAL_DOUBLE_SIDED
N = gl_FrontFacing ? N : -N;
# ifdef STANDARDMATERIAL_NORMAL_MAP
T = gl_FrontFacing ? T : -T;
B = gl_FrontFacing ? B : -B;
# endif
# endif
# ifdef STANDARDMATERIAL_NORMAL_MAP
mat3 TBN = mat3(T, B, N);
N = TBN * normalize(texture(sampler2D(StandardMaterial_normal_map, StandardMaterial_normal_map_sampler), v_Uv).rgb * 2.0 - 1.0);
# endif
# ifdef STANDARDMATERIAL_OCCLUSION_TEXTURE
float occlusion = texture(sampler2D(StandardMaterial_occlusion_texture, StandardMaterial_occlusion_texture_sampler), v_Uv).r;
# else
float occlusion = 1.0;
# endif
# ifdef STANDARDMATERIAL_EMISSIVE_TEXTURE
// TODO use .a for exposure compensation in HDR
emissive.rgb *= texture(sampler2D(StandardMaterial_emissive_texture, StandardMaterial_emissive_texture_sampler), v_Uv).rgb;
# endif
vec3 V = normalize(CameraPos.xyz - v_WorldPosition.xyz);
// Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"
float NdotV = max(dot(N, V), 1e-4);
@ -310,7 +378,9 @@ void main() {
vec3 diffuse_ambient = EnvBRDFApprox(diffuseColor, 1.0, NdotV);
vec3 specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV);
output_color.rgb = light_accum + (diffuse_ambient + specular_ambient) * AmbientColor;
output_color.rgb = light_accum;
output_color.rgb += (diffuse_ambient + specular_ambient) * AmbientColor * occlusion;
output_color.rgb += emissive.rgb * output_color.a;
// tone_mapping
output_color.rgb = reinhard_luminance(output_color.rgb);

View file

@ -4,6 +4,10 @@ layout(location = 0) in vec3 Vertex_Position;
layout(location = 1) in vec3 Vertex_Normal;
layout(location = 2) in vec2 Vertex_Uv;
#ifdef STANDARDMATERIAL_NORMAL_MAP
layout(location = 3) in vec4 Vertex_Tangent;
#endif
layout(location = 0) out vec3 v_WorldPosition;
layout(location = 1) out vec3 v_WorldNormal;
layout(location = 2) out vec2 v_Uv;
@ -12,6 +16,10 @@ layout(set = 0, binding = 0) uniform CameraViewProj {
mat4 ViewProj;
};
#ifdef STANDARDMATERIAL_NORMAL_MAP
layout(location = 3) out vec4 v_WorldTangent;
#endif
layout(set = 2, binding = 0) uniform Transform {
mat4 Model;
};
@ -21,5 +29,8 @@ void main() {
v_WorldPosition = world_position.xyz;
v_WorldNormal = mat3(Model) * Vertex_Normal;
v_Uv = Vertex_Uv;
#ifdef STANDARDMATERIAL_NORMAL_MAP
v_WorldTangent = vec4(mat3(Model) * Vertex_Tangent.xyz, Vertex_Tangent.w);
#endif
gl_Position = ViewProj * world_position;
}

View file

@ -237,6 +237,9 @@ impl Mesh {
/// The direction the vertex normal is facing in.
/// Use in conjunction with [`Mesh::set_attribute`]
pub const ATTRIBUTE_NORMAL: &'static str = "Vertex_Normal";
/// The direction of the vertex tangent. Used for normal mapping
pub const ATTRIBUTE_TANGENT: &'static str = "Vertex_Tangent";
/// Where the vertex is located in space. Use in conjunction with [`Mesh::set_attribute`]
pub const ATTRIBUTE_POSITION: &'static str = "Vertex_Position";
/// Texture coordinates for the vertex. Use in conjunction with [`Mesh::set_attribute`]

View file

@ -23,3 +23,4 @@ pub struct ShaderLayout {
pub const GL_VERTEX_INDEX: &str = "gl_VertexIndex";
pub const GL_INSTANCE_INDEX: &str = "gl_InstanceIndex";
pub const GL_FRONT_FACING: &str = "gl_FrontFacing";

View file

@ -3,7 +3,7 @@ use crate::{
BindGroupDescriptor, BindType, BindingDescriptor, BindingShaderStage, InputStepMode,
UniformProperty, VertexAttribute, VertexBufferLayout, VertexFormat,
},
shader::{ShaderLayout, GL_INSTANCE_INDEX, GL_VERTEX_INDEX},
shader::{ShaderLayout, GL_FRONT_FACING, GL_INSTANCE_INDEX, GL_VERTEX_INDEX},
texture::{TextureSampleType, TextureViewDimension},
};
use bevy_core::AsBytes;
@ -33,6 +33,7 @@ impl ShaderLayout {
for input_variable in module.enumerate_input_variables(None).unwrap() {
if input_variable.name == GL_VERTEX_INDEX
|| input_variable.name == GL_INSTANCE_INDEX
|| input_variable.name == GL_FRONT_FACING
{
continue;
}

View file

@ -1,21 +1,39 @@
use bevy::prelude::*;
use bevy::{pbr::AmbientLight, prelude::*};
fn main() {
App::build()
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 1.0 / 5.0f32,
})
.insert_resource(Msaa { samples: 4 })
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.add_system(rotator_system.system())
.run();
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn_scene(asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"));
commands.spawn_bundle(LightBundle {
transform: Transform::from_xyz(4.0, 5.0, 4.0),
..Default::default()
});
commands.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
..Default::default()
});
commands
.spawn_bundle(LightBundle {
transform: Transform::from_xyz(3.0, 5.0, 3.0),
..Default::default()
})
.insert(Rotates);
}
/// this component indicates what entities should rotate
struct Rotates;
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
for mut transform in query.iter_mut() {
*transform = Transform::from_rotation(Quat::from_rotation_y(
(4.0 * std::f32::consts::PI / 20.0) * time.delta_seconds(),
)) * *transform;
}
}

View file

@ -0,0 +1,66 @@
use std::f32::consts::PI;
use bevy::{pbr::AmbientLight, prelude::*};
fn main() {
App::build()
.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 1.0 / 5.0f32,
})
.insert_resource(Msaa { samples: 4 })
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.add_system(rotator_system.system())
.run();
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn_scene(asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"));
// Add a rotating light with a sphere to show it's position
commands
.spawn_bundle((Transform::default(), GlobalTransform::default(), Rotates))
.with_children(|parent| {
parent
.spawn_bundle(LightBundle {
transform: Transform::from_translation(Vec3::new(0.0, 0.7, 2.0)),
..Default::default()
})
.with_children(|parent| {
parent.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Icosphere {
radius: 0.05,
subdivisions: 32,
})),
material: materials.add(StandardMaterial {
base_color: Color::YELLOW,
emissive: Color::WHITE * 10.0f32,
..Default::default()
}),
transform: Transform::default(),
..Default::default()
});
});
});
commands.spawn_bundle(PerspectiveCameraBundle {
transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
..Default::default()
});
}
/// this component indicates what entities should rotate
struct Rotates;
/// rotates the parent, which will result in the child also rotating
fn rotator_system(time: Res<Time>, mut query: Query<&mut Transform, With<Rotates>>) {
for mut transform in query.iter_mut() {
transform.rotation *= Quat::from_rotation_y((2.0 * PI / 20.0) * time.delta_seconds());
}
}

View file

@ -88,7 +88,7 @@ Example | File | Description
`msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges
`orthographic` | [`3d/orthographic.rs`](./3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look games or CAD applications)
`parenting` | [`3d/parenting.rs`](./3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations
`pbr` | [`3d/pbr.rs`](./3d/[pbr].rs) | Demonstrates PBR properties Roughness/Metallic
`pbr` | [`3d/pbr.rs`](./3d/[pbr].rs) | Demonstrates use of Physically Based Rendering (PBR) properties
`spawner` | [`3d/spawner.rs`](./3d/spawner.rs) | Renders a large number of cubes with changing position and material
`texture` | [`3d/texture.rs`](./3d/texture.rs) | Shows configuration of texture materials
`update_gltf_scene` | [`3d/update_gltf_scene.rs`](./3d/update_gltf_scene.rs) | Update a scene from a gltf file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene