webgl_gpgpu_birds 样例分析
创始人
2025-05-29 03:46:11
0

webgl_gpgpu_birds 是一个 three.js 的官方样例,这个例子模拟了鸟群的运动,是一个群组动画,并且动画的帧率也很高;鸟群的运动很自然,非常值得研究。类似的群组动画还有鱼群,boid是‘类鸟群’的英文

大概两年前,第一次看这个例子时,很枯燥,看不懂,有很多地方不知道是什么意思。第一次看这个例子时,才知道原来 纹理 texture 可以这样来使用,这个例子可以作为一个通用的并行计算框架的例子来看待。

这个例子的鸟群中一共有 32 x 32 共 1024 只鸟。鸟群中的每只鸟的位置是一个三维坐标,保存在一张 32 x 32 像素的纹理图片缓存中,初始化时,fillPositionTexture函数负责为 每只鸟 赋予一个 [-400, 400] 闭区间内的一个随机值。鸟的位置的x y z 分量都在 -400 到 400内取随机值。

鸟群中的每只鸟的速度是一个三维向量,保存在另外一张 32 x 32 像素的纹理图片缓存中,初始化时,fillVelocityTexture函数负责为 每只鸟 的速度的x y z分向量都赋予一个 [-5, 5] 闭区间内的一个随机值。每只鸟的速率和方向都是不同的。

例子中的 GPUComputationRenderer 负责在每一帧渲染前,都去以一定的规律 或 计算模式去更新鸟群的位置纹理 和 速度纹理。这两个纹理的分辨率都是 32 x 32 像素,对于渲染器来说,分辨率很小。渲染器更新的纹理的分辨率一般都是屏幕分辨率,1024 * 768 等;所以,更新这两张贴图对于渲染器来说很轻量,写这两张纹理对应的片元着色器代码时,不用过于考虑效率问题。
第一次看这个例子时,差不多就知道这些。片元着色器和顶点着色器的代码完全看不懂。
这个例子一共有四个着色器。
a. 片元着色器 birdFS,负责更新鸟群中每只鸟的颜色,最简单
b. 顶点着色器birdVS,负责更新鸟群中每只鸟的姿态和位置坐标,第二难理解
c. 片元着色器fragmentShaderVelocity,负责更新鸟群中每只鸟的速度,相对来说最难理解,
d. 片元着色器fragmentShaderPosition,负责更新鸟群中每只鸟的三维坐标,第二简单
这四个着色器,是透彻理解这个例子绕不过去的。

着色器先放一放,先来讲场景构建,BirdGeometry其实名称不确切,应该叫BirdFlockGeometry.因为这个几何体实际上是描述鸟群的。vertices 属性保存每只鸟的几何顶点,birdColors属性保存每只鸟的顶点颜色,references 属性保存每只鸟在鸟群中的编号,可以通过这个编号找到每只鸟的在纹理图片中的三维坐标和三维速度向量。birdVertex属性保存一只鸟的顶点编号,每只鸟由三个三角面组成,每个三角面又由三个顶点组成。一只鸟就有九个顶点。这个编号就是从 0 到 8,每只鸟都是 0 到 8,这个birdVertex属性 只用于 birdVS 顶点着色器,用于找到鸟翅膀的两个顶点,修改两个顶点 y 坐标,这样每只鸟的一双翅膀就上下扇动起来了。

BirdGeometry的构造函数中,定义了每只鸟的形状;每只鸟顶点颜色,是从深灰到浅灰的不同数值

场景构建时,这行代码要留意一下 camera.position.z = 350; 摆在了正对着世界坐标的 xy 平面,并且世界坐标的原点位于屏幕的正中心。函数 fillPositionTexture 和 fillVelocityTexture 分别用于初始化每只鸟的位置和速度。

在绘制每一帧前都要调用gpuCompute.compute(),去更新两张 32 x 32像素的纹理图片,每只鸟的位置和速度就变化起来了。这两张纹理然后再传递给 鸟群的顶点着色器birdVS ,更新每只鸟的位置和姿态。

birdFS 中根据每只鸟的位置 z 坐标,来更新鸟的灰度,

