ARTICLE DETAIL

资讯详情

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

Unity 3D像素画渲染:点采样与着色器实现锐利像素风

Unity 3D像素画渲染:点采样与着色器实现锐利像素风 1. 为什么3D像素画渲染总是不够“锐”做3D像素画风格的项目最常听到的抱怨就是“糊”“脏”“像蒙了一层雾”。模型是低多边形的贴图是像素的可一进引擎跑起来画面就是不对劲。这个问题我在早期做独立游戏原型的时候踩过无数次后来才慢慢摸清楚问题基本不在美术资源本身而在渲染管线的采样方式和后处理策略上。3D像素画也有人叫像素风3D、低分辨率3D和传统2D像素画最大的区别在于它的“像素感”不是画出来的而是渲染出来的。你给一个低模贴上低分辨率贴图如果引擎用默认的双线性或三线性过滤去采样GPU会自动在像素之间做插值结果就是边缘发虚、色块糊成一团。这跟2D像素画里“一个像素就是一个色块”的原则完全冲突。所以这篇内容要解决的核心问题就一个怎么让3D场景在渲染阶段保持像素的硬边和色块感同时不牺牲可玩性和性能。适合正在做像素风3D游戏、或者想尝试这种美术风格的Unity开发者不管你是刚入门还是已经做过几个项目这里面的参数逻辑和踩坑记录都能直接拿去用。我下面讲的东西核心围绕Unity的渲染管线展开但底层原理对任何引擎都通用。关键词里的点采样、着色器、渲染这几个点我会拆开揉碎讲清楚它们各自在哪个环节起作用、参数怎么算、为什么这么选。2. 核心原理拆解像素感到底从哪里来2.1 像素感的三个来源分辨率、采样、后处理很多人以为像素风就是“把贴图做成像素的”这个理解只对了三分之一。3D像素画的视觉特征其实由三个独立环节共同决定任何一个环节没控制好最终画面都会走样。第一个环节是渲染分辨率。这是最容易被忽略但影响最大的因素。如果你的游戏在1080p屏幕上以原生分辨率渲染每个像素只有1/1080的高度像素感自然就消失了。真正的像素风3D通常会把内部渲染分辨率压到320x180、480x270或者640x360这种级别然后再放大到屏幕尺寸。这个“先低分辨率渲染、再整数倍放大”的流程是像素感的基础。第二个环节是纹理采样方式。Unity默认的纹理过滤是Bilinear双线性过滤它会在相邻像素之间做颜色插值让贴图看起来平滑。但像素画要的是硬边所以必须把过滤模式改成Point点采样也叫最近邻采样。点采样的逻辑很简单每个屏幕像素只取纹理上离它最近的那个纹素texel的颜色不做任何混合。这样色块边界就是硬的。第三个环节是后处理。抗锯齿AA、各向异性过滤、屏幕空间反射这些默认开启的效果都会破坏像素的硬边。尤其是MSAA和FXAA它们的设计目标就是消除锯齿而像素画恰恰需要保留锯齿。所以做像素风3D后处理栈要大幅精简。这三个环节的关系可以用一个类比来理解渲染分辨率决定了“画布有多大”采样方式决定了“每个格子怎么填色”后处理决定了“画完之后要不要磨皮”。三者缺一不可。2.2 点采样与双线性采样的本质区别点采样和双线性采样的区别用数字说话最清楚。假设你有一张4x4的像素贴图要显示在8x8的屏幕上。双线性采样的做法是对于屏幕上每个像素找到它在贴图上的对应位置然后取周围4个纹素按距离加权平均。比如屏幕像素(3,3)对应到贴图坐标(1.5,1.5)它会取贴图上(1,1)、(2,1)、(1,2)、(2,2)四个纹素各取25%混合。结果是原本4个不同的色块变成了一个中间色边缘完全糊掉。点采样的做法是屏幕像素(3,3)对应贴图坐标(1.5,1.5)直接四舍五入到最近的整数坐标(2,2)取那一个纹素的颜色不做任何混合。结果是每个屏幕像素都精确对应一个纹素色块边界清晰锐利。在Unity里设置点采样有两种方式。一种是在纹理导入设置里把Filter Mode改成Point (no filter)。另一种是在Shader里用sampler2D配合tex2D采样时通过SamplerState指定过滤模式。前者是全局设置后者可以在Shader里精细控制。我一般两个都做纹理导入设PointShader里也显式声明双保险。注意点采样在纹理被非整数倍缩放时会产生“像素抖动”或“纹理闪烁”因为相邻屏幕像素可能跳到不同的纹素上。解决办法是尽量让纹理显示尺寸是原始尺寸的整数倍或者用mipmap配合点采样来缓解。2.3 渲染分辨率与像素网格的数学关系渲染分辨率的选择不是拍脑袋决定的它和最终显示分辨率之间有一个整数倍关系。假设你的目标显示分辨率是1920x1080那么内部渲染分辨率应该是1920/n x 1080/n其中n是整数。常见的组合有内部渲染分辨率放大倍数适用场景320x1806x强像素感适合复古风格480x2704x平衡像素感和可读性640x3603x轻度像素感适合需要UI清晰的场景960x5402x微弱像素感接近高清为什么必须是整数倍因为非整数倍放大时GPU需要做重采样会引入不均匀的像素尺寸导致某些像素比其他的宽或高画面看起来“不干净”。整数倍放大时每个内部像素精确对应n x n个屏幕像素像素网格均匀视觉上最舒服。在Unity里实现这个流程通常的做法是创建一个RenderTexture尺寸设为内部渲染分辨率让主相机渲染到这个RT上然后用一个全屏的Blit操作把RT放大到屏幕。Blit时的过滤模式也要设成Point否则放大过程又会引入插值。2.4 着色器在像素化流程中的角色着色器在3D像素画渲染里承担两个任务一是控制纹理采样方式二是处理光照和阴影的“像素化”。纹理采样前面讲过了这里重点说光照。传统的光照计算是逐像素的每个屏幕像素都会计算一次光照颜色结果是平滑的明暗过渡。但像素画的光照应该是“块状”的明暗边界要硬。实现方式有两种一种是把光照计算降到低分辨率比如在顶点着色器里算光照然后插值到片元另一种是在片元着色器里对光照结果做量化把连续的亮度值映射到有限的几个等级。我一般用第二种因为控制更精细。具体做法是在片元着色器里把计算出的光照强度乘以一个量化系数比如8取整后再除以量化系数这样光照就被分成了8个等级明暗边界自然就硬了。这个技巧在卡通渲染里也常用原理是一样的。3. 实操流程从项目设置到最终画面3.1 项目初始设置与渲染管线选择拿到一个新项目第一步是确定渲染管线。Unity现在有三套Built-in、URP、HDRP。做3D像素画我强烈建议用URP。原因有三个一是URP的性能开销比HDRP小得多像素风游戏通常不需要HDRP那些高级光照效果二是URP的Render Feature机制很方便插入自定义的全屏Blit操作三是URP的Shader Graph对新手友好做像素化着色器不用从零写HLSL。创建项目时选URP模板然后在Project Settings里把Color Space设成Gamma而不是Linear。为什么因为Linear空间下颜色计算是物理正确的但像素画要的是“所见即所得”的色块感Gamma空间下颜色更接近美术在绘图软件里看到的效果。这个选择没有绝对对错但Gamma空间下做像素风更省心。接下来是Quality Settings。把Anti Aliasing关掉Anisotropic Textures设成DisableTexture Quality设成Full Res。这些设置都是为了消除任何可能引入插值的环节。3.2 纹理导入设置点采样的正确配置纹理导入是像素风3D最基础也最容易出错的一步。在Unity里选中一张像素贴图Inspector面板里有一堆参数要调。Filter Mode必须设成Point (no filter)。这是最关键的一步不设这个后面全白搭。Compression建议设成None或者High Quality。默认的Normal Quality压缩会在像素边缘引入伪影尤其是颜色对比强烈的像素画压缩后会出现不该有的杂色。如果显存够用直接None最保险。Mip Maps要不要开这个要看情况。如果贴图在场景里会被拉远显示比如地面纹理开Mip Maps可以避免远处的闪烁但Mip Maps的生成过程会做平均可能让远处的像素感变弱。我的做法是近处物体用的贴图关Mip Maps远处大面积贴图开Mip Maps但把Mip Map Filtering设成Point。Max Size根据贴图原始尺寸设不要超过原始尺寸。比如一张64x64的贴图Max Size设64就够了设大了只会浪费显存。Wrap Mode看需求一般设Clamp避免边缘重复。如果是可平铺的纹理就设Repeat。这些设置看起来琐碎但每一项都会影响最终画面。我见过太多项目因为Filter Mode忘了改整个画面糊成一片排查半天才发现是导入设置的问题。3.3 低分辨率RenderTexture的创建与相机配置低分辨率渲染的核心是创建一个RenderTexture让相机渲染到它上面。在Unity里可以代码创建也可以在编辑器里手动创建。代码创建的方式public class PixelPerfectCamera : MonoBehaviour { public int internalWidth 480; public int internalHeight 270; private RenderTexture rt; private Camera cam; void Start() { cam GetComponentCamera(); rt new RenderTexture(internalWidth, internalHeight, 24); rt.filterMode FilterMode.Point; rt.antiAliasing 1; cam.targetTexture rt; } void OnRenderImage(RenderTexture src, RenderTexture dest) { Graphics.Blit(src, dest); } }这段代码做了三件事创建了一个480x270的RenderTexture把过滤模式设成Point把相机的目标纹理设成这个RT。antiAliasing 1表示关闭抗锯齿。然后在OnRenderImage里做Blit把低分辨率画面放大到屏幕。注意Blit的时候源纹理的filterMode已经是Point了所以放大过程也是点采样像素块会保持硬边。相机的其他设置也要调整Field of View不要太大像素风3D通常用较小的FOV40-60度避免透视变形太强导致像素拉伸。Clipping Planes的Near设大一点比如0.3Far根据场景大小设不要设太大以免深度精度不够。3.4 像素化着色器的编写要点URP下写像素化着色器可以用Shader Graph也可以用HLSL。我用HLSL写一个最简版本方便理解原理。Shader Custom/PixelLit { Properties { _MainTex (Texture, 2D) white {} _Color (Color, Color) (1,1,1,1) _LightSteps (Light Steps, Range(2, 16)) 8 } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; float3 normalOS : NORMAL; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 normalWS : TEXCOORD1; float3 positionWS : TEXCOORD2; }; TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _Color; float _LightSteps; Varyings vert(Attributes input) { Varyings output; output.positionCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; output.normalWS TransformObjectToWorldNormal(input.normalOS); output.positionWS TransformObjectToWorld(input.positionOS.xyz); return output; } half4 frag(Varyings input) : SV_Target { half4 texColor SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv) * _Color; Light mainLight GetMainLight(); float NdotL dot(normalize(input.normalWS), mainLight.direction); float lightIntensity saturate(NdotL * 0.5 0.5); float quantized floor(lightIntensity * _LightSteps) / _LightSteps; half3 finalColor texColor.rgb * quantized * mainLight.color; return half4(finalColor, texColor.a); } ENDHLSL } } }这个着色器的核心逻辑在frag函数里。先采样纹理得到基础颜色然后计算主光源的方向和法线的点积得到光照强度。关键的一步是floor(lightIntensity * _LightSteps) / _LightSteps把连续的光照强度量化成_LightSteps个等级。_LightSteps设成8光照就被分成8档明暗边界是硬的。sampler_MainTex的过滤模式由纹理导入设置决定如果纹理设了Point这里就是点采样。如果想在Shader里强制点采样可以用SamplerState point_clamp_sampler然后传给SAMPLE_TEXTURE2D。3.5 后处理栈的精简与自定义URP默认的后处理栈里有好几个效果会破坏像素感必须关掉。Anti-aliasing在Camera组件和URP Asset里都要关。MSAA设DisabledFXAA和SMAA都关。BloomBloom会让亮部向外扩散产生柔光效果这和像素画的硬边冲突。如果一定要用把Threshold调高、Intensity调低让扩散范围尽量小。Depth of Field景深模糊会直接糊掉像素必须关。Motion Blur同理关。TonemappingTonemapping会改变颜色曲线可能让像素画的颜色偏离美术的预期。建议设成None或者用Neutral模式。Color Grading可以保留但不要做太强的颜色偏移否则像素色块的颜色会失真。如果需要在后处理阶段做像素化比如把整个画面再降一次分辨率可以写一个自定义的Render Feature。核心逻辑是在RenderPass里把相机颜色目标Blit到一个低分辨率RT再Blit回来。这个操作要放在所有其他后处理之前否则其他效果会在高分辨率下计算浪费性能。4. 常见问题与排查实录4.1 画面模糊的五个排查方向画面模糊是3D像素画最常见的问题原因可能有五种按排查优先级排列第一纹理Filter Mode没设Point。这是最高频的原因检查所有像素贴图的导入设置确保Filter Mode是Point。有时候美术更新了贴图导入设置被重置了也会导致这个问题。第二渲染分辨率太高。如果内部渲染分辨率接近屏幕分辨率像素感自然就弱了。检查RenderTexture的尺寸确保是目标分辨率的整数分之一。第三后处理开了抗锯齿。检查URP Asset和Camera组件上的Anti-aliasing设置全部关掉。第四Blit时的过滤模式不对。放大RenderTexture到屏幕时如果Blit用的材质过滤模式是Bilinear会引入插值。确保源RT的filterMode是Point。第五纹理压缩产生了伪影。如果贴图压缩率太高像素边缘会出现杂色看起来像模糊。把Compression设成None或High Quality试试。这五个方向按顺序排查基本能解决90%的模糊问题。4.2 像素抖动与纹理闪烁的解决方案像素抖动pixel shimmering是点采样的固有缺陷。当纹理在屏幕上移动时相邻屏幕像素可能突然跳到不同的纹素上导致画面看起来在“抖”。这个问题在相机移动或物体旋转时特别明显。解决方案有三个层次最彻底的是用整数倍缩放。如果纹理在屏幕上的显示尺寸始终是原始尺寸的整数倍每个纹素精确对应n x n个屏幕像素就不会有抖动。但这在3D场景里很难保证因为透视会导致远处的纹理缩小。其次是开Mip Maps配合点采样。Mip Maps会预生成一系列降采样的纹理远处物体用低级别的Mip减少纹素和屏幕像素的比率从而减轻抖动。但Mip Maps的生成过程会做平均可能让远处的像素感变弱。折中方案是把Mip Map Filtering设成Point这样Mip级别之间的过渡也是硬切的。最后是在Shader里做纹理坐标的量化。把UV坐标乘以纹理尺寸取整后再除以纹理尺寸这样采样点就被“吸附”到纹素中心减少抖动。代价是纹理在斜面上会有轻微的变形。float2 pixelUV floor(input.uv * _MainTex_TexelSize.zw) / _MainTex_TexelSize.zw; half4 texColor SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, pixelUV);这段代码把UV量化到纹素网格上实测能明显减轻抖动但纹理在斜面上会有阶梯感。用不用要看具体场景。4.3 性能优化像素风3D的渲染开销控制像素风3D的性能开销主要来自三个方面低分辨率渲染的Blit操作、着色器的光照计算、以及后处理。Blit操作的开销其实很小因为低分辨率RT的像素数量少Blit的填充率很低。真正需要注意的是Blit的次数每多一次全屏Blit就多一次带宽消耗。尽量把多个后处理合并到一个Pass里。着色器的光照计算是开销大头。如果场景里有很多动态光源每个光源都要在片元着色器里算一次开销会线性增长。优化方式是把光源数量限制在4个以内用Lightmap烘焙静态光照动态光源只保留主光源。像素风的光照量化本身就会掩盖光照精度不足的问题所以用低精度的光照计算完全够用。后处理方面前面说了要精简但有些效果是必要的比如颜色校正。颜色校正的开销很小可以保留。Bloom如果一定要用把分辨率降到1/4再计算能省不少性能。还有一个容易忽略的点是Overdraw。像素风3D的场景通常比较简单但如果有很多半透明物体比如粒子效果Overdraw会很高。低分辨率渲染下Overdraw的影响比高分辨率小但也不能完全忽视。尽量用不透明材质粒子效果用Alpha Test而不是Alpha Blend。4.4 常见问题速查表问题现象可能原因排查方法解决方案画面整体模糊纹理Filter Mode不是Point检查所有像素贴图的导入设置改为Point (no filter)画面整体模糊渲染分辨率太高检查RenderTexture尺寸降到目标分辨率的1/3或1/4画面整体模糊后处理开了抗锯齿检查URP Asset和Camera设置关闭MSAA/FXAA/SMAA像素边缘有杂色纹理压缩伪影对比压缩前后的贴图Compression设为None相机移动时画面抖动点采样固有缺陷移动相机观察开Mip Maps或量化UV光照过渡太平滑光照未量化检查着色器加光照量化逻辑远处纹理闪烁Mip Maps未开拉远相机观察开Mip MapsFiltering设Point颜色偏暗或偏亮Color Space不对检查Project Settings设成Gamma性能突然下降光源太多检查场景光源数量限制在4个以内用烘焙半透明物体边缘糊Alpha Blend导致检查材质设置改用Alpha Test这张表是我在实际项目里积累的基本上覆盖了像素风3D渲染的常见问题。遇到问题先查表能省不少排查时间。4.5 实操心得与避坑建议做了几个像素风3D项目之后我总结了几条文档里不会写的经验。第一条美术和程序要提前对齐像素网格。像素风3D的贴图尺寸、模型UV、场景比例之间有一个隐含的网格关系。如果美术做的贴图是64x64但模型UV把贴图拉伸到了非整数倍点采样就会出问题。最好在项目初期就定好“一个像素对应多少世界单位”所有资源按这个标准做。第二条不要迷信“全屏点采样”。有些教程说把所有纹理都设成Point就完事了但实际上UI、字体、粒子效果用点采样可能效果很差。UI和字体通常需要清晰的小字点采样会让字变得难以辨认。我的做法是3D场景用点采样UI用双线性两者分开处理。第三条测试要在目标分辨率下做。在编辑器里用高分辨率预览和最终在目标设备上跑效果可能差很多。尤其是移动端屏幕小、像素密度高像素感会比显示器上弱。一定要在真机上测试根据实际效果调整内部渲染分辨率。第四条光照量化等级不要设太高。_LightSteps设成8或16就够了设成32以上就接近连续光照了像素感会消失。但也不要设太低比如4否则明暗过渡太生硬看起来像色阶断裂。8是一个比较安全的默认值。第五条保留一个“关闭像素化”的调试开关。有时候需要对比像素化和非像素化的效果或者排查某个问题是像素化引起的还是其他原因。在相机脚本里加一个bool开关一键切换渲染模式调试效率会高很多。第六条注意RenderTexture的内存管理。每次切换分辨率或场景时旧的RenderTexture要手动Release否则会内存泄漏。在OnDestroy里加Release调用或者用using语句管理生命周期。第七条像素风3D的相机移动要“慢”。因为低分辨率下像素块很大相机快速移动时画面变化剧烈容易让人眼疲劳。适当降低相机移动速度或者加一点平滑阻尼观感会好很多。这个不是技术问题是体验设计问题但很影响最终效果。5. 进阶技巧让像素风3D更有质感5.1 法线贴图在像素风中的特殊处理法线贴图通常用来增加表面细节但在像素风3D里法线贴图的逐像素光照会破坏光照量化的效果。因为法线贴图让每个像素的法线方向都不同量化后的光照等级会在相邻像素之间跳变产生噪点。解决方案是把法线贴图的信息“降频”。具体做法是在采样法线贴图后对法线方向做量化或者干脆不用法线贴图用顶点法线配合低多边形模型来表现细节。低多边形模型的硬边本身就很适合像素风不需要法线贴图来增加细节。如果一定要用法线贴图把法线贴图的分辨率降到和基础贴图一样低并且关闭Mip Maps减少法线方向的突变。5.2 阴影的像素化处理实时阴影在像素风3D里也是个麻烦。Unity的阴影贴图默认是柔和的Soft Shadows阴影边缘有渐变和像素画的硬边冲突。处理方式有两种一是把阴影质量设成Hard Shadows阴影边缘就是硬的二是用屏幕空间的阴影量化在着色器里对阴影强度做和光照一样的量化处理。我一般用第一种简单直接。在Quality Settings里把Shadow Resolution设成Low或MediumShadow Cascades设成No Cascades或Two Cascades阴影边缘自然就硬了。缺点是阴影会有锯齿但这在像素风里反而是加分项。5.3 像素风3D的UI适配策略UI是像素风3D项目里最容易出戏的部分。3D场景是像素化的UI如果是高清的两者放在一起会很违和。我的做法是UI也用低分辨率渲染但用独立的Canvas和RenderTexture。UI的RenderTexture分辨率可以比3D场景高一些比如2倍保证文字可读但整体风格还是像素化的。UI的字体用像素字体图标用像素图标和3D场景的风格统一。如果UI需要和3D场景交互比如世界空间的UI把UI的RenderTexture和3D场景的RenderTexture合成时注意两者的像素网格要对齐否则UI会“浮”在场景上面缺乏融入感。5.4 从像素风到像素艺术的提升方向技术上的像素化只是基础要让画面真正有“像素艺术”的感觉还需要在美术和设计上下功夫。色彩限制真正的像素画通常只用有限的调色板比如16色或32色。在3D场景里可以通过后处理的颜色量化来模拟这个效果把画面颜色映射到有限的调色板上。这个操作要谨慎弄不好会让画面变得很脏。抖动Dithering像素画常用抖动来在有限颜色下表现渐变。在3D渲染里可以在着色器里加抖动图案让光照过渡看起来更有像素画的质感。抖动图案可以用Bayer矩阵或者蓝噪声纹理。像素完美的相机相机的移动要尽量对齐像素网格避免半像素偏移。在相机脚本里对位置做量化让相机每次移动都是整数个像素。这个技巧在2D像素游戏里很常见3D场景里也可以用但要注意透视投影下的像素网格不是均匀的量化要谨慎。这些进阶技巧不是必须的但能让你的像素风3D从“技术正确”提升到“视觉出彩”。我个人的经验是先把基础的像素化流程跑通确保画面清晰锐利然后再逐步加这些进阶效果。不要一上来就追求完美容易陷入参数调优的泥潭。最后分享一个我在实际项目中反复验证的配置组合URP管线Gamma颜色空间内部渲染分辨率480x270所有3D纹理Point采样光照量化8级关闭所有抗锯齿和景深阴影用Hard Shadows。这套配置在PC和移动端都能跑出稳定的像素风效果性能开销也很低。当然具体项目要具体调整但作为一个起点它能帮你省掉大量试错时间。
返回列表