motion_content/vector/ink/brush/advanced/
material.rs1use super::super::{BrushStroke, Prepared, invalid};
4use super::geometry::{outline, rotated, samples};
5use crate::ImageAsset;
6use motion_core::{GradientExtend, ImagePattern, Paint, Point, Rect, Transform, ValidationError};
7use std::sync::Arc;
8
9const MATERIAL_BLOCK: usize = 16;
11
12#[derive(Clone, Debug)]
14pub struct Paper {
15 pub texture: Arc<ImageAsset>,
17 pub grain_scale: f64,
19 pub absorbency: f64,
21}
22
23fn image_sample(pattern: &ImagePattern, point: Point) -> Result<[f64; 4], ValidationError> {
24 let inverse = pattern
25 .transform
26 .inverse()
27 .ok_or_else(|| invalid("paint", "Noninvertible paint transform"))?;
28 let point = inverse.apply(point);
29 let width = pattern.image.width() as usize;
30 let height = pattern.image.height() as usize;
31 if pattern.decal
32 && (point.x < 0.0 || point.y < 0.0 || point.x >= width as f64 || point.y >= height as f64)
33 {
34 return Ok([0.0; 4]);
35 }
36 let horizontal = point.x - 0.5;
37 let vertical = point.y - 0.5;
38 let column = horizontal.floor();
39 let row = vertical.floor();
40 let fraction = [horizontal - column, vertical - row];
41 let address = |index: i64, length: usize| match pattern.extend {
42 GradientExtend::Pad => index.clamp(0, length as i64 - 1) as usize,
43 GradientExtend::Repeat => index.rem_euclid(length as i64) as usize,
44 GradientExtend::Reflect => {
45 let index = index.rem_euclid(length as i64 * 2) as usize;
46 if index >= length {
47 length * 2 - index - 1
48 } else {
49 index
50 }
51 }
52 };
53 let mut result = [0.0; 4];
54 for vertical in 0..2 {
55 for horizontal in 0..2 {
56 let weight = (if horizontal == 0 {
57 1.0 - fraction[0]
58 } else {
59 fraction[0]
60 }) * (if vertical == 0 {
61 1.0 - fraction[1]
62 } else {
63 fraction[1]
64 });
65 let texel = pattern.image.texel(
66 0,
67 address(column as i64 + horizontal, width),
68 address(row as i64 + vertical, height),
69 );
70 for channel in 0..4 {
71 result[channel] += f64::from(texel[channel]) * weight;
72 }
73 }
74 }
75 Ok(result)
76}
77
78pub(super) fn paint_sample(
79 paint: &Paint,
80 point: Point,
81 bounds: Rect,
82) -> Result<[f64; 4], ValidationError> {
83 let linear = match paint {
84 Paint::Solid(color) => color.to_linear_premultiplied(),
85 Paint::Linear(gradient) => gradient.sample(point, bounds)?,
86 Paint::Radial(gradient) => gradient.sample(point, bounds)?,
87 Paint::Sweep(gradient) => gradient.sample(point, bounds)?,
88 Paint::Pattern(pattern) => image_sample(pattern, point)?,
89 _ => return Err(invalid("paint", "Unsupported brush paint")),
90 };
91 let mut encoded = [0.0; 4];
92 encoded[3] = linear[3];
93 if linear[3] > 0.0 {
94 for channel in 0..3 {
95 encoded[channel] = motion_core::color::linear_to_srgb(linear[channel] / linear[3]);
96 }
97 }
98 Ok(encoded)
99}
100
101pub(super) fn material(
102 stroke: &BrushStroke,
103 bounds: Rect,
104) -> Result<Vec<Prepared>, ValidationError> {
105 let options = &stroke.advanced;
106 let groups = samples(stroke, 1.0)?;
107 let path = Arc::new(outline(stroke, &groups)?);
108 let segments: Vec<_> = groups
109 .iter()
110 .flat_map(|group| group.windows(2).map(|pair| (pair[0], pair[1])))
111 .collect();
112 let boxes: Vec<_> = segments
115 .iter()
116 .map(|(first, second)| {
117 (
118 [
119 first.point.x.min(second.point.x),
120 first.point.y.min(second.point.y),
121 ],
122 [
123 first.point.x.max(second.point.x),
124 first.point.y.max(second.point.y),
125 ],
126 first.radius.max(second.radius),
127 )
128 })
129 .collect();
130 let blocks: Vec<_> = boxes
131 .chunks(MATERIAL_BLOCK)
132 .map(|chunk| {
133 chunk.iter().skip(1).fold(chunk[0], |total, item| {
134 (
135 [total.0[0].min(item.0[0]), total.0[1].min(item.0[1])],
136 [total.1[0].max(item.1[0]), total.1[1].max(item.1[1])],
137 total.2.max(item.2),
138 )
139 })
140 })
141 .collect();
142 let length = stroke.path.try_length()?;
143 let width = bounds.width().ceil().max(1.0);
144 let height = bounds.height().ceil().max(1.0);
145 if width > 16384.0
146 || height > 16384.0
147 || width * height > 16_777_216.0
148 || (width * height) as u64 * segments.len() as u64 > stroke.material_work_limit
149 {
150 return Err(invalid(
151 "material",
152 "Extended material exceeds pixel or total evaluation allowance",
153 ));
154 }
155 let width = width as u32;
156 let height = height as u32;
157 let columns = width.div_ceil(options.tile_size);
158 let rows = height.div_ceil(options.tile_size);
159 if u64::from(columns) * u64::from(rows) > 256 {
160 return Err(invalid("material_tiles", "At most 256 material tiles"));
161 }
162 let paint = options.paint.clone().unwrap_or_else(|| stroke.color.into());
163 let ink = stroke.ink.unwrap_or_default();
164 let paper = options.paper.as_ref().map(|paper| {
165 ImagePattern::from_asset(paper.texture.clone())
166 .transform(Transform::scale(paper.grain_scale))
167 .extend(GradientExtend::Repeat)
168 });
169 let mut tiles = Vec::new();
170 for row in 0..rows {
171 for column in 0..columns {
172 let first = column * options.tile_size;
173 let top = row * options.tile_size;
174 let tile_width = (width - first).min(options.tile_size);
175 let tile_height = (height - top).min(options.tile_size);
176 let mut pixels = vec![[0.0; 4]; tile_width as usize * tile_height as usize];
177 let evaluate = |start: usize, pixels: &mut [[f64; 4]]| -> Result<(), ValidationError> {
178 for (offset, pixel) in pixels.iter_mut().enumerate() {
179 let index = start + offset;
180 let point = Point::new(
181 bounds.left + f64::from(first) + (index % tile_width as usize) as f64 + 0.5,
182 bounds.top + f64::from(top) + (index / tile_width as usize) as f64 + 0.5,
183 );
184 let lower_bound = |bounds: &([f64; 2], [f64; 2], f64)| {
185 let horizontal =
186 (bounds.0[0] - point.x).max(point.x - bounds.1[0]).max(0.0);
187 let vertical = (bounds.0[1] - point.y).max(point.y - bounds.1[1]).max(0.0);
188 libm::hypot(horizontal, vertical) - bounds.2
189 };
190 let measure = |index: usize| {
191 let (first, second) = &segments[index];
192 let delta = second.point - first.point;
193 let square = delta.x * delta.x + delta.y * delta.y;
194 let offset = point - first.point;
195 let amount = if square > 0.0 {
196 ((offset.x * delta.x + offset.y * delta.y) / square).clamp(0.0, 1.0)
197 } else {
198 0.0
199 };
200 let center = first.point + delta * amount;
201 let offset = point - center;
202 let angle = first.angle + (second.angle - first.angle) * amount;
203 let local = rotated(offset.x, offset.y, -angle);
204 let radius = first.radius + (second.radius - first.radius) * amount;
205 let distance = libm::sqrt(
206 local.x * local.x + local.y * local.y / (stroke.aspect * stroke.aspect),
207 ) - radius;
208 (
209 distance,
210 first.arc + (second.arc - first.arc) * amount,
211 offset.x * first.tangent.y - offset.y * first.tangent.x,
212 radius,
213 )
214 };
215 let mut nearest = (f64::INFINITY, 0.0, 0.0, 0.0);
218 let mut nearest_index = usize::MAX;
219 if let Some((block, _)) = blocks
220 .iter()
221 .enumerate()
222 .map(|(block, bounds)| (block, lower_bound(bounds)))
223 .min_by(|left, right| left.1.total_cmp(&right.1))
224 {
225 for index in block * MATERIAL_BLOCK
226 ..((block + 1) * MATERIAL_BLOCK).min(segments.len())
227 {
228 let candidate = measure(index);
229 if candidate.0 < nearest.0 {
230 nearest = candidate;
231 nearest_index = index;
232 }
233 }
234 }
235 let mut overlaps = 0usize;
236 let mut previous_overlap = None;
237 for (block, bounds) in blocks.iter().enumerate() {
238 let lower = lower_bound(bounds);
239 if lower > 1e-6 && lower > nearest.0 + 1e-6 {
240 continue;
241 }
242 for index in block * MATERIAL_BLOCK
243 ..((block + 1) * MATERIAL_BLOCK).min(segments.len())
244 {
245 let candidate = measure(index);
246 if candidate.0 < 0.0
247 && previous_overlap.is_none_or(|previous| index > previous + 1)
248 {
249 overlaps += 1;
250 previous_overlap = Some(index);
251 }
252 if candidate.0 < nearest.0
253 || (candidate.0 == nearest.0 && index < nearest_index)
254 {
255 nearest = candidate;
256 nearest_index = index;
257 }
258 }
259 }
260 let paper_point = options.paper_transform.apply(point);
261 let material = ink.sample((point.x, point.y), (paper_point.x, paper_point.y));
262 let load = stroke.load[0]
263 + (stroke.load[1] - stroke.load[0])
264 * if length > 0.0 {
265 nearest.1 / length
266 } else {
267 0.0
268 };
269 let mut alpha = material
270 .shade(
271 material.coverage(nearest.0, 1.0),
272 nearest.0,
273 (nearest.1, nearest.2 * options.anisotropy),
274 load,
275 )
276 .clamp(0.0, 1.0);
277 if let (Some(paper), Some(parameters)) = (&paper, &options.paper) {
278 let sample = image_sample(paper, paper_point)?;
279 let height = if sample[3] > 0.0 {
280 (sample[0] * 0.2126 + sample[1] * 0.7152 + sample[2] * 0.0722)
281 / sample[3]
282 } else {
283 0.0
284 };
285 alpha *= 1.0 - parameters.absorbency * height.clamp(0.0, 1.0);
286 }
287 if overlaps > 1 && options.build_up > 0.0 {
288 alpha = 1.0
289 - libm::pow(
290 1.0 - alpha,
291 1.0 + options.build_up * (overlaps - 1) as f64,
292 );
293 }
294 let mut color = paint_sample(&paint, point, stroke.path.bounding_box())?;
295 let rim = (1.0 - nearest.0.abs() / 2.0).clamp(0.0, 1.0);
296 let darken = 1.0
297 - options.edge_darkening * rim
298 - options.build_up * (1.0 - 1.0 / overlaps.max(1) as f64) * 0.5;
299 for channel in &mut color[..3] {
300 *channel *= darken;
301 }
302 if options.height > 0.0 {
303 let height = options.height
304 * libm::sqrt((-nearest.0 / nearest.3.max(1e-12)).clamp(0.0, 1.0))
305 * load
306 * (1.0 + options.build_up * overlaps.saturating_sub(1) as f64);
307 for channel in &mut color[..3] {
308 *channel = motion_core::color::linear_to_srgb(height);
309 }
310 }
311 color[3] *= alpha;
312 *pixel = color;
313 }
314 Ok(())
315 };
316 if options.parallel {
317 let threads = std::thread::available_parallelism()
318 .map_or(1, usize::from)
319 .min(8);
320 let chunk = pixels.len().div_ceil(threads).max(1);
321 std::thread::scope(|scope| {
322 let handles: Vec<_> = pixels
323 .chunks_mut(chunk)
324 .enumerate()
325 .map(|(index, pixels)| {
326 let evaluate = &evaluate;
327 scope.spawn(move || evaluate(index * chunk, pixels))
328 })
329 .collect();
330 for handle in handles {
331 handle.join().map_err(|_| {
332 invalid("material_thread", "Material worker panicked")
333 })??;
334 }
335 Ok::<_, ValidationError>(())
336 })?;
337 } else {
338 evaluate(0, &mut pixels)?;
339 }
340 let image = ImageAsset::from_rgba_float(tile_width, tile_height, pixels)
341 .map_err(|error| invalid("material", &error.to_string()))?;
342 let mut pattern = ImagePattern::from_asset(Arc::new(image))
343 .transform(Transform::translate(
344 bounds.left + f64::from(first),
345 bounds.top + f64::from(top),
346 ))
347 .extend(GradientExtend::Pad)
348 .sampling(motion_core::Sampling::Bilinear);
349 pattern.decal = true;
350 tiles.push(Prepared {
351 path: path.clone(),
352 paint: pattern.into(),
353 bounds,
354 });
355 }
356 }
357 Ok(tiles)
358}