Initial commit
This commit is contained in:
145
skills/web-shader-extractor/SKILL.md
Executable file
145
skills/web-shader-extractor/SKILL.md
Executable file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: web-shader-extractor
|
||||
description: |
|
||||
从网页中提取 WebGL/Canvas/Shader 视觉特效代码,反混淆后移植为独立原生 JS 项目。
|
||||
触发条件:用户提供网址并要求提取 shader、提取特效、提取动画效果、提取 canvas 效果、
|
||||
复刻某网站的视觉效果、"把这个网站的背景效果扒下来" 等。
|
||||
---
|
||||
|
||||
# Web Shader Extractor
|
||||
|
||||
从网页提取 WebGL/Canvas/Shader 特效,反混淆并移植为独立项目。
|
||||
|
||||
核心原则:
|
||||
- **先 1:1 复刻,确认正确后再考虑简化框架**
|
||||
- **全程自主执行,不中断用户** — 提取是只读操作,安全性无风险。除 Phase 6 简化提议外,所有步骤自动完成,不询问用户确认。遇到问题自行判断最佳方案继续推进,只在需要用户做产品决策时才询问。
|
||||
|
||||
## Phase 0: 环境检查(首次自动执行)
|
||||
|
||||
在开始提取前,检查并自动安装所需依赖。**不要询问用户,直接安装**。
|
||||
|
||||
```bash
|
||||
# 1. 检查 Node.js
|
||||
node --version 2>/dev/null || {
|
||||
echo "Node.js not found, installing..."
|
||||
# macOS
|
||||
brew install node 2>/dev/null || {
|
||||
# fallback: 直接下载 LTS
|
||||
curl -fsSL https://nodejs.org/dist/v22.15.0/node-v22.15.0-darwin-arm64.tar.gz | tar xz -C /usr/local --strip-components=1
|
||||
}
|
||||
}
|
||||
|
||||
# 2. Playwright 及浏览器(fetch-rendered-dom.mjs 内置自动安装,但这里预检可提前发现问题)
|
||||
RUNNER_DIR="$HOME/.cache/playwright-runner"
|
||||
if [ ! -d "$RUNNER_DIR/node_modules/playwright" ]; then
|
||||
echo "Installing Playwright (one-time setup)..."
|
||||
mkdir -p "$RUNNER_DIR"
|
||||
echo '{"type":"module"}' > "$RUNNER_DIR/package.json"
|
||||
npm install playwright --prefix "$RUNNER_DIR"
|
||||
npx --prefix "$RUNNER_DIR" playwright install chromium
|
||||
echo "Playwright + Chromium installed."
|
||||
fi
|
||||
```
|
||||
|
||||
如果安装过程中遇到权限或网络问题,尝试以下备选方案:
|
||||
- npm 权限问题 → 使用 `--prefix` 安装到用户目录
|
||||
- 网络问题(Chromium 下载慢)→ 设置 `PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright` 使用国内镜像
|
||||
- 实在无法安装 Playwright → 降级为纯 curl 模式(跳过 DOM 渲染,仅分析静态 HTML + JS bundle),在 Phase 2 中标注可能缺失 canvas-info
|
||||
|
||||
## Phase 1: 获取源码
|
||||
|
||||
**并行执行**:Playwright 获取渲染后 DOM + curl 获取静态 HTML。
|
||||
|
||||
```bash
|
||||
# Playwright(获取 canvas 引擎版本、组件树、运行时网络请求)
|
||||
node ~/.claude/skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs '<URL>'
|
||||
# → /tmp/rendered/: dom.html, canvas-info.json, network.json, screenshot.png, console.log
|
||||
|
||||
# curl(获取原始 HTML,用于提取内嵌配置和密钥)
|
||||
curl -s -L --compressed '<URL>' > /tmp/page.html
|
||||
```
|
||||
|
||||
如果 Playwright 脚本失败(未安装/启动异常),先尝试自动修复(重新安装依赖),若仍失败则降级为纯 curl 模式继续工作,不要停下来询问用户。
|
||||
|
||||
从 network.json 和 HTML 交叉提取 JS URL,批量下载到 /tmp/。
|
||||
|
||||
### Phase 2: 技术栈识别
|
||||
|
||||
```
|
||||
canvas-info.json 的 dataEngine 字段:
|
||||
├─ "three.js rXXX" → Three.js(r170+ 可能是 TSL → references/tsl-extraction.md)
|
||||
├─ "Babylon.js vX.X" → Babylon.js
|
||||
├─ null → 进一步区分:
|
||||
│ ├─ bundle 含 createShader/shaderSource → Raw WebGL / PixiJS
|
||||
│ └─ bundle 含 getContext('2d') 且无 WebGL 调用 → 2D Canvas(→ references/porting-strategy.md § 2D Canvas)
|
||||
└─ 无 canvas → CSS/SVG 动画
|
||||
|
||||
URL 或 HTML 特征匹配已知平台 → 直接跳转专用工作流(跳过通用 Phase 3-4):
|
||||
├─ unicorn.studio → references/unicorn-studio.md(Firestore REST API 直取配置+shader)
|
||||
└─ shaders.com → references/shaders-com.md(Nuxt payload + XOR 解码 + TSL→GLSL 翻译)
|
||||
|
||||
扫描确认:bash scripts/scan-bundle.sh /tmp/*.js
|
||||
→ 框架特征速查 references/tech-signatures.md
|
||||
```
|
||||
|
||||
### Phase 3: 配置提取
|
||||
|
||||
```
|
||||
1. 搜索公开 API → 直接获取配置(API 返回可能是编码的 → references/encoded-definitions.md)
|
||||
2. 从 Nuxt payload / __NEXT_DATA__ / HTML 内嵌 JSON 提取
|
||||
3. 从 JS bundle 提取默认值
|
||||
→ 详见 references/config-extraction.md
|
||||
```
|
||||
|
||||
### Phase 4: Shader 代码提取
|
||||
|
||||
用 **Agent** 分析 JS bundle(1MB+ 不适合主上下文)。
|
||||
→ Agent prompt 模板和反混淆规则 `references/extraction-workflow.md`
|
||||
|
||||
### Phase 5: 移植
|
||||
|
||||
```
|
||||
纯 2D 全屏 shader → 原生 WebGL2(零依赖)
|
||||
3D / PBR / GPGPU → 保留原始框架(CDN importmap)
|
||||
不确定 → 先用原始框架,Phase 6 再评估
|
||||
→ 详见 references/porting-strategy.md
|
||||
```
|
||||
|
||||
### Phase 6: 简化评估
|
||||
|
||||
移植完成后,自行验证效果(打开页面截图对比)。如果效果正确且存在简化空间,**向用户提议简化方案**,由用户决定是否执行。
|
||||
|
||||
### Phase 7: 提取报告(询问用户是否生成)
|
||||
|
||||
提取完成后,**询问用户**是否生成 `EXTRACTION-REPORT.md`(会消耗额外 token 回顾对话历史)。
|
||||
|
||||
报告内容结构:
|
||||
```markdown
|
||||
# 提取报告:{项目名}
|
||||
**来源/作者/平台/时间**
|
||||
|
||||
## 目标效果(一句话描述)
|
||||
## 提取思路与时间线(每个迭代的问题→修复)
|
||||
## 场景结构(组件树/图层结构)
|
||||
## 最终渲染管线(pass 列表)
|
||||
## 关键资源文件
|
||||
## 发现的关键经验(表格:经验/影响/沉淀位置)
|
||||
## 剩余已知差异
|
||||
## 技术栈(原始 vs 移植)
|
||||
```
|
||||
|
||||
报告放在项目目录内(如 `ascii-glyph-dither/EXTRACTION-REPORT.md`)。
|
||||
|
||||
## Reference 索引
|
||||
|
||||
| 需要时 | 读取 |
|
||||
|--------|------|
|
||||
| 识别框架(Three.js/WebGL/PixiJS 特征) | `references/tech-signatures.md` |
|
||||
| Agent 提取 prompt + 反混淆规则 | `references/extraction-workflow.md` |
|
||||
| 获取配置参数(API/payload/内嵌) | `references/config-extraction.md` |
|
||||
| Three.js TSL 节点 shader 重建 | `references/tsl-extraction.md` |
|
||||
| 编码/加密配置解码 | `references/encoded-definitions.md` |
|
||||
| onBeforeCompile GLSL 注入陷阱 | `references/shader-injection.md` |
|
||||
| 移植框架选择 + 项目结构 | `references/porting-strategy.md` |
|
||||
| **Unicorn Studio** 专用流程(curtains.js + Firestore) | `references/unicorn-studio.md` |
|
||||
| **shaders.com** 专用流程(TSL + XOR 编码 + Y-flip 陷阱) | `references/shaders-com.md` |
|
||||
50
skills/web-shader-extractor/references/config-extraction.md
Executable file
50
skills/web-shader-extractor/references/config-extraction.md
Executable file
@@ -0,0 +1,50 @@
|
||||
# 配置参数提取
|
||||
|
||||
配置值必须从源站提取,不猜测。(猜错实例:颜色差 60x、图案完全不同)
|
||||
|
||||
## 来源(按优先级)
|
||||
|
||||
### 1. 公开 REST API
|
||||
|
||||
```bash
|
||||
# 在 JS bundle 中搜索 API 端点
|
||||
grep -oE 'api/(presets|shaders|collections)[^"]*' /tmp/*.js
|
||||
# 直接调用
|
||||
curl -s -L --compressed 'https://example.com/api/collections/slug/uuid'
|
||||
```
|
||||
|
||||
API 返回可能是编码的 → 见 `encoded-definitions.md`
|
||||
|
||||
### 2. Nuxt.js Payload
|
||||
|
||||
```bash
|
||||
grep -oE '_payload\.json[^"]*' /tmp/page.html # payload URL
|
||||
grep -oE 'public:\{[^}]*\}' /tmp/page.html # runtime config(可能含密钥)
|
||||
```
|
||||
|
||||
### 3. Next.js
|
||||
|
||||
```bash
|
||||
# App Router (RSC)
|
||||
grep -oE '"(scene|glass|postProcessing)":\{' /tmp/page.html
|
||||
# Pages Router
|
||||
grep -o '<script id="__NEXT_DATA__"[^>]*>[^<]*' /tmp/page.html | sed 's/.*>//'
|
||||
```
|
||||
|
||||
### 4. 内联 JSON / window 全局变量
|
||||
|
||||
```bash
|
||||
grep -oE 'window\.__CONFIG__\s*=\s*\{[^;]+' /tmp/page.html
|
||||
```
|
||||
|
||||
### 5. JS Bundle 默认值(最后手段)
|
||||
|
||||
```bash
|
||||
grep -oE '(config|options|settings)\s*=\s*\{' /tmp/entry-chunk.js
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
- 颜色范围:0-1 还是 0-255?
|
||||
- resolution:像素值还是比例系数?
|
||||
- 布尔值:`false` 是否跳过整个渲染 pass?
|
||||
53
skills/web-shader-extractor/references/encoded-definitions.md
Executable file
53
skills/web-shader-extractor/references/encoded-definitions.md
Executable file
@@ -0,0 +1,53 @@
|
||||
# 编码配置解码
|
||||
|
||||
## 识别信号
|
||||
|
||||
1. API 返回有 `_encoded: true` 标志
|
||||
2. `definition` 字段是 Base64 字符串而非 JSON
|
||||
3. JS 中有 `atob()`/`btoa()` + `TextEncoder`/`TextDecoder` + XOR
|
||||
4. Runtime config 中有 `obfuscationKey`
|
||||
|
||||
```bash
|
||||
grep -oE '(atob|btoa|obfuscation|_encoded)' /tmp/*.js | sort | uniq -c
|
||||
grep -oE 'obfuscationKey:"[^"]*"' /tmp/page.html
|
||||
```
|
||||
|
||||
## 常见方案
|
||||
|
||||
### Base64 + XOR
|
||||
|
||||
```python
|
||||
import base64, json
|
||||
|
||||
def decode(encoded, key):
|
||||
raw = base64.b64decode(encoded)
|
||||
key_bytes = key.encode('utf-8')
|
||||
decrypted = bytes([raw[i] ^ key_bytes[i % len(key_bytes)] for i in range(len(raw))])
|
||||
return json.loads(decrypted.decode('utf-8'))
|
||||
```
|
||||
|
||||
### 短码映射(组件/属性名缩写)
|
||||
|
||||
配置中 `C74`/`p29` 代替 `StudioBackground`/`color`。
|
||||
|
||||
```bash
|
||||
grep -oE '(codeToComponent|codeToProp)' /tmp/bundle.js
|
||||
```
|
||||
|
||||
映射表通常是动态生成的——所有名称按字母排序分配 `C{nn}`/`p{nn}` 编号。
|
||||
|
||||
## 密钥来源
|
||||
|
||||
| 框架 | 位置 |
|
||||
|------|------|
|
||||
| Nuxt.js | `public:{obfuscationKey:"..."}` in HTML |
|
||||
| Next.js | `__NEXT_DATA__` 的 runtimeConfig |
|
||||
| SPA | bundle 常量或 `window.__CONFIG__` |
|
||||
|
||||
## 查找解码函数
|
||||
|
||||
```bash
|
||||
grep -l '_encoded' /tmp/*.js
|
||||
grep -A3 '_encoded' /tmp/bundle.js
|
||||
# 模式:if (t._encoded) { return decode(t.definition, key) }
|
||||
```
|
||||
61
skills/web-shader-extractor/references/extraction-workflow.md
Executable file
61
skills/web-shader-extractor/references/extraction-workflow.md
Executable file
@@ -0,0 +1,61 @@
|
||||
# Shader 代码提取:Agent Prompt 与反混淆
|
||||
|
||||
## Agent 深度提取 Prompt 模板
|
||||
|
||||
启动 Agent(subagent_type: general-purpose)分析 bundle:
|
||||
|
||||
```
|
||||
分析文件 /tmp/main.js(约 X MB 的 minified JS bundle),提取所有与视觉特效相关的代码。
|
||||
|
||||
需要提取的内容:
|
||||
|
||||
1. **GLSL Shader 源码**:搜索包含 "precision", "uniform", "void main",
|
||||
"gl_FragColor", "gl_Position" 的字符串。提取完整的 vertex/fragment shader。
|
||||
|
||||
2. **渲染相关 JS 类**:canvas/renderer 创建、粒子/几何体管理、
|
||||
requestAnimationFrame 动画循环、鼠标交互代码。
|
||||
|
||||
3. **符号映射表**:minified 变量名 → 原始含义
|
||||
(THREE.Vector2, THREE.Color, THREE.Scene 等)
|
||||
|
||||
4. **配置和参数**:默认颜色、尺寸、密度等可调参数
|
||||
|
||||
将所有提取的代码保存到 /tmp/extracted-effects.txt,按功能分段标注。
|
||||
```
|
||||
|
||||
**关键**:Agent 能看到完整文件,主上下文放不下 1MB+ 的 bundle。
|
||||
|
||||
## 识别 Minified 符号
|
||||
|
||||
通过构造参数和方法调用推断:
|
||||
|
||||
```javascript
|
||||
new ??(40, w/h, 0.1, 1000) → PerspectiveCamera(fov, aspect, near, far)
|
||||
new ??(-1, 1, 1, -1, 0, 1) → OrthographicCamera
|
||||
new ??({canvas, antialias, ...}) → WebGLRenderer
|
||||
new ??(2, 2) → PlaneGeometry(w, h)
|
||||
new ??(data, w, h, fmt, type) → DataTexture
|
||||
new ??(w, h, {minFilter, ...}) → WebGLRenderTarget
|
||||
|
||||
??.setRenderTarget() → renderer
|
||||
??.getElapsedTime() → clock
|
||||
??.setAttribute() → bufferGeometry
|
||||
```
|
||||
|
||||
## 框架脱壳(React / Vue 等 → Vanilla JS)
|
||||
|
||||
Canvas 效果常被 React/Vue 等框架包装。脱壳思路:
|
||||
|
||||
1. **找副作用入口**:`useEffect`/`onMounted` 中的 Canvas 初始化代码就是核心逻辑
|
||||
2. **收集 cleanup**:所有销毁操作(`removeEventListener`、`cancelAnimationFrame`、`observer.disconnect()`)汇总为 `destroy()` 函数
|
||||
3. **丢弃响应式包装**:Canvas 状态(粒子位置、帧计数等)只在 RAF 内读写,直接用 `let` 变量,不需要 `useState`/`ref` 等响应式容器
|
||||
|
||||
原生 API(`IntersectionObserver`、`ResizeObserver`、`matchMedia` 等)不受框架影响,原样保留。
|
||||
|
||||
## 反混淆规则
|
||||
|
||||
1. **类名**:根据构造参数和方法调用推断
|
||||
2. **变量名**:根据用途命名(`ringPos`, `particleScale`, `simMaterial`)
|
||||
3. **Shader 变量**:uniform/varying 名通常未被 minify(`uTime`, `vPosition`)
|
||||
4. **保留原始 GLSL**:shader 代码通常是完整字符串,直接提取
|
||||
5. **字符串注入**:`${someVar.noise}` 表示噪声库被注入到 shader 中
|
||||
164
skills/web-shader-extractor/references/porting-strategy.md
Executable file
164
skills/web-shader-extractor/references/porting-strategy.md
Executable file
@@ -0,0 +1,164 @@
|
||||
# 移植策略
|
||||
|
||||
## 框架选择
|
||||
|
||||
```
|
||||
纯 2D Canvas(getContext('2d'),无 WebGL)?
|
||||
├─ YES → Vanilla JS(零依赖,见下方 § 2D Canvas)
|
||||
└─ NO → 纯 2D 全屏 shader / 后处理?
|
||||
├─ YES → 原生 WebGL2(零依赖)
|
||||
└─ NO → 涉及 3D / PBR / GPGPU / onBeforeCompile?
|
||||
├─ YES → 保留原始框架(CDN importmap)
|
||||
└─ 不确定 → 先用原始框架,Phase 6 再评估
|
||||
```
|
||||
|
||||
## 原生 WebGL 项目结构
|
||||
|
||||
```
|
||||
<name>/
|
||||
├── index.html # <canvas>
|
||||
├── js/
|
||||
│ ├── main.js # WebGL2 初始化 + 多 pass 渲染循环
|
||||
│ └── shaders/ # .glsl.js(export const fragmentShader)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
- `canvas.getContext('webgl2')` + `#version 300 es`(`in`/`out`、`texture()`)
|
||||
- 多 pass 用 framebuffer + texture attachment
|
||||
- `requestAnimationFrame` 驱动
|
||||
|
||||
## Three.js 项目结构
|
||||
|
||||
```
|
||||
<name>/
|
||||
├── index.html # importmap CDN
|
||||
├── js/
|
||||
│ ├── main.js # 场景/相机/渲染器 + RTT 管线
|
||||
│ └── shaders/ # .glsl.js
|
||||
└── README.md
|
||||
```
|
||||
|
||||
- `RawShaderMaterial` + `glslVersion: THREE.GLSL3`
|
||||
- `WebGLRenderTarget` 做多 pass
|
||||
- CDN importmap,零安装
|
||||
|
||||
CDN 模板:
|
||||
```html
|
||||
<script type="importmap">
|
||||
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js" } }
|
||||
</script>
|
||||
```
|
||||
|
||||
验证:`curl -sI '<url>' | head -3`
|
||||
|
||||
## 多层合成场景移植要点
|
||||
|
||||
从 Unicorn Studio / curtains.js 等 no-code 工具提取的场景通常有复杂的 FBO 链。以下是常见陷阱和正确做法:
|
||||
|
||||
### 1. 理解 parentLayer 与 effects 的父子关系
|
||||
|
||||
在 Unicorn Studio 中:
|
||||
- **Element 层**(shape/text/image)的 `effects[]` 数组存储子效果的 UUID
|
||||
- **Effect 层**的 `parentLayer` 字段指向父元素的 UUID
|
||||
- 子效果按 effects 数组顺序依次渲染,每个 pass 读取前一个 pass 的 FBO
|
||||
|
||||
移植时必须还原这个链式 FBO 结构,不能简单合并成一个 pass。
|
||||
|
||||
### 2. showBg=0 的透明背景
|
||||
|
||||
`showBg=0` 意味着 shader 在未命中几何体的区域输出 `vec4(0)`(完全透明)。
|
||||
这不是"黑色",而是**透明**,后续合成时会显示下方图层。
|
||||
|
||||
```glsl
|
||||
// 正确:showBg=0 → 透明
|
||||
if (hit < 0.5) { fragColor = vec4(0.0); return; }
|
||||
|
||||
// 错误:输出黑色(会覆盖下方图层)
|
||||
if (hit < 0.5) { fragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }
|
||||
```
|
||||
|
||||
移植时必须用 alpha composite pass(`fg + bg * (1 - fg.a)`)将结果叠加到下方图层。
|
||||
|
||||
### 3. 文字/图片元素的合成方式
|
||||
|
||||
Element 层(text/image)需要用 Canvas 2D 渲染后作为纹理上传 WebGL。
|
||||
**合成方式决定了视觉效果**:
|
||||
|
||||
```glsl
|
||||
// 错误:alpha-over 覆盖(丢失背景纹理)
|
||||
fragColor = mix(bg, vec4(txt.rgb, 1.0), txt.a);
|
||||
|
||||
// 错误:additive(饱和为白色,丢失色彩变化)
|
||||
fragColor = vec4(bg.rgb + txt.rgb * txt.a, 1.0);
|
||||
|
||||
// 正确:亮度放大(保留背景噪声的色彩纹理变化)
|
||||
fragColor = vec4(bg.rgb * mix(1.0, amplifyFactor, txt.a), 1.0);
|
||||
```
|
||||
|
||||
**原理**:原始场景中文字元素叠加在噪声层上,经过 glyph dither 后,
|
||||
字符颜色取自该位置的像素色。如果文字区域是平坦单色,ASCII 字符就是单色的。
|
||||
用亮度放大方式,噪声的色相比例完整保留(紫:青:暗 按相同系数放大),
|
||||
glyph dither 后的字符就带有噪声纹理的色彩变化。
|
||||
|
||||
### 4. 重复效果实例
|
||||
|
||||
同一种效果(如 noiseFill)可能在场景中出现多次:一次作为背景独立层,
|
||||
一次作为 shape group 的子效果。参数可能相同但在管线中位置不同,
|
||||
子效果的输出会被后续效果(如 SDF 折射)处理,产生不同的视觉。
|
||||
|
||||
### 5. Glyph Atlas 兼容性
|
||||
|
||||
base64 内嵌的 PNG glyph atlas 在某些浏览器/WebGL 环境中 `texImage2D` 会报
|
||||
`INVALID_VALUE: bad image data`。推荐用 Canvas 2D 动态生成。
|
||||
|
||||
### 6. 颜色空间一致性(最常见的视觉偏差来源)
|
||||
|
||||
Three.js / shaders.com 等工具全程在 **linear 空间** 工作。移植到原生 WebGL 时:
|
||||
|
||||
```
|
||||
错误做法(每个 pass 独立 gamma):
|
||||
Pass1: 输出 pow(linear, 1/2.2) ← sRGB
|
||||
Pass2: 读入 sRGB + 计算高光(linear) + 输出 pow(result, 1/2.2) ← 混乱!
|
||||
|
||||
正确做法(全程 linear,最终一次 gamma):
|
||||
Pass1~N: 全部输出 linear 值
|
||||
Final: pow(linear, 1/2.2) ← 唯一一次 sRGB 编码
|
||||
```
|
||||
|
||||
hex 颜色定义 → `pow(srgb, 2.2)` 转 linear → 全程 linear 计算 → 最终 `pow(linear, 1/2.2)` 输出。
|
||||
|
||||
### 7. 参数精确对齐原则
|
||||
|
||||
**绝对禁止手动调参来"补偿"视觉差异**。所有公式乘数必须与原始代码完全一致。
|
||||
如果效果不对,应排查颜色空间、噪声实现、时间基准等根因,而不是改乘数。
|
||||
手动调参在当前配置下可能看起来更好,但会在其他参数组合下崩溃。
|
||||
|
||||
## 2D Canvas
|
||||
|
||||
每个效果一个文件,导出 `create<Name>Effect(container)` → `{ destroy }`。多效果用 `main.js` 管理切换。
|
||||
|
||||
### 性能要求
|
||||
|
||||
- `IntersectionObserver` + `visibilitychange` — 不可见时停止 RAF
|
||||
- DPR 上限 `Math.min(devicePixelRatio, 2)`
|
||||
- 后处理用离屏 Canvas 缓存,静态内容仅 resize 时重建
|
||||
- 大量粒子数据用 `Float32Array`
|
||||
|
||||
## 通用规范
|
||||
|
||||
- ES Module(`import`/`export`)
|
||||
- minified 变量名替换为有意义名称
|
||||
- README 含效果说明、技术原理、可调参数
|
||||
|
||||
## Phase 6:简化评估
|
||||
|
||||
**触发**:移植完成后自行验证效果正确。**提议而非自动执行** — 这是全流程唯一需要用户决策的步骤。
|
||||
|
||||
```
|
||||
只用了 RawShaderMaterial + WebGLRenderTarget + fullscreen quad?
|
||||
├─ → 可简化为原生 WebGL2(减少 ~600KB)
|
||||
用到 PBR / onBeforeCompile / 3D 场景?
|
||||
├─ → 不简化
|
||||
不确定?
|
||||
└─ → 不提议
|
||||
```
|
||||
126
skills/web-shader-extractor/references/shader-injection.md
Executable file
126
skills/web-shader-extractor/references/shader-injection.md
Executable file
@@ -0,0 +1,126 @@
|
||||
# onBeforeCompile 注入 GLSL 的陷阱
|
||||
|
||||
## 场景
|
||||
|
||||
使用 `MeshPhysicalMaterial` 的 `transmission` 功能但需要增强效果时(如 drei 的 MeshTransmissionMaterial),
|
||||
通过 `material.onBeforeCompile` 注入自定义 GLSL 代码。
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
### 1. 函数签名版本差异
|
||||
|
||||
Three.js 不同版本的内置函数签名不同:
|
||||
|
||||
```glsl
|
||||
// r166 及之前
|
||||
vec4 getIBLVolumeRefraction(n, v, roughness, diffuseColor, specularColor, specularF90,
|
||||
pos, modelMatrix, viewMatrix, projectionMatrix, ior, thickness,
|
||||
attenuationColor, attenuationDistance)
|
||||
|
||||
// r167+ 新增 dispersion 参数
|
||||
vec4 getIBLVolumeRefraction(n, v, roughness, diffuseColor, specularColor, specularF90,
|
||||
pos, modelMatrix, viewMatrix, projectionMatrix, dispersion, ior, thickness,
|
||||
attenuationColor, attenuationDistance)
|
||||
```
|
||||
|
||||
**必须检查目标版本的实际签名**:
|
||||
```bash
|
||||
curl -s "https://cdn.jsdelivr.net/npm/three@0.167.0/src/renderers/shaders/ShaderChunk/transmission_pars_fragment.glsl.js" \
|
||||
| tr '\n' ' ' | grep -oE 'vec4 getIBLVolumeRefraction\([^)]+\)'
|
||||
```
|
||||
|
||||
### 2. GLSL 不允许嵌套函数定义
|
||||
|
||||
```glsl
|
||||
// 错误!GLSL 不支持函数内定义函数
|
||||
void main() {
|
||||
float myRand(vec2 co) { return fract(sin(...)); } // 编译失败
|
||||
}
|
||||
|
||||
// 正确:函数必须在全局作用域
|
||||
float myRand(vec2 co) { return fract(sin(...)); }
|
||||
void main() {
|
||||
float r = myRand(uv);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 条件编译宏保护
|
||||
|
||||
某些变量只在特定宏下可用:
|
||||
- `vWorldPosition` → 需要 `USE_TRANSMISSION` 启用
|
||||
- `vTransmissionMapUv` → 需要 `USE_TRANSMISSIONMAP` 启用
|
||||
- `roughnessFactor` → 在 `lights_physical_fragment` 之后可用
|
||||
|
||||
```glsl
|
||||
// 在替换 #include <transmission_fragment> 时
|
||||
// 原始代码自带 #ifdef USE_TRANSMISSION,替换代码也必须包含
|
||||
#ifdef USE_TRANSMISSION
|
||||
// ... 你的代码
|
||||
#endif
|
||||
```
|
||||
|
||||
### 4. 变量名冲突
|
||||
|
||||
注入的全局函数/变量可能与 Three.js 内部冲突:
|
||||
- 避免使用 `hash`, `random`, `noise` 等通用名
|
||||
- 自定义函数加前缀:`snoise` → OK,`random` → 可能冲突
|
||||
- uniform 名称加前缀 `u`:`uDistortion`, `uNoiseTime`
|
||||
|
||||
## 推荐模式
|
||||
|
||||
### 安全注入:修改 normal 而不替换整个 chunk
|
||||
|
||||
```javascript
|
||||
material.onBeforeCompile = (shader) => {
|
||||
shader.uniforms.uDistortion = { value: 0 };
|
||||
shader.uniforms.uNoiseTime = { value: 0 };
|
||||
|
||||
// 在 fragment shader 最前面加 uniform 声明 + 工具函数
|
||||
shader.fragmentShader = `
|
||||
uniform float uDistortion;
|
||||
uniform float uNoiseTime;
|
||||
${noiseGLSL}
|
||||
` + shader.fragmentShader;
|
||||
|
||||
// 在 transmission_fragment 之前插入法线扰动
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
'#include <transmission_fragment>',
|
||||
`
|
||||
#ifdef USE_TRANSMISSION
|
||||
{
|
||||
// 扰动 normal 影响折射方向
|
||||
if (uDistortion > 0.0) {
|
||||
normal = normalize(normal + uDistortion * vec3(
|
||||
snoiseFractal(vWorldPosition * 0.08 + vec3(uNoiseTime)),
|
||||
snoiseFractal(vWorldPosition.zxy * 0.08 - vec3(uNoiseTime)),
|
||||
snoiseFractal(vWorldPosition.yxz * 0.08)
|
||||
));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#include <transmission_fragment>
|
||||
`
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 完整替换:需要随机多采样 + 色差时
|
||||
|
||||
当需要 MeshTransmissionMaterial 的颗粒感(随机采样噪声)和色差效果时,
|
||||
必须完整替换 `#include <transmission_fragment>`。关键点:
|
||||
|
||||
1. 保留 `#ifdef USE_TRANSMISSION` / `#endif` 包裹
|
||||
2. 保留 transmissionMap 和 thicknessMap 的 `#ifdef` 块
|
||||
3. 使用正确版本的 `getIBLVolumeRefraction` 签名
|
||||
4. 自己处理色差时,传 `dispersion = 0.0`,用不同 IOR 采样 R/G/B
|
||||
5. 低采样数(6)+ 每像素随机偏移 → 产生可见的胶片颗粒感
|
||||
|
||||
## 视觉效果来源速查
|
||||
|
||||
| 效果 | 来源 | 实现方式 |
|
||||
|------|------|----------|
|
||||
| 玻璃折射 | MeshPhysicalMaterial `transmission` | Three.js 内置 |
|
||||
| 色差 (chromatic aberration) | 不同 IOR 采样 R/G/B | 替换 transmission_fragment |
|
||||
| 胶片颗粒感 | 低采样数 + 每像素随机方向 | 替换 transmission_fragment |
|
||||
| 有机扭曲 | simplex noise 扰动法线/折射方向 | onBeforeCompile 注入 |
|
||||
| 颜色偏移 | `dispersion` 属性 (r167+) | MeshPhysicalMaterial 内置 |
|
||||
190
skills/web-shader-extractor/references/shaders-com.md
Executable file
190
skills/web-shader-extractor/references/shaders-com.md
Executable file
@@ -0,0 +1,190 @@
|
||||
# shaders.com 提取工作流
|
||||
|
||||
shaders.com 是一个 shader 设计工具,使用 Nuxt.js + Three.js r183 TSL + Supabase。
|
||||
|
||||
## 识别特征
|
||||
|
||||
- URL: `shaders.com/collection/{slug}/{presetId}` 或 `shaders.com/preset/{id}`
|
||||
- Canvas: `data-renderer="shaders"` + `data-engine="three.js r183"`
|
||||
- Nuxt.js (`_nuxt/` 路径)
|
||||
- Clerk 认证
|
||||
- Supabase 存储 (`data.shaders.com/storage/v1/`)
|
||||
|
||||
## 关键架构差异
|
||||
|
||||
与 Unicorn Studio 完全不同:
|
||||
- **不使用 GLSL** — 使用 Three.js TSL (Three Shader Language) 节点系统
|
||||
- **87 种组件类型** — 每种有自己的 TSL `fragmentNode` 函数
|
||||
- **定义数据是 XOR + base64 编码的**
|
||||
- **组件可嵌套** — 树形结构(Glass 的 children 是其内部效果)
|
||||
|
||||
## 数据获取
|
||||
|
||||
### API 端点
|
||||
|
||||
```bash
|
||||
# 集合变体(含编码定义)— 公开,无需认证
|
||||
curl -s "https://shaders.com/api/collections/{slug}/{variantId}"
|
||||
|
||||
# 预览 API(含编码定义 + 水印注入)
|
||||
curl -s "https://shaders.com/api/preview/preset/{presetId}"
|
||||
|
||||
# Nuxt payload(只含元数据,不含 shader 定义)
|
||||
curl -s "https://shaders.com/collection/{slug}/{id}/_payload.json"
|
||||
```
|
||||
|
||||
### 定义解码
|
||||
|
||||
定义使用 XOR + base64 编码,有两套密钥:
|
||||
|
||||
1. **网站 API**(`/api/collections/`):
|
||||
- 混淆密钥: `a5e7244ad0973f07e10285bfa75ddbe4`(来自 Nuxt runtime config)
|
||||
- 组件/属性名用短代码(`C52`=Plasma, `p06`=angle, 等)
|
||||
- 解码: `JSON.parse(XOR(base64decode(encoded), keyBytes))`
|
||||
- 然后需要 code→name 映射表还原可读名称
|
||||
|
||||
2. **预览 API**(`/api/preview/`):
|
||||
- 密钥: `shaders-preview-key`
|
||||
- 使用人类可读属性名(无需映射)
|
||||
- 注意:会注入水印 `ImageTexture` 组件
|
||||
|
||||
### 代码映射表
|
||||
|
||||
87 种组件按字母排序编号 `C00-C86`,233 种属性按字母排序编号 `p00-p232`。
|
||||
映射表可从 JS bundle 中提取。
|
||||
|
||||
## 已知陷阱
|
||||
|
||||
### Y 轴翻转(反复出现!)
|
||||
|
||||
**SDF 纹理和 UV 坐标系统性 Y 翻转** — 已在多次提取中确认:
|
||||
|
||||
shaders.com 的 SDF 二进制(`.bin`)使用**图像坐标系**(Y=0 在顶部),
|
||||
而 WebGL 纹理坐标 Y=0 在底部。直接加载会导致形状上下翻转。
|
||||
|
||||
```glsl
|
||||
// 错误:直接用 shapeUV 采样
|
||||
float sdf = texture(tSDF, shapeUV).r;
|
||||
|
||||
// 正确:翻转 Y
|
||||
vec2 sdfUV = vec2(shapeUV.x, 1.0 - shapeUV.y);
|
||||
float sdf = texture(tSDF, sdfUV).r;
|
||||
|
||||
// 注意:梯度的 Y 分量也需要取反
|
||||
float dSdy = -(texture(tSDF, sdfUV - vec2(0, eps)).r - sdf) / eps;
|
||||
```
|
||||
|
||||
同样,组件定义中的 `center.y` 使用 DOM 坐标(Y=0 在顶部),
|
||||
在 Glass shader 中需要翻转:`center.y = 1.0 - center.y`。
|
||||
|
||||
### SDF 二进制格式
|
||||
|
||||
- 格式:512×512 Float32 单通道(1,048,576 bytes = 512² × 4)
|
||||
- 值域:有符号距离,负值=内部,正值=外部(如 [-0.065, 0.486])
|
||||
- **不需要重映射**(不要做 `*2-1`),直接使用原始值
|
||||
- 需要 `OES_texture_float_linear` 扩展做线性过滤
|
||||
- WebGL2 加载:`gl.texImage2D(gl.TEXTURE_2D, 0, gl.R32F, 512, 512, 0, gl.RED, gl.FLOAT, data)`
|
||||
|
||||
## 组件类型速查
|
||||
|
||||
| 类别 | 组件 | 复杂度 |
|
||||
|------|------|--------|
|
||||
| 纹理 | Plasma, Godrays, SimplexNoise, LinearGradient, RadialGradient | 中 |
|
||||
| 形状 | Glass, Blob, Circle, Ring, Star, RoundedRect, Polygon | 高(Glass 最复杂) |
|
||||
| 畸变 | WaveDistortion, ChromaticAberration, Liquify, Twirl, Bulge | 低-中 |
|
||||
| 风格化 | FilmGrain, Halftone, Ascii, Dither, Glow, Bloom | 低-中 |
|
||||
| 后处理 | Blur, ProgressiveBlur, BrightnessContrast, HueShift | 低 |
|
||||
|
||||
## 渲染管线
|
||||
|
||||
```
|
||||
Three.js r183 TSL 渲染器
|
||||
├─ 优先尝试 WebGPU,降级到 WebGL
|
||||
├─ 正交相机 + 单个全屏四边形
|
||||
├─ 组件树从底到顶合成
|
||||
├─ 有 children 的组件用 RTT (render-to-texture) 捕获子内容
|
||||
├─ blend mode 用自定义混合函数
|
||||
└─ Glass 组件最复杂:SDF 评估 → 梯度法线 → 折射 → 色差 → 模糊 → 着色 → 高光 → 菲涅尔 → 合成
|
||||
```
|
||||
|
||||
## 移植策略
|
||||
|
||||
1. **TSL 不能直接复制** — 需要翻译为 GLSL
|
||||
2. **从 JS bundle 提取 TSL `fragmentNode`** → 反混淆 → 翻译为 GLSL
|
||||
3. **组件树 → multi-pass FBO 管线**
|
||||
4. **SDF 纹理需要 Y-flip**(见上方陷阱)
|
||||
5. **Glass 组件参数多**(20+),需要精确匹配每个值
|
||||
|
||||
## 颜色空间处理(关键)
|
||||
|
||||
shaders.com 的 Three.js 渲染器全程在 **linear 空间** 工作:
|
||||
- 组件定义中的 hex 颜色(如 `#2c2c42`)是 **sRGB** 值
|
||||
- TSL 的 `color()` 函数自动将 sRGB→linear
|
||||
- 所有中间 FBO 均存储 linear 值
|
||||
- 最终由渲染器做 linear→sRGB 输出编码
|
||||
|
||||
移植时:
|
||||
```glsl
|
||||
// 1. 颜色定义时:sRGB hex → linear
|
||||
vec3 colorA = pow(vec3(0.173, 0.173, 0.259), vec3(2.2)); // #2c2c42
|
||||
|
||||
// 2. 中间 pass:全部在 linear 空间计算,不做 gamma
|
||||
// 3. 最终输出 pass(仅一次):linear → sRGB
|
||||
fragColor = vec4(pow(color.rgb, vec3(1.0/2.2)), color.a);
|
||||
```
|
||||
|
||||
**常见错误**:在中间 pass 做 gamma 校正,导致后续 pass 在错误空间累加高光/菲涅尔。
|
||||
|
||||
## 参数精确对齐原则
|
||||
|
||||
**绝对禁止手动调参**。所有参数必须严格匹配 TSL 翻译中的公式和乘数:
|
||||
|
||||
```
|
||||
TSL 原始乘数 → GLSL 必须使用的值
|
||||
aberration * 0.06 → 不能改为 0.12
|
||||
fresnelSoftness * 0.06 → 不能改为 0.12
|
||||
fresnel (0.17) → 不能改为 0.4
|
||||
SDF gradient eps = 0.01 → 不能改为 0.005
|
||||
```
|
||||
|
||||
如果视觉效果不匹配,应检查:
|
||||
1. 颜色空间是否正确(sRGB/linear 混乱是最常见原因)
|
||||
2. 噪声函数实现差异(Perlin 实现 vs `mx_noise_float`)
|
||||
3. 时间基准是否正确
|
||||
4. FBO 管线顺序是否与组件树匹配
|
||||
|
||||
**不要**通过修改乘数来"补偿"视觉差异 — 这会在其他参数配置下崩溃。
|
||||
|
||||
## TSL 时间约定
|
||||
|
||||
`timerLocal(speed)` = 每秒递增 `speed` 单位。移植时:`uTime = seconds * speed`。
|
||||
|
||||
然后 shader 内部再乘自己的系数:
|
||||
|
||||
| 组件 | speed 参数 | shader 内部乘数 | 实际速率/秒 |
|
||||
|------|-----------|----------------|------------|
|
||||
| Plasma | 2 | × 0.125 | 0.25 |
|
||||
| Godrays | 0.7 | × 0.2 | 0.14 |
|
||||
| WaveDistortion | 0.8 | × 0.5 | 0.4 |
|
||||
| FilmGrain | — | 无时间(静态) | 0 |
|
||||
|
||||
## TSL→GLSL 标识符映射(SPCVwBqR.js)
|
||||
|
||||
常用映射(随构建版本变化,需动态提取):
|
||||
|
||||
| 本地名 | TSL 函数 | GLSL |
|
||||
|--------|---------|------|
|
||||
| C / z | vec4() | vec4 |
|
||||
| x / D | vec2() | vec2 |
|
||||
| q / N | vec3() | vec3 |
|
||||
| P / J | resolution | u_resolution |
|
||||
| A / $ | uv | vUv |
|
||||
| se / Oe | sin() | sin() |
|
||||
| W / I | cos() | cos() |
|
||||
| ne | mix() | mix() |
|
||||
| D | smoothstep() | smoothstep() |
|
||||
| fe | clamp() | clamp() |
|
||||
| ar | mx_noise_float() | perlinNoise3D() |
|
||||
| dr / Gt | timerLocal() | u_time × speed |
|
||||
| Me / wt | rtt() | FBO pass |
|
||||
| Ce | renderOutput() | fragColor |
|
||||
54
skills/web-shader-extractor/references/tech-signatures.md
Executable file
54
skills/web-shader-extractor/references/tech-signatures.md
Executable file
@@ -0,0 +1,54 @@
|
||||
# 框架识别特征
|
||||
|
||||
## Three.js
|
||||
|
||||
**未混淆**:`THREE.`, `WebGLRenderer`, `ShaderMaterial`, `BufferGeometry`
|
||||
|
||||
**混淆后**(通过构造参数推断):
|
||||
|
||||
| 调用模式 | 原始类 |
|
||||
|---------|--------|
|
||||
| `new X({canvas, antialias, alpha})` | `WebGLRenderer` |
|
||||
| `new X(fov, aspect, near, far)` — 4 数字 | `PerspectiveCamera` |
|
||||
| `new X(-1, 1, 1, -1, 0, 1)` — 6 参数 | `OrthographicCamera` |
|
||||
| `new X(w, h, {wrapS, minFilter})` | `WebGLRenderTarget` |
|
||||
| `new X(data, w, h, format, type)` — Float32Array | `DataTexture` |
|
||||
| `new X(2, 2)` 作为几何体 | `PlaneGeometry` |
|
||||
| `new X({uniforms, vertexShader, fragmentShader})` | `ShaderMaterial` |
|
||||
| `X.getElapsedTime()` | `Clock` |
|
||||
|
||||
**常量**:`ClampToEdgeWrapping`, `NearestFilter`, `RGBAFormat`, `FloatType`, `DoubleSide`
|
||||
|
||||
## 2D Canvas
|
||||
|
||||
`dataEngine: null` 时,用 `getContext('2d')` 有无 + `createShader`/`shaderSource` 有无区分:
|
||||
- 有 `getContext('2d')`,无 WebGL 调用 → 纯 2D Canvas
|
||||
- 有 WebGL 调用 → Raw WebGL / PixiJS
|
||||
|
||||
## Raw WebGL
|
||||
|
||||
```javascript
|
||||
gl.createShader / gl.shaderSource / gl.compileShader / gl.createProgram
|
||||
gl.bindBuffer / gl.bindFramebuffer / gl.drawArrays
|
||||
```
|
||||
|
||||
## PixiJS
|
||||
|
||||
`PIXI.Application`, `PIXI.Filter`, `new PIXI.Filter(vertSrc, fragSrc, uniforms)`
|
||||
|
||||
## Babylon.js
|
||||
|
||||
`BABYLON.Engine`, `BABYLON.ShaderMaterial`, `BABYLON.Effect.ShadersStore`
|
||||
|
||||
## GPGPU 模式
|
||||
|
||||
两个 `WebGLRenderTarget`(ping-pong)+ `OrthographicCamera(-1,1,1,-1,0,1)` + `PlaneGeometry(2,2)` + `DataTexture` 初始位置 + `setRenderTarget` 循环
|
||||
|
||||
## 常见噪声
|
||||
|
||||
| 函数 | 类型 |
|
||||
|------|------|
|
||||
| `snoise` | Simplex noise (Ashima) |
|
||||
| `cnoise` | Classic Perlin |
|
||||
| `cellular` | Worley/Voronoi |
|
||||
| `fbm` | Fractal Brownian Motion |
|
||||
41
skills/web-shader-extractor/references/tsl-extraction.md
Executable file
41
skills/web-shader-extractor/references/tsl-extraction.md
Executable file
@@ -0,0 +1,41 @@
|
||||
# Three.js TSL 识别与重建
|
||||
|
||||
Three.js r170+ 的 TSL (Three Shading Language) 用 JS 函数调用链组合 shader 节点图,运行时编译为 GLSL。
|
||||
|
||||
## 识别信号
|
||||
|
||||
1. Bundle 有大量 `uniform`/`shader` 但几乎没有 `precision`/`gl_FragColor`
|
||||
2. canvas `data-engine` 显示 r170+
|
||||
3. 存在 `.mul()`/`.add()`/`.toVar()`/`.assign()` 链式调用
|
||||
|
||||
## TSL → GLSL 映射
|
||||
|
||||
| TSL | GLSL |
|
||||
|-----|------|
|
||||
| `screenUV` | `gl_FragCoord.xy / resolution` |
|
||||
| `viewportSize` | `uniform vec2 resolution` |
|
||||
| `float()`/`vec2()`/`vec3()`/`vec4()` | 同名(但 TSL 中是 JS 函数) |
|
||||
| `.mul()`/`.add()`/`.sub()`/`.div()` | `*`/`+`/`-`/`/` |
|
||||
| `sin()`/`cos()`/`mix()`/`smoothstep()` | 同名 |
|
||||
| `clamp()`/`abs()`/`fract()`/`floor()` | 同名 |
|
||||
| `pow()`/`exp()`/`sqrt()`/`dot()`/`length()` | 同名 |
|
||||
| `Fn()` | shader 函数包裹器(内联到 GLSL) |
|
||||
| `uniform()` | `uniform <type> name` |
|
||||
| `convertToTexture()` | RTT(多 pass 渲染) |
|
||||
| `.sample(uv)` | `texture(sampler, uv)` |
|
||||
| `.toVar()`/`.assign()` | 声明/赋值可变变量 |
|
||||
| `.oneMinus()` | `1.0 - x` |
|
||||
|
||||
## 重建步骤
|
||||
|
||||
1. **定位**:搜索组件名附近的 `fragmentNode` 属性
|
||||
2. **映射表**:从 bundle 顶部 import 语句推断 minified → TSL 函数名
|
||||
```javascript
|
||||
import { A as screenUV, W as sin, ... } from "three-module"
|
||||
```
|
||||
3. **翻译**:链式调用 → GLSL 表达式
|
||||
```javascript
|
||||
// TSL: screenUV.x.sub(center.x).mul(aspect)
|
||||
// GLSL: (uv.x - center.x) * aspect
|
||||
```
|
||||
4. **RTT**:`convertToTexture(childNode)` → 独立 pass 渲染到 FBO,主 shader 中 `texture()` 采样
|
||||
353
skills/web-shader-extractor/references/unicorn-studio.md
Executable file
353
skills/web-shader-extractor/references/unicorn-studio.md
Executable file
@@ -0,0 +1,353 @@
|
||||
# Unicorn Studio 提取工作流
|
||||
|
||||
Unicorn Studio (unicorn.studio) 是一个 no-code WebGL 设计工具,使用 curtains.js 作为渲染引擎,Firebase/Firestore 作为后端。
|
||||
|
||||
## 识别特征
|
||||
|
||||
- URL 模式: `unicorn.studio/remix/{remixId}` 或 `unicorn.studio/edit/{designId}`
|
||||
- Meta tag: `<meta name="ai:technical-stack" content="Vue 3, curtains.js, Firebase, JavaScript SDK">`
|
||||
- 嵌入 SDK: `unicornStudio-*.js`(~84KB embed 版本)
|
||||
- 主应用 bundle: `index-*.js`(~2.1MB,含 shader 模板)
|
||||
|
||||
## 数据获取路径
|
||||
|
||||
### 路径 1: Firestore REST API(推荐,适用于 remix)
|
||||
|
||||
```bash
|
||||
# Firebase 配置(从 unicorn.studio 前端 JS bundle 中提取,每次提取时动态获取)
|
||||
# 获取方式:curl -s https://www.unicorn.studio/ | grep -oP 'apiKey:"[^"]+"' | head -1
|
||||
API_KEY="<从网站 bundle 动态提取>"
|
||||
PROJECT="unicorn-studio"
|
||||
|
||||
# Step 1: 获取 remix 元数据(含 versionId、designId、cre建者信息)
|
||||
curl -s "https://firestore.googleapis.com/v1/projects/$PROJECT/databases/(default)/documents/remixes/{REMIX_ID}?key=$API_KEY"
|
||||
|
||||
# Step 2: 获取版本数据(含所有图层定义、参数、纹理引用)
|
||||
# versionId 从 Step 1 的 fields.versionId.stringValue 提取
|
||||
curl -s "https://firestore.googleapis.com/v1/projects/$PROJECT/databases/(default)/documents/versions/{VERSION_ID}?key=$API_KEY"
|
||||
```
|
||||
|
||||
### 路径 2: GCS/CDN Embed 数据(适用于已发布的嵌入)
|
||||
|
||||
```bash
|
||||
# 非 Pro 用户
|
||||
curl -s "https://storage.googleapis.com/unicornstudio-production/embeds/{DESIGN_ID}"
|
||||
|
||||
# Pro 用户
|
||||
curl -s "https://assets.unicorn.studio/embeds/{DESIGN_ID}"
|
||||
```
|
||||
|
||||
Embed JSON 格式: `{ options: {...}, layers/history: [...], modules: [...] }`
|
||||
包含 `compiledFragmentShaders[]` 和 `compiledVertexShaders[]`(已编译的 GLSL)。
|
||||
|
||||
### 路径 3: 从页面内嵌 JSON 提取
|
||||
|
||||
Unicorn Studio 嵌入使用 `data-us-project` 或 `data-us-project-src` HTML 属性,
|
||||
SDK `init()` 会扫描这些属性并加载对应项目。
|
||||
|
||||
## 先判断数据形态
|
||||
|
||||
Unicorn Studio 至少有两种常见数据形态:
|
||||
|
||||
### 1. embed/export scene
|
||||
|
||||
- 一般是最终给 `addScene()` 的 scene JSON
|
||||
- 往往已经带 `compiledFragmentShaders[]` / `compiledVertexShaders[]`
|
||||
- 这种格式可以直接喂给 embed runtime
|
||||
|
||||
### 2. editor/version history
|
||||
|
||||
- 常见来源是 Firestore `versions/{id}` 的 `history`
|
||||
- 这是编辑器原始层数据,**不能**直接喂给 `addScene()`
|
||||
|
||||
如果把 `history` 误当 embed scene,典型症状是:
|
||||
|
||||
- `Plane: No fragment shader provided, will use a default one`
|
||||
- `Plane: No vertex shader provided, will use a default one`
|
||||
- `No composite shader data for element`
|
||||
- canvas 创建成功,但画面全黑或只剩默认层
|
||||
|
||||
## Firestore 集合结构
|
||||
|
||||
| Collection | 用途 | 关键字段 |
|
||||
|---|---|---|
|
||||
| `designs` | 设计元数据 | creatorId, name, versionId, hasEmbed |
|
||||
| `versions` | 版本数据(核心) | history[], options |
|
||||
| `remixes` | 可 remix 设计 | designId, versionId, creatorId, thumbnail |
|
||||
|
||||
## 版本数据结构
|
||||
|
||||
Firestore REST 返回格式用 `{stringValue, integerValue, arrayValue, mapValue, ...}` 包裹,需递归解析。
|
||||
|
||||
`history` 数组中每个元素是一个图层:
|
||||
|
||||
```
|
||||
layerType: "effect" | "text" | "image" | "model" | "shape"
|
||||
type: 效果类型 (gradient, noiseFill, sdf_shape, glyphDither, bloomFast, ...)
|
||||
```
|
||||
|
||||
### 图层参数(常见)
|
||||
|
||||
- `pos`, `scale`, `speed`, `opacity`, `blendMode`
|
||||
- `trackMouse`, `trackAxes`, `mouseMomentum`
|
||||
- `parentLayer`: UUID 或 false(关联父元素)
|
||||
- `breakpoints[]`: 响应式断点配置
|
||||
- `states`: appear/scroll/hover/mousemove 动画
|
||||
- `customFragmentShaders[]`, `customVertexShaders[]`(通常为空,用内置效果时)
|
||||
|
||||
## 正确初始化策略
|
||||
|
||||
如果拿到的是 Firestore `version/history`,优先模仿站点自己的初始化链路,不要硬套 embed API。
|
||||
|
||||
典型调用顺序:
|
||||
|
||||
1. `unpackageHistory()` 或 `unpackVersion()`
|
||||
2. `createFontScript()`
|
||||
3. `createCurtains()`
|
||||
4. `handleItemPlanes()`
|
||||
5. `fullRedraw()`
|
||||
|
||||
如果页面 bundle 里有专门的 Remix/Preview 组件,优先跟着它走,不要只看公开 UMD/SDK 文档。
|
||||
|
||||
### 资源本地化
|
||||
|
||||
- image/font/texture 尽量下载到本地
|
||||
- `history` 里的 `src`、`fontCSS.src` 要改成本地路径
|
||||
- 某些对象字段可能是数字 key 的 map,落地前要规整成数组
|
||||
|
||||
### 效果类型特有参数
|
||||
|
||||
| 效果 | 关键参数 |
|
||||
|---|---|
|
||||
| gradient | fill[], stops[], gradientType, gradientAngle, wrap |
|
||||
| noiseFill | noiseType, turbulence, color1, color2, colorPhase, chroma, direction |
|
||||
| sdf_shape | shape(0-22), refraction, extrude, smoothing, axis, animationDirection, lightPosition |
|
||||
| glyphDither | characters, glyphSet, scale, gamma, monochrome, texture(sprite atlas) |
|
||||
| bloomFast | amount, intensity, exposure, tint |
|
||||
|
||||
## Shader 代码提取
|
||||
|
||||
**关键发现**: Embed SDK(~84KB)不含 GLSL shader 代码。Shader 模板在主应用 bundle(~2.1MB)中,经 7 步编译管线处理后存入 embed JSON。
|
||||
|
||||
### Shader 在 App Bundle 中的位置
|
||||
|
||||
Shader 模板是字符串字面量,通过变量名标识:
|
||||
|
||||
```
|
||||
效果名 → 变量名
|
||||
glyphDither → X$ (fragment)
|
||||
noiseFill → WY (fragment)
|
||||
sdf_shape → XY (fragment)
|
||||
gradient → eX (fragment)
|
||||
bloomFast → Hj (fragment)
|
||||
通用顶点 → ye (vertex)
|
||||
梯度顶点 → ko (vertex)
|
||||
合成片段 → Uz (composite fragment)
|
||||
合成顶点 → Nz (composite vertex)
|
||||
```
|
||||
|
||||
注意:变量名会随构建版本变化,需搜索关键特征定位。
|
||||
|
||||
### 模板变量
|
||||
|
||||
Shader 模板中含 `${variable}` 占位符,编译时替换:
|
||||
|
||||
| 变量 | 内容 |
|
||||
|---|---|
|
||||
| `${fe}` | mask 相关 uniform 声明 |
|
||||
| `${Vt}` | 图层混合辅助函数 (applyLayerMix, applyLayerMixAlpha, applyLayerMixClip) |
|
||||
| `${gt}` | PCG hash / 随机数函数 (pcg2d, randFibo) |
|
||||
| `${ht}` | 混合模式函数 (17 种模式: Normal, Add, Multiply, Screen, Overlay, ...) |
|
||||
| `${pe("var")}` | mask 应用 + fragColor 输出 |
|
||||
| `${wf}` | BCC noise derivatives (OpenSimplex2S) |
|
||||
| `${Aa}` | Perlin noise 函数 |
|
||||
| `${yr}` | deband 抖动函数 |
|
||||
| `${cm}` | 渐变颜色/停止点 uniform 声明 |
|
||||
| `${xz}` | 高斯权重函数 (bloom blur) |
|
||||
|
||||
### 编译管线
|
||||
|
||||
```
|
||||
1. Fz(): 替换 uniform 值为常量
|
||||
2. Dz(): 处理渐变颜色数量(switch case 裁剪)
|
||||
3. Mz(): 求值常量 switch(死代码消除)
|
||||
4. Rz(): 处理 #ifelseopen/#ifelseclose 块(条件编译)
|
||||
5. Iz(): 移除未使用函数
|
||||
6. Cz(): 移除未使用 uniform 声明
|
||||
7. Bp(): 去注释、规范化空白
|
||||
```
|
||||
|
||||
## 渲染管线(核心,移植时必须正确还原)
|
||||
|
||||
```
|
||||
curtains.js WebGL2 渲染器
|
||||
├─ 每个效果图层 = 一个 Plane + 独立 FBO
|
||||
├─ 图层按 renderOrder 线性链式渲染,每个 plane 读前一个 FBO 为 uTexture
|
||||
├─ Element(shape/text/image)+ 子效果形成 render group:
|
||||
│ 1. Element 自身 plane 先渲染 → FBO_elem
|
||||
│ 2. 子效果按 effects 数组顺序依次渲染 → FBO_child1, FBO_child2, ...
|
||||
│ 3. Composite plane 最后渲染:alpha-blend 子效果输出到背景场景
|
||||
├─ 独立后处理效果 (parentLayer=false) 处理全局场景
|
||||
└─ 最后一个 plane 直接输出到 canvas(无 FBO)
|
||||
```
|
||||
|
||||
### Element + 子效果的 FBO 链(关键)
|
||||
|
||||
```
|
||||
以 shape group (sdf + noise) 为例:
|
||||
|
||||
FBO_before ─────────────────────────────────────────┐
|
||||
│
|
||||
Shape 自身 plane → FBO_shape (渲染基础几何) │
|
||||
↓ │
|
||||
Child noiseFill → FBO_noise (uBgTexture = FBO_shape) │
|
||||
↓ │
|
||||
Child sdf_shape → FBO_sdf (uTexture = FBO_noise) │
|
||||
↓ (showBg=0: 形状外 = vec4(0) 透明) │
|
||||
↓ │
|
||||
Composite plane → FBO_result │
|
||||
uTexture = FBO_sdf (最后一个子效果输出) │
|
||||
uBgTexture = FBO_before (element 之前的场景) ←─────┘
|
||||
output = alpha_blend(fg, bg) = fg + bg * (1 - fg.a)
|
||||
```
|
||||
|
||||
### 子效果关联机制
|
||||
|
||||
```js
|
||||
// Element 的 effects 数组 → 子效果的 parentLayer UUID 列表
|
||||
shape.effects = ["e270a7cd-...", "fb591190-..."]
|
||||
|
||||
// 每个子效果引用父 element 的 UUID
|
||||
noiseFill.parentLayer = "e270a7cd-..." // effects[0]
|
||||
sdf_shape.parentLayer = "fb591190-..." // effects[1]
|
||||
|
||||
// embed SDK 中查找子效果:
|
||||
getChildEffectItems() {
|
||||
return this.effects.map(uuid =>
|
||||
state.layers.find(l => l.parentLayer === uuid)
|
||||
).filter(Boolean)
|
||||
}
|
||||
```
|
||||
|
||||
### uTime 时间基准(关键陷阱)
|
||||
|
||||
Embed SDK 中 `uTime` **不是秒数**,而是逐帧累加:
|
||||
```js
|
||||
// setEffectPlaneUniforms() 中:
|
||||
t.uniforms.time.value += speed * 60 / this.fps;
|
||||
```
|
||||
|
||||
在 60fps 下:`uTime += speed` 每帧。1 秒后 `uTime = speed × 60`。
|
||||
|
||||
| 效果层 | speed | 1 秒后 uTime |
|
||||
|--------|-------|-------------|
|
||||
| noiseFill | 0.25 | 15 |
|
||||
| sdf_shape | 0.5 | 30 |
|
||||
| gradient | 0.25 | 15 |
|
||||
|
||||
**移植时必须乘以 `speed × 60`**,否则动画慢 15-60 倍:
|
||||
```js
|
||||
// 正确:
|
||||
uni1f(prog, 'uTime', elapsedSeconds * speed * 60);
|
||||
// 错误:
|
||||
uni1f(prog, 'uTime', elapsedSeconds);
|
||||
```
|
||||
|
||||
### showBg 参数的关键作用
|
||||
|
||||
- `showBg=0`:光线未命中几何体时输出 `vec4(0)` **透明**(不是黑色!)
|
||||
- `showBg=1`:光线未命中时采样 `uTexture/uBgTexture`(显示背景内容)
|
||||
|
||||
**移植时 showBg=0 是最常见的陷阱**:如果错误地输出 `vec4(0,0,0,1)` 不透明黑色,
|
||||
composite alpha blend 会被覆盖而不是透过下方图层。必须确保 alpha=0。
|
||||
|
||||
## 移植策略
|
||||
|
||||
1. **纯 2D 后处理效果** (glyphDither, bloomFast): 原生 WebGL2 全屏四边形
|
||||
2. **生成式效果** (noiseFill, gradient): 原生 WebGL2
|
||||
3. **3D SDF** (sdf_shape): 原生 WebGL2 raymarching
|
||||
4. **复杂场景** (多图层合成): 需要 multi-pass FBO 管线
|
||||
5. **文字图层**: Canvas 2D 渲染文字 → 作为纹理上传 WebGL
|
||||
|
||||
## Playwright 验证
|
||||
|
||||
Playwright 默认 headless 环境可能没有可用 WebGL。出现下面症状时,先怀疑环境,不要立刻怀疑提取逻辑:
|
||||
|
||||
- `Renderer: WebGL context could not be created`
|
||||
- `0 canvas(es) found`
|
||||
- `Error creating Curtains instance`
|
||||
- 截图纯黑
|
||||
|
||||
这时改用 `swiftshader` 再验证:
|
||||
|
||||
```bash
|
||||
--use-angle=swiftshader
|
||||
--use-gl=angle
|
||||
--enable-unsafe-swiftshader
|
||||
--ignore-gpu-blocklist
|
||||
```
|
||||
|
||||
建议验证顺序:
|
||||
|
||||
1. 看 console 是 shader/runtime 错误,还是 WebGL context 创建失败
|
||||
2. 看 DOM 里是否真的生成了 `canvas`
|
||||
3. 用 `swiftshader` 截图
|
||||
4. 和原站缩略图或首屏截图做构图对比
|
||||
|
||||
### Glyph Atlas 生成
|
||||
|
||||
原始 glyph atlas 是 base64 PNG(存在跨浏览器兼容问题)。
|
||||
推荐用 Canvas 2D 动态生成:
|
||||
|
||||
```js
|
||||
function createGlyphAtlas(chars, size = 40) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size * chars.length;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = `bold ${size * 0.8}px monospace`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
ctx.fillText(chars[i], size * i + size / 2, size / 2);
|
||||
}
|
||||
return canvas; // → gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas)
|
||||
}
|
||||
```
|
||||
|
||||
## Cloud Functions 端点
|
||||
|
||||
所有位于 `https://us-central1-unicorn-studio.cloudfunctions.net/`:
|
||||
- `publishEmbedTest` — 发布/更新 embed(需认证)
|
||||
- `getUserIdByUsername` — 用户名→userId
|
||||
- `handleVideos/handleModels/handleImages` — 资源处理
|
||||
- `generateImprovedMSDF` — MSDF 文字渲染
|
||||
- `generateDepthMap` — 深度图生成
|
||||
- `copyRemixAssets` — remix 资源复制
|
||||
|
||||
## 示例:完整提取流程
|
||||
|
||||
```bash
|
||||
# 1. 从 URL 提取 remix ID
|
||||
REMIX_ID="QZxhNFb1X1OaUqaJLT9S"
|
||||
|
||||
# 2. 获取 remix 元数据
|
||||
curl -s "https://firestore.googleapis.com/v1/projects/unicorn-studio/databases/(default)/documents/remixes/$REMIX_ID?key=$API_KEY" > remix.json
|
||||
|
||||
# 3. 提取 versionId
|
||||
VERSION_ID=$(python3 -c "import json; print(json.load(open('remix.json'))['fields']['versionId']['stringValue'])")
|
||||
|
||||
# 4. 获取版本数据
|
||||
curl -s "https://firestore.googleapis.com/v1/projects/unicorn-studio/databases/(default)/documents/versions/$VERSION_ID?key=$API_KEY" > version.json
|
||||
|
||||
# 5. 解析版本数据中的图层和参数 → 用 Python/Node 递归解析 Firestore REST 格式
|
||||
|
||||
# 6. 从 app bundle 提取对应效果类型的 shader 模板
|
||||
curl -s "https://www.unicorn.studio/assets/index-*.js" > app-bundle.js
|
||||
# 搜索效果类型名定位 shader 代码
|
||||
|
||||
# 7. 组合参数 + shader 模板 → 构建独立 WebGL2 项目
|
||||
```
|
||||
153
skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs
Executable file
153
skills/web-shader-extractor/scripts/fetch-rendered-dom.mjs
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 使用 Playwright 获取 JS 渲染后的完整 DOM 和 WebGL 元数据
|
||||
*
|
||||
* Usage:
|
||||
* node fetch-rendered-dom.mjs <URL> [outDir]
|
||||
*
|
||||
* 首次运行会自动安装 playwright 到 ~/.cache/playwright-runner/
|
||||
*
|
||||
* 输出到 outDir (默认 /tmp/rendered):
|
||||
* dom.html — 完整渲染后 HTML
|
||||
* canvas-info.json — 所有 canvas 元素的信息
|
||||
* webgl-info.json — WebGL 上下文元数据
|
||||
* console.log — 页面 console 输出
|
||||
* screenshot.png — 页面截图
|
||||
* network.json — 运行时加载的 JS/资源 URL
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
const url = process.argv[2];
|
||||
const outDir = process.argv[3] || '/tmp/rendered';
|
||||
|
||||
if (!url) {
|
||||
console.error('Usage: node fetch-rendered-dom.mjs <URL> [outDir]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Auto-install playwright to a persistent cache directory
|
||||
const runnerDir = join(homedir(), '.cache', 'playwright-runner');
|
||||
const pwDir = join(runnerDir, 'node_modules', 'playwright');
|
||||
const chromiumMarker = join(runnerDir, '.chromium-installed');
|
||||
|
||||
if (!existsSync(pwDir)) {
|
||||
console.log('Installing playwright (one-time setup)...');
|
||||
mkdirSync(runnerDir, { recursive: true });
|
||||
writeFileSync(join(runnerDir, 'package.json'), '{"type":"module"}');
|
||||
try {
|
||||
execSync('npm install playwright', { cwd: runnerDir, stdio: 'inherit' });
|
||||
} catch {
|
||||
console.log('npm install failed, retrying with registry mirror...');
|
||||
execSync('npm install playwright --registry=https://registry.npmmirror.com', { cwd: runnerDir, stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
|
||||
if (!existsSync(chromiumMarker)) {
|
||||
console.log('Installing chromium browser...');
|
||||
try {
|
||||
execSync('npx playwright install chromium', { cwd: runnerDir, stdio: 'inherit' });
|
||||
} catch {
|
||||
console.log('Chromium download failed, retrying with mirror...');
|
||||
execSync('PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright npx playwright install chromium', { cwd: runnerDir, stdio: 'inherit' });
|
||||
}
|
||||
writeFileSync(chromiumMarker, new Date().toISOString());
|
||||
}
|
||||
|
||||
// Dynamic import from the cached location
|
||||
const pw = await import(join(pwDir, 'index.mjs'));
|
||||
const { chromium } = pw;
|
||||
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const consoleLogs = [];
|
||||
const networkRequests = [];
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
// Capture console
|
||||
page.on('console', msg => {
|
||||
consoleLogs.push(`[${msg.type()}] ${msg.text()}`);
|
||||
});
|
||||
|
||||
// Capture network (JS, WASM, bin, glsl, image files)
|
||||
page.on('response', async response => {
|
||||
const reqUrl = response.url();
|
||||
const type = response.headers()['content-type'] || '';
|
||||
if (/\.(js|mjs|wasm|bin|glsl|frag|vert|svg)(\?|$)/.test(reqUrl) || type.includes('javascript')) {
|
||||
networkRequests.push({
|
||||
url: reqUrl,
|
||||
status: response.status(),
|
||||
type: type.split(';')[0],
|
||||
size: parseInt(response.headers()['content-length'] || '0'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Navigating to ${url} ...`);
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
|
||||
|
||||
// Wait for WebGL initialization
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 1. Full rendered DOM
|
||||
const html = await page.content();
|
||||
writeFileSync(join(outDir, 'dom.html'), html);
|
||||
console.log(`dom.html — ${html.length} bytes`);
|
||||
|
||||
// 2. Canvas info
|
||||
const canvasInfo = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('canvas')).map((c, i) => ({
|
||||
index: i,
|
||||
outerHTML: c.outerHTML.slice(0, 500),
|
||||
width: c.width,
|
||||
height: c.height,
|
||||
clientWidth: c.clientWidth,
|
||||
clientHeight: c.clientHeight,
|
||||
dataEngine: c.dataset.engine || null,
|
||||
id: c.id || null,
|
||||
className: c.className || null,
|
||||
parentTag: c.parentElement?.tagName || null,
|
||||
parentClass: c.parentElement?.className?.slice(0, 100) || null,
|
||||
}));
|
||||
});
|
||||
writeFileSync(join(outDir, 'canvas-info.json'), JSON.stringify(canvasInfo, null, 2));
|
||||
console.log(`canvas-info.json — ${canvasInfo.length} canvas(es) found`);
|
||||
|
||||
// 3. WebGL info
|
||||
const webglInfo = await page.evaluate(() => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (!canvas) return { error: 'no canvas found' };
|
||||
// Don't create new context, just report what we can
|
||||
return {
|
||||
found: true,
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
dataEngine: canvas.dataset.engine || null,
|
||||
};
|
||||
});
|
||||
writeFileSync(join(outDir, 'webgl-info.json'), JSON.stringify(webglInfo, null, 2));
|
||||
console.log(`webgl-info.json — ${JSON.stringify(webglInfo).slice(0, 100)}`);
|
||||
|
||||
// 4. Console logs
|
||||
writeFileSync(join(outDir, 'console.log'), consoleLogs.join('\n'));
|
||||
console.log(`console.log — ${consoleLogs.length} entries`);
|
||||
|
||||
// 5. Screenshot
|
||||
await page.screenshot({ path: join(outDir, 'screenshot.png'), fullPage: false });
|
||||
console.log(`screenshot.png — saved`);
|
||||
|
||||
// 6. Network requests
|
||||
writeFileSync(join(outDir, 'network.json'), JSON.stringify(networkRequests, null, 2));
|
||||
console.log(`network.json — ${networkRequests.length} JS/resource requests captured`);
|
||||
|
||||
await browser.close();
|
||||
console.log(`\nDone. Files saved to ${outDir}`);
|
||||
76
skills/web-shader-extractor/scripts/scan-bundle.sh
Executable file
76
skills/web-shader-extractor/scripts/scan-bundle.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# Scan JS bundle(s) for WebGL/shader keywords and report tech stack
|
||||
# Usage: scan-bundle.sh <file1.js> [file2.js ...]
|
||||
# Output: keyword counts + tech stack guess
|
||||
|
||||
set -eu
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: scan-bundle.sh <file1.js> [file2.js ...]"
|
||||
echo "Scans JS files for WebGL/shader keywords and identifies tech stack."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILES=("$@")
|
||||
|
||||
echo "=== SHADER/WEBGL KEYWORD SCAN ==="
|
||||
echo ""
|
||||
|
||||
# Core GLSL keywords
|
||||
echo "--- GLSL Keywords ---"
|
||||
for kw in "gl_FragColor" "gl_Position" "gl_PointSize" "gl_PointCoord" \
|
||||
"precision" "uniform" "varying" "attribute" \
|
||||
"FRAGMENT_SHADER" "VERTEX_SHADER" "createShader" \
|
||||
"sampler2D" "texture2D" "smoothstep" "discard"; do
|
||||
count=$(grep -o "$kw" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "$count" -gt 0 ] && printf " %-25s %s\n" "$kw" "$count" || true
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- WebGL/Canvas Keywords ---"
|
||||
for kw in "canvas" "webgl" "webgl2" "getContext" "shader" "glsl" \
|
||||
"framebuffer" "renderbuffer" "drawArrays" "drawElements" \
|
||||
"bufferData" "texImage2D" "POINTS"; do
|
||||
count=$(grep -oi "$kw" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "$count" -gt 0 ] && printf " %-25s %s\n" "$kw" "$count" || true
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- Noise/Math Keywords ---"
|
||||
for kw in "snoise" "simplex" "perlin" "noise" "PoissonDisk" "Poisson"; do
|
||||
count=$(grep -o "$kw" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "$count" -gt 0 ] && printf " %-25s %s\n" "$kw" "$count" || true
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== TECH STACK DETECTION ==="
|
||||
|
||||
detect() {
|
||||
local label="$1" pattern="$2"
|
||||
local count
|
||||
count=$(grep -oE "$pattern" "${FILES[@]}" 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "$count" -gt 0 ] && printf " %-25s %s hits\n" "$label" "$count" || true
|
||||
}
|
||||
|
||||
# Frameworks (patterns specific enough to avoid false positives)
|
||||
detect "Three.js" "THREE\.|WebGLRenderer|ShaderMaterial|BufferGeometry|PerspectiveCamera|OrthographicCamera"
|
||||
detect "Three.js (minified)" "setRenderTarget|DataTexture|setPixelRatio|setClearColor"
|
||||
detect "PixiJS" "PIXI\.|pixi\.js|PixiJS"
|
||||
detect "Babylon.js" "BABYLON\.|babylonjs"
|
||||
detect "Raw WebGL" "gl\.bindBuffer|gl\.bindTexture|gl\.useProgram|gl\.attachShader|gl\.linkProgram"
|
||||
detect "Regl" "regl\(|regl\.frame|regl\.texture"
|
||||
detect "OGL" "ogl\.|ogl/"
|
||||
|
||||
# Patterns
|
||||
detect "GPGPU" "setRenderTarget|RenderTarget|ping.pong|gpgpu|GPGPU"
|
||||
detect "Particles" "gl_PointSize|gl_PointCoord|PointSize|particl"
|
||||
detect "Post-processing" "EffectComposer|RenderPass|ShaderPass|postprocess"
|
||||
detect "Ray marching" "rayMarch|sdSphere|sdBox|sdRoundBox"
|
||||
detect "Instancing" "InstancedMesh|InstancedBufferGeometry|instanceMatrix"
|
||||
|
||||
echo ""
|
||||
echo "=== FILE SIZES ==="
|
||||
for f in "${FILES[@]}"; do
|
||||
size=$(wc -c < "$f" | tr -d ' ')
|
||||
echo " $(basename "$f"): ${size} bytes"
|
||||
done
|
||||
Reference in New Issue
Block a user