Rendering text was tricky, and quite involved. To be honest I wanted to give up and switch to some game library, but the next day, I fixed some bugs, and finally get this working. The end result is not pretty, it’s aliased text, but for starters this is fine. And I will use the text for development mode for now. But if I want the game to render any dynamic text, I would have to find a way to anti-alias rendering, but let’s keep it for some other time.
I am not going to pretend, I know what I am doing, what a glyph is or the main ingredients necessary for rendering text are. I will just show you one way how this is possible, so you can get some sense, and work your way through working example code.
Rendering Glyphs with DirectWrite
This is the full DirectWrite API necessary to render individual characters.
pub const MyFontFactory = struct {
dwrite_factory: *IDWriteFactory,
font_face: *IDWriteFontFace,
const Self = @This();
pub fn deinit(self: *Self) void {
self.dwrite_factory.IUnknown.Release();
self.font_face.IUnknown.Release();
}
pub fn init(font_path_w: [*:0]const u16) !MyFontFactory {
var dwrite_factory: ?*IDWriteFactory = null;
var hr = DWriteCreateFactory(
.SHARED,
IID_IDWriteFactory,
@ptrCast(&dwrite_factory),
);
if (hr != HRESULT.S_OK) return error.FailedCreateWicFactory;
errdefer _ = dwrite_factory.?.IUnknown.Release();
var font_file: *IDWriteFontFile = undefined;
hr = dwrite_factory.?.CreateFontFileReference(
font_path_w,
null,
&font_file,
);
if (hr != HRESULT.S_OK) return error.FailedCreateFontFileReference;
defer _ = font_file.IUnknown.Release();
var font_face: *IDWriteFontFace = undefined;
hr = dwrite_factory.?.CreateFontFace(
.TRUETYPE,
1,
@ptrCast(&font_file),
0,
.{},
&font_face,
);
if (hr != HRESULT.S_OK) return error.FailedCreateFontFace;
errdefer _ = font_face.IUnknown.Release();
return .{ .dwrite_factory = dwrite_factory.?, .font_face = font_face };
}
pub fn alphaBufferForOneGlyphRun(self: *Self, allocator: Allocator, font_size_px: f32, glyph_index: u16, baseline_x: f32, baseline_y: f32) !struct { buf: []u8, w: u32, h: u32 } {
const raw_glyph_indices = [_]u16{glyph_index};
const glyph_indices: ?*const u16 = &raw_glyph_indices[0];
const glyph_run = DWRITE_GLYPH_RUN{
.fontFace = self.font_face,
.fontEmSize = font_size_px,
.glyphCount = 1,
.glyphIndices = glyph_indices,
.glyphAdvances = null,
.glyphOffsets = null,
.isSideways = 0,
.bidiLevel = 0,
};
var analysis: ?*IDWriteGlyphRunAnalysis = null;
var hr = self.dwrite_factory.CreateGlyphRunAnalysis(
&glyph_run,
1.0,
null,
.ALIASED,
.NATURAL,
baseline_x,
baseline_y,
@ptrCast(&analysis),
);
if (hr != HRESULT.S_OK) return error.FailedCreateGlyphRun;
defer _ = analysis.?.IUnknown.Release();
var bounds: RECT = undefined;
hr = analysis.?.GetAlphaTextureBounds(.ALIASED_1x1, &bounds);
if (hr != HRESULT.S_OK) return error.FailedGetAlphaTextureBounds;
if (bounds.left >= bounds.right or bounds.top >= bounds.bottom) {
return .{
.buf = try allocator.alloc(u8, 0),
.w = 0,
.h = 0,
};
}
const w: u32 = @intCast(bounds.right - bounds.left);
const h: u32 = @intCast(bounds.bottom - bounds.top);
const buf = try allocator.alloc(u8, w * h);
errdefer allocator.free(buf);
hr = analysis.?.CreateAlphaTexture(
.ALIASED_1x1,
&bounds,
@ptrCast(buf.ptr),
@intCast(buf.len),
);
if (hr != HRESULT.S_OK) return error.FailedCreateAlphaTexture;
// buf is now w * h single-byte alpha coverage, ready to copy into your atlas
return .{ .buf = buf, .w = w, .h = h };
}
pub fn GetGlyphIndices(self: *Self, font_size_px: f32, codepoints: []const u32, glyph_indices: []u16, advances: []f32, bearings_x: []f32, bearings_y: []f32) !void {
var hr = self.font_face.GetGlyphIndices(
codepoints.ptr,
@intCast(codepoints.len),
@ptrCast(glyph_indices.ptr),
);
if (hr != HRESULT.S_OK) return error.FailedGetGlyphIndices;
var design_metrics: [256]DWRITE_GLYPH_METRICS = undefined;
hr = self.font_face.GetDesignGlyphMetrics(
@ptrCast(glyph_indices.ptr),
@intCast(glyph_indices.len),
&design_metrics,
0,
);
if (hr != HRESULT.S_OK) return error.FailedGetDesignGlyphMetrics;
var font_metrics: DWRITE_FONT_METRICS = undefined;
self.font_face.GetMetrics(&font_metrics);
const scale = font_size_px / @as(f32, @floatFromInt(font_metrics.designUnitsPerEm));
for (0..codepoints.len) |i| {
advances[i] = @as(f32, @floatFromInt(design_metrics[i].advanceWidth)) * scale;
bearings_x[i] = @as(f32, @floatFromInt(design_metrics[i].leftSideBearing)) * scale;
bearings_y[i] = @as(f32, @floatFromInt(design_metrics[i].topSideBearing)) * scale;
}
}
};
alphaBufferForOneGlyphRun gives you the rendered character as a byte buffer []u8. But this contains a single channel of alpha values, our font shader will only use the alpha channel, and color will be set manually as a separate input.
GetGlyphIndices gives you the font metrics, like positioning of the character.
Next we will use this Api to render every glyph we want into a font atlas.
const MyFontAtlasRasterizer = struct {
atlas_table: [All_Glyphs.len]GlyphEntry = undefined,
device: *ID3D11Device,
context: *ID3D11DeviceContext,
atlas_tex: *ID3D11Texture2D,
const Atlas_Size: usize = 2048;
const All_Glyphs = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const Self = @This();
fn deinit(self: *Self) void {
_ = self.atlas_tex.IUnknown.Release();
}
fn init(device: *ID3D11Device, context: *ID3D11DeviceContext) !Self {
var atlas_desc = D3D11_TEXTURE2D_DESC{
.Width = Atlas_Size,
.Height = Atlas_Size,
.MipLevels = 1,
.ArraySize = 1,
.Format = DXGI_FORMAT_R8_UNORM,
.SampleDesc = .{ .Count = 1, .Quality = 0 },
.Usage = D3D11_USAGE_DEFAULT,
.BindFlags = .{ .SHADER_RESOURCE = 1 },
.CPUAccessFlags = .{},
.MiscFlags = .{},
};
var atlas_tex: *ID3D11Texture2D = undefined;
const hr = device.CreateTexture2D(&atlas_desc, null, @ptrCast(&atlas_tex));
if (hr != HRESULT.S_OK) return error.CreateGameTextureFailed;
errdefer _ = atlas_tex.IUnknown.Release();
return .{
.atlas_table = undefined,
.context = context,
.atlas_tex = atlas_tex,
.device = device,
};
}
fn buildAtlas(self: *Self, io: std.Io, allocator: Allocator) !MyTexture {
var buf: [std.fs.max_path_bytes]u8 = undefined;
const exePath = try MyAssetsPathLocator.executableDirPath(io, &buf);
const fontPath = try MyAssetsPathLocator.PngPath(allocator, exePath, "GoogleSansFlex_24pt-Light.ttf");
defer allocator.free(fontPath);
const fontU16 = try MyAssetsPathLocator.convertToU16WindowsPath(allocator, fontPath);
defer allocator.free(fontU16);
var font_Factory = try myFont.MyFontFactory.init(fontU16);
var packer: ShelfPack = .{ .atlas_width = Atlas_Size, .atlas_height = Atlas_Size };
const font_size_px: u8 = 64;
const baseline_x: usize = 0;
const baseline_y: usize = 0;
var code_points: [All_Glyphs.len]u32 = undefined;
for (All_Glyphs, 0..) |c, i| {
code_points[i] = @intCast(c);
}
var glyph_indices: [All_Glyphs.len]u16 = undefined;
var advances: [All_Glyphs.len]f32 = undefined;
var bearings_x: [All_Glyphs.len]f32 = undefined;
var bearings_y: [All_Glyphs.len]f32 = undefined;
try font_Factory.GetGlyphIndices(font_size_px, &code_points, &glyph_indices, &advances, &bearings_x, &bearings_y);
for (glyph_indices, 0..) |glyph_index, i| {
const glyphImage = try font_Factory.alphaBufferForOneGlyphRun(
allocator,
font_size_px,
glyph_index,
baseline_x,
baseline_y,
);
defer allocator.free(glyphImage.buf);
const xy = packer.allocate(glyphImage.w, glyphImage.h) orelse return error.IncreaseFontAtlasSize;
self.uploadGlyph(.{ .x = @floatFromInt(xy.x), .y = @floatFromInt(xy.y), .width = @floatFromInt(glyphImage.w), .height = @floatFromInt(glyphImage.h) }, glyphImage.buf);
self.atlas_table[i] = .{
.uv_rect = .{
@as(f32, @floatFromInt(xy.x)),
@as(f32, @floatFromInt(xy.y)),
@as(f32, @floatFromInt(xy.x + glyphImage.w)),
@as(f32, @floatFromInt(xy.y + glyphImage.h)),
},
.bearing_x = bearings_x[i],
.bearing_y = bearings_y[i],
.width = glyphImage.w,
.height = glyphImage.h,
.advance = advances[i],
};
}
return self.CreateTexture();
}
fn uploadGlyph(self: *Self, rect: Rect, buf: []const u8) void {
const box = D3D11_BOX{
.left = @intFromFloat(rect.x),
.top = @intFromFloat(rect.y),
.right = @intFromFloat(rect.x + rect.width),
.bottom = @intFromFloat(rect.y + rect.height),
.front = 0,
.back = 1,
};
self.context.UpdateSubresource(@ptrCast(self.atlas_tex), 0, &box, buf.ptr, @intFromFloat(rect.width), 0);
}
fn CreateTexture(self: *Self) !MyTexture {
var Srv: *ID3D11ShaderResourceView = undefined;
const hr = self.device.CreateShaderResourceView(@ptrCast(self.atlas_tex), null, @ptrCast(&Srv));
if (hr != HRESULT.S_OK) return error.CreateAtlasSRVFailed;
errdefer _ = Srv.IUnknown.Release();
return .{ .Width = Atlas_Size, .Height = Atlas_Size, .Srv = Srv };
}
};
pub const GlyphEntry = struct {
uv_rect: [4]f32,
bearing_x: f32,
bearing_y: f32,
width: u32,
height: u32,
advance: f32,
};
If you have been following carefully, there is a ShelfPacker that packs our glyph boxes into the atlas. Here’s it’s implementation:
atlas_width: u32,
atlas_height: u32,
cursor_x: u32 = 0,
cursor_y: u32 = 0, // top of current shelf
shelf_height: u32 = 0, // tallest glyph placed on current shelf so far
const Self = @This();
pub fn allocate(self: *Self, w: u32, h: u32) ?XY {
if (self.cursor_x + w > self.atlas_width) {
self.cursor_y += self.shelf_height;
self.cursor_x = 0;
self.shelf_height = 0;
}
if (self.cursor_y + h > self.atlas_height) {
return null;
}
const pos: XY = .{ .x = self.cursor_x, .y = self.cursor_y };
self.cursor_x += w;
self.shelf_height = @max(self.shelf_height, h);
return pos;
}
pub const XY = struct { x: u32, y: u32 };
At the start of the program we render each glyph into our font atlas so we can sample each glyph from the texture onto the screen.
Here’s a part of the font shader looks like:
Texture2D atlas_tex : register(t0);
SamplerState samp : register(s0);
float4 PSMain(PSInput input) : SV_TARGET
{
float coverage = atlas_tex.Sample(samp, input.uv).r;
return float4(input.color.rgb, input.color.a * coverage);
}
Now we need a separate MyFontBatcher struct similar to our Sprite Batcher, but uses this font shader instead.
I will show you some relevant parts:
const FontVertex = extern struct {
pos: [3]f32,
uv: [2]f32,
color: [4]f32,
};
This one is drawing individual characters of a text we want to render.
pub fn drawText(self: *Self, texture: *MyTexture, atlas_table: []GlyphEntry, x: f32, y: f32, text: []const u8, color: [4]f32) !void {
var pen_x: f32 = x;
const pen_y: f32 = y;
for (text) |c| {
const i = std.mem.indexOfScalar(u8, MyFontAtlasRasterizer.All_Glyphs, c) orelse return error.MissingCharacter;
const glyph = atlas_table[i];
const dest_rect: Rect = .{ .x = pen_x + glyph.bearing_x, .y = pen_y + glyph.bearing_y, .width = @floatFromInt(glyph.width), .height = @floatFromInt(glyph.height) };
const source_rect: Rect = .{ .x = glyph.uv_rect[0], .y = glyph.uv_rect[1], .width = glyph.uv_rect[2] - glyph.uv_rect[0], .height = glyph.uv_rect[3] - glyph.uv_rect[1] };
try self.drawSprite(texture, source_rect, dest_rect, color);
pen_x += glyph.advance;
}
}
My #1 mistake trying to get this right, was using Vertex struct instead of FontVertex struct. Such that I was writing wrong data into the vertex buffer. Glad I fixed that, and everything works correctly now.
Finally I didn’t consider the case to draw different size font, that will be homework for you and me also. As always you can check out the tagged version of the source code at v0.6.0.
Cheers.