ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Unity ShaderLab入门:从零编写第一个表面着色器

Unity ShaderLab入门:从零编写第一个表面着色器 1. 项目概述为什么你需要亲手写一个Shader如果你在Unity里做过项目大概率用过Material材质和Shader着色器。你可能从Asset Store下载过炫酷的材质包或者在网上复制粘贴过一段Shader代码然后拖拽几张贴图看着模型瞬间变得流光溢彩。但当你想要实现一个特定的效果比如让角色受伤时身体边缘泛红或者让水面有独特的波纹时却发现现有的Shader怎么调参数都不对劲。这时你需要的不是另一个现成的Shader而是理解如何从零开始自己写一个。Shader简单来说就是一段运行在GPU上的小程序。它决定了屏幕上每一个像素或顶点最终呈现的颜色。Unity的ShaderLab语言结合了Cg/HLSL为我们提供了一套相对友好的工具来编写这些程序。很多人觉得Shader是图形程序员的专属领域门槛极高但事实并非如此。一个基础的、能用的Shader其核心逻辑可能只有十几行代码。关键在于理解它的运作模式和基本结构。这篇教程就是为你准备的无论你是刚接触Unity的开发者还是已经熟悉C#脚本但对渲染管线一知半解的美术或TA。我们将彻底抛开那些复杂的数学公式和图形学理论从一个最纯粹的“Hello World”开始——一个只显示单一颜色的Shader。通过这个过程你将亲手搭建起Shader的骨架理解属性Properties、子着色器SubShader、表面函数Surface Function这些核心概念是如何串联起来的。当你完成第一个属于你自己的Shader时你获得的不仅是一个可以运行的代码文件更是一把打开图形渲染世界大门的钥匙。之后无论是修改别人的Shader还是实现自己天马行空的效果你都将拥有清晰的思路和扎实的起点。2. 核心概念与ShaderLab基础结构拆解在动笔写代码之前我们必须先搞清楚Unity Shader的基本构成。一个Unity Shader文件.shader并不是直接用Cg或HLSL写的而是用一种叫做ShaderLab的包装语言来组织的。ShaderLab定义了Shader的整体结构、属性面板的显示、以及在不同硬件平台下该使用哪段代码。你可以把它看作一个“容器”或者“说明书”而真正的计算逻辑Cg/HLSL代码被包裹在这个容器内部。2.1 Shader文件的基本骨架一个最简单的、合法的Unity Shader文件看起来是这样的Shader MyShader/SimpleColor { Properties { // 属性定义将在这里 } SubShader { Tags { RenderTypeOpaque } Pass { CGPROGRAM // Cg/HLSL代码将在这里 ENDCG } } FallBack Diffuse }我们来逐行拆解这个骨架第一行Shader MyShader/SimpleColor这是Shader的声明和它的“路径名”。MyShader/SimpleColor决定了你在材质球的Shader下拉菜单里如何找到它。MyShader是分类文件夹SimpleColor是具体的Shader名称。这个命名对组织管理非常重要。Properties { }块这里定义了Shader对外暴露的参数。你在材质球Inspector面板里能看到的颜色拾取器、滑动条、贴图槽都是在这里定义的。这些属性是Shader与美术、策划进行交互的界面。SubShader { }块这是Shader的核心主体。一个Shader可以包含多个SubShader。Unity会从第一个SubShader开始检查如果当前运行平台支持这个SubShader的所有特性就使用它如果不支持就尝试下一个SubShader。这主要是为了兼容不同的显卡和图形API如OpenGL ES, DirectX等。我们的大部分代码都写在这里面。Pass { }块位于SubShader内部。一次Pass代表了一次完整的渲染流程。一个复杂的效果可能需要多个Pass来实现比如第一个Pass渲染主体颜色第二个Pass添加边缘光。我们第一个Shader只需要一个Pass。CGPROGRAM和ENDCG这对标记告诉Unity它们之间的代码是用Cg/HLSL语言编写的是需要被编译并运行在GPU上的核心程序。FallBack Diffuse回退指令。如果上面所有的SubShader在当前硬件上都无法运行Unity就会使用这个指定的回退Shader通常是内置的Diffuse来渲染物体避免物体变成可怕的“粉红色错误材质”。2.2 理解“表面着色器”与“顶点/片元着色器”在Unity的ShaderLab体系里我们主要写两种类型的着色器表面着色器Surface Shader和顶点/片元着色器Vertex/Fragment Shader。表面着色器Surface Shader这是Unity提供的一个高级抽象层。你只需要定义一个“表面函数surface function”告诉Unity物体表面的颜色、法线、高光等属性是什么Unity会自动帮你生成处理光照、阴影所需的顶点/片元着色器代码。它写法更简洁非常适合需要与标准光照模型交互的物体。我们本篇教程将使用表面着色器入门因为它能让我们快速看到效果避开很多底层细节。顶点/片元着色器Vertex/Fragment Shader这是更底层、更灵活的方式。你需要完全自己编写从顶点变换到最终像素颜色的每一个步骤。这让你能实现任何天马行空的效果如扭曲、溶解、全屏后处理但同时也需要处理更多细节如光照、雾效等。这是我们掌握表面着色器后的进阶方向。对于初学者从表面着色器开始是明智的选择。它能让你在理解核心概念属性、结构、输出的同时快速获得成就感。注意表面着色器本质上是一个“代码生成模板”。你在写#pragma surface surf Lambert时Unity的编辑器会在背后为你生成对应的顶点/片元着色器代码。这虽然方便但也意味着你对最终代码的控制力会弱一些。在性能极度敏感或需要特殊优化的场景最终可能会转向手写顶点/片元着色器。3. 从零开始编写第一个单色表面着色器现在让我们打开Unity创建一个新的Shader文件。在Project窗口右键 - Create - Shader - Standard Surface Shader。Unity会为你生成一个模板文件但为了彻底理解我们将其内容清空从头开始敲入以下代码。3.1 第一步搭建基础框架与定义属性我们首先搭建ShaderLab的外壳并定义一个颜色属性。Shader MyFirstShader/SolidColor { Properties { _Color (Main Color, Color) (1, 0.5, 0, 1) // 一个橙色的默认值 }_Color这是我们在Shader内部使用的变量名必须以_开头这是一个常见的约定。Main Color这是在材质Inspector面板上显示的名称可以起一个友好的中文名如“主颜色”。Color属性的类型。这里定义为Color颜色对应一个四维向量(R, G, B, A)。(1, 0.5, 0, 1)默认值。这是一个RGBA颜色代表不透明的橙色红1绿0.5蓝0Alpha1。3.2 第二步编写SubShader与表面着色器代码接下来我们编写SubShader部分。我们将使用表面着色器。SubShader { Tags { RenderTypeOpaque } // 标签这是一个渲染不透明物体的Shader CGPROGRAM // 编译指令声明一个表面着色器表面函数名为surf使用Lambert漫反射光照模型 #pragma surface surf Lambert // 为了在CG代码中使用Properties里定义的_Color我们必须在这里重新声明一遍变量 // 并且类型要匹配。fixed4在CG中是一种低精度颜色类型性能较好。 fixed4 _Color; // Input结构体用于从顶点着色器向表面函数surf传递数据。 // 即使我们现在不需要UV坐标也最好先定义一个空结构这是标准写法。 struct Input { float2 uv_MainTex; // 虽然我们不用贴图但保留这个声明很常见 }; // 表面函数surf这是核心我们在这里计算表面的输出。 // Input IN: 输入数据。 // inout SurfaceOutput o: 输出结构体inout表示我们可以修改它。 void surf (Input IN, inout SurfaceOutput o) { // 将我们定义的_Color赋值给输出结构的Albedo漫反射颜色。 // Albedo决定了物体表面的基础颜色。 o.Albedo _Color.rgb; // 将_Color的透明度赋值给输出结构的Alpha。 o.Alpha _Color.a; } ENDCG }让我们深入理解surf函数和SurfaceOutputSurfaceOutput是一个预定义的结构体它描述了物体表面的各种属性以便Unity的后继光照计算使用。其主要成员包括Albedo (float3)漫反射颜色。这是我们最常操作的属性。Normal (float3)表面法线。用于修改光照计算。Emission (float3)自发光颜色。即使没有光照物体也会显示这个颜色。Specular (float3)和Gloss (float)用于控制高光反射。Alpha (float)透明度。在我们的surf函数里我们只做了最简单的事把用户通过材质球设置的_Color直接赋给了输出的Albedo和Alpha。这意味着这个物体将呈现纯色并且完全遵循Lambert光照模型简单的漫反射。3.3 第三步完成Shader并测试最后我们加上回退指令并关闭所有括号。FallBack Diffuse // 如果上述SubShader不支持则回退到内置的Diffuse Shader }现在完整的Shader代码如下Shader MyFirstShader/SolidColor { Properties { _Color (Main Color, Color) (1, 0.5, 0, 1) } SubShader { Tags { RenderTypeOpaque } CGPROGRAM #pragma surface surf Lambert fixed4 _Color; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { o.Albedo _Color.rgb; o.Alpha _Color.a; } ENDCG } FallBack Diffuse }测试步骤在Unity中将这段代码保存为一个.shader文件例如SolidColor.shader。在Project窗口中右键该Shader文件选择Create - MaterialUnity会自动创建一个使用此Shader的材质球。在Scene中创建一个Cube或Sphere。将新建的材质球拖拽到场景中的物体上。选中材质球在Inspector面板中你应该能看到一个名为“Main Color”的颜色选择器。尝试改变颜色场景中的物体会实时变化并且会根据场景灯光产生明暗效果Lambert光照。恭喜你已经成功编写并运行了你的第一个自定义Shader。它虽然简单但包含了所有必要元素属性定义、SubShader、CG程序块、输入输出结构。你已经跨出了最关键的一步。4. 核心环节深化添加纹理与理解UV采样一个纯色Shader显然不能满足需求。接下来我们让Shader支持贴图这是几乎所有Shader的基础功能。我们将创建一个经典的“漫反射纹理”Shader。4.1 扩展属性添加纹理属性首先在Properties块中添加一个贴图属性。Properties { _MainTex (Albedo (RGB), 2D) white {} // 添加主纹理 _Color (Tint Color, Color) (1,1,1,1) // 将颜色改为“染色”颜色默认白色表示不影响纹理原色 }_MainTex变量名。Albedo (RGB)显示名。2D属性类型表示这是一张2D纹理。white {}默认值。white表示当没有指定纹理时使用纯白色填充。{}是纹理属性必需的选项占位符可以为空也可以在里面设置一些高级纹理坐标模式。4.2 修改CG代码声明、输入与采样我们需要在CGPROGRAM块中重新声明_MainTex以便在CG代码中使用它。同时我们需要一种方式来告诉CG程序“对纹理的哪个位置进行采样”这就是UV坐标。CGPROGRAM #pragma surface surf Lambert // 声明在Properties中定义的变量 sampler2D _MainTex; // sampler2D是CG中用于纹理采样的类型 fixed4 _Color; struct Input { float2 uv_MainTex; // 关键声明一个名为uv_MainTex的二维浮点向量。 // Unity会自动将模型网格上的UV坐标填充到这个变量中。 }; void surf (Input IN, inout SurfaceOutput o) { // 使用tex2D函数进行纹理采样。 // 参数1: sampler2D类型的纹理即_MainTex。 // 参数2: 二维坐标即IN.uv_MainTex。 // 返回值: fixed4类型即纹理在该坐标点的颜色值(R,G,B,A)。 fixed4 c tex2D (_MainTex, IN.uv_MainTex); // 将采样到的颜色c与染色颜色_Color相乘得到最终颜色。 // 这样可以方便地调整纹理的整体色调。 o.Albedo c.rgb * _Color.rgb; o.Alpha c.a * _Color.a; } ENDCG核心解释sampler2D _MainTex;这行代码链接了ShaderLab属性_MainTex和CG程序中的采样器。它们是同一个数据的不同接口。float2 uv_MainTex;在Input结构体中变量名uv_MainTex有特殊含义。Unity的Surface Shader编译器有一套约定如果你定义了一个名为uv后接纹理变量名这里是_MainTex的变量Unity会自动将模型的第一套UV坐标通常用于漫反射贴图传递给它。这是Surface Shader提供的一个非常方便的语法糖。tex2D(_MainTex, IN.uv_MainTex);这是CG标准库函数用于在纹理_MainTex的IN.uv_MainTex坐标处进行采样。UV坐标的范围通常是(0,0)到(1,1)对应纹理的左下角到右上角。4.3 实操与观察更新你的Shader代码或者新建一个Shader文件。创建一个新材质并应用此Shader。将一个纹理图片例如一张砖墙贴图拖拽到材质Inspector的“Albedo (RGB)”贴图槽。将材质赋予一个3D物体。你会发现纹理被正确地“贴”在了模型表面。尝试调整“Tint Color”纹理的颜色会随之变化。设置为白色(1,1,1)时纹理保持原色设置为红色(1,0,0)时纹理会偏红。实操心得uv_MainTex这个命名约定非常有用。如果你的纹理变量叫_DetailTex那么对应的UV变量就应该是uv_DetailTex。如果你想使用模型的第二套UV常用于光照贴图或细节贴图则需要使用uv2_DetailTex。理解并善用这些约定能省去很多手动传递UV坐标的麻烦。5. 进阶探索实现自定义简单效果自发光掌握了纹理之后我们可以尝试修改Shader的输出实现一些简单的自定义效果。让我们给Shader添加一个自发光Emission效果让物体即使在暗处也能自己发光。5.1 添加自发光属性在Properties块中添加两个新属性一个自发光颜色一个控制发光强度的滑动条。Properties { _MainTex (Albedo (RGB), 2D) white {} _Color (Tint Color, Color) (1,1,1,1) _EmissionColor (Emission Color, Color) (0,0,0,1) // 自发光颜色默认黑色不发光 _EmissionIntensity (Emission Intensity, Range(0, 5)) 1 // 强度范围0到5 }5.2 修改表面函数自发光信息存储在SurfaceOutput结构体的Emission成员中。我们只需要在surf函数中计算它并赋值即可。void surf (Input IN, inout SurfaceOutput o) { fixed4 c tex2D (_MainTex, IN.uv_MainTex); o.Albedo c.rgb * _Color.rgb; o.Alpha c.a * _Color.a; // 计算自发光颜色 * 强度 // 这里使用乘法是因为_EmissionIntensity是一个标量用来整体调节发光亮度。 // 你也可以设计更复杂的公式比如让自发光也受纹理影响。 o.Emission _EmissionColor.rgb * _EmissionIntensity; }代码解析o.Emission这个值会被直接加到最终像素颜色上不受场景光照影响。即使物体处在阴影中Emission部分也会完全显示。_EmissionColor.rgb * _EmissionIntensity这是一种常见的控制方式。_EmissionColor决定了发光的色调_EmissionIntensity作为一个乘数可以方便地从0不发光调到很高的值非常亮。5.3 效果测试与理解应用更新后的Shader。在材质面板将“Emission Color”调成一个明亮的颜色如亮蓝色(0, 0.5, 1)。慢慢调高“Emission Intensity”。你会看到物体开始自己发出蓝光即使你关掉场景中的所有灯光它依然可见。尝试将“Emission Color”设置为纹理采样的颜色c.rgb并调整强度可以实现类似“泛光”或者能量核心的效果。这个简单的例子展示了修改SurfaceOutput其他成员来实现不同效果的基本模式。通过组合Albedo、Normal法线、Emission、Specular等你可以创造出丰富的表面外观。6. 常见问题、调试技巧与性能初探在编写和调试Shader的过程中你一定会遇到各种问题。以下是一些新手最常见的问题和解决思路。6.1 常见编译错误与警告错误 CSxxxx: undefined identifier ‘xxx’未定义的标识符。99%的情况是你忘了在CGPROGRAM块中重新声明在Properties里定义的变量。确保每个在CG代码中使用的属性变量如_Color,_MainTex都在CGPROGRAM块顶部有对应的声明fixed4 _Color;,sampler2D _MainTex;。错误invalid output semantic ‘SV_Target’通常意味着你的表面函数surf没有正确地为SurfaceOutput结构体的所有必要成员赋值。对于SurfaceOutput不是SurfaceOutputStandardAlbedo和Alpha通常是必须赋值的。确保你的代码路径比如if-else的所有分支都设置了这些值。粉色材质Missing Shader这表示Shader编译失败或者当前SubShader不被支持。首先检查Console窗口的编译错误信息。如果没错误可能是你的FallBack也失效了尝试改成FallBack Standard或FallBack Legacy Shaders/VertexLit。6.2 调试技巧使用颜色输出Shader调试不像C#代码可以设断点。最有效的调试方法是“可视化”。你可以临时修改surf函数将中间变量的值直接输出到颜色上来观察它们是否正常。例如你想查看UV坐标是否正确void surf (Input IN, inout SurfaceOutput o) { // 将UV的U通道水平方向作为红色V通道垂直方向作为绿色输出 o.Albedo fixed3(IN.uv_MainTex.x, IN.uv_MainTex.y, 0); o.Alpha 1; }如果模型表面显示从黑到红左到右和从黑到绿下到上的渐变说明UV坐标传递正确。再比如你想看纹理采样结果void surf (Input IN, inout SurfaceOutput o) { fixed4 c tex2D (_MainTex, IN.uv_MainTex); // 只输出红色通道 o.Albedo fixed3(c.r, 0, 0); o.Alpha 1; }6.3 性能考量入门虽然第一个Shader不用太担心性能但建立良好的意识很重要。精度选择注意我们用的变量类型是fixed4、half4还是float4。fixed低精度通常用于颜色范围0-1。在移动平台或低端GPU上性能最好。half中精度范围更大。是性能和精度的良好折中适用于大部分计算。float全精度用于需要高精度的计算如世界空间位置、复杂的数学运算。原则在满足效果的前提下使用尽可能低的精度。对于颜色和UVfixed和half通常足够。纹理采样次数tex2D操作是相对耗时的。尽量避免在同一个Shader中多次采样同一张纹理除非必要。如果需要在不同地方使用同一纹理的不同通道如R通道做遮罩RGB做颜色尽量只采样一次然后复用采样结果。复杂的数学运算sin,cos,pow,sqrt等函数比加减乘除要慢。在片元着色器surf函数运行在片元级别中大量使用会显著影响性能。如果可能考虑将计算移到顶点着色器通过修改Input结构体传递数据或者使用查找表LUT等技巧。6.4 材质参数动画你可能会问如何在运行时通过脚本改变Shader的属性比如让颜色闪烁这非常简单。在C#脚本中你可以使用Material类的SetColor,SetFloat,SetTexture等方法。using UnityEngine; public class ShaderAnimator : MonoBehaviour { public Color targetColor; private Material myMaterial; void Start() { // 获取当前物体材质实例 myMaterial GetComponentRenderer().material; // 注意material会创建实例 // 如果希望修改共享材质使用.sharedMaterial } void Update() { // 动态修改_Color属性 myMaterial.SetColor(_Color, Color.Lerp(Color.white, targetColor, Mathf.Sin(Time.time) * 0.5f 0.5f)); // 动态修改_EmissionIntensity属性 myMaterial.SetFloat(_EmissionIntensity, Mathf.PingPong(Time.time, 2.0f)); } }重要提示GetComponentRenderer().material会在运行时创建一个该材质的独立实例Instance对这个实例的修改不会影响其他使用同一材质的物体。如果你想让所有使用该材质的物体一起变化请使用sharedMaterial但要小心这会永久改变项目中的材质资源文件。走到这里你已经不再是Shader的完全门外汉了。你理解了ShaderLab的基本结构能够编写一个带纹理和自发光的表面着色器并且知道如何调试和动态控制它。这为你探索更广阔的Shader世界——如法线贴图、高光反射、透明效果、顶点动画乃至屏幕后处理——打下了坚实的基础。记住学习Shader是一个不断“模仿-修改-创造”的过程多阅读Unity内置的Shader源码在Unity安装目录的Editor/Data文件夹中可以找到多尝试修改其中的参数和逻辑是提升最快的方式。当你卡住时把问题拆解用颜色输出法一步步调试复杂的特效往往都是由这些简单的模块组合而成的。
返回列表