今回は、華やかな流体のような効果を生み出すのに便利な技術「Curl-Noise」のOpenSiv3Dでの実装について説明します。
...と思ったのですが、時間の都合上、詳しい解説は参考リンクに譲り、ここでは遊んでみた結果の記録を貼り付けます。
2D
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
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次元空間のベクトル場を生成する。その後、そのベクトル場に回転演算子を適用することにより、流体の流れを定義する。
ここで、ポテンシャル場からベクトル場を生成すると、そのベクトル場の発散はゼロになります。つまり、より自然界の流体の挙動に近い場を作ることができるのです。