一、坐标系相关
shader中会有几种空间:
模型空间:有建模决定的,一般情况下以物体自己为中心原点,单位是unity的坐标单位
世界空间:就是unity的世界坐标
观察空间(视图空间):以相机为中心的坐标系
裁剪空间:是一个4d空间,有x,y,z,w分量,x,y,z的范围,都是要在-w到w之间,否则会被裁剪。远处的物体的w值更大,靠近摄像机的w值更小.
NDC空间(归一化设备空间):这个是一个三维空间,有x,y,z分量。在对裁剪空间的x,y,z分别除以w,得到一个[-1,1]之间的坐标范围,这个归一化坐标就是NDC坐标
为什么要有这个w值呢?我们需要实现透视效果,即近大远小,实现的逻辑是将x,y,z分别除以w,在得到NDC空间
因为靠近相机的坐标w值更小,除以w后更大,坐标之间的差距越大,绘制出来的图像看起来越大;
而远离摄像机的坐标w值更大,除以w后更小,坐标之间的差距越小,也就是汇聚于一点,这就使得物体看起来近大远小
二、坐标系在shader中的变化
在顶点着色器vert中,使用的是模型空间,在传入片元着色器之前,必须要先将这个模型坐标转换成裁剪空间下的坐标
这个转换过程是必须的,那么还要自己写转换,而不是shader自动做转换呢?因为我们可能会实现一些自定义效果,比如水面的波纹等效果
总结下坐标空间的通常用法如下:
模型空间:进入顶点着色器时传入的顶点坐标
世界空间:光照的坐标、光照的法线
裁剪空间:进入片元着色器时传入的坐标
三、unity shader语法
1.Shader代码结构
写shader时,先要有
Shader "NewMyShader"
{
}
上面这一段,表示的是声明一个shader文件,然后
Shader "NewMyShader"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1)
}
}
上面这一段,表示我要声明一个可以在unity中编辑的变量,名字叫"Color",他的类型是Color,他在shader中的名字是_Color
之后
Shader "NewMyShader"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
}
}
}
上面这一段,SubShader表示对某个显卡的支持,可以写多个SubShader,显卡会选择一个能用的执行
Pass表示一次渲染流程,一个SubShader可以有多个Pass,他们会进行渲染叠加
Shader "NewMyShader"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1)
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
上面这一段,CGPROGRAM和ENDCG是一对的,表示使用HLSL,HLSL是Direct3D的语言(unity默认使用Direct3D),openGL使用的语言是GLSL
#pragma表示预编译指令,这里他的意思是告诉GPU说使用vert函数作为顶点着色器,frag函数作为片元着色器,并非定义一个函数。这个预编译指令是必不可少的!如果没有则会找不到怎么渲染,导致渲染错误出现粉色
注意:#pragma和函数定义,是写在CGPROGAM里的
2.语法
strcut appdata
{
float4 pos : POSITION;
float2 uv : TEXTOORD0;
}
float4 vert(appdata data) : POSITION
{
return UnityObjectToClipPos(data.pos);
}
float4 frag(v2f v) : SV_Target
{
return float4(0, 0, 1, 1);
}
这一段是vert函数将顶点坐标转为裁剪坐标,其中vert的参数,可以自己定义需要哪些参数,参数写少了也不会报错。这里我要的是appdata类型的,他是一个struct
变量后面的 : POSITION表示的是语义,说明这个pos表示的是一个坐标
vert后面的 :POSITION也是,因为这里只返回了float4,所以需要给float4加上语义,如果返回一个struct,则不用添加语义
我们都知道顶点着色器的核心职责就是将模型坐标转换为裁剪坐标,所以这里vert只有一行代码就是转为裁剪坐标然后返回给frag
frag的返回值是一个渲染目标,一般都是float4,一个颜色
3.vert函数
vert是顶点着色器,他的职责就是要将顶点的模型坐标转为裁剪坐标。
vert函数可能会被调用多次,比如说cube虽然只有8个顶点,但是他有6个面,每个面4个顶点,所以最多是6*4=24次调用。因为虽然一个顶点会被3个面共享,但是这3个面的法线等参数都不一样,所以可能会被调用。不过不同的设备会进行优化,所以可能不会有24次顶点着色器的调用
4.frag函数
frag函数的传入参数,是由vert返回的,他的职责就是纹理采样,并且计算像素颜色。但是最终的渲染操作不是由frag执行的,而是后面的逐像素操作。
frag函数的返回值,一般都是一个渲染目标SV_Target,就是一个color
疑问:为什么经过frag函数计算后的颜色,呈现出来的效果是渐变颜色?因为在片元着色器的前一阶段:三角形遍历中,会将片元的深度做插值运算,这会导致最终的颜色是渐变的
四、各种语义对应什么
POSITION:模型坐标
SV_POSITION:裁剪空间下的裁剪坐标
SV_TARGET:渲染目标
五、Shader计算过程
1.float之间的乘法,遵循分量相乘,比如float4 f = (1,1,1,1) float4 f2 = (2,2,2,2) 结果是(12, 12, 12, 12)。不同阶之间不可计算,float4*float3会报错!
2.shader的unity_WorldToObject和unity_ObjectToWorld互为逆矩阵,他们相乘是单位矩阵
3.mul函数 这个函数有很多坑!
首先要知道,任何情况下mul函数的返回值都是一个列向量
mul函数他的官方提示是:float4 mul(float4x4 M, float4 v),也就是说正常情况下是矩阵在第一个参数,向量放在第二个参数。但是我们老是看到有一种写法是向量在左,矩阵在右,但是他们的结果其实确是完全相等的!这与数学中的矩阵相乘规则都不符!
其实mul函数不是单纯的数学中的两个矩阵相乘,而是做了很多处理。
当矩阵在左,向量在右时,是标准的数学的矩阵相乘,即向量为列向量;
而矩阵在右,向量在左时,出现问题了,如果是列向量,那么向量与矩阵根本没法相乘。所以这时向量被自动转为了行向量,但是这时再与矩阵相乘,就没法变为列向量了,只是行向量。所以这时mul函数做了处理,将右矩阵自动转为转置矩阵,此时再进行计算,结果才是相等的。
结论:两者计算结果通常都是一致的,但是我们都是将矩阵放在左边,向量放在右边减少不必要的计算
六、渲染相关
1.shader的环境光照:shader中的环境光照UNITY_LIGHTMODEL_AMBIENT.rgb,并非物体多次反射后的光照,而是一个全局的固定颜色的光照,任何角度都是这个光照,主要是为了保证性能
2.关于颜色的相乘和相加:
颜色相乘表示混合、过滤,并不会改变光照强度,比如红色×蓝色=紫色,绿×红=黄
颜色相加表示叠加,会改变光照强度,越叠加越接近白色(1,1,1,1)
同个颜色相乘表示提高对比度,让暗的地方更暗,亮的地方更亮。比如:0.20.2=0.04(更暗了),0.90.9=0.81(相对较亮)。所以我们经常会看见高光时pow(saturate(dot(-ref, viewDir)), _qiangdu),这个_qiangdu就是相当于对比度强度,会让光线更集中。原先的高亮是一大片区域,增加_qiangdu的参数,就可以让光线集中到一个点上。
3.法线纹理(法线贴图)
- 在一些情况下,我们需要存储一个模型的全部法线信息以便纹理采样,那么我们可以将这些法线信息存储到一张图片中,这张图片就是模型的法线纹理。里面的像素的rgb信息,就是法线的映射。
- 但是我们知道,法线方向的范围是[-1, 1],而rgb的范围是[0, 1],我们需要建立一种映射关系,可以使用公式:pixel=(normal+1)/2 这样就可以将法线映射到rgb上了,反过来,通过rgb,也就可以得到法线。
- 法线贴图有两种,一种是在模型空间下的法线纹理,一种是在切线空间下的法线纹理。我们先说什么是切线空间,切线空间就是以模型顶点为原点,法线方向就是z轴方向,x是切线方向,y是垂直于法线和切线的,如图1
[图1] - 我们再接着说两个空间有什么区别,在模型空间下的法线纹理,他们都是同一个空间,所以法线方向会有各种各样的坐标,比如(0, 0.6, 1),他映射到法线纹理,rgb变成了(0.5, 0.8, 1),是浅蓝色。所以这样会导致法线纹理是五颜六色的,如图2
[图2] - 我们再看切线空间下的法线纹理:
原点是模型顶点
z轴是模型的原始法线方向
x轴是原始法线的切线方向
y轴是x轴和z轴做叉乘得到的
他在切线空间下的坐标,表示的是这个法线在顶点的原始法线的偏移方向。也就是说他是一个相对位置。越接近z轴,说明这个法线与原始法线越靠近,如果是(0,0,z),则是完全重合
假如法线在切线空间下的位置是(0.2, 0.1, 0.8),那么转换成rgb就是(0.6, 0.5, 0.9),对应颜色是蓝紫色。因为z轴方向是顶点的原始法线方向,而rgb中(0,0,1)是纯蓝色,所以切线空间下的法线纹理始终是偏蓝色,只不过蓝色的深浅不一样,如图3
[图3] - 问:我们知道法线纹理的uv坐标对应的就是模型顶点的坐标,但是uv是二维坐标,而顶点是三维坐标,他们是怎么对应上的?
答:可以通过uv展开的方法来将三维坐标映射到二维坐标上,在vert函数中会一起传入顶点对应的uv坐标。
问:模型空间下的法线贴图与切线空间下的法线贴图有什么最大区别?
答:我们知道切线空间是以顶点的原始法线方向建立的,在这个坐标系中的向量,不跟任何模型关联,只是一个相对与原始的位置,所以这一张贴图可以用在任何模型上;而模型空间下的法线贴图,是一个绝对位置,每一个颜色对应的都是这个模型的固定法线,所以只能应用在这个模型上,切换模型则会出现光照错误。
七、数学计算
1.投影
①
shader计算中经常会需要计算投影,投影可以基于点乘
a·b=|a||b|cos(θ) 假如此时a是单位向量,那么就变成了
a·b=|b|cos(θ) 这就是b在a方向上的投影长度,可得b在a上的投影向量就是a(a·b)
注意:只有在a是单位向量的情况下才能算是投影,否则只是投影长度乘了a倍
②
a·b和b·a数值是完全一致的,任意调换位置不会影响长度,但是他们表达的意思其实不一样
a·b表示:a在b方向上的投影×|b|;b·a表示:b在a方向上的投影×|a|
③
如果a和b都不是单位向量怎么办?假如是a在b上的投影,那么可以将b转为单位向量
dot(a,normalize(b)),这样就可以得出a在b方向上的投影长度
简单的技巧:想投影到哪个向量上,就把哪个向量归一化
2. 矩阵计算
矩阵相乘的条件:左矩阵的列数=右矩阵的行数
相乘后的矩阵:行数=左矩阵行数,列数=右矩阵列数
其实也就是说:
列向量计算时,只能放在右边,乘完还是列向量
行向量计算时,只能放在左边,乘完还是行向量
八、易踩坑点
- shader中涉及到的方向,基本都是默认单位向量,很多书和文章中都不会明确告知,但是他是默认的,比如法线方向、光照方向、视角方向,因为这些都涉及到公式计算,但是某些手动算出来的方向,是要看情况的,比如a-b,得到的向量得看情况要不要归一化
- shader中的reflect(i, n),这里的i应该传入表面指向光照的方向,而不是直接传光照方向,一般用法都是
//反射方向
fixed3 reflectDir = normalize(reflect(-worldLightDir, worldNormal));
注意是-worldLightDir,如果哪次计算出来发现光照反了,那就是这里传错了