Unity Shader实战:5分钟搞懂顶点着色器与片元着色器的区别与应用

📅 发布时间:2026/7/5 16:19:04 👁️ 浏览次数:
Unity Shader实战:5分钟搞懂顶点着色器与片元着色器的区别与应用
Unity Shader实战5分钟搞懂顶点着色器与片元着色器的区别与应用很多刚接触Unity Shader的朋友一看到“顶点着色器”和“片元着色器”这两个词就有点发怵感觉是图形学里深不可测的黑魔法。其实它们并没有想象中那么复杂。你可以把渲染管线想象成一个制作动画电影的流水线顶点着色器负责给3D模型“摆好姿势”确定每个关键点在屏幕上的位置而片元着色器则是给这个姿势下的模型“上色化妆”决定最终观众看到的每一帧画面是什么颜色和质感。今天我们不谈枯燥的理论就从你手头可能正在做的水面波纹、动态光照这些具体效果出发拆解这两个核心着色器到底怎么用区别在哪以及如何用它们玩出花样。无论你是想优化角色特效的初级开发者还是希望深入定制渲染流程的中级技术美术这篇文章都能给你带来即学即用的清晰思路。1. 渲染管线理解着色器工作的舞台在深入顶点与片元着色器之前我们必须先了解它们所处的“工作环境”——渲染管线。如果把最终呈现在屏幕上的精美画面比作一顿大餐那么渲染管线就是厨房里从备菜到装盘的完整流程。这个流程是高度标准化和流水线化的每个环节各司其职。现代实时渲染管线如Unity的URP/HDRP或传统的内置管线大致遵循一个经典的顺序应用阶段 - 几何阶段 - 光栅化阶段。你的CPU和GPU在这个流程中分工明确。应用阶段 (CPU主导)这是你的游戏逻辑主场。CPU在这里决定要画什么哪些物体在摄像机视野内、用什么画调用哪个Shader、设置哪些材质参数然后通过一个叫做DrawCall的指令把“绘画任务”连同所有数据模型顶点、纹理、变换矩阵等打包交给GPU。这个阶段的核心是组织和准备数据。几何阶段 (GPU主导)GPU拿到数据包后首先进入几何阶段。这里的明星就是顶点着色器 (Vertex Shader)。它的任务是对模型的每一个顶点进行处理比如将顶点从模型自身的局部坐标系经过一系列变换模型变换、视图变换、投影变换最终转换到屏幕坐标系。此外裁剪掉视野之外的图元也发生在这里。你可以把这个阶段理解为确定3D物体在2D屏幕上投影的“骨架”或“线稿”。光栅化阶段 (GPU主导)几何阶段输出的是由顶点构成的三角形图元。光栅化阶段的任务就是把这些三角形“填充”成像素。它会计算每个三角形覆盖了屏幕上的哪些像素并为每个被覆盖的像素生成一个片元 (Fragment)。片元包含了位置、深度、以及从顶点插值而来的各种属性如颜色、纹理坐标。接着片元着色器 (Fragment Shader)登场它对每一个片元进行最终的颜色计算包括纹理采样、光照计算、特效叠加等。最后通过深度测试、模板测试、混合等操作决定这个片元的颜色是否以及如何写入最终的屏幕图像缓冲区。理解这个管线流程至关重要因为它清晰地划分了顶点着色器和片元着色器的职责边界和操作时机。下面这个表格能帮你快速抓住核心阶段主导处理器核心输入核心处理单元核心输出开发者控制程度应用阶段CPU场景数据、渲染状态游戏逻辑DrawCall指令、渲染数据包完全可编程几何阶段GPU顶点数据包顶点着色器等屏幕空间的图元顶点着色器可编程其他阶段可配置光栅化阶段GPU屏幕空间图元片元着色器等最终的像素颜色片元着色器可编程逐片元操作可配置提示一次DrawCall调用会触发整个渲染管线对该次调用所提交数据的一次完整处理。优化DrawCall数量是提升游戏性能的关键手段之一通常通过静态/动态批处理、GPU Instancing等技术实现。2. 顶点着色器掌控模型的“形变”与运动顶点着色器是几何阶段的核心可编程单元。它的工作对象是模型的每一个顶点。输入的是顶点的原始属性位置、法线、纹理坐标、颜色等输出的是经过处理后的顶点属性其中最重要的就是变换后的齐次裁剪空间坐标。2.1 核心职责坐标变换与顶点属性处理顶点着色器最基本、最强制性的任务就是将顶点从模型局部空间转换到齐次裁剪空间。在Unity中这通常通过乘以一系列矩阵来完成// 在顶点着色器中常见的坐标变换代码 v2f vert (appdata v) { v2f o; // 将顶点从模型空间转换到世界空间 float4 worldPos mul(unity_ObjectToWorld, v.vertex); // 将顶点从世界空间转换到观察摄像机空间 float4 viewPos mul(UNITY_MATRIX_V, worldPos); // 将顶点从观察空间转换到齐次裁剪空间 o.vertex mul(UNITY_MATRIX_P, viewPos); // 传递纹理坐标到片元着色器 o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; }除了这个“规定动作”顶点着色器可以自由地修改任何传入的顶点属性从而实现丰富的动态效果顶点动画通过随时间变化的函数如正弦波修改顶点的y坐标就能轻松实现旗帜飘动、水面波纹的效果。这是顶点着色器的典型应用场景因为计算在顶点级别进行效率远高于在片元级别对每个像素做类似计算。程序化变形根据顶点到某个中心点的距离缩放或扭曲其位置可以制作出物体膨胀、收缩或融化的效果。传递计算数据可以在顶点着色器中预先进行一些计算如光照模型中的世界空间法线、视角方向等然后将结果传递给片元着色器避免在片元着色器中重复计算这是一种常见的优化手段。2.2 实战案例实现简单水面波纹让我们看一个具体的例子。假设你想让一个平面网格产生类似水面的波动。在片元着色器里通过纹理采样模拟虽然可行但缺乏真实的几何起伏感。用顶点着色器实现则更加自然高效。Shader Custom/SimpleWaterVertex { Properties { _MainTex (Texture, 2D) white {} _WaveSpeed (Wave Speed, Float) 1.0 _WaveFrequency (Wave Frequency, Float) 1.0 _WaveAmplitude (Wave Amplitude, Float) 0.1 } SubShader { 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; float4 _MainTex_ST; float _WaveSpeed; float _WaveFrequency; float _WaveAmplitude; v2f vert (appdata v) { v2f o; // 在模型空间直接修改顶点Y坐标 float4 modifiedVertex v.vertex; // 使用正弦函数结合时间和顶点XZ位置计算Y轴偏移 float wave sin(_Time.y * _WaveSpeed (v.vertex.x v.vertex.z) * _WaveFrequency) * _WaveAmplitude; modifiedVertex.y wave; // 将修改后的顶点变换到裁剪空间 o.vertex UnityObjectToClipPos(modifiedVertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { // 片元着色器只需简单采样纹理 fixed4 col tex2D(_MainTex, i.uv); return col; } ENDCG } } }这个Shader的关键在于vert函数中modifiedVertex.y wave;这一行。它根据顶点自身的XZ坐标和全局时间计算出一个正弦波偏移值直接改变了顶点的几何位置。这种直接影响模型形状的操作是顶点着色器的专属领域。片元着色器在这里只负责后续的上色。注意顶点着色器处理的是离散的顶点。如果模型顶点数很少如一个低精度的平面波纹看起来会有棱角。要获得平滑的波纹需要足够细分的网格或者在曲面细分着色器中增加顶点这是另一个高级话题。3. 片元着色器决定像素的“颜值”如果说顶点着色器定义了形状的轮廓那么片元着色器就决定了这个轮廓内每一处的视觉细节。它的工作对象是光栅化后产生的每一个片元你可以近似理解为最终屏幕上的一个像素候选者。输入的是经过插值后的顶点数据如纹理坐标、颜色、法线等输出的是该片元的最终颜色值。3.1 核心职责光照、纹理与颜色计算片元着色器是视觉效果的“主战场”。几乎所有你看到的材质表面细节都在这里计算纹理采样 (Texture Sampling)这是最基本也是最常用的操作。根据插值得到的纹理坐标从纹理贴图中取出对应的颜色。fixed4 albedo tex2D(_MainTex, i.uv);光照计算 (Lighting Calculation)结合法线、光源方向、视角方向等信息计算漫反射、高光反射等让物体看起来有立体感和质感。在URP/HDRP中这部分通常由内置的光照函数完成但你也可以完全自定义。特效叠加实现边缘光、溶解、积雪、卡通渲染等复杂视觉效果的核心逻辑都在片元着色器中编写。3.2 性能考量片元着色器的计算成本片元着色器的执行频率远高于顶点着色器。一个拥有上万个三角形的模型经过光栅化后可能产生数百万个片元尤其是当物体靠近摄像机在屏幕上占据大量像素时。因此片元着色器中的计算必须非常高效。一个复杂的、每帧执行数百万次的计算会迅速成为性能瓶颈通常称为“像素填充率”瓶颈。优化片元着色器的常见策略包括减少纹理采样次数合并纹理使用纹理图集。简化复杂数学运算用查找表LUT或近似函数替代实时计算。利用Mipmap和纹理过滤减少远处像素的采样开销。尽早进行丢弃操作使用clip()函数在计算完成前就丢弃不需要的片元。3.3 实战案例实现动态漫反射光照我们来看一个结合了顶点着色器传递数据和片元着色器进行计算的经典例子在片元着色器中实现逐像素的漫反射光照。Shader Custom/DiffuseLighting { Properties { _MainTex (Texture, 2D) white {} _Color (Color, Color) (1,1,1,1) _Gloss (Gloss, Range(8,256)) 20 } SubShader { Pass { Tags {LightModeForwardBase} CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; float _Gloss; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); // 在顶点着色器中将法线转换到世界空间并传递给片元着色器 o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag (v2f i) : SV_Target { // 准备数据 fixed4 albedo tex2D(_MainTex, i.uv) * _Color; float3 worldNormal normalize(i.worldNormal); float3 worldLightDir normalize(_WorldSpaceLightPos0.xyz); float3 viewDir normalize(_WorldSpaceCameraPos.xyz - i.worldPos); // 计算漫反射 (Lambert) fixed3 diffuse _LightColor0.rgb * albedo.rgb * max(0, dot(worldNormal, worldLightDir)); // 计算高光反射 (Blinn-Phong) float3 halfDir normalize(worldLightDir viewDir); fixed3 specular _LightColor0.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); // 环境光 fixed3 ambient UNITY_LIGHTMODEL_AMBIENT.xyz * albedo.rgb; fixed4 finalColor fixed4(diffuse specular ambient, albedo.a); return finalColor; } ENDCG } } }在这个例子中顶点着色器vert函数承担了数据准备和坐标转换的工作它将顶点位置变换到裁剪空间同时将法线从模型空间变换到世界空间并将世界空间下的法线和顶点位置传递给片元着色器。片元着色器frag函数则承担了核心的颜色计算它利用传入的插值后的法线和位置信息进行逐像素的光照计算漫反射、高光并结合纹理颜色输出最终像素颜色。这是一个清晰的分工协作范例。4. 核心差异与协同应用指南理解了各自的功能后我们来系统性地对比一下顶点着色器和片元着色器并探讨如何根据需求选择或组合使用它们。4.1 根本区别对比特性顶点着色器 (Vertex Shader)片元着色器 (Fragment/Pixel Shader)操作对象模型的顶点离散数量相对少光栅化后的片元连续数量极多执行频率每个顶点执行一次每个片元近似每个屏幕像素执行一次主要职责几何变换坐标空间转换、顶点动画、变形。视觉计算纹理、光照、颜色、特效。输出影响影响模型的形状、轮廓和顶点数据。影响模型表面的颜色、质感和细节。性能敏感度受模型顶点数量影响大。高模会加重负担。受**屏幕覆盖像素数量分辨率**影响大。过度复杂计算会导致帧率下降。典型应用旗帜飘动、水面波纹、程序化变形、顶点颜色传递。纹理贴图、复杂光照模型PBR、卡通着色、后期屏幕特效。可访问数据顶点的本地属性位置、法线、UV等。插值后的顶点属性、屏幕坐标、深度等。4.2 如何选择顶点计算 vs 片元计算一个常见的问题是某个效果到底该在顶点做还是在片元做这里有一些决策原则在顶点着色器中做如果...效果与几何形状直接相关如扭曲、波浪。你需要的结果是逐顶点的并且可以通过插值平滑地传递给片元如计算顶点光照虽然效果不如逐像素精细但性能更好。计算本身较复杂但模型顶点数很少总体开销可控。在片元着色器中做如果...效果依赖于连续的表面属性如基于纹理的细节、平滑的渐变。你需要高精度的视觉效果如精确的逐像素光照、复杂的材质反射。计算依赖于屏幕空间信息如屏幕坐标、相邻像素信息用于边缘检测等后处理效果。4.3 协同工作案例顶点着色器准备片元着色器精加工最强大的Shader往往是两者协同的结果。一个经典的优化模式是在顶点着色器中进行昂贵的计算然后将结果传递给片元着色器进行插值和最终应用。例如要实现一个基于顶点距离的渐变溶解效果顶点着色器计算每个顶点到某个中心点的距离。这个计算在每个顶点上只做一次。// 在顶点着色器中 o.distanceToCenter length(v.vertex.xyz - _Center.xyz);光栅化阶段distanceToCenter值会在三角形内被自动插值生成每个片元对应的距离值。片元着色器使用插值后的距离值与一个阈值可随时间变化比较决定片元是显示、溶解还是丢弃。// 在片元着色器中 float dissolve step(i.distanceToCenter, _DissolveThreshold); clip(dissolve - 0.5); // 丢弃阈值之外的片元这样我们既利用了顶点计算的效率只算几次又获得了片元级别的平滑渐变效果。掌握顶点与片元着色器的区别本质上是掌握了在渲染管线的不同阶段施加影响的权力。从用顶点着色器让模型“动起来”到用片元着色器让它“靓起来”这中间的每一步都充满了创造的可能。下次当你面对一个Shader需求时先问问自己这个效果是改变形状还是改变颜色计算量有多大答案自然会指引你找到最高效的实现路径。多动手写多观察变化这两个核心概念很快就会从书本上的名词变成你手中创造视觉奇迹的得力工具。