《Unity Shader入门精要》学习笔记

发布时间 2023-05-21 00:13:18作者: 二律背反GG

shaderLab语法

名字

第一行确定,用/分隔

Shader "Custom/MyShader" {  }

properties

定义了着色器所需的各种属性,这些属性会出现在材质面板中

properties {
    Name ("display name", PropertyType) = DefaultValue
}
// Name表示属性的名字,通常是下划线开始
// display name则是出现在材质面板上的名字
// PropertyType是它的类型

属性类型

属性类型 默认值的定义语法 例子
Int number _Int("Int", Int) = 2
Float number _Float("Float", Float) = 1.5
Range(min, max) number _Range("Range", Range(0.0, 5.0)) = 3.0
Color (num,num,num,num) _Color("Color", Color) = (1,1,1,1)
Vector (num,num,num,num) _Vector("Vector", Vector) = (2,3,6,1)
2D "defaulttexture" {} _2D("2D", 2D) = "" {}
Cube "defaulttexture" {} _Cube("Cube", Cube) = "white" {}
3D "defaulttexture" {} _3D("3D", 3D) = "black" {}

Subshader

每个shader文件都可以包含多个Subshader语义块,但最少要有一个。

当unity需要加载这个Unity shader时,unity会扫描所有的Subshader语义块,然后选择第一个能够在目标平台上运行的Subshader,如果都不支持的话,unity就会使用Fallback语义定义的Unity shader。
使用这种方式的原因是为了支持在旧的显卡上使用较低的着色器,而高级显卡上使用较多的着色器

SubShader {
    //可选的
    [Tags]
    
    //可选的
    [RenderSetup]

    Pass {
    }
    // Other Passes
}

shaderLab 状态设置

状态名称 设置指令 解释
Cull Cull Back/Front/Off 设置剔除模式:剔除背面/正面/关闭剔除
ZTest ZTest Less Greater/LEqual/GEqual/Equal/NotEqual/Always 设置深度测试时使用的函数
ZWrite ZWrite On/Off 开启/关闭深度写入
Blend Blend SrcFactor DstFactor 开启并设置混合模式

Pass标签

// 例子
Pass {
    Tags{
        "LightMode" = "ForwardBase"
        "RequireOptions" = "SoftVegetation"
    }
}
标签类型 说明
LightMode 定义该Pass在Unity的管线中的角色。只有定义了正确,才能得到一些内置光照变量,例如_LightColor0
RequireOptions 用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串。目前Unity支持的选项有:SoftVegetation。

Tags

// 例子
Tags {
    "Queue" = "Transparent"
    "RenderType" = "Opaque"
    "DisableBatching" = "True"
    "ForceNoShadowCasting" = "True"
    "IgnoreProjector" = "True"
    "CanUseSpriteAtlas" = "False"
    "PreviewType" = "Plane"
}
标签类型 说明
Queue 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以控制所有的透明物体在不透明物体之后渲染
RenderType 对着色器进行分类,假如这是一个不透明的着色器,或是一个透明的着色器等
DisableBatching 一些Subshader在使用Unity的批处理功能时会出现问题,例如使用了模型空间下的坐标进行定点动画
ForceNoShadowCasting 控制使用该Subshader的物体是否会投影阴影
IgnoreProjector 如果为True,name是使用该Subshader的物体将不会受Projector的影响。通常用于半透明物体
CanUseSpriteAtlas 当该Subshader是用于sprite时,将该标签设置为false
PreviewType 指明材质面板将如何预览该材质。默认情况下,材质将显示为一个球体

Fallback

保底指令,如果上面所有Subshader都跑不了,就走这个

着色器的选择

  • 如果要在非常旧的设备上运行,或者有明确固定需求的,用固定函数着色器
  • 如果想和各种光源打交道的,使用表面着色器,但需要小心它在移动平台上的性能表现
  • 如果光照数目非常少,比如只有一个平行光,使用顶点/片元着色器
  • 如果有很多自定义的渲染效果,使用顶点/片元着色器

