初入三渲二的大门,将这个过程记录下来,供后辈们参考。
文章如下:
【注:三渲二非常喜欢,长期更新,但是不是该专业,所以可能是“年更”,哈哈🙄】
1. 三渲二
<details>
<summary>通用快捷引导</summary>
代码只能作为入门学习,实际生产中还是要靠 可视化编辑器。因此ShaderForge 、 Shader Graph 成为必备!
卡通渲染(常称为:三渲二,NPR(非真实渲染))主要分为:美式、日式。
主要技术包括:描边。可以通过:G-Buffer (几何缓存)中的深度、法线贴图计算。
- 术语:NPR(Non-photorealistic rendering)、RBR(Physically Based Rendering)
- 1. 三渲二
- 2. 初级卡通渲染
- 3. B站视频观察总结
- 4. 开始编写Shader
2. 初级卡通渲染
由于我只有基础的Shader 知识,并没有多的ShaderForge 知识。因此需要一些基础的ShaderForge知识。
2.1. 基本思路
分出亮面、暗面。(1,0) = 法线 dot 光向。
Step节点 + 阈值 分出亮暗面。—— S值
亮面:S值=1,乘亮面颜色。 此时暗面Color = 黑(0,0,0)
暗面:S值=0,Remap 一下。得到暗面 =1.乘 暗面颜色。
亮暗面颜色相加得到完整值。
最终 颜色 Multiply 光源颜色、纹理颜色 即可。 此时,卡通渲染由于是自定义 光照模型,不会受到 其他模型阴影、光线衰减等影响。因此需要添加 Light Attenuation 节点,和最初的1 步骤结果相乘。
需要注意 受到阴影影响。Light Attenuation 值会在阴影处 =0 .而 Step的阈值 相等 ,算通过,因此阈值不要设置为 临界值:0.
3. B站视频观察总结
- 卡通渲染分为多种,我追求的的是“次世代卡通渲染”。(实时渲染CG技术)
- 次世代卡通渲染除开:shader部分,最关键的是 各种需求的:(合成)图。把光照信息、落影容易度、AO(Ambient Occlusion环境光遮蔽)、高光通过 RGB通道 存储起来。
3.1. 卡通渲染需要做什么?
虽然不知道到底怎么做,但一定要明白需要做什么,这是 “无中生有”的第一步。
视频一观察:
- 头发高光
- 暗部颜色
- 受光颜色
- 轮廓光影色
- 阴影影色(和暗部颜色有区别吗?)
- 软硬边(阴影)过度
- 阴影Fade Out(褪去)、Cut Out(裁剪)
米哈游技术文章
此次只针对人物,所以场景就不用分析了。
我们的目标是实现完全动态的光照和阴影,所有材质都对各种光照现象做出正确的反应,包括主光源和区域环境光。这就要求我们不能使用任何在纹理上画死的光照表现。
主要(关键)特性有:
多通道Ramp的材质Shading(遮蔽,绘影)方法
眼睛,头发和其它各向异性材料等特殊材料的处理
PCSS角色软阴影和高品质的勾线。
pcss:一种好看的阴影模式。相关联的有:软阴影。
3.2. 米哈游技术文章分析
分析文章,扩展见识。
3.2.1. 多通道Ramp的Shading方法
我们希望角色的阴影和颜色的变化可以表现出更细腻的插画风格,所以我们使用2D ramp纹理来表示这些细微的变化,其中RGB通道分辨用于描述于不同阴影层的漫射阴影范围。每个层都可以制定不同的颜色,这样就能在明暗变化中做到精细的色彩变化控制。
- 分析:所以我前面看见的哪些 合成纹理 应该就是—— 2D Ramp 纹理。他们用来表示细微变化(漫射、阴影范围)。
对于卡通风格的画面,如果上色只是纯明暗变化,阴影处就会显得比较脏,缺乏表现力,而如果提升暗处的饱和度和色相变化,整体色彩看起来就会比较鲜活。而且通过调整垂直纹理采样坐标,我们可以实现动态的软硬风格转换。 从另一角度来看这种方法还间接表现了皮肤的次表面散射效果。
- 分析:卡通风格画面,阴影的变化并非 纯明暗变化,而需要伴随 色相饱和度的变化。 同时通过 纹理坐标采样(垂直,由于纹理)可以实现软硬风格。无意中达到皮肤此表面散射。
高质量的边缘光。
同样是基于菲涅尔方法,我们有参数来控制它,比如:边缘宽度和平滑度;除了这些全局控制参数之外,我们也使用笔刷纹理来增加一些局部变化。我们定义边缘光既可以来自于方向光源也可以来自于环境贴图,使用方向光我们可以按需求定义边缘光,使用环境贴图,我们可以根据环境光照来获得边缘光以显得更真实,二者都比较有用,可以结合使用。
为避免边边缘光出现在不需要的区域,我们使用AO纹理和shadowmap来频闭掉遮挡区域。我们可以看到,对比图中左边带有边缘光的形状显得效果更突出。
- 分析:卡通风格是需要边缘光。但是边缘光 需要被控制。边缘光使用的是涅斐尔,控制是通过AO 纹理、Shadowmap 精细控制。不过边缘光可以不弄,我没看懂 图中的原理。
卡通风格对于面部一般不会有太多阴影层次的变化,如果我们直接套用之前的ramp方法应用在脸部,效果就会像右侧的图看起来一样不自然,为了改善这种情况我们使用顶点色的一个通道作为mask来控制脸部的上色层的强弱,通过压低漫反射表现来达到想要的卡通效果。
- 分析:卡通风格面部和身体 略有不同。主要压低脸部的漫反射。同时漫反射导致的阴影为软,投影阴影为硬看起来效果更好。
高质量角色软阴影的实现。
如果我们直接使用Unity内置的CSM阴影,在镜头靠近角色的时候阴影品质并不能满足需求,所以我们就为角色单独渲染了一张shadowmap,以确保恒定的阴影品质。为此我们还实现了基于视锥的shadowmap,根据角色的boundingbox和视锥求交集部分,以此作为渲染区域。就可以最大化阴影贴图的使用率,
此外还使用了Variance shadow map以及PCSS来减少阴影瑕疵以及获得自然的软阴影效果。另外,如果要实现正确的透明材质阴影,还需要额外的通道根据材质的透明度来存储阴影强度。我们可以从实例图片中看到半透明的裙子可以投射出自然的阴影。
- 分析:高质量角色软阴影就先不管。
3.2.2. 特殊材料处理
眼睛的处理我们使用了基于物理的折射计算。普通卡通模型处理眼部的做法通常是把眼白留空,瞳孔凹陷下去,这样在侧面的时候也不会鼓出来显得比较自然,然而如果要做眼部近距离特写,这种做法看上去就不能令人信服。使用真实折射算法,眼球本身还是按照球面来做,然后根据视线角度算出折射系数去偏移查找贴图对应点 。
下面对比图显示了有无折射的实际效果, 我们可以看到,如果没有折射效果,眼部侧面看上去较为奇怪。
- 分析:眼睛和头发都属于难点。后面单独分析。眼睛 关键 :焦散计算。 头发关键: 各项异性计算。
头发是卡通渲染角色较为重要且独特的部分。我们想要实现根据光源动态变化的高光和阴影渐变,并且这个实现还应具备直观的所见即所得的色彩调节能力。
和皮肤的材质一样,对于头发的漫反射渲染我们同样使用了Multi-ramp的方法,而镜面反射高光我们则使用了二层高光做叠加,通过组合高低频的高光成分在一起,我们可以得到满意的结果。此外,我们还使用Glossy Map和AO纹理来进一步增强头发的质感。
- 分析:头发渲染上次的各项异性因该是对的。但是缺少 要给Rampa 图控制高光的位置。外加一个发丝通道。
3.2.3. 高品质勾线
使用backface (背面渲染)方法勾线,缺点是折线硬表面会有问题,可以预处理,将这些部分单独提出来处理。
- 分析:或使用基于边缘检测的Sober算子后处理。米哈游通过更多的色相、饱和度、深度、法线 综合计算边缘。
3.3. 剩下两个视频分析
主要更加深入了解 不同贴图的使用。为接下来的实践做准备。
3.3.1. 视频一分析
底色贴图
SSS(Subsurface Scattering)次表面散射图。专门用来做皮肤 透光效果。—— 色系偏向红紫色。
还可以用来修饰阴影的颜色。ILM——篇绿、灰系。包含四个通道。
G通道:一个Shadow map 用来表示落影的容易度。类似于原话术语中的“闭塞”。
A通道:表示内部需要描线的地方。主体使用BackFace 方式描边。
R通道:记录Specular(高光)的强度。
B通道:记录Specular(高光)的区域大小。
卡通渲染要出效果,第一步:落影的计算。正常球体Diffuse 一半黑一半白。
题外:还有一种落影方案。AO贴图。+ 顶点色绘制。
3.3.2. 视频二分析
大佬开场.正常的 卡通渲染有 三张贴图.(底色贴图+光照(光相关)贴图(ILM)+此表面散射贴图(SSS))
着色器没什么,主要是贴图。
案例中只使用了 Base+ILM贴图.
本视频中ILM贴图:
R通道:高光的范围。
G通道:阴影贴图,假的AO贴图。
B通道:高光的强度。
利用不同通道颜色在模型上绘制完后,将贴图导出。
在PS-图像-模式-灰度。转化为灰度贴图。 此时只有一个灰色的通道。
其他两张相同操作。
转化完成后,选择合并通道。将打开的三个窗口中的贴图合并即可。
Shader界面:
主要了解需要哪些参数。
阴影强度、阴影面积受到G通道影响。
参数
4. 开始编写Shader
人物Shader 主要分为:身体、脸部、眼睛、头发。四种Shader。
4.1. 遇到一个大坑—— 阴影(身体)
用来接受阴影 的面片,不论正反面,都会接受到阴影。不会因为你是不渲染而接受不到阴影。
正常情况下,是不会遇到这个问题,因为投影和 漫反射暗面 相互融合。但是,卡通渲染,导致需求漫反射 暗面小。导致 问题出现明显。
—— 基于这个问题,就导致了必须先将阴影融合(不论PBR、NPR)。或者换一种阴影计算方式。
—— 阴影计算,在光源出放置一个摄像机,拍摄ShadowMap。再基于位置是否光照等信息确定是否 采样Shadowmap。 这也就是说:采样过程是封闭的。
—— 这个阴影计算,还是比较复杂的。如果是光追的话,就不会出现这种问题吧。
—— 回过头去看:视频中的“动态投影”并未被实现过,(ShaderGraph)中也一样。
4.1.1. 尝试解决——阴影
- 首先明确本次只关注受光(是否接受到投影)区域。
- 在 光源方向 假设 Camera,对区域渲染。只需要深度信息。
- 目标Shader 中,对深度进行比较。比其小的进行采样…… 这种方式行不通。
4.1.2. 尝试第二种方式。
多次渲染——……也好像不行。 毕竟ShadowMap的机制摆在这里。
先用摄像机渲染
4.1.3. 尝试解决方案三。
结果: 效果还不错。
- 可以缩减:漫反射的暗部区域—— Diffuse暗部。
- 融合:投影阴影。—— 投影的不完美导致最低可以缩减的 漫反射暗部 被限制了最低。不过调到最低 效果还不错。
- 可以融合ILM 图中的G 通道,辅助生成阴影。并通过LN点积值计算是否落影。
注意,阴影采样模块不应褪色。这导致阴影的物体丢失了其“色相”。因该直接链接,不褪色。
4.2. 比较简单—— 高光(身体)
主要考虑三个因素:高光范围、高光位置、高光强度。
高光位置:比较简单就是 BPhone模型(半角向量和视向量的点积的 高次幂)—— 次方计算的底数。
高光范围:分两个部分。
- 漫反射暗部+接受投影 (不包含ILM图中的辅助阴影贴图)
- ILM贴图的 R 通道遮罩。
- 高光强度,需要结合ILM纹理的B通道与一个基础Float值。——次方计算的幂。
结果如下:
4.3. 脸部Shader
之所以不用身体的Shader?
- 脸部基本没有高光区域。
- 脸部的受光要多(阴影要少) 才符合卡通渲染。
4.3.1. 一个注意点—— 暗部
为了缩减脸部的阴影。采用Remap 的方式。 但是由于 对Text 采用Uv 采样的方式获取对应的值,所以此时出现一个 临界值问题。 Uv >1、<0 时都会出错,此时注意把 图片格式修改一下。
也可能是因为如上原因,导致我上次错怪了 Dot 的 “Normalize”模式。
还好我有这部分知识,否则郁闷一下午。所以见识很重要。
4.3.2. 最终决定——自定义阴影采样
卡通渲染的脸部重中之重,因此 采用 自己用纹理渲染的方式渲染你。
由于采用自定义阴影采样的方式,可以轻松解决头发投影的问题,因此漫反射暗部就不用那么麻烦了。
4.3.3. 相机Shader替换渲染技术
unity官方手册有介绍。
一个复合Shader 里面有多个 SUbShader(具有特定标签),该SHader 被设置为替换SHader。
当相机 渲染一个物体时。该物体的Shader 的SubShader的 标签的 值 和替换Shader中的标签值 相同,则判断成立,SubShader 会被替换。
4.3.4. 获取深度纹理
仔细一思考,我不能沿用传统的阴影方式。而是基于深度的的阴影。
主要有两种解决思路:
4.3.4.1. 方案一、 通过Shader 内置的获取深度方式,渲染出深度。—— 推荐,方便
- 通过代码 设置相机为深度相机。
- 编写输出屏幕深度渲染缓冲信息的Shader。
- 通过代码:相机后处理方式,直接输出深度SHader的信息。
- 特别注意摄像机的 边界长度,太长 差距太大,输出一片白色。视野越小越精确。
深度Shader:
Shader "Unlit/NewUnlitShader01"
{
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass{
ZTest Always Cull Off ZWrite Off
CGPROGRAM
// Use shader model 3.0 target, to get nicer looking lighting
#pragma glsl
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#include "unityCG.cginc"
sampler2D _CameraDepthTexture;
struct v2f {
float4 pos : SV_POSITION;
float4 scrPos:TEXCOORD0;
};
//Vertex Shader
v2f vert(appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.scrPos = ComputeScreenPos(o.pos);
return o;
}
//Fragment Shader
float4 frag(v2f i) :COLOR{
float depthValue = 1 - Linear01Depth(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos)).r);
return float4(depthValue, depthValue, depthValue, 1.0f);
}
ENDCG
}
}
}
camera.depthTextureMode = DepthTextureMode.Depth;
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (camera != null)
{
Graphics.Blit(src, dest, r_material);
}
}
4.3.4.2. 方案二、 通过相机渲染替换Shader技术获取—— 为实验,理论可行
- 用于替换的Shader 需要获取到摄像机的距离(ShaderForge有节点)
- 获取摄像机的视野范围。用于归一化。
- 利用相机渲染时,替换Shader的技术,将深度信息输出。
4.3.5. 最终决定
放弃! 渲染出来的质量有点差,效果并不好。而且还非性能,需要多两个摄像机,以及控制摄像机 的移动。
根据观察,大多数unity实时渲染都没有投影。
所以还是直接采用 身体的Shader,稍微调整后还是能够观看的。
4.4. 头发Shader——各项异性
准备采用各向异性达到效果。
4.4.1. 各项异性高光——完美
前提:由于我的模型头发法线反了所以这里给出 球体的图片参考。
- 主要参数:半角向量(视角、光源)、切线方向、副切线方向(叉积运算)
- 主要运算:点积、叉积、ACos、Sin、Power、+SmoothStep
途中的其余参数都是调整参数,经过以前的实验:法线副切线、主切线的权重将影响 结果的 横竖方向。
- 参数参考:K系数:01、P系数:011.
途中球体效果参数:0.28、0、0.65、11.
4.4.2. 完美完成
本次还 法线了 投影和漫反射暗部的最佳融合方式:暗部映射0~1 + Light Atten
4.5. 本次卡通渲染意外
记录一些其他杂七杂八的问题。
4.5.1. 模型导入FBX没有贴图问题
Unity2019.3中,如果默认使用新的Load 方式导入模型。Material 会嵌入到“模型文件之下” ,并不会有 单独的文件夹存放 材质、纹理。甚至会丢失纹理贴图。
此时需要切换为 “Legacy” 的Load 模式。——在模型的Material面板