☰
卡通渲染(TOON)核心原理与角色制作实战全解析
2026/10/20 14:20:27 网站建设 项目流程

前几天有位做独立游戏的朋友发来一张截图,问我为什么画面看起来这么“动画”,明明模型和贴图都是3D的。我扫了一眼就告诉他:这是Toon Shading,也就是卡通渲染,大家在二次元风格项目里一般直接叫它TOON。现在不管是热门手游、独立动画,还是三维短片,TOON几乎是绕不开的词,但真正能解释清楚它背后原理的人并不多。我会把做这类渲染的完整经验摊开讲:TOON到底指什么,核心原理在哪,生产环节会踩哪些坑,美术和程序之间怎么配合。

1. TOON圈内到底在说什么:它不是“加个描边”这么简单

1.1 行业里TOON的三层意思

很多人第一次接触TOON,是从“描边”或者“赛璐璐上色”这种美术效果开始的,但真正从业内角度去理解,TOON其实是一个覆盖三层内容的系统。

第一层是Shading,也就是光照模型本身。写实渲染会把光线、粗糙度、菲涅尔全部揉进一个连续的物理模型,输出接近真实相机的亮度;TOON则完全不一样,它故意把光照结果离散化,不让亮度平滑变化,而是让画面出现清晰的明暗分界。这个分界不是偷懒,是一种审美选择,模仿的是二维手绘动画里赛璐璐片上色的方式——只有几层有限的颜色表达光影。

第二层是Linework,通常就是我们说的描边。手绘动画里人物有黑色的轮廓线,3D模型本身没有线条,所以要用专门的Pass或后处理把轮廓找出来。描边的粗细、颜色、在转角处的变化,都直接影响TOON的气质,这一层在写实渲染里基本不需要考虑。

第三层是后处理与合成。二维动画的画面有色差、景深、叠色这些手工痕迹,做TOON时经常需要把边缘检测、色板化、颗粒叠加等后处理手段带上,才能让3D画面真正“像手绘”,而不只是像一个上了色的塑料模型。

我之所以要掰开这三层,是因为很多刚入门的朋友把TOON简单理解为“卡通贴图加描边”,结果做出来的东西永远差一口气,原因就是Shading层没做对,后续叠再多的后期也救不回来。

1.2 为什么写实容易“藏拙”,TOON却很难

这里举个实际体会的例子。写实渲染里你给材质设错了粗糙度,通常只会让高光偏软或偏硬一点,普通人肉眼不一定能第一时间看出毛病,因为人脑对真实世界的材质判断本身就有宽容度。但TOON没有这种宽容度。

TOON里的明暗边界是锐利的,颜色是成块状的,任何一个阈值选得不对、颜色配得不舒服,画面里就会立刻出现一条“脏兮兮”的灰带或者一块突兀的阴影。看的人不一定能说出来哪里不对劲,但一定会觉得“这画面好廉价”。

我在几个项目里反复体会到:TOON对审美和参数调校的要求远高于写实。写实有物理定律兜底,TOON完全靠艺术判断来定规则。所以如果你想做好TOON,第一个要接受的事实是——它不是一条偷懒捷径,而是一条更需要抠细节的路。

2. 核心原理拆解:把平滑光线切成几个“挡位”

2.1 Lambert的平滑是怎么被“砍”出风格的

先说光照。传统漫反射最常用的模型是Lambert:取法线方向N和光方向L的点积,得到0到1之间连续变化的亮度值。物理上这个结果是平滑的,所以物体受光面到背光面会有细腻的渐变。

TOON的第一步,就是把这个渐变“切”成几个台阶。你可以理解成普通旋钮灯光能连续调节亮度,而卡通渲染的灯只有“开”“关”或者“低亮”“中亮”“高亮”几个挡位。人眼看到这种不连续的色阶,就会产生“手绘感”,因为二维上色本来就不追求连续物理光影。

标准的做法是使用step函数或者smoothstep,我写一段最简化的核心逻辑:

// 传统漫反射 float ndotl = max(0, dot(normalWS, lightDirWS)); // 最简单的卡通化:一刀切成两档 float toonBand = step(0.3, ndotl); // 带过渡带的版本:两档之间留一点点渐变 float softBand = smoothstep(0.28, 0.32, ndotl);

