Another project
1use bone_render::{
2 AtlasGrid, AtlasPage, ClearColor, OffscreenContext, Result, SnapshotFrame, SolidRenderer,
3 Style, TILE_PAD, ViewportExtent, ViewportPx,
4};
5use bone_types::{DisplayMode, IconId};
6
7use crate::icon::{IconModel, icon_camera, icon_style};
8use crate::models::model;
9use crate::offscreen_context;
10
11pub const MASTER_TILE: u32 = 192;
12
13const SHADOW_OFFSET: usize = 7;
14const SHADOW_BLUR: usize = 9;
15const SHADOW_ALPHA: u32 = 90;
16const SHADOW_LEVEL: u32 = 10;
17
18#[derive(Clone, Debug)]
19pub struct BakedPage {
20 pub page: AtlasPage,
21 pub extent: ViewportExtent,
22 pub rgba: Vec<u8>,
23}
24
25pub fn bake() -> Result<Vec<BakedPage>> {
26 let ctx = offscreen_context(ViewportExtent::square(ViewportPx::new(MASTER_TILE)))?;
27 let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
28 let master = to_usize(MASTER_TILE);
29 let shadowed: Vec<(IconId, Vec<u8>)> = IconId::ALL
30 .iter()
31 .map(|&id| {
32 let premul = render_master(&mut renderer, &ctx, &model(id))?;
33 Ok((id, with_shadow(master, &premul)))
34 })
35 .collect::<Result<Vec<_>>>()?;
36
37 let grid = AtlasGrid::DEFAULT;
38 Ok(AtlasPage::ALL
39 .into_iter()
40 .map(|page| {
41 let page_side = to_usize(page.side_px());
42 let tiles: Vec<(IconId, Vec<u8>)> = shadowed
43 .iter()
44 .map(|(id, premul)| (*id, downsample(master, premul, page_side)))
45 .collect();
46 let (width, height) = grid.pixel_size(page);
47 BakedPage {
48 page,
49 extent: ViewportExtent::new(ViewportPx::new(width), ViewportPx::new(height)),
50 rgba: pack(grid, page, &tiles),
51 }
52 })
53 .collect())
54}
55
56fn render_master(
57 renderer: &mut SolidRenderer,
58 ctx: &OffscreenContext,
59 m: &IconModel,
60) -> Result<Vec<u8>> {
61 let black = render_one(renderer, ctx, m, ClearColor::opaque(0.0, 0.0, 0.0))?;
62 let white = render_one(renderer, ctx, m, ClearColor::opaque(1.0, 1.0, 1.0))?;
63 Ok(black
64 .rgba()
65 .chunks_exact(4)
66 .zip(white.rgba().chunks_exact(4))
67 .flat_map(|(dark, light)| {
68 let drop =
69 |channel: usize| u32::from(light[channel]).saturating_sub(u32::from(dark[channel]));
70 let coverage_drop = (drop(0) + drop(1) + drop(2)) / 3;
71 let alpha = u8_of(255u32.saturating_sub(coverage_drop));
72 [dark[0], dark[1], dark[2], alpha]
73 })
74 .collect())
75}
76
77fn render_one(
78 renderer: &mut SolidRenderer,
79 ctx: &OffscreenContext,
80 m: &IconModel,
81 background: ClearColor,
82) -> Result<SnapshotFrame> {
83 let (scene, edges) = m.tessellate();
84 let style: Style = icon_style().with_background(background);
85 renderer.render_display(
86 ctx,
87 &scene,
88 &edges,
89 icon_camera(m.view()),
90 &style,
91 DisplayMode::ShadedWithEdges,
92 )
93}
94
95fn with_shadow(side: usize, icon: &[u8]) -> Vec<u8> {
96 let alpha: Vec<u8> = icon.chunks_exact(4).map(|px| px[3]).collect();
97 let blurred = box_blur(side, &alpha, SHADOW_BLUR);
98 icon.chunks_exact(4)
99 .enumerate()
100 .flat_map(|(index, top)| {
101 let x = index % side;
102 let y = index / side;
103 let source = if y >= SHADOW_OFFSET {
104 u32::from(blurred[(y - SHADOW_OFFSET) * side + x])
105 } else {
106 0
107 };
108 let shadow_alpha = source * SHADOW_ALPHA / 255;
109 let shadow_rgb = u8_of(SHADOW_LEVEL * shadow_alpha / 255);
110 let shadow = [shadow_rgb, shadow_rgb, shadow_rgb, u8_of(shadow_alpha)];
111 over([top[0], top[1], top[2], top[3]], shadow)
112 })
113 .collect()
114}
115
116fn box_blur(side: usize, src: &[u8], radius: usize) -> Vec<u8> {
117 let horizontal = blur_axis(side, src, radius, true);
118 blur_axis(side, &horizontal, radius, false)
119}
120
121fn blur_axis(side: usize, src: &[u8], radius: usize, horizontal: bool) -> Vec<u8> {
122 (0..side)
123 .flat_map(|y| (0..side).map(move |x| (x, y)))
124 .map(|(x, y)| {
125 let center = if horizontal { x } else { y };
126 let lo = center.saturating_sub(radius);
127 let hi = (center + radius).min(side - 1);
128 let (sum, count) = (lo..=hi).fold((0u32, 0u32), |(sum, count), p| {
129 let (sx, sy) = if horizontal { (p, y) } else { (x, p) };
130 (sum + u32::from(src[sy * side + sx]), count + 1)
131 });
132 div_round(sum, count)
133 })
134 .collect()
135}
136
137fn downsample(master_side: usize, master: &[u8], page_side: usize) -> Vec<u8> {
138 assert_eq!(
139 master_side % page_side,
140 0,
141 "master tile must divide evenly into each atlas page side",
142 );
143 let factor = master_side / page_side;
144 let area = to_u32(factor * factor);
145 (0..page_side)
146 .flat_map(|oy| (0..page_side).map(move |ox| (ox, oy)))
147 .flat_map(|(ox, oy)| {
148 let sums = (0..factor)
149 .flat_map(|dy| (0..factor).map(move |dx| (dx, dy)))
150 .fold([0u32; 4], |mut acc, (dx, dy)| {
151 let base = ((oy * factor + dy) * master_side + (ox * factor + dx)) * 4;
152 acc[0] += u32::from(master[base]);
153 acc[1] += u32::from(master[base + 1]);
154 acc[2] += u32::from(master[base + 2]);
155 acc[3] += u32::from(master[base + 3]);
156 acc
157 });
158 [
159 div_round(sums[0], area),
160 div_round(sums[1], area),
161 div_round(sums[2], area),
162 div_round(sums[3], area),
163 ]
164 })
165 .collect()
166}
167
168fn pack(grid: AtlasGrid, page: AtlasPage, tiles: &[(IconId, Vec<u8>)]) -> Vec<u8> {
169 let (width, height) = grid.pixel_size(page);
170 let width = to_usize(width);
171 let height = to_usize(height);
172 let page_side = to_usize(page.side_px());
173 let cell = to_usize(page.cell_px());
174 let pad = to_usize(TILE_PAD);
175 tiles
176 .iter()
177 .fold(vec![0u8; width * height * 4], |mut atlas, (id, tile)| {
178 let grid_cell = grid.cell(id.tile());
179 let cell_x = grid_cell.col * cell;
180 let cell_y = grid_cell.row * cell;
181 (0..cell).for_each(|ly| {
182 (0..cell).for_each(|lx| {
183 let sx = lx.saturating_sub(pad).min(page_side - 1);
184 let sy = ly.saturating_sub(pad).min(page_side - 1);
185 let src = (sy * page_side + sx) * 4;
186 let dst = ((cell_y + ly) * width + (cell_x + lx)) * 4;
187 atlas[dst..dst + 4].copy_from_slice(&tile[src..src + 4]);
188 });
189 });
190 atlas
191 })
192}
193
194fn over(top: [u8; 4], bottom: [u8; 4]) -> [u8; 4] {
195 let inverse = 255 - u32::from(top[3]);
196 let blend = |channel: usize| {
197 u8_of(u32::from(top[channel]) + (inverse * u32::from(bottom[channel]) + 127) / 255)
198 };
199 [blend(0), blend(1), blend(2), blend(3)]
200}
201
202fn div_round(sum: u32, count: u32) -> u8 {
203 u8_of((sum + count / 2) / count)
204}
205
206fn u8_of(value: u32) -> u8 {
207 let Ok(byte) = u8::try_from(value.min(255)) else {
208 panic!("value clamped to 255 fits a u8");
209 };
210 byte
211}
212
213fn to_usize(value: u32) -> usize {
214 let Ok(out) = usize::try_from(value) else {
215 panic!("atlas dimension fits usize");
216 };
217 out
218}
219
220fn to_u32(value: usize) -> u32 {
221 let Ok(out) = u32::try_from(value) else {
222 panic!("atlas count fits u32");
223 };
224 out
225}