从渲染管线到GPGPU
当我们使用渲染管线进行通用计算而不是进行渲染计算时,意味着我们使用了GPU进行了绘制像素以外的其它用途。在WebGL中我们将Texture看作一个多维数组时,通过shader中的计算对这些多维数组进行读取与计算,并且将计算结果输出,上述几句话就简要概括了GPGPU的根本使用方式。
这里设计到了几个关键概念:
- 将Texture看作多维数组
- 通过shader读取多维数组并进行计算
- 将计算结果输出
这里假定读者已经具备对于渲染管线知识的掌握,仅描述如何使用渲染管线进行通用计算,使用WebGL作为代码示例
将Texture看作多维数组计算以及输出
一张Texture一般包含了4个通道,我们可以将其看作d*d*c
大小的矩阵,每一个channel中,我们可以存储不同的数据,这样能够通过加载Texture的方式,将所需要进行加速加速的数据放置到GPU中。
const resolution = {
x : 4,
y : 4
}
// 载入数据,假设数据仅为单通道
gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE,
resolution.x,
resolution.y,
0, gl.LUMINANCE, gl.UNSIGNED_BYTE,
new Uint8Array([
1, 2, 3, 4
5, 6, 7, 8
9, 10,11,12
13,14,15,16
]));
通过shader读取多维数组
在输入时,当我们确保Texture大小为矩形时,我们可以通过片元着色器中的texture2D
函数正确的读出Texture。
我们可以用顶点模拟出一个 [ − 1 , 1 ] [-1,1] [−1,1]的裁切空间,使用两个三角形创建出-1到+1的四边形顶点。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]), gl.STATIC_DRAW);
之后在片元着色器中获取这些数据共有两种方式
-
使用gl_FragCoord获取该像素点坐标,然后转换得到uv坐标:
uniform vec2 u_resolution; // 假设我们传入了该Texture的大小 uniform sampler2D textureData; // 用于读取数据的纹理 void main() { vec2 texCoord = gl_FragCoord.xy / resolution; // 从纹理中采样数据 vec4 data = texture2D(textureData, texCoord); // 对数据进行一些计算 vec4 result = someComputation(data); // 输出计算结果 gl_FragColor = result; }
-
在顶点着色器中使用顶点数据传递,使用插值结果获得uv坐标
vec4 a_position; varying vec2 index; void main() { index = (a_position.xy + 1.0) / 2.0; gl_Position=a_position; }
之后在片元着色器中,由于传递数据会自动在片元中进行插值,因此可以index即为uv坐标
varying vec2 index; // 从顶点着色器传递过来的纹理坐标 uniform sampler2D textureData; // 用于读取数据的纹理 void main() { vec4 data = texture2D(textureData, index); // 对数据进行一些计算 vec4 result = someComputation(data); // 输出计算结果 gl_FragColor = result; }
读取输出
使用gl.readPixels
可以对输出进行读取,需要注意,该输出会将RGBA四个通道均进行输出
一个Slime模拟器
使用了twjs库,简化webgl代码
Slime模拟效果简述
该模拟来源于论文中对于Physarum polycephalum一种粘液真菌的扩散机制模拟,如下图所示
简言之,真菌在向前移动时,会在当前位置留下信息素,同时向移动方向释放传感器,文章中将传感器固定为三个方向,分别如图所示为F,FL以及FR。传感器会感知这三个方向上,之前真菌留下的轨迹信息素,最终真菌会更倾向选择向着之前真菌留下信息素的方向进行移动。如果真菌在移动中碰到障碍物,则会选择随机一个方向继续向前移动。
我们使用WebGL去创建模拟真菌移动的效果,并且使用GPGPU进行加速计算,得到了模拟器中所展示的效果。
模拟说明
位置与扩散信息记录
使用Texture(pos_tex)去记录当前每个真菌的位置(2d),以及方向角(1d)。Texture大小设置为1024*1024为四通道,其中rg通道记录位置信息,b通道记录角度信息。
public convertToTextureData(positions: number[], angles: number[]) {
const data = new Float32Array(1024 * 1024 * 4);
for (let i = 0; i < angles.length; ++i) {
const src = i * 2;
const dst = i * 4;
data[dst] = positions[src];
data[dst + 1] = positions[src + 1];
data[dst + 2] = angles[i];
}
return data;
}
public createAgents(positions: number[], angles: number[]) {
const texture = twgl.createTexture(this.gl, {
src: this.convertToTextureData(positions, angles),
width: 1024,
height: 1024,
min: this.gl.NEAREST,
wrap: this.gl.CLAMP_TO_EDGE,
});
const framebufferInfo = twgl.createFramebufferInfo(this.gl, [
{ attachment: texture, }
], 1024, 1024);
return {
texture,
framebufferInfo,
};
}
同理使用相同大小的Texture(spread_tex)的去记录真菌在空间中留下信息素的情况,初始化设置为空Texture
位置更新与信息扩散模拟
位置更新遵循上述真菌移动模式,设置三个方向的传感器,对于spread_tex的信息进行采样比较,选取最终的前进方向
...
...
float sense(vec2 position,float sensorAngle){
vec2 sensorDir=vec2(cos(sensorAngle),sin(sensorAngle));
vec2 sensorCenter=position+sensorDir*sensorOffsetDist;
vec2 sensorUV=sensorCenter/resolution;
vec4 s=texture2D(spread_tex,sensorUV,sensorSize);
return s.r;
}
void main(){
float vertexId=floor(gl_FragCoord.y*agentsTexResolution.x+gl_FragCoord.x);
vec2 uv=gl_FragCoord.xy/agentsTexResolution;
vec4 temp=texture2D(tex_pos,uv);
vec2 position=temp.rg; // rg --> pos
float angle=temp.b; // b --> angle
float width=resolution.x;
float height=resolution.y;
float random=hash(vertexId/(agentsTexResolution.x*agentsTexResolution.y)+time);
// sense environment
float weightForward=sense(position,angle);
float weightLeft=sense(position,angle+sensorAngleSpacing);
float weightRight=sense(position,angle-sensorAngleSpacing);
float randomSteerStrength=random;
if(weightForward>weightLeft&&weightForward>weightRight){
angle+=0.; //F
}else if(weightForward<weightLeft&&weightForward<weightRight){
angle+=(randomSteerStrength-.5)*2.*turnSpeed*deltaTime;
}else if(weightRight>weightLeft){
angle-=randomSteerStrength*turnSpeed*deltaTime; //FR
}else if(weightLeft>weightRight){
angle+=randomSteerStrength*turnSpeed*deltaTime; //FL
}
// move agent
vec2 direction=vec2(cos(angle),sin(angle));
vec2 newPos=position+direction*moveSpeed*deltaTime;
// clamp to boundries and pick random angle if hit boundries
...
gl_FragColor=vec4(newPos,angle,0);
}
同时在spread_tex中模拟信息素的扩散,采样当前位置的信息强度,并且与周围3*3大小的区域进行平滑处理,利用超参数控制信息的扩散(diffuseSpeed)以及蒸发(evaporateSpeed)速度
void main(){
vec4 originalValue=texture2D(tex,v_texcoord);
// Simulate diffuse
vec4 sum;
for(int offsetY=-1;offsetY<=1;++offsetY){
for(int offsetX=-1;offsetX<=1;++offsetX){
vec2 sampleOff=vec2(offsetX,offsetY)/resolution;
sum+=texture2D(tex,v_texcoord+sampleOff);
}
}
vec4 blurResult=sum/9.;
vec4 diffusedValue=mix(originalValue,blurResult,diffuseSpeed*deltaTime);
vec4 diffusedAndEvaporatedValue=max(vec4(0),diffusedValue-evaporateSpeed*deltaTime);
gl_FragColor=vec4(diffusedAndEvaporatedValue.rgb,1);
}
效果可视化
根据信息素强度以及真菌所在位置,对模拟结果进行可视化。生成了16种模拟的配色方案,将其存储在一张Texture上,按照位置与强度对贴图进行采样得到最终可视化效果