具有自己输入的多通道 ShaderToy

Multipass ShaderToy with it's own input

我一直在尝试将着色器从 ShaderToy 移植到常规 WebGL,但遇到了一些困难。我设法使多通道着色器工作,但不是那些将自己作为输入通道的着色器。我在这里做了一个测试例子:https://www.shadertoy.com/view/WsfSzj。着色器有两个通道,都将缓冲区 A 作为输入。据我了解,当缓冲区将自身作为输入时,它会使用上一个时间步长自身的输出。

所以我尝试用以下结构制作两个渲染目标:

class RenderTarget { 
   tex: Texture 
   fbo: FrameBuffer 
}

使用渲染循环:

gl.useProgram(prog1) 
gl.bindFramebuffer(gl.TEXTURE_2D, rt1.fbo) 
gl.bindTexture(gl.TEXTURE_2D, rt2.texture); 
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

gl.useProgram(prog2) 
gl.bindFramebuffer(gl.TEXTURE_2D, null) 
gl.bindTexture(gl.TEXTURE_2D, rt1.texture); 
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

swap(rt1, rt2)

然而,这只渲染底部栏而不是粒子,我不明白为什么。我这里有一个缩小的例子:https://jsfiddle.net/f7jv8s6y/7/

const fsSource = document.getElementById("shader-fs").text;

class RenderTarget {
  constructor(gl, width, height) {
    this.texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, this.texture);
    this.level = 0;
    const internalFormat = gl.RGBA;
    const border = 0;
    const format = gl.RGBA;
    const type = gl.UNSIGNED_BYTE;
    const data = new Uint8Array(width * height * 4);

    gl.texImage2D(gl.TEXTURE_2D, this.level, internalFormat, width, height, border, format, type, data);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    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);
    this.fbo = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
  }
}

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const info = String(gl.getShaderInfoLog(shader));
    console.log(info, source);
    gl.deleteShader(shader);
    return info;
  }
  return shader;
}

function initProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  return shaderProgram;
}

function initQuad(gl) {
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  const positions = [-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0];
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
  return positionBuffer;
}


const vs = `#version 300 es
in vec2 aVertexPosition;
out vec2 uv;
void main() {
  uv = aVertexPosition;
  gl_Position =  vec4(aVertexPosition, 0., 1.0);
}`;


const fs = `#version 300 es
precision highp float;
uniform sampler2D iChannel0;
out vec4 fColor;
void main() {
vec2 p = gl_FragCoord.xy / vec2(420., 320.);
  fColor =  texture(iChannel0, p);
}`;

