zoukankan      html  css  js  c++  java
  • DirectX11——粒子系统

    DirectX11 粒子系统

    前言

    粒子系统表示三维计算机图形学中模拟一些特定的模糊现象的技术,而这些现象用其它传统的渲染技术难以实现真实感的物理运动规律。(百度百科)

    我们经常使用粒子系统来模拟火焰、爆炸、烟雾等现象,在Unity3D中就有一套成熟粒子系统以供游戏开发者使用。最近在学习使用DirectX11中的着色器反射来构建粒子系统渲染框架,并尝试使用这一套框架实现爆炸和喷泉特效。

    本人学习的DirectX11是基于Windows SDK实现的,本次粒子系统的所有前置知识以及粒子系统的实现均来自DirectX11 with Windows SDK教程:https://www.cnblogs.com/X-Jun/p/9028764.html

    本次粒子系统的框架也使用了https://github.com/MKXJun/DirectX11-With-Windows-SDK中第35个项目中的ParticleEffect框架ParticleRender

    涵盖知识

    粒子系统的实现属于教程中高级部分的内容,需要的知识比较全面,具体请看思维导图:

     

    总结一下:着色器的书写与C++中创建着色器、流输出阶段、利用着色器反射修改缓冲区数据、混合状态和深度/模板测试、利用几何着色器实现公告板效果。

    通过粒子系统的实现学习,可以学习到:

    • 利用几何着色器和流输出阶段高效产生和储存粒子

    • 使用物理知识来模拟物理运动规律

    • 锻炼设计渲染框架的能力

    粒子系统

    粒子系统需要较多的属性,不同的粒子系统会拥有不同的属性,在通用的粒子系统中,我们定义以下属性:

    1、发射位置

    2、发射方向

    3、发射间隔

    4、粒子存活时间

    5、系统时长

    6、系统步长

    7、最大粒子数目

    8、观察位置和观察矩阵

    这些属性应该不难理解,观察位置和观察矩阵在其他渲染系统中也是必备的。

    粒子

    粒子本身也有不少的属性,通用的粒子属性如下:

    struct ParticleVertex
    {
        XMFLOAT3 initialPos;   // 初始位置
        XMFLOAT3 initialVel;   // 初始速度
        XMFLOAT3 size;         // 粒子大小
        float age;             // 粒子年龄
        uint type;             // 粒子类型
    }

    粒子类型包括发射器粒子普通发射粒子,发射器粒子会根据自身年龄(解释为年龄不太合适,因为它会不断地循环置零)来判断是否发射新粒子,发射新粒子后重新积累时间,又一次达到系统的发射间隔时发射新粒子。在几何着色器中,发射粒子总是被输出到顶点缓冲区,如果没控制好粒子最大数量、粒子发射间隔和粒子存活时间,缓冲区无法发容纳射器粒子,粒子系统就会消失(没有粒子)。

    随机数值

    我们可以在C++使用random来产生随机数,但在HLSL中并没有产生随机数的方法,因此我们需要自己定义一些产生随机数的方法。通常情况下,对一个Texture1D资源进行不同位置的采样可以达到获得随机数的效果。创建一个1D纹理,里面每个元素是float4DXGI_FORMAT_R32G32B32A32_FLOAT),然后我们使用区间[-1, 1]的随机4D向量来填满纹理,采样的时候则使用wrap寻址模式即可。采样的结果也是在[-1, 1],可根据需要通过计算使之落在指定范围。

    下面是获得随机单位向量的方法:

    float3 RandUnitVec3(float offset)
    {
        // 使用游戏时间加上偏移值来从随机纹理采样
        float u = (g_GameTime + offset);
        // 分量均在[-1,1]
        float3 v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).xyz;
        // 标准化向量
        return normalize(v);
    }
     

    接下来的爆炸喷泉特效的简单模拟,在这里只讲解主要的步骤。

     

    喷泉

    喷泉系统:粒子从某个点产生,并沿着圆锥体范围内的随机方向向上发射,最终重力会使得它们掉落到地面。

    重力的模拟比较简单,在Fountain.hlsli中定义重力加速度G,在计算粒子位置时使用物理匀加速直线运动位移公式:x = 1/2 * a * t * t + v * t计算位移,加上初始位置即可。

    // Fountain.hlsli
    ...
    cbuffer CBFixed : register(b1)
    {
        // 重力加速度
        float3 g_G = float3(0.0f, -9.8f, 0.0f);
        
        // 纹理坐标
        float2 g_QuadTex[4] =
        {
            float2(0.0f, 1.0f),
            float2(1.0f, 1.0f),
            float2(0.0f, 0.0f),
            float2(1.0f, 0.0f)
        };
    }
    ...
    ​
    // Fountain_VS.hlsl
    VertexOut VS(VertexParticle vIn)
    {
        VertexOut vOut;
        
        float t = vIn.Age;
        
        // 恒定加速度等式
        vOut.PosW = 0.5f * t * t * g_G + t * vIn.InitialVelW + vIn.InitialPosW;
        
        // 颜色随着时间褪去
        float opacity = 1.0f - smoothstep(0.0f, 1.0f, t / 1.0f);
        vOut.Color = float4(1.0f, 1.0f, 1.0f, opacity);
        
        vOut.SizeW = vIn.SizeW;
        vOut.Type = vIn.Type;
        
        return vOut;
    }
    ​

    在随机数部分已经提及,我们定义了产生随机单位向量的方法:

    float3 RandUnitVec3(float offset)
    {
        // 使用游戏时间加上偏移值来从随机纹理采样
        float u = (g_GameTime + offset);
        // 分量均在[-1,1]
        float3 v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).xyz;
        // 标准化向量
        return normalize(v);
    }

    在产生新粒子我们可以使用以下计算使向量集中在圆锥区域:

    float3 vRandom = RandUnitVec3(offset);
    vRandom.x *= 0.5f;  // 可根据圆锥的范围乘上特定值使向量集中在指定区域
    vRandom.z *= 0.5f;
    vRandom.y = sqrt(1 - vRandom.x * vRandom.x - vRandom.z * vRandom.z); // 落在单位圆上

    在模拟喷泉中发现,一帧发射一个粒子的效果仍然很差,需要使用循环来产生更多的粒子。

    注意:几何着色器的每次调用最多只能处理1024个标量

    // Fountain_SO_GS.hlsl
    #include " Fountain.hlsli"
    ​
    [maxvertexcount(4)]
    void GS(point VertexParticle gIn[1], inout PointStream<VertexParticle> output)
    {
        gIn[0].Age += g_TimeStep;
        
        if (gIn[0].Type == PT_EMITTER)
        {
            // 是否到时间发射新的粒子
            if (gIn[0].Age > g_EmitInterval)
            {
            
                // for循环产生更多粒子,以三个粒子为例
                for (int i = 0; i < 3; i++)
                {
                    float3 vRandom = RandUnitVec3(g_GameTime * i);  // 不同的偏移量进行采集
                    vRandom.x *= 0.5f;  // 可根据圆锥的范围乘上特定值使向量集中在指定区域
                    vRandom.z *= 0.5f;
                    vRandom.y = sqrt(1 - vRandom.x * vRandom.x - vRandom.z * vRandom.z);// 落在单位圆上
                
                    VertexParticle p;
                    p.InitialPosW = g_EmitPosW.xyz;
                    p.InitialVelW = 4.0f * vRandom;
                    p.SizeW       = float2(3.0f, 3.0f);
                    p.Age         = 0.0f;
                    p.Type        = PT_FLARE;
                
                    output.Append(p);
                
                }
                
                // 重置时间准备下一次发射
                gIn[0].Age = 0.0f;
            }
            
            // 总是保留发射器
            output.Append(gIn[0]);
        }
        else
        {
            // 用于限制粒子数目产生的特定条件,对于不同的粒子系统限制也有所变化
            if (gIn[0].Age <= g_AliveTime)
                output.Append(gIn[0]);
        }
    }
    ​

    爆炸

    发射器粒子产生N个随机方向的外壳粒子。在经过一个短暂时间后,每个外壳粒子应当爆炸产生M个粒子。每个外壳不需要在同一个时间发生爆炸——通过随机性赋上不同的爆炸倒计时。

    在这里为了方便观察将N和M设为10,保存发射器粒子观察随机性。

    与喷泉相比爆炸系统更加复杂,发射器会发射两种粒子,故我们需要添加第三种粒子:外壳粒子PT_SHELL。外壳粒子会随机时间爆炸,故我们需要获得一个随机数来定义爆炸时间。我们先定义一个方法来获得[-1, 1]间的随机数。

    float RandNum(float offset)
    {
        // 使用游戏时间加上偏移值来从随机纹理采样
        float u = (g_GameTime + offset);
        // 在[-1,1]
        float v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).x;
       
        return v;
    }

    再通过以下计算使其落在粒子的最大存活时间内:

    float Randomtime = (RandNum(i * g_GameTime) + 1) * 0.5f * g_AliveTime;

    外壳粒子的初始年龄(Age)就是粒子存活时间减去获得的随机爆炸时间:

    p.Age = g_AliveTime - Randomtime;

    这样就可以在外壳粒子生命周期结束时产生新的普通粒子。

    完整的Explosion_SO_GS如下:

    #include "Explosion.hlsli"
    ​
    [maxvertexcount(11)]
    void GS(point VertexParticle gIn[1], inout PointStream<VertexParticle> output)
    {
        gIn[0].Age += g_TimeStep;
        
        if (gIn[0].Type == PT_EMITTER)
        {
            // 是否到时间发射新的粒子
            if (gIn[0].Age > g_EmitInterval)
            {
                uint ShellParticleCount = 10;
                
                [unroll]
                for (int i = 0; i < ShellParticleCount; i++)
                {
                    float3 vRandom = RandUnitVec3(i * g_GameTime);
                    float Randomtime = (RandNum(i * g_GameTime) + 1) * 0.5f * g_AliveTime;
                    
                    VertexParticle p;
                    p.InitialPosW = g_EmitPosW.xyz;
                    p.InitialVelW = 5.0f * vRandom;
                    p.SizeW = float2(2.0f, 2.0f);
                    p.Age = g_AliveTime - Randomtime;
                    p.Type = PT_SHELL;
                
                    output.Append(p);
                    
                }
                          
                // 重置时间准备下一次发射
                gIn[0].Age = 0.0f;
            }
            
            // 总是保留发射器
            output.Append(gIn[0]);
        }
        else if (gIn[0].Type == PT_SHELL)
        {
            if (gIn[0].Age > g_AliveTime)
            {
                uint FlareParticleCount = 10;
                    
                [unroll]
                for (int i = 0; i < FlareParticleCount; i++)
                {
                    float3 vRandom = RandUnitVec3(i * g_GameTime * 0.9f);
                    
                    VertexParticle p;
                    p.InitialPosW = gIn[0].InitialPosW;
                    p.InitialVelW = 10.0f * vRandom;
                    p.SizeW = float2(1.0f, 1.0f);
                    p.Age = 0;
                    p.Type = PT_FLARE;
                
                    output.Append(p);
                    
                }
            }
            else
            {
                gIn[0].InitialPosW = gIn[0].InitialPosW + g_TimeStep * gIn[0].InitialVelW;
                gIn[0].InitialVelW = gIn[0].InitialVelW * (1 + g_AccelW  * g_TimeStep);
                
                output.Append(gIn[0]);
            }
        }
        else
        {
            // 用于限制粒子数目产生的特定条件,对于不同的粒子系统限制也有所变化
            if (gIn[0].Age <= g_AliveTime)
            {
                gIn[0].InitialPosW = gIn[0].InitialPosW + g_TimeStep * gIn[0].InitialVelW;
                gIn[0].InitialVelW = gIn[0].InitialVelW * (1 + g_AccelW * g_TimeStep);
                
                output.Append(gIn[0]);
            }
        }
    }

    你会发现在这个系统内粒子的位置是不断更新的,在不改动渲染框架的前提下,我将粒子属性中InitialPosWInitialVelW重新定义为当前位置和当前速度, 将原本在VS中计算的粒子位置迁移到SO_GS中,并将新的位置和速度信息保存起来,以便在外壳粒子”爆炸“时可以拿到它的位置信息。更好的处理方法是修改粒子的属性,添加当前位置,当然,这样的处理得修改ParticleEffect

    在位置和速度计算中,g_AccelW是粒子运动中受到的空气阻力产生的加速度,忽略重力影响。

    演示

    下面的动图演示喷泉和爆炸效果:

    作者:YIMG
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    基于前后端分离的身份认证方式——JWT
    java远程文件操作
    BZOJ3181: [Coci2012]BROJ
    回归本源--位运算及其应用
    BZOJ 1226: [SDOI2009]学校食堂Dining
    BZOJ2734: [HNOI2012]集合选数
    BZOJ2064: 分裂
    BZOJ2679: [Usaco2012 Open]Balanced Cow Subsets
    OI队内测试——石门一
    Codeforces Round #376 (Div. 2)
  • 原文地址:https://www.cnblogs.com/YIMG/p/13398966.html
Copyright © 2011-2022 走看看