scimesh 0.3.4
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
text.cpp
Go to the documentation of this file.
1
8
9#include <scimesh/text.h>
10
11#include <scimesh/math_utils.h>
12
13#include <algorithm>
14#include <cmath>
15#include <cstdint>
16#include <limits>
17#include <stdexcept>
18#include <vector>
19
20namespace scimesh {
21
22namespace {
23
30constexpr float kDepthTolerance = 1.0e-3f;
31
33uint8_t to_byte(float value) {
34 const float clamped = std::clamp(value, 0.0f, 1.0f);
35 return static_cast<uint8_t>(std::lround(clamped * 255.0f));
36}
37
41std::vector<std::string> split_lines(const std::string &text) {
42 std::vector<std::string> lines;
43 std::string current;
44 for (char c : text) {
45 if (c == '\r') {
46 continue;
47 }
48 if (c == '\n') {
49 lines.push_back(current);
50 current.clear();
51 continue;
52 }
53 current += c;
54 }
55 lines.push_back(current);
56 if (lines.size() > 1 && lines.back().empty()) {
57 lines.pop_back();
58 }
59 return lines;
60}
61
67void blend_pixel(Image &image, int x, int y, float coverage, const Color &color) {
68 if (coverage <= 0.0f || color.a <= 0.0f) {
69 return;
70 }
71 if (x < 0 || x >= image.width || y < 0 || y >= image.height) {
72 return;
73 }
74 const float sa = std::clamp(color.a, 0.0f, 1.0f) * coverage;
75 if (sa <= 0.0f) {
76 return;
77 }
78
79 uint8_t dr, dg, db, da;
80 image.get_pixel(x, y, dr, dg, db, da);
81 const float dst_alpha = static_cast<float>(da) / 255.0f;
82 const float out_alpha = sa + dst_alpha * (1.0f - sa);
83 if (out_alpha <= 0.0f) {
84 return;
85 }
86 const float inv_alpha = 1.0f / out_alpha;
87 const float keep = dst_alpha * (1.0f - sa);
88 const float r = (color.r * sa + (static_cast<float>(dr) / 255.0f) * keep) * inv_alpha;
89 const float g = (color.g * sa + (static_cast<float>(dg) / 255.0f) * keep) * inv_alpha;
90 const float b = (color.b * sa + (static_cast<float>(db) / 255.0f) * keep) * inv_alpha;
91 image.set_pixel(x, y, to_byte(r), to_byte(g), to_byte(b), to_byte(out_alpha));
92}
93
99void blend_coverage(Image &image, const GlyphBitmap &glyph, int dst_x, int dst_y,
100 const Color &color) {
101 if (glyph.empty() || color.a <= 0.0f) {
102 return;
103 }
104 for (int y = 0; y < glyph.height; ++y) {
105 const int py = dst_y + y;
106 if (py < 0 || py >= image.height) {
107 continue;
108 }
109 for (int x = 0; x < glyph.width; ++x) {
110 const int px = dst_x + x;
111 if (px < 0 || px >= image.width) {
112 continue;
113 }
114 const uint8_t coverage = glyph.at(x, y);
115 if (coverage == 0) {
116 continue;
117 }
118 blend_pixel(image, px, py, static_cast<float>(coverage) / 255.0f, color);
119 }
120 }
121}
122
129float sample_coverage(const GlyphBitmap &glyph, float u, float v) {
130 const float max_u = static_cast<float>(glyph.width - 1);
131 const float max_v = static_cast<float>(glyph.height - 1);
132 const float fu = std::clamp(u - 0.5f, 0.0f, max_u);
133 const float fv = std::clamp(v - 0.5f, 0.0f, max_v);
134 const int x0 = static_cast<int>(std::floor(fu));
135 const int y0 = static_cast<int>(std::floor(fv));
136 const int x1 = std::min(x0 + 1, glyph.width - 1);
137 const int y1 = std::min(y0 + 1, glyph.height - 1);
138 const float sx = fu - static_cast<float>(x0);
139 const float sy = fv - static_cast<float>(y0);
140 const float c00 = glyph.at(x0, y0);
141 const float c10 = glyph.at(x1, y0);
142 const float c01 = glyph.at(x0, y1);
143 const float c11 = glyph.at(x1, y1);
144 const float top = c00 * (1.0f - sx) + c10 * sx;
145 const float bottom = c01 * (1.0f - sx) + c11 * sx;
146 return (top * (1.0f - sy) + bottom * sy) / 255.0f;
147}
148
157void blend_glyph_rotated(Image &image, const GlyphBitmap &glyph, float origin_x,
158 float origin_y, float rotation_degrees, float pivot_x,
159 float pivot_y, const Color &color) {
160 if (glyph.empty() || color.a <= 0.0f) {
161 return;
162 }
163 const float radians = rotation_degrees * 3.14159265358979323846f / 180.0f;
164 const float c = std::cos(radians);
165 const float s = std::sin(radians);
166
167 // Destination bounding box: rotate the four corners of the glyph rect.
168 // Forward transform (text space -> image space) is a rotation by
169 // -rotation_degrees, so that positive angles turn counter-clockwise on
170 // screen (where y grows downwards).
171 const float corners_x[4] = {origin_x, origin_x + static_cast<float>(glyph.width),
172 origin_x, origin_x + static_cast<float>(glyph.width)};
173 const float corners_y[4] = {origin_y, origin_y,
174 origin_y + static_cast<float>(glyph.height),
175 origin_y + static_cast<float>(glyph.height)};
176 float min_x = std::numeric_limits<float>::max();
177 float min_y = std::numeric_limits<float>::max();
178 float max_x = std::numeric_limits<float>::lowest();
179 float max_y = std::numeric_limits<float>::lowest();
180 for (int i = 0; i < 4; ++i) {
181 const float dx = corners_x[i] - pivot_x;
182 const float dy = corners_y[i] - pivot_y;
183 const float rx = pivot_x + dx * c + dy * s;
184 const float ry = pivot_y - dx * s + dy * c;
185 min_x = std::min(min_x, rx);
186 min_y = std::min(min_y, ry);
187 max_x = std::max(max_x, rx);
188 max_y = std::max(max_y, ry);
189 }
190
191 const int x_start = std::max(0, static_cast<int>(std::floor(min_x)) - 1);
192 const int x_end = std::min(image.width - 1, static_cast<int>(std::ceil(max_x)) + 1);
193 const int y_start = std::max(0, static_cast<int>(std::floor(min_y)) - 1);
194 const int y_end = std::min(image.height - 1, static_cast<int>(std::ceil(max_y)) + 1);
195
196 for (int py = y_start; py <= y_end; ++py) {
197 for (int px = x_start; px <= x_end; ++px) {
198 // Inverse transform (image space -> text space): rotate the pixel
199 // centre back by +rotation_degrees.
200 const float dx = static_cast<float>(px) + 0.5f - pivot_x;
201 const float dy = static_cast<float>(py) + 0.5f - pivot_y;
202 const float tx = pivot_x + dx * c - dy * s;
203 const float ty = pivot_y + dx * s + dy * c;
204
205 const float u = tx - origin_x;
206 const float v = ty - origin_y;
207 if (u < 0.0f || v < 0.0f || u >= static_cast<float>(glyph.width) ||
208 v >= static_cast<float>(glyph.height)) {
209 continue;
210 }
211 const float coverage = sample_coverage(glyph, u, v);
212 if (coverage <= 0.002f) {
213 continue;
214 }
215 blend_pixel(image, px, py, coverage, color);
216 }
217 }
218}
219
221struct PlacedGlyph {
222 const GlyphBitmap *bitmap = nullptr;
223 int x = 0;
224 int y = 0;
225};
226
234void draw_line(Image &image, const std::string &line, const Font &font, float x,
235 float baseline_y, const TextDrawStyle &style, float pivot_x,
236 float pivot_y) {
237 const std::vector<uint32_t> codepoints = Font::decode_utf8(line);
238
239 std::vector<PlacedGlyph> placed;
240 placed.reserve(codepoints.size());
241 float pen = x;
242 uint32_t previous = 0;
243 bool have_previous = false;
245 if (have_previous) {
246 pen += font.kerning(previous, codepoint);
247 }
248 const GlyphBitmap *glyph = font.glyph(codepoint);
249 if (glyph != nullptr) {
250 if (!glyph->empty()) {
251 PlacedGlyph pg;
252 pg.bitmap = glyph;
253 pg.x = static_cast<int>(std::lround(pen)) + glyph->x_offset;
254 pg.y = static_cast<int>(std::lround(baseline_y)) + glyph->y_offset;
255 placed.push_back(pg);
256 }
257 pen += glyph->advance;
258 }
260 have_previous = true;
261 }
262
263 const bool rotated = std::abs(style.rotation) > 1.0e-4f;
264 const auto stamp = [&](const GlyphBitmap &bitmap, float glyph_x, float glyph_y,
265 const Color &color) {
266 if (rotated) {
268 pivot_x, pivot_y, color);
269 } else {
270 blend_coverage(image, bitmap, static_cast<int>(std::lround(glyph_x)),
271 static_cast<int>(std::lround(glyph_y)), color);
272 }
273 };
274
275 if (style.halo_color.a > 0.0f && style.halo_width > 0.0f) {
276 const int width = std::max(1, static_cast<int>(std::lround(style.halo_width)));
277 const int limit = width * width + width; // slightly rounded kernel
278 for (const PlacedGlyph &pg : placed) {
279 for (int dy = -width; dy <= width; ++dy) {
280 for (int dx = -width; dx <= width; ++dx) {
281 if (dx == 0 && dy == 0) {
282 continue; // the glyph itself is drawn below
283 }
284 if (dx * dx + dy * dy > limit) {
285 continue;
286 }
287 stamp(*pg.bitmap, static_cast<float>(pg.x + dx),
288 static_cast<float>(pg.y + dy), style.halo_color);
289 }
290 }
291 }
292 }
293
294 for (const PlacedGlyph &pg : placed) {
295 stamp(*pg.bitmap, static_cast<float>(pg.x), static_cast<float>(pg.y),
296 style.color);
297 }
298}
299
303void render_one_layer(const TextLayer &layer, Image &output,
304 const Mat4 &view_projection, const Mat4 &transform,
305 float pixel_scale, const std::vector<float> &z_buffer,
306 const Color &default_color) {
307 if (layer.empty() || output.width <= 0 || output.height <= 0) {
308 return;
309 }
310 const float font_size = layer.size * pixel_scale;
311 if (!(font_size > 0.0f)) {
312 return;
313 }
314
315 const Font font = cached_font(layer.font_file, font_size);
316 const FontMetrics metrics = font.metrics();
317 const float line_step = metrics.box_height() * std::max(layer.line_spacing, 0.0f);
318
319 const bool depth_test = layer.depth_test && (layer.space == TextSpace::WORLD) &&
320 !z_buffer.empty();
321
322 for (size_t i = 0; i < layer.count(); ++i) {
323 const std::string &text = layer.strings[i];
324 if (text.empty()) {
325 continue;
326 }
327 const Color color = layer.color_or(i, default_color);
328 if (color.a <= 0.0f) {
329 continue;
330 }
331
332 // --- anchor position in output pixels (origin top left, y down) -----
333 float anchor_x = 0.0f, anchor_y = 0.0f;
334 float anchor_depth = 0.0f;
335 if (layer.space == TextSpace::WORLD) {
336 const Vec3 world = Vec3(transform * Vec4(layer.position_or(i), 1.0f));
338 if (clip.w <= 1.0e-6f) {
339 continue; // behind the camera
340 }
341 float sx = 0.0f, sy = 0.0f, depth = 0.0f;
342 ndc_to_screen(perspective_divide(clip), output.width, output.height, sx, sy, depth);
343 anchor_x = sx;
344 anchor_y = sy;
345 anchor_depth = depth;
346 } else {
347 const Vec3 position = layer.position_or(i);
348 anchor_x = position.x * pixel_scale;
349 anchor_y = position.y * pixel_scale;
350 }
351 anchor_x += layer.offset.x * pixel_scale;
352 anchor_y += layer.offset.y * pixel_scale;
353
354 // A degenerate camera (or a point exactly on the camera plane) can
355 // produce non-finite coordinates; such labels are simply skipped.
356 if (!std::isfinite(anchor_x) || !std::isfinite(anchor_y)) {
357 continue;
358 }
359
360 // --- hide labels that are behind the rendered geometry --------------
361 if (depth_test) {
362 const int px = static_cast<int>(std::floor(anchor_x));
363 const int py = static_cast<int>(std::floor(anchor_y));
364 if (px < 0 || px >= output.width || py < 0 || py >= output.height) {
365 continue; // anchor outside the image
366 }
367 const float surface_depth =
368 z_buffer[static_cast<size_t>(py) * static_cast<size_t>(output.width) +
369 static_cast<size_t>(px)];
371 continue; // geometry in front of the anchor
372 }
373 }
374
375 // --- lay the text out around the anchor -----------------------------
376 const std::vector<std::string> lines = split_lines(text);
377 float block_width = 0.0f;
378 for (const std::string &line : lines) {
379 block_width = std::max(block_width, font.measure(line));
380 }
381 const float block_height =
382 metrics.box_height() + static_cast<float>(lines.size() - 1) * line_step;
383 const float left = anchor_x - layer.adj.x * block_width;
384 const float first_baseline =
385 anchor_y + metrics.ascent - (1.0f - layer.adj.y) * block_height;
386
387 TextDrawStyle style;
388 style.color = color;
389 style.halo_color = layer.halo_color;
390 style.halo_width = layer.halo_width * pixel_scale;
391 style.line_spacing = layer.line_spacing;
392 style.rotation = layer.rotation;
393
394 for (size_t line_index = 0; line_index < lines.size(); ++line_index) {
395 // A rotated label rotates about its anchor, so the anchor stays put
396 // while the text swings around it.
398 first_baseline + static_cast<float>(line_index) * line_step,
400 }
401 }
402}
403
404} // namespace
405
406void draw_text(Image &image, const std::string &text, const Font &font, float x,
407 float baseline_y, const TextDrawStyle &style) {
408 if (!font.valid()) {
409 throw std::invalid_argument("scimesh: draw_text() needs a valid font");
410 }
411 const float line_step = font.metrics().box_height() * std::max(style.line_spacing, 0.0f);
412 const std::vector<std::string> lines = split_lines(text);
413 for (size_t i = 0; i < lines.size(); ++i) {
414 // A rotated draw_text() rotates about the given pen origin, i.e. about
415 // the left end of the first baseline.
416 draw_line(image, lines[i], font, x,
417 baseline_y + static_cast<float>(i) * line_step, style, x,
418 baseline_y);
419 }
420}
421
422TextExtent measure_text(const std::string &text, float size,
423 const std::string &font_file, float line_spacing) {
424 if (!(size > 0.0f)) {
425 throw std::invalid_argument("scimesh: text size must be > 0");
426 }
427 const Font font = cached_font(font_file, size);
428 const FontMetrics metrics = font.metrics();
429 const std::vector<std::string> lines = split_lines(text);
430
432 extent.line_count = lines.size();
433 for (const std::string &line : lines) {
434 extent.width = std::max(extent.width, font.measure(line));
435 }
436 const float line_step = metrics.box_height() * std::max(line_spacing, 0.0f);
437 extent.height = metrics.box_height() +
438 static_cast<float>(lines.size() - 1) * line_step;
439 // The ascent/descent of the *block*: the first baseline sits one ascent
440 // below the top of the block, the last one one descent above its bottom.
441 extent.ascent = metrics.ascent;
442 extent.descent = extent.height - metrics.ascent;
443 return extent;
444}
445
446namespace detail {
447
448void render_text_layers(const std::vector<TextNodeRef> &layers, Image &output,
449 const Mat4 &view_projection, float pixel_scale,
450 const std::vector<float> &z_buffer,
451 const Color &default_color) {
452 for (const TextNodeRef &node : layers) {
453 if (node.layer == nullptr) {
454 continue;
455 }
456 render_one_layer(*node.layer, output, view_projection, node.transform,
457 pixel_scale, z_buffer, default_color);
458 }
459}
460
461} // namespace detail
462
463} // namespace scimesh
A TrueType font, loaded at a fixed pixel size, with a glyph cache.
Definition font.h:143
static std::vector< uint32_t > decode_utf8(const std::string &text)
Decode a UTF-8 string into Unicode code points.
Definition font.cpp:311
Low-level math utilities for the rendering pipeline.
void render_text_layers(const std::vector< TextNodeRef > &layers, Image &output, const Mat4 &view_projection, float pixel_scale, const std::vector< float > &z_buffer, const Color &default_color)
Draw all text layers into an already rendered image.
Definition text.cpp:448
void draw_text(Image &image, const std::string &text, const Font &font, float x, float baseline_y, const TextDrawStyle &style)
Draw text into an image at a given position and baseline.
Definition text.cpp:406
TextExtent measure_text(const std::string &text, float size, const std::string &font_file, float line_spacing)
Measure a (possibly multi-line) string in a given font and size.
Definition text.cpp:422
void ndc_to_screen(const Vec3 &ndc, int width, int height, float &screen_x, float &screen_y, float &depth)
Convert from normalized device coordinates (NDC) to screen (pixel) coordinates.
Definition math_utils.h:125
@ WORLD
3D world coordinates, projected with the scene camera.
glm::mat4 Mat4
4×4 floating-point matrix.
Definition types.h:65
glm::vec3 Vec3
3-component floating-point vector (xyz).
Definition types.h:46
Vec3 perspective_divide(const Vec4 &clip)
Perform perspective division: divide xyz by w.
Definition math_utils.h:105
Font cached_font(const std::string &path, float pixel_size)
Load a font, re-using an already loaded one when possible.
Definition font.cpp:405
Vec4 transform_point_homogeneous(const Mat4 &m, const Vec3 &p)
Transform a point by a 4×4 matrix, returning the full Vec4 result.
Definition math_utils.h:75
glm::vec4 Vec4
4-component floating-point vector (xyzw).
Definition types.h:52
float width
Definition renderer.cpp:36
An RGBA color with floating-point components.
Definition types.h:88
Vertical metrics of a font at a given pixel size.
Definition font.h:59
float ascent
Distance from the baseline to the top of the line, in pixels.
Definition font.h:61
float box_height() const
Height of the glyph box of one line, in pixels (ascent + descent).
Definition font.h:75
A 2D RGBA image buffer.
Definition image.h:92
How to draw text onto an image with draw_text().
Definition text.h:70
Size of a (possibly multi-line) piece of text.
Definition text.h:105
size_t line_count
Number of lines (a trailing newline does not add one).
Definition text.h:119
A non-owning reference to one text layer in a scene, together with its placement transform (and optio...
Definition text.h:323
int y
Top edge of the bitmap, in image pixels.
Definition text.cpp:224
int x
Left edge of the bitmap, in image pixels.
Definition text.cpp:223
const GlyphBitmap * bitmap
Definition text.cpp:222
Text labels — the TextLayer.