前言

随着《原神》游戏的流行,国内对于二次元游戏这块儿畛域越来越看重了。二次元我的项目中自身基于日本的卡通动漫而来,所以最初的实质都是为了尽量还原2D立绘,而并不像PBR谋求物理正确,只有难看,还原立绘,那么就是胜利的。所以说到这里,咱们的指标就是还原立绘。

卡通渲染畛域,其实有一些卡通格调独有的成果,这里就集体对于二次元和日本动漫的了解,收集了一些卡通渲染中独有的成果,以及集体在这块儿爬过的坑记录下来,抛砖引玉,给大家分享下本人的实现思路,如果能帮忙到各位,则甚之好矣(注:本文中的所有代码思路均基于Built in内置管线)。

一、眼睛眉毛穿过头发,俗称飘眉

这个也属于惯例成果了,个别做老二次元美术都会要求这个成果,也是动画摄影中很重要的一个评判成果,目前有两种伎俩,基于深度Depth和基于Stencil。

基于深度Depth代码(劣势:多一个Pass,多一个Draw Call)。

  1. 先画脸部和头发,都保留默认不通明队列即可"Queue" = "Geometry"。
  2. 再画眉毛(须要两个Pass),设置队列在脸部和头发之后"Queue" = "Geometry+10"。
  3. 画眉毛的第一次Pass {ZTest LEqual........}(画没有被头发挡住的局部)。
  4. 画眉毛的第二次Pass {ZTest GEqual......}(画被头发挡住的局部)。

基于Stencil代码。

  1. 先画眉毛,设置眉毛队列 "Queue" = "Geometry-10",同时设置Stencil:

    Stencil{ Ref 2 Comp GEqual Pass Replace Fail Keep}
  2. 再画脸部和头发,对于头发的Stencil设置:

    Stencil{ Ref 1 Comp Greater Pass Keep Fail Keep}

留神:不做任何设置状况下,Stencil的默认值是0。

二、自定义管制Bloom区域

这个是比拟一般的公众需要了,很多二次元游戏中只让脸部泛光,但身材偏白的区域还是能够不泛光的,当然这个如果是齐全本人写前期Bloom成果会容易很多,咱们这里通过批改Unity默认的PPSV2,将该成果融进去,因为目前很多公司都间接采纳了Unity自带的后处理了,的确不便,成果调整也能够。

大略实现思路:

  1. 失常渲染角色,而后将须要Bloom的遮罩区域,通过设置输入色彩的A通道outcolor.a =黑白区域 ,保留到A通道中,会跟着流水线走到前期所须要用的色彩缓冲区的RT中。
  2. 失常Post Bloom,批改Bloom.shader文件,利用以后屏幕RT的Alpha值。

三、基于深度的额发投影

这个分游戏我的项目,国内的确也有大部分游戏我的项目间接不让头发投影解决完事。然而这样会少一层投影关系,立体感可能会削弱,不过对于玩家而言也无所谓了。如果采纳Unity自带的Shadowmap,因为Shadowmap是依据灯光方向计算出来,很难管制,比方右边额发投下来的影子适合了,左边就可能投到脸上了,因为灯光方向不好把控,无奈做到正好只是整个额发投下来的成果,看上去很薄,很纸片的美妙感觉。

参考文章《【Unity URP】以Render Feature实现卡通渲染中的刘海投影》


找到的资源,头发和眼睛在一个网格上,所以眼睛区域也绘制出了投影,真正做的时候必定会离开的

大略思路:

  1. 画脸部和眼睛(头发以外的其余头部区域Mesh)通过第一个Pass,只写入深度。
  2. 画头发,采纳第2个Pass,画出头发区域遮罩(只保留洁净的头发区域),黑白图。
  3. 在脸部Shader中采样黑白图,在灯光的摄像机空间,依照灯光方向偏移第2点中的黑白图,失去额发投影。

代码实现(基于Built in管线)。

画头发用的C#局部:

