Hotdry.
application-security

使用 WebGL 构建浏览器端交互式着色器预览工具

本文指导如何利用 WebGL 开发浏览器内的交互式 GLSL 着色器预览器,支持实时实验和效果链组合,实现无编译开销的开发流程。

在图形编程领域,交互式着色器工具已成为开发者实验 GLSL 代码的利器。传统桌面工具如 ShaderGlass 提供了强大的 shader 叠加功能,但受限于平台。本文探讨如何使用 WebGL 在浏览器中构建一个交互式 shader 预览器,实现实时 GLSL 实验和效果链组合,而无需额外的编译开销。这种浏览器端工具的优势在于跨平台性强,便于分享和协作,开发者可以直接在网页中修改代码并即时看到效果。

首先,理解构建此类工具的核心观点:实时性和无编译开销。WebGL 作为浏览器原生 API,直接支持 GLSL ES 着色器编译,无需外部编译器。相比桌面工具,浏览器环境允许无缝集成代码编辑器,如 CodeMirror 或 Monaco Editor,实现编辑 - 预览一体化。证据显示,现有的开源项目如 ShaderToy 已证明这种模式的有效性,它支持用户实时编辑片段着色器并预览动态效果。同样,VSCode 扩展如 shader-toy 进一步将此功能集成到 IDE 中,证明了 WebGL 在实时实验中的潜力。

ShaderGlass 作为一个桌面参考,其功能如应用 shader 到桌面叠加或窗口克隆,启发我们设计浏览器工具的交互模式。[1] 例如,ShaderGlass 支持 RetroArch shader 库,包含 1200 多个预设,用于 CRT 模拟、上采样和模糊效果。这表明效果链(chaining)是关键特性:在 WebGL 中,通过帧缓冲对象(FBO)实现多 pass 渲染,即可链式应用多个 shader。例如,先应用一个模糊 pass,再叠加 CRT 效果,而无需重载整个场景。

要落地构建,我们从基础 WebGL 上下文入手。使用 HTML5 Canvas 元素获取 WebGLRenderingContext:

const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
    console.error('WebGL not supported');
}

接下来,加载和编译 shader。创建一个函数动态编译 vertex 和 fragment shader:

function createShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('Shader compile error:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}

function createProgram(gl, vertexSource, fragmentSource) {
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error('Program link error:', gl.getProgramInfoLog(program));
        gl.deleteProgram(program);
        return null;
    }
    return program;
}

对于实时实验,集成一个代码编辑器。使用 CodeMirror 库,监听代码变化事件,动态更新 fragment shader:

const editor = CodeMirror.fromTextArea(document.getElementById('shaderEditor'), {
    mode: 'text/x-glsl',
    lineNumbers: true,
    theme: 'monokai'
});

let currentProgram = null;
editor.on('change', () => {
    const fragmentSource = editor.getValue();
    currentProgram = createProgram(gl, vertexSource, fragmentSource);
    if (currentProgram) {
        render(); // 重新渲染
    }
});

渲染循环使用 requestAnimationFrame,确保 60 FPS 平滑预览:

function render() {
    if (!currentProgram) return;
    gl.useProgram(currentProgram);
    // 设置 uniforms,如时间、鼠标位置
    gl.uniform1f(gl.getUniformLocation(currentProgram, 'u_time'), performance.now() / 1000);
    gl.uniform2f(gl.getUniformLocation(currentProgram, 'u_resolution'), canvas.width, canvas.height);
    
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLES, 0, 3); // 全屏三角形
    requestAnimationFrame(render);
}

效果链是高级功能。通过创建多个 FBO,实现 ping-pong 渲染:

  1. 创建两个 FBO:readFBO 和 writeFBO,每个附着纹理。
function createFBO(gl, width, height) {
    const fbo = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
    
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
    
    const renderbuffer = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height);
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer);
    
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    return { fbo, texture, renderbuffer };
}
  1. 对于链式效果,交替渲染到 read/write FBO:
// 假设 shaders 数组包含多个 fragment sources
let readFBO = createFBO(gl, canvas.width, canvas.height);
let writeFBO = createFBO(gl, canvas.width, canvas.height);

function chainRender(shaders) {
    let currentRead = readFBO;
    let currentWrite = writeFBO;
    
    for (let i = 0; i < shaders.length; i++) {
        gl.bindFramebuffer(gl.FRAMEBUFFER, currentWrite.fbo);
        const program = createProgram(gl, vertexSource, shaders[i]);
        gl.useProgram(program);
        gl.uniform1i(gl.getUniformLocation(program, 'u_texture'), 0);
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, currentRead.texture);
        
        gl.viewport(0, 0, canvas.width, canvas.height);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLES, 0, 3);
        
        // Swap
        [currentRead, currentWrite] = [currentWrite, currentRead];
    }
    
    // 最终渲染到屏幕
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, currentRead.texture);
    // 使用最终 shader 渲染到 canvas
}

可落地参数和清单:

  • 分辨率处理:始终设置 u_resolution uniform 为 canvas.clientWidth * devicePixelRatio 和 height * devicePixelRatio,确保高 DPI 支持。

  • 性能监控:集成 stats.js 库,显示 FPS。阈值:如果 FPS < 30,建议简化 shader 或降低分辨率。

  • 错误处理清单

    1. 检查 gl.getError () 在每个 API 调用后。
    2. 使用 try-catch 包裹 shader 编译。
    3. 提供用户友好错误提示,如 "着色器语法错误在第 X 行"。
  • uniforms 参数

    • 时间:u_time (float, 自动递增)。
    • 鼠标:u_mouse (vec2, 归一化坐标 0-1)。
    • 帧率:iSampleRate (float, 1/60)。
    • 纹理通道:iChannel0-3,支持图像加载 via ImageBitmap。
  • 回滚策略:保存上一个有效 shader 源代码,如果新编译失败,自动回滚并显示警告。

  • 集成预设:加载外部 shader 库,如从 ShaderToy API 导入,支持一键应用 CRT 或模糊效果。

这种构建方式确保了无编译开销:shader 源代码变化时,直接调用 gl.compileShader 和 gl.linkProgram,浏览器 GPU 即时处理,通常 <100ms。相比桌面工具,浏览器版本便于部署到 GitHub Pages,实现全球协作实验。

最后,带上资料来源:[1] https://github.com/mausimus/ShaderGlass (ShaderGlass 项目,提供交互式 shader 工具灵感)。[2] https://www.shadertoy.com/ (浏览器端实时 shader 预览示例)。

(字数约 1250)

查看归档