第24章 无限延伸的穹顶——三维天空系统的实现艺术
第24章 无限延伸的穹顶——三维天空系统的实现艺术
24.1 引言:虚拟世界的苍穹之幕
在三维游戏的世界里,天空不仅仅是背景,它是构建沉浸感的关键要素。从《上古卷轴5:天际》中绚丽多彩的极光,到《荒野大镖客2》中随时间动态变化的云层,再到《微软模拟飞行2020》中基于真实气象数据生成的天空,这些令人惊叹的效果都离不开先进的三维天空技术。天空系统不仅提供了视觉上的深度和空间感,更是游戏氛围、时间系统和天气系统的视觉载体。
在早期三维游戏中,天空通常只是一张静态的背景图片,当玩家移动时,天空会显得不自然。随着图形技术的发展,天空盒技术应运而生,它通过将玩家置于一个纹理立方体的中心,创造了无论玩家如何移动都看似无限远的天空幻觉。这项技术至今仍是绝大多数三维游戏的基础。
然而,现代游戏对天空的要求早已超越了简单的静态天空盒。《塞尔达传说:旷野之息》实现了基于物理的大气散射,创造了随时间自然过渡的昼夜循环;《战神4》中的天空系统与游戏叙事紧密相连,不同领域的天空具有独特的视觉特征;《艾尔登法环》则通过动态天气系统,让天空成为游戏体验的核心部分。
本章将深入探讨三维天空系统的设计与实现,从基础的天空盒技术开始,逐步深入到更高级的天空渲染方法。我们将构建一个完整的、可扩展的天空渲染系统,该系统不仅可以模拟静态天空,还能实现昼夜循环、动态云层和基本天气效果。
24.2 天空渲染技术基础
24.2.1 天空盒:概念与原理
天空盒是一种将玩家置于立方体中心的技术,立方体的每个内表面都贴有纹理,这些纹理共同构成一个无缝的环境背景。由于立方体足够大(在视觉上无限大),且玩家始终处于中心位置,因此无论玩家如何移动或旋转,看到的天空背景都是连贯且不随视角变化的,从而创造出天空无限远的错觉。
从数学角度理解,天空盒的渲染基于一个简单的概念:使用视角方向向量作为纹理坐标。当渲染天空盒时,我们通常禁用深度测试(或设置深度值为最大),然后根据当前视角方向,在立方体贴图上进行采样。这意味着我们不是真正渲染一个巨大的立方体,而是基于当前视角渲染一个始终包围摄像机的环境。
在商业游戏引擎中,天空盒的实现有许多变体。Unity引擎提供了内置的天空盒系统,支持立方体贴图和6面单独纹理两种格式;Unreal Engine则使用动态天空大气组件,实现了基于物理的天空渲染;Source引擎(《半条命2》、《传送门》系列)则使用了较为传统的天空盒技术,但通过多个天空盒层实现了云层和远景的视差效果。
天空盒技术的主要优势包括:
- 性能高效:只需要渲染6个面,且通常使用相对较低的纹理分辨率
- 实现简单:概念直观,易于理解和实现
- 兼容性好:几乎所有图形硬件都支持立方体贴图
- 资源友好:可以重复使用纹理资源,减少内存占用
然而,传统的天空盒技术也有其局限性:
- 静态不变:传统天空盒无法模拟动态变化的天空效果
- 分辨率限制:纹理分辨率限制了天空的视觉质量
- 缺乏体积感:难以表现云层的体积和光照效果
- 大气效果有限:难以实现真实的大气散射和光照效果
24.2.2 天空球与天空穹顶
除了天空盒,游戏开发中还经常使用天空球和天空穹顶技术。天空球使用球体或半球体代替立方体,可以更好地模拟自然天空的曲率。这种技术在飞行模拟和太空游戏中特别常见,如《微软模拟飞行》系列就使用了高度复杂的球面天空渲染系统。
天空穹顶是另一种变体,它通常使用圆顶几何体,特别适合表现室内的穹顶效果或部分天空场景。在《生化奇兵:无限》中,哥伦比亚天空城的背景就使用了精心设计的天空穹顶,营造出漂浮城市的独特氛围。
从技术实现角度看,天空球和天空盒的主要区别在于几何形状和纹理映射方式。天空球通常使用球面坐标或经纬度映射,这可能导致纹理在极点处产生扭曲。为了解决这个问题,现代游戏引擎通常使用立方体贴图到球面的转换,或者在着色器中进行复杂的坐标计算。
以下是三种天空技术的对比:
| 技术类型 | 几何形状 | 纹理映射 | 优点 | 缺点 | 典型应用 |
|---|---|---|---|---|---|
| 天空盒 | 立方体 | 立方体贴图 | 实现简单,性能高 | 边缘可见,缺乏曲率 | 大多数传统3D游戏 |
| 天空球 | 球体/半球 | 球面/经纬度映射 | 自然曲率,无缝 | 极点扭曲,纹理浪费 | 飞行模拟,太空游戏 |
| 天空穹顶 | 圆顶/半球 | 多种映射方式 | 适合特定场景 | 通用性较差 | 室内穹顶,特殊场景 |
24.2.3 立方体贴图技术
立方体贴图是天空盒技术的核心,它是一种特殊类型的纹理,由6个二维纹理面组成,分别对应立方体的正X、负X、正Y、负Y、正Z、负Z方向。在DirectX和OpenGL中,立方体贴图都有原生支持,可以通过特殊的纹理坐标(三维方向向量)进行采样。
创建高质量的立方体贴图需要考虑多个因素:
-
纹理来源:可以是美术绘制的,也可以是从真实环境拍摄的HDR照片,或是通过算法程序生成的。
-
分辨率选择:需要平衡视觉质量和性能。现代游戏通常使用1024x1024或2048x2048的纹理分辨率,每个面独立。
-
无缝处理:立方体贴图的边缘必须无缝连接,否则在视角旋转时会看到明显的接缝。这需要在纹理制作阶段仔细处理。
-
色彩空间:传统天空盒使用LDR(低动态范围)纹理,而现代游戏越来越多地使用HDR(高动态范围)纹理,以支持更真实的光照和后期处理效果。
-
Mipmap链:为立方体贴图生成完整的Mipmap链可以提高渲染质量,特别是当天空盒部分内容在远处时。
在商业游戏开发中,立方体贴图的创建通常是一个多步骤的过程。《神秘海域4》的开发团队分享了他们的天空制作流程:首先在真实世界拍摄HDR全景照片,然后在Photoshop中进行色彩校正和接缝处理,最后在引擎中与程序化云层和大气效果混合。
24.3 天空盒系统的设计与实现
24.3.1 天空盒类架构设计
一个完整的天空盒系统需要管理纹理资源、几何数据、渲染状态和着色器程序。良好的类设计应该遵循单一职责原则,同时提供清晰的接口供游戏引擎的其他部分调用。以下是天空盒类的完整设计:
#include
#include
#include
#include
#include
using namespace DirectX;
class Skybox
{
public:
// 构造函数和析构函数
Skybox();
virtual ~Skybox();
// 初始化天空盒
bool Initialize(
ID3D11Device* device,
ID3D11DeviceContext* deviceContext,
const std::wstring& cubeMapFileName,
float size = 1000.0f);
// 渲染天空盒
void Render(ID3D11DeviceContext* deviceContext, const XMMATRIX& viewMatrix, const XMMATRIX& projectionMatrix);
// 更新天空盒(用于动态效果)
void Update(float deltaTime);
// 资源管理
void Shutdown();
// 设置天空盒大小
void SetSize(float newSize);
// 获取天空盒大小
float GetSize() const;
// 设置天空颜色(用于调试或特效)
void SetTintColor(const XMFLOAT4& color);
// 设置亮度
void SetBrightness(float brightness);
// 获取立方体贴图资源(用于反射等效果)
ID3D11ShaderResourceView* GetCubeMapResource() const;
private:
// 顶点结构
struct SkyboxVertex
{
XMFLOAT3 position;
};
// 常量缓冲区结构
struct SkyboxConstantBuffer
{
XMMATRIX worldViewProjection;
XMFLOAT4 tintColor;
float brightness;
float padding[3]; // 对齐到16字节边界
};
// 私有方法
bool LoadCubeMapTexture(const std::wstring& fileName);
bool CreateCubeGeometry(float size);
bool CreateBuffers(ID3D11Device* device);
bool CreateShaders(ID3D11Device* device);
bool CreateConstantBuffer(ID3D11Device* device);
void CleanupResources();
// 计算天空盒的世界矩阵(始终围绕摄像机)
XMMATRIX CalculateWorldMatrix(const XMFLOAT3& cameraPosition) const;
private:
// Direct3D资源
ID3D11Device* mDevice;
ID3D11DeviceContext* mDeviceContext;
ID3D11Buffer* mVertexBuffer;
ID3D11Buffer* mIndexBuffer;
ID3D11ShaderResourceView* mCubeMapTexture;
ID3D11SamplerState* mSamplerState;
ID3D11Buffer* mConstantBuffer;
ID3D11VertexShader* mVertexShader;
ID3D11PixelShader* mPixelShader;
ID3D11InputLayout* mInputLayout;
ID3D11DepthStencilState* mDepthStencilState;
ID3D11RasterizerState* mRasterizerState;
// 天空盒数据
std::vector<SkyboxVertex> mVertices;
std::vector<UINT> mIndices;
UINT mVertexCount;
UINT mIndexCount;
float mSize;
// 渲染状态
XMFLOAT4 mTintColor;
float mBrightness;
// 着色器字节码(用于输入布局创建)
std::vector<BYTE> mVertexShaderByteCode;
};
24.3.2 立方体贴图加载与创建
加载立方体贴图是天空盒初始化的关键步骤。在DirectX 11中,可以使用DDS(DirectDraw Surface)格式的纹理文件,这种格式原生支持立方体贴图,并包含完整的Mipmap链。如果使用其他格式(如6张单独的图像文件),则需要手动创建立方体贴图资源。
以下是立方体贴图加载的完整实现:
bool Skybox::LoadCubeMapTexture(const std::wstring& fileName)
{
HRESULT result;
// 检查文件扩展名
size_t dotPos = fileName.find_last_of(L".");
if (dotPos == std::wstring::npos)
{
return false;
}
std::wstring extension = fileName.substr(dotPos);
// 使用DDS格式(推荐,支持立方体贴图和Mipmap)
if (_wcsicmp(extension.c_str(), L".dds") == 0)
{
result = DirectX::CreateDDSTextureFromFile(
mDevice,
fileName.c_str(),
nullptr,
&mCubeMapTexture
);
if (FAILED(result))
{
return false;
}
}
else
{
// 对于非DDS格式,我们需要手动创建立方体贴图
// 这里简化处理,实际项目中应实现完整的6面加载逻辑
return false;
}
// 创建纹理采样器状态
D3D11_SAMPLER_DESC samplerDesc;
ZeroMemory(&samplerDesc, sizeof(samplerDesc));
samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
samplerDesc.MipLODBias = 0.0f;
samplerDesc.MaxAnisotropy = 1;
samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
samplerDesc.BorderColor[0] = 0.0f;
samplerDesc.BorderColor[1] = 0.0f;
samplerDesc.BorderColor[2] = 0.0f;
samplerDesc.BorderColor[3] = 0.0f;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;
result = mDevice->CreateSamplerState(&samplerDesc, &mSamplerState);
if (FAILED(result))
{
return false;
}
return true;
}
24.3.3 天空盒几何体创建
天空盒的几何体是一个简单的立方体,但渲染顺序和深度测试设置需要特别注意。为了让天空盒始终出现在所有其他物体后面,我们需要禁用深度写入,或者设置特定的深度比较函数。
以下是天空盒几何体创建的实现:
bool Skybox::CreateCubeGeometry(float size)
{
mSize = size;
float halfSize = size * 0.5f;
// 定义立方体的8个顶点
XMFLOAT3 vertices[] =
{
// 前面
XMFLOAT3(-halfSize, halfSize, -halfSize), // 左上
XMFLOAT3( halfSize, halfSize, -halfSize), // 右上
XMFLOAT3(-halfSize, -halfSize, -halfSize), // 左下
XMFLOAT3( halfSize, -halfSize, -halfSize), // 右下
// 后面
XMFLOAT3(-halfSize, halfSize, halfSize), // 左上
XMFLOAT3( halfSize, halfSize, halfSize), // 右上
XMFLOAT3(-halfSize, -halfSize, halfSize), // 左下
XMFLOAT3( halfSize, -halfSize, halfSize), // 右下
};
// 定义36个索引(6个面 * 2个三角形 * 3个顶点)
UINT indices[] =
{
// 前面
0, 1, 2,
2, 1, 3,
// 后面
5, 4, 7,
7, 4, 6,
// 左面
4, 0, 6,
6, 0, 2,
// 右面
1, 5, 3,
3, 5, 7,
// 顶面
4, 5, 0,
0, 5, 1,
// 底面
2, 3, 6,
6, 3, 7
};
// 复制到成员变量
mVertexCount = 8;
mIndexCount = 36;
mVertices.resize(mVertexCount);
mIndices.resize(mIndexCount);
for (UINT i = 0; i < mVertexCount; i++)
{
mVertices[i].position = vertices[i];
}
for (UINT i = 0; i < mIndexCount; i++)
{
mIndices[i] = indices[i];
}
return true;
}
bool Skybox::CreateBuffers(ID3D11Device* device)
{
HRESULT result;
// 创建顶点缓冲区
D3D11_BUFFER_DESC vertexBufferDesc;
ZeroMemory(&vertexBufferDesc, sizeof(vertexBufferDesc));
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.ByteWidth = sizeof(SkyboxVertex) * mVertexCount;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = 0;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vertexData;
ZeroMemory(&vertexData, sizeof(vertexData));
vertexData.pSysMem = mVertices.data();
vertexData.SysMemPitch = 0;
vertexData.SysMemSlicePitch = 0;
result = device->CreateBuffer(&vertexBufferDesc, &vertexData, &mVertexBuffer);
if (FAILED(result))
{
return false;
}
// 创建索引缓冲区
D3D11_BUFFER_DESC indexBufferDesc;
ZeroMemory(&indexBufferDesc, sizeof(indexBufferDesc));
indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
indexBufferDesc.ByteWidth = sizeof(UINT) * mIndexCount;
indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
indexBufferDesc.CPUAccessFlags = 0;
indexBufferDesc.MiscFlags = 0;
indexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA indexData;
ZeroMemory(&indexData, sizeof(indexData));
indexData.pSysMem = mIndices.data();
indexData.SysMemPitch = 0;
indexData.SysMemSlicePitch = 0;
result = device->CreateBuffer(&indexBufferDesc, &indexData, &mIndexBuffer);
if (FAILED(result))
{
return false;
}
// 创建常量缓冲区
D3D11_BUFFER_DESC constantBufferDesc;
ZeroMemory(&constantBufferDesc, sizeof(constantBufferDesc));
constantBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
constantBufferDesc.ByteWidth = sizeof(SkyboxConstantBuffer);
constantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
constantBufferDesc.MiscFlags = 0;
constantBufferDesc.StructureByteStride = 0;
result = device->CreateBuffer(&constantBufferDesc, nullptr, &mConstantBuffer);
if (FAILED(result))
{
return false;
}
return true;
}
24.3.4 天空盒着色器实现
天空盒的着色器需要特别注意纹理采样和深度处理。顶点着色器负责将天空盒立方体变换到正确的位置,像素着色器负责从立方体贴图中采样正确的颜色。
以下是天空盒着色器的完整实现:
// Skybox.hlsl
// 常量缓冲区
cbuffer SkyboxConstantBuffer : register(b0)
{
matrix worldViewProjection;
float4 tintColor;
float brightness;
float3 padding;
};
// 顶点着色器输入
struct VertexInput
{
float3 position : POSITION;
};
// 像素着色器输入
struct PixelInput
{
float4 position : SV_POSITION;
float3 texCoord : TEXCOORD0;
};
// 纹理和采样器
TextureCube skyboxTexture : register(t0);
SamplerState textureSampler : register(s0);
// 顶点着色器
PixelInput SkyboxVertexShader(VertexInput input)
{
PixelInput output;
// 变换到齐次裁剪空间
output.position = mul(float4(input.position, 1.0f), worldViewProjection);
// 将位置作为纹理坐标(立方体贴图采样使用方向向量)
// 注意:我们使用位置向量而不是UV坐标
output.texCoord = input.position;
return output;
}
// 像素着色器
float4 SkyboxPixelShader(PixelInput input) : SV_TARGET
{
// 从立方体贴图采样
float4 color = skyboxTexture.Sample(textureSampler, input.texCoord);
// 应用色调和亮度调整
color *= tintColor;
color.rgb *= brightness;
return color;
}
编译和使用着色器的代码:
bool Skybox::CreateShaders(ID3D11Device* device)
{
HRESULT result;
// 编译顶点着色器
ID3DBlob* vertexShaderBlob = nullptr;
ID3DBlob* errorBlob = nullptr;
result = D3DCompileFromFile(
L"Skybox.hlsl",
nullptr,
nullptr,
"SkyboxVertexShader",
"vs_5_0",
D3DCOMPILE_ENABLE_STRICTNESS,
0,
&vertexShaderBlob,
&errorBlob
);
if (FAILED(result))
{
if (errorBlob)
{
OutputDebugStringA((char*)errorBlob->GetBufferPointer());
errorBlob->Release();
}
if (vertexShaderBlob) vertexShaderBlob->Release();
return false;
}
// 创建顶点着色器
result = device->CreateVertexShader(
vertexShaderBlob->GetBufferPointer(),
vertexShaderBlob->GetBufferSize(),
nullptr,
&mVertexShader
);
if (FAILED(result))
{
vertexShaderBlob->Release();
return false;
}
// 保存着色器字节码用于输入布局创建
mVertexShaderByteCode.resize(vertexShaderBlob->GetBufferSize());
memcpy(mVertexShaderByteCode.data(),
vertexShaderBlob->GetBufferPointer(),
vertexShaderBlob->GetBufferSize());
// 编译像素着色器
ID3DBlob* pixelShaderBlob = nullptr;
result = D3DCompileFromFile(
L"Skybox.hlsl",
nullptr,
nullptr,
"SkyboxPixelShader",
"ps_5_0",
D3DCOMPILE_ENABLE_STRICTNESS,
0,
&pixelShaderBlob,
&errorBlob
);
if (FAILED(result))
{
if (errorBlob)
{
OutputDebugStringA((char*)errorBlob->GetBufferPointer());
errorBlob->Release();
}
if (vertexShaderBlob) vertexShaderBlob->Release();
if (pixelShaderBlob) pixelShaderBlob->Release();
return false;
}
// 创建像素着色器
result = device->CreatePixelShader(
pixelShaderBlob->GetBufferPointer(),
pixelShaderBlob->GetBufferSize(),
nullptr,
&mPixelShader
);
pixelShaderBlob->Release();
vertexShaderBlob->Release();
if (FAILED(result))
{
return false;
}
// 创建输入布局
D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};
result = device->CreateInputLayout(
inputLayoutDesc,
1,
mVertexShaderByteCode.data(),
mVertexShaderByteCode.size(),
&mInputLayout
);
if (FAILED(result))
{
return false;
}
return true;
}
24.4 高级天空渲染技术
24.4.1 动态天空系统
静态天空盒虽然高效,但无法满足现代游戏对动态天空的需求。动态天空系统可以模拟昼夜循环、天气变化、移动云层等效果。实现动态天空的常用技术包括:
-
多纹理混合:在不同时间或天气条件下使用不同的天空盒纹理,并在它们之间平滑过渡。
-
程序化生成:使用数学函数实时生成天空纹理,实现无限的变化可能性。
-
体积渲染:使用体积纹理或粒子系统模拟云层的三维结构和动态变化。
-
大气散射模拟:基于物理模型计算大气对阳光的散射效果,实现真实的日出日落和天空颜色变化。
以下是动态天空系统的简化实现框架:
class DynamicSkybox : public Skybox
{
public:
DynamicSkybox();
virtual ~DynamicSkybox();
// 初始化动态天空
bool InitializeDynamic(
ID3D11Device* device,
ID3D11DeviceContext* deviceContext,
const std::vector<std::wstring>& timeOfDayTextures,
float dayDuration = 86400.0f); // 默认24小时(秒)
// 更新动态天空
virtual void Update(float deltaTime) override;
// 设置当前时间(0.0-1.0,表示一天中的时间)
void SetTimeOfDay(float time);
// 获取当前时间
float GetTimeOfDay() const;
// 设置天气条件
void SetWeatherCondition(WeatherType weather, float transitionTime = 5.0f);
// 添加云层系统
bool AddCloudLayer(const CloudLayerDescription& description);
private:
// 私有方法
bool CreateDynamicTextures();
void UpdateSkyColor();
void UpdateCloudLayers(float deltaTime);
void BlendTexturesBasedOnTime();
// 动态天空数据
std::vector<ID3D11ShaderResourceView*> mTimeOfDayTextures;
std::vector<CloudLayer> mCloudLayers;
// 时间系统
float mCurrentTime; // 0.0到1.0,表示一天中的时间
float mDayDuration; // 一天的总时长(秒)
float mTimeScale; // 时间流逝速度
// 天气系统
WeatherType mCurrentWeather;
WeatherType mTargetWeather;
float mWeatherTransitionTime;
float mWeatherTransitionProgress;
// 渲染状态
ID3D11BlendState* mBlendState;
ID3D11PixelShader* mDynamicPixelShader;
// 常量缓冲区扩展
struct DynamicSkyboxConstants
{
float timeOfDay;
float weatherBlend;
float cloudCoverage;
float cloudSpeed;
XMFLOAT4 sunColor;
XMFLOAT4 moonColor;
XMFLOAT4 zenithColor;
XMFLOAT4 horizonColor;
};
ID3D11Buffer* mDynamicConstantBuffer;
};
24.4.2 大气散射模拟
大气散射是使天空看起来真实的关键物理现象。当阳光穿过地球大气层时,与空气分子和气溶胶发生相互作用,导致光线向各个方向散射。瑞利散射(Rayleigh scattering)主要影响短波长光线(蓝色),这解释了为什么晴朗天空是蓝色的;米氏散射(Mie scattering)则影响所有波长的光线,这解释了为什么云雾和污染会使天空变白。
在游戏《无人深空》和《微软模拟飞行》中,大气散射的精确模拟创造了极其真实的天空效果。以下是简化的基于物理的大气散射实现:
// AtmosphericScattering.hlsl
// 大气参数
struct AtmosphereParameters
{
float earthRadius; // 地球半径(米)
float atmosphereHeight; // 大气高度(米)
float3 rayleighCoeff; // 瑞利散射系数
float rayleighScale; // 瑞利散射高度比例
float3 mieCoeff; // 米氏散射系数
float mieScale; // 米氏散射高度比例
float mieDirectionalG; // 米氏散射方向性参数
float sunIntensity; // 太阳强度
};
// 计算大气散射
float3 CalculateAtmosphericScattering(
float3 cameraPosition,
float3 viewDirection,
float3 sunDirection,
AtmosphereParameters atmosphere)
{
// 标准化向量
viewDirection = normalize(viewDirection);
sunDirection = normalize(sunDirection);
// 计算视线与大气层的交点
float t0, t1;
if (!RaySphereIntersection(cameraPosition, viewDirection,
atmosphere.earthRadius + atmosphere.atmosphereHeight,
t0, t1))
{
// 视线没有穿过大气层
return float3(0.0, 0.0, 0.0);
}
// 计算光线在大气中的路径长度
float rayLength = t1 - t0;
// 分段积分
const int numSamples = 16;
float segmentLength = rayLength / numSamples;
float tCurrent = t0;
float3 totalRayleigh = float3(0.0, 0.0, 0.0);
float3 totalMie = float3(0.0, 0.0, 0.0);
// 光学深度
float opticalDepthRayleigh = 0.0;
float opticalDepthMie = 0.0;
for (int i = 0; i < numSamples; i++)
{
// 当前采样点位置
float3 samplePosition = cameraPosition + viewDirection * (tCurrent + segmentLength * 0.5);
// 计算采样点高度
float height = length(samplePosition) - atmosphere.earthRadius;
// 计算高度密度
float densityRayleigh = exp(-height / atmosphere.rayleighScale);
float densityMie = exp(-height / atmosphere.mieScale);
// 累积光学深度
opticalDepthRayleigh += densityRayleigh * segmentLength;
opticalDepthMie += densityMie * segmentLength;
// 计算从采样点到太阳的光线路径
float sunRayLength;
if (!RaySphereIntersection(samplePosition, sunDirection,
atmosphere.earthRadius + atmosphere.atmosphereHeight,
sunRayLength))
{
// 采样点可见太阳
sunRayLength = 1000000.0; // 足够大的值
}
// 计算太阳光的光学深度
const int numLightSamples = 8;
float lightSegmentLength = sunRayLength / numLightSamples;
float lightOpticalDepthRayleigh = 0.0;
float lightOpticalDepthMie = 0.0;
for (int j = 0; j < numLightSamples; j++)
{
float3 lightSamplePosition = samplePosition + sunDirection * (lightSegmentLength * j + lightSegmentLength * 0.5);
float lightHeight = length(lightSamplePosition) - atmosphere.earthRadius;
lightOpticalDepthRayleigh += exp(-lightHeight / atmosphere.rayleighScale) * lightSegmentLength;
lightOpticalDepthMie += exp(-lightHeight / atmosphere.mieScale) * lightSegmentLength;
}
// 计算透射率
float3 transmittance = exp(-(atmosphere.rayleighCoeff * (opticalDepthRayleigh + lightOpticalDepthRayleigh) +
atmosphere.mieCoeff * (opticalDepthMie + lightOpticalDepthMie)));
// 累积散射贡献
totalRayleigh += densityRayleigh * transmittance * segmentLength;
totalMie += densityMie * transmittance * segmentLength;
tCurrent += segmentLength;
}
// 计算最终颜色
float cosTheta = dot(viewDirection, sunDirection);
float miePhase = MiePhaseFunction(cosTheta, atmosphere.mieDirectionalG);
float rayleighPhase = RayleighPhaseFunction(cosTheta);
float3 rayleighColor = totalRayleigh * atmosphere.rayleighCoeff * rayleighPhase;
float3 mieColor = totalMie * atmosphere.mieCoeff * miePhase;
float3 finalColor = (rayleighColor + mieColor) * atmosphere.sunIntensity;
return finalColor;
}
// 米氏散射相位函数
float MiePhaseFunction(float cosTheta, float g)
{
float g2 = g * g;
float numerator = 3.0 * (1.0 - g2) * (1.0 + cosTheta * cosTheta);
float denominator = 2.0 * (2.0 + g2) * pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5);
return numerator / denominator;
}
// 瑞利散射相位函数
float RayleighPhaseFunction(float cosTheta)
{
return 3.0 / (16.0 * 3.14159265) * (1.0 + cosTheta * cosTheta);
}
// 射线与球体相交测试
bool RaySphereIntersection(float3 origin, float3 direction, float radius, out float t0, out float t1)
{
float a = dot(direction, direction);
float b = 2.0 * dot(origin, direction);
float c = dot(origin, origin) - radius * radius;
float discriminant = b * b - 4.0 * a * c;
if (discriminant < 0.0)
{
return false;
}
float sqrtDiscriminant = sqrt(discriminant);
t0 = (-b - sqrtDiscriminant) / (2.0 * a);
t1 = (-b + sqrtDiscriminant) / (2.0 * a);
// 确保t0 < t1
if (t0 > t1)
{
float temp = t0;
t0 = t1;
t1 = temp;
}
return true;
}
24.4.3 云层渲染技术
云层是天空视觉效果的重要组成部分,但也是最难真实渲染的部分之一。现代游戏使用了多种云层渲染技术:
- 2D云层纹理:使用平移和混合的2D纹理,性能高效但缺乏体积感。
- 体积云:使用3D纹理或程序化生成的体积数据,可以渲染具有真实体积感的云层。
- 粒子系统:使用大量粒子组成云层,灵活但性能消耗大。
- 平面云层:使用带有半透明纹理的平面网格,性能和效果平衡。
《微软模拟飞行2020》使用了革命性的体积云技术,基于气象数据实时生成全球云层。以下是简化的体积云渲染实现思路:
class VolumetricClouds
{
public:
bool Initialize(ID3D11Device* device, ID3D11DeviceContext* deviceContext);
void Update(float deltaTime, const XMFLOAT3& cameraPosition, const XMFLOAT3& sunDirection);
void Render(ID3D11DeviceContext* deviceContext, ID3D11RenderTargetView* renderTarget,
ID3D11DepthStencilView* depthStencil);
private:
// 体积纹理生成
void GenerateNoiseTextures();
void UpdateWeatherTexture();
// 光线步进渲染
void RenderRayMarching(ID3D11DeviceContext* deviceContext);
// 资源
ID3D11Texture3D* mNoiseTexture;
ID3D11ShaderResourceView* mNoiseSRV;
ID3D11Texture2D* mWeatherTexture;
ID3D11ShaderResourceView* mWeatherSRV;
// 渲染状态
ID3D11VertexShader* mCloudVertexShader;
ID3D11PixelShader* mCloudPixelShader;
ID3D11InputLayout* mCloudInputLayout;
ID3D11BlendState* mCloudBlendState;
ID3D11DepthStencilState* mCloudDepthState;
// 云层参数
float mCloudCoverage;
float mCloudDensity;
float mCloudAltitude;
float mCloudThickness;
float mWindSpeed;
XMFLOAT2 mWindDirection;
// 动画
float mTime;
XMFLOAT3 mCloudOffset;
};
24.5 完整示例程序:D3DDemo19
24.5.1 程序架构设计
我们将创建一个完整的天空渲染演示程序,展示从基础天空盒到动态天空系统的完整实现。程序的主要功能包括:
- 基础天空盒渲染
- 动态昼夜循环
- 简单云层系统
- 基本天气效果
- 交互式摄像机控制
程序类结构设计:
class SkyDemo
{
public:
SkyDemo();
~SkyDemo();
bool Initialize(HINSTANCE hInstance, HWND hwnd, int screenWidth, int screenHeight);
void Shutdown();
bool Frame();
private:
bool Render();
bool Update(float deltaTime);
void HandleInput(float deltaTime);
// 初始化方法
bool InitializeDirect3D();
bool InitializeShaders();
bool InitializeSkySystem();
bool InitializeTerrain();
bool InitializeCamera();
// 渲染方法
void RenderSky();
void RenderTerrain();
void RenderUI();
// 资源管理
bool LoadTextures();
bool CreateRenderStates();
private:
// Direct3D组件
ID3D11Device* mDevice;
ID3D11DeviceContext* mDeviceContext;
IDXGISwapChain* mSwapChain;
ID3D11RenderTargetView* mRenderTargetView;
ID3D11DepthStencilView* mDepthStencilView;
ID3D11Texture2D* mDepthStencilBuffer;
D3D11_VIEWPORT mViewport;
// 天空系统
std::unique_ptr<DynamicSkybox> mSkybox;
// 地形
Terrain mTerrain;
// 摄像机
Camera mCamera;
// 时间系统
float mTimeOfDay; // 0.0到1.0
float mTimeScale; // 时间流逝速度
bool mPauseTime; // 是否暂停时间
// 天气系统
WeatherType mCurrentWeather;
// 窗口参数
HWND mHwnd;
int mScreenWidth;
int mScreenHeight;
// 计时器
LARGE_INTEGER mFrequency;
LARGE_INTEGER mLastTime;
float mDeltaTime;
// 输入状态
bool mKeys[256];
POINT mLastMousePos;
bool mRightMouseDown;
};
24.5.2 初始化与主循环
以下是天空演示程序的初始化代码和主循环实现:
bool SkyDemo::Initialize(HINSTANCE hInstance, HWND hwnd, int screenWidth, int screenHeight)
{
// 保存窗口参数
mHwnd = hwnd;
mScreenWidth = screenWidth;
mScreenHeight = screenHeight;
// 初始化计时器
QueryPerformanceFrequency(&mFrequency);
QueryPerformanceCounter(&mLastTime);
// 初始化Direct3D
if (!InitializeDirect3D())
{
MessageBox(hwnd, L"无法初始化Direct3D", L"错误", MB_OK);
return false;
}
// 初始化着色器
if (!InitializeShaders())
{
MessageBox(hwnd, L"无法初始化着色器", L"错误", MB_OK);
return false;
}
// 初始化天空系统
if (!InitializeSkySystem())
{
MessageBox(hwnd, L"无法初始化天空系统", L"错误", MB_OK);
return false;
}
// 初始化地形
if (!InitializeTerrain())
{
MessageBox(hwnd, L"无法初始化地形", L"错误", MB_OK);
return false;
}
// 初始化摄像机
InitializeCamera();
// 初始化输入状态
ZeroMemory(mKeys, sizeof(mKeys));
mLastMousePos.x = mLastMousePos.y = 0;
mRightMouseDown = false;
// 初始化时间系统
mTimeOfDay = 0.25f; // 清晨
mTimeScale = 60.0f; // 1秒游戏时间 = 60秒真实时间
mPauseTime = false;
// 初始化天气
mCurrentWeather = WEATHER_CLEAR;
// 显示初始化成功消息
std::wstring message = L"天空演示程序初始化成功
";
message += L"控制说明:
";
message += L"WASD - 移动摄像机
";
message += L"鼠标右键拖动 - 旋转摄像机
";
message += L"T - 加速时间
";
message += L"Y - 减速时间
";
message += L"P - 暂停/继续时间
";
message += L"1-4 - 切换天气
";
message += L"ESC - 退出程序";
MessageBox(hwnd, message.c_str(), L"控制说明", MB_OK);
return true;
}
bool SkyDemo::Frame()
{
// 计算帧时间
LARGE_INTEGER currentTime;
QueryPerformanceCounter(¤tTime);
mDeltaTime = static_cast<float>(currentTime.QuadPart - mLastTime.QuadPart) /
static_cast<float>(mFrequency.QuadPart);
mLastTime = currentTime;
// 处理输入
HandleInput(mDeltaTime);
// 更新游戏状态
if (!Update(mDeltaTime))
{
return false;
}
// 渲染场景
if (!Render())
{
return false;
}
return true;
}
bool SkyDemo::Update(float deltaTime)
{
// 更新时间系统
if (!mPauseTime)
{
mTimeOfDay += deltaTime / (24.0f * 60.0f * 60.0f / mTimeScale); // 转换为游戏时间
if (mTimeOfDay >= 1.0f)
{
mTimeOfDay -= 1.0f;
}
}
// 更新天空系统
if (mSkybox)
{
mSkybox->SetTimeOfDay(mTimeOfDay);
mSkybox->Update(deltaTime);
}
// 更新摄像机
mCamera.Update(deltaTime);
return true;
}
void SkyDemo::HandleInput(float deltaTime)
{
// 检查退出
if (mKeys[VK_ESCAPE])
{
PostQuitMessage(0);
return;
}
// 摄像机移动
float moveSpeed = 10.0f * deltaTime;
if (mKeys['W']) mCamera.MoveForward(moveSpeed);
if (mKeys['S']) mCamera.MoveForward(-moveSpeed);
if (mKeys['A']) mCamera.MoveRight(-moveSpeed);
if (mKeys['D']) mCamera.MoveRight(moveSpeed);
if (mKeys['E']) mCamera.MoveUp(moveSpeed);
if (mKeys['Q']) mCamera.MoveUp(-moveSpeed);
// 时间控制
if (mKeys['T'] && !mKeysLastFrame['T']) // 加速时间
{
mTimeScale *= 2.0f;
if (mTimeScale > 960.0f) mTimeScale = 960.0f;
}
if (mKeys['Y'] && !mKeysLastFrame['Y']) // 减速时间
{
mTimeScale /= 2.0f;
if (mTimeScale < 3.75f) mTimeScale = 3.75f;
}
if (mKeys['P'] && !mKeysLastFrame['P']) // 暂停/继续时间
{
mPauseTime = !mPauseTime;
}
// 天气控制
if (mKeys['1'] && !mKeysLastFrame['1'])
{
mCurrentWeather = WEATHER_CLEAR;
if (mSkybox) mSkybox->SetWeatherCondition(mCurrentWeather);
}
if (mKeys['2'] && !mKeysLastFrame['2'])
{
mCurrentWeather = WEATHER_CLOUDY;
if (mSkybox) mSkybox->SetWeatherCondition(mCurrentWeather);
}
if (mKeys['3'] && !mKeysLastFrame['3'])
{
mCurrentWeather = WEATHER_RAINY;
if (mSkybox) mSkybox->SetWeatherCondition(mCurrentWeather);
}
if (mKeys['4'] && !mKeysLastFrame['4'])
{
mCurrentWeather = WEATHER_STORMY;
if (mSkybox) mSkybox->SetWeatherCondition(mCurrentWeather);
}
// 鼠标输入
POINT mousePos;
GetCursorPos(&mousePos);
ScreenToClient(mHwnd, &mousePos);
// 鼠标右键旋转摄像机
if (mRightMouseDown)
{
float deltaX = static_cast<float>(mousePos.x - mLastMousePos.x);
float deltaY = static_cast<float>(mousePos.y - mLastMousePos.y);
mCamera.Rotate(deltaY * 0.001f, deltaX * 0.001f, 0.0f);
}
mLastMousePos = mousePos;
// 保存当前按键状态供下一帧比较
memcpy(mKeysLastFrame, mKeys, sizeof(mKeys));
}
24.5.3 渲染系统实现
以下是天空演示程序的渲染系统实现:
bool SkyDemo::Render()
{
// 清除渲染目标
float clearColor[4] = {0.0f, 0.0f, 0.0f, 1.0f};
mDeviceContext->ClearRenderTargetView(mRenderTargetView, clearColor);
mDeviceContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH, 1.0f, 0);
// 设置渲染状态
mDeviceContext->RSSetViewports(1, &mViewport);
mDeviceContext->OMSetRenderTargets(1, &mRenderTargetView, mDepthStencilView);
// 渲染天空(先渲染天空,作为背景)
RenderSky();
// 渲染地形
RenderTerrain();
// 渲染UI
RenderUI();
// 呈现交换链
HRESULT result = mSwapChain->Present(0, 0);
if (FAILED(result))
{
return false;
}
return true;
}
void SkyDemo::RenderSky()
{
if (!mSkybox)
{
return;
}
// 获取摄像机矩阵
XMMATRIX viewMatrix = mCamera.GetViewMatrix();
XMMATRIX projectionMatrix = mCamera.GetProjectionMatrix();
// 渲染天空盒
mSkybox->Render(mDeviceContext, viewMatrix, projectionMatrix);
}
void SkyDemo::RenderTerrain()
{
// 设置地形着色器
// 这里假设已经初始化了地形着色器
// 实际实现中需要设置相应的着色器和常量缓冲区
// 获取摄像机矩阵
XMMATRIX viewMatrix = mCamera.GetViewMatrix();
XMMATRIX projectionMatrix = mCamera.GetProjectionMatrix();
XMMATRIX worldMatrix = XMMatrixIdentity();
// 更新地形常量缓冲区
// 实际实现中需要设置光照、材质等参数
// 渲染地形
mTerrain.Render(mDeviceContext);
}
void SkyDemo::RenderUI()
{
// 在实际实现中,这里会使用Sprite渲染UI文本
// 显示当前时间、天气、控制说明等信息
// 简化实现:使用调试输出
char debugText[256];
sprintf_s(debugText, "时间: %.2f:00, 天气: %s, 时间速度: %.1fx",
mTimeOfDay * 24.0f,
GetWeatherName(mCurrentWeather),
mTimeScale / 60.0f);
// 在实际实现中,这里会将文本渲染到屏幕
// 为简化,我们只输出到调试窗口
OutputDebugStringA(debugText);
OutputDebugStringA("
");
}
const char* SkyDemo::GetWeatherName(WeatherType weather)
{
switch (weather)
{
case WEATHER_CLEAR: return "晴朗";
case WEATHER_CLOUDY: return "多云";
case WEATHER_RAINY: return "雨天";
case WEATHER_STORMY: return "暴风雨";
default: return "未知";
}
}
24.6 优化与进阶技术
24.6.1 性能优化策略
天空渲染虽然不涉及复杂几何体,但纹理采样和着色器计算仍然可能成为性能瓶颈。以下是一些常用的优化策略:
-
纹理压缩:使用BC(Block Compression)格式压缩天空盒纹理,减少内存占用和带宽需求。
-
Mipmap使用:为天空盒生成完整的Mipmap链,确保在远处使用较低分辨率的纹理。
-
着色器优化:简化天空盒着色器,减少复杂计算。对于动态天空效果,考虑使用查找表(LUT)预计算复杂函数。
-
细节层次(LOD):为远距离天空元素使用简化的表示方法。
-
渲染顺序:正确设置渲染顺序和深度状态,避免不必要的像素着色器调用。
24.6.2 现代图形API中的天空渲染
随着DirectX 12和Vulkan等现代图形API的普及,天空渲染技术也有了新的发展:
-
光线追踪天空:使用DXR或Vulkan光线追踪扩展,可以实现更真实的大气散射和云层渲染。
-
计算着色器:使用计算着色器预处理天空数据,减少运行时计算量。
-
异步计算:利用现代GPU的异步计算能力,并行处理天空渲染和其他图形任务。
-
可变速率着色:对天空区域使用较低的着色速率,提高整体渲染性能。
24.6.3 商业游戏中的天空技术实例
分析商业游戏中的天空实现可以提供宝贵的实践参考:
-
《荒野大镖客2》:使用了基于物理的大气散射系统,配合体积云和动态天气系统,创造了极其真实的美国西部天空。
-
《赛博朋克2077》:未来城市的天空系统结合了传统天空盒和程序化生成的霓虹灯光效果,营造了独特的赛博朋克氛围。
-
《刺客信条:英灵殿》:北欧风格的天空系统,重点表现了极光、风暴和神话般的天空效果。
-
《战神4》:每个领域都有独特的天空设计,技术与艺术紧密结合,支持游戏的叙事需求。
24.7 章节总结
三维天空系统的实现是游戏开发中连接技术与艺术的关键环节。从基础的天空盒技术到复杂的大气散射模拟,天空渲染技术的发展反映了图形学在游戏中的应用演进。
本章详细介绍了:
- 天空盒的基本原理和实现方法
- 动态天空系统的设计与实现
- 大气散射的物理模型和简化实现
- 云层渲染的常用技术
- 完整的天空渲染演示程序
在实际游戏开发中,天空系统的选择需要综合考虑艺术需求、技术限制和目标平台性能。独立开发者可能从简单的天空盒开始,而3A工作室则可能投资开发完整的物理大气系统。
无论复杂程度如何,优秀的天空渲染都能显著提升游戏的沉浸感和视觉质量。随着图形硬件的不断进步和新技术的出现,我们可以期待未来游戏中的天空将变得更加真实和动态,为玩家创造更加引人入胜的虚拟世界。
记住,最好的天空系统是那些玩家不会特别注意到,但如果没有它就会明显感到缺失的系统。它应该自然地融入游戏世界,支持游戏玩法和叙事,而不是作为一个孤立的技术展示。











