Skip to main content

做了一个glsl在线调试工具

· 16 min read

在线体验地址:ShaderPad ,对应 github 仓库

最近在复习 threejs 和 GLSL,每次想写一段 shader 看下效果,都要新开一个 HTML、写 <script> 标签、配 <canvas>、起本地服务、改完刷新看效果 —— 即便有 AI 加持,这一套流程的心智负担还是太重了。说到底,我只是想验证一些变换公式,查看顶点变换或颜色变化等效果,而不是搭一个项目

试了一圈现成的在线 GLSL playground,要么 UI 像十年前的 CodePen,要么功能停在「能跑就行」,再要么就是必须登录或者收费的复杂产品。于是决定自己撸一个 —— ShaderPad

目标

  • 「打开浏览器 30 秒内跑起一个 shader」:零登录、零配置、零下载
  • 面向 Web 着色器学习的极简 playground,后续横向扩展到 TSL / WGSL(WebGPU 时代的新 shader 语言)

整体效果

shaderpad首页

整体设计跟大部分在线编辑器类似,左边是代码编辑区,提供顶点着色器和片元着色器的编辑功能,会高亮glsl语法。右边是基于threejs的3d场景,实时预览当前编辑的代码的效果,并且提供了悬浮的控制台面板,方便查看一些报错或log

并且为了方便用户观察,3d场景内置了辅助网格和辅助坐标轴,并且引入了 OrbitControls,用户可以通过鼠标旋转场景,查看不同的角度

技术栈

维度选型理由
前端框架Astro 4.15 + React Island群岛架构 + 首屏友好
编辑器Monaco Editor 0.50VSCode 同款,TS 智能提示、GLSL 语法高亮
3d渲染Three.js 0.170(WebGLRenderer + RawShaderMaterial)
状态管理nanostores 0.11极小(<1KB),React 集成通过 @nanostores/react
包管理pnpm 8.11 workspace硬链接节省空间,monorepo 友好
部署腾讯云轻量服务器 + Nginx Proxy Manager + GitHub Actions

Astro

做 ShaderPad 之前没用过 Astro,这次用下来印象最深的就是它「默认零 JS」的设计 —— 它本身不是基于 React 的,而是通过 integrations 让你把 React / Vue / Svelte / Solid 当作「岛屿」嵌入到静态 HTML 里。对比 Gatsby、Docusaurus、Next.js 这些默认就把整个 React 运行时推到浏览器的方案,Astro 反而是个异类。

它有两个我特别喜欢的点:

  • 群岛架构与部分水合:把整个页面想成一片静态海洋,里面零散分布着几个交互小岛。ShaderPad 的文档内容基本是静态海面,只有 Playground 那个组件是真正的交互孤岛。在底层原理上,Astro 的编译器在 build 时会把所有 .astro 组件跑一遍,暴力剥离掉所有不需要在浏览器执行的 JS 逻辑,只输出纯 HTML
  • 按需加载机制:由于 Playground 强依赖 WebGL(完全没法在 Node 端执行),我给它加了 client:only="react" 指令。Astro 看到这个指令,就会在那个位置留个空 div,然后注入一小段极其轻量的 Astro 调度器代码。等页面加载完,调度器才会去拉取 React 运行时和组件代码,在客户端完成「水合」。

