文章

Unity Build-in 渲染管线

Build-in渲染管线的基本知识,同时包含部分坐标转换的知识

注:此基于内置渲染管线(build-in),使用CG语言

Unity Shader

关于坐标

Pasted image 20240714213901.png

  • 注意 clip space与NDC之间的区别容易混淆的Clip Space vs NDC,透视除法

    剪切空间与归一化设备坐标【NDC】

  • 注意三个坐标之间的区别:Clip Space, NDC, Screen Space
    1. Cilp Space: 是顶点着色器输出的坐标, 是将view space的坐标进行裁剪,并投影变换后的坐标,其范围为[-w, w]的1立方体。
    2. NDC: 是将Clip Space的坐标应用透视除法并进行归一化后的坐标,(Direct中)其范围为x,y[-1,1],z轴的范围为[0,1]的立方体空间
    3. Screen Space: 是将NDC坐标映射到屏幕坐标的坐标,其范围为屏幕的宽高,一般为[0, width]和[0, height]
      • (Vertex Shader) => Clip Space => (透视除法) => NDC => (视口变换) => Screen Space => (Fragment Shader)
  • 注意:在URP提供的GetVertexPositionInputs函数中,positionNDC与实际的NDC坐标有所不同,其z轴的范围为[-w, w],而不是[-1,1],实际只对x,y进行了一定缩放将其从[-w,w]缩放到[0,w]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      VertexPositionInputs GetVertexPositionInputs(float3 positionOS)
      {
      	VertexPositionInputs input;
      	input.positionWS = TransformObjectToWorld(positionOS);
      	input.positionVS = TransformWorldToView(input.positionWS);
      	input.positionCS = TransformWorldToHClip(input.positionWS);
    
      	float4 ndc = input.positionCS * 0.5f;
      	input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
      	input.positionNDC.zw = input.positionCS.zw;
    
      	return input;
      }
    
  • 注意:Unity在生成深度图时使用的深度是非线性的高精度浮点,其实就是_CameraDepthTexture.r = input.positionNDC.z / input.positionNDC.w。可以使用LinearEyeDepth(offsetDepth, _ZBufferParams)将其转换为线性深度。

  • View Space -> Clip Space: \(\begin{aligned}\boldsymbol{p}_{clip}&=\boldsymbol{M}_{projection}\boldsymbol{p}_{view}\\&=\begin{bmatrix}\frac{\cot\frac{FOV}{2}}{Aspect}&0&0&0\\0&\cot\frac{FOV}{2}&0&0\\0&0&\frac{Far+Near}{Far-Near}&-\frac{2.Near\cdot Far}{Far-Near}\\0&0&-1&0\end{bmatrix}\begin{bmatrix}x\\y\\z\\1\end{bmatrix}\\&=\begin{bmatrix}\cot\frac{FOV}{2}\\x\frac{\cot\frac{FOV}{2}}{Aspect}\\y\cot\frac{FOV}{2}\\-z\frac{Far+Near}{Far-Near}-\frac{2.Near\cdot Far}{Far-Near}\\-z\end{bmatrix}\end{aligned}\)

其中,$FOV$为视野角度(视锥的全顶角),$Aspect$为宽高比,$Near$和$Far$分别为近裁剪面和远裁剪面的距离。在viewspace - > clip space过程中,x,y分量做了缩放,使其坐标从矩形范围映射到一正方形范围内,z分量做了缩放与平移,其表示的距离是非线性的,z越大,缩放越大。w分量为-z,用于透视除法。

  • 关于Linear01Depth/LinearEyeDepth/rawDepth:

关于渲染流水线

这里是指前向渲染

  • ref : https://blog.csdn.net/qq_30100043/article/details/125883016

  • 前向渲染流程

cpu上进行剔除和排序,剔除为相机的视椎体剔除,将相机视角外的模型剔除掉,不再渲染。遮挡剔除,将视椎体内不可见的模型剔除掉,不再渲染。排序为了保证性能,不透明为从近到远,减少不必要的渲染,半透明物体为了保证正确性,从远到近。

