Siv3D
Siv3DDay 1

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

More than 1 year has passed since last update.

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 さんです。

よろしくお願いします。