落到 ShaderPad 这个项目上:构建产物里整个文档站的 index 页面只有 6.84 kB(gzip 后 2.73 kB),大头全部在 Playground 这个 React Island 里,500+ kB。打开任意 /learn/* 文档页几乎是瞬开的,而真正需要 React 能力的 playground 才会按需加载。

稍微深入一下实现原理:

Astro 的魔法主要在编译时(Build Time)和一小段运行时(Runtime)调度器里:

  1. 编译时:HTML 静态化与标记 Astro 的默认渲染模式是 SSG(Static Site Generation)。当你运行 pnpm build 时,Astro 编译器会解析所有的 .astro 组件,并执行里面的服务端逻辑(Frontmatter 中的代码)。如果遇到普通的 HTML 标签或没加 client:* 指令的 UI 组件,Astro 会直接把它们渲染成静态的 HTML 字符串并落盘。在这个过程中,所有与渲染无关的 JS 都会被无情丢弃。 如果遇到了像 <Playground client:only="react" /> 这样的指令,Astro 就会在生成的 HTML 里留下一个特殊的占位符(一个带有 astro-island 自定义标签和特定属性的 DOM 节点),同时把这个组件及其依赖记录下来。

  2. 运行时:轻量调度器与按需水合 浏览器加载页面时,虽然没有庞大的 React 运行时,但 Astro 会注入一个极小的(仅几 KB)调度器脚本。这个调度器会扫描页面上的 astro-island 节点,并根据指令属性执行不同的加载策略:

    • client:load:页面一加载,立刻去拉取 React 运行时和该组件的 JS 代码,然后执行水合。
    • client:visible:用 IntersectionObserver 监听,等用户滚动到这个组件可见时,再去拉取和水合。
    • client:only:跳过任何服务端渲染(SSG/SSR),只在客户端执行渲染(ShaderPad 的 Playground 因为强依赖浏览器 API 和 WebGL,用的就是这个)。

通过这种方式,Astro 完美地解耦了"页面内容展示"和"复杂组件交互"的加载时机。

对比一下 Gatsby 和 Docusaurus,Astro 在工具或内容型网站上的优势更明显:

  • Gatsby(过重的全栈包袱):骨子里是重度依赖 React 的 SSG 框架。它的核心壁垒是 GraphQL 数据聚合(适合打通多源 CMS),但在客户端,浏览器仍需加载庞大的 React 运行时来完成页面的全量水合(Hydration)。对于 ShaderPad 这种局部极度吃性能的「重客户端工具」,Gatsby 的全站 React 渲染反而成了拖累首屏性能的包袱。
  • Docusaurus(缺乏动静分离):作为文档站的标杆,它开箱即用且生态完善。但其底层依然是传统的 React SPA 架构(全量水合)。这意味着页面上的导航、侧边栏、纯文本都会产生 React 运行时开销。Docusaurus 能够运行 Three.js,但它无法做到“动静剥离”,所有内容都被强制绑定在 React 的渲染树上。
  • Astro(极致的孤岛架构):天然支持多框架混用(React、Vue、Svelte 等可按需接入)。它的核心理念是“海面是 0 运行时成本的纯 HTML,仅对孤岛组件(如 WebGL Playground)进行按需水合”。对于 ShaderPad 来说,Astro 可以实现“文档正文 0 JS 开销,而核心渲染器作为 React 孤岛独立加载”,这种「主静态、副交互」的颗粒度控制,是目前最贴合该场景的终极形态

所以这也不难理解为什么 Astro 在「文档站 + 工具型网站」这两类站点上越来越受欢迎——它把"该省的省到极致"这件事做得很彻底。

架构设计

Monorepo 结构

为什么用 monorepo?主要是加这一层,几乎没有什么开发成本,反而方便后续扩展桌面端或者定制化项目,前端的很多组件和定义都可以复用,也能强迫自己思考"哪些是核心、哪些是 UI"

  • @shaderpad/runtime 抽离出 LanguageAdapter 接口(GLSL 轻量语法预检),未来扩展到 Node / Tauri 桌面端可直接复用。
  • apps/web 主站保持轻量,部署产物干净
shaderPad/
├── apps/
│ └── web/ # 主站(Astro)
│ ├── src/
│ │ ├── pages/ # 路由(index / play / learn/*)
│ │ ├── components/ # React 组件
│ │ ├── lib/
│ │ │ ├── runtime/ # 浏览器侧渲染引擎
│ │ │ └── share/ # URL/localStorage 持久化
│ │ └── shaders/examples.ts # 内置示例库
│ └── astro.config.mjs
├── packages/
│ └── shader-runtime/ # 跨端共享核心
│ └── src/languages/ # GLSL / TSL / WGSL adapter
└── .github/workflows/
└── deploy-web.yml # 自动化部署

数据流

[Monaco Editor]  --change-->  Playground state (codeRef)
|
|--auto save (1s debounce)--> localStorage
|
'--compileAndRun()--> [ShaderEngine]
|
+---------+---------+
| |
(vertex/fragment) (uniforms)
| |
v v
Three.js RawShaderMaterial <-- OrbitControls / Grid / Axes

核心渲染模块

ShaderEngine(渲染引擎)

封装 Three.js 的脏活累活,对外暴露极简 API:

class ShaderEngine {
init() // 创建 WebGLRenderer + OrbitControls + helpers
applyShader(source, mode) // 注入 RawShaderMaterial
forceCompile() // 直接调 WebGL API 编译,捕获原始错误行号
setGeometry(type) // 切换几何体(material 复用)
start() / stop() / dispose() // 生命周期
}

内置模块

提供了以下几种 threejs 常见的几何体:

  • PlaneGeometry 平面
  • BoxGeometry 立方体
  • SphereGeometry 球体

默认是 PlaneGeometry,用户可以自行切换。针对不同几何体提供了几种不同的常见 shader 示例,比如时间渐变、鼠标跟随、噪声效果等,选中后就可以查看效果,用户可以根据需要选择。

另外比较关键的是,我还内置了一些开发中常见的 uniform 变量,如下:

uniform float u_time;       // 自启动以来的秒数,每帧递增
uniform vec2 u_resolution; // 画布宽高(像素)
uniform vec2 u_mouse; // 鼠标位置,归一化到 [0,1](Y 已翻转)
uniform float u_random; // applyShader 时的随机数 [0,1)

这样就可以在 shader 中直接使用这些变量来做一些动态效果。

当然目前没法完全自定义 uniform,暂时是逐步加入一些常见变量,有需求的可以评论或者追加 github issue

RawShaderMaterial

为什么用 RawShaderMaterial 而不是 ShaderMaterial?

ShaderMaterial 会自动注入一堆 uniforms/attributes(cameraPosition、modelViewMatrix 等),用户写 shader 时反而困惑——「我声明的 uniform 真的生效了吗?」

RawShaderMaterial 不注入任何东西,用户 source 即最终 GLSL,错误行号 1:1 对应。代价是用户必须显式声明:

attribute vec3 position;
attribute vec2 uv;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;

但每个示例的 header 都注释清楚了,权衡下来值得。

部署

刚好最近换了台新服务器,ShaderPad 就作为第一个部署的应用上线了。关于新服务器配置环境,还专门写了一篇文章 《linux个人云服务器开荒指南》

顺便吐槽一句腾讯云:旧那台 1 核 2G 的云服务器续费依旧贵得离谱,反而新买一台 2 核 4G 首年还有大折扣,算下来差不多。旧机器上也没跑几个应用,迁移成本不高,索性换台配置高一点的 —— 顺便服务器在海外,对海外 LLM 模型的调用也更稳定。

部署的核心组件是 Nginx Proxy Manager(下文简称 NPM,注意和 Node 的 npm 不是一回事)。

Nginx Proxy Manager

NPM 是一个基于 Nginx 的可视化反向代理管理工具,包装成 Docker 镜像后一行命令就能起。真香点在于:

  • 提供 Web 管理界面,不用手写 nginx.conf、不用 nginx -s reload
  • SSL 证书申请 + 部署一条龙(Let's Encrypt 自动化),告别以前去腾讯云控制台手动申请再 vim nginx.conf 的繁琐

端口规划(默认会占三个,记得在腾讯云防火墙里放行规则):

端口用途暴露建议
80HTTP公开
443HTTPS公开
81管理界面只对可信 IP 开放

自动化部署

走 GitHub Actions + rsync:

  • push 到 main → 触发 .github/workflows/deploy-web.yml
  • pnpm install + pnpm --filter web build,产物在 apps/web/dist/
  • rsync-deployments action 把 dist/ 推到服务器的 /var/www/shaderpad/dist/(与 NPM 静态资源目录保持一致)

踩过的坑

  1. Nginx 500 错误:启动NPM容器的时候没正确挂载静态资源的目录比如 /home → docker-compose 加 volumes
  2. pnpm@10 lockfile 冲突:CI 报 ERR_PNPM_LOCKFILE_BREAKING_CHANGE → 统一降到 pnpm@8.11.0

最后

以前就一直想做一个在线工具(比如压缩图片、在线转换等),但碍于市面上工具都相对完善,并且自己做的话有开发和部署成本,就迟迟没落地。现在有ai提高开发效率(这个项目大部分代码来自minimax-M3 coding),并且确实是一个合理且实在的需求(因为现在对于webgl/webgpu的开发需求越来越大),然后再优化一下部署流程,就促成了这次 shaderPad 的开发和上线

第一版先这样,后面会持续关注平台的使用情况,如果用户量上来的话,会考虑扩展更多功能

在线体验地址:ShaderPad ,对应 github 仓库