Last active
December 1, 2019 20:55
-
-
Save etscrivner/a1da2589796ddff066f454765400d275 to your computer and use it in GitHub Desktop.
Parse and render text from SDF font generated by Hiero.
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/gl.h> | |
#include <scaffold/memory_arena.h> | |
#include <scaffold/matrix.h> | |
#include <scaffold/vec.h> | |
#define STB_IMAGE_IMPLEMENTATION | |
#include "stb_image.h" | |
#define WINDOW_WIDTH 1920 | |
#define WINDOW_HEIGHT 1080 | |
typedef enum tokenizer_state_t { | |
TOKENIZER_STATE_context, | |
TOKENIZER_STATE_key, | |
TOKENIZER_STATE_value, | |
TOKENIZER_STATE_MAX | |
} tokenizer_state; | |
typedef struct property_t { | |
char Name[25]; | |
union { | |
int IntValue; | |
char StrValue[50]; | |
struct { | |
int TupleValue[10]; | |
int TupleValueCount; | |
}; | |
}; | |
struct property_t* Next; | |
} property; | |
typedef struct property_list_t { | |
char Name[25]; | |
// NOTE(eric): Head of a linked list of properties associated with this name. | |
property* Properties; | |
struct property_list_t* Next; | |
} property_list; | |
typedef struct font_data_t { | |
i32 StartChar; | |
i32 NumChars; | |
i16 LineHeight; | |
i16 CharX[96]; | |
i16 CharY[96]; | |
i16 CharW[96]; | |
i16 CharH[96]; | |
i16 CharXOffset[96]; | |
i16 CharYOffset[96]; | |
i16 CharXAdvance[96]; | |
} font_data; | |
void ParseFile(memory_arena* Arena, font_data* FontData, const char* FilePath) { | |
temporary_arena TempArena = BeginTemporaryArena(Arena); | |
FILE* FontFile = fopen(FilePath, "rb"); | |
fseek(FontFile, 0, SEEK_END); | |
umm FileSize = ftell(FontFile); | |
fseek(FontFile, 0, SEEK_SET); | |
char* Data = (char*)ArenaAlloc(TempArena.Arena, FileSize); | |
fread(Data, FileSize, 1, FontFile); | |
fclose(FontFile); | |
tokenizer_state State = TOKENIZER_STATE_context; | |
char* Next = Data; | |
property_list* ListHead = NULL; | |
property_list* Entry = NULL; | |
property_list* LastEntry = NULL; | |
property* Property = NULL; | |
u32 Index = 0; | |
b32 InString = false; | |
while (*Next != '\0') { | |
switch (State) { | |
case TOKENIZER_STATE_context: | |
{ | |
if (!Entry) { | |
Entry = (property_list*)ArenaAlloc(TempArena.Arena, sizeof(property_list)); | |
Entry->Next = NULL; | |
if (!ListHead) { | |
ListHead = Entry; | |
} else { | |
LastEntry->Next = Entry; | |
} | |
} | |
if (isalpha(*Next)) { | |
if (Index < 24) { | |
Entry->Name[Index++] = *Next; | |
} | |
} | |
else if (*Next == ' ') { | |
Entry->Name[Index] = '\0'; | |
Index = 0; | |
Property = (property*)ArenaAlloc(TempArena.Arena, sizeof(property)); | |
Entry->Properties = Property; | |
State = TOKENIZER_STATE_key; | |
} | |
} | |
break; | |
case TOKENIZER_STATE_key: | |
{ | |
if (*Next == '=') { | |
Property->Name[Index] = '\0'; | |
Index = 0; | |
State = TOKENIZER_STATE_value; | |
} else if (isalpha(*Next)) { | |
if (Index < 24) { | |
Property->Name[Index++] = *Next; | |
} | |
} | |
} | |
break; | |
case TOKENIZER_STATE_value: | |
{ | |
if (*Next == ' ' && *(Next+1) != '\n' && !InString) { | |
property* NewProp = (property*)ArenaAlloc(TempArena.Arena, sizeof(property)); | |
Property->Next = NewProp; | |
Property->StrValue[Index] = '\0'; | |
Index = 0; | |
Property = NewProp; | |
State = TOKENIZER_STATE_key; | |
} else if (*Next == '\n') { | |
Property->StrValue[Index] = '\0'; | |
Index = 0; | |
LastEntry = Entry; | |
Entry = NULL; | |
State = TOKENIZER_STATE_context; | |
} else { | |
if (*Next == '"') { | |
InString = !InString; | |
} | |
Property->StrValue[Index++] = *Next; | |
} | |
} | |
break; | |
default: break; | |
} | |
Next++; | |
} | |
LastEntry->Next = NULL; | |
property_list* ListNext = ListHead; | |
int CurrentCh = 0; | |
Index = 0; | |
while (ListNext) { | |
property* NextProp = ListNext->Properties; | |
while (NextProp) { | |
if (strncmp(NextProp->Name, "lineHeight", 25) == 0) { | |
FontData->LineHeight = atoi(NextProp->StrValue); | |
} else if (strncmp(ListNext->Name, "char", 25) == 0) { | |
if (strncmp(NextProp->Name, "id", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
if (Value < 32 || Value > 127) { | |
CurrentCh = 0; | |
} else { | |
CurrentCh = Value - 32; | |
} | |
} | |
else if (strncmp(NextProp->Name, "x", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
FontData->CharX[CurrentCh] = Value; | |
} | |
else if (strncmp(NextProp->Name, "y", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
FontData->CharY[CurrentCh] = Value; | |
} | |
else if (strncmp(NextProp->Name, "width", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
FontData->CharW[CurrentCh] = Value; | |
} | |
else if (strncmp(NextProp->Name, "height", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
FontData->CharH[CurrentCh] = Value; | |
} | |
else if (strncmp(NextProp->Name, "xoffset", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
FontData->CharXOffset[CurrentCh] = Value; | |
} | |
else if (strncmp(NextProp->Name, "yoffset", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
FontData->CharYOffset[CurrentCh] = Value; | |
} | |
else if (strncmp(NextProp->Name, "xadvance", 25) == 0) { | |
int Value = atoi(NextProp->StrValue); | |
FontData->CharXAdvance[CurrentCh] = Value; | |
} | |
} | |
NextProp = NextProp->Next; | |
} | |
ListNext = ListNext->Next; | |
} | |
EndTemporaryArena(TempArena); | |
} | |
typedef struct render_item_t { | |
v4 Source; | |
v3 V0, V1, V2, V3; | |
v4 Color; | |
} render_item; | |
typedef struct game_state_t { | |
u32 Shader; | |
u32 TextureID; | |
vertex_buffers Buffers; | |
font_data FontData; | |
v2 BitmapDim; | |
render_item RenderItem; | |
} game_state; | |
typedef struct game_input_t { | |
f32 DeltaTimeSecs; | |
} game_input; | |
u32 CompileTextShader(memory_arena* Arena); | |
void RenderText(game_state* State, char* Text, v2 Pos, f32 Scale, f32 Delta) { | |
char* Next = Text; | |
i16 LineHeight = State->FontData.LineHeight; | |
f32 XOff = 0.0; | |
m4x4 Projection = Orthographic(0, WINDOW_WIDTH, 0, WINDOW_HEIGHT, -1, 1); | |
f32 Length = (f32)strlen(Text); | |
f32 Index = 1.0f; | |
while (*Next) { | |
int Ch = *Next; | |
if (Ch >= 32 && Ch < 128) { | |
Ch -= 32; | |
v4 LetterSource = {{ State->FontData.CharX[Ch], State->FontData.CharY[Ch], State->FontData.CharW[Ch], State->FontData.CharH[Ch] }}; | |
f32 ChXOff = State->FontData.CharXOffset[Ch]; | |
f32 ChYOff = State->FontData.CharYOffset[Ch]; | |
State->RenderItem.Source = LetterSource; | |
v3 Offset = V3(Pos.X, Pos.Y, 0); | |
State->RenderItem.V0 = Offset + V3(XOff, ChYOff, 0) * Scale; | |
State->RenderItem.V1 = Offset + V3(XOff, ChYOff + LetterSource.W, 0) * Scale; | |
State->RenderItem.V2 = Offset + V3(XOff + LetterSource.Z, ChYOff, 0) * Scale; | |
State->RenderItem.V3 = Offset + V3(XOff + LetterSource.Z, ChYOff + LetterSource.W, 0) * Scale; | |
State->RenderItem.Color = V4(1, 1, 1, 1);//Lerp(V4(0.8, 0.5, 0.8, 1), V4(0.5, 1, 1, 1), Index / Length); | |
Index += 1.0f; | |
XOff += State->FontData.CharXAdvance[Ch] + ChXOff; | |
{ | |
glBindBuffer(GL_ARRAY_BUFFER, State->Buffers.VBO); | |
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(render_item), &State->RenderItem); | |
glBindBuffer(GL_ARRAY_BUFFER, 0); | |
} | |
glUseProgram(State->Shader); | |
glBindVertexArray(State->Buffers.VAO); | |
{ | |
glUniformMatrix4fv(glGetUniformLocation(State->Shader, "Projection"), 1, GL_FALSE, (f32*)Projection.E); | |
u32 TexID = State->TextureID; | |
v2 TexDim = State->BitmapDim; | |
glActiveTexture(GL_TEXTURE0 + TexID); | |
glBindTexture(GL_TEXTURE_2D, TexID); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | |
glUniform1i(glGetUniformLocation(State->Shader, "Texture"), TexID); | |
glUniform2f(glGetUniformLocation(State->Shader, "TextureDim"), TexDim.W, TexDim.H); | |
glUniform1f(glGetUniformLocation(State->Shader, "Delta"), Delta); | |
GLint First = 0; | |
GLsizei Count = 4; | |
GLsizei InstanceCount = 1; | |
glDrawArraysInstanced(GL_TRIANGLE_STRIP, First, Count, InstanceCount); | |
} | |
glBindVertexArray(0); | |
} | |
Next++; | |
} | |
} | |
void GameUpdateAndRender(game_state* State, game_input* Input) { | |
RenderText(State, "QTest hello ThEre! {[(McKk/RrCcdD 1.245!", V2(0, 0), 0.25, 0.20); | |
RenderText(State, "QTest hello ThEre! {[(McKk/RrCcdD 1.245!", V2(0, 10), 0.50, 0.10); | |
RenderText(State, "QTest hello ThEre! {[(McKk/RrCcdD 1.245!", V2(0, 30), 1.0, 0.05); | |
RenderText(State, "QTest hello ThEre! {[(McKk/RrCcdD 1.245!", V2(0, 70), 2.0, 0.03); | |
RenderText(State, "QTest hello ThEre! {[(McKk/RrCcdD 1.245!", V2(0, 140), 5.0, 0.015); | |
} | |
int main() { | |
SDL_Init(SDL_INIT_EVERYTHING); | |
SDL_Window* Window = SDL_CreateWindow( | |
"SDF Font", | |
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); | |
memory_arena PermanentArena = ArenaInit(PermanentStorage, PermanentStorageSize); | |
game_state State = {}; | |
ParseFile(&PermanentArena, &State.FontData, "hacktest.fnt"); | |
// Initialize shaders | |
{ | |
State.Shader = CompileTextShader(&PermanentArena); | |
glGenTextures(1, &State.TextureID); | |
State.Buffers = CreateVertexBuffers(1, sizeof(render_item), &State.RenderItem); | |
SetVertexBufferAttribInstanced(&State.Buffers, 0, 4, 0); | |
SetVertexBufferAttribInstanced(&State.Buffers, 1, 3, sizeof(v4)); | |
SetVertexBufferAttribInstanced(&State.Buffers, 2, 3, sizeof(v3)+sizeof(v4)); | |
SetVertexBufferAttribInstanced(&State.Buffers, 3, 3, 2*sizeof(v3)+sizeof(v4)); | |
SetVertexBufferAttribInstanced(&State.Buffers, 4, 3, 3*sizeof(v3)+sizeof(v4)); | |
SetVertexBufferAttribInstanced(&State.Buffers, 5, 4, 4*sizeof(v3)+sizeof(v4)); | |
int X, Y, N; | |
u8* Data = stbi_load("hacktest.png", &X, &Y, &N, 0); | |
State.BitmapDim = {{ (f32)X, (f32)Y }}; | |
glActiveTexture(GL_TEXTURE0 + State.TextureID); | |
glBindTexture(GL_TEXTURE_2D, State.TextureID); | |
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); | |
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, State.BitmapDim.W, State.BitmapDim.H, 0, GL_RGBA, GL_UNSIGNED_BYTE, 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); | |
stbi_image_free(Data); | |
} | |
game_input Input = {}; | |
u32 LastTime = SDL_GetTicks(); | |
b32 IsRunning = true; | |
SDL_Event Event; | |
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); | |
glDeleteProgram(State.Shader); | |
glDeleteTextures(1, &State.TextureID); | |
DestroyVertexBuffers(&State.Buffers); | |
SDL_GL_DeleteContext(GLContext); | |
SDL_DestroyWindow(Window); | |
SDL_Quit(); | |
return 0; | |
} | |
u32 CompileTextShader(memory_arena* Arena) { | |
const GLchar* VertexShader = R"END( | |
#version 330 core | |
layout (location = 0) in vec4 Source; | |
layout (location = 1) in vec3 V0; | |
layout (location = 2) in vec3 V1; | |
layout (location = 3) in vec3 V2; | |
layout (location = 4) in vec3 V3; | |
layout (location = 5) in vec4 Color; | |
uniform mat4 Projection; | |
out vec4 FragColor; | |
out vec4 FragSource; | |
out vec2 UV; | |
void main() | |
{ | |
vec3 Verts[] = vec3[](V0, V1, V2, V3); | |
vec2 TexUV[] = vec2[](vec2(0, 0), vec2(0, 1), vec2(1, 0), vec2(1, 1)); | |
FragColor = Color; | |
FragSource = Source; | |
UV = TexUV[gl_VertexID]; | |
vec4 WorldSpace = vec4(Verts[gl_VertexID], 1.0); | |
gl_Position = WorldSpace * Projection; | |
} | |
)END"; | |
const GLchar* FragmentShader = R"END( | |
#version 330 core | |
in vec4 FragColor; | |
in vec4 FragSource; | |
in vec2 UV; | |
uniform sampler2D Texture; | |
uniform vec2 TextureDim; | |
uniform float Delta; | |
out vec4 ResultingColor; | |
void main() | |
{ | |
vec2 UVOffset = FragSource.xy; | |
vec2 UVRange = FragSource.zw; | |
vec2 Pixel = (UVOffset + (UV * UVRange)); | |
// Emulate point sampling | |
vec2 SampleUV = floor(Pixel) + vec2(0.5, 0.5); | |
// Subpixel anti-aliasing | |
SampleUV.x += 1.0 - clamp(1.0 - fract(Pixel.x), 0.0, 1.0); | |
SampleUV.y += 1.0 - clamp(1.0 - fract(Pixel.y), 0.0, 1.0); | |
//float Boldness = 0.65; | |
//float Softness = 0.2;//1.0 - Boldness; | |
float Distance = texture(Texture, SampleUV / TextureDim).a; | |
//float SmoothStep = smoothstep(1.0 - Boldness, (1.0 - Boldness) + Softness, Distance); | |
//float Delta = 0.1; | |
float SmoothStep = smoothstep(0.5-Delta, 0.5+Delta, Distance); | |
ResultingColor = vec4(FragColor.rgb, SmoothStep); | |
ResultingColor.rgb /= ResultingColor.a; | |
} | |
)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