先日、OpenSiv3Dがメジャーアップデートされ、v0.6になりました。OpenSiv3DはWindows、macOS、Linux、Webブラウザに対応する高性能なグラフィックライブラリで、C++版Cities Boxやミツデスマスの開発に利用させていただいています。
これまで3Dは暫定的な対応のみで基本的には2D表示のみ対応していましたが、v0.6からはついに3D本格対応です。開発者の方々には本当に頭が下がります。今回は3Dを実際に描画させて実行してみたいと思います。なお、今回はMacで実行していますが、もちろんWindowsやLinux、Web版でも動作可能です。

※バグの修正などが行われたので現在の最新版はv0.6.2です(2021年10月6日現在)

参考文献

Siv3D リファレンス v0.6.2

  • チュートリアル 36 | 3D 形状を描く

基本的な概念 〜2Dとの違い〜

色空間の違い

色空間というのは要は画面上の物体だったり画像だったりの色を表す方法のことを指しますが、2D描画ではガンマ色空間(sRGB色空間)だったのに対し、3D描画ではリニア色空間を用いているとのことです。ガンマ色空間は明るさが非線形であるため、陰影を扱う3D空間上では不適合とのこと。

描画方法

  1. 3D描画用のレンダーテクスチャを作成
  2. レンダーテクスチャに3Dシーンを描く
  3. メインのシーン(2D空間)に転送&2D描画

基本的には仮想的に3D空間内に物体などを描き、それを2D空間に転送した上で実際の画面上に描画するという流れ。このとき、3D空間上ではリニア色空間で色を表現します。これは最終的に2D空間に転送した際にガンマ色空間に変換されます。

基本的な概念 〜座標系について〜

座標系は左手系です。x軸が画面右、y軸が画面上に向かって座標値が増加し、z軸は画面奥に向かって座標値が増加します。
left-hand
3D座標の表現にはVec3型を利用します。

描画してみる

リファレンスにあるもののうち幾つかを実行してみます。
コピペではないのでサンプルのプログラムと若干異なりますが、やっていることは基本的に同じです。

3Dシーンに図形と床を表示してみる

#include <Siv3D.hpp>

void Main() {
	// 3D空間の背景色
	// removeSRGBCurveでsRGBカーブを除去する必要あり
	const ColorF backgroundColor = ColorF{0.4, 0.6, 0.8}.removeSRGBCurve();
	
	// UVチェック用テクスチャ(地面に表示される)
	// uv.pngからテクスチャ生成
	const Texture uvChecker{U"example/texture/uv.png", TextureDesc::MippedSRGB};
	
	// 3D描画用レンダーテクスチャ
	const MSRenderTexture renderTexture{Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes};
	
	// 3Dシーン用のデバッグカメラ
	DebugCamera3D camera{renderTexture.size(), 30_deg, Vec3{10, 16, -32}};
	
	while (System::Update()) {
		// カメラの移動スピード:2.0でデバッグカメラを更新
		camera.update(2.0);
		
		// 3Dシーンにカメラを設定
		Graphics3D::SetCameraTransform(camera);
		
		// 3D描画
		{
			// renderTextureを3D描画のレンダーターゲットにする
			const ScopedRenderTarget3D target{renderTexture.clear(backgroundColor)};
			
			// 床(=uvChecker)を描画
			Plane{64}.draw(uvChecker);
			
			// ボックスの描画
			Box{-8, 2, 0, 4}.draw(ColorF{0.8, 0.6, 0.4}.removeSRGBCurve());
			
			// 球の描画
			Sphere{0, 2, 0, 2}.draw(ColorF{0.4, 0.8, 0.6}.removeSRGBCurve());
			
			// 円柱の描画
			Cylinder{8, 2, 0, 2, 4}.draw(ColorF{0.6, 0.4, 0.8}.removeSRGBCurve());
		}
		
		// 3Dシーンを2Dシーンに描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			
			// 転送
			Shader::LinearToScreen(renderTexture);
		}
	}
}

