WebGL:具有动态数据的 2D 片段着色器

WebGL: 2D Fragment Shader With Dynamic Data

我正在开发一款自上而下的 2D HTML5 游戏,该游戏使用逐像素区域采样和操作来创建波浪涟漪效果。我在 JavaScript 中得到了它并 运行ning,但是在 FireFox 上的性能是不确定的,在 Chrome 上完全不能接受。我考虑过将我的整个原型移植到性能更好的平台上,但在这个过程中了解了 GL 着色器。

我认为让我的算法适应 GL 片段着色器会很简单。我连续第四天试图让我的着色器产生任何输出。我已尽最大努力将其他问题和教程中的解决方案应用到我正在做的事情中,但是 none 其中的解决方案非常接近我的特定需求。

首先,我将从概念上概述我需要发生的事情。然后我将提供代码并解释我迄今为止尝试采用的方法。如果我能弄清楚我需要做什么,我愿意从头开始。

波浪效果的算法按照描述的方式工作here。它涉及通过基于每个像素的波高数据置换某些像素来从源图像渲染新图像,存储在两个矩阵中,图像中的每个像素都有对应的条目。一个用作当前水的状态,另一个存储上一帧的结果以用于计算当前。

每帧:waveMapCurrent 由 waveMapPrevious 的平均值计算

每像素:根据 waveMapCurrent 中的高度和(在伪代码中)newPixelData[current] = sourcePixelData[current+displacement]

计算位移

至少,我需要我的片段着色器能够访问来自当前波高矩阵的数据和用作源的图像。如果我理解正确,那么最大限度地减少我将新数据传递给 GL 管道的次数并在着色器中执行波高计算对性能最有利,但我也可以在我的脚本中进行计算,每帧将更新版本的波高矩阵传递给片段着色器。

在我什至可以考虑片段着色器正在做什么之前,有一个任务是设置一个对象来实际绘制片段。据我所知,这需要设置顶点来表示 canvas 并将它们设置到 canvas 的角以使 WebGL 将其渲染为平面二维图像,但这似乎不直观.我要么需要将其渲染为图像以用作背景纹理,要么初始化第二个 canvas 并将第一个的背景设置为透明(这是我在下面的代码中尝试做的)。如果有任何方法可以让片段着色器达到 运行 并简单地渲染其输出,每个片段对应 1:1 与 canvas/image 的像素,那将是崇高的,但我不假设使用 GLSL。

我尝试做的是将当前波高矩阵打包为纹理并将其作为统一采样器2D 发送。在当前状态下,我的代码 运行s,但 WebGL 告诉我活动纹理 1(我打包为纹理的波高矩阵)不完整,并且它的 minification/magnification 过滤未设置为 NEAREST ,即使我已尝试将其显式设置为 NEAREST。我不知道如何进一步调试它,因为 WebGL 引用我对 gl.drawElements 的调用作为错误的来源。

