How I built a wind map with WebGL 源码理解(没完)

前言: 感谢XXHolic 大佬的翻译以及从大体上的解读内容,接着大佬的内容补充部分不够详细的点?(大概是对纹理一无所知的人,这篇文章看到你会失声尖叫然后迷恋上我)适合至少有webgl部分基础人群观看。

参考与感谢

译者XXHolic
How I built a wind map with WebGL译文
【译】A GPU Approach to Particle Physics

作者Clawko图形学底层探秘 - 纹理采样、环绕、过滤与Mipmap的那些事

绘制风场数据与为何使用图片存储的道理

在这里插入图片描述
根据原文中的部分,水平 即 u ,垂直为v ,对应 rgba 颜色里的 red 与 green 的值, 如何求这个值 以及渲染这个图像?

  • 别问我风场数据长什么样

/**
 *  根据 windata  绘制 风场图
 * @param {*} data 
 * @returns 
 */
function generateWindMap(data) {
    let canvas = document.createElement('canvas')

    //  init 
    let [u_obj, v_obj] = data
    let [u_header, v_header] = [u_obj.header, v_obj.header]
    let [u_data, v_data] = [u_obj.data, v_obj.data]
    let { nx, ny } = u_header
    let [u_min, u_max, v_min, v_max] = [u_header.minData, u_header.maxData, v_header.minData, v_header.maxData]

    let ctx = canvas.getContext('2d')
    canvas.width = nx
    canvas.height = ny
    let imgData = new ImageData(nx, ny)

    // 映射  return array from [u,v]
    function reflect(datau, datav) {
        // 0 - 255 min - max
        let u = (datau - u_min) / (u_max - u_min) * 255
        let v = (datav - v_min) / (v_max - v_min) * 255
        return [u, v]
    }

    for (let i = 0; i < ny; i++) {
        for (let j = 0; j < nx; j++) {
            let dataIndex = i * nx + j;
            let res = reflect(u_data[dataIndex], v_data[dataIndex])
            imgData.data[4 * dataIndex + 0] = res[0];
            imgData.data[4 * dataIndex + 1] = res[1];
            imgData.data[4 * dataIndex + 2] = 0;
            imgData.data[4 * dataIndex + 3] = 255;
        }
    }

    ctx.putImageData(imgData, 0, 0);
    console.log(canvas.toDataURL());
    return canvas.toDataURL();
}
  • 具体原理
    在这里插入图片描述
  • 为何将 u v 值 转换成rgb值 最后保存成一张图片?
    最简单、快速的理解:存7,8M的数据还是存一个80K的图片好?这张图片上面根据这个映射的规律或者说是解码 我们就可以通过纹理采样读取到原来的数据。

特别注意:

  • 通过这种方式处理的数据,再取的时候是取不回初始的数据的。这点主要是有天跟公瑾大佬聊的时候确认的东西,本来我认为在imageData里可以接收浮点数,是不是意味着数据其实没有任何改变。显然不是,后来我测试了一下,确认rgb值的渲染是整数类型的,它只存在于0 - 255的整形。即使我的代码里往imageData设置的是浮点数,但在进入到canvas的处理时,仍然会被Math.round()处理成整数,望周知。这导致我们处理出的数据只要是映射到0 - 255之间的浮点数,都会有一定的精度丢失。
  • 因此,这种处理方式会导致数据并不是原始的数据,而是一个随着最大最小值的差值变化的值:如果最大最小差值越大,这个数据偏移的值会越大。而可以使用这种数据处理方法的最主要原因是: 风场数据是线性的,而且在这种风场的表现形式下,数据不完全准确也属于可接受的范围。

处理粒子数量的内容

由于在一开始粒子显现的第一帧粒子的位置是不确定的,因此我们绘制静态点的位置可以是个随机数值,只要它的范围仍然在webgl的-1 ----- 1 就好了。首先需要对粒子数量的变量进行读写的劫持处理。
下面代码是我跟着学习写的, 大致 就是 开平方根,对这个开平方根后的数取最接近的正整数,然后将它做成一张纹理图片,由于位置状态是随机的,所以生成的数也是随机的, 以下代码做了如下图中的事。
在这里插入图片描述

