关于javascript:风场可视化绘制轨迹

5次阅读

共计 7669 个字符,预计需要花费 20 分钟才能阅读完成。

引子

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

  • 源库:webgl-wind
  • Origin
  • My GitHub

绘制轨迹

在原文中提到绘制轨迹的办法是将粒子绘制到纹理中,而后在下一帧上应用该纹理作为背景(略微变暗),并每一帧替换输出 / 指标纹理。这里波及两个重点应用的 WebGL 性能点:

  • JavaScript WebGL 图片通明解决
  • JavaScript 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 一次?集体也没找到比拟好的解释。

参考资料

  • How I built a wind map with WebGL
正文完
 0