这是我所能描述的最聪明的说法。这是我拥有的:

    ws.glProgram = function(gl, tex) {

    var flExt = gl.getExtension("OES_texture_float");
    ws.program = gl.createProgram();
    var vertShader = gl.createShader(gl.VERTEX_SHADER);
    var fragShader = gl.createShader(gl.FRAGMENT_SHADER);

    var vertSrc = [

    "attribute vec4 position;",

    "void main(void) {",
        "gl_Position = position;",
    "}"

    ]

    var fragSrc = [
    "precision highp float;",

    "uniform sampler2D canvasTex;",
    "uniform sampler2D dataTex;",

    "uniform vec2 mapSize;",
    "uniform float dispFactor;",
    "uniform float lumFactor;",

    "void main(void) {",


        "vec2 mapCoord = vec2(gl_FragCoord.x+1.5, gl_FragCoord.y+1.5);",
        "float wave = texture2D(dataTex, mapCoord).r;",
        "float displace = wave*dispFactor;",

        "if (displace < 0.0) {",
            "displace = displace+1.0;",
        "}",

        "vec2 srcCoord = vec2(gl_FragCoord.x+displace,gl_FragCoord.y+displace);",

        "if (srcCoord.x < 0.0) {",
            "srcCoord.x = 0.0;",
        "}",
        "else if (srcCoord.x > mapSize.x-2.0) {",
            "srcCoord.x = mapSize.x-2.0;",
        "}",

        "if (srcCoord.y < 0.0) {",
            "srcCoord.y = 0.0;",
        "}",
        "else if (srcCoord.y > mapSize.y-2.0) {",
            "srcCoord.y = mapSize.y-2.0;",
        "}",

        "float lum = wave*lumFactor;",
        "if (lum > 40.0) { lum = 40.0; }",
        "else if (lum < -40.0) { lum = -40.0; }",

        "gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);", // Fragment Shader is not producing output

        /*"gl_FragColor = texture2D(canvasTex, srcCoord);",
        "gl_FragColor.r = gl_FragColor.r + lum;",
        "gl_FragColor.g = gl_FragColor.g + lum;",
        "gl_FragColor.b = gl_FragColor.b + lum;",*/

    "}"];

    vertSrc = vertSrc.join('\n');
    fragSrc = fragSrc.join('\n');

    gl.shaderSource(vertShader, vertSrc);
    gl.compileShader(vertShader);
    gl.attachShader(ws.program, vertShader);

    gl.shaderSource(fragShader, fragSrc);
    gl.compileShader(fragShader);
    gl.attachShader(ws.program, fragShader);

    console.log(gl.getShaderInfoLog(vertShader));

    gl.linkProgram(ws.program);
    gl.useProgram(ws.program);

    // Vertex Data for rendering surface
    var vertices = [ 0,0,0, 1,0,0,
                     0,1,0, 1,1,0 ];
    var indices = [  0,1,2, 0,2,3 ];

    ws.program.vertices = new Float32Array(vertices);
    ws.program.indices = new Float32Array(indices);

    gl.enableVertexAttribArray(0);
    var vBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, ws.program.vertices, gl.STATIC_DRAW);
    gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);

    var iBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, ws.program.indices, gl.STATIC_DRAW);

    // Send texture data from tex to WebGL
    var canvasTex = gl.createTexture();
    gl.activeTexture(gl.TEXTURE2);
    gl.bindTexture(gl.TEXTURE_2D, canvasTex);

    // Non-Power-of-Two Texture Dimensions
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, tex.imageData);
    gl.uniform1i(gl.getUniformLocation(ws.program, "canvasTex"), 2);

    // Send empty wave map to WebGL
    ws.activeWaveMap = new Float32Array((ws.width+2)*(ws.height+2));
    ws.dataPointerGL = gl.createTexture();
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, ws.dataPointerGL);

    // Non-Power-of-Two Texture Dimensions
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, ws.width+2,ws.height+2,0, gl.LUMINANCE, gl.FLOAT, ws.activeWaveMap);
    gl.uniform1i(gl.getUniformLocation(ws.program, "dataTex"), 1);

    // Numeric Uniforms
    gl.uniform2f(gl.getUniformLocation(ws.program, "mapSize"), ws.width+2,ws.height+2);
    gl.uniform1f(gl.getUniformLocation(ws.program, "dispFactor"), ws.dispFactor);
    gl.uniform1f(gl.getUniformLocation(ws.program, "lumFactor"), ws.lumFactor);

    return ws.program;

}

    ws.render = function(gl, moves, canvas) {
    //canvas.clear();
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); // specify gl.clearColor?

    for (g=0, fl=0; g < ws.tempWaveMap.length; g++) {
        for (b=0; b < ws.tempWaveMap[g].length; b++) {
            ws.tempWaveMap[g][b] = ws.activeWaveMap[fl];
            fl += 1;
        }
    }

    for (j=0; j < moves.length; j++) {
        ws.setWave(moves[j],ws.tempWaveMap);
    }

    for (x=1; x <= ws.width; x++) {
        for (y=1; y <= ws.height; y++) {
            ws.resolveWaves(ws.inactiveWaveMap, ws.tempWaveMap, x,y);
        }
    }

    for (g=0, fl=0; g < ws.inactiveWaveMap.length; g++) {
        for (b=0; b < ws.inactiveWaveMap[g].length; b++) {
            ws.outgoingWaveMap[fl] = ws.inactiveWaveMap[g][b];
            ws.inactiveWaveMap[g][b] = ws.tempWaveMap[g][b];
            fl += 1;
        }
    }

    ws.activeWaveMap.set(ws.outgoingWaveMap);

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, ws.width+2,ws.height+2,0, gl.LUMINANCE, gl.FLOAT, ws.activeWaveMap);

    gl.drawElements(gl.TRIANGLES, ws.program.indices.length, gl.UNSIGNED_BYTE, 0);
}

更新: 我已经成功地使用角顶点设置了我的 2D 绘图表面。 (教程 here 对我使用 VAO 打下基础非常有帮助。)现在我正在努力寻找上传、存储和操作数据的最佳方法。

已解决:感谢 gman,我的代码可以正常工作。 wave 行为本身仍然需要调试,但 GL 管道方面的一切都 运行ning 是应该的。除了奇怪的波浪行为外,游戏每隔几秒就会延迟片刻,然后以正常速度恢复。性能测试表明非增量垃圾收集是原因,当禁用水效果时不会发生这种情况,所以它肯定是我的代码中的东西,可能数组 newIndices 每帧都被重新初始化,但我不确定。除非它与GL的行为有关,否则它超出了这个问题的范围。

这是相关代码。除了这里的内容之外,您真正需要知道的是 GL 上下文、用于绘制 2D 表面的顶点着色器和 VAO 是从另一个对象传入的,并且该对象 运行 是 render 函数帧.

