📦 GLES3 renderer and demo examples using it

- **Initialize Window**:
  - Successfully created a GLFW window with dimensions 1280x720.
  - Set up window hints for OpenGL version and core profile, enabling multisampling, and enabling depth testing.

- **Setup Renderer**:
  - Initialized the Clay rendering context with a memory arena and dimensions.
  - Set up the measure text and render text functions using stb_image.h and stb_truetype.h.
  - Initialized the GLES3 renderer with 4096 texture units.
  - Loaded a Roboto-Regular font atlas and set it as the default font for rendering.

- **Main Loop**:
  - Called `Clay_UpdateScrollContainers` to handle scroll events.
  - Set the layout dimensions and cleared the color buffer and depth buffer.
  - Render the Clay video demo layout.
  - Swapped the window buffers to display the rendered video.

- **Cleanup**:
  - Cleaned up the GLFW window and renderer resources when the application is closed.

This setup provides a basic framework for rendering videos in GLES3 with GLFW, leveraging stb_image.h for asset loading and Clay for the rendering engine.

- Configure GLFW and SDL2 in the main files
- Fix the video bugs in the main file

🪝 Stb dependency to be managed with cmake in examples

💀 Allow clients to configure headers, also expose Gles3_Renderer through
header-only mode

🧹 Quality of life: automatically set screen dimensions to renderer

Before users had to set them manually

📚 **🎨 Renderers/GLES3:** Improve round-rectangle clipping with uniform border thickness

Implemented improvements to the renderer for GLES3, ensuring better handling of rounded rectangles with borders, making the layout more visually appealing.

- Added two new functions `RenderHeaderButton1`, `RenderHeaderButton2`, and `RenderHeaderButton3` for creating header buttons with different styles.
- Updated the `CreateLayout` function to include these new buttons in the right panel.
- Added a TODO note for handling the outer radius calculation, as it seems to be incorrect in the current implementation.

- Replace `bl_i + B` and `br_i + B` with `bl` and `br` respectively to simplify the code.
- Simplify the logic for checking pixel inside the inner rounded rect by directly using `innerLocal`.

📥 Change borders to be inset

- Fixed incorrect border calculation in the shader.
- Added support for inset borders by adjusting the boundary calculations based on `CLAY_BORDERS_ARE_INSET`.

This change also gives the renderer more choice in handling different border styles.

🏗️ CMake builds for GLES3 renderer examples
This commit is contained in:
Luke 10X 2025-12-12 19:31:50 -05:00
parent 389a044cd2
commit 24b42b7b1c
27 changed files with 2768 additions and 1 deletions

View file

@ -0,0 +1,2 @@
/build/
/website-demo-macos-glfw*

View file

@ -0,0 +1,98 @@
cmake_minimum_required(VERSION 3.27)
project(GLES3_GLFW_video_demo C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
# -------------------------------------------------
# FetchContent
# -------------------------------------------------
include(FetchContent)
set(FETCHCONTENT_QUIET FALSE)
# -------------------------------------------------
# STB (header-only)
# -------------------------------------------------
FetchContent_Declare(
stb
GIT_REPOSITORY https://github.com/nothings/stb.git
GIT_TAG master
)
FetchContent_MakeAvailable(stb)
# -------------------------------------------------
# GLFW
# -------------------------------------------------
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.4
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(glfw)
# Disable examples/tests/docs (important)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
# -------------------------------------------------
# Executable
# -------------------------------------------------
add_executable(GLES3_GLFW_video_demo
main.c
)
target_include_directories(GLES3_GLFW_video_demo
PUBLIC
.
../..
${stb_SOURCE_DIR}
)
# -------------------------------------------------
# Link libraries
# -------------------------------------------------
target_link_libraries(GLES3_GLFW_video_demo
PRIVATE
glfw
)
# -------------------------------------------------
# Platform-specific OpenGL / GLES
# -------------------------------------------------
if(APPLE)
find_library(OPENGL_FRAMEWORK OpenGL)
target_link_libraries(GLES3_GLFW_video_demo PRIVATE ${OPENGL_FRAMEWORK})
# Needed for GLFW on macOS
target_link_libraries(GLES3_GLFW_video_demo
PRIVATE
"-framework Cocoa"
"-framework IOKit"
"-framework CoreVideo"
)
elseif(WIN32)
target_link_libraries(GLES3_GLFW_video_demo PRIVATE opengl32)
elseif(UNIX)
target_link_libraries(GLES3_GLFW_video_demo PRIVATE GL)
endif()
# -------------------------------------------------
# Build flags (kept minimal)
# -------------------------------------------------
if(MSVC)
target_compile_options(GLES3_GLFW_video_demo PRIVATE /W3)
else()
target_compile_options(GLES3_GLFW_video_demo PRIVATE -Wall -Wextra)
endif()
# -------------------------------------------------
# Copy resources
# -------------------------------------------------
add_custom_command(
TARGET GLES3_GLFW_video_demo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/resources
${CMAKE_CURRENT_BINARY_DIR}/resources
)

View file

@ -0,0 +1,33 @@
# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab:
#
# PROGRAM COMPONENTS
#
CXX = emcc
CXXFLAGS = -std=c99
CXXFLAGS += -O0
CXXFLAGS += -I../..
CXXFLAGS += -I./build/_deps/stb-src
LDLIBS += -s USE_ZLIB=1
LDLIBS += -s USE_GLFW=3
LDLIBS += -s FULL_ES2=1 -s USE_WEBGL2=1
LDLIBS += -s ALLOW_MEMORY_GROWTH=1 -s GL_UNSAFE_OPTS=0
LDLIBS += -s STACK_SIZE=2048kb
LDLIBS += -s EXPORTED_FUNCTIONS=['_main']
LDLIBS += -s ASSERTIONS=1 -s SAFE_HEAP=1
LDLIBS += --preload-file $(PWD)/resources/Roboto-Regular.ttf@resources/Roboto-Regular.ttf
main:
mkdir -p build/emscripten
time $(CXX) $(CXXFLAGS) \
$(PWD)/main.c \
$(LDLIBS) -o build/emscripten/index.html
test:
make -f Makefile.emscripten main \
&& (cd build/emscripten && python3 -mhttp.server)
.PHONY: main

View file

@ -0,0 +1,29 @@
# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab:
CXX = clang
CXXFLAGS = -std=c99
CXXFLAGS += -g -O0 -fno-omit-frame-pointer
CXXFLAGS += -ferror-limit=1
CXXFLAGS += -I../..
CXXFLAGS += -I./build/_deps/stb-src
CXXFLAGS += -DGL_SILENCE_DEPRECATION
# GLFW (Homebrew)
CXXFLAGS += -I$(shell brew --prefix glfw)/include
LDLIBS += -L$(shell brew --prefix glfw)/lib -lglfw
# macOS system frameworks (OpenGL needs these)
LDLIBS += -framework OpenGL
LDLIBS += -framework Cocoa
LDLIBS += -framework IOKit
LDLIBS += -framework CoreVideo
main:
mkdir -p build
time $(CXX) $(CXXFLAGS) \
$(PWD)/main.c \
$(LDLIBS) \
-o build/website-demo-macos-glfw

View file

@ -0,0 +1,55 @@
GLES3 Renderer Video Demo (Using GLFW)
======================================
This directory contains a standard Video-Demo example
using work-in-progress GLES3 renderer.
While it still needs refinement, the renderer is already functional and demonstrates the core rendering pipeline.
Current features
- Supports all draw commands except custom.
- In the best-case scenario (no clipping):
- All quad-based commands (Rectangle, Image, Border) are rendered in a single draw call.
- All glyphs belonging to the same font are rendered in one instanced draw call.
- When clipping (scissoring) is used:
- The renderer flushes draw calls before and after each scissor region.
- Supports up to 4 fonts and 4 image textures.
- Image textures may also be used as texture atlases.
- Custom UserData provides per-image UV coordinates, allowing multiple images to share a single OpenGL texture.
- Uses stb_image.h and stb_truetype.h as single-header dependencies for asset loading.
- The loading layer is modular and can be replaced with a different asset pipeline if needed.
Currently builds on:
- Emscripten
- clang++ / macOS
- CMake support is not available yet.
Windowing and platform support
This example uses GLFW, and the renderer is framework agnostic
How to build it with CMake?
---------------------------
Cmake build is the easiest way to build it:
mkdir build
cmake -S . -B ./build
How to build and run on Emscripten:
----------------------------------
For Emscripten the build is a bit custom,
but it depends on CMakeBuild to install header-stb dependency.
So you still need to build it with CMake first.
And then you have to source the Emscripten SDK:
source /path/to/emscripten/emsdk/emsdk_env.sh
Then build it with hand-crafted Makefile.emscripten:
make -f Makefile.emscripten test
and then navigate to http://localhost:8080

View file

@ -0,0 +1,207 @@
#include <stdio.h>
#include <GLFW/glfw3.h>
#define STB_IMAGE_IMPLEMENTATION
#define STB_TRUETYPE_IMPLEMENTATION
#define CLAY_IMPLEMENTATION
#define CLAY_RENDERER_GLES3_IMPLEMENTATION
#include <clay.h>
#include "../../renderers/GLES3/clay_renderer_gles3.h"
#include "../shared-layouts/clay-video-demo.c"
#include "../../renderers/GLES3/clay_renderer_gles3_loader_stb.c"
typedef struct VideoCtx
{
int shouldContinue;
GLFWwindow *glfwWindow;
int screenWidth, screenHeight;
} VideoCtx;
VideoCtx g_ctx;
static int initVideo(VideoCtx *ctx, const int initialWidth, const int initialHeight)
{
if (!glfwInit())
{
fprintf(stderr, "Failed to init GLFW\n");
return 0;
}
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_SAMPLES, 4); // enable multisampling
// NO solution for high DPI yet
glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE);
g_ctx.glfwWindow = glfwCreateWindow(initialWidth, initialHeight, "GLES3 GLFW Video Demo", NULL, NULL);
if (g_ctx.glfwWindow == NULL)
{
fprintf(stderr, "Failed to create GLFW window\n");
glfwTerminate();
return 0;
}
glfwMakeContextCurrent(g_ctx.glfwWindow);
glfwGetWindowSize(g_ctx.glfwWindow, &g_ctx.screenWidth, &g_ctx.screenHeight);
glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight);
printf("Frame buffer size %dx%d\n", g_ctx.screenWidth, g_ctx.screenHeight);
glEnable(GL_BLEND);
// Enables blending, which allows transparent textures to be rendered properly.
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Sets the blending function.
// - `GL_SRC_ALPHA`: Uses the alpha value of the source (texture or color).
// - `GL_ONE_MINUS_SRC_ALPHA`: Makes the destination color blend with the background based on alpha.
// This is commonly used for standard transparency effects.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
// Enables depth testing, ensuring that objects closer to the camera are drawn in front of those farther away.
// This prevents objects from rendering incorrectly based on draw order.
return 1;
}
void My_ErrorHandler(Clay_ErrorData errorData)
{
printf("[ClaY ErroR] %s", errorData.errorText.chars);
}
Stb_FontData g_stbFonts[MAX_FONTS]; // Fonts userData
Gles3_Renderer g_gles3; // The renderer itself
static Clay_Vector2 g_scrollDelta = {0.0f, 0.0f};
static double g_lastTime = 0.0;
static double g_deltaTime = 0.0;
static void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
g_scrollDelta.x += (float)xoffset;
g_scrollDelta.y += (float)yoffset;
}
void init()
{
size_t clayRequiredMemory = Clay_MinMemorySize();
g_gles3.clayMemory = (Clay_Arena){
.capacity = clayRequiredMemory,
.memory = (char *)malloc(clayRequiredMemory),
};
Clay_Context *clayCtx = Clay_Initialize(
g_gles3.clayMemory,
(Clay_Dimensions){
.width = (float)g_ctx.screenWidth,
.height = (float)g_ctx.screenHeight,
},
(Clay_ErrorHandler){
.errorHandlerFunction = My_ErrorHandler,
});
// Note that MeasureText has to be set after the Context is set!
Clay_SetCurrentContext(clayCtx);
Clay_SetMeasureTextFunction(Stb_MeasureText, &g_stbFonts);
// This example uses stb loader, but you can inject your custom loader
// to load Images and Fonts if you don't want to use STB library
Gles3_SetRenderTextFunction(&g_gles3, Stb_RenderText, &g_stbFonts);
Gles3_Initialize(&g_gles3, 4096);
int atlasW = 1024;
int atlasH = 1024;
if (!Stb_LoadFont(
&g_gles3.fontTextures[0],
&g_stbFonts[0],
"resources/Roboto-Regular.ttf",
24.0f, // bake pixel height
atlasW,
atlasH))
abort();
Clay_SetDebugModeEnabled(true);
glfwSetScrollCallback(g_ctx.glfwWindow, scroll_callback);
}
void loop()
{
Clay_Vector2 scrollDelta = {0.0f, 0.0f};
glfwPollEvents();
/* Quit handling */
if (glfwWindowShouldClose(g_ctx.glfwWindow))
{
g_ctx.shouldContinue = false;
}
/* Consume scroll delta (accumulated via callback) */
scrollDelta = g_scrollDelta;
g_scrollDelta.x = 0.0f;
g_scrollDelta.y = 0.0f;
/* Delta time (milliseconds, like your SDL version) */
double now = glfwGetTime(); // seconds
g_deltaTime = (now - g_lastTime) * 1000.0;
g_lastTime = now;
double mouseX = 0.0;
double mouseY = 0.0;
glfwGetCursorPos(g_ctx.glfwWindow, &mouseX, &mouseY);
Clay_Vector2 mousePosition = {
(float)mouseX,
(float)mouseY
};
int mousePressed =
glfwGetMouseButton(g_ctx.glfwWindow, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
Clay_SetPointerState(mousePosition, mousePressed);
Clay_UpdateScrollContainers(
true,
(Clay_Vector2){scrollDelta.x, scrollDelta.y},
g_deltaTime);
glfwGetWindowSize(g_ctx.glfwWindow, &g_ctx.screenWidth, &g_ctx.screenHeight);
glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight);
Clay_SetLayoutDimensions((Clay_Dimensions){(float)g_ctx.screenWidth, (float)g_ctx.screenHeight});
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE); // Clay renderer is simple and never writes to depth buffer
glClearColor(0.1f, 0.2f, 0.1f, 1.0f);
ClayVideoDemo_Data data = ClayVideoDemo_Initialize();
Clay_RenderCommandArray cmds = ClayVideoDemo_CreateLayout(&data);
Gles3_Render(&g_gles3, cmds, g_stbFonts);
glfwSwapBuffers(g_ctx.glfwWindow);
}
// Just initializes and spins the animation loop
int main()
{
initVideo(&g_ctx, 1280, 720);
init();
g_ctx.shouldContinue = true;
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(loop, 0, 1);
#else
while (g_ctx.shouldContinue)
{
loop();
}
#endif
}

