参考内容:https://docs.qq.com/doc/DUFlzT3ByV2tHanpT
渲染管线(渲染流程)
- CPU通过调用Draw Call让GPU进行渲染(会将顶点数据传给GPU)
GPU分为2个阶段:几何阶段和光栅化阶段
顶点着色器
裁剪
屏幕映射
三角形设置
三角形遍历
片元着色器
输出合并阶段(逐片元操作)
执行流程:片元 --》模板测试 --》深度测试 --》混合 --》颜色缓冲区 --》输出屏幕
如果一个片元通过了所有的测试,就要把这个片元的颜色值和颜色缓冲区的颜色进行混合
深度缓冲和深度写入
深度缓冲区:它是一个二维纹理,用来存储每个像素在观察空间中里相机的距离,即深度值
帧缓冲区:深度缓冲区和颜色缓冲区的组成的,存储最终渲染的图像。
深度缓冲区原理
- 对于每个像素,渲染管线会计算出其观察空间中的深度值
- 当一个像素需要进行渲染时,会将其深度值与深度缓冲区对于像素的深度值进行比较。
- 如果当前像素的深度值大于深度缓冲区的深度值,说明当前像素被前面的物体遮挡,它会被认为是不可见的,深度缓冲区中的深度值不会更新。
- 如果当前像素的深度值小于深度缓冲区的深度值,说明当前像素位于前面,它被认为是不可见的,并将其深度值写入深度缓冲区。
深度值:深度值的范围是 0 - 1, 0表示最近可见距离,1表示最远可见距离。在渲染过程中,深度缓冲区需要进行读取和写入操作,因此在图形硬件中占有一定的显存。(也就是说深度值越大,越靠后)
深度测试:是一种在渲染过程中用于决定像素是否可见的技术,它通过比较当前像素的深度值和深度缓冲区中对应的深度值来确定是否绘制。
深度测试函数:如何比较深度缓冲区和当前像素的深度值?
- Less:(当前像素的深度值)小于(深度缓冲区的深度值),进行绘制(也就是说当前像素位于物体的前面)
- Greater 位于物体后面,进行绘制
- Equal 覆盖物体
- LessEqual
- GreaterEqual
- NotEqual 也就是说重叠时,不会绘制
// 在Unity中使用深度测试函数
Pass {
ZTest Less // 使用Less深度测试函数
}
深度写入:控制是否将当前片段的深度值写入到深度缓冲区中。
在Unity中,可以使用深度写入指令来设置写入的方式
- ZWrite On: 将当前片段的深度值写入深度缓冲区
- ZWrite Off:不写入,保持原有深度值
Pass {
ZWrite On // 开启深度写入
}
一个模型时如何组成的
帧缓存
当模型的图元经过上面的层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区的颜色值。
但是为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略,或者叫帧缓存更合适一些。因为现在还有三重缓冲(即加一个中缓存)的情况。
默认情况下有两个帧缓存,即Back Buffer和Front Buffer,也被我们称为前缓存和后缓存。
显卡在渲染画面的时候并不会直接交给显示器去显示,而是先写入BackBuffer也就是后缓存中,等待后缓存写入完毕,前后缓存发生交替,后缓存就变成了前缓存,前缓存就变为了后缓存。这个交替的过程被我们称为Bufferswap(帧传递)。
帧FPS:60FPS,就是一秒钟 显卡 画了60张图片,然后显卡交给显示器。
显示器显示画面是通过逐行扫描完成,显示器在收到一个完整的帧以后,会从左上角一行一行的地进行绘制。一直绘制到画面的右下角。
然后显示器会将扫描点从右下角挪回到左上角。这个重置扫描点的过程叫VBlack。
VBlack结束以后继续扫描下一帧,然后一直循环
GPU渲染
纹理采用
每个顶点都有uv坐标,uv坐标通过某个公式去计算,从而找到纹素地址对应的颜色
纹理过滤机制
有一个问题:如果将一张小图片放大,对应的uv坐标去查找到纹素地址就会找不到,就会将 通过uv坐标计算得到的地址进行取舍(如13.8变为14),这种方式会导致图片有锯齿。所有就有了纹理过滤机制用来解决。
他会得到这个地址(x,y)的上下左右四个像素,然后进行差值计算
对应Unity图片属性中的Advanced
矩阵
向量点乘
参考文章:https://zhuanlan.zhihu.com/p/616207329
几何意义:一个向量在另一个向量的投影长度
图形学的表现:两向量方向同时,结果为1(白色),方向相反,结果为 -1(黑色),方向垂直,结果为0(黑色)
运用于平方:两个相同的向量点乘,不就是这个向量的平方吗?
运用于光:光的反方向与物体表面的法线点乘
矩阵几何意义
- 矩阵理解为 变换。将一个向量变换为另外一个向量
向量 和 矩阵 相乘
其中a是行向量,M是矩阵,b是另一个行向量。
行向量左乘矩阵得行向量,几何意义:M矩阵将a向量转化为b向量
列向量右乘矩阵得列向量。
什么是变换?
将数据进行转换,点、方向,颜色等进行变换。
- 线性变换
你可以理解为 一个特殊的函数,输入向量A,输出向量B
而且这个函数满足
从直观上来理解
- 原点保存不动
- 变换前是直线,变换后也是直线。
那么就可以解释旋转(以原点进行)和缩放(以原点进行)是线性变换
而平移不是线性变换,因为原点动了。
- 仿射变换
合并线性变换 和 平移变换 的变换类型。
- 齐次坐标空间
你就把它理解为一个 4 * 4 的矩阵。为什么要四维的呢?因为三维的矩阵不能表示平移变换。
齐次坐标
齐次坐标是一个四维的向量。 相比于三维向量多了一个w分量。
这个分量w有着很重要的地位。
- 如果你想要将三维空间的一个点转化为齐次坐标,设置w = 1
- 如果你想要将三维空间的一个向量转化为齐次坐标,设置w = 0
分解基础变换矩阵
其中M表示旋转和缩放,t表示平移,0表示零矩阵
平移矩阵
我们发现当我们左乘一个矩阵的时候,坐标(x,y,z)发生了平移
那么如果我们将平移后坐标恢复为原坐标呢?
那么我们需要这个矩阵的逆矩阵
缩放矩阵
旋转矩阵
- 二维旋转矩阵
其中q’和p’基向量
- 三维旋转矩阵 (这里以左手坐标系进行旋转,不同的坐标系是不同的矩阵)
我们先说一下左手坐标系是什么?
其中z轴是front、y轴是up、x轴是right
接着我们说说绕轴旋转的方向,如果我们绕y轴旋转,根据左手定则,大拇指指向y轴,剩下的4指方向就是顺时针。如果我们绕x轴旋转,大拇指指向x轴,4指方向就是顺时针。
绕x轴旋转:x轴所在的基向量不变(1,0,0),y轴所在的基向量变为(0, cos,sin),z轴所在的基向量变为(0, -sin,cos)故它的旋转矩阵为
复合矩阵
shader的去色
在片元着色器回调函数中,设置灰色
float grey = dot(color.rgb, fixed3(0.22, 0.707, 0.071));
color.rgb = float3(grey, grey, grey);
Unity Shader
常见的API和函数
常用函数和api | 说明 |
---|---|
UnityObjectToClipPos(data.vertex) | 将模型空间转化为裁剪空间,参数:模型空间的顶点 |
UnityObjectToWorldNormal(data.normal) | 模型空间转法线化为世界空间的法线,参数:模型空间的法线 |
mul(unity_ObjectToWorld, data.vertex); | unity_ObjectToWorld 模型空间到世界空间 |
TRANSFORM_TEX(data.texcoord, _MainTex) | 设置uv偏移和缩放并获取uv,第一个参数:贴图信息,第二个参数:主贴图 |
UnityWorldSpaceLightDir(data.worldPos) | 找到世界空间下的灯光方向 |
tex2D(_MainTex, data.uv) | 通过uv从贴图中读取 相应的颜色 |
UNITY_LIGHTMODEL_AMBIENT.xyz; | 获得环境光 |
UnityObjectToViewPos(data.vertex) | 通常用于将模型空间下的顶点转化为视角空间下的点 |
mul((float3x3)UNITY_MATRIX_IT_MV, data.normal) | 通常用于将法线从模型空间转视角空间 |
lightDot = smoothstep(0, 1, lightDot); | smoothstep平滑颜色,第一个参数设置的越大,越暗,第二个参数设置的越小,光越亮 |
normalize(UnityWorldSpaceViewDir(data.worldPos)); | 世界空间下的视角方向 , 也可以通过normalize(_WorldSpaceCameraPos.xyz - data.worldPos);来获取世界空间下的视角方向 |
#pragma multi_compile_fwdbase | 宏变量,用于获得"LightMode"= "ForwardBase"下的光照变量 |
#pragma multi_compile_fwdadd | 宏变量,用于获得LightMode下为ForwardAdd |
#include “AutoLight.cginc” | 用于相关的变量和衰减等 |
USING_DIRECTIONAL_LIGHT | 宏变量,如果当前片段是平行光,Unity会启动这个宏变量 |
_WorldSpaceLightPos0 | 世界空间下的灯光点的位置 |
_WorldSpaceCameraPos | 世界空间下摄像机的位置 |
mul(unity_WorldToLight, float4(data.worldPos,1)) | 将世界空间转化为灯光 空间 |
tex2D(_LightMatrix0, dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL; | 获得灯光贴图的衰减值 |
理解Pass
Pass是一个渲染流程,会执行顶点着色器->…->片元着色器->合并输出…
多个pass,不同的Pass处理的信息不同。
我的理解:不同的pass处理着不同的信息,当然如果你处理的相同的光照信息会被下一个pass覆盖。每个pass之间不相互影响。
uniform
由uniform定义的变量,默认可以省略,在外部可以通过c#进行调用
// 在shader中的CG代码块定义
// float4 _SecondColor; 也可以省略
uniform float4 _SecondColor;
// 在C#中,可以找到对应的材质设置_SecondColor的值
GetComponent<Render>().material.setVextex("_SecondColor", 对应的值)
光照模型
标准光照模型
漫反射
漫反射:(光源颜色 * 材质的漫反射颜色) * max(0, 表面法线 点乘 指向光源的方向)。
光源颜色:Unity内置变量 _LightColor0
(需要定义合适的LightMode标签)
材质的漫反射颜色:自定义
表面法线:穿过来的顶点法线,可以通过语义 NORMAL来得到
指向光源的方向:如果是平行光,用 _WorldSpaceLightPos0
来得到
内置函数:normalize(向量)标准化向量,saturate(x)将x截取到[0,1]范围内
半兰伯特模型
// 基本光照的方式
fixed3 Lambert = max(0, dot(worldNormal, worldLightDir));
// 半兰伯特的方式
fixed3 halfLambert = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
基本纹理
tex2D函数
tex2D是对二维纹理进行采用,从给定二维纹理中,根据指定的纹理坐标,获取对应的纹理颜色值
// sampler是进行采用的二维纹理
// coord是纹理坐标,指定从为纹理中那个位置进行采样。
// 返回值:采样得到的纹理颜色
fixed4 tex2D(sampler2D sampler, float2 coord);
UnpackNormal函数
UnpackNormal函数通常用于将压缩的法线数据解压成可用的法线向量。这个函数的具体实现可能因不同的图形API或引擎而有所不同。
下面是 UnpackNormal函数的一种实现方式
fixed3 UnpackNormal(fixed4 packedNormal)
{
// 这里假设packedNormal的x和y分量存储了压缩的法线的x和y分量,
// 且压缩的方式是将它们映射到了[0, 1]的范围。
// 那么我们可以通过下面的方式解压它们:
packedNormal.xy = packedNormal.xy * 2 - 1;
// 然后我们需要重新计算z分量,因为法线是单位向量,所以我们可以利用勾股定理来求得z分量。
packedNormal.z = sqrt(1 - dot(packedNormal.xy, packedNormal.xy));
return packedNormal;
}
凹凸映射
理解法线: 我们知道法线是垂直物体的表面的。而计算光照时,就需要用到法线。
对于平行光而言,物体在该点的光照强度 是有法线与平行光构成的夹角有关,夹角为0,该点的光照强度越强,夹角为90度,该点的光照强度几乎为0.。
也就是说我可以通过控制该点的法线,来改变该点的光照强度。从而将2D图像在视觉效果上是3D立体的。
所以我的理解就是,法线在这里是一个骗子,欺骗你的眼睛。
切线空间下计算
在片元着色器通过纹理采样得到切点空间下的法线,然后在于切线空间下的视角方向,光照方向等计算。
- 那么如何求切线空间到模型空间的变换矩阵呢?
结论:可以通过推理得到
得到了切线空间到模型空间的变换矩阵,我们很容易得到模型空间到切线空间的变换矩阵。
对这个矩阵进行转置。为什么?因为它是正交矩阵,它的逆矩阵就是它的转置矩阵
- 代码思路以及步骤
- 我们需要两张贴图,一个是主贴图,一个是法贴图
// "bump"是内置的法线纹理,当我们没有任何法线纹理时,"bump"就对应了模型自带的法线信息
_BumpMap("Bump Map", 2D) = "bump"{}
- 我们还需要一个控制凹凸程度的属性
_BumpScale("Bump Scale", Float) = 1.0
- 我们需要指定光照模式,以便获取光照信息
Tags{ "LightNode" = "ForwardBase" }
- 声明变量,以及获取纹理的属性
// 2D纹理采用sampler2D类型的
sampler2D _BumpMap;
// 一般声明了2D纹理,就必须有一个_ST,来获取纹理属性
float4 _BumpMap_ST;
- 定义结构体
struct a2v {
float4 vertex : POSITION; // 告诉Unity,将顶点信息给vertex
float3 normal : NORAML; // 告诉Unity,将法线给normal
// tangent.w 是表示副切线的方向性的
float4 tangent : TANGENT; // 告诉Unity,将切线信息给tangent
// 用float4来存储主纹理的uv信息和法线纹理的uv信息
// 其中xy表示主纹理的uv信息,zw表示法线纹理的uv信息
float4 texcoord : TEXCOORD0; // 告诉Unity,将贴图信息的uv信息传给 texcoord
}
- 因为我们需要在顶点着色器中,在切线空间中计算光照和视角方向,所以v2f结构体需要2个变量来存储变换后的光照和视角方向
struct v2f{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 lightDIr : TEXCOORD1;
float3 viewDir : TEXCOORD2;
}
- 找到模型空间到切线空间的变换矩阵
// 方式一
// 进行叉乘获取副切线, v.tangent.w来记录副切线的方向
// float3 binormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
// 模型空间下切线方向、副切线方向、法线方向按行排列
// float3×3 rotation = float3×3(tangent.xyz, binormal, v.noraml);
// 方式二:unity提供了内置宏,来帮我们计算rotation
TANGENT_SPACE_ROTATION;
- 计算切线空间下的灯光和视角方向
// 首先我们需要知道模型空间下的光照和视角方向
// ObjSpaceLightDIr(v.vertex).xyz // 模型空间下的灯光方向
// ObjSpaceViewDIr(v.vertex).xyz // 模型空间的视角方向
// 将模型空间的灯光和视角方向转化为切线空间的灯光方向和视角方向
o.lightDir = mul(rotation, ObjSpaceLightDIr(v.vertex).xyz);
o.viewDir = mul(rotation, ObjSpaceViewDIr(v.vertex).xyz );
-
接下来处理,在片元着色器进行光照计算了
-
对法线纹理进行采样
// 这里注意:前面需要对uv进行偏移和缩放
// 对法线纹理进行采样 ,得到的是切线坐标下的法线
fixed4 packedNormal = tex2D(_BumpMap, i.uv.zw);
// 法线纹理存储的是法线经过映射得到的像素值,所以我们需要反射回来,得到正确的法线信息。
// 因为 法线到颜色的映射关系是 0.5 * (x, y) + 0.5 = (r, g)
// 那么 颜色到法线的映射关系是 [(r, g,) - 0.5 ] * 2 = (x, y)
fixed3 tangentNormal;
// tangentNormal.xy = (packedNormal.xy * 2 - 1) * _BumpScale;
// 这里我没有理解?
// 追加:因为切线空间下的法线向量都是单位向量
// x^2 + y^2 + z^2 = 1
// 那么我们要求z = 根号下 1 - x^2 - y^2
// 而 x^2 + y^2 ,对应的坐标点,就是两者进行点乘
// tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)));
// 使用Unity的内置函数 UnpackNormal来得到正确的法线信息
tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1.0 - saturate(dot(tangentNormal.xy, tangentNormal.xy)))
// 如果我们没有在面板中将图片设置类型设置为 Normal map,就需要在代码中手动进行这个过程
对上面步骤的再一次理解
首先,上面的代码主要处理从法线贴图中提取和转换法线信息的过程。这里的核心是从贴图中获取压缩的法线信息,然后解压它。
tangentNormal.xy *= _BumpScale;
这段代码是调整x和y分量,改变表面的凹凸程度。
xy的分量影响着法线z。x和y的分量余越大,就意味着法线越斜。也就在这一点处就会越暗。
- 对主纹理进行采样
// 这里时对这个纹理进行混色
fixed3 albedo = tex2D(_MainTex, i.uv).rgb * _Color.rgb;
- 计算光照信息
// 计算光照模型
fixed3 tangentLightDir = normalize(data.lightDir);
fixed3 tangentViewDir = normalize(data.viewDir);
// 环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * abledo;
// 漫反射
fixed3 diffuse = _LightColor0.rgb * abledo * saturate(dot(tangentNormal, tangentLightDir));
// 高光反射
half3 h = normalize(tangentViewDir + tangentLightDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal, h)), _Gloss);
return fixed4(ambient + specular + diffuse, 1.0);
在世界坐标下计算
就是在世界空间下计算光照模型。
我们的思路是在片元着色器下将法线方向从切线空间变换到世界空间
Unity中法线纹理类型
渐变纹理
在Unity制作渐变纹理
参考:https://github.com/Zzzzohar/Ramp-Tools
核心代码
//使用内置的宏来计算平铺和偏移
o.uv = TRANSFORM_TEX(v.texcoord, _RampTex);
// 使用半兰伯特让整体偏亮一点
fixed halfLambet = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
// 因为渐变纹理是一维的(纵轴方向上的颜色是一样的)
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambet, halfLambet)).rgb * _Color.rgb;
遮罩纹理
透明效果
在Unity如何实现透明效果呢?
第一种是使用透明度测试(这种方法无法达到真正的半透明效果)
第二种是透明度混合。
透明度测试: 它是用来确定像素是否应该被绘制为透明或半透明的一种技术。
在实现透明度测试时,通常会有一个阈值来决定像素是否被绘制为透明,这个阈值被存储在材质的一个通道中,例如Alpha通道,渲染引擎会使用这个阈值来进行透明度测试,当前像素的Alpha值小于阈值时,像素会被认为是透明的,不会被绘制。
需要注意的是,透明度测试是一种简单的透明度处理方法,它并不能处理复杂的半透明效果,例如折射和次表面散射等。对于这些更复杂的半透明效果,可能需要使用更高级的透明度处理技术,例如透明度排序
透明度混合:它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。
渲染顺序
问题: 我们在使用透明度混合需要关闭深度写入,为什么呢?
关闭深度写入是为了解决深度冲突问题。
注意:渲染的执行顺序是先进行深度测试,然后再进行深度写入。
如果我们不关闭深度写入,一个半透明表面背后的表面本来是可以透过它被我们看到的,但由于深度测试时判断结果是半透明表面距离摄像机更近,导致后面的表面被剔除,我们就无法透过半透明物体看到后面的物体。
关闭深度写入可能会导致不透明物体之间的深度排序不正确。因此,在使用透明度混合时,通常需要先渲染所有不透明物体,然后按照距离从远到近的顺序渲染透明物体(并开启深度测试,关闭深度写入),以保证正确的深度排序和混合效果。
透明度测试
透明度测试: 在实现透明度测试时,通常会有一个阈值来决定像素是否被绘制为透明,这个阈值被存储在材质的一个通道中,例如Alpha通道,渲染引擎会使用这个阈值来进行透明度测试,当前像素的Alpha值小于阈值时,像素会被认为是透明的,不会被绘制。
clip函数
Clip函数用来裁剪,给定任何一个分量是负数,就会舍弃当前像素的输出颜色
fixed4 fragColor = texture(myTexture, fragTexCoord);
// 就如同这种方式
if (fragColor.a < alphaThreshold)
discard;
else
FragColor = fragColor;
相关代码
- 设置渲染顺序
Tags{ "Queue"= "AlphaTest" "IgnoreProjector"="True", RenderType=TransparentCutout }
在Unity中透明度测试使用的渲染队列是AlphaTest队列。
RenderType标签可以让Unity把这个Shader归入到提前定义的组。(这里的组是TransparentCutout组)。
IgnoreProjector为true,说明shader不会受到投影器的影响
投影器:投影器(Projector)是一个组件,它用于为贴图或模型投射光线并产生阴影、倒影等效果。投影器在Unity引擎中常用于实现各种投影效果,如弹孔、血溅到地面上、贴花等。
- 设置光照标签
Tags { "LightMode"="ForwardBase" }
通过这个标签可以获得一些Unity内置的光照变量,如_LightColor0
- 设置属性
_Cutoff("Alpha CutOff", Range(0, 1)) = 0.5
- 核心代码
clip(texColor.a - _Cutoff)
如果texColor.a 小于 _Cutoff
,就会完全透明
- 计算光照模型
透明度混合
透明度混合:它会使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。但是,透明度混合需要关闭深度写入,这使得我们要非常小心物体的渲染顺序。
在Unity中,Blend是设置混合模式的命令。
- Blend Off 关闭混合
- Blend SrcFactor Dstfactor 开启混合,并设置混合因子。源颜色(该片元产生的颜色)会乘以SrcFactor,而目标颜色(已经存在于颜色缓存的颜色)会乘以 Dstfactor,然后将两者相加后在存入颜色缓冲区。
- Blend SrcFactor Dstfactor, SrcFactorA DstfactorA 和上面几乎一样,只是使用不同的混合因子来混合透明通道
- BlendOp BlendOperation 并非把源颜色和目标颜色简单相加后混合。而是使用BlendOperation对它们进行其他操作
我们一般源颜色的混合因子SrcFactor设置为SrcAlpha,目标颜色的混合因子设置为OneMinusSrcAlpha,这就意味着经过混合后新的颜色是:
实现思路
- 使用_AlphaScale控制整体的透明度
_AlphaScale("Alpha Scale", Range(0, 1)) = 1
- 修改SubShader使用的标签
Tags{ "Queue"= "AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
- 在Pass中进行混合度设置
Tags { "LightMode"="ForwardBase" }
ZWrite Off // 关闭深度写入
Blend SrcAlpha OneMinusSrcAlpha
- 片元着色器
// ambient环境光 diffuse漫反射
// texColor.a 贴图的alpha通道
return fixed4(ambient + diffuse, texColor.a * _AlphaScale);
开启深度写入的半透明效果
上述中,如果我们关闭深度写入,可以让模型变得透明。但是如果遇到非常复杂的模型。就会造成错误的排序。
解决办法:使用两个pass来渲染模型。第一个Pass开启深度写入,但不输出颜色,它的目的仅仅是将该模型的深度值写入深度缓存中;第二个Pass进行正常的透明度混合。由于上一个Pass已经得到了逐像素的正确深度信息,该pass就可以按像素级别的深度排序结果进行透明渲染。
缺点:多一个pass可能会造成一定的性能。
ColorMask 设置颜色通道的写掩码
当ColorMask为0是,意味着不写入任何颜色通道,也不会输出任何颜色。
核心代码
pass {
ZWrite On
ColorMask 0
}
pass {
// 和上述一样
}
ShaderLab 的混合命令
混合是如何实现的?
当片元着色器产生一个颜色值值时,可以选择与颜色缓存区的颜色进行混合。
而混合与两个操作数有关:源颜色和目标颜色。
源颜色:我们用S表示,指的是由片元着色器产生的颜色值。
目标颜色:我们用D表示,指的是从颜色缓冲区中读取到的颜色值。
最终颜色:我们用O表示,指的是 源颜色和目标颜色进行混合的颜色,它会重新写入颜色缓冲区。
混合等式参数
ShaderLab中设置混合因子的命令
// 开启混合,并设置混合因子。源颜色 S 会乘以SrcFactor,而目标颜色 O 会乘以 Dstfactor,然后将两者相加后在存入颜色缓冲区。
Blend SrcFactor DstFactor
而这些混合因子有哪些呢?
参数 | 描述 |
---|---|
One | 因子为1时,表示完全使用源颜色。这意味着在渲染过程中,源颜色将完全覆盖目标颜色,没有任何混合。 |
Zero | 因子为0时,表示完全使用目标颜色。这意味着在渲染过程中,目标颜色将没有任何混合,完全被源颜色所覆盖。 |
SrcColor | 源颜色 |
SrcAlpha | 源颜色的透明值 |
OneMinusSrcAlpha | 1-源颜色的透明度 |
加法混合和乘法混合效果
- 加法混合效果(Additive blending)
混合函数:Blend One One
源因子权重为1,目标因子权重为1
操作:将源颜色和目标颜色相加
效果:将源颜色添加到目标颜色,产生增亮或发光的效果,用于创建光照、火焰、粒子效果。
- 乘法混合效果(Multiplicative blending)
混合函数:Blend DestColor Zero
源因子权重:目标颜色,目标因子权重:0
操作:源颜色乘以目标颜色
将源颜色和目标颜色相乘,使得混合颜色变暗。适用于创建阴影、叠加颜色、透明效果等。
常见的混合类型
- Blend SrcAlpha OneMinusSrcAlpha:正常(Normal),即透明度混合。
假设我们有两个颜色A和B,其中A是源颜色,B是目标颜色。假设A的透明度为a,B的透明度为b。
使用Blend SrcAlpha OneMinusSrcAlpha
混合类型时,最终的混合颜色将是:
混合颜色 = A的透明度 * (1-B的透明度) + B的透明度
这个公式的含义是,当源颜色的透明度(SrcAlpha)越大,混合颜色将越接近于源颜色;而当源颜色的透明度(SrcAlpha)越小,混合颜色将越接近于目标颜色。同时,如果目标颜色的透明度越大,混合颜色将越接近于目标颜色;而如果目标颜色的透明度越小,混合颜色将越接近于源颜色。
- Blend OneMinusDstColor One:柔和相加(Soft Additive)。
- Blend DstColor Zero:正片叠底(Multiply),即相乘。
- Blend DstColor SrcColor:两倍相乘(2x Multiply)。
- BlendOp Min Blend One One:变暗(Darken)。
- BlendOp Max Blend One One:变量(Lighten)。
- Blend OneMinusDstColor One 等同于 Blend One OneMinusSrcColor:滤色(Screen)。
- Blend One One:线性减淡(Linear Dodge)。
Blend One On
指的是在当前颜色和缓存颜色之间进行线性插值,具体来说,它计算当前颜色和缓存颜色的线性组合,得到的结果颜色等于当前颜色乘以缓存颜色。这种混合类型通常用于实现透明度混合、滤色等效果。
在ShaderLab中,这些混合类型可以被应用在各种不同的材质和表面属性上,以实现各种复杂的渲染效果。
双面渲染的透明效果
在Unity中使用Cull指令控制需要剔除哪个面的渲染图元。
- Cull Back 背着摄像机的渲染图元不会被渲染
- Cull Front 朝着摄像机的渲染图元不会被渲染
- Cull Off 关闭剔除功能,所有的渲染图元都会被渲染
Cull Back | Front | Off
更复杂的光照
Unity渲染路径
参考资料:https://www.yuque.com/sugelameiyoudi-jadcc/okgm7e/bumivr
参考文章:https://zhuanlan.zhihu.com/p/408238134
渲染路径:1. 与光照打交道 2. 要为pass指定渲染路径,这样Unity才会明白程序员想要使用的渲染路径,然后将光源和处理后的光照信息放到这些数据里,程序员才能在着色器中进行访问。
大多数情况下,一个项目只使用一种渲染路径。您可以在Unity的Edit->Project Settings->Player->Other Settings->Rendering Path中选择项目所需的渲染路径。默认情况下,该选择的是前向渲染路径。但一个摄像机下可指定不同的渲染路径,同样在Rendering Path下指定。利用pass标签下的LightMode来指定渲染路径,例如:Pass{ Tag{"LightMode"="ForwardBase"} }。
,上述代码将告诉Unity,该Pass将使用前向渲染路径中的ForwardBase路径。
如果我们在每个摄像机的渲染路径设置中设置该摄像机的渲染路径,会覆盖Project Setting中的设置。
我的理解:就是处理多盏灯光对物体产生的影响的方式。
Unity中LightMode支持的标签
标签名 | 描述 |
---|---|
ForwardBase | 用于前向渲染。该Pass会计算环境光、最重要的平行光、逐顶点/SH光源和LightMaps |
ForwardAdd | 定义添加额外光照的前向渲染阶段,即渲染受光照影响的物体 |
不同光源类型的影响
光源属性一般有:位置、方向、颜色、强度、衰减
衰减:光照强度随着距离而不断改变
平行光:没有位置、只有方向的。而且也没有衰减的概念。
点光源:照亮空间有限,在一个球形空间中。而且点光源的强度是由球中心向外慢慢衰减的。
聚光灯:由一个点向特定方向延伸的。
逐像素光源规则
- 当光源类型设置为important时,是逐像素光源(不受限制于Project-Setting->Quality->Pixel light Count)(Forward Add)
- 当光源类型为auto。个数在Pixel light Count以内的光源都是逐像素光源(Forward Add)
- 当光源类型为auto。个数超过Pixel light Count,那么该光源对物体影响重要程度排序后,前Pixel light Count个数的光源为逐像素光源(Forward Add)
- 最重要的平行光为逐像素光源(Forward Base)
逐顶点光源规则 (默认是4个,超过4个采用球谐函数)
- 当光照类型设置为 Not important时,是逐顶点光源(Forward Base)
- 超过pixel light count的光源为逐顶点光源(Forward Base)
球谐函数
就把它理解为灯光的渲染方式(性能开销低,比逐顶点和最逐素低,但是效果差)
参考文件:https://docs.qq.com/doc/DUFdKZE1oVFd3ZlBs
光照衰减
参考文章:https://zhuanlan.zhihu.com/p/113087810
Unity使用一张纹理作为查找表来在片元着色器计算逐像素光照衰减。这张纹理_LightTexture0
。我们将这些纹理对应的颜色值 表明在光源空间下不同位置的点的衰减值。
例如:(0,0)表明光源位置重合的点的衰减值。(1,1)表明了光源空间中所关心的距离最远的点的衰减。
// unity_WorldToLight 是世界空间到光源空间的变换矩阵
float3 lightCoord = mul(unity_WorldToLight, float4(o.worldPos,1)).xyz;
// lightCoord 这个坐标的模的平方对衰减纹理进行采样
// UNITY_ATTEN_CHANNEL来得到衰减值所在的分量
fixed4 atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
前向渲染路径 和 Unity中的前后渲染
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
Shader "DY/Lighting/FowardRendering"{
Properties {
_Specular("Specular Color", Color) = (1, 1, 1, 1)
_Diffuse("Diffuse Color", Color) = (1, 1, 1, 1)
_Gross("Gorss", Range(8, 255)) = 8
}
SubShader{
pass {
Tags { "LightMode"= "ForwardBase"}
CGPROGRAM
// 保证我们在使用光照衰减等光照变量可以被正确赋值
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
fixed4 _Specular;
float _Gross;
fixed4 _Diffuse;
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(unity_ObjectToWorld, v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag(v2f o) : SV_Target{
// 我们希望环境光只计算一次,所以后面的Additional Pass就不需要计算了
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 如果场景中有多个平行光,Unity会选择最亮的平行光传递给Base Pass进行逐像素处理
// 其他的平行光会按照逐顶点 或 在Additional Pass中逐像素的方式进行处理
// 如果场景中没有平行光,Base Pass会被当成全黑的光源处理
// 对于Base Pass来说,它处理的逐像素光源一定是平行光。
// 所以:使用 _WorldSpaceLightPos0来获取平行光的方向,_LightColor0来获取它的颜色和强度
// 而且平行光 没有衰减,我们可以直接令 衰减值为1
half3 worldNormal = normalize(o.worldNormal);
half3 worldLight = normalize(_WorldSpaceLightPos0.xyz);
half3 worldViewDir = normalize(_WorldSpaceCameraPos.xyz - o.worldPos.xyz);
half3 halfDir = normalize(worldViewDir + worldLight);
// 计算漫反射
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLight));
// 计算高光反射
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gross);
// 衰减值
float atten = 1.0;
return fixed4(ambient + (specular + diffuse) * atten, 1.0);
}
ENDCG
}
pass {
// 处理其他的逐像素光源
Tags { "LightMode" = "ForwardAdd" }
// 如果没有Blend命令,Addtional Pass会直接覆盖掉之前的光照信息
// 这个Blend命令就是 在帧缓存中 让该Pass通道的光照信息 与 之前的光照信息进行 叠加
Blend One One
CGPROGRAM
// 保证我们在ForwardAdd访问到正确的光照变量
#pragma multi_compile_fwdadd
// 该Pass与上面的内容差不多一样,需要去掉环境光、自发光、逐顶点光照、SH光照
// 我们需要添加一些对不同光源类型的支持
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
fixed4 _Specular;
float _Gross;
fixed4 _Diffuse;
v2f vert(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = mul(unity_ObjectToWorld, v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag(v2f o) : SV_Target{
// 我们需要添加一些对不同光源类型的支持,以及计算它们的 位置、方向、颜色、强度、衰减
// 颜色和强度:我们通过 _LightColor0来获得
// 不同光源的方向
// 如果当前 前向渲染pass处理的光源是平行光,Unity底层渲染引擎会定义USING_DIRECTIONAL_LIGHT
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - o.worldPos.xyz);
#endif
// 处理不同光源的衰减
// _LightMatrix0 从世界空间到光源空间的变换矩阵
// Unity使用一张纹理作为查找表(Lookup Table,LUT),在片元作色器中得到光源的衰减
// 我们首先获得光源空间下的坐标,然后对该坐标对衰减纹理进行采用得到衰减值
#ifdef USING_DIRECTIONAL_LIGHT
float atten = 1.0;
#else
float3 lightCoord = mul(unity_WorldToLight, float4(o.worldPos,1)).xyz;
fixed4 atten = tex2D(_LightTexture0, dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
half3 worldNormal = normalize(o.worldNormal);
half3 worldViewDir = normalize(_WorldSpaceCameraPos.xyz - o.worldPos.xyz);
half3 halfDir = normalize(worldViewDir + worldLightDir);
// 计算漫反射
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal, worldLightDir));
// 计算高光反射
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gross);
return fixed4((specular + diffuse) * atten, 1.0);
}
ENDCG
}
}
}
Unity阴影
参考文章:https://zhuanlan.zhihu.com/p/579883552
阴影映射算法的步骤
- 以光源为视点,生成场景的深度图。
- 如果摄像机的深度图中记录的表面深度大于阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是处于光源的阴影中。
延迟渲染路径
延迟渲染的本质:先不要做迭代三角形做光照计算,而是先找出来你能看到的所有像素,再去迭代光照
G缓冲:延迟渲染有额外的缓冲区,这些缓冲区统称为G缓冲,它主要用于存储每个像素对应的位置(Position),法线(Normal),漫反射颜色(Diffuse Color)以及其他有用材质参数。
G-Buffer:包含4张纹理RT
RT0:格式ARGB32(4个通道都是32bit,RGB存储的是漫反射,A存储遮罩(默认为1)
RT1:格式ARGB32,RGB存放的是高光反射的颜色,A存放的是光泽度
RT2:格式ARGB2101010,(A通道2个bit,RGB都是10个bit)存储的是世界法线,A没有存东西
RT3:格式ARGB2101010,或者ARGBHalf(4个通道都是16bit),存储的是自发光,lightmap等
原理:它包含2个pass。
第一个pass:首先将场景渲染一次,获取到的待渲染对象的各种几何信息存储到名为G-buffer的缓冲区中,这些缓冲区用来之后进行更复杂的光照计算。 由于有深度测试,所以最终写入G-buffer中的,都是离摄像机最近的片元的集合属性,这就意味着,在G-buffer中的片元必定要进行光照计算。
第二个Pass:这个pass会遍历所有G-buffer中的位置、颜色、法线等参数,执行一次光照计算。
我的理解:第一个Pass是找G-Buffer,第二个Pass是渲染G-Buffer里的光照计算
值得注意的是:延迟渲染不支持透明物体的渲染
卡通着色
描边效果
物理空间外拓
Cull Front
....
// 顶点着色器
data.vertex.xyz = data.vertex + data.normal * _OutlineWidth;
视角空间外拓
float4 viewPos = float4(UnityObjectToViewPos(data.vertex), 1.0); // 视角坐标的点
float3 viewNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, data.normal)); // 视角坐标下的法线
viewPos = viewPos + float4(viewNormal,1.0) * _OutlineWidth;
o.vertex = mul(UNITY_MATRIX_P, viewPos);
裁剪空间外拓
颜色处理
float lightDot = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
lightDot = smoothstep(0, 1, lightDot); // 将颜色平滑在[0,1]之间
float toon = floor(lightDot * _Steps) / _Steps;
lightDot = lerp(lightDot, toon, _ToonEffect);
边缘光
// 视角方向与切线方向一样的时候,就是边缘光
// 视角方向与法线垂直时,就是边缘光
fixed3 rim = 1 - dot(worldNormal, worldViewDir);
fixed3 rimColor = _RimColor * pow(rim, 1 / _RimPower);
XRay实现遮挡效果
Tags { "Queue"="Geometry+1000" "RenderType"="Queue"}
pass{
Blend SrcAlpha One
ZWrite Off
ZTest Greater
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f{
float4 vertex :SV_POSITION;
float3 worldNormal: TEXCOORD0;
float3 worldPos: TEXCOORD1;
};
fixed4 _XRayColor;
float _XRayPower;
v2f vert(appdata_base data){
v2f o;
o.vertex = UnityObjectToClipPos(data.vertex);
o.worldNormal = UnityObjectToWorldNormal(data.normal);
o.worldPos = mul(unity_ObjectToWorld, data.vertex);
return o;
}
fixed4 frag(v2f data):SV_Target{
float3 worldNormal = normalize(data.worldNormal);
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(data.worldPos));
float rim = 1 - dot(worldNormal, worldViewDir);
fixed3 rimColor = _XRayColor * pow(rim, 1/_XRayPower);
return fixed4(rimColor, 1.0);
}
ENDCG
}
完整代码
Shader "DY/Cartoon/1"{
Properties{
_MainTex("MainTex", 2D) = "white"{}
_Diffuse("Diffuse Color", Color) = (1,1,1,1)
_OutlineColor("Outline Color", Color) = (1,1,1,1)
_OutlineWidth("OutlineWidth", Range(0,1)) =1
_Steps("Steps", Range(1,30)) = 1
_ToonPower("ToonPower", Range(0, 1)) = 0.5
_RimColor("RimColor", Color) = (1,1,1,1)
_RimPower("RimPower", Range(1,20)) = 1
_XRayColor("XRayColor", Color) = (1,1,1,1)
_XRayPower("XRayPower", Range(1,20)) = 1
}
SubShader{
Tags { "Queue"="Geometry+1000" "RenderType"="Queue"}
pass{
Blend SrcAlpha One
ZWrite Off
ZTest Greater
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f{
float4 vertex :SV_POSITION;
float3 worldNormal: TEXCOORD0;
float3 worldPos: TEXCOORD1;
};
fixed4 _XRayColor;
float _XRayPower;
v2f vert(appdata_base data){
v2f o;
o.vertex = UnityObjectToClipPos(data.vertex);
o.worldNormal = UnityObjectToWorldNormal(data.normal);
o.worldPos = mul(unity_ObjectToWorld, data.vertex);
return o;
}
fixed4 frag(v2f data):SV_Target{
float3 worldNormal = normalize(data.worldNormal);
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(data.worldPos));
float rim = 1 - dot(worldNormal, worldViewDir);
fixed3 rimColor = _XRayColor * pow(rim, 1/_XRayPower);
return fixed4(rimColor, 1.0);
}
ENDCG
}
pass{
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f{
float4 vertex :SV_POSITION;
};
float _OutlineWidth;
fixed4 _OutlineColor;
v2f vert(appdata_base data){
v2f o;
data.vertex.xyz = data.vertex.xyz + data.normal * _OutlineWidth;
o.vertex = UnityObjectToClipPos(data.vertex);
return o;
}
fixed4 frag(v2f data):SV_Target{
return _OutlineColor;
}
ENDCG
}
pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct v2f{
float4 vertex :SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos: TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Diffuse;
float _Steps;
float _ToonPower;
fixed4 _RimColor;
float _RimPower;
v2f vert(appdata_base data){
v2f o;
o.vertex = UnityObjectToClipPos(data.vertex);
o.uv = TRANSFORM_TEX(data.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(data.normal);
o.worldPos = mul(unity_ObjectToWorld, data.vertex);
return o;
}
fixed4 frag(v2f data):SV_Target{
fixed3 textureColor = tex2D(_MainTex, data.uv);
float3 worldNormal = normalize(data.worldNormal);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(data.worldPos));
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(data.worldPos));
float lightDot = dot(worldNormal, worldLightDir) * 0.5 + 0.5;
lightDot = smoothstep(0,1,lightDot);
float toon = floor(lightDot * _Steps) / _Steps;
lightDot = lerp(lightDot, toon, _ToonPower);
fixed3 diffuse = _LightColor0.rgb * textureColor * _Diffuse.rgb * lightDot;
float rim = 1 - dot(worldNormal, worldViewDir);
fixed3 rimColor = _RimColor.rgb * pow(rim, 1/_RimPower);
return fixed4(diffuse + rimColor,1);
}
ENDCG
}
}
}
评论区