function waterStage(gl, vao, vShader) {

var ws = new Object();

ws.width = game.world.width; ws.height = game.world.height;

// Initialize Background Texture
ws.img = game.make.bitmapData(ws.width, ws.height);

ws.img.fill(0,10,40);
ws.img.ctx.strokeStyle = "#5050FF";
ws.img.ctx.lineWidth = 2;
ws.img.ctx.moveTo(0,0);
for (y=0; y < ws.height; y+=10) {
    ws.img.ctx.beginPath();
    ws.img.ctx.moveTo(0,y);
    ws.img.ctx.lineTo(ws.width,y);
    ws.img.ctx.closePath();
    ws.img.ctx.stroke();
}

ws.img.update();

gl.flExt = gl.getExtension("OES_texture_float");

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

// Source Image
ws.srcTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, ws.srcTexture);

    // Enable all texture sizes
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, ws.img.imageData);

delete ws.img;

// Map Textures

ws.clearProgram = gl.createProgram();
gl.attachShader(ws.clearProgram, vShader);

var clearSrc = [
    "precision highp float;",

    "void main(void) {",
        "gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);",
    "}"
];

clearSrc = clearSrc.join("\n");
var clearShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(clearShader, clearSrc);
gl.compileShader(clearShader);
gl.attachShader(ws.clearProgram, clearShader);
gl.linkProgram(ws.clearProgram);

ws.mapTextures = [];
ws.frameBuffers = [];
for (t=0; t < 2; t++) {

    var map = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, map);

        // Enable all texture sizes
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

    // Initialize empty texture of the same size as the canvas
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ws.width, ws.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

    ws.mapTextures.push(map);

    var fbo = gl.createFramebuffer()
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, map, 0);

    ws.frameBuffers.push(fbo);

    gl.useProgram(ws.clearProgram);
    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); // Set output to new map
    gl.vao_ext.bindVertexArrayOES(vao);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
}

gl.bindFramebuffer(gl.FRAMEBUFFER, null);

// Blank texture to be copied to in render()
ws.copyTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, ws.copyTexture);

    // Enable all texture sizes
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ws.width, ws.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

// Blank texture for entering new wave values through GL
ws.nwTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, ws.nwTexture);

    // Enable all texture sizes
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, ws.width, ws.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

ws.newWaves = new Array(ws.width*ws.height);

ws.nwProgram = gl.createProgram();
ws.mapProgram = gl.createProgram();
ws.displaceProgram = gl.createProgram();

gl.attachShader(ws.nwProgram, vShader);
gl.attachShader(ws.mapProgram, vShader);
gl.attachShader(ws.displaceProgram, vShader);

var nwSrc = [
    "precision highp float;",

    "uniform sampler2D newWaves;",
    "uniform sampler2D previous;",
    "uniform vec2 size;",

    "void main(void) {",
        "vec2 texCoord = vec2((gl_FragCoord.x/size.x),(gl_FragCoord.y/size.y));",
        "float nw = texture2D(newWaves, texCoord).r;",

        "if (nw == 0.0) {",
            "gl_FragColor = texture2D(previous, texCoord);",
        "}",
        "else {",
            "float current = texture2D(previous, texCoord).r;",
            "nw = float(current+nw);",
            "gl_FragColor = vec4(nw, nw, nw, 1.0);",
        "}",
    "}"
]

nwSrc = nwSrc.join("\n");
var nwShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(nwShader, nwSrc);
gl.compileShader(nwShader);

console.log(gl.getShaderInfoLog(nwShader));

gl.attachShader(ws.nwProgram, nwShader);
gl.linkProgram(ws.nwProgram);

var mapSrc = [
    "precision highp float;",

    "uniform sampler2D previous;",
    "uniform sampler2D current;",
    "uniform vec2 size;",
    "uniform float damper;",

    "void main(void) {",
        "vec4 surrounding;",
        "vec2 texCoord = vec2((gl_FragCoord.x/size.x),(gl_FragCoord.y/size.y));",

        "float active = texture2D(current, texCoord).r-0.5;",

        "vec2 shifted = vec2(((gl_FragCoord.x-1.0)/size.x),texCoord.y);", // x-1

        "if (gl_FragCoord.x == 0.0) {",
            "surrounding.x = 0.0;",
        "}",
        "else {",
            "surrounding.x = texture2D(previous, shifted).r-0.5;",
        "}",

        "shifted = vec2(((gl_FragCoord.x+1.0)/size.x),texCoord.y);", // x+1

        "if (gl_FragCoord.x == size.x-1.0) {",
            "surrounding.z = 0.0;",
        "}",
        "else {",
            "surrounding.z = texture2D(previous, shifted).r-0.5;",
        "}",

        "shifted = vec2(texCoord.x,((gl_FragCoord.y-1.0)/size.y));", // y-1

        "if (gl_FragCoord.y == 0.0) {",
            "surrounding.y = 0.0;",
        "}",
        "else {",
            "surrounding.y = texture2D(previous, shifted).r-0.5;",
        "}",

        "shifted = vec2(texCoord.x,((gl_FragCoord.y+1.0)/size.y));", // y+1

        "if (gl_FragCoord.y == size.y-1.0) {",
            "surrounding.w = 0.0;",
        "}",
        "else {",
            "surrounding.w = texture2D(previous, shifted).r-0.5;",
        "}",

        "active = ((surrounding.x+surrounding.y+surrounding.z+surrounding.w)/2.0)-active;",
        "active = active-(active/damper);",
        "gl_FragColor = vec4(active+0.5, active+0.5, active+0.5, 1.0);",
        // "gl_FragColor = texture2D(current, vec2(gl_FragCoord.x/size.x),(gl_FragCoord.y/size.y));",
    "}"
];


