主頁 >  其他 > DirectX11 With Windows SDK--35 粒子系統

DirectX11 With Windows SDK--35 粒子系統

2020-09-13 21:49:51 其他

前言

在這一章中,我們主要關注的是如何模擬一系列粒子,并控制它們運動,這些粒子的行為都是類似的,但它們也帶有一定的隨機性,這一堆粒子的幾何我們叫它為粒子系統,它可以被用于模擬一些比較現象,如:火焰、雨、煙霧、爆炸、法術效果等,

在這一章開始之前,你需要先學過如下章節:

章節
11 混合狀態
15 幾何著色器初探
16 流輸出階段
17 利用幾何著色器實作公告板效果

學習目標

  1. 熟悉如何利用幾何著色器和流輸出階段來高效存盤、渲染粒子
  2. 了解我們如何利用基本的物理概念來讓我們的粒子能夠以物理上的真實方式來運動
  3. 設計一個靈活的粒子系統框架使得我們可以方便地創建新的自定義粒子系統

DirectX11 With Windows SDK完整目錄

Github專案原始碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,

粒子的表示

粒子是一種非常小的物件,通常在數學上可以表示為一個點,因此我們在D3D中可以考慮使用圖元型別D3D11_PRIMITIVE_TOPOLOGY_POINTLIST來將一系列點傳入,然而,點圖元僅僅會被光柵化為單個像素,考慮到粒子可以有不同的大小,并且甚至需要將整個紋理都貼到這些粒子上,我們將采用前面公告板的繪制策略,即在頂點著色器輸出頂點,然后在幾何著色器將其變成一個四邊形并朝向攝像機,此外需要注意的是,這些粒子的y軸是與攝像機的y軸是對齊的,

如果我們知道世界坐標系的上方向軸向量j,公告板中心位置C,以及攝像機位置E,這樣我們可以描述公告板在世界坐標系下的區域坐標軸(即粒子的世界變換)是怎樣的:

\[\mathbf{w}=\frac{\mathbf{E-C}}{\|\mathbf{E-C}\|}\\ \mathbf{u}=\frac{\mathbf{j\times w}}{\|\mathbf{j\times w}\|}\\ \mathbf{v}=\mathbf{w\times u}\\ \mathbf{W}=\begin{bmatrix} u_x & u_y & u_z & 0\\ v_x & v_y & v_z & 0\\ w_x & w_y & w_z & 0\\ C_x & C_y & C_z & 1\\ \end{bmatrix} \]

粒子的屬性如下:

struct Particle
{
    XMFLOAT3 InitialPos;
    XMFLOAT3 InitialVel;
    XMFLOAT2 Size;
    float Age;
    unsigned int Type;
};

注意:我們不需要將頂點變成四邊形,例如,使用LineList來渲染雨看起來作業的也挺不錯,但而我們可以用不同的幾何著色器來將點變成線,通常情況下,在我們的系統中,每個粒子系統擁有自己的一套特效和著色器集合,

粒子運動

我們將會讓粒子以物理上的真實方式進行運動,為了簡便,我們將限制粒子的加速度為恒定常數,例如,讓加速度取決于重力,又或者是純粹的風力,此外,我們不對粒子做任何的碰撞檢測,

p(t)為粒子在t時刻的位置,它的運動軌跡為一條光滑曲線,它在t時刻的瞬時速度為:

