虽然所知甚少,但康大的《GPU编程与Cg编程之阳春白雪下里巴人》确实带我入了shader的门,在里面我第一次清晰地知道了“语义”的意思,非常感谢。
入门shader,我觉得可以先读3本书:《GPU编程与Cg编程之阳春白雪下里巴人》=》《cg教程》=》《Real-Time Rendering 3rd》(在读,最近忙,搁下了),打下理论基础。
下面是《cg教程》的读书笔记。
1.基本cg函数
1)数学函数:abs,acos反余弦,all(x)x的所有分量都不为0则为true,any(x)x有分量不为0则为true,ceil/floor,clamp(x, a,b),cross(A,B)/dot(A,B)叉乘与点乘,degrees/radians弧度角度互转,exp(x)e的x次方,exp2(x)2的x次方,fmod(x, y) x/y的余数(符号同x),frac取小数部分,lerp(a, b, f)f=0取a,f=1取b,否则权重混合,log/log2/log10基于e/2/10的对数,pow,round,sign(x)x>0返回1,否则0,saturate(x)把x限制到[0,1]之间,smoothstep(min, max, x)x<=min返回0,x>=max返回1,否则返回一个(0,1)的平滑值,step(a, b)a<=b返回1,否则0,mul矩阵相乘/向量矩阵相乘,sincos,transpose转置,noise噪声函数,lit(NdotL, NdotH, m)计算环境光系数x,漫反射系数y,高光系数z, w=1
2)几何函数:distance点和点的欧氏距离,length向量的欧氏长度,normalize归1化,reflect(I, N)反射向量,可以根据pt-eyepos得到入射反向,然后算出反射向量,然后采样环境贴图,混合当前颜色和环境颜色,就可以在物体表面反射环境了,refract(I, N, eta)折射向量,可以根据pt-eyepos得到入射反向,然后算出折射向量,然后采用环境贴图并根据透明度插值物体颜色和环境颜色,然后就可以“透过物体”看到周围景色了
3)纹理采样函数:1维tex1D,2维tex2D,3维tex3D和立方贴图采样texCUBE
2.cg程序
opengl/dx的渲染管线和通常说的gpu渲染管线一样,我猜测其中的关系是:gpu里嵌入了opengl/dx库,来完成其渲染管线,所以其实是一个东西。
那cg程序呢,cg程序一开始是文本形式,应用程序通过cg编译器把它翻译成中间形式,然后通过cg核心运行库把中间形式搞成最终形式,这个最终形式被运行库送入gpu中执行。cg也是这样嵌入应用程序的:应用程序使用cg编译器编译cg程序,然后调用cg核心运行库和cgGL/cgDXD库把程序送给驱动程序即可,不用和gpu直接打交道。
下面是使用opengl接口的cg程序文本到gpu执行过程:
cg程序是动态编译的,即应用程序可以动态运行上图 流程,更新在gpu里的cg最终形式,gpu会对每个输入的顶点执行上面的最终形式程序,即并行计算,片段类似。
cg程序编译需要cg程序外,还需指定profile(顶点程序指定一个,片段程序指定一个),一个Cg profile定义了一个“被特定图形硬件或API所支持的Cg语言子集”,profile决定了你的cg程序能使用哪些cg功能,如果使用了不支持的功能就编译不通过,也决定了编译后的程序能在哪些硬件上运行,硬件不支持则不能运行。
cg编译错误包括传统的错误和依赖profile的错误,前者就是语法错误,后者是cg程序使用了profile不支持的功能,后者或者可以考虑使用更高级的profile解决。
3.顶点着色器
Attributes:由 vertext array 提供的顶点数据,如空间位置,法向量,纹理坐标以及顶点颜色,它是针对每一个顶点的数据。属性只在顶点着色器中才有,片元着色器中没有属性。属性可以理解为针对每一个顶点的输入数据。OpenGL ES 2.0 规定了所有实现应该支持的最大属性个数不能少于 8 个。
Uniforms:uniforms保存由应用程序传递给着色器的只读常量数据。在顶点着色器中,这些数据通常是变换矩阵,光照参数,颜色等。由 uniform 修饰符修饰的变量属于全局变量,该全局性对顶点着色器与片元着色器均可见,也就是说,这两个着色器如果被连接到同一个应用程序中,它们共享同一份 uniform 全局变量集。因此如果在这两个着色器中都声明了同名的 uniform 变量,要保证这对同名变量完全相同:同名+同类型,因为它们实际是同一个变量。此外,uniform 变量存储在常量存储区,因此限制了 uniform 变量的个数,OpenGL ES 2.0 也规定了所有实现应该支持的最大顶点着色器 uniform 变量个数不能少于 128 个,最大的片元着色器 uniform 变量个数不能少于 16 个。
uniforms变量只是初始值和其他变量不同,它是允许被改变的(除非被const修饰),和其他变量一样。在unity的shader中,除了上述数据算uniforms外,开放出来的属性应该也算uniforms,比如经常有的纹理贴图等
Samplers:一种特殊的 uniform,用于呈现纹理。sampler 可用于顶点着色器和片元着色器。
顶点着色器的输出:
Varying:varying 变量用于存储顶点着色器的输出数据,当然也存储片元着色器的输入数据,varying 变量最终会在光栅化处理阶段被线性插值。顶点着色器如果声明了 varying 变量,它必须被传递到片元着色器中才能进一步传递到下一阶段,因此顶点着色器中声明的 varying 变量都应在片元着色器中重新声明同名同类型的 varying 变量。OpenGL ES 2.0 也规定了所有实现应该支持的最大 varying 变量个数不能少于 8 个。
在顶点着色器阶段至少应输出位置信息-即内建变量:gl_Position,其它两个可选的变量为:gl_FrontFacing 和 gl_PointSize。
4.片段着色器,类似顶点着色器。
5.顶点变换
投影变换是把眼空间变换到裁剪空间=》平截体四个角对应正方体四个角,各个点都右乘此矩阵,不在正方体内的剔除(-w<x<w,yz同),此时正方体不是单位正方体;
接着,透视除法,映射到单位正方体中,然后根据各个视口尺寸(相机尺寸)映射到视口对应位置【最后这个变换了解甚少,存疑】,这里还有个问题,裁剪是在裁剪空间搞的,还是在单位正方体中搞的?其实这个问题的答案未必重要,因为可能只是不同做法而已。我在一个前辈实现的光栅器代码中看到,他是在-w,w,w中剪切的,切完后进行透视除法,然后窗口变换,完成顶点变换。
6.应用程序传入的数据,也即shader能读到的数据(如果模型提供,比如法线),unityshader中应该有对应的全局变量或顶点分别数据
7.粒子系统,所谓把顶点视作粒子,然后控制粒子的位置、大小和颜色,可是正常而已,顶点是木有大小的,书中这点木有给出具体做法;粒子图是什么?是怎么使用的?疑惑。
8.顶点混合,和蒙皮一起理解,顶点就是皮肤网格上的点,矩阵就是骨头集,我们有基本姿势:基本姿势的矩阵集(全是单位矩阵),顶点集,矩阵集中各个矩阵对各个顶点的影响权重(一般不超过4个矩阵影响一个顶点)和 其他姿势信息:对应基本姿势矩阵集的新矩阵集,这个矩阵集很可能大部分和基本姿势对应的一样---单位矩阵。这样我们就能够表达多种动作了,矩阵集里的矩阵物理意义是变换:从基础姿势的位置变换到其他姿势的位置的变换。书中的那一段代码值得研究,有2个点需要思考:
为何model是3x4,即为何不是正常变换的4x4?估计:model在这里是用来计算顶点变换后的位置的,只需要xyz,而model的第4行只影响pos的第4个变量,所以可以略去,其实说白了,本来变换搞成4x4也是为了方便矩阵,这里抛弃无用信息而已。
为何只取model得一个角3x3去变换法线向量?首先因为是仿射变换所以用model而不是其逆的转置,再者,4x4的矩阵变换3维向量是可以直接用其3x3部分左乘向量的,因为:
| a1, a2, a3, a4|
| b1, b2, b3, b4|
| c1, c2, c3, c4|
| d1, d2,d3, d4|
向量normal扩展成4维后为(normal, 0),结果为4x1向量,此向量的前3维只受左上角3x3*normal影响,而第4维只是辅助计算,直接干掉,证毕。此结论可用于变换法线向量或其他向量。记住不要混淆了为什么选model和为什么选model的左上角的3x3!前者是选变换矩阵,后者是删繁就简直取本质。
环境映射
9.立方贴图纹理
新的GPU支持立方贴图纹理(texCUBE可采样之),一个立方贴图是由6幅纹理组成的立方体,对其采样就像从中心发射的光线,穿透立方体的点即为采样所得,相当于在中心向外看所看到的颜色,一般用于环境映射。
生成立方贴图:在场景中心放一个摄像机,朝左、右、上、下、前、后拍一张照片即可,摄像机在每个朝向的放置都是左右成90度,上下成90度,如下图:
这样,6幅图紧凑在一起就能完全展示四周环境数据了。
R = reflect(N, I),I是指向顶点的入射光线,返回值是从顶点发出的反射光线,N需要归一化,但I不需要,并且返回值R去查询纹理时也不需要归一化,因为I的长度不影响reflect返回R的方向,而R的长度不影响采样结果,并且,R在片段程序中采样之前先被光栅器插值,不归一化的R更能准确插值(有点像球面插值和椭球体面插值,为什么后者更准确就没研究了)。
环境映射只依赖方向而不依赖位置,所以方向相同位置不同得到的采样结果是一样的,要想展现其斑驳之美,最好用于曲面。
环境贴图通常需要在用世界坐标系的向量来采样,所以我们必须把N和I转到世界坐标系后reflect,【当然也可以先在模型坐标系reflect,再把reflect的结果转到世界坐标系,未测试】,向量变换的方法参考上面第8点。
为什么用3x3去左乘,看上面第8点的证明就好。另外,这里我们假设position.w = 1(这也是正常情况下的取值),如果其w分量被特殊处理过导致不为1,我们需要先把其所有分量都除以w,来使得position.w=1,方便后面只进行3维的计算(默认w=1)
10.控制贴图
用纹理去控制可变参数,这种想法被此书所推崇,不知是因其新颖还是确实是好,没研究过shader采样纹理和多纹理采样的性能。不过这种做法对美术而言确实好一些,比如环境贴图的例子,加一个控制纹理,对每个片段都给一个“反射系数”,然后就可以控制各点反射的效果了,这些系数就存在控制贴图中。其实法线贴图也算一种控制贴图:控制法向量。
11.折射,透过物体看环境:从视点到顶点发出光线,经物体折射后(只算进入物体的折射),采样环境贴图,done。
refract(I, N, etaRatio);类似reflect,N需要归一化I不需要。
12.菲涅尔效果:进入眼睛的有反射光和折射光,书中给出了计算2者权重的近似公式:
最终顶点的色值是:
基本只是提供了一个更加准确的混合参数,比控制贴图、全局变量等更方便,但如果想应用开来,比如看水里的鱼等真实折射呢?书中木有提,难道要用光线跟踪,看折射的光线被什么反射了?疑惑。
带颜色色散的菲涅尔效果:在顶点程序中,计算反射向量,计算菲涅尔系数,分别计算RGB3个分量对应的折射向量(因为折射系数不同),在片段程序中先得到反射的环境色值,然后得到RGB3个折射向量的色值,把这3个色值分别取RGB分量构成最终的折射色值,然后用菲涅尔系数混合反射色值和折射色值,done.
13.凹凸贴图。
已成此文:深入理解法线贴图
14.性能方面,散入此文:写shader的一些小细节
15.附录
技巧(Technique),“每个技巧描述一种实现效果,一个cg程序中可以有多个技巧,每个技巧针对一种级别的图形处理器”,所以这里的技巧类同于Unity shader的subshader,一般地unity shader都是一个或多个subshader+一个fallback,2+个技巧,以适应不同硬件。执行shader时,从第一个subshader开始,硬件不符合就执行下一个subshader,直到符合。
过程(Pass),“一个技巧可以包括一个或多个过程,一个过程代表一个单独的渲染过程,其包括一组渲染状态(一些额外设置)和一组着色器,例如过程0可以只设置深度值,以便后面过程使用等”,这里的过程就是unity shader里的pass了,执行subshader时,有时候只用其中一个pass渲染,有时按顺序逐个应用各个pass渲染。默认好像是后者,存疑。