坐标空间

  • 模型空间: 模型独立的空间,左手坐标系
  • 世界空间: 绝对的坐标空间,如果没有父节点,transform的坐标就是世界坐标,左手坐标系
  • 观察空间: 相机的空间,右手坐标系
  • 屏幕空间:二维的空间,由观察空间投影得到
  • 裁剪空间:目标是能够方便地对渲染图元进行裁剪
  • 切线空间:模型中的每一个顶点都对应一个空间,其中原点就是顶点坐标,x轴是切线方向t,z轴是法线方向,y轴是副切线方向

名词解释

  • Early-Z技术: 深度测试(比着色)提前执行的技术
  • DrawCall: 开发者通过图像编程接口发出的渲染命令

变量属性

纹理

float4 _MainTex_ST;

纹理类型的属性需要在后面加上_ST,ST表示缩放和平移,其中.xy是缩放值,.zw是偏移值

POSITION

POSITION:用来存储,模型在本地坐标下,模型空间中(objcet space)的顶点坐标,转换为剪裁空间坐标前的坐标,unity告诉我们的模型顶点坐标,没经过转换的。可用作定点着色器(vertex shader)的输入、输出;片元着色器(frag)的输入。

SV_POSITION:用来存储,模型在剪裁空间,投影空间中的位置信息,即把模型空间的定点坐标,转化为剪裁空间的坐标,可用作定点着色器(vertex shader)的输出;片元着色器(frag)的输入。

saturate 函数

x表示矢量,用处是把x截取在[0, 1]范围内

step 函数

第一个参数参考值,第二个待比较的数值,2大于1则返回1,否则返回0

一些常用的帮助函数

函数名 描述
float3 WorldSpaceViewDir(float4 v) 输入一个模型空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向。内部实现使用了UnityWorldSpaceViewDir函数
float3 UnityWorldSpaceViewDir(float4 v) 输入一个世界空间中的顶点位置,返回世界空间中从该点到摄像机的观察方向
float3 ObjSpaceViewDir(float4 v) 输入一个模型空间中的顶点位置,返回模型空间中从该点到摄像机的观察方向
float3 WorldSpaceLightDir(float4 v) 仅可用于前向渲染中。 输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部实现使用了UnityWorldSpaceLightDir函数。没有被归一化
float3 UnityWorldSpaceLightDir(float4 v) 仅可用于前向渲染中。 输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化
float3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染中。 输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化
float3 UnityObjectToWorldNormal(float3 norm) 把法线方向从模型空间变换到世界空间中
float3 UnityObjectToWorldDir(float3 dir) 把方向矢量从模型空间变换到世界空间中
float3 UntiyWorldToObjectDir(float3 dir) 把方向矢量从世界空间变换到模型空间中

纹理

texture

Wrap Mode

  • Repeat: 如果超过1,那么它的整数部分将会被舍弃,用小数部分采样,结果就是不断重复
  • Clamp: 如果纹理坐标大于1,将会截取到1,如果小于0,会截取到0
  • Mirror: 纹理采样时就是镜像处理,实现方式和repeat一样
  • Mirror One: 同上,但是实现方式和Clamp一样
  • Per-axis: 分别设置UV两个轴上的mode

Filter Mode

据诶方了当纹理由于变换而产生拉伸时将会采用哪种滤波模式

  • Point
  • Bilinear
  • Trilinear

这三个滤波效果一次提升,但是耗费的性能也依次增大

凹凸映射:模型空间和切线空间的法线纹理优劣对比

模型空间优点:

  • 实现简单,更加直观。而且不需要模型原始的发现和切线等信息,计算更少
  • 在纹理坐标的缝合处和尖锐的边角部分,可见的突变较少,即可以提供平滑的边界。

