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,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 项目
```