计算着色器
在GPU编程中,我们编写的着色器程序会同时给大量的线程运行,可以将这些线程按网格来划分成线程组。一个线程组由一个多处理器来执行,如果你的GPU有16个多处理器,你会想要把问题分解成至少16个线程组以保证每个多处理器都工作。为了获取更好的性能,让每个多处理器来处理至少2个线程组是一个比较不错的选择,这样当一个线程组在等待别的资源时就可以先去考虑完成另一个线程组的工作。
基于GPU编程的优势,我们可以通过GPU编程模拟波浪的顶点数据。(使用CPU编程并不是不行,但是帧率很低。)
前言
本波浪模拟项目基于DirectX11 With Windows SDK--28 计算着色器:波浪(水波)。并自行编写了GerstnerWave对应的Render。
其他参考博客: https://blog.csdn.net/qq_29523119/article/details/81055532
Directx11进阶之基于GerstnerWave模拟一之基于CPU计算的实现
https://www.cnblogs.com/YIMG/p/13449711.html
DirectX11 GerstnerWaves模拟(计算着色器)
参考书目:《GPU精粹1:实时图形编程的技术、技巧和技艺》
Gerstner波函数
在《GPU精粹1:实时图形编程的技术、技巧和技艺》中,已经给出了Gerstner波函数的计算公式:
其中:
(1) x,y是坐标点在XZ平面的位置分量
(2) Q为波浪的陡峭参数
(3) D控制波浪的移动方向,参数类型为float2
(4) A为波浪的振幅
(5) t 为游戏开始到目前运行的时间
(6) 假设波长为λ, 则 频率 w = 2* π / λ,
(7) 假设我们的波长每秒移动的速度为 S,则相位常量 δ = w * S
总体上计算GerstnerWave的顶点位置的方式是,初始化一个大网格,对网格的每个顶点初始化XZ屏幕的值,代入上面的公式计算。
同时,书中也提供了波函数相对应的法线计算公式。
Gerstner波的HLSL代码
目前已知,我们需要获得波的参数有波长Length、振幅Amplitude、波速Speed、陡峭参数Steempness、波的方向Direction和时间t。并且要获得线程组中列的数目。
因此,在GerstnerWaves.hlsli中需要定义Gerstner结构体和每个顶点的结构体:
//GerstnerWaves.hlsli #define WaveCount 3 static const float PI = 3.14159267f; static const float g = 9.8f; struct GerstnerWave { float g_WaveL; // 波长 float g_WaveA; // 振幅 float g_WaveSpeed; // 速度 float g_WaveSteepness; // 陡度 float2 g_WaveD; // 方向 float2 g_pad; // 打包 }; struct VertexPosNormalTex { float3 PosL : POSITION; float3 NormalL : NORMAL; float2 Tex : TEXCOORD; }; RWStructuredBuffer<VertexPosNormalTex> g_Input : register(u0); RWStructuredBuffer<VertexPosNormalTex> g_Output : register(u1); // 用于更新模拟 cbuffer cbUpdateSettings : register(b0) { GerstnerWave g_gerstnerData[WaveCount]; // 几个波叠加 float g_TotalTime; // 总时长 float g_GroundCountX; // X方向上的线程团数 float2 g_Pad; }
叠加三个Gerstner波和计算顶点的着色器代码如下:
//GerstnerWaves_CS.hlsl #include "GerstnerWaves.hlsli" [numthreads(16, 16, 1)] void CS(uint3 DTid : SV_DispatchThreadID) { int index = DTid.x + DTid.y * g_GroundCountX * 16; float posx = g_Input[index].PosL.x; float posy = g_Input[index].PosL.z; float3 Wavepos = { posx, g_Input[index].PosL.y, posy }; float3 Wavenormal = { 0.0f, 0.0f, 0.0f }; // GerstnerWaves计算 for (int i = 0; i < WaveCount; i++) { //频率 float w = sqrt(g * 2 * PI / g_gerstnerData[i].g_WaveL); //相位差常量 float phaseConstant = g_gerstnerData[i].g_WaveSpeed * w; // g_WaveSteepness 相同, 使用以下公式计数分波的Qi float Q = g_gerstnerData[i].g_WaveSteepness / (w * g_gerstnerData[i].g_WaveA * WaveCount); float phase = w * g_gerstnerData[i].g_WaveD.x * posx + w * g_gerstnerData[i].g_WaveD.y * posy + phaseConstant * g_TotalTime; Wavepos.x += Q * g_gerstnerData[i].g_WaveA * g_gerstnerData[i].g_WaveD.x * cos(phase); Wavepos.y += Q * g_gerstnerData[i].g_WaveA * g_gerstnerData[i].g_WaveD.y * cos(phase); Wavepos.z += g_gerstnerData[i].g_WaveA * sin(phase); // 计算法向量 float WaveA = w * g_gerstnerData[i].g_WaveA; float WaveT = w * (g_gerstnerData[i].g_WaveD.x * Wavepos.x + g_gerstnerData[i].g_WaveD.y * Wavepos.z) + phaseConstant * g_TotalTime; Wavenormal.x = -g_gerstnerData[i].g_WaveD.x * WaveA * cos(WaveT); Wavenormal.z = -g_gerstnerData[i].g_WaveD.y * WaveA * cos(WaveT); Wavenormal.y = -Q * WaveA * sin(WaveT); } Wavenormal.y += 1; Wavenormal = normalize(Wavenormal); // 保存 g_Output[index].PosL = float3(Wavepos); g_Output[index].NormalL = float3(Wavenormal); g_Output[index].Tex = g_Input[index].Tex; }
GerstnerWavesRender
本处GerstnerWavesRender类分三步构成。分别是初始化、数据更新和绘制。
初始化
从DirectX11 With Windows SDK--28 计算着色器:波浪(水波)中的WavesRender和GerstnerWaves中,可知我们需要初始化的数据包括:
①顶点数量(水平面网格)
②纹理坐标
③时间、空间步长
④结构体GerstnerWave的各项数据
需要初始化的资源包括:
三大缓冲区:顶点缓冲区、索引缓冲区、常量缓冲区。计算着色器、无序访问视图、结构体缓冲区
GerstnerWavesRender类定义如下
class GerstnerWavesRender { public: template<class T> using ComPtr = Microsoft::WRL::ComPtr<T>; void SetMaterial(const Material& material); Transform& GetTransform(); const Transform& GetTransform() const; UINT RowCount() const; UINT ColumnCount() const; struct GerstnerWave { float WaveL; // 波长 float WaveA; // 振幅 float WaveSpeed; // 速度 float WaveSteepness; // 陡度 DirectX::XMFLOAT2 WaveD; // 方向 DirectX::XMFLOAT2 pad; // 打包 }; public: GerstnerWavesRender() = default; ~GerstnerWavesRender() = default; // 不允许拷贝,允许移动 GerstnerWavesRender(const GerstnerWavesRender&) = delete; GerstnerWavesRender& operator= (const GerstnerWavesRender&) = delete; GerstnerWavesRender(GerstnerWavesRender&&) = default; GerstnerWavesRender& operator= (GerstnerWavesRender&&) = default; HRESULT InitResource(ID3D11Device* device, const std::wstring& texFileName, // 纹理文件名 UINT rows, // 顶点行数 UINT cols, // 顶点列数 float texU, // 纹理坐标U方向最大值 float texV, // 纹理坐标V方向最大值 float timeStep, // 时间步长 float spatialStep, // 空间步长 GerstnerWave* gerstnerData // 数据 ); // 设置数据 void SetData(GerstnerWave* gerstnerData); // 更新 void Update(ID3D11DeviceContext* deviceContext, float t); // 绘制 void Draw(ID3D11DeviceContext* deviceContext, BasicEffect& effect); // 设置DeBug名称 void SetDebugObjectName(const std::string& name); private: void Init( UINT rows, // 顶点行数 UINT cols, // 顶点列数 float texU, // 纹理坐标U方向最大值 float texV, // 纹理坐标V方向最大值 float timeStep, // 时间步长 float spatialStep, // 空间步长 GerstnerWave* gerstnerData // 数据 ); UINT m_NumRows = 0; // 顶点行数 UINT m_NumCols = 0; // 顶点列数 UINT m_VertexCount = 0; // 顶点数目 UINT m_IndexCount = 0; // 索引数目 Transform m_Transform = {}; // 水面变换 DirectX::XMFLOAT2 m_TexOffset = {}; // 纹理坐标偏移 float m_TexU = 0.0f; // 纹理坐标U方向最大值 float m_TexV = 0.0f; // 纹理坐标V方向最大值 Material m_Material = {}; // 水面材质 GerstnerWave m_gerstnerwaveData[3] = {}; float m_TimeStep = 0.0f; // 时间步长 float m_SpatialStep = 0.0f; // 空间步长 float m_AccumulateTime = 0.0f; // 累积时间 float m_TotalTime = 0.0f; // 总时长 private: ComPtr<ID3D11Buffer> m_pCurrVertex; // 保存当前模拟结果的顶点 ComPtr<ID3D11UnorderedAccessView> m_pCurrVertexUAV; // 缓存当前模拟结果的顶点 无序访问视图 ComPtr<ID3D11Buffer> m_pVertex; // 初始顶点 缓冲区 ComPtr<ID3D11UnorderedAccessView> m_pVertexUAV; // 初始顶点 无序访问视图 ComPtr<ID3D11Buffer> m_pVertexBuffer; // 顶点缓冲区 ComPtr<ID3D11Buffer> m_pIndexBuffer; // 索引缓冲区 ComPtr<ID3D11Buffer> m_pConstantBuffer; // 常量缓冲区 ComPtr<ID3D11Buffer> m_pTempBuffer; // 用于顶点数据拷贝的缓冲区 ComPtr<ID3D11ComputeShader> m_pWavesUpdateCS; // 用于计算模拟结果的着色器 ComPtr<ID3D11ShaderResourceView> m_pTextureDiffuse; // 水面纹理 struct { GerstnerWave gerstnerData[3]; float TotalTime; // 总时长 float GroundCountX; // X方向上的线程团数 DirectX::XMFLOAT2 Pad; } m_CBUpdateSettings = {}; };
注意,类中定义了HLSL中对应的Gerstner结构体。
GerstnerWavesRender类的InitResource代码如下:
HRESULT GerstnerWavesRender::InitResource(ID3D11Device* device, const std::wstring& texFileName, UINT rows, UINT cols, float texU, float texV, float timeStep, float spatialStep, GerstnerWave* gerstnerData) { // 清空内存 m_pVertexBuffer.Reset(); m_pIndexBuffer.Reset(); m_pConstantBuffer.Reset(); m_pTempBuffer.Reset(); m_pCurrVertex.Reset(); m_pCurrVertexUAV.Reset(); m_pVertex.Reset(); m_pVertexUAV.Reset(); m_pWavesUpdateCS.Reset(); m_pTextureDiffuse.Reset(); // 使用 16x16 的线程组 // 指定行顶点数和列顶点数都为16的倍数 if (rows % 16 || cols % 16) return E_INVALIDARG; // 初始化水波数据 Init(rows, cols, texU, texV, timeStep, spatialStep, gerstnerData); auto meshData = Geometry::CreateTerrain<VertexPosNormalTex, DWORD>(XMFLOAT2((cols - 1) * spatialStep, (rows - 1) * spatialStep), XMUINT2(cols - 1, rows - 1)); HRESULT hr; // 创建顶点缓冲区 hr = CreateVertexBuffer(device, meshData.vertexVec.data(), (UINT)meshData.vertexVec.size() * sizeof(VertexPosNormalTex), m_pVertexBuffer.GetAddressOf(), true); if (FAILED(hr)) return hr; // 创建索引缓冲区 hr = CreateIndexBuffer(device, meshData.indexVec.data(), (UINT)meshData.indexVec.size() * sizeof(DWORD), m_pIndexBuffer.GetAddressOf()); if (FAILED(hr)) return hr; // 创建常量缓冲区 hr = CreateConstantBuffer(device, nullptr, sizeof(m_CBUpdateSettings), m_pConstantBuffer.GetAddressOf()); if (FAILED(hr)) return hr; // 创建计算着色器 ComPtr<ID3DBlob> blob; hr = CreateShaderFromFile(L"HLSL\GerstnerWaves_CS.cso", L"HLSL\GerstnerWaves_CS.hlsl", "CS", "cs_5_0", blob.GetAddressOf()); if (FAILED(hr)) return hr; hr = device->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pWavesUpdateCS.GetAddressOf()); if (FAILED(hr)) return hr; // 创建GPU结构体缓冲区 hr = CreateStructuredBuffer(device, meshData.vertexVec.data(), (UINT)meshData.vertexVec.size() * sizeof(VertexPosNormalTex), (UINT)sizeof(VertexPosNormalTex), m_pCurrVertex.GetAddressOf(), false, true ); if (FAILED(hr)) return hr; hr = CreateStructuredBuffer(device, meshData.vertexVec.data(), (UINT)meshData.vertexVec.size() * sizeof(VertexPosNormalTex), (UINT)sizeof(VertexPosNormalTex), m_pVertex.GetAddressOf(), false, true); if (FAILED(hr)) return hr; // 创建无序访问视图 D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc; uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; uavDesc.Format = DXGI_FORMAT_UNKNOWN; uavDesc.Buffer.FirstElement = 0; uavDesc.Buffer.NumElements = (UINT)meshData.vertexVec.size(); uavDesc.Buffer.Flags = 0; hr = device->CreateUnorderedAccessView(m_pCurrVertex.Get(), &uavDesc, m_pCurrVertexUAV.GetAddressOf()); if (FAILED(hr)) return hr; hr = device->CreateUnorderedAccessView(m_pVertex.Get(), &uavDesc, m_pVertexUAV.GetAddressOf()); if (FAILED(hr)) return hr; // 用于顶点数据拷贝的缓冲区 D3D11_BUFFER_DESC bdesc; ZeroMemory(&bdesc, sizeof bdesc); m_pCurrVertex.Get()->GetDesc(&bdesc); bdesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; bdesc.Usage = D3D11_USAGE_STAGING; bdesc.BindFlags = 0; bdesc.MiscFlags = 0; hr = device->CreateBuffer(&bdesc, nullptr, m_pTempBuffer.GetAddressOf()); if (FAILED(hr)) return hr; // 读取纹理 if (texFileName.size() > 4) { // 判断纹理文件类型 if (texFileName.substr(texFileName.size() - 3, 3) == L"dds") { hr = CreateDDSTextureFromFile(device, texFileName.c_str(), nullptr, m_pTextureDiffuse.GetAddressOf()); } else { hr = CreateWICTextureFromFile(device, texFileName.c_str(), nullptr, m_pTextureDiffuse.GetAddressOf()); } } return hr; }
其中关于无序访问视图的创建可以查阅本博客:DirectX11 With Windows SDK--26 计算着色器:入门
数据更新
数据的更新包括GerstnerWavesRender类中的两个函数:
// 设置数据 void SetData(GerstnerWave* gerstnerData); // 更新 void Update(ID3D11DeviceContext* deviceContext, float t);
每次先通过SetData将Gerstner波的参数传入,再通过Update在指定的时间间隔后更新数据。
函数代码如下:
void GerstnerWavesRender::SetData(GerstnerWave* gerstnerData) { for (int i = 0; i < sizeof(m_gerstnerwaveData) / sizeof(GerstnerWave); ++i) { m_gerstnerwaveData[i] = *(gerstnerData + i); } }
void GerstnerWavesRender::Update(ID3D11DeviceContext* deviceContext, float t) { // 时间累加 m_AccumulateTime += t; m_TotalTime += t; // 纹理位移 for (int i = 0; i < sizeof(m_gerstnerwaveData) / sizeof(GerstnerWave); ++i) { float DirSide = sqrt(m_gerstnerwaveData[i].WaveD.x * m_gerstnerwaveData[i].WaveD.x + m_gerstnerwaveData[i].WaveD.y * m_gerstnerwaveData[i].WaveD.y); m_TexOffset.x -= m_gerstnerwaveData[i].WaveSpeed * m_gerstnerwaveData[i].WaveD.x / DirSide * t * 0.02f; m_TexOffset.y -= m_gerstnerwaveData[i].WaveSpeed * m_gerstnerwaveData[i].WaveD.y / DirSide * t * 0.02f; } if (m_AccumulateTime > m_TimeStep) { // 更新常量缓冲区 D3D11_MAPPED_SUBRESOURCE data; m_CBUpdateSettings.gerstnerData[0] = m_gerstnerwaveData[0]; m_CBUpdateSettings.gerstnerData[1] = m_gerstnerwaveData[1]; m_CBUpdateSettings.gerstnerData[2] = m_gerstnerwaveData[2]; m_CBUpdateSettings.TotalTime = m_TotalTime; m_CBUpdateSettings.GroundCountX = m_NumCols / 16; deviceContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &data); memcpy_s(data.pData, sizeof m_CBUpdateSettings, &m_CBUpdateSettings, sizeof m_CBUpdateSettings); deviceContext->Unmap(m_pConstantBuffer.Get(), 0); // 设置计算资源 deviceContext->CSSetShader(m_pWavesUpdateCS.Get(), nullptr, 0); deviceContext->CSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf()); ID3D11UnorderedAccessView* pUAVs[2] = { m_pVertexUAV.Get() ,m_pCurrVertexUAV.Get() }; deviceContext->CSSetUnorderedAccessViews(0, 2, pUAVs, nullptr); // 开始调度 deviceContext->Dispatch(m_NumCols / 16, m_NumRows / 16, 1); // 清除绑定 pUAVs[0] = pUAVs[1] = nullptr; deviceContext->CSSetUnorderedAccessViews(0, 2, pUAVs, nullptr); // 数据copy // 读取 deviceContext->CopyResource(m_pTempBuffer.Get(), m_pCurrVertex.Get()); D3D11_MAPPED_SUBRESOURCE rsSrc; VertexPosNormalTex* dataSrc; deviceContext->Map(m_pTempBuffer.Get(), 0, D3D11_MAP_READ, 0, &rsSrc); dataSrc = (VertexPosNormalTex*)rsSrc.pData; deviceContext->Unmap(m_pTempBuffer.Get(), 0); // 写入 D3D11_MAPPED_SUBRESOURCE rsDest; deviceContext->Map(m_pVertexBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &rsDest); memcpy_s(rsDest.pData, m_VertexCount * sizeof(VertexPosNormalTex), dataSrc, m_VertexCount * sizeof(VertexPosNormalTex)); deviceContext->Unmap(m_pVertexBuffer.Get(), 0); m_AccumulateTime = 0.0f; // 重置时间 } }
绘制
此处绘制使用了DirectX11 With Windows SDK--28 计算着色器:波浪(水波) 提供的BasicEffect类。
// 绘制 void Draw(ID3D11DeviceContext* deviceContext, BasicEffect& effect);
void GerstnerWavesRender::Draw(ID3D11DeviceContext* deviceContext, BasicEffect& effect) { UINT strides[1] = { sizeof(VertexPosNormalTex) }; UINT offsets[1] = { 0 }; deviceContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), strides, offsets); deviceContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0); effect.SetWavesStates(false); effect.SetMaterial(m_Material); effect.SetTextureDiffuse(m_pTextureDiffuse.Get()); effect.SetWorldMatrix(m_Transform.GetLocalToWorldMatrixXM()); effect.SetTexTransformMatrix(XMMatrixScaling(m_TexU, m_TexV, 1.0f) * XMMatrixTranslationFromVector(XMLoadFloat2(&m_TexOffset))); effect.Apply(deviceContext); deviceContext->DrawIndexed(m_IndexCount, 0, 0); }
GerstnerWave模拟演示
其中包括了用于方便调整参数的IMGUI,这里暂时不做IMGUI的相关实现演示。