ARTICLE DETAIL

资讯详情

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

3D像素艺术轮廓线方案:屏幕空间深度法线边缘检测实战

3D像素艺术轮廓线方案:屏幕空间深度法线边缘检测实战 把 3D 场景做成像素游戏的样子第一步其实很简单把内部渲染分辨率降到 320×180关掉抗锯齿再用最近邻采样放大到窗口。这一套做完模型立刻会变成一堆方块感极强的像素块画面确实有了复古味。但真正做过的人都知道第二步才是关键角色站在深色背景前轮廓糊成一团模型造型明明不复杂却根本看不出脸和身体的分界线。缺的正是 2D 像素游戏里那圈不起眼的黑色描边。轮廓线在 2D 像素艺术里是画师一笔一笔画上去的属于美术资源的一部分。但 3D 场景中的模型要旋转、镜头要移动轮廓必须每帧动态计算。很多人在第一次接触这个需求时会习惯性打开贴图往模型上画黑边或者给材质加一圈边缘发光结果动画一跑起来就露馅线是贴在表面上的转动角度后根本不是真正的外轮廓。这篇文章要讲的是一套更适合 3D 像素艺术的轮廓线方案包含两条主流技术路线的对比、一个可直接落地的 Unity 后处理实现以及像素艺术场景里最容易踩的坑。读完你能分清“轮廓”和“描边”的区别知道为什么屏幕空间深度法线边缘检测天然适合像素风能照着代码在项目里做出稳定、干净的像素描边效果也能理解为什么直接抄网络上的通用描边 Shader十有八九会在低分辨率像素画风里翻车。1. 这篇文章真正要解决的问题3D 像素艺术的核心是“可读性”。低分辨率会抹掉大量细节如果物体之间没有一个明确的边界画面就会变成一堆颜色接近的色块。这时轮廓线承担的任务已经不只是装饰而是把角色、道具、地面从视觉上强制分离。可以说轮廓线是 3D 像素艺术风格的骨架而不是后处理的锦上添花。一个常见的误解是只要把抗锯齿关掉像素化效果就自然完成了。实际上低分辨率解决了“像素格子”的问题但没有解决“空间关系”的问题。两个模型深度接近、颜色接近时即使分辨率再低眼睛也很难从杂乱的色块里认出轮廓。这也是为什么像素游戏的角色普遍带黑色描边描边在二维画面上重新定义了物体的边界让角色从背景中跳出来。这篇文章主要面向以下几类读者正在做像素风格 3D 游戏想把角色和场景的轮廓做得干净利落喜欢体素风格模型想从 MagicaVoxel 这类工具导出模型放进 Unity 渲染对风格化渲染感兴趣想知道卡通描边和像素描边之间的本质差异被网络上通用描边 Shader 坑过想搞清楚参数为什么调不出来。需要说明的是本文的核心思路不绑定具体引擎。示例采用 Unity 内置渲染管线 HLSL是因为这套组合对深度法线纹理的支持最直接、代码最短。只要理解了“低分辨率渲染 屏幕空间边缘检测”这条管线搬到 URP、Godot 甚至 WebGL 都只是换采样 API 的问题。2. 轮廓线到底是什么三种线不能混为一谈很多人把“轮廓线”当成一种效果其实它至少由三种不同来源的线组成并且在 3D 实时渲染里需要用不同方式处理。第一种是外轮廓线也就是物体和背景的交界行话叫 occlusion boundary 或 silhouette。比如一个立方体悬浮在空中它与背景色之间的那圈线就是外轮廓。外轮廓只出现在物体边缘像素上是玩家判断物体形状最重要的线索。第二种是内轮廓线也就是物体表面法线发生明显转折的地方。例如立方体的棱、圆柱的顶盖与侧面的交界。这类线并不在物体边界上而是在物体内部用于提示模型表面的体积变化。第三种是材质边界线例如角色脸颊、眼睛贴图区域的边界。这类线往往不是几何意义上的转折而是美术上为了保持风格统一而强行加的线条。2D 像素艺术里画师可以自由决定三种线画在哪里、多粗、什么颜色。3D 渲染里没有画师只有数据。外轮廓需要从深度或法线数据中提取内轮廓可以通过对比相邻像素的法线方向得到材质边界则通常要靠美术额外制作 Mask 纹理来控制。很多效果“看起来不对”本质上是因为三种线的来源不同却试图用同一个参数处理。还有一个容易混淆的概念描边贴图和动态轮廓线。描边贴图是把线条预先画在模型的 UV 上适合锁定镜头角度的 UI 道具但模型旋转时线条不会跟着真实轮廓变化。动态轮廓线则是实时计算出来的是 3D 像素艺术真正需要的东西。本文后面讨论的轮廓线全部指动态轮廓线。下表总结了常见轮廓实现方式的基本特点方法原理优点缺点适合场景贴图手工描边把黑线画在 UV 上实现零成本随模型旋转失效固定朝向的 UI、道具边缘发光 / Rim视角与法线接近垂直时叠加颜色参数少是光晕不是硬线像素风不成立卡通发光、能量光效膨胀背面法复制网格翻转法线放大背面涂黑外轮廓完整线宽随距离变化容易穿模配合屏幕空间方案补强屏幕空间深度法线边缘检测对比相邻像素的深度和法线差异像素对齐、稳定内部细节线需要额外控制3D 像素艺术首选几何着色器线框在三角形边生成线条精确控制线条管线复杂、性能开销大工程实验、低模技术演示从表格可以直观看到屏幕空间深度法线边缘检测虽然代码量比贴图描边大但它不会随相机距离变化出现线宽变化也不会在模型转动时露出破绽。更关键的是边缘检测是以像素为单位进行的这正好和像素艺术的“格子”逻辑完全吻合。3. 两条主流路线膨胀背面 vs 屏幕空间边缘检测膨胀背面法也叫 Inverted Hull是卡通渲染里非常经典的描边方案。原理是给模型多渲染一遍这一遍把模型的背面朝向摄像机并沿着顶点法线方向把顶点向外偏移一段距离再整体涂成黑色。因为背面在正常情况下不可见渲染出来恰好是模型周围一圈黑色外轮廓。膨胀背面法的优点是外轮廓非常完整并且不需要深度纹理只要模型是封闭的就可以工作。缺点也很明显顶点偏移是在模型空间里做的当相机远离时偏移量在屏幕上的像素数目会变小相机靠近时又会变粗。要修正这一点通常要在顶点着色器里把偏移量转换成屏幕空间大小但透视变化仍会带来微妙的抖动。更麻烦的是遇到相邻模型、地面穿插等情况时膨胀出来的黑色背面会被前景物体遮挡轮廓线突然断开。屏幕空间边缘检测则完全是另一套思路。它不碰几何体而是先把场景渲染到一张低分辨率纹理上同时生成一张包含深度和法线信息的纹理。后处理阶段逐像素对比“当前像素”和“相邻像素”的深度差、法线夹角差。差值超过阈值就判定为轮廓线所在位置直接覆盖成线条颜色。两者的本质区别在于膨胀背面法画的是“几何体的外圈”屏幕空间法做的是“像素层面的不连续检测”。前者更依赖模型拓扑后者更依赖画面信息。对于 3D 像素艺术来说屏幕空间法有几个明显的优势检测发生在低分辨率 RenderTexture 上1 个像素的检测结果经过最近邻放大后会变成整齐的 N×N 像素方块天然是像素风的“硬边”。轮廓线不是贴在模型表面而是贴在画面像素上所以线条粗细不随相机距离剧烈变化。外轮廓和内轮廓能通过深度阈值与法线阈值分别控制可调性更强。因此本文的核心实现选择屏幕空间深度法线边缘检测。膨胀背面法不做主线但会在后面的进阶章节里作为“粗轮廓补强”方案出现因为有些场景要求角色轮廓特别醒目单靠后处理的 1 像素线条会显得太细。4. 环境准备低分辨率像素化相机先说明环境示例使用 Unity 内置渲染管线版本不必太新Unity 2020.3 以上都可以。建模部分可以直接用 MagicaVoxel 导出 OBJ也可以用任意低模。需要准备的最小场景如下一个 Perspective 相机建议把 FOV 调小比如 20~30 度降低透视畸变对轮廓稳定性的影响几个低模或体素模型一个足够亮的平行光保证模型表面明暗可分辨所有模型使用基础材质即可像素风的色阶量化放在后处理里做不需要依赖复杂的 PBR 材质。核心思路是先让相机渲染到一张低分辨率的 RenderTexture相机本身不直接输出到屏幕。随后在OnRenderImage阶段把这张低分辨率纹理作为后处理 Shader 的输入先做边缘检测放大到屏幕。实现代码如下保存为PixelArtCamera.csusing UnityEngine; [RequireComponent(typeof(Camera))] public class PixelArtCamera : MonoBehaviour { [Header(内部渲染分辨率)] public Vector2Int internalResolution new Vector2Int(320, 180); [Header(轮廓后处理材质)] public Material contourMaterial; private Camera targetCamera; private RenderTexture lowResRT; private void Awake() { targetCamera GetComponentCamera(); lowResRT new RenderTexture( internalResolution.x, internalResolution.y, 24, RenderTextureFormat.ARGB32 ); lowResRT.filterMode FilterMode.Point; lowResRT.useMipMap false; lowResRT.autoGenerateMips false; lowResRT.name PixelArtLowResRT; targetCamera.targetTexture lowResRT; targetCamera.allowMSAA false; targetCamera.allowHDR false; targetCamera.depthTextureMode | DepthTextureMode.DepthNormals; } private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (contourMaterial ! null) { Graphics.Blit(source, destination, contourMaterial); } else { Graphics.Blit(source, destination); } } private void OnDestroy() { if (lowResRT ! null) { lowResRT.Release(); Destroy(lowResRT); } } }几个关键点值得仔细解释第一internalResolution一般取 320×180、480×270 这类 16:9 分辨率。这个数值决定了最终像素块的大小。如果目标窗口是 1280×720像素放大倍数是 4 倍一个内部像素正好对应屏幕上 4×4 个像素。放大倍数不一定是整数也没关系但非整数倍会导致像素块大小不均匀观感会差很多。第二filterMode FilterMode.Point必须设置。默认的双线性过滤会在放大时把像素之间做插值画面会变得模糊这是很多“像素化”效果看起来不像像素艺术的原因。只有 Point 模式才能保证每个像素方块是实色。第三depthTextureMode | DepthTextureMode.DepthNormals是开启深度法线纹理的关键。开启后相机在渲染场景时会同时生成一张深度法线纹理后处理 Shader 可以直接采样它来判断轮廓。第四这段脚本把相机直接渲染到低分辨率 RT再由OnRenderImage放大到屏幕本质上是“先低分辨率渲染再后处理最后点采样放大”的完整管线。这一步顺序不能反如果在全分辨率下做边缘检测轮廓线会变成屏幕像素宽度细到几乎看不见画面的像素格子也会被破坏。5. 核心 Shader深度法线边缘检测现在写核心的轮廓检测 Shader。它的逻辑是取中心像素的深度与法线再取周围 8 个像素的深度与法线只要周围的任何一个像素与中心像素的差异超过阈值就认为当前像素属于轮廓线直接涂成线条颜色。之所以用 3×3 邻域而不是简单的一阶差分是因为 3×3 能同时照顾到水平和垂直方向的边缘并且当轮廓线穿过斜方向时不会断线。像素艺术要求轮廓必须是硬边所以这里不使用 smoothstep而是用step函数做硬判定。Shader PixelArt/PixelContour { Properties { _MainTex (Scene, 2D) white {} _LineColor (Line Color, Color) (0, 0, 0, 1) _LineWidth (Line Width, Float) 1.0 _DepthThreshold (Depth Threshold, Float) 0.12 _NormalThreshold (Normal Threshold, Float) 0.35 _DepthStrength (Depth Strength, Float) 1.0 _NormalStrength (Normal Strength, Float) 1.0 } SubShader { Tags { RenderType Opaque } Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; sampler2D _CameraDepthNormalsTexture; float4 _CameraDepthNormalsTexture_TexelSize; fixed4 _LineColor; float _LineWidth; float _DepthThreshold; float _NormalThreshold; float _DepthStrength; float _NormalStrength; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag(v2f i) : SV_Target { float2 uv i.uv; float2 texelSize _CameraDepthNormalsTexture_TexelSize.xy * _LineWidth; float centerDepth; float3 centerNormal; DecodeDepthNormal( tex2D(_CameraDepthNormalsTexture, uv), centerDepth, centerNormal ); centerNormal normalize(centerNormal); float edge 0.0; for (int x -1; x 1; x) { for (int y -1; y 1; y) { if (x 0 y 0) continue; float2 sampleUV uv float2(x, y) * texelSize; float sampleDepth; float3 sampleNormal; DecodeDepthNormal( tex2D(_CameraDepthNormalsTexture, sampleUV), sampleDepth, sampleNormal ); sampleNormal normalize(sampleNormal); float depthDiff abs(sampleDepth - centerDepth); float normalDiff 1.0 - saturate(dot(sampleNormal, centerNormal)); float depthEdge step(_DepthThreshold, depthDiff) * _DepthStrength; float normalEdge step(_NormalThreshold, normalDiff) * _NormalStrength; edge max(depthEdge, normalEdge); } } edge saturate(edge); fixed4 sceneColor tex2D(_MainTex, uv); fixed4 finalColor lerp(sceneColor, _LineColor, edge); return finalColor; } ENDCG } } }代码中的DecodeDepthNormal是 Unity 内置函数它从_CameraDepthNormalsTexture解码出线性深度和法线方向。解码出的深度范围约 0 到 1相邻像素差值超过_DepthThreshold就说明这里有深度跃变法线方向则通过点积判断夹角变化超过_NormalThreshold就判定为表面转折。_LineWidth是最容易被误解的参数。它是以内部分辨率的像素为单位的默认 1 表示只在相邻 1 个像素范围内比较。由于整个后处理发生在 320×180 的低分辨率纹理上_LineWidth1最终经过放大后就是 N×N 像素的粗黑线而不是屏幕上的 1 像素细线。如果你想稍微加粗设置成 2 就可以但要注意像素艺术风格里过于宽的轮廓线会淹没模型细节。这个 Shader 的输出是“场景原色 黑色线条”叠加。如果想做白描边、蓝描边只需要改_LineColor。如果想保留轮廓的透明通道用于后面合成也可以把最终颜色拆成两部分输出到不同 RenderTexture工程化时会用到这种拆分。在 URP 工程里OnRenderImage不可用需要把这段 Blit 逻辑改成ScriptableRendererFeature里的自定义 Pass。核心 Shader 的采样逻辑完全不变只是把_CameraDepthNormalsTexture的来源换成 URP 提供的深度法线纹理。URP 12 以上的版本可以在管线资产里开启 Depth Texture再通过渲染特性获取对应纹理更保守的做法是自己的 RenderPass 里单独生成一张深度法线图。6. 进阶一色阶量化让轮廓更“像素”如果直接把上面的代码跑起来会发现轮廓线已经有了但画面整体还不是像素艺术的味道。原因是模型表面经过了光照插值同一个面上会出现渐变这在真正的像素游戏里很少见。像素风格的一个典型特征是色阶量化把一个面的亮度强制压到少数几档产生类似“二值化光照”“三档阴影”的效果。量化可以在PixelContour的片元着色器中顺手完成。片段着色器在输出前对sceneColor做一次量化让颜色变化呈现阶梯感// 片段着色器末尾替换原来的 sceneColor 计算 fixed3 ColorQuantize(fixed3 color, float steps) { color saturate(color); color floor(color * steps 0.5) / steps; return color; } fixed4 frag(v2f i) : SV_Target { // ...边缘检测逻辑保持不变... fixed4 sceneColor tex2D(_MainTex, uv); sceneColor.rgb ColorQuantize(sceneColor.rgb, _ColorSteps); fixed4 finalColor lerp(sceneColor, _LineColor, edge); return finalColor; }此时需要在 Properties 中添加_ColorSteps参数默认给 4 到 6。_ColorSteps的含义是把每个颜色通道分成多少档。3 档会得到非常强烈的色块感适合低多边形 像素风格6 档相对柔和适合角色相对精细的场景。这里有一个非常实用的细节先量化颜色再做边缘检测还是先做边缘检测再量化颜色从工程效果来看边缘检测应该使用深度法线纹理与颜色量化无关量化只作用于最终显示的漫反射颜色。因为轮廓线本身是硬边颜色量化会加剧同一面上相邻像素的颜色跳变如果针对颜色做边缘检测会产生大量伪轮廓画面会变得非常脏。正确顺序是深度法线负责判断轮廓颜色量化负责处理表面明暗两者各司其职。除了颜色量化还可以对深度做量化。具体做法是把DecodeDepthNormal得到的sampleDepth和centerDepth先经过floor(depth * steps) / steps处理再计算差值。这样相隔很远的物体如果落在同一个深度量化档内就不会被判成轮廓能有效抑制远处噪点。缺点是模型自遮挡处的轮廓可能变淡需要根据场景距离范围微调steps和_DepthThreshold。色阶量化还有一个额外好处当表面亮度被压缩成几个色阶原本连续的渐变变成了明显的色块边界像素艺术味道更浓。此时轮廓线在画面中的作用会被放大因为色块之间的边界和真正的轮廓线混在一起玩家默认会认为两者是统一的绘画风格。7. 进阶二膨胀背面补充粗轮廓屏幕空间边缘检测的优势是稳定但它画出来的轮廓线宽度永远和像素格子挂钩。有些像素游戏角色需要特别厚的外轮廓例如 8 像素宽的黑边单靠后处理去膨胀轮廓线会让线条变得圆润模糊反而不像素。这时可以叠加一层膨胀背面法让模型真正向外扩一圈几何轮廓。膨胀背面法需要给模型准备一个双 Pass Shader第一遍只渲染背面并涂黑Shader PixelArt/InvertedHullOutline { Properties { _MainTex (Main Texture, 2D) white {} _Color (Tint, Color) (1, 1, 1, 1) _OutlineColor (Outline Color, Color) (0, 0, 0, 1) _OutlineWidth (Outline Width, Float) 0.02 } SubShader { // 第一遍渲染背面作为轮廓 Pass { Name OUTLINE Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; }; float _OutlineWidth; fixed4 _OutlineColor; v2f vert(appdata v) { v2f o; float3 normal normalize(v.normal); float3 pos v.vertex.xyz normal * _OutlineWidth; o.pos UnityObjectToClipPos(pos); return o; } fixed4 frag(v2f i) : SV_Target { return _OutlineColor; } ENDCG } // 第二遍正常渲染 Pass { Name MAIN Cull Back CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; }; sampler2D _MainTex; fixed4 _Color; v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv) * _Color; return fixed4(col.rgb, 1.0); } ENDCG } } }当镜头靠近模型时_OutlineWidth在屏幕上的厚度会变大镜头远离时又会缩小。为了解决这个问题需要在顶点着色器里做屏幕空间补偿把模型空间偏移量统一换算成“至少 N 个像素”。一种常见近似是取模型包围盒和相机距离计算当前物体在屏幕上的缩放比例再把_OutlineWidth乘上这个比例。更精确的实现还可以使用顶点在裁剪空间中的 w 分量让偏移量在 NDC 空间固定。膨胀背面法适合给角色、宝箱、门这类需要强视觉引导的物体加粗轮廓不适合地形、岩石这类细节多的物体。因为岩石表面法线凌乱膨胀出来的背面会在镂空处穿模产生大量奇怪的黑色破面。工程上一般把屏幕空间边缘检测做成全局后处理让所有物体都有基础细描边再给关键物体单独挂膨胀背面材质形成“细线描边 粗线强调”的分层结构。需要提醒的是膨胀背面法在第一遍渲染时已经把背面涂黑如果模型本身有半透明材质背面轮廓会透过正面显示出现画面会变得很脏。因此像素风格场景里最好避免大范围半透明模型或者单独控制半透明物体的描边开关。8. 运行验证、参数调试与常见问题把PixelArtCamera.cs挂到相机上材质选择新建的PixelContourShader运行场景后应该立即看到画面整体是低分辨率像素块角色与背景交界处出现黑色硬边模型内部棱角也有线条。判断效果是否合格只看三个标准轮廓线是否稳定模型旋转时线条不闪烁、不消失轮廓线是否像素对齐放大后线条是整齐的方块而不是半透明渐变的细边轮廓线是否区分内外外轮廓清晰内轮廓只在模型折角处出现不在平面上产生噪点。如果希望肉眼调试参数可以在 Shader 里临时把输出改成法线可视化用颜色直观判断法线波动位置// 临时调试代码把返回颜色改成法线颜色 return float4(centerNormal * 0.5 0.5, 1.0);法线可视化后平面区域应当呈单一颜色法线突然变化的部位会呈现不同颜色这些位置就是_NormalThreshold会判定的轮廓候选区。深度可视化同理把depth直接乘上颜色输出观察深度跳变位置。参数调试优先级建议先调_LineWidth确定线条粗细再调_NormalThreshold控制内轮廓最后调_DepthThreshold处理模型穿插。因为内轮廓由法线决定最容易看出问题深度阈值影响相邻物体的边界调得太大两个物体之间的线会消失调得太小地面的微小起伏都会变成轮廓线。常见问题与排查思路如下问题现象可能原因排查方式解决方案描边完全不显示相机没有开启 DepthNormals 纹理或材质未挂载检查depthTextureMode是否包含DepthNormals确认材质 Shader 正确在 Awake 中设置DepthNormals检查后处理 Blit 材质描边很细且有半透明过渡后处理在全分辨率下执行或_LineWidth用了小数观察像素块大小是否均匀保证后处理在低分辨率 RT 上执行_LineWidth用整数像素平坦表面上出现噪点线法线阈值太小或深度纹理精度不足输出法线可视化观察平面是否有颜色波动调大_NormalThreshold对深度做量化线条随镜头距离忽粗忽细用了膨胀背面法且宽度未做屏幕空间补偿切换近远景观察轮廓变化改成屏幕空间边缘检测补偿_OutlineWidth模型自遮挡处轮廓断裂深度阈值过大忽略了相近深度的小变化旋转模型观察断裂位置降低_DepthThreshold提高_DepthStrengthURP 下_CameraDepthNormalsTexture无效URP 默认不生成该纹理检查 URP 设置中的 Depth Texture开启 URP Depth Texture改用 RenderFeature 自建深度法线图半透明物体边缘出现黑块深度法线对透明物体无效检查模型材质透明度透明物体关闭描边或统一改成不透明这些排查步骤并不复杂但能解决绝大多数“描边效果不对”的问题。最容易被忽略的是分辨率顺序只要后处理不是在低分辨率 RenderTexture 上做的无论参数怎么调最终都不会有像素风硬边。9. 最佳实践与后续学习方向在实际游戏项目里轮廓线很少是直接放在 Blit 里的一个全局效果就收工通常要考虑三件事线条分层、艺术控制、性能边界。线条分层指的是外轮廓线和内轮廓线分开控制。有些美术风格希望只保留外轮廓不要内部折痕线有些风格则相反。在 Shader 里可以分别让_DepthStrength和_NormalStrength控制两类线条权重并在材质面板暴露两个参数。更精细的做法是把轮廓检测结果输出到单独的 RenderTexture再通过合成阶段选择哪些区域使用轮廓线哪些区域跳过。艺术控制建议使用 Mask 纹理。像素角色经常有眼睛、腮红、衣服纹理等细节这些区域一旦被内轮廓线切过就会显得凌乱。可以在模型 UV 上追加一张 Mask 贴图白色区域禁止生成轮廓线。实现时在边缘检测 Shader 里采样 Mask 纹理如果 Mask 值大于阈值就把edge强制置为 0。这个技巧在像素风角色上几乎是刚需因为像素画的线条逻辑和三维表面转折并不完全一致。性能方面320×180 分辨率下每个像素做 9 次深度法线采样总量只有几十万次纹理读取现代 GPU 完全可以忽略。但如果你做的是 480×270 甚至 640×360 的内部分辨率并且目标平台是移动端建议把 3×3 邻域降到“上下左右”四方向检测性能进一步减半。轮廓检测阶段还可以考虑提前结束如果中心像素周围 4 个方向都判断为无边缘就直接返回场景颜色减少法线解码计算。后续学习方向可以从两条线深入。一条是渲染管线本身的工程化把内置管线的OnRenderImage改写成 URP 的ScriptableRendererFeature再接上景深、发光等后处理形成一个完整的像素风渲染框架。另一条是轮廓线算法本身只看深度和法线是简化做法更接近手绘风格的方案是研究 suggestive contours、occluding contours 这类基于视点与几何关系的轮廓提取算法。对于实时渲染来说前者已经够用对于风格化实验和教学后者能帮你理解轮廓线为什么应该出现在某些位置而不是所有法线突变的地方。最后回到 3D 像素艺术本身。判断一个像素风效果是否成立不要只看单个模型的截图要动起来看模型旋转时轮廓是否有跳动角色走到背景前时轮廓是否稳定放大后线条是否都是整齐的方块。轮廓线不是给画面增加一道黑色线条这么简单它决定了玩家如何理解低分辨率下的世界。把本篇的低分辨率相机、深度法线检测、色阶量化这三个环节跑通再根据项目风格调整阈值和线宽你的 3D 像素项目就已经具备了一套非常可靠的视觉骨架。建议把文中的两个 Shader 和相机脚本收藏起来作为基础模板后续做风格化渲染时直接在它上面扩展会比每次从零开始调参高效得多。
返回列表