public class HairMaskGenerate : MonoBehaviour{    public Renderer faceRenderer1;//脸部Renderer    public Renderer faceRenderer2;//脸部Renderer    public Renderer eyeBrowRenderer;//眉毛    //public Renderer eyeRenderer;//眼睛    public Renderer hairRenderer;//头发Renderer    public Material hairMaskMaterial;    private CommandBuffer cmb = null;    private RenderTexture hairMaskRT = null;    private Camera mRTGenerateCamera;    void Start()    {        mRTGenerateCamera = GetComponent<Camera>();        cmb = new CommandBuffer();        cmb.name = "Cmb_DrawHairMask";        hairMaskRT = new RenderTexture(mRTGenerateCamera.pixelWidth, mRTGenerateCamera.pixelHeight, 24);        cmb.SetRenderTarget(hairMaskRT);        cmb.ClearRenderTarget(true, true, Color.black);        //脸部有两局部mesh组成, 只画深度        cmb.DrawRenderer(faceRenderer1, hairMaskMaterial, 0, 0);        cmb.DrawRenderer(faceRenderer2, hairMaskMaterial, 0, 0);        cmb.DrawRenderer(eyeBrowRenderer, hairMaskMaterial, 0, 0);        //cmb.DrawRenderer(eyeRenderer, hairMaskMaterial, 0, 0);        //画出头发的区域,黑白遮罩图        cmb.DrawRenderer(hairRenderer, hairMaskMaterial, 0, 1);        mRTGenerateCamera.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, cmb);    }    // Update is called once per frame    void Update()    {        mRTGenerateCamera.CopyFrom(Camera.main);//对立地位和角度信息,放弃和主相机统一        mRTGenerateCamera.farClipPlane = Camera.main.farClipPlane;        mRTGenerateCamera.nearClipPlane = Camera.main.nearClipPlane;        mRTGenerateCamera.fieldOfView = Camera.main.fieldOfView;        Shader.SetGlobalTexture("_FaceShadow", hairMaskRT);    }}

画头发遮罩用的Shader:

Shader "Unlit/HairMask"{    Properties    {        _MainTex ("Texture", 2D) = "white" {}    }    SubShader    {        Tags { "RenderType"="Opaque" }        LOD 100        Pass        {              ColorMask 0              ZTest LEqual             ZWrite On            CGPROGRAM            #pragma vertex vert            #pragma fragment frag            // make fog work            #pragma multi_compile_fog            #include "UnityCG.cginc"            struct appdata            {                float4 vertex : POSITION;                float2 uv : TEXCOORD0;            };            struct v2f            {                float2 uv : TEXCOORD0;                UNITY_FOG_COORDS(1)                float4 vertex : SV_POSITION;            };            sampler2D _MainTex;            float4 _MainTex_ST;            v2f vert (appdata v)            {                v2f o;                o.vertex = UnityObjectToClipPos(v.vertex);                o.uv = TRANSFORM_TEX(v.uv, _MainTex);                UNITY_TRANSFER_FOG(o,o.vertex);                return o;            }            fixed4 frag (v2f i) : SV_Target            {                // sample the texture                fixed4 col = tex2D(_MainTex, i.uv);                // apply fog                UNITY_APPLY_FOG(i.fogCoord, col);                return fixed4(0,0,0,1);            }            ENDCG        }        Pass        {            ZTest Less             ZWrite Off            CGPROGRAM            #pragma vertex vert            #pragma fragment frag            // make fog work            #pragma multi_compile_fog            #include "UnityCG.cginc"            struct appdata            {                float4 vertex : POSITION;                float2 uv : TEXCOORD0;            };            struct v2f            {                float2 uv : TEXCOORD0;                UNITY_FOG_COORDS(1)                float4 vertex : SV_POSITION;            };            sampler2D _MainTex;            float4 _MainTex_ST;            v2f vert (appdata v)            {                v2f o;                o.vertex = UnityObjectToClipPos(v.vertex);                o.uv = TRANSFORM_TEX(v.uv, _MainTex);                UNITY_TRANSFER_FOG(o,o.vertex);                return o;            }            fixed4 frag (v2f i) : SV_Target            {                // sample the texture                fixed4 col = tex2D(_MainTex, i.uv);                // apply fog                UNITY_APPLY_FOG(i.fogCoord, col);                return fixed4(1,1,1,1);            }            ENDCG        }    }}

脸部Shader(额发投影局部):

half hairShadow = 1.0;#if USE_SUPER_SHADOW    float2 scrUV = input.scrPos.xy / input.scrPos.w;    //计算该像素的Screen Position    //float2 scrPos = i.positionSS.xy / i.positionSS.w;    //获取屏幕信息    float4 scaledScreenParams = _ScreenParams;    //计算View Space的光照方向    float3 viewLightDir = normalize(input.viewLightDir) * (1.0 / input.ndcW);    //计算采样点,其中_HairShadowDistace用于管制采样间隔    float2 samplingPoint = scrUV + _HairShadowDistace * viewLightDir.xy * float2(1 / scaledScreenParams.x, 1 / scaledScreenParams.y);    //若采样点在暗影区内,则获得的value为1,作为暗影的话还得用1 - value;    hairShadow = tex2D(_FaceShadow, samplingPoint).r;#endifhalf4 color = lerp(diffuse , diffuse * _ShadowColor.xyz ,hairShadow);

四、基于深度屏幕等距边缘光

采纳传统NOV的形式,对于法线变换比拟小的立体,会呈现不合理的大面积泛光,原理也好了解,因为整个面法线朝向都统一。除此之外做不到等距,所以有了基于屏幕深度的等距边缘光。等距边缘光还能够配合原来的一起来做,而后Lerp,做出更好的成果。

基本思路:摄像机渲染出深度,通过深度方向上做偏移,做出边缘轮廓成果。

相机上要设置开启深度:MainCam.depthTextureMode |= DepthTextureMode.Depth;

物体身上Shader:

CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct v2f{    float2 uv : TEXCOORD0;    float clipW :TEXCOORD1;    float4 vertex : SV_POSITION;    float signDir : TEXCOORD2;};sampler2D _CameraDepthTexture;float4 _MainTex_ST;float4 _Color;float  _RimOffect;float _Threshold;v2f vert (appdata_full v){    v2f o;    o.vertex = UnityObjectToClipPos(v.vertex);    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);    o.clipW = o.vertex.w ;    float3 viewNormal = mul(UNITY_MATRIX_IT_MV, v.normal);    float3 clipNormal = mul(UNITY_MATRIX_P, viewNormal);    o.signDir = sign(-v.normal.x);    return o; }fixed4 frag (v2f i) : SV_Target{    float2 screenParams01 = float2(i.vertex.x/_ScreenParams.x,i.vertex.y/_ScreenParams.y);    float2 offectSamplePos = screenParams01-float2(_RimOffect/i.clipW,0) * i.signDir;    float offcetDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, offectSamplePos);    float trueDepth   = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, screenParams01);    float linear01EyeOffectDepth = Linear01Depth(offcetDepth);    float linear01EyeTrueDepth = Linear01Depth(trueDepth);    float depthDiffer = linear01EyeOffectDepth-linear01EyeTrueDepth;    float rimIntensity = step(_Threshold,depthDiffer);    float4 col = float4(rimIntensity,rimIntensity,rimIntensity,1);    return col;}ENDCG

五、脸部伦勃朗光

卡通动漫中如果疾速建设人体脸部构造采纳了这个,也能够叫仿米哈游脸部暗影, 当然也有法线改正来实现。法线改正美术实现起来难度较大,而且特地繁琐。最终抉择Lightmap图。代码思路简略,关键在于Lightmap图的制作。

https://www.youku.com/video/X...

代码实现:

float3 _Up    = float3(0,1,0);                          //人物上方向 用代码传进来float3 _Front = float3(0,0,-1);                         //人物前方向 用代码传进来float3 Left = cross(_Up,_Front);float3 Right = -Left;//也能够间接从模型的世界矩阵中拿取出 各个方向//这要求模型在制作的时候得应用正确的朝向: X Y Z 别离是模型的 右 上 前//float4 Front = mul(unity_ObjectToWorld,float4(0,0,1,0));//float4 Right = mul(unity_ObjectToWorld,float4(1,0,0,0));//float4 Up = mul(unity_ObjectToWorld,float4(0,1,0,0));float FL =  dot(normalize(_Front.xz), normalize(L.xz));float LL = dot(normalize(Left.xz), normalize(L.xz));float RL = dot(normalize(Right.xz), normalize(L.xz));float faceLight = faceLightMap.r + _FaceLightmpOffset ; //用来和 头发 身材的明暗过渡对齐float faceLightRamp = (FL > 0) * min((faceLight > LL),(1 > faceLight+RL ) ) ;float3 Diffuse = lerp( _ShadowColor*BaseColor,BaseColor,faceLightRamp);

对于Lightmap图的制作,个别有以下几个思路:

