Demo的卡通渲染方案
欢迎参与讨论,转载请注明出处。
前言
本篇文章按理来说在三月便该发布了,因为插队原因延宕至今,不过好饭不怕晚,干就完了奥利给!阅读本文最好拥有一定的图形学知识,当然看个热闹也是好的。
游戏画面的风格是一开始便要定下的大事,这在古法2D主要通过素材本身及后期调色决定,没有太多文章可作。而在现代游戏(尤其是3D)则会通过Shader在原本的元素上进行加料,如通过基于物理的渲染(PBR)将模型凸显出金属、石头、布料等材质倾向。而在早期为了凸显3D模型的立体感,一般会采用经验总结出来的冯氏光照模型(Blinn-Phong),这也是许多3D软件的默认方案,那将会让我们的模型长成这样:
嗯,这有够雕塑风的,让我想起了当初名震一时的猴赛雷,有着异曲同工之妙:
由此可见,对于讲究卡通风格的游戏,这种通用的光照模型肯定得枪毙,于是本文才会诞生。对于这类非写实方向的渲染方案,业界称之为NPR。而往下细分则是日式卡通渲染,其中佼佼者当属《罪恶装备》系列,而《崩坏3rd》也是不少人在这方面的启蒙者。当然美术这一块没有绝对的风格一致,渲染也不例外,所以Demo里的卡通渲染方案乃是个人的方案,不代表业界的标准实现与效果。
Demo基于Unity2019.3开发,渲染管线为URP7.3.1,采用直接编写Shader的方式(HLSL),将一一介绍其中要点。本文所谓的卡通效果以日式2D赛璐璐风格为准,不论厚涂之类的风格。
着色
首先我们先抛开一切:冯氏光照不好那咱们就是了。直接把贴图显示了,什么料都不要加。
// 根据uv坐标获取对应贴图上的颜色half4 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
嗯,虽然很原始,但好歹没那股恶心感了,把投影也加上:
// 根据渲染管线提供的shadowCoord获取光照信息,并计算出投影颜色Light mainLight = GetMainLight(shadowCoord);
color *= mainLight.color * (mainLight.distanceAttenuation * mainLight.shadowAttenuation);
哎呀,有了投影瞬间立体起来了,开始有《塞尔达传说:风之杖》内味了:
要加投影记得添加Pass:ShadowCaster,并且获取光照信息也需要开启一定的宏,这些并非本文重点,详情请查阅URP的Shader实现。
但只是如此还不够:颜色太鲜艳了,看久了会累。那么有两种方案:调色与着色,调色则是进行总体的颜色调节,使之不要这么鲜艳,着色则是根据模型面对光的吸收度决定明暗。这里还是选择着色:它将会增强模型的立体感。
这里说的着色其实就是冯氏光照中的漫反射(Diffuse):当光照射到非平面的物体上,将根据与光的夹角决定吸收度(越是与光垂直的面越亮)。而在3D模型中,每个模型面都会往上发射一条射线,也就事实上构成了一条垂直于平面向量,这在数学中称之为法线(Normal)。我们可以使用**向量点积(Dot)**获取法线与光照方向之间的夹角,以此决定模型面的光亮程度。
half NDotL = dot(normal, lightDir); // 计算法线与光照方向的夹角系数
NDotL = saturate(NDotL); // 保证系数在0-1
color *= NDotL;
啊这,这不是跟一开始差不多么?这是当然的,因为一开始便是冯氏光照的方案。其漫反射的思想其实并无问题,但原罪在于过渡太丰富了,每个模型面与光的夹角都不同,导致颜色都不同。整个模型看起来就过于立体,以至于产生了雕塑感。
而在日式2D卡通的世界里(尤其是赛璐璐),着色并不会有太详细的过渡,只是到了某个角度统一涂暗,反之为亮,最多在两者之间加点过渡而已。那么便基于此思想进行改造即可:
half NDotL = dot(normal, lightDir); // 计算法线与光照方向的夹角系数// 根据_DiffuseRange约束系数,输出0-1的值
// 由于使用了smoothstep,在接近_DiffuseRange上下限时会做柔滑处理,使之产生过渡感
// _DiffuseRange的参考值为0.5, 0.7
half v = smoothstep(_DiffuseRange[0], _DiffuseRange[1], NDotL);// 根据根据v的值决定输出_LightRange范围内的值
// _LightRange的参考值为0.9, 1
v = lerp(_LightRange[0], _LightRange[1], v);color *= v;
还不错,这下便为模型划分了明暗,并在两者之间做了过渡,这种方式称之为二值化。着色并没有采用很明显的暗色,只是想凸显一点立体感,以及让画面更柔和,不那么刺眼罢了。当然目前可以说是非常不明显了,这是有原因的,且待后续调色。
描边
接下来需要补上日式2D卡通不可或缺的一部分:描边(Outline),描边有助于划分物体,明确空间上的层次,并提供一定的风味。
关于描边的实现方式,业界主要有模型多画一遍并将边缘外扩以及屏幕后处理的方案。前者方案在日式游戏较为流行,优点在于实现简单,性能也还算过得去,缺点是必须开抗锯齿不然没眼看。后者实现方式多样,并且根据实现方式能达到不一样的效果(如一定程度的内描边),但有些更适合搭配延迟渲染(Deferred Rendering),而这代表着对显卡带宽与光照方案有要求。
另外在显示方案上也有区别,有追求任何缩放下描边大小不变的,也有自然派的。有让描边纯色的,也有要让描边根据贴图颜色决定的。本人采用的是模型外扩、自然缩放、根据贴图颜色决定的描边方案。
多显示一遍模型在Unity增加一个Pass即可,并且开启正面剔除(只显示背面,不然会干扰到正常模型)。并且在顶点着色器对模型顶点进行外扩,外扩的方向由所在模型面的法线决定。而颜色方面则在片元着色器根据贴图颜色进行置暗显示即可:
// 顶点着色器half4 viewPos = mul(UNITY_MATRIX_MV, input.positionOS); // 将顶点从模型空间转为观察空间
half3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, input.normalOS); // 同上,将法线转为观察空间
viewPos += float4(normalize(normal), 0) * 0.0075; // 顶点沿法线外扩
output.positionCS = mul(UNITY_MATRIX_P, viewPos); // 将顶点从观察空间转为裁剪空间output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); // 提取uvreturn output;
// 片源着色器
color *= SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); // 提取贴图颜色return color * 0.3; // 压暗
果不其然,没有抗锯齿的话就很搓,跟早期的跑跑卡丁车似的。安排一波MSAA8x:
哎,这就舒服多了,当然实际上由于小泥人的关系,4x和8x实际上看不出区别,而2x也算可以接受的效果,这么看来能耗也还好。当然关于描边实际上还有内描边这个大题,但小泥人不需要这么丰富的细节,这就很舒服。
发光
目前模型的显示还欠缺一些发光的元素,如一般头发和武器会有一些高光效果。这在冯氏光照称之为镜面光照(Specular):本质上与漫反射一样,只是由视角方向与光照方向相加,并与法线做点积获得两者的夹角系数,如此便可实现根据摄像机与光照运动结合决定模型高光的位置。
当然仅此而已是不够的,显而易见仅此而已的话将会如漫反射一般范围很大,而高光实际上只需要一点即可。实际上会将之范围缩小:
half3 halfVec = normalize(lightDir + viewDir); // 将摄像头方向与光照方向相加
half NdotH = dot(normal, halfVec); // 与法线点积,获取夹角系数
NdotH = saturate(NdotH); // 保证在0-1half v = pow(NdotH, _Smoothness); // 缩小夹角系数的值,由于NdotH在0-1,所以pow后会变得更小,_Smoothness参考值为8-64
color += color * v; // 在原有颜色的基础上叠加
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c5JP9lox-1589122401873)(https://musoucrow.github.io/images/lbbn_shading/10.png)]
与之前一样,这样的高光过渡太强了,不够卡通,将之二值化:
half v = pow(NdotH, _Smoothness); // 缩小夹角系数的值,由于NdotH在0-1,所以pow后会变得更小,_Smoothness参考值为8-64
v = step(_SpecularRamp, v); // 小于_SpecularRamp的值将为0,反之为1
v = v * _SpecularStrength; // 定义高光强度,参考值为0.2color += color * v; // 在原有颜色的基础上叠加
这样的高光就更有手绘的感觉了,牛屎一块。但很显然对于头发而言光是一块牛屎高光是不够的,让美术自由的进行创作显然是更好的方案。于是引入了发光贴图(Emission),其本身很简单:就是在最后把发光贴图的内容显示出来即可。而之所以要单独划分贴图而不是画死在原贴图,在于要自由的控制透明度甚至曝光,以及让发光参与单独的光照运算(与高光类似的方法,摄像机视角与光照方向相加后与法线点积)。
到了目前仍缺一个日式2D卡通的一个特性:边缘光(Rim),一般为了表达物体处于光亮的环境下,属于光溢出的一种表达,有助于提升画面的层次感。实现原理也很简单:视角方向与法线点积,根据夹角系数取得当前视角下的模型边缘部分,为之加光即可。
half VDotN = dot(viewDir, normal); // 视角方向与法线点积,获取夹角系数
VDotN = 1 - saturate(VDotN); // 取反,方便计算half v = smoothstep(_RimRange[0], _RimRange[1], VDotN); // 与漫反射部分类似,做二值化,参考值为0.4-1
v = step(0.5, v); // 小于0.5的部分都不要了
v = v * _RimStrength; // 设定边缘光强度,参考值为0.1color += color * v; // 叠加
发光的构成大致如此,目前也许看起来不够明显,实是尚未调色所致,且看下文。
调色
先来看看目前的效果:
首先是整体颜色风格不符合主题,这个场景属于有着岩浆的密室,应该符合昏暗以及灼热的色调,使用Split Toning进行调色:
嗯,至少色调上像样了,但还是缺乏灼热的感觉,上Bloom看看:
哎呀,看着只是稍微亮了点的样子,那是因为Bloom需要配合HDR使用,将颜色突破0-1的限制下进行运算,才能做到光溢出的效果:
唔……这溢出的实在是有限,因为目前还处于Linear颜色空间,显示器对于颜色会进行处理,使得颜色之间的区间变小(明暗不明显),需要转成Gamma才能抵消之:
成了,如此便得出了昏暗且灼热的场景风格,高对比度(亮者更亮、暗者更暗)的画面。
后记
这算是本人进入图形渲染的一个里程碑,感觉这的确是个美术活。技术不过是让你能进入赛道罢了,真正决定效果的还得看美术的理念。
Demo的卡通渲染方案相关推荐
- 【非真实渲染】【卡通渲染技术点介绍】
阅读指南 文本介绍卡通渲染的基本技术,实现会放在另外的文档 关键词 Cel Shading,ToonShading,色块.色调,各向异性,描边,高光 特征 看起来像手绘的图片 少渐变(指光影的变换), ...
- 卡通渲染及其相关技术总结
原文链接https://blog.uwa4d.com/archives/usparkle_cartoonshading.html 这是侑虎科技第246篇原创文章,感谢作者洛城供稿,欢迎转发分享,未经作 ...
- 从《BLAME!》说开去——新一代生产级卡通真实感混合的渲染方案Maneki
<BLAME!>是Polygon Pictures Inc.(以下简称PPI)创业33周年以来制作的第一部CG剧场电影,故事来自于贰瓶勉的同名漫画作品(中文译名为<探索者>或者 ...
- unity 3d物体描边效果_从零开始的卡通渲染描边篇
序言: 一直对卡通渲染非常感兴趣,前后翻找了不少的文档,做了一些工作.前段时间<从零开始>的手游上线了,试着渲染了一下的其中模型,觉得效果很不错.打算写一个专栏记录其中的渲染技术.在后面的 ...
- Unity-ShaderLab 逆向还原《原神》角色卡通渲染思路与实现(保姆级教学)-2
一转眼已经有一个多月没更新了额,时间过得真快.今天终于决定要更新Part 2了. ----那我们就继续吧. 上篇已经讲完了漫反射.高光和光照贴图,本篇将会介绍角色背光时边缘光与基于阈值图的面部阴影的实 ...
- Unity Shader卡通渲染 · 高清渲染管线·HDRP
Unity Shader卡通渲染 · 高清渲染管线·HDRP 前言 最近在研究HDRP管线中的卡通渲染,就想着能不能把官方的UCTS移植到HDRP管线里面去,说干就干,到昨天晚上上传了github,今 ...
- 二次元卡通渲染之描边
前言 本文为"优梦创客"原创文章,您可以自由转载,但必须加入完整的版权声明 更多学习资源请加QQ:1517069595获取(企业级性能优化/热更新/Shader特效/服务器/商业项 ...
- 日式卡通渲染笔记(罪恶装备 碧蓝幻想 原神 战双)
**裁边漫反射(StepDiffuse) 传统的漫反射是这样** float NL = dot(N,L); float3 Diffuse = max(0,NL);//也可以除以PI 而卡通渲染里希望明 ...
- [图形学] 《Real-Time Rendering》卡通渲染
原文:<Real-Time Rendering,Third Version> Toon Shading章节 11.1 卡通着色 正如不同的字体能给文字带来不同的风格一样,不同的渲染风格也有 ...
最新文章
- Python基础02-序列及通用操作
- Django(part41)--中间键Middleware
- 在STM32CubeMX生成的MDK5工程上添加RT-Thread Nano后双击工程名无法打开.map文件的解决方法
- 【文末有赠书】从历史角度讲现代数学
- 对bmp文件内存压缩 与 解压缩
- git学习(五)分支操作和解决冲突
- 解决电脑右键新建没有文本文档的问题
- 如何读取H264文件获得每一帧的数据(VsParserPro)
- Maven的安装与配置
- google chrome 浏览器 必备插件
- WordPress直接调用头像地址
- 单核cpu多核cpu如何执行多线程
- Logic Synthesis And Verification Algorithms Gary D. Hachtel Fabio Somenzi 第十章
- 使用记录6_发布微信小游戏
- Flink基础系列8-Flink on yarn运行wordcount程序
- java ca认证_java编程方式生成CA证书
- 幻核退出 “数字藏品有何用”阶段性无解
- openssl RSA 内存读取密钥
- 《Oracle Concept》第三章 - 12
- 程序员成就技术大拿之路
热门文章
- 获取安卓应用包名和入口 Activity
- shell编程实例练习
- 什么是归一化,它与标准化的区别是什么?
- 纽约市街道规划设计与治理启示
- python opencv设置不同的视频编解码器参数
- 【推荐】2022年公用事业行业研究报告产业发展前景市场投资行情分析白皮书(附件中为网盘地址,报告持续更新)
- mongoose http 源码解析(1)
- 诸葛亮86字的家训 影响一生!
- oracle ssd加速,评测 | Intel Optane SSD 加速 SmartX 超融合在 Oracle 等场景下的系统性能...
- 攻防世界MISC练习区(SimpleRAR、base64stego、功夫再高也怕菜刀)