当前位置: 华文问答 > 游戏

如何选择Unity 3d Shader编写语言?有哪些Shader入门技巧及学习资料?

2015-05-19游戏

游戏图形学与数学(Unity Shader篇)

前文我们提到了Unity Shader渲染的一个流程图,大家还记得嘛。没错啦,就是这个东西。

那么今天这篇文章,我们就带大家来具体了解一下每一部分的含义和运用。

1️⃣顶点数据与顶点着色器

Unity 中的 Shader顶点数据 是指在 顶点着色器 中处理的顶点相关信息,包括顶点的 位置 法线 颜色 UV坐标 等。这些数据是用来描述三维模型表面的属性。

Unity 中, 顶点数据 是通过 顶点缓冲区(Vertex Buffer) 来传递的。在顶点着色器中,你可以定义输入变量来接收这些顶点数据,并对其进行处理。一般情况下,你会用 顶点着色器 来进行顶点位置的变换、法线的计算以及其他顶点相关的操作。

以下是一个简单的 Unity顶点着色器 示例,用来对顶点进行基本的变换:

Shader " Custom / Example " { Properties { _MainTex ( " Texture " , 2 D ) = " white " {} } SubShader { Tags { " RenderType " = " Opaque " } LOD 200 CGPROGRAM # pragma surface surf Lambert vertex : vert sampler2D _MainTex ; struct Input { float2 uv_MainTex ; }; void vert ( inout appdata_full v , out Input o ) { // 顶点位置变换 v . vertex . xyz += sin ( _Time . y + v . vertex . y ) * 0.1 ; // 将顶点位置传递给顶点着色器 o . uv_MainTex = v . texcoord . xy ; } void surf ( Input IN , inout SurfaceOutput o ) { // 获取纹理颜色 fixed4 c = tex2D ( _MainTex , IN . uv_MainTex ); // 输出最终的颜色 o . Albedo = c . rgb ; o . Alpha = c . a ; } ENDCG } FallBack " Diffuse " }

在这个例子中, vert 函数接收了顶点位置信息,并对其进行了简单的变换。 surf 函数则接收了顶点着色器处理后的数据,并将纹理颜色输出作为最终的结果。

具体演示步骤

1️⃣首先我们建立一个简单的Unity工程,可以直接命名为Example之类的

2️⃣好了之后,我们直接创建项目。接着进入Unity,我们先在屏幕右键新建一个material的材质球和一个shader脚本

3️⃣然后我们把刚才建立好的Shader脚本拖拽给材质球

4️⃣双击打开Shader脚本,将我们上面的代码复制进来

5️⃣然后我们新建一个物体,并在材质球中加入一张图片,再将得到的材质球赋给屏幕中的物体

6️⃣最后展示效果,我们就会得到一个这样的物体

2️⃣曲面细分着色器

Unity 中, 曲面细分着色器 是一种特殊类型的着色器,它可以通过增加三角形数量来增加模型的细节和曲面的光滑度。这种着色器可以用来创建更加真实的模型表面,例如在角落、边缘或者圆润的表面。

Unity 中的曲面细分着色器是通过 Surface Shader 或者 Shader Graph(或者ASE) 来实现的。

使用Surface Shader实现曲面细分:

Shader " CCustom / Example " { Properties { _MainTex ( " Texture " , 2 D ) = " white " {} _WireframeColor ( " Wireframe Color " , Color ) = ( 0 , 0 , 0 ) _WireframeSmoothing ( " Wireframe Smoothing " , Range ( 0 , 10 )) = 1 _WireframeThickness ( " Wireframe Thickness " , Range ( 0 , 10 )) = 1 _TessellationUniform ( " Tessellation Uniform " , Range ( 1 , 64 )) = 1 } SubShader { Tags { " RenderType " = " Opaque " } LOD 100 Pass { CGPROGRAM # pragma vertex tessVert # pragma hull hul # pragma domain dom # pragma geometry geo # pragma fragment frag # include " UnityCG . cginc " //顶点着色器数据 struct vertexData { float4 vertex : POSITION ; float4 tangent : TANGENT ; float3 normal : NORMAL ; float2 uv : TEXCOORD0 ; }; //细分着色器数据,一般与顶点着色器数据保持一致 struct tessVertexData { float4 vertex : INTERNALTESSPOS ; float4 tangent : TANGENT ; float3 normal : NORMAL ; float2 uv : TEXCOORD0 ; }; struct TessellationFactors { float edge [ 3 ] : SV_TessFactor ; float inside : SV_InsideTessFactor ; }; //几何着色器数据 struct v2g { float4 vertex : SV_POSITION ; float4 tangent : TANGENT ; float3 normal : NORMAL ; float2 uv : TEXCOORD0 ; }; //片段着色器数据 struct g2f { v2g data ; float3 barycentricCoordinates : TEXCOORD3 ; //三角形的重心坐标 }; sampler2D _MainTex ; float4 _MainTex_ST ; float3 _WireframeColor ; float _WireframeSmoothing ; float _WireframeThickness ; float _TessellationUniform ; //将原始顶点数据传递到细分着色器中 tessVertexData tessVert ( vertexData v ) { tessVertexData o ; o . vertex = v . vertex ; o . tangent = v . tangent ; o . normal = v . normal ; o . uv = v . uv ; return o ; } //顶点着色器 v2g vert ( vertexData v ) { v2g o ; o . vertex = UnityObjectToClipPos ( v . vertex ); o . uv = TRANSFORM_TEX ( v . uv , _MainTex ); return o ; } //细分函数定义 TessellationFactors hullFun ( InputPatch < tessVertexData , 3 > v ) { TessellationFactors o ; o . edge [ 0 ] = _TessellationUniform ; o . edge [ 1 ] = _TessellationUniform ; o . edge [ 2 ] = _TessellationUniform ; o . inside = _TessellationUniform ; return o ; } //细分规则定义 [ UNITY_domain ( " tri " )] //三角形 [ UNITY_outputcontrolpoints ( 3 )] //三角形三个顶点 [ UNITY_outputtopology ( " triangle_cw " )] //三角形生成顺序(顺时针/逆时针) [ UNITY_partitioning ( " fractional_odd " )] //细分只与整数有关,舍入规则 [ UNITY_patchconstantfunc ( " hullFun " )] //细分函数 //hull着色器:定义细分规则 tessVertexData hul ( InputPatch < tessVertexData , 3 > v , uint id : SV_OutputControlPointID ) { return v [ id ]; } [ UNITY_domain ( " tri " )] //domain着色器:计算细分后的顶点位置和数据,同时执行顶点着色器 v2g dom ( TessellationFactors tessFactors , const OutputPatch < tessVertexData , 3 > vi , float3 bary : SV_DomainLocation ) { vertexData v ; v . vertex = vi [ 0 ]. vertex * bary . x + vi [ 1 ]. vertex * bary . y + vi [ 2 ]. vertex * bary . z ; v . tangent = vi [ 0 ]. tangent * bary . x + vi [ 1 ]. tangent * bary . y + vi [ 2 ]. tangent * bary . z ; v . normal = vi [ 0 ]. normal * bary . x + vi [ 1 ]. normal * bary . y + vi [ 2 ]. normal * bary . z ; v . uv = vi [ 0 ]. uv * bary . x + vi [ 1 ]. uv * bary . y + vi [ 2 ]. uv * bary . z ; return vert ( v ); } [ maxvertexcount ( 3 )] //几何着色器 void geo ( triangle v2g v [ 3 ], inout TriangleStream < g2f > tStream ) { float4 barycenter = ( v [ 0 ]. vertex + v [ 1 ]. vertex + v [ 2 ]. vertex ) / 3 ; float3 normal = ( v [ 0 ]. normal + v [ 1 ]. normal + v [ 2 ]. normal ) / 3 ; v [ 0 ]. normal = normal ; v [ 1 ]. normal = normal ; v [ 2 ]. normal = normal ; g2f g0 , g1 , g2 ; g0 . data = v [ 0 ]; g1 . data = v [ 1 ]; g2 . data = v [ 2 ]; // g0 . barycentricCoordinates = float3 ( 0 , 0 , 1 ); g1 . barycentricCoordinates = float3 ( 0 , 1 , 0 ); g2 . barycentricCoordinates = float3 ( 1 , 0 , 0 ); tStream . Append ( g0 ); tStream . Append ( g1 ); tStream . Append ( g2 ); tStream . RestartStrip (); } fixed4 frag ( g2f i ) : SV_Target { fixed4 col = tex2D ( _MainTex , i . data . uv ); float3 barys = i . barycentricCoordinates ; float3 deltas = fwidth ( barys ); float3 smoothing = deltas * _WireframeSmoothing ; float3 thickness = deltas * _WireframeThickness ; barys = smoothstep ( thickness , thickness + smoothing , barys ); float minBary = min ( barys . x , min ( barys . y , barys . z )); return float4 ( lerp ( _WireframeColor , col , minBary ), 1 ); // return col ; } ENDCG } } }

这是一个使用Surface Shader实现曲面细分的示例。你可以将此代码粘贴到Unity的Shader文件中,并将其应用到你的模型上。然后,通过调整 _DisplacementStrength 属性的值来控制曲面细分的强度。

把上面代码复制到我们刚的shader脚本里来替换掉,就能得到这样的效果图

使用Shader Graph(或者)实现曲面细分,关于这一部分,后面我们会再开个专题直接学习Shader工具的一些使用,因为使用工具总体比较简单一点,这里不做过多赘述,我们旨在了解内部代码原理。

3️⃣几何着色器

我们在说到曲面细分着色器的时候,代码里就运用了几何着色器。那么什么事几何着色器呢?

Unity 几何着色器(Geometry Shader) 是一种用于在渲染管线中处理几何图元(例如点、线、三角形)的特殊类型的着色器。与 顶点着色器(Vertex Shader )和 片段着色器(Fragment Shader) 不同,几何着色器在渲染管线的几何阶段中运行,可以对传入的几何数据进行操作和变换。

使用几何着色器,你可以执行一系列的几何操作,如创建新的几何图元、删除几何图元、改变几何图元的形状等。这为实现一些高级效果提供了便利,如几何细分、法线扰动、雾效果等。

以下是一个简单的示例,展示了如何在Unity中编写和使用几何着色器:

Shader " Custom / Example " { Properties { _MainTex ( " Texture " , 2 D ) = " white " {} _WireframeColor ( " Wireframe Color " , Color ) = ( 0 , 0 , 0 ) _WireframeSmoothing ( " Wireframe Smoothing " , Range ( 0 , 10 )) = 1 _WireframeThickness ( " Wireframe Thickness " , Range ( 0 , 10 )) = 1 } SubShader { Tags { " RenderType " = " Opaque " } LOD 100 Pass { CGPROGRAM # pragma vertex vert # pragma geometry geo # pragma fragment frag # include " UnityCG . cginc " //顶点着色器数据 struct appdata { float4 vertex : POSITION ; float2 uv : TEXCOORD0 ; }; //几何着色器数据 struct v2g { float4 vertex : SV_POSITION ; float2 uv : TEXCOORD0 ; float3 normal : TEXCOORD2 ; }; //片段着色器数据 struct g2f { v2g data ; float3 barycentricCoordinates : TEXCOORD3 ; //三角形的重心坐标 }; sampler2D _MainTex ; float4 _MainTex_ST ; float3 _WireframeColor ; float _WireframeSmoothing ; float _WireframeThickness ; v2g vert ( appdata v ) { v2g o ; o . vertex = UnityObjectToClipPos ( v . vertex ); o . uv = TRANSFORM_TEX ( v . uv , _MainTex ); return o ; } //参考:https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-geometry-shader [ maxvertexcount ( 3 )] void geo ( triangle v2g v [ 3 ], inout TriangleStream < g2f > tStream ) { float3 normal = ( v [ 0 ]. normal + v [ 1 ]. normal + v [ 2 ]. normal ) / 3 ; v [ 0 ]. normal = normal ; v [ 1 ]. normal = normal ; v [ 2 ]. normal = normal ; g2f g0 , g1 , g2 ; g0 . data = v [ 0 ]; g1 . data = v [ 1 ]; g2 . data = v [ 2 ]; // g0 . barycentricCoordinates = float3 ( 0 , 0 , 1 ); g1 . barycentricCoordinates = float3 ( 0 , 1 , 0 ); g2 . barycentricCoordinates = float3 ( 1 , 0 , 0 ); tStream . Append ( g0 ); tStream . Append ( g1 ); tStream . Append ( g2 ); tStream . RestartStrip (); } fixed4 frag ( g2f i ) : SV_Target { fixed4 col = tex2D ( _MainTex , i . data . uv ); float3 barys = i . barycentricCoordinates ; float3 deltas = fwidth ( barys ); float3 smoothing = deltas * _WireframeSmoothing ; float3 thickness = deltas * _WireframeThickness ; barys = smoothstep ( thickness , thickness + smoothing , barys ); float minBary = min ( barys . x , min ( barys . y , barys . z )); return float4 ( lerp ( _WireframeColor , col , minBary ), 1 ); // } ENDCG } } }

在这个示例中,我们定义了一个简单的几何着色器,它将传入的点扩展成一个由三个点组成的三角形。在几何着色器的 geo 函数中,我们通过迭代传入的点,为每个点创建一个新的顶点,并将其添加到三角形流中。在片段着色器中,我们简单地将顶点的颜色作为最终的颜色输出。最终效果图如下

4️⃣片元着色器


Unity 中的 片元着色器(Fragment Shader) 是一种用于计算每个屏幕像素最终颜色的着色器。它在顶点着色器处理完顶点数据之后,对每个像素进行处理,确定最终像素的颜色输出。

通常, 片元着色器 用于执行诸如光照计算、纹理采样、颜色混合等操作,以确定像素的最终颜色。在 Unity 中,你可以通过编写片段着色器来实现各种各样的视觉效果和渲染技术。

以下是一个简单的 Unity片元着色器 示例:

Shader " Custom / Example " { Properties { _MainTex ( " Texture " , 2 D ) = " white " {} _Color ( " Color " , Color ) = ( 1 , 1 , 1 , 1 ) } SubShader { Tags { " RenderType " = " Opaque " } LOD 100 Pass { CGPROGRAM # pragma vertex vert # pragma fragment frag # include " UnityCG . cginc " struct appdata { float4 vertex : POSITION ; float2 uv : TEXCOORD0 ; }; struct v2f { float2 uv : TEXCOORD0 ; float4 vertex : SV_POSITION ; }; sampler2D _MainTex ; fixed4 _Color ; v2f vert ( appdata v ) { v2f o ; o . vertex = UnityObjectToClipPos ( v . vertex ); o . uv = v . uv ; return o ; } fixed4 frag ( v2f i ) : SV_TARGET { fixed4 texColor = tex2D ( _MainTex , i . uv ); return texColor * _Color ; } ENDCG } } }

在这个示例中,我们定义了一个简单的 片元着色器 。它从输入的纹理中采样颜色,并与属性 _Color 相乘,输出最终的像素颜色。

要使用这个 片元着色器 ,只需将其保存为一个Shader文件,并将其应用到Unity中的对象上即可。你可以通过调整属性 _MainTex _Color 的值来改变纹理和颜色效果。

上面主要讲了几个能通过编程来控制的渲染过程,接下来我们说一下,不可编程部分和GPU固定实现的部分。

5️⃣裁剪

Unity 中,渲染流程中的 裁剪 部分通常是指在图形渲染管线的「 几何处理 」阶段,进行 视锥体裁剪 遮挡剔除 的过程。这个过程的目的是 优化渲染性能 ,确保只有在相机视野范围内可见的物体才会被送入后续的渲染阶段,从而避免不必要的绘制操作, 提高渲染效率

Unity中的渲染流程通常包含以下步骤:

  1. 几何处理(Geometry Processing) :这是渲染管线的第一个阶段,其中包括顶点着色器的执行、裁剪和三角形装配等操作。在这个阶段,视锥体裁剪是首先进行的操作。视锥体裁剪会将场景中不在相机视锥体内的顶点和三角形剔除,从而减少后续处理的对象数量。
  2. 光栅化(Rasterization) :在几何处理后,剩余的可见三角形被转换成屏幕上的像素,并且通过光栅化过程进行片段处理。
  3. 片段处理(Fragment Processing) :这个阶段涉及像素着色器的执行,包括纹理采样、光照计算等。在这个阶段,可能会进行遮挡剔除操作,即根据深度缓冲区等信息,确定哪些像素是不可见的,从而避免对它们进行着色和混合操作。

总的来说,裁剪部分主要发生在几何处理阶段,通过视锥体裁剪和遮挡剔除来提高渲染效率,确保只有可见的物体才会被送入后续的渲染阶段,从而减少不必要的计算和绘制操作。

6️⃣逐片元操作

Unity 渲染过程中, 逐片元操作 指的是 渲染管线 中的 片段处理阶段 。这个阶段是在屏幕上的 每个像素 或称为片元 )上执行的操作,其主要目的是计算出每个 像素的最终颜色值 。逐片元操作包括以下主要步骤:

  1. 光栅化(Rasterization) :在几何处理阶段后,剩余的可见三角形会被转换成屏幕上的像素,这个过程就是光栅化。每个三角形都会被分解成覆盖它的像素。
  2. 片段着色器(Fragment Shader) :在光栅化后,针对每个像素会执行片段着色器。片段着色器是一段代码,用来计算每个像素最终的颜色。这里进行了光照计算、纹理采样、环境光遮蔽等操作,以确定片元最终的颜色。
  3. 深度测试(Depth Testing) :在计算了每个片元的颜色后,会将该片元的深度值与深度缓冲区中对应位置的值进行比较。如果当前片元的深度值比深度缓冲区中的值更接近相机视角,那么这个片元就是最终的可见片元,其颜色值将被写入到帧缓冲区中。否则,这个片元将被丢弃。
  4. 混合(Blending) :在深度测试之后,可能会进行混合操作。混合允许将新的片元颜色与帧缓冲区中已存在的颜色进行混合,这通常在透明物体的渲染中使用,以实现透明效果。

总的来说,逐片元操作是渲染管线中的一个重要阶段,它负责计算每个屏幕像素的最终颜色值,是实现图形渲染效果的关键之一。

7️⃣屏幕映射


在Unity渲染过程中,GPU的屏幕映射是指将场景中的3D对象投影到屏幕上的过程。这个过程是由GPU执行的,通常在渲染管线的几何处理阶段进行。以下是屏幕映射的一般步骤:

  1. 投影变换(Projection Transformation) :在场景中,摄像机会对3D物体进行投影。这个投影是由投影矩阵来实现的,通常包括透视投影(perspective projection)或正交投影(orthographic projection)。投影矩阵的作用是将3D场景中的点投影到摄像机的近裁剪面上。
  2. 视口变换(Viewport Transformation) :在投影后,得到的2D坐标通常是以屏幕空间的方式表示的,其中坐标原点位于屏幕的左下角,x和y轴的范围通常是从0到屏幕的宽度和高度。视口变换将屏幕空间的坐标映射到实际显示设备上的像素坐标,确保图像正确呈现在屏幕上。
  3. 裁剪(Clipping) :在视口变换后,可能会执行裁剪操作,将超出屏幕范围的图元剔除,从而提高渲染效率并防止渲染超出屏幕的不必要像素。
  4. 光栅化(Rasterization) :经过投影、视口变换和裁剪后,3D对象的顶点将被转换成屏幕上的像素,这个过程称为光栅化。GPU会根据这些像素的位置和属性来生成最终的图像。

总的来说,GPU的屏幕映射是渲染管线中的一个重要步骤,它负责将3D场景中的对象投影到屏幕上,从而实现图形的显示。

8️⃣三角形设置

在Unity渲染过程中,GPU的三角形设置是指在图形渲染管线中的几何处理阶段,GPU如何处理和渲染场景中的三角形(或其他多边形)的设置。这些设置通常由图形编程人员在编写顶点着色器和片段着色器时指定。

以下是一些GPU三角形设置的主要方面:

  1. 顶点数据传输(Vertex Data Transmission) :在几何处理阶段,场景中的三角形被分解成顶点,并且这些顶点的数据需要传输到GPU中进行处理。这包括顶点的位置、法线、纹理坐标等信息。
  2. 图元组装(Primitive Assembly) :GPU需要将顶点数据组装成图元(通常是三角形),以便进行后续的处理和渲染。图元可以是三角形、线段或点等。
  3. 顶点着色器(Vertex Shader) :顶点着色器是在每个顶点上执行的程序,用于对顶点进行变换、光照计算和其他操作。在顶点着色器中,通常会进行一些GPU三角形设置,比如变换顶点位置、计算顶点法线等。
  4. 图元剔除(Primitive Culling) :在进行光栅化之前,GPU可能会执行图元剔除操作,根据特定的条件丢弃不需要的图元,从而提高渲染效率。
  5. 三角形的渲染顺序(Triangle Rendering Order) :在光栅化之后,GPU需要确定渲染三角形的顺序。通常情况下,这由顶点数据的顺序决定,但也可以通过调整顶点顺序或者在几何着色器中手动设置来改变渲染顺序。
  6. 裁剪(Clipping) :在渲染过程中,GPU可能会执行裁剪操作,将超出屏幕范围的图元或片段剔除,以提高性能和减少不必要的渲染。

总的来说,GPU的三角形设置是指在图形渲染管线的几何处理阶段中,GPU对场景中的三角形进行处理和渲染的设置和操作。这些设置包括顶点数据传输、图元组装、顶点着色器的操作、图元剔除、三角形的渲染顺序以及裁剪等。

9️⃣三角形遍历

在Unity中,GPU的三角形遍历是指在图形渲染管线的几何处理阶段,GPU对场景中的三角形进行迭代处理的过程。这个过程通常包括以下步骤:

  1. 顶点着色器(Vertex Shader) :在顶点着色器中,每个顶点都经过一系列的变换和计算,以确定其在屏幕空间的位置、法线方向、纹理坐标等信息。顶点着色器是在GPU上执行的,可以并行地处理大量的顶点数据。
  2. 图元组装(Primitive Assembly) :一旦顶点经过了顶点着色器的处理,GPU会将顶点组装成图元,通常是三角形。这个阶段也是在GPU上并行处理的,每个图元的组装都可以独立进行。
  3. 三角形遍历(Triangle Traversal) :在图元组装之后,GPU会对场景中的三角形进行遍历。遍历的过程实际上是将三角形投影到屏幕上,并确定每个像素(或称为片元)的位置和属性。
  4. 光栅化(Rasterization) :在遍历过程中,GPU会根据三角形的屏幕投影来确定其覆盖的像素区域。这个过程称为光栅化,它会将三角形分解为覆盖它的像素,并将这些像素送入后续的片段处理阶段。
  5. 片段处理(Fragment Processing) :在光栅化之后,GPU会对每个像素(片元)进行处理,包括执行片段着色器、深度测试、混合等操作,以计算最终的像素颜色值。

在整个渲染过程中,三角形遍历是其中一个关键的步骤,它负责将场景中的三角形映射到屏幕上,并为后续的光栅化和片段处理阶段提供输入。这个过程需要高效地处理大量的三角形数据,以实现流畅的图形渲染效果。

1️⃣0️⃣屏幕图像


在Unity中,GPU的屏幕图像是指最终呈现在屏幕上的图像,它是整个渲染过程的结果。GPU在渲染过程中负责计算每个像素的颜色值,并将这些颜色值写入帧缓冲区(Frame Buffer)中,最终形成屏幕上的图像。

以下是Unity中GPU的屏幕图像生成的主要步骤:

  1. 顶点处理和光栅化 :在渲染管线的几何处理阶段,GPU会对场景中的顶点进行处理,将它们转换为屏幕空间坐标,并通过光栅化过程将三角形分解成覆盖像素的片元。
  2. 片段处理 :对每个片元进行片段处理,包括执行片段着色器、深度测试、纹理采样等操作。这些操作确定了每个像素的最终颜色值。
  3. 深度测试和混合 :在片段处理后,GPU执行深度测试来确定哪些片元是可见的,哪些是被遮挡的。然后,根据混合操作来合并不同片元的颜色值,以产生最终的像素颜色。
  4. 输出到屏幕 :最终,GPU将帧缓冲区中的像素颜色值输出到屏幕上,形成最终的屏幕图像。

整个过程是高度并行化的,GPU能够同时处理多个像素和片元,以实现高效的图形渲染。屏幕图像的质量和性能取决于许多因素,包括渲染管线的设置、光照效果、材质属性、渲染质量设置等。

总之,GPU的屏幕图像是渲染过程的最终输出,它是用户最终看到的游戏画面,包含了所有渲染效果和场景内容。

这篇文章我们熟悉了渲染的一个流程,了解了Unity一个物体是怎样通过渲染最后呈现到我们眼中成为屏幕中我们所看到的游戏物体那样,最后也希望这篇文章对你们有所帮助。后面文章我会分享一些 Shader的制作效果以及如何去优化我们写的Shader

享受游戏,热爱生活!我是皮皮,咱们下期见!

Unity常用发光效果和Shader代码