  1. 通过Pencil软件,美术绘制等高线办法绘制
    【教程】应用csp等高线填充工具制作三渲二面部暗影贴图
  2. 通过在Unity或UE4游戏引擎中,本人编写工具来生成
    雪涛:卡通脸部暗影贴图生成 渲染原理
  3. 通过内部工具,无需进入引擎自动化生成,不便好使(举荐该办法).
    橘子猫:如何疾速生成混合卡通光照图

六、闪电式流动式头发高光

卡通渲染中的头发个别有三种做法。

  1. 高级一点的Kaijiaya来做出各向异性成果(劣势:不太容易管制形态,下方链接内有代码实现)。

羽扇轩轩:COS_NPR非实在渲染_头发_ABigDeal的博客-CSDN博客

  1. 利用Matcap伎俩实现(基于视角的,和灯光L无关,下方链接内有代码实现)。

Hugh86:Unity NPR之日式卡通渲染(根底篇)

  1. 还有米哈游最早在《崩坏3》中钻研进去的流动式高光,集体喜爱这种做法。原理大略是把灯光方向投影到XY立体,在XY立体内依照Blinphong形式计算高光并联合高光Mask贴图打造天使环形态,具体实现看如下代码,二次元成果更浓重,更容易管制,更合乎画师的要求,制作出的成果。
    https://www.youku.com/video/X...
float4 uv0 = i.uv0;float3 L = UnityWorldSpaceLightDir(i.positionWS);float3 V = UnityWorldSpaceViewDir(i.positionWS);float3 H = normalize(L + V);float3 N = normalize(i.normalWS);float3 NV = mul(UNITY_MATRIX_V, N);//顶点normal去做float3 HV = mul(UNITY_MATRIX_V, H);float NdotH = dot(normalize(NV.xz), normalize(HV.xz));NdotH = pow(NdotH, 6) * _LightWidth;//6管制高光锐利水平,能够替换为属性NdotH = pow(NdotH, 1 / _LightLength);//_LightLength管制高光长度float lightFeather = _LightFeather * NdotH;float lightStepMax = saturate(1 - NdotH + lightFeather);float lightStepMin = saturate(1 - NdotH - lightFeather);float3 lightColor_H = smoothstep(lightStepMin, lightStepMax, clamp(lightMap.r, 0, 0.99)) * _LightColor_H.rgb;float3 lightColor_L = smoothstep(_LightThreshold, 1, lightMap.r) * _LightColor_L.rgb;float4 specularColor = (lightColor_H + lightColor_L) * (1 - lightMap.b) * lerp(1, _LightIntShadow, shadowStep);return specularColo


上述代码中Lightm.r和.b通道图

七、相机Fov描边修改

网络上根本也就三种描边:在物体空间的、基于相机空间的、还有为了实现不随屏幕变大变小的NDC裁剪空间的。但根本都没有讲述Fov这个因素,在二次元卡通渲染中,很多大招镜头特效,动画人员在做动作的时候并不会扭转相机的间隔,而是间接扭转相机的Fov,很可能从微小的广角60度间接变为18度等等,这样上述三种描边将全副泡汤,因为并没有思考Fov影响。

这里先标记下惯例的三种空间的描边代码,假如法线数据曾经通过修改,没有断边,不晓得如何修改的,参考以下链接:

Jason Ma:【Job/Toon Shading Workflow】主动生成硬外表模型Outline Normal

如下代码中没有退出解决顶点色对于描边的影响,能够用顶点色管制描边粗细和ZOffset,简略不写了。

基于物体空间的(劣势:因为批改的是物体空间顶点的缩放,所以有时候须要波及到一些须要深度Depth的前期成果的时候,可能放弃正确,因为深度Depth须要的是物体的世界空间地位),在这里退出方向改正因子:

v2f o;float3 fixedVerterxNormal = v.tangent.xyz;//这里能够写入切线空间或顶点色都行float3 dir = normalize(v.vertex.xyz);float3 dir2 = fixedVerterxNormal;float D = dot(dir,dir2);dir = dir * sign(D);dir = dir * _Factor + dir2 * (1 - _Factor);v.vertex.xyz += dir * _Outline*0.001;o.pos = UnityObjectToClipPos(v.vertex);

基于相机空间的(个别卡通渲染的惯例做法,能够保障物体在不等比缩放状况下仍然正确):

v2f o;float3 fixedVerterxNormal = v.tangent;float3 viewSpaceNormal = mul(UNITY_MATRIX_IT_MV, fixedVerterxNormal);viewSpaceNormal.z = 0.001;//拍扁,解决凹面穿帮问题产生正色float4 viewSpacePos = mul(UNITY_MATRIX_MV, v.vertex);float dis2Cam = length(viewSpacePos.xyz);float width = _OutlineWidth * dis2Cam;viewSpacePos.xy += normalize(viewSpaceNormal).xy * width * dis2Cam;o.pos = mul(UNITY_MATRIX_P, viewSpacePos);

基于NDC裁剪空间的(能够做到描边粗细恒定,然而相机很远的时候看上去全是黑点,个别都做Clamp改进):

v2f o;float3 fixedVerterxNormal = v.tangent;float4 pos = UnityObjectToClipPos(v.vertex);float ScaleX = abs(_ScreenParams.x / _ScreenParams.y);float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, fixedVerterxNormal);float3 ndcNormal = normalize(TransformViewToProjection(viewNormal.xyz)) * clamp(pos.w, 0, 1);//clamp(0,1)加上后当相机拉远后会削弱看上去全是彩色描边显著问题,也能够不加float2 offset = 0.01 * _OutlineWidth * ndcNormal.xy;offset.x /= ScaleX;//为了解决屏幕长宽比引起的高低和左右的宽度差别pos.xy += offset;o.vertex = pos;


