风场可视化:绘制轨迹
引子
了解绘制粒子之后,接着去看如何绘制粒子轨迹。
源库: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 framethis.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_pos
:quadBuffer
中每个顶点二维数据。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.0gl_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 interpolationvec2 lookup_wind(const vec2 uv) {// return texture2D(u_wind, uv).rg; // lower-res hardware filteringvec2 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 RGBAvec2 velocity = mix(u_wind_min, u_wind_max, lookup_wind(pos));// take EPSG:4236 distortion into account for calculating where the particle movedfloat 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 linepos = fract(1.0 + pos + offset);// encode the new particle position back into 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() {// 代码省略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 frameutil.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 frameconst 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 oneconst 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
风场可视化:绘制轨迹相关推荐
- 风场可视化:绘制粒子
引子 了解风场数据之后,接着去看如何绘制粒子. 源库:webgl-wind Origin My GitHub 绘制地图粒子 查看源库,发现单独有一个 Canvas 绘制地图,获取的世界地图海岸线坐标, ...
- 风场可视化与原理剖析
最近因为项目需要,做风场可视化,也不是什么新鲜的东西了,站在前人的肩膀上鼓捣了两天也算是完成了,特此记录一下. 网上关于风场可视化的文章也挺多,可以拜读以下几位博主文章,在此表示感谢. 数据可视化之风 ...
- python画地图轨迹图_如何使用python在单张地图上绘制轨迹?
我正在处理如下所示的数据帧.x和y是轨迹随时间变化的墨卡托x,y坐标.具有相同航向的行是属于同一轨迹的点.我们可以看到,所有0的行分隔了不同的轨迹.在VoyageID X Y Time 27 2 -7 ...
- android百度地图轨迹实现,android 获取GPS经纬度在百度地图上绘制轨迹
实现将一组GPS模块获取的经纬度数据在百度地图上绘制轨迹 1.将经纬度转换成百度地图坐标 /** * 标准的GPS经纬度坐标直接在地图上绘制会有偏移,这是测绘局和地图商设置的加密,要转换成百度地图坐标 ...
- js插值计算_Python IDW插值计算及可视化绘制
前面几篇推文我们分辨介绍了使用Python和R绘制了二维核密度空间插值方法,并使用了Python可视化库plotnine.Basemap以及R的ggplot2完成了相关可视化教程的绘制推文,详细内容如 ...
- R语言可视化绘制及PDF使用字体参数列表:查看字体列表、可视化绘制图像中的字体参数列表、字体示例并写入pdf
R语言可视化绘制及PDF使用字体参数列表:查看字体列表.可视化绘制图像中的字体参数列表.字体示例并写入pdf 目录 R语言可视化绘制及PDF使用字体参数列表:查看字体列表.可视化绘制图像中的字体参数列 ...
- R语言ggplot2可视化绘制一头奶牛、Linux下使用cowsay打印奶牛(cow)
R语言ggplot2可视化绘制一头奶牛.Linux下使用cowsay打印奶牛(cow) 目录 R语言ggplot2可视化绘制一头奶牛.Linux下使用cowsay打印奶牛
- seaborn可视化绘制双变量分组条形图(Annotating Grouped Barplot: Side-by-side)、添加数值标签进行标记
seaborn可视化绘制双变量分组条形图(Annotating Grouped Barplot: Side-by-side).添加数值标签进行标记 目录
- R语言ggplot2可视化绘制线图(line plot)、使用gghighlight包突出高亮满足条件的线图、并保留其它线图的色彩(而不是灰色)自定义非高亮线图的透明度
R语言ggplot2可视化绘制线图(line plot).使用gghighlight包突出高亮满足条件的线图.并保留其它线图的色彩(而不是灰色)自定义非高亮线图的透明度 目录
最新文章
- 【人物】徐小平:既然做老大,你就得让兄弟们有肉吃
- 【每日SQL打卡】​​​​​​​​​​​​​​​DAY 20丨查询球队积分【难度中等】​
- android P版本ro.build.date时间修改
- OpenVINO 中的BFYX解释
- 【OpenCV】OpenCV函数精讲之 -- namedWindow()函数
- 解决input获取焦点时底部菜单被顶上来问题
- JVM·垃圾收集器与内存分配策略之对象是否可被回收!
- 中国2017 Google 开发者大会第二天简单回顾
- 声卡调试精调效果都用那些宿主(DAW)机架和效果器插件
- 小程序毕设作品之微信酒店预订小程序毕业设计(8)毕业设计论文模板
- java-opencv 米粒数_Python opencv学习音符的米粒数,返回每个米粒的位置面积和总米粒数的平均面积,pythonopencv,笔记,之数,并,一个,及,个数...
- Android Studio运行app启动模拟器一直卡在“Wating for target device to come online”解决方法
- java徽章_java
- 要求用户在Python中输入整数| 限制用户仅输入整数值
- JWS 批注参考WebService注解
- 深度学习算法之-SSD(一)
- 27-什么是自旋锁?自旋的好处和后果是什么呢?
- R语言rjags使用随机效应进行臭氧数据分析
- 玲珑杯2.5 1032 A-B
- 存储ic载板_ic载板的定义
热门文章
- 使用shiro+aop实现权限控制
- 7-4 最短路径之Dijkstra(朴素dijkstra打印路径)
- 手机屏幕分辨率:物理分辨率和逻辑分辨率
- 致得E6协同文档管理软件 推出4.0免费版
- 同网络的计算机能共享音箱吗,2019年PC“老爷机”局域网内DLNA共享音乐到小度智能音箱全记录...
- allow_url_include和allow_url_fopen 详解
- 网页学习——创建HTML框架(frameset、frame、iframe)
- [Win10] 代理服务器出现问题或地址有误
- Autograd:你没有使用过的最佳的机器学习库?
- 请检查ftp文件服务器是否开启,查看服务器是否开启ftp服务