2.WebGPU 顶点、片元和空间

179 阅读6分钟

前言

上一篇文章,介绍了webGPU的环境、工具、例子以及一些知识的补充。
如果之前没有接触过图形相关的东西,很多概念是很模糊的;那么,这篇文章主要就是简单分享一下相关的内容,顺便补充一些其他知识点。

概念

wgsl

上一篇一笔带过wgsl(WebGPU Shading Language),这里再简单讲讲:wgsl是一种着色器语言,其他图形API(D3D12、Vulkan、Metal)也有对应着shader,shader可以简单理解为GPU的程序。

裁剪空间

裁剪空间是GPU渲染的3D空间范围,超出该范围,GPU不执行渲染。这里需要一点想象力:我们需要将裁减空间想象为一个长方体

空间坐标计算方式(归一化设备坐标:NDC):

image.png

如上图,以屏幕的中心为原点,X轴的取值范围是:[-1,1],Y轴的取值范围:[-1,1],Z轴的取值范围是:[0,1],屏幕是0,向里延伸。

接回上文,长方体无法满足近大远小的视觉效果,所以,引入了视锥。如下图:

视锥:

image.png

最后,根据近远平面,对视锥体进行缩放,包含视锥体内的物体也进行缩放,缩放为一个长方体;长方体内的物体也跟随缩放,最终靠近近平面的物体变大,靠近远平面的物体缩小,形成了近大远小的视觉效果。

管线和着色器

我们都知道GPU计算能力特别强(CPU还要控制,协调等),为了充分利用GPU的能力,webGPU提供了两种能力,计算管线渲染管线

  1. 计算管线:提供并行计算能力
  2. 渲染管线:渲染管线对外提供了两个阶段:顶点着色片元着色

顶点着色

这里以我们上一篇的代码为例,解析一下:

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"

image.png

line-list

image.png

line-strip

image.png

triangle-list

image.png

triangle-strip

image.png

这里主要的是后缀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

调整后点的位置:

image.png

现在,我们再来看line和triangle的区别

line-list

image.png

line-strip

image.png

triangle-list

image.png

triangle-strip

image.png

综上,我们可以得出片元的组合规则:

  1. 对于要渲染点(point)来说,只有属性:point-list
  2. 对于要渲染线(line),线是由两个点组成,有属性:line-listline-strip
    1. line-list:根据数据集,每两个数据集组合为一条线。如果,最后不足两个点数据,则废弃;
    2. line-strip:根据数据集,前一个点和后一个点组合为一条线。如果有上一个点信息,会共用上一个点数据
  3. 对于要渲染三角形(triangle),三角形是由3个点组成,有属性:triangle-listtriangle-strip,规则与线相似
    1. triangle-list:根据数据集,每三个数据集组合为一个三角形。如果,最后不足三个点数据,则废弃;
    2. triangle-strip:根据数据集,每三个数据集组合为一个三角形。如果,如果后续还有数据,会共用前两个点数据(两个点数据组成线,三角形是共用线,如果不采用上一篇文章的点位置,则绘制如下:) image.png

尾言

至此,大体的概念差不多描述完了,由于我也是学习者,如果有遗漏、理解有误,提前感谢各位读者评论斧正。

附言

本文截取了许多网图,不能尽数列举,侵删