\[\mathbf{v}(t)=\mathbf{p'}(t) \]

同樣,粒子在t時刻的瞬時加速度為:

\[\mathbf{a}(t)=\mathbf{v'}(t)=\mathbf{p''}(t) \]

學過高數的話下面的公式不難理解:

\[\int\mathbf{f(t)}dt=\mathbf{F}(t) + C\\ [\mathbf{F}(t)+C]'=\mathbf{f}(t) \]

連續函式f(t)的不定積分得到的函式有無限個,即C可以為任意常數,這些函式求導后可以還原回f(t)

通過對速度求不定積分就可以得到位置函式,對加速度求則得到的是速度函式:

\[\mathbf{p}(t)=\int\mathbf{v}(t)dt\\ \mathbf{v}(t)=\int\mathbf{a}(t)dt\\ \]

現在設加速度a(t)是一個恒定大小,方向,不隨時間變化的函式,并且我們知道t=0時刻的初始位置p0和初始速度v0,因此速度函式可以寫作:

\[\mathbf{v}(t)=\int\mathbf{a}(t)dt=t\cdot\mathbf{a} + \mathbf{c} \]

而t=0時刻滿足

\[\mathbf{v}(0)=\mathbf{c}=\mathbf{v_0} \]

故速度函式為:

\[\mathbf{v}(t) =t\cdot\mathbf{a} + \mathbf{v_0} \]

繼續積分并代入p(0),我們可以求出位置函式:

\[\mathbf{p}(t) = \frac{1}{2}t^2\mathbf{a}+t\mathbf{v_0}+\mathbf{p_0} \]

換句話說,粒子的運動軌跡p(t) (t>=0)完全取決于初始位置、初始速度和恒定加速度,只要知道了這些引數,我們就可以畫出它的運動軌跡了,因為它是關于t的二次函式,故它的運動軌跡一般為拋物線,

若令a=(0, -9.8, 0),則物體的運動可以看做僅僅受到了重力的影響,

注意:你也可以選擇不使用上面匯出的函式,如果你已經知道了粒子的運動軌跡函式p(t),你也可以直接用到你的程式當中,比如橢圓的引數方程等,

隨機性

在一個粒子系統中,我們想要讓粒子的表現相似,但不是讓他們都一樣,這就意味著我們需要給粒子系統加上隨機性,例如,如果我們在模擬余地,我們想讓它從不同的地方降下來,以及稍微不同的下落角度、稍微不同的降落速度,

當然,如果只是在C++中生成亂數還是一件比較簡單的事情的,但我們還需要在著色器代碼中使用亂數,而我們沒有著色器能夠直接使用的亂數生成器,所以我們的做法是創建一個1D紋理,里面每個元素是float4(使用DXGI_FORMAT_R32G32B32A32_FLOAT),然后我們使用區間[-1, 1]的隨機4D向量來填滿紋理,采樣的時候則使用wrap尋址模式即可,著色器通過對該紋理采樣來獲取亂數,這里有許多對隨機紋理進行采樣的方法,如果每個粒子擁有不同的x坐標,我們可以使用x坐標來作為紋理坐標來獲取亂數,然而,如果每個粒子的x坐標都相同,它們就會獲得相同的亂數,另一種方式是,我們可以使用當前的游戲時間值作為紋理坐標,這樣,不同時間生成的粒子將會獲得不同的隨機值,這也意味著同一時間生成的粒子將會獲得相同的隨機值,這樣如果粒子系統就不應該在同一時間生成多個粒子了,因此,我們可以考慮把兩者結合起來,當系統在同一時刻生成許多粒子的時候,我們可以添加一個不同的紋理坐標偏移值給游戲時間,這樣就可以盡可能確保同一時間內產生的不同粒子在進行采樣的時候能獲得不同的亂數,例如,當我們回圈20次來創建出20個粒子的時候,我們可以使用回圈的索引再乘上某個特定值作為紋理坐標的偏移,然后再進行采樣,現在我們就能夠拿到20個不同的亂數了,

下面的代碼用來生成亂數1D紋理:

需要注意的是,對于亂數紋理,我們只有一個mipmap等級,所以我們得使用SampleLevel的采樣方法來限制采樣mipmap等級為0,

下面的函式用于獲得一個隨機的單位向量:

float3 RandUnitVec3(float offset)
{
    // 使用游戲時間加上偏移值來從隨機紋理采樣
    float u = (g_GameTime + offset);
    // 分量均在[-1,1]
    float3 v = g_RandomVecMap.SampleLevel(g_SamLinear, u, 0).xyz;
    // 標準化向量
    return normalize(v);
}

混合與粒子系統

粒子系統通常以某些混合形式來繪制,對于火焰和法術釋放的效果,我們想要讓處于顆粒位置的顏色強度變數,那我們可以使用加法混合的形式,雖然我們可以只是將源顏色與目標顏色相加起來,但是粒子通常情況下是透明的,我們需要給源粒子顏色乘上它的alpha值,因此混合引數為:

SrcBlend = SRC_ALPHA;
DestBlend = ONE;
BlendOp = ADD;

即混合等式為:

\[\mathbf{C}=a_s\cdot\mathbf{C_{src}} + \mathbf{C_{dst}} \]

換句話說,源粒子給最終的混合顏色產生的貢獻程度是由它的不透明度所決定的:粒子越不透明,貢獻的顏色值越多,另一種辦法是我們可以在紋理中預先乘上它的不透明度(由alpha通道描述),以便于稀釋它的紋理顏色,這種情況下的混合引數為:

SrcBlend = ONE;
DestBlend = ONE;
BlendOp = ADD;

加法混合還有一個很好的效果,那就是可以使區域的亮度與那里的粒子濃度成正比,濃度高的區域會顯得格外明亮,這通常也是我們想要的

而對于煙霧來說,加法混合是行不通的,因為加入一堆重疊的煙霧粒子的顏色,最侄訓使得煙霧的顏色變亮,甚至變白,使用減法混合的話效果會更好一些(D3D11_BLEND_OP_REV_SUBTRACT),煙霧粒子會從目標色中減去一部分顏色,通過這種方式,可以使高濃度的煙霧粒子區域會變得更加灰黑,雖然這樣做對黑煙的效果很好,但是對淺灰煙、蒸汽的效果表現不佳,煙霧的另一種可能的混合方式是使用透明度混合,我們只需要將煙霧粒子視作半透明物體,使用透明度混合來渲染它們,但透明度混合的主要問題是將系統中的粒子按照相對于眼睛的前后順序進行排序,這種做法非常昂貴且不實際,考慮到粒子系統的隨機性,這條規則有時候可以打破,這并不會產生比較顯著的渲染問題,注意到如果場景中有許多粒子系統,這些系統應該按照從后到前的順序進行排序;但我們也不想對系統內的粒子進行排序,

基于GPU的粒子系統

粒子系統一般隨著時間的推移會產生和消滅粒子,一種看起來比較合理的方式就是使用動態頂點緩沖區并在CPU跟蹤粒子的生成和消滅,頂點緩沖區裝有當前存活或者剛生成的粒子,但是,我們從前面的章節已經知道一個獨立的、僅以流輸出階段為輸出的一趟渲染就可以在GPU完全控制粒子的生成和摧毀,這種做法是非常高效的,因為它不需要從CPU上傳資料給GPU,并且它將粒子生成/摧毀的作業從CPU搬移到了GPU,然后可以減少CPU的作業量了,

粒子系統特效

現在,我們可以將粒子的生成、變化、摧毀和繪制程序完全寫在HLSL檔案上,而不同的粒子系統的這些程序各有各的不同之處,比如說:

  1. 摧毀條件不同:我們可能要在雨水打中地面的時候將它摧毀,然而對于火焰粒子來說它是在幾秒鐘后被摧毀
  2. 變化程序不同:煙霧粒子可能隨著時間推移變得暗淡,對于雨水粒子來說并不是這樣的,同樣地,煙霧粒子隨時間推移逐漸擴散,而雨水大多是往下掉落的,
  3. 繪制程序不同:線圖元通常在模擬雨水的時候效果良好,但火焰/煙霧粒子更多使用的是公告板的四邊形,
  4. 生成條件不同:雨水和煙霧的初始位置和速度的設定方式明顯也是不同的,

但這樣做好處是可以讓C++代碼的作業量盡可能地減到最小,

ParticleEffect類

按照慣例,粒子系統分為了ParticleEffectParticleRender兩個部分,其中ParticleEffect對粒子系統的HLSL實作有所約束,它可以讀取一套HLSL檔案并負責資料的傳入,

class ParticleEffect : public IEffect
{
public:
    ParticleEffect();
    virtual ~ParticleEffect() override;

    ParticleEffect(ParticleEffect&& moveFrom) noexcept;
    ParticleEffect& operator=(ParticleEffect&& moveFrom) noexcept;

    // 初始化所需資源
    // 若effectPath為HLSL/Fire
    // 則會尋找檔案: 
    // - HLSL/Fire_SO_VS.hlsl
    // - HLSL/Fire_SO_GS.hlsl
    // - HLSL/Fire_VS.hlsl
    // - HLSL/Fire_GS.hlsl
    // - HLSL/Fire_PS.hlsl
    bool Init(ID3D11Device* device, const std::wstring& effectPath);

    // 產生新粒子到頂點緩沖區
    void SetRenderToVertexBuffer(ID3D11DeviceContext* deviceContext);
    // 繪制粒子系統
    void SetRenderDefault(ID3D11DeviceContext* deviceContext);

    void XM_CALLCONV SetViewProjMatrix(DirectX::FXMMATRIX VP);

    void SetEyePos(const DirectX::XMFLOAT3& eyePos);

    void SetGameTime(float t);
    void SetTimeStep(float step);

    void SetEmitDir(const DirectX::XMFLOAT3& dir);
    void SetEmitPos(const DirectX::XMFLOAT3& pos);

    void SetEmitInterval(float t);
    void SetAliveTime(float t);

    void SetTextureArray(ID3D11ShaderResourceView* textureArray);
    void SetTextureRandom(ID3D11ShaderResourceView* textureRandom);

    void SetBlendState(ID3D11BlendState* blendState, const FLOAT blendFactor[4], UINT sampleMask);
    void SetDepthStencilState(ID3D11DepthStencilState* depthStencilState, UINT stencilRef);

    void SetDebugObjectName(const std::string& name);

    // 
    // IEffect
    //

    // 應用常量緩沖區和紋理資源的變更
    void Apply(ID3D11DeviceContext* deviceContext) override;

private:
    class Impl;
    std::unique_ptr<Impl> pImpl;
};

其中用戶需要手動設定的有渲染時的混合狀態、深度/模板狀態,以及ViewProjEyePos,其余可以交給接下來要講的ParticleRender類來完成,

ParticleRender類

該類代表一個粒子系統的實體,用戶需要設定與該系統相關的引數、使用的紋理等屬性:

class ParticleRender
{
public:
    template<class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    ParticleRender() = default;
    ~ParticleRender() = default;
    // 不允許拷貝,允許移動
    ParticleRender(const ParticleRender&) = delete;
    ParticleRender& operator=(const ParticleRender&) = delete;
    ParticleRender(ParticleRender&&) = default;
    ParticleRender& operator=(ParticleRender&&) = default;

    // 自從該系統被重置以來所經過的時間
    float GetAge() const;

    void SetEmitPos(const DirectX::XMFLOAT3& emitPos);
    void SetEmitDir(const DirectX::XMFLOAT3& emitDir);

    void SetEmitInterval(float t);
    void SetAliveTime(float t);

    HRESULT Init(ID3D11Device* device, UINT maxParticles);
    void SetTextureArraySRV(ID3D11ShaderResourceView* textureArraySRV);
    void SetRandomTexSRV(ID3D11ShaderResourceView* randomTexSRV);

    void Reset();
    void Update(float dt, float gameTime);
    void Draw(ID3D11DeviceContext* deviceContext, ParticleEffect& effect, const Camera& camera);

    void SetDebugObjectName(const std::string& name);

private:
    
    UINT m_MaxParticles = 0;
    bool m_FirstRun = true;

    float m_GameTime = 0.0f;
    float m_TimeStep = 0.0f;
    float m_Age = 0.0f;

    DirectX::XMFLOAT3 m_EmitPos = {};
    DirectX::XMFLOAT3 m_EmitDir = {};

    float m_EmitInterval = 0.0f;
    float m_AliveTime = 0.0f;

    ComPtr<ID3D11Buffer> m_pInitVB;
    ComPtr<ID3D11Buffer> m_pDrawVB;
    ComPtr<ID3D11Buffer> m_pStreamOutVB;

    ComPtr<ID3D11ShaderResourceView> m_pTextureArraySRV;
    ComPtr<ID3D11ShaderResourceView> m_pRandomTexSRV;
    
};

注意:粒子系統使用一個紋理陣列來對粒子進行貼圖,因為我們可能不想讓所有的粒子看起來都是一樣的,例如,為了實作一個煙霧的粒子系統,我們可能想要使用幾種煙霧紋理來添加變化,圖元ID在像素著色器中可以用來對紋理陣列進行索引,

發射器粒子

因為幾何著色器負責創建/摧毀粒子,我們需要一個特別的發射器粒子,發射器粒子本身可以繪制出來,也可以不被繪制,假如你想讓你的發射器粒子不能被看見,那么在繪制時的幾何著色器的階段你就可以不要將它輸出,發射器粒子與當前粒子系統中的其它粒子的行為有所不同,因為它可以產生其它粒子,例如,一個發射器粒子可能會記錄累計經過的時間,并且到達一個特定時間點的時候,它就會發射一個新的粒子,此外,通過限制哪些粒子可以發射其它粒子,它讓我們對粒子的發射方式有了一定的控制,比如說現在我們只有一個發射器粒子,我們可以很方便地控制每一幀所生產的粒子數目,流輸出幾何著色器應當總是輸出至少一個發射器粒子,因為如果粒子系統丟掉了所有的發射器,粒子系統終究會消亡;但對于某些粒子系統來說,讓它最終消亡也許是一種理想的結果,

在本章中,我們將只使用一個發射器粒子,但如果需要的話,當前粒子系統的框架也可以進行擴展,

起始頂點緩沖區

在我們的粒子系統中,有一個比較特別的起始頂點緩沖區,它僅僅包含了一個發射器粒子,而我們用這個頂點緩沖區來啟動粒子系統,發射器粒子將會開始不停地產生其它粒子,需要注意的是起始頂點緩沖區僅僅繪制一次(除了系統被重置以外),當粒子系統經發射器粒子啟動后,我們就可以使用兩個流輸出頂點緩沖區來進行后續繪制,

起始頂點緩沖區在系統被重置的時候也是有用的,我們可以使用下面的代碼來重啟粒子系統:

void ParticleRender::Reset()
{
    m_FirstRun = true;
    m_Age = 0.0f;
}

更新/繪制程序

繪制程序如下:

  1. 通過流輸出幾何著色器階段來更新當前幀的粒子
  2. 使用更新好的粒子進行渲染
void ParticleRender::Draw(ID3D11DeviceContext* deviceContext, ParticleEffect& effect, const Camera& camera)
{
    effect.SetGameTime(m_GameTime);
    effect.SetTimeStep(m_TimeStep);
    effect.SetEmitPos(m_EmitPos);
    effect.SetEmitDir(m_EmitDir);
    effect.SetEmitInterval(m_EmitInterval);
    effect.SetAliveTime(m_AliveTime);
    effect.SetTextureArray(m_pTextureArraySRV.Get());
    effect.SetTextureRandom(m_pRandomTexSRV.Get());

    // ******************
    // 流輸出
    //
    effect.SetRenderToVertexBuffer(deviceContext);
    UINT strides[1] = { sizeof(VertexParticle) };
    UINT offsets[1] = { 0 };

    // 如果是第一次運行,使用初始頂點緩沖區
    // 否則,使用存有當前所有粒子的頂點緩沖區
    if (m_FirstRun)
        deviceContext->IASetVertexBuffers(0, 1, m_pInitVB.GetAddressOf(), strides, offsets);
    else
        deviceContext->IASetVertexBuffers(0, 1, m_pDrawVB.GetAddressOf(), strides, offsets);

    // 經過流輸出寫入到頂點緩沖區
    deviceContext->SOSetTargets(1, m_pStreamOutVB.GetAddressOf(), offsets);
    effect.Apply(deviceContext);
    if (m_FirstRun)
    {
        deviceContext->Draw(1, 0);
        m_FirstRun = false;
    }
    else
    {
        deviceContext->DrawAuto();
    }

    // 解除緩沖區系結
    ID3D11Buffer* nullBuffers[1] = { nullptr };
    deviceContext->SOSetTargets(1, nullBuffers, offsets);

    // 進行頂點緩沖區的Ping-Pong交換
    m_pDrawVB.Swap(m_pStreamOutVB);

    // ******************
    // 使用流輸出頂點繪制粒子
    //
    effect.SetRenderDefault(deviceContext);

    deviceContext->IASetVertexBuffers(0, 1, m_pDrawVB.GetAddressOf(), strides, offsets);
    effect.Apply(deviceContext);
    deviceContext->DrawAuto();
}

火焰

火焰粒子雖然是沿著指定方向發射,但給定了隨機的初速度來火焰四散,并產生火球,

// Fire.hlsli

cbuffer CBChangesEveryFrame : register(b0)
{
    matrix g_ViewProj;
    
    float3 g_EyePosW;
    float g_GameTime;
    
    float g_TimeStep;
    float3 g_EmitDirW;
    
    float3 g_EmitPosW;
    float g_EmitInterval;
    
    float g_AliveTime;
}

cbuffer CBFixed : register(b1)
{
    // 用于加速粒子運動的加速度
    float3 g_AccelW = float3(0.0f, 7.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)
    };
}

// 用于貼圖到粒子上的紋理陣列
Texture2DArray g_TexArray : register(t0);

// 用于在著色器中生成亂數的紋理
Texture1D g_RandomTex : register(t1);

// 采樣器
SamplerState g_SamLinear : register(s0);


float3 RandUnitVec3(float offset)
{
    // 使用游戲時間加上偏移值來采樣隨機紋理
    float u = (g_GameTime + offset);
    
    // 采樣值在[-1,1]
    float3 v = g_RandomTex.SampleLevel(g_SamLinear, u, 0).xyz;
    
    // 投影到單位球
    return normalize(v);
}

#define PT_EMITTER 0
#define PT_FLARE 1

struct VertexParticle
{
    float3 InitialPosW : POSITION;
    float3 InitialVelW : VELOCITY;
    float2 SizeW : SIZE;
    float Age : AGE;
    uint Type : TYPE;
};

// 繪制輸出
struct VertexOut
{
    float3 PosW : POSITION;
    float2 SizeW : SIZE;
    float4 Color : COLOR;
    uint Type : TYPE;
};

struct GeoOut
{
    float4 PosH : SV_Position;
    float4 Color : COLOR;
    float2 Tex : TEXCOORD;
};


// Fire_SO_VS.hlsl
#include "Fire.hlsli"

VertexParticle VS(VertexParticle vIn)
{
    return vIn;
}

// Fire_SO_GS.hlsl
#include "Fire.hlsli"

[maxvertexcount(2)]
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)
        {
            float3 vRandom = RandUnitVec3(0.0f);
            vRandom.x *= 0.5f;
            vRandom.z *= 0.5f;
            
            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]);
    }
}

