|
| 1 | +/** |
| 2 | + * Generates tiny preview thumbnails for effect types. |
| 3 | + * Uses a single shared mini-pipeline to render each effect at low res, |
| 4 | + * then caches the result as a data URL. Renders in batches to avoid freezing. |
| 5 | + */ |
| 6 | +import * as THREE from "three"; |
| 7 | +import { FxgenShader, FxgenShaderUtils } from "../pixy-api.js"; |
| 8 | +import { getDefaultEffectController, getDefaultEffectParameters } from "../defaults.js"; |
| 9 | +import { isCustomEffect, getCustomShaderKey } from "../custom-shader-registry.js"; |
| 10 | +import { SHADERS } from "../../shader-defs.js"; |
| 11 | + |
| 12 | +const THUMB_SIZE = 48; |
| 13 | +const BATCH_SIZE = 4; |
| 14 | +const cache = new Map(); // effectType → dataURL |
| 15 | + |
| 16 | +let renderer = null; |
| 17 | +let scene = null; |
| 18 | +let camera = null; |
| 19 | +let rt = null; |
| 20 | +let quad = null; |
| 21 | +let readBuf = null; |
| 22 | +let thumbCanvas = null; |
| 23 | +let thumbCtx = null; |
| 24 | + |
| 25 | +function ensureSetup(sourceRenderer) { |
| 26 | + if (renderer) return; |
| 27 | + renderer = sourceRenderer; |
| 28 | + scene = new THREE.Scene(); |
| 29 | + camera = new THREE.Camera(); |
| 30 | + quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), new THREE.MeshBasicMaterial()); |
| 31 | + scene.add(quad); |
| 32 | + rt = new THREE.WebGLRenderTarget(THUMB_SIZE, THUMB_SIZE, { |
| 33 | + minFilter: THREE.LinearFilter, |
| 34 | + magFilter: THREE.LinearFilter, |
| 35 | + stencilBuffer: false |
| 36 | + }); |
| 37 | + readBuf = new Uint8Array(THUMB_SIZE * THUMB_SIZE * 4); |
| 38 | + thumbCanvas = document.createElement("canvas"); |
| 39 | + thumbCanvas.width = THUMB_SIZE; |
| 40 | + thumbCanvas.height = THUMB_SIZE; |
| 41 | + thumbCtx = thumbCanvas.getContext("2d"); |
| 42 | +} |
| 43 | + |
| 44 | +function renderOne(effectType) { |
| 45 | + if (cache.has(effectType)) return cache.get(effectType); |
| 46 | + |
| 47 | + const prevRT = renderer.getRenderTarget(); |
| 48 | + try { |
| 49 | + if (isCustomEffect(effectType)) { |
| 50 | + const key = getCustomShaderKey(effectType); |
| 51 | + const src = key && SHADERS[key]; |
| 52 | + if (!src) { renderer.setRenderTarget(prevRT); return null; } |
| 53 | + const mat = new THREE.ShaderMaterial({ |
| 54 | + uniforms: { |
| 55 | + uTime: { value: 1.5 }, |
| 56 | + uResolution: { value: new THREE.Vector2(THUMB_SIZE, THUMB_SIZE) }, |
| 57 | + uMouse: { value: new THREE.Vector2(0.5, 0.5) }, |
| 58 | + }, |
| 59 | + vertexShader: `varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position,1.0); }`, |
| 60 | + fragmentShader: src, |
| 61 | + depthTest: false, depthWrite: false, |
| 62 | + }); |
| 63 | + quad.material = mat; |
| 64 | + renderer.setRenderTarget(rt); |
| 65 | + renderer.render(scene, camera); |
| 66 | + renderer.setRenderTarget(prevRT); |
| 67 | + mat.dispose(); |
| 68 | + } else { |
| 69 | + const shader = new FxgenShader(); |
| 70 | + shader.enable(effectType.toUpperCase()); |
| 71 | + shader.enable("TOON"); |
| 72 | + shader.enable("GLSL3"); |
| 73 | + const uniforms = shader.generateUniforms(); |
| 74 | + const mat = shader.createMaterial(uniforms); |
| 75 | + mat.defines = shader.generateDefines(); |
| 76 | + |
| 77 | + const ec = Object.assign(getDefaultEffectController(), getDefaultEffectParameters()); |
| 78 | + ec.type = effectType; |
| 79 | + ec.time = 1.5; |
| 80 | + |
| 81 | + for (const key in ec) { |
| 82 | + if (uniforms[key] !== undefined) { |
| 83 | + FxgenShaderUtils.SetShaderParameter(uniforms, key, ec[key]); |
| 84 | + } |
| 85 | + } |
| 86 | + FxgenShaderUtils.SetShaderParameter(uniforms, "resolution", new THREE.Vector2(THUMB_SIZE, THUMB_SIZE)); |
| 87 | + if (uniforms.time) uniforms.time.value = 1.5; |
| 88 | + |
| 89 | + quad.material = mat; |
| 90 | + renderer.setRenderTarget(rt); |
| 91 | + renderer.render(scene, camera); |
| 92 | + renderer.setRenderTarget(prevRT); |
| 93 | + mat.dispose(); |
| 94 | + } |
| 95 | + |
| 96 | + // Read pixels and draw to canvas |
| 97 | + renderer.readRenderTargetPixels(rt, 0, 0, THUMB_SIZE, THUMB_SIZE, readBuf); |
| 98 | + const imgData = thumbCtx.createImageData(THUMB_SIZE, THUMB_SIZE); |
| 99 | + // WebGL reads bottom-up, flip vertically |
| 100 | + for (let y = 0; y < THUMB_SIZE; y++) { |
| 101 | + const srcRow = (THUMB_SIZE - 1 - y) * THUMB_SIZE * 4; |
| 102 | + const dstRow = y * THUMB_SIZE * 4; |
| 103 | + for (let x = 0; x < THUMB_SIZE * 4; x++) { |
| 104 | + imgData.data[dstRow + x] = readBuf[srcRow + x]; |
| 105 | + } |
| 106 | + } |
| 107 | + thumbCtx.putImageData(imgData, 0, 0); |
| 108 | + const url = thumbCanvas.toDataURL("image/jpeg", 0.7); |
| 109 | + cache.set(effectType, url); |
| 110 | + return url; |
| 111 | + } catch (e) { |
| 112 | + renderer.setRenderTarget(prevRT); |
| 113 | + cache.set(effectType, null); |
| 114 | + return null; |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +/** |
| 119 | + * Generate thumbnails for a list of effect types in batches. |
| 120 | + * Calls onThumb(effectType, dataURL) for each completed thumbnail. |
| 121 | + * Returns a cancel function. |
| 122 | + */ |
| 123 | +export function generateThumbnails(sourceRenderer, effectTypes, onThumb) { |
| 124 | + ensureSetup(sourceRenderer); |
| 125 | + let idx = 0; |
| 126 | + let cancelled = false; |
| 127 | + |
| 128 | + function processBatch() { |
| 129 | + if (cancelled || idx >= effectTypes.length) return; |
| 130 | + const end = Math.min(idx + BATCH_SIZE, effectTypes.length); |
| 131 | + for (let i = idx; i < end; i++) { |
| 132 | + const fx = effectTypes[i]; |
| 133 | + if (cache.has(fx)) { |
| 134 | + onThumb(fx, cache.get(fx)); |
| 135 | + } else { |
| 136 | + const url = renderOne(fx); |
| 137 | + onThumb(fx, url); |
| 138 | + } |
| 139 | + } |
| 140 | + idx = end; |
| 141 | + if (idx < effectTypes.length) { |
| 142 | + requestAnimationFrame(processBatch); |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + requestAnimationFrame(processBatch); |
| 147 | + return () => { cancelled = true; }; |
| 148 | +} |
| 149 | + |
| 150 | +export function getCachedThumbnail(effectType) { |
| 151 | + return cache.get(effectType) || null; |
| 152 | +} |
0 commit comments