mapSrc = mapSrc.join("\n");
var mapShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(mapShader, mapSrc);
gl.compileShader(mapShader);

console.log(gl.getShaderInfoLog(mapShader));

gl.attachShader(ws.mapProgram, mapShader);
gl.linkProgram(ws.mapProgram);

var displaceSrc = [
    "precision highp float;",

    "uniform sampler2D current;",
    "uniform sampler2D srcImg;",
    "uniform vec2 size;",
    "uniform float dspFactor;",
    "uniform float lumFactor;",

    "void main(void) {",

        "vec2 texCoord = vec2((gl_FragCoord.x/size.x),(gl_FragCoord.y/size.y));",

        "float wave = texture2D(current, texCoord).r-0.5;",
        "float displacement = wave * dspFactor * 1.5;",

        "if (displacement == 0.0) {",
            "gl_FragColor = texture2D(srcImg, texCoord);",
        "}",
        "else {",

            "if (displacement < 0.0) {",
                "displacement = displacement + 1.0;",
            "}",

            "float lum = wave * lumFactor;",
            "if (lum > 0.16) { lum = 0.16; }",
            "else if (lum < -0.16) { lum = -0.16; }",

            "float dspX = (gl_FragCoord.x+displacement);",
            "float dspY = (gl_FragCoord.y+displacement);",

            "if (dspX < 0.0) { dspX = 0.0; }",
            "else if (dspX >= size.x) { dspX = size.x-1.0; }",
            "if (dspY < 0.0) { dspY = 0.0; }",
            "else if (dspY >= size.y) { dspY = size.y-1.0; }",

            "vec2 srcCoord = vec2((dspX/size.x),(dspY/size.y));",

            // Just for testing
            //"gl_FragColor = texture2D(current, vec2((gl_FragCoord.x/size.x),(gl_FragCoord.y/size.y)));",

            "vec4 newColor = texture2D(srcImg, srcCoord);", // srcCoord
            "gl_FragColor.r = newColor.r+lum;",
            "gl_FragColor.g = newColor.g+lum;",
            "gl_FragColor.b = newColor.b+lum;",
        "}",
    "}"

];

displaceSrc = displaceSrc.join("\n");
var displaceShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(displaceShader, displaceSrc);
gl.compileShader(displaceShader);

console.log(gl.getShaderInfoLog(displaceShader));

gl.attachShader(ws.displaceProgram, displaceShader);
gl.linkProgram(ws.displaceProgram);

