大数跨境
0
0

Unity技术美术日记——基于移动平台仿艾尔登法环的低成本岩浆

Unity技术美术日记——基于移动平台仿艾尔登法环的低成本岩浆 游戏开发技术教程
2025-12-10
7
导读:主美突然说想要一个艾尔登法环的岩浆效果,我第一反应就是“打咩!”,我们一个卡性能很紧的手游要上老头环的美术效果简直是天方夜谭。后来仔细看了下老头环的岩浆效果后发现,也不是不能做( ﹁ ﹁ )

0. 前言

项目初期接到“复刻《艾尔登法环》岩浆效果”的需求时,团队第一反应是性能风险极高——作为一款卡性能的手游,直接移植主机级美术效果并不现实。但深入分析原作后发现,其核心视觉逻辑具备移动端落地可行性。

先看《艾尔登法环》岩浆效果特征:

再看本文在手游中实现的效果(2025年3月24日更新版,引入GersterWave算法优化):

视频演示链接

该方案在保留关键视觉特性前提下,针对移动平台进行了合理取舍与轻量化重构。

以下将从视觉特征拆解出发,逐步说明Shader实现逻辑。

1. 特性拆分

  1. 存在明显色阶区分,且主色区域随时间动态流动、交替位置;
  2. UV坐标呈现规律性扭曲,符合FlowMap原理;
  3. 与邻接模型(如岩石、建筑)接触边缘具备自然渐变过渡;
  4. 红色基底中嵌入深红点缀,增强体积感与层次感。

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扰动、边缘深度混合与多层噪声驱动的高光交替。实践表明,真正制约效果落地的并非代码复杂度,而是纹理资产的抽象设计能力:需在制图前完成脑内建模,这对美术与程序协同提出更高要求。

【声明】内容源于网络
0
0
游戏开发技术教程
各类跨境出海行业相关资讯
内容 1532
粉丝 0
游戏开发技术教程 各类跨境出海行业相关资讯
总阅读13.1k
粉丝0
内容1.5k