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

[Siv3D Advent Calendar 2016](http://qiita.com/advent-calendar/2016/siv3d) 1 日目の記事です。
Siv3D August 2016 v2 で使える 3D プログラムのテクニックを 3 つ紹介します。
#1. スカイスフィア
ゲームワールドでリアリティのある空を表現するには、Equirectangular 形式の 360° 天球画像を Sphere に貼り付けて描画するのが簡単です。

![sky.jpg](https://qiita-image-store.s3.amazonaws.com/0/30255/37c52f4c-e96a-0867-05e2-203abc09338a.jpeg)

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

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

```cpp
# 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](https://qiita-image-store.s3.amazonaws.com/0/30255/4ceedf14-301e-acda-e26c-3e65aeae4c77.gif)

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

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

![ss2.gif](https://qiita-image-store.s3.amazonaws.com/0/30255/e004fa9a-8602-aee6-9215-e1aee6e9e7db.gif)


```cpp
# 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](https://qiita-image-store.s3.amazonaws.com/0/30255/ec5e141b-81f4-0dcb-495c-e6fb894f3c47.png)

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