三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity表面着色器:简化复杂光照渲染的高级抽象框架

Unity表面着色器:简化复杂光照渲染的高级抽象框架

1. 项目概述:为什么我们需要表面着色器?

如果你在Unity里写过Shader,尤其是想实现一个带光照的材质效果,大概率会从内置的“Standard”或“Standard Specular”着色器开始。它们开箱即用,效果不错,但当你需要一些定制化效果,比如让物体表面随着距离改变颜色,或者实现一个复杂的卡通渲染时,直接修改这些内置着色器的代码,或者从头手写一个顶点/片元着色器,往往会让你陷入泥潭。你需要处理前向渲染路径、延迟渲染路径、不同的光源类型(平行光、点光源、聚光灯)、阴影接收、光照贴图、全局光照探针……光是想想就头大。

这就是Unity表面着色器(Surface Shader)存在的意义。它不是一个全新的着色器语言,而是一个位于HLSL/CG之上的高级抽象层。你可以把它理解为一个“代码生成器”或“框架”。你只需要告诉它:“我的表面长什么样(漫反射颜色、法线、高光等)”,它就会自动帮你生成处理所有复杂光照计算、多光源混合、不同渲染路径适配的底层顶点和片元着色器代码。

简单来说,表面着色器让你能用写一个函数的精力,得到一个能应对各种光照环境的完整着色器。这对于需要与Unity复杂光照系统交互的材质开发来说,效率提升是巨大的。它特别适合那些需要动态光照、阴影、反射探针等“标准”PBR或非PBR光照模型的场景,比如角色、场景物件、武器等绝大部分游戏内实体对象。

2. 核心原理与工作流程拆解

理解表面着色器,关键在于理解它的“声明式”编程思想。你不是在一步步指挥GPU绘图,而是在描述一个表面的物理或视觉属性。

2.1 核心结构:一个“表面函数”

一个最基本的表面着色器框架如下:

