LoginSignup
9
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-01

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 さんです。
よろしくお願いします。

9
5
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
9
5