直接使用step的问题是边界会产生非常锐利的锯齿,一帧帧播放时还能看到边缘跳变。生产环境中我更推荐smoothstep,过渡带宽通常控制在0.02到0.05之间,既能保留清晰的卡通边界,又能避免过于生硬的像素级切换。

2.2 Ramp贴图:把明暗装进一张纹理

step函数只能切出两三个挡位,一旦美术想要更多层次,比如四层阴影、五层高光,代码里写一大堆if和step又难维护又难调。行业里更通用的做法是使用Ramp贴图。

Ramp贴图本质上是一张一维渐变纹理,横轴是ndotl,从0到1;纵轴可以放更多维度,比如粗糙度或者视角夹角。Shading时直接采样这张图:

float3 toonColor = tex2D(_RampTex, float2(ndotl, 0.5)).rgb;

美术在Photoshop里把明暗色阶画出来,程序在线性空间里采样。每个色条之间的边界就是卡通明暗线,边界附近留几个像素的过渡羽化,用来消除硬边锯齿。

二维Ramp还可以根据材质类型分别定义不同的明暗配色。比如皮肤用低反差、带一点透红的渐变;布料用高反差、干净利落的色块;金属用带高光压缩的曲线。这样做的好处是材质差异从贴图层就分开了,不需要在Shader里写大量分支。

2.3 一个能跑通的核心Shader框架

这里我给出一个去掉平台专属代码的HLSL风格框架,纯讲结构,你在不同引擎里稍微改造就能用:

half ndotl = saturate(dot(N, L)); half3 rampColor = tex2D(_RampTex, half2(ndotl, 0.5)).rgb; half ndoth = saturate(dot(N, H)); half spec = pow(ndoth, _Smoothness); half specBand = smoothstep(_SpecThreshold, _SpecThreshold + _SpecSoft, spec); half rim = 1.0 - saturate(dot(N, V)); half rimBand = smoothstep(_RimStart, _RimEnd, rim); half3 color = _BaseColor.rgb * rampColor * mainLightColor; color += _SpecColor.rgb * specBand; color += _RimColor.rgb * rimBand;

这只是用于演示原理的极简版本,真正的角色Shader还会叠加阴影贴图、间接光、球谐环境光、深度雾等。但核心骨架就是这几行:漫反射走Ramp,高光走阈值,边缘光走菲涅尔。

3. 从“能跑”到“好看”:角色TOON调参实战笔记

3.1 高光:从一条白线到一层色块

很多人在漫反射处理完之后,直接用Blinn-Phong的pow值来推高光。结果做出来的高光要么是一条硬邦邦的白线,要么是很大一块过曝。之所以翻车,是因为卡通高光本质上应该是一块有边界的色块,而不是物理高光。

我的做法是先算Blinn-Phong的spec值,然后用阈值把它量化成一个开关量,再乘上一个高光色:

half3 specColor = smoothstep(_SpecThreshold, _SpecThreshold + _SpecSoft, spec) * _SpecColor.rgb;

阈值的控制逻辑很微妙。阈值太低,高光面积过大,像满屏油光;阈值太高,高光只剩一两个像素,像干裂的墙皮。一般角色脸上我会把阈值控制在0.5左右,soft控制在0.1以内,同时用专门的高光贴图来控制哪些区域允许出现高光。

头发的高光处理也有讲究。真实头发的各向异性高光在卡通渲染里可以简化成一层或两层条状色块。我会先采样一张高光偏移贴图,把高光沿发丝方向拉长,再叠加一层弱高光在发梢,模拟手绘的笔触感。这一条建议值得记下来,很多新手在头发上直接套用皮肤的高光参数,效果总是不对。

3.2 描边:法线外扩为何在头发上翻车

描边是TOON最有辨识度的特征,但也最能暴露问题。基础做法是背面外扩:渲染模型正面之后,再渲染一次背面,把背面顶点沿法线方向稍微推开一点,并输出黑色。代码大致是:

v2f vert(appdata v) { v2f o; float3 pos = v.vertex.xyz + v.normal * _OutlineWidth; o.pos = TransformObjectToHClip(float4(pos, 1)); return o; }

硬表面物体用这个方法效果很好,但一旦碰到头发、裙子、手指这类薄片结构,就很容易穿模。因为头发是大量片状面片拼起来的,法线方向非常混乱,外扩时每个面片都往各自方向膨胀,正面看不到问题,背面却会露出各种三角面破绽。