View file

@ -0,0 +1,3 @@
/build/
/website-demo-macos-sdl2*
/macos-sidebar-scrolling-container*

View file

@ -0,0 +1,82 @@
cmake_minimum_required(VERSION 3.27)
set(CMAKE_C_STANDARD 99)
project(GLES3_SDL2_sidebar_scrolling_container C)
# -------------------------------------------------
# FetchContent
# -------------------------------------------------
include(FetchContent)
set(FETCHCONTENT_QUIET FALSE)
# -------------------------------------------------
# STB (header-only)
# -------------------------------------------------
FetchContent_Declare(
stb
GIT_REPOSITORY https://github.com/nothings/stb.git
GIT_TAG master
)
FetchContent_MakeAvailable(stb)
# -------------------------------------------------
# SDL2
# -------------------------------------------------
FetchContent_Declare(
SDL2
GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git"
GIT_TAG "release-2.30.10"
GIT_PROGRESS TRUE
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(SDL2)
# -------------------------------------------------
# Executable
# -------------------------------------------------
add_executable(GLES3_SDL2_sidebar_scrolling_container main.c)
target_compile_options(GLES3_SDL2_sidebar_scrolling_container PUBLIC)
target_include_directories(GLES3_SDL2_sidebar_scrolling_container
PUBLIC
. # This renderer
../.. # Clay
${stb_SOURCE_DIR} # STB header only depencency that does not have its own CMake build
)
# -------------------------------------------------
# Link libraries
# -------------------------------------------------
target_link_libraries(GLES3_SDL2_sidebar_scrolling_container PUBLIC
SDL2::SDL2main
SDL2::SDL2-static
)
# -------------------------------------------------
# Platform-specific OpenGL / GLES
# -------------------------------------------------
find_package(SDL2 REQUIRED)
find_library(OPENGL_FRAMEWORK OpenGL)
target_link_libraries(GLES3_SDL2_sidebar_scrolling_container
PRIVATE
${OPENGL_FRAMEWORK}
)
# -------------------------------------------------
# Build flags (kept minimal)
# -------------------------------------------------
if(MSVC)
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
else()
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
endif()
# -------------------------------------------------
# Copy resources
# -------------------------------------------------
add_custom_command(
TARGET GLES3_SDL2_sidebar_scrolling_container POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/resources
${CMAKE_CURRENT_BINARY_DIR}/resources
)

View file

@ -0,0 +1,36 @@
# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab:
#
# PROGRAM COMPONENTS
#
CXX = emcc
CXXFLAGS = -std=c99
CXXFLAGS += -O0
CXXFLAGS += -I../..
CXXFLAGS += -I./build/_deps/stb-src
LDLIBS += -s USE_ZLIB=1
LDLIBS += -s USE_SDL=2
LDLIBS += -s FULL_ES2=1 -s USE_WEBGL2=1
LDLIBS += -s ALLOW_MEMORY_GROWTH=1 -s GL_UNSAFE_OPTS=0
LDLIBS += -s STACK_SIZE=2048kb
LDLIBS += -s EXPORTED_FUNCTIONS=['_main']
LDLIBS += -s ASSERTIONS=1 -s SAFE_HEAP=1
LDLIBS += --preload-file $(PWD)/resources/profile-picture.png@resources/profile-picture.png
LDLIBS += --preload-file $(PWD)/resources/millbank.jpeg@resources/millbank.jpeg
LDLIBS += --preload-file $(PWD)/resources/Roboto-Regular.ttf@resources/Roboto-Regular.ttf
LDLIBS += --preload-file $(PWD)/resources/RobotoMono-Medium.ttf@resources/RobotoMono-Medium.ttf
main:
mkdir -p build/emscripten
time $(CXX) $(CXXFLAGS) \
$(PWD)/main.c \
$(LDLIBS) -o build/emscripten/index.html
test:
make -f Makefile.emscripten main \
&& (cd build/emscripten && python3 -mhttp.server)
.PHONY: main

View file

@ -0,0 +1,28 @@
# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab:
CXX = clang
CXXFLAGS = -std=c99
CXXFLAGS += -g -O0 -fno-omit-frame-pointer
CXXFLAGS += -ferror-limit=1
CXXFLAGS += -I../..
CXXFLAGS += -I./build/_deps/stb-src
CXXFLAGS += -DGL_SILENCE_DEPRECATION
# SDL2 (Homebrew)
CXXFLAGS += -I$(shell brew --prefix sdl2)/include/SDL2
LDLIBS += -L$(shell brew --prefix sdl2)/lib -lSDL2
# macOS system frameworks (OpenGL needs these)
LDLIBS += -framework OpenGL
LDLIBS += -framework Cocoa
LDLIBS += -framework IOKit
LDLIBS += -framework CoreVideo
main:
mkdir -p build
time $(CXX) $(CXXFLAGS) \
$(PWD)/main.c \
$(LDLIBS) \
-o build/macos-sidebar-scrolling-container-sdl2

View file

@ -0,0 +1,57 @@
GLES3 Renderer Scrolling Container (Using SDL2)
===============================================
This directory contains a complete example thatn can me used to test all different draw commands
using work-in-progress GLES3 renderer.
While it still needs refinement, the renderer is already functional and demonstrates the core rendering pipeline.
The images used as resources in this example, namely:
- millbank.jpeg a window with a panoramic city view;
- and profile-picture.png showing objects aligned in a circle;
are taken with my phone camera and are dedicated to the Public Domain.
How to build it with CMake?
---------------------------
Cmake build is the easiest way to build it:
mkdir build
cmake -S . -B ./build
How to build and run on Emscripten:
----------------------------------
For Emscripten the build is a bit custom,
but it depends on CMakeBuild to install header-stb dependency.
So you still need to build it with CMake first.
And then you have to source the Emscripten SDK:
source /path/to/emscripten/emsdk/emsdk_env.sh
Then build it with hand-crafted Makefile.emscripten:
make -f Makefile.emscripten test
and then navigate to http://localhost:8080
How to build and run on Mac:
----------------------------
Requires SDL2:
brew install sdl2
Requires STB:
cmake -B build
Build it with:
make -f Makefile.macos
And run with:
./macos-sidebar-scrolling-container-sdl2

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View file

@ -0,0 +1,2 @@
/build/
/website-demo-macos-sdl2*

View file

@ -0,0 +1,83 @@
cmake_minimum_required(VERSION 3.27)
set(CMAKE_C_STANDARD 99)
project(GLES3_SDL2_video_demo C)
# -------------------------------------------------
# FetchContent
# -------------------------------------------------
include(FetchContent)
set(FETCHCONTENT_QUIET FALSE)
# -------------------------------------------------
# STB (header-only)
# -------------------------------------------------
FetchContent_Declare(
stb
GIT_REPOSITORY https://github.com/nothings/stb.git
GIT_TAG master
)
FetchContent_MakeAvailable(stb)
# -------------------------------------------------
# SDL2
# -------------------------------------------------
FetchContent_Declare(
SDL2
GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git"
GIT_TAG "release-2.30.10"
GIT_PROGRESS TRUE
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(SDL2)
# -------------------------------------------------
# Executable
# -------------------------------------------------
add_executable(GLES3_SDL2_video_demo main.c)
target_compile_options(GLES3_SDL2_video_demo PUBLIC)
target_include_directories(GLES3_SDL2_video_demo
PUBLIC
. # This renderer
../.. # Clay
${stb_SOURCE_DIR} # STB header only depencency that does not have its own CMake build
)
# -------------------------------------------------
# Link libraries
# -------------------------------------------------
target_link_libraries(GLES3_SDL2_video_demo PUBLIC
SDL2::SDL2main
SDL2::SDL2-static
)
# -------------------------------------------------
# Platform-specific OpenGL / GLES
# -------------------------------------------------
find_package(SDL2 REQUIRED)
find_library(OPENGL_FRAMEWORK OpenGL)
target_link_libraries(GLES3_SDL2_video_demo
PRIVATE
${OPENGL_FRAMEWORK}
)
# -------------------------------------------------
# Build flags (kept minimal)
# -------------------------------------------------
if(MSVC)
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
else()
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG}")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
endif()
# -------------------------------------------------
# Copy resources
# -------------------------------------------------
add_custom_command(
TARGET GLES3_SDL2_video_demo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/resources
${CMAKE_CURRENT_BINARY_DIR}/resources
)

View file

@ -0,0 +1,33 @@
# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab:
#
# PROGRAM COMPONENTS
#
CXX = emcc
CXXFLAGS = -std=c99
CXXFLAGS += -O0
CXXFLAGS += -I../..
CXXFLAGS += -I./build/_deps/stb-src
LDLIBS += -s USE_ZLIB=1
LDLIBS += -s USE_SDL=2
LDLIBS += -s FULL_ES2=1 -s USE_WEBGL2=1
LDLIBS += -s ALLOW_MEMORY_GROWTH=1 -s GL_UNSAFE_OPTS=0
LDLIBS += -s STACK_SIZE=2048kb
LDLIBS += -s EXPORTED_FUNCTIONS=['_main']
LDLIBS += -s ASSERTIONS=1 -s SAFE_HEAP=1
LDLIBS += --preload-file $(PWD)/resources/Roboto-Regular.ttf@resources/Roboto-Regular.ttf
main:
mkdir -p build/emscripten
time $(CXX) $(CXXFLAGS) \
$(PWD)/main.c \
$(LDLIBS) -o build/emscripten/index.html
test:
make -f Makefile.emscripten main \
&& (cd build/emscripten && python3 -mhttp.server)
.PHONY: main

View file

@ -0,0 +1,28 @@
# vim: set tabstop=4 shiftwidth=4 expandtab noexpandtab:
CXX = clang
CXXFLAGS = -std=c99
CXXFLAGS += -g -O0 -fno-omit-frame-pointer
CXXFLAGS += -ferror-limit=1
CXXFLAGS += -I../..
CXXFLAGS += -I./build/_deps/stb-src
CXXFLAGS += -DGL_SILENCE_DEPRECATION
# SDL2 (Homebrew)
CXXFLAGS += -I$(shell brew --prefix sdl2)/include/SDL2
LDLIBS += -L$(shell brew --prefix sdl2)/lib -lSDL2
# macOS system frameworks (OpenGL needs these)
LDLIBS += -framework OpenGL
LDLIBS += -framework Cocoa
LDLIBS += -framework IOKit
LDLIBS += -framework CoreVideo
main:
mkdir -p build
time $(CXX) $(CXXFLAGS) \
$(PWD)/main.c \
$(LDLIBS) \
-o build/website-demo-macos-sdl2

View file

@ -0,0 +1,38 @@
GLES3 Renderer Video Demo (Using SDL2)
======================================
This directory contains a standard Video-Demo example
using work-in-progress GLES3 renderer.
While it still needs refinement, the renderer is already functional and demonstrates the core rendering pipeline.
Current features
- Supports all draw commands except custom.
- In the best-case scenario (no clipping):
- All quad-based commands (Rectangle, Image, Border) are rendered in a single draw call.
- All glyphs belonging to the same font are rendered in one instanced draw call.
- When clipping (scissoring) is used:
- The renderer flushes draw calls before and after each scissor region.
- Supports up to 4 fonts and 4 image textures.
- Image textures may also be used as texture atlases.
- Custom UserData provides per-image UV coordinates, allowing multiple images to share a single OpenGL texture.
- Uses stb_image.h and stb_truetype.h as single-header dependencies for asset loading.
- The loading layer is modular and can be replaced with a different asset pipeline if needed.
Currently builds on:
- Emscripten
- clang++ / macOS
- CMake support is not available yet.
Windowing and platform support
This example uses SDL2, but the renderer is framework agnostic.
For sake of example you can also build it with
hand-crafted Makefile.macos
make -f Makefile.emscripten test
and then navigate to http://localhost:8080
On Emscripten it works well.

View file

@ -0,0 +1,198 @@
#include <SDL.h>
#define STB_IMAGE_IMPLEMENTATION
#define STB_TRUETYPE_IMPLEMENTATION
#define CLAY_IMPLEMENTATION
#define CLAY_RENDERER_GLES3_IMPLEMENTATION
#include <clay.h>
#include "../../renderers/GLES3/clay_renderer_gles3.h"
#include "../shared-layouts/clay-video-demo.c"
#include "../../renderers/GLES3/clay_renderer_gles3_loader_stb.c"
typedef struct VideoCtx
{
int shouldContinue;
SDL_Window *sdlWindow;
SDL_GLContext sdlContext;
int screenWidth, screenHeight;
} VideoCtx;
VideoCtx g_ctx;
static int initVideo(VideoCtx *ctx, const int initialWidth, const int initialHeight)
{
SDL_Init(SDL_INIT_VIDEO);
#if defined(__EMSCRIPTEN__)
// OpenGL ES 3 profile
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#else
// Apple MacOs will use it own legacy desktop GL instead
// I know, I lied, I said this was an GLES3
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
g_ctx.sdlWindow = SDL_CreateWindow(
"SDL2 GLES3",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
initialWidth,
initialHeight,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN
);
g_ctx.sdlContext = SDL_GL_CreateContext(g_ctx.sdlWindow);
SDL_ShowWindow(g_ctx.sdlWindow);
SDL_Delay(1);
SDL_GL_GetDrawableSize(g_ctx.sdlWindow, &g_ctx.screenWidth, &g_ctx.screenHeight);
glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight);
glEnable(GL_BLEND);
// Enables blending, which allows transparent textures to be rendered properly.
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Sets the blending function.
// - `GL_SRC_ALPHA`: Uses the alpha value of the source (texture or color).
// - `GL_ONE_MINUS_SRC_ALPHA`: Makes the destination color blend with the background based on alpha.
// This is commonly used for standard transparency effects.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
// Enables depth testing, ensuring that objects closer to the camera are drawn in front of those farther away.
// This prevents objects from rendering incorrectly based on draw order.
return 1;
}
void My_ErrorHandler(Clay_ErrorData errorData)
{
printf("[ClaY ErroR] %s", errorData.errorText.chars);
}
Stb_FontData g_stbFonts[MAX_FONTS]; // Fonts userData
Gles3_Renderer g_gles3; // The renderer itself
Uint64 NOW = 0;
Uint64 LAST = 0;
double deltaTime = 0;
// is executed before everything
void init()
{
size_t clayRequiredMemory = Clay_MinMemorySize();
g_gles3.clayMemory = (Clay_Arena){
.capacity = clayRequiredMemory,
.memory = (char *)malloc(clayRequiredMemory),
};
Clay_Context *clayCtx = Clay_Initialize(
g_gles3.clayMemory,
(Clay_Dimensions){
.width = (float)g_ctx.screenWidth,
.height = (float)g_ctx.screenHeight,
},
(Clay_ErrorHandler){
.errorHandlerFunction = My_ErrorHandler,
});
// Note that MeasureText has to be set after the Context is set!
Clay_SetCurrentContext(clayCtx);
Clay_SetMeasureTextFunction(Stb_MeasureText, &g_stbFonts);
Gles3_SetRenderTextFunction(&g_gles3, Stb_RenderText, &g_stbFonts);
Gles3_Initialize(&g_gles3, 4096);
int atlasW = 1024;
int atlasH = 1024;
if (!Stb_LoadFont(
&g_gles3.fontTextures[0],
&g_stbFonts[0],
"resources/Roboto-Regular.ttf",
24.0f, // bake pixel height
atlasW,
atlasH))
abort();
Clay_SetDebugModeEnabled(true);
}
void loop()
{
glClearColor(0.1f, 0.2f, 0.1f, 1.0f);
Clay_Vector2 scrollDelta = {};
SDL_Event event;
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
{
g_ctx.shouldContinue = false;
}
case SDL_MOUSEWHEEL:
{
scrollDelta.x = event.wheel.x;
scrollDelta.y = event.wheel.y;
break;
}
}
}
LAST = NOW;
NOW = SDL_GetPerformanceCounter();
deltaTime = (double)((NOW - LAST) * 1000 / (double)SDL_GetPerformanceFrequency());
int mouseX = 0;
int mouseY = 0;
Uint32 mouseState = SDL_GetMouseState(&mouseX, &mouseY);
Clay_Vector2 mousePosition = (Clay_Vector2){(float)mouseX, (float)mouseY};
Clay_SetPointerState(mousePosition, mouseState & SDL_BUTTON(1));
Clay_UpdateScrollContainers(
true,
(Clay_Vector2){scrollDelta.x, scrollDelta.y},
deltaTime);
SDL_GL_GetDrawableSize(g_ctx.sdlWindow, &g_ctx.screenWidth, &g_ctx.screenHeight);
glViewport(0, 0, g_ctx.screenWidth, g_ctx.screenHeight);
Clay_SetLayoutDimensions((Clay_Dimensions){(float)g_ctx.screenWidth, (float)g_ctx.screenHeight});
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDisable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE); // Clay renderer is simple and never writes to depth buffer
ClayVideoDemo_Data data = ClayVideoDemo_Initialize();
Clay_RenderCommandArray cmds = ClayVideoDemo_CreateLayout(&data);
Gles3_Render(&g_gles3, cmds, g_stbFonts);
SDL_GL_SwapWindow(g_ctx.sdlWindow);
}
// Just initializes and spins the animation loop
int main()
{
initVideo(&g_ctx, 1280, 720);
init();
g_ctx.shouldContinue = true;
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop(loop, 0, 1);
#else
while (g_ctx.shouldContinue)
{
loop();
}
#endif
}

