大数跨境

鸣潮角色效果分析还原

鸣潮角色效果分析还原 游戏开发技术教程
2025-09-16
388
导读:为什么会选择基于PBR的NPR渲染方案,因为团队预研技术方案的选择了逆向鸣潮的角色效果,做点技术积累,方便以后回顾。

一、前言

本文基于《鸣潮》角色渲染效果进行技术复现,旨在积累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 / 801.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.005110);    float3 weight = float3(0.130.31.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引擎算法。
【声明】内容源于网络
0
0
游戏开发技术教程
各类跨境出海行业相关资讯
内容 1546
粉丝 0
游戏开发技术教程 各类跨境出海行业相关资讯
总阅读18.0k
粉丝0
内容1.5k