// Fire_VS.hlsl
#include "Fire.hlsli"

VertexOut VS(VertexParticle vIn)
{
    VertexOut vOut;
    
    float t = vIn.Age;
    
    // 恒定加速度等式
    vOut.PosW = 0.5f * t * t * g_AccelW + 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;
}

// Fire_GS.hlsl
#include "Fire.hlsli"

[maxvertexcount(4)]
void GS(point VertexOut gIn[1], inout TriangleStream<GeoOut> output)
{
    // 不要繪制用于產生粒子的頂點
    if (gIn[0].Type != PT_EMITTER)
    {
        //
        // 計算該粒子的世界矩陣讓公告板朝向攝像機
        //
        float3 look  = normalize(g_EyePosW.xyz - gIn[0].PosW);
        float3 right = normalize(cross(float3(0.0f, 1.0f, 0.0f), look));
        float3 up = cross(look, right);
        
        //
        // 計算出處于世界空間的四邊形
        //
        float halfWidth  = 0.5f * gIn[0].SizeW.x;
        float halfHeight = 0.5f * gIn[0].SizeW.y;
        
        float4 v[4];
        v[0] = float4(gIn[0].PosW + halfWidth * right - halfHeight * up, 1.0f);
        v[1] = float4(gIn[0].PosW + halfWidth * right + halfHeight * up, 1.0f);
        v[2] = float4(gIn[0].PosW - halfWidth * right - halfHeight * up, 1.0f);
        v[3] = float4(gIn[0].PosW - halfWidth * right + halfHeight * up, 1.0f);
    
        //
        // 將四邊形頂點從世界空間變換到齊次裁減空間
        //
        GeoOut gOut;
        [unroll]
        for (int i = 0; i < 4; ++i)
        {
            gOut.PosH  = mul(v[i], g_ViewProj);
            gOut.Tex   = g_QuadTex[i];
            gOut.Color = gIn[0].Color;
            output.Append(gOut);
        }
    }
}

// Fire_PS.hlsl
#include "Fire.hlsli"

float4 PS(GeoOut pIn) : SV_Target
{
    return g_TexArray.Sample(g_SamLinear, float3(pIn.Tex, 0.0f)) * pIn.Color;
}

在C++中,我們還需要設定下面兩個渲染狀態用于粒子的渲染:

m_pFireEffect->SetBlendState(RenderStates::BSAlphaWeightedAdditive.Get(), nullptr, 0xFFFFFFFF);
m_pFireEffect->SetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);

雨水

雨水粒子系統也是由一系列的HLSL檔案所組成,它的形式和火焰粒子系統有所相似,但在生成/摧毀/渲染的規則上有所不同,例如,我們的雨水加速度是向下的,并帶有小幅度的傾斜角,然而火焰的加速度是向上的,此外,雨水粒子系統最終產生的繪制圖元是線,而不是四邊形;并且雨水的產生位置與攝像機位置有聯系,它總是在攝像機的上方周圍(移動的時候在上方偏前)產生雨水粒子,這樣就不需要在整個世界產生雨水了,這樣就可以造成一種當前正在下雨的假象(當然移動起來的話就會感覺有些假,雨水量減少了),需要注意該系統并沒有使用任何的混合狀態,

// Rain.hlsli

cbuffer CBChangesEveryFrame : register(b0)
{
    matrix g_ViewProj;
    
    float3 g_EyePosW;
    float g_GameTime;
    
    float g_TimeStep;
    float3 g_EmitDirW;
    
    float3 g_EmitPosW;
    float g_EmitInterval;
    
    float g_AliveTime;
}

cbuffer CBFixed : register(b1)
{
    // 用于加速粒子運動的加速度
    float3 g_AccelW = float3(-1.0f, -9.8f, 0.0f);
}

// 用于貼圖到粒子上的紋理陣列
Texture2DArray g_TexArray : register(t0);

// 用于在著色器中生成亂數的紋理
Texture1D g_RandomTex : register(t1);

// 采樣器
SamplerState g_SamLinear : register(s0);


float3 RandUnitVec3(float offset)
{
    // 使用游戲時間加上偏移值來采樣隨機紋理
    float u = (g_GameTime + offset);
    
    // 采樣值在[-1,1]
    float3 v = g_RandomTex.SampleLevel(g_SamLinear, u, 0).xyz;
    
    // 投影到單位球
    return normalize(v);
}

float3 RandVec3(float offset)
{
    // 使用游戲時間加上偏移值來采樣隨機紋理
    float u = (g_GameTime + offset);
    
    // 采樣值在[-1,1]
    float3 v = g_RandomTex.SampleLevel(g_SamLinear, u, 0).xyz;
    
    return v;
}

