Skip to main content

ferroui_base/rendering/composition/server/
server_composition_light.rs

1//! The server-side counterparts of the WinUI lights (`AmbientLight`,
2//! `DistantLight`, `PointLight`, `SpotLight`) and of
3//! `VisualUnorderedCollection`, the light math, and the lighting of the
4//! content of the visuals they reach.
5//!
6//! # How a light is drawn
7//!
8//! WinUI lights flat visuals that face the viewer (+Z) with diffuse
9//! (Lambert) lighting: the colour of a lit point is the colour of the content
10//! times the sum, over the lights that reach the visual, of the colour of the
11//! light times its intensity, its attenuation and `N.L`, the cosine of the
12//! angle between the light and the normal of the visual. A visual that a
13//! light reaches is dark where no light shines.
14//!
15//! Here the render walk asks [`LitContent::begin`] before it draws the own
16//! content of a visual. When enabled lights reach the visual, the content is
17//! drawn into a multiply layer of the drawing context
18//! (`IDrawingContextImplWithLayerEffects::push_multiply_layer`) whose factors
19//! are a light map over the bounds of the content: the summed light sampled
20//! every [`LIGHT_MAP_SAMPLE_SPACING`] device pixels (at most
21//! [`LIGHT_MAP_MAX_SAMPLES`] per side) and interpolated bilinearly between
22//! the samples. Each channel of the content is multiplied by the light, the
23//! alpha of the content is kept, and a light brighter than 1 brightens the
24//! content (the factors carry a scale above 1); the result is clamped per
25//! channel, as WinUI clamps its output. Each visual is lit in its own layer
26//! (its children after it, each in theirs), the shadow of a sprite visual is
27//! lit with its content, and a layer visual itself is not lit (its children
28//! are).
29//!
30//! A drawing context without layer effects keeps the approximation of
31//! source-atop blending ([`LightColor::atop_pixel`]): a light map pixel is
32//! black with the alpha `1 - m` plus the light `L - m` above it (`m` the
33//! smallest channel of the light `L`, clamped to 1), which gives
34//! `C * m + (L - m) * alpha(C)`.
35//!
36//! The lights of a visual are diffuse (Lambert), as WinUI's are; the scene
37//! lighting effect (winui-materials.md) adds the specular (Blinn-Phong)
38//! lighting of its `SpecularAmount` and `SpecularShine`
39//! ([`LightingMaterial`]). A light whose `CoordinateSpace` visual moves
40//! draws what it lights again ([`invalidate_lights_with_moved_spaces`]).
41
42use super::{
43    impl_server_render_resource, CompositionProperty, IAnimatedServerObject, IServerObject, IServerRenderResource,
44    IServerRenderResourceObserver, ServerCompositionVisual, ServerCompositor, ServerLayerVisual, ServerRenderResource,
45};
46use crate::media::imaging::BitmapBlendingMode;
47use crate::media::{Color, RenderOptions};
48use crate::platform::{AlphaFormat, IDrawingContextImpl, MultiplyLayerFactors, PixelFormats, SharedBitmapImpl};
49use crate::rendering::composition::generated::{
50    ServerAmbientLightHooks, ServerAmbientLightProps, ServerCompositionLightHooks, ServerCompositionLightProps,
51    ServerDistantLightHooks, ServerDistantLightProps, ServerPointLightHooks, ServerPointLightProps,
52    ServerSpotLightHooks, ServerSpotLightProps, ServerVisualUnorderedCollectionHooks,
53    ServerVisualUnorderedCollectionProps,
54};
55use crate::rendering::composition::transport::BatchStreamReader;
56use crate::{Matrix, PixelSize, Point, Rect, Vector, Vector3D};
57use std::any::{Any, TypeId};
58use std::cell::RefCell;
59use std::rc::{Rc, Weak};
60use std::sync::Arc;
61use std::time::Duration;
62
63/// The size of a cell of the light map grid of [`LightMap::size_for`], in
64/// units (the grid of 0.4; the render walk samples the map by device pixels,
65/// [`LightMap::size_for_scale`]).
66pub const LIGHT_MAP_CELL: f64 = 4.0;
67
68/// The largest number of cells of the grid of [`LightMap::size_for`] along a
69/// side.
70pub const LIGHT_MAP_MAX_CELLS: i32 = 128;
71
72/// The distance, in device pixels, between two samples of a light map.
73pub const LIGHT_MAP_SAMPLE_SPACING: f64 = 2.0;
74
75/// The largest number of samples of a light map along a side.
76pub const LIGHT_MAP_MAX_SAMPLES: i32 = 1024;
77
78/// The largest factor of a light map: a light up to this many times the
79/// full light brightens the content.
80pub const LIGHT_MAP_MAX_SCALE: f64 = 16.0;
81
82/// How far up the parent chain a light is looked for: a cycle would loop
83/// for ever.
84const MAX_LIGHT_DEPTH: usize = 4096;
85
86// --- the light math ----------------------------------------------------------------
87
88/// An amount of light per channel: 0 is none, 1 is full.
89#[derive(Clone, Copy, Debug, Default, PartialEq)]
90pub struct LightColor {
91    /// The red channel.
92    pub r: f64,
93    /// The green channel.
94    pub g: f64,
95    /// The blue channel.
96    pub b: f64,
97}
98
99impl LightColor {
100    /// No light.
101    pub const BLACK: LightColor = LightColor { r: 0.0, g: 0.0, b: 0.0 };
102
103    /// Full white light.
104    pub const WHITE: LightColor = LightColor { r: 1.0, g: 1.0, b: 1.0 };
105
106    /// A light of the given channels.
107    pub const fn new(r: f64, g: f64, b: f64) -> LightColor {
108        LightColor { r, g, b }
109    }
110
111    /// The light of a colour at an intensity: the channels and the alpha of
112    /// the colour as fractions, times the intensity.
113    ///
114    /// Deviation (winui-composition.md): the alpha of the colour of a light
115    /// scales it (a transparent light lights nothing); WinUI does not say.
116    pub fn from_color(color: Color, intensity: f64) -> LightColor {
117        let scale = intensity.max(0.0) * color.a as f64 / 255.0;
118        LightColor {
119            r: color.r as f64 / 255.0 * scale,
120            g: color.g as f64 / 255.0 * scale,
121            b: color.b as f64 / 255.0 * scale,
122        }
123    }
124
125    /// The light times a factor.
126    pub fn scale(self, factor: f64) -> LightColor {
127        LightColor { r: self.r * factor, g: self.g * factor, b: self.b * factor }
128    }
129
130    /// The sum of two lights.
131    pub fn add(self, other: LightColor) -> LightColor {
132        LightColor { r: self.r + other.r, g: self.g + other.g, b: self.b + other.b }
133    }
134
135    /// The light between `self` (at 0) and `other` (at 1).
136    pub fn lerp(self, other: LightColor, t: f64) -> LightColor {
137        LightColor {
138            r: self.r + (other.r - self.r) * t,
139            g: self.g + (other.g - self.g) * t,
140            b: self.b + (other.b - self.b) * t,
141        }
142    }
143
144    /// Every channel limited to 0..1.
145    pub fn clamped(self) -> LightColor {
146        LightColor { r: self.r.clamp(0.0, 1.0), g: self.g.clamp(0.0, 1.0), b: self.b.clamp(0.0, 1.0) }
147    }
148
149    /// The light-map pixel that turns content into content lit by this
150    /// light under source-atop blending: premultiplied `(L - m, 1 - m)`, `m`
151    /// the smallest channel, as RGBA bytes.
152    pub fn atop_pixel(self) -> [u8; 4] {
153        let light = self.clamped();
154        let m = light.r.min(light.g).min(light.b);
155        let byte = |value: f64| (value * 255.0).round().clamp(0.0, 255.0) as u8;
156        [byte(light.r - m), byte(light.g - m), byte(light.b - m), byte(1.0 - m)]
157    }
158}
159
160/// The attenuation of a point or spot light with the distance.
161#[derive(Clone, Copy, Debug, PartialEq)]
162pub struct LightAttenuation {
163    /// The constant term of the denominator.
164    pub constant: f64,
165    /// The term proportional to the distance.
166    pub linear: f64,
167    /// The term proportional to the square of the distance.
168    pub quadratic: f64,
169    /// The distance up to which the light is attenuated as at this distance.
170    pub min_cutoff: f64,
171    /// The distance beyond which the light lights nothing.
172    pub max_cutoff: f64,
173}
174
175impl LightAttenuation {
176    /// No attenuation (the default of WinUI: constant 1).
177    pub const NONE: LightAttenuation =
178        LightAttenuation { constant: 1.0, linear: 0.0, quadratic: 0.0, min_cutoff: 0.0, max_cutoff: f64::MAX };
179
180    /// The factor of the light at `distance`: none beyond the maximum
181    /// cutoff, `1 / (constant + linear * d + quadratic * d^2)` with the
182    /// distance raised to the minimum cutoff otherwise (1 when the
183    /// denominator is not positive).
184    ///
185    /// Deviation (winui-composition.md): WinUI does not say how the cutoffs
186    /// apply; closer than the minimum cutoff the light is attenuated as at
187    /// the cutoff, beyond the maximum cutoff it lights nothing.
188    pub fn factor(&self, distance: f64) -> f64 {
189        if distance > self.max_cutoff {
190            return 0.0;
191        }
192        let d = distance.max(self.min_cutoff).max(0.0);
193        let denominator = self.constant + self.linear * d + self.quadratic * d * d;
194        if denominator <= 0.0 {
195            1.0
196        } else {
197            1.0 / denominator
198        }
199    }
200}
201
202fn length(v: Vector3D) -> f64 {
203    (v.x * v.x + v.y * v.y + v.z * v.z).sqrt()
204}
205
206/// `N.L` of a flat visual facing +Z at `point` for a light at `position`:
207/// `z / distance`, 0 for a light at or below the plane. Also the distance.
208pub fn point_light_incidence(position: Vector3D, point: Point) -> (f64, f64) {
209    let to_light = Vector3D::new(position.x - point.x, position.y - point.y, position.z);
210    let distance = length(to_light);
211    if distance <= 0.0 || position.z <= 0.0 {
212        return (0.0, distance);
213    }
214    (position.z / distance, distance)
215}
216
217/// `N.L` of a flat visual facing +Z for a distant light shining in
218/// `direction`: `-z / |direction|`, 0 for a light from behind or a zero
219/// direction.
220pub fn distant_light_incidence(direction: Vector3D) -> f64 {
221    let len = length(direction);
222    if len <= 0.0 {
223        return 0.0;
224    }
225    (-direction.z / len).max(0.0)
226}
227
228fn smoothstep(t: f64) -> f64 {
229    let t = t.clamp(0.0, 1.0);
230    t * t * (3.0 - 2.0 * t)
231}
232
233/// Where `point` is in the cones of a spot light at `position` shining in
234/// `direction`: 1 inside the inner cone, 0 outside the outer cone, a smooth
235/// step between. The angles are from the direction to the edge of the cone;
236/// an inner angle wider than the outer one is the outer one.
237pub fn spot_light_cone(
238    position: Vector3D,
239    direction: Vector3D,
240    inner_cone_angle: f64,
241    outer_cone_angle: f64,
242    point: Point,
243) -> f64 {
244    let to_point = Vector3D::new(point.x - position.x, point.y - position.y, -position.z);
245    let (to_point_length, direction_length) = (length(to_point), length(direction));
246    if to_point_length <= 0.0 || direction_length <= 0.0 {
247        return 0.0;
248    }
249    let cos_angle = (to_point.x * direction.x + to_point.y * direction.y + to_point.z * direction.z)
250        / (to_point_length * direction_length);
251    let outer = outer_cone_angle.clamp(0.0, std::f64::consts::PI);
252    let inner = inner_cone_angle.clamp(0.0, outer);
253    let (cos_inner, cos_outer) = (inner.cos(), outer.cos());
254    if cos_angle >= cos_inner {
255        return 1.0;
256    }
257    if cos_angle <= cos_outer || cos_inner <= cos_outer {
258        return 0.0;
259    }
260    smoothstep((cos_angle - cos_outer) / (cos_inner - cos_outer))
261}
262
263/// A light in its coordinate space, on the plane `z = 0` of that space.
264#[derive(Clone, Copy, Debug, PartialEq)]
265pub enum LightSource {
266    /// The same light everywhere.
267    Ambient { color: LightColor },
268    /// A light from far away shining in `direction`.
269    Distant { color: LightColor, direction: Vector3D },
270    /// A light at `position`.
271    Point { color: LightColor, position: Vector3D, attenuation: LightAttenuation },
272    /// A light at `position` shining into the cones around `direction`.
273    Spot {
274        inner_color: LightColor,
275        outer_color: LightColor,
276        position: Vector3D,
277        direction: Vector3D,
278        inner_cone_angle: f64,
279        outer_cone_angle: f64,
280        attenuation: LightAttenuation,
281    },
282}
283
284impl LightSource {
285    /// The light at `point` of the plane `z = 0` (not clamped).
286    pub fn light_at(&self, point: Point) -> LightColor {
287        match *self {
288            LightSource::Ambient { color } => color,
289            LightSource::Distant { color, direction } => color.scale(distant_light_incidence(direction)),
290            LightSource::Point { color, position, attenuation } => {
291                let (incidence, distance) = point_light_incidence(position, point);
292                color.scale(incidence * attenuation.factor(distance))
293            }
294            LightSource::Spot {
295                inner_color,
296                outer_color,
297                position,
298                direction,
299                inner_cone_angle,
300                outer_cone_angle,
301                attenuation,
302            } => {
303                let cone = spot_light_cone(position, direction, inner_cone_angle, outer_cone_angle, point);
304                if cone <= 0.0 {
305                    return LightColor::BLACK;
306                }
307                let (incidence, distance) = point_light_incidence(position, point);
308                // Deviation (winui-composition.md): between the cones the
309                // colour goes from the inner to the outer one and the light
310                // falls off to nothing at the edge of the outer cone.
311                outer_color.lerp(inner_color, cone).scale(cone * incidence * attenuation.factor(distance))
312            }
313        }
314    }
315}
316
317/// How a surface reflects the lights: the amounts of the ambient, diffuse
318/// and specular lighting and the shininess of the specular highlight (the
319/// members of the scene lighting effect of WinUI).
320#[derive(Clone, Copy, Debug, PartialEq)]
321pub struct LightingMaterial {
322    pub ambient: f64,
323    pub diffuse: f64,
324    pub specular: f64,
325    pub shine: f64,
326}
327
328impl LightingMaterial {
329    /// The lighting of the content of a visual: ambient and diffuse at full
330    /// amount, no specular (WinUI's lights light visuals diffusely).
331    pub const VISUAL: LightingMaterial = LightingMaterial { ambient: 1.0, diffuse: 1.0, specular: 0.0, shine: 16.0 };
332}
333
334/// The specular (Blinn-Phong) factor of a flat surface facing +Z, seen from
335/// +Z, for light arriving from `to_light`: `(N.H)^shine`.
336fn specular_factor(to_light: Vector3D, shine: f64) -> f64 {
337    let len = length(to_light);
338    if len <= 0.0 || to_light.z <= 0.0 {
339        return 0.0;
340    }
341    let half = Vector3D::new(to_light.x / len, to_light.y / len, to_light.z / len + 1.0);
342    let half_length = length(half);
343    if half_length <= 0.0 {
344        return 0.0;
345    }
346    (half.z / half_length).max(0.0).powf(shine.max(1.0))
347}
348
349impl LightSource {
350    /// The light at `point` of the plane `z = 0` reflected by `material`
351    /// (not clamped): the ambient light times the ambient amount, the other
352    /// lights times the diffuse amount and `N.L`, plus their specular
353    /// highlight times the specular amount.
354    pub fn lit_at(&self, point: Point, material: &LightingMaterial) -> LightColor {
355        if let LightSource::Ambient { color } = *self {
356            return color.scale(material.ambient);
357        }
358        let diffuse = if material.diffuse == 0.0 { LightColor::BLACK } else { self.light_at(point).scale(material.diffuse) };
359        if material.specular == 0.0 {
360            return diffuse;
361        }
362        let specular = match *self {
363            LightSource::Ambient { .. } => LightColor::BLACK,
364            LightSource::Distant { color, direction } => {
365                color.scale(specular_factor(Vector3D::new(-direction.x, -direction.y, -direction.z), material.shine))
366            }
367            LightSource::Point { color, position, attenuation } => {
368                let to_light = Vector3D::new(position.x - point.x, position.y - point.y, position.z);
369                color.scale(specular_factor(to_light, material.shine) * attenuation.factor(length(to_light)))
370            }
371            LightSource::Spot {
372                inner_color,
373                outer_color,
374                position,
375                direction,
376                inner_cone_angle,
377                outer_cone_angle,
378                attenuation,
379            } => {
380                let cone = spot_light_cone(position, direction, inner_cone_angle, outer_cone_angle, point);
381                if cone <= 0.0 {
382                    LightColor::BLACK
383                } else {
384                    let to_light = Vector3D::new(position.x - point.x, position.y - point.y, position.z);
385                    outer_color
386                        .lerp(inner_color, cone)
387                        .scale(cone * specular_factor(to_light, material.shine) * attenuation.factor(length(to_light)))
388                }
389            }
390        };
391        diffuse.add(specular.scale(material.specular))
392    }
393}
394
395/// The lights that reach a visual, each with the transform from the space
396/// of the visual to the space of the light.
397#[derive(Clone, Debug, Default)]
398pub struct LightMap {
399    lights: Vec<(LightSource, Matrix)>,
400}
401
402impl LightMap {
403    /// A map without lights.
404    pub fn new() -> LightMap {
405        LightMap::default()
406    }
407
408    /// Adds a light; `to_light_space` maps the coordinates of the lit visual
409    /// to those of the coordinate space of the light.
410    pub fn add(&mut self, source: LightSource, to_light_space: Matrix) {
411        self.lights.push((source, to_light_space));
412    }
413
414    /// Whether the map has no lights.
415    pub fn is_empty(&self) -> bool {
416        self.lights.is_empty()
417    }
418
419    /// The sum of the lights at `point` of the lit visual reflected by
420    /// `material`, not clamped.
421    pub fn lit_at(&self, point: Point, material: &LightingMaterial) -> LightColor {
422        let mut light = LightColor::BLACK;
423        for (source, to_light_space) in &self.lights {
424            light = light.add(source.lit_at(to_light_space.transform(point), material));
425        }
426        light
427    }
428
429    /// The sum of the lights at `point` of the lit visual, clamped to 0..1.
430    pub fn light_at(&self, point: Point) -> LightColor {
431        self.lit_at(point, &LightingMaterial::VISUAL).clamped()
432    }
433
434    /// The size of the light map of `rect` on the grid of 0.4: a cell per
435    /// [`LIGHT_MAP_CELL`] units, at least 2 and at most [`LIGHT_MAP_MAX_CELLS`].
436    /// The render walk uses [`size_for_scale`](Self::size_for_scale).
437    pub fn size_for(rect: Rect) -> PixelSize {
438        let cells = |length: f64| ((length / LIGHT_MAP_CELL).ceil() as i32).clamp(2, LIGHT_MAP_MAX_CELLS);
439        PixelSize::new(cells(rect.width), cells(rect.height))
440    }
441
442    /// The number of samples of the light map of `rect` along each side,
443    /// for a visual drawn `device_scale` device pixels per unit: a sample
444    /// every [`LIGHT_MAP_SAMPLE_SPACING`] device pixels, at least 2 and at
445    /// most [`LIGHT_MAP_MAX_SAMPLES`].
446    pub fn size_for_scale(rect: Rect, device_scale: f64) -> PixelSize {
447        let scale = if device_scale.is_finite() && device_scale > 0.0 { device_scale } else { 1.0 };
448        let samples = |length: f64| {
449            ((length * scale / LIGHT_MAP_SAMPLE_SPACING).ceil().min(f64::from(LIGHT_MAP_MAX_SAMPLES)) as i32)
450                .clamp(2, LIGHT_MAP_MAX_SAMPLES)
451        };
452        PixelSize::new(samples(rect.width), samples(rect.height))
453    }
454
455    /// The light (reflected by `material`) at the centre of each sample of
456    /// the light map of `rect` at `size`, row by row from the top.
457    fn samples(&self, rect: Rect, size: PixelSize, material: &LightingMaterial) -> Vec<LightColor> {
458        let (columns, rows) = (size.width.max(1), size.height.max(1));
459        let mut samples = Vec::with_capacity((columns * rows) as usize);
460        for row in 0..rows {
461            let y = rect.y + (row as f64 + 0.5) * rect.height / rows as f64;
462            for column in 0..columns {
463                let x = rect.x + (column as f64 + 0.5) * rect.width / columns as f64;
464                samples.push(self.lit_at(Point::new(x, y), material));
465            }
466        }
467        samples
468    }
469
470    /// The factors of the multiply layer that lights the content of `rect`:
471    /// the light at each sample (reflected by `material`), over a scale of
472    /// the brightest channel (at least 1, at most
473    /// [`LIGHT_MAP_MAX_SCALE`]). `None` when the light is exactly full
474    /// everywhere, so the layer would change nothing.
475    pub fn factors(&self, rect: Rect, size: PixelSize, material: &LightingMaterial) -> Option<MultiplyLayerFactors> {
476        let samples = self.samples(rect, size, material);
477        if samples.iter().all(|light| light.r == 1.0 && light.g == 1.0 && light.b == 1.0) {
478            return None;
479        }
480        let brightest = samples.iter().fold(0.0f64, |max, light| max.max(light.r).max(light.g).max(light.b));
481        let scale = brightest.clamp(1.0, LIGHT_MAP_MAX_SCALE);
482        let byte = |value: f64| (value / scale * 255.0).round().clamp(0.0, 255.0) as u8;
483        let mut rgba = Vec::with_capacity(samples.len() * 4);
484        for light in samples {
485            rgba.extend_from_slice(&[byte(light.r), byte(light.g), byte(light.b), 255]);
486        }
487        Some(MultiplyLayerFactors { size, rgba: Arc::new(rgba), scale: scale as f32 })
488    }
489
490    /// The RGBA pixels (premultiplied, row by row from the top) of the
491    /// source-atop light map of `rect` at `size` (for a drawing context
492    /// without layer effects): each pixel the [`LightColor::atop_pixel`] of
493    /// the light at the centre of its sample. `None` when the light is full
494    /// everywhere, so the map would change nothing.
495    pub fn pixels(&self, rect: Rect, size: PixelSize) -> Option<Vec<u8>> {
496        let samples = self.samples(rect, size, &LightingMaterial::VISUAL);
497        let mut pixels = Vec::with_capacity(samples.len() * 4);
498        let mut changes = false;
499        for light in samples {
500            let pixel = light.atop_pixel();
501            changes |= pixel[3] != 0;
502            pixels.extend_from_slice(&pixel);
503        }
504        changes.then_some(pixels)
505    }
506}
507
508// --- the visual collection -------------------------------------------------------------
509
510fn no_composition_property(_name: &str) -> Option<&'static CompositionProperty> {
511    None
512}
513
514/// The server-side counterpart of a
515/// [`VisualUnorderedCollection`](crate::rendering::composition::VisualUnorderedCollection):
516/// a render resource, so a change of the collection invalidates the light
517/// that holds it.
518pub struct ServerVisualUnorderedCollection {
519    base: ServerRenderResource,
520    props: ServerVisualUnorderedCollectionProps,
521}
522
523impl ServerVisualUnorderedCollection {
524    /// Creates the collection, active.
525    pub fn new(compositor: &Rc<ServerCompositor>) -> Rc<ServerVisualUnorderedCollection> {
526        let collection = Rc::new_cyclic(|this: &Weak<ServerVisualUnorderedCollection>| {
527            ServerVisualUnorderedCollection {
528                base: ServerRenderResource::new(
529                    compositor,
530                    this.clone() as Weak<dyn IAnimatedServerObject>,
531                    this.clone() as Weak<dyn IServerRenderResource>,
532                ),
533                props: ServerVisualUnorderedCollectionProps::new(),
534            }
535        });
536        collection.base.object().activate();
537        collection
538    }
539
540    /// The server-side visuals of the collection.
541    pub fn visuals(&self) -> Vec<Rc<ServerCompositionVisual>> {
542        self.props
543            .list()
544            .list()
545            .into_iter()
546            .filter_map(|visual| visual.into_any_rc().downcast::<ServerCompositionVisual>().ok())
547            .collect()
548    }
549
550    /// The number of visuals.
551    pub fn count(&self) -> usize {
552        self.props.list().count()
553    }
554
555    /// Whether `visual` is in the collection.
556    pub fn contains(&self, visual: &ServerCompositionVisual) -> bool {
557        let target = visual as *const ServerCompositionVisual;
558        self.props.list().list().iter().any(|item| std::ptr::addr_eq(Rc::as_ptr(item), target))
559    }
560
561    pub fn is_disposed(&self) -> bool {
562        self.base.is_disposed()
563    }
564
565    /// The collection behind an object property, if it is one.
566    pub fn of(object: &Option<Rc<dyn IServerObject>>) -> Option<&ServerVisualUnorderedCollection> {
567        object.as_ref().and_then(|object| object.as_any().downcast_ref::<ServerVisualUnorderedCollection>())
568    }
569}
570
571impl_server_render_resource!(ServerVisualUnorderedCollection, base, composition_property: no_composition_property);
572
573impl ServerVisualUnorderedCollectionHooks for ServerVisualUnorderedCollection {}
574
575impl IServerObject for ServerVisualUnorderedCollection {
576    fn deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
577        self.props.deserialize_changes_core(self, reader, committed_at);
578    }
579
580    fn values_invalidated(&self) {
581        self.base.core().invalidated(self);
582    }
583
584    fn dispose(&self) {
585        self.props.list().clear();
586        self.base.core().dispose();
587    }
588
589    fn as_render_resource(self: Rc<Self>) -> Option<Rc<dyn IServerRenderResource>> {
590        Some(self)
591    }
592
593    fn as_animated(self: Rc<Self>) -> Option<Rc<dyn IAnimatedServerObject>> {
594        Some(self)
595    }
596
597    fn get_props(&self, type_id: TypeId) -> Option<&dyn Any> {
598        self.props.find_props(type_id)
599    }
600
601    fn as_any(&self) -> &dyn Any {
602        self
603    }
604
605    fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
606        self
607    }
608}
609
610// --- the lights ----------------------------------------------------------------------
611
612/// What every server-side light has: the properties of `CompositionLight`,
613/// where it shines from and what it lights.
614pub trait IServerCompositionLight {
615    /// The properties of `CompositionLight`.
616    fn light_props(&self) -> &ServerCompositionLightProps;
617
618    /// The bookkeeping of what the light lit.
619    fn light_state(&self) -> &ServerLightState;
620
621    fn is_disposed(&self) -> bool;
622
623    /// The visual whose coordinates the light is in; `None` for the targeted
624    /// visual (and for an ambient light, which has no position).
625    fn coordinate_space(&self) -> Option<Rc<ServerCompositionVisual>>;
626
627    /// The light in its coordinate space.
628    fn source(&self) -> LightSource;
629
630    /// Whether `visual` is lit by the light: in the subtree of a target and
631    /// not in the subtree of an exclusion (an exclusion that is also a
632    /// target is excluded). The target, if it is.
633    fn target_of(&self, visual: &ServerCompositionVisual) -> Option<Rc<ServerCompositionVisual>> {
634        let props = self.light_props();
635        let (targets, exclusions) = (props.targets(), props.exclusions_from_targets());
636        let targets = ServerVisualUnorderedCollection::of(&targets)?;
637        if targets.count() == 0 {
638            return None;
639        }
640        let exclusions = ServerVisualUnorderedCollection::of(&exclusions);
641        let mut current = visual.weak().upgrade();
642        let mut depth = 0;
643        while let Some(candidate) = current {
644            if exclusions.is_some_and(|exclusions| exclusions.contains(&candidate)) {
645                return None;
646            }
647            if targets.contains(&candidate) {
648                return Some(candidate);
649            }
650            depth += 1;
651            if depth > MAX_LIGHT_DEPTH {
652                return None;
653            }
654            current = candidate.parent();
655        }
656        None
657    }
658}
659
660/// The transform from the coordinates of `visual` to those of the root of
661/// its tree: its own transform and those of its ancestors.
662fn to_root(visual: &ServerCompositionVisual) -> Matrix {
663    let mut matrix = visual.own_transform().unwrap_or(Matrix::IDENTITY);
664    let mut parent = visual.parent();
665    let mut depth = 0;
666    while let Some(current) = parent {
667        if let Some(transform) = current.own_transform() {
668            matrix = matrix * transform;
669        }
670        depth += 1;
671        if depth > MAX_LIGHT_DEPTH {
672            break;
673        }
674        parent = current.parent();
675    }
676    matrix
677}
678
679/// The lights that reach `visual`, as a light map in the coordinates of the
680/// visual; `None` when no enabled light reaches it.
681pub fn light_map_of(visual: &ServerCompositionVisual) -> Option<LightMap> {
682    let lights: Vec<_> = visual.compositor()?.live_lights().into_iter().filter(|light| light.light_props().is_enabled()).collect();
683    if lights.is_empty() {
684        return None;
685    }
686    let mut map = LightMap::new();
687    let mut visual_to_root: Option<Matrix> = None;
688    for light in lights {
689        let Some(target) = light.target_of(visual) else { continue };
690        let space = light.coordinate_space().unwrap_or(target);
691        let to_root_of_visual = *visual_to_root.get_or_insert_with(|| to_root(visual));
692        // A space that cannot be inverted maps nothing: the light is left out.
693        let Some(from_root_to_space) = to_root(&space).try_invert() else { continue };
694        map.add(light.source(), to_root_of_visual * from_root_to_space);
695    }
696    (!map.is_empty()).then_some(map)
697}
698
699/// Draws again what the lights whose `CoordinateSpace` visual moved (it, or
700/// an ancestor, changed its transform) since the last check light: the
701/// position of such a light moved with its space. Called by the composition
702/// targets after their update; returns whether a light drew something
703/// again, so the target collects the dirty areas once more.
704pub fn invalidate_lights_with_moved_spaces(compositor: &ServerCompositor) -> bool {
705    let mut invalidated = false;
706    for light in compositor.live_lights() {
707        let current = light.coordinate_space().map(|space| to_root(&space));
708        let state = light.light_state();
709        let previous = state.space_transform.replace(current);
710        if let (Some(previous), Some(current)) = (previous, current) {
711            if previous != current && light.light_props().is_enabled() {
712                state.invalidate_lit_visuals(&*light);
713                invalidated = true;
714            }
715        }
716    }
717    invalidated
718}
719
720/// Invalidates the content of `visual` and of its subtree.
721fn invalidate_subtree(visual: &Rc<ServerCompositionVisual>) {
722    let mut stack = vec![visual.clone()];
723    while let Some(current) = stack.pop() {
724        current.invalidate_content();
725        if let Some(children) = current.children() {
726            stack.extend(children.list().iter().cloned());
727        }
728    }
729}
730
731/// Re-renders what a light lights when the light changes: observes the
732/// light itself (a render resource), which is invalidated when one of its
733/// members changes (an animation included) or its collections change.
734struct LightObserver {
735    light: Weak<dyn IServerCompositionLight>,
736}
737
738impl IServerRenderResourceObserver for LightObserver {
739    fn dependency_queued_invalidate(&self, _sender: &dyn IServerRenderResource) {
740        if let Some(light) = self.light.upgrade() {
741            light.light_state().invalidate_lit_visuals(&*light);
742        }
743    }
744}
745
746/// The bookkeeping of a light: the observer of its changes and the targets
747/// it lit, whose subtrees are drawn again when the light changes.
748#[derive(Default)]
749pub struct ServerLightState {
750    observer: RefCell<Option<Rc<dyn IServerRenderResourceObserver>>>,
751    lit_targets: RefCell<Vec<Weak<ServerCompositionVisual>>>,
752    /// The id of the XAML light the light belongs to and the visual of the
753    /// element that holds it: the light lights the XAML brushes targeted by
754    /// that id in the subtree of that visual (winui-materials.md).
755    xaml_binding: RefCell<Option<(String, Option<Weak<ServerCompositionVisual>>)>>,
756    /// The visuals whose brushes the light was offered to by a scene
757    /// lighting effect: drawn again when the light changes.
758    brush_lit: RefCell<Vec<Weak<ServerCompositionVisual>>>,
759    /// The transform from the coordinate space of the light to the root
760    /// when it was last checked: a light whose space moved draws what it
761    /// lights again ([`invalidate_lights_with_moved_spaces`]).
762    space_transform: std::cell::Cell<Option<Matrix>>,
763}
764
765/// The XAML part of a light that a batch carries (see
766/// `CompositionLight::set_xaml_binding`).
767pub(crate) struct XamlLightChanges {
768    pub(crate) id: Option<String>,
769    pub(crate) scope: Option<super::ServerObjectId>,
770}
771
772impl ServerLightState {
773    /// Registers a new light on this thread and observes its changes.
774    fn attach(base: &ServerRenderResource, state: &ServerLightState, light: Weak<dyn IServerCompositionLight>) {
775        let observer: Rc<dyn IServerRenderResourceObserver> = Rc::new(LightObserver { light: light.clone() });
776        base.core().add_observer(&observer);
777        *state.observer.borrow_mut() = Some(observer);
778        if let Some(compositor) = base.object().compositor() {
779            compositor.register_light(light);
780        }
781    }
782
783    /// The id of the XAML light and the visual of the element that holds
784    /// it, when the light belongs to a XAML light.
785    pub fn xaml_binding(&self) -> Option<(String, Option<Rc<ServerCompositionVisual>>)> {
786        self.xaml_binding
787            .borrow()
788            .as_ref()
789            .map(|(id, scope)| (id.clone(), scope.as_ref().and_then(Weak::upgrade)))
790    }
791
792    fn set_xaml_binding(&self, id: Option<String>, scope: Option<Rc<ServerCompositionVisual>>) {
793        *self.xaml_binding.borrow_mut() = id.map(|id| (id, scope.as_ref().map(Rc::downgrade)));
794    }
795
796    /// Records that a scene lighting effect painted in `visual` was offered
797    /// the light.
798    fn note_brush_lit(&self, visual: &Rc<ServerCompositionVisual>) {
799        let mut brush_lit = self.brush_lit.borrow_mut();
800        brush_lit.retain(|existing| existing.strong_count() > 0);
801        if !brush_lit.iter().any(|existing| std::ptr::eq(existing.as_ptr(), Rc::as_ptr(visual))) {
802            brush_lit.push(Rc::downgrade(visual));
803        }
804    }
805
806    /// The targets the light lit when it last changed.
807    pub fn lit_targets(&self) -> Vec<Rc<ServerCompositionVisual>> {
808        self.lit_targets.borrow().iter().filter_map(Weak::upgrade).collect()
809    }
810
811    /// Draws again the subtrees of the targets the light lit and of those it
812    /// lights now (none once it is disposed).
813    fn invalidate_lit_visuals(&self, light: &dyn IServerCompositionLight) {
814        let current = if light.is_disposed() {
815            Vec::new()
816        } else {
817            let targets = light.light_props().targets();
818            ServerVisualUnorderedCollection::of(&targets).map(|targets| targets.visuals()).unwrap_or_default()
819        };
820        let previous = std::mem::take(&mut *self.lit_targets.borrow_mut());
821        for visual in previous.iter().filter_map(Weak::upgrade).chain(current.iter().cloned()) {
822            invalidate_subtree(&visual);
823        }
824        *self.lit_targets.borrow_mut() = current.iter().map(Rc::downgrade).collect();
825        // The brushes the light was offered to are drawn again, alone: their
826        // visuals, not the subtrees.
827        let brush_lit: Vec<_> = self.brush_lit.borrow().iter().filter_map(Weak::upgrade).collect();
828        for visual in brush_lit {
829            visual.invalidate_content();
830        }
831    }
832
833    /// The light is disposed: what it lit is drawn without it.
834    fn detach(&self) {
835        let previous = std::mem::take(&mut *self.lit_targets.borrow_mut());
836        for visual in previous.iter().filter_map(Weak::upgrade) {
837            invalidate_subtree(&visual);
838        }
839        for visual in std::mem::take(&mut *self.brush_lit.borrow_mut()).iter().filter_map(Weak::upgrade) {
840            visual.invalidate_content();
841        }
842        self.observer.borrow_mut().take();
843    }
844}
845
846/// The visual of an object property, as a server visual.
847fn visual_of(object: Option<Rc<dyn IServerObject>>) -> Option<Rc<ServerCompositionVisual>> {
848    object.and_then(|visual| visual.into_any_rc().downcast::<ServerCompositionVisual>().ok())
849}
850
851fn attenuation_of(constant: f32, linear: f32, quadratic: f32, min_cutoff: f32, max_cutoff: f32) -> LightAttenuation {
852    LightAttenuation {
853        constant: constant as f64,
854        linear: linear as f64,
855        quadratic: quadratic as f64,
856        min_cutoff: min_cutoff as f64,
857        max_cutoff: max_cutoff as f64,
858    }
859}
860
861/// Declares a server-side light class: the struct, its constructor, the
862/// render resource contracts, `IServerObject` and
863/// [`IServerCompositionLight`] over the coordinate space and the source of
864/// its properties.
865macro_rules! server_light {
866    (
867        $(#[$meta:meta])* $name:ident, $props:ident, $hooks:ident,
868        coordinate_space: $coordinate_space:expr,
869        source: $source:expr $(,)?
870    ) => {
871        $(#[$meta])*
872        pub struct $name {
873            base: ServerRenderResource,
874            props: $props,
875            state: ServerLightState,
876        }
877
878        impl $name {
879            /// Creates the light, active and registered with its compositor:
880            /// the UI-thread light activates its server object when it is
881            /// created.
882            pub fn new(compositor: &Rc<ServerCompositor>) -> Rc<$name> {
883                let light = Rc::new_cyclic(|this: &Weak<$name>| $name {
884                    base: ServerRenderResource::new(
885                        compositor,
886                        this.clone() as Weak<dyn IAnimatedServerObject>,
887                        this.clone() as Weak<dyn IServerRenderResource>,
888                    ),
889                    props: $props::new(),
890                    state: ServerLightState::default(),
891                });
892                light.base.object().activate();
893                let weak = Rc::downgrade(&light) as Weak<dyn IServerCompositionLight>;
894                ServerLightState::attach(&light.base, &light.state, weak);
895                light
896            }
897
898            /// The properties of the light.
899            pub fn props(&self) -> &$props {
900                &self.props
901            }
902
903            pub fn is_disposed(&self) -> bool {
904                self.base.is_disposed()
905            }
906        }
907
908        impl_server_render_resource!($name, base, composition_property: $props::get_composition_property);
909
910        impl ServerCompositionLightHooks for $name {}
911
912        impl $hooks for $name {}
913
914        impl IServerCompositionLight for $name {
915            fn light_props(&self) -> &ServerCompositionLightProps {
916                self.props.base()
917            }
918
919            fn light_state(&self) -> &ServerLightState {
920                &self.state
921            }
922
923            fn is_disposed(&self) -> bool {
924                self.base.is_disposed()
925            }
926
927            fn coordinate_space(&self) -> Option<Rc<ServerCompositionVisual>> {
928                let coordinate_space: fn(&$props) -> Option<Rc<dyn IServerObject>> = $coordinate_space;
929                visual_of(coordinate_space(&self.props))
930            }
931
932            fn source(&self) -> LightSource {
933                let source: fn(&$props) -> LightSource = $source;
934                source(&self.props)
935            }
936        }
937
938        impl IServerObject for $name {
939            fn deserialize_changes_core(&self, reader: &mut BatchStreamReader<'_>, committed_at: Duration) {
940                self.props.deserialize_changes_core(self, reader, committed_at);
941                if let Some(changes) = reader.read_value::<XamlLightChanges>() {
942                    let compositor = self.base.compositor();
943                    let scope = changes.scope.and_then(|id| compositor.as_ref().and_then(|compositor| compositor.get_object(id)));
944                    self.state.set_xaml_binding(changes.id, visual_of(scope));
945                    self.base.core().invalidated(self);
946                }
947            }
948
949            fn values_invalidated(&self) {
950                self.base.core().invalidated(self);
951            }
952
953            fn dispose(&self) {
954                self.state.detach();
955                self.base.core().dispose();
956            }
957
958            fn as_render_resource(self: Rc<Self>) -> Option<Rc<dyn IServerRenderResource>> {
959                Some(self)
960            }
961
962            fn as_animated(self: Rc<Self>) -> Option<Rc<dyn IAnimatedServerObject>> {
963                Some(self)
964            }
965
966            fn get_props(&self, type_id: TypeId) -> Option<&dyn Any> {
967                self.props.find_props(type_id)
968            }
969
970            fn as_any(&self) -> &dyn Any {
971                self
972            }
973
974            fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
975                self
976            }
977        }
978    };
979}
980
981server_light!(
982    /// The server-side counterpart of an
983    /// [`AmbientLight`](crate::rendering::composition::AmbientLight).
984    ServerAmbientLight,
985    ServerAmbientLightProps,
986    ServerAmbientLightHooks,
987    coordinate_space: |_props| None,
988    source: |props| LightSource::Ambient { color: LightColor::from_color(props.color(), props.intensity() as f64) },
989);
990
991server_light!(
992    /// The server-side counterpart of a
993    /// [`DistantLight`](crate::rendering::composition::DistantLight).
994    ServerDistantLight,
995    ServerDistantLightProps,
996    ServerDistantLightHooks,
997    coordinate_space: |props| props.coordinate_space(),
998    source: |props| LightSource::Distant {
999        color: LightColor::from_color(props.color(), props.intensity() as f64),
1000        direction: props.direction(),
1001    },
1002);
1003
1004server_light!(
1005    /// The server-side counterpart of a
1006    /// [`PointLight`](crate::rendering::composition::PointLight).
1007    ServerPointLight,
1008    ServerPointLightProps,
1009    ServerPointLightHooks,
1010    coordinate_space: |props| props.coordinate_space(),
1011    source: |props| LightSource::Point {
1012        color: LightColor::from_color(props.color(), props.intensity() as f64),
1013        position: props.offset(),
1014        attenuation: attenuation_of(
1015            props.constant_attenuation(),
1016            props.linear_attenuation(),
1017            props.quadratic_attenuation(),
1018            props.min_attenuation_cutoff(),
1019            props.max_attenuation_cutoff(),
1020        ),
1021    },
1022);
1023
1024server_light!(
1025    /// The server-side counterpart of a
1026    /// [`SpotLight`](crate::rendering::composition::SpotLight).
1027    ServerSpotLight,
1028    ServerSpotLightProps,
1029    ServerSpotLightHooks,
1030    coordinate_space: |props| props.coordinate_space(),
1031    source: |props| LightSource::Spot {
1032        inner_color: LightColor::from_color(props.inner_cone_color(), props.inner_cone_intensity() as f64),
1033        outer_color: LightColor::from_color(props.outer_cone_color(), props.outer_cone_intensity() as f64),
1034        position: props.offset(),
1035        direction: props.direction(),
1036        inner_cone_angle: props.inner_cone_angle() as f64,
1037        outer_cone_angle: props.outer_cone_angle() as f64,
1038        attenuation: attenuation_of(
1039            props.constant_attenuation(),
1040            props.linear_attenuation(),
1041            props.quadratic_attenuation(),
1042            props.min_attenuation_cutoff(),
1043            props.max_attenuation_cutoff(),
1044        ),
1045    },
1046);
1047
1048// --- the lighting of the content of a visual -----------------------------------------
1049
1050/// The scale of `matrix`: the device pixels per unit along its longer axis.
1051fn device_scale(matrix: &Matrix) -> f64 {
1052    let x = (matrix.m11 * matrix.m11 + matrix.m12 * matrix.m12).sqrt();
1053    let y = (matrix.m21 * matrix.m21 + matrix.m22 * matrix.m22).sqrt();
1054    x.max(y)
1055}
1056
1057/// The lighting of the own content of a visual in the render walk: begun
1058/// before the content is drawn, ended after it (before the children).
1059pub struct LitContent {
1060    kind: LitContentKind,
1061}
1062
1063/// How the content of a lit visual is drawn.
1064enum LitContentKind {
1065    /// In a multiply layer of the light map.
1066    Multiplied,
1067    /// In a layer, under the source-atop light map (a drawing context
1068    /// without layer effects).
1069    Atop { bitmap: Arc<SharedBitmapImpl>, rect: Rect, size: PixelSize },
1070}
1071
1072impl LitContent {
1073    /// When enabled lights reach `visual`, pushes the layer the content is
1074    /// drawn into; `None` (nothing pushed) when no light reaches the visual,
1075    /// it has no content bounds, the light is full everywhere, or the
1076    /// visual is a layer visual (whose layer stays open for its children,
1077    /// which are lit each by itself). Without layer effects, also `None`
1078    /// when the compositor has no render interface for the bitmap of the map.
1079    pub fn begin(visual: &ServerCompositionVisual, canvas: &mut dyn IDrawingContextImpl) -> Option<LitContent> {
1080        if visual.content_as::<ServerLayerVisual>().is_some() {
1081            return None;
1082        }
1083        let map = light_map_of(visual)?;
1084        let rect = visual.compute_own_content_bounds()?.to_rect();
1085        if rect.width <= 0.0 || rect.height <= 0.0 {
1086            return None;
1087        }
1088        let size = LightMap::size_for_scale(rect, device_scale(&canvas.transform()));
1089        if let Some(layers) = canvas.as_drawing_context_impl_with_layer_effects() {
1090            let factors = map.factors(rect, size, &LightingMaterial::VISUAL)?;
1091            layers.push_multiply_layer(rect, &factors);
1092            return Some(LitContent { kind: LitContentKind::Multiplied });
1093        }
1094        let pixels = map.pixels(rect, size)?;
1095        let compositor = visual.compositor()?;
1096        let bitmap = compositor.render_interface().with_platform_render_interface(|render_interface| {
1097            render_interface.load_bitmap_from_pixels(
1098                PixelFormats::RGBA8888,
1099                AlphaFormat::Premul,
1100                &pixels,
1101                size,
1102                Vector::new(96.0, 96.0),
1103                size.width * 4,
1104            )
1105        })?;
1106        canvas.push_layer(rect);
1107        Some(LitContent { kind: LitContentKind::Atop { bitmap, rect, size } })
1108    }
1109
1110    /// Ends the lighting: pops the multiply layer, or draws the light map
1111    /// over the content and pops the layer.
1112    pub fn end(self, canvas: &mut dyn IDrawingContextImpl) {
1113        match self.kind {
1114            LitContentKind::Multiplied => {
1115                if let Some(layers) = canvas.as_drawing_context_impl_with_layer_effects() {
1116                    layers.pop_multiply_layer();
1117                }
1118            }
1119            LitContentKind::Atop { bitmap, rect, size } => {
1120                canvas.push_render_options(RenderOptions {
1121                    bitmap_blending_mode: BitmapBlendingMode::SourceAtop,
1122                    ..RenderOptions::default()
1123                });
1124                let source = Rect::new(0.0, 0.0, size.width as f64, size.height as f64);
1125                canvas.draw_bitmap(&*bitmap, 1.0, source, rect);
1126                canvas.pop_render_options();
1127                canvas.pop_layer();
1128            }
1129        }
1130    }
1131}
1132
1133// --- scene lighting (winui-materials.md) -----------------------------------------------
1134
1135/// The unit of the distances of the attenuation of the scene lighting
1136/// effect, in device independent pixels.
1137///
1138/// Deviation (winui-materials.md): WinUI does not say in which unit the
1139/// attenuation of a light is computed; with the constants of Reveal
1140/// (constant 2, linear 0.5, a light 256 above the visual) only a unit of a
1141/// hundred pixels gives the light WinUI shows, so the scene lighting uses
1142/// it. The lighting of the content of a visual keeps pixels.
1143pub const SCENE_LIGHTING_DISTANCE_UNIT: f64 = 100.0;
1144
1145impl LightAttenuation {
1146    /// The attenuation with distances measured in units of `unit` pixels.
1147    fn in_units_of(self, unit: f64) -> LightAttenuation {
1148        LightAttenuation { linear: self.linear / unit, quadratic: self.quadratic / (unit * unit), ..self }
1149    }
1150}
1151
1152impl LightSource {
1153    /// The light with its attenuation in units of `unit` pixels.
1154    fn with_distance_unit(self, unit: f64) -> LightSource {
1155        match self {
1156            LightSource::Point { color, position, attenuation } => {
1157                LightSource::Point { color, position, attenuation: attenuation.in_units_of(unit) }
1158            }
1159            LightSource::Spot { inner_color, outer_color, position, direction, inner_cone_angle, outer_cone_angle, attenuation } => {
1160                LightSource::Spot {
1161                    inner_color,
1162                    outer_color,
1163                    position,
1164                    direction,
1165                    inner_cone_angle,
1166                    outer_cone_angle,
1167                    attenuation: attenuation.in_units_of(unit),
1168                }
1169            }
1170            other => other,
1171        }
1172    }
1173}
1174
1175impl LightMap {
1176    /// The opaque RGBA pixels (row by row from the top) of the lighting of
1177    /// a scene lighting effect over `rect` at `size`: the lights reflected
1178    /// by `material`, clamped to 1, at the centre of each sample.
1179    pub fn scene_pixels(&self, rect: Rect, size: PixelSize, material: &LightingMaterial) -> Vec<u8> {
1180        let channel = |value: f64| (value * 255.0).round().clamp(0.0, 255.0) as u8;
1181        let mut pixels = Vec::with_capacity((size.width.max(1) * size.height.max(1) * 4) as usize);
1182        for light in self.samples(rect, size, material) {
1183            let light = light.clamped();
1184            pixels.extend_from_slice(&[channel(light.r), channel(light.g), channel(light.b), 255]);
1185        }
1186        pixels
1187    }
1188}
1189
1190/// The lights a scene lighting effect painted in `visual` receives: the
1191/// composition lights that target the visual (or an ancestor), and the
1192/// lights of XAML lights whose id is one of `brush_light_ids` and whose
1193/// element holds the visual. The light map is in the coordinates of the
1194/// visual, with the attenuation in units of
1195/// [`SCENE_LIGHTING_DISTANCE_UNIT`]. Every such light, enabled or not,
1196/// remembers the visual, which it draws again when it changes.
1197pub fn scene_light_map(visual: &Rc<ServerCompositionVisual>, brush_light_ids: &[String]) -> LightMap {
1198    let mut map = LightMap::new();
1199    let mut visual_to_root: Option<Matrix> = None;
1200    let Some(compositor) = visual.compositor() else { return map };
1201    for light in compositor.live_lights() {
1202        let space = if let Some(target) = light.target_of(visual) {
1203            light.coordinate_space().unwrap_or(target)
1204        } else if let Some((id, Some(scope))) = light.light_state().xaml_binding() {
1205            if !brush_light_ids.iter().any(|existing| *existing == id) || !super::is_self_or_ancestor(&scope, visual) {
1206                continue;
1207            }
1208            light.light_state().note_brush_lit(visual);
1209            light.coordinate_space().unwrap_or(scope)
1210        } else {
1211            continue;
1212        };
1213        if !light.light_props().is_enabled() {
1214            continue;
1215        }
1216        let to_root_of_visual = *visual_to_root.get_or_insert_with(|| to_root(visual));
1217        let Some(from_root_to_space) = to_root(&space).try_invert() else { continue };
1218        map.add(light.source().with_distance_unit(SCENE_LIGHTING_DISTANCE_UNIT), to_root_of_visual * from_root_to_space);
1219    }
1220    map
1221}
1222
1223/// Paints the lighting of a scene lighting effect over `rect` (in the
1224/// coordinates of `canvas`) for the visual being drawn and the XAML brush
1225/// being painted: an opaque light map, black where no light shines.
1226pub fn paint_scene_lighting(canvas: &mut dyn IDrawingContextImpl, rect: Rect, material: &LightingMaterial) {
1227    if rect.width <= 0.0 || rect.height <= 0.0 {
1228        return;
1229    }
1230    let Some(visual) = super::painting_visual() else { return };
1231    let ids = super::brush_light_ids();
1232    let map = scene_light_map(&visual, &ids);
1233    let size = LightMap::size_for_scale(rect, device_scale(&canvas.transform()));
1234    let pixels = map.scene_pixels(rect, size, material);
1235    let Some(compositor) = visual.compositor() else { return };
1236    let bitmap = compositor.render_interface().with_platform_render_interface(|render_interface| {
1237        render_interface.load_bitmap_from_pixels(
1238            PixelFormats::RGBA8888,
1239            AlphaFormat::Premul,
1240            &pixels,
1241            size,
1242            Vector::new(96.0, 96.0),
1243            size.width * 4,
1244        )
1245    });
1246    if let Some(bitmap) = bitmap {
1247        let source = Rect::new(0.0, 0.0, size.width as f64, size.height as f64);
1248        canvas.draw_bitmap(&*bitmap, 1.0, source, rect);
1249    }
1250}