粒子系统与雨的效果 (DirectX11 with Windows SDK)
前言
最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用Windows SDK来实现.
顺手安利一波:我最近在学DirectX11 with Windows SDK 教程
博客地址:https://www.cnblogs.com/X-Jun/p/9028764.html
所以下面的都是基于这个教程和上面提到的那本书来写的
推荐看到教程17章和书里第15章之后再来看这篇博客,有助于更好的理解
特别要是理解好渲染管线,要多使用图形调试发现问题 (上面博客有教
PS:这篇博客记录下我怎么去实现一个粒子系统并且用一个粒子系统去实现一个简单的雨特效和记录下自己的理解,一些具体知识点并不会在此提到.
粒子系统
粒子属性:
1.粒子生成速度(即单位时间粒子生成的数量)
2.粒子初始速度向量
3.粒子寿命
4.粒子颜色
5.其他一些特定的参数
粒子系统更新循环:模拟阶段和绘制阶段
粒子系统使用Billboard技术来进行纹理映射和图元渲染
定义粒子的顶点结构
struct ParticleVertex
{
XMFLOAT3 initialPos; //粒子的初始中心位置
XMFLOAT3 initialVel; //粒子的初始速度
XMFLOAT3 size; //粒子的大小
float age; //粒子的存活时间
uint type; //粒子的类型
}
这里粒子的类型包括触发器粒子和渲染粒子
1.触发器粒子在粒子系统中只有一个,用于粒子的生成,不会被绘制
粒子的位置函数(推导过程忽略)
p(t)=(1/2)*a*t^2+v0*t+p0
每个粒子有自己的随机行为,所以我们引用了一个工具类--Random类来为粒子创建一些随机函数,下面的雨的实现就是利用了这个Random类来产生随机位置
产生随机纹理的函数和着色器里面的实现在这就不详细讲了,因为在书上有具体代码
若没有实现随机位置,雨就只有一条直线

雨的粒子特效实现
用雨的特效实现来理解粒子系统
ParticleEffect(粒子框架)
这个框架根据博客里面的BasicEffect进行编写的
我们先来看着色器
着色器头文件(Particle.hlsli)
Texture2D g_Tex : register(t0);
Texture1D g_Random : register(t1);
SamplerState g_Sam : register(s0);
cbuffer CBChangesEveryFrame : register(b0)
{
float3 gEmitPosW;
float gGameTime;
float3 gEmitDirW;
float gTimeStep;
matrix g_View;
}
cbuffer CBChangesOnResize : register(b1)
{
matrix g_Proj;
}
struct Particle
{
float3 InitialPosW : POSITION;
float3 InitialVelW : VELOCITY;
float2 SizeW : SIZE;
float Age : AGE;
uint Type : TYPE;
};
struct Vertexout
{
float3 PosW : POSITION;
uint Type : TYPE;
};
struct GeoOut
{
float4 PosH : SV_POSITION;
float2 Tex : TEXCOORD;
};
一.因为绘制粒子的着色器和原来的不同,所以这里用到了两个顶点着色器,两个几何着色器,一个像素着色器

1.首先两个顶点着色器的输入布局都是相同的
const D3D11_INPUT_ELEMENT_DESC Particle::inputLayout[5] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "VELOCITY", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "SIZE", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "AGE", 0, DXGI_FORMAT_R32_FLOAT, 0, 32, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TYPE", 0, DXGI_FORMAT_R32_UINT, 0, 36, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
着色器头文件中另一个结构的布局如下:
const D3D11_INPUT_ELEMENT_DESC Vertexout::inputLayout[2] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TYPE", 0, DXGI_FORMAT_R32_UINT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
第一个顶点着色器(Particle_VS.hlsl)没有做任何处理,然后第二个(ParticleDW_VS.hlsl)即为实现上面那个粒子的位置函数
2.然后下一步到几何着色器做处理,第一个几何着色器(ParticleSO_GS.hlsl)是用于流输出更新粒子的,第二个(ParticleDW_GS.hlsl是实现用于渲染粒子
1.第一个几何着色器(ParticleSO_GS.hlsl)代码
[maxvertexcount(6)]
void GS(
point Particle gin[1],
inout PointStream<Particle> ptStream)
{
gin[0].Age += gTimeStep;
if(gin[0].Type==0)
{
if (gin[0].Age >= 0.002f)
{
for (int i = 0; i < 5;i++)
{
float3 vRandom = 35.0f * RandVec3((float) i / 5.0f);
vRandom.y = 20.0f;
Particle p;
p.InitialPosW = gEmitPosW.xyz + vRandom;
p.InitialVelW = float3(0.0f, 0.0f, 0.0f);
p.SizeW = float2(1.0f, 1.0f);
p.Age = 0.0f;
p.Type = 1;
ptStream.Append(p);
}
//重置发射时间
gin[0].Age = 0.0f;
}
//总是保持发射器
ptStream.Append(gin[0]);
}
else
{
**//指定保存粒子的条件;age>3.0f即销毁粒子**
if (gin[0].Age <= 3.0f)
ptStream.Append(gin[0]);
}
}
第二个几何着色器(ParticleDW_GS.hlsl)
//公告板技术
#include"Particle.hlsli"
[maxvertexcount(2)]
void GS(
point Vertexout gin[1],
inout LineStream<GeoOut> lineStream)(这里是线图元,因为是雨)
{
//不要绘制发射器粒子。
if(gin[0].Type!=0)
{
//向加速度方向倾斜的直线。
float3 po = gin[0].PosW;
float3 p1 = gin[0].PosW + 0.07f * (float3(-1.0f, -9.8f, 0.0f));
matrix viewProj = mul(g_View, g_Proj);
GeoOut v0;
v0.PosH = mul(float4(po, 1.0f), viewProj);
v0.Tex = float2(0.0f, 0.0f);
lineStream.Append(v0);
GeoOut v1;
v1.PosH = mul(float4(p1, 1.0f), viewProj);
v1.Tex = float2(1.0f, 1.0f);
lineStream.Append(v1);
}
}
3.最后的像素着色器就实现简单的纹理采样
PS:最后说明下这只是针对雨粒子的着色器具体实现,不同的粒子有不同的着色器具体实现
二丶设置绘制状态(相当于把这些东西绑定到渲染管线上)
1.流输出更新粒子的绘制状态
void ParticleEffect::SetRenderStreamOutParticle(ComPtr<ID3D11DeviceContext> deviceContext, ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
UINT stride = sizeof(Particle);
UINT offset = 0;
ID3D11Buffer* nullBuffer = nullptr;
deviceContext->SOSetTargets(1, &nullBuffer, &offset);
deviceContext->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_POINTLIST);
deviceContext->IASetInputLayout(pImpl->m_pParticleLayout.Get());
deviceContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);
deviceContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);
deviceContext->VSSetShader(pImpl->m_pParticleVS.Get(), nullptr, 0);
deviceContext->GSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->GSSetShader(pImpl->m_pParticleSOGS.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSWireframe.Get());
deviceContext->PSSetShader(nullptr, nullptr, 0);
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(RenderStates::BSAdditiveP.Get(), nullptr, 0xFFFFFFFF);
}
上面的主要是把下面的着色器绑定到渲染管线上并且开启流输出


