卡通渲染略讲

本来呢,我是不打算写卡通渲染相关的东西的。虽然我挺喜欢玩女神异闻录5这种卡通风格的游戏,但是从技术路线来讲,我更希望走写实路线的渲染,例如最令我震撼的神秘海域4的画面。不过由于我现在待的公司搞了大半年的卡通风格的游戏,(项目中道崩殂,成为每个开发心中永远的痛。。。)所以我还是写一下相关的东西,万一以后还要搞,我可以有个入口知道要从哪里开始弄。好了,废话说到这儿,开始正篇。

由于卡通渲染是旨在还原美术人员手绘的感觉,所以它的漫反射呈现色块的感觉,而不是渐变。然后我们利用光的方向和法线方向做点乘后得到的结果作为范围划定的依据,分了四层,像这样:

 fixed diff = dot(worldNormal,normalize(worldLight));
 diff = diff * 0.5 + 0.5;
 fixed w = fwidth(diff) * 2.0;
 if (diff < _DiffuseSeg.x + w){
        diff = lerp(_DiffuseSeg.x,_DiffuseSeg.y,smoothstep(_DiffuseSeg.x - w,_DiffuseSeg.x + w,diff));
  }else if (diff < _DiffuseSeg.y + w){
        diff = lerp(_DiffuseSeg.y,_DiffuseSeg.z,smoothstep(_DiffuseSeg.y - w,_DiffuseSeg.y + w,diff));
  }else if (diff < _DiffuseSeg.z + w){
        diff = lerp(_DiffuseSeg.z,_DiffuseSeg.w,smoothstep(_DiffuseSeg.z - w,_DiffuseSeg.z + w,diff));
  }else{
        diff = _DiffuseSeg.w;
  }

其中_DiffuseSeg为(0.1,0.3,0.6,1.0),这个可以根据需求自己调整,不必拘泥。为什么要写的那么复杂而不是直接给 diff设值呢,因为直接赋值颜色的分界线会有明显的抗锯齿,所以用fwidth函数先求出邻域内的梯度值w,再在边界处+-w进行渐变混合来消除锯齿感。
高光区域也类似,我们先得到高光项,然后判断范围,超过这个范围就是1,否则就是0,没有高光。为了抗锯齿,我们也得做类似之前做过的事。

 fixed spec = saturate(dot(worldNormal,halfVector));
 spec = pow(spec,_Gloss); 
 w = fwidth(spec);
 if (spec < _SpecularSeg + w){
      spec = lerp(0,1,smoothstep(_SpecularSeg - w,_SpecularSeg + w,spec));
 }else{
      spec = 1;
 }

后来我们觉得在shader里写了一大堆ifelse效率不高,所以换了个实现方式,这种方式是这篇论文A Non-Photorealistic Lighting Model for Automatic Technical Illustration中提出的一个公式。

公式1

其中Kcool和Kwarm分别由公式2得到。
公式2

其中 Kd是漫反射颜色, Kblue = (0,0,b),b[0,1],Kwarm = (y,y,0),y[0,1],alpha和beta都是用户可调节的参数。

    fixed4 k_blue = fixed4(0,0,_Blue,1);
    fixed4 k_yellow = fixed4(_Yellow,_Yellow,0,1);
    fixed4 k_cool = k_blue + _Alpha * kd;
    fixed4 k_warm = k_yellow + _Beta * kd;

    fixed temp = dot(normalize(worldLight),worldNormal);
    fixed4 diffuse = (1 + temp)/2 * k_cool + (1 - (1+temp/2)) * k_warm;
    diffuse *= _DiffuseCol * _LightColor0 * atten;

最后根据这篇论文Stylized highlights for cartoon rendering and animation还可以对高亮区域进行风格化,其主要思想就是对Blinn-Phong模型中的半角向量进行修改操作,实现高亮区域的缩放、旋转、平移、分块和方块化。(注意这里的顺序不要弄错了!!!)
代码如下:

                //缩放
                halfVector -= _ScaleX * halfVector.x * float3(1,0,0);
                halfVector = normalize(halfVector);
                halfVector -= _ScaleY * halfVector.y * float3(0,1,0);
                halfVector = normalize(halfVector);

                //旋转
                float xR = _RotationX * DegreeToRadian;
                float3x3 rotX = float3x3(1, 0, 0,
                                        0, cos(xR), sin(xR),
                                        0, -sin(xR), cos(xR));
                float yR = _RotationY * DegreeToRadian;
                float3x3 rotY = float3x3(cos(yR), 0, -sin(yR),
                                        0, 1, 0,
                                        sin(yR), 0, cos(yR));
                float zR = _RotationZ * DegreeToRadian;
                float3x3 rotZ = float3x3(cos(zR), sin(zR), 0,
                                        -sin(zR), cos(zR), 0,
                                        0, 0, 1);
                halfVector = mul(rotZ,mul(rotY,mul(rotZ,halfVector)));

                //平移
                halfVector += float3(_TranslationX,_TranslationY,0);
                halfVector = normalize(halfVector);

                //分块
                fixed signX = 1;
                if (halfVector.x < 0){
                    signX = -1;
                }
                fixed signY = 1;
                if (halfVector.y < 0){
                    signY = -1;
                }
                halfVector -= _SplitX * signX * float3(1,0,0) - _SplitY * signY * float3(0,1,0);
                halfVector = normalize(halfVector);

                //方块化
                float sqrThetaX = acos(halfVector.x);
                float sqrThetaY = acos(halfVector.y);
                fixed sqrnormX = sin(pow(2 * sqrThetaX, _SquareN));
                fixed sqrnormY = sin(pow(2 * sqrThetaY, _SquareN));
                fixed minority = min(sqrnormX,sqrnormY);
                halfVector -= _SquareScale * (minority * halfVector.x * fixed3(1, 0, 0) + minority * halfVector.y * fixed3(0, 1, 0));
                halfVector = normalize(halfVector);

这里我就不展开讲了,我对此兴趣不是很大>_<
另外我照着公式写,也没有采用冯乐乐前辈的方法同时用两个角度去改变半角向量,结果调来调去没调出四个方块的高亮,奇怪了。。。

最后,卡通渲染里必不可少的效果,黑色描边,经过项目中的实践,过程式几何轮廓渲染的方法效果比较令人满意(虽然它对cube这种东西没有办法)。它的思想是先弄个pass渲染背面,让顶点沿着扁平化(这里的扁平化其实就是让法线向量的z统一成一个值,我这里取0.01,网上也有取-0.05或者其他什么值的)过后的法线方向扩张,使得背部可见,再把这部分渲染成轮廓线的颜色即可。然后第二个pass里就做正常的卡通渲染做的事。
代码如下:

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); 
                normal.z = 0.01;
                float2 offset = TransformViewToProjection(normal.xy);
                float height = o.vertex.z / unity_CameraProjection._m11;//加入这个参数可让物体描边在离摄像头远的时候不至于太细,近的时候不至于太粗
                float scale = sqrt(height / _OutlineScale);
                o.vertex.xy += offset * scale * _Outline;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {  
                return _OutlineCol;
            }

这里有个技巧,为了让描边在镜头拉远时不至于看不见,拉近时不至于过粗,加入了height这个变量,并且进行开方操作使得它的变化平缓一些,_OutlineScale来控制平缓的度。

最后放上实现的效果,模型都是从冯乐乐前辈的NPR Labs那儿弄来的。

效果图

项目地址

PS. 如果想深入学习卡通渲染的化,unity chan是个很好的入手项目,unity官方商店有一代,最近他们日本unity官方又在GitHub上弄了个二代,这是地址

参考
【NPR】卡通渲染

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355