Skip to content

Instantly share code, notes, and snippets.

@etscrivner
Last active December 1, 2019 20:55
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/a1da2589796ddff066f454765400d275 to your computer and use it in GitHub Desktop.
Save etscrivner/a1da2589796ddff066f454765400d275 to your computer and use it in GitHub Desktop.
Parse and render text from SDF font generated by Hiero.
#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