成果根本差不多

接下来就开始摸索让描边反对Fov的影响,首先了解以下两个知识点:

获取相机Fov:因为咱们的相机是一个对称的视见体,所以投影矩阵 如下(基于OpenGL):

所以投影矩阵的第二行第二列就是Fov一半角度的tan的倒数。如果求Fov即要求arctan须要耗费不小的指令数,为了挪动端性能思考,这里间接用tan(Fov/2)来作为影响因子:float fov = 1.0 / unity_CameraProjection[1].y。

获取与相机的间隔:

float3 positionVS = mul(UNITY_MATRIX_MV, input.positionOS).xyz;float viewDepth = abs(positionVS.z);

通过上述两个操作,就能够将间隔和Fov因子同时思考进去了,而后在视空间解决描边即可,自己改进后的代码:

v2f o;float3 fixedVerterxNormal = v.tangent;float4 viewSpacePos = mul(UNITY_MATRIX_MV, v.vertex);float4 vert = viewPos / viewPos.w;float s = -(viewPos.z / unity_CameraProjection[1].y);float power = pow(s, 0.5);float3 viewSpaceNormal = mul(UNITY_MATRIX_IT_MV, fixedVerterxNormal);viewSpaceNormal.z = 0.01;viewSpaceNormal = normalize(viewSpaceNormal);float width = power*_OutlineWidth;vert.xy += viewSpaceNormal.xy *width;vert = mul(UNITY_MATRIX_P, vert);o.vertex = vert;