并且要为GS设置一个采样器,因为在GS着色器中会用到采样器,采样器进行1D随机纹理采样产生[-1,1]的3D向量来产生大范围随机位置
若对给定的2D纹理(如这个雨的纹理)用SampleLevel方法,容易会产生下面的效果

这是因为对雨的纹理采样,所采样得到的位置是固定的(雨的纹理信息是固定的),所以产生了上面这种狭小范围的雨
因初始化生成了随机1D纹理进行采样产生大范围的随机位置
2.默认的绘制状态
void ParticleEffect::SetRenderDefault(ComPtr<ID3D11DeviceContext> deviceContext)
{
deviceContext->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_POINTLIST);
deviceContext->IASetInputLayout(pImpl->m_pParticleLayout.Get());
deviceContext->VSSetShader(pImpl->m_pParticleDWVS.Get(), nullptr, 0);
// 关闭流输出
deviceContext->GSSetShader(pImpl->m_pParticleDWGS.Get(), nullptr, 0);
ID3D11Buffer* bufferArray[1] = { nullptr };
UINT offset = 0;
deviceContext->SOSetTargets(1, bufferArray, &offset);
deviceContext->RSSetState(RenderStates::RSWireframe.Get());
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->PSSetShader(pImpl->m_pParticlePS.Get(), nullptr, 0);
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(RenderStates::BSAdditiveP.Get(), nullptr, 0xFFFFFFFF);
}
上面的代码表示把下面的这些着色器绑定到渲染管线上