Shader "Custom/MySurfaceShader" { Properties { // 定义在材质面板上显示的属性,如 _MainTex, _Color 等 _MainTex ("Albedo (RGB)", 2D) = "white" {} _Color ("Color", Color) = (1,1,1,1) } SubShader { Tags { "RenderType"="Opaque" } // 渲染队列和类型标签 CGPROGRAM // 关键指令:声明这是一个表面着色器,使用Lambert光照模型 #pragma surface surf Lambert // 声明要使用的Properties中的变量 sampler2D _MainTex; fixed4 _Color; // 输入结构体:从顶点着色器传递到表面函数的数据 struct Input { float2 uv_MainTex; // 自动匹配名为_MainTex的纹理坐标 }; // 核心:表面函数 void surf (Input IN, inout SurfaceOutput o) { // 采样纹理,乘以颜色 fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color; // 将结果赋值给输出结构体的对应成员 o.Albedo = c.rgb; // 漫反射颜色 o.Alpha = c.a; // 透明度 } ENDCG } FallBack "Diffuse" // 备选着色器 }

关键点解析:

  1. #pragma surface surf Lambert:这是表面着色器的灵魂指令。
    • surface:告诉Unity这是一个表面着色器。
    • surf:你定义的表面函数的名字。
    • Lambert:使用的光照模型。这里用了简单的漫反射(Lambert)模型。Unity内置了Lambert(漫反射)、BlinnPhong(高光)、Standard(基于物理的金属工作流)、StandardSpecular(基于物理的高光工作流)等模型。
  2. Input结构体:定义了surf函数需要哪些输入数据。最常见的就是纹理坐标uv_MainTex。Unity会根据变量名自动为你填充这些数据。例如,名为uv_MainTex的变量会自动获取_MainTex纹理的UV坐标。
  3. SurfaceOutput结构体:这是surf函数的输出。你在这里填充表面的视觉属性。对于Lambert模型,使用的是基础的SurfaceOutput结构体。
  4. surf函数:这是你发挥创意的地方。你根据Input中的数据(如UV),计算出表面的各种属性(如颜色、法线),然后填充到SurfaceOutput o中。之后,光照模型和渲染路径的处理就全部交给Unity生成的代码了。

2.2 Unity在背后做了什么?

当你点击编译或运行游戏时,Unity的着色器编译器会读取你的#pragma surface指令和surf函数,然后进行以下操作:

  1. 分析需求:根据你指定的光照模型(如Standard)和可选的编译指令(如是否需要阴影、光照贴图),确定这个着色器需要支持哪些功能。
  2. 生成顶点着色器:自动生成一个顶点着色器,负责将模型顶点从本地空间转换到裁剪空间,并计算和传递Input结构体中需要的所有数据(如纹理坐标、世界空间法线、视线方向等)。
  3. 生成片元着色器:生成一个或多个片元着色器。这是最复杂的部分。生成的代码会:
    • 调用你的surf函数,获取表面属性。
    • 根据光照模型(如Standard的PBR方程),计算每个光源对该片元的贡献。这包括平行光、点光源、聚光灯,并正确处理它们的光照衰减和Cookie。
    • 处理光照贴图光照探针(Light Probes)的间接光照。
    • 处理阴影(接收阴影,如果支持的话)。
    • 将所有这些光照贡献叠加起来,输出最终的屏幕颜色。
  4. 生成多个Pass/变体:为了支持不同的渲染路径(前向、延迟)和不同的质量设置(有无阴影、有无光照贴图),Unity会为同一个表面着色器生成多个“着色器变体”。这就是为什么表面着色器编译后体积可能较大的原因。

实操心得:你可以通过材质Inspector面板下方的“Compiled and Show code”按钮(或Shader代码中点击“Show generated code”),查看Unity为你生成的所有底层HLSL代码。这对于理解表面着色器的原理和调试复杂问题(如为什么某个变体没被编译)至关重要。第一次看可能会觉得眼花缭乱,但它是学习光照计算和优化着色器变体的绝佳资料。

3. 深入核心:输入、输出与光照模型

要玩转表面着色器,必须吃透三个部分:Input结构体、SurfaceOutput结构体家族,以及光照模型。

3.1 扩展Input结构体:获取更多数据

Input结构体是你的数据入口。除了自动匹配的uv纹理坐标,你可以手动添加许多有用的内置变量。

struct Input { float2 uv_MainTex; // 世界空间下的视线方向(从表面指向摄像机) float3 worldPos; // 世界空间下的法线向量(如果surf函数中不写法线,这里需要声明INTERNAL_DATA才能用) float3 worldNormal; INTERNAL_DATA // 顶点颜色(如果网格数据包含) float4 vertexColor : COLOR; // 屏幕空间位置,用于屏幕特效UV计算 float4 screenPos; // 视差贴图等效果可能需要切线空间下的视线方向 float3 viewDir; };
  • worldPos:非常常用,可以用来做基于世界坐标的三维纹理采样(如全局雾效)、计算到某个点的距离等。
  • worldNormal; INTERNAL_DATA:这是一个固定搭配。当你声明了INTERNAL_DATA,并且surf函数中没有o.Normal进行赋值时,你可以在后续(比如自定义光照函数中)使用WorldNormalVector(IN, o.Normal)来获取基于法线贴图扰动后的每像素世界法线。这是实现正确高光和反射的关键。
  • vertexColor:利用顶点颜色驱动材质变化,比如植被的枯萎效果、地形的高度混合。
  • screenPos:用于制作屏幕空间效果,如水渍、雨滴。通常需要经过一步处理:float2 screenUV = IN.screenPos.xy / IN.screenPos.w;

3.2 理解SurfaceOutput家族:描述你的表面

根据你选择的光照模型,你需要使用不同的输出结构体。

1.SurfaceOutput(用于Lambert/BlinnPhong)这是最基础的结构,适用于简单的非物理渲染。

struct SurfaceOutput { fixed3 Albedo; // 漫反射颜色(RGB) fixed3 Normal; // 切线空间法线(如果提供了) fixed3 Emission; // 自发光颜色(RGB) half Specular; // 高光指数(0-1范围) fixed Gloss; // 高光强度/光滑度 fixed Alpha; // 透明度 };

2.SurfaceOutputStandard(用于Standard PBR)这是现代Unity项目中最常用的结构,对应金属工作流(Metallic Workflow)。

struct SurfaceOutputStandard { fixed3 Albedo; // 基础色(漫反射或镜面反射,取决于金属度) fixed3 Normal; // 切线空间法线 half3 Emission; // 自发光 half Metallic; // 金属度:0=非金属(如塑料、木材),1=金属(如金、铁) half Smoothness; // 光滑度:0=粗糙,1=光滑 half Occlusion; // 环境光遮蔽(AO),默认1 fixed Alpha; // 透明度 };
  • 核心逻辑:当Metallic=1时,Albedo被视为镜面反射颜色(F0);当Metallic=0时,Albedo就是漫反射颜色。Smoothness控制高光反射的清晰程度和反射贴图的模糊度。

3.SurfaceOutputStandardSpecular(用于StandardSpecular PBR)对应高光工作流(Specular Workflow),为美术提供了更直接的高光颜色控制。

struct SurfaceOutputStandardSpecular { fixed3 Albedo; // 漫反射颜色 fixed3 Specular; // 高光颜色(RGB) fixed3 Normal; // 切线空间法线 half3 Emission; half Smoothness; // 光滑度 half Occlusion; // 环境光遮蔽 fixed Alpha; };
  • 与Standard的区别:它将Metallic拆分成了独立的Specular颜色和Smoothness。这对于表现有颜色的高光(如铜、漆面)非常有用。

注意事项:在surf函数中,你必须填充你所用结构体的所有字段(除了标记为可选的,如Occlusion默认是1)。即使某个字段你用不到,也要赋予一个默认值(如o.Emission = fixed3(0,0,0);),否则会导致未定义行为,出现奇怪的渲染错误。

3.3 选择与自定义光照模型

#pragma surface指令中的lightModel参数决定了光照如何计算。

  • 内置模型

    • Lambert: 简单的漫反射,计算快,适合风格化或低端设备。
    • BlinnPhong: 包含高光的经典模型。
    • Standard: 基于物理的渲染(PBR)金属工作流。这是当前Unity高清渲染管线(HDRP)和通用渲染管线(URP)内置Lit着色器的基础。
    • StandardSpecular: 基于物理的渲染高光工作流。
  • 自定义光照模型: 这是表面着色器进阶的必经之路。你可以定义自己的光照计算函数。

    // 1. 声明使用自定义光照函数 #pragma surface surf MyCustomLighting // 2. 定义自定义光照函数 // 函数签名是固定的 half4 LightingMyCustomLighting (SurfaceOutput s, half3 lightDir, half atten) { // s: 你的surf函数输出的表面属性 // lightDir: 从表面点到光源的归一化方向(在表面切线空间?注意,通常在世界或切线空间,需查阅文档) // atten: 光源衰减(包含距离衰减和阴影衰减) // 计算漫反射:兰伯特系数 half NdotL = max(0, dot(s.Normal, lightDir)); half3 diffuse = s.Albedo * _LightColor0.rgb * NdotL * atten; // 返回最终颜色(RGB)和透明度(A) return half4(diffuse, s.Alpha); }

    更完整的自定义光照函数通常有多个重载版本,以处理不同光源类型(如点光源、聚光灯)和不同的渲染路径(前向渲染的附加通道)。这是一个相对高级的话题,需要你仔细对照Unity内置光照函数的写法。

4. 高级应用与实战技巧

掌握了基础,我们来看看如何用表面着色器实现一些酷炫且实用的效果。

4.1 实现积雪效果

这是一个经典案例,结合了世界法线、顶点修改和纹理混合。

Shader "Custom/SnowCoverage" { Properties { _MainTex ("Ground Texture", 2D) = "white" {} _SnowTex ("Snow Texture", 2D) = "white" {} _SnowColor ("Snow Color", Color) = (1,1,1,1) _SnowDirection ("Snow Direction", Vector) = (0, 1, 0, 0) // 雪下落方向,通常向上 _SnowDepth ("Snow Depth", Range(0, 0.3)) = 0.1 _SnowThreshold ("Snow Threshold", Range(0, 1)) = 0.5 // 法线点积阈值 } SubShader { Tags { "RenderType"="Opaque" } CGPROGRAM #pragma surface surf Standard vertex:vert addshadow // 使用顶点修改,并添加阴影支持 #pragma target 3.0 sampler2D _MainTex, _SnowTex; fixed4 _SnowColor; float3 _SnowDirection; float _SnowDepth; float _SnowThreshold; struct Input { float2 uv_MainTex; float2 uv_SnowTex; float3 worldNormal; // 获取世界法线用于计算积雪 INTERNAL_DATA // 需要这个来使用WorldNormalVector }; // 顶点修改函数:根据法线方向挤出顶点模拟积雪堆积 void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); // 将法线从物体空间转换到世界空间 float3 worldNormal = UnityObjectToWorldNormal(v.normal); // 计算世界法线与雪方向的点积。越朝上(与雪方向一致)值越大。 float snowFactor = max(0, dot(worldNormal, normalize(_SnowDirection))); // 如果超过阈值,则挤出顶点 if (snowFactor > _SnowThreshold) { // 挤出顶点沿法线方向(简单模拟) v.vertex.xyz += v.normal * _SnowDepth * snowFactor; } } void surf (Input IN, inout SurfaceOutputStandard o) { // 采样地面和雪纹理 fixed4 ground = tex2D(_MainTex, IN.uv_MainTex); fixed4 snow = tex2D(_SnowTex, IN.uv_SnowTex) * _SnowColor; // 再次计算世界法线点积(也可以在Input中计算一次传递过来) // 注意:这里使用surf阶段计算的法线更准确(如果使用了法线贴图) float3 worldNormal = WorldNormalVector(IN, o.Normal); float snowFactor = saturate(dot(worldNormal, normalize(_SnowDirection))); // 平滑过渡 snowFactor = smoothstep(_SnowThreshold - 0.1, _SnowThreshold + 0.1, snowFactor); // 混合地面和雪的颜色 o.Albedo = lerp(ground.rgb, snow.rgb, snowFactor); // 雪更光滑,地面更粗糙 o.Smoothness = lerp(0.1, 0.8, snowFactor); // 雪是非金属的 o.Metallic = 0.0; o.Alpha = 1; } ENDCG } FallBack "Diffuse" }

技巧解析

  • vertex:vert:使用了自定义顶点函数,在顶点阶段修改几何体形状。
  • addshadow至关重要!当修改了顶点位置后,必须添加此指令,Unity才会生成正确的阴影投射Pass,否则阴影会停留在原网格位置,导致“影身分离”。
  • worldNormal; INTERNAL_DATAWorldNormalVector:在surf中获取经过法线贴图扰动后的每像素世界法线,用于计算更精确的积雪覆盖。
  • smoothstep:用于在阈值附近产生平滑的混合过渡,避免生硬的边界。

4.2 实现动态溶解效果

溶解效果常用于角色死亡、物体消失等场景。

Shader "Custom/Dissolve" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _NoiseTex ("Noise Tex", 2D) = "white" {} _EdgeColor ("Edge Color", Color) = (1,1,0,1) _EdgeWidth ("Edge Width", Range(0, 0.2)) = 0.05 _DissolveThreshold ("Dissolve Threshold", Range(0, 1)) = 0 } SubShader { Tags { "RenderType"="Opaque" "Queue"="AlphaTest" // 使用AlphaTest队列,便于裁剪 } CGPROGRAM #pragma surface surf Standard alphatest:_DissolveThreshold addshadow #pragma target 3.0 sampler2D _MainTex, _NoiseTex; fixed4 _EdgeColor; float _EdgeWidth; float _DissolveThreshold; struct Input { float2 uv_MainTex; float2 uv_NoiseTex; }; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 mainColor = tex2D(_MainTex, IN.uv_MainTex); // 采样噪声图,用单个通道(如r)作为溶解因子 fixed noise = tex2D(_NoiseTex, IN.uv_NoiseTex).r; // 核心溶解逻辑 float dissolve = step(noise, _DissolveThreshold); // 小于阈值的地方为1(被溶解),否则为0 float edge = step(noise, _DissolveThreshold + _EdgeWidth); // 边缘区域 // 计算边缘颜色贡献 fixed3 edgeColor = _EdgeColor.rgb * (edge - dissolve); // 只在边缘带显示 o.Albedo = mainColor.rgb; o.Emission = edgeColor; // 将边缘颜色作为自发光,产生发光边效果 o.Metallic = 0; o.Smoothness = 0; // Alpha通道用于裁剪。当noise小于阈值时,discard该像素。 // 注意:这里直接使用noise值,surf输出的Alpha值本身不直接用于混合,而是由alphatest指令控制裁剪。 // 我们需要一个变量传递给alphatest。通常的做法是定义一个与_DissolveThreshold同名的变量。 // 但更直接的方式是在surf中计算一个裁剪因子,并赋值给o.Alpha。 // 实际上,`alphatest:_DissolveThreshold`指令会使用材质属性_DissolveThreshold进行裁剪。 // 我们只需确保需要保留的像素Alpha不为0即可。 o.Alpha = mainColor.a * (1 - dissolve); // 被溶解的区域Alpha为0,将被裁剪 } ENDCG } FallBack "Standard" }

技巧解析

  • Tags { "Queue"="AlphaTest" }:将渲染队列设置为AlphaTest,这是一个在Geometry之后、Transparent之前的队列,专门用于需要进行Alpha测试(像素丢弃)的物体,性能优于透明度混合。
  • alphatest:_DissolveThreshold:这是表面着色器处理Alpha测试的关键指令。它会根据_DissolveThreshold的值,在生成的片元着色器中对o.Alpha进行裁剪(discard)。注意,这里的变量名必须与一个Properties中的floatRange属性同名。
  • step函数:是HLSL中的阶跃函数,step(a, x)x >= a时返回1,否则返回0。用于创建硬边。
  • Emission的妙用:将溶解边缘的颜色赋给o.Emission,可以轻松实现发光边效果,且不受场景光照影响,非常醒目。

4.3 利用finalcolor进行后处理色彩调整

finalcolor指令允许你在所有光照计算完成后,对最终输出颜色进行最后一次修改。这非常适合实现全屏色彩效果、颜色分级或简单的屏幕特效。

Shader "Custom/ColorGradeSurface" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _Brightness ("Brightness", Range(0, 2)) = 1 _Contrast ("Contrast", Range(0, 2)) = 1 _Saturation ("Saturation", Range(0, 2)) = 1 } SubShader { Tags { "RenderType"="Opaque" } CGPROGRAM #pragma surface surf Standard finalcolor:myFinalColor #pragma target 3.0 sampler2D _MainTex; float _Brightness, _Contrast, _Saturation; struct Input { float2 uv_MainTex; }; // 最终颜色修改函数 void myFinalColor (Input IN, SurfaceOutputStandard o, inout fixed4 color) { // color 是经过所有光照计算后的最终片元颜色 // 1. 亮度 color.rgb *= _Brightness; // 2. 对比度 fixed3 avgColor = fixed3(0.5, 0.5, 0.5); color.rgb = lerp(avgColor, color.rgb, _Contrast); // 3. 饱和度 fixed luminance = Luminance(color.rgb); color.rgb = lerp(fixed3(luminance, luminance, luminance), color.rgb, _Saturation); } void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Metallic = 0; o.Smoothness = 0; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }

注意事项finalcolor函数在每个像素的最终输出阶段被调用,性能开销需要考量。它看到的是经过所有光照、阴影、雾效等处理后的颜色。这对于需要基于最终结果进行操作的屏幕空间效果非常有用,但要注意它是在物体层面,而非真正的全屏后处理。

5. 性能优化与常见问题排查

表面着色器虽然方便,但生成的代码可能很庞大,变体众多,容易成为性能瓶颈。

5.1 关键编译指令与优化

通过#pragma surface指令后的可选参数,可以精确控制生成哪些代码,剔除不必要的功能。

  • exclude_path系列:如果你的游戏只使用前向渲染路径,可以禁用延迟渲染路径的变体。

    #pragma surface surf Standard exclude_path:deferred

    同样,exclude_path:forwardexclude_path:prepass(用于旧版延迟)也是如此。

  • noshadow:如果这个物体绝对不需要接收任何阴影。

    #pragma surface surf Standard noshadow
  • nolightmap:如果这个物体永远不会被烘焙光照贴图。

    #pragma surface surf Standard nolightmap
  • noforwardadd极其重要的优化指令。它禁用了前向渲染中的附加逐像素光源(Additional Per-Pixel Lights)通道。这意味着你的着色器只会为一个最主要的逐像素光源(通常是主平行光)进行完整计算,其他光源将以更高效的逐顶点或球谐函数(SH)方式计算。这对于大量使用实时光源的移动端或低配平台是首选。

    #pragma surface surf Lambert noforwardadd

    实操心得:对于场景中大部分静态或动态物体,使用noforwardadd可以显著减少Draw Call和着色器指令数。只有那些需要被多个强光源(如聚光灯)照亮的特殊物体(如主角、主要交互物体)才需要完整的逐像素多光源支持。

  • fullforwardshadows:与noforwardadd相对,如果你需要在前向渲染中让附加光源也投射阴影,就需要这个指令。但这会显著增加着色器变体数量和复杂度。

5.2 常见问题排查实录

问题1:材质显示为洋红色(Missing Shader)

  • 原因:这是Unity的“粉红错误”,表示着色器编译失败。
  • 排查
    1. 检查控制台(Console)窗口,会有具体的编译错误信息。通常是语法错误、未定义的变量或函数签名错误。
    2. 检查Properties块中声明的变量名是否与CGPROGRAM中使用的完全一致(包括大小写)。
    3. 检查Input结构体中的变量名是否规范(如uv_MainTex)。

问题2:法线贴图看起来不对,或者高光错位

  • 原因:法线信息处理错误。表面着色器期望o.Normal中的法线是切线空间下的。
  • 排查
    1. 确保你赋给o.Normal的是从法线贴图采样并解压后的切线空间法线。通常需要UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap))
    2. 如果你在Input中使用了worldNormal; INTERNAL_DATA,并在其他地方用WorldNormalVector(IN, o.Normal),请确保surf函数中没有o.Normal进行赋值。如果赋值了,INTERNAL_DATA机制可能失效。

问题3:透明效果不正常,或者物体排序错误

  • 原因:渲染队列(Queue)和混合模式设置不当。
  • 排查
    1. 对于完全透明(Alpha Blending)物体,Tags应为"Queue"="Transparent",并且通常需要"IgnoreProjector"="True""RenderType"="Transparent"。在#pragma surface中可能需要alpha:blend
    2. 对于镂空(Alpha Test)物体,Tags应为"Queue"="AlphaTest",使用alphatest:VariableName指令。
    3. 检查是否在SubShaderPass中设置了正确的混合命令,如Blend SrcAlpha OneMinusSrcAlpha

问题4:在移动设备上运行非常慢

  • 原因:着色器过于复杂,变体太多,或使用了高代价指令。
  • 排查与优化
    1. 使用Shader Variant Collection工具收集并预编译实际用到的着色器变体,减少运行时编译卡顿。
    2. Graphics Settings中设置Shader Stripping级别,剔除不需要的变体。
    3. 简化surf函数中的计算,避免循环、分支和复杂的数学函数(如sin,pow)。
    4. 尽可能使用fixedhalf精度代替float,尤其是在移动平台。
    5. 积极使用exclude_pathnoforwardaddnolightmap等指令精简功能。
    6. 考虑将复杂的表面着色器功能拆解,部分效果通过后处理或脚本计算来实现。

问题5:自定义顶点修改后,阴影位置不对

  • 原因:这是最常见的问题之一。修改了顶点位置,但阴影投射Pass仍然使用原始的、未修改的网格数据。
  • 解决方案必须在#pragma surface指令中添加addshadow。这会告诉Unity生成一个适配你顶点修改函数的阴影投射Pass。这是实现顶点动画、曲面细分、积雪等效果时必须的一步。

表面着色器是Unity给图形开发者的一份厚礼,它极大地降低了编写复杂光照着色器的门槛。但要想真正驾驭它,必须理解其“代码生成”的本质,熟悉其输入输出流程,并善用各种编译指令进行优化。从简单的材质定义到复杂的顶点动画、屏幕后处理,表面着色器都能提供一套相对高效的解决方案。当你遇到性能瓶颈或需要极致控制时,再深入到底层的顶点/片元着色器也不迟。在绝大多数游戏对象的渲染需求上,表面着色器依然是那个最趁手、最高效的工具。

← 返回列表