ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Clay GLES3 渲染器实战:用 SDL2 构建带侧边栏与浮动滚动条的滚动容器

2026/9/13 20:20:32 拓冰建站 浏览量
Clay GLES3 渲染器实战:用 SDL2 构建带侧边栏与浮动滚动条的滚动容器 Clay GLES3 渲染器实战用 SDL2 构建带侧边栏与浮动滚动条的滚动容器【免费下载链接】clayHigh performance UI layout library in C.项目地址: https://gitcode.com/GitHub_Trending/clay9/clayClay 仓库中的 examples/GLES3-SDL2-sidebar-scrolling-container 示例是一个面向尚在打磨中的 GLES3 渲染器的完整演示工程它用 SDL2 创建窗口与 OpenGL 上下文用 Clay 构建“侧边栏 可滚动主内容区 浮动滚动条”的经典界面并把文本、图片、圆角、边框、裁剪、浮动等全部绘制命令跑通一遍。读完后你能掌握该示例的三种构建方式CMake / Emscripten / macOS、逐帧“事件 → 布局 → 绘制”的主循环结构以及 Clay 滚动容器scroll container与 GLES3 渲染器底层的实现原理。一、示例定位GLES3 渲染器的“全命令”测试场README 对这个目录的定位非常明确This directory contains a complete example that can be 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.也就是说这个示例的首要目标是验证 GLES3 渲染器能否正确执行所有类型的绘制命令矩形、圆角、边框、图片、文本、裁剪、浮动元素等。示例目录包含以下文件文件作用main.c完整入口窗口初始化、Clay 上下文、布局声明、渲染循环CMakeLists.txtCMake 构建脚本负责拉取依赖Makefile.emscripten编译到 WebWebGL2的手工 MakefileMakefile.macosmacOS 上用 clang 直接编译的 Makefileresources/运行时资源profile-picture.png、millbank.jpeg、Roboto-Regular.ttf、RobotoMono-Medium.ttfREADME 还说明了图片来源millbank.jpeg是一张窗户中城市全景照片profile-picture.png是一组环形排列的物体照片均为作者手机拍摄并奉献给公有领域Public Domain。从 main.c 的加载逻辑看millbank.jpeg被切出四段窗口区域g_window1~g_window4通过 UV 裁剪u0/v0/u1/v1实现profile-picture.png则被用作圆形头像cornerRadius {30, 30, 30, 30}配合 60x60 固定尺寸以及多张照片展示位字体则烘焙成 1024x1024 的纹理图集。二、构建与运行2.1 CMake 构建最简单的方式README 给出的最简流程mkdir build cmake -S . -B ./build注意这里的cmake -S .指示例自身目录需要 CMake 3.27 及以上。CMakeLists.txt 的关键配置值得逐段看C 标准set(CMAKE_C_STANDARD 99)工程名GLES3_SDL2_sidebar_scrolling_containerFetchContent 拉取依赖stbheader-only无自带 CMake 构建CMakeLists 中直接以${stb_SOURCE_DIR}作为头文件包含路径和SDL2release-2.30.10标签浅克隆链接SDL2::SDL2main与SDL2::SDL2-static另外find_package(SDL2 REQUIRED)并在 macOS 上查找 OpenGL 框架find_library(OPENGL_FRAMEWORK OpenGL)资源拷贝POST_BUILD自定义命令把resources/整目录复制到构建目录保证程序运行时能用相对路径加载图片与字体头文件路径包含当前目录渲染器、../..Clay 仓库根即 clay.h 所在处、stb 源码目录。2.2 Emscripten 构建Web 端 WebGL2README 说明 Emscripten 构建“稍显定制”但仍然依赖先做一次 CMake 构建因为 Makefile 里的-I./build/_deps/stb-src直接引用了 CMake FetchContent 拉下来的 stb 头文件。完整步骤# 1. 先完成上面的 CMake 构建得到 build/_deps/stb-src # 2. 加载 Emscripten SDK 环境 source /path/to/emscripten/emsdk/emsdk_env.sh # 3. 用手工 Makefile 构建并启动本地服务器 make -f Makefile.emscripten test # 4. 浏览器访问 # http://localhost:8080Makefile.emscripten 中有几个关键点编译器为emcc-stdc99 -O0包含路径-I../..Clay 根目录与-I./build/_deps/stb-src-s USE_SDL2使用 Emscripten 的 SDL2 端口-s FULL_ES21 -s USE_WEBGL21启用 WebGL2对应 GLES3 能力-s ALLOW_MEMORY_GROWTH1 -s GL_UNSAFE_OPTS0并显式导出_main-s ASSERTIONS1 -s SAFE_HEAP1打开调试断言属于开发期配置四个--preload-file把resources/下的两张图片和两个字体打进虚拟文件系统路径映射为resources/xxx与桌面版读取的路径一致test目标先编译生成build/emscripten/index.html再进入该目录启动python3 -m http.server默认 8080 端口。Web 端主循环也不走原生 while 循环main.c 中通过emscripten_set_main_loop(loop, 0, 1)把loop()注册为每帧回调#ifdef __EMSCRIPTEN__分支第三参数为 1 表示每帧后emscripten_request_animation_frame_loop式地同步帧。同时窗口初始化时Emscripten 分支通过SDL_HINT_OPENGL_ES_DRIVER与SDL_GL_CONTEXT_PROFILE_ES请求 ES 3.0 上下文。2.3 macOS 构建README 给出的步骤# 依赖SDL2 brew install sdl2 # 依赖STB借助 CMake 拉取到 build/_deps cmake -B build # 用 Makefile 编译 make -f Makefile.macos # 运行 ./macos-sidebar-scrolling-container-sdl2Makefile.macos 用clang直接编译main.c-stdc99 -g -O0 -fno-omit-frame-pointer并定义-DGL_SILENCE_DEPRECATION抑制 macOS 遗留 OpenGL API 的弃用警告。SDL2 的头文件与库路径通过$(shell brew --prefix sdl2)动态获取链接-lSDL2及OpenGL、Cocoa、IOKit、CoreVideo四个系统框架产物输出到build/macos-sidebar-scrolling-container-sdl2README 中写作./macos-sidebar-scrolling-container-sdl2实际以 Makefile 的-o路径为准。一个值得注意的细节initVideo()中非 Emscripten 平台请求的是 Core 3.3 上下文源码注释坦承 “Apple MacOs will use it own legacy desktop GL instead / I know, I lied, I said this was an GLES3” —— 即桌面端实际跑在遗留桌面 GL 上这也是 Makefile 里GL_SILENCE_DEPRECATION存在的原因。三、初始化Clay 上下文、STB 字体与 GLES3 渲染器main.c 的init()展示了 Clay GLES3 渲染器 STB 三者装配的标准顺序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 g_ctx.screenWidth, .height 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);要点无全局静态内存Clay 的所有内部状态放在调用方提供的 arena 中。Clay_MinMemorySize()返回最小所需字节数clay.h 中还提供了配套的Clay_CreateArenaWithCapacityAndMemory本示例直接malloc等量内存文本测量必须后置Clay_SetMeasureTextFunction必须在Clay_Initialize之后设置源码注释明确标注 “has to be set after the Context is set!”本例用 clay_renderer_gles3_loader_stb.c 中的Stb_MeasureTextuserData 为g_stbFonts[MAX_FONTS]数组字体烘焙Stb_LoadFont分别加载Roboto-Regular.ttf与RobotoMono-Medium.ttfbake 高度 24px、图集 1024x1024分别写入g_gles3.fontTextures[0/1]。布局中通过fontId如FONT_ID_BODY_24 1、FONT_ID_BODY_16 0选择字体图片加载Stb_LoadImage加载头像与窗景两张图到g_gles3.imageTextures渲染器容量Gles3_Initialize(g_gles3, 4096)的第二个参数是每帧实例四边形与字形缓冲的最大容量对应当例中上百个元素加文本是充裕的调试模式Clay_SetDebugModeEnabled(true)打开 Clay 内置调试视图状态保留无需每帧重设GL 信息采集用glGetString抓 OpenGL / GLSL 版本、vendor、renderer作为动态Clay_String.isStaticallyAllocated false指向栈/静态缓冲在布局里以CLAY_TEXT渲染方便运行时确认实际 GL 后端。四、逐帧主循环事件 → 指针状态 → 滚动更新 → 布局 → 渲染main()先initVideo(g_ctx, 1280, 720)1280x720 可调整窗口再init()随后进入loop()Web 端则交给emscripten_set_main_loop。loop()的固定顺序值得逐行对照// 1. 用 SDL_GetPerformanceCounter 计算 deltaTime毫秒 // 2. 轮询事件 while (SDL_PollEvent(event)) { switch (event.type) { case SDL_QUIT: g_ctx.shouldContinue false; case SDL_MOUSEWHEEL: scrollDelta { event.wheel.x, event.wheel.y }; } } // 3. 指针位置 左键按下状态 Clay_SetPointerState(mousePosition, mouseState SDL_BUTTON(1)); // 4. 滚动容器更新启用拖拽惯性滚动 Clay_UpdateScrollContainers(true, scrollDelta, deltaTime); // 5. 窗口尺寸自适应 SDL_GL_GetDrawableSize(...); glViewport(0, 0, w, h); Clay_SetLayoutDimensions((Clay_Dimensions){(float)w, (float)h}); // 6. 清屏并关闭深度写入 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 // 7. 布局 渲染 Clay_RenderCommandArray cmds CreateLayout(); Gles3_Render(g_gles3, cmds, g_stbFonts); // 8. FPS 统计每 3 秒刷新一次文本、换帧 SDL_GL_SwapWindow(g_ctx.sdlWindow);几个细节滚动输入鼠标滚轮事件被累积成Clay_Vector2 scrollDelta传给Clay_UpdateScrollContainers(true, scrollDelta, deltaTime)。对照 clay.h 的 API 注释enableDragScrolling为true时启用类移动端“触摸拖拽”滚动及松手后的惯性滚动deltaTime单位是秒用于推进惯性衰减。本例传毫秒属于示例自身的时间单位约定阅读源码时可留意这一差异每帧重建布局Clay 是即时模式immediate mode布局库CreateLayout()每帧完整重新声明 UIClay_EndLayout()计算出Clay_RenderCommandArray交给渲染器这与“声明式 UI”的核心设计一致绘制调用计数渲染前后各读一次g_gles3.totalDrawCallsToOpenGl差值显示在界面底部的 “Last frame got: N draw calls” 文本里便于验证实例化渲染的批次合并效果FPS 节流帧率文本每 3 秒才用snprintf刷新一次measureInterval 3000.0避免每帧格式化字符串。五、布局结构侧边栏、浮动容器与自绘滚动条CreateLayout()main.c声明了完整的界面层级正是 README 所说“sidebar scrolling container”的由来OuterContainer (铺满全屏, padding 16, gap 16) ├── SideBar (固定宽 300, 自上而下) │ ├── ProfilePictureOuter (头像 文本, 垂直居中) │ └── SidebarBlob1..4 (各 50px 高的占位条) ├── RightPanel (剩余宽度, 自上而下) │ ├── 头部按钮行 (4 个不同 border/cornerRadius 的按钮) │ ├── MainContent (滚动容器: clip.vertical childOffset) │ │ ├── FloatingContainer (inline 浮动容器, 悬浮在内容顶部) │ │ ├── 多段文本、四张窗景缩略图、GL 信息 │ │ └── Photos (aspectRatio1 的图片 图注) │ ├── 绘制调用统计条 │ └── FPS 统计条 ├── Blob4Floating2 (浮动在 SidebarBlob4 上) │ └── ScrollContainer (100 个列表项的第二个滚动容器) └── ScrollBar (自绘滚动条, 浮动吸附到 MainContent 右侧)其中体现 Clay 核心能力的三处写法1. 滚动容器 clip 滚动偏移。MainContent的声明是滚动容器的标准模式CLAY(CLAY_ID(MainContent), { .layout { .layoutDirection CLAY_TOP_TO_BOTTOM, .padding {16,16,16,16}, .childGap 16, .sizing { .width CLAY_SIZING_GROW(0) } }, .backgroundColor {200, 200, 255, 255}, .clip { .vertical true, .childOffset Clay_GetScrollOffset() }, }) { ... }Clay_GetScrollOffset()clay.h返回当前打开元素内部存储的滚动偏移与clip配合子内容被裁剪在容器范围内并随滚动偏移平移。共享布局 clay-video-demo.c 中的MainContent用的是完全相同的写法可见这是 Clay 滚动容器的惯用法。2. 自绘滚动条 滚动数据 浮动元素。布局结束后通过命令式 API 查询滚动容器状态Clay_ScrollContainerData scrollData Clay_GetScrollContainerData(Clay_GetElementId(CLAY_STRING(MainContent))); if (scrollData.found) { CLAY(CLAY_ID(ScrollBar), { .floating { .attachTo CLAY_ATTACH_TO_ELEMENT_WITH_ID, .offset { .y -(scrollData.scrollPosition-y / scrollData.contentDimensions.height) * scrollData.scrollContainerDimensions.height }, .zIndex 1, .parentId Clay_GetElementId(CLAY_STRING(MainContent)).id, .attachPoints { .element CLAY_ATTACH_POINT_RIGHT_TOP, .parent CLAY_ATTACH_POINT_RIGHT_TOP } } }) { CLAY(CLAY_ID(ScrollBarButton), { .layout { .sizing { CLAY_SIZING_FIXED(12), CLAY_SIZING_FIXED((scrollData.scrollContainerDimensions.height / scrollData.contentDimensions.height) * scrollData.scrollContainerDimensions.height) } }, .backgroundColor Clay_PointerOver(Clay_GetElementId(CLAY_STRING(ScrollBar))) ? (Clay_Color){100,100,140,150} : (Clay_Color){120,120,160,150}, .cornerRadius CLAY_CORNER_RADIUS(6) }) {} } }这里用到了 clay.h 的Clay_GetScrollContainerData返回结构带found标志成功时提供scrollPosition当前滚动位置、contentDimensions内容总尺寸、scrollContainerDimensions容器可视尺寸。代码据此完成两个经典计算——滑块位置-(scrollPosition.y / contentH) * containerH、滑块高度(containerH / contentH) * containerH——并用CLAY_ATTACH_TO_ELEMENT_WITH_ID把滚动条浮动吸附到MainContent的右上角RIGHT_TOP锚点对齐再用Clay_PointerOver实现悬停变色。这是“Clay 只提供滚动数据、滚动条 UI 完全由声明式元素自建”的完整范例。3. 浮动容器inline floating。MainContent内的FloatingContainer声明了.floating {.attachTo CLAY_ATTACH_TO_PARENT, .zIndex 1, .attachPoints {CENTER_TOP, CENTER_TOP}}即使页面滚走它仍悬浮在容器顶部不动第二处Blob4Floating2则把另一个 100 项的滚动列表for (int i 0; i 100; i) RenderDropdownTextItem(i)浮动吸附到SidebarBlob4之上parentId通过Clay_GetElementId(CLAY_STRING(SidebarBlob4))计算演示了“浮动 裁剪 滚动”三者叠加时渲染器都要正确处理裁剪区域。六、GLES3 渲染器底层实例化四边形与字形缓冲该示例之所以能“全命令跑通”底气来自 clay_renderer_gles3.h 中渲染器的结构。从源码看它把 Clay 输出的绘制命令归并为两大类 GPU 批次矩形批次实例化四边形Gles3_Initializeclay_renderer_gles3.h编译 quad 着色器程序后创建一个 0..1 单位四边形 VBO 和一个按maxInstances本例 4096预分配的实例 VBO。每个实例携带整帧所需的属性且全部设置glVertexAttribDivisor(..., 1)逐实例属性ATTR_QUAD_RECT4 floats矩形位置尺寸ATTR_QUAD_COLOR4 floats颜色ATTR_QUAD_UV4 floats纹理坐标实现millbank.jpeg四段窗景式的 UV 裁剪ATTR_QUAD_RAD4 floats四个角的圆角半径ATTR_QUAD_BORDER4 floats四边边框宽度ATTR_QUAD_TEX1 float纹理选择器。这意味着圆角、边框、图片 UV 都是在单个实例化 draw call 内由着色器完成的与示例底部实时显示的 draw call 计数相互印证。文本批次字形缓冲同样按maxInstances预分配每字形 6 个顶点的GlyphVtx缓冲位置/UV/颜色/图集纹理单元Gles3_Render遇到CLAY_RENDER_COMMAND_TYPE_TEXT命令时调用外部注入的renderTextFunction本例为Stb_RenderText见Gles3_SetRenderTextFunctionclay_renderer_gles3.h由 STB 侧查字体图集、展开字形三角形并累加进缓冲。Gles3_Renderclay_renderer_gles3.h遍历Clay_RenderCommandArray对每个命令先把 bounding box 取整roundf以避免亚像素接缝再按命令类型分发遇到带clip的元素时切换 scissor 区域scissorChanged标志来实现MainContent与ScrollContainer的内容裁剪。文本测量与渲染的对接由 clay_renderer_gles3_loader_stb.c 完成Stb_MeasureText满足 Clay 的Clay_Dimensions (*)(Clay_StringSlice, Clay_TextElementConfig*, void*)接口clay.hStb_LoadFont负责按 bake 像素高度把 TTF 烘进图集并记录每个字形的 UV 与推进量。七、小结与代码索引这个示例串起了 Clay 从“声明 UI”到“GPU 出图”的完整链路SDL2 提供窗口与事件Clay 以即时模式每帧产出带包围盒与裁剪信息的渲染命令数组GLES3 渲染器用两类实例化/动态批次把命令压缩为极少量 draw callSTB 负责字体测量、烘焙与字形展开。滚动容器则展示了 Clay 的惯用组合clip Clay_GetScrollOffset()建容器、Clay_UpdateScrollContainers每帧推进滚动、Clay_GetScrollContainerData取数据自绘滚动条。关键代码索引均为仓库根相对路径关注点位置构建脚本CMake 依赖拉取、资源拷贝examples/GLES3-SDL2-sidebar-scrolling-container/CMakeLists.txtWeb 构建参数WebGL2、preload-fileexamples/GLES3-SDL2-sidebar-scrolling-container/Makefile.emscripten窗口/GL 上下文、init、主循环、布局声明examples/GLES3-SDL2-sidebar-scrolling-container/main.c滚动 API 与注释Update/Offset/ScrollContainerDataclay.h实例化 VAO/VBO 与渲染分发renderers/GLES3/clay_renderer_gles3.hSTB 字体加载 / 文本测量renderers/GLES3/clay_renderer_gles3_loader_stb.c同为 clip 滚动容器写法的共享布局examples/shared-layouts/clay-video-demo.c【免费下载链接】clayHigh performance UI layout library in C.项目地址: https://gitcode.com/GitHub_Trending/clay9/clay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考