切线空间优点:

  • 自由度很高。模型空间下的法线纹理记录是绝对法线信息,仅可用于创建的那个模型,而切线的纹理应用到一个完全不同的网格上,也可以得到一个合理的结果
  • 可以进行UV动画。比如水体或者岩浆
  • 可以重用法线纹理。比如一个砖块,仅使用一张纹理就可以得到6个面
  • 可压缩。切线空间下的法线纹理中法线的Z方向总是正方向,因此我们可以仅存储XY方向,从而推导Z方向。

渐变纹理

使用渐变色的纹理。如果渐变色是突变的,可以实现卡通渲染效果

遮罩纹理

核心代码:

fixed specularMask = tex2D(_SpecularMask, i.uv).r * _SpecularScale;

用mask图片中的某个值,算出当前像素的掩码值,最后在高光反射里乘上

透明度测试

  • 透明度测试:和不透明片元一样,只是会根据透明度来判断是否舍弃片元
  • 透明度混合:真正的半透明效果

渲染队列

SubShader的Queue决定归于哪个渲染队列

名称 队列索引号 描述
Background 1000 会在任何其他对垒之前被渲染,通常使用该队列渲染背景
Geometry 2000 默认的渲染队列,大多数物体都使用这个队列。不透明物体使用这个队列
AlphaTest 2450 需要透明度测试的物体。在所有不透明物体渲染之后在渲染他们会更加高效
Transparent 3000 任何使用了透明度混合的物体都应该使用该队列
Overlay 4000 该队列用于实现一些叠加效果

透明度测试开启方法:

Tags { "Queue" = "AlphaTest" }

透明度混合开启方法:

Tags { "Queue" = "Transparent" }
Pass { ZWrite Off }
clip函数
如果参数小于零的话,会直接舍弃掉该片元的输出,类似return

透明度混合

使用当前片元的透明度作为混合因子,与已经存储在颜色缓冲中的颜色值进行混合,得到新的颜色。混合需要关闭深度写入,所以要非常小心物体的渲染顺序

blend命令

语义 描述
Blend Off 关闭混合
Blend SrcFactor DstFactor 开启混合,设置混合因子。片元的颜色 * SrcFactor + DstFactor * 缓存的颜色 = 新缓存的颜色
Blend SrcFactor DstFactor, SrcFactorA DstFactorA 同上,只是使用不同的因子来混合透明通道
BlendOp BlendOption 并非把两边的颜色简单相加后混合,而是使用BlendOption对它们进行其他操作

开启深度写入的半透明效果

当模型网格之间有互相交叉的结构时,往往会得到错误的半透明效果。

这时可以使用两个pass来渲染模型,第一个pass开启深度写入,但是不输出颜色,第二个pass再按照深度排序结果进行透明渲染。

Pass {
    ZWrite On
    ColorMask 0
}

常见的混合类型

  • Blend SrcAlpha OneMinusSrcAlpha
    • 正常,即透明度混合
  • Blend OneMinusDstColor One
    • 柔和相加
  • Blend DstColor Zero
    • 正片叠底,即相乘
  • Blend DstColor SrcColor
    • 两倍相乘
  • BlendOp Min + Blend One One
    • 变暗
  • BlendOp Max + Blend One One
    • 变亮
  • Blend OneMinusDstColor One = Blend One OneMinusSrcColor
    • 滤色
  • Blend One One
    • 线性减淡

渲染路径

LightMode标签选项:p181 表9.1
前向渲染可以使用的内置光照变量:p184 表9.2
前向渲染可以使用的内置光照函数:p185 表9.3

前向渲染

原理:渲染对象的渲染图元,并计算两个缓冲区的信息:颜色缓冲区和深度缓冲区。用后者决定图元是否可见,然后用前者更新颜色值

三种处理光照的方式:逐顶点处理、逐像素处理、球谐函数(SH)处理

光源处理方式:

  • 场景中最亮的平行光总是按照逐像素处理
  • 渲染模式被设置成Not Important的光源,会按照逐顶点或者SH处理
  • Important的光源,逐像素处理
  • 如果以上规则的得到的逐像素光源数量小于Quality Setting中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染

顶点照明渲染路径