set particleNums(particleNums) {
        const gl = this.gl
        // 开根 取 去掉小数后 最大的那个树
        const particleRes = this.particleEdge = Math.ceil(Math.sqrt(particleNums))
        this._particleNums = particleRes * particleRes

        // 将 该缓冲数据 存入 rgba值 , 最后将其创建为纹理,此纹理保存 粒子的状态
        // 因为我们一开始 不确定粒子的位置,所以 随机就好了。
        const particleState = new Uint8Array(this._particleNums * 4)
        for (let i = 0, ii = particleState.length; i < ii; i++) {
            particleState[i] = Math.floor(Math.random() * 256)
        }

        this.particleStateTexture = util.createTexture(gl, gl.NEAREST, particleState, particleRes, particleRes);

        const particleIndices = new Float32Array(this._particleNums)
        for (let i = 0; i < this._particleNums; i++) particleIndices[i] = i;
        this.particleIndexBuffer = util.createBuffer(gl, particleIndices)
    }

    get particleNums() {
        return this._particleNums;
    }

特别注意:

  • 之前有同样在研究风场实现的朋友询问我,为什么这里我们要做粒子数量做开根填充到图片处理?突然间一下问到了我,其实想想理由很简单,你得知一个确定的值,比如2000 我们大家都知道 10 * 200 或者 200 * 10 再或者 4 * 500 的图片大小也可以容纳这些粒子的状态,但是关键是,你从一个 确定的 2000 如何 倒推 回 之前的两个未知数呢? 即 2000 = x * y ? 故此 有 2000 = x * x,无他,通用而已。

绘制点的顶点着色器分析

precision mediump float;

attribute float a_index;

uniform sampler2D texture_particles;
uniform float edge_particle;

varying vec2 particle_pos;

void main() {
   vec4 color = texture2D(texture_particles, vec2(fract(a_index / edge_particle), floor(a_index / edge_particle) / edge_particle));

   particle_pos = vec2(color.r / 255.0 + color.b, color.g / 255.0 + color.a);

   gl_PointSize = 1.0;
   
   gl_Position = vec4(2.0 * particle_pos.x - 1.0, 1.0 - 2.0 * particle_pos.y, 0, 1);
}

假设粒子数量为 particlesNum , 我们用纹理(图片)保存他的状态,如法炮制,但首先明确的是这个只是一个数字,但我们可以对他开根号,得到一个长宽同样的空的纹理图片面积
u_particles_res 则是粒子数量开根的结果,就相当于映射的边界,统一看图。

  • vec4 color 这段有啥意义?
    texture2D 获取u_particles 这张纹理,将在此纹理坐标上的颜色取出来。
    为啥是这个计算?这个计算想表达什么?一图给你讲明白。
    在这里插入图片描述
    x轴方向上应该比较好理解,y轴上为什么还要再除一次res? 获得的整数部分 相当于只是我们在普通的xy的坐标上的位置,再除一次 才是它真实的纹理y方向上的值。

图像可能稍微有点不准确的描述位置,读者可自行手绘,顺着这个思路,相信你能秒懂。

绘制点的片元着色器分析

precision mediump float;

uniform sampler2D wind_map;
uniform vec2 wind_min;
uniform vec2 wind_max;
uniform sampler2D color_ramp;

varying vec2 particle_pos;

void main() {

    vec2 velocity = mix(wind_min, wind_max, texture2D(wind_map, particle_pos).rg);
    float speed_t = length(velocity) / length(wind_max);

    // color_ramp 是 一个 16 * 16 的纹理
    vec2 ramp_pos = vec2(fract(16.0 * speed_t), floor(16.0 * speed_t) / 16.0);

    gl_FragColor = texture2D(color_ramp, ramp_pos);

}