ws.render = function(gl, vao, moves) {
    // Calculate wave values as texture data, then render to screen with displacement fragment shader

    if (moves.length > 0) {

        for (x=0, len=ws.width*ws.height; x < len; x++) {
            ws.newWaves[x] = 0;
        }

        var newIndices = [];
        for (m=0; m < moves.length; m++) {
            newIndices.push(moves[m].y*ws.width + moves[m].x);
        }

        for (i=0; i < newIndices.length; i++) {
            ws.newWaves[newIndices[i]] = moves[i].magnitude/1;
        }

        gl.useProgram(ws.nwProgram);

        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, ws.nwTexture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, ws.width, ws.height, 0, gl.LUMINANCE, gl.FLOAT, new Float32Array(ws.newWaves));
        gl.uniform1i(gl.getUniformLocation(ws.nwProgram, "newWaves"), 0);

        gl.activeTexture(gl.TEXTURE1);
        gl.bindTexture(gl.TEXTURE_2D, ws.copyTexture);
        gl.uniform1i(gl.getUniformLocation(ws.nwProgram, "previous"), 1);

        gl.bindFramebuffer(gl.FRAMEBUFFER, ws.frameBuffers[0]); // Set output to previous map texture [0]
        gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, ws.width, ws.height, 0); // Copy mapTextures[0] into copyTexture

        gl.uniform2f(gl.getUniformLocation(ws.nwProgram, "size"), ws.width, ws.height);

        gl.vao_ext.bindVertexArrayOES(vao);
        gl.drawArrays(gl.TRIANGLES, 0, 6);
    }

    // Map Texture Manipulation
    gl.useProgram(ws.mapProgram);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, ws.mapTextures[0]);
    gl.uniform1i(gl.getUniformLocation(ws.mapProgram, "previous"), 0);

    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, ws.copyTexture);
    gl.uniform1i(gl.getUniformLocation(ws.mapProgram, "current"), 1);

    gl.uniform2f(gl.getUniformLocation(ws.mapProgram, "size"), ws.width, ws.height);
    gl.uniform1f(gl.getUniformLocation(ws.mapProgram, "damper"), 1000);

    gl.bindFramebuffer(gl.FRAMEBUFFER, ws.frameBuffers[1]); // Set output to current map texture [1]
    gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, ws.width, ws.height, 0); // Copy mapTextures[1] into copyTexture
    gl.vao_ext.bindVertexArrayOES(vao);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    // Output Texture Manipulation
    gl.useProgram(ws.displaceProgram);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, ws.mapTextures[1]);
    gl.uniform1i(gl.getUniformLocation(ws.displaceProgram, "current"), 0);
    gl.activeTexture(gl.TEXTURE1);
    gl.bindTexture(gl.TEXTURE_2D, ws.srcTexture);
    gl.uniform1i(gl.getUniformLocation(ws.displaceProgram, "srcImg"), 1);
    gl.uniform2f(gl.getUniformLocation(ws.displaceProgram, "size"), ws.width, ws.height);
    gl.uniform1f(gl.getUniformLocation(ws.displaceProgram, "dspFactor"), 20);
    gl.uniform1f(gl.getUniformLocation(ws.displaceProgram, "lumFactor"), 0.5);

    gl.bindFramebuffer(gl.FRAMEBUFFER, null); // Output to canvas
    gl.vao_ext.bindVertexArrayOES(vao);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    ws.mapTextures.sort(function(a,b) { return 1; });
    ws.frameBuffers.sort(function(a,b) { return 1; });
}

您无法从着色器读取 WebGL 中 canvas 的内容,因此您需要创建纹理并将该纹理附加到帧缓冲区(帧缓冲区只是附件的集合)。这样你就可以渲染到纹理并在其他渲染中使用结果

接下来,textures, at least in WebGL 1, are always referenced by texture coordinates which go from 0 to 1 所以这段代码没有多大意义

  vec2 mapCoord = vec2(gl_FragCoord.x+1.5, gl_FragCoord.y+1.5);
  float wave = texture2D(dataTex, mapCoord).r;

gl_FragCoord 不在纹理坐标中,它在绝对目标像素中,这意味着如果您在屏幕中间的矩形 gl_FragCoord 将具有目标像素坐标(不是从 0 开始)。至于+1.5,纹理坐标中任意方向的1.5都是1.5 * 纹理的宽度或高度。

如果您想向左或向右看一个像素,您需要知道纹理的大小。

纹理坐标中的一个像素单位是横向 1 / width 和向下 1 / height。所以换句话说,如果你有一些引用像素的纹理坐标

 vec2 texcoord = vec2(0.5, 0.5);  // center of texture

你想要向右移动一个像素是

 vec2 onePixelRight = texcoord + vec2(1.0 / textureWidth, 0);

textureWidth 的宽度没有在 WebGL1 中传入,所以你必须制作一个制服并自己传入它。

可以看到an example of rendering to a texture through a framebuffer and reading from nearby pixels here

查看您发布的 link 您需要 4 个纹理

  • 1 个纹理是您的图像(我猜您想要替换它?)
  • 1 个纹理是您当前的 wave
  • 1 个纹理是您的上一波
  • 1 个纹理是您的下一波

并且您需要 3 个帧缓冲区。每波一个

  • 每帧

    • 绑定使用下一波的帧缓冲区,以便您渲染到下一波
    • 使用根据前一波计算当前波的着色器进行渲染
    • 为帧缓冲区绑定 null,这样您将渲染到 canvas
    • 使用使用当前波形纹理作为图像纹理位移的着色器进行渲染
    • 将 newWave 切换为当前,当前为上一个,上一个为下一个

      注意:(这只是在代码中更改变量,不需要移动数据)

这是一些基于原始代码的代码。因为我不能 运行 原版,所以我不知道它应该是什么样子。