function init() {
  // Set up canvas and webgl2 context
  const width = 420;
  const height = 320;
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const gl = canvas.getContext("webgl2");
  document.body.appendChild(canvas);

  // Compile shaders and set up two render targets
  const prog1 = initProgram(gl, vs, fsSource);
  const prog2 = initProgram(gl, vs, fs);
  const rt1 = new RenderTarget(gl, width, height);
  const rt2 = new RenderTarget(gl, width, height);
  const rts = [rt1, rt2];

  // Bind vertex quad
  gl.useProgram(prog1);
  const quadPos = gl.getAttribLocation(prog1, 'aVertexPosition');
  const quad = initQuad(gl);
  gl.bindBuffer(gl.ARRAY_BUFFER, quad);
  gl.vertexAttribPointer(quadPos, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(quadPos);
  gl.viewport(0, 0, width, height);
  // Set up needed uniforms
  const iTimePos = gl.getUniformLocation(prog1, "iTime");
  const iTimeDeltaPos = gl.getUniformLocation(prog1, "iTimeDelta");
  const iFramePos = gl.getUniformLocation(prog1, "iFrame");
  const iResolutionPos = gl.getUniformLocation(prog1, "iResolution");
  const iChannelResolutionPos = gl.getUniformLocation(prog1, "iChannelResolution");
  const resos = [width, height, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
  gl.uniform2f(iResolutionPos, width, height);
  gl.uniform3fv(iChannelResolutionPos, new Float32Array(resos));
  const prog1Channel0Pos = gl.getUniformLocation(prog1, "iChannel0");
  gl.useProgram(prog2);
  const prog2Channel0Pos = gl.getUniformLocation(prog2, "iChannel0");

  let frame = 0;
  let time = 0;
  let lastTime = 0;

  let setTexture = function(gl, tex, location, spot) {
    gl.activeTexture(gl.TEXTURE0 + spot);
    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.uniform1i(location, spot);
  }

  let animate = function() {
    time = performance.now() / 1000;
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    // FIRST RENDER PASS
    gl.useProgram(prog1);
    gl.uniform1f(iTimePos, time);
    gl.uniform1i(iFramePos, frame);
    gl.uniform1f(iTimeDeltaPos, time - lastTime);
    gl.bindFramebuffer(gl.FRAMEBUFFER, rts[0].fbo);
    gl.bindTexture(gl.TEXTURE_2D, rts[1].texture);
    //setTexture(gl, rts[1].texture, prog1Channel0Pos, 0);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    //SECOND RENDER PASS
    gl.useProgram(prog2);
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, rts[0].texture);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    //setTexture(gl, rts[0].texture, prog2Channel0Pos, 0);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    rts.reverse();
    lastTime = time;
    frame++;

    requestAnimationFrame(animate);
  }
  lastTime = performance.now();
  animate();
}


init();
<script type="x-shader/x-fragment" id="shader-fs" src="util/fs">#version 300 es
#ifdef GL_ES
precision highp float;
precision highp int;
precision mediump sampler3D;
#endif
uniform vec3 iChannelResolution[4];
uniform float iTime;
uniform float iTimeDelta;
uniform float timeDelta;
uniform vec2 iResolution; 
uniform vec4 iMouse;
uniform int iFrame;
in vec2 uv;
out vec4 fColor;

uniform sampler2D iChannel0;
// The MIT License
// Copyright © 2018 Ian Reichert-Watts
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// SHARED PARAMS (Must be same as Image :/)
const int NUM_PARTICLES = 64;
const float INTERACT_DATA_INDEX = float(NUM_PARTICLES)+1.0;
const float KINETIC_MOUSE_INDEX = INTERACT_DATA_INDEX+1.0;

// SHARED FUNCTIONS (Must be same as Image :/)
vec4 loadData( in float index ) 
{ 
    return texture( iChannel0, vec2((index+0.5)/iChannelResolution[0].x,0.0), -100.0 ); 
}

float floorHeight( in vec3 p )
{
    return (sin(p.z*0.00042)*0.2)+(sin(p.z*0.008)*0.64) + (sin(p.x*0.42+sin(p.z*0.000042)*420.0))*0.42-1.0;
}

// PARAMS
const float PARTICLE_LIFETIME_MIN = 0.02;
const float PARTICLE_LIFETIME_MAX = 4.2;
const float FALL_SPEED = 42.0;
const float JITTER_SPEED = 300.0;
const vec3 WIND_DIR = vec3(0.0,0.0,-1.0);
const float WIND_INTENSITY = 4.2;

// CONST
const float PI = 3.14159;
const float TAU = PI * 2.0;

float randFloat( in float n )
{
    return fract( sin( n*64.19 )*420.82 );
}
vec2 randVec2( in vec2 n )
{
    return vec2(randFloat( n.x*12.95+n.y*43.72 ),randFloat( n.x*16.21+n.y*90.23 )); 
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{    
    if ( fragCoord.y > iResolution.y-2.0 )
    {
        // Discard top pixels to avoid persistent data getting included in blur
        discard;
    }
    else if ( fragCoord.y < 2.0 )
    {
        if ( fragCoord.y >= 1.0 || fragCoord.x > float(NUM_PARTICLES+4) )
        {
            discard;
        }
        // Store persistent data in bottom pixel row
        if ( fragCoord.x < float(NUM_PARTICLES) )
        {
            vec4 particle;
            float pidx = floor(fragCoord.x);

            if ( iFrame == 0 )
            {
                float padding = 0.01;
                float particleStep = (1.0-(padding*2.0))/float(NUM_PARTICLES);
                particle = vec4(0.0);
                float r1 = randFloat(pidx);
                particle.xy = vec2(padding+(particleStep*pidx), 1.0+(1.0*r1));
                particle.xy *= iResolution.xy;
                particle.a = r1*(PARTICLE_LIFETIME_MAX-PARTICLE_LIFETIME_MIN);
            }
            else
            {   
                vec4 interactData = loadData(INTERACT_DATA_INDEX);
                
                // Tick particles
          particle = loadData(pidx);
                vec2 puv = particle.xy / iResolution.x;
                vec4 pbuf = texture(iChannel0, puv);
                
                // Camera must be the same as Image :/
                float rotYaw = -(interactData.x/iResolution.x)*TAU;
                float rotPitch = (interactData.y/iResolution.y)*PI;
                vec3 rayOrigin = vec3(0.0, 0.1, iTime*80.0);
                float floorY = floorHeight(rayOrigin);
                rayOrigin.y = floorY*0.9 + 0.2;

                vec3 forward = normalize( vec3(sin(rotYaw), rotPitch, cos(rotYaw)) );
                vec3 wup = normalize(vec3((floorY-floorHeight(rayOrigin+vec3(2.0,0.0,0.0)))*-0.2,1.0,0.0));
                vec3 right = normalize( cross( forward, wup ) );
                vec3 up = normalize( cross( right, forward ) );
                mat3 camMat = mat3(right, up, forward);

                vec3 surfforward = normalize( vec3(sin(rayOrigin.z*0.01)*0.042, ((floorY-floorHeight(rayOrigin+vec3(0.0,0.0,-20.0)))*0.2)+0.12, 1.0) );
                vec3 wright = vec3(1.0,0.0,0.0);
                mat3 surfMat = mat3(wright, up, surfforward); 

                vec2 centeredCoord = puv-vec2(0.5);
                vec3 rayDir = normalize( surfMat*normalize( camMat*normalize( vec3(centeredCoord, 1.0) ) ) );
                vec3 rayRight = normalize( cross( rayDir, up ) );
                vec3 rayUp = normalize( cross( rayRight, rayDir ) );

                // Wind
                vec2 windShield = (puv-vec2(0.5, 0.0))*2.0;
                float speedScale = 0.0015*(0.1+1.9*(sin(PI*0.5*pow( particle.z/particle.a, 2.0 ))))*iResolution.y;
                particle.x += (windShield.x+WIND_INTENSITY*dot(rayRight, WIND_DIR))*FALL_SPEED*speedScale*iTimeDelta;
                particle.y += (windShield.y+WIND_INTENSITY*dot(rayUp, WIND_DIR))*FALL_SPEED*speedScale*iTimeDelta;

                // Jitter
                particle.xy += 0.001*(randVec2( particle.xy+iTime )-vec2(0.5))*iResolution.y*JITTER_SPEED*iTimeDelta;

                // Age
                // Don't age as much when traveling over existing particle trails
                particle.z += (1.0-pbuf.b)*iTimeDelta;

                // Die of old age. Reset
                if ( particle.z > particle.a )
                {
                    float seedX = particle.x*25.36+particle.y*42.92;
                    float seedY = particle.x*16.78+particle.y*93.42;
                    particle = vec4(0.0);
                    particle.x = randFloat( seedX )*iResolution.x;
                    particle.y = randFloat( seedY )*iResolution.y;
                    particle.a = PARTICLE_LIFETIME_MIN+randFloat(pidx)*(PARTICLE_LIFETIME_MAX-PARTICLE_LIFETIME_MIN);
                }
            }
            fragColor = particle;
        }
  else
        {
            float dataIndex = floor(fragCoord.x);
            vec4 interactData = loadData(INTERACT_DATA_INDEX);
            vec4 kineticMouse = loadData(KINETIC_MOUSE_INDEX);
            
            if ( iMouse.z > 0.0 )
            {
             vec2 mouseDelta = iMouse.xy-kineticMouse.xy;
                if ( length(iMouse.xy-iMouse.zw) < 4.0 )
                {
                    mouseDelta = vec2(0.0);
                }
                interactData.xy += mouseDelta;
                interactData.y = clamp( interactData.y, -iResolution.y, iResolution.y );
                kineticMouse = vec4(iMouse.xy, mouseDelta);
            }
            else
            {
                kineticMouse.zw *= 0.9;
                interactData.xy += kineticMouse.zw;
                interactData.y = clamp( interactData.y, -iResolution.y, iResolution.y );
                kineticMouse.xy = iMouse.xy;
            }
            fragColor = (dataIndex == KINETIC_MOUSE_INDEX) ? kineticMouse : interactData;
        }
    }
    else
    {
        // Draw Particles
        vec2 blurUV = fract( (fragCoord.xy + (fract( float(iFrame)*0.5 )*2.0-0.5)) / iResolution.xy );
        vec2 uv = fragCoord.xy / iResolution.xy;
        fragColor = texture( iChannel0, uv );
        vec4 prevColor = fragColor;

        if ( fragColor.a < 1.0 )
        {
            fragColor = texture( iChannel0, blurUV );
        }
        fragColor.b *= 0.996;

        for ( int i=0; i<NUM_PARTICLES; i++ )
        {
      vec4 particle = loadData(float(i));
            vec2 delta = fragCoord.xy-particle.xy;
            float dist = length(delta);
            float radius = 0.002*(0.5+2.0*particle.a+abs(sin(1.0*iTime+float(i))))*iResolution.y;
            radius += 4.0*randFloat( particle.x*35.26+particle.y*93.12 )*pow((particle.z/particle.a), 12.0);
            if ( dist < radius )
            {
                // normal
                vec2 dir = delta/dist;
                fragColor.r = dot(dir, vec2(1.0,0.0))*0.5+0.5;
                fragColor.g = dot(dir, vec2(0.0,1.0))*0.5+0.5;
                // height
                float height = sin( dist/radius*PI*0.5 );
                height = pow( height, 8.0 );
                height = 1.0-height;
                fragColor.b = max( height, prevColor.b );
                // age
                fragColor.a = 0.0;
            }
        }
        fragColor.a += 0.1*iTimeDelta;
    }
}void main() {
  vec4 color = vec4(0.0,0.0,0.0,1.0);
  mainImage(color, gl_FragCoord.xy);
  color.w = 1.0;
  fColor = color;
}
</script>

是否有人指出出了什么问题?

问题是您需要使用 FLOATHALF_FLOAT 纹理与您的帧缓冲区。

请注意,能够呈现 FLOATHALF_FLOAT 是 WebGL2 的可选功能,因此您必须检查并启用 EXT_color_buffer_floatEXT_color_buffer_half_float

过滤FLOATHALF_FLOAT 纹理也是一项可选功能。我不知道 Shadertoy 使用 NEAREST 还是 LINEAR。我在下面将内容设置为 NEAREST。如果您想使用 LINEAR,您还需要检查并启用 OES_texture_float_linearOES_texture_half_float_linear

如果你好奇我是怎么想出来的...

首先,我通过添加

验证了第二遍是否有效
 fColor = vec4(p, 0, 1);

到第二遍着色器的底部。

这很有效,所以接下来我在第一遍着色器中尝试了类似的东西添加

 fragColor = vec4(fragCoord / iResolution, 0, 1);
 return;

这很有效,所以接下来我查看了着色器。我看到它被分成 3~ish 部分。一部分跳过了第一行,所以我在其中放了一个 fragColor = vec4(1,0,0,1) 作为完整性检查,以验证它是否按照我的预期进行

然后下一部分进行粒子模拟,我跳过了这部分,只是足以理解它只是使用底行进行存储。

最后一部分进行了模糊处理,所以我再次放入 fragColor = vec4(0,1,0,1) 只是为了验证该区域变绿了。确实如此。

所以,鉴于它应该模糊我添加了一些代码来使用

手动设置纹理
gl.bindFramebuffer(gl.FRAMEBUFFER, rts[1].fbo);
gl.enable(gl.SCISSOR_TEST);
gl.scissor(100, 100, 10, 10);
gl.clearColor(.2, .5, .7, .9);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.disable(gl.SCISSOR_TEST);

我在渲染循环中尝试过,然后仅在初始化时尝试过。当我看到小矩形完全没有受到影响时,它想到了可能是什么问题,那时我认为问题可能出在 8 位渲染目标上。我检查了 shadertoy 网站上的文档,但文档什么也没说(shadertoy.com 上的文档非常糟糕)。 This source is linked from shadertoy.com. It's for an iOS app, not the website, and so I searched for glTexImage2D 看看他们是否全部使用浮点纹理。答案是 'yes' 所以我试了一下,至少得到了一些似乎可以解决问题的结果。

关于代码的几点建议

  • 如果您没有在纹理中放置任何数据,则没有理由将数组传递给 gl.texImage2D。只需传递 null.

  • 没有理由将类型化数组与 gl.uniform 一起使用,因此此代码

    const resos = [width, height, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    gl.uniform3fv(iChannelResolutionPos, new Float32Array(resos));
    

    可能只是

    const resos = [width, height, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    gl.uniform3fv(iChannelResolutionPos, resos);
    
  • gl.getShaderInfoLog已经returns一个字符串所以

    const info = String(gl.getShaderInfoLog(shader));
    

    可能只是

    const info = gl.getShaderInfoLog(shader);
    
  • initProgram 代码未检查 link 错误。

    Link 错误可能出于任何原因发生。最常见的是 mis-matching varyings 但规范实际上说只要 linking 失败如果着色器编译失败,着色器编译总是可以成功。

    gl.linkProgram(prg);
    const success = gl.getProgramParameter(gl.LINK_STATUS);
    if (!success) {
      console.log(gl.getProgramInfoLog(prg);
    }
    

const fsSource = document.getElementById("shader-fs").text;

class RenderTarget {
  constructor(gl, width, height) {
    this.texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, this.texture);
    this.level = 0;
    const internalFormat = gl.RGBA32F;
    const border = 0;
    const format = gl.RGBA;
    const type = gl.FLOAT;
    const data = null;

    gl.texImage2D(gl.TEXTURE_2D, this.level, internalFormat, width, height, border, format, type, data);
    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);
    this.fbo = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.texture, 0);
  }
}

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const info = String(gl.getShaderInfoLog(shader));
    console.log(info, source);
    gl.deleteShader(shader);
    return info;
  }
  return shader;
}

function initProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  return shaderProgram;
}

function initQuad(gl) {
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  const positions = [-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0];
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
  return positionBuffer;
}


const vs = `#version 300 es
in vec2 aVertexPosition;
out vec2 uv;
void main() {
  uv = aVertexPosition;
  gl_Position =  vec4(aVertexPosition, 0., 1.0);
}`;


const fs = `#version 300 es
precision highp float;
uniform sampler2D iChannel0;
out vec4 fColor;
void main() {
vec2 p = gl_FragCoord.xy / vec2(420., 320.);
  fColor =  texture(iChannel0, p);
}`;

function init() {
  // Set up canvas and webgl2 context
  const width = 420;
  const height = 320;
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const gl = canvas.getContext("webgl2");
  document.body.appendChild(canvas);
  const ext = gl.getExtension('EXT_color_buffer_float');
  if (!ext) {
    return alert('need EXT_color_buffer_float');
  }

  // Compile shaders and set up two render targets
  const prog1 = initProgram(gl, vs, fsSource);
  const prog2 = initProgram(gl, vs, fs);
  const rt1 = new RenderTarget(gl, width, height);
  const rt2 = new RenderTarget(gl, width, height);
  const rts = [rt1, rt2];

  // Bind vertex quad
  gl.useProgram(prog1);
  const quadPos = gl.getAttribLocation(prog1, 'aVertexPosition');
  const quad = initQuad(gl);
  gl.bindBuffer(gl.ARRAY_BUFFER, quad);
  gl.vertexAttribPointer(quadPos, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(quadPos);
  gl.viewport(0, 0, width, height);
  // Set up needed uniforms
  const iTimePos = gl.getUniformLocation(prog1, "iTime");
  const iTimeDeltaPos = gl.getUniformLocation(prog1, "iTimeDelta");
  const iFramePos = gl.getUniformLocation(prog1, "iFrame");
  const iResolutionPos = gl.getUniformLocation(prog1, "iResolution");
  const iChannelResolutionPos = gl.getUniformLocation(prog1, "iChannelResolution");
  const resos = [width, height, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
  gl.uniform2f(iResolutionPos, width, height);
  gl.uniform3fv(iChannelResolutionPos, new Float32Array(resos));
  const prog1Channel0Pos = gl.getUniformLocation(prog1, "iChannel0");
  gl.useProgram(prog2);
  const prog2Channel0Pos = gl.getUniformLocation(prog2, "iChannel0");

  let frame = 0;
  let time = 0;
  let lastTime = 0;

  let setTexture = function(gl, tex, location, spot) {
    gl.activeTexture(gl.TEXTURE0 + spot);
    gl.bindTexture(gl.TEXTURE_2D, tex);
    gl.uniform1i(location, spot);
  }

  let animate = function() {
    time = performance.now() / 1000;
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    // FIRST RENDER PASS
    gl.useProgram(prog1);
    gl.uniform1f(iTimePos, time);
    gl.uniform1i(iFramePos, frame);
    gl.uniform1f(iTimeDeltaPos, time - lastTime);
    gl.bindFramebuffer(gl.FRAMEBUFFER, rts[0].fbo);
    gl.bindTexture(gl.TEXTURE_2D, rts[1].texture);
    //setTexture(gl, rts[1].texture, prog1Channel0Pos, 0);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    //SECOND RENDER PASS
    gl.useProgram(prog2);
    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    gl.bindTexture(gl.TEXTURE_2D, rts[0].texture);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    //setTexture(gl, rts[0].texture, prog2Channel0Pos, 0);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    rts.reverse();
    lastTime = time;
    frame++;

    requestAnimationFrame(animate);
  }
  lastTime = performance.now();
  animate();
}


init();
<script type="x-shader/x-fragment" id="shader-fs" src="util/fs">#version 300 es
#ifdef GL_ES
precision highp float;
precision highp int;
precision mediump sampler3D;
#endif
uniform vec3 iChannelResolution[4];
uniform float iTime;
uniform float iTimeDelta;
uniform float timeDelta;
uniform vec2 iResolution; 
uniform vec4 iMouse;
uniform int iFrame;
in vec2 uv;
out vec4 fColor;

uniform sampler2D iChannel0;
// The MIT License
// Copyright © 2018 Ian Reichert-Watts
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// SHARED PARAMS (Must be same as Image :/)
const int NUM_PARTICLES = 64;
const float INTERACT_DATA_INDEX = float(NUM_PARTICLES)+1.0;
const float KINETIC_MOUSE_INDEX = INTERACT_DATA_INDEX+1.0;

// SHARED FUNCTIONS (Must be same as Image :/)
vec4 loadData( in float index ) 
{ 
    return texture( iChannel0, vec2((index+0.5)/iChannelResolution[0].x,0.0), -100.0 ); 
}

float floorHeight( in vec3 p )
{
    return (sin(p.z*0.00042)*0.2)+(sin(p.z*0.008)*0.64) + (sin(p.x*0.42+sin(p.z*0.000042)*420.0))*0.42-1.0;
}

// PARAMS
const float PARTICLE_LIFETIME_MIN = 0.02;
const float PARTICLE_LIFETIME_MAX = 4.2;
const float FALL_SPEED = 42.0;
const float JITTER_SPEED = 300.0;
const vec3 WIND_DIR = vec3(0.0,0.0,-1.0);
const float WIND_INTENSITY = 4.2;

// CONST
const float PI = 3.14159;
const float TAU = PI * 2.0;

float randFloat( in float n )
{
    return fract( sin( n*64.19 )*420.82 );
}
vec2 randVec2( in vec2 n )
{
    return vec2(randFloat( n.x*12.95+n.y*43.72 ),randFloat( n.x*16.21+n.y*90.23 )); 
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{    
    if ( fragCoord.y > iResolution.y-2.0 )
    {
        // Discard top pixels to avoid persistent data getting included in blur
        discard;
    }
    else if ( fragCoord.y < 2.0 )
    {
        if ( fragCoord.y >= 1.0 || fragCoord.x > float(NUM_PARTICLES+4) )
        {
            discard;
        }
        // Store persistent data in bottom pixel row
        if ( fragCoord.x < float(NUM_PARTICLES) )
        {
            vec4 particle;
            float pidx = floor(fragCoord.x);

            if ( iFrame == 0 )
            {
                float padding = 0.01;
                float particleStep = (1.0-(padding*2.0))/float(NUM_PARTICLES);
                particle = vec4(0.0);
                float r1 = randFloat(pidx);
                particle.xy = vec2(padding+(particleStep*pidx), 1.0+(1.0*r1));
                particle.xy *= iResolution.xy;
                particle.a = r1*(PARTICLE_LIFETIME_MAX-PARTICLE_LIFETIME_MIN);
            }
            else
            {   
                vec4 interactData = loadData(INTERACT_DATA_INDEX);
                
                // Tick particles
          particle = loadData(pidx);
                vec2 puv = particle.xy / iResolution.x;
                vec4 pbuf = texture(iChannel0, puv);
                
                // Camera must be the same as Image :/
                float rotYaw = -(interactData.x/iResolution.x)*TAU;
                float rotPitch = (interactData.y/iResolution.y)*PI;
                vec3 rayOrigin = vec3(0.0, 0.1, iTime*80.0);
                float floorY = floorHeight(rayOrigin);
                rayOrigin.y = floorY*0.9 + 0.2;

                vec3 forward = normalize( vec3(sin(rotYaw), rotPitch, cos(rotYaw)) );
                vec3 wup = normalize(vec3((floorY-floorHeight(rayOrigin+vec3(2.0,0.0,0.0)))*-0.2,1.0,0.0));
                vec3 right = normalize( cross( forward, wup ) );
                vec3 up = normalize( cross( right, forward ) );
                mat3 camMat = mat3(right, up, forward);

                vec3 surfforward = normalize( vec3(sin(rayOrigin.z*0.01)*0.042, ((floorY-floorHeight(rayOrigin+vec3(0.0,0.0,-20.0)))*0.2)+0.12, 1.0) );
                vec3 wright = vec3(1.0,0.0,0.0);
                mat3 surfMat = mat3(wright, up, surfforward); 

                vec2 centeredCoord = puv-vec2(0.5);
                vec3 rayDir = normalize( surfMat*normalize( camMat*normalize( vec3(centeredCoord, 1.0) ) ) );
                vec3 rayRight = normalize( cross( rayDir, up ) );
                vec3 rayUp = normalize( cross( rayRight, rayDir ) );

                // Wind
                vec2 windShield = (puv-vec2(0.5, 0.0))*2.0;
                float speedScale = 0.0015*(0.1+1.9*(sin(PI*0.5*pow( particle.z/particle.a, 2.0 ))))*iResolution.y;
                particle.x += (windShield.x+WIND_INTENSITY*dot(rayRight, WIND_DIR))*FALL_SPEED*speedScale*iTimeDelta;
                particle.y += (windShield.y+WIND_INTENSITY*dot(rayUp, WIND_DIR))*FALL_SPEED*speedScale*iTimeDelta;

                // Jitter
                particle.xy += 0.001*(randVec2( particle.xy+iTime )-vec2(0.5))*iResolution.y*JITTER_SPEED*iTimeDelta;

                // Age
                // Don't age as much when traveling over existing particle trails
                particle.z += (1.0-pbuf.b)*iTimeDelta;

                // Die of old age. Reset
                if ( particle.z > particle.a )
                {
                    float seedX = particle.x*25.36+particle.y*42.92;
                    float seedY = particle.x*16.78+particle.y*93.42;
                    particle = vec4(0.0);
                    particle.x = randFloat( seedX )*iResolution.x;
                    particle.y = randFloat( seedY )*iResolution.y;
                    particle.a = PARTICLE_LIFETIME_MIN+randFloat(pidx)*(PARTICLE_LIFETIME_MAX-PARTICLE_LIFETIME_MIN);
                }
            }
            fragColor = particle;
        }
  else
        {
            float dataIndex = floor(fragCoord.x);
            vec4 interactData = loadData(INTERACT_DATA_INDEX);
            vec4 kineticMouse = loadData(KINETIC_MOUSE_INDEX);
            
            if ( iMouse.z > 0.0 )
            {
             vec2 mouseDelta = iMouse.xy-kineticMouse.xy;
                if ( length(iMouse.xy-iMouse.zw) < 4.0 )
                {
                    mouseDelta = vec2(0.0);
                }
                interactData.xy += mouseDelta;
                interactData.y = clamp( interactData.y, -iResolution.y, iResolution.y );
                kineticMouse = vec4(iMouse.xy, mouseDelta);
            }
            else
            {
                kineticMouse.zw *= 0.9;
                interactData.xy += kineticMouse.zw;
                interactData.y = clamp( interactData.y, -iResolution.y, iResolution.y );
                kineticMouse.xy = iMouse.xy;
            }
            fragColor = (dataIndex == KINETIC_MOUSE_INDEX) ? kineticMouse : interactData;
        }
    }
    else
    {
        // Draw Particles
        vec2 blurUV = fract( (fragCoord.xy + (fract( float(iFrame)*0.5 )*2.0-0.5)) / iResolution.xy );
        vec2 uv = fragCoord.xy / iResolution.xy;
        fragColor = texture( iChannel0, uv );
        vec4 prevColor = fragColor;

        if ( fragColor.a < 1.0 )
        {
            fragColor = texture( iChannel0, blurUV );
        }
        fragColor.b *= 0.996;

        for ( int i=0; i<NUM_PARTICLES; i++ )
        {
      vec4 particle = loadData(float(i));
            vec2 delta = fragCoord.xy-particle.xy;
            float dist = length(delta);
            float radius = 0.002*(0.5+2.0*particle.a+abs(sin(1.0*iTime+float(i))))*iResolution.y;
            radius += 4.0*randFloat( particle.x*35.26+particle.y*93.12 )*pow((particle.z/particle.a), 12.0);
            if ( dist < radius )
            {
                // normal
                vec2 dir = delta/dist;
                fragColor.r = dot(dir, vec2(1.0,0.0))*0.5+0.5;
                fragColor.g = dot(dir, vec2(0.0,1.0))*0.5+0.5;
                // height
                float height = sin( dist/radius*PI*0.5 );
                height = pow( height, 8.0 );
                height = 1.0-height;
                fragColor.b = max( height, prevColor.b );
                // age
                fragColor.a = 0.0;
            }
        }
        fragColor.a += 0.1*iTimeDelta;
    }
}void main() {
  vec4 color = vec4(0.0,0.0,0.0,1.0);
  mainImage(color, gl_FragCoord.xy);
  color.w = 1.0;
  fColor = color;
}
</script>