最近在复习 threejs 和 GLSL,每次想写一段 shader 看下效果,都要新开一个 HTML、写 <script> 标签、配 <canvas>、起本地服务、改完刷新看效果 —— 即便有 AI 加持,这一套流程的心智负担还是太重了。说到底,我只是想验证一些变换公式,查看顶点变换或颜色变化等效果,而不是搭一个项目。
试了一圈现成的在线 GLSL playground,要么 UI 像十年前的 CodePen,要么功能停在「能跑就行」,再要么就是必须登录或者收费的复杂产品。于是决定自己撸一个 —— ShaderPad
目标:
- 「打开浏览器 30 秒内跑起一个 shader」:零登录、零配置、零下载
- 面向 Web 着色器学习的极简 playground,后续横向扩展到 TSL / WGSL(WebGPU 时代的新 shader 语言)
整体效果

整体设计跟大部分在线编辑器类似,左边是代码编辑区,提供顶点着色器和片元着色器的编辑功能,会高亮glsl语法。右边是基于threejs的3d场景,实时预览当前编辑的代码的效果,并且提供了悬浮的控制台面板,方便查看一些报错或log
并且为了方便用户观察,3d场景内置了辅助网格和辅助坐标轴,并且引入了 OrbitControls,用户可以通过鼠标旋转场景,查看不同的角度
技术栈
| 维度 | 选型 | 理由 |
|---|---|---|
| 前端框架 | Astro 4.15 + React Island | 群岛架构 + 首屏友好 |
| 编辑器 | Monaco Editor 0.50 | VSCode 同款,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)调度器里:
编译时: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 节点),同时把这个组件及其依赖记录下来。运行时:轻量调度器与按需水合 浏览器加载页面时,虽然没有庞大的 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的繁琐
端口规划(默认会占三个,记得在腾讯云防火墙里放行规则):
| 端口 | 用途 | 暴露建议 |
|---|---|---|
| 80 | HTTP | 公开 |
| 443 | HTTPS | 公开 |
| 81 | 管理界面 | 只对可信 IP 开放 |
自动化部署
走 GitHub Actions + rsync:
- push 到 main → 触发
.github/workflows/deploy-web.yml - 跑
pnpm install+pnpm --filter web build,产物在apps/web/dist/ rsync-deploymentsaction 把dist/推到服务器的/var/www/shaderpad/dist/(与 NPM 静态资源目录保持一致)
踩过的坑
- Nginx 500 错误:启动NPM容器的时候没正确挂载静态资源的目录比如
/home→ docker-compose 加 volumes pnpm@10lockfile 冲突:CI 报ERR_PNPM_LOCKFILE_BREAKING_CHANGE→ 统一降到pnpm@8.11.0
最后
以前就一直想做一个在线工具(比如压缩图片、在线转换等),但碍于市面上工具都相对完善,并且自己做的话有开发和部署成本,就迟迟没落地。现在有ai提高开发效率(这个项目大部分代码来自minimax-M3 coding),并且确实是一个合理且实在的需求(因为现在对于webgl/webgpu的开发需求越来越大),然后再优化一下部署流程,就促成了这次 shaderPad 的开发和上线
第一版先这样,后面会持续关注平台的使用情况,如果用户量上来的话,会考虑扩展更多功能