特点:对硬件配置要求最少、运算性能最高,但是效果最差,且功能范围仅仅是前向渲染的子集

延迟渲染路径

前向渲染的问题:当场景中包含大量实时光源时,前向渲染的性能会急速下降
延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关

实现原理:除了颜色缓冲和深度缓冲外,还增加了G缓冲(G-buffer),存储了离相机最近的表面的其他信息,例如法线、位置、材质等

Pass 1 {
    // 第一个Pass不进行真正的光照计算
    // 仅仅把光照计算需要的信息存储到G缓冲中

    for (each primitive in this model) {
        for (each fragment coverd by this primitive) {
            if (failed in depth test) {
                // 如果没有通过深度测试,说明该片元是不可见的
                discard;
            } else {
                // G缓冲
                writeGBuffer(materialInfo, pos, normal)
            }
        }
    }
}

Pass 2 {
    // 利用G缓冲中的信息进行真正的光照计算

    for (each pixel in the screen) {
        if (the pixel is vaild) {
            // 如果像素有效,读取G缓冲的信息
            readGBuffer(pixel, materialInfo, pos, normal);

            // 根据读取到的新戏进行光照计算
            float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
            // 更新帧缓冲
            writeFrameBuffer(pixel, color);
        }
    }
}

缺点:

  • 不支持真正的抗锯齿功能
  • 不能处理半透明物体
  • 对显卡有一定要求

声明:UnityDeferredLibrary.cginc可以找到

文档:docs.unity3d/Manual/RenderingPaths.html

阴影技术

shader map

为了防止多余pass的计算,专门选择额外的pass来专门更新光源的阴影映射纹理:LightMode = ShadowCaster

通常我们不会每个shader单独写一个类型,会直接用【FallBack "Specular"】用默认的pass。详细实现在: builtin-shaders-xxx -> DefaultResourcesExtra -> Normal-VertexLit.shader

衍生:screenspace shadow map

接收阴影

宏:#include "AutoLight.cginc"

计算阴影三剑客:

struct v2f {
    float4 pos : SV_POSITION;
    float3 worldNormal : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
    // 声明了一个名为_ShadowCoord的阴影纹理坐标,因为上面坐标用到了0和1,所以这里传入2
    SHADOW_COORDS(2)
};

v2f vert(a2v v) {
    v2f o;
    ...
    // 不同平台有差异。如果没定义UNITY_NO_SCREENSPACE_SHADOWS
    // 则调用内置的ComputeScreenPos计算_ShadowCoord
    TRANSFER_SHADOW(o);
    return o;
}

fixed4 frag(v2f i) : SV_Target {
    ...
    // 使用_ShadowCoord对相关纹理进行采样,得到阴影信息
    fixed shadow = SHADOW_ATTENUATION(i);	
    return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0);

    ...
    //统一管理光照衰减和阴影
    // UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    return fixed4((diffuse + specular) * atten, 1.0);
}

注意:这些宏会使用上下文变量来进行计算,例如TRANSFER_SHADOW会使用v.vertex或a.pos来计算坐标。a2v的顶点坐标变量名必须是vertex,a2v实例必须命名为v,且v2f中的顶点位置变量必须为pos

透明度物体的阴影

如果使用VertexLit的话,会有问题,需要改成
FallBack "Transparent/Cutout/VertexLit"

因为它的实现里面用到了_Cutoff的属性来进行透明度测试,所以必须要提供这个参数

由于半透明的物体想要产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,会使阴影厨力变得非常复杂,所以在unity中,所有内置的半透明shader是不会产生任何阴影效果的

渲染纹理

  • RTT: 渲染目标纹理
  • MRT: 多重渲染目标

GrabPass

定义一个grabpass后,unity会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的pass中访问它。通常会用来实现诸如玻璃等透明材质的模拟

SubShader {
    // We must be transparent, so other objects are drawn before this one.
    Tags { "Queue"="Transparent" "RenderType"="Opaque" }
    
    // This pass grabs the screen behind the object into a texture.
    // We can access the result in the next pass as _RefractionTex
    GrabPass { "_RefractionTex" }
    
    ...

动画

p230

名称 类型 描述
_Time float4 t是自该场景加载开始所经过的时间,分量(t/20, t, 2t, 3t)
_SinTime float4 t是时间的正弦值,分量(t/8, t/4, t/2, t)
_CosTime float4 t是时间的余弦值,分量同上
unity_DeltaTime float4 dt是时间增量,分量(dt, 1/ft, smoothDt, 1/smoothDt)

注意事项:
1、DisableBatching会取消批处理,导致会带来一定的性能下降,增加了DrawCall
2、顶点动画不能用内置的pass渲染阴影,因为这个pass中没有进行顶点动画,最后的效果就是不动。需要自己添加ShadowCaster

后处理

通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行一系列操作实现各种屏幕特效,例如景深、运动模糊等

// PostEffectsBase.cs
[ExecuteInEditMode]
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {
    // 提前检查各种资源和条件是否满足
    protected void Start() {
		CheckResources();
	}

    // 后处理效果通常都需要指定一个shader来创建一个用于处理渲染纹理的材质
    protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
        ...
    }
}

// BrightnessSaturationAndContrast.cs
public class BrightnessSaturationAndContrast : PostEffectsBase {
	void OnRenderImage(RenderTexture src, RenderTexture dest) {
        // public static void Blit(Texture src, RenderTexture dest);
        // public static void Blit(Texture src, RenderTexture dest, Material mat, int pass = -1);
        // public static void Blit(Texture src, Material mat, int pass = -1);
        // dest是目标渲染纹理,如果为null将直接显示在屏幕上
        // pass默认值为-1,表示依次调用所有pass,否则只调用指定索引的pass
        Graphics.Blit(src, dest);
	}
}

注意事项:
1、所有屏幕后处理效果都需要绑定在某个摄像上
2、后处理使用的shader需要关闭深度写入。因为实际是在场景中绘制了一个与屏幕同宽同高的四边形面片,不关闭深度写入就会影响后面透明的pass的渲染
3、OnRenderImage会在所有不透明和透明的oass执行完毕后调用,如果希望在不透明pass执行完后立刻调用,可以在OnRenderImage函数前添加 ImageEffectOpaque 属性

边缘检测卷积核

  • Sobel
  • Prewitt
  • Roberts

模糊效果

  • 均值模糊: 卷积后得到的像素值是其邻域内各个像素值的平均值
  • 中值模糊: 领域内所有像素排序后的中值替换替换掉原来的颜色
  • 高斯模糊: 带权重的卷积核。距离越近,影响越大

bloom效果

泛光效果
首先按根据一个阈值提取出图像中的较亮区域,存储到一张渲染纹理中,再利用高斯模糊进行处理,模拟光线扩散效果,左后将其和原图像进行混合,得到最终的效果

运动模糊

使用速度缓存:创建一个缓存纹理保存之前的渲染效果,然后不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果

非真实感渲染(NPR)

卡通风格渲染

基于色调的着色技术:根据漫反射的稀系数,对一张一维纹理进行采样

渲染轮廓线

  1. 基于观察角度和表面法线的轮廓线渲染
  2. 过程式几何轮廓线渲染
  3. 基于图像处理的轮廓线渲染
  4. 基于轮廓边检测的轮廓线渲染
  5. 混合了上述的几种渲染方法

渲染优化技术

影响性能的因素

1、CPU

  • 过多的DrawCall
  • 复杂的脚本或者物理模拟

2、GPU

  • 顶点处理
    • 过多的顶点
    • 过多的逐顶点计算
  • 片元处理
    • 过多的片元
    • 过多的逐片元计算

3、带宽

  • 使用了尺寸很大且未压缩的纹理
  • 分辨率很高的帧缓存

对应的技术

1、CPU

  • 使用批处理技术减少draw call数目

