Fix shader rendering: visibility-aware WebGL contexts, fix 2 GLSL shaders
ShaderCanvas rewrite: - IntersectionObserver-driven rendering: WebGL context only created when canvas enters viewport, released when it leaves. Prevents context starvation when 20+ shaders are in the feed simultaneously. - Graceful fallback UI when WebGL context unavailable (hexagon + 'scroll to load') - Context loss/restore event handlers - powerPreference: 'low-power' for feed thumbnails - Pause animation loop when off-screen (saves GPU even with context alive) - Separate resize observer (no devicePixelRatio scaling for feed — saves memory) Fixed shaders: - Pixel Art Dither: replaced mat4 dynamic indexing with unrolled Bayer lookup (some WebGL drivers reject mat4[int_var][int_var]) - Wave Interference 2D: replaced C-style array element assignment with individual vec2 variables (GLSL ES 300 compatibility)
This commit is contained in:
parent
1047a1f5fe
commit
164dda4760
2 changed files with 118 additions and 38 deletions
|
|
@ -692,24 +692,27 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|||
{"chaos_level": 0.2, "color_temperature": "warm", "motion_type": "breathing"})
|
||||
|
||||
s("Pixel Art Dither", """
|
||||
float dither(vec2 p, float v) {
|
||||
mat4 bayer = mat4(
|
||||
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
|
||||
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
|
||||
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
|
||||
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
|
||||
);
|
||||
int x = int(mod(p.x, 4.0));
|
||||
int y = int(mod(p.y, 4.0));
|
||||
float threshold = bayer[y][x];
|
||||
return step(threshold, v);
|
||||
float bayer4(vec2 p) {
|
||||
vec2 q = mod(p, 4.0);
|
||||
float b = mod(q.x + q.y * 4.0, 16.0);
|
||||
// Bayer 4x4 unrolled
|
||||
float t = 0.0;
|
||||
if (b < 1.0) t = 0.0; else if (b < 2.0) t = 8.0;
|
||||
else if (b < 3.0) t = 2.0; else if (b < 4.0) t = 10.0;
|
||||
else if (b < 5.0) t = 12.0; else if (b < 6.0) t = 4.0;
|
||||
else if (b < 7.0) t = 14.0; else if (b < 8.0) t = 6.0;
|
||||
else if (b < 9.0) t = 3.0; else if (b < 10.0) t = 11.0;
|
||||
else if (b < 11.0) t = 1.0; else if (b < 12.0) t = 9.0;
|
||||
else if (b < 13.0) t = 15.0; else if (b < 14.0) t = 7.0;
|
||||
else if (b < 15.0) t = 13.0; else t = 5.0;
|
||||
return t / 16.0;
|
||||
}
|
||||
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 uv = fragCoord / iResolution.xy;
|
||||
vec2 pixel = floor(fragCoord / 4.0);
|
||||
float t = iTime * 0.5;
|
||||
float wave = sin(uv.x * 5.0 + t) * sin(uv.y * 3.0 + t * 0.7) * 0.5 + 0.5;
|
||||
float d = dither(pixel, wave);
|
||||
float d = step(bayer4(pixel), wave);
|
||||
vec3 dark = vec3(0.08, 0.04, 0.15);
|
||||
vec3 light = vec3(0.3, 0.8, 0.6);
|
||||
vec3 col = mix(dark, light, d);
|
||||
|
|
@ -1218,15 +1221,14 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|||
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
||||
float v = 0.0;
|
||||
vec2 sources[4];
|
||||
sources[0] = vec2(sin(iTime), cos(iTime)) * 0.4;
|
||||
sources[1] = vec2(-sin(iTime * 0.7), sin(iTime * 0.5)) * 0.5;
|
||||
sources[2] = vec2(cos(iTime * 0.3), -sin(iTime * 0.8)) * 0.3;
|
||||
sources[3] = vec2(-cos(iTime * 0.6), cos(iTime * 0.4)) * 0.35;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float d = length(uv - sources[i]);
|
||||
v += sin(d * 30.0 - iTime * 5.0) / (1.0 + d * 5.0);
|
||||
}
|
||||
vec2 s0 = vec2(sin(iTime), cos(iTime)) * 0.4;
|
||||
vec2 s1 = vec2(-sin(iTime * 0.7), sin(iTime * 0.5)) * 0.5;
|
||||
vec2 s2 = vec2(cos(iTime * 0.3), -sin(iTime * 0.8)) * 0.3;
|
||||
vec2 s3 = vec2(-cos(iTime * 0.6), cos(iTime * 0.4)) * 0.35;
|
||||
v += sin(length(uv - s0) * 30.0 - iTime * 5.0) / (1.0 + length(uv - s0) * 5.0);
|
||||
v += sin(length(uv - s1) * 30.0 - iTime * 5.0) / (1.0 + length(uv - s1) * 5.0);
|
||||
v += sin(length(uv - s2) * 30.0 - iTime * 5.0) / (1.0 + length(uv - s2) * 5.0);
|
||||
v += sin(length(uv - s3) * 30.0 - iTime * 5.0) / (1.0 + length(uv - s3) * 5.0);
|
||||
vec3 col = 0.5 + 0.5 * cos(v * 3.0 + vec3(0, 2, 4));
|
||||
fragColor = vec4(col, 1.0);
|
||||
}""", ["wave", "interference", "physics", "ripple", "colorful", "multi-source"]),
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@
|
|||
* Shadertoy-compatible: accepts mainImage(out vec4 fragColor, in vec2 fragCoord)
|
||||
* Injects uniforms: iTime, iResolution, iMouse
|
||||
*
|
||||
* Used in the editor (full-size), feed items (thumbnail), and shader detail page.
|
||||
* Features:
|
||||
* - Visibility-aware: only renders when in viewport (IntersectionObserver)
|
||||
* - Graceful WebGL context failure handling
|
||||
* - Debounced recompilation on code change
|
||||
* - Mouse tracking for iMouse uniform
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useRef, useEffect, useCallback, useState } from 'react';
|
||||
|
||||
interface ShaderCanvasProps {
|
||||
code: string;
|
||||
|
|
@ -74,6 +78,8 @@ export default function ShaderCanvas({
|
|||
const animRef = useRef<number>(0);
|
||||
const startTimeRef = useRef<number>(0);
|
||||
const mouseRef = useRef<[number, number, number, number]>([0, 0, 0, 0]);
|
||||
const isVisibleRef = useRef(false);
|
||||
const [glFailed, setGlFailed] = useState(false);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
if (animRef.current) {
|
||||
|
|
@ -91,19 +97,39 @@ export default function ShaderCanvas({
|
|||
const canvas = canvasRef.current;
|
||||
if (!canvas || !code.trim()) return;
|
||||
|
||||
// Don't compile if not visible (saves WebGL contexts)
|
||||
if (!isVisibleRef.current && animate) return;
|
||||
|
||||
let gl = glRef.current;
|
||||
if (!gl) {
|
||||
gl = canvas.getContext('webgl2', {
|
||||
antialias: false,
|
||||
preserveDrawingBuffer: true,
|
||||
preserveDrawingBuffer: false,
|
||||
powerPreference: 'low-power',
|
||||
});
|
||||
if (!gl) {
|
||||
onError?.('WebGL2 not supported');
|
||||
setGlFailed(true);
|
||||
onError?.('WebGL2 not available — too many active contexts');
|
||||
return;
|
||||
}
|
||||
glRef.current = gl;
|
||||
setGlFailed(false);
|
||||
}
|
||||
|
||||
// Handle context loss
|
||||
canvas.addEventListener('webglcontextlost', (e) => {
|
||||
e.preventDefault();
|
||||
cleanup();
|
||||
glRef.current = null;
|
||||
setGlFailed(true);
|
||||
}, { once: true });
|
||||
|
||||
canvas.addEventListener('webglcontextrestored', () => {
|
||||
setGlFailed(false);
|
||||
glRef.current = null; // Will be re-created on next compile
|
||||
compile();
|
||||
}, { once: true });
|
||||
|
||||
// Clean previous program
|
||||
if (programRef.current) {
|
||||
gl.deleteProgram(programRef.current);
|
||||
|
|
@ -148,6 +174,12 @@ export default function ShaderCanvas({
|
|||
// Start render loop
|
||||
const render = () => {
|
||||
if (!programRef.current || !glRef.current) return;
|
||||
// Pause rendering when not visible
|
||||
if (!isVisibleRef.current && animate) {
|
||||
animRef.current = requestAnimationFrame(render);
|
||||
return;
|
||||
}
|
||||
|
||||
const gl = glRef.current;
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
|
|
@ -176,12 +208,49 @@ export default function ShaderCanvas({
|
|||
} catch (e: any) {
|
||||
onError?.(e.message || 'Compilation failed');
|
||||
}
|
||||
}, [code, animate, onError, onCompileSuccess]);
|
||||
}, [code, animate, onError, onCompileSuccess, cleanup]);
|
||||
|
||||
// Visibility observer — only initialize WebGL when in viewport
|
||||
useEffect(() => {
|
||||
compile();
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const wasVisible = isVisibleRef.current;
|
||||
isVisibleRef.current = entries[0]?.isIntersecting ?? false;
|
||||
|
||||
// When becoming visible for the first time, compile
|
||||
if (isVisibleRef.current && !wasVisible) {
|
||||
if (!programRef.current) {
|
||||
compile();
|
||||
}
|
||||
}
|
||||
|
||||
// When going out of viewport, release the WebGL context to free resources
|
||||
if (!isVisibleRef.current && wasVisible && animate) {
|
||||
cleanup();
|
||||
if (glRef.current) {
|
||||
const ext = glRef.current.getExtension('WEBGL_lose_context');
|
||||
ext?.loseContext();
|
||||
glRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
|
||||
observer.observe(canvas);
|
||||
return () => observer.disconnect();
|
||||
}, [compile, cleanup, animate]);
|
||||
|
||||
// Recompile when code changes (for editor use)
|
||||
useEffect(() => {
|
||||
if (isVisibleRef.current || !animate) {
|
||||
compile();
|
||||
}
|
||||
return cleanup;
|
||||
}, [compile, cleanup]);
|
||||
}, [code]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Resize handling
|
||||
useEffect(() => {
|
||||
|
|
@ -193,8 +262,8 @@ export default function ShaderCanvas({
|
|||
const w = entry.contentRect.width;
|
||||
const h = entry.contentRect.height;
|
||||
if (w > 0 && h > 0) {
|
||||
canvas.width = w * (window.devicePixelRatio > 1 ? 1.5 : 1);
|
||||
canvas.height = h * (window.devicePixelRatio > 1 ? 1.5 : 1);
|
||||
canvas.width = Math.floor(w);
|
||||
canvas.height = Math.floor(h);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -216,13 +285,22 @@ export default function ShaderCanvas({
|
|||
};
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width || 640}
|
||||
height={height || 360}
|
||||
className={`block ${className}`}
|
||||
style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%' }}
|
||||
onMouseMove={handleMouseMove}
|
||||
/>
|
||||
<div className={`relative ${className}`} style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%' }}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width || 640}
|
||||
height={height || 360}
|
||||
className="block w-full h-full"
|
||||
onMouseMove={handleMouseMove}
|
||||
/>
|
||||
{glFailed && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-surface-2">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl mb-1">⬡</div>
|
||||
<div className="text-xs text-gray-500">Scroll to load</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue