7
1

More than 1 year has passed since last update.

プロシージャルな流体エフェクトを OpenSiv3D で実装する【Curl-Noise】

Last updated at Posted at 2021-12-15

今回は、華やかな流体のような効果を生み出すのに便利な技術「Curl-Noise」のOpenSiv3Dでの実装について説明します。
...と思ったのですが、時間の都合上、詳しい解説は参考リンクに譲り、ここでは遊んでみた結果の記録を貼り付けます。

2D

ezgif.com-gif-maker.gif

Main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.6.3


struct Particle
{
    Vec2 pos;

    Vec2 velocity;

    double startTime;

    double radius;

    double weight;

    double offset;
};

void Main()
{
    Window::Resize(1280, 720);

    // 使用する画像
    const Image image{ U"example/siv3d-kun.png" };

    // テクスチャ
    const Texture texture{ image };
    // アルファ値 1 以上の領域を Polygon 化
    const Polygon polygon = image.alphaToPolygon(1, AllowHoles::No);

    // Polygon 単純化時の基準距離(ピクセル)
    double maxDistance = 4.0;

    // 単純化した Polygon
    Polygon simplifiedPolygon = polygon.simplified(maxDistance);

    simplifiedPolygon.moveBy(100, 100);

    PerlinNoise noise;




    Array<Particle> particles;


    const double lifeTime = 2.0;



    const Size sceneSize{ Scene::Size() };
    RenderTexture gaussianA1{ sceneSize }, gaussianB1{ sceneSize };
    RenderTexture gaussianA4{ sceneSize / 4 }, gaussianB4{ sceneSize / 4 };
    RenderTexture gaussianA8{ sceneSize / 8 }, gaussianB8{ sceneSize / 8 };

    double a1 = 1.0, a4 = 1.0, a8 = 1.0;

    auto drawFunction = [&]() {



        for (const auto& p : particles)
        {
            const double t = (Scene::Time() - p.startTime) / lifeTime;
            const double alpha = Sin(t * Math::Pi) * Sin(t * Math::Pi);
            const double wave = (Sin((t + p.offset) * Math::TwoPi * 5)) * 1.0;

            Circle(p.pos, p.radius).draw(ColorF(1.0, 0.5, 0.1, alpha + alpha * wave));
        }};

    while (System::Update())
    {

        if (MouseL.down())
        {
            Array<Particle> newParticles(1000);

            const double direction = Random() * Math::TwoPi;

            for (auto& particle : newParticles)
            {
                particle.pos = Cursor::Pos() + RandomVec2(Circle(30.0));

                particle.velocity = (Vec2::Right().rotated(direction) + RandomVec2(Circle(0.2))) * 10.0; 

                particle.startTime = Scene::Time();

                particle.radius = Random(1.0, 3.0);

                particle.weight = Random();

                particle.offset = Random();
            }

            std::copy(newParticles.begin(), newParticles.end(), std::back_inserter(particles));
        }



        Erase_if(particles, [](const Particle& p) {return p.startTime + lifeTime < Scene::Time(); });

        {
            const double delta = 0.1;

            for (auto& particle : particles)
            {
                const Vec2& p = particle.pos;
                const double x0 = noise.normalizedOctave2D0_1(p / 128.0, 1, 0.0);
                const double x1 = noise.normalizedOctave2D0_1((p + Vec2(delta, 0.0)) / 128.0, 1, 0.0);
                const double y0 = noise.normalizedOctave2D0_1(p / 128.0, 1, 0.0);
                const double y1 = noise.normalizedOctave2D0_1((p + Vec2(0.0, delta)) / 128.0, 1, 0.0);
                const double distance = (1.0 - 1.0 / (Max(1.0, Pow(Geometry2D::Distance(p, simplifiedPolygon), 1.0))));
                const Vec2 curl = distance * Vec2((y1 - y0) / delta, -(x1 - x0) / delta);
                const Vec2 add = curl * Scene::DeltaTime() * 50000;

                particle.velocity += -10 * (1.0 - particle.weight * 0.5) * particle.velocity * Scene::DeltaTime();

                particle.pos += particle.velocity * distance + add * particle.velocity * 0.5 + add * 0.5;
            }
        }

        drawFunction();

        texture.draw(100, 100);

        {
            // ガウスぼかし用テクスチャにもう一度シーンを描く
            {
                const ScopedRenderTarget2D target{ gaussianA1.clear(ColorF{ 0.0 }) };
                const ScopedRenderStates2D blend{ BlendState::Additive };

                drawFunction();
            }

            // オリジナルサイズのガウスぼかし (A1)
            // A1 を 1/4 サイズにしてガウスぼかし (A4)
            // A4 を 1/2 サイズにしてガウスぼかし (A8)
            Shader::GaussianBlur(gaussianA1, gaussianB1, gaussianA1);
            Shader::Downsample(gaussianA1, gaussianA4);
            Shader::GaussianBlur(gaussianA4, gaussianB4, gaussianA4);
            Shader::Downsample(gaussianA4, gaussianA8);
            Shader::GaussianBlur(gaussianA8, gaussianB8, gaussianA8);
        }

        {
            const ScopedRenderStates2D blend{ BlendState::Additive };

            if (a1)
            {
                gaussianA1.resized(sceneSize).draw(ColorF{ a1 });
            }

            if (a4)
            {
                gaussianA4.resized(sceneSize).draw(ColorF{ a4 });
            }

            if (a8)
            {
                gaussianA8.resized(sceneSize).draw(ColorF{ a8 });
            }
        }


    }
}

3D

ezgif.com-gif-maker (1).gif

Main.cpp
# include <Siv3D.hpp>


struct Particle
{
    Vec3 pos;

    Vec3 velocity;

    double startTime;

    double radius;

    double weight;

    double offset;

    double lifeTime;
};

Vec3 GetVec3Noise(const Vec3& p, const PerlinNoise& noise)
{   
    return Vec3(noise.normalizedOctave3D(p / 128.0, 1, 0.0), noise.normalizedOctave3D(p / 128.0 + Vec3(100, 100, 100), 1, 0.0), noise.normalizedOctave3D(p / 128.0 + Vec3(-100, -100, -100), 1, 0.0));
}


void Main()
{
    Window::Resize(1280, 720);
    Scene::SetResizeMode(ResizeMode::Keep);

    const PixelShader psBright = HLSL{ U"example/shader/hlsl/extract_bright_linear.hlsl", U"PS" }
    | GLSL{ U"example/shader/glsl/extract_bright_linear.frag", {{U"PSConstants2D", 0}} };

    if (not psBright)
    {
        return;
    }

    const ColorF backgroundColor = ColorF{ 0.02 }.removeSRGBCurve();
    const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R16G16B16A16_Float, HasDepth::Yes };
    const RenderTexture gaussianA4{ renderTexture.size() / 4 }, gaussianB4{ renderTexture.size() / 4 };
    const RenderTexture gaussianA8{ renderTexture.size() / 8 }, gaussianB8{ renderTexture.size() / 8 };
    const RenderTexture gaussianA16{ renderTexture.size() / 16 }, gaussianB16{ renderTexture.size() / 16 };
    DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } * 50 };

    bool glowEffect = true;


    Array<Particle> particles;


    PerlinNoise noise;



    while (System::Update())
    {
        camera.update(2.0);

        if (MouseL.down())
        {
            Array<Particle> newParticles(1000);

            const Vec3 direction = RandomVec3(1.0);

            for (auto& particle : newParticles)
            {
                particle.pos = RandomVec3(Sphere(10.0));

                particle.velocity = (direction + RandomVec3(Sphere(0.3))) * 20.0;

                particle.startTime = Scene::Time();

                particle.radius = Random(1.0, 5.0);

                particle.weight = Random();

                particle.offset = Random();

                particle.lifeTime = 2.0;
            }

            std::copy(newParticles.begin(), newParticles.end(), std::back_inserter(particles));
        }

        Erase_if(particles, [](const Particle& p) {return p.startTime + p.lifeTime < Scene::Time(); });

        {
            const double delta = 0.1;

            for (auto& particle : particles)
            {
                const Vec3& p = particle.pos;
                const Vec3 x0 = GetVec3Noise((p + Vec3(-delta, 0.0, 0.0)), noise);
                const Vec3 x1 = GetVec3Noise((p + Vec3(delta, 0.0, 0.0)) , noise);
                const Vec3 y0 = GetVec3Noise((p + Vec3(0.0, -delta, 0.0)), noise);
                const Vec3 y1 = GetVec3Noise((p + Vec3(0.0, delta, 0.0)) , noise);
                const Vec3 z0 = GetVec3Noise((p + Vec3(0.0, 0.0, -delta)), noise);
                const Vec3 z1 = GetVec3Noise((p + Vec3(0.0, 0.0, delta)) , noise);
                const Vec3 dx = x1 - x0;
                const Vec3 dy = y1 - y0;
                const Vec3 dz = z1 - z0;

                const Vec3 curl = Vec3(dy.z-dz.y, dz.x-dx.z, dx.y-dy.x)/(2.0 * delta);
                const Vec3 add = curl * Scene::DeltaTime() * 20000;

                particle.velocity += -10 * (1.0 - particle.weight * 0.5) * particle.velocity * Scene::DeltaTime();

                particle.pos +=  particle.velocity  + add * particle.velocity * 0.5 + add * 0.3;
            }
        }

        // 3D
        {
            Graphics3D::SetCameraTransform(camera);

            const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

            PhongMaterial phong;
            phong.amibientColor = ColorF{ 1.0 };
            phong.diffuseColor = ColorF{ 0.0 };


            for (const auto& p : particles)
            {
                const double t = (Scene::Time() - p.startTime) / p.lifeTime;
                const double alpha = Sin(t * Math::Pi) * Sin(t * Math::Pi);
                const double wave = (Sin((t + p.offset) * Math::TwoPi * 5)) * 1.0;

                phong.emissionColor = ColorF(0.1, 0.5, 1.0).removeSRGBCurve() * ( alpha + alpha * wave) * 10.0;
                Sphere{ p.pos, p.radius }
                .draw(phong);
            }

        }

        // RenderTexture を 2D シーンに描画
        {
            Graphics3D::Flush();
            renderTexture.resolve();
            Shader::LinearToScreen(renderTexture);
        }

        if (glowEffect)
        {
            // 高輝度部分を抽出
            {
                const ScopedCustomShader2D shader{ psBright };
                const ScopedRenderTarget2D target{ gaussianA4.clear(ColorF{0.0}) };
                renderTexture.scaled(0.25).draw();
            }

            // 高輝度部分のぼかしテクスチャの生成
            {
                Shader::GaussianBlur(gaussianA4, gaussianB4, gaussianA4);
                Shader::Downsample(gaussianA4, gaussianA8);
                Shader::GaussianBlur(gaussianA8, gaussianB8, gaussianA8);
                Shader::GaussianBlur(gaussianA8, gaussianB8, gaussianA8);
                Shader::Downsample(gaussianA8, gaussianA16);
                Shader::GaussianBlur(gaussianA16, gaussianB16, gaussianA16);
                Shader::GaussianBlur(gaussianA16, gaussianB16, gaussianA16);
                Shader::GaussianBlur(gaussianA16, gaussianB16, gaussianA16);
            }

            // Glow エフェクト
            {
                const ScopedRenderStates2D blend{ BlendState::AdditiveRGB };

                {
                    const ScopedRenderTarget2D target{ gaussianA8 };
                    gaussianA16.scaled(2.0).draw(ColorF{ 3.0 });
                }

                {
                    const ScopedRenderTarget2D target{ gaussianA4 };
                    gaussianA8.scaled(2.0).draw(ColorF{ 1.0 });
                }

                gaussianA4.resized(Scene::Size()).draw(ColorF{ 1.0 });
            }
        }

        SimpleGUI::CheckBox(glowEffect, U"Glow", Vec2{ 20,20 });

    }

}


補足

Curl-Noiseは、2007年のSIGGRAPHでブリティッシュ・コロンビア大学のRobert Bridsonらが提案した、流体的な効果を簡単に発生させる方法です。https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph2007-curlnoise.pdf

参考サイト
https://prideout.net/blog/old/blog/index.html@p=63.html

3次元のパーリンノイズを持つポテンシャル場を用いて、3次元空間のベクトル場を生成する。その後、そのベクトル場に回転演算子を適用することにより、流体の流れを定義する。
ここで、ポテンシャル場からベクトル場を生成すると、そのベクトル場の発散はゼロになります。つまり、より自然界の流体の挙動に近い場を作ることができるのです。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1