#define PT_EMITTER 0
#define PT_FLARE 1

struct VertexParticle
{
    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;
};


// Rain_SO_VS.hlsl
#include "Rain.hlsli"

VertexParticle VS(VertexParticle vIn)
{
    return vIn;
}

// Rain_SO_GS.hlsl
#include "Rain.hlsli"

[maxvertexcount(6)]
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)
        {
            [unroll]
            for (int i = 0; i < 5; ++i)
            {
                // 在攝像機上方的區域讓雨滴降落
                float3 vRandom = 30.0f * RandVec3((float)i / 5.0f);
                vRandom.y = 20.0f;
                
                VertexParticle p;
                p.InitialPosW = g_EmitPosW.xyz + vRandom;
                p.InitialVelW = float3(0.0f, 0.0f, 0.0f);
                p.SizeW       = float2(1.0f, 1.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]);
    }
}

// Rain_VS.hlsl
#include "Rain.hlsli"

VertexOut VS(VertexParticle vIn)
{
    VertexOut vOut;
    
    float t = vIn.Age;
    
    // 恒定加速度等式
    vOut.PosW = 0.5f * t * t * g_AccelW + t * vIn.InitialVelW + vIn.InitialPosW;
    
    vOut.Type = vIn.Type;
    
    return vOut;
}

