本文主要讲述在unity shader的顶点着色器内对顶点的坐标进行矩阵变换,通过对顶点进行变换,我们可以在GPU中实现很多复杂效果,如顶点动画,海水效果的模拟等等;
一、基础变换之伸缩变换
仿射变换:渲染管线顶点着色器内在空间坐标系中做的各种变换都是仿射变换;仿射变换是线性变换加平移变换,而在基础变换中,旋转变换和缩放变换都是线性变换,它们和平移变换一起组成的仿射变换成为了顶点着色器内坐标转换的基础;
主要的基础变换有平移变换,旋转变换和缩放变换,这里讲述缩放变换,其它两种类似的思路。
v.vertex *= scale;
这里的scale大小可以由用户指定。
这里扩展了两个方面,增加了shader的灵活性。
A.可以自由指定放缩的方向,
B.可以严格控制缩放的大小
代码如下:
属性内声明:
_ScaleMaxX("ScaleMaxX",Range(1.5,5))=2
_ScaleMinX("ScaleMinX",Range(0.4,1.5))=1
_ScaleMaxY("ScaleMaxY",Range(1.5,5))=2
_ScaleMinY("ScaleMinY",Range(0.4,1.5))=1
_ScaleMaxZ("ScaleMaxZ",Range(1.5,5))=2
_ScaleMinZ("ScaleMinZ",Range(0.4,1.5))=1
vert函数里:
float scalex=(_ScaleMaxX-_ScaleMinX)*0.5*clamp(_SinTime.w, -1.0, 1.0)+0.5*(_ScaleMinX+_ScaleMaxX);
float scaley=(_ScaleMaxY-_ScaleMinY)*0.5*clamp(_SinTime.w, -1.0, 1.0)+0.5*(_ScaleMinY+_ScaleMaxY);
float scalez=(_ScaleMaxZ-_ScaleMinZ)*0.5*clamp(_SinTime.w, -1.0, 1.0)+0.5*(_ScaleMinZ+_ScaleMaxZ);
v.vertex.x *= scalex;
v.vertex.y *= scaley;
v.vertex.z *= scalez;
关于指定方向这一点,比较好理解,就是在属性面板上设定值即可,
那么这个缩放大小的计算公式是怎么得到的呢?
这里是用到了三角函数的相关变换,如果了解y=Asin(ωx+φ)+b的相关知识就比较容易理解,如果我们假设,最小放缩为A,最大放缩为B,那么三角函数公式应该为y = ((B-A)/2) * sinx + (A+B)/2;
这里的sinx对应clamp(_SinTime.w, -1.0, 1.0)。
所有的坐标空间的转换都是基于这三种基础变换,而且这三种基础变换的应用不仅仅在于矩阵变换,它所提供的只是一种坐标变换的思路,其应用可有开发者自行发挥。比如,可以参考这两种文章来体会一下基础变换的简单用法:图像图像的平移和放缩,
二、顶点着色器中的坐标空间介绍
1.模型空间(对象空间/局部空间)
每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转;模型空间的原点和坐标是由美术人员在建模软件里确定好的。当导入到Unity中,我们可以在顶点着色器中访问到模型的顶点坐标,其中包含了每个顶点的坐标。
2.世界空间
世界空间可以被用于描述绝对位置,世界空间的原点放置在游戏空间的中心。
顶点变换的第一步:将顶点坐标从模型空间变换到世界空间中,这个变换通常叫做模型变换(Model Transform)。
3.观察空间(摄像机空间 camera space)
摄像机决定了我们渲染游戏所使用的视角,关键字是视角,摄像机仅仅决定了观察的位置和观察的方向,所以观察变换仅仅需要旋转变换和平移变换即可完成,在观察空间中,摄像机位于原点,同样,其坐标轴的选择可以是任意的。
观察空间是一个三维空间,而屏幕空间是一个二维空间。从观察空间到屏幕空间的转换需要经过一个操作,那就是投影。
顶点变换的第二步,就是将顶点坐标从世界空间到观察空间中,这个变换叫观察变换(View Transform)。
4.裁剪空间(投影矩阵)
投影矩阵:变换到裁剪空间使用的是投影矩阵,投影矩阵的工作并不是真正意义上的投影,它只是在做投影的准备工作,它通过对x,y,z做了不同程度的缩放(z还做了平移);投影其实是一个空间的降维,从四维空间到二维屏幕空间,这一过程在转换到屏幕空间时完成,但是最主要的准备工作就是在裁剪空间的投影矩阵这里;
顶点接下来要从观察空间转换到裁剪空间中,这个用于变换的矩阵叫做裁剪矩阵/投影矩阵。我们都知道相机的观察如同人眼观察一样,它是有视角限制的,就像人眼的最大观察范围大概在120度一样,所以裁剪矩阵就是期望设定一个特定的范围,也就是相机的平截头体,所有不在这个范围内的顶点都要被裁剪掉,最后再将范围内的坐标都变换到-1到1,也就是归一化(NDC)操作。
裁剪空间的目标使能够方便地对渲染图元进行裁剪,完全位于裁剪空间内的图元将会被保留,完全位于空间外的图元将会被剔除,而与这块空间相交的图元就会裁剪,这块空间由视锥体决定。
视锥体是指空间的一块区域,这块区域决定了摄像机可以看到的空间。视锥体由六个平面包围而成,这些平面也被称为裁剪平面。视锥体有两种类型,一种是正交投影,一种是透视投影。
近裁剪平面(near clip plane)和远裁剪屏幕(far clip plane):它们决定了摄像机可以看到的深度范围。
具体的投影矩阵推导过程参考:https://zhuanlan.zhihu.com/p/152280876
5.屏幕空间
经过投影矩阵的变换后就可以进行裁剪工作了,当完成了所有的裁剪工作,就需要真正的投影了。
我们需要把视锥体投影到屏幕空间,经过这一步变换,我们会得到真正的像素位置,而不是虚拟的三维坐标。
首先做齐次除法计算,将四维坐标变换为三维坐标,完成之后所有的坐标都变成了归一化的设备坐标,对于OpenGL来说,所有的坐标都是[-1,1],而不在-1到1之间的坐标会被OpenGL忽视掉,也就是被裁剪掉,这一步才是最终目的,之前所有的计算都是为了将其转换到[-1,1],然后将不在这个空间内的顶点坐标由OpenGL自动忽略;从裁剪空间到屏幕空间的变换为视口变换,变换矩阵为视口变换矩阵,它与用于显示画面的屏幕分辨率有很大关系。
6.unity内置的变换矩阵
三、顶点着色器内的矩阵变换
上面我们直接是通过一些参数对顶点进行了一些操作,然而在unity的顶点着色器中,我们通常会看到这个函数:
UnityObjectToClipPos();
从字面上容易看出,这是讲顶点从模型空间转换到裁剪空间的一个函数,这个函数也就是顶点着色器需要做的一个最重要的工作。它的转换就是通过转换矩阵来实现的,至于这个函数是如何实现这个转换的功能的,可以参考这两篇文章:
第二篇文章里详细分析了为什么矩阵变换的顺序必须要按照缩放、旋转和平移的顺序,如果按照其它顺序都会出错,理解这一点对以后使用矩阵进行各种其它坐标空间的转换非常有帮助。