第4篇 Unity中各种shader - Unity 3D ShaderLab开发实践
15 Pass的通用指令开关
15.1 LOD
Unity LOD-Level of Detail(多层次细节)用法教程 - Chinar - 博客园 (cnblogs.com)
shader中的实现方式:
脚本中的控制:
using UnityEngine;
public class_SetShaderLOD : MonoBehaviour {
public Shader myShader;
// ...
private float val = 6;
void Update() {
myShader.maximumLOD = (int)val * 100; // 设定shader的最大LOD数值
}
}
内置的LOD示例:
15.2 渲染队列
RenderQueue会改变物体渲染的先后顺序,但并不会改变物体的空间位置。对于透明物体,混合材质,RenderQueue的数值对最终的渲染输出都比较敏感。
15.3 透明的产生
Alpha检测用于在fragment函数完成最终的计算之后,在即将写入到帧中之前,通过和一个固定的数值比较,来决定当前fragment函数的计算结果到底要不要写入到帧中,从而输出到屏幕。通过Alpha test可以实现半透效果绘制。
15.4 混合操作
与OpenGL的blend类似。还支持BlendOp选项,可以在通常的加操作之外执行Max,Min,Sub以及RevSub这4种操作。
15.5 Color Mask
colormask的作用是指定渲染结果的输出通道,而不是通常情况下的RGBA这四个通道都会被写入。
15.6 ZTest 深度测试
ZTest:深度测试,开启后测试结果决定片元是否被舍弃,可配置
ZTest可设置的测试规则:
- ZTest Less:深度小于当前缓存则通过
- ZTest Greater:深度大于当前缓存则通过
- ZTest LEqual:深度小于等于当前缓存则通过
- ZTest GEqual:深度大于等于当前缓存则通过
- ZTest Equal:深度等于当前缓存则通过
- ZTest NotEqual:深度不等于当前缓存则通过
- ZTest Always:不论如何都通过
15.7 对Z深度的偏移
Unity Shader - Offset 的测试,解决简单的z-fighting情况_Jave.Lin 的学习笔记-CSDN博客
z-fighting(直译,就是z值的竞争)
原因是因为我们的不同的多边形共面时,在光栅阶段生成的fragment的屏幕xy坐标一样,但depth值又不一样的浮点误差引起的问题,然后浮点round-off的四舍五入,导致z-fighting
glPolygonOffset(GLfloat factor, GLfloat units);
当启用Offset的时候,每个片段的深度都需要加上一个offset值,offset操作需要在进行深度测试和深度写入之前执行。offset的值的计算公式如下:
offset = m * factor + r * units
其中,m是多边形最大的深度斜率,r是窗口坐标系下深度可识别的最小分辨率,r是特定实现的常量。
当offset > 0的时候,好比将物体推动远离你;offset < 0是,将物体拉近你。遍历多边形时,depth slope是z值的改变量除以x或y轴的改变量,depth是窗口坐标系下[0,1]的范围,
15.8 面的剔除操作
【Unity Shader】 Cull(表面剔除)_赞美月亮的专栏-CSDN博客
15.9 自动贴图坐标的生成
15.10 抓屏操作
15.11 Fog
17 SurfaceShader
可以通过exclude_path排除特定的renderpass,如:
lighting model见:Custom Lighting models in Surface Shaders_Passion 的博客-CSDN博客。
17.3 Forward渲染路径下的SurfaceShader
常规的代码实现如下:
Shader "forwardSurf" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Bumpmap", 2D) = "bump" {}
_ColorTint ("Tint", COlor) = (1.0, 0.6, 0.6, 1.0)
_FogColor ("Fog Color", Color) = (0.3, 0.4, 0.7, 1.0)
}
SubShader {
Tags {"RenderType"="Opaque"}
LOD 200
CGPROGRAM
#pragma surface surf LitModel exclude_path:prepass vertex:vert finalcolor:mycolor
#pragma debug
sampler2D _MainTex;
sampler2D _BumpMap;
fixed4 _ColorTint;
fixed4 _FogColor;
struct Input {
float3 viewDir;
float4 cc:COLOR;
float4 screenPos;
float3 worldPos;
float3 worldRef1;
float3 worldNormal;
float2 uv_MainTex;
float2 uv_BumpMap;
half fog;
INTERNAL_DATA
};
// 此函数会在Vertex函数中调用
void vert(inout appdata_full v, out Input o)
{
float4 hpos = mul(UNITY_MATRIX_MVP, v.vertex);
o.fog = min(1, dot(hpos.xy, hpos.xy)*0.1);
}
// Fragment中调用
void surf(Input IN, inout SurfaceOutput o) {
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
half4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb;
o.Alpha = c.a;
}
half4 LightingLitModel(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)
{
#ifndef USING_DIRECTIONAL_LIGHT
lightDir = normalize(lightDir);
#endif
viewDir = normalize(viewDir);
half3 h = normalize(lightDir + viewDir);
half diff = max(0, dot(s.Normal, lightDir));
float nh = max(0, dot(s.Normal, h));
float3 spec = pow(nh, s.Specular*128.0) * s.Gloss;
half4 c;
c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * (atten * 2);
c.a = s.Alpha + _LightColor0.a * Luminace(spec) * atten; //????
return c;
}
void mycolor(Input IN, SurfaceOutput o, inout fixed4 color)
{
color *= _ColorTint;
fixed3 fogColor = _FogColor.rgb;
#ifdef UNITY_PASS_FORWARDADD
fogColor = 0;
#endif
color.rgb = lerp(color.rgb, fogColor, IN.fog);
}
ENDCG
}
Fallback Off
}
上面自定义的几个函数,vert在vertex shader中最先调用,生成的Input结果传递给vertex shader,得到v2f_surf结果传递给fragment shader,fragment shader中,SurfaceOutput传递给出了surf的计算结果,LightingLitModel计算出一个颜色,最后被mycolor进行修改,成为最终fragment shader的输出。
上面的实现是没有光照贴图部分的。
其他略。后面主要讲了surfaceshader编译后得到的vertex,fragment结果,TODO
18 凹凸材质
18.1 切空间
以你自己为中心所观察到的世界就是切空间。所谓切空间就是法线Normal(从脚到头的方向,Z),Tangent切方向(双眼直视正前方,X),Binormal(水平抬起你的右胳膊,Y),这三个矢量构造的空间。
18.2 凹凸贴图
法线贴图的信息是表示在切空间中,因此光照计算需要在切空间中进行。
18.2.1 计算到切空间的矩阵
常见的实现方式如下:
Shader "Bump_1" {
Properties {
_BumpMap ("BumpMap", 2D) = "white" {}
}
SubShader {
Pass {
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
sampler2D _BumpMap;
struct v2f {
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1; // 传递到Fragment Shader中的一个切空间光源方向
}
v2f vert (appdata_full v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
float3 binormal = cross(v.normal,v.tangent)*v.tangent.w;
float3x3 rotation = float3x3(v.tangent.xyz, binormal, v.normal); // 切空间矩阵
o.lightDir = mul(_World2Object, _WorldSpaceLightPos0).xyz; // 平行光
o.lightDir = mul(rotation, o.lightDir); // 光源转换到切向量空间
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=1;
float4 packedN = tex2D(_BumpMap, i.uv);
// 解DXT5nm压缩格式
float3 N = float3(2.0*packedN.wy-1,1.0);
N.z = sqrt(1-N.x*N.x-N.y*N.y);
float diff = max(0, dot(N, i.lightDir)); //计算漫反射
c = _LightColor0*diff;
return c*2; // 乘以2是为了和默认的Unity光照传统保持一致,这个是unity早期为了更容易的产生过度曝光人为加上去的
}
ENDCG
}
}
}
unity对切空间计算的支持:
struct v2f {
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1; // 传递到Fragment Shader中的一个切空间光源方向
}
v2f vert (appdata_full v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
TANGENT_SPACE_ROTATION;
o.lightDir = mul(_World2Object, _WorldSpaceLightPos0).xyz; // 平行光
o.lightDir = mul(rotation, o.lightDir); // 光源转换到切向量空间
return o;
}
float4 frag(v2f i):COLOR
{
float4 c=1;
float3 N = UnpackNormal(tex2D(_BumpMap, i.uv));
float diff = max(0, dot(N, i.lightDir)); //计算漫反射
c = _LightColor0*diff;
return c*2; // 乘以2是为了和默认的Unity光照传统保持一致,这个是unity早期为了更容易的产生过度曝光人为加上去的
}
在SurfaceShader中,surf函数中使用的lightDir和viewDir已经是切空间的值了。Unity中,无高光和有高光surface lightmode分别是Lambert和BlinnPhong。
18.3 Parallax Mapping 视差映射
UV偏移。
用来欺骗眼睛,模拟出高度的交换。
视角来决定UV偏差,通过与平面垂直的Z分量的大小对UV基于xy分量的偏移大小做出约束,使得视角与切平面角度较小时,UV的偏差更大,如下:
inline float2 ParallaxOffset(half h, half height, half3 viewDir)
{
h = h * height - height /2.0;
float3 v = normalize(viewDir);
v.z += 0.42;
return h * (v.xy/v.z);
}
18.4 Relief Mapping (地势映射)
能够提供小视角下,更深的效果。效果对比如下:
TODO: 文中对于原理介绍的还是比较清楚的。
19 卡通材质
19.1 描边
19.1.1 沿法线挤出轮廓
代码实现:
Shader "Outline_1" {
Properties {
_Outline("Outline", range(0,0.2)) = 0.02
}
SubShader {
Pass{//对物体进行描边
Tags{"LightMode"="Always"} // 总会执行,不会有光照处理
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert(appdata_full v) {
v2f o;
v.vertex.xyz += v.normal * _Outline; // 沿着法向量向外扩
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
return o;
}
float4 frag(v2f i) : COLOR
{
float4 c=0;
return c;
}
ENDCG
}
Pass {//对物体进行光照计算
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float4 _LightColor0;
struct v2f {
float4 pos : SV_POSITION;
float3 lightDir : TEXCOORD0;
float3 viewDir : TEXCOORD1;
float3 normal : TEXCOORD2;
};
v2f vert(appdata_full v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.normal = v.normal;
o.lightDir = ObjSpaceLightDir(v.vertex);
o.viewDir = ObjSpaceViewDir(v.vertex);
return o;
}
float4 frag(v2f i) : COLOR {
float4 c = 1;
float3 N = normalize(i.normal);
float3 viewDir = normalize(i.viewDir);
float diff = dot(N, i.lightDir);
diff = (diff + 1)/2;
diff = smoothstep(diff/12, 1, diff); // 用来调显示效果的
c = _LightColor0 * diff;
return c;
}
ENDCG
}
}
}
该方法对应如下问题:
- 两个物体的重叠区域,描边没有出现(因为我们在绘制的时候关闭了Z的写入);
- 物体轮廓的粗线并不是常量,而是和距离相机远近相关的值,距离越远越细;
- 我们是沿着法线方向挤出的轮廓,如果相邻面的法线彼此分离,那么最后的效果会是间断的;
- 正方位的时候,边界出现如下问题;
19.1.3 在相机空间中挤出
相关代码如下:
Pass {
Tags {"LightMode"="Always"}
Cull Front
ZWrite On
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
struct v2f {
float4 pos:SV_POSITION;
};
v2f vert(appdata_full v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
float3 norm = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
float2 offset = TransformViewToProjection(norm.xy);
o.pos.xy += offset * o.pos.z * _Outline;
return o;
}
float4 frag(v2f i) : COLOR
{
float4 c=0;
return c;
}
ENDCG
}
这种方式绘制的结果还是会有2,3,4的问题。
使用UNITY_MATRIX_IT_MV的原因以及推导见:
[坑]UNITY_MATRIX_IT_MV – Matrix 64 Blog,其中关键内容见下图:
上述描边的方法,是将顶点往外挤出一定距离实现的。还可以通过Z偏移来描边,也可以在绘制结果的基础上进行描边。
20 镜面材质
暂时没有这样的需求,TODO
21 半透明材质
光在半透明物体中沿着各个方向进行折射,反射,所以就产生一个必然结果,那就是光的方向性消失了,但是光在物体内部的所能前进的深度依然和物体的密度、物体到光源的距离密切相关。
Shader中提供了两个变量,一个用于控制物体到光源距离偏差,用来表达从哪个距离开始,光进入到了物体内部并开始衰减;另一个用来控制光在物体中的衰减速度,表示物体密度和透明度的控制。
22 体积雾
23 Wrap Model新解
24 面积光
25 体积光
26 材质替代渲染
Shader Replacement的存在目的是为了能够让我们以完全不同的另一套材质来渲染当前场景,从而达到特定的渲染效果,可以通过Camera的RenderWithShader和SetReplacementShader来做这件事情。
26.1 相机和渲染消息
渲染周期:
26.3 如何使用RenderWithShader方法
Unity提供了使用Camera实现场景中物体替换shader渲染的方法,来满足一些特殊需求。
public void RenderWithShader(Shader shader, string replacementTag);
public void SetReplacementShader(Shader shader, string replacementTag);
两种方法都在挂载在Camera上的脚本上调用,其中RenderWithShader 方法调用一次之后只渲染场景一次,SetReplacementShader方法调用之后会一直使用,直到调用ResetReplacementShader方法,则恢复所有物体的正常渲染。
通过接口中的replacementTag,来选择调用Shader中的那个SubShader。
RenderWithShader通常使用方式是,你需要一个新的相机,并且disable掉挂载的Camera组件,这个相机不会再发送OnPreCull, OnPreRender or OnPostRender 消息,最好结合RenderTexture使用。所以我使用它的方法基本就是将它作为一种触发性质的渲染方式,在你需要的时候调用一次渲染方法,渲染到RenderTexture并进行使用。
27 后期效果
Render To Texture渲染到纹理,利用这种特性, 可以实现各种各样难以在普通渲染过程中实现的华丽效果。
27.1 Graphics的两个方法
想要做后期屏幕效果,就必须使用Graphics的Blit和BlitMultiTap方法。和相机Render、RenderWithShader方法的不同之处在于,Graphics的这两个方法是在屏幕上又做了一个和屏幕大小一样的平面,对此平面使用第三个参数提供的材质,然后把第一个参数作为渲染替代材质的_MainTex,第二个Texture参数作为输出。Graphics的两个方法是渲染一个平面,相机的Render和RenderWithShader方法渲染的仍然是场景中的物体。
27.1.2 Blit方法的简单示例
_GraphicsFuncs/Lab_1
文件夹下的场景,右边相机显示场景的原始状态,还有一个屏幕,用来输出左边相机的内容。左边相机有一个Blit_3.cs脚本:
最后运行效果如下:
Blit_3脚本对应的内容为:
using UnityEngine;
using System.Collections;
public class Bilt_3 : MonoBehaviour {
public Material mat;
public Material displayMat;
public RenderTexture dstRT;
void Start () {
dstRT = new RenderTexture(Screen.width, Screen.height, 16);
displayMat.mainTexture = dstRT;
}
void OnRenderImage(RenderTexture src,RenderTexture dst)
{
src.wrapMode = TextureWrapMode.Repeat;
Graphics.Blit(src,dstRT,mat);
Graphics.Blit(dstRT, dst);
}
}
该脚本的作用是先通过mat,将src渲染到右半部平面需要的texture(此处为displayMat.mainTexture
,Graphics.Blit(src,dstRT,mat)
),然后将纹理绘制到屏幕中(Graphics.Blit(dstRT, dst)
)。
有半部分的矩形内,用Blit_1.shader进行绘制。
27.1.3 使用BlitMultiTap方法进行多重采样
Graphics-BlitMultiTap - Unity 脚本 API
可以用来做图像模糊的效果。