varying vec4 vColor;
varying float z;uniform vec3 color;void main() {// Fake colors for nowfloat z2 = 0.2 + ( 1000. - z ) / 1000. * vColor.x;gl_FragColor = vec4( z2, z2, z2, 1. );}

z 越接近相机,越接近350,颜色边深,变暗,超过350,飞到相机后面,看不见了。

birdVS中,

if ( birdVertex == 4.0 || birdVertex == 7.0 ) {// flap wingsnewPosition.y = sin( tmpPos.w ) * 5.;
}

使每只鸟的翅膀上下扇动起来

velocity.z *= -1.;
float xz = length( velocity.xz );
float xyz = 1.;
float x = sqrt( 1. - velocity.y * velocity.y );float cosry = velocity.x / xz;
float sinry = velocity.z / xz;float cosrz = x / xyz;
float sinrz = velocity.y / xyz;

根据速度向量,求方位角 cosry sinry 和俯仰角 cosrz sinrz
假设 velocity 等于 (0, 0, 1.0), 那么 sinry == 1.0;表示需要绕 y轴 旋转90°,进行偏航;
在 BirdGeometry 中对单只鸟的形状构建,可以看到单只鸟的原始朝向就是 (0, 0, 1.0),也就是,velocity 等于 (0, 0, 1.0)时,其实不应该有 偏航;代码中的 576行,birdMesh.rotation.y = Math.PI / 2;
又把这种不一致纠正回来。

newPosition =  maty * matz * newPosition;
newPosition += pos;

每只鸟的每个顶点,先绕z轴 (俯仰角)旋转,再绕y轴(方位角)旋转

fragmentShaderPosition片元着色器负责更新每只鸟的三维坐标,其中的 phase 保存在 w 分量中,用于在之后的 birdVS 顶点着色器中使用,来更新翅膀的摆动幅度,期望速率越大时,摆动的幅度也越大,频率也越快, 其中的 62.83 约等于 PI 的20倍。

uniform float time;
uniform float delta;void main()	{vec2 uv = gl_FragCoord.xy / resolution.xy;vec4 tmpPos = texture2D( texturePosition, uv );vec3 position = tmpPos.xyz;vec3 velocity = texture2D( textureVelocity, uv ).xyz;float phase = tmpPos.w;phase = mod( ( phase + delta +length( velocity.xz ) * delta * 3. +max( velocity.y, 0.0 ) * delta * 6. ), 62.83 );gl_FragColor = vec4( position + velocity * delta * 15. , phase );}

最后一个最复杂,代码最多的fragmentShaderVelocity片元着色器,更新每只鸟的速度向量。
可以看到优先级最高的是规避 捕食者,让鸟群远离捕食者一定的距离;
可以看作是来自捕食者的排斥力,这是有条件的,只有鸟靠近捕食者一定距离,才会收到这种斥力,
第二优先级是,使鸟始终向着屏幕的中心移动,这些鸟始终都受到来自屏幕中心的引力;如果没有这个力,鸟群就散开了,很快飞到相机看不见的位置了。
紧接着是一个32 * 32的二重循环,来对鸟群中每只鸟应用 来自其他鸟的排斥力,吸引力,偏向力

if ( dist < 0.0001 ) continue;

表示如果当前像素就是自己,直接跑完这次循环

if ( distSquared > zoneRadiusSquared ) continue;

表示这只鸟 离当前自己太远,不会对我产生排斥力,偏向力,吸引力,直接跑完这次循环,忽略掉。
接下来,就是 if … else if … else … 三个分支,其实可以想象一个三个大小不同的圆组成一个同心圆环。最内层的圆表示,如果我自己和其他鸟的距离小于圆半径,则我受到来自这只鸟的排斥力;
如果我自己和其他鸟的距离在最小圆半径 和 次小圆半径之间,则我受到来自这只鸟的偏向力;我的飞行姿态要向这只鸟看齐,如果我自己和其他鸟的距离在次小圆半径 和最大圆半径之间,则受到来自这只鸟的吸引力。

排斥力,偏向力,吸引力三个力是鸟群之间的相互作用力。三个力是互斥的,鸟A 只能受到 鸟B三个力中的一种,也可能 鸟A 和 鸟B之间完全没有相互作用力。三个力的优先级是 排斥力 > 偏向力 > 吸引力。

separationDistance 定义排斥力半径, separationDistance + alignmentDistance的次圆面积 减去 半径为 separationDistance的最小圆面积,得到一个圆环区域;以我自己为圆心,如果其他鸟在这个圆环区域内,则我向这只鸟看齐,受到来自只鸟的偏向力;最大圆的半径是 separationDistance + alignmentDistance + cohesionDistance;最大圆 减去 次小圆又是另一个圆环;这个圆环内小鸟对我产生吸引力

// Attraction / Cohesion - move closer
float threshDelta = 1.0 - alignmentThresh;
float adjustedPercent;
if( threshDelta == 0. ) adjustedPercent = 1.;
else adjustedPercent = ( percent - alignmentThresh ) / threshDelta;f = ( 0.5 - ( cos( adjustedPercent * PI_2 ) * -0.5 + 0.5 ) ) * delta;velocity += normalize( dir ) * f;

上面代码里还考虑了除零异常。cohesionDistance是允许为零的,为零时,f = 1.5 * delta;
delta表示前一帧和当前帧之间流逝了多少时间,以毫秒为单位;

代码中当 cohesionDistance == 0, 并且 alignmentDistance == 0,当percent == 1时,直接进入else分支,这时鸟群之间没有偏向力,只有吸引力和排斥力两种。

相关内容

热门资讯

证监会再次修订《上市公司治理准... 7月25日晚上,证监会发布通知对《上市公司治理准则(修订征求意见稿)》公开征求意见。本次修订目的是为...
从“闭眼买”到“不敢信”,山姆... 蓝鲨导读:中国零售崛起 作者 | 张二河 编辑 | 卢旭成 山姆会员店,因上架了一款韩国品牌陷入舆论...
原创 欧... 近日,欧盟委员会突然宣布了一则令人震惊的消息:7月25日,该委员会正式对中国太阳能玻璃发起第二次双反...
7月28日财经早餐:欧美达成贸... 来源:市场资讯 汇通财经APP讯——周一(北京时间7月28日),现货黄金交投于3335.78美元/盎...
科技早报 | 贝索斯完成大规模... 贝索斯完成一轮大规模亚马逊股票出售,套现57亿美元 亚马逊公司当地时间7月25日提交至美国证券交易...
股市必读:紫光股份(00093... 截至2025年7月25日收盘,紫光股份(000938)报收于25.04元,上涨0.64%,换手率1....
15%!美国与欧盟达成贸易协议... 据央视新闻报道,当地时间7月27日,美国总统特朗普表示,美国已与欧盟达成贸易协议,对欧盟输美商品征收...
早新闻|央行4000亿元MLF... 宏观热点 央行、农业农村部印发《关于加强金融服务农村改革 推进乡村全面振兴的意见》 近日,中国人...
21专访|细胞存储,《繁花》爷... 21世纪经济报道记者 赵云帆 上海报道 “我是一个真正意义上的创业者”,年过古稀的瞿建国,在采访中如...
华熙生物赵燕谈胶原蛋白乱象:科... 21世纪经济报道记者雷晨 北京报道 近年来,重组胶原蛋白成为医美和护肤领域的热门概念,市场宣传中不乏...
富春染织完成董事会选举换届 开... 7月25日晚间,富春染织公告显示,当日,公司2025年第一次临时股东会和富春染织第四届第一次董事会在...
圣湘生物:两款产品取得医疗器械... 每经AI快讯,圣湘生物(SH 688289,收盘价:22.94元)7月27日晚间发布公告称,圣湘生物...
10年期国债收益率升至1.73... 近期债券市场出现显著调整,多重因素交织推动收益率持续上行。权益市场强势表现与大宗商品价格上涨形成合力...
当对手都在做下沉 蜜雪冰城旗下... [ 今年5月,蜜雪集团跟巴西签署40亿元人民币的采购意向大单,其中大多数是咖啡豆。 ] 当星巴克、瑞...
新手必看!股指期货交易规则基础... 股指期货交易规则,看似复杂抽象,实则与我们的日常生活有着奇妙的共通之处。它就像一场精心编排的生活交响...
王登发履新茅台技开公司“一把手... 一则微信公众号发布的信息,披露了茅台集团旗下的技术开发公司“一把手”已换人。 近日,南都湾财社-酒水...
特斯拉机器人V3量产版亮相!马... 快科技7月27日消息,特斯拉的Optimus人形机器人V3量产版终于要来了!马斯克在最近的财报电话会...
原创 中... 在金融全球化的浪潮中,中国资本市场始终勇立潮头,不断探索前行。7月26日,中国资本市场学会成立大会暨...
报告:我国经济增长保持韧性 下... 央广网北京7月27日消息(记者 樊瑞)近日,中国金融四十人论坛(CF40论坛)发布《2025年第二季...
超6300亿元!A股银行“分红... 7月25日,成都银行完成权益分派股权登记,将于7月28日发放现金红利,这标志着A股上市银行2024年...