three_d_text_builder/lib.rs
1#![warn(clippy::all)]
2#![warn(missing_docs)]
3#![warn(unsafe_code)]
4//!
5//! Glyph atlas based text rendering for the [three-d](https://crates.io/crates/three-d)
6//! crate, using [fontdue](https://crates.io/crates/fontdue).
7//!
8//! This crate's aim is to provide decent performance and quality of the rendered text.
9//!
10//! It is suited for both large static text elements as well as dynamically
11//! formatted text that quickly changes.
12//!
13//! # Quick Example
14//!
15//! ```rust
16//! // Create a text builder from a TTF font
17//! let mut text_builder = TextBuilder::new(
18//! &include_str!("font.ttf"),
19//! TextBuilderSettings::default()
20//!
21//! ).unwrap();
22//!
23//! // Create some text
24//! let text = TextRef {
25//! // The text to render
26//! text: "The quick brown fox jumps over the lazy dog",
27//! // Set the color
28//! color: Srgba::RED,
29//! // Align to the lower center edge of the viewport
30//! align: TextAlign::Viewport(0, -1),
31//! // Add some padding
32//! padding: vec2(0.0, 8.0),
33//! // Move up by 25% of the viewport's height
34//! position: TextPosition::Percentage(vec2(0.0, 0.25)),
35//! // Add a simple shadow effect
36//! shadow: Some((Srgba::BLACK, vec2(1.0, -1.0))),
37//! ..Default::default()
38//! };
39//!
40//! // Build a 3D model for our text - this could also be cached for later usage
41//! // to avoid building the same model multiple times
42//! let text_model = text_builder.build(&context, &[text]);
43//!
44//! // Setup the viewport so our alignment works as expected
45//! text_builder.set_viewport(viewport);
46//!
47//! // Then render it to a target
48//! render_target.render(&camera, text_model, &[]);
49//! ```
50//!
51//! For more use cases, check out the standalone examples.
52//!
53//!
54//! # Implementation Notes
55//!
56//! Each [``TextBuilder``] uses a set of [``GlyphCache``]s (one for each font size)
57//! which produce materials from their glyph atlases. These get mapped to
58//! the UVs of generated character quads, which in turn get combined into a
59//! set of meshes. Text meshes that share the same underlying atlas material
60//! get combined into a single mesh in order to improve drawing performance.
61//!
62//! In addition, a copy on write approach is employed to keep the number of
63//! uploaded GPU materials to a minimum, while also allow generated models to
64//! be cached by the application code.
65//!
66
67// STD Dependencies -----------------------------------------------------------
68// ----------------------------------------------------------------------------
69use std::cmp::Ordering;
70
71
72// External Dependencies ------------------------------------------------------
73// ----------------------------------------------------------------------------
74use three_d::{Mat4, vec2, Vec2, Vec3, Transform};
75
76
77// Modules --------------------------------------------------------------------
78// ----------------------------------------------------------------------------
79mod builder;
80mod cache;
81mod geometry;
82mod glyph;
83mod text;
84mod text_ref;
85
86
87// Re-Exports -----------------------------------------------------------------
88// ----------------------------------------------------------------------------
89pub use builder::{TextBuilder, TextBuilderSettings};
90pub use cache::GlyphCache;
91pub use geometry::material::TextMaterial;
92pub use geometry::mesh::TextMesh;
93pub use text::Text;
94pub use text_ref::TextRef;
95
96
97///
98/// Specifies the unit used by the `position` property of a [``TextRef``].
99///
100#[derive(Debug, Clone, Copy)]
101pub enum TextPosition {
102 ///
103 /// Position is interpreted as pixel values.
104 ///
105 /// # Examples
106 ///
107 /// - `TextPosition::Pixels(vec2(0.0, 64.0))`: Move the text up by exactly `64` pixels
108 ///
109 Pixels(Vec2),
110 ///
111 /// Position is interpreted as a percentage of the viewport's size.
112 ///
113 /// # Examples
114 ///
115 /// - `TextPosition::Percentage(vec2(0.5, 0.0))`: Move the text to the right by half the viewport's width in pixels
116 ///
117 Percentage(Vec2)
118}
119
120impl TextPosition {
121 fn to_pixels(self, viewport: Vec2) -> Vec2 {
122 match self {
123 Self::Pixels(p) => p,
124 Self::Percentage(p) => vec2(viewport.x * p.x, viewport.y * p.y)
125 }
126 }
127}
128
129///
130/// Specifies how the `position` property of a [``TextRef``] is applied.
131///
132#[derive(Debug, Clone, Copy)]
133pub enum TextAlign {
134 ///
135 /// Positions the text relative to the bottom left corner of the screen,
136 /// then anchors it with respect to its bounding box.
137 ///
138 /// # Examples
139 ///
140 /// - `TextAlign::Screen(0, 0)`: Anchors at the center
141 /// - `TextAlign::Screen(1, -1)`: Anchors at the upper left corner
142 ///
143 ScreenAbsolute(i32, i32),
144
145 ///
146 /// Positions the text relative to the center of the screen,
147 /// then anchors it with respect to its bounding box.
148 ///
149 /// # Examples
150 ///
151 /// - `TextAlign::Screen(0, 0)`: Anchors at the center
152 /// - `TextAlign::Screen(1, -1)`: Anchors at the upper left corner
153 ///
154 ScreenRelative(i32, i32),
155
156 ///
157 /// Anchors the text to the edges of the viewport (as set by
158 /// [``TextBuilder::set_viewport``]),
159 /// then positions it with respect to its bounding box.
160 ///
161 /// # Examples
162 ///
163 /// - `TextAlign::Viewport(0, 0)`: Anchors at the lower left corner of the viewport
164 /// - `TextAlign::Viewport(1, 0)`: Anchors at the right edge of the viewport
165 ///
166 Viewport(i32, i32),
167
168 ///
169 /// Anchors the text to the projected position of the 3D point (as described by
170 /// [``TextBuilder::set_projection_view``]),
171 /// then positions it with respect to its bounding box.
172 ///
173 /// # Examples
174 ///
175 /// - `TextAlign::Scene(vec(0.0, 0.0, 0.0), 0, 0)`: Projects the scene's origin then anchors at the center
176 ///
177 Scene(Vec3, i32, i32)
178}
179
180impl Default for TextAlign {
181 fn default() -> Self {
182 Self::ScreenRelative(0, 0)
183 }
184}
185
186impl TextAlign {
187 /// Calculates the global and local offets required to align a rectangle with
188 /// the given bounds inside the specified viewport.
189 ///
190 /// > **Important:** This assumes that the rectangle's lower left corner is
191 /// > at the coordinates `0, 0`.
192 pub fn into_global_and_local_offset(
193 self,
194 world_to_screen: Mat4,
195 viewport: Vec2,
196 bounds: Vec2
197
198 ) -> Option<(Vec2, Vec2)> {
199 fn align_bounds(axis: i32, value: f32) -> f32 {
200 match axis.cmp(&0) {
201 // CEIL here so right-aligned text is stable in its position
202 Ordering::Equal => -(value * 0.5).ceil(),
203 Ordering::Greater => 0.0,
204 Ordering::Less => -value
205 }
206 }
207
208 fn align_viewport(axis: i32, value: f32) -> f32 {
209 // FLOOR here so right-aligned text is stable in its position
210 (axis as f32 * value * 0.5).floor()
211 }
212
213 fn edges(axis: i32, value: f32) -> (f32, f32) {
214 match axis.cmp(&0) {
215 Ordering::Equal => ((value * 0.5).ceil(), -(value * 0.5).floor()),
216 Ordering::Greater => (value, 0.0),
217 Ordering::Less => (0.0, -value)
218 }
219 }
220
221 match self {
222 Self::ScreenAbsolute(x, y) => Some((
223 // Offset from the bottom left of the viewport
224 vec2(align_bounds(0, viewport.x), align_bounds(0, viewport.y)),
225 // Center on the given position
226 vec2(align_bounds(x, bounds.x), -align_bounds(-y, bounds.y))
227 )),
228 Self::ScreenRelative(x, y) => Some((
229 // Offset from the center of the viewport
230 vec2(0.0, 0.0),
231 // Center on the given position
232 vec2(align_bounds(x, bounds.x), -align_bounds(-y, bounds.y))
233 )),
234 Self::Viewport(x, y) => Some((
235 // Offset from the edges of the viewport
236 vec2(
237 align_viewport(x, viewport.x) - align_viewport(x, bounds.x),
238 align_viewport(y, viewport.y) - align_viewport(y, bounds.y)
239 ),
240 // Center on the given position
241 vec2(align_bounds(0, bounds.x), -align_bounds(0, bounds.y))
242 )),
243 Self::Scene(p, x, y) => {
244 // Project the scene point onto the screen
245 let p = world_to_screen.transform_point(three_d::Point3::new(p.x, p.y, p.z));
246
247 // Check if in front of the camera
248 if p.z < 1.0 {
249 let center = vec2(align_bounds(0, viewport.x), align_bounds(0, viewport.y));
250 let sx = p.x * 0.5 + 0.5;
251 let sy = p.y * 0.5 + 0.5;
252
253 // Calculate position on screen
254 let global_offset = vec2(sx * viewport.x, sy * viewport.y);
255 let local_offset = vec2(align_bounds(x, bounds.x), -align_bounds(-y, bounds.y));
256
257 // Clip bounds against viewport
258 let (t, b) = edges(y, bounds.y);
259 let (r, l) = edges(x, bounds.x);
260 let within_x = global_offset.x + r >= 0.0 && global_offset.x + l < viewport.x;
261 let within_y = global_offset.y + t >= 0.0 && global_offset.y + b < viewport.y;
262 if within_x && within_y {
263 Some((
264 // Offset from the bottom left of the viewport
265 center + global_offset,
266 // Center on the given position
267 local_offset
268 ))
269
270 } else {
271 None
272 }
273
274 } else {
275 None
276 }
277 }
278 }
279 }
280}
281