これを実行してみます。すると…
スクリーンショット 2021-10-05 22.48.52
おお!見事に床と背景と3つの図形が表示されました。
なお、キー操作によってカメラの移動や角度の調整が可能です。

キー 操作
角度:カメラを上に向ける(チルト)
角度:カメラを下に向ける(チルト)
角度:カメラを右に向ける(パン)
角度:カメラを左に向ける(パン)
W 位置(z軸):前進
S 位置(z軸):後退
D 位置(x軸):右に移動
A 位置(x軸):左に移動
E 位置(y軸):上に移動
X 位置(y軸):下に移動
Shift + キー 早く移動
control(Ctrl) + キー めちゃめちゃ早く移動

カメラの状態を表示する

カメラの位置、フォーカスしている座標値、視野角を表示してみます。

#include <Siv3D.hpp>

void Main() {
	// 3D空間の背景色
	// removeSRGBCurveでsRGBカーブを除去する必要あり
	const ColorF backgroundColor = ColorF{0.4, 0.6, 0.8}.removeSRGBCurve();
	
	// UVチェック用テクスチャ(地面に表示される)
	// uv.pngからテクスチャ生成
	const Texture uvChecker{U"example/texture/uv.png", TextureDesc::MippedSRGB};
	
	// 3D描画用レンダーテクスチャ
	const MSRenderTexture renderTexture{Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes};
	
	// 3Dシーン用のデバッグカメラ
	DebugCamera3D camera{renderTexture.size(), 30_deg, Vec3{10, 16, -32}};
	
	while (System::Update()) {
		ClearPrint();
		
		// カメラの移動スピード:2.0でデバッグカメラを更新
		camera.update(2.0);
		
		// カメラの状態を表示
		Print << U"eyePosition: {:.1f}"_fmt(camera.getEyePosition());
		Print << U"focusPosition: {:.1f}"_fmt(camera.getFocusPosition());
		Print << U"verticalFOV: {:.1f}°"_fmt(Math::ToDegrees(camera.getVerticlaFOV()));
		
		// 3Dシーンにカメラを設定
		Graphics3D::SetCameraTransform(camera);
		
		// 3D描画
		{
			// renderTextureを3D描画のレンダーターゲットにする
			const ScopedRenderTarget3D target{renderTexture.clear(backgroundColor)};
			
			// 床(=uvChecker)を描画
			Plane{64}.draw(uvChecker);
			
			// ボックスの描画
			Box{-8, 2, 0, 4}.draw(ColorF{0.8, 0.6, 0.4}.removeSRGBCurve());
			
			// 球の描画
			Sphere{0, 2, 0, 2}.draw(ColorF{0.4, 0.8, 0.6}.removeSRGBCurve());
			
			// 円柱の描画
			Cylinder{8, 2, 0, 2, 4}.draw(ColorF{0.6, 0.4, 0.8}.removeSRGBCurve());
		}
		
		// 3Dシーンを2Dシーン描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			
			// 転送
			Shader::LinearToScreen(renderTexture);
		}
	}
}

スクリーンショット 2021-10-05 23.09.56
これは3Dゲームなどを作ってる時のデバッグに使えそうです。

視野角を変更する

上記のプログラムではカメラの視野角を30°に設定していましたが、もう少し広げて60°にしてみます。

// 3Dシーン用のデバッグカメラ
DebugCamera3D camera{renderTexture.size(), 60_deg, Vec3{10, 16, -32}};

スクリーンショット 2021-10-05 23.15.55
先程の画像と比べると、より広域に表示されていることがわかります。

環境光の色を変更

環境光というのは、陰影に関わらず物体全体に与えられる光のことです。夕方をイメージして、Graphics3D::SetGlobalAmbientColor(color)で少し赤っぽくしてみます。

#include <Siv3D.hpp>

