卡通渲染略讲

2019-10-17  本文已影响0人  上善若水_2019

本来呢,我是不打算写卡通渲染相关的东西的。虽然我挺喜欢玩女神异闻录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】卡通渲染

上一篇下一篇

猜你喜欢

热点阅读