impeller_core/vertices.rs
1//! A triangle mesh a caller supplies directly.
2//!
3//! Everything else this canvas draws is a shape it tessellates itself, which
4//! means the triangles are always well formed and the question of what a
5//! caller might hand over does not arise. Here it does, so the checking
6//! happens once at construction rather than at every draw: a mesh that exists
7//! is a mesh that can be drawn.
8
9use crate::Color;
10use glam::{Affine2, Vec2};
11use impeller_hal::{Error, Result};
12
13/// How positions are grouped into triangles.
14///
15/// The two compact forms exist because a strip or a fan states a triangle in
16/// one vertex where a list states it in three, and a caller who has that shape
17/// already should not have to expand it. They are expanded here rather than
18/// carried to a backend, since the difference is an index buffer and both
19/// backends draw indexed triangles.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum VertexMode {
22 /// Every three positions are one triangle.
23 #[default]
24 Triangles,
25 /// Each position after the second closes a triangle with the two before
26 /// it, alternating winding so the strip is consistently wound.
27 TriangleStrip,
28 /// Each position after the second closes a triangle with the first and the
29 /// one before it.
30 TriangleFan,
31}
32
33/// A mesh of triangles in user space, with optional texture coordinates.
34///
35/// Texture coordinates run from zero to one across the image and are read per
36/// vertex, which is the point of supplying them: every other way of drawing an
37/// image here maps it by position, and a mesh that wanted that mapping would
38/// not need coordinates at all.
39#[derive(Debug, Clone, PartialEq, Default)]
40pub struct Vertices {
41 positions: Vec<Vec2>,
42 texture_coords: Vec<Vec2>,
43 colors: Vec<Color>,
44 indices: Vec<u32>,
45}
46
47impl Vertices {
48 /// A mesh from positions alone, filled by the paint.
49 ///
50 /// The paint is sampled by position, as it is for any shape: a gradient
51 /// runs across the mesh the way it would across a path covering the same
52 /// area.
53 pub fn new(mode: VertexMode, positions: Vec<Vec2>) -> Result<Self> {
54 Self::build(mode, positions, Vec::new(), Vec::new(), None)
55 }
56
57 /// A mesh whose vertices name where in the paint they read.
58 ///
59 /// Any shader reads there, which is what `dart:ui` means by a color source
60 /// at a mesh's coordinates: an image samples its texture, a gradient
61 /// measures its ramp, and a caller's program receives them as `uv` beside
62 /// the clip position and chooses. This used to say "requires an image
63 /// paint", which was true of neither the last two.
64 pub fn textured(
65 mode: VertexMode,
66 positions: Vec<Vec2>,
67 texture_coords: Vec<Vec2>,
68 ) -> Result<Self> {
69 Self::build(mode, positions, texture_coords, Vec::new(), None)
70 }
71
72 /// A mesh whose vertices each carry a color.
73 ///
74 /// The color multiplies whatever the paint produced, so a white paint
75 /// leaves the mesh's own colors, and a gradient paint is shaded by them.
76 /// Multiplying is the only combination offered: `dart:ui` takes a blend
77 /// mode here, and every other mode either discards one of the two inputs
78 /// or is not expressible without a second value per fragment.
79 pub fn colored(mode: VertexMode, positions: Vec<Vec2>, colors: Vec<Color>) -> Result<Self> {
80 Self::build(mode, positions, Vec::new(), colors, None)
81 }
82
83 /// The same, with the triangles named by index rather than by order.
84 pub fn indexed(
85 mode: VertexMode,
86 positions: Vec<Vec2>,
87 texture_coords: Vec<Vec2>,
88 indices: Vec<u32>,
89 ) -> Result<Self> {
90 Self::build(mode, positions, texture_coords, Vec::new(), Some(indices))
91 }
92
93 /// Everything at once, for a caller who has all of it.
94 pub fn full(
95 mode: VertexMode,
96 positions: Vec<Vec2>,
97 texture_coords: Vec<Vec2>,
98 colors: Vec<Color>,
99 indices: Vec<u32>,
100 ) -> Result<Self> {
101 Self::build(mode, positions, texture_coords, colors, Some(indices))
102 }
103
104 fn build(
105 mode: VertexMode,
106 positions: Vec<Vec2>,
107 texture_coords: Vec<Vec2>,
108 colors: Vec<Color>,
109 indices: Option<Vec<u32>>,
110 ) -> Result<Self> {
111 if !texture_coords.is_empty() && texture_coords.len() != positions.len() {
112 return Err(Error::Unsupported(
113 "a mesh with texture coordinates needs one per position",
114 ));
115 }
116 if !colors.is_empty() && colors.len() != positions.len() {
117 return Err(Error::Unsupported(
118 "a mesh with colors needs one per position",
119 ));
120 }
121 // Checked here rather than clamped at draw time. An index past the end
122 // reads whatever follows the buffer, which on one backend is a
123 // validation error and on the other is a triangle drawn from
124 // uninitialized floats -- so this is the difference between a refusal
125 // and a picture that differs between backends for no visible reason.
126 if let Some(indices) = &indices {
127 if let Some(bad) = indices.iter().find(|i| **i as usize >= positions.len()) {
128 let _ = bad;
129 return Err(Error::Unsupported(
130 "a mesh index names a position the mesh does not have",
131 ));
132 }
133 }
134 if !positions.iter().all(|p| p.is_finite()) || !texture_coords.iter().all(|c| c.is_finite())
135 {
136 return Err(Error::Unsupported(
137 "a mesh position or texture coordinate is not a finite number",
138 ));
139 }
140
141 // The order the triangles are actually drawn in, which is where the
142 // mode stops mattering: a strip and a fan are two ways of writing an
143 // index buffer, and writing it here means nothing below this point has
144 // to know which was used.
145 let order: Vec<u32> = match indices {
146 Some(indices) => indices,
147 None => (0..positions.len() as u32).collect(),
148 };
149 let indices = expand(mode, &order);
150 Ok(Self {
151 positions,
152 texture_coords,
153 colors,
154 indices,
155 })
156 }
157
158 pub fn positions(&self) -> &[Vec2] {
159 &self.positions
160 }
161
162 /// Texture coordinates, or empty where the mesh carries none.
163 pub fn texture_coords(&self) -> &[Vec2] {
164 &self.texture_coords
165 }
166
167 /// Per-vertex colors, or empty where the mesh carries none.
168 pub fn colors(&self) -> &[Color] {
169 &self.colors
170 }
171
172 /// Triangle indices, three per triangle, whatever mode built them.
173 pub fn indices(&self) -> &[u32] {
174 &self.indices
175 }
176
177 pub fn is_empty(&self) -> bool {
178 self.indices.is_empty()
179 }
180}
181
182/// One piece of a sprite sheet, and where it goes.
183///
184/// The unit a batched sprite draw is built from. `source` is in texels of the
185/// sheet, because that is how a sheet's layout is known -- a packer emits
186/// pixel rectangles, and normalizing them at every call site is how one of
187/// them eventually gets normalized twice. The quad is that rectangle's size in
188/// user units before the transform, so a sprite drawn with the identity lands
189/// at its own size with its top-left corner at the origin.
190#[derive(Debug, Clone, Copy, PartialEq)]
191pub struct Sprite {
192 /// The part of the sheet to draw, in texels.
193 pub source: SourceRect,
194 /// A color multiplied into this sprite alone.
195 ///
196 /// White changes nothing, and is what [`Sprite::new`] and [`Sprite::at`]
197 /// give. This is what makes one sheet serve a whole palette -- and it is
198 /// per sprite rather than per batch, which is the difference between one
199 /// draw and one draw per color.
200 pub color: Color,
201 /// Where it goes, applied to a quad running from the origin to the
202 /// source's size.
203 ///
204 /// A full affine rather than the rotation-scale-translation that
205 /// `dart:ui` restricts this to. That restriction buys a smaller per-sprite
206 /// payload for a shader that applies the transform itself; these are
207 /// applied here, where a general transform costs exactly the same and a
208 /// caller who wants to skew a sprite is not told they may not.
209 pub transform: Affine2,
210}
211
212/// A rectangle in texels, as a sprite's source.
213///
214/// Deliberately not the canvas `Rect`, which is in user space: a sheet
215/// rectangle and a destination rectangle are different things, and the one
216/// mistake this call invites is passing one where the other belongs.
217#[derive(Debug, Clone, Copy, PartialEq)]
218pub struct SourceRect {
219 pub x: f32,
220 pub y: f32,
221 pub width: f32,
222 pub height: f32,
223}
224
225impl SourceRect {
226 pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
227 Self {
228 x,
229 y,
230 width,
231 height,
232 }
233 }
234
235 pub fn is_finite(&self) -> bool {
236 self.x.is_finite()
237 && self.y.is_finite()
238 && self.width.is_finite()
239 && self.height.is_finite()
240 }
241}
242
243impl Sprite {
244 pub fn new(source: SourceRect, transform: Affine2) -> Self {
245 Self {
246 source,
247 color: Color::WHITE,
248 transform,
249 }
250 }
251
252 /// The same sprite, tinted.
253 pub fn with_color(mut self, color: Color) -> Self {
254 self.color = color;
255 self
256 }
257
258 /// A sprite placed without rotation or scaling.
259 pub fn at(source: SourceRect, position: Vec2) -> Self {
260 Self::new(source, Affine2::from_translation(position))
261 }
262}
263
264/// Rewrite an order of vertices as a list of triangles.
265///
266/// A degenerate tail -- one or two vertices left over from a list, or fewer
267/// than three altogether -- produces no triangle rather than an error, which
268/// is what a mesh with nothing in it should do. An error would be a different
269/// claim: that the caller made a mistake, when a mesh built from a loop that
270/// found nothing is not one.
271fn expand(mode: VertexMode, order: &[u32]) -> Vec<u32> {
272 let mut out = Vec::new();
273 match mode {
274 VertexMode::Triangles => {
275 for triangle in order.chunks_exact(3) {
276 out.extend_from_slice(triangle);
277 }
278 }
279 VertexMode::TriangleStrip => {
280 for (i, window) in order.windows(3).enumerate() {
281 // Every other triangle has the opposite winding, so two of its
282 // vertices swap. Emitting them in strip order instead would
283 // alternate front and back faces, which matters the moment
284 // anything culls -- and a mesh whose triangles disagree about
285 // which side they face is wrong even where nothing does.
286 if i % 2 == 0 {
287 out.extend_from_slice(&[window[0], window[1], window[2]]);
288 } else {
289 out.extend_from_slice(&[window[1], window[0], window[2]]);
290 }
291 }
292 }
293 VertexMode::TriangleFan => {
294 for window in order.windows(2).skip(1) {
295 out.extend_from_slice(&[order[0], window[0], window[1]]);
296 }
297 }
298 }
299 out
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 /// The signed area of a triangle, whose sign is its winding.
307 fn winding(a: Vec2, b: Vec2, c: Vec2) -> f32 {
308 (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y)
309 }
310
311 #[test]
312 fn a_strip_comes_out_consistently_wound() {
313 // The claim the alternation exists to make, and the only one that says
314 // why it is there: emitted in strip order the triangles would alternate
315 // front and back faces, and a mesh whose triangles disagree about which
316 // way they face is wrong the moment anything culls -- and wrong in a way
317 // that draws correctly until something does.
318 //
319 // Beside the test that pins the index sequence rather than instead of
320 // it. That one builds its strip from `points`, which are collinear, so
321 // every triangle in it encloses nothing and has no winding to be
322 // consistent about: it fixes the ordering, and this fixes what the
323 // ordering is for.
324 //
325 // A zigzag rather than a straight run, because a degenerate strip has no
326 // winding to be consistent about: three collinear points enclose nothing
327 // and their signed area is zero, which agrees with everything.
328 let strip: Vec<Vec2> = (0..6)
329 .map(|i| Vec2::new(i as f32 * 10.0, if i % 2 == 0 { 0.0 } else { 12.0 }))
330 .collect();
331 let indices = expand(VertexMode::TriangleStrip, &[0, 1, 2, 3, 4, 5]);
332 assert_eq!(indices.len(), 4 * 3, "six points make four triangles");
333
334 let signs: Vec<f32> = indices
335 .chunks_exact(3)
336 .map(|t| {
337 winding(
338 strip[t[0] as usize],
339 strip[t[1] as usize],
340 strip[t[2] as usize],
341 )
342 .signum()
343 })
344 .collect();
345 assert!(
346 signs.iter().all(|s| *s == signs[0]),
347 "every triangle in a strip should wind the same way, got {signs:?}"
348 );
349
350 // And the alternation is a reordering rather than a different set: each
351 // triangle still covers the three points the strip says it does.
352 for (i, t) in indices.chunks_exact(3).enumerate() {
353 let mut got = [t[0], t[1], t[2]];
354 got.sort_unstable();
355 let want = [i as u32, i as u32 + 1, i as u32 + 2];
356 assert_eq!(got, want, "triangle {i} covers the wrong points");
357 }
358 }
359
360 fn points(n: usize) -> Vec<Vec2> {
361 (0..n).map(|i| Vec2::new(i as f32, 0.0)).collect()
362 }
363
364 #[test]
365 fn a_strip_alternates_winding_so_every_triangle_faces_the_same_way() {
366 let mesh = Vertices::new(VertexMode::TriangleStrip, points(4)).expect("strip");
367 assert_eq!(mesh.indices(), &[0, 1, 2, 2, 1, 3]);
368 }
369
370 #[test]
371 fn a_fan_shares_its_first_vertex_with_every_triangle() {
372 let mesh = Vertices::new(VertexMode::TriangleFan, points(4)).expect("fan");
373 assert_eq!(mesh.indices(), &[0, 1, 2, 0, 2, 3]);
374 }
375
376 #[test]
377 fn a_list_drops_a_partial_triangle_rather_than_inventing_a_vertex() {
378 let mesh = Vertices::new(VertexMode::Triangles, points(5)).expect("list");
379 assert_eq!(mesh.indices(), &[0, 1, 2]);
380 }
381
382 #[test]
383 fn too_few_positions_for_any_triangle_is_an_empty_mesh_rather_than_an_error() {
384 for mode in [
385 VertexMode::Triangles,
386 VertexMode::TriangleStrip,
387 VertexMode::TriangleFan,
388 ] {
389 let mesh = Vertices::new(mode, points(2)).expect("two points");
390 assert!(mesh.is_empty(), "{mode:?} made a triangle from two points");
391 }
392 }
393
394 #[test]
395 fn an_index_past_the_end_is_refused_where_it_is_written() {
396 let error = Vertices::indexed(VertexMode::Triangles, points(3), Vec::new(), vec![0, 1, 3]);
397 assert!(error.is_err(), "an out-of-range index was accepted");
398 }
399
400 #[test]
401 fn texture_coordinates_have_to_match_the_positions_they_belong_to() {
402 let error = Vertices::textured(VertexMode::Triangles, points(3), points(2));
403 assert!(error.is_err(), "a short coordinate list was accepted");
404 }
405}