通用的解决办法有两个方向。第一个是顶点色控制:美术在建模时把描边宽度需求刷进顶点色,程序用顶点色的红色通道乘以基础宽度,达到头发根部细、发梢更细、脸边保持正常的效果。第二个是曲率检测:利用法线和视线夹角计算边缘强度,在描边Pass里动态压低宽度,这个方案对程序更友好,但美术需要额外理解算法逻辑。

3.3 阴影、暗部与色带治理

TOON的阴影边界通常比写实更锐利。阴影贴图带来的自阴影、接触阴影、平行光级联阴影会让暗部出现很多杂乱边界,和Ramp里手工画的干净色阶冲突。我的习惯是给TOON材质单独一个ShadowMap采样版本,把PCF软阴影的范围调小,或者直接用自定义的阴影衰减去贴合Ramp的阈值。

暗部色带问题也很值得单独说。当Ramp阶梯跨度特别大时,屏幕上会出现明显的色阶断层,尤其在暗部,8bit显示下更容易看到一条一条的“等高线”。这是一个典型的量化误差问题。

我项目里常用的解决方式是给ndotl加微量的抖动:

float noise = (Random(uv) - 0.5) * 0.01; ndotl = saturate(ndotl + noise);

这个抖动量必须控制得非常小。太小没作用,太大会让明暗边界出现噪点颗粒。开了TAA的项目里,可以配合时域抖动,把色粒度分散到多帧,视觉上会更干净。

还有一点是暗部不能直接压成死黑。手绘动画的阴影色其实是偏冷或者偏环境色的颜色,而不是纯黑。我一般会在Ramp最暗色阶上叠一层环境球谐光或菲涅尔反射,让暗部保留一点体积感,画面整体会耐看很多。

4. 美术与程序的生产协作:Ramp贴图和参数约定

4.1 Ramp贴图到底怎么画

Ramp贴图不是随手拉一条渐变就行。美术在绘制时要注意几个生产规范,否则到程序那边会有一堆沟通成本。

首先,贴图分辨率不需要很大,256x16或者512x32完全足够,重点是横轴要代表光照方向,左暗右亮,色阶边界的位置必须和程序里约定好的ndotl阈值对应。想让暗部宽一些,就把左边色阶拉长;想让高光范围尖锐,就把右边边界压缩。

其次,色阶之间的过渡羽化要克制。我见过很多美术喜欢把边界羽化得很柔和,觉得这样过渡很舒服,但结果就是一个跑不出来的伪TOON,画面灰扑扑的。羽化宽度建议控制在贴图宽度的2%到4%,也就是256宽度的图上大概5到10个像素。这个宽度可以在渲染时配合smoothstep消除锯齿,又不会把锐利边界弄丢。

第三,Ramp要在线性空间里处理。如果美术给的图是在sRGB空间里画的,程序采样时需要转成线性,否则明暗深浅会和预期不一致。这个坑在跨平台项目里坑过我好几次,后来约定做成规范:所有Ramp贴图必须勾选线性采样或者在有需要时由程序统一转换。

4.2 材质参数怎么暴露给美术

程序写Shader时最容易犯的毛病是把所有细节都钉死成常量。这样做调起来非常不方便。我会把以下参数固定在材质面板:

参数名作用典型范围
_BaseColor基础色贴图或颜色拾色器
_ShadowColor阴影色拾色器,避免纯黑
_RampTex明暗渐变默认给一张标准三阶Ramp
_SpecThreshold高光阈值0.4-0.6
_SpecSoft高光过渡软硬0.02-0.1
_OutlineWidth描边基础宽度0.01-0.05(模型单位相关)
_RimStart / _RimEnd边缘光起止0.0-1.0

这些参数不仅是为了给美术调节,更是为了让同一个Shader能适配不同角色。比如主角的脸部高光阈值和配角的可以完全不同,但材质球里统一调节可比每个模型单独维护一套Shader省事多了。

如果团队用可视化Shader工具,我建议把上面这套整理成自定义节点,端口画清楚,再给美术写一份简短的使用文档。美术最怕的不是参数多,而是不知道每个参数控制什么。

4.3 和PBR资产共存的过渡方案

很多二次元项目的美术资产实际上是以PBR规范制作的:Albedo、Normal、Metallic、Roughness、AO都有。直接放弃这些资产重做TOON专用贴图成本非常高。

