Created
February 1, 2020 19:34
-
-
Save etscrivner/eaef7fa1f45ba4bdb54c164c6ec9258e to your computer and use it in GitHub Desktop.
Bake SDF font bitmaps using stb libraries.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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