Unity Shader 动态森林效果的幕后—— 地面球面化弧度

01

为什么需要球面化?

在开放世界游戏中,地形本质上是一张巨大的平面。但真实的星球表面是弯曲的——当你站在大地上远眺,地平线应当是一条柔和的弧线,而非一条笔直的切缝。

平面地形的远处看起来像一张「无限延伸的纸」,缺乏纵深感与空间包裹感。球面化(World Curvature)正是为此而生:它在 Vertex Shader 中对顶点位置施加一个球面偏移,使远处的地面自然下沉,模拟出星球表面的弧度。

🎮

经典案例:《塞尔达传说:旷野之息》《原神》《巫师3》的远景都使用了类似技术——它们并不真正弯曲整个世界,只是在渲染时给远处的顶点加了一个"下沉"偏移。

核心原理:从平面到球面

球面化的核心思想极其简洁:距离摄像机越远的顶点,越往下(−Y)偏移。这个偏移量随距离的平方增长,恰好近似球面的几何特性。

关键直觉:球面上某点相对于切面的下沉量,与它到切点的弧距平方成正比。这意味着我们不需要真正把地形贴到球上,只需要在 Shader 里根据水平距离计算一个偏移,就能模拟出弧度。

数学推导

假设球体半径为 R,观察者站在球面上某点。水平距离为 d 的位置,其球面下沉量 Δy 可以通过简单的几何关系推导。

设球心为 O,观察者 P 在球面上,Q 为水平距离 d 处的球面点。由几何关系:

Δy = R − R·cos(θ) = R·(1 − cos(d/R))当 d ≪ R 时,泰勒展开 cos(x) ≈ 1 − x²/2:Δy ≈ d² / (2R)这就是我们 Shader 中使用的核心公式

💡

R 的取值:R 越小,弯曲越剧烈。实际项目中 R 通常在 2000–8000 之间。R=3000 时,1000 单位远处的下沉约 167 单位——视觉上刚好产生明显的弧度感而不至于失真。

04

Vertex Shader 实现

在 URP 中,我们可以通过自定义 Shader 或 Shader Graph 来实现球面化。以下是在 Vertex Shader 中的 HLSL 实现:

4.1 核心函数

// 球面化偏移计算
float3 ApplyWorldCurvature(float3 positionWS, float3 cameraPos, float curvatureRadius)
{
    // 计算顶点到相机的水平距离 (XZ平面)
    float2 offset = positionWS.xz - cameraPos.xz;
    float  distance = length(offset);

    // 球面下沉量: Δy = d² / (2R)
    float dropOffset = (distance * distance) / (2.0 * curvatureRadius);

    // 只在 Y 方向施加偏移
    positionWS.y -= dropOffset;

    return positionWS;
}

4.2 在 URP Lit Shader 中集成

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lit.hlsl"
#include "CurvatureLib.hlsl"

// 在 Attributes 中接收 positionOS
struct Attributes
{
    float4 positionOS : POSITION;
    float3 normalOS   : NORMAL;
    float2 uv          : TEXCOORD0;
};

struct Varyings
{
    float4 positionCS : SV_POSITION;
    float2 uv          : TEXCOORD0;
    float3 normalWS    : TEXCOORD1;
};

Varyings Vert(Attributes input)
{
    Varyings output;

    // 1. Object → World
    float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);

    // 2. 应用球面化
    positionWS = ApplyWorldCurvature(
        positionWS,
        _WorldSpaceCameraPos,
        _CurvatureRadius
    );

    // 3. World → Clip
    output.positionCS = TransformWorldToHClip(positionWS);
    output.uv = input.uv;
    output.normalWS = TransformObjectToWorldNormal(input.normalOS);

    return output;
}

4.3 渐进式弯曲

为了在近处保持地形精度、远处才产生弧度,我们可以加入一个距离衰减:

float3 ApplyWorldCurvatureFalloff(
    float3 positionWS,
    float3 cameraPos,
    float  curvatureRadius,
    float  falloffStart,    // 开始弯曲的距离
    float  falloffEnd       // 完全弯曲的距离
) {
    float2 offset = positionWS.xz - cameraPos.xz;
    float  dist = length(offset);

    // 平滑阶梯: 0→1 (falloffStart → falloffEnd)
    float factor = smoothstep(falloffStart, falloffEnd, dist);

    float dropOffset = (dist * dist) / (2.0 * curvatureRadius);
    positionWS.y -= dropOffset * factor;

    return positionWS;
}

05

Shader Graph 搭建

如果你更偏好可视化编程,URP Shader Graph 同样可以实现。以下是节点连接图:

5.1 Shader Graph 步骤概要

  1. 创建 Position 节点(Space: World)和 Camera 节点
  2. 各自 Split,提取 X 和 Z 分量
  3. Subtract 得到 XZ 平面的偏移向量,Combine 为 Vector2
  4. Length 计算水平距离 d
  5. Multiply (d × d) 得到 d²
  6. Divide (d² ÷ 2R),R 来自 Property: _CurvatureRadius
  7. 结果 Subtract 到 Position.Y,输出到 Vertex Position

06

动态森林:植被与风

球面化只是基础。一个有生命力的动态森林还需要:风场摆动、LOD 层级、 impostor 远景树。我们逐一展开。

6.1 风场摆动 Shader

树叶和草在风中的摆动是动态森林的灵魂。核心方法是用 正弦函数 + 顶点颜色蒙版 控制摆动幅度:

// 风场摆动 — 在 Vertex Shader 中调用
float3 ApplyWind(
    float3 positionOS,
    float3 normalOS,
    float4 vertexColor,     // R: 主摆动蒙版, G: 细节摆动蒙版
    float  windStrength,
    float  windSpeed,
    float  windFrequency,
    float  time
) {
    // 主摆动: 整体大波浪
    float mainPhase = dot(positionOS.xz, 0.01) * windFrequency + time * windSpeed;
    float mainWind = sin(mainPhase) * windStrength * vertexColor.r;

    // 细节摆动: 高频微颤
    float detailPhase = dot(positionOS.xz, 0.05) * windFrequency * 3.0 + time * windSpeed * 1.5;
    float detailWind = sin(detailPhase) * windStrength * 0.3 * vertexColor.g;

    // 沿法线方向偏移
    float3 windOffset = normalOS * (mainWind + detailWind);

    return positionOS + windOffset;
}

6.2 完整的植被 Vertex 流程

把球面化和风场组合在一起,完整的植被 Vertex Shader 流程如下:

07

性能优化策略

策略说明适用场景
smoothstep 衰减近处不弯曲,减少不必要的顶点偏移计算所有项目
LOD 切换远处用低模+Impostor,降低 Draw Call 和顶点数大规模森林
GPU Instancing相同材质的树/草实例化绘制,减少状态切换大量重复植被
SRP BatcherURP 内建支持,合并相同 Shader 变体的绘制URP 项目默认开启
Compute Shader 风场将风场计算从 Vertex Shader 移至 Compute Shader百万级草体
Distance Culling超出可视距离的植被直接剔除,不进入渲染管线超远视距

关键参数:球面化本身的性能开销极小——每个顶点只多了一次距离计算、一次平方、一次除法。真正的性能瓶颈在植被数量,而非球面化本身。优先优化 LOD 和 Instancing。

08

完整代码

以下是一个可直接用于 URP 的完整 Shader,集成了球面化、风场和标准光照:

Shader "Custom/CurvedForestLit"
{
    Properties
    {
        // 基础光照
        _BaseMap("Base Map", 2D) = "white" {}
        _BaseColor("Base Color", Color) = (1,1,1,1)

        // 球面化
        _CurvatureRadius("Curvature Radius", Float) = 3000
        _FalloffStart("Falloff Start", Float) = 50
        _FalloffEnd("Falloff End", Float) = 300

        // 风场
        _WindStrength("Wind Strength", Float) = 0.3
        _WindSpeed("Wind Speed", Float) = 1.0
        _WindFrequency("Wind Frequency", Float) = 1.0
    }

    SubShader
    {
        Tags {
            "RenderPipeline" = "UniversalPipeline"
            "RenderType" = "Opaque"
            "Queue" = "Geometry"
        }

        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

        CBUFFER_START(UnityPerMaterial)
            float4 _BaseMap_ST;
            half4  _BaseColor;
            float  _CurvatureRadius;
            float  _FalloffStart;
            float  _FalloffEnd;
            float  _WindStrength;
            float  _WindSpeed;
            float  _WindFrequency;
        CBUFFER_END
        ENDHLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _SHADOWS_SOFT

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float4 tangentOS  : TANGENT;
                float2 uv          : TEXCOORD0;
                float4 color       : COLOR;  // 顶点色: R=主摆动 G=细节摆动
            };

            struct Varyings
            {
                float4 positionCS   : SV_POSITION;
                float2 uv            : TEXCOORD0;
                float3 normalWS      : TEXCOORD1;
                float3 positionWS    : TEXCOORD2;
            };

            // ── 风场 ──
            float3 ApplyWind(float3 posOS, float3 nrmOS, float4 vColor)
            {
                float t = _Time.y;
                float mainP = dot(posOS.xz, 0.01) * _WindFrequency + t * _WindSpeed;
                float mainW = sin(mainP) * _WindStrength * vColor.r;

                float detailP = dot(posOS.xz, 0.05) * _WindFrequency * 3.0 + t * _WindSpeed * 1.5;
                float detailW = sin(detailP) * _WindStrength * 0.3 * vColor.g;

                return posOS + nrmOS * (mainW + detailW);
            }

            // ── 球面化 ──
            float3 ApplyCurvature(float3 posWS)
            {
                float2 off = posWS.xz - _WorldSpaceCameraPos.xz;
                float  d = length(off);
                float  factor = smoothstep(_FalloffStart, _FalloffEnd, d);
                float  drop = (d * d) / (2.0 * _CurvatureRadius);
                posWS.y -= drop * factor;
                return posWS;
            }

            Varyings Vert(Attributes input)
            {
                Varyings o;

                // Step 1: 风场摆动 (Object Space)
                float3 posOS = ApplyWind(input.positionOS.xyz, input.normalOS, input.color);

                // Step 2: Object → World
                float3 posWS = TransformObjectToWorld(posOS);

                // Step 3: 球面化偏移
                posWS = ApplyCurvature(posWS);

                // Step 4: 输出
                o.positionCS  = TransformWorldToHClip(posWS);
                o.positionWS  = posWS;
                o.uv          = TRANSFORM_TEX(input.uv, _BaseMap);
                o.normalWS    = TransformObjectToWorldNormal(input.normalOS);
                return o;
            }

            half4 Frag(Varyings input) : SV_Target
            {
                half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
                half3 ambient = GlossyEnvironmentReflection(input.normalWS, 0, 1.0);

                Light mainLight = GetMainLight();
                half3 diffuse = mainLight.color * saturate(dot(input.normalWS, mainLight.direction));

                half3 color = albedo.rgb * (ambient + diffuse);
                return half4(color, albedo.a);
            }
            ENDHLSL
        }
    }
}

09

总结

球面化弧度效果看似只改了一个 Y 偏移,但它给玩家带来的空间感提升是立竿见影的。配合风场摆动与 LOD 体系,一个看似简单的公式 Δy ≈ d²/2R 就能撑起整个动态森林的氛围基础。

核心要记住的:

  1. 球面化在 World Space 施加,先转世界坐标再偏移
  2. 用 smoothstep 做近处零弯曲的衰减,避免脚下地面"融化"
  3. 风场在 Object Space 施加,先风场再转世界坐标
  4. 顶点色蒙版让美术精确控制哪些部位摆动、摆多少
  5. 性能瓶颈在植被数量,不在球面化——优先优化 LOD 和 Instancing

附录:参数速查

参数类型推荐值说明
_CurvatureRadiusFloat2000 ~ 8000球面半径,越小弯曲越强
_FalloffStartFloat30 ~ 80开始弯曲的水平距离
_FalloffEndFloat200 ~ 500完全弯曲的水平距离
_WindStrengthFloat0.1 ~ 0.5风场强度,过大则植被"飘"
_WindSpeedFloat0.5 ~ 2.0风场速度
_WindFrequencyFloat0.5 ~ 2.0空间频率,影响波纹密度
内容概要:本文围绕可变桨叶四旋翼无人机的规范控制与点对点运动模拟展开,重点研究优化推力分配策略在翻转动作中的应用与性能比较。通过Matlab代码实现,构建了四旋翼动力学模型,并设计了多种控制算法以实现精确的姿态调整与轨迹跟踪。研究对比了不同推力分配方案在执行高机动性翻转动作时的稳定性、能耗效率与响应速度,旨在提升无人机在复杂飞行任务中的动态性能与控制精度。该仿真研究为无人机飞控系统的设计与优化提供了理论依据和技术支持。; 适合人群:具备一定自动控制理论基础和Matlab编程能力,从事无人机控制、飞行器动力学或机器人系统研究的科研人员及研究生。; 使用场景及目标:① 实现四旋翼无人机在三维空间中的精确点对点运动控制;② 对比分析不同推力分配策略在执行翻转等高难度动作时的控制效果与能耗表现,优化飞行性能;③ 为无人机自主飞行、特技飞行及复杂环境下的机动控制提供算法验证平台。; 阅读建议:此资源以Matlab仿真为核心,建议读者结合相关控制理论知识,深入理解代码实现细节,重点关注动力学建模、控制律设计与推力分配模块。在学习过程中,应动手调试参数,复现文中翻转动作的仿真结果,并尝试拓展至其他复杂飞行任务,以加深对无人机控制机理的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值