View file

@ -219,7 +219,7 @@ Clay_RenderCommandArray ClayVideoDemo_CreateLayout(ClayVideoDemo_Data *data) {
SidebarClickData *clickData = (SidebarClickData *)(data->frameArena.memory + data->frameArena.offset);
*clickData = (SidebarClickData) { .requestedDocumentIndex = i, .selectedDocumentIndex = &data->selectedDocumentIndex };
data->frameArena.offset += sizeof(SidebarClickData);
CLAY_AUTO_ID({ .layout = sidebarButtonLayout, .backgroundColor = (Clay_Color) { 120, 120, 120, Clay_Hovered() ? 120 : 0 }, .cornerRadius = CLAY_CORNER_RADIUS(8) }) {
CLAY_AUTO_ID({ .layout = sidebarButtonLayout, .backgroundColor = (Clay_Color) { 120, 120, 120, (float)(Clay_Hovered() ? 120 : 0) }, .cornerRadius = CLAY_CORNER_RADIUS(8) }) {
Clay_OnHover(HandleSidebarInteraction, clickData);
CLAY_TEXT(document.title, CLAY_TEXT_CONFIG({
.fontId = FONT_ID_BODY_16,

View file

@ -0,0 +1,822 @@
#ifndef CLAY_RENDERER_GLES3_H
#define CLAY_RENDERER_GLES3_H
// There may be custom header customizations, very client specific
// let client indicate that they manage headers by setting GLSL_VERSION
#ifndef GLSL_VERSION
#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#include <emscripten/html5.h>
#include <GLES3/gl3.h>
#define GLSL_VERSION "#version 300 es"
#else
// Only apple computers now sorry
// That means it is not really GLES3 but desktop OpenGL 3
// Luckily, it is compatible with GLES3
#include <OpenGL/gl3.h>
#define GLSL_VERSION "#version 330 core"
#endif
#endif
#define MAX_IMAGES 4
#define MAX_FONTS 4
/*
* Instanced rendering for Rects/Images/Borders
* will use this data
* Note, it needs to be padded to 4 floats
* Draws:
* - One rectangular with possibly rounded corner
* - And possibly with a hole inside (with rounded edges too, if corners are rounded)
* - It could also draw a picture with alsoe rounded corner
*/
typedef struct RectInstance
{
float x, y, w, h; // 4 Draw where on screen
float u0, v0, u1, v1; // 4 Atlas region
float r, g, b, a; // 4 Color
float radiusTL, radiusTR; // 2 Corner rounding
float radiusBL, radiusBR; // 2
float borderL, borderR; // 2 Border widths
float borderT, borderB; // 2
float texToUse; // 1 Texture atlas to take an image from (1-4)
float pad[3]; // 3
} RectInstance;
/*
* Struct for glyph instanced rendering
* Each glyph consists of 6 vertexes (to make 2 triangle of a quad)
*/
typedef struct GlyphVtx
{
float x, y; // To draw Where
float u, v; // To draw What
float r, g, b, a; // Text color
float atlasTexUnit; // Shader will have all samples loaded but this will point which to use
float pad[3]; // 3
} GlyphVtx;
typedef struct Gles3_GlyphVtxArray
{
GlyphVtx *instData;
int capacity;
int count;
} Gles3_GlyphVtxArray;
typedef struct Gles3_QuadInstanceArray
{
RectInstance *instData; // packed per-instance floats
int capacity; // how many instances it can hold
int count; // how many instances does it actually hold
} Gles3_QuadInstanceArray;
typedef struct Gles3_ImageConfig
{
int textureToUse;
float u0, v0;
float u1, v1;
} Gles3_ImageConfig;
#ifndef CLAY_RENDERER_GLES3_IMPLEMENTATION
typedef struct Gles3_Renderer Gles3_Renderer;
#endif
#ifdef CLAY_RENDERER_GLES3_IMPLEMENTATION
#include <math.h>
#include <clay.h>
#include <stdlib.h>
#include "clay_renderer_gles3.h"
enum
{
ATTR_QUAD_POS = 0,
ATTR_QUAD_RECT = 1,
ATTR_QUAD_COLOR = 2,
ATTR_QUAD_UV = 3,
ATTR_QUAD_RAD = 4,
ATTR_QUAD_BORDER = 5,
ATTR_QUAD_TEX = 6,
};
enum
{
ATTR_GLYPH_POS = 0,
ATTR_GLYPH_UV = 1,
ATTR_GLYPH_COLOR = 2,
ATTR_GLYPH_TEX = 3,
};
/*
* rendering
*/
const char *GLES3_QUAD_VERTEX_SHADER =
GLSL_VERSION
"\n"
"precision mediump float;\n"
"layout(location = 0) in vec2 aPos; // unit quad (0..1)\n"
"layout(location = 1) in vec4 aRect; // x,y,w,h (pixels)\n"
"layout(location = 3) in vec4 aUV; // u0,v0,u1,v1\n"
"layout(location = 2) in vec4 aColor; // rgba\n"
"layout(location = 4) in vec4 aCornerRadii;\n"
"layout(location = 5) in vec4 aBorderWidths;\n"
"layout(location = 6) in float aTexSlot;\n"
"uniform vec2 uScreen; // screen size in pixels\n"
"out vec2 vPos;\n"
"out vec4 vRect;\n"
"out vec4 vColor;\n"
"out vec2 vUV;\n"
"out vec4 vCornerRadii;\n"
"out vec4 vBorderWidths;\n"
"out float vTexSlot;\n"
"void main() {\n"
" vec2 pos = vec2(aPos.x * aRect.z + aRect.x, aPos.y * aRect.w + aRect.y);\n"
" vec2 ndc = pos / uScreen * 2.0 - 1.0; // ndc.y increases up; pos y increases down (we will inve\n"
" ndc.y = -ndc.y;\n"
" gl_Position = vec4(ndc, 0.0, 1.0);\n"
" vPos = aPos;\n"
" vRect = aRect;\n"
" vColor = aColor;\n"
" vUV = mix(aUV.xy, aUV.zw, aPos);\n"
" vCornerRadii = aCornerRadii;\n"
" vBorderWidths = aBorderWidths;\n"
" vTexSlot = aTexSlot;\n"
"}\n";
const char *GLES3_QUAD_FRAGMENT_SHADER =
GLSL_VERSION
"\n"
"precision mediump float;\n"
"in vec2 vPos;\n"
"in vec4 vRect;\n"
"in vec4 vColor;\n"
"in vec2 vUV;\n"
"in vec4 vCornerRadii;\n"
"in vec4 vBorderWidths;\n"
"in float vTexSlot;\n"
"uniform sampler2D uTex0;\n"
"uniform sampler2D uTex1;\n"
"uniform sampler2D uTex2;\n"
"uniform sampler2D uTex3;\n"
"out vec4 frag;\n"
"void main() {\n"
" // Pixel coordinates in pixel space\n"
" vec2 pix = vRect.xy + vPos * vRect.zw;\n"
" float x0 = vRect.x;\n"
" float y0 = vRect.y;\n"
" float w = vRect.z;\n"
" float h = vRect.w;\n"
" // Local position inside the rectangle (0..w, 0..h)\n"
" vec2 local = pix - vec2(x0, y0);\n"
" // Original corner radii\n"
" float tl = vCornerRadii.x;\n"
" float tr = vCornerRadii.y;\n"
" float bl = vCornerRadii.z;\n"
" float br = vCornerRadii.w;\n"
" // Border thicknesses\n"
" float L = vBorderWidths.x;\n"
" float R = vBorderWidths.y;\n"
" float T = vBorderWidths.z;\n"
" float B = vBorderWidths.w;\n"
" bool CLAY_BORDERS_ARE_INSET = true; // it is true\n"
" bool isBorder = (L > 0.0 || R > 0.0 || T > 0.0 || B > 0.0);\n"
" float outerAlpha = 1.0;\n"
" // If it is not a border but rect or image, then it only has outer border what is provided\n"
" // Otherwise it increases the outter border, but the provided borde is the ineer border\n"
" // I think is better not increase rounding radius when that radius is smaller than border thickness\n"
" float outter_tl;\n"
" float outter_tr;\n"
" float outter_bl;\n"
" float outter_br;\n"
" if (CLAY_BORDERS_ARE_INSET) {\n"
" // Actural behaviour\n"
" outter_tl = tl;\n"
" outter_tr = tr;\n"
" outter_bl = bl;\n"
" outter_br = br;\n"
" tl = (tl > min(T, L)) ? tl - min(T, L) : tl;\n"
" tr = (tr > min(T, R)) ? tr - min(T, R) : tr;\n"
" bl = (bl > min(B, L)) ? bl - min(B, L) : bl;\n"
" br = (br > min(B, R)) ? br - min(B, R) : br;\n"
" } else {\n"
" // Hypothetical behaviour\n"
" outter_tl = (tl > min(T, L)) ? tl + min(T, L) : tl;\n"
" outter_tr = (tr > min(T, R)) ? tr + min(T, R) : tr;\n"
" outter_bl = (bl > min(B, L)) ? bl + min(B, L) : bl;\n"
" outter_br = (br > min(B, R)) ? br + min(B, R) : br;\n"
" }\n"
" if (outter_tl > 0.0 && local.x < outter_tl && local.y < outter_tl)\n"
" outerAlpha = step(length(local - vec2(outter_tl, outter_tl)), outter_tl);\n"
" if (outter_tr > 0.0 && local.x > w - outter_tr && local.y < outter_tr)\n"
" outerAlpha *= step(length(local - vec2(w - outter_tr, outter_tr)), outter_tr);\n"
" if (outter_bl > 0.0 && local.x < outter_bl && local.y > h - outter_bl)\n"
" outerAlpha *= step(length(local - vec2(outter_bl, h - outter_bl)), outter_bl);\n"
" if (outter_br > 0.0 && local.x > w - outter_br && local.y > h - outter_br)\n"
" outerAlpha *= step(length(local - vec2(w - outter_br, h - outter_br)), outter_br);\n"
" if (outerAlpha < 0.5)\n"
" discard;\n"
" // -------- Border logic --------\n"
" if (isBorder) {\n"
" float iw = w - L - R;\n"
" float ih = h - T - B;\n"
" vec2 innerLocal = local - vec2(L, T);\n"
" // Check if pixel is inside inner rounded rect\n"
" bool insideInner = true;\n"
" if (tl > 0.0 && innerLocal.x < tl && innerLocal.y < tl)\n"
" insideInner = (length(innerLocal - vec2(tl, tl)) <= tl);\n"
" if (tr > 0.0 && innerLocal.x > iw - tr && innerLocal.y < tr)\n"
" insideInner = insideInner && (length(innerLocal - vec2(iw - tr, tr)) <= tr);\n"
" // Bottom-left\n"
" if (bl> 0.0 && innerLocal.x < bl && innerLocal.y > ih - bl) \n"
" insideInner = insideInner && (length(innerLocal - vec2(bl, ih - bl)) <= bl);\n"
" // Bottom-right\n"
" if (br > 0.0 && innerLocal.x > iw - br && innerLocal.y > ih - br)\n"
" insideInner = insideInner && (length(innerLocal - vec2(iw - br, ih - br)) <= br);\n"
" // Discard pixels inside inner rounded rect\n"
" if (insideInner && innerLocal.x >= 0.0 && innerLocal.x <= iw && innerLocal.y >= 0.0 && innerLocal.y <= ih)\n"
" discard;\n"
" frag = vColor;\n"
" return;\n"
" }\n"
" // -------- Non-border rectangle or image --------\n"
" if (vTexSlot < 0.0) {\n"
" frag = vColor;\n"
" } else {\n"
" int slot = int(vTexSlot + 0.5);\n"
" if (slot == 0) frag = texture(uTex0, vUV);\n"
" if (slot == 1) frag = texture(uTex1, vUV);\n"
" if (slot == 2) frag = texture(uTex2, vUV);\n"
" if (slot == 3) frag = texture(uTex3, vUV);\n"
" }\n"
"}\n";
const char *GLES3_TEXT_VERTEX_SHADER =
GLSL_VERSION
"\n"
"precision mediump float;\n"
"layout(location = 0) in vec2 aPos;\n"
"layout(location = 1) in vec2 aUV;\n"
"layout(location = 2) in vec4 aColor;\n"
"layout(location = 3) in float aTexSlot;\n"
"uniform vec2 uScreen;\n"
"out vec2 vUV;\n"
"out vec4 vColor;\n"
"out float vTexSlot;\n"
"void main() {\n"
" vec2 ndc = (aPos / uScreen) * 2.0 - 1.0;\n"
" gl_Position = vec4(ndc * vec2(1.0, -1.0), 0.0, 1.0);\n"
" vUV = aUV;\n"
" vColor = aColor;\n"
" vTexSlot = aTexSlot;\n"
"}\n";
const char *GLES3_TEXT_FRAGMENT_SHADER =
GLSL_VERSION
"\n"
"precision mediump float;\n"
"in vec2 vUV;\n"
"in vec4 vColor;\n"
"in float vTexSlot;\n"
"uniform sampler2D uTex0;\n"
"uniform sampler2D uTex1;\n"
"uniform sampler2D uTex2;\n"
"uniform sampler2D uTex3;\n"
"out vec4 fragColor;\n"
"void main() {\n"
" int slot = int(vTexSlot + 0.5);\n"
" float coverage;\n"
" if (slot == 0) coverage = texture(uTex0, vUV).r;\n"
" if (slot == 1) coverage = texture(uTex1, vUV).r;\n"
" if (slot == 2) coverage = texture(uTex2, vUV).r;\n"
" if (slot == 3) coverage = texture(uTex3, vUV).r;\n"
" fragColor = vec4(vColor.rgb, vColor.a * coverage);\n"
"} \n";
/**
* This renderer accumulates all quads and glyphs of every draw coommand
* in their array, and flushes them in just 2 instanced draw calls to OpenGL
*/
typedef struct Gles3_Renderer
{
Clay_Arena clayMemory;
// It is super important keep track on the performance of this renderer:
uint64_t totalDrawCallsToOpenGl;
float screenWidth;
float screenHeight;
/* Quads rendering */
GLuint quadVAO;
GLuint quadVBO;
GLuint quadInstanceVBO;
GLuint quadShaderId;
GLuint imageTextures[MAX_IMAGES];
Gles3_QuadInstanceArray quadInstanceArray; // Each instance is one quad
/* Fonts rendering */
GLuint textVAO;
GLuint textVBO;
GLuint textShader;
GLuint fontTextures[MAX_FONTS];
Gles3_GlyphVtxArray glyphVtxArray; // Instance data: every vertex is an element,
// 6 elements per each instance
// Text renderer is delegated to external function, which is supposed
// to add glyph data based on passed render text command
void (*renderTextFunction)(
Clay_RenderCommand *cmd, // Will be always of CLAY_RENDER_COMMAND_TYPE_TEXT
Gles3_GlyphVtxArray *accum, // 6 vertices need to be added to this array
void *userData // Fonts pallete
);
} Gles3_Renderer;
static GLuint Gles3__CompileShader(GLenum type, const char *source)
{
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
char infoLog[512];
glGetShaderInfoLog(shader, 512, NULL, infoLog);
printf("ERROR::SHADER::COMPILATION_FAILED\n");
printf("SHADER SOURCE:\n%s\n", source);
printf("SHADER TYPE: ");
if (type == GL_VERTEX_SHADER)
printf("Vertex Shader");
else if (type == GL_FRAGMENT_SHADER)
printf("Fragment Shader");
else
printf("Unknown");
printf("\nSHADER COMPILATION ERROR:\n%s\n", infoLog);
abort();
}
return shader;
}
GLuint Gles3__CreateShaderProgram(
const char *vertexShaderSource,
const char *fragmentShaderSource)
{
GLuint vertexShader =
Gles3__CompileShader(GL_VERTEX_SHADER, vertexShaderSource);
GLuint fragmentShader =
Gles3__CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return shaderProgram;
}
void Gles3_Initialize(Gles3_Renderer *renderer, int maxInstances)
{
renderer->totalDrawCallsToOpenGl = 0;
// compile shader
renderer->quadShaderId = Gles3__CreateShaderProgram(
GLES3_QUAD_VERTEX_SHADER, GLES3_QUAD_FRAGMENT_SHADER);
glUseProgram(renderer->quadShaderId);
glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex0"), 0);
glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex1"), 1);
glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex2"), 2);
glUniform1i(glGetUniformLocation(renderer->quadShaderId, "uTex3"), 3);
// create unit quad VBO (0..1)
const float quadVerts[8] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
glGenVertexArrays(1, &renderer->quadVAO);
glBindVertexArray(renderer->quadVAO);
glGenBuffers(1, &renderer->quadVBO);
glBindBuffer(GL_ARRAY_BUFFER, renderer->quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
// attribute 0: aPos (vec2), per-vertex
glEnableVertexAttribArray(ATTR_QUAD_POS);
glVertexAttribPointer(ATTR_QUAD_POS, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);
glVertexAttribDivisor(ATTR_QUAD_POS, 0);
// create instance buffer big enough
Gles3_QuadInstanceArray *quads = &renderer->quadInstanceArray;
quads->capacity = maxInstances;
quads->instData =
(RectInstance *)malloc(sizeof(RectInstance) * quads->capacity);
quads->count = 0;
glGenBuffers(1, &renderer->quadInstanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, renderer->quadInstanceVBO);
glBufferData(GL_ARRAY_BUFFER,
sizeof(RectInstance) * quads->capacity,
NULL,
GL_DYNAMIC_DRAW);
// set up instance attributes
GLsizei stride = sizeof(RectInstance);
glEnableVertexAttribArray(ATTR_QUAD_RECT);
glVertexAttribPointer(ATTR_QUAD_RECT, 4, GL_FLOAT, GL_FALSE,
stride, (void *)offsetof(RectInstance, x));
glVertexAttribDivisor(ATTR_QUAD_RECT, 1);
glEnableVertexAttribArray(ATTR_QUAD_COLOR);
glVertexAttribPointer(ATTR_QUAD_COLOR, 4, GL_FLOAT, GL_FALSE,
stride, (void *)offsetof(RectInstance, r));
glVertexAttribDivisor(ATTR_QUAD_COLOR, 1);
glEnableVertexAttribArray(ATTR_QUAD_UV);
glVertexAttribPointer(ATTR_QUAD_UV, 4, GL_FLOAT, GL_FALSE,
stride, (void *)offsetof(RectInstance, u0));
glVertexAttribDivisor(ATTR_QUAD_UV, 1);
glEnableVertexAttribArray(ATTR_QUAD_RAD);
glVertexAttribPointer(ATTR_QUAD_RAD, 4, GL_FLOAT, GL_FALSE,
stride, (void *)offsetof(RectInstance, radiusTL));
glVertexAttribDivisor(ATTR_QUAD_RAD, 1);
glEnableVertexAttribArray(ATTR_QUAD_BORDER);
glVertexAttribPointer(ATTR_QUAD_BORDER, 4, GL_FLOAT, GL_FALSE,
stride, (void *)offsetof(RectInstance, borderL));
glVertexAttribDivisor(ATTR_QUAD_BORDER, 1);
glEnableVertexAttribArray(ATTR_QUAD_TEX);
glVertexAttribPointer(ATTR_QUAD_TEX, 1, GL_FLOAT, GL_FALSE,
stride, (void *)offsetof(RectInstance, texToUse));
glVertexAttribDivisor(ATTR_QUAD_TEX, 1);
glBindVertexArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
// Ok now we will initialize text!
Gles3_GlyphVtxArray *gVerts = &renderer->glyphVtxArray;
// configure capacity
gVerts->capacity = maxInstances;
gVerts->count = 0;
// allocate CPU-side vertex buffer: 6 vertices per glyph
gVerts->instData = (GlyphVtx *)malloc(sizeof(GlyphVtx) * 6 * gVerts->capacity);
if (!gVerts->instData)
{
fprintf(stderr, "Failed to allocate glyph_vertices\n");
gVerts->capacity = 0;
}
// create VAO/VBO for text rendering
glGenVertexArrays(1, &renderer->textVAO);
glBindVertexArray(renderer->textVAO);
glGenBuffers(1, &renderer->textVBO);
glBindBuffer(GL_ARRAY_BUFFER, renderer->textVBO);
glBufferData(GL_ARRAY_BUFFER,
sizeof(GlyphVtx) * 6 * gVerts->capacity,
NULL,
GL_DYNAMIC_DRAW);
GLsizei gv_stride = sizeof(GlyphVtx);
glEnableVertexAttribArray(ATTR_GLYPH_POS);
glVertexAttribPointer(ATTR_GLYPH_POS, 2, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, x)));
glEnableVertexAttribArray(ATTR_GLYPH_UV);
glVertexAttribPointer(ATTR_GLYPH_UV, 2, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, u)));
glEnableVertexAttribArray(ATTR_GLYPH_COLOR);
glVertexAttribPointer(ATTR_GLYPH_COLOR, 4, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, r)));
glEnableVertexAttribArray(ATTR_GLYPH_TEX);
glVertexAttribPointer(ATTR_GLYPH_TEX, 1, GL_FLOAT, GL_FALSE, gv_stride, (void *)(offsetof(GlyphVtx, atlasTexUnit)));
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
renderer->textShader = Gles3__CreateShaderProgram(
GLES3_TEXT_VERTEX_SHADER, GLES3_TEXT_FRAGMENT_SHADER);
glUseProgram(renderer->textShader);
// Link sampler uniforms in the text shader to the correct texture units.
// Each uniform tells the shader which unit to read from.
glUniform1i(glGetUniformLocation(renderer->textShader, "uTex0"), 0);
glUniform1i(glGetUniformLocation(renderer->textShader, "uTex1"), 1);
glUniform1i(glGetUniformLocation(renderer->textShader, "uTex2"), 2);
glUniform1i(glGetUniformLocation(renderer->textShader, "uTex3"), 3);
}
void Gles3_SetRenderTextFunction(
Gles3_Renderer *renderer,
void (*renderTextFunction)(
Clay_RenderCommand *cmd, Gles3_GlyphVtxArray *accum, void *userData),
void *userData)
{
renderer->renderTextFunction = renderTextFunction;
}
void Gles3_Render(
Gles3_Renderer *renderer,
Clay_RenderCommandArray cmds,
void *userData // eg. fonts
)
{
Clay_Dimensions layoutDimensions = Clay_GetCurrentContext()->layoutDimensions;
renderer->screenWidth = layoutDimensions.width;
renderer->screenHeight = layoutDimensions.height;
Gles3_QuadInstanceArray *quads = &renderer->quadInstanceArray;
Gles3_GlyphVtxArray *gVerts = &renderer->glyphVtxArray;
gVerts->count = 0;
for (int i = 0; i < cmds.length; i++)
{
Clay_RenderCommand *cmd = Clay_RenderCommandArray_Get(&cmds, i);
Clay_BoundingBox boundingBox = (Clay_BoundingBox){
.x = roundf(cmd->boundingBox.x),
.y = roundf(cmd->boundingBox.y),
.width = roundf(cmd->boundingBox.width),
.height = roundf(cmd->boundingBox.height),
};
bool scissorChanged = false;
switch (cmd->commandType)
{
case CLAY_RENDER_COMMAND_TYPE_TEXT:
{
renderer->renderTextFunction(
cmd,
&renderer->glyphVtxArray,
userData);
break;
}
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE:
case CLAY_RENDER_COMMAND_TYPE_IMAGE:
{
Clay_RectangleRenderData *config = &cmd->renderData.rectangle;
Clay_Color c = config->backgroundColor;
// Convert to float 0..1
float rf = c.r / 255.0f;
float gf = c.g / 255.0f;
float bf = c.b / 255.0f;
float af = c.a / 255.0f;
bool isImage = cmd->commandType == CLAY_RENDER_COMMAND_TYPE_IMAGE;
// Ensure we don't overflow the capacity
if (quads->count >= quads->capacity)
{
printf("Clay renderer: instance overflow!\n");
break;
}
int idx = quads->count;
RectInstance *dst = &quads->instData[idx];
dst->x = boundingBox.x;
dst->y = boundingBox.y;
dst->w = boundingBox.width;
dst->h = boundingBox.height;
if (isImage)
{
Gles3_ImageConfig *imgConf = (Gles3_ImageConfig *)cmd->renderData.image.imageData;
dst->u0 = imgConf->u0;
dst->v0 = imgConf->v0;
dst->u1 = imgConf->u1;
dst->v1 = imgConf->v1;
dst->texToUse = (float)imgConf->textureToUse;
}
else
{
dst->u0 = dst->v0 = 0.0f;
dst->u1 = dst->v1 = 1.0f;
dst->texToUse = -1.0f; // This means no image, use albedo color
}
// colour
dst->r = rf;
dst->g = gf;
dst->b = bf;
dst->a = af;
// corner radii
Clay_CornerRadius r = config->cornerRadius;
dst->radiusTL = r.topLeft;
dst->radiusTR = r.topRight;
dst->radiusBL = r.bottomLeft;
dst->radiusBR = r.bottomRight;
dst->borderT = 0.0f;
dst->borderR = 0.0f;
dst->borderB = 0.0f;
dst->borderL = 0.0f;
quads->count++;
break;
}
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START:
{
scissorChanged = true;
break;
}
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END:
{
scissorChanged = true;
break;
}
case CLAY_RENDER_COMMAND_TYPE_BORDER:
{
Clay_BorderRenderData *br = &cmd->renderData.border;
float rf = br->color.r / 255.0f;
float gf = br->color.g / 255.0f;
float bf = br->color.b / 255.0f;
float af = br->color.a / 255.0f;
float x = boundingBox.x;
float y = boundingBox.y;
float w = boundingBox.width;
float h = boundingBox.height;
float top = br->width.top;
float bottom = br->width.bottom;
float left = br->width.left;
float right = br->width.right;
int idx = quads->count;
RectInstance *dst = &quads->instData[idx];
dst->x = x - left;
dst->y = y - top;
dst->w = w + right;
dst->h = h + bottom;
dst->borderB = bottom;
dst->borderL = left;
dst->borderT = top;
dst->borderR = right;
// Clay borders are inset, but adding support to outset borders
// Is as easy as this + some minor changes in shader too
bool CLAY_BORDERS_ARE_INSET = true;
if (CLAY_BORDERS_ARE_INSET)
{
// Normal behaviour
dst->x = x;
dst->y = y;
dst->w = w;
dst->h = h;
}
else
{
// Hypotethical behaviour, if the borders were outside
dst->x = x - left;
dst->y = y - top;
dst->w = w + left + right;
dst->h = h + top + bottom;
}
dst->u0 = 0.0f;
dst->v0 = 0.0f;
dst->u1 = 1.0f;
dst->v1 = 1.0f;
dst->r = rf;
dst->g = gf;
dst->b = bf;
dst->a = af;
dst->radiusTL = br->cornerRadius.topLeft;
dst->radiusTR = br->cornerRadius.topRight;
dst->radiusBR = br->cornerRadius.bottomRight;
dst->radiusBL = br->cornerRadius.bottomLeft;
dst->texToUse = -1.0f;
quads->count++;
break;
}
case CLAY_RENDER_COMMAND_TYPE_CUSTOM:
{
// printf("Unhandled clay cmd: custom\n");
break;
}
default:
{
printf("Error: unhandled render command\n");
exit(1);
}
}
// Flush draw calls if scissors about to change in this iteration
if (i == cmds.length - 1 || scissorChanged)
{
scissorChanged = false;
// Render Recatangles and Images
if (quads->count > 0)
{
glUseProgram(renderer->quadShaderId);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[0]);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[1]);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[2]);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, renderer->imageTextures[3]);
// set uniforms
GLint locScreen = glGetUniformLocation(renderer->quadShaderId, "uScreen");
glUniform2f(locScreen,
(float)renderer->screenWidth,
(float)renderer->screenHeight);
glBindVertexArray(renderer->quadVAO);
// upload all instances at once
glBindBuffer(GL_ARRAY_BUFFER, renderer->quadInstanceVBO);
// rectangles are solid colour — disable atlas use
glBufferSubData(GL_ARRAY_BUFFER,
0,
quads->count * sizeof(RectInstance),
quads->instData);
// draw unit quad (4 verts) instanced
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, quads->count);
renderer->totalDrawCallsToOpenGl += 1;
glBindVertexArray(0);
glUseProgram(0);
}
// Clrear instance arrays, as they were flushed to their render calls
quads->count = 0;
// Text rendering
if (renderer->glyphVtxArray.count > 0)
{
glUseProgram(renderer->textShader);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[0]);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[1]);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[2]);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, renderer->fontTextures[3]);
GLint uScreenLoc = glGetUniformLocation(renderer->textShader, "uScreen");
glUniform2f(uScreenLoc, renderer->screenWidth, renderer->screenHeight);
glBindVertexArray(renderer->textVAO);
glBindBuffer(GL_ARRAY_BUFFER, renderer->textVBO);
glBufferSubData(
GL_ARRAY_BUFFER,
0,
sizeof(struct GlyphVtx) * 6 * gVerts->count,
renderer->glyphVtxArray.instData);
glDrawArrays(GL_TRIANGLES, 0, renderer->glyphVtxArray.count * 6);
renderer->totalDrawCallsToOpenGl += 1;
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}
renderer->glyphVtxArray.count = 0;
if (cmd->commandType == CLAY_RENDER_COMMAND_TYPE_SCISSOR_START)
{
Clay_BoundingBox bb = cmd->boundingBox;
GLint x = (GLint)bb.x;
GLint y = (GLint)(renderer->screenHeight - (bb.y + bb.height));
GLsizei w = (GLsizei)bb.width;
GLsizei h = (GLsizei)bb.height;
glEnable(GL_SCISSOR_TEST);
glScissor(x, y, w, h);
}
else
{
glDisable(GL_SCISSOR_TEST);
}
}
}
}
#endif
#endif