cpu上获取到所需的相关模型数据:顶点数据 贴图数据 还有材质属性,将数据提交给gpu,并调用gpu进行渲染,每调用渲染一次,为一次drawcall。

在gpu上,分为两部分,一部分是顶点着色器,另一部分是片元着色器。 在顶点着色器里,主要是位置变换,将顶点的位置,依次是 模型空间 世界空间 视线空间 裁剪空间 这个过程是 MVP矩阵变换。

在顶点着色器到片元着色器中间还有一个过程,这个过程中,将进行裁剪剔除,将不在屏幕中的片元裁剪到只在裁剪显示空间内的,显示范围为[-w, w],在这里得到的四维变量 xy是裁剪空间下的xy坐标,z是深度,w就是w,注意这里,因为获取屏幕uv也需要在这一步操作,而且不同平台的兼容的问题也需要从这一步开始出现,因为深度也就是z从这一步根据平台也不同。然后进行NDC(归一化设备坐标系),归一化操作,在unity里面,xy限制在0到w之间,而深度z,根据平台不同,则为-w到w或者0-w。NDC操作完成之后,将进行背面剔除,将一些无法看到图元(主要是三角形面,点和线不需要)给剔除掉。图元三角形的三个点如果是顺时针渲染的为背面,将被剔除,逆时针渲染的面为正面,将被渲染。

背面剔除完成以后,将要转换到屏幕空间(或者缓冲区空间),这里将根据渲染的宽高进行转换,转换完成后,将进行图元装配,然后进行光栅化,光栅化可以理解为获取到图元占用屏幕的像素(一般叫片元),占用多少像素,将调用多少次的片元着色器进行运算颜色。这个步骤是图元的所有的片元结果同步计算的。

片元着色器运算完成,就到了最后输出合并阶段。你计算完成后,也不一定能够给像素填充颜色,我们还需要进行多个测试,通过这些测试以后,才可以将颜色写入帧缓冲区。这些步骤分别是:

半透明测试 根据透明度,将多少透明度以下的颜色直接放弃写入。unity里面函数是clip,内部如果传入的值小于0,则不会写入颜色。 模板测试 根据之前设置的模板,对比,如果通过将继续下一步。 深度测试 对比帧缓冲区的深度,通过继续下一步。 混合测试 这一步主要针对于半透明物体的,当前的颜色和帧缓冲区的像素颜色按比例进行混合,得到最终颜色。 最后,完成后,可以顺利的将颜色写入到缓冲区。注意,缓冲区不单单只存储了像素的颜色,还有深度信息,以及模板信息。

结构

ShaderLab

用于统一各种render 接口的高级抽象接口, 书写为一文本语言

Shaderlab Property

说明:

image.png |

结构总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	// name: TestShader
	Shader "Custom/TestShader" 
		{
			// 定义一系列属性,便于调节
			Properties
			{  
				Name {"display name", PropertyType} = DefaultValue 
			}
			
			SubShader
			{
				[RenderSetup] // 可选, 设置渲染状态(深度测试/混合模式等)
				
				[Tags] //可选, 设置标签块
				Tags {"Queue" = "Transparent"} //e.g.

				Pass{
					//[Name]
					Name "TestShader/MYPASSNAME"
					
					//[Tags]
					Tags {"LightMode" = "ForwardBase"} //e.g.
					
					//[RenderSetup]
					
					//other code
					
				}// other passes
			}// other subshaders

			Fallback "name" // 当所有的SubShader 都无法使用时的最低级shader
			
		}

Pasted image 20240701123536.png

参考链接

Unity ShaderLab 命令:ShaderLab:命令 - Unity 手册 Subshader Tags ref:ShaderLab:子着色器标签 (SubShader Tags) - Unity 手册

