2022-12-219145次浏览
1评论
30收藏
7点赞
分享
文章分为上下篇,为了更好的阅读感受,请按顺序阅读。上篇链接:日式卡通渲染基础技术(上)
描边在日式卡通渲染中主要是为了将物体的轮廓显示出来,这样可以将物体与背景隔离,形成强烈的对比。
描边按实现方式可以分为以下三种:
基于视点的描边
基于过程几何方法的描边
基于边缘检测的描边
使用视点方向(view point) 和 表面法线(surface normal) 之间的点乘结果得到轮廓线信息。
点乘结果越接近于0,说明这个表面更大可能是在侧向的视角方向,则我们可以将其当作轮廓边缘进行描边。
开销小,只需要一个Pass
效果差(如下图)
基于过程几何方法的描边的基本步骤有以下两步:
①渲染正向表面(剔除背面)
②肉渲染背向表面(剔除正面)
他使用双Pass来进行描边,分为Z-Bias和BackFacing两种。
这个方法是通过在观察空间
,将模型沿z轴移动然后绘制描边层来进行实现
实现
使用两个Pass,一个Pass绘制主体,一个Pass绘制描边。
特点
-双Pass实现
-实现简单
-在某些视角下,存在瑕疵
Back Facing是使用两个Pass实现的,其中一个Pass绘制本体,另一个Pass绘制沿法线向外扩张的描边。
特点:
双Pass实现
解决了Z-Bias在某些视角下描边位置偏移的问题
实现
v.vertex.xyz = v.vertex.xyz + v.normal * _OutlineWidth;
基本思想就是将顶点沿法线方向扩张,然后在其前方绘制物体本体。
注意点
绘制描边时,需要将正面剔除,否则会产生对本体的遮挡。
存在的问题
描边的宽度会随着物体离相机的远近而变化,描边的宽度现在是相对世界空间不变的,这相机拉近后,描边就会变粗。
解决方案:转到NDC空间进行扩张
主要实现方式是将法线转到ndc空间,然后需要注意获得屏幕宽高比,否则描边边缘会出现不均匀的情况。
float4 nearUpperRight = mul(unity_CameraInvProjection, float4(1, 1, UNITY_NEAR_CLIP_VALUE, _ProjectionParams.y));
float
aspect =
abs
(nearUpperRight.y / nearUpperRight.x);
另外还做了一个优化:添加一张描边贴图用于控制描边宽度大小。
具体实现代码如下图:
效果如下图:
主要思想:通过使用边缘检查算子对图像数据进行卷积操作来找出轮廓
-Roberts算子对边缘比较敏感,适合具有陡峭边缘且含噪声少的图像
-Sobel算子对噪声较多的图像处理效果更好
代码中使用的算子是Sobel
算子。
①求出近似梯度值
②根据Edge显示边缘
-使用该种方式得到的结果会存在一些不合适的边缘,主要是因为Sobel算子对边缘的不敏感导致,可以考虑换成别的算子。
-另外使用此种方式也存在一些局限性,例如无法通过参数来微调局部的边缘宽度等。
-可以考虑使用基于深度和法线的边缘检测方法来获得更好的效果。
对于头发的渲染,这里使用的是各向异性的头发渲染着色模型Kajiya-Kay模型。通过使用合适的法线偏移贴图,我们可以达到日式卡通中W型头发高光的效果。
Kajiya-Kay模型是一个非常经典的头发渲染模型,《神秘海域》中头发的实现也是在这一渲染模型的基础上实现的。
实现这个渲染模型有一个基础的前提,就是头发的面的切线方向需要从发根指向发尾。有了上面这个基础,我们可以通过使用偏移贴图对切线进行偏移以获得抖动的高光。
Kajiya-Kay中高光的实现有别于Blinn-Phong中使用半程向量(H)和法线方向(N)的点积(HdotN
)这种计算方式,而使用了半程向量(H)和发尾至发根方向(T)的夹角正弦值(sin(T,H))来进行高光的计算。根据三角函数的公式可以知道,sin(T,H)=dot(H,N)。
具体推导过程如下(参照下方向量图进行推导):
在Kajiya-Kay模型中,我们使用双高光项进行叠加渲染,这是基于日常对于头发的一个观察:
Kajiya-Kay中高光项的计算主要有以下步骤:
1.将高光在发根->发尾方向上进行随机偏移
2.重复偏移高光的方法获得两个高光项
3.对两个高光项进行叠加
这里会使用一张偏移贴图来获得不同位置的高光偏移量,通过高光偏移量与法线相乘后并与切线求和,我们就可以将高光进行偏移。
具体代码如下:
float3 ShiftTangent(float3 T, float3 N, float shift)
{
return normalize(T + N * shift);
}
高光项的计算和Blinn-Phong类似,也是进行一个Pow操作:
float StrandSpecular(float3 T, float3 V, float3 L, float exponent)
{
float3 H = normalize(L + V);
float dotTH = dot(T, H);
float sinTH = sqrt(1 - dotTH * dotTH);
float dirAtten = smoothstep(-1.0, 0.0, dotTH);
return dirAtten * pow(sinTH, exponent);
}
其中需要注意的是dirAtten
这一项,他的作用是当T和H的夹角变化时,控制高光的衰减。他遵循以下规则:
T,H夹角为钝角时,进行衰减
T,H夹角为锐角时,不衰减
接下来是头发光照的函数,需要注意的是,其实公司中的T,传入的是副法线。
fixed4 HairLighting(float3 tangent, float3 normal, float3 lightDir, float3 viewDir, float2 uv)
{
fixed3 shiftColor = tex2D(_HairTex, uv);
float
shiftTex = shiftColor.g;
float
noise = shiftColor.b;
float3 t1 = ShiftTangent(tangent, normal, _PrimaryShift + shiftTex);
float3 t2 = ShiftTangent(tangent, normal, _SecondaryShift + shiftTex);
float3 specular = _SpecularColor1 * StrandSpecular(t1, viewDir, lightDir, _Glossiness1);
specular += _SpecularColor2 * StrandSpecular(t2, viewDir, lightDir, _Glossiness2) * noise;
fixed4 o;
o.rgb = specular;
return
o;
}
通过以上的计算,可以获得下面的效果:
当眼睛被头发遮挡时,在日式卡通渲染中做法是将被头发遮挡的眼睛显示出来,这里的实现原理比较简单,主要使用了模板测试来进行。
在进行眼睛渲染的Pass时,将模板值设置为某一固定值(代码如下):
Stencil {
Ref 1
Comp Always
Pass Replace
Fail Replace
}
如以上的代码,我们在模板测试成功/失败后都会将模板值改为1(当然你也可以指定其它值)。接着在对头发进行渲染的Pass内,对模板值进行比较,让模板值不等于1的片元通过,通过这种方式则可以渲染出头发遮挡下的眼睛。头发的Shader如下:
Stencil {
Ref 1
Comp NotEqual
Pass Keep
Fail Keep
}
如以上的代码,我们在模板测试成功/失败后都会将模板值改为1(当然你也可以指定其它值)。接着在对头发进行渲染的Pass内,对模板值进行比较,让模板值不等于1的片元通过,通过这种方式则可以渲染出头发遮挡下的眼睛。头发的Shader如下:
Stencil {
Ref 1
Comp NotEqual
Pass Keep
Fail Keep
}
具体效果可见UTS2中的人物表现:
本村线是由《罪恶装备》的TA提出来的内轮廓线方案。普通的手绘内轮廓线方案,在进行放大时会出现锯齿:
通过使用本村线的方案,在模型放大后也不会出现走样的问题:
本村线的原则
-内轮廓线与u轴v轴平行
-没有手绘的轮廓线
-需要美术对UV进行拆分、排列
参考:
https://www.bilibili.com/read/cv3347514
https://zhuanlan.zhihu.com/c_1215952152252121088
https://blog.csdn.net/puppet_master/article/details/83759180
https://blog.csdn.net/poem_of_sunshine/article/details/79853066
http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/Scheuermann_HairRendering.pdf
https://learnopengl-cn.readthedocs.io/zh/latest/02%20Lighting/02%20Basic%20Lighting/
评论 (1)