八、透视成果改正

咱们参考《原神》的编队界面,当同屏幕呈现多个角色时,处于相机外侧的角色会呈现显著的变形,即使相机在Fov 40度状况下,也会很显著。我置信读者必定想到了说要用正交投影,然而正交投影下,角色会齐全失去透视关系,尤其留神角色的鞋会发现后边的跑到前边,因为透视关系失落了,而这并不是美术想要的,美术还是心愿有透视关系。说白了也就是美术心愿站在外侧的角色成果也能和屏幕两头的那个一样,没有呈现透视的影响。参考了如下文章。

《bluerose:在Ue4中实现对二次元模型进行透视校对》


《原神》多个角色站在一起,外侧的角色成果毫无透视造成的畸形

通过深度思考,失去了本人的一套实现思路,将透视矩阵的前两行的X和Y偏移值改为固定值,可能就是因为这两个值和Fov透视有关系,才导致的近大远小的后果,才导致外侧的角色看起来透视显著的起因。

Unity中代码实现:

half _ShiftX;//能够C#传入进去,在X方向上的偏移,由美术调节half _ShiftY;//能够C#传入进去,在Y方向上的偏移,由美术调节v2f vert (appdata v){    v2f o;    float4 positionVS = mul(UNITY_MATRIX_MV, v.vertex);    float4x4 PMatrix = UNITY_MATRIX_P;    PMatrix[0][9] = _ShiftX;    PMatrix[1][10] = _ShiftY;    o.pos = mul(PMatrix, positionVS);}

