Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the infrastructure needed to support portals and mirrors. #13797

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3193,6 +3193,16 @@ description = "Displays an example model with anisotropy"
category = "3D Rendering"
wasm = false

[[example]]
name = "mirror"
path = "examples/3d/mirror.rs"
doc-scrape-examples = true

[package.metadata.example.mirror]
name = "Mirror"
description = "Demonstrates how to create a mirror"
category = "3D Rendering"

[profile.wasm-release]
inherits = "release"
opt-level = "z"
Expand Down
29 changes: 29 additions & 0 deletions assets/shaders/screen_space_texture_material.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#import bevy_pbr::{
forward_io::{VertexOutput, FragmentOutput},
pbr_bindings::base_color_texture,
pbr_fragment::pbr_input_from_standard_material,
pbr_functions::{alpha_discard, apply_pbr_lighting, main_pass_post_lighting_processing}
}

struct ScreenSpaceTextureMaterial {
screen_rect: vec4<f32>,
}

@group(2) @binding(100) var<uniform> material: ScreenSpaceTextureMaterial;

@fragment
fn fragment(in: VertexOutput, @builtin(front_facing) is_front: bool) -> FragmentOutput {
let screen_rect = material.screen_rect;

var pbr_input = pbr_input_from_standard_material(in, is_front);
pbr_input.material.base_color = textureLoad(
base_color_texture,
vec2<i32>(floor(in.position.xy) - screen_rect.xy),
0
);

var out: FragmentOutput;
out.color = apply_pbr_lighting(pbr_input);
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
return out;
}
2 changes: 2 additions & 0 deletions crates/bevy_math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod compass;
pub mod cubic_splines;
mod direction;
mod float_ord;
mod mat3;
pub mod primitives;
mod ray;
mod rects;
Expand All @@ -32,6 +33,7 @@ pub use aspect_ratio::AspectRatio;
pub use common_traits::*;
pub use direction::*;
pub use float_ord::*;
pub use mat3::*;
pub use ray::{Ray2d, Ray3d};
pub use rects::*;
pub use rotation2d::Rot2;
Expand Down
30 changes: 30 additions & 0 deletions crates/bevy_math/src/mat3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! Extra utilities for 3×3 matrices.

use glam::{Mat3A, Vec3, Vec3A};

/// Creates a 3×3 matrix that reflects points across the plane at the origin
/// with the given normal.
///
/// This is also known as a [Householder matrix]. It has the general form I -
/// 2NNᵀ, where N is the normal of the plane and I is the identity matrix.
///
/// If the plane across which points are to be reflected isn't at the origin,
/// you can create a translation matrix that translates the points to the
/// origin, then apply the matrix that this function returns on top of that, and
/// finally translate back to the original position.
///
/// See the `mirror` example for a demonstration of how you might use this
/// function.
///
/// [Householder matrix]: https://en.wikipedia.org/wiki/Householder_transformation
#[doc(alias = "householder")]
pub fn reflection_matrix(plane_normal: Vec3) -> Mat3A {
// N times Nᵀ.
let n_nt = Mat3A::from_cols(
Vec3A::from(plane_normal) * plane_normal.x,
Vec3A::from(plane_normal) * plane_normal.y,
Vec3A::from(plane_normal) * plane_normal.z,
);

Mat3A::IDENTITY - n_nt * 2.0
}
4 changes: 4 additions & 0 deletions crates/bevy_pbr/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,10 @@ pub fn queue_material_meshes<M: Material>(
view_key |= MeshPipelineKey::IRRADIANCE_VOLUME;
}

if view.invert_culling {
view_key |= MeshPipelineKey::INVERT_CULLING;
}