function main() {
  var width = 256;
  var height = 256;
  var gl = document.querySelector("canvas").getContext("webgl");
  var flExt = gl.getExtension("OES_texture_float");
  // you should always check for this unless you want confused users
  if (!flExt) {
    alert("no floating point textures available on your system");
    return;
  }

  // the algoritm from the article requires all textures and the canvas
  // to be the same size
  gl.canvas.width = width;
  gl.canvas.height = height;


  // template literals or script tags are way easier than arrays of strings
  var vertSrc = `
attribute vec4 position;

void main(void) {
  gl_Position = position;
}
`;

  var waveFragSrc = `
precision highp float;

uniform sampler2D currentSourceMap;
uniform sampler2D previousResultMap;

uniform float damp;
uniform vec2 textureSize;

void main(void) {
  vec2 onePixel = 1. / textureSize;

  // this only works because we're drawing a quad the size of the texture
  // normally I'd pass in texture coords
  vec2 xy = gl_FragCoord.xy / textureSize;

  vec4 n = 
    (texture2D(currentSourceMap, xy + onePixel * vec2(-1, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(-2, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+1, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+2, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,-1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,-2)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,+1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,+2)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(-1,-1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+1,-1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(-1,+1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+1,+1))
    ) / 6.0 - texture2D(previousResultMap, xy);
   gl_FragColor = n - n / damp;

}

`;

  // need another shader to draw the result texture to the screen
  var displaceFragSrc = `
precision highp float;

uniform vec2 resolution;
uniform sampler2D waveMap;
uniform sampler2D backgroundImage;
uniform float rIndex;

// this code assumes the wavemap and the image and the destination
// are all the same resolution
void main() {
   vec2 onePixel = 1. / resolution;

   // this only works because we're drawing a quad the size of the texture
   // normally I'd pass in texture coords
   vec2 xy = gl_FragCoord.xy / resolution;

   float xDiff = floor(texture2D(waveMap, xy + onePixel * vec2(1, 0)) -
                       texture2D(waveMap, xy)).r;
   float yDiff = floor(texture2D(waveMap, xy + onePixel * vec2(0, 1)) -
                       texture2D(waveMap, xy)).r;

   float xAngle = atan( xDiff );
   float xRefraction = asin( sin( xAngle ) / rIndex );
   float xDisplace = floor( tan( xRefraction ) * xDiff );

   float yAngle = atan( yDiff );
   float yRefraction = asin( sin( yAngle ) / rIndex );
   float yDisplace = floor( tan( yRefraction ) * yDiff );

   if (xDiff < 0.) {
      // { Current position is higher - Clockwise rotation }
      if (yDiff < 0.) {
        gl_FragColor = texture2D(
            backgroundImage, xy + onePixel * vec2(-xDisplace, -yDisplace));
   } else {
        gl_FragColor = texture2D(
             backgroundImage, xy + onePixel * vec2(-xDisplace, +yDisplace));
      }
   } else {
      // { Current position is lower - Counterclockwise rotation }
      if (yDiff < 0.) {
        gl_FragColor = texture2D(backgroundImage, vec2(+xDisplace, -yDisplace));
   } else {
        gl_FragColor = texture2D(backgroundImage, vec2(+xDisplace, +yDisplace));
      }
   }
}

`;

  // use some boilerplate. I'm too lazy to type all the code for looking
  // up uniforms and setting them when a tiny piece of code can hide all
  // that for me. Look up the library if it's not clear that `setUniforms`
  // does lots of `gl.uniformXXX` etc...

  // also Use **MUST** look up the attribute locations or assign them with
  // gl.bindAttribLocation **BEFORE** linking otherwise your code
  // is not portable and may not match across programs. This boilerplate
  // calls gl.bindAttributeLocation for the names passed in the 3rd argument
  var waveProgramInfo = twgl.createProgramInfo(gl, [vertSrc, waveFragSrc], ["position"]);
  var displaceProgramInfo = twgl.createProgramInfo(gl, [vertSrc, displaceFragSrc], ["position"]);

  var positionLocation = 0; // see above

  // Vertex Data for rendering surface
  // no reason for 3d points when drawing 2d
  // Not using indices. It's several more lines of un-needed code
  var vertices = new Float32Array([
    -1,-1,  1,-1, -1,1,
     1,-1, -1, 1,  1,1,
  ]);

  var vBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  // Send texture data from tex to WebGL
  var imageTex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, imageTex);

  // since we don't have an image lets make one with a canvas 2d.
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = width;
  ctx.canvas.height = height;
  for (var y = 0; y < width; y += 16) {
    for (var x = 0; x < height; x += 16) {
      ctx.fillStyle = "rgb(" + x / width * 256 +
                      ","    + y / height * 256 +
                      ","    + (x / 16 + y / 16) % 2 * 255 +
                      ")";
      ctx.fillRect(x, y, 16, 16);
    }
  }

  // Non-Power-of-Two Texture Dimensions
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, ctx.canvas);


  // make some data for the wave
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.strokeStyle = "rgb(1,1,1)";
  ctx.lineWidth = 30;
  ctx.beginPath();
  ctx.arc(ctx.canvas.width / 2, ctx.canvas.height / 2,
          ctx.canvas.width / 3, 0, Math.PI * 2, false);
  ctx.stroke();

  // You can NOT use any kind of filtering on FLOAT textures unless
  // you check for and enable OES_texture_float_linear. Note that
  // no mobile devices support it as of 2017/1

  // create 3 wave textures and 3 framebuffers, prevs, current, and next
  var waves = [];
  for (var i = 0; i < 3; ++i) {
    var tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);

    // Non-Power-of-Two Texture Dimensions
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.FLOAT, ctx.canvas);

    var fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D,
                            tex, 0);
    if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
      alert("can not render to floating point textures");
      return;
    }
    waves.push({ texture: tex, framebuffer: fb });
  }

  function render() {
    var previousWave = waves[0];
    var currentWave = waves[1];
    var nextWave = waves[2];

    // while you're only drawing 1 thing at the moment if you want to draw
    // more than one you'll need to set attributes before each draw if
    // data changes

    gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

    // draw to next wave
    gl.bindFramebuffer(gl.FRAMEBUFFER, nextWave.framebuffer);
    gl.viewport(0, 0, width, height);

    gl.useProgram(waveProgramInfo.program);

    // pass current and previous textures to shader
    twgl.setUniforms(waveProgramInfo, {
      currentSourceMap: currentWave.texture,
      previousResultMap: previousWave.texture,
      textureSize: [width, height],
      damp: 4,
    });

    gl.drawArrays(gl.TRIANGLES, 0, 6);

    // draw to canvas
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.useProgram(displaceProgramInfo.program);

    // pass in the next wave to the displacement shader
    twgl.setUniforms(displaceProgramInfo, {
      waveMap: nextWave.texture,
      backgroundImage: imageTex,
      resolution: [gl.canvas.width, gl.canvas.height],
      rIndex: 4,
    });

    gl.drawArrays(gl.TRIANGLES, 0, 6);

    // swap the buffers. 
    var temp = waves[0];
    waves[0] = waves[1];
    waves[1] = waves[2];
    waves[2] = temp;
    
    requestAnimationFrame(render);
  }
  
  requestAnimationFrame(render);
}
main();
<script src="https://twgljs.org/dist/2.x/twgl.min.js"></script>
<canvas></canvas>