因为上面有用到流输出,所以我们要关闭了这个流输出
PS:光栅化用线框模式,不用深度测试,混合状态用下面的混合公式:
Color = SrcAlpha *SrcColor + DestColor
不同的粒子会使用不同的混合公式,是具体情况而视
三丶应用缓冲区、纹理资源和进行更新
直接看函数把
void ParticleEffect::Apply(ComPtr<ID3D11DeviceContext> deviceContext)
{
auto& pCBuffers = pImpl->m_pCBuffers;
// 将缓冲区绑定到渲染管线上
pCBuffers[0]->BindGS(deviceContext);
pCBuffers[1]->BindGS(deviceContext);
// 设置SRV
deviceContext->PSSetShaderResources(0, 1, pImpl->m_pTexture.GetAddressOf()); //把资源视图绑定到PS上,并且对于槽(t0)
deviceContext->GSSetShaderResources(1, 1, pImpl->m_pRamTexture.GetAddressOf()); //把资源视图绑定到GS上,并且对于槽(t1)
if (pImpl->m_IsDirty)
{
pImpl->m_IsDirty = false;
for (auto& pCBuffer : pCBuffers)
{
pCBuffer->UpdateBuffer(deviceContext);
}
}
}
四丶我们弄好了粒子的框架,那么我们怎么去实现粒子的更新和绘制呢?
首先我们要更新每帧改变的常量缓冲区
cbuffer CBChangesEveryFrame : register(b0)
{
float3 gEmitPosW;
float gGameTime;
float3 gEmitDirW;
float gTimeStep;
matrix g_View;
}
在UpdateScene函数中我们要更新这个缓冲区里面的数据,在ParticleEffect实现一些方法,然后再UpdateScene里面调用它们
然后我们每次调用绘制时使用Effect框架Apply方法来应用缓冲区、纹理资源并进行更新
下面便是实现粒子绘制
1.首先我们要创建3个顶点缓冲区,一个用于初始化,另两个用于更新和绘制
// 设置顶点缓冲区描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_DEFAULT; // 这里需要允许流输出阶段通过GPU写入
vbd.ByteWidth = sizeof(Particle);
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER ;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
Particle p;
ZeroMemory(&p, sizeof(Particle));
p.Age = 0.0f;
p.Type = 0;
// 新建顶点缓冲区
D3D11_SUBRESOURCE_DATA InitData;
InitData.pSysMem = &p;
HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffers[0].ReleaseAndGetAddressOf())); //用于初始化
vbd.ByteWidth = sizeof(Particle)*m_MaxParticles; //m_maxparticle是最大的粒子数
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;
HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[1].ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreateBuffer(&vbd, nullptr, m_pVertexBuffers[2].ReleaseAndGetAddressOf()));
2.然后进行绘制函数的实现,下面是进行一个绘制过程理解
(1)第一帧初始化时首先先设置流输出绘制状态
m_ParticleEffect.SetRenderStreamOutParticle(m_pd3dImmediateContext, m_pVertexBuffers[0], m_pVertexBuffers[1]);
(2)然后使用
m_pd3dImmediateContext->Draw(1, 0);
把粒子实现更新(初始化),即把更新后的粒子数据流输出到第二个缓冲区
(3)最后设置设置默认状态并用
m_ParticleEffect.SetRenderDefault(m_pd3dImmediateContext);
m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffers[inputIndex].GetAddressOf(), &stride, &offset);
当前的inputIndex为1,即表示第二个缓冲区绑定到渲染管线上,然后使用
m_pd3dImmediateContext->DrawAuto();
用第二个缓冲区(即更新后的)来实现粒子的绘制
PS:从上面博客的流输出篇,可以理解到DrawAuto要求第一次流输出绘制时使用Draw、DrawIndexed系列的方法,后面使用DrawAuto即可以不使用参数来正确绘制
3.(1)第二帧开始可以便不再使用第一个顶点缓冲区,只要第二和第三个即可以了
然后就设置流输出状态绘制(即更新)(inputIndex当前为1)
m_ParticleEffect.SetRenderStreamOutParticle(m_pd3dImmediateContext, m_pVertexBuffers[inputIndex], m_pVertexBuffers[inputIndex % 2 + 1]);
(2)然后使用DrawAuto把第二个缓冲区更新数据流输出到第三个缓冲区
(3)最后绑定第三个顶点缓冲区调用DrawAuto来进行绘制
4.后面的帧数只是把第二和第三个顶点缓冲区不断进行和第二帧一样的步骤
用最新的缓冲区数据设置流输出绘制流输出更新到另一个缓冲区,这时另一个缓冲区变为最新的缓冲区,然后设置默认绘制并绑定最新的缓冲区来进行绘制
具体的C++代码自己实现下,毕竟踩过的坑才是自己的
最后便能用粒子系统实现雨的效果,下面是效果显示

