十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用着色器动态绘制箭头:Shader Graph与HLSL双方案实战

用着色器动态绘制箭头:Shader Graph与HLSL双方案实战 1. 为什么要在着色器里画箭头方向指示需求的几种实现路线先聊一个实际场景。做RTS游戏要显示单位行进的朝向箭头做地图编辑器要在平面上标出刷子方向做数据可视化项目要在模型表面叠加风向或流向指示。这些需求本质上都是同一件事在一个二维或者三维表面上画出一批方向性非常明确的图案而且图案最好还能动态变化——流动、闪烁、变色。这类需求如果全部丢给美术出贴图贴图数量会非常可观每换一个角度就要重新烘焙运行时想要流动效果还得靠UV动画硬撑性能和灵活性都吃亏。那为什么不试试用着色器直接生成箭头图案呢这就是动态着色思路的核心不依赖贴图在GPU上用数学计算逐像素生成图案。好处非常直接分辨率无限不管相机拉多近都不会糊参数实时可控方向、颜色、箭头密度、流动速度全部可以暴露成材质参数包体体积零增加不用额外塞贴图资源。我最初接触“动态着色”这个概念是因为一个比较特殊的项目要在Pico设备上做一套空间方向引导箭头必须浮在真实场景的平面上而且箭头要随着目标的移动实时转动。一开始我确实想过用UI方案用两张箭头图片旋转后来发现一个问题——箭头需要在世界空间贴合地面UI Image在屏幕空间里旋转出来的效果和透视关系对不上。也试过用LineRenderer连线加箭头模型场景里挂一堆实例化物体Draw Call很快就压不住了。最后绕回到着色器方案反而最干脆。只需要一个平面网格和一个自定义Shader箭头图案完全在片元着色器里算出来方向用一个向量参数控制整个方案轻量到只有一个材质球。而且这套思路换到Shader Graph和手写HLSL里都能用并且也能很容易地扩展成三向贴花、表面流线等更复杂的变体。这篇文章将把这条路完整走一遍先讲清楚箭头图案在着色器里的数学原理再分别给出Shader Graph和手写HLSL两套可复现的实现接着把动态效果加进去最后说说我在实际项目里踩过的坑。不管你是刚入门的Unity学习者还是被类似需求困扰的开发者照着这篇文章推一遍以后遇到方向指示类效果都能心里有数。2. 箭头图案的数学本质从网格坐标到方向判定的几步推导在写任何一个自定义着色器之前我都建议先在脑子里把图案拆成数学表达式。不要一上来就拖节点或者敲代码先把“这个图案到底是怎么算出来的”想清楚后面会顺很多。2.1 用横纵坐标画形状一个最基本的坐标系构建思路假设现在有一块平面网格使用了默认的Quad或Plane它的UV坐标范围是(0,0)到(1,1)。片元着色器跑在每个像素上时我们能拿到这个像素对应的UV值那就可以据此判断这个像素应该是什么颜色。箭头图案需要一条中线这个中线是沿着箭头本身的方向走的。但一个关键问题是UV坐标系是固定的如果箭头方向旋转了固定坐标系就不够用了必须先把坐标变换到箭头自己的局部坐标系里。这个变换其实非常简单。假设箭头方向单位向量是(dirX, dirY)那么与之垂直的向量就是(-dirY, dirX)。任意一个像素的UV坐标值经过一个二维点积运算就能投影到这两个方向上沿箭头方向的分量dot(uv, dir)垂直箭头方向的分量dot(uv, perp)这两个分量正好构成了一个跟随箭头方向旋转的局部坐标系。箭头图形无论转多少度只需要修改dir的值所有计算都会自动跟着旋转。这正是“动态着色”最方便的地方——形状变了但数学骨架完全不用动。2.2 箭头形状分三段杆、尖、镂空的数学判定一个典型的实心箭头从几何上可以拆成三段区域矩形箭杆、三角形箭头、以及可选的中空镂空区域。假定箭头方向局部坐标系中沿方向的分量是along垂直方向的分量是cross可以这样描述箭杆区域abs(cross) halfThickness且along -bodyLength且along headStart。由于箭头指向为正方向箭杆沿负方向延伸。箭头区域along headStart且along headLength headStart同时abs(cross) headHalfWidth * (1.0 - (along - headStart) / headLength)。这个公式让三角形宽度从头部末端到箭尖线性收缩到0。镂空区域可以在箭杆和箭头之间挖掉中间的一部分形成中空效果也可以添加一个中心镂空槽。在Shader里也就是把判定条件取反、混合或者减掉。全部判定逻辑执行完后把三个区域的结果按权重叠加就能输出一条清晰的箭头。颜色和透明度也可以分层箭杆颜色和箭头颜色一致时是实心箭头分开时就能做出双色箭头。如果你觉得这几个判定条件很绕不要急下面两节会分别用Shader Graph和HLSL各实现一遍。每个节点的连线、每行代码的含义我都会解释清楚。3. 用Shader Graph搭建箭头着色器半个小时的落地全过程Shader Graph是Unity可视化着色器编辑工具它的优势是改参数就能实时看效果。不用写一行代码只要拖节点特别适合快速验证图案设计。我建议所有纯Shader新手都先从这一节开始跑通了再看代码版本。3.1 新建Shader Graph与基础渲染模式设置打开Unity在Project窗口里右键Create - Shader - Universal Render Pipeline - Unlit Shader Graph命名为ArrowShader。为什么用Unlit而不是Lit因为箭头图案通常是自发光的方向指示不需要光照计算。如果希望箭头受光影响也可以选Lit但默认我们只要一个不受光照干扰的颜色层。双击打开Shader Graph后第一件事是把Master Stack里的Surface Type改为TransparentBlend Mode改为Alpha。不改透明模式的话后面输出透明区域会变成黑色或白色这是新手最常见的坑之一。还需要把Render Face设为Both。因为箭头可能被从背面看到尤其是画在地面上的时候相机绕到另一侧如果剔除背面箭头就消失了。3.2 UV坐标的旋转一个极坐标节点的替代方案Shader Graph里有一个很有用的工具节点Rotate位于Math - Trigonometry类别。它可以直接把UV坐标按照某个角度旋转。但这里我们不打算用角度而是直接使用方向向量因此更灵活的做法是手搭旋转公式。其实Bezier曲线、三角函数这些我们用得很少旋转一个二维坐标系只需要两个节点Dot Product。做法是添加一个Vector2属性Direction默认值为(1,0)表示箭头指向右侧。用Normalize节点归一化该方向确保长度不影响结果。添加一个Vector2属性UV使用UV节点这是每个像素的坐标。计算沿方向分量Dot Product(UV, DirectionNormalized)。计算垂直方向分量Dot Product(UV, PerpVector)其中PerpVector (-Direction.y, Direction.x)可以用Vector2节点手动输入。这样我们就得到了两路数值ProjectedAlong和ProjectedCross。后面的所有形状判定都基于这两个值。3.3 箭头各部分的判定函数从画线区域到头部三角形得到投影坐标后开始判断像素是否属于箭头图案。先添加三个Float属性方便在Inspector里调整形状HalfThickness箭杆半宽默认0.05BodyLength箭杆长度默认0.4HeadLength箭头头部长度默认0.25HeadHalfWidth箭头头部半宽默认0.15箭杆的判定分三步先用Step节点或者Less节点判断abs(ProjectedCross) HalfThickness这一步得到0或1的mask。注意Shader Graph里有Absolute节点在Math - Basic下。再判断ProjectedAlong -BodyLength且ProjectedAlong 0用And或者Multiply组合。三部分相乘得到箭杆mask。头部的判定稍微复杂一点因为三角形宽度是线性变化的。尾部到尾尖的过程里宽度从HeadHalfWidth缩小到0。具体做法计算t saturate((ProjectedAlong - 0) / HeadLength)当ProjectedAlong0时t0当ProjectedAlongHeadLength时t1。那么该像素允许的最大abs(ProjectedCross)是HeadHalfWidth * (1 - t)。判定abs(ProjectedCross) 该值且ProjectedAlong 0且ProjectedAlong HeadLength就能得到头部的mask。如果觉得Saturate节点隐藏得深可以通过Smoothstep配合Clamp值实现相同的效果。把mask计算出来后用Add把箭杆mask和头部mask合在一起整个箭头区域就算出来了。3.4 颜色、透明度与深度写入的串联整个箭头区域mask最终会是一个0到1之间的值。现在是把它变成最终颜色的时候了。添加两个Color属性BaseColor和HeadColor。用Lerp节点把两种颜色混合起来——用哪个值作为Lerp的T值呢可以用头部mask。这样整个箭头主体是BaseColor只有头部三角区域是HeadColor。这就是双色箭头的做法。如果不想要双色直接让BaseColor和HeadColor相同即可。将合成后的RGB数据连到Master Stack的Base Color端口将mask连到Alpha端口。由于材质是Transparent模式透明区域会正常消失。还有一个细节必须处理透明物体的深度写入默认是关闭的这会带来两个物体穿插时排序异常的问题。如果箭头只是画在地面上一般问题不大但如果箭头和建筑模型交叉建议把Master Stack里的Depth Write开启并设置Depth Test为LEqual。开启后箭头会正确地遮挡在没有被其他物体遮挡的区域之后。以上Shader Graph版本的核心搭建就完成了。保存后在场景里建一个Quad把材质指上去在Inspector里调节Direction属性的X和Y值就能看到箭头旋转了。如果方向数值看起来不对记得先归一化——否则箭头宽度和头部形状会被拉伸变形。4. 手写HLSL一个更轻量、可复用的动态箭头Unlit ShaderShader Graph虽然直观但有些场合并不适合它包体需要极简、需要把多个图案合并到同一份Pass里做合批、或者在URP的Forward路径下手动控制渲染标签。这时候手写Shader会更合适。下面是我在实际项目中用过的精简版本删掉了无关注释保留核心逻辑。配合URP管线直接扔进_InvokeWithCmd(Or) Asdef(Standard)即可。Shader Custom/ArrowShader { Properties { _BaseColor (Base Color, Color) (1,1,1,1) _HeadColor (Head Color, Color) (1,0.2,0.2,1) _Direction (Direction, Vector) (1,0,0,0) _HalfThickness (Half Thickness, Float) 0.03 _BodyLength (Body Length, Float) 0.35 _HeadLength (Head Length, Float) 0.2 _HeadHalfWidth (Head Half Width, Float) 0.12 _FlowSpeed (Flow Speed, Float) 0.0 [Toggle] _EnableFlow (Enable Flow, Float) 0 } SubShader { Tags { RenderTypeTransparent QueueTransparent RenderPipelineUniversalPipeline } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_local _ _ENABLEFLOW_ON #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl CBUFFER_START(UnityPerMaterial) float4 _BaseColor; float4 _HeadColor; float4 _Direction; float _HalfThickness; float _BodyLength; float _HeadLength; float _HeadHalfWidth; float _FlowSpeed; CBUFFER_END struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (appdata v) { v2f o; o.vertex TransformObjectToHClip(v.vertex.xyz); o.uv v.uv; return o; } half4 frag (v2f i) : SV_Target { // 1. 归一化方向向量 float2 dir normalize(_Direction.xy); float2 perp float2(-dir.y, dir.x); // 2. 将UV投影到箭头局部坐标系 float along dot(i.uv, dir); float cross dot(i.uv, perp); // 3. 箭杆判定 float body step(abs(cross), _HalfThickness) * step(-_BodyLength, along) * step(along, 0.0); // 4. 头部三角形判定 float t saturate(along / _HeadLength); float head step(along, _HeadLength) * step(along, _HeadLength) * step(abs(cross), _HeadHalfWidth * (1.0 - t)); // 上面的 step(along, _HeadLength) 是当 along HeadLength 时淘汰along 0 时也由 saturate 处理成0 // 5. 合并mask float mask saturate(body head); // 6. 颜色混合 half3 col lerp(_BaseColor.rgb, _HeadColor.rgb, head * mask); // 7. 动态流动效果 #ifdef _ENABLEFLOW_ON float phase _Time.y * _FlowSpeed; float flow 0.5 0.5 * sin(along * 20.0 - phase); col * flow; mask * flow; #endif return half4(col.rgb, mask * _BaseColor.a); } ENDHLSL } } }这段代码有几个地方值得单独解释否则直接抄的话容易翻车。第一CBUFFER_START(UnityPerMaterial)这行很重要。URP管线通过这个常量缓冲区对材质属性做批量上传和SRP Batcher合批。如果不写在CBUFFER里属性依然能用但无法参与SRP Batcher性能会下降不少。这个细节在编辑器里看Stats窗口的SRP Batcher指标时才能直观感受到差异。第二头部的判定里我连写了两个step(along, _HeadLength)这其实是冗余的保留一个就够。这是我在调试时留下的痕迹读者可以自行删掉。更严谨的写法应该是float head step(along, _HeadLength) * step(0.0, along) * step(abs(cross), _HeadHalfWidth * (1.0 - t));其中step(0.0, along)确保along为负时不算头部区域。saturate虽然能约束t但不会让along 0时的saturate结果变成0之外的负数——也就是说along-0.5时tsaturate(-2.5)0这个三角形判定的abs(cross) _HeadHalfWidth * 1.0可能是成立的所以必须再补一个along 0的判断。这个坑我刚写的时候踩过箭头尾部长了一个倒着的三角形排查了半天。第三流动效果的实现用了sin函数。这里along * 20.0中的20.0决定了条纹频率数值越大条纹越密。_Time.y * _FlowSpeed产生随时间平移的相位。0.5 0.5 * sin(...)把结果映射到0到1之间这样彩色和透明度一起波动。如果不想让透明度波动只让颜色变亮变暗就把mask * flow去掉。第四_Direction的Z和W分量在Shader里完全没用到。留在这里是因为Inspector里调Vector4最方便输入XY两个值即可Z和W忽略。这个设计是为了编辑器操作方便不值得为这点冗余浪费时间去写自定义Drawer。把Shader保存后在Material里把Direction设为(1,0,0)箭头就指向物体局部坐标的X轴正方向。如果想指向任意方向只需要旋转Quad物体即可。看起来是不是比Shader Graph简洁很多不过简洁归简洁代码方式有一个劣势没有可视化预览改参数只能靠反复看Game视图。所以我在项目里常把两种方式搭配使用先用Shader Graph做原型验证再把手写Shader精简化最后合入正式项目。5. 动态着色的进阶玩法流动、脉冲、贴花与三向投影动态着色的真正价值在于“动”而不只是画一个静态箭头。这一节我们聊聊几个高频的动态效果怎么实现它们可以独立使用也可以叠加组合。5.1 流动方向的视觉强化条纹推进与相位偏移上节代码里的flow效果本质上是一个沿along方向传播的正弦波。这个方向不一定是箭头方向也可以是垂直方向取决于用along还是cross作为相位输入。我在项目里用实际测试确认沿箭头方向流动确实最能体现方向感尤其是当箭头比较细长时流动感比颜色变化更醒目。如果你觉得正弦波太规律、太“机械”可以叠加两层不同频率的波float flow1 0.5 0.5 * sin(along * 20.0 - _Time.y * _FlowSpeed); float flow2 0.5 0.5 * sin(along * 11.0 - _Time.y * _FlowSpeed * 0.7); float flow flow1 * flow2;两层频率互质叠加出来的纹理不那么容易看出周期性重复观感更自然。但要注意这运算量在片元着色器里翻了一倍。移动端跑Pico这种设备时要斟酌一下我实测两层正弦在Quest 2上并不会有可见的压力但老一点的设备还是保守为好。5.2 脉冲闪烁与Alpha淡出用Overlay贴花思路提升立体感有时候箭头仅作为临时标记比如游戏里点击地面后出现一个指引标记随时间淡出。这个效果不需要额外写复杂代码只需要在片元着色器末尾把alpha乘以一个随时间衰减的系数float life 1.0 - saturate(_Time.y * 0.5); return half4(col.rgb, mask * life);这里的0.5是衰减速度可根据实际项目调节。如果是周期性闪烁可以改成float blink 0.6 0.4 * sin(_Time.y * 5.0);将blink乘到alpha或者颜色上都能起到闪烁提示的效果。这两种动态效果对性能的额外开销几乎可以忽略非常适合用于UI提示、地图标记、任务指引等场景。5.3 从平面到表面三向投影贴花的扩展思路前面实现的箭头是画在Quad平面上的。如果要把箭头贴到任意朝向的模型表面比如地形、墙体、车上就不能简单使用UV了而是要使用世界坐标投影。三向贴花Tri-Planar Projection的基本思想是对于世界空间中的每个点分别向X、Y、Z三个方向投影各投影一次然后根据法线方向加权混合。应用到箭头图案上只需要把片元着色器里的i.uv替换成世界坐标的某个轴平面坐标比如float2 uvX worldPos.zy; float2 uvY worldPos.xz; float2 uvZ worldPos.xy;分别调用箭头判定函数获得三份mask再用世界法线做权重混合。这样无论模型表面朝哪个方向箭头都能贴合表面且没有拉伸畸形。这是我在做数字孪生类项目时最常用的扩展方式——场景里的管道要标流向三向投影直接把同样的着色器用在所有管道模型上一次搞定完全不需要美术拆UV。不过这种方案的代价是片元着色器的运算量翻了接近三倍。我在实际项目里通常只在物体数量有限的高亮对象上使用绝不在整个场景铺开。5.4 多方向密度控制在同一个平面生成多个箭头有些场景希望一片区域内均匀分布多个箭头每个箭头方向都一致或渐变。实现思路有两种用程序化平铺把UV乘上一个整数N然后用frac取小数部分每个小格子里就有一个独立箭头。但这里有个问题箭头图案以零点为中心frac范围是0到1箭头会有一部分越过格子边界。解决办法是把UV中心移到0.5float2 cellUV frac(i.uv * _Density) - 0.5;_Density控制每个方向多少个格子如此在0.5范围内的箭头就不会跨格子。逐实例数组方式使用StructuredBuffer传入一组箭头的位置和方向片元着色器遍历数组计算每个箭头的mask。这种方法更适合少量但位置精确的箭头比如玩家路径点。缺点是遍历次数会随箭头数量线性上升几百个以内问题不大几千个就要慎重。我在编辑器工具里用过平铺方式给地形预览标水流方向效率很高。在运行时路径导航指示上用的则是逐实例数组方式。6. 项目中的避坑记录旋转、半透明排序与移动端着色器变体走到这一步箭头应该已经能显示且能动态流动了。但真正的项目环境比隔离Demo复杂得多有几个坑我几乎每次都会遇到索性一次性写出来免得后来者再踩一遍。6.1 方向向量没归一化导致的宽度变形这是最隐蔽的坑。很多人在Material里手动设Direction的X和Y随手填了个(2,1)箭头形状就直接拉伸了。原因在于局部坐标系里如果方向向量长度不是1那么投影到该方向上的值会被缩放最终宽度判断和长度判断都会出错。解决方式是始终在Shader里加一行normalize或者在C#侧提交方向前先调用Normalize。我在Shader里写了normalize方便调试时直接用材质Inspector改值效果上安全得多。代价是一点额外的GPU开销但和箭头本身的像素运算量相比可以忽略。6.2 半透明材质排序混乱地面箭头被建筑遮挡错误Transparent队列的排序一直是老问题。默认情况下Unity先渲染不透明物体再按距离排序透明物体。地面上的箭头如果被一个半透明柱子遮挡渲染顺序一旦错了箭头就会穿透柱子显示在前面看起来很假。三种解决思路按推荐程度排序如果箭头贴在地表面直接改成QueueGeometry1放进不透明渲染队列里只是加一个alpha测试或深度测试。用clip(mask - 0.5)把mask低于0.5的像素丢弃。这样这个箭头会参与不透明物体的深度测试遮挡关系就对了。如果箭头必须半透明把ZWrite On打开并且把Shader的渲染队列放在不透明物体之后、其他透明物体之前。大多数情况下能解决排序错误但需要观察是否有两个透明物体互相穿插的极端情况。使用Depth Priming或Render Feature来强制指定渲染顺序这是性能和效果最可控的方案但实现复杂度会上升。我个人的实用建议是箭头这类强视觉提示尽量使用不透明模式加alpha clip省心避免透明排序的连锁问题。6.3 移动端Shader变体的性能陷阱Shader里的[Toggle]关键字看起来人畜无害但其实它会生成两个变体一个启用一个关闭。如果你在材质上同时使用了多个Toggle变体数量会呈指数增加。如果材质还要参与场景烘培或阴影变体爆炸会直接拖慢打包。我的建议是流动效果这类开关可以在开发期用Toggle方便调试但发布前应确定最终状态删掉多余的Toggle和multi_compile指令只保留一个编译目标。如果必须保留多个功能开关使用multi_compile_local并配合SkipVariants做裁剪。另一个移动端的坑是半精度浮点。在某些移动GPU上half和float的行为不对等导致箭头在特定角度下边缘出现锯齿。这是因为我上面代码里用了half4做返回值但中途计算用了float2。在Shader Model 3.0及以上平台问题不大但保守起见建议统一用float做中间计算只在最终输出时转half。画面质量和性能损失之间的平衡在移动端要拿实测数据说话。6.4 编辑器预览与运行时效果不一致Shader Graph的Main Preview默认使用一个立方体和Quad的UV映射不同预览效果可能和实际场景差别很大。我见过不少人把箭头Shader直接挂到默认Cube上看效果箭头拉伸得不像样误以为Shader写错了。正确的做法是在场景里放一个单位Quad或者自定义的Plane把材质挂上去预览。如果要模拟地面贴花就把Quad拉平贴近地面从斜上方观察。Shader Graph的自定义预览Mesh功能也可以使用在Preview窗口的右上角下拉菜单里选Quad即可。6.5 从Unity版本迁移与URP升级注意事项这个Shader在Unity 2021和Unity 6的URP 14和URP 17上都能正常运行但要检查一个细节URP管线的宏定义版本。早期URP7.x和后面的URP12在引入UniversalPipeline的Tag和Core.hlsl路径上没有差异但如果你的项目还在使用Built-in管线需要把#include替换成UnityCG.cginc并且把TransformObjectToHClip替换成UnityObjectToClipPos。另外如果项目同时使用了HDRP这套Shader完全不能直接用HDRP的透明材质系统和光照模型差异很大需要重新用Shader Graph制作。我把这个坑写在最后是因为它在项目升级时最致命。一个看起来没问题的Shader换个管线版本就变成粉色或全黑排查半天才发现是宏定义问题。7. 最后的实操心得把这套方案做成一个可复用的工具写到这里基本就覆盖了我的实现思路和避坑记录。最后分享一个我实践后觉得很有用的经验把这套箭头Shader做成了一个小工具包包含三个东西——核心Shader文件、一个编辑器脚本、一个C#的辅助组件。编辑器脚本的作用是在Unity菜单栏加一个GameObject - UI - Dynamic Arrow入口点击后自动创建Quad、附加材质、把Shader的各个参数暴露成Inspector里的属性。C#辅助组件则会在Update里根据目标位置动态修改_Direction省去手动设置的麻烦。using UnityEngine; [ExecuteAlways] public class DynamicArrow : MonoBehaviour { public Transform targetTransform; public float offsetAngle 0f; private Material mat; void Awake() { var renderer GetComponentRenderer(); if (renderer ! null) { mat renderer.sharedMaterial; } } void Update() { if (mat null || targetTransform null) return; Vector3 dir targetTransform.position - transform.position; dir.y 0f; dir Quaternion.Euler(0, offsetAngle, 0) * dir.normalized; mat.SetVector(_Direction, new Vector4(dir.x, dir.z, 0, 0)); } }这段代码把世界空间的方向转换成Shader局部空间的XY值。因为箭头画在XZ平面上所以取了dir.x和dir.z。需要注意的是如果箭头所在Quad被旋转过方向还需要坐上Quad自身的旋转矩阵我这里直接简化了——实际项目中让Script的朝向与Quad的局部坐标对齐并用自定义的forward向量做正交化能少踩很多坑。针对不同项目需求这套方案还能扩展成颜色由中心到边缘渐变的版本、根据速度改变箭头密度的版本、以及用贴花投影方式贴在任意模型表面的版本。每次扩展我都能明显感受到“动态着色”这种用数学去表达图案的方式——它让你不再受限于美术资源和纹理尺寸而是直接用GPU算力去生成视觉这在我看来是Unity渲染方面最值得掌握的一种能力。
返回列表