当前位置: 首页 > Web前端 > JavaScript

风场可视化:绘制轨迹

时间:2023-03-26 23:57:57 JavaScript

介绍了解了粒子的绘制,我们来看看如何绘制粒子轨迹。源库:webgl-windOriginMyGitHub绘制轨迹原文中提到,绘制轨迹的方式是将粒子绘制成纹理,然后在下一帧使用该纹理作为背景(稍微变暗),并交换输入/定位每一帧纹理。这里有两个关键的WebGL函数:JavaScriptWebGL图片透明度处理JavaScriptWebGL基于绘制粒子的帧缓冲对象,添加逻辑的主要思路:初始化时,添加背景纹理B和屏幕纹理S。创建数据时,每个粒子相关,存储了两个纹理T20和T21。绘制时,先绘制背景纹理B,然后根据纹理T20绘制所有粒子,再绘制屏幕纹理S,再将屏幕纹理S作为下一帧的背景纹理B。最后根据纹理T21绘制新的结果,生成新的状态纹理覆盖T20,开始绘制下一帧。请参阅不包含随机生成的粒子轨迹效果示例。我们来看看具体的实现。为纹理添加了与纹理相关的逻辑://codeomittedresize(){constgl=this.gl;constemptyPixels=newUint8Array(gl.canvas.width*gl.canvas.height*4);//屏幕纹理来保存前一帧和当前帧的绘制屏幕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);}//代码省略初始化的背景纹理和屏幕纹理都是基于宽高的画布,每个像素也存储4个组件。屏幕着色器程序新增一个屏幕着色器程序对象,最终可见的内容就是这个负责绘制的对象:this.screenProgram=webglUtil.createProgram(gl,quadVert,screenFrag);顶点数据顶点相关逻辑://代码省略了这个。quadbuffer=util.createBuffer(gl,newFloat32Array([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,再看下面的shader。顶点着色器添加顶点着色器和相应的绑定变量:constquadVert=`precisionmediumpfloat;属性vec2a_pos;改变vec2v_tex_pos;voidmain(){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);}//代码省略从这些零散的逻辑中,找到shader中变量对应的实际值:a_pos:quadBuffer中每个顶点的二维数据.v_tex_pos:与a_pos相同的值,将在相应的片段着色器中使用。这里gl_Position的计算方式,结合上面提到的顶点坐标都是0和1,发现计算结果的范围是[-1.0,+1.0],可以显示在裁剪空间内。片段着色器片段着色器和对应的绑定变量:constscreenFrag=`precisionmediumpfloat;均匀采样器2Du_screen;统一浮动u_opacity;改变vec2v_tex_pos;voidmain(){vec4color=texture2D(u_screen,1.0-v_tex_pos);//即使值接近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);}从这些零散的逻辑中,找到shader中变量对应的实际值:u_screen:Dynamicallychangingtexture,需要根据上下文判断。u_opacity:透明度,需要根据上下文判断。v_tex_pos:从vertexshader传过来的,即quadBuffer中的数据。1.0-v_tex_pos的范围是[0,1],正好包含整个纹理的范围。最终颜色乘以动态u_opacity的效果就是原文中“稍微变暗”的目的。更新着色器程序添加一个更新的着色器程序对象是让粒子产生运动轨迹的关键:this.updateProgram=webglUtil.createProgram(gl,quadVert,updateFrag);顶点数据和屏幕着色器程序的顶点数据共享一个集合。顶点着色器与屏幕着色器程序的顶点着色器共享一个集合。片元着色器针对更新的元着色器和对应用绑定的变量:constupdateFrag=`precisionhighpfloat;均匀采样器2Du_particles;均匀采样器2Du_wind;统一vec2u_wind_res;统一vec2u_wind_min;统一vec2u_wind_max;改变vec2v_tex_pos;//风速查询;使用基于4个相邻像素的手动双线性过滤进行平滑插值vec2lookup_wind(constvec2uv){//returntexture2D(u_wind,uv).rg;//低分辨率硬件过滤vec2px=1.0/u_wind_res;vec2vc=(floor(uv*u_wind_res))*px;vec2f=fract(uv*u_wind_res);vec2tl=texture2D(u_wind,vc).rg;vec2tr=texture2D(u_wind,vc+vec2(px.x,0)).rg;vec2bl=texture2D(u_wind,vc+vec2(0,px.y)).rg;vec2br=texture2D(u_wind,vc+px).rg;返回混合(混合(tl,tr,f.x),混合(bl,br,f.x),f.y);}voidmain(){vec4color=texture2D(u_particles,v_tex_pos);vec2pos=vec2(color.r/255.0+color.b,color.g/255.0+颜色.a);//从像素RGBAvec2解码粒子位置velocity=mix(u_wind_min,u_wind_max,lookup_wind(pos));//将EPSG:4236畸变考虑在内以计算粒子移动的位置floatdistortion=cos(radians(pos.y*180.0-90.0));vec2offset=vec2(velocity.x/distortion,-velocity.y)*0.0001*0.25;//更新粒子位置,环绕日期线pos=fract(1.0+pos+offset);//将新的粒子位置编码回RGBAgl_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(){//代码省略constprogram=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.windDatagl.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);//代码省略}从这些零散的逻辑中,找到shader中变量对应的实际值:u_wind:风产生的贴图fieldimagewindTextureu_particles:all粒子颜色信息的纹理particleStateTexture0u_wind_res:生成图像的宽高u_wind_min:风场数据分量的最小值u_wind_max:风场数据分量的最大值获取根据quadBuffer的顶点数据从纹理particleStateTexture0中获取对应位置的像素信息,用像素信息解码粒子位置,通过lookup_wind方法得到相邻4个像素的平滑插值,然后cal根据风场偏移量的最大值和最小值计算偏移量,最终得到新的位置并转为颜色输出。在这个过程中,我发现了以下几个关键点:如何得到相邻的4个像素点?在二维地图中,如何区分极地和赤道粒子?如何获得4个相邻像素?看主要方法:vec2lookup_wind(constvec2uv){vec2px=1.0/u_wind_res;vec2vc=(floor(uv*u_wind_res))*px;vec2f=fract(uv*u_wind_res);vec2tl=texture2D(u_wind,vc).rg;vec2tr=texture2D(u_wind,vc+vec2(px.x,0)).rg;vec2bl=texture2D(u_wind,vc+vec2(0,px.y)).rg;vec2br=texture2D(u_wind,vc+px).rg;returnmix(mix(tl,tr,f.x),mix(bl,br,f.x),f.y);}以生成图片的宽高为基准,得到基本单位px;在新的测量标准下,向下舍入得到近似位置vc作为第一参考点,移动基本单位px.x的单个分量得到第二参考点;移动基本单位px.y的单个分量得到第三个参考点,移动基本单位px得到第四个参考点。在二维地图中,如何区分极地和赤道粒子?如原文所述:在两极附近,粒子沿X轴的移动速度应该比在赤道移动得快得多,因为相同的经度代表的距离要小得多。对应的处理逻辑:floatdistortion=cos(radians(pos.y*180.0-90.0));vec2offset=vec2(velocity.x/distortion,-velocity.y)*0.0001*u_speed_factor;radians方法将角度转换为弧度值,pos.y*180.0-90.0猜测是将风数据转换为角度的规则。cos的余弦值在[0,π]之间逐渐变小,偏移量对应的第一个分量会逐渐变大,效果似乎更快。符号-被添加到第二个分量,假定与图像纹理一致,图像纹理默认在Y轴上反转。绘图绘图这个块变化很大:draw(){//代码省策略this.drawScreen();这个.updateParticles();}drawScreen(){constgl=this.gl;//将屏幕绘制到临时帧缓冲区中以将其保留为下一帧的背景util.bindFramebuffer(gl,this.framebuffer,this.screenTexture);gl.viewport(0,0,gl.canvas.width,gl.canvas.height);this.drawTexture(this.backgroundTexture,this.fadeOpacity);这个.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.禁用(gl.混合);//将当前屏幕保存为下一帧的背景consttemp=this.backgroundTexture;this.backgroundTexture=this.screenTexture;this.screenTexture=temp;}drawTexture(texture,opacity){constgl=this.gl;常数程序=this.screenProgram;gl.useProgram(program.program);//省略代码gl.drawArrays(gl.TRIANGLES,0,6);}drawParticles(){constgl=this.gl;constprogram=this.drawProgram;gl.useProgram(program.program);//省略代码gl.drawArrays(gl.POINTS,0,this._numParticles);}updateParticles(){constgl=this.gl;util.bindFramebuffer(gl,this.framebuffer,this.particleStateTexture1);gl.viewport(0,0,this.particleStateResolution,this.particleStateResolution);constprogram=this.updateProgram;gl.useProgram(program.program);//省略代码gl.drawArrays(gl.TRIANGLES,0,6);//交换粒子状态纹理,使新的成为当前的consttemp=this.particleStateTexture0;this.particleStateTexture0=this.particleStateTexture1;this.particleStateTexture1=temp;指定的纹理是screenTexture,注意从这里绘制出来的结果是不可见的,然后绘制整个背景纹理backgroundTexture和texture-basedparticleStateTexture0,然后取消绑定帧缓冲区。这部分绘制结果会保存在纹理screenTexture中。切换到默认的颜色缓冲区,注意从这里开始绘制的结果是可见的,开启α混合,blendFunc设置的两个参数的作用是第一次绘制后重叠的部分会被覆盖。然后整个纹理screenTexture被绘制出来,也就是说framebuffer的绘制结果显示在画布上。绘制完成后,使用中间变量进行替换,纹理backgroundTexture成为当前纹理内容,作为下一帧的背景。然后切换到帧缓冲区更新粒子状态。指定的纹理是particleStateTexture1。请注意,此处的绘图结果是不可见的。基于纹理particleStateTexture0绘制生成偏移状态,整个绘制结果将存储在纹理particleStateTexture1中。绘制完成后,使用中间变量进行替换,纹理particleStateTexture0移动后成为纹理内容,作为下一帧粒子呈现的依据。这样连续的画框看起来像是一种动态效果。疑惑似乎是这样,但有些还是不太明白。为什么offsets要用lookup_wind中的计算方式?原文解释是findsmoothinterpolation,但是里面的数学原理是什么?为什么找到之后还要再混?我个人还没有找到更好的解释。参考HowIbuiltawindmapwithWebGL