Use the infinite reverse right-handed perspective projection (#2543)

# Objective

Forward perspective projections have poor floating point precision distribution over the depth range. Reverse projections fair much better, and instead of having to have a far plane, with the reverse projection, using an infinite far plane is not a problem. The infinite reverse perspective projection has become the industry standard. The renderer rework is a great time to migrate to it.

## Solution

All perspective projections, including point lights, have been moved to using `glam::Mat4::perspective_infinite_reverse_rh()` and so have no far plane. As various depth textures are shared between orthographic and perspective projections, a quirk of this PR is that the near and far planes of the orthographic projection are swapped when the Mat4 is computed. This has no impact on 2D/3D orthographic projection usage, and provides consistency in shaders, texture clear values, etc. throughout the codebase.

## Known issues

For some reason, when looking along -Z, all geometry is black. The camera can be translated up/down / strafed left/right and geometry will still be black. Moving forward/backward or rotating the camera away from looking exactly along -Z causes everything to work as expected.

I have tried to debug this issue but both in macOS and Windows I get crashes when doing pixel debugging. If anyone could reproduce this and debug it I would be very grateful. Otherwise I will have to try to debug it further without pixel debugging, though the projections and such all looked fine to me.
This commit is contained in:
Robert Swain 2021-08-27 20:15:09 +00:00
parent cea7db1050
commit 045f324e97
13 changed files with 153 additions and 44 deletions

View file

@ -179,6 +179,10 @@ path = "examples/3d/msaa.rs"
name = "orthographic"
path = "examples/3d/orthographic.rs"
[[example]]
name = "orthographic_pipelined"
path = "examples/3d/orthographic_pipelined.rs"
[[example]]
name = "parenting"
path = "examples/3d/parenting.rs"

View file

@ -36,11 +36,7 @@ fn setup(
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.insert_resource(AmbientLight {
color: Color::ORANGE_RED,
brightness: 0.02,
});
// plane
// ground plane
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 10.0 })),
material: materials.add(StandardMaterial {
@ -51,6 +47,7 @@ fn setup(
..Default::default()
});
// left wall
let mut transform = Transform::from_xyz(2.5, 2.5, 0.0);
transform.rotate(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2));
commands.spawn_bundle(PbrBundle {
@ -63,7 +60,7 @@ fn setup(
}),
..Default::default()
});
// back (right) wall
let mut transform = Transform::from_xyz(0.0, 2.5, -2.5);
transform.rotate(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2));
commands.spawn_bundle(PbrBundle {
@ -76,6 +73,7 @@ fn setup(
}),
..Default::default()
});
// cube
commands
.spawn_bundle(PbrBundle {
@ -104,7 +102,13 @@ fn setup(
})
.insert(Movable);
// light
// ambient light
commands.insert_resource(AmbientLight {
color: Color::ORANGE_RED,
brightness: 0.02,
});
// red point light
commands
.spawn_bundle(PointLightBundle {
// transform: Transform::from_xyz(5.0, 8.0, 2.0),
@ -131,7 +135,7 @@ fn setup(
});
});
// light
// green point light
commands
.spawn_bundle(PointLightBundle {
// transform: Transform::from_xyz(5.0, 8.0, 2.0),
@ -158,7 +162,7 @@ fn setup(
});
});
// light
// blue point light
commands
.spawn_bundle(PointLightBundle {
// transform: Transform::from_xyz(5.0, 8.0, 2.0),
@ -185,6 +189,7 @@ fn setup(
});
});
// directional 'sun' light
const HALF_SIZE: f32 = 10.0;
commands.spawn_bundle(DirectionalLightBundle {
directional_light: DirectionalLight {

View file

@ -0,0 +1,71 @@
use bevy::{
ecs::prelude::*,
math::Vec3,
pbr2::{PbrBundle, PointLightBundle, StandardMaterial},
prelude::{App, Assets, Transform},
render2::{
camera::OrthographicCameraBundle,
color::Color,
mesh::{shape, Mesh},
},
PipelinedDefaultPlugins,
};
fn main() {
App::new()
.add_plugins(PipelinedDefaultPlugins)
.add_startup_system(setup.system())
.run();
}
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// set up the camera
let mut camera = OrthographicCameraBundle::new_3d();
camera.orthographic_projection.scale = 3.0;
camera.transform = Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y);
// camera
commands.spawn_bundle(camera);
// plane
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })),
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
..Default::default()
});
// cubes
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform: Transform::from_xyz(1.5, 0.5, 1.5),
..Default::default()
});
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform: Transform::from_xyz(1.5, 0.5, -1.5),
..Default::default()
});
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform: Transform::from_xyz(-1.5, 0.5, 1.5),
..Default::default()
});
commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform: Transform::from_xyz(-1.5, 0.5, -1.5),
..Default::default()
});
// light
commands.spawn_bundle(PointLightBundle {
transform: Transform::from_xyz(3.0, 8.0, 5.0),
..Default::default()
});
}