这个着色器从后往前看比较容易理解,gl_FragColor = texture2D(color_ramp, ramp_pos); 意味着从color_ramp中对ramp_pos此位置进行纹理采样。专业术语是这样,说人话就是读ramp_pos位置的rgba值,然后渲染此颜色。也就是问题转换成ramp_pos是代表什么?

  • velocity

    观察等式 mix(wind_min, wind_max, texture2D(wind_map, particle_pos).rg) , 即返回 此前生成的记录粒子的值的风速图的rg ,即 u v 值, 与最大的u ,v 值 做线性插值。 得到一个经过线性插值的值。

  • speed_t
    length(velocity) / length(wind_max) a平方加b 平方等于 矢量长度 懂了吧?也就是说 这个变量是一个当前粒子的经过线性插值 的uv 与 最大风速的 u v 矢量和之比。
    在这里插入图片描述
    为何要求此矢量和之比?联想一下我们之前生成风速图的那个片段,以及处理粒子数量的原理, colorRamp是什么样一个东西?
    在这里插入图片描述
    它是一个长度为256 高度为1(高度为0 就不显示了)的一张颜色渐变的贴图,我们需要把它转换为 16 * 16 的一张纹理图片用于判断,此时我们的粒子 在的位置,它的颜色应该是什么。这是speed_t的意义所在:它是用于我们查找对应颜色的一个比例。

  • ramp_pos
    vec2(fract(16.0 * speed_t), floor(16.0 * speed_t) / 16.0) ,这样肯定 会有人产生疑惑,那这为啥是乘的这个比例呢?首先先看它生成的代码

getColorRamp(colors) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        canvas.width = 256;
        canvas.height = 1;

        const gradient = ctx.createLinearGradient(0, 0, 256, 0);
        for (const stop in colors) {
            gradient.addColorStop(+stop, colors[stop]);
        }

        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, 256, 1);
        console.log(canvas.toDataURL());
        return new Uint8Array(ctx.getImageData(0, 0, 256, 1).data);
    }
setColorRamp(colors) {
        // lookup texture for colorizing the particles according to their speed
        this.colorRampTexture = util.createTexture(this.gl, this.gl.LINEAR, this.getColorRamp(colors), 16, 16);
    }

这里做了个用canvas生成一个渐变色的色板,也许有部分人不知道imageData是的值 是从左上角计算的,是从左上角计算的是从左上角计算的。即原点(0,0)在左上角,现在你知道为啥 * 16.0了把,原理其实跟顶点着色器中的读取是一致的。只不过需要绕一个弯。

绘制静态随机点

此时这个步骤应该说是能看到点是随机生成的,我们刷新多几次会发现颜色分布是固定的,这就对了,也就是说:我们绘制的会是这个流线中的任一状态的点。看图。
在这里插入图片描述

那么在开始绘制前还有一项重要的内容,(其实也不复杂,应该说对比别的关键内容轻松许多),就是赋值,传递变量,常量进着色器里面。

drawPaticles() {
        const gl = this.gl

        const program = this.drawProgram
        gl.useProgram(program.program)

        util.bindAttribute(gl, this.particleIndexBuffer, program.a_index, 1)
        util.bindTexture(gl, this.windTexture, 0);
        util.bindTexture(gl, this.particleStateTexture, 1);
        util.bindTexture(gl, this.colorRampTexture, 2)

        gl.uniform1i(program.wind_map,0)
        gl.uniform1i(program.texture_particles,1)
        gl.uniform1i(program.color_ramp,2)

        gl.uniform1f(program.edge_particle, this.particleEdge)
        gl.uniform2f(program.wind_min, this.uMin, this.vMin)
        gl.uniform2f(program.wind_max, this.uMax, this.vMax)
        gl.disable(gl.DEPTH_TEST);
        gl.disable(gl.STENCIL_TEST);
        gl.clearColor(0.0,0.0,0.0,1.0)
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        gl.drawArrays(gl.POINTS, 0, this._particleNums)

    }

绘制移动的粒子

这里先去除繁杂的计算,直接探寻线状的粒子是如何让人看上去是移动的。注意刚才这句话已经暗示了风流线的最深层的原理。通常如果没有接触过任何的其他类似的东西,我们很容易误以为这个线是通过矢量长度的表示,通过一些复杂的计算公式让他产生一种弧线的形状,但其实有很多东西说不通。
最简单的说明是:即使产生了这种形状,如何使他根据时间生成或者消散?
这段说明是很有必要的,因为我在研究这个风场可视化之前我的预估、猜测就是错误的,导致在这一部分甚是不解。所以如果你也是这么想的,摒弃之前的猜测,看看作者是如何实现的。

首先从最简单的思考开始,如何增加线的长度?这个就有意思了,大家可以类比一下以前玩贪吃蛇的时候,我们吃到豆豆的时候是什么样子?是贪吃蛇的尾部多了一个像素点对吗?可能说道这里有些意识已经懵懵懂懂了,但肯定还差点意思。上图
在这里插入图片描述

应该恍然大悟了吧?这其实就是他移动的原理:保存上一帧被绘制过的图像,将其叠加到当前这一帧的图像中,重复这个过程,我们就能得到移动的粒子,当然这里是夸张化处理了,实际上我们的粒子的偏移量计算一般都是很小的。

阐述完了他的绘制原理,就到绘制过程中的探索了,我们有不少问题要去处理比如:粒子的消散、状态的重置、计算的偏移量等等。一次性消化实在不是一件容易的事,我们抛弃这些处理,在这个标题下我只想看到我的粒子移动,怎么做?

根据上面的内容,我们应该保存一个screenFrame 用于表示上一帧 ,此时又有新的概念产生了,我们需要保存一种能够处于在最终绘制的结果中间的一个结果,首先需要用到FBO(帧缓冲对象),而这种借助于中间结果而显示最终结果的技术也成为离屏渲染

  • FBO如何使用?
 gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer) // 绑定当前缓冲区对象 表示准备往里绘制
 gl.viewport(0, 0, gl.canvas.width, gl.canvas.height)
 draw() // 绘制在帧缓冲区上
 gl.bindFramebuffer(gl,null) // 解绑
 // 解绑 表示对 该frameBuffer 的输入完成
  • 如何合成这两帧?
    如果是一般的项目内容里,我们只需要把上一帧的内容 输出texture 保存就可以了,但是风场可视化这个内容里,我们还需要做一个 透明度 的处理 , 为何要做透明度的处理?你试想下,每一帧里,都对当前这一帧的颜色减弱,直到可预见的某一帧中 最开始的那一帧绘制有粒子的图像 的 a 值 直至 0 变为透明 也就意味着消散了。

我们理一下他的保存状态的着色器代码就知道了。

截屏顶点着色器

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);
}

没啥可说的,就是获取当前webgl坐标系里的顶点边界作为三角形填色。
在这里插入图片描述

截屏片元着色器

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);
}

代码里内容不多,也就是说读取u_screen 这个纹理(此纹理表示当前帧的图像)然后把颜色填充,所以实际上显示出来的就是透明度 为 u_opacity 的 当前帧的图像,当然,从此处也可以应证一点: 随着 一次次的叠加,最开始的粒子图像经过N次 的 透明度下降之后 会 趋近于0 ,也就是 消失

粒子运动的程序过程、调试图

调试工具为 spectorjs
在这里插入图片描述
上一帧
在这里插入图片描述
当前帧
在这里插入图片描述
合成帧(当前画布上渲染的图像)
在这里插入图片描述
细看某一帧整个程序离屏绘制运行的过程
在这里插入图片描述

	/**
     * 绘制屏幕纹理 
     */
    drawScreen(){
        const gl = this.gl
        util.bindFramebuffer(gl,this.framBuffer,this.screenTexture)
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
        
        this.drawTexture(this.backgroundTexture, 0.9)
        this.drawParticles()

        util.bindFramebuffer(gl,null)
        // 混合颜色 见下文
        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);

        util.bindAttribute(gl, this.quadBuffer, program.a_pos, 2);
        util.bindTexture(gl, texture, 2);
        gl.uniform1i(program.u_screen, 2);
        gl.uniform1f(program.u_opacity, opacity);

        gl.drawArrays(gl.TRIANGLES, 0, 6);
    }
    /**
     *  传输 数据 进gpu 同时绘制当前一帧  粒子的状态
     */
    drawParticles() {
        const gl = this.gl

        const program = this.drawProgram
        gl.useProgram(program.program)
        util.bindAttribute(gl, this.particleIndexBuffer, program.a_index, 1)
        // util.bindTexture(gl, this.windTexture, 0);
        // util.bindTexture(gl, this.particleStateTexture, 1);
        util.bindTexture(gl, this.colorRampTexture, 2)
        
        gl.uniform1i(program.wind_map, 0)
        gl.uniform1i(program.texture_particles, 1)
        gl.uniform1i(program.color_ramp, 2)

        gl.uniform1f(program.edge_particle, this.particleEdge)
        gl.uniform2f(program.wind_min, this.uMin, this.vMin)
        gl.uniform2f(program.wind_max, this.uMax, this.vMax)
        
        gl.drawArrays(gl.POINTS, 0, this._particleNums)

    }

结合代码的内容,我们在绑定一个缓冲区后,将视口设置为canvas的宽高,绘制当前帧的图像 保存为纹理,接着解绑表示绘制完毕。然后混合颜色。最后 交换。一个一个解释。

为何要交换纹理

	draw(){
        const gl = this.gl;
        gl.disable(gl.DEPTH_TEST);
        gl.disable(gl.STENCIL_TEST);
        
        util.bindTexture(gl, this.windTexture, 0);
        util.bindTexture(gl, this.particleStateTexture0, 1);
        
        this.drawScreen();
        this.updateParticles();
    }
    
    updateParticles(){
        const gl = this.gl
        util.bindFramebuffer(gl,this.framBuffer,this.particleStateTexture1)
        gl.viewport(0, 0, this.particleEdge, this.particleEdge)
        
        const program = this.updateProgram
        gl.useProgram(program.program)

        util.bindAttribute(gl, this.quadBuffer,program.a_pos, 2)
        gl.uniform1i(program.particle_texture, 1)

        gl.uniform1f(program.edge_particle, this.particleEdge)
        gl.uniform2f(program.wind_min, this.uMin, this.vMin)
        gl.uniform2f(program.wind_max, this.uMax, this.vMax)

        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;

    }

从代码里可以看到 无论是drawScreen函数里还是updateParticles函数里,都有存在交对纹理进行交换的这种操作。这种操作的目的是什么呢?如果你记忆深刻你应该能从这段gl.uniform1i(program.particle_texture, 1) 找到答案,即从始至终截屏的渲染都是以particleStateTexture0 这个纹理进行渲染的。也就是说他始终作为当前帧状态的纹理去传送到GPU中,而我们updateParticles的时候处理的是particleStateTexture1 因此,为何交换?原因就是我们使用了particleStateTexture1 去保存了当前帧的粒子状态。从原文swap the particle state textures so the new one becomes the current one这也就是源码中注释的正确解读。而调试图中也应证了这个想法。而drawScreen函数里的交换则是保存合成出来的当前的 帧 图像 作为背景纹理 作为此文中所指的下一帧中的上一帧,与当前时刻的合成帧,即图像已经被渲染到帧缓冲区中了,此时把这一帧的图像保存。有点绕,但我找不到更好的描述了。
在这里插入图片描述

混合颜色


        gl.enable(gl.BLEND)
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
        this.drawTexture(this.screenTexture,1.0)
        gl.disable(gl.BLEND)

在这里插入图片描述
如何用最直白的语言描述?这个混合颜色可以类比地图中图层的概念,但他与图层有些许不同,地图中的图层 每个图层叠加是以纯色叠加的,也就是说该图层本身是什么颜色 叠加完后 它的颜色还是什么颜色。而同样以图层为例,抛开webgl概念里的混合因子,试想一张纯黑色的图像与纯白色的图像把颜色混合,叠加出来的颜色是什么?大致是灰色。

在这里插入图片描述

那么在代码里为什么要做混合这件事?我目前感觉是多此一举,我认为可以直接去掉,徒增读代码人的误解。从结果上来说,删掉这段混合的代码效果是同样的。之前我们说过了,通过上一帧调整透明度 变暗,接着对当前一帧的记录粒子状态的纹理完整输出的合成帧, 这个合成的过程,则是透明度变暗的背景与纯色1.0的屏幕背景合成。也就是说gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)这就只是两纯色图层叠一起的,实际我们不需手动得做混色处理。我没有查找资料佐证。仅做推测。当然,若时间推移,我能完美得回答这个问题时会更正与补充。

此时的与数据无关移动粒子图

在这里插入图片描述
这个为我为了实现粒子移动的着色器代码简略版,也就是省去了很多复杂计算的着色器代码。没什么研究的价值。粒子成功的绘制了流线的形状,消失的粒子后,就到了下一部分的内容,轨迹的探索。

// update frag glsl
precision highp float;

uniform sampler2D particle_texture;
uniform vec2 wind_min;
uniform vec2 wind_max;
varying vec2 v_tex_pos;


void main() {
    vec4 color = texture2D(particle_texture, 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(wind_min, wind_max, pos.xy);
    // 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);
    
    gl_FragColor = vec4(
        fract(pos * 255.0),
        floor(pos * 255.0) / 255.0);
}

完整的更新代码解读

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;
uniform float u_rand_seed;
uniform float u_speed_factor;
uniform float u_drop_rate;
uniform float u_drop_rate_bump;

varying vec2 v_tex_pos;

// pseudo-random generator
const vec3 rand_constants = vec3(12.9898, 78.233, 4375.85453);
float rand(const vec2 co) {
    float t = dot(rand_constants.xy, co);
    return fract(sin(t) * (rand_constants.z + t));
}

// 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));
    float speed_t = length(velocity) / length(u_wind_max);

    // 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 * u_speed_factor;

    // update particle position, wrapping around the date line
    pos = fract(1.0 + pos + offset);

    // a random seed to use for the particle drop
    vec2 seed = (pos + v_tex_pos) * u_rand_seed;

    // drop rate is a chance a particle will restart at random position, to avoid degeneration
    float drop_rate = u_drop_rate + speed_t * u_drop_rate_bump;
    float drop = step(1.0 - drop_rate, rand(seed));

    vec2 random_pos = vec2(
        rand(seed + 1.3),
        rand(seed + 2.1));
    pos = mix(pos, random_pos, drop);

    // encode the new particle position back into RGBA
    gl_FragColor = vec4(
        fract(pos * 255.0),
        floor(pos * 255.0) / 255.0);
}

太多了。。。一个一个来吧。首先从主函数的第一个看起, 对重点变量进行细致地解析。

  • velocity
    相信经过前面的内容,mix 这个函数,大家应该是不陌生了,大致怎么形容这个函数做了什么事?
    在这里插入图片描述
    如果只是单纯的通过线段的开始与结束,查找pos的位置 这就只是一个普通的线性插值,而现在我们应该关注的重点是这个lookup_wind(pos) ,弄懂了他 这个velocity 也就弄懂了。

  • lookup_wind

// 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);
}

他接收的参数是一个位置,我们细细解读他的每个变量。
在这里插入图片描述
奶奶的,写字太累了, 我还是继续在电脑上说吧。

  • f
    f 变量则是求它的小数,其实也就是相对于左上角的偏移量。
  • tl
    为何是texture2D(u_wind, vc).rg ?理由在此: u_wind 是最开始 我们通过 uv方向的速度在每一个格子里转换为 r g 的一个记录着uv方向速度的图片,将此转换反过来,我们通过查找此纹理上的位置的rg,也就等于获得了该位置的速度,也就是说,这个tl 实际上是表示 u ,v的值。只不过需要绕一下,通过纹理去获取。
    那么相信解释到此处, tr(topright右上),bl(左下),br(右下)如法炮制,加一个格子的位移距离(px),我们就此得到了将传进来的参数uv 包裹的一个extent,同时,我们也已经知道他的偏移量f,在此基础上 分别对x方向 ,y方向 进行线性插值 mix(这个还不清楚的话建议反复观看上方内容)

到这,整个函数的实现已经说明清楚了,它就是一个查找粒子的移动状态(位置)的函数。为什么要这么做?试想下,一个粒子的移动(“线”的移动)不会总是在一个准确的边缘上,想象一个粒子总是坐落在四方形格子的顶点上(当然这是不可能的),随着移动,他必然会落入在每个四方形的格子内部,此时我们需要根据四个顶点已知的速度估算这个点所在的速度,也就是说这个函数的作用是为了查找出包裹他的4个顶点速度在x轴方向上与y轴方向上线性插值的一个平滑速度

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值