Skip to content

Instantly share code, notes, and snippets.

@etscrivner
Created February 1, 2020 19:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save etscrivner/eaef7fa1f45ba4bdb54c164c6ec9258e to your computer and use it in GitHub Desktop.
Save etscrivner/eaef7fa1f45ba4bdb54c164c6ec9258e to your computer and use it in GitHub Desktop.
Bake SDF font bitmaps using stb libraries.
#include <SDL.h>
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <scaffold/types.h>
#include <scaffold/vec.h>
#include <scaffold/matrix.h>
#include <scaffold/gl.h>
#define STB_RECT_PACK_IMPLEMENTATION
#include "stb_rect_pack.h"
#define STB_TRUETYPE_IMPLEMENTATION
#include "stb_truetype.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// TODO(eric):
// [ ] Fix issue with packing empty rects
#define WINDOW_WIDTH 1920
#define WINDOW_HEIGHT 1080
typedef struct font_metadata_t {
i32 StartChar;
i32 NumChars;
i16 LineHeight;
i16* CharX;
i16* CharY;
i16* CharW;
i16* CharH;
i16* CharXOffset;
i16* CharYOffset;
i16* CharXAdvance;
} font_metadata;
typedef struct font_data_t {
f32 Advance;
i8 XOff;
i8 YOff;
u8 Width;
u8 Height;
u8* Data;
} font_data;
typedef struct game_state_t {
v2 Window;
memory_arena PermanentArena;
memory_arena TransientArena;
f32 SDFSize;
f32 PixelDistScale;
i32 OnEdgeValue;
i32 Padding;
f32 Scale;
f32 AmountChange;
u32 Shader;
vertex_buffers Buffers;
u32 Texture;
textured_vertex Verts[4];
font_data Glyph;
font_data FontData[96];
} game_state;
typedef struct game_input_t {
f32 DeltaTimeSecs;
} game_input;
u32 CompileGlyphShader(memory_arena* Arena);
void PackFontData(game_state* State, const char* FontFilePath) {
u8* FontData = NULL;
stbtt_fontinfo Font;
v2 Dim = {{ 1024, 512 }};
temporary_arena TempArena = BeginTemporaryArena(&State->TransientArena);
// Load font file data from file
{
FILE* FontFile = fopen(FontFilePath, "rb");
fseek(FontFile, 0L, SEEK_END);
umm FontFileSize = ftell(FontFile);
fseek(FontFile, 0L, SEEK_SET);
FontData = ArenaAlloc(&State->PermanentArena, FontFileSize);
fread(FontData, 1, FontFileSize, FontFile);
fclose(FontFile);
stbtt_InitFont(&Font, FontData, 0);
}
u32 StartChar = 32;
u32 NumChars = 96;
u32 NumAvailableChars = 0;
// Extract font glyph data
{
f32 Scale = stbtt_ScaleForPixelHeight(&Font, State->SDFSize);
for (i32 Ch = 0; Ch < NumChars; Ch++) {
int glyph = stbtt_FindGlyphIndex(&Font, StartChar + Ch);
if (glyph == 0) {
State->FontData[Ch].Width = 0;
State->FontData[Ch].Height = 0;
continue;
}
font_data CharData = {};
i32 XOff, YOff, W, H, Advance;
CharData.Data = stbtt_GetCodepointSDF(
&Font, Scale, Ch + StartChar, State->Padding, State->OnEdgeValue, State->PixelDistScale,
&W, &H, &XOff, &YOff
);
stbtt_GetCodepointHMetrics(&Font, Ch + StartChar, &Advance, NULL);
CharData.XOff = XOff;
CharData.YOff = YOff;
CharData.Width = W;
CharData.Height = H;
CharData.Advance = Advance;
State->FontData[Ch] = CharData;
}
}
// Pack glyphs into a single bitmap
{
stbrp_context Context;
stbrp_rect* Rects = (stbrp_rect*)ArenaAlloc(TempArena.Arena, NumChars * sizeof(stbrp_rect));
stbrp_node* Nodes = (stbrp_node*)ArenaAlloc(TempArena.Arena, NumChars * sizeof(stbrp_node));
stbrp_init_target(&Context, Dim.W, Dim.H, Nodes, NumChars);
for (i32 I = 0; I < NumChars; I++) {
int glyph = stbtt_FindGlyphIndex(&Font, StartChar + I);
if (glyph == 0) {
Rects[I].w = Rects[I].h = 0;
continue;
}
Rects[I].w = State->FontData[I].Width;
Rects[I].h = State->FontData[I].Height;
printf("%c (%d - %d)", (char)(StartChar + I), I, 'A' - StartChar);
}
stbrp_pack_rects(&Context, Rects, NumChars);
u8* Bitmap = ArenaAlloc(TempArena.Arena, Dim.W * Dim.H);
// Persist the bitmap to a single-channel PNG
{
u32 StrideBytes = Dim.W;
for (i32 I = 0; I < NumChars; I++) {
stbrp_rect* R = Rects + I;
if (R->was_packed && R->w != 0 && R->h != 0 && State->FontData[I].Data != NULL) {
for (u32 Y = 0; Y < R->h; ++Y) {
u8* Row = &Bitmap[R->x + (R->y+Y)*StrideBytes];
for (u32 X = 0; X < R->w; ++X) {
*Row++ = State->FontData[I].Data[X + Y*R->w];
}
}
}
}
stbi_write_png("test.png", Dim.W, Dim.H, 1, Bitmap, Dim.W);
}
font_metadata FontMetadata = {};
FontMetadata.StartChar = StartChar;
FontMetadata.NumChars = NumChars;
FontMetadata.LineHeight = (i16)State->SDFSize;
FontMetadata.CharX = (i16*)ArenaAlloc(TempArena.Arena, sizeof(i16)*NumChars);
FontMetadata.CharY = (i16*)ArenaAlloc(TempArena.Arena, sizeof(i16)*NumChars);
FontMetadata.CharW = (i16*)ArenaAlloc(TempArena.Arena, sizeof(i16)*NumChars);
FontMetadata.CharH = (i16*)ArenaAlloc(TempArena.Arena, sizeof(i16)*NumChars);
FontMetadata.CharXOffset = (i16*)ArenaAlloc(TempArena.Arena, sizeof(i16)*NumChars);
FontMetadata.CharYOffset = (i16*)ArenaAlloc(TempArena.Arena, sizeof(i16)*NumChars);
FontMetadata.CharXAdvance = (i16*)ArenaAlloc(TempArena.Arena, sizeof(i16)*NumChars);
FILE* MetadataFile = fopen("test.fnt", "wb");
fwrite(&FontMetadata.StartChar, sizeof(i32), 1, MetadataFile);
fwrite(&FontMetadata.NumChars, sizeof(i32), 1, MetadataFile);
fwrite(&FontMetadata.LineHeight, sizeof(i16), 1, MetadataFile);
printf("\n");
for (u32 Index = 0; Index < NumChars; ++Index) {
stbrp_rect* R = Rects + Index;
FontMetadata.CharX[Index] = R->x;
FontMetadata.CharY[Index] = R->y;
FontMetadata.CharW[Index] = R->w;
FontMetadata.CharH[Index] = R->h;
FontMetadata.CharXOffset[Index] = R->w;
FontMetadata.CharYOffset[Index] = R->h;
f32 AdvScale = stbtt_ScaleForPixelHeight(&Font, State->SDFSize);
FontMetadata.CharXAdvance[Index] = AdvScale * State->FontData[Index].Advance;
printf("'%c' (%d - %d)\n", (char)(Index + StartChar), Index, 'A' - StartChar);
printf(" %d %d %d %d %04X %04X\n", R->x, R->y, R->w, R->h, R->x, R->y);
printf(" %d %d %d %d\n",
FontMetadata.CharX[Index], FontMetadata.CharY[Index],
FontMetadata.CharW[Index], FontMetadata.CharH[Index]);
}
fwrite(FontMetadata.CharX, sizeof(i16), NumChars, MetadataFile);
fwrite(FontMetadata.CharY, 1, sizeof(i16)*NumChars, MetadataFile);
fwrite(FontMetadata.CharW, 1, sizeof(i16)*NumChars, MetadataFile);
fwrite(FontMetadata.CharH, 1, sizeof(i16)*NumChars, MetadataFile);
fwrite(FontMetadata.CharXOffset, 1, sizeof(i16)*NumChars, MetadataFile);
fwrite(FontMetadata.CharYOffset, 1, sizeof(i16)*NumChars, MetadataFile);
fwrite(FontMetadata.CharXAdvance, 1, sizeof(i16)*NumChars, MetadataFile);
fclose(MetadataFile);
}
EndTemporaryArena(TempArena);
}
void GameUpdateAndRender(game_state* State, game_input* Input) {
State->Scale += State->AmountChange * Input->DeltaTimeSecs;
if (State->Scale >= 1.50f) {
State->AmountChange = -0.1f;
}
if (State->Scale <= 0.10f) {
State->AmountChange = 0.1f;
}
m4x4 Projection = Orthographic(0, State->Window.W, 0, State->Window.H, -1, 1);
m4x4 Model = ScalingMatrix(State->Scale);
v2 Pos = {{ 0, 0 }};
v2 Dim = {{ (f32)State->Glyph.Width, (f32)State->Glyph.Height }};
glUseProgram(State->Shader);
{
i32 TextureLocation = glGetUniformLocation(State->Shader, "Texture");
glBindTexture(GL_TEXTURE_2D, State->Texture);
glActiveTexture(GL_TEXTURE0 + State->Texture);
glUniform1i(TextureLocation, State->Texture);
i32 ProjectionLocation = glGetUniformLocation(State->Shader, "Projection");
glUniformMatrix4fv(ProjectionLocation, 1, false, (float*)Projection.E);
i32 ModelLocation = glGetUniformLocation(State->Shader, "Model");
glUniformMatrix4fv(ModelLocation, 1, false, (float*)Model.E);
v4 Color = {{ 1.0f, 0.0f, 1.0f, 1.0f }};
textured_vertex* Verts = State->Verts;
for (u32 I = 0; I < ArrayCount(State->Verts); ++I) {
Verts[I].Color = Color;
}
Verts[0].Pos = {{ Pos.X, Pos.Y, 0.0f }};
Verts[1].Pos = {{ Pos.X, Pos.Y + Dim.H, 0.0f }};
Verts[2].Pos = {{ Pos.X + Dim.W, Pos.Y, 0.0f }};
Verts[3].Pos = {{ Pos.X + Dim.W, Pos.Y + Dim.H, 0.0f }};
Verts[0].UV = {{ 0, 0 }};
Verts[1].UV = {{ 0, 1 }};
Verts[2].UV = {{ 1, 0 }};
Verts[3].UV = {{ 1, 1 }};
// Stream our data
glBindBuffer(GL_ARRAY_BUFFER, State->Buffers.VBO);
glBufferSubData(GL_ARRAY_BUFFER, 0, State->Buffers.TotalSize, State->Verts);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(State->Buffers.VAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, State->Buffers.NumItems);
glBindVertexArray(0);
}
glUseProgram(0);
}
int main() {
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Window* Window = SDL_CreateWindow(
"FontBake SDF",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
WINDOW_WIDTH,
WINDOW_HEIGHT,
SDL_WINDOW_OPENGL
);
SDL_GLContext GLContext = SDL_GL_CreateContext(Window);
glewInit();
GLEnableDebugOutput();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
u32 PermanentStorageSize = Megabytes(64);
void* PermanentStorage = calloc(1, PermanentStorageSize);
u32 TransientStorageSize = Megabytes(64);
void* TransientStorage = calloc(1, TransientStorageSize);
game_state State = {};
{
State.PermanentArena = ArenaInit(PermanentStorage, PermanentStorageSize);
State.TransientArena = ArenaInit(TransientStorage, TransientStorageSize);
State.Window = {{ WINDOW_WIDTH, WINDOW_HEIGHT }};
State.SDFSize = 64.0f;
State.PixelDistScale = 10.0f;
State.OnEdgeValue = 128;
State.Padding = 10;
#if 1
const char* FontFileName = "Inconsolata-Regular.ttf";
#else
const char* FontFileName = "manaspc.ttf";
#endif
PackFontData(&State, FontFileName);
}
// Intialize vertex buffers and shaders
{
State.Buffers = CreateVertexBuffers(4, sizeof(textured_vertex), State.Verts);
SetVertexBufferAttrib(&State.Buffers, 0, 3, 0);
SetVertexBufferAttrib(&State.Buffers, 1, 4, sizeof(v3));
SetVertexBufferAttrib(&State.Buffers, 2, 2, sizeof(v3)+sizeof(v4));
State.Shader = CompileGlyphShader(&State.PermanentArena);
State.Scale = 0.1f;
State.AmountChange = 0.1f;
}
// Initialize glyph texture
{
State.Glyph = State.FontData['B' - 32];
glGenTextures(1, &State.Texture);
glActiveTexture(GL_TEXTURE0 + State.Texture);
glBindTexture(GL_TEXTURE_2D, State.Texture);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, State.Glyph.Width, State.Glyph.Height, 0, GL_RED, GL_UNSIGNED_BYTE, State.Glyph.Data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // select linear filtering for minification/magnification
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // select what happens with texture coordinates outside [0,1]
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glHint(GL_GENERATE_MIPMAP_HINT, GL_NICEST);
glGenerateMipmap(GL_TEXTURE_2D);
}
game_input Input = {};
{
}
b32 IsRunning = true;
SDL_Event Event;
u32 LastTime = SDL_GetTicks();
while (IsRunning) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
IsRunning = false;
}
if (Event.type == SDL_KEYDOWN) {
switch (Event.key.keysym.sym) {
case SDLK_ESCAPE:
IsRunning = false;
break;
default:
break;
}
}
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
u32 CurrentTime = SDL_GetTicks();
Input.DeltaTimeSecs = (f32)(CurrentTime - LastTime) / 1000.0f;
GameUpdateAndRender(&State, &Input);
LastTime = CurrentTime;
SDL_GL_SwapWindow(Window);
}
free(PermanentStorage);
free(TransientStorage);
glDeleteProgram(State.Shader);
glDeleteTextures(1, &State.Texture);
DestroyVertexBuffers(&State.Buffers);
for (u32 I = 0; I < ArrayCount(State.FontData); ++I)
{
stbtt_FreeSDF(State.FontData[I].Data, NULL);
}
SDL_GL_DeleteContext(GLContext);
SDL_DestroyWindow(Window);
SDL_Quit();
return 0;
}
u32 CompileGlyphShader(memory_arena* Arena) {
const GLchar* VertexShader = R"END(
#version 330 core
layout (location = 0) in vec3 Position;
layout (location = 1) in vec4 Color;
layout (location = 2) in vec2 TexUV;
uniform mat4 Model;
uniform mat4 Projection;
out vec4 FragmentColor;
out vec2 UV;
void main() {
FragmentColor = Color;
UV = TexUV;
gl_Position = vec4(Position, 1.0) * Model * Projection;
}
)END";
const GLchar* FragmentShader = R"END(
#version 330 core
in vec4 FragmentColor;
in vec2 UV;
uniform sampler2D Texture;
out vec4 ResultingColor;
void main() {
vec2 Pixel = UV;
float Distance = texture(Texture, Pixel).r;
if (Distance >= 0.5f) {
ResultingColor = FragmentColor;
} else {
discard;
}
}
// void main() {
// float Distance = texture(Texture, UV / 1.0).r;
// float SmoothStep = smoothstep(1.0 - 0.6, (1.0 - 0.6) + 0.5, Distance);
// ResultingColor = FragmentColor * SmoothStep;
// ResultingColor.xyz /= ResultingColor.a;
// if(ResultingColor.a < 0.02)
// {
// discard;
// }
// }
)END";
u32 Result = CompileShaders(Arena, VertexShader, FragmentShader);
return(Result);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment