Unity-Shader

Unity语境下的Shader基础知识 · 36 分钟阅读

  • Unity
  • Shader
  • 游戏
  • VR

Unity-Shader

Hi Shader | Unity 中文课堂

Mesh基本概念

具体见顶部链接==Chapter1==

  • Mesh网格:物体表面信息

    • Vertices 顶点数组

      • Vertex Data
        顶点数据

        • 顶点的位置
          (狭义的顶点数组)
        • Normal法线
        • Tangent切线
        • UV纹理(Texture)坐标
    • Topology拓扑类型

    • Indices索引数组

      • Index Data索引数据
      • 取决于Topology,
        存储了构成面的顶点的下标
  • MeshFilter

    • 存储Mesh网格
  • MeshRenderer

    • 从MeshFilter处获取Mesh信息,再获取Material,最后提交给渲染管线
  • Material

    • Shader
    • Texture

构建Mesh代码:

		               // 新建一个Mesh
		               Mesh mesh = new Mesh();
		               // 用构建的数据初始Mesh
		               mesh.vertices = vertices;//顶点坐标数组
		               mesh.triangles = triangles;//索引数组,使用的Topology默认是三角形
		               mesh.uv = uvs;
		               // 法线是根据顶点数据计算出来的,所以在修改完顶点后,需要更新一下法线
		               mesh.RecalculateNormals();
		               // 将构建好的Mesh替换上
		               gameObject.GetComponent<MeshFilter>().mesh = mesh;
  • 法线: Unity提供了 mesh.RecalculateNormals()​ 方法,通过顶点和三角形数据自动计算面法线并插值为顶点法线. 这是必要的,==因为法线直接影响光照计算,大多数渲染场景都需要它们==.

  • 切线: ==Unity==​**==没有内置的自动计算切线的方法==**(如 RecalculateTangents()​). 如果未显式设置 mesh.tangents​,==切线数组默认为空==. 此时:

    • ==若着色器需要切线,模型会因数据缺失出现渲染错误(如粉红色警告). ==
    • ==若着色器不需要切线(如未用法线贴图),则不影响基础渲染. ==

==Material和Texture的关系==

  • Texture 是“颜料” : 纹理提供具体的图案数据(如红色砖块、锈迹斑斑的铁皮).
  • Material 是“油漆配方” : 材质决定如何混合颜料(纹理)并添加其他特性(如反光强度、透明度).

示例: 同一张木纹纹理(Texture)可以用于两种材质(Material):

  1. 哑光木质:低光滑度(Smoothness = 0.2),无金属(Metallic = 0).
  2. 涂漆木质:高光滑度(Smoothness = 0.8),叠加清漆反射效果.

==Texture只是Material的Shader所需的众多参数的一种==

渲染管线基本概念

具体见顶部链接==Chapter2==

  • 应用程序阶段(CPU):准备数据

  • 几何阶段(GPU)

    • ==顶点着色(可编程)==:根据顶点生成图元
    • ==几何着色(可选可编程)==:把简单图元变复杂
    • 裁剪:对图元进行裁剪,去点不可见部分等
  • 光栅化阶段(GPU)

    • 屏幕映射:裁剪空间 → NDC空间 → 屏幕空间
    • 图元装配
    • 光栅化:根据像素生成一个或多个片元
  • 像素处理阶段(GPU)

    • ==像素(片元)着色器(可编程)==:输出像素的色彩值
    • ==合并(可配置不可编程)==:筛选像素点

==数据 → 图元 → 片元 → 像素==

==图元:可以简单理解为它是渲染管线中所有点,线,面的统称==

==片元:是光栅化之后产生的候选像素点==,因为没有被画到屏幕上,不能被直接称为像素一个像素的最终结果可能是由多个片元来决定的,渲染管线为了细分,就单独创造了片元这个词来描述它,片元只是渲染管线的概

==像素:最后写到图像上的值==

==NDC:全称Normalized Device Coordinates==,一般来说裁剪完成后,会通过透视除法,将物体从裁剪空间 变换为==标准化设备坐标NDC==,透视除法是将裁剪空间中,顶点的4个分量都除以w分量,从裁剪空间转换到NDC. 它是一个长宽高取值范围为[-1,1]的立方体,之所以要转到NDC,是为了方面我们后面进行视口变换把它映射到屏幕空间,不过Unity已经帮我们都完成这些啦,所以具体的数学推算这里不做讲解,有需要的小伙伴这里推荐大家看一下闫令琪老师的 GAMES101: 现代计算机图形学入门 (ucsb.edu)

坐标空间

模型空间 以物体本身为原点的坐标空间,世界空间以世界的(0,0)为原点的坐标空间,视图空间相机为原点的坐标空间,描述的物体在相机的哪个位置,裁剪空间 顶点坐标乘以MVP矩阵之后所在的空间,屏幕空间 窗口屏幕上的二维像素坐标空间.

渲染管线和Mesh Renderer的关系:

==Mesh Renderer负责将== ==网格(Mesh)== ==和== ==材质(Material)== ==提交给渲染管线==

Shader基本概念

具体见顶部链接==Chapter2==

Shader比较学术的百科回答就是用来实现图像渲染的,==用来替代固定渲染管线的可编辑程序==. 其中==Vertex Shader(顶点着色器)主要负责顶点的几何关系等的运算==,==Pixel Shader(像素着色器)主要负责片元颜色==等的计算.

Unity中的Shader

  • Standard Surface Shader:标准表面着色器,基于物理实现
  • Unlit Shader:无光照着色器,最简单
  • Image Effect Shader:顶点片元着色器,实现后处理效果,针对整个屏幕画面
  • Compute Shader:计算着色器,需要大量并行计算时使用
  • Ray Tracing Shader:光线追踪着色器,适合模拟光学效果

概念之间的关系

Shader Lab

具体见顶部链接==Chapter2==

着色器语言(Shader Language) 是用于编写 着色器(Shader) 的专用编程语言,它直接控制图形渲染管线(Graphics Pipeline)中不同阶段的计算行为,例如顶点变换、光照计算、颜色混合等.

  • 主流着色器语言

    • GLSL

      • 基于OpenGL
    • HLSL/CG

      • 基于Direct3D

      • 可以看成同一种语言

  • ShaderLab

    • ==在HLSL/CG的基础上封装的只属于Unity的着色器语言==
// Shader 的路径名称  默认为文件名,也可以与文件名不同
Shader "Unlit/HiShader"
{
    // 属性 
    // Material Inspector显示的所有参数都需要在这里进行声明
    Properties
    {
        // 通常所有属性名都以下划线字符开头 _MainTex
        _MainTex ("Texture", 2D) = "white" {}
                                            
        // 比较常见的属性类型
        // ————————————————————————————————————————————————
		//属性名称(“Inspector显示的名称”,属性类型)= 初始值,纹理类型初始值后需要加{}
        _Integer ("整数(新版)", Integer) = 1
        _Int ("整数(旧版)", Int) = 1
        _Float ("浮点数", Float) = 0.5
        _FloatRange ("浮点数滑动条", Range(0.0, 1.0)) = 0.5
        // Unity包含以下内置纹理, 可以直接填充
        // “white”(RGBA:1,1,1,1)
        // “black”(RGBA:0,0,0,1)
        // “gray”(RGBA:0.5,0.5,0.5,1)
        // “bump”(RGBA:0.5,0.5,1,0.5)
        // “red”(RGBA:1,0,0,1)
        _Texture2D ("2D纹理贴图", 2D) = "red" {}
        // 字符串留空或输入无效值,则它默认为 “gray”
        _DefaultTexture2D ("2D纹理贴图", 2D) = "" {}
        // 默认值为 “gray”(RGBA:0.5,0.5,0.5,1)
        _Texture3D ("3D纹理贴图", 3D) = "" {}
        _Cubemap ("立方体贴图", Cube) = "" {}
        // Inspector会显示四个单独的浮点数字段
        _Vector ("Example vector", Vector) = (0.25, 0.5, 0.5, 1)
        // Inspector会显示拾色器拾取色彩RGBA值
        _Color("色彩", Color) = (0.25, 0.5, 0.5, 1)
        // ————————————————————————————————————————————————
                                            
        // 除此之外 属性声明还可以具有一个可选特性 用来告知Unity如何处理它们
        // HDR可以使色彩亮度的值超过1
        [HDR]_HDRColor("HDR色彩", Color) = (1,1,1,1)
        // Inspector隐藏此属性
        [HideInInspector]_Hide("看不见我~", Color) = (1,1,1,1)
        // Inspector隐藏此纹理属性的Scale Offset字段
        [NoScaleOffset]_HideScaleOffset("隐藏ScaleOffset", 2D) = "" {}
        // 指示纹理属性为法线贴图,如果分配了不兼容的纹理,编辑器则会显示警告. 
        [Normal]_Normal("法线贴图", 2D) = "" {}
    }
                                        
    // 子着色器 
    // 一个Shader至少有一个或者多个子着色器SubShader,这些子着色器互不干扰,且只有一个会运行
    // 在加载shader时Unity会遍历所有SubShader列表,并最终选择用户机器支持的第一个
    SubShader
    {
        // 可以通过Tags来向子着色器分配标签
        // 只可以写在SubShader语块内,不可写在Pass内
        /* 以键值对的形式存在,可以出现多个键值对
            Tags { 
                "TagName1" = "Value1"
                "TagName2" = "Value2"
                "TagName3" = "Value3"
                ...
                 }
        */
                                            
        // RenderPipeline: 声明子着色器是否与通用渲染管线 (URP) 或高清渲染管线 (HDRP) 兼容
        // 仅与 URP 兼容
        // Tags { "RenderPipeline"="UniversalRenderPipeline" }
        // 仅与 HDRP 兼容
        // Tags { "RenderPipeline"="HighDefinitionRenderPipeline" }
        // RenderPipeline不声明或任何其他值表示与 URP 和 HDRP 不兼容
        // ————————————————————————————————————————————————
                                            
        // Queue: 声明渲染队列
        // Tags { "Queue"="Background" } 
        // 最早被调用的渲染,用来渲染天空盒或者背景
        // Tags { "Queue"="Geometry" }   
        // 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的)
        // Tags { "Queue"="AlphaTest" }  
        // 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑
        // Tags { "Queue"="Transparent" }
        // 以从后往前的顺序渲染透明物体
        // Tags { "Queue"="Overlay" }    
        // 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)
        // ————————————————————————————————————————————————
                                            
        // RenderType: 用来区别这个Shader要渲染的对象是属于什么类别的. 
        // 设置渲染类型 用一种称为着色器替换的技术在运行时交换子着色器,用来区别这个Shader要渲染的对象是属于什么类别的
        // 这里表示非透明物体渲染
        Tags { "RenderType"="Opaque" }
        // 更多详细内容可参考官网文档 https://docs.unity.cn/cn/2021.3/Manual/SL-SubShaderTags.html
                                            
        // LOD (Level of Detail)
        // 根据设备性能动态降低Shader复杂度
        // 更多详细内容可参考官网文档 https://docs.unity3d.com/Manual/SL-ShaderLOD.html
        LOD 100
                                    
        // 每个子着色器由多个通道(Pass)组成,许多简单的着色器只使用一个通道,但想要一些更复杂的效果,着色器可能需要更多通道
        // 一个Pass就是一次绘制,可以看成是一个Draw Call,而Pass的意义在于多次渲染,
        // 如果你有一个Pass,那么着色器只会被调用一次,如果你有多个Pass的话,
        // 那么就是绘制多次,就相当于执行多次SubShader了,这就叫双通道或者多通道. 
                                            
        // Draw Call:其实就是CPU调用图像编程接口的渲染命令,CPU每次调用DrawCall,都需要向GPU发送许多数据啊、渲染状态等等,
        // 一旦CPU执行完应用阶段,GPU就会开始执行这次的渲染流程. 而GPU渲染的速度比CPU提交命令的速度要快的多,
        // 所以如果DrawCall数量过多的情况下,CPU需要进行大量的计算,进而就会导致CPU过载,影响游戏的运行效率. 
        Pass
        {
            CGPROGRAM
            // 声明顶点着色器
            #pragma vertex vert
            // 声明像素着色器
            #pragma fragment frag
            // 使雾生效
            #pragma multi_compile_fog
                                    
            // 引用CG的核心代码库
            #include "UnityCG.cginc"
                                    
            // 应用程序阶段结构体,CPU传递给GPU(顶点着色器)的数据结构
			//命名可以自定义
            struct appdata
            {
                // 参考:https://docs.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
                // POSITION 着色器语言的语义,标志了数据从哪里的缓冲区获取
                // 模型空间的顶点坐标
                float4 vertex : POSITION;
                // 模型的第一套UV坐标
                float2 uv : TEXCOORD0;
            };
                                    
            struct v2f//顶点着色器传输给像素着色器的数据结构
            {
                // UV
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                // SV_POSITION 当这个值需要作为输出值输出给系统用的时候 前面需要加SV_前缀
                // 当然因为有向下兼容的机制 不加也没啥太大问题
                float4 vertex : SV_POSITION;
            };
                                    
            // 在Properties中声明的参数要在这里相对应的定义后才可以使用
            sampler2D _MainTex;
            float4 _MainTex_ST;
                                    
            // 定义顶点着色器函数 函数名要与声明顶点着色器名称相同
			//这是着色器的核心实现
			//这是顶点着色器函数,那么参数就是CPU传给顶点着色器的数据类型,
			//返回值就是顶点着色器传递给像素着色器的数据类型
            v2f vert (appdata v)
            {
                v2f o;
                // 将顶点坐标从模型空间变换到裁剪空间
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Transforms 2D UV by scale/bias property
                // #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
                // 等价于v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // 简单来说,TRANSFORM_TEX主要作用是拿顶点的uv去和材质(这里指Texture)球的tiling和offset作运算,
                // 确保材质球里的缩放和偏移设置是正确的
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				//雾效相关,不用管                         
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
            //像素着色器函数,参数为v2f类型,返回值为最终的像素色彩值    
			//: SV_Target: 输出语义,表示返回值将写入渲染目标(如屏幕、Render Texture)                    
            // SV_Target可以视为COLOR ,虽说他也是作为输出值输出给系统的
            // 但它其实是告诉系统把输出的颜色值存储到RenderTarget中
            // 所以这里我们用SV_Target
            fixed4 frag (v2f i) : SV_Target
            {
                // 采样2D纹理贴图
                fixed4 col = tex2D(_MainTex, i.uv);
                // 应用雾
                UNITY_APPLY_FOG(i.fogCoord, col);
                // 返回经过处理后的最终色彩
                return col;
            }
            ENDCG
        }
    }
}

graph TD
    A[Shader路径Shader Unlit/HiShader] --> B["Properties{}"]
    A --> C["SubShader{}"]
  
    B --> D[定义参数]
    C --> E["Tags{键值对}"]
    C --> M[LOD]
    C --> F["Pass{}  CGPROGRAM...ENDCG"]
  
    F --> G[顶点着色器函数]
    F --> H[片元着色器函数]
    F --> I[结构体]
    F --> N[重复声明
	    Properties
	    中的变量]
  
    G --> J[坐标变换]
    H --> K[颜色计算]
  
    classDef root fill:#F0F8FF,stroke:#333
    classDef props fill:#E0FFFF,stroke:#333
    classDef subshader fill:#98FB98,stroke:#333
    classDef pass fill:#FFDAB9,stroke:#333
  
    class A root
    class B props
    class C subshader
    class F pass
  • ==Properties属性(变量)==定义:[可选特性]_name ("display name", type) = "defaulttexture"

    • 如果是纹理属性,那么末尾加一个 {}
  • T==ags用于标记子着色器的配置等特性==,定义:

    • Tags { "TagName1" = "Value1" "TagName2" = "Value2" "TagName3" = "Value3" ... }
  • ==Pass通道(Shader核心)==​==,就是一次绘制流程==,具体示例见上述代码

  • ==LOD:Level of Detail,根据设备性能动态降低Shader复杂度==

卡通渲染

  • NPR(Non-Photorealistic Rendering):非真实感渲染

    • 卡通渲染(应用最广)
    • 油画渲染
    • 像素感渲染
    • 素描画
    • 水墨画
  • 卡通渲染

    • 轮廓描边

      • 渲染轮廓线的方式有很多种, 在这里带大家熟悉其中最简单的一种, ==对物体做两次渲染, 第二次渲染时开启正面剔除,将顶点沿法线向外延深一段距离,(放大物体),实现轮廓线==,这里就用到我们之前提到的==多Pass渲染==
    • 色阶

      • 通常来说都是由它来==决定画面色彩的丰富度饱满度精细度==,而大部分卡通渲染习惯==降低色阶==,用简单的明暗关系来描述世界,使画面扁平又不失层次感,这里还是用上节讲的==half Lambert光照模型==,不过这看起来一点都不卡通,我们需要让它==明暗分明==一点
    • 高光

      • ==用相机的位置减去世界位置得到视向量,也就是当前物体表面指向摄像机的方向,由于反射不太好算,所以这里通过 视向量 和 光照方向 得到角平分线,也就是半程向量. 通过 法线方向 点乘 半程向量 就可以得到 法线 和 半程向量 的 夹角,由此就 可以推断出 视向量 和 反射向量 的 接近程度==,用 noh 来 计算高光 的 亮度值,而这个参数 SpecPow 则是 控制高光的 光泽度,也就是 高光 亮斑的 范围,和色阶同样,用smoothStep来做个柔边的效果再把高光颜色和强度值加上,最后我们把漫反射和高光混合,就可以来调试效果啦.
    • 边缘光

      • 首先我们需要得知哪里是我们看到的边缘,==当我们的视向量和法线向量的夹角越接近直角时它就越靠近边缘,先拿到视向量和法向量的夹角,就可以看到,越是接近边缘的地方越暗,但边缘光一般都是越接近边缘越亮,所以给 1- 反转一下==,但正常来说阴影部分是不应该有边缘光的,所以要把漫反射加一下,那到至此边缘光就正确啦

具体代码见顶部链接==Chapter3==

Skybox与Shader

  • ==Skybox结合Shader==

    • 而天空本身可以包含任何无法触及的像如太阳月亮星星这些对象,来营造遥远的空间视觉感. ==本质上是把场景放置在天空的立方体内,在渲染场景前,Unity会首先渲染这个立方体由于是从里向外看,所以我们的天空永远在做背面渲染==,在Unity中通常将它称为SkyBox 天空盒.

    • Skybox与替换Shader:==Windows-> Rendering -> Lighting -> Environment==,在Environment面板替换**==Skybox Material==**

      先创建Shader,再右键Shader创建Mateiral,Material就会自动绑定该Shader

  • ==RayMarch概念==

    • ==RayMarch中文的意思就是光线步进==,可以简单理解为光==线在一步一步的向前行进==,==核心理念呢就是以相机为原点,向屏幕上的每一个像素,发射一条射线,射线按照一定的步长向前步进,这个所谓的步长呢,就是每步的Size==,==检测当前光线与物体表面之间的距离 然后根据这个距离 调整光线的步长,直到抵达物体的表面,每向前步进一次 都会累积计算一些 我们所需的信息,当步进到某个状态的时侯,结束步进,根据最终返回的累积数据,对屏幕上的像素进行绘制==
  • ==Noise概念==

    • 它其实就是图像中一种==亮度或颜色信息的随机变化==,也可以说是 计算机图形学 中一类 随机算法. 通常我们会利用==噪声算法,混合色彩贴图、高度贴图、法线贴图、UV贴图等,去构造模拟自然界中各式各样的物体表面的纹理材质细节==
  • 制作极光天空盒,代码等详见顶部链接==Chapter4==

PBR与Standard Shader

PBR

  • PBR(Physically Based Rendering)

    • PBR是 Physically Based Rendering的简称,也就是==图形渲染中的真实感渲染==,简单理解就是基于物理的渲染技术,但==它只是对现实世界物理的一种近似,并非完全的物理渲染==,与之前学习的NPR属于完全不同的两种渲染理念,PBR是以==基于微平面理论,能量守恒,以及物理的BRDF为核心==的渲染理念.
  • 微平面理论

    • 简单来说就是==物体表面持续放大至微观尺度后,任何平面都是由 无数微小的无序随机朝向的镜面组成==.

      平面越是粗糙,这个平面上的微平面的排列就越混乱,平面越是光滑,光线大体上会更趋向于同一个方向反射,形成更小更锐利的反射.

      其实在微观尺度下,没有任何平面是完全光滑的. 但由于这些微平面已经微小到无法逐像素继续进行区分,因此会假设一个粗糙度参数,然后用统计学的方法来估算微平面的粗糙程度,通过这个粗糙度计算出某个向量的方向,与微平面平均取向的一致性.

      一般粗糙度越高的显示出来的镜面反射的轮廓要更大一些,相反则会更小更锐利. ==在实际的PBR 中,这种物体表面的不规则性一般会用粗糙度贴图或者高光度贴图来表示==~

  • 能量守恒

    • 说白了就是==反射光的能量永远不能超过入射光的能量,为了遵守能量守恒定律,通常我们会对漫反射光和镜面反射光之间做出明确的区分==.

      ==在PBR中==,==镜面光指的就是光线射入物体表面时所反射的那部分能量==,而==漫反射光则是指光线进入物体后再均匀的发散出来的那部分能量==. 当光线碰撞到一个表面的时候,一部分能量被反射,另一部分能量进入物体内部.

    • ==不过不同的材质表面它的反射细节也会有所不同,通常会分为金属和非金属材质==,其中==金属表面由于有自由电子,所以进入表面的能量会被完全吸收(所以漫反射光就少)== . 而==非金属表面由于没有自由电子,进入表面的能量有一部分会被均匀的释放出来(所以漫反射多)== ,所以==金属度低,但是完全光滑的物体,不会呈现金属那样的镜面效果,而是带有自身的颜色晕染==.

    • 除此之外对于==玉石这类 次表面 散射的 材质==来说,能量进去后,会吸收一部分,而余下的能量会从正面出来一部分,从侧面也漏出来一部分.

    • 那对于玻璃这类透明的材质来说,能量进去后,吸收的部分很少,正面出来也很少,基本都会从另一面出来.

  • 菲涅尔现象

    • 一般来说==漫反射光和镜面反射光之间都是受菲涅尔参数所控制==,通过菲涅尔我们可以根据观察的角度得到被反射的能量所占的百分比. 利用这个反射比率和能量守恒原则,可以直接得出光线进入物体的部分以及光线剩余的能量.

      ==菲涅尔现象表示的是看到的光线的反射率与光线射入角度相关的现象,射线垂直于表面时反射较弱,而当射线与表面夹角越小时,反射越明显. ==

  • PBR真实感光照

    • ==PBR的光照不止是直接光,如果只是直接光,那PBR渲染出来的画面,其实和以前并没有多少明显的进步. 通常我们会通过叠加计算直接光和间接光得到最终的光照结果==,这个结果被称为==全局光照==,也就是常听到的==GI==.
    • ==说白点就是通过模拟光线的传播路径,将物体反射的间接光纳入计算,从而提高画面的真实感. ==直接光就是光线从光源直接照射到物体表面上的光,而==间接光则是光线经过多次弹射到物体表面上的光==. 不过因为我们无法完全模拟现实中如此复杂的物理环境,像如阳光的大气折射与场景弹射次数和质量等,==所以通常会加入==​==环境光==​==去做近似模拟. ==
    • ==这也是PBR中比较重要的光照部分 -== ==IBL== ==也就是==​==基于图像的光照==. 它会==把周围的环境颜色完整的保存到一张贴图上,==​==PBR材质会把这个贴图当作光源来进行采样==​==,通过一系列的公式计算得到最终的==​==环境光==. 如果是金属材质,并且粗糙度比较低,他就能够近乎完整的映射出周围的环境的镜像倒影. ==不过IBL不处理多次反射==,它只是单纯的把环境贴图当成了光源的预积分,这里不做过多赘述,有个大致概念即可.
  • BRDF

    • 学术一点的叫法就是双向反射分布函数,==通过入射光方向,反射光方向,平面法线以及微平面粗糙程度作为函数的输入参数,求出最终反射光的强度==. 其实我们之前做的Blinn-Phone也可以看作是一个BRDF,但由于Blinn-Phong并没有遵循能量守恒的定律,所以它不被认为是基于物理的渲染.

    • ==关于BRDF这里我们不去剖析这些复杂的公式,只对它最终的参数进行了解即可==~

    • ==最终==​==Disney BRDF得到了1个颜色参数以及10个标量参数==

      • BaseColor

        • 颜色参数,表面颜色,通常由纹理贴图提供
      • Subsurface

        • 使用 次表面近似 控制 漫反射的形状
      • Metallic

        • 金属与非金属两种不同模型之间的线性混合. 金属模型没有漫反射的成分
      • Specular

        • 一般用来控制镜面反射的强度
      • SpecularTint

        • 镜面反射的颜色啦,不过这个相对过时,通常很少有对美术控制的让步,一般会用于对基础色的入射镜面反射,进行颜色的控制.
      • Roughness

        • 用来控制表面的粗糙度
      • Anisotropic

        • 各向异性的强度,主要用于头发啊金属之类的材质
      • Sheen & SheenTint

        • 控制的是光泽的强度和颜色,一般会用于布料
      • Clearcoat & ClearcoatGloss

        • 则是清漆强度和光泽度,平时用到的很少,知道有这么个东西就好啦

==Standard Shader==

  • Rendering Mode

    • 控制的是物体的渲染模式,==简单理解就是设定物体是透明的渲染模式还是不透明的渲染模式==
  • Albedo

    • 其实就是==材质的基础固有色==,你可以给它一张纹理贴图也可以选择==单色==,当你选择==纹理贴图==的时候这个调色板则为纹理之上的==叠加色==
  • Metallic & Smoothness

    • 这里则是用来==控制材质的金属度和光滑度==,==金属度==只是决定了物体最终渲染的结果是==以高光为主,还是漫反射为主==. 而==光滑度==则是决定了物体的==高光部分是否清晰. ==通常来说==不使用金属度贴图的话材质只能具有单一的金属度==,这并不理想,毕竟同一个物体表面 可能会具有不同的表面特性,那这里==如果你选择了金属度贴图,这里的滑动条会直接隐藏掉,通过贴图所存储的 金属度信息来进行采样计算==
  • Source

    • 可以理解为就是选择==光滑贴图==,它的==数据可以来源于金属贴图,也可以是色彩贴图==,这就根据美术来定啦
  • Normal Map

    • ==法线贴图==,==它属于凹凸贴图的一种,通常用来给模型增加一些微小的凸起、凹槽和划痕等细节==,在逐像素计算光照时,每个像素都会根据该点的法向量来计算最终该点的光照结果,可以通过法线贴图,==改变这个点法线方向==,==影响它的光照结果,进而影响模型表面凹凸感==.
  • Height Map

    • ==视差贴图==,又可以称之为==高度贴图==,是==法线贴图的改进版==,属于一个经常被忽略的高级功能. 大家都知道法线,可以将一个平面做成凹凸不平的效果,但是当视角方向水平于该平面的时候,==理论上凸起的部分会遮挡住后面的部分,而法线贴图却没有这个效果,但是高度贴图==,就可以. 那原理呢,就是根据该点的高度以及该点指向摄像机的向量,计算出一个UV偏移,来影响之后的采样.
  • Occlusion

    • ==环境光遮蔽贴图==,通常又会叫==AO贴图==,在PBR中计算光照的时候,一般直接通过采样IBL来得到环境光,这个环境光是该点上一个半球上的积分. 但是因为自身的之间会有凹凸,==在凹陷的地方,环境光应该被周围给遮挡,所以看起来并不是那么亮,通过AO贴图我们可以让调整环境光的大小,从而达到更真实的效果==.
  • Emission

    • ==材质的自发光属性==,通常用于==控制从表面发出的光的颜色和强度==,用的比较多的地方像==如霓虹灯、LED屏幕==等等.
  • Tiling & Offset

    • ==控制的则是以上所有贴图的Tiling Offset==

Standard Shader的具体使用见顶部链接==Chapter5==

Unity的渲染管线的概念,升级与简单使用

Unity的渲染管线大致可分2大类,==内置渲染管线 Built-in==以及==可编程渲染管线SRP==,==之前的内容所接触到的全都是内置的渲染管线==.

Built-in内置渲染管线

它的==渲染流程实现,全部都是写在引擎源码里的==. 而大部分开发者是不会去修改源码的,那这基本上算是不能改动,所以过去的渲染管线对开发者来说,是很难进行定制开发的. 而==内置渲染管线在一个管线里面支持了所有的二十多个平台,高端的PC支持,很老的手机也要支持==,==结果就导致代码越来越臃肿,很难使性能和效果做到最好==.

Scriptable Render Pipeline

为了解决原本的内置渲染管线不够灵活的这一问题,Unity在2018之后提供了新的渲染系统 - SRP,全称就是Scriptable Render Pipeline

它可以在Unity里通过C# 脚本调用一系列的API配置和执行渲染命令 , 来实现一套自定义的渲染流程,你可以根据自己的需求来调整流程或着修改功能. ==相当于就是帮我们在复杂的底层图形API基础之上,封装了一套通俗易懂的C# API,本质上最后调用的还是底层的图形API. 那所谓的图形API就是我们之前提到的像如OpenGL、Direct3D这类的底层渲染API层. ==

其实SRP简单理解就像是我们的汽车产业,把所有东西都定死的成品车,拆成了各式各样的零部件,让你根据自己的需求来组装. 但这就又产生了另一个问题,SRP只丢给了我们一堆散装的零件,而这个拼装过程又非常复杂,很少有人能知晓整个拼装流程. ==这需要开发者对底层的渲染流程和图形API有足够的认知才可以实现. 无疑是增加了开发者的入门门槛,即使费了好大劲组装起来,安全上也无法保证. ==

URP & HDRP

为了解决这一问题,==Unity给我们提供了两个可以直接上路的解决方案URP 和 HDRP==. ==他们都是在SRP的API基础之上构建的渲染管线 解决方案==,你可以直接开车上路也可以根据自己的需求换个轮胎或者改个内饰.

其中==URP 全称是Universal Render Pipeline 通用渲染管线==,==早期被称为LWRP Lightweight RP 也就是轻量级渲染管线==, 在2019.3开始改名为URP,它涵盖了范围广泛的不同平台,是针对跨平台开发而构建的,==性能和画质都要比内置管线好==,另外还可以进行自定义,实现不同风格的渲染,那这也是目前最推荐大家使用的渲染管线.

而==HDRP(High-Definition Render Pipeline)高清渲染管线==,则是==针对高端设备下的高真实感图形和渲染,像如如 PC、Xbox 和 PlayStation 等高端硬件==.

那除此之外我们还可以在==Unity的官方文档看到他们之间详细的对比==,像如它们所支持的平台呀光照呀相机等相关得到信息,你都可以轻松的在这里找到想要的答案.

Built-in升级URP

在Window下打开Package Manager,选择Unity Registry,

找到HDRRP Package,点击安装就可以啦,

然后打开Window>Rendering>HDRP Wizard窗口

点击FixAll,Unity则会自动修复HDRP相关的Error

修复完成后会弹出一个框,选择CreateOne,创建HDRP Asset资源

那现在场景中的材质依然是101品红色,这是因为Shader还是用的内置渲染管线的Shader,

所以,这里点击HDRP Wizard窗口下面这栏的第一个按钮,将项目中的所有不兼容材质转换为 HDRP 材质,

转换完成后,将Sky Fog Volume添加到场景里用来设置环境照明,

打开Window > Rendering > Lighting,选择刚刚Volume的配置文件

下面这个静态光照天空属性,选择PhysicallyBasedSky,

调节一下光照色温

那简单的HDRP升级就完成啦,是不是一下就比原来细致了许多呢

image

Built-in 升级URP

在Window下打开Package Manager,选择Unity Registry,

找到URP Package,点击安装就可以啦,不过安装了并不代表我们现在用的就是URP管线,

需要在Assets目录下点击Create>Rendering>URP Asset,给它改个名字,创建完成后,

点击Editor>Project Settings>Graphics>Scriptable Render Pipeline Settings,选择刚刚创建好的URPAsset

然后会弹出一个对话框,点击Continue

我们发现设置完成后场景中的材质变成了我们之前所说的101色,那不要担心,接下来在Window>Rendering>Render Pipeline Converter 打开转换器

打开后选择Built-in to URP,然后选择要转换的内容

勾选完成之后点击左下角按钮进行转换 初始化设置

初始化完成之后点击 右下角按钮 进行资产转换,就可以啦

细心的小伙伴可能发现之前做的轮廓线不见了,通过官方的文档 URP ShaderLab Pass 标签 | Universal RP | 12.1.1 (unity3d.com) 可以看到,由于在URP里新增了特定的Pass tag,需要给Pass添加特定的Tag LightMode才可以进行多pass渲染

但其实URP 本身的设计思想并不鼓励在shader里写多pass,而是利用 Render feature 实现各种功能,那既然轮廓线只用到了两个pass,所以就简单的给pass加一下tag就好啦,

正常的渲染就用UniversalForward即可,

Pass
	{	
     Tags
   		{
     		"LightMode" = "UniversalForward"
        }
     }

像如描边这种需要在渲染对象时绘制额外的 Pass,这里就可以选择SRPDefaultUnlit

Pass
	{	
	Tags
		{
			"LightMode" = "SRPDefaultUnlit"
		}
	}

升级URP后阴影出现严重锯齿,解决:

升级URP后,URP管线设置在我们自己创建的URP Assert中,许多原本在Project Settings中的设置也会出现在这个Assert中

调整URP Assert-Shadow-Max Distance,将其调小到10左右即可

Unity知识记录–URP通用渲染管线调整阴影 - 哔哩哔哩

案例-URP水面效果

URP除了对管线的升级外,==ShaderLab也从原来的CG升级到了HLSL==,虽说有向前兼容,原来的CG也不是不能用,==但在升级管线之后,Unity SRP 使用的都是==​==HLSL 语言==​==,继续使用CG容易出现许多不知所以的问题==. 而且==URP在ShaderLibrary里面还提供了许多好用的API库==

那这里通过简单的水面效果来熟悉一下新的HLSL吧~ 其实它与之前的CG大体上还是差不多的,首先新建一个Unlit Shader,==把CGPROGRAM替换成HLSLPROGRAM==,==下面的End也记得换一下==

把==渲染管线标记为URP==,由于是水面效果,水是透明的,所以渲染类型也要改成透明,同样把它的渲染队列也标注为透明,最后给它加个透明度混合即可.

通常来说水深不同水面的颜色也就不同,因为水分子会吸收通过它的 光的能量,所以这里给它添加两个属性,一个深水色,一个浅水色

_ShallowWater ("shallowColor", Color) = (1.0, 1.0, 1.0, 1.0)
_DeepWater ("DeepColor", Color) = (1.0, 1.0, 1.0, 1.0)

那如何得知水的深浅呢?==这里则需要通过拿到场景的深度信息来计算水面的深度,在以往这是一个比较麻烦的步骤,而现在Unity给我们提供了许多好用的函数库,其中有一个就是深度相关的,这里我们直接给它include进来,把之前的CG库替换掉==

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"

看到下面的代码报错不要慌,这里fixed给它改成half就可以啦,==由于fixed精度过于低,所以hlsl干脆给它丢掉了,当然想要更高的精度也可以使用float==.

==函数报错呢也是因为更新了核心库,所以相关的API也变了==

那我们该怎么找呢,有点一头雾水. ==打开ShaderLibary里的核心库Core==

==搜索关键字,跳转着看看一般就可以找到了,你会发现所有的顶点位置信息都合并到了这一个结构体里啦,==

声明并且通过==get给它初始化一下==,这样我们的数据就全都拿好啦,是不是比之前的要方便许多呢,把之前报错的API删掉就可以啦,

VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS);

那深度纹理是全屏纹理,它和屏幕的尺寸相同. 而我们希望在当前像素相同的位置对深度进行采样. 所以,这里需要要把顶点在屏幕空间位置算好,

v2f vert(a2v v)
{
	v2f o;
	VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS);
	o.positionCS = positionInputs.positionCS;
	o.screenPosition = ComputeScreenPos(positionInputs.positionCS);
	return o;
}

通过引入的库函数来获取屏幕深度,然后把这个深度转换到视图空间.

half4 frag(v2f i) : SV_Target
{
	// 通过深度纹理的采样 计算屏幕深度
	float sceneRawDepth = SampleSceneDepth(i.screenPosition.xy / i.screenPosition.w);
	// 深度纹理的采样结果转换到视图空间下的深度值
	float sceneEyeDepth = LinearEyeDepth(sceneRawDepth, _ZBufferParams);
	return col;
}

由于关心的是这个深度值相对于水面有多深,所以需要把视图深度,减去模型顶点的深度,得到最终水的深度,然后把刚刚的 深浅水颜色 根据水的深度做个lerp,就可以啦

// 最终得到水的深度
float waterDepth = sceneEyeDepth - i.screenPosition.w; 
// 拿到水的颜色
float3 waterColor = lerp(_ShallowWater, _DeepWater, waterDepth);

为了使水多少有点流动感,这里通过noise给它做个简单的漂浮扰动效果,为了让它有流动效果所以给它加个时间和速度进行采样,

float surfaceSample = tex2D(_Surface, i.UV + _Time.y * _MoveSpeed * 0.1).r;

然后根据深度加一圈浮沫,和刚刚的波动混合一下,最后把水的颜色和浮沫的颜色叠加一下,把透明度公开出来,就大功告成啦

// 浮沫
float foam = saturate(waterDepth / _FoamDistance);
float surface = smoothstep(0, foam, surfaceSample) ;
// 混合水面透明度
float4 col = float4(waterColor + surface * _FoamColor, _WaterAlpha) ;
return col;

完整代码

Shader "URP/Water"
{
	Properties
	{
		_ShallowWater ("shallowColor", Color) = (1.0, 1.0, 1.0, 1.0)
		_DeepWater ("DeepColor", Color) = (1.0, 1.0, 1.0, 1.0)
		_WaterAlpha("WaterAlpha",Range(0,1)) = 0.5
                                            
		_Surface("Surface ", 2D) = "white" {}
		_MoveSpeed("MoveSpeed",Range(0,1)) = 0.5
                                            
		_FoamDistance("Foam Distance",Range(0,10)) = 0.4
		_FoamColor("FoamColor", Color) = (1.0, 1.0, 1.0, 1.0)
	}
	SubShader
		{
		Tags
			{
			"RenderPipeline"="UniversalPipeline"
			"RenderType"="Transparent"
			"Queue"="Transparent"
			}
		Pass
		{
			Blend SrcAlpha OneMinusSrcAlpha
                                    
			HLSLPROGRAM
			#pragma vertex vert
			#pragma fragment frag
                                                
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
                                                
			float4 _ShallowWater;
			float4 _DeepWater;
			float _WaterAlpha;
                                    
			sampler2D _Surface;
			float4 _Surface_ST;
			float _MoveSpeed;
                                                
			float _FoamDistance;
			float4 _FoamColor;
                                    
			// 顶点着色器的输入
			struct a2v
			{
				float3 positionOS : POSITION;
				float4 uv : TEXCOORD0;
			};
                                    
			// 顶点着色器的输出
			struct v2f
			{
				float4 positionCS : SV_POSITION;
				float4 screenPosition : TEXCOORD0;
				float2 UV : TEXCOORD1;
				float2 distortUV : TEXCOORD2;
			};
                                                
			v2f vert(a2v v)
			{
				v2f o;
				VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS);
				o.positionCS = positionInputs.positionCS;
				o.UV = TRANSFORM_TEX(v.uv, _Surface);
				o.screenPosition = ComputeScreenPos(positionInputs.positionCS);
				return o;
			}
                                                
			half4 frag(v2f i) : SV_Target
			{
				// 通过深度纹理的采样 计算屏幕深度
				float sceneRawDepth = SampleSceneDepth(i.screenPosition.xy / i.screenPosition.w);
				// 深度纹理的采样结果转换到视图空间下的深度值
				float sceneEyeDepth = LinearEyeDepth(sceneRawDepth, _ZBufferParams);
				// 因为关心的是这个深度值相对于我们的水面有多深,所以需要把视图深度,减去模型顶点的深度
				// 最终得到水的深度
				float waterDepth = sceneEyeDepth - i.screenPosition.w; 
				// 拿到水的颜色
				float3 waterColor = lerp(_ShallowWater, _DeepWater, waterDepth);
                                                    
				float surfaceSample = tex2D(_Surface, i.UV + _Time.y * _MoveSpeed * 0.1).r;
                                                    
				// 浮沫
				float foam = saturate(waterDepth / _FoamDistance);
				float surface = smoothstep(0, foam, surfaceSample) ;
				// 混合水面透明度
				float4 col = float4(waterColor + surface * _FoamColor, _WaterAlpha) ;
				return col;
			}
		ENDHLSL
		}
	}
}
graph TD
    A[Shader路径Shader Unlit/HiShader] --> B["Properties{}"]
    A --> C["SubShader{}"]
  
    B --> D[定义参数]
    C --> E["Tags
	    {RenderPipeline: UniversalPipeline}
	    "]
    C --> M[LOD]
    C --> F["Pass{}  HLSLPROGRAM...ENDHLSL"]
  
    F --> G[顶点着色器函数]
    F --> H[片元着色器函数]
    F --> I[结构体]
    F --> N[重复声明
	    Properties
	    中的变量]
  
    G --> J[坐标变换]
    H --> K[颜色计算]
  
    classDef root fill:#F0F8FF,stroke:#333
    classDef props fill:#E0FFFF,stroke:#333
    classDef subshader fill:#98FB98,stroke:#333
    classDef pass fill:#FFDAB9,stroke:#333
  
    class A root
    class B props
    class C subshader
    class F pass

Tags内的键值对的键和值都要加双引号,这里不加是因为Mermaid会误识别

Shader Graph

可视化Shader Lab

魔法球案例,详见Chapter7