// Rain_GS.hlsl
#include "Rain.hlsli"

[maxvertexcount(6)]
void GS(point VertexOut gIn[1], inout LineStream<GeoOut> output)
{
    // 不要繪制用于產生粒子的頂點
    if (gIn[0].Type != PT_EMITTER)
    {
        // 使線段沿著一個加速度方向傾斜
        float3 p0 = gIn[0].PosW;
        float3 p1 = gIn[0].PosW + 0.07f * g_AccelW;
        
        GeoOut v0;
        v0.PosH = mul(float4(p0, 1.0f), g_ViewProj);
        v0.Tex = float2(0.0f, 0.0f);
        output.Append(v0);
        
        GeoOut v1;
        v1.PosH = mul(float4(p1, 1.0f), g_ViewProj);
        v1.Tex = float2(0.0f, 0.0f);
        output.Append(v1);
    }
}

// Rain_PS.hlsl
#include "Rain.hlsli"

float4 PS(GeoOut pIn) : SV_Target
{
    return g_TexArray.Sample(g_SamLinear, float3(pIn.Tex, 0.0f));
}

在C++中,我們還需要設定下面的渲染狀態用于粒子的渲染:

m_pRainEffect->SetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);

C++代碼實作

在本章中,與粒子系統直接相關的類為ParticleEffectParticleRedner類,GameApp類承擔了實作程序,

首先是初始化關于粒子系統和特效的部分:

bool GameApp::InitResource()
{
    // ...

    // ******************
    // 初始化特效
    //

    // ...

    m_pFireEffect->SetBlendState(RenderStates::BSAlphaWeightedAdditive.Get(), nullptr, 0xFFFFFFFF);
    m_pFireEffect->SetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);

    m_pRainEffect->SetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);

    // ...

    // ******************
    // 初始化粒子系統
    //
    ComPtr<ID3D11ShaderResourceView> pFlareSRV, pRainSRV, pRandomSRV;
    HR(CreateTexture2DArrayFromFile(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get(),
        std::vector<std::wstring>{ L"..\\Texture\\flare0.dds" }, nullptr, pFlareSRV.GetAddressOf()));
    HR(CreateRandomTexture1D(m_pd3dDevice.Get(), nullptr, pRandomSRV.GetAddressOf()));
    m_pFire->Init(m_pd3dDevice.Get(), 500);
    m_pFire->SetTextureArraySRV(pFlareSRV.Get());
    m_pFire->SetRandomTexSRV(pRandomSRV.Get());
    m_pFire->SetEmitPos(XMFLOAT3(0.0f, -1.0f, 0.0f));
    m_pFire->SetEmitDir(XMFLOAT3(0.0f, 1.0f, 0.0f));
    m_pFire->SetEmitInterval(0.005f);
    m_pFire->SetAliveTime(1.0f);
    

    HR(CreateTexture2DArrayFromFile(m_pd3dDevice.Get(), m_pd3dImmediateContext.Get(),
        std::vector<std::wstring>{ L"..\\Texture\\raindrop.dds" }, nullptr, pRainSRV.GetAddressOf()));
    HR(CreateRandomTexture1D(m_pd3dDevice.Get(), nullptr, pRandomSRV.ReleaseAndGetAddressOf()));
    m_pRain->Init(m_pd3dDevice.Get(), 10000);
    m_pRain->SetTextureArraySRV(pRainSRV.Get());
    m_pRain->SetRandomTexSRV(pRandomSRV.Get());
    m_pRain->SetEmitDir(XMFLOAT3(0.0f, -1.0f, 0.0f));
    m_pRain->SetEmitInterval(0.0015f);
    m_pRain->SetAliveTime(3.0f);

    // ...
}