void Main() {
	// 3D空間の背景色
	// removeSRGBCurveでsRGBカーブを除去する必要あり
	const ColorF backgroundColor = ColorF{0.4, 0.6, 0.8}.removeSRGBCurve();
	
	// UVチェック用テクスチャ(地面に表示される)
	// uv.pngからテクスチャ生成
	const Texture uvChecker{U"example/texture/uv.png", TextureDesc::MippedSRGB};
	
	// 3D描画用レンダーテクスチャ
	const MSRenderTexture renderTexture{Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes};
	
	// 3Dシーン用のデバッグカメラ
	DebugCamera3D camera{renderTexture.size(), 60_deg, Vec3{10, 16, -32}};
	
	// 環境光の色(赤っぽく)
	ColorF ambiemtColor = ColorF(0.5, 0.0, 0.0);
	
	while (System::Update()) {
		ClearPrint();
		
		// カメラの移動スピード:2.0でデバッグカメラを更新
		camera.update(2.0);
		
		// 3Dシーンにカメラを設定
		Graphics3D::SetCameraTransform(camera);
		
		// 環境光を設定
		Graphics3D::SetGlobalAmbientColor(ambiemtColor);
		
		// 3D描画
		{
			// renderTextureを3D描画のレンダーターゲットにする
			const ScopedRenderTarget3D target{renderTexture.clear(backgroundColor)};
			
			// 床(=uvChecker)を描画
			Plane{64}.draw(uvChecker);
			
			// ボックスの描画
			Box{-8, 2, 0, 4}.draw(ColorF{0.8, 0.6, 0.4}.removeSRGBCurve());
			
			// 球の描画
			Sphere{0, 2, 0, 2}.draw(ColorF{0.4, 0.8, 0.6}.removeSRGBCurve());
			
			// 円柱の描画
			Cylinder{8, 2, 0, 2, 4}.draw(ColorF{0.6, 0.4, 0.8}.removeSRGBCurve());
		}
		
		// 3Dシーンを2Dシーン描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			
			// 転送
			Shader::LinearToScreen(renderTexture);
		}
	}
}

スクリーンショット 2021-10-06 0.42.18

さらに夕方っぽくする

太陽光の色を変えてみましょう。Graphics3D::SetSunColor(color)で太陽光の色を変更できます。

#include <Siv3D.hpp>

void Main() {
	// 3D空間の背景色
	// removeSRGBCurveでsRGBカーブを除去する必要あり
	const ColorF backgroundColor = ColorF{0.4, 0.6, 0.8}.removeSRGBCurve();
	
	// UVチェック用テクスチャ(地面に表示される)
	// uv.pngからテクスチャ生成
	const Texture uvChecker{U"example/texture/uv.png", TextureDesc::MippedSRGB};
	
	// 3D描画用レンダーテクスチャ
	const MSRenderTexture renderTexture{Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes};
	
	// 3Dシーン用のデバッグカメラ
	DebugCamera3D camera{renderTexture.size(), 60_deg, Vec3{10, 16, -32}};
	
	// 環境光の色(赤っぽく)
	ColorF ambiemtColor = ColorF(0.5, 0.0, 0.0);
	
	// 太陽光の色
	ColorF sunColor = ColorF(0.7, 0.2, 0.0);
	
	while (System::Update()) {
		ClearPrint();
		
		// カメラの移動スピード:2.0でデバッグカメラを更新
		camera.update(2.0);
		
		// 3Dシーンにカメラを設定
		Graphics3D::SetCameraTransform(camera);
		
		// 環境光を設定
		Graphics3D::SetGlobalAmbientColor(ambiemtColor);
		
		// 太陽光の色を設定
		Graphics3D::SetSunColor(sunColor);
		
		// 3D描画
		{
			// renderTextureを3D描画のレンダーターゲットにする
			const ScopedRenderTarget3D target{renderTexture.clear(backgroundColor)};
			
			// 床(=uvChecker)を描画
			Plane{64}.draw(uvChecker);
			
			// ボックスの描画
			Box{-8, 2, 0, 4}.draw(ColorF{0.8, 0.6, 0.4}.removeSRGBCurve());
			
			// 球の描画
			Sphere{0, 2, 0, 2}.draw(ColorF{0.4, 0.8, 0.6}.removeSRGBCurve());
			
			// 円柱の描画
			Cylinder{8, 2, 0, 2, 4}.draw(ColorF{0.6, 0.4, 0.8}.removeSRGBCurve());
		}
		
		// 3Dシーンを2Dシーン描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			
			// 転送
			Shader::LinearToScreen(renderTexture);
		}
	}
}

