float NL01 = 0.5*NL+0.5;
float Threshold = step(_LightThreshold,(NL01 + _RampOffset +RampOffsetMask )*ShadowAOMask);
BaseMap*= InnerLineMask;
BaseMap = lerp(BaseMap,BaseMap*LineMap,_LineIntensity);
float3 Diffuse = lerp( lerp( ShadowMap*BaseMap,BaseMap,_DarkIntensity),BaseMap,Threshold);
边缘光可以使用传统的NV做一个Step,但这种做法在某些角度下,会出现大面积的边缘光,比如角色蹲下时的腿部,可以使用一张额外的边缘光Mask来控制哪些区域出现。
裁边边缘光,将法线转到视角空间下,直接取X分量做为边缘光的Mask,只出现在暗部。
/==========================边缘光 ==========================/
float3 N_VS = mul((float3x3)UNITY_MATRIX_V, T);
float3 Rim = step(1-_RimWidth,abs( N_VS.x))*_RimIntensity*BaseMap;
Rim = lerp(Rim,0,Threshold);
Rim = max(0,Rim);
高光
金属除了有BlinPhong高光外,还额外有一个与视角无关的 光源裁边高光,这可以使用NL加一个Step来模拟。
float3 MetallicStepSpecular = step(NL,_MetallicStepSpecularWidth)*_MetallicStepSpecularIntensity*BaseMap;
金属的光源裁边高光:
裁边高光仅出现在皮革上,有的特殊材质上也有裁边高光,但那是单独做的,不在通用材质的考虑范围。
鞋子上的裁边高光:
高光代码:
float3 Specular =0;
Specular = pow(saturate(NH),_SpecularPowerValue)*_SpecularIntensity * SpecularIntensityMask*BaseMap ;
Specular = max(Specular,0);
float LinearMask = pow(LightMap.r, 1 / 2.2);
float Layer = LinearMask * 255;
if(Layer>190)
float3 MetallicStepSpecular = step(NL,_MetallicStepSpecularWidth)*_MetallicStepSpecularIntensity*BaseMap;
MetallicStepSpecular = max(0,MetallicStepSpecular);
Specular += MetallicStepSpecular;
if(Layer<=60 && Layer>0)
float SpecularIntensity = pow(SpecularIntensityMask,1/2.2)*255;
float StepSpecularMask = float(SpecularIntensity<180 && SpecularIntensity>0);
float3 LeatherSpecular = step(1-_LeatherStepSpecularWidth,NV)*_LeatherStepSpecularIntensity*BaseMap * StepSpecularMask;
LeatherSpecular = max(0,LeatherSpecular);
Specular = lerp(Specular, LeatherSpecular,StepSpecularMask);
if(Layer>60 && Layer<190)
float SpecularIntensity = pow(SpecularIntensityMask,1/2.2)*255;
float StepSpecularMask = float(SpecularIntensity<128 && SpecularIntensity>0);
float3 LeatherSpecular = step(1-_LeatherStepSpecularWidth,NV)*_LeatherStepSpecularIntensity*BaseMap * StepSpecularMask;
LeatherSpecular = max(0,LeatherSpecular);
Specular = lerp(Specular, LeatherSpecular,StepSpecularMask)
;
头发
头发也分明暗部,头发高光仅显示在亮部,暗部的高光可以使用一个数值来调整:
float NL01 = 0.5*NL+0.5;
float Threshold = step(_LightThreshold,(NL01 + _RampOffset +RampOffsetMask )*ShadowAOMask);
BaseMap*= InnerLineMask;
BaseMap = lerp(BaseMap,BaseMap*LineMap,_LineIntensity);
float3 Diffuse = lerp( lerp( ShadowMap*BaseMap,BaseMap,_DarkIntensity),BaseMap,Threshold);
float3 Specular =0;
Specular = SpecularIntensityMask*BaseMap*lerp(_HairSpecularDarkIntensity,_HairSpecularBrightIntensity,Threshold);
FinalColor = Diffuse + Specular;
头发 裁边漫反射与高光:
罪恶装备中,脸部和头发使用同一个材质,并用VertexColor.g通道来区分,暗部有的模型用贴图控制,有的直接用颜色,因此代码中考虑到了这种情况。
if (VertexColor.g<0.9)
NL = dot(T.xz,L.xz);
float halfLambert = 0.5*NL+0.5;
float Threshold = step(_LightThreshold,(halfLambert + _RampOffset +0 )ShadowAOMask);
float3 FaceShadowSide = 0;
#ifdef FACESHADOWMODE_TEX
FaceShadowSide = ShadowMapBaseMap;
#endif
#ifdef FACESHADOWMODE_COLOR
FaceShadowSide = BaseMap*_FaceShadowColor;
#endif
float3 Diffuse = lerp( lerp( FaceShadowSide ,BaseMap,_DarkIntensity),BaseMap,Threshold);
return Diffuse.xyzz;
贴花
罪恶装备的贴花,使用了单独的Mesh。贴画中可以画一些图案,而不规则的图案是会产生锯齿,因此罪恶装备的贴花使用了单独的Mesh 与 额外的贴花贴图,已减少锯齿。
贴花贴图:
代码中使用clip裁剪掉没有图案的像素,也可以使用透明混合的方式。
clip (0.1-DecalMap.r );
Decal = DecalMap;
自发光
罪恶装备模型的自发光也是做了单独的Mesh。
描边
传统的按法线基础模型,会导致硬边描出的线产生“破裂”的效果,使用平滑后的法线可以有效的避免这个问题。罪恶装备处理过的法线存储在tangent通道,用顶点色的alpha值控制描边的粗细,比如鼻子眼睛出不需要描边,这里的顶底色Alpha通道直接填黑;比如头发上的顶点Alpha通道还有个过渡处理,可以实现描边从上倒下的粗细渐变。
描边代码:
v2f vert(appdata v)
v2f o;
v.vertex.xyz += v.tangent.xyz *_OulineScale*0.01*v.vertexColor.a;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
float4 frag(v2f i) : SV_Target
return _OutlineColor;
描边Pass仅显示背面,如果去掉模型正常显示,那么效果就是一个纯黑,为了方便观察,将模型显示为纯白:
碧蓝幻想
碧蓝幻想与罪恶装备都是同一个公司ARC(Arc System Works) 开发,大体做法基本一致,只是有一些改进,因此相同的部分不再重复说明。
碧蓝幻想贴图与模型信息:
需要注意的点:
UV垂直水平展开,与罪恶装备一致,有“本村线”
法线N是编辑平滑后的法线,可直接进行光照计算与平滑描边
3.暗部颜色贴图是直接画出来的,而不是像罪恶装备那样需要,用Base乘上Tint色
4.ShadowAO(VertexColor.b)是常暗区域
5.Ramp偏移值(LightMap.g )用来控制 在特定的光照角度下,那些区域更易感光,即更容易处于亮部,越不易感光的区域值越小,反之则越大。
6.Decal存储着磨损线条,可以用一个Lerp来控制磨损的程度
7.材质的分层信息存储在Base.a,总共有11个层,每个层都有属于自己的特性,特性比较多
相比于罪恶装备多了一张BlinPhong的指数参数贴图(LightMap.r),有这张图会使光泽度更细腻,但这更多地是一种美术风格上的选择。
具体的用法是直接乘在BlinPhong的指数里:
float3 Specular = pow(NH,SpecularExp * SpecularExpMask) * SpecularIntensityMask*SpecularIntensity;
材质分层的设计
材质分层设计原则:
固定式材质分层设计
碧蓝幻想相比于罪恶装备有了更多材质分层的概念,而每个层中都有属于自己的特性。碧蓝幻想有11个材质分层,Shader的设计思路如下:
组合式材质分层设计
另外一种材质分成的设计思路是组合式的,即每个层都可以自由地选择所需要的特性。艺术家在使用时,只需在相应的特性上打上勾,那么就能使用该特性,可以为艺术家提供更高的自由度!
碧蓝幻想的材质特性
这两个角色中,包含的特性如图所示,其他英雄中还有一些其他特性,要全部还原需要大量观察与检验,但按照这个思路去开发一套自己的渲染方案是简单明了的。
根据吐血观察与测试,碧蓝幻想的材质分层与特性如下:
//BaseMap.a是材质分层的信息
//0.0 - 0.2 =>基础材质:一个明暗面 无边缘光 无高光Mask无裁边高光 无视角光 无裁边视角光 (可能包含皮肤 布料 金属)
//0.21 - 0.25 =>布料1 :两层明暗面 裁边缘光 无高光Mask无裁边高光 无视角光 无裁边视角光
//0.26 - 0.30 =>布料2 :两层明暗面 裁边缘光 有高光Mask无裁边高光 无视角光 无裁边视角光
//0.31 - 0.46 =>布料3 :两层明暗面 裁边缘光 无高光Mask无裁边高光 无视角光 无裁边视角光
//0.47 - 0.55 =>皮革1 :一层明暗面 无边缘光 有高光Mask无裁边高光 无视角光 无裁边视角光
//0.56 - 0.57 =>皮革2 :两层明暗面 裁边缘光 有高光Mask有裁边高光 无视角光 无裁边视角光
//0.58 - 0.61 =>皮革3 :两层明暗面 裁边缘光 有高光Mask无裁边高光 无视角光 无裁边视角光 (可能包含 皮革 金属)
//0.62 - 0.66 =>金属1 :两层明暗面 裁边缘光 有高光Mask有裁边高光 无视角光 无裁边视角光
//0.67 - 0.71 =>金属2 :两层明暗面 裁边缘光 有高光Mask无裁边高光 无视角光 无裁边视角光
//0.72 - 0.87 =>金属3 :一层明暗面 裁边缘光 有高光Mask无裁边高光 无视角光 无裁边视角光
//0.88 - 1.0 =>金属4 :一层明暗面 裁边缘光 有高光Mask无裁边高光 无视角光 无裁边视角光 (布料 金属 英雄就一层基础材质+金属4材质)
为此只挑一些有差别的特性分析:
1.金属明暗过度。有些金属,会有明暗过渡的特性,这种特性可以用菲尼尔来模拟,只不过这个菲尼尔不是用来点亮边缘,而是加黑边缘,只需要将MetallicFresnelIntensity调成一个0到1之间的小数值,那么就能有明暗过渡的效果。
Diffuse = Diffuse* pow(1-saturate(NV),MetallicFresnelExp)*MetallicFresnelIntensity;
2.裁边光种类丰富,有 StepNV StepNL StepNH,每种特性开放,艺术家自己可选。
实现
考虑到碧蓝幻想特性较多的情况,我使用了,组合式分层材质设计。主要分为三部分
1.通用贴图与参数
2.材质分层
3.特性选择开关
首先,定义出每层所需参数的Struct:
struct NPRData
bool HasTwoSide,HasRim,HasSpecular,HasStepSpecular,HasViewLight;
float SpecularExp,SpecularIntensity,StepSpecular_Intensity,StepSpecular_Exp,StepSpecular_Width,Rim_Intensity,Rim_Exp,Rim_Width,ViewLight_Intensity,ViewLight_Exp,RampOffset2,RimType,StepSpecularType;
float3 BaseMap,ShadowMap;
float ShadowMask, RampOffsetMask, SpecularIntensityMask, SpecularExpMask , HalfLambert, NH, NV,NL;
其次实现出所有特性:
float3 NPRLighting(in NPRData nprData)
float stepValue = 0;
float3 Final =0;
float3 DarkSide = nprData.ShadowMap * _DarkIntensity;
float3 BrightSide = nprData.BaseMap * _BrightIntensity;
if(nprData.HasRim)
float RimType = nprData.RimType;
bool BothSideRim = RimType == 0;
bool OnlyBrightSideRim = RimType == 1;
bool OnlyDarkSideRim = RimType == 2;
float3 Rim = pow(step(1-nprData.Rim_Width, 1 - nprData.NV), nprData.Rim_Exp) * nprData.Rim_Intensity*nprData.ShadowMask;
if(BothSideRim) Final += Rim*nprData.BaseMap;
if(OnlyBrightSideRim) BrightSide += Rim*nprData.BaseMap;
if(OnlyDarkSideRim) DarkSide += Rim*nprData.BaseMap;
if (nprData.HasTwoSide)
if (_LightThreshold < nprData.ShadowMask * (nprData.HalfLambert +
_RampOffset + nprData.RampOffset2 + nprData.RampOffsetMask))
stepValue = 0.5;
if (_LightThreshold < nprData.ShadowMask * (nprData.HalfLambert + _RampOffset + 0 + nprData.RampOffsetMask))
stepValue = 1;
if (nprData.HasViewLight)
float3 ViewLight = saturate( pow(saturate(nprData.NV), nprData.ViewLight_Exp) * nprData.ViewLight_Intensity);
BrightSide *= ViewLight;
DarkSide *= ViewLight;
float3 Diffuse = lerp(DarkSide, BrightSide, saturate(stepValue));
Final+=Diffuse;
float3 FinalSpecular =0;
if(nprData.HasSpecular)
float3 Specular = max(0, pow((nprData.NH), nprData.SpecularExp * nprData.SpecularExpMask) * nprData.SpecularIntensity * nprData.SpecularIntensityMask);
FinalSpecular += Specular*nprData.BaseMap;
if(nprData.HasStepSpecular)
float stepSpecularTypeValue = nprData.StepSpecularType ==0 ? nprData.NH:nprData.NV;
float3 StepSpecular = step(1 - nprData.StepSpecular_Width, pow(stepSpecularTypeValue, nprData.StepSpecular_Exp)) * nprData.StepSpecular_Intensity;
FinalSpecular = max(FinalSpecular, StepSpecular* Diffuse);
Final += FinalSpecular;
return Final;
每一层可以设置NprData参数,选择所需的特性。
//第一层
//0.0 - 0.2 => 基础材质: 一个明暗面 无边缘光 无高光Mask 无边高光 无视角光 (可能包含皮肤 布料 金属)
if(Layer>=0.0 && Layer <=0.2 )
nprData.HasTwoSide = _Layer1_HasTwoSide;
nprData.HasRim = _Layer1_HasRim;
nprData.HasSpecular = _Layer1_HasSpecular;
nprData.HasStepSpecular = _Layer1_HasStepSpecular;
nprData.HasViewLight = _Layer1_HasViewLight;
nprData.RampOffset2 = _Layer1_RampOffset2;
nprData.SpecularExp = _Layer1_SpecularExp;
nprData.SpecularIntensity = _Layer1_SpecularIntensity;
nprData.StepSpecular_Intensity = _Layer1_StepSpecular_Intensity;
nprData.StepSpecular_Exp = _Layer1_StepSpecular_Exp;
nprData.StepSpecular_Width = _Layer1_StepSpecular_Width;
nprData.Rim_Intensity = _Layer1_Rim_Intensity;
nprData.Rim_Exp = _Layer1_Rim_Exp;
nprData.Rim_Width = _Layer1_Rim_Width;
nprData.ViewLight_Intensity = _Layer1_ViewLight_Intensity;
nprData.ViewLight_Exp = _Layer1_ViewLight_Exp;
nprData.RimType = _Layer1_RimType;
nprData.StepSpecularType = _Layer1_StepSpecularType;
float3 Light = NPRLighting(nprData);
return Light.xyzz;
//第二层
//0.21 - 0.25 => 布料1 : 两层明暗面 裁边缘光 无高光Mask 无边高光 无视角光
if(Layer>=0.21 && Layer <= 0.25)
nprData.HasTwoSide = _Layer2_HasTwoSide;
nprData.HasRim = _Layer2_HasRim;
nprData.HasSpecular = _Layer2_HasSpecular;
nprData.HasStepSpecular = _Layer2_HasStepSpecular;
nprData.HasViewLight = _Layer2_HasViewLight;
nprData.RampOffset2 = _Layer2_RampOffset2;
nprData.SpecularExp = _Layer2_SpecularExp;
nprData.SpecularIntensity = _Layer2_SpecularIntensity;
nprData.StepSpecular_Intensity = _Layer2_StepSpecular_Intensity;
nprData.StepSpecular_Exp = _Layer2_StepSpecular_Exp;
nprData.StepSpecular_Width = _Layer2_StepSpecular_Width;
nprData.Rim_Intensity = _Layer2_Rim_Intensity;
nprData.Rim_Exp = _Layer2_Rim_Exp;
nprData.Rim_Width = _Layer2_Rim_Width;
nprData.ViewLight_Intensity = _Layer2_ViewLight_Intensity;
nprData.ViewLight_Exp = _Layer2_ViewLight_Exp;
nprData.RimType = _Layer2_RimType;
nprData.StepSpecularType = _Layer2_StepSpecularType;
float3 Light = NPRLighting(nprData);
return Light.xyzz;
//第N层
另外碧蓝幻想的头发、脸部、描边、实现基本与罪恶装备保持一致,不做过多解析。
完整渲染图:
原神
原神的贴图模型信息:
信息含义:
LightMap.r :高光类型Layer,根据值域选择不同的高光类型(eg:BlinPhong 裁边视角光)
LightMap.g :阴影AO ShadowAOMask
LightMap.b :BlinPhong高光强度Mask SpecularIntensityMask
LightMap.a :Ramp类型Layer,根据值域选择不同的Ramp
VertexColor.g :Ramp偏移值,值越大的区域 越容易"感光"(在一个特定的角度,偏移光照明暗)
VertexColor.a :描边粗细
渲染特性
原神的基本做法还是沿用碧蓝幻想,新增了 漫反射分层 与 高光分层。
部分特性:
1.漫反射分层
漫反射的DarkSide部分,由Base乘Ramp图得到,BrightSide则为Base。根据LightMap.a通道的不同值域,选择Ramp图中的不同层。Ramp共10层,分上下两部分,对应着夜晚与白天。
float SpecularLayerMask = LightMap.r;
float ShadowAOMask = LightMap.g;
float SpecularIntensityMask = LightMap.b;
float LayerMask = LightMap.a;
float RampOffsetMask = VertexColor.g;
float RampPixelY = 0.05;
float RampPixelX = 0.00390625;
float halfLambert = (NL * 0.5 + 0.5 + _RampOffset + RampOffsetMask);
halfLambert = clamp(halfLambert, RampPixelX, 1 - RampPixelX);
float RampIndex =
1;
if (LayerMask >= 0 && LayerMask <= 0.1)
RampIndex = 6;
if (LayerMask >= 0.11 && LayerMask <= 0.33)
RampIndex = 2;
if (LayerMask >= 0.34 && LayerMask <= 0.55)
RampIndex = 3;
if (LayerMask >= 0.56 && LayerMask <= 0.9)
RampIndex = 4;
if (LayerMask >= 0.95 && LayerMask <= 1.0)
RampIndex = _RampIndex;
float PixelInRamp = RampPixelY * (RampIndex * 2 - 1);
ShadowAOMask = 1 - smoothstep(saturate(ShadowAOMask), 0.2, 0.6);
float3 ramp = tex2D(_RampMap, saturate(float2(halfLambert * lerp(0.5, 1.0, ShadowAOMask), PixelInRamp)));
float3 BaseMapShadowed = lerp(BaseMap * ramp, BaseMap, ShadowAOMask);
BaseMapShadowed = lerp(BaseMap, BaseMapShadowed, _ShadowRampLerp);
float IsBrightSide = ShadowAOMask * step(_LightThreshold, halfLambert);
float3 Diffuse = lerp(lerp(BaseMapShadowed, BaseMap * ramp, _RampLerp) * _DarkIntensity,_BrightIntensity * BaseMapShadowed,IsBrightSide * _RampIntensity * 1) * _CharacterIntensity;
2.高光分层
原神的高光分层,是沿用碧蓝幻想的思路,使用LightMap.r通道,来控只各种高光表现。与前面重复的渲染特性就不再赘述。值得注意的点在于,原神的金属使用了一张MatCap图来做 金属的裁边视角光。
float MetalMap = saturate(tex2D(_MetalMap, mul((float3x3)UNITY_MATRIX_V, N).xy * 0.5f + 0.5f ).r);
MetalMap = step(_MetalMapV,MetalMap)*_MetalMapIntensity;
MatCap的本质就是Lut,其作用于Ramp图一致,只不过把NL换成了NV(相机空间下)。这和前面实现的裁边视角光效果一样(对值域进行映射):
float3 Specular = 0;
float3 StepSpecular = 0;
float3 StepSpecular2 = 0;
float LinearMask = pow(LightMap.r, 1 / 2.2);
float SpecularLayer = LinearMask * 255;
if (SpecularLayer > 100 && SpecularLayer < 150)
StepSpecular = step(1 - _StepSpecularWidth, saturate(dot(N, V))) * 1 *_StepSpecularIntensity;
StepSpecular *= BaseMap;
//裁边高光 (StepSpecular2常亮 无视明暗部分)
if (SpecularLayer > 150 && SpecularLayer < 250)
float StepSpecularMask = step(200, pow(SpecularIntensityMask, 1 / 2.2) * 255);
StepSpecular = step(1 - _StepSpecularWidth2, saturate(dot(N, V))) * 1 *_StepSpecularIntensity2;
StepSpecular2 = step(1 - _StepSpecularWidth3 * 5, saturate(dot(N, V))) *StepSpecularMask * _StepSpecularIntensity3;
StepSpecular = lerp(StepSpecular, 0, StepSpecularMask);
StepSpecular2 *= BaseMap;
StepSpecular *= BaseMap;
//BlinPhong高光
if (SpecularLayer >= 250)
Specular = pow(saturate(NH), 1 * _SpecularExp) * SpecularIntensityMask *_SpecularIntensity;
Specular = max(0, Specular);
Specular += MetalMap;
Specular *= BaseMap;
Specular = lerp(StepSpecular, Specular, LinearMask);
Specular = lerp(0, Specular, LinearMask);
Specular = lerp(0, Specular, IsBrightSide) + StepSpecular2;
FinalColor.rgb = Diffuse + Specular;
3.头发高光
原神头发渲染中,高光在亮部出现,也在暗部出现,暗部的其他部分则消失。
为了达到这种效果,除了用明暗分割来控制高光外,还有一个视角Mask也影响。因此计算高光的时候考虑到这种效果:
float SpecularRange = step(1 - _HairSpecularRange, saturate(NH));
float ViewRange = step(1 - _HairSpecularViewRange, saturate(NV));
HairSpecular = SpecularIntensityMask *_HairSpecularIntensity * SpecularRange * ViewRange;
HairSpecular = max(0,
HairSpecular);
头发的漫反射部分和身体上的一致。
4.脸
使用SDF图来表现脸部的明暗过渡,相比于调整法线,使用SDF可以更快速地实现效果。
算法:将灯光方向转到局部坐标,求出变换后的XZ极坐标,去step 脸部SDF图做明暗过渡。
#define InvHalfPi 0.159154943071114
float4 Left = mul(unity_ObjectToWorld,float4(0,0,1,0));
float4 Up = mul(unity_ObjectToWorld,float4(-1,0,0,0));
float4 Forward = mul(unity_ObjectToWorld,float4(0,1,0,0));
float4x4 XYZ = float4x4(Left,Up,Forward,float4(0,0,0,1));
float4 Light = mul(XYZ,float4(-L.xyz,0));
Light.xz = normalize(Light.xz);
float angle = atan2(Light.x,Light.z);
float angle01 = angle*InvHalfPi+0.5;
float angle360 = angle01*360.0;
float value = 0;
if(angle360>=0 && angle360<180)
value = Remap(angle360,0,180,0.01,0.99);
value = Remap(angle360,180,360,0.99,0.01);
float NeedFlip = angle360>180;
float4 FaceSDFMap = tex2D(_FaceSDFMap, lerp(uv,float2(1-uv.x,uv.y),NeedFlip));
float FaceLight = step(value,FaceSDFMap.g)*FaceSDFMask;
float3 Diffuse = lerp(_ShadowColor * BaseColor, BaseColor, FaceLight);
脸部SDF图的生成
SDF (SignedDistanceFunction)有向距离场记录的是,当前点到目标的距离。可以参考下图:
原图中白色表示内部,黑色表示外部。生成的SDF图,小于0.5的部分代表着 内部到方块边界的距离,大于0.5的部分代表着外部到边界的部分。
对生成后的SDF做Step操作,可以实现这种过渡效果。
8ssdet SDF生成算法
使用暴力算法,遍历所有像素点,求出最近距离,是最容易的思路,但是对于像素大小超过256x256以后会非常地慢,暴力算法的时间复杂度是O(n*n)。SDF的一种优化算法是8ssdet(8-points Signed Sequential Euclidean Distance Transform),时间复杂度是O(n)。
1.将图片中的像素看做是0与1,0代表黑色,1代表白色。那么一张6x7像素的图片就可以表示如下:
2.定义如下四个算子 A、B、C、D
3.将内部与外部分别定义为Grid1,Grid2。
4.Grid1与Grid2分别进行以下计算
遍历1:
for(从下到上)
for(从左到右)
运用算子A
for(从右到左)
运用算子B
遍历2:
for(从上到下)
for(从右到左)
运用算子C
for(从左到右)
运用算子D
4.将最终的结果归一化
Grid1计算的过程(二维向量代表着该点到最近边界点的向量):
脸部SDF
脸部为了做出阴影平滑过渡的效果,需要画多个过渡关键帧,将这些过渡关键帧分别生成SDF在进行融合,即可得到最终的脸部SDF图。
将两张SDF融合算法:
private Texture2D SDFBlend(Texture2D sdf1, Texture2D sdf2, int sampletimes)
int WIDTH = sdf1.width;
int HEIGHT = sdf1.height;
Color[] pixels = new Color[WIDTH * HEIGHT];
for (int y = 0; y < HEIGHT; y++)
for (int x = 0; x < WIDTH; x++)
var dis1 = sdf1.GetPixel(x, y);
var dis2 = sdf2.GetPixel(x, y);
var c = SDFLerp(sampletimes, dis1.r, dis2.r);
pixels[y * WIDTH + x] = new Color(c, c, c);
Texture2D outTex = new Texture2D(WIDTH, HEIGHT);
outTex.SetPixels(pixels);
return outTex;
private float SDFLerp(int sampletimes, float dis1, float dis2)
float res = 0f;
if (dis1 < 0.5f && dis2 < 0.5f)
return 1.0f;
if (dis1 >= 0.5f && dis2 >= 0.5f)
return 0f;
for (int i = 0; i < sampletimes; i++)
float lerpValue = (float)i / sampletimes;
res += Mathf.Lerp(dis1, dis2, lerpValue) < 0.5f ? 1 : 0;
return res / sampletimes;
将所有融合和的SDF,取平均值可以得到最终的脸部SDF图。但是会有锯齿,因此需要进行抗锯齿操作。
15张关键过渡关键帧:
最终融合生成的脸部SDF图:
其他
其他的一些生成SDF图的算法,比如MarchingParabolas(步进抛物线)也是挺有趣的算法,时间复杂度是O(n)。
https://prideout.net/blog/distance_fields/#min-erosion
http://cs.brown.edu/people/pfelzens/papers/dt-final.pdf
抗锯齿
1.增加关键帧
经过测试15帧以上效果为佳。
2.模糊SDF图
用半径为1、3、5的均值模糊,依次模糊处理SDF图(其他的模糊算法如高斯模糊、场景模糊,经测试效果不佳)。使用模糊算法后会导致SDF图范围扩大,可以使用Mask将模糊后的SDF图范围框住。
3.Shader中SDF抗锯齿
float GetFaceSDF(in float2 uv, in float Threshold)
float dist = (tex2D(_FaceSDFTex, (uv)).r);
float color = dist;
float2 duv = fwidth(uv);
float dtex = length(duv * _FaceSDFTex_TexelSize.zw);
float pixelDist = (Threshold - color) / _FaceSDFTex_TexelSize.x * 2 / dtex;
return step(pixelDist, 0.5);
以下是是否抗锯齿的对比:
在生成SDF图的时候进行抗锯齿处理 配合Shader中抗锯齿处理,最终可以得到一个比较理想的没有锯齿的效果。
5.后处理边缘光
后处理边缘光分两部分
1.对深度图进行检测,某一个像素与周围深度差值大于一定阀值的则判定为是边缘。
2.这个边缘只作用在人物身上,因此需要单独渲染一张人物Mask来控制。
float pixel = tex2D(NPRMaskTex, i.uv).r;
float2 Op1[] = {float2(-1, 0), float2(1, 0), float2(0, 1), float2(0, -1)};
float4 infoRim = Black;
if (pixel > 0.01)
float far = _ProjectionParams.z;
float depthPixel = far* Linear01Depth(UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, i.screenPos )));
for (int k = 0; k < 4; k++)
float depth = far*Linear01Depth(UNITY_SAMPLE_DEPTH(tex2Dproj(_CameraDepthTexture, i.screenPos + float4(RimPixelOffset*Op1[k]* _CameraDepthTexture_TexelSize.xy,0,0))));
if (abs(depthPixel - depth) > DepthThreshold)
infoRim = RimColor;
break;
FinalColor = MainTex + infoRim;
检测了周围的四个像素来判读深度插值,经过测试只检测左与右两个像素 效果也差不多;使用一个参数RimPixelOffset来控制边缘检测的像素差值,最终表现为边缘光的大小。
人是一根带着生殖器乱跑的芦苇!
战双帕弥什
战双的卡渲基本沿用了罪恶装备,这个基础上引入PBR的光照表现,要做PBR效果必然会使用到Roughness、Metallic、AO等贴图用来控制更细腻的光影表现。既然要混合PBR与卡通渲染,就必然会多出一张PBRMask来控制哪些地方显示PBR。
战双的模型贴图信息:
说明:
1.PBRMixMap
PBRMixMap.r Metallic 金属度 PBRMixMap.g Smoothness 光滑度(1-粗糙度) PBRMixMap.b AO 高光遮罩 PBRMixMap.a PBRMask PBR高光类型Mask
2.LightMap
LightMap.r RampOffset Ramp偏移值 LightMap.g ShadowAO 常暗区域Mask LightMap.b SpecularMask 高光类型Mask(决定是否是PBR)
其他
Ramp 模拟皮肤SSS
法线 结合NormalMap做PBR光影表现
切线 是平滑调整后的法线
顶点颜色 控制表变粗细
脸部 与罪恶装备一致,调整法线来做平滑的明暗过渡
头发 明暗高光
战双并不像原神或者碧蓝幻想那样有着统一的渲染特性,而是每个角色几乎都有自己的渲染特性,这在角色设计上会给艺术家更多的发挥空间,只要画风统一就行了!但由于渲染特性的不统一,导致每个角色基本都需要一个单独的Shader来表现,会导致多人协作不方便。
战双的渲染代码做法参考前文基本都能实现,由于特性过多只实现了部分。
部分代码:
float3 Diffuse = lerp( BaseColor*_DarkIntensity,BaseColor*_BrightIntensity,step( (_RampOffset + RampAdd),NL01) *ShadowAO);
float3 SpecularHair = BaseColor*lerp(_HairDarkIntensity,_HairBrightIntensity,IsBright)*SpecularMask;
//高光 有很多高光类型,这里只实现了两种,GGX 与 BlinPhong高光形变
float3 Specular = Specular_GGX(N,L,H,V,Roughness,F0) * AO*_SpecularIntensity * GGXMask *MatMask;
#ifdef _SPECSHIFT_GGX
StylizedSpecularParam param;
param.BaseColor = BaseColor;
param.Normal = N;
param.Shininess = _Shininess;
param.Gloss = _Gloss;
param.Threshold = _Threshold;
param.dv = T;
param.du = B;
Specular += StylizedSpecularLight_GGX(param,V,L,H,Roughness,Metallic)*_SpecShiftIntensity* GGXMask*MatMask;
#endif
#ifdef _SPECSHIFT_BLINPHONG
StylizedSpecularParam param;
param.BaseColor = BaseColor;
param.Normal = N;
param.Shininess = _Shininess;
param.Gloss = _Gloss;
param.Threshold = _Threshold;
param.dv = T;
param.du = B;
Specular += StylizedSpecularLight_BlinPhong( param, H)*_SpecShiftIntensity* GGXMask*MatMask;
#endif
渲染图
总结
日式卡通渲染漫反射公式:
Diffuse = lerp(DarkSide,BrightSide, step(_LightThreshold,(NL01+RampOffset)*ShadowAO ) );
文章中所有代码与高清渲染图连接:
源代码
最后欢迎大家交流学习
fixed4 col = tex2D(_MainTex,i.uv)*_Brightness;
fixed3 BumpMap = UnpackNormal(tex2D(_Bump, i.uv));
首先从分析半兰伯特模型开始:
半兰伯特模型在之前计算光照的时候被应用,主要是为了解决blinn模型不能计算阴影面的问题,而这个问题的表述在于在计算光照的时候,算法直接将计算结果的负数剪掉,从而在背面其实也就不计算光照。
那么在半兰伯特模型中,采用了先缩放再平移的方式将本该在[-1,1]的受光照范围扩展到了[0,1]。这样的计算就能保...
1. **安装 syslog 服务**:
对于 CentOS/RHEL 用户,可以使用命令 `sudo yum install sysklogd`;对于 Debian/Ubuntu 用户,则需运行 `sudo apt-get install syslog`.
2. **编辑 syslog 配置文件**:通常位于 `/etc/syslog.conf` 中,使用文本编辑器如 vim 或 nano 来打开并修改。
3. **设置日志级别**:
在配置文件中添加或调整日志级别,例如:
*.* @local7
local7.* /var/log/local7.log
这里将所有日志信息发送到名为 `local7` 的本地日志文件中。
4. **重启 syslog 服务**:
使用命令 `sudo service syslog restart` 或 `sudo systemctl restart syslog.service`(对于 systemd 环境)来应用更改。
### 设置用户级审计策略
Linux 支持通过 SELinux、AppArmor 等工具进行更细粒度的审计策略设定。
#### SELinux:
1. **启用 SELinux**:
如果未启用,可以使用 `sudo setenforce 1` 启动 SELinux,并通过 `sudo visudo` 更新 `sudoers` 文件以允许管理员临时启用 SELinux。
2. **编写策略**:
SELinux 策略通常由管理员手动编写,也可以使用自动生成策略脚本来基于已有的包提供基本的安全框架。策略文件位于 `/etc/selinux/policy/` 目录下,包括默认策略(如 `bochs`, `targeted`, `strict` 等)。
3. **配置安全上下文**:
您可以在 `/etc/selinux/config` 文件中配置 SELinux 安全上下文,如 `SELINUX=enforcing` 表示 SELinux 应处于强制模式。
#### AppArmor:
1. **安装 AppArmor**:
对于 Ubuntu 用户,可通过 `sudo apt-get install apparmor` 安装;对 RedHat/CentOS 用户则使用 `sudo yum install apparmor`。
2. **创建或编辑 profiles**:
AppArmor 需要为每个应用或服务创建配置文件(profiles),详细指定其允许的操作。这些文件通常位于 `/etc/apparmor.d/` 目录中。
3. **应用配置**:
将生成的 profile 文件应用于特定的服务或应用,通常通过更新 `/etc/apparmor.d/usr.sbin.nologin` 或类似文件来覆盖默认配置。
### 总结:
配置 Linux 系统的日式审计策略涉及到全局的日志管理和局部的安全策略设定。通过 SELinux 和 AppArmor 等工具,可以进一步增强系统的安全性,监控和限制进程的行为,确保数据的安全性和完整性。建议根据具体的业务需求和安全标准选择合适的审计策略,并定期审查和更新这些策略以适应新的威胁环境。