PixelForge 是一个跨端渲染工程模板(是web端图文编辑器的一种实践方案, 不是一个完整的项目),重点是要讲清楚三件事:
- 如何用同一套 C++ 渲染代码同时支持 Native 和 Web。
- 如何通过 Emscripten 把 OpenGL/SDL 代码编译成浏览器可运行的 WASM。
- 如何接入第三方静态库(
.a),并在 Web/Native 分别完成链接。
| Native | Web |
![]() |
![]() |
src/core/engine.cpp- 渲染核心,实现分层场景(图片层 + 文本层)并在底层合成。
src/platform/sdl_runtime.cpp- SDL 窗口生命周期、GL Context、主循环、启动时场景下发。
src/api/pixelforge_c_api.cpp+src/include/pf/pixelforge_c_api.h- 对外 C ABI(
pf_scene_begin/pf_scene_add_image_rgba/pf_scene_add_text_utf8)。
- 对外 C ABI(
web/app.js- Web 调用方:解析协议、加载资源、调用 WASM 导出函数。
scripts/build_web.sh- Web 构建入口:
em++编译、链接第三方.a、预加载资源到虚拟文件系统。
- Web 构建入口:
src/third-party/thorvg/build.sh/src/third-party/thorvg/build_web.sh- 第三方库 ThorVG 的 Native / WASM 静态库构建脚本。
src/core/engine.cpp 里的着色器源码使用了 #ifdef __EMSCRIPTEN__ 条件编译,这是有必要的。
原因不是渲染逻辑不同,而是目标平台使用的 GLSL 方言不同:
-
Web(Emscripten + WebGL / GLES2)
- 使用 GLSL ES 风格
- 片元着色器通常需要精度声明(例如
precision mediump float;) - 不使用 desktop GLSL 的
#version 120写法
-
Native(SDL + OpenGL 2.1)
- 使用 desktop GLSL 1.20 风格
- 通常会写
#version 120 - 不使用 GLES 的精度声明语法
因此当前实现采用“同一套渲染逻辑 + 两套着色器字符串头部语法”来兼容两端。
如果后续要进一步统一,可以改为“共享主体代码 + 按平台注入 shader header”的方式,但底层仍需要区分目标 GLSL 语法。
- 准备 Emscripten 环境(确保
em++可用)。 - 执行 Web 构建脚本:
cd PixelForge
source ../emsdk/emsdk_env.sh
bash scripts/build_web.sh- 启动静态服务预览:
cd dist/web
python3 -m http.server 8000浏览器打开 http://localhost:8000。
build_web.sh 的关键职责:
- 用
em++编译engine.cpp、sdl_runtime.cpp、pixelforge_c_api.cpp等源码。 - 通过
-s EXPORTED_FUNCTIONS暴露 C API 给 JS 调用。 - 通过
--preload-file预加载字体/协议资源到 WASM 文件系统。 - 链接 WASM 版本第三方静态库(例如
deploy-wasm/lib/libthorvg-1.a)。
这部分描述的是“如何接入”而不是 OpenGL 理论。
web/app.js 通过 createPixelForgeModule(...) 初始化运行时,并把页面上的 canvas 传给 Emscripten。
之后 SDL/WebGL 会直接渲染到这个 canvas。
关键点:
locateFile用来定位.wasm文件canvas指定渲染目标ccall用于调用 C 导出函数(pf_scene_*)
图片层流程:
- JS
fetch图片并绘制到临时 2D canvas getImageData()得到 RGBA 字节数组- 在 WASM 堆里
_malloc一段内存 module.HEAPU8.set(...)把 JS 字节拷贝到 WASM 内存ccall("pf_scene_add_image_rgba", ...)把指针和尺寸传给 C++- 调用后
_free释放 WASM 内存
文本层流程:
- 先走
pf_scene_add_text_utf8 - 如果底层文字渲染失败,web 会 fallback:在 JS 2D canvas 里栅格化文本,再按图片层方式走
pf_scene_add_image_rgba
底层 C++ 渲染是两步:
- 将图层绘制到离屏 FBO(layer FBO)
- 将 FBO 颜色纹理全屏绘制到默认 framebuffer
最后 SDL 调用 SDL_GL_SwapWindow 提交帧。
在 Emscripten 环境下,这个默认 framebuffer 对应的就是你传入的 DOM canvas,所以浏览器页面能看到最终画面。
简化理解:
- JS 负责“喂数据”(协议、RGBA、交互)
- WASM/C++ 负责“渲染合成”(图层、FBO、着色器)
- Canvas 只是最终显示目标
项目中第三方库分为两类:
- Native 静态库:如
src/third-party/thorvg/deploy/lib/libthorvg-1.a - WASM 静态库:如
src/third-party/thorvg/deploy-wasm/lib/libthorvg-1.a
接入原则:
- Native 构建(CMake)链接 Native
.a。 - Web 构建(Emscripten)只能链接 WASM 目标
.a,不能复用 arm64/x86 的 Native.a。
推荐流程:
- 先构建第三方库
- Native:
bash src/third-party/thorvg/build.sh - Web:
bash src/third-party/thorvg/build_web.sh
- Native:
- 在主工程构建脚本里加 include + 链接
- Web:在
scripts/build_web.sh中添加-I...和对应.a - Native:在
CMakeLists.txt中target_link_libraries(...)
- Web:在
- 统一 API 调用层,不在上层分叉渲染逻辑。
cd PixelForge
bash scripts/build_native.sh
./build/pixelforge依赖:SDL2、OpenGL,以及已构建好的 Native 第三方静态库。

