一、前言
本文基于《鸣潮》角色渲染效果进行技术复现,旨在积累PBR(基于物理的渲染)与NPR(非真实感渲染)结合方案的经验。选择该方向源于团队对逆向技术的研究需求,便于后续回顾与应用。
NPR渲染在多款知名游戏中已有成熟实践,如《罪恶装备》《塞尔达传说》《原神》等。近年来,《碧蓝幻想Relink》《绝区零》《尘白禁区》以及鹰角网络的《终末地》也在角色表现上做出差异化探索。而《鸣潮》作为库洛游戏继《战双》之后的作品,延续了其基于PBR的NPR技术路线,具备较高的研究价值。
本文实现采用前向渲染,虽与《鸣潮》所用的延迟渲染存在差异(例如无法直接使用ShaderModelID区分特效区域),但仍力求在视觉效果上尽可能接近原作。
二、美术资源分析
2.1 模型资源
模型包含4套UV及顶点色、法线信息
2.2 贴图资源
共8个部位,每个部位使用1–4张贴图
三、角色效果还原
3.1 PBR光照模型
采用Unity内置PBR框架,分为直接光与间接光两部分处理。
3.1.1 直接光
- DirectDiffuse:由Albedo和金属度参数计算得出。
- DirectSpecular:基于Cook-Torrance BRDF简化版本,通过近似计算提升性能。
half DirectBRDFSpecular(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS){float3 halfDir = SafeNormalize(float3(lightDirectionWS) + float3(viewDirectionWS));float NoH = saturate(dot(normalWS, halfDir));half LoH = saturate(dot(lightDirectionWS, halfDir));float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f;half LoH2 = LoH * LoH;half specularTerm = brdfData.roughness2 / ((d * d) * max(0.1h, LoH2) * brdfData.normalizationTerm);return specularTerm;}
3.1.2 间接光
GI(全局光照)由LightProbe、Lightmap(烘焙GI)和ReflectionProbe(实时GI)构成。为避免与后续边缘光冲突,去除了菲涅尔项(fresnelTerm)。
half3 NPRGlobalIllumination(BRDFData brdfData, half3 bakedGI, half3 normalWS, half3 viewDirectionWS, half occlusion = 1.0f){half3 reflectVector = reflect(-viewDirectionWS, normalWS);half fresnelTerm = 0;// Pow4(1.0 - NoV);half3 indirectDiffuse = bakedGI * occlusion;half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, brdfData.perceptualRoughness, 1);half3 color = EnvironmentBRDF(brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);return color;}
3.2 Matcap的应用
《鸣潮》的Matcap用于增强金属材质高光表现,其特殊之处在于将Matcap叠加至Albedo上,而非传统方式中的独立高光通道。通过截帧可发现Albedo已包含高光信息。
为防止色彩过饱和,采用降饱和处理:
half4 matcap = SAMPLE_TEXTURE2D(_MatCap, sampler_MatCap, input.matcapUV);albedo += Desaturate(matcap.rgb, 0.1) * step( .80, metallic) * _MatcapStrength;
3.3 阴影
原版使用LUT贴图,以NdotL为X轴、部位ID为Y轴采样阴影值。但实测结果存在偏差,推测因UE引擎后期图像调校(亮度、饱和度等)影响。
LUT贴图示意
3.3.1 阴影区域
通过smoothstep对NdotL进行平滑处理:
float smoothLambert = smoothstep(0.0, _Grey, (NdotL * 0.5 + 0.5) + _Dark);
3.3.2 阴影颜色
- 可使用LUT贴图、顶点色绘制或HSV转换等方式。
- 本文采用基础方案:设定阴影色并与漫反射相乘。
3.4 上下渐变
利用模型UV中预设的垂直通道实现上下渐变,避免使用模型空间Y坐标导致动作变形时渐变错乱。
添加偏紫色渐变后,视觉重心上移,有助于突出角色上半身表现力。
finalColor = lerp(finalColor * lowColor, finalColor, gradingValue);
3.5 边缘光
常见实现方式包括菲涅尔函数、深度卷积与ShaderModelID卷积。
3.5.1 菲涅尔边缘光
通过法线与视角夹角计算菲涅尔效应,并用step或smoothstep控制边缘宽度。缺点是可能在低面数区域出现断裂或内部误触发。
float3 edgeLight(float NdotV, float3 baseColor){float3 fresnel = pow(1 - NdotV, _fresnel);fresnel = step(0.5, fresnel) * _edgeLight * baseColor;return fresnel;}
3.5.2 深度卷积边缘光
适用于前向渲染,需PreDepthBuffer支持,否则需在Transparent队列中处理并解决排序问题。
//Depth Sobelfloat Character_Rim(float2 screenUV, float rimWidth, float rimLightThreshold){float Center = SampleSceneDepth(uv);float dis = LinearEyeDepth(Center, _ZBufferParams);float DistanceAlpha = (1.0 - min(dis / 80, 1.0));float Width = 1.0f * rimWidth * DistanceAlpha;float2 Offset = float2(1 / _ScreenParams.x, 1 / _ScreenParams.y) * Width;float Sobel = sqrt((dX*dX + dY*dY) * 5.0f);Sobel = step(rimLightThreshold * Center, Sobel);return Sobel * DistanceAlpha;}
3.5.3 ShaderModelID卷积
常用于延迟渲染,通过标识对象ID通道替代深度进行边缘检测,原理与深度卷积类似。
3.6 头发高光
又称“天使环”,通过一张高光范围贴图配合反向菲涅尔实现动态高光效果。
float NdotV = step( 0.3, dot(normalWS, viewWS));float4 hmMap = SAMPLE_TEXTURE2D(_Hair_HM_Tex, sampler_Hair_HM_Tex, input.uv);specular += hmMap.r * _SpecularColor.rgb * NdotV;
3.7 刘海阴影
需先渲染头发RT,再采样偏移后的UV生成面部阴影。高级做法可随光源方向动态调整偏移量。
float hairShadow = SAMPLE_TEXTURE2D(_HairShadowRT, sampler_HairShadowRT, positionSS + float2(_FaceShadowOffsetX, _FaceShadowOffsetY)).r;NdotL *= (1 - hairShadow.r);
3.8 脸部SDF阴影
采用SDF图区分脸部明暗区域,两个通道分别控制伦勃朗光与亮暗过渡,细节优于常规方案。
| 脸部SDF图a通道 | 脸部SDF图b通道 |
|---|---|
SDF效果示意图
float NPRFaceSDF(float3 lightDir, float3 frontDir, TEXTURE2D_PARAM(_SDFMap, sampler_SDFMap), float2 uv, float lightSmooth){float3 L = normalize(float3(lightDir.x, 0.0, lightDir.z));float3 F = TransformObjectToWorldDir(frontDir, true);F = float3(F.x, 0, F.z);float lightAtten = 1 - (dot(L, F) * 0.5 + 0.5);float3 up = float3(0,1,0);float3 left = cross(F, up);float flipSign = sign(dot(L, left));float4 sdf = SAMPLE_TEXTURE2D(_SDFMap, sampler_SDFMap, float2(flipSign , 1) * uv);return smoothstep(lightAtten - lightSmooth, lightAtten, sdf.b) * step(lightAtten, sdf.a);}
3.9 眼睛渲染
3.9.1 眼球结构
卡通风格眼睛重点表现瞳孔、虹膜与巩膜区域,前房结构带来的深度感也常被还原。
3.9.2 简化的眼球渲染
通过PBR模型叠加自发光或高光遮罩模拟折射与焦散效果,省略复杂光学计算。
float2 viewL = mul(viewW, (float3x2) worldInverse);float2 offset = height * viewL;offset.y = -offset.y;texcoord -= parallaxScale * offset;
建议在切线空间中计算UV偏移以保证跨模型兼容性。
3.10 描线
采用多Pass法线外扩描边,未使用切线平滑,可能因模型面数较高无需额外处理。
3.10.1 描线宽度
使用顶点色作为描边遮罩,并根据摄像机距离动态调整描线粗细:
//outline width with distancefloat DistanceOutlineWidthFuntion(float3 posWS, float3 cameraPos){float3 nearfar = float3(0.005, 1, 10);float3 weight = float3(0.13, 0.3, 1.5);float dis = distance(cameraPos.xyz, posWS);float disNear = saturate((dis - nearfar.x) / (nearfar.y - nearfar.x));float disFar = saturate((dis - nearfar.y) / (nearfar.z - nearfar.y));return lerp(weight.x, weight.y, disNear) + (weight.z - weight.y) * disFar;}
3.10.2 描线颜色
- 可通过颜色贴图或ID图定义不同区域描边色。
- 《鸣潮》可能使用顶点色区分皮肤与其他部分。
- 也可融合主贴图颜色实现自然过渡。
四、后处理
关键后处理包括抗锯齿、Bloom与Tonemapping。
- 抗锯齿:采用URP内置FXAA。
- Bloom:使用URP Bloom模块。
- Tonemapping:移植自UE引擎算法。