if let Some(projection) = projection {
view_key |= match projection {
Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE,
Expand Down
22 changes: 13 additions & 9 deletions crates/bevy_pbr/src/pbr_material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1385,15 +1385,19 @@ impl Material for StandardMaterial {
}
}

descriptor.primitive.cull_mode = if key
.bind_group_data
.contains(StandardMaterialKey::CULL_FRONT)
{
Some(Face::Front)
} else if key.bind_group_data.contains(StandardMaterialKey::CULL_BACK) {
Some(Face::Back)
} else {
None
// Generally, we want to cull front faces if `CULL_FRONT` is present and
// backfaces if `CULL_BACK` is present. However, if the view has
// `INVERT_CULLING` on (usually used for mirrors and the like), we do
// the opposite.
descriptor.primitive.cull_mode = match (
key.bind_group_data
.contains(StandardMaterialKey::CULL_FRONT),
key.bind_group_data.contains(StandardMaterialKey::CULL_BACK),
key.mesh_key.contains(MeshPipelineKey::INVERT_CULLING),
) {
(true, false, false) | (false, true, true) => Some(Face::Front),
(false, true, false) | (true, false, true) => Some(Face::Back),
_ => None,
};

if let Some(label) = &mut descriptor.label {
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_pbr/src/render/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,7 @@ pub fn prepare_lights(
clip_from_view: cube_face_projection,
hdr: false,
color_grading: Default::default(),
invert_culling: false,
},
*frustum,
LightEntity::Point {
Expand Down Expand Up @@ -978,6 +979,7 @@ pub fn prepare_lights(
clip_from_world: None,
hdr: false,
color_grading: Default::default(),
invert_culling: false,
},
*spot_light_frustum.unwrap(),
LightEntity::Spot { light_entity },
Expand Down Expand Up @@ -1074,6 +1076,7 @@ pub fn prepare_lights(
clip_from_world: Some(cascade.clip_from_world),
hdr: false,
color_grading: Default::default(),
invert_culling: false,
},
frustum,
LightEntity::Directional {
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_pbr/src/render/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1412,7 +1412,8 @@ bitflags::bitflags! {
const SCREEN_SPACE_REFLECTIONS = 1 << 16;
const HAS_PREVIOUS_SKIN = 1 << 17;
const HAS_PREVIOUS_MORPH = 1 << 18;
const LAST_FLAG = Self::HAS_PREVIOUS_MORPH.bits();
const INVERT_CULLING = 1 << 19;
const LAST_FLAG = Self::INVERT_CULLING.bits();

// Bitfields
const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS;
Expand Down
12 changes: 12 additions & 0 deletions crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ pub struct Camera {
pub msaa_writeback: bool,
/// The clear color operation to perform on the render target.
pub clear_color: ClearColorConfig,

/// Whether to switch culling mode so that materials that request backface
/// culling cull front faces, and vice versa.
///
/// This is typically used for cameras that mirror the world that they
/// render across a plane, because doing that flips the winding of each
/// polygon.
///
/// This setting doesn't affect materials that disable backface culling.
pub invert_culling: bool,
}

impl Default for Camera {
Expand All @@ -241,6 +251,7 @@ impl Default for Camera {
hdr: false,
msaa_writeback: true,
clear_color: Default::default(),
invert_culling: false,
}
}
}
Expand Down Expand Up @@ -917,6 +928,7 @@ pub fn extract_cameras(
viewport_size.y,
),
color_grading,
invert_culling: camera.invert_culling,
},
visible_entities.clone(),
*frustum,
Expand Down
162 changes: 149 additions & 13 deletions crates/bevy_render/src/camera/projection.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::array;
use std::f32::consts::FRAC_PI_2;
use std::marker::PhantomData;
use std::ops::{Div, DivAssign, Mul, MulAssign};
use std::ops::{Div, DivAssign, Mul, MulAssign, Range};

use crate::primitives::Frustum;
use crate::view::VisibilitySystems;
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
use bevy_ecs::prelude::*;
use bevy_math::{AspectRatio, Mat4, Rect, Vec2, Vec3A};
use bevy_math::{vec2, vec3a, vec4, AspectRatio, Mat4, Rect, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4};
use bevy_reflect::{
std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize,
};
Expand Down Expand Up @@ -182,11 +184,104 @@ pub struct PerspectiveProjection {
///
/// Defaults to a value of `1000.0`.
pub far: f32,

/// The offset of the center of the screen from the direction that the
/// camera points.
///
/// Typically, this vector is zero. It can be set to a nonzero value in
/// order to "cut out" a portion of the viewing frustum. This can be useful
/// if, for example, you want the render the screen in pieces for a large
/// multimonitor display.
///
/// This is in world units and specifies a portion of the near plane, so it
/// ranges from `(-near * aspect, -near)` to `(near * aspect, near)`.
///
/// See the `mirror` example for an example of usage.
pub offset: Vec2,

/// The orientation of the near plane.
///
/// Typically, this is (0, 0, -1), indicating a near plane pointing directly
/// away from the camera. It can be set to a different, normalized, value in
/// order to achieve an *oblique* near plane. This is commonly used for
/// mirrors, in order to avoid reflecting objects behind them.
///
/// See the `mirror` example for an example of usage.
pub near_normal: Vec3,
}

impl CameraProjection for PerspectiveProjection {
fn get_clip_from_view(&self) -> Mat4 {
Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near)
// Unpack.
let PerspectiveProjection {
fov,
aspect_ratio: aspect,
near,
far: _,
offset: xy_offset,
near_normal: normal,
} = *self;

// We start with the standard right-handed reversed-depth perspective
// matrix with an infinite far plane.

let inv_f = f32::tan(fov * 0.5);
let f = 1.0 / inv_f;

// The upper left 2×2 matrix is a straightforward scale, which we call
// `xy_scale`.
let xy_scale = vec2(f / aspect, f);

// We now make our first adjustment, in order to take `offset` into
// account. `offset`, along with `fov` and `aspect`, correspond to the
// `left`/`right`/`bottom`/`top` values of a traditional frustum matrix
// API like [`glFrustum`]. A normal perspective matrix M defines M₁₃ and
// M₂₃ like so:
//
// left + right
// M₁₃ = ─────────────
// -left + right
//
// bottom + top
// M₂₃ = ─────────────
// -bottom + top
//
// With some algebraic manipulation, we can calculate these values from
// `fov`, `aspect`, and `offset`, and we store them in `xy_skew`.

let xy_skew = xy_offset * xy_scale / near;

// Now we need to attach the oblique clip plane. Given a plane normal N
// and the near plane distance along the Z axis d, [Lengyel 2005]
// describes how to do this by replacing the third row of the
// perspective matrix M with the following:
//
// -2⋅Qz
// M₃′ = ──────⋅C + (0, 0, 1, 0)
// C⋅Q
//
// where:
//
// Q = M⁻¹⋅Q′
// Q′ = (sign(Cx) + sign(Cy), 1, 1)
// C = (Nx, Ny, Nz, -d)
//
// Substituting and rearranging, we arrive at the following formula for
// the third row of the matrix M₃′, which we call `near_skew`.
//
// [Lengyel 2005]: https://terathon.com/lengyel/Lengyel-Oblique.pdf

let denom = normal.dot(xy_offset.extend(-near))
+ inv_f * near * (aspect * normal.x.abs() + normal.y.abs());
let near_skew = Vec4::NEG_Z - normal.extend(normal.z * near) * near / denom;

// Now we're ready to put together the final matrix:
Mat4 {
x_axis: vec4(xy_scale.x, 0.0, near_skew.x, 0.0),
y_axis: vec4(0.0, xy_scale.y, near_skew.y, 0.0),
z_axis: vec4(xy_skew.x, xy_skew.y, near_skew.z, -1.0),
w_axis: vec4(0.0, 0.0, near_skew.w, 0.0),
}
}

fn update(&mut self, width: f32, height: f32) {
Expand All @@ -202,17 +297,26 @@ impl CameraProjection for PerspectiveProjection {
let a = z_near.abs() * tan_half_fov;
let b = z_far.abs() * tan_half_fov;
let aspect_ratio = self.aspect_ratio;

let n_off = self.offset;
let f_off = z_far * self.offset / z_near;

// NOTE: These vertices are in the specific order required by [`calculate_cascade`].
[
Vec3A::new(a * aspect_ratio, -a, z_near), // bottom right
Vec3A::new(a * aspect_ratio, a, z_near), // top right
Vec3A::new(-a * aspect_ratio, a, z_near), // top left
Vec3A::new(-a * aspect_ratio, -a, z_near), // bottom left
Vec3A::new(b * aspect_ratio, -b, z_far), // bottom right
Vec3A::new(b * aspect_ratio, b, z_far), // top right
Vec3A::new(-b * aspect_ratio, b, z_far), // top left
Vec3A::new(-b * aspect_ratio, -b, z_far), // bottom left
]
static CORNERS: [Vec2; 4] = [
vec2(1.0, -1.0), // bottom right
vec2(1.0, 1.0), // top right
vec2(-1.0, 1.0), // top left
vec2(-1.0, -1.0), // bottom left
];

// Far corners follow near corners.
array::from_fn(|i| {
if i < 4 {
(CORNERS[i] * a * vec2(aspect_ratio, 1.0) - n_off).extend(z_near).into()
} else {
(CORNERS[i - 4] * b * vec2(aspect_ratio, 1.0) - f_off).extend(z_far).into()
}
})
}
}

Expand All @@ -223,6 +327,38 @@ impl Default for PerspectiveProjection {
near: 0.1,
far: 1000.0,
aspect_ratio: 1.0,
offset: Vec2::ZERO,
near_normal: Vec3::NEG_Z,
}
}
}

impl PerspectiveProjection {
/// Given the coordinates of the clipping planes, computes and returns a
/// perspective projection.
///
/// `x` specifies the left and right clipping planes; `y` specifies the
/// bottom and top clipping planes; `z` specifies the near and far clipping
/// planes. All values are in world space units (meters).
pub fn from_frustum_bounds(
x: Range<f32>,
y: Range<f32>,
z: Range<f32>,
) -> PerspectiveProjection {
let (left, right) = (x.start, x.end);
let (bottom, top) = (y.start, y.end);
let (near, far) = (z.start, z.end);
let fovy = -2.0 * (FRAC_PI_2 - f32::atan(2.0 * near / (bottom - top)));
let aspect = (right - left) / (top - bottom);
let (x_offset, y_offset) = ((left + right) * 0.5, (bottom + top) * 0.5);

PerspectiveProjection {
fov: fovy,
aspect_ratio: aspect,
near,
far,
offset: vec2(x_offset, y_offset),
near_normal: Vec3::NEG_Z,
}
}
}
Expand Down
Loading
Loading