Open stefanpartheym opened 1 month ago
Thanks for the report! I'll check this out.
Ok, I basically hacked it so that i can intuitively render text as expected. Please be aware, the example below uses camera projection with a flipped Y-axis (growing downwards) (as I find it more intuitive). Also stbtt uses it this way I guess. However, I think you could easily change the code to use it with a "normal" Y-axis (growing upwards).
const std = @import("std");
const stbtt = @import("stb_truetype").stbtt;
const d = @import("delve");
const math = d.math;
const spatial = d.spatial;
const graphics = d.graphics;
const platform_graphics = d.platform.graphics;
const input = d.platform.input;
var batcher: graphics.batcher.SpriteBatcher = undefined;
var shader: platform_graphics.Shader = undefined;
var camera: platform_graphics.CameraMatrices = undefined;
var pos = math.vec2(0, 0);
pub fn main() !void {
const mod = d.modules.Module{
.name = "main",
.init_fn = init,
.cleanup_fn = cleanup,
.tick_fn = tick,
.pre_draw_fn = pre_draw,
.draw_fn = draw,
};
try d.init(std.heap.c_allocator);
try d.modules.registerModule(mod);
try d.app.start(.{
.title = "delve-fonts-test",
.width = 800,
.height = 600,
});
}
fn init() !void {
camera = getCamera2d();
_ = try d.fonts.loadFont("default", "assets/VT323-Regular.ttf", 1024, 200);
shader = try platform_graphics.Shader.initDefault(.{ .blend_mode = .BLEND });
batcher = try graphics.batcher.SpriteBatcher.init(.{ .shader = shader });
}
fn cleanup() !void {
batcher.deinit();
shader.destroy();
}
fn tick(_: f32) void {
if (input.isKeyJustPressed(.ESCAPE) or
input.isKeyJustPressed(.Q))
{
d.platform.app.exit();
}
const speed: f32 = 5;
if (input.isKeyPressed(.UP)) {
pos.y -= speed;
}
if (input.isKeyPressed(.DOWN)) {
pos.y += speed;
}
if (input.isKeyPressed(.LEFT)) {
pos.x -= speed;
}
if (input.isKeyPressed(.RIGHT)) {
pos.x += speed;
}
}
fn pre_draw() void {
batcher.reset();
defer batcher.apply();
if (d.fonts.getLoadedFont("default")) |font| {
var pos_x: f32 = pos.x;
var pos_y: f32 = pos.y;
addStringToSpriteBatch(
font,
&batcher,
"Test Text 123",
&pos_x,
&pos_y,
0.2,
d.colors.red,
);
}
}
fn draw() void {
batcher.draw(camera, math.Mat4.identity);
}
//------------------------------------------------------------------------------
fn getView2d() math.Mat4 {
return math.Mat4.lookat(
.{ .x = 0, .y = 0, .z = 1 },
math.Vec3.zero,
math.Vec3.up,
);
}
pub fn getCamera2d() platform_graphics.CameraMatrices {
return platform_graphics.CameraMatrices{
.view = getView2d(),
// NOTE: Y-axis of camera projection is flipped.
.proj = platform_graphics.getProjectionOrtho(-1, 1, true),
};
}
//------------------------------------------------------------------------------
fn getCharQuad(
font: *d.fonts.LoadedFont,
char_index: usize,
x_pos: *f32,
y_pos: *f32,
max_char_height: f32,
) d.fonts.CharQuad {
var aligned_quad: stbtt.stbtt_aligned_quad = undefined;
stbtt.stbtt_GetPackedQuad(
@ptrCast(font.char_info),
@intCast(font.tex_size),
@intCast(font.tex_size),
@intCast(char_index),
@ptrCast(x_pos),
@ptrCast(y_pos),
&aligned_quad,
1,
);
var char_quad: d.fonts.CharQuad = .{
.tex_region = graphics.sprites.TextureRegion.default(),
.rect = spatial.Rect.fromSize(math.Vec2.new(1.0, 1.0)),
};
// tex region
char_quad.tex_region.u = aligned_quad.s0;
char_quad.tex_region.v = aligned_quad.t0;
char_quad.tex_region.u_2 = aligned_quad.s1;
char_quad.tex_region.v_2 = aligned_quad.t1;
// pos rect
char_quad.rect.x = aligned_quad.x0;
char_quad.rect.y = aligned_quad.y0;
char_quad.rect.width = aligned_quad.x1 - aligned_quad.x0;
char_quad.rect.height = aligned_quad.y1 - aligned_quad.y0;
// NOTE: Place the char with position at top-left corner of the quad.
char_quad.rect.y += max_char_height;
return char_quad;
}
fn getMaxCharHeight(font: *d.fonts.LoadedFont) f32 {
var max_height: f32 = 0;
for (font.char_info) |char_info| {
// std.debug.print("max charheight: {d}\n", .{char_info.y1 - char_info.y0});
max_height = @max(max_height, @as(f32, @floatFromInt(char_info.y1 - char_info.y0)));
}
return max_height;
}
fn addStringToSpriteBatchWithKerning(
font: *d.fonts.LoadedFont,
sprite_batch: *graphics.batcher.SpriteBatcher,
string: []const u8,
x_pos: *f32,
y_pos: *f32,
line_height_mod: f32,
kerning_mod: f32,
scale: f32,
color: d.colors.Color,
) void {
sprite_batch.useTexture(font.texture);
const max_char_height = getMaxCharHeight(font);
const orig_x: f32 = x_pos.*;
for (string) |char| {
if (char == '\n') {
x_pos.* = orig_x;
y_pos.* += font.font_size + line_height_mod;
continue;
}
const char_quad = getCharQuad(
font,
char - 32,
x_pos,
y_pos,
max_char_height,
);
// NOTE: Necessary when Y-axis of camera projection is flipped.
const region = char_quad.tex_region.flipY();
sprite_batch.addRectangle(
char_quad.rect.scale(scale),
region,
color,
);
x_pos.* += kerning_mod;
}
x_pos.* = orig_x;
y_pos.* += font.font_size + line_height_mod;
}
fn addStringToSpriteBatch(
font: *d.fonts.LoadedFont,
sprite_batch: *graphics.batcher.SpriteBatcher,
string: []const u8,
x_pos: *f32,
y_pos: *f32,
scale: f32,
color: d.colors.Color,
) void {
addStringToSpriteBatchWithKerning(
font,
sprite_batch,
string,
x_pos,
y_pos,
0,
0,
scale,
color,
);
}
Note how I'm using getMaxCharHeight()
to get the max height of a char for the font. I think there is actually a stbtt
API function for that stbtt_GetFontBoundingBox()
, which operates on a stbtt_fontinfo
struct and does not require iterating each charinfo element. So, one would need to get the max char height when loading the font and set it in the LoadedFont
struct.
Hope this helps.
Thanks, I should roll these changes into the base code. Was the Y-Axis being flipped the main culprit of the two view matrices being offset, or was there another mismatch? Maybe there should be a y-axis flip toggle in there somewhere.
Was the Y-Axis being flipped the main culprit of the two view matrices being offset, or was there another mismatch?
Initially, I tried it with a non-flipped y-axis. The result however wasn't 100% correct either (as seen in my initial post). I think it has to do with the way the chars are positioned on screen in the original getCharQuad
function. The function tries to correctly position the chars on a "baseline". The baseline is offset however, which makes the whole text appear in different position than expected. I think in order to correctly position the text in a "non-y-axis-flipped" scenario, one would still need information about the max height of a char.
And yes, I think a y-axis flip toggle would be convenient.
Hi @Interrupt ,
recently I tried to use the builtin
addStringToSpriteBatch
andaddStringToSpriteBatchWithKerning
functions to render text to a 2D surface. However, positioning the text seems to not work as it does for regular shapes or textures (for instance withBatcher.addRectangle
).In the following example I positioned both – a red rectangle and the text – at X: 50 and Y: 50 (using a math grid, meaning Y grows up and X grows to the right). The result looks as follows:
One can clearly see that the text is in a completely different position than the rectangle. I would have expected to be below the left-bottom corner of the rectangle.
You can run the example by putting in the
examples
directory of the repo. Maybe you can also temporarily overwrite thefonts.zig
example and the runzig build run-fonts
.Thanks :)