九、动画摄影前期

这个概念很大,蕴含很多方面,这样的技术文章绝对较少,找到了两篇参考,讲的相当不错,放上如下链接:

流朔:【Unity URP】一次对卡通渲染仿动画摄影的摸索

公主癌菜小干:【项目分析笔记】卡通感格调的渲染办法和思考

第二篇文章中讲了两点,Flare和Parama等成果。其实上述提到的2部分Bloom成果也能够属于这个畛域。除了这两个以外,文章2也提到了比拟显著阐明,如果你采纳了Unity自带的PPSV中的ACES色调映射,会让饱和度升高,尤其对于高超度的物体,解决形式魔改PPSV Package中的ACES改正参数,说白了相当于扭转了曲线的歪斜,使之尽可能的保留饱和度。

ACES革新Unity默认Post:

对于Flare和Parama找机会再实际(其实依照上述链接应该好实现,用上Post前期自带的暗角Vignette增强画面比照,而后再加个自定义前期画张黑白遮罩图,左上到右下有个突变模仿光感成果,成果也就差不多了)。

十、追加

这些二次元独有的成果如果在一款高质量卡通游戏中不存在,总感觉会短少点什么。 以上的实现思路也只是集体的实现,欢送留言探讨。以下放上各种技术对应的参考链接,浏览后了解会更加粗浅。


Referrence:

  1. MinGQ1:卡通渲染描边
  2. bluerose:在Ue4中实现对二次元模型进行透视校对
  3. Cutano:Unity URP Shader 与 HLSL 自学笔记六 等宽屏幕空间边缘光
  4. 2173:【03】从零开始的卡通渲染-着色篇2
  5. 流朔:【Unity URP】以Render Feature实现卡通渲染中的刘海投影
  6. 雪涛:卡通脸部暗影贴图生成 渲染原理
  7. 公主癌菜小干:【项目分析笔记】卡通感格调的渲染办法和思考

这是侑虎科技第1172篇文章,感激作者一滴血的记忆供稿。欢送转发分享,未经作者受权请勿转载。如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/...

再次感激一滴血的记忆的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)