NPR/卡通渲染学习笔记

卡通渲染是图形学中一个有趣的话题,属于非真实感计算机图形学(NPR)的范畴,在NPR领域中也最多地被应用到实际游戏中,近年来流行的《守望先锋》,《英雄联盟》,《原神》,《崩坏3》等游戏中都或多或少地出现过卡通渲染的身影,最近对这个领域的内容作了一些了解和探索,所以就对其中涉及的一些经典技术做一个简单概述,并使用Unity引擎制作了一个demo。

卡通渲染最关键的特征包括不同于真实感渲染的艺术化光影效果和描边。以这两个关键的特征为卡通渲染分类的话,根据着色方式的不同,可以将近年来游戏中常用的卡通渲染分为cel shading和Tone based shading。

Celshading是模仿传统日式动漫的赛璐璐风格,着色趋向大片连续纯色色块,有明显的明暗交界线,如《崩坏三》,《原神》。

Tone based shading的风格化基于美术指定的色调,色彩连续,有渐变色,阴影和高光常采用夸张手法,得到的是美式卡通风格,如《军团要塞2》,《堡垒之夜》。

描边方法大致有四类

1 基于法线和视角的描边,计算依赖于直觉观察,即视角方向与模型表面相切时,表面上的像素点往往就是模型的边缘,dot(viewDir,normal),物体法线和相机视角方向的点乘结果越接近于0,说明该偏远越接近于边缘,将该这些偏远赋值为黑色即可完成描边。缺点:描边粗细依赖于物体表面的曲率,当曲率变化不均一时,描边的粗细也会不均一,所以这类描边使用较少。

2 背面扩展法、过程式几何描边

使用一个额外的pass描绘物体背面,同时剔除正面。再用剔除背面的pass盖在之前的pass上。在顶点着色器中把backface的顶点沿着顶点法线方向向外扩。

优点:不需要关于相邻顶点的信息,渲染速度快,线条宽度可控,这里学习使用shaderlab语音简单实现了背面扩展法描边,并支持分别设置明暗区域的颜色。

Shader "Unlit/Ouline"
{
    Properties
    {
        _MainTex("MainTex",2D)="white"{}
        _MainColor("Main Color",Color)=(1,1,1)
        _ShadowColor("Shadow Color",Color)=(0.7,0.7,0.8)
        _ShadowRange("Shadow Range",Range(0,1))=0.5
        _ShadowSmooth("Shadow Smooth",Range(0,1))=0.2

        _RimColor("RimColor",Color)=(1,1,1,1)
        _RimMin("RimMin",Float)=0
        _RimMax("RimMax",Float)=1
        _RimSmooth("RimSmooth",Float)=1

        [Space(10)]
        _OutlineWidth("Outline Width",Range(0.01,1))=0.24
        _OutlineColor("Outline Color",Color)=(0.5,0.5,0.5,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }

        Pass
        {
            Tags{"LightMode"="ForwardBase"}
            Cull Back
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            half3 _MainColor;
            half3 _ShadowColor;
            half _ShadowRange;
            half _ShadowSmooth;
            half4 _RimColor;
            float _RimMin;
            float _RimMax;
            float _RimSmooth;

            struct a2v
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float2 uv:TEXCOORD0;
            };
            struct v2f
            {
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD0;
                float3 worldNormal:TEXCOORD1;
                float3 worldPos:TEXCOORD2;
            };

            v2f vert(a2v v)
            {
                v2f o;
                UNITY_INITIALIZE_OUTPUT(v2f,o);
                o.uv=TRANSFORM_TEX(v.uv,_MainTex);
                o.worldNormal=UnityObjectToWorldNormal(v.normal);
                o.worldPos=mul(unity_ObjectToWorld,v.vertex).xyz;
                o.pos=UnityObjectToClipPos(v.vertex);
                return o;
            }
            half4 frag(v2f i):SV_TARGET
            {
                half4 col=1;
                half4 mainTex=tex2D(_MainTex,i.uv);
                half3 viewDir=normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
                half3 worldNormal=normalize(i.worldNormal);
                half3 worldLightDir=normalize(_WorldSpaceLightPos0.xyz);
                half halfLambert=dot(worldNormal,worldLightDir)*0.5+0.5;
                //half3 diffuse=halfLambert>_ShadowRange?_MainColor:_ShadowColor;
                half ramp=smoothstep(0,_ShadowSmooth,halfLambert-_ShadowRange);
                half3 diffuse=lerp(_ShadowColor,_MainColor,ramp);
                diffuse*=mainTex;

                half f=1.0-saturate(dot(viewDir,worldNormal));
                half rim=smoothstep(_RimMin,_RimMax,f);
                rim=smoothstep(0,_RimSmooth,rim);
                half3 rimColor=rim*_RimColor.rgb*_RimColor.a;

                col.rgb=_LightColor0.rgb*(diffuse+rimColor);
                return col;
            }
            ENDCG
        }

        Pass
        {
            Tags{"LightMode"="ForwardBase"}
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            half _OutlineWidth;
            half4 _OutlineColor;

            struct a2v
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float2 uv:TEXCOORD0;
                float4 vertColor:COLOR;
                float4 tangent:TANGENT;
            };
            struct v2f
            {
                float4 pos:SV_POSITION;
            };
            v2f vert(a2v v)
            {
                v2f o;
                UNITY_INITIALIZE_OUTPUT(v2f,o);
                float4 pos=UnityObjectToClipPos(v.vertex);
                float3 viewNormal=mul((float3x3)UNITY_MATRIX_IT_MV,v.normal.xyz);
                //讲法线变换到NDC空间
                float3 ndcNormal=normalize(TransformViewToProjection(viewNormal.xyz))*pos.w;
                //将近裁面右上角位置的顶点变换到观察空间
                float4 nearUpperRight=mul(unity_CameraInvProjection,float4(1,1,UNITY_NEAR_CLIP_VALUE,_ProjectionParams.y));
                //求得屏幕宽高比
                float aspect=abs(nearUpperRight.y/nearUpperRight.x);
                //顶点延法线方向外扩
                ndcNormal.x*=aspect;
                pos.xy+=0.01*_OutlineWidth*ndcNormal.xy;
                o.pos=pos;
                return o;
            }
            half4 frag(v2f i):SV_TARGET
            {
                return _OutlineColor;
            }
            ENDCG
        }
    }
}

3 基于图像处理的边缘检测算法

在图片上找到深度或者法线不连续的位置,用Sobel算子对深度信息进行边缘检测来获得边缘像素。优点是性能开销固定,基于渲染目标的分辨率。不会因为物体突然增多而增加额外的计算压力,即可绘制外描边也可绘制内描边,描边粗细稳定易控制。缺点是需要额外的法线和深度信息,当然,由于近年来流行的延迟渲染框架,法线和深度本来就是G-Buffer的一部分,因此往往不需要额外绘制法线和深度的信息。

全部评论

相关推荐

大摆哥:刚好要做个聊天软件,直接让你帮他干活了
点赞 评论 收藏
分享
评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客企业服务