作者:Ligo丶
出处:https://www.cnblogs.com/Ligo-Z/
本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。
粒子系统与雨的效果 (DirectX11 with Windows SDK)的更多相关文章
- DirectX11 With Windows SDK--35 粒子系统
前言 在这一章中,我们主要关注的是如何模拟一系列粒子,并控制它们运动.这些粒子的行为都是类似的,但它们也带有一定的随机性.这一堆粒子的几何我们叫它为粒子系统,它可以被用于模拟一些比较现象,如:火焰.雨 ...
- DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果
前言 上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区.接下来我们继续利用它来实现一些新的效果,在这一章,你将了解: 实现公告板效果 Alpha-To-Coverage 对 ...
- DirectX11 With Windows SDK--10 摄像机类
前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html 由于考虑后续的项目需要有一个比较好的演示环境 ...
- DirectX11 With Windows SDK--25 法线贴图
前言 在很早之前的纹理映射中,纹理存放的元素是像素的颜色,通过纹理坐标映射到目标像素以获取其颜色.但是我们的法向量依然只是定义在顶点上,对于三角形面内一点的法向量,也只是通过比较简单的插值法计算出相应 ...
- DirectX11 With Windows SDK--24 Render-To-Texture(RTT)技术的应用
前言 尽管在上一章的动态天空盒中用到了Render-To-Texture技术,但那是针对纹理立方体的特化实现.考虑到该技术的应用层面非常广,在这里抽出独立的一章专门来讲有关它的通用实现以及各种应用. ...
- DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现
前言 上一章的静态天空盒已经可以满足绝大部分日常使用了.但对于自带反射/折射属性的物体来说,它需要依赖天空盒进行绘制,但静态天空盒并不会记录周边的物体,更不用说正在其周围运动的物体了.因此我们需要在运 ...
- DirectX11 With Windows SDK--01 DirectX11初始化
前言 由于个人觉得龙书里面第4章提供的Direct3D 初始化项目封装得比较好,而且DirectX SDK Samples里面的初始化程序过于精简,不适合后续使用,故选择了以Init Direct3D ...
- DirectX11 With Windows SDK--07 添加光照与常用几何模型
前言 对于3D游戏来说,合理的光照可以让游戏显得更加真实.接下来会介绍光照的各种分量,以及常见的光照模型.除此之外,该项目还用到了多个常量缓冲区,因此还会提及HLSL的常量缓冲区打包规则以及如何设置多 ...
- DirectX11 With Windows SDK--08 Direct2D与Direct3D互操作性以及利用DWrite显示文字
前言 注意:从这一章起到后面的所有项目无一例外都利用了Direct2D与Direct3D互操作性,但系统要求为Win10, Win8.x 或 Win7 SP1且安装了KB2670838补丁以支持Dir ...
随机推荐
- [luogu] zpl的数学题1
https://www.luogu.org/problemnew/show/U16887 $f[1] + f[2] + f[3] + .... + f[n] = f[n + 2] - 1$ 矩阵快速幂 ...
- Educational Codeforces Round 75
目录 Contest Info Solutions A. Broken Keyboard B. Binary Palindromes C. Minimize The Integer D. Salary ...
- 幽默的讲解六种Socket I/O模型
很幽默的讲解六种Socket I/O模型 本文简单介绍了当前Windows支持的各种Socket I/O模型,如果你发现其中存在什么错误请务必赐教. 一:select模型 二:WSAAsyncSele ...
- 去掉 webstorm 灰色的数据类型提示
- Hadoop Aggregate Resource Allocation解释
1.在hadoop里面运行程序的时候,查看某个任务的具体信息如下: [hadoop@master monitor]$ yarn application -list 如上图,这里面的Aggregate ...
- MySQL表连接
有3种: JOIN 按照功能大致分为如下三类: CROSS JOIN (交叉连接) INNER JOIN (内连接或等值连接). OUTER JOIN (外连接) 交叉连接CROSS JOIN 交叉连 ...
- linux tcp 高并发最大连接数
Linux下高并发socket最大连接数所受的限制问题 1.修改用户进程可打开文件数限制 在Linux平台上,无论编写客户端程序还是服务端程序,在进行高并发TCP连接处理时,最高的并发数量都要受到系统 ...
- CMMI基础知识扫盲
CMMI全称是Capability Maturity Model Integration,CMMI是个好东西来的,但行内人士对她的认识并不全面,甚至有种种的误解.尽管网上有很多CMMI相关介绍,但一般 ...
- 设计自用的golang日志模块
设计自用的golang日志模块 golang的原生日志模块不能满足需求,而开源的第三方包,也不完全够用.用户较多的logrus,却没有rotate功能,这已经是众所周知的.对于运维来说,当然是希望日志 ...
- NTC热敏电阻基础以及应用和选择(转)
源:NTC热敏电阻基础以及应用和选择 NTC被称为负温度系数热敏电阻,是由Mn-Co-Ni的氧化物充分混合后烧结而成的陶瓷材料制备而来,它在实现小型化的同时,还具有电阻值-温度特性波动小.对各种温度变 ...