关于ShaderLab处理过程的一些理解

  1. 一个shader中可以有多个SubShader,每个SubShader中可以有多个Pass
  2. 当渲染一个物体时,Unity会遍历shader中的每个SubShader,找到第一个支持当前平台的SubShader,然后按照前后顺序执行其中的所有Pass,并将输出传递到同一个输出中(如ZBuffer, ColorBuffer等)。
  3. subshader和pass的Tags可以用来确定是否使用该subshader/pass(如LightMode = “ForwardBased”),以及在渲染队列中的位置(如Queue = “Transparent”,表示使用透明队列)

DirectX HLSL

在HLSL(High-Level Shading Language)中,语义(Semantics)是一种特殊的标记,用于告诉编译器某个变量的用途或者它应该如何被处理。

语义可以用于变量、结构体成员、函数参数和函数返回值。它们可以帮助编译器理解数据的用途,并进行相应的优化。例如,POSITION语义告诉编译器该变量包含了顶点的位置信息,COLOR语义表示该变量包含了颜色信息。

系统值语义(如SV_POSITIONSV_Target)是一种特殊的语义,它们在所有HLSL程序中都有特殊的含义。例如,SV_POSITION表示顶点在屏幕空间中的位置,SV_Target表示渲染目标(通常是一个像素的颜色)。

总的来说,语义在HLSL中起着非常重要的作用,它们帮助编译器理解和优化数据,确保数据在GPU中正确处理。

语义

reference:语义 - Win32 apps

Unity 应用阶段由unity传递给vertex着色器支持的语义

语义描述
POSITION模型空间中的顶点位置,通常是float4 类型
NORMAL顶点法线,通常是float3 类型
TANGENT顶点切线,通常是float4 类型
TEXCOORDn,如 TEXCOORD0该顶点的纹理坐标,TEXCOORD0 表示第一组纹理坐标,依此类推,通常是
TEXCOORD1float2或 float4 类型
COLOR顶点颜色,通常是float4 类型

SV_VERTEXID: 顶点ID,从0开始递增,每个顶点都有一个唯一的ID; 在URP中,可以通过 float4 pos = GetFullScreenTriangleVertexPosition(input.vertexID); float2 uv = GetFullScreenTriangleTexCoord(input.vertexID);获取顶点位置和纹理坐标

Unity 从vertex着色器传递给fragment着色器支持的语义

语义描述
SV POSTIION裁剪空间中的顶点坐标,结构体中必须包含一个用该距义修饰的变量。等同于
SV POSTIION我的空间中的顶点坐标,结构体中必须包含一个用该距又修饰的变量。等同于 DirectX 9 中的 POSTION, 但最好用 SV POSITION
COLOR0通常用于输出第一组顶点频色,但不是必需的
COLOR1通常用于输出第二组顶点频色,但不是必需的
TEXCOORD0-TEXCOORD7通常用于输出纹理坐标,但不是必需的

Unity 从fragment着色器输出支持的语义

语义描述
SV Target输出值将会存储到渲染目标(render target)中。等同于 DirectX 9 中的 COLOR,但最好使用 SV Target

