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"})
|
{"chaos_level": 0.2, "color_temperature": "warm", "motion_type": "breathing"})
|
||||||
|
|
||||||
s("Pixel Art Dither", """
|
s("Pixel Art Dither", """
|
||||||
float dither(vec2 p, float v) {
|
float bayer4(vec2 p) {
|
||||||
mat4 bayer = mat4(
|
vec2 q = mod(p, 4.0);
|
||||||
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
|
float b = mod(q.x + q.y * 4.0, 16.0);
|
||||||
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
|
// Bayer 4x4 unrolled
|
||||||
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
|
float t = 0.0;
|
||||||
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.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;
|
||||||
int x = int(mod(p.x, 4.0));
|
else if (b < 5.0) t = 12.0; else if (b < 6.0) t = 4.0;
|
||||||
int y = int(mod(p.y, 4.0));
|
else if (b < 7.0) t = 14.0; else if (b < 8.0) t = 6.0;
|
||||||
float threshold = bayer[y][x];
|
else if (b < 9.0) t = 3.0; else if (b < 10.0) t = 11.0;
|
||||||
return step(threshold, v);
|
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) {
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||||
vec2 uv = fragCoord / iResolution.xy;
|
vec2 uv = fragCoord / iResolution.xy;
|
||||||
vec2 pixel = floor(fragCoord / 4.0);
|
vec2 pixel = floor(fragCoord / 4.0);
|
||||||
float t = iTime * 0.5;
|
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 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 dark = vec3(0.08, 0.04, 0.15);
|
||||||
vec3 light = vec3(0.3, 0.8, 0.6);
|
vec3 light = vec3(0.3, 0.8, 0.6);
|
||||||
vec3 col = mix(dark, light, d);
|
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) {
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||||
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
||||||
float v = 0.0;
|
float v = 0.0;
|
||||||
vec2 sources[4];
|
vec2 s0 = vec2(sin(iTime), cos(iTime)) * 0.4;
|
||||||
sources[0] = vec2(sin(iTime), cos(iTime)) * 0.4;
|
vec2 s1 = vec2(-sin(iTime * 0.7), sin(iTime * 0.5)) * 0.5;
|
||||||
sources[1] = vec2(-sin(iTime * 0.7), sin(iTime * 0.5)) * 0.5;
|
vec2 s2 = vec2(cos(iTime * 0.3), -sin(iTime * 0.8)) * 0.3;
|
||||||
sources[2] = vec2(cos(iTime * 0.3), -sin(iTime * 0.8)) * 0.3;
|
vec2 s3 = vec2(-cos(iTime * 0.6), cos(iTime * 0.4)) * 0.35;
|
||||||
sources[3] = 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);
|
||||||
for (int i = 0; i < 4; i++) {
|
v += sin(length(uv - s1) * 30.0 - iTime * 5.0) / (1.0 + length(uv - s1) * 5.0);
|
||||||
float d = length(uv - sources[i]);
|
v += sin(length(uv - s2) * 30.0 - iTime * 5.0) / (1.0 + length(uv - s2) * 5.0);
|
||||||
v += sin(d * 30.0 - iTime * 5.0) / (1.0 + d * 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));
|
vec3 col = 0.5 + 0.5 * cos(v * 3.0 + vec3(0, 2, 4));
|
||||||
fragColor = vec4(col, 1.0);
|
fragColor = vec4(col, 1.0);
|
||||||
}""", ["wave", "interference", "physics", "ripple", "colorful", "multi-source"]),
|
}""", ["wave", "interference", "physics", "ripple", "colorful", "multi-source"]),
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,14 @@
|
||||||
* Shadertoy-compatible: accepts mainImage(out vec4 fragColor, in vec2 fragCoord)
|
* Shadertoy-compatible: accepts mainImage(out vec4 fragColor, in vec2 fragCoord)
|
||||||
* Injects uniforms: iTime, iResolution, iMouse
|
* 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 {
|
interface ShaderCanvasProps {
|
||||||
code: string;
|
code: string;
|
||||||
|
|
@ -74,6 +78,8 @@ export default function ShaderCanvas({
|
||||||
const animRef = useRef<number>(0);
|
const animRef = useRef<number>(0);
|
||||||
const startTimeRef = useRef<number>(0);
|
const startTimeRef = useRef<number>(0);
|
||||||
const mouseRef = useRef<[number, number, number, number]>([0, 0, 0, 0]);
|
const mouseRef = useRef<[number, number, number, number]>([0, 0, 0, 0]);
|
||||||
|
const isVisibleRef = useRef(false);
|
||||||
|
const [glFailed, setGlFailed] = useState(false);
|
||||||
|
|
||||||
const cleanup = useCallback(() => {
|
const cleanup = useCallback(() => {
|
||||||
if (animRef.current) {
|
if (animRef.current) {
|
||||||
|
|
@ -91,19 +97,39 @@ export default function ShaderCanvas({
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas || !code.trim()) return;
|
if (!canvas || !code.trim()) return;
|
||||||
|
|
||||||
|
// Don't compile if not visible (saves WebGL contexts)
|
||||||
|
if (!isVisibleRef.current && animate) return;
|
||||||
|
|
||||||
let gl = glRef.current;
|
let gl = glRef.current;
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
gl = canvas.getContext('webgl2', {
|
gl = canvas.getContext('webgl2', {
|
||||||
antialias: false,
|
antialias: false,
|
||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: false,
|
||||||
|
powerPreference: 'low-power',
|
||||||
});
|
});
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
onError?.('WebGL2 not supported');
|
setGlFailed(true);
|
||||||
|
onError?.('WebGL2 not available — too many active contexts');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
glRef.current = gl;
|
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
|
// Clean previous program
|
||||||
if (programRef.current) {
|
if (programRef.current) {
|
||||||
gl.deleteProgram(programRef.current);
|
gl.deleteProgram(programRef.current);
|
||||||
|
|
@ -148,6 +174,12 @@ export default function ShaderCanvas({
|
||||||
// Start render loop
|
// Start render loop
|
||||||
const render = () => {
|
const render = () => {
|
||||||
if (!programRef.current || !glRef.current) return;
|
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 gl = glRef.current;
|
||||||
const w = canvas.width;
|
const w = canvas.width;
|
||||||
const h = canvas.height;
|
const h = canvas.height;
|
||||||
|
|
@ -176,12 +208,49 @@ export default function ShaderCanvas({
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
onError?.(e.message || 'Compilation failed');
|
onError?.(e.message || 'Compilation failed');
|
||||||
}
|
}
|
||||||
}, [code, animate, onError, onCompileSuccess]);
|
}, [code, animate, onError, onCompileSuccess, cleanup]);
|
||||||
|
|
||||||
|
// Visibility observer — only initialize WebGL when in viewport
|
||||||
useEffect(() => {
|
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;
|
return cleanup;
|
||||||
}, [compile, cleanup]);
|
}, [code]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Resize handling
|
// Resize handling
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -193,8 +262,8 @@ export default function ShaderCanvas({
|
||||||
const w = entry.contentRect.width;
|
const w = entry.contentRect.width;
|
||||||
const h = entry.contentRect.height;
|
const h = entry.contentRect.height;
|
||||||
if (w > 0 && h > 0) {
|
if (w > 0 && h > 0) {
|
||||||
canvas.width = w * (window.devicePixelRatio > 1 ? 1.5 : 1);
|
canvas.width = Math.floor(w);
|
||||||
canvas.height = h * (window.devicePixelRatio > 1 ? 1.5 : 1);
|
canvas.height = Math.floor(h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -216,13 +285,22 @@ export default function ShaderCanvas({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<div className={`relative ${className}`} style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%' }}>
|
||||||
ref={canvasRef}
|
<canvas
|
||||||
width={width || 640}
|
ref={canvasRef}
|
||||||
height={height || 360}
|
width={width || 640}
|
||||||
className={`block ${className}`}
|
height={height || 360}
|
||||||
style={{ width: width ? `${width}px` : '100%', height: height ? `${height}px` : '100%' }}
|
className="block w-full h-full"
|
||||||
onMouseMove={handleMouseMove}
|
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