8. 2.5D光照与阴影
2.5D光照与阴影
光照为什么重要?
想象一下,你去看一场话剧。舞台上的演员、布景,如果没有灯光,观众什么都看不见。但有了灯光,不仅能让观众看清楚,还能通过不同颜色、角度的灯光,营造出白天、夜晚、恐怖、温馨等各种氛围。
游戏中的光照也是同样的道理。光照不只是让东西"看得见",更是营造游戏氛围的重要工具:
- 温暖的橙黄色光:营造傍晚、温馨的感觉
- 冷蓝色光:营造夜晚、神秘的感觉
- 红色光:营造危险、紧张的感觉
- 强烈的阴影:增加立体感和深度感
在2.5D游戏中,光照尤为重要,因为它能让3D场景看起来更有层次感,同时也能强化游戏的视觉风格。
Godot中的光源类型
Godot 4.x 提供了三种主要的3D光源节点,就像舞台上的三种灯具:
DirectionalLight3D:方向光(太阳光)
方向光就像太阳——它没有具体的位置,只有方向。无论你在地图的哪个角落,太阳光都从同一个方向照射过来。
特点:
- 没有位置,只有方向(旋转角度决定光的方向)
- 光线是平行的,不会随距离衰减
- 适合模拟太阳光、月光等自然光源
- 性能消耗较低
常见用途:
- 室外场景的主光源
- 2.5D游戏中最常用的光源
设置建议:
- 旋转X轴约 -45度(从斜上方照射)
- 颜色:白色或淡黄色(白天),淡蓝色(夜晚)
- 能量(Energy):1.0(正常亮度)
OmniLight3D:点光源(灯泡)
点光源就像一个灯泡——它有具体的位置,向四面八方发光,离得越远越暗。
特点:
- 有具体位置
- 向所有方向发光(360度)
- 光线随距离衰减(越远越暗)
- 适合模拟灯泡、火把、魔法光球等
常见用途:
- 室内场景的灯光
- 火把、路灯等道具光源
- 爆炸、魔法特效的光照效果
重要属性:
- Range(范围):光能照到多远
- Attenuation(衰减):光线如何随距离减弱(0=线性,1=平方衰减)
- Energy(能量):光的亮度
SpotLight3D:聚光灯(手电筒)
聚光灯就像手电筒——它有具体位置,只向一个方向发光,形成一个锥形的光束。
特点:
- 有具体位置和方向
- 只照亮一个锥形区域
- 适合模拟手电筒、舞台聚光灯、车灯等
常见用途:
- 手电筒效果
- 舞台灯光
- 监控摄像头的"视野"可视化
重要属性:
- Range(范围):光能照到多远
- Spot Angle(锥角):光锥的角度(越大,照射范围越宽)
- Spot Attenuation(锥形衰减):光锥边缘的柔和程度
2.5D游戏的光照策略
不同类型的2.5D游戏,光照策略也不同。
横版游戏的光照
横版游戏(如《空洞骑士》、《奥日与黑暗森林》)通常使用:
- 一个主方向光:模拟太阳光,从斜上方照射
- 少量点光源:用于特殊区域(洞穴里的火把、魔法光源)
- 环境光:防止阴影区域完全黑暗
推荐设置:
- DirectionalLight3D:旋转 (-45, 0, 0),颜色白色,能量1.0
- 环境光颜色:深灰色(不要完全黑暗)
俯视游戏的光照
俯视游戏(如《塞尔达传说》、《暗黑破坏神》)通常使用:
- 一个主方向光:从正上方或斜上方照射
- 多个点光源:用于室内、地下城等区域
- 较强的环境光:让整个场景都清晰可见
推荐设置:
- DirectionalLight3D:旋转 (-90, 0, 0)(正上方),能量1.0
- 环境光颜色:中灰色
低多边形风格的光照
低多边形(Low Poly)风格游戏通常:
- 关闭阴影:低多边形风格不需要精细的阴影
- 使用平面着色(Flat Shading):让每个面都是纯色
- 使用鲜艳的颜色:通过颜色而不是光照来表现层次
性能优势:关闭阴影可以大幅提升性能,适合手机游戏。
阴影设置
阴影是光照系统中最消耗性能的部分,需要仔细配置。
开启阴影
在 DirectionalLight3D 的检查器中:
Shadow→Enabled:勾选开启阴影
阴影质量
位置:Shadow → Blur
- Hard:硬阴影,边缘清晰,性能最好
- Soft Low:软阴影(低质量),边缘稍微模糊
- Soft Medium:软阴影(中等质量)
- Soft High:软阴影(高质量),边缘非常柔和,性能消耗最大
- Soft Ultra:超高质量软阴影
推荐:
- PC游戏:Soft Medium 或 Soft High
- 手机游戏:Hard 或 Soft Low
阴影距离
位置:DirectionalLight3D → Directional Shadow → Max Distance
这个值决定了多远的物体会产生阴影。值越大,阴影范围越广,但性能消耗越大。
推荐:
- 横版游戏:50-100(玩家视野范围内即可)
- 俯视游戏:100-200
阴影偏移(避免阴影痤疮)
"阴影痤疮"(Shadow Acne)是一种常见的阴影问题:物体表面出现奇怪的条纹或斑点。
这是因为物体的阴影投射到自身上,产生了自我遮挡。
解决方法:调整阴影偏移(Shadow Bias)
位置:Shadow → Bias
- 值太小:出现阴影痤疮(条纹)
- 值太大:阴影和物体分离("彼得潘阴影")
- 推荐值:0.1 到 0.5 之间,根据实际效果调整
调试技巧
出现阴影问题时,先把 Bias 调到 0.5,如果阴影痤疮消失了,再慢慢减小,找到最小的有效值。
环境光(Environment)
环境光(Environment)控制整个场景的"基础光照",就像舞台上的"底光",防止阴影区域完全黑暗。
创建环境
- 在场景树中,选择
WorldEnvironment节点(如果没有,添加一个) - 在检查器中,点击
Environment属性旁边的"新建"按钮 - 这会创建一个
Environment资源
天空盒(Sky)
天空盒就是游戏世界的"背景",就像舞台的背景幕布。
位置:Environment → Background → Mode
- Sky:使用天空盒(可以是纯色、渐变色或全景图)
- Color:纯色背景
- Canvas:使用2D画布作为背景(适合2D游戏)
简单天空盒设置:
- Mode 设为 Sky
- Sky 属性中,创建一个
ProceduralSkyMaterial - 调整天空颜色、地平线颜色、地面颜色
环境光颜色
位置:Environment → Ambient Light
- Source:环境光来源
- Disabled:没有环境光(阴影区域完全黑暗)
- Color:使用纯色环境光
- Sky:从天空盒采样环境光(最真实)
- Color:环境光颜色
- Energy:环境光强度
推荐设置:
- Source:Color
- Color:深灰色(比如 RGB: 50, 50, 60)
- Energy:0.3 到 0.5
雾效(Fog)
雾效可以让远处的物体逐渐消失在雾中,增加神秘感,也可以隐藏地图边界。
位置:Environment → Fog
- Enabled:开启雾效
- Light Color:雾的颜色
- Light Energy:雾的亮度
- Density:雾的浓度(越大越浓)
- Sky Affect:雾对天空的影响程度
雾效的用途
- 隐藏地图边界:让玩家看不到地图的"边缘"
- 营造氛围:白色雾=清晨,灰色雾=阴天,黑色雾=恐怖
- 性能优化:可以减少需要渲染的远处物体
烘焙光照(Lightmap)简介
烘焙光照是一种"预计算"光照的技术。就像提前把阴影"画"在贴图上,游戏运行时直接使用这张贴图,不需要实时计算光照。
优点:
- 性能极好(光照已经预计算好了)
- 可以实现非常高质量的全局光照效果
缺点:
- 光照是静态的,不能动态变化
- 需要额外的烘焙时间
- 移动的物体不能使用烘焙光照
适用场景:
- 静态场景(不会移动的地形、建筑)
- 手机游戏(性能要求高)
- 需要高质量光照但不需要动态光照的游戏
如何使用:
- 给静态物体的
MeshInstance3D添加LightmapGI节点 - 在场景中添加
LightmapGI节点 - 点击"烘焙"(Bake)按钮,等待计算完成
注意
烘焙光照是一个进阶话题,初学者可以先跳过,等熟悉了基本光照后再学习。
性能优化建议
光照是游戏性能的重要影响因素,以下是一些优化建议:
1. 限制动态光源数量
每个动态光源都需要额外的渲染计算。建议:
- 场景中的动态光源不超过 4-8 个
- 远处的光源可以关闭或使用烘焙光照代替
2. 合理设置光源范围
点光源和聚光灯的 Range(范围)越大,影响的物体越多,性能消耗越大。把 Range 设置为刚好够用的值。
3. 使用光照遮罩(Light Mask)
光照遮罩可以让某个光源只影响特定的物体,减少不必要的计算。
位置:光源节点 → Light → Cull Mask
4. 关闭不必要的阴影
不是所有光源都需要产生阴影。只给主光源(通常是方向光)开启阴影,其他光源关闭阴影。
5. 使用 LOD(细节层次)
对于远处的物体,可以使用低精度的光照或完全关闭阴影。
代码示例:动态光源
下面是一个动态点光源的示例,可以用于火把、魔法光球等效果:
using Godot;
/// <summary>
/// 动态火把光源
/// 模拟火焰的闪烁效果
/// </summary>
public partial class TorchLight : OmniLight3D
{
// 基础亮度
[Export] private float _baseEnergy = 1.5f;
// 闪烁幅度(0=不闪烁,0.3=轻微闪烁,1.0=强烈闪烁)
[Export] private float _flickerAmount = 0.2f;
// 闪烁速度
[Export] private float _flickerSpeed = 8.0f;
// 基础颜色(橙黄色,像火焰)
[Export] private Color _baseColor = new Color(1.0f, 0.6f, 0.2f);
private float _time = 0.0f;
public override void _Ready()
{
// 设置初始颜色
LightColor = _baseColor;
LightEnergy = _baseEnergy;
}
public override void _Process(double delta)
{
_time += (float)delta;
// 使用噪声函数模拟不规则的闪烁
// sin 函数产生规律波动,叠加多个频率产生不规则感
float flicker = Mathf.Sin(_time * _flickerSpeed) * 0.5f
+ Mathf.Sin(_time * _flickerSpeed * 1.7f) * 0.3f
+ Mathf.Sin(_time * _flickerSpeed * 2.3f) * 0.2f;
// 应用闪烁效果
LightEnergy = _baseEnergy + flicker * _flickerAmount;
// 轻微改变颜色(让火焰看起来更真实)
float colorShift = flicker * 0.05f;
LightColor = new Color(
_baseColor.R + colorShift,
_baseColor.G - colorShift * 0.5f,
_baseColor.B
);
}
}extends OmniLight3D
## 动态火把光源
## 模拟火焰的闪烁效果
# 基础亮度
@export var base_energy: float = 1.5
# 闪烁幅度(0=不闪烁,0.3=轻微闪烁,1.0=强烈闪烁)
@export var flicker_amount: float = 0.2
# 闪烁速度
@export var flicker_speed: float = 8.0
# 基础颜色(橙黄色,像火焰)
@export var base_color: Color = Color(1.0, 0.6, 0.2)
var _time: float = 0.0
func _ready() -> void:
# 设置初始颜色
light_color = base_color
light_energy = base_energy
func _process(delta: float) -> void:
_time += delta
# 使用多个sin函数叠加,模拟不规则的闪烁
var flicker = sin(_time * flicker_speed) * 0.5 \
+ sin(_time * flicker_speed * 1.7) * 0.3 \
+ sin(_time * flicker_speed * 2.3) * 0.2
# 应用闪烁效果
light_energy = base_energy + flicker * flicker_amount
# 轻微改变颜色(让火焰看起来更真实)
var color_shift = flicker * 0.05
light_color = Color(
base_color.r + color_shift,
base_color.g - color_shift * 0.5,
base_color.b
)代码示例:昼夜循环
昼夜循环是很多游戏中的重要功能,通过改变方向光的角度和颜色来模拟一天中光照的变化:
using Godot;
/// <summary>
/// 昼夜循环系统
/// 控制太阳的位置和颜色,模拟一天中的光照变化
/// </summary>
public partial class DayNightCycle : Node3D
{
// 一天的时长(秒)
[Export] private float _dayDuration = 120.0f;
// 当前时间(0.0 = 午夜,0.5 = 正午,1.0 = 下一个午夜)
[Export(PropertyHint.Range, "0,1")] private float _timeOfDay = 0.25f;
// 太阳光节点
[Export] private DirectionalLight3D _sunLight;
// 环境节点
[Export] private WorldEnvironment _worldEnvironment;
// 不同时间段的颜色
private readonly Color _dawnColor = new Color(1.0f, 0.6f, 0.3f); // 黎明:橙红色
private readonly Color _noonColor = new Color(1.0f, 1.0f, 0.9f); // 正午:白色
private readonly Color _duskColor = new Color(1.0f, 0.4f, 0.2f); // 黄昏:深橙色
private readonly Color _nightColor = new Color(0.1f, 0.1f, 0.3f); // 夜晚:深蓝色
public override void _Process(double delta)
{
// 推进时间
_timeOfDay += (float)(delta / _dayDuration);
if (_timeOfDay >= 1.0f) _timeOfDay -= 1.0f;
UpdateSunPosition();
UpdateLightColor();
}
private void UpdateSunPosition()
{
if (_sunLight == null) return;
// 太阳绕X轴旋转一圈(360度 = 一天)
// 0.0(午夜)= 180度(在地平线下)
// 0.25(黎明)= 90度(从东方升起)
// 0.5(正午)= 0度(在正上方)
// 0.75(黄昏)= -90度(向西方落下)
float sunAngle = (_timeOfDay - 0.5f) * 360.0f;
_sunLight.RotationDegrees = new Vector3(sunAngle, -30, 0);
// 根据太阳角度调整亮度(夜晚变暗)
float sunHeight = Mathf.Sin(_timeOfDay * Mathf.Pi * 2);
_sunLight.LightEnergy = Mathf.Max(0, sunHeight);
}
private void UpdateLightColor()
{
if (_sunLight == null) return;
Color targetColor;
// 根据时间选择颜色
if (_timeOfDay < 0.25f)
{
// 午夜到黎明
float t = _timeOfDay / 0.25f;
targetColor = _nightColor.Lerp(_dawnColor, t);
}
else if (_timeOfDay < 0.5f)
{
// 黎明到正午
float t = (_timeOfDay - 0.25f) / 0.25f;
targetColor = _dawnColor.Lerp(_noonColor, t);
}
else if (_timeOfDay < 0.75f)
{
// 正午到黄昏
float t = (_timeOfDay - 0.5f) / 0.25f;
targetColor = _noonColor.Lerp(_duskColor, t);
}
else
{
// 黄昏到午夜
float t = (_timeOfDay - 0.75f) / 0.25f;
targetColor = _duskColor.Lerp(_nightColor, t);
}
_sunLight.LightColor = targetColor;
}
/// <summary>
/// 设置时间(0.0=午夜,0.5=正午)
/// </summary>
public void SetTime(float time)
{
_timeOfDay = Mathf.Clamp(time, 0.0f, 1.0f);
}
/// <summary>
/// 获取当前时间(0.0=午夜,0.5=正午)
/// </summary>
public float GetTime() => _timeOfDay;
}extends Node3D
## 昼夜循环系统
## 控制太阳的位置和颜色,模拟一天中的光照变化
# 一天的时长(秒)
@export var day_duration: float = 120.0
# 当前时间(0.0 = 午夜,0.5 = 正午,1.0 = 下一个午夜)
@export_range(0, 1) var time_of_day: float = 0.25
# 太阳光节点
@export var sun_light: DirectionalLight3D
# 环境节点
@export var world_environment: WorldEnvironment
# 不同时间段的颜色
const DAWN_COLOR = Color(1.0, 0.6, 0.3) # 黎明:橙红色
const NOON_COLOR = Color(1.0, 1.0, 0.9) # 正午:白色
const DUSK_COLOR = Color(1.0, 0.4, 0.2) # 黄昏:深橙色
const NIGHT_COLOR = Color(0.1, 0.1, 0.3) # 夜晚:深蓝色
func _process(delta: float) -> void:
# 推进时间
time_of_day += delta / day_duration
if time_of_day >= 1.0:
time_of_day -= 1.0
_update_sun_position()
_update_light_color()
func _update_sun_position() -> void:
if not sun_light:
return
# 太阳绕X轴旋转一圈(360度 = 一天)
var sun_angle = (time_of_day - 0.5) * 360.0
sun_light.rotation_degrees = Vector3(sun_angle, -30, 0)
# 根据太阳角度调整亮度(夜晚变暗)
var sun_height = sin(time_of_day * PI * 2)
sun_light.light_energy = max(0.0, sun_height)
func _update_light_color() -> void:
if not sun_light:
return
var target_color: Color
# 根据时间选择颜色
if time_of_day < 0.25:
# 午夜到黎明
var t = time_of_day / 0.25
target_color = NIGHT_COLOR.lerp(DAWN_COLOR, t)
elif time_of_day < 0.5:
# 黎明到正午
var t = (time_of_day - 0.25) / 0.25
target_color = DAWN_COLOR.lerp(NOON_COLOR, t)
elif time_of_day < 0.75:
# 正午到黄昏
var t = (time_of_day - 0.5) / 0.25
target_color = NOON_COLOR.lerp(DUSK_COLOR, t)
else:
# 黄昏到午夜
var t = (time_of_day - 0.75) / 0.25
target_color = DUSK_COLOR.lerp(NIGHT_COLOR, t)
sun_light.light_color = target_color
## 设置时间(0.0=午夜,0.5=正午)
func set_time(time: float) -> void:
time_of_day = clamp(time, 0.0, 1.0)
## 获取当前时间(0.0=午夜,0.5=正午)
func get_time() -> float:
return time_of_day常见光照问题
场景太暗怎么办?
- 增加方向光的 Energy 值
- 增加环境光(Ambient Light)的 Energy 值
- 检查是否有物体遮挡了光源
阴影出现条纹(阴影痤疮)怎么办?
调整 Shadow Bias 值,通常设置为 0.1 到 0.5 之间。
光照性能太差怎么办?
- 减少动态光源数量
- 关闭不必要的阴影
- 降低阴影质量(从 Soft High 改为 Hard)
- 减小光源的 Range 值
- 考虑使用烘焙光照
室内场景太亮怎么办?
室内场景通常不应该受到室外方向光的影响。可以:
- 使用光照遮罩(Cull Mask)让方向光不影响室内物体
- 在室内场景中单独设置光源
- 使用
LightmapGI烘焙室内光照
小结
在这一章中,我们学习了:
- ✅ 光照的重要性(营造氛围,就像舞台灯光)
- ✅ 三种光源类型(方向光、点光源、聚光灯)
- ✅ 不同2.5D游戏的光照策略(横版、俯视、低多边形)
- ✅ 阴影设置(质量、距离、偏移)
- ✅ 环境光配置(天空盒、环境光颜色、雾效)
- ✅ 烘焙光照简介(预计算光照,性能好)
- ✅ 性能优化建议(减少光源、关闭阴影)
- ✅ 动态火把光源(闪烁效果)
- ✅ 昼夜循环系统(动态改变光照)
光照系统是游戏视觉效果的核心,掌握好光照能让你的游戏看起来更专业、更有氛围。到这里,2.5D游戏基础知识的学习就告一段落了,接下来我们将进入更具体的游戏开发实践!