vertex/fragment 着色器

  1. vertex 写在SubShader Pass 中, vertex着色器在几何处理阶段,一般用于传递位置, 法向顶点等信息。 着色器的输出通常是一个包含了各种顶点属性(如位置、颜色、纹理坐标等)的结构体。这些属性将被用于后续的图形管线阶段,如光栅化和片元着色。

    在HLSL和Unity Shader中,顶点着色器的输出通常包含以下几个部分:

    1. 顶点在裁剪空间(Clip Space)中的位置,通常使用SV_POSITION语义标记。
    2. 顶点的颜色,如果有的话。
    3. 顶点的纹理坐标,如果有的话。
    4. 顶点的法线和切线,如果进行光照计算的话。

    例如,一个顶点着色器的输出可能是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    
    struct v2f
    {
        float4 pos : SV_POSITION;
        float4 color : COLOR;
        float2 uv : TEXCOORD0;
        float3 normal : NORMAL;
        float3 tangent : TANGENT;
    };
    

    在这个例子中,v2f结构体包含了顶点的位置、颜色、纹理坐标、法线和切线。这些属性将被传递到后续的图形管线阶段。

  2. fragment 是对剪切空间(屏幕空间)的处理, 其输出一般是最终画面

  3. Tips/Hints/坑

    1. vertex/fragment 着色器一般都需要输入
    2. 输入输出建议使用struct, 且struct中需要补全个成员的语义,不然不支持
    3. 注意语义
    4. 常使用#include "UnityCG.cginc"
    5. 当要传递多个坐标语义时, 可以使用语义TEXCOORDn,n为0-7的整数,表示第n组坐标
    6. 注意tangent为四位向量,其中tangent.w为切线的方向(副法向),通常为1或-1
    7. 注意颜色与法线的线性变换关系,颜色取值范围为[0,1],法线取值范围为[-1,1], 故线性变换为normal = color*2-1
    8. 注意顶点着色器从应用阶段传递的顶点数据,一般是物体空间的数据,注意转换到世界空间或者切线空间
    9. 在创建半透明材质时,注意混合因子的设置,一般使用Blend SrcAlpha OneMinusSrcAlpha,即源颜色*源alpha + 目标颜色*(1-源alpha)
    10. 在多光源光照中,ambient总共只用计算一次
    11. frame debugger可以查看每一帧的渲染过程,对于调试很有帮助,The Frame Debugger window - Unity 手册
    12. 一般UV的坐标范围为[0,1],但在一些情况下,如立方体贴图,UV坐标可能为三维向量,不需要归一化处理

补充:切线空间

ref:https://zhuanlan.zhihu.com/p/361417740

  1. 切线空间是一个局部坐标系,它的原点位于顶点的位置,x轴指向切线方向(Normal, N),y轴指向副法线方向(binomarl, B),z轴指向法线方向(tangent, T)。其变换矩阵互为转置矩阵(由于是正交矩阵,所以其逆矩阵等于其转置矩阵)

  2. 物体坐标->切线坐标:(L代表物体空间中坐标) \(\left.\left(\begin{array}{ccc}Tx&Ty&Tz\\Bx&By&Bz\\Nx&Ny&Nz\end{array}\right.\right)\cdot\left(\begin{array}{c}Lx\\Ly\\Ly\end{array}\right)\)

  3. 切线坐标 -> 世界坐标:TBN矩阵(注:也可以扩展到其它坐标变换中) \(\left.\left(\begin{array}{lll}worldTx&worldBx&worldNx\\worldTy&worldBy&worldNy\\worldTz&worldBz&worldNz\end{array}\right.\right)\cdot\left(\begin{array}{ll}tangentNx\\tangentNy\\tangentNz\end{array}\right)\) code:

    1
    2
    3
    4
    5
    6
    7
    8
    
     float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
     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);  
    

切线空间:

目前使用过的内置函数/变量

一般Unity内置变量以_unity_UNITY_开头

refrence:

内置着色器变量 - Unity 手册

内置着色器 helper 函数 - Unity 手册

编写顶点和片元着色器 - Unity 手册

  • 常用库:

    1
    2
    
      #include "UnityCG.cginc"
      #include "Lighting.cginc" // 光照计算
    