スクリーンショット 2021-10-06 0.49.28
ちょっと赤っぽくしすぎたかな?

太陽の方向を変更する

光源の位置はGraphics3D::SetSunDirection(direction)で変更できます。太陽の方向は単位ベクトル(長さ1のベクトル)で指定します。

#include <Siv3D.hpp>

void Main() {
	// 3D空間の背景色
	// removeSRGBCurveでsRGBカーブを除去する必要あり
	const ColorF backgroundColor = ColorF{0.4, 0.6, 0.8}.removeSRGBCurve();
	
	// UVチェック用テクスチャ(地面に表示される)
	// uv.pngからテクスチャ生成
	const Texture uvChecker{U"example/texture/uv.png", TextureDesc::MippedSRGB};
	
	// 3D描画用レンダーテクスチャ
	const MSRenderTexture renderTexture{Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes};
	
	// 3Dシーン用のデバッグカメラ
	DebugCamera3D camera{renderTexture.size(), 60_deg, Vec3{10, 16, -32}};
	
	// 太陽の向き
	double direction = 90_deg;
	double evelation = 45_deg;
	const Vec3 sunDirection = Spherical{1.0, (90_deg - evelation), (-direction + 90_deg)};
	
	while (System::Update()) {
		ClearPrint();
		
		// カメラの移動スピード:2.0でデバッグカメラを更新
		camera.update(2.0);
		
		// 3Dシーンにカメラを設定
		Graphics3D::SetCameraTransform(camera);
		
		// 太陽の向きの設定
		Graphics3D::SetSunDirection(sunDirection);
		
		// 3D描画
		{
			// renderTextureを3D描画のレンダーターゲットにする
			const ScopedRenderTarget3D target{renderTexture.clear(backgroundColor)};
			
			// 床(=uvChecker)を描画
			Plane{64}.draw(uvChecker);
			
			// ボックスの描画
			Box{-8, 2, 0, 4}.draw(ColorF{0.8, 0.6, 0.4}.removeSRGBCurve());
			
			// 球の描画
			Sphere{0, 2, 0, 2}.draw(ColorF{0.4, 0.8, 0.6}.removeSRGBCurve());
			
			// 円柱の描画
			Cylinder{8, 2, 0, 2, 4}.draw(ColorF{0.6, 0.4, 0.8}.removeSRGBCurve());
		}
		
		// 3Dシーンを2Dシーン描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			
			// 転送
			Shader::LinearToScreen(renderTexture);
		}
	}
}

色が赤すぎて分かりづらかったので環境光と太陽光の色は元に戻しました。

変更前:
スクリーンショット 2021-10-06 1.04.15
変更後:
スクリーンショット 2021-10-06 0.58.15
これは3D都市開発ゲームにも使えそうです。パラメータで指定できるので、ゲーム内の現在時刻に合わせて太陽の位置や色を変更させれば一日の再現ができますね。

おわりに

まだまだ3D描画で出来ることはたくさんありますが、とりあえず今回はここまで。今回は図形と画像しか使いませんでしたが、3Dモデル(Wavefront .obj形式)にも対応しているようです。近いうちに3Dモデルを読み込んでいろいろ試してみたいと思います。