2、GPU

  • 减少需要处理的顶点数目
    • 优化几何体
    • 使用模型的LOD(level of detail)技术
    • 使用遮挡剔除(occlusion culling)技术
  • 减少需要处理的片元数目
    • 控制绘制的顺序
    • 警惕透明物体
    • 减少实时光照
  • 减少计算复杂度
    • 使用shader的LOD技术
    • 代码方面的优化

3、带宽

  • 减少纹理大小
  • 利用分辨率缩放

批处理

同一个材质的物品,整合到一起,提交drawcall

动态批处理

不需要我们操作,unity会自动选择符合的对象。

优点:

  • 经过批处理的物体仍然可以移动

缺点:

  • 顶点属性有限制,超过某个值,就不行了(unity5是900)

静态批处理

对象的inspector右上方,勾上static

缺点:

  • 不能动
  • 会占用更多的内存,因为需要占用更多的内存来存储合并后的几何结构

LOD

根据物体和相机的距离,控制模型上面片的数量,甚至是调用不用等级的模型
unity中有LOD Group组件

其他

名词记录

  • 软阴影(soft shadows)
  • TBDR:基于瓦片的延迟渲染。PowerVR芯片(移动设备)上使用的渲染优化技术
  • PBS:基于物理的渲染技术

减少drawcall的开销

1、避免使用大量很小的网格。当不可避免地需要使用很小的网格结构时,考虑是否可以合并它们
2、避免使用过多的材质。尽量在不同的网格之间共用同一个材质

OpenGL和DirectX差异

OpenGL的(0, 0)点是在左下角,DirectX是在左上角

uniform关键字

是一种修饰词,仅仅用于提供一些关于该变量的初始值是如何制定和存储的相关信息。在unity shader中,这个关键词是可以省略的。

pass中判断是否是平行光

#ifdef USING_DIRECTIONAL_LIGHT
#else
#endif

光源类型

  • 平行光: 只有方向,没有具体位置,且光照强度不会衰减
  • 点光源
  • 聚光灯: 一点出发的锥体,有衰减

光照衰减计算

1、用纹理控制衰减。计算位置公式:

// 某一点在光源空间中的位置
float3 lightCoord = mul(_LightMatrix0, float4(i.worldPosition, 1)).xyz

// 得到衰减值
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;

2、数学公式计算衰减(线性)

float distance = length(_WorldSpaceLigthPos0,xyz - i.worldPosition.xyz);
atten = 1.0 / distance;  // linear attenuation

菲涅尔近似等式

\[F_{schlick}(v, n) = F_{0} + (1 - F_{0})(1 - v \cdot n) ^ 5 \]

\(F_0\)是菲涅尔反射强度,v是视角方向,n是表面法线

另一个

\[F_{schlick}(v, n) = max(0, min(1, bias + scale \times (1 - v \cdot n) ^ {power})) \]

bias、scale和power是控制项

Materials中的instance

材质名称后面带instance,表示这个是代码生成的实例。

除此外,还有一类专门使用程序纹理的材质,叫做程序材质,使用Substance Designer软件在unity外部生成的,后缀名是.sbsar

开根号的替代

因为开根号耗性能,所以有时会用绝对值来替代

\[\sqrt{a^2 + b^2} \Rightarrow \vert a \vert + \vert b \vert \]

伽马校正

来自伽马曲线。因为人眼对于教暗区域的变化比较敏感,比如亮度【0.5-1】和【100 - 100.5】,人眼对前者就更加敏感,所以在存储图片时,过多的存储较亮的区域就是一种浪费,为了节省区域,用了指数函数的形式存储。

而我们在工作是通常用的是线性空间,如果直接用伽马值,亮度会偏暗,所以需要还原到正常的值,这个操作就是伽马校正

HDR和LDR

高动态范围和低动态范围

在游戏场景中,可能会存在超出亮度存储值的物体,比如直视太阳,亮度通常用8位存储,这个亮度显然超过了,而HDR提供了高精度亮度存储位数(32位),可以让亮度大于1

链接