• 风场可视化:绘制轨迹


    目录

    引子

    了解绘制粒子之后,接着去看如何绘制粒子轨迹。

    绘制轨迹

    在原文中提到绘制轨迹的方法是将粒子绘制到纹理中,然后在下一帧上使用该纹理作为背景(稍微变暗),并每一帧交换输入/目标纹理。这里涉及两个重点使用的 WebGL 功能点:

    基于绘制粒子的基础上,增加逻辑的主要思路:

    • 初始化时,增加了背景纹理 B 和屏幕纹理 S 。
    • 创建每个粒子相关信息的数据时,存了两个纹理 T20 和 T21 中。
    • 绘制时,先绘制背景纹理 B ,再根据纹理 T20 绘制所有粒子,接着绘制屏幕纹理 S,之后将屏幕纹理 S 作为下一帧的背景纹理 B 。
    • 最后基于纹理 T21 绘制新的结果,生成新的状态纹理覆盖 T20 ,开始下一帧绘制。

    不包含随机生成的粒子轨迹效果见示例,下面看看具体的实现。

    纹理

    新增纹理相关逻辑:

    // 代码省略
    resize() {
      const gl = this.gl;
      const emptyPixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
      // screen textures to hold the drawn screen for the previous and the current frame
      this.backgroundTexture = util.createTexture(gl, gl.NEAREST, emptyPixels, gl.canvas.width, gl.canvas.height);
      this.screenTexture = util.createTexture(gl, gl.NEAREST, emptyPixels, gl.canvas.width, gl.canvas.height);
    }
    // 代码省略
    

    初始化的背景纹理和屏幕纹理都是以 Canvas 的宽高作为标准,同样是以每个像素 4 个分量存储。

    屏幕着色器程序

    新增屏幕着色器程序对象,最终显示可见的内容就是这个对象负责绘制:

    this.screenProgram = webglUtil.createProgram(gl, quadVert, screenFrag);
    

    顶点数据

    顶点相关逻辑:

    // 代码省略
      this.quadBuffer = util.createBuffer(gl, new Float32Array([0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1]));
    // 代码省略
      util.bindAttribute(gl, this.quadBuffer, program.a_pos, 2);
    // 代码省略
      gl.drawArrays(gl.TRIANGLES, 0, 6);
    // 代码省略
    

    这里可以看出以顶点数据按照二维解析,总共 6 个点,绘制的是一个矩形,为什坐标都是 0 和 1 ,接着看下面的着色器。

    顶点着色器

    新增顶点着色器和对应绑定的变量:

    const quadVert = `
      precision mediump float;
    
      attribute vec2 a_pos;
    
      varying vec2 v_tex_pos;
    
      void main() {
          v_tex_pos = a_pos;
          gl_Position = vec4(1.0 - 2.0 * a_pos, 0, 1);
      }
    `;
    // 代码省略
    this.drawTexture(this.backgroundTexture, this.fadeOpacity);
    // 代码省略
    drawTexture(texture, opacity) {
      // 代码省略
      util.bindAttribute(gl, this.quadBuffer, program.a_pos, 2);
      // 代码省略
      gl.drawArrays(gl.TRIANGLES, 0, 6);
    }
    // 代码省略
    

    从这些分散的逻辑中,找到着色器中的变量对应的实际值:

    • a_posquadBuffer 中每个顶点二维数据。
    • v_tex_pos : 跟 a_pos 的值一样,会在对应的片元着色器中使用。

    这里 gl_Position 的计算方式,结合前面说到的顶点坐标都是 0 和 1 ,发现计算结果的范围是 [-1.0, +1.0] ,在裁减空间范围内,就可以显示出来。

    片元着色器

    片元着色器和对应绑定的变量:

    const screenFrag = `
      precision mediump float;
    
      uniform sampler2D u_screen;
      uniform float u_opacity;
    
      varying vec2 v_tex_pos;
    
      void main() {
          vec4 color = texture2D(u_screen, 1.0 - v_tex_pos);
          // a hack to guarantee opacity fade out even with a value close to 1.0
          gl_FragColor = vec4(floor(255.0 * color * u_opacity) / 255.0);
      }
    `;
    this.fadeOpacity = 0.996;
    // 代码省略
    drawTexture(texture, opacity) {
      // 代码省略
      gl.uniform1i(program.u_screen, 2);
      gl.uniform1f(program.u_opacity, opacity);
    
      gl.drawArrays(gl.TRIANGLES, 0, 6);
    }
    

    从这些分散的逻辑中,找到着色器中的变量对应的实际值:

    • u_screen : 动态变化的纹理,需根据上下文判断 。
    • u_opacity : 透明度,需根据上下文判断。
    • v_tex_pos : 从顶点着色器传递过来,也就是 quadBuffer 中的数据。

    1.0 - v_tex_pos 的范围是 [0, 1] ,正好包含了整个纹理的范围。最终颜色乘以动态 u_opacity 的效果就是原文中所说“稍微变暗”的目的。

    更新着色器程序

    新增更新着色器程序对象,是让粒子产生移动轨迹的关键:

    this.updateProgram = webglUtil.createProgram(gl, quadVert, updateFrag);
    

    顶点数据

    与屏幕着色器程序的顶点数据公用一套。

    顶点着色器

    与屏幕着色器程序的顶点着色器公用一套。

    片元着色器

    针对更新的片元着色器和对应绑定的变量:

    const updateFrag = `
      precision highp float;
    
      uniform sampler2D u_particles;
      uniform sampler2D u_wind;
      uniform vec2 u_wind_res;
      uniform vec2 u_wind_min;
      uniform vec2 u_wind_max;
    
      varying vec2 v_tex_pos;
    
      // wind speed lookup; use manual bilinear filtering based on 4 adjacent pixels for smooth interpolation
      vec2 lookup_wind(const vec2 uv) {
          // return texture2D(u_wind, uv).rg; // lower-res hardware filtering
          vec2 px = 1.0 / u_wind_res;
          vec2 vc = (floor(uv * u_wind_res)) * px;
          vec2 f = fract(uv * u_wind_res);
          vec2 tl = texture2D(u_wind, vc).rg;
          vec2 tr = texture2D(u_wind, vc + vec2(px.x, 0)).rg;
          vec2 bl = texture2D(u_wind, vc + vec2(0, px.y)).rg;
          vec2 br = texture2D(u_wind, vc + px).rg;
          return mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y);
      }
    
      void main() {
          vec4 color = texture2D(u_particles, v_tex_pos);
          vec2 pos = vec2(
              color.r / 255.0 + color.b,
              color.g / 255.0 + color.a); // decode particle position from pixel RGBA
    
          vec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(pos));
    
          // take EPSG:4236 distortion into account for calculating where the particle moved
          float distortion = cos(radians(pos.y * 180.0 - 90.0));
          vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * 0.25;
    
          // update particle position, wrapping around the date line
          pos = fract(1.0 + pos + offset);
    
          // encode the new particle position back into RGBA
          gl_FragColor = vec4(
              fract(pos * 255.0),
              floor(pos * 255.0) / 255.0);
      }
    `;
    // 代码省略
    setWind(windData) {
      // 风场图片的源数据
      this.windData = windData;
    }
    // 代码省略
    util.bindTexture(gl, this.windTexture, 0);
    util.bindTexture(gl, this.particleStateTexture0, 1);
    // 代码省略
    this.updateParticles();
    // 代码省略
    updateParticles() {
      // 代码省略
      const program = this.updateProgram;
      gl.useProgram(program.program);
    
      util.bindAttribute(gl, this.quadBuffer, program.a_pos, 2);
    
      gl.uniform1i(program.u_wind, 0); // 风纹理
      gl.uniform1i(program.u_particles, 1); // 粒子纹理
    
      gl.uniform2f(program.u_wind_res, this.windData.width, this.windData.height);
      gl.uniform2f(program.u_wind_min, this.windData.uMin, this.windData.vMin);
      gl.uniform2f(program.u_wind_max, this.windData.uMax, this.windData.vMax);
    
      gl.drawArrays(gl.TRIANGLES, 0, 6);
      // 代码省略
    }
    

    从这些分散的逻辑中,找到着色器中的变量对应的实际值:

    • u_wind :风场图片生成的纹理 windTexture
    • u_particles :所有粒子颜色信息的纹理 particleStateTexture0
    • u_wind_res : 生成图片的宽高。
    • u_wind_min : 风场数据分量最小值。
    • u_wind_max : 风场数据分量最大值。

    根据 quadBuffer 的顶点数据从纹理 particleStateTexture0 中获取对应位置的像素信息,用像素信息解码出粒子位置,通过 lookup_wind 方法获取相邻 4 个像素的平滑插值,之后基于风场最大值和最小值得出偏移量 offset ,最后得到新的位置转为颜色输出。在这个过程中发现下面几个重点:

    • 怎么获取相邻 4 个像素?
    • 二维地图中,两极和赤道粒子如何区别?

    怎么获取相邻 4 个像素?

    看主要方法:

    vec2 lookup_wind(const vec2 uv) {
      vec2 px = 1.0 / u_wind_res;
      vec2 vc = (floor(uv * u_wind_res)) * px;
      vec2 f = fract(uv * u_wind_res);
      vec2 tl = texture2D(u_wind, vc).rg;
      vec2 tr = texture2D(u_wind, vc + vec2(px.x, 0)).rg;
      vec2 bl = texture2D(u_wind, vc + vec2(0, px.y)).rg;
      vec2 br = texture2D(u_wind, vc + px).rg;
      return mix(mix(tl, tr, f.x), mix(bl, br, f.x), f.y);
    }
    
    • 以生成图片的宽高作为基准,得到基本单位 px
    • 在新衡量标准下,向下取整得到近似位置 vc 作为第 1 个参考点,移动基本单位单个分量 px.x 得到第 2 个参考点;
    • 移动基本单位单个分量 px.y 得到第 3 个参考点,移动基本单位 px 得到第 4 个参考点。

    二维地图中,两极和赤道粒子如何区别?

    就像原文中:

    在两极附近,粒子沿 X 轴的移动速度应该比赤道上的粒子快得多,因为相同的经度表示的距离要小得多。

    对应的处理逻辑:

    float distortion = cos(radians(pos.y * 180.0 - 90.0));
    vec2 offset = vec2(velocity.x / distortion, -velocity.y) * 0.0001 * u_speed_factor;
    

    radians 方法将角度转换为弧度值,pos.y * 180.0 - 90.0 猜测是风数据转为角度的规则。cos 余弦值在 [0,π] 之间逐渐变小,对应 offset 的第一个分量就会逐渐变大,效果看起来速度变快了。第二个分量加上了符号 -,推测是要跟图片纹理一致,图片纹理默认在 Y 轴上是反的。

    绘制

    绘制这块变化很大:

      draw() {
        // 代码省略
        this.drawScreen();
        this.updateParticles();
      }
      drawScreen() {
        const gl = this.gl;
        // draw the screen into a temporary framebuffer to retain it as the background on the next frame
        util.bindFramebuffer(gl, this.framebuffer, this.screenTexture);
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    
        this.drawTexture(this.backgroundTexture, this.fadeOpacity);
        this.drawParticles();
    
        util.bindFramebuffer(gl, null);
        // enable blending to support drawing on top of an existing background (e.g. a map)
        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        this.drawTexture(this.screenTexture, 1.0);
        gl.disable(gl.BLEND);
    
        // save the current screen as the background for the next frame
        const temp = this.backgroundTexture;
        this.backgroundTexture = this.screenTexture;
        this.screenTexture = temp;
      }
      drawTexture(texture, opacity) {
        const gl = this.gl;
        const program = this.screenProgram;
        gl.useProgram(program.program);
        // 代码省略
        gl.drawArrays(gl.TRIANGLES, 0, 6);
      }
      drawParticles() {
        const gl = this.gl;
        const program = this.drawProgram;
        gl.useProgram(program.program);
        // 代码省略
        gl.drawArrays(gl.POINTS, 0, this._numParticles);
      }
      updateParticles() {
        const gl = this.gl;
        util.bindFramebuffer(gl, this.framebuffer, this.particleStateTexture1);
        gl.viewport(
          0,
          0,
          this.particleStateResolution,
          this.particleStateResolution
        );
    
        const program = this.updateProgram;
        gl.useProgram(program.program);
        // 代码省略
        gl.drawArrays(gl.TRIANGLES, 0, 6);
    
        // swap the particle state textures so the new one becomes the current one
        const temp = this.particleStateTexture0;
        this.particleStateTexture0 = this.particleStateTexture1;
        this.particleStateTexture1 = temp;
      }
    
    • 先切换到帧缓冲区,指定的纹理是 screenTexture ,注意从这里开始绘制的结果是不可见的,接着绘制了整个背景纹理 backgroundTexture 和基于纹理 particleStateTexture0 的所有单个粒子,然后解除帧缓冲区绑定。这部分绘制结果会存储在纹理 screenTexture 中。
    • 切换到默认的颜色缓冲区,注意从这里开始绘制的结果可见,开启 α 混合,blendFunc 设置的两个参数效果是重叠的部分后绘制会覆盖先绘制。然后绘制了整个纹理 screenTexture ,也就是说帧缓冲区的绘制结果都显示到了画布上。
    • 绘制完成后,使用了中间变量进行替换,纹理 backgroundTexture 变成了现在呈现的纹理内容,作为下一帧的背景。
    • 接着切换到帧缓冲区更新粒子状态,指定的纹理是 particleStateTexture1,注意从这里开始绘制的结果是不可见的,基于纹理 particleStateTexture0 绘制产生偏移后的状态,整个绘制结果会储存在纹理 particleStateTexture1 中。
    • 绘制完成后,使用了中间变量进行替换,纹理 particleStateTexture0 变成了移动后的纹理内容,作为下一帧粒子呈现的依据。这样连续的帧绘制,看起来就是动态的效果。

    疑惑

    感觉好像是那么回事,但有的还是不太明白。

    偏移为什么要用 lookup_wind 里面的计算方式 ?

    原文解释说找平滑插值,但这里面的数学原理是什么?找到之后为什么又要 mix 一次?个人也没找到比较好的解释。

    参考资料

  • 相关阅读:
    [译]CasperJS,基于PhantomJS的工具包
    [译]JavaScript:typeof的用途
    [译]JavaScript写的一个quine程序
    [译]Ruby中的解构赋值
    [译]DOM:元素ID就是全局变量
    [译]ECMAScript 6中的集合类型,第一部分:Set
    [译]JavaScript:Array.prototype和[]的性能差异
    [译]Web Inspector开始支持CSS区域
    [译]JavaScript:反科里化"this"
    [译]JavaScript:用什么来缩进
  • 原文地址:https://www.cnblogs.com/thyshare/p/15996336.html
Copyright © 2020-2023  润新知