更务实的做法是让TOON Shader保留PBR贴图通道,用Roughness和Metallic来决定Ramp的采样维度和高光参数。比如Roughness高的布料,Ramp采样靠近暗色色阶;Roughness低的金属,采样靠近高光压缩段。这样贴图资产不用重新绘制,切换写实和TOON只换Shader,材质球里的参数照用。

Normal贴图在TOON里同样有效,它能让明暗边界沿着法线变化,增加细节。AO则可以直接混合到阴影色里,让缝隙处更暗。这套过渡方案能显著降低制作成本,是我个人最推荐的生产路线。

5. 真实项目踩坑实录:色带、描边与平台差异

5.1 暗部色带的完整排查链路

有次在项目里,角色裙摆暗部出现非常明显的色带,一张一合的,动态下尤其让人难受。一开始以为是Ramp没画好,但换了一张全新建模的Ramp后问题依旧。

后来我做了几个对照实验:静态单帧没色带,动态播放有色带;开了TAA有色带,不开TAA反而轻;压缩贴图格式后色带变重。最后定位到是明暗色阶量化加上光照变化时,暗部产生了步进式跳变。

解决方式分三步:第一步,把Ramp暗部色阶的过渡羽化加宽一点;第二步,给ndotl加上基于屏幕坐标的微抖动态;第三步,也是最重要的,把暗部色阶的数量从四档减少到三档,同时让每档之间的宽度接近人眼感知的均匀程度。最终色带肉眼几乎消失。

我想说的是,排查这种问题不要一上来就怀疑工具,先分清是不是量化精度问题、是不是贴图格式问题、是不是显示位深问题,做一个变量控制实验,很快就能定位。

5.2 描边粗细不均的排查链路

另一个典型问题是同一条裙子,正面描边清晰,侧面描边特别粗,背面直接糊成一团。一开始认为是模型法线有问题,重新平滑法线后改善有限。

后来才想明白,描边是沿法线外扩的,而裙摆模型有很多褶皱,不同位置法线方向差异大,外扩时每个顶点的偏移方向不一致,视觉上就会粗细不均。

解决方法是引入曲率因子来抑制描边宽度。我用顶点法线与视线方向夹角做判断,夹角接近90度的边缘处,把描边宽度乘以一个小于1的系数;同时把顶点色里预刷的权重与这个系数相乘。这样正面、侧面、背面的描边宽度就基本一致了。核心经验是:描边宽度的控制要放在顶点层做,而不是在片段层做,片段里调整只会越描越脏。

5.3 雾效、多光源与移动端精度

TOON对雾效极其敏感。场景里一旦有距离雾,明暗边界就会被雾的插值蒙上一层灰色,整个画面立刻失去干净感。我在项目里的做法是关闭TOON材质默认的雾效接收,改成手动的距离混合,让雾混合时也按TOON的明暗边界去分档。

多光源也是常见问题。主光之外再射来一盏补光时,每一盏光都会跑一遍Ramp,结果模型身上出现好几层互不搭调的明暗带,观感非常碎。我的处理方式是只让主光源跑完整TOON光照,其他光源只做少量颜色增益,不做新的明暗分界。

移动端上还有半精度浮点的坑。低端GPU的fp16精度不够,Ramp贴图采样后会出现更明显的色阶条纹。我会在移动端版本里把Ramp采样结果整体提高精度,或者对Ramp贴图做一次预过滤,把邻域色阶平滑掉。这点在Shader代码里加上平台宏判断即可,成本不高但效果明显。

6. 做完几年TOON项目后的几句心里话

最后分享一点个人体会。做了几年TOON相关项目之后,我最大的感受是它并没有比写实渲染简单,反而在审美层面更苛刻。写实渲染出问题可以通过物理参数一点点调回来,TOON一旦色阶、阈值或描边不对,整个画面都会显得假和廉价。

如果给新人一个学习路径,我会建议先把Lambert、Smoothstep、Ramp这三个概念彻底揉进自己的直觉里;再去找一个人体或头部模型,亲手调一版包含漫反射分阶、高光色块、描边外扩、边缘光的最简Shader。这一套做完,你对TOON的理解会比看十篇教程都快。

至于工具选型,Unity、虚幻引擎、Blender甚至WebGL都能做,关键是先把“量化明暗”这套思维建立起来。跨过这道门槛,你会发现TOON真正的乐趣在于用极少的光照信息,去表达极强的画面气质。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询