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}