View file

@ -0,0 +1,368 @@
#pragma once
#include <stdbool.h>
#include <clay.h>
#include <stb_image.h>
#include <stb_truetype.h>
#include "clay_renderer_gles3.h"
typedef struct LoadedImage
{
unsigned char *data;
int width;
int height;
int channels;
} LoadedImage;
typedef struct LoadedImageInternal
{
LoadedImage pub;
} LoadedImageInternal;
static LoadedImageInternal g_imageSlot;
const LoadedImage *loadImage(const char *path, bool flip)
{
if (!path)
return NULL;
stbi_set_flip_vertically_on_load(flip ? 1 : 0);
int w = 0;
int h = 0;
int c = 0;
unsigned char *data = stbi_load(path, &w, &h, &c, 0);
if (!data)
{
// Failed
g_imageSlot.pub.data = NULL;
g_imageSlot.pub.width = 0;
g_imageSlot.pub.height = 0;
g_imageSlot.pub.channels = 0;
return NULL;
}
g_imageSlot.pub.data = data;
g_imageSlot.pub.width = w;
g_imageSlot.pub.height = h;
g_imageSlot.pub.channels = c;
return &g_imageSlot.pub;
}
void freeImage(const LoadedImage *img)
{
if (!img || !img->data)
return;
// cast back to internal container
stbi_image_free((void *)img->data);
// reset slot
g_imageSlot.pub.data = NULL;
g_imageSlot.pub.width = 0;
g_imageSlot.pub.height = 0;
g_imageSlot.pub.channels = 0;
}
typedef struct Stb_FontData
{
float bakePxH; // font baking height (e.g. 48.0f)
float ascentPx; // in baked pixels (at bake_px size)
float descentPx; // usually negative (at bake_px size)
int firstChar; // e.g. 32
int charCount; // e.g. 96
stbtt_bakedchar *cdata;
int atlasW;
int atlasH;
} Stb_FontData;
bool Stb_LoadFont(
GLuint *textureOut,
Stb_FontData *fontOut,
const char *ttfPath,
float bakePxH, // Height of a char in pixels
int atlasW, // Width of atlas in pixels
int atlasH // Height of atlas in pixels
)
{
fontOut->firstChar = 32; // ASCII space
fontOut->charCount = 96; // 32..127
fontOut->bakePxH = bakePxH;
fontOut->atlasW = atlasW;
fontOut->atlasH = atlasH;
// allocate baked-char array
fontOut->cdata = (stbtt_bakedchar *)malloc(
sizeof(stbtt_bakedchar) // Store baked info
* fontOut->charCount // For each char
);
if (!fontOut->cdata)
{
fprintf(stderr, "Cannot allocate cdata\n");
return false;
}
FILE *f = fopen(ttfPath, "rb");
if (!f)
{
fprintf(stderr, "Could not open font: %s\n", ttfPath);
return false;
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char *ttf_buf = (unsigned char *)malloc(sz);
fread(ttf_buf, 1, sz, f);
fclose(f);
// temporary atlas memory
unsigned char *atlas = (unsigned char *)malloc(atlasW * atlasH);
memset(atlas, 0, atlasW * atlasH);
// bake
int res = stbtt_BakeFontBitmap(
ttf_buf, // raw TTF file
0, // font index inside TTF (0 = first font)
bakePxH, // pixel height of glyphs to generate
atlas, // OUT: bitmap buffer (unsigned char*)
atlasW, atlasH, // size of bitmap buffer
fontOut->firstChar, // first character to bake (e.g., 32 = space)
fontOut->charCount, // how many sequential chars to bake
fontOut->cdata // OUT: array of stbtt_bakedchar
);
stbtt_fontinfo fi;
if (!stbtt_InitFont(&fi, ttf_buf, stbtt_GetFontOffsetForIndex(ttf_buf, 0)))
{
return false;
}
int ascent, descent, lineGap;
stbtt_GetFontVMetrics(&fi, &ascent, &descent, &lineGap);
// Convert the font's "font units" to pixels proportional to bakePxH size:
float scaleForBake = stbtt_ScaleForPixelHeight(&fi, bakePxH);
fontOut->ascentPx = ascent * scaleForBake;
fontOut->descentPx = descent * scaleForBake; // this is typically negative
free(ttf_buf);
if (res <= 0)
{
fprintf(stderr, "Font baking failed\n");
free(atlas);
free(fontOut->cdata);
fontOut->cdata = NULL;
return false;
}
// Creating glyphVtxArray atlas texture
glGenTextures(1, textureOut);
glBindTexture(GL_TEXTURE_2D, *textureOut);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8,
atlasW, atlasH,
0, GL_RED, GL_UNSIGNED_BYTE, atlas);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
free(atlas);
return true;
}
static inline Clay_Dimensions Stb_MeasureText(
Clay_StringSlice glyphVtxArray,
Clay_TextElementConfig *config,
void *userData)
{
Stb_FontData *fontData = (Stb_FontData *)userData;
if (!fontData->cdata)
{
fprintf(
stderr,
"MeasureText cannot do anything when cdata is not baked: '%.*s' → %d x %d px\n",
(int)glyphVtxArray.length, glyphVtxArray.chars, 0, 0);
return (Clay_Dimensions){.width = 0, .height = 0};
}
float x = 0.0f;
float y = 0.0f;
const char *str = glyphVtxArray.chars;
int len = glyphVtxArray.length;
float scale = config->fontSize / fontData->bakePxH;
float letterSpacing = (float)config->letterSpacing;
float lineHeight = (config->lineHeight > 0)
? (float)config->lineHeight
: fontData->bakePxH;
for (int i = 0; i < len; i++)
{
unsigned char c = str[i];
if (c < fontData->firstChar // before range
|| c >= fontData->firstChar + fontData->charCount // after range
)
{
// Unsupported char: treat as space
fprintf(stderr, "illegal char %d\n", (int)c);
x += fontData->bakePxH * 0.25f;
continue;
}
stbtt_bakedchar *b = &fontData->cdata[c - fontData->firstChar];
// horizontal advance while moving along word characters
x += b->xadvance * scale + letterSpacing;
}
float ascent = fontData->ascentPx * scale;
float descent = fontData->descentPx * scale; // negative
float lineH = (ascent - descent); // total line height in pixels (at requested fontSize)
return (Clay_Dimensions){
.width = x,
.height = y + lineH,
};
}
static inline void Stb_RenderText(
Clay_RenderCommand *cmd,
Gles3_GlyphVtxArray *glyphVtxArray,
void *userData)
{
const Clay_TextRenderData *tr = &cmd->renderData.text;
float cr = tr->textColor.r / 255.0f;
float cg = tr->textColor.g / 255.0f;
float cb = tr->textColor.b / 255.0f;
float ca = tr->textColor.a / 255.0f;
float fontToUse = (float)tr->fontId;
Stb_FontData *fontArray = (Stb_FontData *)userData;
Stb_FontData *stbFontData = &fontArray[tr->fontId];
if (!stbFontData->cdata)
return;
Clay_StringSlice ss = tr->stringContents;
const char *txt = ss.chars;
int len = (int)ss.length;
float scale = tr->fontSize / stbFontData->bakePxH;
float ascent = stbFontData->ascentPx * scale; // pixels above baseline
float x = cmd->boundingBox.x;
float y = cmd->boundingBox.y + ascent; // baseline (note: no descent)
for (int i = 0; i < len; i++)
{
char ch = txt[i];
int idx = ch - stbFontData->firstChar;
if (idx < 0 || idx >= stbFontData->charCount)
{
continue;
}
stbtt_bakedchar *bc = &stbFontData->cdata[idx];
float gw = (float)(bc->x1 - bc->x0); // glyph width in atlas pixels
float gh = (float)(bc->y1 - bc->y0); // glyph height
float sw = gw * scale; // scaled width on screen
float sh = gh * scale; // scaled height
float ox = bc->xoff * scale; // baseline offset
float oy = bc->yoff * scale;
// top-left corner on screen (pixel coords)
float x0 = x + ox;
float y0 = y + oy;
float x1 = x0 + sw;
float y1 = y0 + sh;
// atlas size (you can make it configurable later)
float atlasW = stbFontData->atlasW;
float atlasH = stbFontData->atlasH;
float u0 = bc->x0 / atlasW;
float v0 = bc->y0 / atlasH;
float u1 = bc->x1 / atlasW;
float v1 = bc->y1 / atlasH;
// append 6 vertices (two triangles) to your buffer
GlyphVtx *v = &glyphVtxArray->instData[glyphVtxArray->count * 6];
v[0] = (GlyphVtx){x0, y0, u0, v0, cr, cg, cb, ca, fontToUse};
v[1] = (GlyphVtx){x1, y0, u1, v0, cr, cg, cb, ca, fontToUse};
v[2] = (GlyphVtx){x0, y1, u0, v1, cr, cg, cb, ca, fontToUse};
v[3] = (GlyphVtx){x0, y1, u0, v1, cr, cg, cb, ca, fontToUse};
v[4] = (GlyphVtx){x1, y0, u1, v0, cr, cg, cb, ca, fontToUse};
v[5] = (GlyphVtx){x1, y1, u1, v1, cr, cg, cb, ca, fontToUse};
// advance pen by baked xadvance + letter spacing
x += (bc->xadvance * scale) + tr->letterSpacing;
// prevent buffer overrun
if (glyphVtxArray->count >= glyphVtxArray->capacity)
{
break;
}
glyphVtxArray->count++;
}
}
bool Stb_LoadImage(GLuint *textureOut, const char *path)
{
const LoadedImage *li = loadImage(path, false);
if (!li || !li->data)
{
fprintf(stderr, "Failed to load texture at: %s\n", path);
return false;
}
glGenTextures(1, textureOut);
glBindTexture(GL_TEXTURE_2D, *textureOut);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
GLenum format = (li->channels == 4) ? GL_RGBA : GL_RGB;
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(
GL_TEXTURE_2D, // target
0, // level
format, // internal format int
li->width,
li->height,
0, // border
format, // format, GLEnum
GL_UNSIGNED_BYTE, // Type
li->data // pixels
);
glGenerateMipmap(GL_TEXTURE_2D);
freeImage(li);
return true;
}