unity shader 相关记录
https://www.geogebra.org/graphing (推荐)
不支持复合公式, 报错
https://www.desmos.com/calculator (推荐)
支持复合公式
http://fooplot.com/?lang=zh_hans
https://zh.numberempire.com/graphingcalculator.php
特效 及其 shader 研究
Unity Shader - 这个人的专栏不错 , 很多 后处理 案例:
https://blog.csdn.net/puppet_master/column/info/12790
卡通渲染: http://sorumi.xyz/posts/unity-toon-shader/
均值模糊
高斯模糊
Unity Shader-后处理:高斯模糊 - https://blog.csdn.net/puppet_master/article/details/52783179
迭代次数越大, 批次越大. 也可以做 毛玻璃 效果 (磨砂)
learnopengl-cn - https://learnopengl-cn.readthedocs.io/zh/latest/05 Advanced Lighting/07 Bloom/#_3
景深
径向模糊
运动模糊
屏幕扭曲
UnityShader-BilateralFilter(双边滤波,磨皮滤镜)- https://blog.csdn.net/puppet_master/article/details/83066572
Unity Shader-Decal贴花 (Projector原理) - https://blog.csdn.net/puppet_master/article/details/84310361
描边
透视
体积光
残影效果实现
Unity角色残影特效 - https://blog.csdn.net/qq992817263/article/details/52994907
这里只针对SkinnedMeshRenderer的网格(也就是带蒙皮的网格)残影,主要原理是根据设定的间隔时间连续的截取当前SkinnedMeshRenderer的网格数据并使用Graphics.DrawMesh画出网格
图形学研究: http://blog.csdn.net/poem_qianmo
下图显示了渲染管线中各个阶段主要完成的工作,蓝色部分代表的是我们可以定义自己的着色器。
在上图中,我们以数组的形式传递3个3D坐标作为渲染管线的输入,用它来表示一个三角形,这个数组叫做顶点数据(Vertex Data);这里顶点数据是几个顶点的集合。每个顶点是用顶点属性(vertex attributes)表示的,它可以包含任何我们希望用的数据,下面我们来看看渲染管线中各个阶段主要完成的工作:
虽然渲染管线有多个阶段,每个阶段都需要对应的着色器,但其实对于大多数场合,我们必须做的只是顶点和像素着色器,几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。现在的OpenGL中,我们必须定义至少一个顶点着色器和一个像素着色器(因为GPU中没有默认的顶点/像素着色器)。
Unity把每一frame绘制的事件进行了拆分,然后在其中定义了一些点,在这些点处,可以通过command buffer嵌入一些事件(比如设置RT,绘制一些物件等)。比如在延迟渲染中,可以当G-Buffer中绘制完毕后,往里面绘制一些额外物件。通过下图可以看到Unty的渲染顺序,并可以清晰的看到在绿色点标记的地方可以嵌入command buffer去执行自定义的命令:
渲染模式总共有四种:
渲染模式 | 意思 | 适用对象举例 | 说明 |
---|---|---|---|
Opaque | 不透明 | 石头 | 适用于所有的不透明的物体 |
Cutout | 镂空 | 破布 | 透明度不是0%就是100%,不存在半透明的区域。 |
Fade | 隐现 | 物体隐去 | 与Transparent的区别为高光反射会随着透明度而消失。 |
Transparent | 透明 | 玻璃 | 适用于像彩色玻璃一样的半透明物体,高光反射不会随透明而消失。 |
以法线为例, 一般会以某个空间为前缀.
// 切线空间下的法线
fixed3 tangentNormal = UnpackNormal(packedNormal);
// 世界空间下的法线
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
unity 内置的 Unlit-Texture,渲染类型 Opaque (不透明物体),所以不用开 Blend,默认也是关闭了混合( Blend Off)
Alpha混合 http://blog.sina.com.cn/s/blog_471132920101d8z5.html
Alpha Test和Alpha Blending http://blog.csdn.net/candycat1992/article/details/41599167
总结一下,就是使用Alpha Test看似更简单,但其实在大多数平台上,相比与Alpha Blending,只有一点小小的性能提升。但是!!!在iOS和某些Android设备上,由于它们使用了PowerVR GPUs,因此Alpha Test的性能消耗反而会更大。因此,一个忠告就是,**尽可能使用Alpha Blending,而不要使用 **Alpha Test。
效果:http://blog.csdn.net/wdsdsdsds/article/details/52535352
透明度测试与透明度混合详解 : https://blog.csdn.net/u013354943/article/details/52797568
Blend Off 不混合
Blend SrcFactor DstFactor SrcFactor是源系数,DstFactor是目标系数
最终颜色 = (Shader计算出的点颜色值 * 源系数)+(点累积颜色 * 目标系数)
值 | 解释 |
---|---|
one | 1 |
zero | 0 |
SrcColor | 源的RGB值,例如(0.5,0.4,1) |
SrcAlpha | 源的A值, 例如0.6 |
DstColor | 混合目标的RGB值例如(0.5,0.4,1) |
DstAlpha | 混合目标的A值例如0.6 |
OneMinusSrcColor | (1,1,1) - SrcColor |
OneMinusSrcAlpha | 1- SrcAlpha |
OneMinusDstColor | (1,1,1) - DstColor |
OneMinusDstAlpha | 1- DstAlpha |
(注:r,g,b,a,x,y,z取值范围为[0,1])
计算方式 |
---|
(r,g,) * a = (ra, ga, b*a) |
(r,g,) * (x,y,z) = (rx, gy, b*z) |
(r,g,) + (x,y,z) = (r+x, g+y, b+z) |
(r,g,) - (x,y,z) = (r-x, g-y, b-z) |
源颜色 为 该次绘制物体颜色, 目标颜色 为 缓冲区颜色
Blend SrcAlpha OneMinusSrcAlpha
最终颜色 = 源颜色 * 源透明值 + 目标颜色*(1 - 源透明值)
结论:贴图alpha值越大,颜色越偏向贴图;alpha值越小,颜色越偏向混合目标
http://ios.jobbole.com/89931/
在线 矩阵计算器 :http://www.yunsuanzi.com/matrixcomputations/solvematrixmultiplication.html
A的列 一定要等于 B的行 ( 不能使用交换律 )
http://www.mashangxue123.com/线性代数/2077705030.html
先来看一下正交矩阵是如何定义的,若方阵M是正交的,则当且仅当M与他的转置矩阵M^T的乘积等于单位矩阵,那么就称矩阵M为正交矩阵.
M T M = I M^{T} M=I MTM=I
在矩阵的逆中我们知道,矩阵的逆和矩阵的乘积为单位矩阵I,由此推理,我们可以知道,如果该矩阵为正交矩阵,那么矩阵的逆和转置矩阵是相等的.
M T = M − 1 M^{T}=M^{-1} MT=M−1
那么正交矩阵存在的意义是什么呢?其实如果一个矩阵是正交矩阵,那么矩阵的逆和转置矩阵是相等的.转置矩阵是非常简单计算的,而计算矩阵的逆如果使用代数余子式计算是非常的麻烦,所以我们可以直接计算转置矩阵然后直接得到该矩阵的逆.
底下是一些重要的性质:
在矩阵的乘法中,有一种矩阵起着特殊的作用,如同数的乘法中的1,这种矩阵被称为单位矩阵。它是个方阵,从左上角到右下角的对角线(称为主对角线)上的元素均为1。除此以外全都为0。
I 1 = [ 1 ] , I 2 = [ 1 0 0 1 ] , I 3 = [ 1 0 0 0 1 0 0 0 1 ] , ⋯   , I n = [ 1 0 ⋯ 0 0 1 ⋯ 0 ⋮ ⋮ ⋱ ⋮ 0 0 ⋯ 1 ] I_{1}=[1], I_{2}=\left[ \begin{array}{cc}{1} & {0} \\ {0} & {1}\end{array}\right], I_{3}=\left[ \begin{array}{ccc}{1} & {0} & {0} \\ {0} & {1} & {0} \\ {0} & {0} & {1}\end{array}\right], \cdots, I_{n}=\left[ \begin{array}{cccc}{1} & {0} & {\cdots} & {0} \\ {0} & {1} & {\cdots} & {0} \\ {\vdots} & {\vdots} & {\ddots} & {\vdots} \\ {0} & {0} & {\cdots} & {1}\end{array}\right] I1=[1],I2=[1001],I3=⎣⎡100010001⎦⎤,⋯,In=⎣⎢⎢⎢⎡10⋮001⋮0⋯⋯⋱⋯00⋮1⎦⎥⎥⎥⎤
但事实上,一个坐标系能用任意3个基向量定义,当然这三个基向量要线性无关(也就是不在同一平面上),
如果这三个基向量(归一化单位向量) 相互垂直 ,那么构成的矩阵是一个正交矩阵
正交矩阵 求 逆矩阵
// 求 TBN 矩阵的逆矩阵,因为 TBN 矩阵由三个互相垂直的单位向量组成,所以它是一个正交矩阵 // 正如前面所说,正交矩阵的逆矩阵等于它的转置,所以无需真的求逆矩阵
Unity Shader-非主流纹理采样研究(流光,溶解,隐身效果)
Unity Shader-热空气扭曲效果 (GrabPass 抓屏)
Unity Shader-遮挡处理(X-Ray,遮挡描边,遮挡半透,遮挡溶解)
复古风格
unity-shader-切线空间.md
使用 法线贴图 是因为在 低模 下想获得 高模 凹凸表面光照效果。(也就是面数不够,法线来凑,基于面的法线建立一个 虚拟坐标系 ,通过 法线贴图 在面上 加多点法线 )
由于需要将同一份法线纹理贴到不同模型的表面,或者同一个模型中不同角度的表面,那么当初生成这份法线纹理时所使用的面元角度(所谓模型local坐标系),就无法适用在其他角度的面元上。所以,人们不记录当时用模型坐标系生成的法线,而使用z轴与该点所在面元的法线平行的一个虚拟的相对坐标系来记录该点法线在该坐标系中的x/y/z的值,相当于记录了该点法线与所在平面法线之间的相对关系,而不是记录绝对坐标。由于点的法向量基本不会偏移面法向量太多,也就是凹凸程度一般不会太夸张,所以最终的法向量值中,z的值总是比x和y的大一些(也就是说 点法向量 与平面的 的夹角一般会大于45度),用(法向量值+1)/2转换为RGB后,就成了蓝色为主的色调了。
经过这样记录下来的法向量,贴到哪个模型表面,就按照当前表面的法向量,计算出该点法向量在世界坐标空间中的值,然后计算光照就可以了。
但是有个问题,看下图,竖轴同样是面法向量,但是有多种不同的x和y轴组合,每种组合生成的点法向量是不一致的,所以需要规定一套固定的x和y轴,大家遵守同样的规则。怎么规定呢?就用纹理的uv坐标来定。具体做法是,取该点所在的三角形的三个顶点P1.P2.P3的纹理U和V坐标,然后x轴的方向就是P3指向P1,又称T轴;y轴方向是P3指向P2,又称B轴。 (完)
参考资料
参考:https://blog.csdn.net/u013354943/article/details/52779991
http://www.opengl-tutorial.org/intermediate-tutorials/tutorial-13-normal-mapping/
法线纹理(Normal Mapping)的实现细节 https://blog.csdn.net/candycat1992/article/details/41605257
unity中 法线纹理 设置
参考 《Unity Shader入门精要》 4.8.1 和 4.9.2
v2f vert(a2v v) {
v2f o;
// Transform the vertex from object space to projection space
// 转换顶点 从 模型空间 到 世界空间
o.pos = UnityObjectToClipPos(v.vertex);
// Transform the normal from object space to world space
// 转换法线 从 模型空间 到 世界空间
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
// o.worldNormal = UnityObjectToWorldNormal(v.normal);
// o.worldNormal = mul((float3x3)IT_Object2World, v.normal);
// 法线是向量, 所以和顶点变换有所不同. 可以参考 : https://forum.unity.com/threads/_object2world-or-unity_matrix_it_mv.112446/
// 转换法线 从 世界空间 到 模型空间
float3 objNormal = mul((float3x3)unity_WorldToObject, o.worldNormal)
// objNormal 等价与 v.normal
// Transform the vertex from object spacet to world space
// 转换顶点 从 模型空间 到 世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 另一种变换 法线 从 模型空间 到 世界空间,其实就是 mul(v.normal, (float3x3)unity_WorldToObject); 的运算展开
// o.worldNormal = normalize(unity_WorldToObject[0].xyz * v.normal.x + unity_WorldToObject[1].xyz * v.normal.y + unity_WorldToObject[2].xyz * v.normal.z);
return o;
}
以变换 顶点 从 模型空间 -> 剪裁空间 为例. 以下三种方式等价
v2f o;
// 方式一
// o.pos = UnityObjectToClipPos (v.vertex);
// 方式二
// float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
// o.pos = mul(UNITY_MATRIX_VP, worldPos);
// 方式三
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
o.pos.x = mul(UNITY_MATRIX_VP[0], worldPos);
o.pos.y = mul(UNITY_MATRIX_VP[1], worldPos);
o.pos.z = mul(UNITY_MATRIX_VP[2], worldPos);
o.pos.w = mul(UNITY_MATRIX_VP[3], worldPos);
顶点 变换, 要用 float4 类型, 四个分量. 因为使用到平移, 用的矩阵是 float4x4.
法线 变换, 用 float3 类型, 三个分量, 因为法线是向量, 不存在平移的概念, 所以用的矩阵是 float3x3.
OpenGL希望在所有顶点着色器运行后,所有我们可见的顶点都变为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标转换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),再将他们转换为屏幕上的二维坐标或像素。
将坐标转换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步,也就是类似于流水线那样子,实现的,在流水线里面我们在将对象转换到屏幕空间之前会先将其转换到多个坐标系统(Coordinate System)。将对象的坐标转换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中进行一些操作或运算更加方便和容易,这一点很快将会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
这些就是我们将所有顶点转换为片段之前,顶点需要处于的不同的状态。
标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴, z轴有深度转化而来):
后处理中的ndc坐标构建
//使用宏和纹理坐标对深度纹理进行采样,得到深度值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
//构建当前像素的NDC坐标,xy坐标由像素的纹理坐标映射而来,z坐标由深度值d映射而来
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1); // 将屏幕坐标(0, 1) 转到ndc坐标(-1, 1)
// gl 的 Zndc 是 -1~1 之间, 所以 d*2-1
// dx 的 Zndc 是 0~1 之间, 所以直接用 d 即可
这里面包含了两个步骤,将 view坐标系 下的顶点乘以透视矩阵,转换到 Clip坐标系,得到 Clip坐标,
( x c l i p y c l i p z c l i p ) = M p r o j e c t i o n ⋅ ( x e y e y e y e z c y e w e y e ) \left( \begin{array}{l}{x_{clip}} \\ {y_{clip}} \\ {z_{clip}}\end{array}\right)=M_{p r o j e c t i o n} \cdot \left( \begin{array}{c}{x_{e y e}} \\ {y_{e y e}} \\ {z_{c y e}} \\ {w_{e y e}}\end{array}\right) ⎝⎛xclipyclipzclip⎠⎞=Mprojection⋅⎝⎜⎜⎛xeyeyeyezcyeweye⎠⎟⎟⎞
然后统一除以w,得到NDC 坐标。
( x n d c y n d c z n d c ) = ( x c l i p / w c l i p y c l i p / w c l i p z c l i p / w c l i p ) \left( \begin{array}{l}{x_{n d c}} \\ {y_{n d c}} \\ {z_{n d c}}\end{array}\right)=\left( \begin{array}{l}{x_{c l i p} / w_{c l i p}} \\ {y_{c l i p} / w_{c l i p}} \\ {z_{c l i p} / w_{c l i p}}\end{array}\right) ⎝⎛xndcyndczndc⎠⎞=⎝⎛xclip/wclipyclip/wclipzclip/wclip⎠⎞
可以参考:
ddx,ddy 只能在 Fragment Shader下使用, 因为它是用来计算相邻像素的某些属性值之差. ( 属性值比如 worldPos )
求出法线的实例. 利用 worldPos 的 x,y 之差查, 得到两个矢量, 通过 叉乘 得到垂直于这两个矢量组成面的 第三个矢量, 也就是法线.
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 dx = ddx(i.worldPos);
float3 dy = ddy(i.worldPos);
float3 normal = normalize(cross(dx, dy));
normal = normal * 0.5 + 0.5;
return fixed4(normal,1.0);
}
那么ddx,ddy 可以用在哪里呢。
六种常见坐标系:
在我们来解释一下这个坐标系为什么叫做“裁剪坐标系”,看看哪里来的“裁剪”二字:
需要特别注意的是,模型坐标系、世界坐标系、视点坐标系中的第四个分量w都是1,但是经过了投影变换之后的坐标的w分量不再为1,这时候就可以发挥w分量的作用了。在我们手动编程计算完gl_Position之后,进入GPU自身的流水管线,GPU会根据裁剪坐标gl_Position中 xyz分量 与 w分量 绝对值的大小进行比较进行裁剪。具体的过程是:GPU依次将gl_Position中x、y、z的绝对值与w的绝对值分别比较,只要有一个分量的绝对值大于w的绝对值 (也就是 Xclip/ Wclip 的值 >1 或 <-1
) ,GPU就认为该点不在视景体内,就会被裁减掉,也就是说裁剪的过程是GPU自己进行的,没有被裁减掉的坐标 xyz分量 的绝对值都小于 w分量 的绝对值 (也就是 -1< Xclip/ Wclip 的值 <1
) 。所以现在应该知道经过投影之后的坐标是为了让GPU进行裁剪用的,所以才叫做“裁剪坐标”。
Gamma计算很简单,只是个power而已,也就是:
Color o u t = Color i n γ \text { Color }_{out}=\text { Color }_{i n}^{\gamma} Color out= Color inγ
其中的γ就是用来校正的gamma值。
可惜,现实是残酷的,显示器的gamma为2.2,所以如果相机仍然是线性的,那么结果就会变成:
这样在显示器上看到的就会有明显的色彩失真。解决方法是把相机的gamma设成1/2.2,这样两次调整之后又能得到真实场景的色彩了:
对渲染的意义
前面讲的输入是对相机拍的照片而言。而对渲染来说,情况又如何呢?渲染中用到的光照都是在线性空间的。因为在设计光照的时候都是认为1的亮度是0.5的2倍。光照如此,texture又如何呢?渲染中用到的 texture一般有两个来源,一个是照片,一个是artist手工画的。前文提到了,照片是gamma = 1/2.2的。一般图象处理软件也都是在gamma空间工作的,所以artist画的图一般也可以认为是gamma = 1/2.2的。所以,我们在pixel shader常可以见到这样的代码:
float4 diff = tex2D(diffuse_texture, uv);
return diff \* max(0, dot(light_dir, normal));
这样的代码对吗?不对也对。
说其不对,是因为这里没显式地做gamma校正。做校正的话应该是这样的:
float4 diff = pow(tex2D(diffuse_texture, uv), 2.2f);
return pow(diff \* max(0, dot(light_dir, normal)), 1 / 2.2f);
也就是说,gamma校正的过程就是把输入的texture都转换到线性空间,并把输出的调整到gamma = 1/2.2的空间。
总结
总之,计算 都要发生在 线性空间,所以输入和输出需要进行gamma校正。最佳选择是采用sRGB格式,这样pow是 硬件内自动实现 ,速度更快,代码也简单。鉴于目前很多texture的数据是gamma = 1/2.2的,而纹理格式却被错误地标记成没有sRGB的,所以需要修改它们的格式标记,并重新建立mipmap。
贴图格式是sRGB, 也就是该贴图是gamma空间的.
下面这几步就是泛光后处理特效的过程,它总结了实现泛光所需的步骤。
首先我们需要根据一定的阈限提取所有明亮的颜色。我们先来做这件事。
参考: https://blog.csdn.net/puppet_master/article/details/79859678
原理 : 顶点阶段, 计算出哪些是背光, 哪些是受光, 让受光的部分的顶点沿着光的方向挤出. (为啥是受光? 因为受光部分离光比较近, 比较早衰减). 背光部分的顶点则不处理. 这样在光栅化后就能差值形成一个沿光方向延伸的模型.
类似 bloom 效果, 只是把 高斯模糊 部分换成了 径向模糊
然而事实上,你的GPU和GLSL并不擅长优化循环和分支。这一缺陷的原因是GPU中着色器的运行是高度并行的,大部分的架构要求对于一个大的线程集合,GPU需要对它运行完全一样的着色器代码从而获得高效率
在 CG 中, 对 float4x4 等类型的变量是 按行有限 的方式进行填充。(入门精要 4.9.2)
类型 | 精度 |
---|---|
float | 最高精度。32位存储 |
half | 中等精度。16位存储,范围 -60 000 ~ +60 000 |
fixed | 最低精度。11位存储,范围 -2.0 ~ +2.0 (常用来存储颜色值(0~1之间)) |
参考 : Cg/HLSL中的数据类型 - https://zhuanlan.zhihu.com/p/48530294
float
高精度类型,32位,通常用于世界坐标下的位置,纹理UV,或涉及复杂函数的标量计算,如三角函数、幂运算等。
half
中精度类型,16位,数值范围为[-60000,+60000],通常用于本地坐标下的位置、方向向量、HDR颜色等。
fixed
低精度类型,11位,数值范围为[-2,+2],通常用于常规的颜色与贴图,以及低精度间的一些运算变量等。
在PC平台不管你Shader中写的是half还是fixed,统统都会被当作float来处理。half与fixed仅在一些移动设备上有效。
比较常用的一个规则是,除了位置和坐标用float以外,其余的全部用half。主要原因也是因为大部分的现代GPU只支持32位与16位,也就是说只支持float和half,不支持fixed。
可以参考: unity-shader-GPU优化_step函数替代if-else - https://blog.csdn.net/yangxuan0261/article/details/89852627
GPU 中尽量减少使用 if else 判断逻辑,执行消耗非常大,可以使用以下方式替代
fixed4 frag(v2f i) : SV_Target { float factor = i.objPos.x - _DissolveThreshold; fixed3 color = tex2D(_MainTex, i.uv).rgb; fixed4 retClr; //方式a, 方式a 与 方式b 等价, 但 方式b 性能更好 //if (i.objPos.x > _DissolveThreshold) { // retClr = _DissolveColor; //} else { // retClr = _DissolveColor; //} //方式b fixed lerpFactor = saturate(sign(i.objPos.x - _DissolveThreshold)); //fixed lerpFactor = step(_DissolveThreshold, i.objPos.x); // 与上一行等价 retClr = lerpFactor * _DissolveColor + (1 - lerpFactor) * fixed4(color, 1); //retClr = lerp(fixed4(color, 1), _DissolveColor, lerpFactor); // 与上一行代码等价 return retClr; }
sign 约束 值 只能是 -1, 0, 1
saturate 约束值 只能是 [0, 1]
step(a, b) 表示 如果 a <= b,返回 1 ;否则,返回 0 。
所以只要 i.objPos.x > _DissolveThreshold,就取 颜色 _DissolveColor
名称 | 类型 | 说明 |
---|---|---|
_Time | float4 | t 是自该场景加载开始所经过的时间,4个分量分别是 (t/20, t, t2, t3) |
_SinTime | float4 | t 是时间的正弦值,4个分量分别是 (t/8, t/4, t/2, t) |
_CosTime | float4 | t 是时间的余弦值,4个分量分别是 (t/8, t/4, t/2, t) |
unity_DeltaTime | float4 | dt 是时间增量,4个分量的值分别是(dt, 1/dt, smoothDt, 1/smoothDt) |
XX纹理的像素相关大小width,height对应纹理的分辨率,x = 1/width, y = 1/height, z = width, w = height
也就是 : Vector4(1 / width, 1 / height, width, height)
//dx中纹理从左上角为初始坐标,需要反向(在写rt的时候需要注意)
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv2.y = 1 - o.uv2.y;
#endif
要注意写法
Shader "test/PostProcCircle" {
Properties {
_MainTex("Base (RGB)", 2D) = "white" {} // 1. 要声明一个 暴露 的参数, 不然会是一个全灰色的图
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex; // 2. 声明 uniform 变量
相关资料
运动模糊是个经常会用到的效果,常见的实现步骤是:
参考 :
官方的解释
And similarly:
If it is orthogonal, the upper-left 3x3 of Object2World will be equal to that of IT_Object2World, and so will also rotate normals from object to world space.
上面这里很好的描述了UNITY_MATRIX_IT_MV的使用场景,专门针对法线进行变换。但是为什么法线的变换和定点不一样呢?让我们来看一篇推导的文章。
注:之所以法线不能直接使用UNITY_MATRIX_MV进行变换,是因为法线是向量,具有 方向,在进行空间变换的时候,如果发生非等比缩放,方向会发生偏移。为什么呢?拿上面的例子来说,我们可以简单的把法线和切线当成三角形的两条边,显然,三角形在空间变换的时候,不管是平移,还是旋转,或者是等比缩放,都不会变形,但是如果非等比缩放,就会发生拉伸。所以法线和切线的夹角也就会发生变化。(而切线在变换前后,方向总是正确的,所以法线方向就不正确了)。
结论 : 法线的变换需要用 xxx_IT
shader示例1
// 变换 法线 从 模型空间 -> 观察空间
// 方式一
// float3 worldNorm = UnityObjectToWorldNormal(v.normal).xyz;
// float3 viewNormal = mul((float3x3)UNITY_MATRIX_V, worldNorm); // 将法线转到观察空间下, 因为matcap贴图是摄像机看到的贴图
// o.uv.zw = viewNormal.xy; // 转换法线值为贴图值
// 方式二
float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal.xyz);
o.uv.zw = viewNormal.xy;
// 方式三
// o.uv.z = mul(UNITY_MATRIX_IT_MV[0], v.normal);
// o.uv.w = mul(UNITY_MATRIX_IT_MV[1], v.normal);
shader示例2 - 切线空间
v2f2 vert2 (appdata2 v)
{
v2f2 o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
o.lightDir = UnityWorldSpaceLightDir(worldPos);
o.viewDir = UnityWorldSpaceViewDir(worldPos);
// 构建 法线 从 切线空间 到 世界空间 的 变换矩阵(三个向量)
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); // 顺便把 世界坐标 也存在这里
return o;
}
fixed4 frag2 (v2f2 i) : SV_Target
{
//获得世界空间中的坐标
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
//计算光照和视角方向在世界坐标系中
fixed3 worldLightDir = normalize(i.lightDir);
fixed3 worldViewDir = normalize(i.viewDir);
fixed4 packedNormal = tex2D(_NormalTex, i.uv);
fixed4 tex = tex2D(_MainTex, i.uv);
// 法线图 解包 后 的 切线空间的法线向量
fixed3 tangentNormal = UnpackNormal(packedNormal);
// 方式一
// 转换 法线向量 从 切线空间 到 世界空间, 等价于 下面注释部分
// fixed3 worldNormal = normalize(half3(dot(i.TtoW0.xyz, tangentNormal), dot(i.TtoW1.xyz, tangentNormal), dot(i.TtoW2.xyz, tangentNormal)));
// 方式二
fixed3 worldNormal = normalize(half3(mul(i.TtoW0.xyz, tangentNormal), mul(i.TtoW1.xyz, tangentNormal), mul(i.TtoW2.xyz, tangentNormal)));
// 方式三
// 构建 转换矩阵
// float3x3 worldNormalMatrix = float3x3(i.TtoW0.xyz, i.TtoW1.xyz, i.TtoW2.xyz);
// fixed3 worldNormal = normalize(mul(worldNormalMatrix, tangentNormal));
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * tex.rgb;
//漫反射
fixed3 diffuse = _LightColor0.rgb * tex.rgb * saturate(dot(worldNormal, worldLightDir));
//Blinn-Phong高光光照模型,相对于普通的Phong高光模型,会更加光
fixed3 halfDir = normalize(worldLightDir + worldViewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
//边缘颜色,对于法线和观察方向,只要在同一坐标系下即可
fixed dotProduct = 1 - saturate(dot(worldNormal, worldViewDir));
fixed3 rim = _RimColor.rgb * pow(dotProduct, 1 / _RimPower);
fixed4 maskCol = tex2D(_MaskTex, i.uv + float2(0, _Time.y * _MoveDir));
return fixed4(ambient + diffuse + specular + rim * maskCol.rgb, 1);
// return fixed4(ambient + diffuse, 1);
}
参考:
获取像素在屏幕空间的位置
内置 ComputeScreenPos 实现
inline float4 ComputeNonStereoScreenPos(float4 pos) {
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
o.zw = pos.zw;
return o;
}
inline float4 ComputeScreenPos(float4 pos) {
float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
return o;
}
内置 tex2Dproj 实现
half4 tex2Dproj(sampler2D s, in half4 t) { return tex2D(s, t.xy / t.w); }
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.projPos = ComputeScreenPos(o.vertex); // 参数是剪裁空间下的坐标
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.projPos.xy / i.projPos.w);
// fixed4 col = tex2Dproj(_MainTex, i.projPos);
return col;
}
一般采样贴图是都是顶点中的uv值, 是个固定值的, 所以不会随着顶点位置的变化而变化, 就会造成拉伸.
可以做图片不被拉伸的效果, 因为采样的 uv 值是 0-1, 所以只用当四个顶点完后只能覆盖屏幕四个角落时, 才会显示出完整的图片.
因为 mesh 的所有的顶点在屏幕空间下的值都会在 (0, 0) 到 (1, 1) 区间内.
参考:
# 指定 表面函数 和 光照函数
#pragma surface surf Toon
struct Input {
float2 uv_MainTex;
float2 uv_Bump;
};
void surf (Input IN, inout SurfaceOutput o) {...}
half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten) {...}
unity中使用雾效的姿势. 主要是四个地方, 加下面注释处.
Pass
{
CGPROGRAM
#pragma multi_compile_fog // 雾效编译指令
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1) // 雾效数据字段, 1 为寄存器语义 TEXCOORD1, 完整展开 - float fogCoord : TEXCOORD1
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex); // 将 剪裁空间 下 顶点 的 z 值 存进 fogCoord 字段中
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
UNITY_APPLY_FOG(i.fogCoord, col); // 最终颜色加上雾效
return col;
}
}
比较热门的两款 可视化 插件 Amplify Shader vs Shader Forge
Shader Forge 貌似更新太慢等很多抱怨
Amplify Shader 的可视化 和 ue4 的材质编辑器 很相似,编辑出来的是 surface着色器 的源码
Shader Forge 编辑出来的是 vertex着色器与fragment着色器 的源码
gpu 并不擅长 循环和判断 语句
分量计算
也就是说
float4 f = float(1,1,1,1);
f = float4(f.x +=1, f.y +=1, f.z +=1, f.w += 1);
算了4系分量
没有
f += 1;
的效率高
答:
(1)简单来说,TRANSFORM_TEX主要作用是拿顶点的uv去和材质球的tiling和offset作运算, 确保材质球里的缩放和偏移设置是正确的。 (v.texcoord就是顶点的uv)
下面这两个函数是等价的。
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
所以使用他有两个前提:
\1. #include “UnityCG.cginc”
\2. 定义name##_ST
(2)而_MainTex_ST的ST是应该是SamplerTexture的意思 ,就是声明_MainTex是一张采样图,也就是会进行UV运算。 如果没有这句话,是不能进行TRANSFORM_TEX的运算的。_MainTex_ST.xy为 下图中的Tiling,zw为下图中的offset.
如果Tiling 和Offset你留的是默认值,即Tiling为(1,1) Offset为(0,0)的时候,可以不用
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
换成o.uv = v.texcoord.xy;也是能正常显示的;相当于Tiling 为(1,1)Offset为(0,0),但是如下图自己填的Tiling值和Offset值就不起作用了
// alpha test
clip (o.Alpha - _CutOff);
clip(x) : 如果输入向量中的任何元素小于0,则丢弃当前像素。
也就是说,是通过 Alpha 值 减去 指定的 _CutOff ,也就是,Alpha 值 低于 _CutOff 的片段都会被丢弃。
根据书上所说,这次学习的 透明裁剪着色器,因为要逐像素判断 Alpha 是否小于 _CutOff ,所以是比较消耗 GPU的,所以在手机并不推荐。
但是在工作中,会发现,其实很大程度上都是一个度的问题。
像这中溶解效果,用在副本里面,不可能几十个怪同时死亡而同时显示溶解的效果的。
所以并没有什么在手机上就不能用这种事情。想用就用。
SV其实是system-value(系统数值)的意思,需要注意的是SV开头的语义考虑到扩平台的问题了,所以能用SV开头的语义的尽量使用SV开头的语义,免去跨平台的烦恼
http://blog.csdn.net/a958832776/article/details/70847033
http://blog.csdn.net/zhao_92221/article/details/46797969
重复使用了 TEXCOORD1 语义,可能类型 UNITY_FOG_COORDS(1) 这样的unity宏里使用了,所以改成 TEXCOORD2\3\4\5 即可
法线贴图每个像素存的都是每点(当然这些点是离散化的)的局部法向量坐标,所以没有扰动法向量的时候,图片全部像素应该是rgb(0.5,0.5,1) 大概就是蓝紫色那样 (假设z轴就是法向)。因为单位法向量的xy分量取值在(-1,1)但是颜色分量取值在(0,1),所以把这个区间映射一下就会发现未被扰动的向量(0,0,1)会被映射成(0.5,0.5,1)这个偏蓝紫色的颜色
映射公式: pixel = (normal + 1) / 2
通常情况下,美工会做出一张使用的RGB三个通道用来存放法线的XYZ三个轴向的坐标的法线贴图。但是Unity在导入法线贴图的时候会自动将法线贴图压缩成 DXT5nm 格式,这个格式的好处是 它只使用AG(透明和绿色)两个通道来存放两个轴向的坐标值。而Z轴向的坐标值由于是 单位坐标 可以通过 1 减去另外前两个轴向坐标的 平方和 来得到(已知2个分量,可求第三个),从而可以以同样的容量存放更大尺寸的法线贴图,我们都知道图片的颜色通道存的都是非负数(法线贴图生成的时候已经把[-1,1]压缩为[0-1]),而我们的三维空间是[-1,1],所以我们要把它解析放大一下,方法就是对对应的颜色通道值乘以2再减1。这就能存储游戏物体所有的法线信息了。例如,一个RGB值为(0.5,0.5,1)或#8080ff(16进制)的颜色向量,它所存储法线向量值为(0,0,1),代表该图向上的法向量,即模型没有凹凸现象。对比本页面之前的模型平面区域,您就会发现这种颜色基调。
参考 : 法线贴图详解之三----凹凸,法线以及高度图的区别及法线贴图蓝紫色的原因 - http://manew.com/thread-90210-1-1.html
因此, 如果切线空间下的法线 和 顶点 ( 插值后的) 法线 重合 (平行) , 则这该点像素值为 ( 0.5, 0.5, 1), 也就是对应的法线值 ( 0, 0, 1), 也就是切线空间坐标系的z轴, 也就是顶点的法线.
等价与 上面那个问题的 映射公式,都是将 法线值转为 像素值
会使用到这个公式 一般都是 使用了 fixed3 normals = UnpackNormal(tex2D(_BumpMap, i.uv_bump)); 将法线值 从 法线图 解包后
pixel = (normal + 1) / 2 //等价与 pixel = (normal * 0.5 + 1 * 0.5 //推导出 normal = pixel * 2 - 1
对,所以用法可以互换,
// Transform the normal from object space to world space // 转换法线 从 模型空间 到 世界空间 o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject); // 内置 UnityObjectToWorldNormal 的实现 //等价于 // o.worldNormal = mul((float3x3)unity_ObjectToWorld, v.normal);
此方式只能用在向量上,不能用在顶点上(v.vertex),用在顶点上会得到不一样的结果
Q: 为什么喜欢用 mul(v.normal, (float3x3)unity_WorldToObject); 转换法线 从 模型空间 到 世界空间
等价与 o.worldNormal = mul((float3x3)unity_ObjectToWorld, v.normal);
但是,