Skip to main content

unlit3d_examples/scenes/
transparent.rs

1//! The transparent, z-sorted scene: overlapping translucent panes composite in
2//! draw order.
3//!
4//! The other scenes draw opaque geometry, so the only blending they exercise is
5//! the UI's own. This scene is the one that pins the unlit pipeline's
6//! transparent path: three translucent panes overlap, and what the middle of
7//! that overlap looks like depends on two things at once — that the draws are
8//! ordered back to front, and that the blend state frames the shader's
9//! straight-alpha output correctly. Either one wrong changes the pixels, which
10//! is what the snapshot freezes.
11//!
12//! The panes carry [`ZSortedDrawing`], so the renderer draws them after the
13//! opaque cubes and sorts them by camera distance rather than by registration
14//! order. They are spawned in an order that is deliberately *not* their draw
15//! order, so a scene that lost the sort would composite them the other way
16//! round and fail the snapshot rather than happening to look right.
17
18use super::{SceneControl, SceneDef, SceneOptions, TEST_SIZE, camera_looking_at};
19use unlit_ecs::{Bundle, DynamicBundle, bundle};
20use unlit3d::prelude::*;
21
22/// How far in front of the panes the camera sits.
23///
24/// The panes are spread along the view direction, so their camera distances
25/// differ by enough that the sort is unambiguous.
26const CAMERA_EYE: glam::Vec3 = glam::Vec3::new(0.0, 0.6, 3.2);
27
28/// The point the camera looks at.
29const CAMERA_TARGET: glam::Vec3 = glam::Vec3::new(0.0, 0.0, 0.0);
30
31/// The tint of one pane, where it sits, and how far along the view direction.
32///
33/// The panes are offset sideways as well as in depth, so they overlap in
34/// bands: one region shows each pane alone, another two of them, and the
35/// middle all three. That is what makes the snapshot discriminate — a wrong
36/// sort or a wrong blend state changes the tint of the overlap bands, not just
37/// the overall brightness.
38///
39/// The order of this table is the order the panes are drawn in after the sort
40/// — farthest first — so drawing them as declared, or reversed, would disagree
41/// with it.
42const PANES: [(glam::Vec4, f32, f32); 3] = [
43    // Farthest: red.
44    (glam::Vec4::new(1.0, 0.1, 0.1, 0.6), -0.85, -0.7),
45    // Middle: green.
46    (glam::Vec4::new(0.1, 1.0, 0.15, 0.6), 0.0, 0.0),
47    // Nearest: blue.
48    (glam::Vec4::new(0.15, 0.3, 1.0, 0.6), 0.85, 0.7),
49];
50
51/// The opaque cube behind the panes, tinted so the composite has something to
52/// blend against that is not the clear colour alone.
53const BACKDROP_TINT: glam::Vec4 = glam::Vec4::new(0.85, 0.8, 0.35, 1.0);
54
55/// The opaque cube the panes composite over.
56///
57/// It is drawn with depth writes, so it sits behind the panes rather than
58/// blending with them.
59struct Backdrop {
60    mesh: GpuMesh,
61    pipeline: GpuRenderPipeline,
62}
63
64impl Bundle for Backdrop {
65    fn into_bundle(self) -> DynamicBundle {
66        bundle![
67            Transform {
68                translation: glam::Vec3::new(0.0, 0.0, -1.6),
69                scale: glam::Vec3::splat(1.1),
70                ..Default::default()
71            },
72            InstanceColor::new(BACKDROP_TINT),
73            self.mesh,
74            self.pipeline,
75        ]
76    }
77}
78
79/// One translucent pane: the shared mesh, tinted and placed where it blends.
80///
81/// The pane is sorted by camera distance, which is what [`ZSortedDrawing`] asks
82/// for; without it the panes would composite in registration order.
83struct Pane {
84    tint: glam::Vec4,
85    translation: glam::Vec3,
86    mesh: GpuMesh,
87    pipeline: GpuRenderPipeline,
88}
89
90impl Bundle for Pane {
91    fn into_bundle(self) -> DynamicBundle {
92        bundle![
93            Transform {
94                translation: self.translation,
95                rotation: glam::Quat::from_rotation_y(0.35),
96                scale: glam::Vec3::new(1.05, 1.05, 0.05),
97            },
98            InstanceColor::new(self.tint),
99            self.mesh,
100            self.pipeline,
101            ZSortedDrawing,
102        ]
103    }
104}
105
106/// The scene: opaque cubes behind three overlapping translucent panes.
107pub static SCENE: SceneDef = SceneDef {
108    id: "transparent",
109    title: "Transparent z-sorted panes",
110    description: "translucent panes composited back to front over opaque cubes",
111    size: TEST_SIZE,
112    baseline: Some(TEST_SIZE),
113    clear: None,
114    frames: 1,
115    step_seconds: None,
116    samples: 1,
117    depth: true,
118    ui: false,
119    reproducible_ui: false,
120    build,
121};
122
123/// Build the transparent scene's world content.
124fn build(
125    world: &mut World,
126    context: RenderContext,
127    pass: Entity,
128    _ui: Option<Entity>,
129    size: (u32, u32),
130    _options: SceneOptions,
131) -> SceneControl {
132    let mut source = MeshSource::new(&world.root_scope(), context);
133    source.register_lit_family(&world.root_scope());
134
135    // Two variants of the same unlit pipeline: the opaque one every other
136    // scene draws with, and a translucent one that blends straight-alpha
137    // output over the target without writing depth.
138    //
139    // Depth writes are off because a blended pane must not hide the pane
140    // behind it, but depth *testing* stays on: the panes are composited among
141    // themselves by their draw order, and an opaque cube in front of them must
142    // still occlude them.
143    // Both variants read the instance tint: the backdrop is tinted through it,
144    // and every pane's tint is its own.
145    let opaque_key = LitPipelineKey::new(super::default_options());
146    let mut translucent = super::default_options();
147    translucent.blend = Some(wgpu::BlendState {
148        color: wgpu::BlendComponent {
149            src_factor: wgpu::BlendFactor::SrcAlpha,
150            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
151            operation: wgpu::BlendOperation::Add,
152        },
153        alpha: wgpu::BlendComponent {
154            src_factor: wgpu::BlendFactor::One,
155            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
156            operation: wgpu::BlendOperation::Add,
157        },
158    });
159    // A pane is seen from the front, so culling its back face keeps it to one
160    // blended layer; drawing both faces of the thin slab would blend it twice.
161    translucent.primitive.cull_mode = Some(wgpu::Face::Back);
162    // Depth *testing* stays on, so an opaque cube in front still occludes a
163    // pane; writes are off because a blended pane must not hide the pane
164    // behind it. The panes are composited among themselves by draw order.
165    translucent.depth_stencil.depth_write_enabled = false;
166    // The pane mesh carries positions and indices only: no UV, no color. The
167    // draw derives exactly those channels back off the mesh, and each pane's
168    // tint is its own instance colour, so one mesh serves all of them.
169    let translucent_key = LitPipelineKey::new(translucent);
170
171    let source_entity = world
172        .scope_mut(pass)
173        .expect("the pass is alive")
174        .spawn(bundle![Source::new(source)]);
175
176    let (positions, uvs, colors, indices) = super::cube();
177
178    // The backdrop the panes composite over: an opaque cube drawn with depth
179    // writes, so it also occludes nothing that should be in front of it.
180    let backdrop = super::with_mesh_source(world, source_entity, |source, world| {
181        source.allocate_lit_mesh(
182            world,
183            &opaque_key.options,
184            LitMeshDesc {
185                positions: &positions,
186                uvs: Some(&uvs),
187                colors: Some(&colors),
188                indices: Some(&indices),
189                ..Default::default()
190            },
191        )
192    });
193
194    // The panes: one mesh, three instances, each tinted and placed by its
195    // instance stream.
196    let pane_mesh = super::with_mesh_source(world, source_entity, |source, world| {
197        source.allocate_lit_mesh(
198            world,
199            &translucent_key.options,
200            LitMeshDesc {
201                positions: &positions,
202                uvs: None,
203                colors: None,
204                indices: Some(&indices),
205                ..Default::default()
206            },
207        )
208    });
209
210    let camera_entity = super::spawn_in(
211        world,
212        source_entity,
213        bundle![camera_looking_at(CAMERA_EYE, CAMERA_TARGET, size)],
214    );
215
216    super::spawn_in(
217        world,
218        source_entity,
219        Backdrop {
220            mesh: backdrop,
221            pipeline: GpuRenderPipeline::new(opaque_key),
222        },
223    );
224
225    // Spawned nearest-first on purpose: registration order is not draw order
226    // for a z-sorted entity, so only a correct sort composites them red,
227    // green, blue from back to front.
228    for (tint, x, z) in PANES.iter().rev() {
229        super::spawn_in(
230            world,
231            source_entity,
232            Pane {
233                tint: *tint,
234                translation: glam::Vec3::new(*x, 0.0, *z),
235                mesh: pane_mesh.clone(),
236                pipeline: GpuRenderPipeline::new(translucent_key.clone()),
237            },
238        );
239    }
240
241    SceneControl {
242        // A single frame: the composite is static, so only the camera's aspect
243        // is advanced, keeping a resized window from stretching the panes.
244        advance: Box::new(move |world, _frame, _delta, frame_size| {
245            super::aim_camera(
246                world,
247                source_entity,
248                camera_entity,
249                CAMERA_EYE,
250                CAMERA_TARGET,
251                frame_size,
252            );
253        }),
254        // The first frame is the snapshot; whichever frame is drawn, the
255        // picture is the same.
256        snapshot: Box::new(|frame| (frame == 0).then(|| "transparent.webp".to_owned())),
257    }
258}