webgl-forensics 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/forensics.yml +44 -0
- package/CLAUDE_CODE.md +49 -0
- package/MASTER_PROMPT.md +202 -0
- package/README.md +579 -0
- package/SKILL.md +1256 -0
- package/package.json +53 -0
- package/project summary.md +33 -0
- package/puppeteer-runner.js +486 -0
- package/scripts/00-tech-stack-detect.js +185 -0
- package/scripts/01-source-map-extractor.js +73 -0
- package/scripts/02-interaction-model.js +129 -0
- package/scripts/03-responsive-analysis.js +115 -0
- package/scripts/04-page-transitions.js +128 -0
- package/scripts/05-loading-sequence.js +124 -0
- package/scripts/06-audio-extraction.js +115 -0
- package/scripts/07-accessibility-reduced-motion.js +133 -0
- package/scripts/08-complexity-scorer.js +138 -0
- package/scripts/09-visual-diff-validator.js +113 -0
- package/scripts/10-webgpu-extractor.js +248 -0
- package/scripts/11-scroll-screenshot-grid.js +76 -0
- package/scripts/12-network-waterfall.js +151 -0
- package/scripts/13-react-fiber-walker.js +285 -0
- package/scripts/14-shader-hotpatch.js +349 -0
- package/scripts/15-multipage-crawler.js +221 -0
- package/scripts/16-gsap-timeline-recorder.js +335 -0
- package/scripts/17-r3f-fiber-serializer.js +316 -0
- package/scripts/18-font-extractor.js +290 -0
- package/scripts/19-design-token-export.js +85 -0
- package/scripts/20-timeline-visualizer.js +61 -0
- package/scripts/21-lighthouse-audit.js +35 -0
- package/scripts/22-sitemap-crawler.js +128 -0
- package/scripts/23-scaffold-generator.js +451 -0
- package/scripts/24-ai-reconstruction.js +226 -0
- package/scripts/25-shader-annotator.js +226 -0
- package/tui.js +196 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCRIPT 13 — React Fiber Walker + R3F Component Extractor
|
|
3
|
+
* webgl-forensics skill | Enhancement #1 (Critical)
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE:
|
|
6
|
+
* Walk the React Fiber tree to extract R3F (React Three Fiber) component
|
|
7
|
+
* hierarchy, Three.js object metadata, useFrame hooks, and component props.
|
|
8
|
+
* This gives you the declarative structure behind the 3D scene, not just
|
|
9
|
+
* the raw WebGL draw calls.
|
|
10
|
+
*
|
|
11
|
+
* RUN VIA:
|
|
12
|
+
* evaluate_script tool — paste the IIFE or call window.__fiberWalk()
|
|
13
|
+
*
|
|
14
|
+
* WHAT IT EXTRACTS:
|
|
15
|
+
* - Full R3F component tree (mesh, group, canvas, etc.)
|
|
16
|
+
* - Props per component (position, rotation, scale, material, geometry)
|
|
17
|
+
* - useFrame hooks and animation callbacks
|
|
18
|
+
* - Zustand/Jotai store usage in 3D components
|
|
19
|
+
* - Custom shader material uniforms declared in JSX
|
|
20
|
+
* - R3F performance config (frameloop, shadows, gl props)
|
|
21
|
+
*/
|
|
22
|
+
(() => {
|
|
23
|
+
// ═══════════════════════════════════════════════
|
|
24
|
+
// STEP 1 — Find the React root fiber node
|
|
25
|
+
// ═══════════════════════════════════════════════
|
|
26
|
+
function findFiber(el) {
|
|
27
|
+
const key = Object.keys(el).find(k =>
|
|
28
|
+
k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance')
|
|
29
|
+
);
|
|
30
|
+
return key ? el[key] : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findReactRoot() {
|
|
34
|
+
// R3F canvas is the most reliable entry point
|
|
35
|
+
const canvases = document.querySelectorAll('canvas');
|
|
36
|
+
for (const canvas of canvases) {
|
|
37
|
+
const fiber = findFiber(canvas);
|
|
38
|
+
if (fiber) return { node: fiber, source: 'canvas', el: canvas };
|
|
39
|
+
}
|
|
40
|
+
// Fall back to root div
|
|
41
|
+
const root = document.getElementById('__next') || document.getElementById('root') || document.body;
|
|
42
|
+
const fiber = findFiber(root);
|
|
43
|
+
if (fiber) return { node: fiber, source: 'root', el: root };
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ═══════════════════════════════════════════════
|
|
48
|
+
// STEP 2 — Walk up to fiber tree root
|
|
49
|
+
// ═══════════════════════════════════════════════
|
|
50
|
+
function walkToRoot(fiber) {
|
|
51
|
+
let node = fiber;
|
|
52
|
+
while (node.return) node = node.return;
|
|
53
|
+
return node;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ═══════════════════════════════════════════════
|
|
57
|
+
// STEP 3 — Classify fiber node type
|
|
58
|
+
// ═══════════════════════════════════════════════
|
|
59
|
+
const R3F_TYPES = new Set([
|
|
60
|
+
'mesh', 'group', 'points', 'line', 'lineSegments', 'instancedMesh',
|
|
61
|
+
'meshStandardMaterial', 'meshPhysicalMaterial', 'meshBasicMaterial',
|
|
62
|
+
'meshNormalMaterial', 'shaderMaterial', 'rawShaderMaterial',
|
|
63
|
+
'boxGeometry', 'sphereGeometry', 'planeGeometry', 'bufferGeometry',
|
|
64
|
+
'ambientLight', 'directionalLight', 'pointLight', 'spotLight', 'rectAreaLight',
|
|
65
|
+
'perspectiveCamera', 'orthographicCamera',
|
|
66
|
+
'canvas', 'primitive',
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const R3F_COMPONENT_NAMES = new Set([
|
|
70
|
+
'Canvas', 'Mesh', 'Group', 'Points', 'InstancedMesh',
|
|
71
|
+
'useFrame', 'useThree', 'useLoader',
|
|
72
|
+
'OrbitControls', 'PerspectiveCamera', 'Environment',
|
|
73
|
+
'MeshTransmissionMaterial', 'Html', 'Text', 'Billboard',
|
|
74
|
+
'Sparkles', 'Stars', 'Cloud', 'Effects',
|
|
75
|
+
'ScrollControls', 'Scroll', 'useScroll',
|
|
76
|
+
'Preload', 'BakeShadows', 'Bounds', 'Center',
|
|
77
|
+
'Float', 'Levitation', 'useAnimations',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
function getNodeType(fiber) {
|
|
81
|
+
if (!fiber) return 'null';
|
|
82
|
+
const { type, stateNode } = fiber;
|
|
83
|
+
if (typeof type === 'string') return R3F_TYPES.has(type) ? `r3f:${type}` : `dom:${type}`;
|
|
84
|
+
if (typeof type === 'function') {
|
|
85
|
+
const name = type.displayName || type.name || 'Anonymous';
|
|
86
|
+
if (R3F_COMPONENT_NAMES.has(name)) return `r3f:${name}`;
|
|
87
|
+
if (name.includes('Canvas')) return `r3f:Canvas`;
|
|
88
|
+
return `component:${name}`;
|
|
89
|
+
}
|
|
90
|
+
if (type === null && stateNode && stateNode.nodeType === 3) return 'text';
|
|
91
|
+
return 'fragment';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ═══════════════════════════════════════════════
|
|
95
|
+
// STEP 4 — Extract meaningful props
|
|
96
|
+
// ═══════════════════════════════════════════════
|
|
97
|
+
function extractProps(fiber) {
|
|
98
|
+
const { memoizedProps: props } = fiber;
|
|
99
|
+
if (!props) return null;
|
|
100
|
+
|
|
101
|
+
const safe = {};
|
|
102
|
+
const matProps = ['position', 'rotation', 'scale', 'args', 'color', 'intensity',
|
|
103
|
+
'fov', 'near', 'far', 'frameloop', 'shadows', 'gl', 'camera',
|
|
104
|
+
'attach', 'object', 'visible', 'castShadow', 'receiveShadow'];
|
|
105
|
+
|
|
106
|
+
matProps.forEach(key => {
|
|
107
|
+
if (props[key] !== undefined) {
|
|
108
|
+
const val = props[key];
|
|
109
|
+
if (typeof val === 'function') {
|
|
110
|
+
safe[key] = `Function:${val.name || 'anonymous'}`;
|
|
111
|
+
} else if (val && typeof val === 'object' && val.isVector3) {
|
|
112
|
+
safe[key] = `Vector3(${val.x.toFixed(3)}, ${val.y.toFixed(3)}, ${val.z.toFixed(3)})`;
|
|
113
|
+
} else if (Array.isArray(val)) {
|
|
114
|
+
safe[key] = val.slice(0, 4); // truncate long arrays
|
|
115
|
+
} else if (typeof val === 'object' && val !== null) {
|
|
116
|
+
safe[key] = Object.keys(val).slice(0, 6).join(', ');
|
|
117
|
+
} else {
|
|
118
|
+
safe[key] = val;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Detect uniforms object (shader materials)
|
|
124
|
+
if (props.uniforms) {
|
|
125
|
+
safe.uniforms = Object.keys(props.uniforms).map(k => {
|
|
126
|
+
const u = props.uniforms[k];
|
|
127
|
+
return `${k}: ${Array.isArray(u?.value) ? `[${u.value.slice(0,3).join(',')}]` : typeof u?.value}`;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Detect event handlers
|
|
132
|
+
const handlers = Object.keys(props).filter(k => k.startsWith('on'));
|
|
133
|
+
if (handlers.length) safe._eventHandlers = handlers;
|
|
134
|
+
|
|
135
|
+
return Object.keys(safe).length ? safe : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ═══════════════════════════════════════════════
|
|
139
|
+
// STEP 5 — Tree walk (depth-first, bounded)
|
|
140
|
+
// ═══════════════════════════════════════════════
|
|
141
|
+
const r3fNodes = [];
|
|
142
|
+
const allComponents = {};
|
|
143
|
+
const MAX_DEPTH = 80;
|
|
144
|
+
const MAX_NODES = 2000;
|
|
145
|
+
let nodeCount = 0;
|
|
146
|
+
|
|
147
|
+
function walk(fiber, depth = 0) {
|
|
148
|
+
if (!fiber || depth > MAX_DEPTH || nodeCount > MAX_NODES) return;
|
|
149
|
+
nodeCount++;
|
|
150
|
+
|
|
151
|
+
const nodeType = getNodeType(fiber);
|
|
152
|
+
const isR3F = nodeType.startsWith('r3f:');
|
|
153
|
+
const isComponent = nodeType.startsWith('component:');
|
|
154
|
+
const name = nodeType.split(':')[1] || nodeType;
|
|
155
|
+
|
|
156
|
+
// Count all component types
|
|
157
|
+
const fullName = typeof fiber.type === 'function'
|
|
158
|
+
? (fiber.type.displayName || fiber.type.name || 'Anonymous')
|
|
159
|
+
: (typeof fiber.type === 'string' ? fiber.type : 'fragment');
|
|
160
|
+
|
|
161
|
+
if (fullName && fullName !== 'Anonymous' && fullName !== 'fragment' && fullName !== 'text') {
|
|
162
|
+
allComponents[fullName] = (allComponents[fullName] || 0) + 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isR3F) {
|
|
166
|
+
const node = {
|
|
167
|
+
type: nodeType,
|
|
168
|
+
depth,
|
|
169
|
+
props: extractProps(fiber),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Check stateNode for actual Three.js object
|
|
173
|
+
const sn = fiber.stateNode;
|
|
174
|
+
if (sn && sn.__r3f) {
|
|
175
|
+
node.threeObject = {
|
|
176
|
+
type: sn.constructor?.name,
|
|
177
|
+
uuid: sn.uuid?.slice(0, 8),
|
|
178
|
+
visible: sn.visible,
|
|
179
|
+
matrixAutoUpdate: sn.matrixAutoUpdate,
|
|
180
|
+
childCount: sn.children?.length,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
r3fNodes.push(node);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Walk children and siblings
|
|
188
|
+
if (fiber.child) walk(fiber.child, depth + 1);
|
|
189
|
+
if (fiber.sibling) walk(fiber.sibling, depth);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ═══════════════════════════════════════════════
|
|
193
|
+
// STEP 6 — Canvas-level R3F config extraction
|
|
194
|
+
// ═══════════════════════════════════════════════
|
|
195
|
+
function extractCanvasConfig() {
|
|
196
|
+
const config = {};
|
|
197
|
+
|
|
198
|
+
// Check for R3F root state on canvas elements
|
|
199
|
+
document.querySelectorAll('canvas').forEach((canvas, i) => {
|
|
200
|
+
const fiber = findFiber(canvas);
|
|
201
|
+
if (!fiber) return;
|
|
202
|
+
|
|
203
|
+
// Walk up to find Canvas component
|
|
204
|
+
let node = fiber;
|
|
205
|
+
while (node) {
|
|
206
|
+
if (typeof node.type === 'function' && (node.type.name === 'Canvas' || node.type.displayName === 'Canvas')) {
|
|
207
|
+
const props = node.memoizedProps || {};
|
|
208
|
+
config[`canvas_${i}`] = {
|
|
209
|
+
frameloop: props.frameloop || 'auto',
|
|
210
|
+
dpr: props.dpr,
|
|
211
|
+
gl: props.gl ? Object.keys(props.gl) : null,
|
|
212
|
+
shadows: props.shadows,
|
|
213
|
+
orthographic: props.orthographic || false,
|
|
214
|
+
linear: props.linear || false,
|
|
215
|
+
flat: props.flat || false,
|
|
216
|
+
camera: props.camera,
|
|
217
|
+
};
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
node = node.return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Also check window.__r3f for internal state
|
|
224
|
+
if (window.__r3f) config.internalState = Object.keys(window.__r3f);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return config;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ═══════════════════════════════════════════════
|
|
231
|
+
// STEP 7 — EXECUTE
|
|
232
|
+
// ═══════════════════════════════════════════════
|
|
233
|
+
const rootEntry = findReactRoot();
|
|
234
|
+
|
|
235
|
+
if (!rootEntry) {
|
|
236
|
+
return { error: 'No React root found. Site may not use React.' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const treeRoot = walkToRoot(rootEntry.node);
|
|
240
|
+
walk(treeRoot);
|
|
241
|
+
|
|
242
|
+
const canvasConfig = extractCanvasConfig();
|
|
243
|
+
|
|
244
|
+
// ═══════════════════════════════════════════════
|
|
245
|
+
// STEP 8 — Summarize + clone blueprint
|
|
246
|
+
// ═══════════════════════════════════════════════
|
|
247
|
+
const r3fSummary = r3fNodes.reduce((acc, n) => {
|
|
248
|
+
acc[n.type] = (acc[n.type] || 0) + 1;
|
|
249
|
+
return acc;
|
|
250
|
+
}, {});
|
|
251
|
+
|
|
252
|
+
const topComponents = Object.entries(allComponents)
|
|
253
|
+
.sort((a, b) => b[1] - a[1])
|
|
254
|
+
.slice(0, 20);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
meta: {
|
|
258
|
+
url: window.location.href,
|
|
259
|
+
timestamp: new Date().toISOString(),
|
|
260
|
+
totalFiberNodes: nodeCount,
|
|
261
|
+
reactEntry: rootEntry.source,
|
|
262
|
+
},
|
|
263
|
+
r3f: {
|
|
264
|
+
detected: r3fNodes.length > 0,
|
|
265
|
+
nodeCount: r3fNodes.length,
|
|
266
|
+
summary: r3fSummary,
|
|
267
|
+
canvasConfig,
|
|
268
|
+
nodes: r3fNodes.slice(0, 50), // top 50 R3F nodes
|
|
269
|
+
},
|
|
270
|
+
components: {
|
|
271
|
+
topByFrequency: topComponents,
|
|
272
|
+
total: Object.keys(allComponents).length,
|
|
273
|
+
},
|
|
274
|
+
cloneBlueprint: {
|
|
275
|
+
hasR3F: r3fNodes.some(n => n.type === 'r3f:canvas' || n.type === 'r3f:Canvas'),
|
|
276
|
+
r3fImports: [...new Set(r3fNodes.map(n => n.type.replace('r3f:', '')))].filter(t =>
|
|
277
|
+
R3F_COMPONENT_NAMES.has(t)
|
|
278
|
+
),
|
|
279
|
+
materialTypes: [...new Set(r3fNodes.filter(n => n.type.includes('Material')).map(n => n.type.replace('r3f:', '')))],
|
|
280
|
+
geometryTypes: [...new Set(r3fNodes.filter(n => n.type.includes('Geometry')).map(n => n.type.replace('r3f:', '')))],
|
|
281
|
+
hasCustomShaders: r3fNodes.some(n => n.type.includes('shaderMaterial') || n.type.includes('rawShaderMaterial') || n.props?.uniforms),
|
|
282
|
+
hasScrollControls: r3fNodes.some(n => n.type.includes('ScrollControls') || n.type.includes('Scroll')),
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
})()
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCRIPT 14 — Shader Hot-Patch System
|
|
3
|
+
* webgl-forensics skill | Enhancement #2
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE:
|
|
6
|
+
* Intercept and live-patch WebGL shader programs to understand:
|
|
7
|
+
* - Exactly what each uniform controls by setting extreme values
|
|
8
|
+
* - Which uniforms map to color, position, time, etc.
|
|
9
|
+
* - The visual effect of each vertex/fragment shader output
|
|
10
|
+
*
|
|
11
|
+
* RUN VIA:
|
|
12
|
+
* evaluate_script ONCE on page load — installs the intercept
|
|
13
|
+
* Then call window.__shaderPatch.api() to use patch commands
|
|
14
|
+
*
|
|
15
|
+
* WORKFLOW:
|
|
16
|
+
* 1. Run this script (installs interceptors)
|
|
17
|
+
* 2. Run script 00-tech-stack-detect.js to get shader list
|
|
18
|
+
* 3. Call window.__shaderPatch.listPrograms() to list captured programs
|
|
19
|
+
* 4. Call window.__shaderPatch.probeUniforms(0) to auto-probe program 0
|
|
20
|
+
* 5. Call window.__shaderPatch.extractGLSL(0) to get raw GLSL source
|
|
21
|
+
*
|
|
22
|
+
* NOTE:
|
|
23
|
+
* Must be injected BEFORE WebGL context creation for full intercept.
|
|
24
|
+
* If page is already loaded, it captures existing programs only.
|
|
25
|
+
*/
|
|
26
|
+
(() => {
|
|
27
|
+
// Guard: don't install twice
|
|
28
|
+
if (window.__shaderPatch) {
|
|
29
|
+
console.log('✅ Shader hot-patch already installed. Use window.__shaderPatch.api()');
|
|
30
|
+
return window.__shaderPatch.api();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const captured = {
|
|
34
|
+
programs: [],
|
|
35
|
+
contexts: [],
|
|
36
|
+
uniformCache: {},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════
|
|
40
|
+
// STEP 1 — INTERCEPT WebGL context creation
|
|
41
|
+
// ═══════════════════════════════════════════════
|
|
42
|
+
const origGetContext = HTMLCanvasElement.prototype.getContext;
|
|
43
|
+
HTMLCanvasElement.prototype.getContext = function(type, ...args) {
|
|
44
|
+
const ctx = origGetContext.apply(this, [type, ...args]);
|
|
45
|
+
if ((type === 'webgl' || type === 'webgl2') && ctx) {
|
|
46
|
+
if (!captured.contexts.includes(ctx)) {
|
|
47
|
+
captured.contexts.push(ctx);
|
|
48
|
+
patchContext(ctx);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return ctx;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ═══════════════════════════════════════════════
|
|
55
|
+
// STEP 2 — Patch shaderSource and linkProgram
|
|
56
|
+
// ═══════════════════════════════════════════════
|
|
57
|
+
function patchContext(gl) {
|
|
58
|
+
const origShaderSource = gl.shaderSource.bind(gl);
|
|
59
|
+
const origLinkProgram = gl.linkProgram.bind(gl);
|
|
60
|
+
const origUniform1f = gl.uniform1f.bind(gl);
|
|
61
|
+
|
|
62
|
+
const shaderSources = new WeakMap();
|
|
63
|
+
|
|
64
|
+
// Capture shader source as it's uploaded
|
|
65
|
+
gl.shaderSource = function(shader, src) {
|
|
66
|
+
shaderSources.set(shader, src);
|
|
67
|
+
return origShaderSource(shader, src);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// After linking, capture program metadata
|
|
71
|
+
gl.linkProgram = function(program) {
|
|
72
|
+
origLinkProgram(program);
|
|
73
|
+
|
|
74
|
+
// Use WEBGL_debug_shaders for verified source
|
|
75
|
+
const ext = gl.getExtension('WEBGL_debug_shaders');
|
|
76
|
+
|
|
77
|
+
const shaders = gl.getAttachedShaders(program);
|
|
78
|
+
const programData = {
|
|
79
|
+
id: captured.programs.length,
|
|
80
|
+
uniforms: [],
|
|
81
|
+
attributes: [],
|
|
82
|
+
vertex: null,
|
|
83
|
+
fragment: null,
|
|
84
|
+
_gl: gl,
|
|
85
|
+
_program: program,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (shaders) {
|
|
89
|
+
shaders.forEach(shader => {
|
|
90
|
+
const type = gl.getShaderParameter(shader, gl.SHADER_TYPE);
|
|
91
|
+
const label = type === gl.VERTEX_SHADER ? 'vertex' : 'fragment';
|
|
92
|
+
// Prefer debug extension (unminified), fall back to captured source
|
|
93
|
+
const src = ext
|
|
94
|
+
? ext.getTranslatedShaderSource(shader)
|
|
95
|
+
: (shaderSources.get(shader) || gl.getShaderSource(shader));
|
|
96
|
+
programData[label] = src;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Extract all active uniforms
|
|
101
|
+
const uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
|
|
102
|
+
for (let i = 0; i < uniformCount; i++) {
|
|
103
|
+
const info = gl.getActiveUniform(program, i);
|
|
104
|
+
if (info) {
|
|
105
|
+
programData.uniforms.push({
|
|
106
|
+
name: info.name,
|
|
107
|
+
type: glTypeToString(info.type),
|
|
108
|
+
size: info.size,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Extract all active attributes
|
|
114
|
+
const attrCount = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
|
|
115
|
+
for (let i = 0; i < attrCount; i++) {
|
|
116
|
+
const info = gl.getActiveAttrib(program, i);
|
|
117
|
+
if (info) programData.attributes.push({ name: info.name, type: glTypeToString(info.type) });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
captured.programs.push(programData);
|
|
121
|
+
return;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ═══════════════════════════════════════════════
|
|
126
|
+
// STEP 3 — Also scan existing contexts (if page loaded)
|
|
127
|
+
// ═══════════════════════════════════════════════
|
|
128
|
+
document.querySelectorAll('canvas').forEach(canvas => {
|
|
129
|
+
// Try to grab existing context via _gl property (Three.js exposes this)
|
|
130
|
+
if (canvas._gl || canvas.__r3f?.gl) {
|
|
131
|
+
const gl = canvas._gl || canvas.__r3f?.gl;
|
|
132
|
+
if (!captured.contexts.includes(gl)) {
|
|
133
|
+
captured.contexts.push(gl);
|
|
134
|
+
console.log('🔌 Found existing WebGL context — patching for future calls');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ═══════════════════════════════════════════════
|
|
140
|
+
// STEP 4 — THREE.JS PROGRAM EXTRACTION
|
|
141
|
+
// Works even on already-loaded pages
|
|
142
|
+
// ═══════════════════════════════════════════════
|
|
143
|
+
function extractFromThreeJS() {
|
|
144
|
+
const results = [];
|
|
145
|
+
|
|
146
|
+
const maybeRenderer = (
|
|
147
|
+
window.__threeRenderer ||
|
|
148
|
+
(window.three && window.three.renderer) ||
|
|
149
|
+
null
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Try to find renderer via R3F internal state
|
|
153
|
+
document.querySelectorAll('canvas').forEach(canvas => {
|
|
154
|
+
const fiber = Object.keys(canvas).find(k => k.startsWith('__reactFiber'));
|
|
155
|
+
if (!fiber) return;
|
|
156
|
+
|
|
157
|
+
// Walk up fiber tree looking for R3F store
|
|
158
|
+
let node = canvas[fiber];
|
|
159
|
+
let depth = 0;
|
|
160
|
+
while (node && depth < 50) {
|
|
161
|
+
const store = node.memoizedState?.memoizedState;
|
|
162
|
+
if (store?.gl) {
|
|
163
|
+
const gl = store.gl;
|
|
164
|
+
const programs = gl.info?.programs || [];
|
|
165
|
+
programs.forEach((prog, idx) => {
|
|
166
|
+
const glProgram = prog.program;
|
|
167
|
+
const dbgExt = gl.getExtension('WEBGL_debug_shaders');
|
|
168
|
+
const uniforms = prog.getUniforms?.() || {};
|
|
169
|
+
|
|
170
|
+
results.push({
|
|
171
|
+
id: idx,
|
|
172
|
+
type: 'three.js-program',
|
|
173
|
+
name: prog.name || `program_${idx}`,
|
|
174
|
+
vertexShader: dbgExt && glProgram
|
|
175
|
+
? (() => {
|
|
176
|
+
try {
|
|
177
|
+
const shaders = gl.getAttachedShaders(glProgram);
|
|
178
|
+
const vs = shaders?.find(s => gl.getShaderParameter(s, gl.SHADER_TYPE) === gl.VERTEX_SHADER);
|
|
179
|
+
return vs ? dbgExt.getTranslatedShaderSource(vs) : prog.vertexShader;
|
|
180
|
+
} catch(e) { return prog.vertexShader; }
|
|
181
|
+
})()
|
|
182
|
+
: prog.vertexShader,
|
|
183
|
+
fragmentShader: dbgExt && glProgram
|
|
184
|
+
? (() => {
|
|
185
|
+
try {
|
|
186
|
+
const shaders = gl.getAttachedShaders(glProgram);
|
|
187
|
+
const fs = shaders?.find(s => gl.getShaderParameter(s, gl.SHADER_TYPE) === gl.FRAGMENT_SHADER);
|
|
188
|
+
return fs ? dbgExt.getTranslatedShaderSource(fs) : prog.fragmentShader;
|
|
189
|
+
} catch(e) { return prog.fragmentShader; }
|
|
190
|
+
})()
|
|
191
|
+
: prog.fragmentShader,
|
|
192
|
+
uniformsMap: Object.keys(uniforms).slice(0, 30),
|
|
193
|
+
_glRef: glProgram,
|
|
194
|
+
_gl: gl,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
node = node.return;
|
|
200
|
+
depth++;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ═══════════════════════════════════════════════
|
|
208
|
+
// STEP 5 — UNIFORM PROBE SYSTEM
|
|
209
|
+
// ═══════════════════════════════════════════════
|
|
210
|
+
function probeUniforms(programId) {
|
|
211
|
+
const allPrograms = [...captured.programs, ...extractFromThreeJS()];
|
|
212
|
+
const prog = allPrograms.find(p => p.id === programId);
|
|
213
|
+
if (!prog) return { error: `Program ${programId} not found` };
|
|
214
|
+
|
|
215
|
+
const gl = prog._gl;
|
|
216
|
+
const glProg = prog._glRef || prog._program;
|
|
217
|
+
if (!gl || !glProg) return { error: 'WebGL reference lost — must intercept at page load' };
|
|
218
|
+
|
|
219
|
+
const report = { programId, probes: [] };
|
|
220
|
+
|
|
221
|
+
// Set each uniform to an extreme value and describe the expected visual effect
|
|
222
|
+
prog.uniforms?.forEach(u => {
|
|
223
|
+
const loc = gl.getUniformLocation(glProg, u.name);
|
|
224
|
+
if (!loc) return;
|
|
225
|
+
|
|
226
|
+
const probe = { name: u.name, type: u.type, probeValue: null, hypothesis: '' };
|
|
227
|
+
|
|
228
|
+
// Based on naming conventions, guess purpose
|
|
229
|
+
const n = u.name.toLowerCase();
|
|
230
|
+
if (/time|tick|clock|elapsed/.test(n)) {
|
|
231
|
+
probe.hypothesis = '⏱ TIME — drives animation loop';
|
|
232
|
+
probe.probeValue = 999;
|
|
233
|
+
} else if (/color|col|rgb/.test(n)) {
|
|
234
|
+
probe.hypothesis = '🎨 COLOR — surface/emission color';
|
|
235
|
+
probe.probeValue = [1, 0, 0]; // solid red
|
|
236
|
+
} else if (/alpha|opacity|dissolve/.test(n)) {
|
|
237
|
+
probe.hypothesis = '🌫 ALPHA — transparency control';
|
|
238
|
+
probe.probeValue = 0;
|
|
239
|
+
} else if (/progress|scroll|offset/.test(n)) {
|
|
240
|
+
probe.hypothesis = '📜 SCROLL/PROGRESS — scroll-driven';
|
|
241
|
+
probe.probeValue = 0.5;
|
|
242
|
+
} else if (/noise|distort|warp/.test(n)) {
|
|
243
|
+
probe.hypothesis = '🌊 DISTORTION — warp/noise intensity';
|
|
244
|
+
probe.probeValue = 10;
|
|
245
|
+
} else if (/mouse|cursor|pointer/.test(n)) {
|
|
246
|
+
probe.hypothesis = '🖱 MOUSE — cursor interaction';
|
|
247
|
+
probe.probeValue = [0.5, 0.5];
|
|
248
|
+
} else if (/resolution|size|dimension/.test(n)) {
|
|
249
|
+
probe.hypothesis = '📐 RESOLUTION — pixel ratio fix';
|
|
250
|
+
probe.probeValue = [window.innerWidth, window.innerHeight];
|
|
251
|
+
} else if (/matrix|proj|view|model/.test(n)) {
|
|
252
|
+
probe.hypothesis = '📷 CAMERA MATRIX — do not patch';
|
|
253
|
+
probe.probeValue = 'SKIP';
|
|
254
|
+
} else if (/envmap|env|ibl/.test(n)) {
|
|
255
|
+
probe.hypothesis = '🌍 ENVIRONMENT — reflection/IBL map';
|
|
256
|
+
probe.probeValue = 'TEXTURE';
|
|
257
|
+
} else {
|
|
258
|
+
probe.hypothesis = '❓ UNKNOWN — probe manually';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
report.probes.push(probe);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return report;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ═══════════════════════════════════════════════
|
|
268
|
+
// STEP 6 — HELPER UTILITIES
|
|
269
|
+
// ═══════════════════════════════════════════════
|
|
270
|
+
function glTypeToString(type) {
|
|
271
|
+
const types = {
|
|
272
|
+
0x8B51: 'vec3', 0x8B52: 'vec4', 0x8B50: 'vec2',
|
|
273
|
+
0x1406: 'float', 0x1404: 'int', 0x8B56: 'bool',
|
|
274
|
+
0x8B5E: 'sampler2D', 0x8B60: 'samplerCube',
|
|
275
|
+
0x8B5B: 'mat3', 0x8B5C: 'mat4',
|
|
276
|
+
};
|
|
277
|
+
return types[type] || `0x${type?.toString(16)}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ═══════════════════════════════════════════════
|
|
281
|
+
// STEP 7 — PUBLIC API
|
|
282
|
+
// ═══════════════════════════════════════════════
|
|
283
|
+
window.__shaderPatch = {
|
|
284
|
+
_captured: captured,
|
|
285
|
+
|
|
286
|
+
listPrograms() {
|
|
287
|
+
const threeProg = extractFromThreeJS();
|
|
288
|
+
const all = [...captured.programs, ...threeProg];
|
|
289
|
+
return all.map(p => ({
|
|
290
|
+
id: p.id,
|
|
291
|
+
type: p.type || 'captured',
|
|
292
|
+
name: p.name || `program_${p.id}`,
|
|
293
|
+
uniformCount: (p.uniforms || p.uniformsMap)?.length || 0,
|
|
294
|
+
hasVertex: !!p.vertexShader || !!p.vertex,
|
|
295
|
+
hasFragment: !!p.fragmentShader || !!p.fragment,
|
|
296
|
+
}));
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
extractGLSL(programId) {
|
|
300
|
+
const all = [...captured.programs, ...extractFromThreeJS()];
|
|
301
|
+
const p = all.find(x => x.id === programId);
|
|
302
|
+
if (!p) return { error: `Program ${programId} not found` };
|
|
303
|
+
return {
|
|
304
|
+
id: programId,
|
|
305
|
+
name: p.name,
|
|
306
|
+
vertex: p.vertex || p.vertexShader,
|
|
307
|
+
fragment: p.fragment || p.fragmentShader,
|
|
308
|
+
uniforms: p.uniforms || p.uniformsMap,
|
|
309
|
+
attributes: p.attributes,
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
probeUniforms(programId) {
|
|
314
|
+
return probeUniforms(programId);
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
extractAll() {
|
|
318
|
+
const allPrograms = [...captured.programs, ...extractFromThreeJS()];
|
|
319
|
+
return {
|
|
320
|
+
programCount: allPrograms.length,
|
|
321
|
+
contextCount: captured.contexts.length,
|
|
322
|
+
programs: allPrograms.map(p => ({
|
|
323
|
+
id: p.id,
|
|
324
|
+
name: p.name || `program_${p.id}`,
|
|
325
|
+
vertexGLSL: (p.vertex || p.vertexShader || '').slice(0, 2000),
|
|
326
|
+
fragmentGLSL: (p.fragment || p.fragmentShader || '').slice(0, 2000),
|
|
327
|
+
uniforms: p.uniforms || p.uniformsMap || [],
|
|
328
|
+
attributes: p.attributes || [],
|
|
329
|
+
})),
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
api() {
|
|
334
|
+
return {
|
|
335
|
+
usage: [
|
|
336
|
+
'window.__shaderPatch.listPrograms() — list all captured shader programs',
|
|
337
|
+
'window.__shaderPatch.extractGLSL(id) — dump vertex+fragment GLSL for program',
|
|
338
|
+
'window.__shaderPatch.probeUniforms(id) — analyze uniforms with hypotheses',
|
|
339
|
+
'window.__shaderPatch.extractAll() — full dump of all programs',
|
|
340
|
+
],
|
|
341
|
+
programsFound: window.__shaderPatch.listPrograms().length,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// Run immediately
|
|
347
|
+
console.log('🔬 Shader Hot-Patch installed. Existing programs will be captured on next render.');
|
|
348
|
+
return window.__shaderPatch.extractAll();
|
|
349
|
+
})()
|