ARTICLE DETAIL

资讯详情

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

Unity Shader实现3D像素风角色描边:法线外扩原理与实战

Unity Shader实现3D像素风角色描边:法线外扩原理与实战 各位做像素风游戏的朋友应该都有过这样的体验模型在建模软件里怎么看怎么顺眼贴图也画得规规矩矩可是一放进游戏场景整只角色就变得灰扑扑、软绵绵跟二维的像素立绘放在一起完全不是同一个画风。问题往往不在模型本身而是缺少一条确定的轮廓线。二维像素画天然有硬朗的边缘而 3D 像素模型一旦进入光照环境就很容易变成普通的低模卡通丢失那种“像素味”。这篇文章就来专门解决这个问题如何为 3D 像素艺术添加轮廓线也就是游戏圈常说的 Outline / 描边效果。我会先讲清楚轮廓线解决的是哪些视觉问题接着对比几种主流实现方案然后重点拆解“法线外扩描边”的原理最后给出一份可以运行的 Unity Shader 完整教程包括动态调参脚本、像素风低分辨率渲染的配合方案以及常见问题和工程建议。如果你用的是 Godot、Three.js核心思路也可以直接迁移过去。1. 3D像素艺术的轮廓线是什么解决什么问题1.1 先理解“3D像素艺术”的特殊性3D 像素艺术通常指以体素Voxel或低多边形模型为载体、配合像素风贴图和低分辨率渲染的画面风格典型游戏如《我的世界》衍生作品、《Terraria》的 3D 化项目、《Crossy Road》等。它的视觉核心并不是“模型精度”而是“像素颗粒感”和“色块边界”。这种风格下模型表面通常有以下特点表面平整法线方向基本都是轴对齐的比如只朝 X、-X、Y、-Y、Z、-Z 六个方向。颜色以大色块为主贴图分辨率低很少出现细腻渐变。光照计算如果太重反而会破坏色块的平整感。也就是说3D 像素艺术的魅力恰恰在于“简化”只有把模型轮廓明确地勾勒出来让每个体素单元在视觉上形成清晰的剪影画面才会回归那种干净的像素游戏质感。1.2 为什么模型放进场景后“散”了没加轮廓线之前3D 场景依赖光照、阴影和深度来区分物体。但像素风格模型本来就是低精度、低贴图细节的一旦光一打角色和背景之间的边界就会模糊。尤其是遇到以下场景角色的深色贴图和暗色背景混在一起玩家分不清前后物体。多个像素角色站在一起时彼此之间没有边界变成一团色块。想让敌人被选中、被锁定、被高亮时缺少一个统一的表现手段。轮廓线就是在这些场景下把“图形边缘”重新捞回来的手段。它相当于给模型加了一圈清晰的剪影涂层让物体在任何光照下都保持独立、明确的存在感。这也是 2D 像素画天然具备、但 3D 模型容易丢失的视觉要素。1.3 轮廓线在像素游戏里的典型用途轮廓线在 3D 像素艺术项目中常见于以下几个需求为角色、怪物、NPC 增加统一的卡通描边强化像素手绘感。在做选中反馈时通过描边颜色变化来表现“锁定”或“高亮”状态。在低分辨率画风下用粗描边代替高精度阴影降低渲染压力。在俯视角或固定视角的游戏中利用轮廓线保证角色从任何角度都清晰可辨。后面我们会看到上述需求大多数都能用同一种描边技术解决只是参数和表现层略有差异。2. 轮廓线实现的三大主流方案实现描边效果的方案很多不同方案适用场景也不同。先做横向对比对理解后面的实战很重要。2.1 Inverted Hull法线外扩描边这是游戏开发中最经典、最常用的描边方式思路可以概括成一句话“画一个放大的黑色背面再把模型正面盖在上面。”具体做法是渲染两遍同一个模型。第一遍不剔除背面或者专门剔除正面把顶点沿着法线方向向外挤出一定距离整层涂成轮廓色通常是黑色。第二遍正常渲染模型正面。因为第二遍的正面向外覆盖后画的模型会把前面的黑色背面遮住最终边缘溢出的黑色部分就构成了一道描边。它的优点非常明显不需要屏幕空间后处理开销较低。描边跟随物体本身旋转、缩放、移动不会出现边缘漂移。可以非常方便地动态调整粗细和颜色。缺点主要是如果模型有向内凹陷的结构描边可能会漏进凹陷区域形成不自然的黑色嵌线。顶点共用情况下法线外扩方向可能不理想这在体素模型上尤其值得注意。2.2 深度加法线后处理描边另一种思路是在屏幕空间做边缘检测。引擎先渲染一张包含深度信息和法线信息的缓冲然后在后处理阶段逐像素判断“前后左右相邻像素的深度或法线是否发生突变”如果发生了突变就把该像素视为轮廓覆盖成描边色。这种方法的最大优势是描边是基于最终画面的边缘不会出现模型内部凹陷漏线的问题而且可以做到整帧统一粗细闭会感特别强。缺点也很明显需要额外的后处理通道移动端和低端设备上有性能压力。边缘粗细受屏幕分辨率影响想调成规范像素笔触需要额外处理。后处理方案是和相机绑定的不适合做“单物体选中高亮”。我做项目时一般会按需求来选如果只是主角和怪物的描边优先用 Inverted Hull如果是全场景统一卡通渲染才考虑深度法线后处理。2.3 菲涅尔边缘发光菲涅尔Fresnel效果是“边缘亮、正面暗”的光照现象。利用视角方向和法线方向的夹角可以给模型边缘叠加一圈颜色。这个方案实现最简单但通常产生的是柔和渐变边缘不是像素游戏想要的“硬朗线条”。除非项目本身是柔和卡通风格否则不适合作为主方案更适合用来做“边缘发光”的辅助效果。2.4 三个方案选哪个对比维度Inverted Hull深度法线后处理菲涅尔边缘实现难度低高最低性能开销低中高低线条风格硬朗适合像素硬朗可控制柔和渐变是否依赖相机否是否是否适合单个高亮非常合适不适合勉强体素模型适配需处理法线问题优秀一般本文的实战部分采用 Inverted Hull因为 3D 像素艺术需要的是干脆利落的轮廓而且这个方案最容易通过 Shader 参数实现动态控制。3. 环境准备与示例项目结构3.1 引擎与渲染管线说明本文的示例代码基于 Unity 制作但不会依赖某个特定版本的私有 API。读者使用的 Unity 版本如果和下面描述不同只要依然是内置渲染管线Built-in Render Pipeline写法和结果都基本一致。如果你正在用 URPUniversal Render Pipeline也不要直接照抄屏幕后处理部分但 Inverted Hull 这个思路是可以通过 Shader Graph 或者手写 HLSL 实现的。第五节最后我会补一段 URP 的适配思路。3.2 示例场景与模型要求为了测试描边效果我们需要准备一个 3D 像素风格模型。最简单的办法是使用 Unity 自带的 Cube把它缩放成“一个小人”的形状或者直接使用任意体素模型。注意事项如下模型必须是封闭网格尽量保证法线正确。描边 Pass 会在顶点层面工作所以模型不需要细分低模即可。如果模型已经导入了像素风贴图也可以一起应用在材质上。3.3 项目文件结构按下面的方式组织场景和脚本Assets/ ├── Scenes/ │ └── OutlineDemo.unity ├── Shaders/ │ └── PixelArtOutline.shader ├── Materials/ │ └── PixelCharMat.mat ├── Scripts/ │ └── OutlineController.cs │ └── PixelizeCamera.cs └── Models/ └── PixelCharacter.fbx后面每个文件的代码我都会单独给出大家按路径对应创建即可。4. 核心原理拆解法线外扩描边是怎么工作的4.1 顶点法线与外扩方向在 Inverted Hull 方案中最重要的操作是“把顶点沿法线方向外扩”。这里要回到一个基础概念顶点法线。法线Normal是垂直于模型表面的方向向量。对于简单的平面法线方向就是面的朝向。对于低多边形模型模型导入引擎时通常会做顶点法线平滑让同一个顶点如果被多个面共用法线变为多个面法线的平均值。描边 Pass 的做法就是获取每个顶点的法线把它当作“外扩方向”然后把顶点位置向外移动外扩后的顶点位置 原始顶点位置 法线方向 × 描边宽度这个操作可以在顶点着色器里完成不需要修改网格数据。这就是 Inverted Hull 高效的根本原因。4.2 用 Cull Front 渲染背面我们需要理解图形渲染中的“剔除Culling”概念。默认情况下渲染管线为了性能会剔除背面只绘制对着相机的那一面。但在描边 Pass 中我们反而需要让模型背面参与绘制。所以关键代码是Cull Front加上这一句之后渲染器只绘制模型的背面。然后我们在顶点着色器里把模型沿法线外扩相当于得到一个“比原模型大一圈的壳”并且只在这个壳上输出轮廓色。随后再进入正常的渲染 Pass绘制模型的正面。因为正面足够大它会覆盖住后画的那层壳的正面部分只有壳从模型边缘溢出的那一圈会露出来这就是轮廓线。4.3 描边宽度与像素风格的关系描边宽度的控制通常归一化到模型自身尺寸。比如一个高 2 米的角色描边宽度取 0.02 到 0.05 就比较合适。在像素风格项目里描边宽度和“像素颗粒感”之间需要保持比例太窄轮廓线细碎低分辨率下容易闪烁。太宽角色显得臃肿像被黑色描边吞掉。建议在项目初期就锁死纹理分辨率和渲染分辨率然后在固定分辨率下调整描边宽度这样效果最可控。4.4 体素模型法线的特殊性这里有个 3D 像素艺术特有的坑体素模型经常是多个立方体拼出来的比如一个角色的头部是一个 Cube手臂是几个 Cube。如果不做合并处理每个 Cube 的法线是干净的轴对齐方向外扩方向也很规则。但如果你把体素模型合并成一个网格顶点会被多个三角面共用Unity 计算出的平滑法线就会变成倾斜方向。结果就是描边外扩方向变得歪歪扭扭不够规则。针对这个问题通常有三种解决思路第一种模型的描边网格单独生成只保留原始立方体的轴对齐法线。第二种不合并模型保持多个 Cube 组件每个 Cube 单独挂材质。第三种写一个编辑器工具为模型重新计算六方向法线。在项目早期做好这一步后面调效果才不会反复返工。这部分内容我也会在第六节的常见问题里单独再强调一次。5. 完整实战为 3D 像素角色添加可调节轮廓线下面进入本文的核心部分。我们的目标很明确在 Unity 中写一个支持轮廓线开关、宽度和颜色动态调节的 Shader并把它应用到一个 3D 像素角色上再做低分辨率像素化渲染得到最终效果。5.1 创建自定义 Shader在 Unity 项目里创建 Shader菜单路径是Assets - Create - Shader - Standard Surface Shader然后把文件命名为 PixelArtOutline.shader用任意代码编辑器打开替换为以下内容。// 文件路径Assets/Shaders/PixelArtOutline.shader Shader PixelArt/Outline { Properties { _MainTex (主纹理, 2D) white {} _Color (主颜色, Color) (1, 1, 1, 1) _OutlineColor (轮廓颜色, Color) (0, 0, 0, 1) _OutlineWidth (轮廓宽度, Float) 0.03 [Toggle(USE_OUTLINE)] _UseOutline (启用轮廓线, Float) 1 } SubShader { Tags { RenderTypeOpaque QueueGeometry } LOD 100 // Pass 1轮廓壳 // 只绘制背面沿法线外扩顶点涂上轮廓色 Pass { Name OUTLINE Tags { LightModeForwardBase } Cull Front ZWrite On CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma shader_feature_local USE_OUTLINE #include UnityCG.cginc float _OutlineWidth; fixed4 _OutlineColor; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; }; v2f vert (appdata v) { v2f o; // 如果关闭轮廓线直接输出原位置相当于这一层不可见 #ifdef USE_OUTLINE float3 worldNormal UnityObjectToWorldNormal(v.normal); float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; worldPos normalize(worldNormal) * _OutlineWidth; o.pos UnityWorldToClipPos(worldPos); #else o.pos UnityObjectToClipPos(v.vertex); #endif return o; } fixed4 frag (v2f i) : SV_Target { return _OutlineColor; } ENDCG } // Pass 2正常模型 // 正常采样贴图输出模型本色 Pass { Name BODY Tags { LightModeForwardBase } Cull Back ZWrite On CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv) * _Color; return col; } ENDCG } } Fallback Diffuse }下面对这个 Shader 的关键部分做逐段解释。Properties 部分定义了四个基础参数主纹理、主颜色、轮廓颜色、轮廓宽度。最后一个[Toggle(USE_OUTLINE)]是一个开关用来快速开启或关闭描边效果。这个开关在后期调试时非常有用可以不改材质直接对比“有轮廓”和“无轮廓”两种效果。Pass 1 是轮廓壳。Cull Front是关键它让 GPU 只绘制背面。在顶点着色器中我们获取顶点的法线转换到世界空间再把顶点位置沿法线方向推出_OutlineWidth的距离。这里我特意使用世界空间计算因为描边宽度预期是一个与模型大小相关的绝对世界单位值这样在场景中缩放物体时描边粗细不会跟着模型一起缩放。如果你希望描边跟随模型缩放可以把外扩操作放在模型空间完成也就是直接用v.vertex.xyz v.normal * _OutlineWidth然后用UnityObjectToClipPos转换到裁剪空间。Pass 2 是正常模型。它的代码非常普通就是采样贴图乘上主颜色保证模型本身以原始画面呈现。渲染顺序上Unity 会先执行 Pass 1 再执行 Pass 2所以正常模型的正面会覆盖掉轮廓壳的正面只有溢出的边缘部分露出来形成轮廓。5.2 创建材质并应用创建材质文件的菜单路径Assets - Create - Material将材质命名为 PixelCharMat然后在 Shader 下拉框中选择PixelArt/Outline。把材质拖到场景中像素角色的 MeshRenderer 上。如果模型贴图已经在 Unity 里准备好可以直接拖到材质的主纹理槽中。此时你可以先把 Scene 视图中的光照调暗一些对比“启用轮廓线”和“关闭轮廓线”的差异。开启时角色应该被一圈纯黑色描边包裹关闭时描边立刻消失。这个开关效果就是 Properties 中 Toggle 参数的作用。5.3 使用 C# 脚本动态调节描边参数实际开发中我们经常需要在运行时动态改变描边颜色和宽度比如敌人被锁定时描边从黑变红主角受击时描边瞬间变宽再恢复。这些都可以通过 Material 的 SetFloat 和 SetColor 完成。创建脚本Assets - Create - C# Script命名为 OutlineController.cs内容如下// 文件路径Assets/Scripts/OutlineController.cs using UnityEngine; public class OutlineController : MonoBehaviour { [Header(材质与参数)] public Material targetMaterial; [Range(0f, 0.1f)] public float outlineWidth 0.03f; public Color outlineColor Color.black; private static readonly int WidthID Shader.PropertyToID(_OutlineWidth); private static readonly int ColorID Shader.PropertyToID(_OutlineColor); private static readonly int UseOutlineID Shader.PropertyToID(_UseOutline); private void Awake() { if (targetMaterial null) { Renderer renderer GetComponentRenderer(); if (renderer ! null) { targetMaterial renderer.material; } } } private void Update() { if (targetMaterial null) { return; } targetMaterial.SetFloat(WidthID, outlineWidth); targetMaterial.SetColor(ColorID, outlineColor); targetMaterial.SetFloat(UseOutlineID, 1f); } public void SetOutlineWidth(float width) { outlineWidth Mathf.Clamp(width, 0f, 0.1f); } public void SetOutlineColor(Color color) { outlineColor color; } public void DisableOutline() { if (targetMaterial ! null) { targetMaterial.SetFloat(UseOutlineID, 0f); } } public void EnableOutline() { if (targetMaterial ! null) { targetMaterial.SetFloat(UseOutlineID, 1f); } } }脚本里的Shader.PropertyToID是一个性能优化做法。字符串形式的Material.SetFloat在 Unity 内部每次都会做哈希运算提前缓存 ID 可以避免每帧重复计算。虽然单次差距很小但在有大量角色和描边参数的场景中这个习惯能减少一些 CPU 开销。把脚本挂到角色物体上将材质赋给脚本上的 targetMaterial 字段。运行场景后在 Inspector 面板里拖动 outlineWidth 滑动条角色的描边宽度会实时变化这会让你快速找到最适合当前场景的描边值。如果你想让描边只出现在部分物体上可以通过一个物体上的多个 Material 元素配合材质数组来实现也可以把材质实例化后单独管理。这里要注意不要直接修改通过GetComponentRenderer().sharedMaterial获得的共享材质否则会影响到其他相同模型。5.4 低分辨率渲染强化像素感描边只是让模型边缘清晰起来了但要让画面真正变成“像素游戏”还要把整个画面分辨率降下来并配合最近邻采样让每个像素呈现出可见颗粒。Unity 中可以用 RenderTexture 轻松实现这个效果。创建一个新脚本 PixelizeCamera.cs// 文件路径Assets/Scripts/PixelizeCamera.cs using UnityEngine; [RequireComponent(typeof(Camera))] public class PixelizeCamera : MonoBehaviour { [Header(像素分辨率)] public int pixelWidth 640; public int pixelHeight 360; private Camera targetCamera; private RenderTexture renderTexture; private void Awake() { targetCamera GetComponentCamera(); } private void Start() { renderTexture RenderTexture.GetTemporary(pixelWidth, pixelHeight, 24); renderTexture.filterMode FilterMode.Point; targetCamera.targetTexture renderTexture; } private void OnDestroy() { if (renderTexture ! null) { // 注意如果还需要在其他地方使用先解除相机的 targetTexture targetCamera.targetTexture null; RenderTexture.ReleaseTemporary(renderTexture); renderTexture null; } } public void UpdateResolution(int width, int height) { pixelWidth width; pixelHeight height; if (renderTexture ! null) { RenderTexture.ReleaseTemporary(renderTexture); } renderTexture RenderTexture.GetTemporary(pixelWidth, pixelHeight, 24); renderTexture.filterMode FilterMode.Point; targetCamera.targetTexture renderTexture; } }这个脚本的原理很简单把相机的输出目标从屏幕改成一张低分辨率的 RenderTexture并让纹理过滤模式为 Point最近邻。最终效果是画面像素块变大轮廓线也跟着变成锯齿色块这种锯齿感正是像素风格重要的视觉组成。低分辨率数值建议从 640x360 起步。如果项目输出竖屏游戏可以试试 360x640。分辨率越低像素颗粒越大。注意分辨率不要设得过低低于 320x240 时角色细节会丢失得很严重轮廓线也可能因为太粗而吞掉面部表情。5.5 URP 与 Shader Graph 的适配思路如果你在使用 URP上面的内置管线 Shader 不能直接使用。适配可以走两条路线第一条路线是直接用 Shader Graph 重建同样的效果。在 Shader Graph 中创建两个 Sub Graph 或者两个 Pass 并不像内置管线那样直观所以更推荐使用 UPR 的 HLSL 手写 Shader。第二条路线是手写 HLSL Shader把 Surface Shader 改成 Lit 或者 Unlit 管线。URP 下的顶点着色器需要包含 URP 的相关库比如#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl然后使用TransformObjectToWorld、TransformWorldToHClip等函数来替代旧版内置函数的调用。整体逻辑和第五节的内置 Shader 是完全一致的一个 Pass 做背面外扩一个 Pass 做正面渲染。如果你是初学者当前最优先的建议是先把内置管线版本跑通理解这个流程后再去对照 URP 的官方模板迁移。一次牵扯太多种细节容易打击信心。6. 常见问题与排查思路6.1 描边显示不出来最直观的现象是材质已经应用了但模型完全没有黑色描边。排查顺序如下检查 Shader 的 Pass 1 是否存在。如果删掉了 Pass 1就只会有正常模型渲染。检查Cull Front是否被误写为Cull Back。如果写错外扩的正面会被直接遮挡看起来没有效果。检查描边宽度是否过小。0.001 这种数量级在视觉上几乎看不见往大了调试试。检查是否把材质的_UseOutline开关关掉了。Toggle 参数在关闭状态下会直接走原位置输出。6.2 描边粗细不均匀这个问题在低多边形模型上很常见。原因是顶点法线的平滑结果不一致。比如一个长方体的角点如果法线是三个面平均得到的外扩方向就是对角线方向导致角部外扩距离比平面区域更大描边看起来“角重边轻”。处理办法有几种如果是体素模型拆开成独立 Cube并给每个 Cube 设置不同的法线让它们保持轴对齐方向。如果模型已经合并可以单独制作一个只用于描边的“外扩网格”把外扩方向预先写死在顶点数据里。接受部分不均匀并把它当成风格的一部分。一些像素游戏反而会利用这种不均匀表现手绘感。6.3 描边被其他物体遮挡或穿透模型描边本质上是膨胀的模型壳有一定厚度所以可能出现以下情况两个角色贴近时一方的描边壳嵌进另一方的模型里。角色贴在墙边时墙会直接切掉描边导致轮廓线忽隐忽现。解决方案包括使用描边层分离渲染把轮廓壳放到单独层让它在深度测试上不和普通物体竞争。让描边壳的深度写入关闭这样它只会画在模型表面之外不会影响后方物体。在相机后处理里用膨胀深度值让描边基于屏幕边缘检测而不是模型外扩。对于像素风格项目最推荐的是第一种“单独层”思路因为场景简单成本最低。6.4 低分辨率渲染下描边闪烁闪烁通常由两个原因造成一是描边宽度太窄在像素化后轮廓线宽度不到一个像素时有时无二是相机和目标物体的相对移动导致轮廓线在像素网格上跳变。解决思路增大_OutlineWidth数值至少让描边在目标分辨率下占到 2 到 4 个物理像素。控制像素渲染分辨率不要过低避免边缘信息丢失。如果是实际玩家角色尽量让相机保持平滑跟随减少高频抖动。下表做一个快速排查参考问题现象常见原因解决思路描边不显示Pass 缺失或 Cull 方向错误检查 Shader 两个 Pass 和剔除方向描边消失于暗背景描边颜色贴近背景颜色替换更明显的轮廓色描边粗细不均体素模型法线平滑拆模型或单独生成描边网格低分辨率下闪烁描边宽度小于像素粒度增大宽度降低分辨率调整幅度描边穿透到模型内部多个描边壳叠加分层渲染控制深度测试6.5 描边颜色没有办法通过代码修改这个问题通常是材质实例化导致的。场景中使用共享材质时targetMaterial.SetColor会修改到所有同材质物体。如果你只希望某个角色的描边变色必须调用renderer.material让 Unity 自动创建一个材质实例每次返回的都是独立实例。但要注意频繁访问renderer.material会创建新材质实例要提前缓存。7. 工程优化与风格化进阶7.1 用顶点色控制局部描边加权在最基础的 Inverted Hull Shader 中所有顶点共用同一个描边宽度。但我们经常希望角色某些部分描边粗一点、某些部分细一点比如头发轮廓更粗、手指描边更细。一个可行的方案是利用顶点色Vertex Color在 Shader 属性里增加_OutlineWidthScale (描边缩放, Float) 1然后在顶点着色器中读取顶点色float scale v.color.r * _OutlineWidthScale; worldPos normalize(worldNormal) * _OutlineWidth * scale;顶点色可以在建模软件里绘制也可以在 Unity 里通过第三方网格工具设置。这样同一个材质就能在不同模型部位产生不同描边效果非常适合像素角色差异化表现。7.2 使用后处理制作描边发光如果需要“被选中单位外发光”的效果可以在现有描边之上叠加一个全屏后处理利用屏幕空间的颜色和深度信息把发光效果限制在描边区域。常见做法是先用后处理采样深度缓冲找出边缘然后在边缘像素上叠加一个 emit 色。在像素风格游戏里这种发光可以做成“格子状”的辉光和普通现代 3D 的泛光不同不建议直接使用带有大量模糊的 Bloom那样会破坏像素颗粒感。7.3 透明物体的描边问题半透明材质比如水晶、能量体、隐形敌人加描边时描边壳和模型半透正面会产生叠影。最简单的方法是把描边 Pass 也设为半透明在 Pass 1 增加Blend SrcAlpha OneMinusSrcAlpha。把_OutlineColor的 alpha 通道作为轮廓透明度。渲染队列设为Transparent。不过透明描边在复杂场景中非常容易有排序问题实际项目中建议单独验证不要一上来就统一改。7.4 性能与批处理建议法线外扩描边本身效率很高一次 Draw Call 额外多渲染一遍模型壳即可。但有两个隐藏的性能坑需要注意如果场景中有大量角色批量渲染时描边壳会增加顶点处理压力。可以只在玩家和重要可交互物体上开描边普通装饰物不加。使用renderer.material实例化后材质之间如果参数不同无法合批。可通过打图集、合并网格降低 Draw Call。内存方面RenderTexture.GetTemporary建议不要每帧创建。PixelizeCamera 的脚本里我只是在 Start 创建一次如果要做动态分辨率也要避免每帧释放再创建尽量只在分辨率变化时更新。7.5 Godot 与 Three.js 工程中的思路迁移Unity 之外很多 3D 像素游戏也使用 Godot 或 Web 端渲染。Inverted Hull 的思路同样适用。在 Godot 中描边可以通过 SpatialMaterial 的“反照率 背面渲染”方式实现也可以用 Shader 的NORMAL外扩顶点shader_type spatial; render_mode cull_front; void vertex() { VERTEX NORMAL * 0.03; } void fragment() { ALBEDO vec3(0.0, 0.0, 0.0); }在 Three.js 中可以在 CustomShader 的 vertex shader 里按法线挤出顶点再关闭背面剔除然后用一个黑色材质单独渲染壳层。核心表达式也是同一个transformed normalize(objectNormal) * outlineWidth;如果你是在做 Web 端像素渲染像素感还需要配合后期采样THREE.RenderTarget和textureFilter NearestFilter实现思路与 Unity 的低分辨率 RenderTexture 完全一致。8. 小结与扩展学习方向本文从 3D 像素艺术的实际视觉痛点出发介绍了轮廓线的必要性对比了 Inverted Hull、深度法线后处理描边、菲涅尔边缘三种实现方式重点拆解了法线外扩描边的原理并给出完整的 Unity Shader 代码、材质创建流程、动态调节脚本以及低分辨率像素化渲染的配套方案。如果你只是想在项目里快速实现描边可以直接把第五节的 Shader 拿去修改参数如果你需要适配体素模型第六章的排查思路和第七章的顶点色加权方案应该能帮你少走很多弯路。接下来可以继续深入的方向包括学习 Shader Graph 制作可复用描边节点研究深度法线后处理在卡通渲染中的统一描边用法尝试把描边宽度写入纹理贴图让不同材质区域自动匹配粗细以及把轮廓线系统扩展到敌人锁定、队友高亮、剧情对话焦点等交互反馈中。3D 像素艺术的视觉风格非常依赖这些小细节的打磨把描边逻辑做成一个独立模块后面的项目和场景复用起来会轻松很多。建议你先在测试场景里跑通这套流程再逐步往正式项目中迁移。
返回列表