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 步骤概要
- 创建 Position 节点(Space: World)和 Camera 节点
- 各自 Split,提取 X 和 Z 分量
- Subtract 得到 XZ 平面的偏移向量,Combine 为 Vector2
- Length 计算水平距离 d
- Multiply (d × d) 得到 d²
- Divide (d² ÷ 2R),R 来自 Property: _CurvatureRadius
- 结果 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 Batcher | URP 内建支持,合并相同 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 就能撑起整个动态森林的氛围基础。
核心要记住的:
- 球面化在 World Space 施加,先转世界坐标再偏移
- 用 smoothstep 做近处零弯曲的衰减,避免脚下地面"融化"
- 风场在 Object Space 施加,先风场再转世界坐标
- 顶点色蒙版让美术精确控制哪些部位摆动、摆多少
- 性能瓶颈在植被数量,不在球面化——优先优化 LOD 和 Instancing
附录:参数速查
| 参数 | 类型 | 推荐值 | 说明 |
|---|---|---|---|
_CurvatureRadius | Float | 2000 ~ 8000 | 球面半径,越小弯曲越强 |
_FalloffStart | Float | 30 ~ 80 | 开始弯曲的水平距离 |
_FalloffEnd | Float | 200 ~ 500 | 完全弯曲的水平距离 |
_WindStrength | Float | 0.1 ~ 0.5 | 风场强度,过大则植被"飘" |
_WindSpeed | Float | 0.5 ~ 2.0 | 风场速度 |
_WindFrequency | Float | 0.5 ~ 2.0 | 空间频率,影响波纹密度 |

555

被折叠的 条评论
为什么被折叠?



