Initial commit

This commit is contained in:
Z User
2026-06-06 05:21:10 +00:00
Unverified
commit 6664758a6d
493 changed files with 135653 additions and 0 deletions

View 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.jsr170+ 可能是 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.mdFirestore REST API 直取配置+shader
└─ shaders.com → references/shaders-com.mdNuxt 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 bundle1MB+ 不适合主上下文)。
→ 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` |

View 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

View 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) }
```

View File

@@ -0,0 +1,61 @@
# Shader 代码提取Agent Prompt 与反混淆
## Agent 深度提取 Prompt 模板
启动 Agentsubagent_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 中

View File

@@ -0,0 +1,164 @@
# 移植策略
## 框架选择
```
纯 2D CanvasgetContext('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.jsexport 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 场景?
├─ → 不简化
不确定?
└─ → 不提议
```

View 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 内置 |

View 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 |

View 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 |

View 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()` 采样

View 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
├─ Elementshape/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 项目
```

View 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}`);

View 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