0. 前言
项目初期接到“复刻《艾尔登法环》岩浆效果”的需求时,团队第一反应是性能风险极高——作为一款卡性能的手游,直接移植主机级美术效果并不现实。但深入分析原作后发现,其核心视觉逻辑具备移动端落地可行性。
先看《艾尔登法环》岩浆效果特征:
再看本文在手游中实现的效果(2025年3月24日更新版,引入GersterWave算法优化):
该方案在保留关键视觉特性前提下,针对移动平台进行了合理取舍与轻量化重构。
以下将从视觉特征拆解出发,逐步说明Shader实现逻辑。
1. 特性拆分
- 存在明显色阶区分,且主色区域随时间动态流动、交替位置;
- UV坐标呈现规律性扭曲,符合FlowMap原理;
- 与邻接模型(如岩石、建筑)接触边缘具备自然渐变过渡;
- 红色基底中嵌入深红点缀,增强体积感与层次感。
2. Shader实现
2.1 岩浆高光(黄色区域)
采用双纹理采样策略:以_MainTex的R通道存储基础岩浆纹理,G通道生成DistortMap实现可控UV扰动:
//MainTex采样
half4 mainTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv.xy);
half2 DistortMap = half2(mainTex.g, 1 - mainTex.g);
half2 Distort = DistortMap * _FlowInt;
//Noise采样,扭曲岩浆贴图
half2 warpUV = i.uv.xy - Distort + _Time.x * _FlowSpeed;
half4 NoiseMap = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, warpUV);
half ramp = NoiseMap.r * NoiseMap.r;
该设计兼顾可调性与直观性:灰度值越接近127,扰动强度越低,便于美术精准控制流动节奏。Perlin Noise经色阶压缩,避免过度扭曲影响性能与观感。
高光纹理由Stable Diffusion基于定制素材生成,反复迭代四通道合成图以匹配目标动态表现。
2.2 岩浆基底与次级颜色
在warpUV基础上叠加TillingOffset参数,二次采样_MainTex构建次级底色(深红色),增强层次深度:
//岩浆次级底色(可根据性能需求裁剪)
half2 subUV = warpUV * _SubLavaTillingOffset.xy + _SubLavaTillingOffset.zw;
half4 subMap = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, subUV);
//岩浆基础颜色(除高光)
half4 LavaBase = step(subMap.r, _SubLavaRange);
LavaBase = saturate(LavaBase * _BaseColor * _SubLavaColorInt + (1 - LavaBase) * _BaseColor);
2.3 边缘过渡处理
通过屏幕空间深度差计算实现与周边模型的柔和衔接:
//深度计算
half2 screenUV = i.projPos.xy / i.projPos.w;
half SceneDepth = LinearEyeDepth(SampleSceneDepth(screenUV), _ZBufferParams);
half Depth = i.pos.w;
half2 Edges = saturate(SceneDepth - Depth);
Edges = smoothstep(saturate(half2(_Alpha, _Slope)), 0, Edges);
2.4 高光动态交替
引入两张Noise图(B/A通道),通过正弦函数控制插值权重,实现高光形态随时间非重复切换:
公式中a控制切换速率,b调节单张Noise持续时长,最终融合至ramp权重:
//计算交替高光的频率和速度mask
half ExtraLava = 2 * _SubFadeSpeed * sin(_Time.y * rcp(_Subduration)) + 0.5;
ExtraLava = saturate(ExtraLava);
//用交替mask和两层noise实现高光交替
ramp *= saturate(NoiseMap.g + NoiseMap.b * ExtraLava + NoiseMap.a * (1 - ExtraLava));
//合并输出
half4 LavaCol = LavaBase + _HighlightColor * ramp;
half a = saturate(_Alpha - Edges.y * _Alpha);
LavaCol.rgb *= a;
LavaCol.a = a;
return LavaCol;
2.5 完整Shader参数定义
Shader "LavaZone"
{
Properties
{
[Header(BaseProperties)]
_MainTex("岩浆图(R:纹理,G:扰动图,B:Noise1,A:Noise2)", 2D) = "white" {}
_BaseColor("岩浆底色", Color) = (0,0,0,1)
[HDR]_HighlightColor("岩浆高光颜色",color) = (1,1,1,1)
_Slope("坡度", range(0,1)) = 1
_Alpha("透明度", range(0,1)) = 1
_FlowInt("扭曲强度", Float) = 0.2
_FlowSpeed("流动速度", Float) = 0.5
[Header(SubLava)]
_SubLavaRange("岩浆次级颜色范围", range(0,1)) = 0.8
_SubLavaColorInt("岩浆次级颜色深度", range(0,1)) = 1
_SubLavaTillingOffset("岩浆次级TillingOffset", vector) = (1,1,0,0)
[Header(SubLava)]
_SubFadeSpeed ("次级岩浆出现速度", range(1,5)) = 2
_Subduration ("次级岩浆持续时间", float) = 1
}
SubShader
{
Tags { "RenderPipeline"="UniversalPipeline" "RenderType"="Transparent" "Queue"="Transparent" }
Pass
{
Name "Forward"
Tags { "LightMode"="UniversalForward" }
Blend SrcAlpha OneMinusSrcAlpha
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
half4 _BaseColor;
half4 _HighlightColor;
half4 _MainTex_ST;
half4 _SubLavaTillingOffset;
half _SubLavaColorInt;
half _SubLavaRange;
half _SubFadeSpeed;
half _Subduration;
half _Slope;
half _Alpha;
half _FlowInt;
half _FlowSpeed;
CBUFFER_END
struct a2v
{
float4 vertex : POSITION;
float4 uv : TEXCOORD0;
float2 lightmapUV : TEXCOORD1;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f
{
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float4 projPos : TEXCOORD2;
};
v2f vert(a2v v)
{
v2f o = (v2f)0;
VertexPositionInputs vertexInput = GetVertexPositionInputs(v.vertex.xyz);
o.pos = vertexInput.positionCS;
o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
o.worldPos = vertexInput.positionWS;
o.projPos = ComputeScreenPos(vertexInput.positionCS);
return o;
}
half4 frag(v2f i) : SV_Target
{
//深度计算
half2 screenUV = i.projPos.xy / i.projPos.w;
half SceneDepth = LinearEyeDepth(SampleSceneDepth(screenUV), _ZBufferParams);
half Depth = i.pos.w;
half2 Edges = saturate(SceneDepth - Depth);
Edges = smoothstep(saturate(half2(_Alpha, _Slope)), 0, Edges);
//MainTex采样
half4 mainTex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv.xy);
half2 DistortMap = half2(mainTex.g, 1 - mainTex.g);
half2 Distort = DistortMap * _FlowInt;
//Noise采样,扭曲岩浆贴图
half2 warpUV = i.uv.xy - Distort + _Time.x * _FlowSpeed;
half4 NoiseMap = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, warpUV);
half ramp = NoiseMap.r * NoiseMap.r;
//岩浆次级底色
half2 subUV = warpUV * _SubLavaTillingOffset.xy + _SubLavaTillingOffset.zw;
half4 subMap = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, subUV);
//岩浆基础颜色(除高光)
half4 LavaBase = step(subMap.r, _SubLavaRange);
LavaBase = saturate(LavaBase * _BaseColor * _SubLavaColorInt + (1 - LavaBase) * _BaseColor);
//计算交替高光的频率和速度mask
half ExtraLava = 2 * _SubFadeSpeed * sin(_Time.y * rcp(_Subduration)) + 0.5;
ExtraLava = saturate(ExtraLava);
//用交替mask和两层noise实现高光交替
ramp *= saturate(NoiseMap.g + NoiseMap.b * ExtraLava + NoiseMap.a * (1 - ExtraLava));
//合并
half4 LavaCol = LavaBase + _HighlightColor * ramp;
half a = saturate(_Alpha - Edges.y * _Alpha);
LavaCol.rgb *= a;
LavaCol.a = a;
return LavaCol;
}
ENDHLSL
}
}
}
3. 结语
本方案未追求对《艾尔登法环》效果的完全复刻,而是聚焦其核心渲染逻辑——动态色阶、可控UV扰动、边缘深度混合与多层噪声驱动的高光交替。实践表明,真正制约效果落地的并非代码复杂度,而是纹理资产的抽象设计能力:需在制图前完成脑内建模,这对美术与程序协同提出更高要求。

