共计 12201 个字符,预计需要花费 31 分钟才能阅读完成。
前言
随着《原神》游戏的流行,国内对于二次元游戏这块儿畛域越来越看重了。二次元我的项目中自身基于日本的卡通动漫而来,所以最初的实质都是为了尽量还原 2D 立绘,而并不像 PBR 谋求物理正确,只有难看,还原立绘,那么就是胜利的。所以说到这里,咱们的指标就是还原立绘。
卡通渲染畛域,其实有一些卡通格调独有的成果,这里就集体对于二次元和日本动漫的了解,收集了一些卡通渲染中独有的成果,以及集体在这块儿爬过的坑记录下来,抛砖引玉,给大家分享下本人的实现思路,如果能帮忙到各位,则甚之好矣(注:本文中的所有代码思路均基于 Built in 内置管线)。
一、眼睛眉毛穿过头发,俗称飘眉
这个也属于惯例成果了,个别做老二次元美术都会要求这个成果,也是动画摄影中很重要的一个评判成果,目前有两种伎俩,基于深度 Depth 和基于 Stencil。
基于深度 Depth 代码(劣势:多一个 Pass,多一个 Draw Call)。
- 先画脸部和头发,都保留默认不通明队列即可 ”Queue” = “Geometry”。
- 再画眉毛(须要两个 Pass),设置队列在脸部和头发之后 ”Queue” = “Geometry+10″。
- 画眉毛的第一次 Pass {ZTest LEqual……..}(画没有被头发挡住的局部)。
- 画眉毛的第二次 Pass {ZTest GEqual……}(画被头发挡住的局部)。
基于 Stencil 代码。
-
先画眉毛,设置眉毛队列 “Queue” = “Geometry-10″,同时设置 Stencil:
Stencil { Ref 2 Comp GEqual Pass Replace Fail Keep }
-
再画脸部和头发,对于头发的 Stencil 设置:
Stencil { Ref 1 Comp Greater Pass Keep Fail Keep }
留神:不做任何设置状况下,Stencil 的默认值是 0。
二、自定义管制 Bloom 区域
这个是比拟一般的公众需要了,很多二次元游戏中只让脸部泛光,但身材偏白的区域还是能够不泛光的,当然这个如果是齐全本人写前期 Bloom 成果会容易很多,咱们这里通过批改 Unity 默认的 PPSV2,将该成果融进去,因为目前很多公司都间接采纳了 Unity 自带的后处理了,的确不便,成果调整也能够。
大略实现思路:
- 失常渲染角色,而后将须要 Bloom 的遮罩区域,通过设置输入色彩的 A 通道 outcolor.a = 黑白区域,保留到 A 通道中,会跟着流水线走到前期所须要用的色彩缓冲区的 RT 中。
- 失常 Post Bloom,批改 Bloom.shader 文件,利用以后屏幕 RT 的 Alpha 值。
三、基于深度的额发投影
这个分游戏我的项目,国内的确也有大部分游戏我的项目间接不让头发投影解决完事。然而这样会少一层投影关系,立体感可能会削弱,不过对于玩家而言也无所谓了。如果采纳 Unity 自带的 Shadowmap,因为 Shadowmap 是依据灯光方向计算出来,很难管制,比方右边额发投下来的影子适合了,左边就可能投到脸上了,因为灯光方向不好把控,无奈做到正好只是整个额发投下来的成果,看上去很薄,很纸片的美妙感觉。
参考文章《【Unity URP】以 Render Feature 实现卡通渲染中的刘海投影》
找到的资源,头发和眼睛在一个网格上,所以眼睛区域也绘制出了投影,真正做的时候必定会离开的
大略思路:
- 画脸部和眼睛(头发以外的其余头部区域 Mesh)通过第一个 Pass,只写入深度。
- 画头发,采纳第 2 个 Pass,画出头发区域遮罩(只保留洁净的头发区域),黑白图。
- 在脸部 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;
#endif
half4 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 图的制作,个别有以下几个思路:
- 通过 Pencil 软件,美术绘制等高线办法绘制
【教程】应用 csp 等高线填充工具制作三渲二面部暗影贴图 - 通过在 Unity 或 UE4 游戏引擎中,本人编写工具来生成
雪涛:卡通脸部暗影贴图生成 渲染原理 - 通过内部工具,无需进入引擎自动化生成,不便好使 (举荐该办法).
橘子猫:如何疾速生成混合卡通光照图
六、闪电式流动式头发高光
卡通渲染中的头发个别有三种做法。
- 高级一点的 Kaijiaya 来做出各向异性成果(劣势:不太容易管制形态,下方链接内有代码实现)。
羽扇轩轩:COS_NPR 非实在渲染_头发_ABigDeal 的博客 -CSDN 博客
- 利用 Matcap 伎俩实现(基于视角的,和灯光 L 无关,下方链接内有代码实现)。
Hugh86:Unity NPR 之日式卡通渲染(根底篇)
- 还有米哈游最早在《崩坏 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:
- MinGQ1:卡通渲染描边
- bluerose:在 Ue4 中实现对二次元模型进行透视校对
- Cutano:Unity URP Shader 与 HLSL 自学笔记六 等宽屏幕空间边缘光
- 2173:【03】从零开始的卡通渲染 - 着色篇 2
- 流朔:【Unity URP】以 Render Feature 实现卡通渲染中的刘海投影
- 雪涛:卡通脸部暗影贴图生成 渲染原理
- 公主癌菜小干:【项目分析笔记】卡通感格调的渲染办法和思考
这是侑虎科技第 1172 篇文章,感激作者一滴血的记忆供稿。欢送转发分享,未经作者受权请勿转载。如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ 群:793972859)
作者主页:https://www.zhihu.com/people/…
再次感激一滴血的记忆的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ 群:793972859)