效果非常老派,因为它是基于像素的并且有很多整数,而着色器使用浮点数。我得到的印象是,确切的算法并不是真正适合着色器。或者更确切地说,您可以使用不同的算法获得更好的结果。

function main() {
  var width = 256;
  var height = 256;
  var gl = document.querySelector("canvas").getContext("webgl");
  var flExt = gl.getExtension("OES_texture_float");
  // you should always check for this unless you want confused users
  if (!flExt) {
    alert("no floating point textures available on your system");
    return;
  }

  // the algoritm from the article requires all textures and the canvas
  // to be the same size
  gl.canvas.width = width;
  gl.canvas.height = height;


  // template literals or script tags are way easier than arrays of strings
  var vertSrc = `
attribute vec4 position;

void main(void) {
  gl_Position = position;
}
`;  
  var waveFragSrc = `
precision highp float;

uniform sampler2D currentSourceMap;
uniform sampler2D previousResultMap;

uniform float damp;
uniform vec2 textureSize;

void main(void) {
  vec2 onePixel = 1. / textureSize;

  // this only works because we're drawing a quad the size of the texture
  // normally I'd pass in texture coords
  vec2 xy = gl_FragCoord.xy / textureSize;

  vec4 n = 
    (texture2D(currentSourceMap, xy + onePixel * vec2(-1, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(-2, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+1, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+2, 0)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,-1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,-2)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,+1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2( 0,+2)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(-1,-1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+1,-1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(-1,+1)) +
     texture2D(currentSourceMap, xy + onePixel * vec2(+1,+1))
    ) / 6.0 - texture2D(previousResultMap, xy);
   gl_FragColor = n - n / damp;

}

`;

  // need another shader to draw the result texture to the screen
  var displaceFragSrc = `
precision highp float;

uniform vec2 resolution;
uniform sampler2D waveMap;
uniform sampler2D backgroundImage;
uniform float fudge;

// this code assumes the wavemap and the image and the destination
// are all the same resolution
void main() {
   vec2 onePixel = 1. / resolution;

   // this only works because we're drawing a quad the size of the texture
   // normally I'd pass in texture coords
   vec2 xy = gl_FragCoord.xy / resolution;

   float xDiff = (texture2D(waveMap, xy + onePixel * vec2(1, 0)) -
                  texture2D(waveMap, xy)).r;
   float yDiff = (texture2D(waveMap, xy + onePixel * vec2(0, 1)) -
                  texture2D(waveMap, xy)).r;

   gl_FragColor = texture2D(
      backgroundImage, xy + onePixel * vec2(xDiff, yDiff) * fudge);
}

`;
  
  var pntVertSrc = `
uniform vec2 position;
uniform float pointSize;

void main(void) {
  gl_Position = vec4(position, 0, 1);
  gl_PointSize = pointSize; 
}
`;  
  var pntFragSrc = `
precision mediump float;

void main(void) {
  gl_FragColor = vec4(1);
}
`;  
  

  // use some boilerplate. I'm too lazy to type all the code for looking
  // up uniforms and setting them when a tiny piece of code can hide all
  // that for me. Look up the library if it's not clear that `setUniforms`
  // does lots of `gl.uniformXXX` etc...

  // also Use **MUST** look up the attribute locations or assign them with
  // gl.bindAttribLocation **BEFORE** linking otherwise your code
  // is not portable and may not match across programs. This boilerplate
  // calls gl.bindAttributeLocation for the names passed in the 3rd argument
  var waveProgramInfo = twgl.createProgramInfo(
    gl, [vertSrc, waveFragSrc], ["position"]);
  var displaceProgramInfo = twgl.createProgramInfo(
    gl, [vertSrc, displaceFragSrc], ["position"]);
  var pntProgramInfo = twgl.createProgramInfo(
    gl, [pntVertSrc, pntFragSrc], ["position"]);
  
  var positionLocation = 0; // see above

  // Vertex Data for rendering surface
  // no reason for 3d points when drawing 2d
  // Not using indices. It's several more lines of un-needed code
  var vertices = new Float32Array([
    -1,-1,  1,-1, -1,1,
     1,-1, -1, 1,  1,1,
  ]);

  var vBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  // Send texture data from tex to WebGL
  var imageTex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, imageTex);

  // since we don't have an image lets make one with a canvas 2d.
  var ctx = document.createElement("canvas").getContext("2d");
  ctx.canvas.width = width;
  ctx.canvas.height = height;
  for (var y = 0; y < width; y += 16) {
    for (var x = 0; x < height; x += 16) {
      ctx.fillStyle = "rgb(" + x / width * 256 +
                      ","    + y / height * 256 +
                      ","    + (x / 16 + y / 16) % 2 * 255 +
                      ")";
      ctx.fillRect(x, y, 16, 16);
    }
  }

  // Non-Power-of-Two Texture Dimensions
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, ctx.canvas);


  // make some data for the wave
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.strokeStyle = "rgb(255,255,255)";
  ctx.lineWidth = 30;
  ctx.beginPath();
  ctx.arc(ctx.canvas.width / 2, ctx.canvas.height / 2,
          ctx.canvas.width / 3, 0, Math.PI * 2, false);
  ctx.stroke();

  // You can NOT use any kind of filtering on FLOAT textures unless
  // you check for and enable OES_texture_float_linear. Note that
  // no mobile devices support it as of 2017/1

  // create 3 wave textures and 3 framebuffers, prevs, current, and next
  var waves = [];
  for (var i = 0; i < 3; ++i) {
    var tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);

    // Non-Power-of-Two Texture Dimensions
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.FLOAT, ctx.canvas);

    var fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D,
                            tex, 0);
    if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
      alert("can not render to floating point textures");
      return;
    }
    waves.push({ texture: tex, framebuffer: fb });
  }

  function render(time) {
    time *= 0.001; // convert to seconds
    
    var previousWave = waves[0];
    var currentWave = waves[1];
    var nextWave = waves[2];

    // while you're only drawing 1 thing at the moment if you want to draw
    // more than one you'll need to set attributes before each draw if
    // data changes

    gl.bindBuffer(gl.ARRAY_BUFFER, vBuffer);
    gl.enableVertexAttribArray(positionLocation);
    gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
    
    // draw to next wave
    gl.bindFramebuffer(gl.FRAMEBUFFER, nextWave.framebuffer);
    gl.viewport(0, 0, width, height);

    gl.useProgram(waveProgramInfo.program);

    // pass current and previous textures to shader
    twgl.setUniforms(waveProgramInfo, {
      currentSourceMap: currentWave.texture,
      previousResultMap: previousWave.texture,
      textureSize: [width, height],
      damp: 40,
    });

    gl.drawArrays(gl.TRIANGLES, 0, 6);
    
    // draw dot to next wave add waves
    gl.useProgram(pntProgramInfo.program);
    
    twgl.setUniforms(pntProgramInfo, {
      position: [ Math.sin(time * 0.71), Math.cos(time) ],
      pointSize: 8,
    });
    gl.drawArrays(gl.POINTS, 0, 1);

    // draw to canvas
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    gl.useProgram(displaceProgramInfo.program);

    // pass in the next wave to the displacement shader
    twgl.setUniforms(displaceProgramInfo, {
      waveMap: nextWave.texture,
      backgroundImage: imageTex,
      resolution: [gl.canvas.width, gl.canvas.height],
      fudge: 100,
    });

    gl.drawArrays(gl.TRIANGLES, 0, 6);

    // swap the buffers. 
    var temp = waves[0];
    waves[0] = waves[1];
    waves[1] = waves[2];
    waves[2] = temp;
    
    requestAnimationFrame(render);
  }
  
  requestAnimationFrame(render);
}
main();
<script src="https://twgljs.org/dist/2.x/twgl.min.js"></script>
<canvas></canvas>