然后是更新部分,角色移動時會在角色頭頂上再往前一些的地方為中心點的范圍來產生粒子:

void GameApp::UpdateScene(float dt)
{
    // ...
    
    // ******************
    // 粒子系統
    //
    if (m_KeyboardTracker.IsKeyPressed(Keyboard::R))
    {
        m_pFire->Reset();
        m_pRain->Reset();
    }
    m_pFire->Update(dt, m_Timer.TotalTime());
    m_pRain->Update(dt, m_Timer.TotalTime());

    m_pFireEffect->SetViewProjMatrix(m_pCamera->GetViewProjXM());
    m_pFireEffect->SetEyePos(m_pCamera->GetPosition());

    static XMFLOAT3 lastCameraPos = m_pCamera->GetPosition();
    XMFLOAT3 cameraPos = m_pCamera->GetPosition();

    XMVECTOR cameraPosVec = XMLoadFloat3(&cameraPos);
    XMVECTOR lastCameraPosVec = XMLoadFloat3(&lastCameraPos);
    XMFLOAT3 emitPos;
    XMStoreFloat3(&emitPos, cameraPosVec + 3.0f * (cameraPosVec - lastCameraPosVec));
    m_pRainEffect->SetViewProjMatrix(m_pCamera->GetViewProjXM());
    m_pRainEffect->SetEyePos(m_pCamera->GetPosition());
    m_pRain->SetEmitPos(emitPos);
    lastCameraPos = m_pCamera->GetPosition();
}

最后是繪制,由于粒子可能是透明物體,并且本例中不寫入深度值,要在畫完天空盒之后才來繪制粒子系統:

void GameApp::DrawScene()
{
    assert(m_pd3dImmediateContext);
    assert(m_pSwapChain);

    m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Silver));
    m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    // ******************
    // 正常繪制場景
    //
    
    // 統計實際繪制的物體數目
    std::vector<Transform> acceptedData;
    // 默認視錐體裁剪
    acceptedData = https://www.cnblogs.com/X-Jun/p/Collision::FrustumCulling(m_InstancedData, m_Trees.GetLocalBoundingBox(),
        m_pCamera->GetViewXM(), m_pCamera->GetProjXM());
    // 默認硬體實體化繪制
    m_pBasicEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), BasicEffect::RenderInstance);
    m_Trees.DrawInstanced(m_pd3dImmediateContext.Get(), m_pBasicEffect.get(), acceptedData);

    // 繪制地面
    m_pBasicEffect->SetRenderDefault(m_pd3dImmediateContext.Get(), BasicEffect::RenderObject);
    m_Ground.Draw(m_pd3dImmediateContext.Get(), m_pBasicEffect.get());

    // 繪制天空盒
    m_pSkyEffect->SetRenderDefault(m_pd3dImmediateContext.Get());
    m_pGrassCube->Draw(m_pd3dImmediateContext.Get(), *m_pSkyEffect, *m_pCamera);

    // ******************
    // 粒子系統留在最后繪制
    //

    m_pFire->Draw(m_pd3dImmediateContext.Get(), *m_pFireEffect, *m_pCamera);
    m_pRain->Draw(m_pd3dImmediateContext.Get(), *m_pRainEffect, *m_pCamera);
    
    // ...
}

演示

下面的動圖演示了火焰和雨水的粒子系統效果:

練習題

  1. 實作一個爆炸的粒子系統,發射器粒子產生N個隨機方向的外殼粒子,在經過一個短暫時間后,每個外殼粒子應當爆炸產生M個粒子,每個外殼不需要在同一個時間發生爆炸——通過隨機性賦上不同的爆炸倒計時,對M和N進行測驗直到你得到不錯的結果,注意發射器在產生所有的外殼粒子后,將其摧毀使得不要產生更多的外殼,
  2. 實作一個噴泉的粒子系統,這些粒子應當從某個點產生,并沿著圓錐體范圍內的隨機方向向上發射,最終重力會使得它們掉落到地面,注意:給粒子一個足夠高的初速度來讓它們克服重力,

DirectX11 With Windows SDK完整目錄

Github專案原始碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/27798.html

標籤:其他

上一篇:蒙皮影片的系結姿勢

下一篇:崩潰中!我終于看明白了,什么是財富自由的底層邏輯!思維導圖+筆記精華

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more