diff --git a/Cargo.toml b/Cargo.toml index 5926ad497c9c0..aa4e05d435f15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/assets/shaders/screen_space_texture_material.wgsl b/assets/shaders/screen_space_texture_material.wgsl new file mode 100644 index 0000000000000..198b72dce6921 --- /dev/null +++ b/assets/shaders/screen_space_texture_material.wgsl @@ -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, +} + +@group(2) @binding(100) var 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(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; +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 868dae094510d..08a4039c64f0e 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -19,6 +19,7 @@ mod compass; pub mod cubic_splines; mod direction; mod float_ord; +mod mat3; pub mod primitives; mod ray; mod rects; @@ -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; diff --git a/crates/bevy_math/src/mat3.rs b/crates/bevy_math/src/mat3.rs new file mode 100644 index 0000000000000..62cd1ee58c7f5 --- /dev/null +++ b/crates/bevy_math/src/mat3.rs @@ -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 +} diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 0deef73a9cd2e..5562c94aed808 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -632,6 +632,10 @@ pub fn queue_material_meshes( 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, diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 1d21348f29589..ab8edf4766a7b 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -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 { diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 0f17c2cabc8f9..bc53636dcb316 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -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 { @@ -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 }, @@ -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 { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 299b719f7b4b5..7eee633b0ae17 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -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; diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 97b94e2762180..c6ccfd67d9dd5 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -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 { @@ -241,6 +251,7 @@ impl Default for Camera { hdr: false, msaa_writeback: true, clear_color: Default::default(), + invert_culling: false, } } } @@ -917,6 +928,7 @@ pub fn extract_cameras( viewport_size.y, ), color_grading, + invert_culling: camera.invert_culling, }, visible_entities.clone(), *frustum, diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 0a6c3ca00afab..d18f2f7c03953 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -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, }; @@ -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) { @@ -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() + } + }) } } @@ -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, + y: Range, + z: Range, + ) -> 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, } } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 17f626d410c5e..b0c667086f5e3 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -184,6 +184,16 @@ pub struct ExtractedView { // uvec4(origin.x, origin.y, width, height) pub viewport: UVec4, pub color_grading: ColorGrading, + + /// 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 ExtractedView { diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index d05c0b541e00e..07e4dd0657ee1 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -771,6 +771,7 @@ pub fn extract_default_ui_camera_view( physical_size.y, ), color_grading: Default::default(), + invert_culling: false, }) .id(); commands diff --git a/examples/3d/mirror.rs b/examples/3d/mirror.rs new file mode 100644 index 0000000000000..e6a07f40306aa --- /dev/null +++ b/examples/3d/mirror.rs @@ -0,0 +1,230 @@ +//! Demonstrates how to create a mirror. + +use std::{array, f32::consts::FRAC_PI_2}; + +use bevy::{ + color::palettes::css::{GOLDENROD, RED}, + math::{bounding::Aabb2d, reflection_matrix, uvec2, vec3, vec4}, + pbr::{ExtendedMaterial, MaterialExtension}, + prelude::*, + render::{ + camera::CameraProjection, + render_resource::{ + AsBindGroup, Extent3d, ShaderRef, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, + }, + }, +}; + +// TODO: we'll use this to handle window resizes +#[derive(Resource)] +struct MirrorImage(Handle); + +#[derive(Clone, AsBindGroup, Asset, Reflect)] +struct ScreenSpaceTextureExtension { + #[uniform(100)] + screen_rect: Vec4, +} + +fn main() { + App::new() + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 2000.0, + }) + .add_plugins(DefaultPlugins) + .add_plugins(MaterialPlugin::< + ExtendedMaterial, + >::default()) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + _asset_server: Res, + mut meshes: ResMut>, + mut standard_materials: ResMut>, + mut screen_space_texture_materials: ResMut< + Assets>, + >, + mut images: ResMut>, + _window: Query<&Window>, +) { + // Spawn the main and mirror cameras. + let camera_origin = vec3(-200.0, 200.0, -200.0); + let camera_target = vec3(-25.0, 20.0, 0.0); + let camera_transform = + Transform::from_translation(camera_origin).looking_at(camera_target, Vec3::Y); + + let mirror_rotation = Quat::from_rotation_x(-FRAC_PI_2); + let mirror_transform = Transform::from_scale(vec3(300.0, 1.0, 150.0)) + .with_rotation(mirror_rotation) + .with_translation(vec3(-25.0, 75.0, 0.0)); + + let proj = PerspectiveProjection::from_frustum_bounds(-0.1..0.1, -0.1..0.1, 0.1..1000.0); + let mvp = proj.get_clip_from_view() + * camera_transform.compute_matrix().inverse() + * mirror_transform.compute_matrix(); + let plane_bounds = [ + mvp * vec4(-0.5, 0.0, -0.5, 1.0), + mvp * vec4(-0.5, 0.0, 0.5, 1.0), + mvp * vec4(0.5, 0.0, -0.5, 1.0), + mvp * vec4(0.5, 0.0, 0.5, 1.0), + ]; + let proj_plane_bounds: [Vec2; 4] = + array::from_fn(|i| (plane_bounds[i].xyz() / plane_bounds[i].w).xy()); + + // Main camera + commands.spawn(Camera3dBundle { + transform: camera_transform, + projection: Projection::Perspective(proj), + ..default() + }); + + // Householder matrix stuff + + // P C'^-1 M == P C^-1 H M + // C'^1 == C^-1 H + // C' == (C^-1 H)^-1 + // C' == H^-1 C + + // NB: This must be calculated in matrix form and then converted to a + // transform! Transforms aren't powerful enough to correctly multiply + // non-uniform scale, which `reflection_matrix` generates, by themselves. + let reflected_transform = Transform::from_matrix( + Mat4::from_mat3a(reflection_matrix(Vec3::NEG_Z)) * camera_transform.compute_matrix(), + ); + + let inverse_linear_camera_transform = camera_transform.compute_affine().matrix3.inverse(); + let mirror_near_plane_dist = + Ray3d::new(camera_origin, (camera_target - camera_origin).normalize()) + .intersect_plane(Vec3::ZERO, InfinitePlane3d::new(mirror_rotation * Vec3::Y)) + .expect("Ray missed mirror"); + + // Y=-1 because the near plane is the opposite plane of the mirror. + let mirror_proj_plane_normal = + (inverse_linear_camera_transform * (mirror_rotation * Vec3::NEG_Y)).normalize(); + + let mirror_proj_near_dist = mirror_near_plane_dist - 0.1; + + // FIXME: This needs to be the actual window size, and should listen to + // resize events and resize the texture as necessary + let window_size = uvec2(1920, 1080); + + // Calculate the projected boundaries of the mirror on screen so that we can + // allocate a texture of exactly the appropriate size. + // + // In reality you'll rarely want to do this, since reallocating textures is + // expensive and portals can in general take up an arbitrarily large portion + // of the screen. However, if the on-screen size of the portal is known to + // be bounded, it can be useful to allocate a smaller texture, so we + // demonstrate the technique here. + let aabb_2d = Aabb2d::from_point_cloud(Vec2::ZERO, Rot2::IDENTITY, &proj_plane_bounds); + let screen_space_aabb_2d = (vec4(aabb_2d.min.x, aabb_2d.max.y, aabb_2d.max.x, aabb_2d.min.y) + * vec4(0.5, -0.5, 0.5, -0.5) + + 0.5) + * window_size.as_vec2().xyxy(); + let rounded_screen_space_aabb_2d = screen_space_aabb_2d + .xy() + .floor() + .extend(screen_space_aabb_2d.z.ceil()) + .extend(screen_space_aabb_2d.w.ceil()); + let near_plane_aabb_2d = Aabb2d { + min: aabb_2d.min * mirror_proj_near_dist, + max: aabb_2d.max * mirror_proj_near_dist, + }; + + let mirror_image_size = + (rounded_screen_space_aabb_2d.zw() - rounded_screen_space_aabb_2d.xy()).as_uvec2(); + + let mirror_image_extent = Extent3d { + width: mirror_image_size.x, + height: mirror_image_size.y, + depth_or_array_layers: 1, + }; + + let mut image = Image { + texture_descriptor: TextureDescriptor { + label: Some("mirror image"), + size: mirror_image_extent, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Bgra8UnormSrgb, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ..default() + }; + image.resize(mirror_image_extent); + let image = images.add(image); + commands.insert_resource(MirrorImage(image.clone())); + + let mut mirror_proj = PerspectiveProjection::from_frustum_bounds( + near_plane_aabb_2d.min.x..near_plane_aabb_2d.max.x, + near_plane_aabb_2d.min.y..near_plane_aabb_2d.max.y, + mirror_proj_near_dist..1000.0, + ); + mirror_proj.near_normal = mirror_proj_plane_normal; + + // Mirror camera + commands.spawn(Camera3dBundle { + camera: Camera { + order: -1, + target: image.clone().into(), + // Reflecting the model across the mirror will flip the winding of + // all the polygons. Therefore, in order to properly backface cull, + // we need to turn on `invert_culling`. + invert_culling: true, + ..default() + }, + transform: reflected_transform, + projection: Projection::Perspective(mirror_proj), + ..default() + }); + + // can't use a fox anymore because of https://github.com/bevyengine/bevy/issues/13796 + /*commands.spawn(SceneBundle { + scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")), + transform: Transform::from_xyz(-50.0, 0.0, -100.0), + ..default() + });*/ + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()), + transform: Transform::from_scale(vec3(80.0, 80.0, 80.0)) + .with_translation(vec3(-50.0, 0.0, -100.0)), + material: standard_materials.add(Color::from(GOLDENROD)), + ..default() + }); + + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()), + material: standard_materials.add(Color::from(RED)), + transform: Transform::from_xyz(10.0, 0.0, 10.0).with_scale(Vec3::splat(10.0)), + ..default() + }); + + commands.spawn(MaterialMeshBundle { + mesh: meshes.add(Plane3d::default().mesh().size(1.0, 1.0)), + material: screen_space_texture_materials.add(ExtendedMaterial { + base: StandardMaterial { + base_color_texture: Some(image), + ..default() + }, + extension: ScreenSpaceTextureExtension { + screen_rect: rounded_screen_space_aabb_2d, + }, + }), + transform: mirror_transform, + ..default() + }); +} + +impl MaterialExtension for ScreenSpaceTextureExtension { + fn fragment_shader() -> ShaderRef { + "shaders/screen_space_texture_material.wgsl".into() + } +}