前言
上一篇文章,介绍了webGPU的环境、工具、例子以及一些知识的补充。
如果之前没有接触过图形相关的东西,很多概念是很模糊的;那么,这篇文章主要就是简单分享一下相关的内容,顺便补充一些其他知识点。
概念
wgsl
上一篇一笔带过wgsl(WebGPU Shading Language),这里再简单讲讲:wgsl是一种着色器语言,其他图形API(D3D12、Vulkan、Metal)也有对应着shader,shader可以简单理解为GPU的程序。
裁剪空间
裁剪空间是GPU渲染的3D空间范围,超出该范围,GPU不执行渲染。这里需要一点想象力:我们需要将裁减空间想象为一个长方体
空间坐标计算方式(归一化设备坐标:NDC):
如上图,以屏幕的中心为原点,X轴的取值范围是:[-1,1],Y轴的取值范围:[-1,1],Z轴的取值范围是:[0,1],屏幕是0,向里延伸。
接回上文,长方体无法满足近大远小的视觉效果,所以,引入了视锥。如下图:
视锥:
最后,根据近远平面,对视锥体进行缩放,包含视锥体内的物体也进行缩放,缩放为一个长方体;长方体内的物体也跟随缩放,最终靠近近平面的物体变大,靠近远平面的物体缩小,形成了近大远小的视觉效果。
管线和着色器
我们都知道GPU计算能力特别强(CPU还要控制,协调等),为了充分利用GPU的能力,webGPU提供了两种能力,计算管线和渲染管线。
- 计算管线:提供并行计算能力
- 渲染管线:渲染管线对外提供了两个阶段:顶点着色和片元着色
顶点着色
这里以我们上一篇的代码为例,解析一下:
vertex的代码:
@vertex
fn main(
@builtin(vertex_index) VertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 6>(
vec2(0.0, 0.5),
vec2(-0.5, -0.5),
vec2(0.5, -0.5)
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
js中的代码:
...
passEncoder.setPipeline(pipeline)
passEncoder.draw(3)
passEncoder.end()
...
我们的代码是从js开始执行的,这里的draw(3),实际上,是执行了3次上面的vertex代码,每次返回的结果,根据VertexIndex的不同,返回不同的vec4数据,
这里的代码,最终return了一个vec4,但是,实际上,我们编辑的是上面代码的pos变量的2个点,例如:vec2(0.0, 0.5),这是因为,我们绘制的三角形是平面,不涉及Z轴,所以,vec4返回的第三个参数是:0.0。
三维坐标就能在三维空间定点,所以,上一篇文章的三角形的点就这么定位出来的。
齐次坐标
裁减空间也称为:齐次空间
这里着重讲一下上面vertex代码的最后一个数。
我们绘制的图形,都是为了达到3D的效果。经过图形学的发展,2D图形变换都可以使用一个:3*3的矩阵来表示;
3D变换则是:4*4的矩阵来表示,GPU内部是使用4*4的方式进行计算的,因为,平移、旋转、缩放对应4*4的矩阵位置不同,所以,都使用4*4更方便;
而我们相乘的4*1矩阵,多了一位数,这个数称为:齐次坐标。
详细可以查看:矩阵变换
投影矩阵
上文描述了视锥的概念,视锥空间和DNC是不一样的,因为我们的设备目前都是2维的(屏幕),所以,三维的物体只是缩放、形变等投影到屏幕上。
如果,我们只按照DNC去进行物体的计算,当我们切换视角之后,物体的位置和颜色都可能会发生改变,可能需要重新构建物体在空间的布局,这是不合理。
例如:游戏人物,我们从正面和其他面看游戏人物,游戏人物模型不应该发生变化,而是我们的视角发生改变。
那么,视角变化后,怎么计算变化后的投影呢?就是:投影矩阵
局部/世界空间
如果我们只有视锥,那么,我们怎么转换视角?
这个时候,我们就需要将所有的物体绘制在世界空间里(把世界空间里的物体当成一个世界空间,这里的坐标就是局部空间),这样,我们转动视锥,世界空间不动,世界空间的物体也不动,只不过我们看到的东西发生变化了。
世界空间乘投影矩阵,就得到了webGPU的空间坐标。
我们上面说了,webGPU的坐标是[-1,1]、[-1,1]、[0,1],我们的世界不能那么小,所以,世界空间是另一套坐标体系,跟我们预期想象的是一样的:
以物体/世界的中心为原点,xyz轴无限延伸。那么,我们视锥范围(观察空间)外的,就是我们裁减空间需要裁减掉的内容。
片元着色
经过上面的空间坐标概念,我们能定位图形在空间的点。那么,现在,我们将标记的点串联,成为标记的区域(光珊化),片元要做的事情是,给光珊化区域设置对应的值(颜色、素材等)。
值得着重讲一下:所有的面都是由三角形组成的,类似圆周率计算的方式
同样的,我们也分析一下上一篇文章的代码:
fragment的代码:
@fragment
fn main() -> @location(0) vec4<f32> {
return vec4(1.0, 1.0, 1.0, 1.0); // 调整为白色,方便后续的内容展示
}
js中的代码:
primitive: {
// 顶点着色器渲染的规则:
// "point-list"|"line-list"|"line-strip"|"triangle-list"|"triangle-strip"
topology: 'triangle-list'
}
分别对应的效果: "point-list"
line-list
line-strip
triangle-list
triangle-strip
这里主要的是后缀list和strip的区别需要详细描述一下
我们改一下代码:
调整一下点的位置先后关系,增加一个点
@vertex
fn main(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 4>(
vec2(-0.5, -0.5),
vec2(0.5, -0.5),
vec2(0.0, 0.5),
vec2(0.5, 0.5),
);
return vec4<f32>(pos[VertexIndex], 0.0, 1.0);
}
绘制4次
passEncoder.draw(4)
调整后点的位置:
现在,我们再来看line和triangle的区别
line-list
line-strip
triangle-list
triangle-strip
综上,我们可以得出片元的组合规则:
- 对于要渲染点(point)来说,只有属性:
point-list - 对于要渲染线(line),线是由两个点组成,有属性:
line-list、line-stripline-list:根据数据集,每两个数据集组合为一条线。如果,最后不足两个点数据,则废弃;line-strip:根据数据集,前一个点和后一个点组合为一条线。如果有上一个点信息,会共用上一个点数据
- 对于要渲染三角形(triangle),三角形是由3个点组成,有属性:
triangle-list、triangle-strip,规则与线相似triangle-list:根据数据集,每三个数据集组合为一个三角形。如果,最后不足三个点数据,则废弃;triangle-strip:根据数据集,每三个数据集组合为一个三角形。如果,如果后续还有数据,会共用前两个点数据(两个点数据组成线,三角形是共用线,如果不采用上一篇文章的点位置,则绘制如下:)
尾言
至此,大体的概念差不多描述完了,由于我也是学习者,如果有遗漏、理解有误,提前感谢各位读者评论斧正。
附言
本文截取了许多网图,不能尽数列举,侵删