粒子系统与雨的效果 (DirectX11 with Windows SDK)

前言

最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用Windows SDK来实现.

顺手安利一波:我最近在学DX11 with WindowSDK 教程

博客地址:
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 },
};
第一个顶点着色器(ParticleSO_VS.hlsl)没有做任何处理,然后第二个(ParticleSO_VS.hlsl)即为实现上面那个粒子的位置函数
2.然后下一步到几何着色器做处理,第一个几何着色器(ParticleSO_GS.hlsl)是用于流输出更新粒子的,第二个(ParticleDW_GS.hlsl是实现用于渲染粒子

1.第一个几何着色器代码

[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]);
    }
}

第二个几何着色器

//公告板技术
#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]的向量

若用采用了2D的纹理SampleLevel方法(如雨的纹理),容易会产生下面的效果

因为这是在一个固定的范围之内进行了采样,导致了在雨的范围狭小(我的理解),详情可以查官方文档了解

用一开始提到的书里面的代码便可以解决

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);

当前的inpurIndex为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++代码自己实现下,毕竟踩过的坑才是自己的(哭兮兮)

最后便能用粒子系统实现雨的效果,下面是效果显示

后言:有机会再写关于自己学习用DX11和HLSL实现一些效果的博客(咕咕咕

原文地址:https://www.cnblogs.com/Ligo-Z/p/11216135.html

时间: 2024-08-29 21:05:45

粒子系统与雨的效果 (DirectX11 with Windows SDK)的相关文章

DirectX11 With Windows SDK 24--Render-To-Texture(RTT)技术的应用、使用ScreenGrab保存纹理到文件

前言 尽管在上一章的动态天空盒中用到了Render-To-Texture技术,但那是针对纹理立方体的特化实现.考虑到该技术的应用层面非常广,在这里抽出独立的一章专门来讲有关它的通用实现以及各种应用.此外,这里还会讲到如何使用DirectXTex的ScreenGrab来保存纹理,可以说是干货满满了. 如果想要看Render-To-Texture在动态天空盒的应用,可以点此回顾: 章节 23 立方体映射:动态天空盒的实现 DirectX11 With Windows SDK完整目录 Github项目

DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果

前言 上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区.接下来我们继续利用它来实现一些新的效果,在这一章,你将了解: 实现公告板效果 Alpha-To-Coverage 对GPU资源进行读/写操作 纹理数组 实现雾效 DirectX11 With Windows SDK完整目录 Github项目源码 实现雾效 虽然这部分与几何着色器并没有什么关系,但是雾的效果在该Demo中会用到,并且前面也没有讲过这部分内容,故先在这里提出来. 有时候我们需要在游戏中模拟一些特定的天

DirectX11 With Windows SDK--11 混合状态与光栅化状态

前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html 虽然这一部分的内容主要偏向于混合(Blending),但这里还需提及一下,关于渲染管线可以绑定的状态主要有如下三种: 光栅化状态(光栅化阶段) 混合状态(输出合并阶段) 深度/模板状态(输出合并阶段) Direct3D是基于状态机的,我们可以通过修改这些状态来修改渲染管线的当前行为. 实际上这一章会讲述光栅化状态和混合状态这两个部分,在后续的

DirectX11 With Windows SDK--12 深度/模板状态

前言 DirectX11 With Windows SDK完整目录:http://www.cnblogs.com/X-Jun/p/9028764.html 深度/模板测试使用的是与后备缓冲区同等分辨率大小的缓冲区,每个元素的一部分连续位用于深度测试,其余的则用作模板测试.两个测试的目的都是为了能够根据深度/模板状态需求的设置来选择需要绘制的像素. 项目源码点此:https://github.com/MKXJun/DX11-Without-DirectX-SDK 深度/模板测试 深度测试.模板测试

DirectX11 With Windows SDK--15 几何着色器初探

# 前言 从这一部分开始,感觉就像是踏入了无人深空一样,在之前初学DX11的时候,这部分内容都是基本上跳过的,现在打算重新认真地把它给拾回来. [DirectX11 With Windows SDK完整目录](http://www.cnblogs.com/X-Jun/p/9028764.html) [Github项目源码](https://github.com/MKXJun/DirectX11-With-Windows-SDK) # 几何着色器 首先用一张图来回顾一下渲染管线的各个阶段,目前为止

DirectX11 With Windows SDK--16 利用几何着色器可选的流输出阶段帮助绘制多种分形

前言 在上一章,我们知道了如何使用几何着色器来重新组装图元,比如从一个三角形分裂成三个三角形.但是为了实现更高阶的分形,我们必须要从几何着色器拿到输出的顶点.这里我们可以使用可选的流输出阶段来拿到顶点集合. 注意: 本章末尾有大量的GIF动图! DirectX11 With Windows SDK完整目录 Github项目源码 流输出阶段 现在我们知道GPU可以写入纹理(textures),例如深度/模板缓冲区以及后备缓冲区.当然,我们也可以通过渲染管线的流输出阶段让GPU将几何着色器输出的顶点

DirectX11 With Windows SDK--20 硬件实例化与视锥体裁剪

前言 这一章将了解如何在DirectX 11利用硬件实例化技术高效地绘制重复的物体,以及使用视锥体裁剪技术提前将位于视锥体外的物体进行排除. 在此之前需要额外了解的章节如下: 章节回顾 18 使用DirectXCollision库进行碰撞检测 19 模型加载:obj格式的读取及使用二进制文件提升读取效率 DirectX11 With Windows SDK完整目录 Github项目源码 硬件实例化(Hardware Instancing) 硬件实例化指的是在场景中绘制同一个物体多次,但是是以不同

DirectX11 With Windows SDK--23 立方体映射:动态天空盒的实现

前言 上一章的静态天空盒已经可以满足绝大部分日常使用了.但对于自带反射/折射属性的物体来说,它需要依赖天空盒进行绘制,但静态天空盒并不会记录周边的物体,更不用说正在其周围运动的物体了.因此我们需要在运行期间构建动态天空盒,将周边物体绘制入当前的动态天空盒. 没了解过静态天空盒的读者请先移步到下面的链接: 章节回顾 22 立方体映射:静态天空盒的读取与实现 DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11

DirectX11 With Windows SDK--25 法线贴图

前言 在很早之前的纹理映射中,纹理存放的元素是像素的颜色,通过纹理坐标映射到目标像素以获取其颜色.但是我们的法向量依然只是定义在顶点上,对于三角形面内一点的法向量,也只是通过比较简单的插值法计算出相应的法向量值.这对平整的表面比较有用,但无法表现出内部粗糙的表面.在这一章,你将了解如何获取更高精度的法向量以描述一个粗糙平面. DirectX11 With Windows SDK完整目录 Github项目源码 法线贴图 法线贴图是指纹理中实际存放的元素通常是经过压缩后的法向量,用于表现一个表面凹凸