Siv3D の 3D プログラムテクニック集

  • 5
    Like
  • 0
    Comment

Siv3D Advent Calendar 2016 1 日目の記事です。
Siv3D August 2016 v2 で使える 3D プログラムのテクニックを 3 つ紹介します。

1. スカイスフィア

ゲームワールドでリアリティのある空を表現するには、Equirectangular 形式の 360° 天球画像を Sphere に貼り付けて描画するのが簡単です。

sky.jpg

(▲ Equirectangular 形式の天球画像の例。配布元: http://freepanorama.blogspot.com/
このような画像は RICOH THETA で撮影して自作することもできますし、フリーの画像も見つかります。

スカイスフィアはシーンのライトの影響を受けないように、個別に光源を設定できる Forward Rendering で描画します。プログラムは次のとおりです。

# include <Siv3D.hpp>

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

    // 空と地面の境界をぼかすためにフォグを設定(空の色を使うと良い)
    Graphics3D::SetFog(Fog::SquaredExponential(ColorF(0.97), 0.005));
    Graphics3D::SetAmbientLight(ColorF(0.77));

    // スカイスフィア用の球状の Mesh
    // 裏側から球を覗くので、カリングされないように flipNormals を true にする。
    const Mesh skyMesh(MeshData::Sphere(480, 24, true));

    // スカイスフィア用の Equirectangular 天球画像
    const Texture skyTexture(L"sky.jpg", TextureDesc::For3D);

    // 地面の Mesh と Texture
    const Texture terrainTexture(L"Example/Ground.jpg", TextureDesc::For3D);
    const Mesh terrainMesh(MeshData::Plane(1000, { 1600, 1600 }));

    while (System::Update())
    {
        Graphics3D::FreeCamera(0.02);

        terrainMesh.draw(terrainTexture);

        // 空の画像をオリジナルの色で表示するために光源をオフ。環境光を 100% に
        Graphics3D::SetLightForward(0, Light::None());
        Graphics3D::SetAmbientLightForward(ColorF(1.0));
        {
            // 空の端に到達しないよう、常にスフィアの中心にいるようにする
            const Vec3 pos = Graphics3D::GetCamera().pos;

            // スカイスフィアを描く
            skyMesh.translated(pos.x, 0, pos.z).drawForward(skyTexture);
        }
        Graphics3D::SetLightForward(0, Light::Default());
        Graphics3D::SetAmbientLightForward(ColorF(0.77));
    }
}

3D モデルやシャドウを配置するとリアリティが増します。
ss.gif

2. マウスカーソルで選択している 3D オブジェクトを調べる

3D 空間で物体を選択するするには、マウスの Ray と Box, Sphere, Plane, Disc, Triangle3D 等との交差判定を利用します。交差する物体が複数存在する場合は、最も交点が近いオブジェクトを選択します。

ss2.gif

# include <Siv3D.hpp>

void Main()
{
    Window::Resize(1280, 720);
    Graphics::SetBackground(ColorF(0.4, 0.5, 0.6));
    Graphics3D::SetAmbientLight(ColorF(0.8));
    Graphics3D::SetAmbientLightForward(ColorF(1.0));
    Graphics3D::SetLightForward(0, Light::None());

    Array<Sphere> spheres;

    for (size_t i = 0; i < 80; ++i)
    {
        spheres.push_back(Sphere(RandomVec3({ -8, 8 }, { 0.5, 0.5 }, { -8, 8 }), 0.5));
    }

    while (System::Update())
    {
        Graphics3D::FreeCamera();

        Optional<size_t> selected;
        double nearestDistance = Largest<double>();
        const Ray mouseRay = Mouse::Ray();
        const Vec3 cameraPos = Graphics3D::GetCamera().pos;

        for (size_t i = 0; i < spheres.size(); ++i)
        {
            if (const auto pos = mouseRay.intersectsAt(spheres[i]))
            {
                const double d = pos->distanceFromSq(cameraPos);

                if (d < nearestDistance)
                {
                    selected = i;
                    nearestDistance = d;
                }
            }
        }

        Plane(24).draw();

        for (size_t i = 0; i < spheres.size(); ++i)
        {
            if (i == selected)
            {
                spheres[i].draw(Palette::Red).drawShadow();
            }
            else
            {
                spheres[i].draw(HSV(i * 3, 0.4, 0.8)).drawShadow();
            }       
        }

        Cursor::SetStyle(selected ? CursorStyle::Hand : CursorStyle::Default);
    }
}

3. プログラムで作った 3D 形状を Mesh として描画する

複雑な 3D 形状をプログラムで作成する場合や、.obj 形式以外のモデルファイルを自前で読み込む場合は、頂点とインデックスの配列を MeshData に用意することで、Mesh として描画できます。

頂点配列 Array<MeshVertex> は各頂点の 座標 (Float3) と テクスチャ UV (Float2) と 法線 (Float3) の配列で構成されます。
インデックス配列 Array<uint32> は、頂点配列の「何番目」と「何番目」と「何番目」の頂点で三角形を構成するかを表す配列で、サイズは必ず 3 の倍数になります。

法線を計算するのが面倒な場合には、自動で計算してくれる Mesh::computeNormals() を使います。

ss3a.png

# include <Siv3D.hpp>

MeshData CreateMeshData()
{
    const int32 N = 400;

    Array<MeshVertex> vertices;

    for (int32 i : step(N))
    {
        MeshVertex v;
        v.texcoord.set(0, 0);
        v.normal.set(0, 1, 0);

        v.position = Vec3(Cylindrical(i * 0.01, i * 0.05, i * 0.01));
        vertices.push_back(v);

        v.position = Vec3(Cylindrical(i * 0.01, i * 0.05, 0.4 + i * 0.01));
        vertices.push_back(v);
    }

    // 裏面用の頂点
    for (int32 i : step(N))
    {
        vertices.push_back(vertices[i * 2 + 0]);
        vertices.push_back(vertices[i * 2 + 1]);
    }

    Array<uint32> indices;

    for (int32 i : step(N - 1))
    {
        indices.push_back(2 * i + 0);
        indices.push_back(2 * i + 2);
        indices.push_back(2 * i + 1);

        indices.push_back(2 * i + 1);
        indices.push_back(2 * i + 2);
        indices.push_back(2 * i + 3);
    }

    const uint32 offset = N * 2;

    // 裏面用のインデックス
    for (int32 i : step(N - 1))
    {
        indices.push_back(offset + 2 * i + 0);
        indices.push_back(offset + 2 * i + 1);
        indices.push_back(offset + 2 * i + 2);

        indices.push_back(offset + 2 * i + 1);
        indices.push_back(offset + 2 * i + 3);
        indices.push_back(offset + 2 * i + 2);
    }

    MeshData meshData(vertices, indices);

    meshData.computeNormals();

    return meshData;
}

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

    Graphics::SetBackground(ColorF(0.5, 0.7, 0.9));

    Graphics3D::SetAmbientLight(ColorF(0.4));

    const Texture textureTerrain(L"Example/Grass.jpg", TextureDesc::For3D);

    const Mesh mesh(CreateMeshData());

    while (System::Update())
    {
        Graphics3D::FreeCamera();

        Plane(20).draw(textureTerrain);

        mesh.translated(0, 0, -4).draw().drawShadow();
    }
}

明日の Siv3D Advent Calendar の記事は @Mitsugoro32 さんです。
よろしくお願いします。