View file

@ -103,6 +103,7 @@ Example | File | Description
`load_gltf_pipelined` | [`3d/load_gltf_pipelined.rs`](./3d/load_gltf_pipelined.rs) | Loads and renders a gltf file as a scene
`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)
`orthographic_pipelined` | [`3d/orthographic_pipelined.rs`](./3d/orthographic_pipelined.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 use of Physically Based Rendering (PBR) properties
`pbr_pipelined` | [`3d/pbr_pipelined.rs`](./3d/pbr_pipelined.rs) | Demonstrates use of Physically Based Rendering (PBR) properties

View file

@ -62,7 +62,7 @@ impl Node for MainPass3dNode {
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: depth_texture,
depth_ops: Some(Operations {
load: LoadOp::Clear(1.0),
load: LoadOp::Clear(0.0),
store: true,
}),
stencil_ops: None,

View file

@ -46,7 +46,7 @@ impl Default for PointLight {
impl PointLight {
pub const DEFAULT_SHADOW_DEPTH_BIAS: f32 = 0.02;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.5;
pub const DEFAULT_SHADOW_NORMAL_BIAS: f32 = 0.6;
}
#[derive(Clone, Debug)]

View file

@ -1,6 +1,8 @@
// NOTE: Keep in sync with pbr.wgsl
[[block]]
struct View {
view_proj: mat4x4<f32>;
projection: mat4x4<f32>;
world_position: vec3<f32>;
};
[[group(0), binding(0)]]
@ -9,7 +11,7 @@ var view: View;
[[block]]
struct Mesh {
transform: mat4x4<f32>;
model: mat4x4<f32>;
};
[[group(1), binding(0)]]
var mesh: Mesh;
@ -25,6 +27,6 @@ struct VertexOutput {
[[stage(vertex)]]
fn vertex(vertex: Vertex) -> VertexOutput {
var out: VertexOutput;
out.clip_position = view.view_proj * mesh.transform * vec4<f32>(vertex.position, 1.0);
out.clip_position = view.view_proj * mesh.model * vec4<f32>(vertex.position, 1.0);
return out;
}

View file

@ -54,8 +54,8 @@ pub type ExtractedDirectionalLightShadowMap = DirectionalLightShadowMap;
#[repr(C)]
#[derive(Copy, Clone, AsStd140, Default, Debug)]
pub struct GpuPointLight {
projection: Mat4,
color: Vec4,
// proj: Mat4,
position: Vec3,
inverse_square_range: f32,
radius: f32,
@ -120,7 +120,7 @@ impl FromWorld for ShadowShaders {
has_dynamic_offset: true,
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(80),
min_binding_size: BufferSize::new(144),
},
count: None,
},
@ -168,7 +168,7 @@ impl FromWorld for ShadowShaders {
depth_stencil: Some(DepthStencilState {
format: SHADOW_FORMAT,
depth_write_enabled: true,
depth_compare: CompareFunction::LessEqual,
depth_compare: CompareFunction::GreaterEqual,
stencil: StencilState {
front: StencilFaceState::IGNORE,
back: StencilFaceState::IGNORE,
@ -205,7 +205,7 @@ impl FromWorld for ShadowShaders {
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
compare: Some(CompareFunction::LessEqual),
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
}),
directional_light_sampler: render_device.create_sampler(&SamplerDescriptor {
@ -215,7 +215,7 @@ impl FromWorld for ShadowShaders {
mag_filter: FilterMode::Linear,
min_filter: FilterMode::Linear,
mipmap_filter: FilterMode::Nearest,
compare: Some(CompareFunction::LessEqual),
compare: Some(CompareFunction::GreaterEqual),
..Default::default()
}),
}
@ -435,7 +435,7 @@ pub fn prepare_lights(
// TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query
for (light_index, light) in point_lights.iter().enumerate().take(MAX_POINT_LIGHTS) {
let projection =
Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, light.range);
Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1);
// ignore scale because we don't want to effectively scale light radius and range
// by applying those as a view transform to shadow map rendering of objects
@ -484,6 +484,7 @@ pub fn prepare_lights(
}
gpu_lights.point_lights[light_index] = GpuPointLight {
projection,
// premultiply color by intensity
// we don't use the alpha at all, so no reason to multiply only [0..3]
color: (light.color.as_rgba_linear() * light.intensity).into(),
@ -492,7 +493,6 @@ pub fn prepare_lights(
inverse_square_range: 1.0 / (light.range * light.range),
near: 0.1,
far: light.range,
// proj: projection,
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
};
@ -656,7 +656,7 @@ impl Node for ShadowPassNode {
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &view_light.depth_texture_view,
depth_ops: Some(Operations {
load: LoadOp::Clear(1.0),
load: LoadOp::Clear(0.0),
store: true,
}),
stencil_ops: None,

View file

@ -53,7 +53,7 @@ impl FromWorld for PbrShaders {
has_dynamic_offset: true,
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(80),
min_binding_size: BufferSize::new(144),
},
count: None,
},
@ -66,7 +66,7 @@ impl FromWorld for PbrShaders {
has_dynamic_offset: true,
// TODO: change this to GpuLights::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(1024),
min_binding_size: BufferSize::new(1424),
},
count: None,
},
@ -225,7 +225,9 @@ impl FromWorld for PbrShaders {
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(80),
// TODO: change this to MeshUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(144),
},
count: None,
}],
@ -291,7 +293,7 @@ impl FromWorld for PbrShaders {
depth_stencil: Some(DepthStencilState {
format: TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: CompareFunction::Less,
depth_compare: CompareFunction::Greater,
stencil: StencilState {
front: StencilFaceState::IGNORE,
back: StencilFaceState::IGNORE,
@ -455,6 +457,7 @@ struct MeshDrawInfo {
#[derive(Debug, AsStd140)]
pub struct MeshUniform {
model: Mat4,
inverse_transpose_model: Mat4,
flags: u32,
}
@ -486,13 +489,16 @@ pub fn prepare_meshes(
.transform_uniforms
.reserve_and_clear(extracted_meshes.meshes.len(), &render_device);
for extracted_mesh in extracted_meshes.meshes.iter_mut() {
let model = extracted_mesh.transform;
let inverse_transpose_model = model.inverse().transpose();
let flags = if extracted_mesh.receives_shadows {
MeshFlags::SHADOW_RECEIVER
} else {
MeshFlags::NONE
};
extracted_mesh.transform_binding_offset = mesh_meta.transform_uniforms.push(MeshUniform {
model: extracted_mesh.transform,
model,
inverse_transpose_model,
flags: flags.bits,
});
}

View file

@ -1,7 +1,9 @@
// TODO: try merging this block with the binding?
// NOTE: Keep in sync with depth.wgsl
[[block]]
struct View {
view_proj: mat4x4<f32>;
projection: mat4x4<f32>;
world_position: vec3<f32>;
};
@ -9,6 +11,7 @@ struct View {
[[block]]
struct Mesh {
model: mat4x4<f32>;
inverse_transpose_model: mat4x4<f32>;
// 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options.
flags: u32;
};
@ -41,9 +44,11 @@ fn vertex(vertex: Vertex) -> VertexOutput {
out.uv = vertex.uv;
out.world_position = world_position;
out.clip_position = view.view_proj * world_position;
// FIXME: The inverse transpose of the model matrix should be used to correctly handle scaling
// of normals
out.world_normal = mat3x3<f32>(mesh.model.x.xyz, mesh.model.y.xyz, mesh.model.z.xyz) * vertex.normal;
out.world_normal = mat3x3<f32>(
mesh.inverse_transpose_model.x.xyz,
mesh.inverse_transpose_model.y.xyz,
mesh.inverse_transpose_model.z.xyz
) * vertex.normal;
return out;
}
@ -100,8 +105,8 @@ let STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u;
let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u;
struct PointLight {
projection: mat4x4<f32>;
color: vec4<f32>;
// projection: mat4x4<f32>;
position: vec3<f32>;
inverse_square_range: f32;
radius: f32;
@ -408,14 +413,22 @@ fn fetch_point_shadow(light_id: i32, frag_position: vec4<f32>, surface_normal: v
let abs_position_ls = abs(frag_ls);
let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z));
// do a full projection
// vec4 clip = light.projection * vec4(0.0, 0.0, -major_axis_magnitude, 1.0);
// float depth = (clip.z / clip.w);
// NOTE: These simplifications come from multiplying:
// projection * vec4(0, 0, -major_axis_magnitude, 1.0)
// and keeping only the terms that have any impact on the depth.
// Projection-agnostic approach:
let z = -major_axis_magnitude * light.projection[2][2] + light.projection[3][2];
let w = -major_axis_magnitude * light.projection[2][3] + light.projection[3][3];
// For perspective_rh:
// let proj_r = light.far / (light.near - light.far);
// let z = -major_axis_magnitude * proj_r + light.near * proj_r;
// let w = major_axis_magnitude;
// For perspective_infinite_reverse_rh:
// let z = light.near;
// let w = major_axis_magnitude;
// alternatively do only the necessary multiplications using near/far
let proj_r = light.far / (light.near - light.far);
let z = -major_axis_magnitude * proj_r + light.near * proj_r;
let w = major_axis_magnitude;
let depth = z / w;
// do the lookup, using HW PCF and comparison
@ -521,12 +534,12 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4<f32> {
// # endif
var V: vec3<f32>;
if (view.view_proj.w.w != 1.0) { // If the projection is not orthographic
if (view.projection.w.w != 1.0) { // If the projection is not orthographic
// Only valid for a perpective projection
V = normalize(view.world_position.xyz - in.world_position.xyz);
} else {
// Ortho view vec
V = normalize(vec3<f32>(-view.view_proj.x.z, -view.view_proj.y.z, -view.view_proj.z.z));
V = normalize(vec3<f32>(view.view_proj.x.z, view.view_proj.y.z, view.view_proj.z.z));
}
// Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886"

View file

@ -21,7 +21,7 @@ pub struct PerspectiveProjection {
impl CameraProjection for PerspectiveProjection {
fn get_projection_matrix(&self) -> Mat4 {
Mat4::perspective_rh(self.fov, self.aspect_ratio, self.near, self.far)
Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near)
}
fn update(&mut self, width: f32, height: f32) {
@ -88,8 +88,10 @@ impl CameraProjection for OrthographicProjection {
self.right * self.scale,
self.bottom * self.scale,
self.top * self.scale,
self.near,
// NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
// This is for interoperability with pipelines using infinite reverse perspective projections.
self.far,
self.near,
)
}

View file

@ -42,6 +42,7 @@ pub struct ExtractedView {
#[derive(Clone, AsStd140)]
pub struct ViewUniform {
view_proj: Mat4,
projection: Mat4,
world_position: Vec3,
}
@ -64,9 +65,11 @@ fn prepare_views(
.uniforms
.reserve_and_clear(extracted_views.iter_mut().len(), &render_resources);
for (entity, camera) in extracted_views.iter() {
let projection = camera.projection;
let view_uniforms = ViewUniformOffset {
offset: view_meta.uniforms.push(ViewUniform {
view_proj: camera.projection * camera.transform.compute_matrix().inverse(),
view_proj: projection * camera.transform.compute_matrix().inverse(),
projection,
world_position: camera.transform.translation,
}),
};

View file

@ -15,7 +15,7 @@ use bevy_render2::{
renderer::{RenderContext, RenderDevice},
shader::Shader,
texture::{BevyDefault, Image},
view::{ViewMeta, ViewUniform, ViewUniformOffset},
view::{ViewMeta, ViewUniformOffset},
RenderWorld,
};
use bevy_transform::components::GlobalTransform;
@ -42,7 +42,9 @@ impl FromWorld for SpriteShaders {
ty: BindingType::Buffer {
ty: BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: BufferSize::new(std::mem::size_of::<ViewUniform>() as u64),
// TODO: change this to ViewUniform::std140_size_static once crevice fixes this!
// Context: https://github.com/LPGhatguy/crevice/issues/29
min_binding_size: BufferSize::new(144),
},
count: None,
}],