Pasted image 20240702224110.png

  • 常用变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      UNITY_LIGHTMODEL_AMBIENT // 光照模型的环境光, 位于Lighting.cginc
      _LightColor0 // 第一个光源的颜色	
      _WorldSpaceLightPos0 // 第一个光源的世界空间位置
      _WorldSapceCameraPos // 相机的世界空间位置
    
    
      unity_ObjectToWorld// 物体空间到世界空间的变换矩阵
    
      _LightTexture0 // 光照贴图, 可以使用宏.UNITY_ATTEN_CHANNEL通过光源坐标查找光照衰减
      unity_WorldToLight // 世界空间到光源空间的变换矩阵
    
  • 常用函数: Unity Shader内置函数列表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
      UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换到裁剪空间
      UnityObjectToWorldNormal(v.normal); // 将法线从对象空间转换到世界空间, 注意为三维向量,注意转换后的法向未归一化!
    
      TRANSFORM_TEX(tex, name); // 将纹理坐标转换为纹理的uv坐标, tex.xy*name_ST.xy+name_ST.zw
      tex2D(name, uv); // 从纹理name中获取uv坐标处的颜色
      texCUBE(name, uv); // 从立方体纹理name中获取uv坐标处的颜色,注意该uv为三维向量,但无需做归一化处理
      UnpackNormal(normal); // 将法线从压缩格式转换为三维向量
    	
      ObjSpaceViewDir(v.vertex); // 物体空间的视线方向
      ObjSpaceLightDir(v.vertex); // 物体空间的光照方向
    	
      UNITYWorldSpaceViewDir(world_pos); // 世界空间的视线方向
      UNITYWorldSpaceLightDir(world_pos); // 世界空间的光照方向
    
      ComputeScreenPos(pos); // 计算屏幕空间的坐标, pos为世界空间的坐标
      ComputeGrabScreenPos(pos); // 计算捕捉屏幕空间的坐标
    
      clip(floatx x); // HLSL内置函数, 如果x中有任一分量小于0, 则discard
      discard; // 丢弃当前片元, HLSL内置函数
      reflect(i, n); // 计算i关于n的反射向量, i为入射向量(指向反射点), n为法线向量, HLSL内置函数
      refract(i, n, eta); // 计算i关于n的折射向量, i为入射向量(指向折射点), n为法线向量, eta为当前介质和目标介质的折射率比值, HLSL内置函数
      saturate(x); // 将x中的每个分量限制在[0,1]之间, HLSL内置函数
    
      lerp(a, b, t); // 线性插值, a和b为两个向量, t为插值因子, HLSL内置函数
      step(edge, x); // 如果x小于edge, 则返回0, 否则返回1, HLSL内置函数,常用于替代if语句
      smoothstep(a, b, x); // 平滑插值,用于产生0~1之间的平滑过渡值,a和b为插值范围(在a和b之外的值将被截断)
      // 定义
      float smoothstep(float a, float b, float x) 
      {
    	x = clamp((x - a) / (b- a), 0.0, 1.0); 
    	return x * x * (3 - 2 * x);
      }
    
      ddx(x_) // 计算x_对x的x方向的偏导数 ref:https://tyson-wu.github.io/blogs/2021/07/08/Ronja_Partial_Derivatives/
      ddy(y_) // 计算y_对y的y方向的偏导数
      fwidth(x_) // 计算x_的梯度
    
    
  • 常用宏:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      TANGENT_SPACE_ROTATION // 物体空间到切线空间的旋转矩阵(3*3) 为宏定义
    
      USING_DIRECTIONAL_LIGHT // 宏定义, 判断是否使用平行光
      POINT // 宏定义, 判断是否使用点光源
      SPOT // 宏定义, 判断是否使用聚光灯
    
      fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; // 通过光源坐标查找光照衰减
    
      SHADOW_COORDS(i); // 用于声明一个阴影坐标, i为阴影贴图索引, 位于"AutoLight.cginc"
      #define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
    
      TRANSFER_SHADOW(o); // 用于计算阴影坐标, o为输出结构体, 位于"AutoLight.cginc",定义如下
      #define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
    
      SHADOW_ATTENUATION(i); // 用于计算阴影衰减, i为阴影贴图索引, 位于"AutoLight.cginc"
      #define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
    
      UNITY_LIGHT_ATTENUATION(); // 可以统一计算光照衰减和阴影衰减,即无需调用SHADOW_ATTENUATION(i)和UNITY_ATTEN_CHANNEL, 位于"AutoLight.cginc"
    
    

Pasted image 20240702223901.png

透明效果(对应第8章)

  1. Unity渲染队列(Queue): Pasted image 20240705151445.png

  2. 定义shader的渲染队列:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
     SubShader
     {
         Tags{"Queue" = "xxx"}
         Pass{
             // other code
         }
     }
     SubShader
     {
         Tags{"Queue" = "Transparent"} // 使用透明队列
         Pass{
             ZWrite Off // 可选, 关闭深度写入
             // other code
         }
     }
    
  3. Blend混合模式:

Pasted image 20240706101340.png

1
Unity Blend ref : [ShaderLab 命令:Blend - Unity 手册](https://docs.unity.cn/cn/2020.3/Manual/SL-Blend.html)

渲染路径

用于实现多灯光渲染,包含ForwardBase, ForwardAdd, Deferred, LegacyDeferredLighting, LegacyDeferredShading等; ref: 前向渲染路径 - Unity 手册 内置渲染管线中的渲染路径 - Unity 手册

Pasted image 20240706212209.png

阴影

Unity的阴影实现主要有两种方式:ShadowMap和ScreenSpaceShadow。

  1. unity中实现阴影的步骤:
    1. 在v2f结构体中声明阴影坐标,常用宏SHADOW_COORDS(i),i为阴影贴图索引
    2. 在vertex shader中计算阴影坐标,常用宏TRANSFER_SHADOW(o),o为输出结构体
    3. 在fragment shader中计算阴影衰减,常用宏SHADOW_ATTENUATION(i),i为阴影贴图索引
    4. 在fragment shader中使用阴影衰减计算阴影效果
  2. 为获得阴影贴图,需要LightMode = ShadowCaster 的Pass来实现,这一步一般由Fallback指定的shader中的Pass来实现(即VertexLit中的Pass) 举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    Shader "xx"
     {
         //other code
         SubShader
         {
             //other code
             Pass
             {
                 //other code
             }
         }
         Fallback "Reflective/VetexLit"
     }
    

    高级纹理

菲涅耳(fresnel)

近似等式: \(F_{schlick}(\mathbf{v},\mathbf{n})=F_0+(1-F_0)(1-\mathbf{v}\cdot\mathbf{n})^5\) 或 \(F_{Empricial}(\mathbf{v},\mathbf{n})=max(0,min(1,bias+scale\times(1-\mathbf{v}\cdot\mathbf{n})^{power}))\)

镜面效果

这里所指的镜面效果是非光线追踪的,而是使用渲染纹理实现

思路:为镜面制作一个简单的显示纹理的shader, 然后放一摄像机在镜面位置,将摄像机的渲染结果作为纹理(render texture)传递给镜面shader,注意镜面shader的渲染队列应该在摄像机的shader之后,以便摄像机的渲染结果能够传递给镜面shader,同时需要注意纹理x坐标的反转。

玻璃效果

使用GrabPass实现

  • GrabPass: 用于捕获屏幕上的图像,然后将其作为纹理传递给shader,以实现一些特殊效果,如玻璃效果,水面效果等
  • 使用:
    1
    2
    3
    4
    5
    6
    7
    8
    
      GrabPass { "TexName" }
      // 或者
      GrabPass {} // 默认TexName为"_GrabTexture"
    	
      // 可以通过ComputeGrabScreenPos(pos)计算屏幕空间的坐标
      Coordinate = ComputeGrabScreenPos(pos);
    
      float4 _TexName_TexelSize // 纹理的像素大小, 用于计算纹理坐标
    

内置时间变量

1
2
3
4
	float4 _Time; // t是自场景加载开始所经过的时间,4分量分别是(t/20, t, 2t, 3t)
	float4 _SinTime; // t'是t的正弦值,4分量分别是(sin(t'/8), sin(t'/4), sin(t'/2), sin(t'))
	float4 _CosTime; // t'是t的余弦值,4分量分别是(cos(t'/8), cos(t'/4), cos(t'/2), cos(t'))
	float4 unity_DeltaTime; // dt为时间增量,4分量分别是(dt, 1/dt, soomth_dt, 1/soomth_dt)

NPR效果

  • 描边效果
    1. 注意,法线扩展最好在view space中进行,而非world space

顶点动画

广告牌效果

  1. 变换在Object Space中进行
本文由作者按照 CC BY 4.0 进行授权