コンテンツにスキップ

3D 描画のサンプル

1. Plot3D

コード
# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture woodTexture{ U"example/texture/wood.jpg", TextureDesc::MippedSRGB };
	MeshData meshDataFront = MeshData::Grid(Vec2{ 20,20 }, 256, 256);
	MeshData meshDataBack = MeshData::Grid(Vec2{ 20,20 }, 256, 256).rotate(Quaternion::RotateX(180_deg));
	DynamicMesh meshFront{ meshDataFront }, meshBack{ meshDataBack };
	const PixelShader ps = HLSL{ U"example/shader/hlsl/forward_triplanar.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/forward_triplanar.frag", { { U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 } } };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 16, 12, -24 } };

	TextEditState te;

	while (System::Update())
	{
		if (not te.active)
		{
			camera.update(2.0);
			Graphics3D::SetCameraTransform(camera);
		}

		// 3D
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			const ScopedCustomShader3D shader{ ps };
			meshFront.draw(woodTexture);
			meshBack.draw(woodTexture);
		}

		// RenderTexture を 2D シーンに描画する
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		if (SimpleGUI::TextBox(te, Vec2{ 20,20 }, 1240))
		{
			// 数式パーサ
			MathParser parser{ (te.text ? te.text : U"0") };

			// 変数 x, y を設定する
			double x = 0;
			double z = 0;
			parser.setVaribale(U"x", &x);
			parser.setVaribale(U"y", &z);

			for (auto& vertex : meshDataFront.vertices)
			{
				x = vertex.pos.x;
				z = vertex.pos.z;

				if (auto y = parser.evalOpt())
				{
					vertex.pos.y = static_cast<float>(*y);
				}
				else
				{
					// 式が不正な場合は処理を行わない
					break;
				}
			}

			for (auto& vertex : meshDataBack.vertices)
			{
				x = vertex.pos.x;
				z = vertex.pos.z;

				if (auto y = parser.evalOpt())
				{
					vertex.pos.y = static_cast<float>(*y);
				}
				else
				{
					// 式が不正な場合は処理を行わない
					break;
				}
			}

			// 法線を計算する
			meshDataFront.computeNormals();
			meshDataBack.computeNormals();

			// DynamicTexture を更新する
			meshFront.fill(meshDataFront);
			meshBack.fill(meshDataBack);
		}
	}
}

2. 3D テキスト

コード
# include <Siv3D.hpp>

class Font3D
{
public:

	Font3D() = default;

	SIV3D_NODISCARD_CXX20
	explicit Font3D(const Font& font)
		: m_font{ font } {}

	[[nodiscard]]
	Array<MeshGlyph> getGlyphs(StringView s) const
	{
		Array<MeshGlyph> results;

		for (auto ch : s)
		{
			auto it = m_table.find(ch);

			if (it == m_table.end())
			{
				it = m_table.emplace(ch, m_font.createMesh(ch)).first;
			}

			results << it->second;
		}

		return results;
	}

	void drawCylindricalInner(StringView s, const Vec3& center, double r, double scale, double startAngle, const ColorF& color) const
	{
		const double perimeter = (r * Math::TwoPi);
		double penPosX = 0.0;
		startAngle += 90_deg;

		for (auto meshGlyph : getGlyphs(s))
		{
			penPosX += (meshGlyph.xOffset * scale);

			if (meshGlyph.mesh)
			{
				const double angle = (penPosX / perimeter) * 360_deg;
				const Quaternion q = Quaternion::RotateY(-90_deg + angle - startAngle);
				const Vec3 pos = Cylindrical{ r, (-180_deg - angle + startAngle), 0.0 } + center;
				const Mat4x4 mat = Mat4x4::Translate(-meshGlyph.xOffset, 0, 0)
					.scaled(scale)
					.rotated(q)
					.translated(pos);
				meshGlyph.mesh.draw(mat, color);
			}

			penPosX += (meshGlyph.xAdvance - meshGlyph.xOffset) * scale;
		}
	}

	void drawCylindricalOuter(StringView s, const Vec3& center, double r, double scale, double startAngle, const ColorF& color) const
	{
		const double perimeter = (r * Math::TwoPi);
		double penPosX = 0.0;
		startAngle += 90_deg;

		for (auto meshGlyph : getGlyphs(s))
		{
			penPosX += (meshGlyph.xOffset * scale);

			if (meshGlyph.mesh)
			{
				const double angle = (penPosX / perimeter) * 360_deg;
				const Quaternion q = Quaternion::RotateY(90_deg - angle - startAngle);
				const Vec3 pos = Cylindrical{ r, (180_deg + angle + startAngle), 0.0 } + center;
				const Mat4x4 mat = Mat4x4::Translate(-meshGlyph.xOffset, 0, 0)
					.scaled(scale)
					.rotated(q)
					.translated(pos);
				meshGlyph.mesh.draw(mat, color);
			}

			penPosX += (meshGlyph.xAdvance - meshGlyph.xOffset) * scale;
		}
	}

private:

	Font m_font;

	mutable HashTable<char32, MeshGlyph> m_table;
};

void Main()
{
	Window::Resize(1280, 720);
	const ColorF backgroundColor = ColorF{ 0.5, 0.6, 0.6 }.removeSRGBCurve();
	const Texture uvChecker{ U"example/texture/uv.png", TextureDesc::MippedSRGB };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };
	const Font3D font3D{ Font{ 60 } };

	while (System::Update())
	{
		const double t = Scene::Time();
		camera.update(2.0);
		Graphics3D::SetCameraTransform(camera);

		// 3D 描画
		{
			Graphics3D::SetGlobalAmbientColor(Graphics3D::DefaultGlobalAmbientColor);
			Graphics3D::SetSunColor(ColorF{ 0.75 });

			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			Plane{ 64 }.draw(uvChecker);
			Cylinder{ Vec3{0,0,0}, Vec3{0, 16, 0}, 5.6 }.draw(ColorF{ 0.25 }.removeSRGBCurve());

			// 3D Text Circle
			{
				// 両面描画を行う
				const ScopedRenderStates3D rasterizer{ RasterizerState::SolidCullNone, BlendState::Additive };

				// ライティングを無効化する
				Graphics3D::SetGlobalAmbientColor(ColorF{ 1.0 });
				Graphics3D::SetSunColor(ColorF{ 0.0 });

				font3D.drawCylindricalOuter(DateTime::Now().format(U"HH:mm:ss"), Vec3{ 0, 11.5, 0 }, 6 * 1.2, 3.0 * 1.2, (t * -25_deg), ColorF{ 1.0, 0.98, 0.9 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(DateTime::Now().format(U"HH:mm:ss"), Vec3{ 0, 11.5, 0 }, 6 * 1.2, 3.0 * 1.2, (t * -25_deg) + 180_deg, ColorF{ 1.0, 0.98, 0.9 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"Monday, September 27, 2021", Vec3{ 0, 10, 0 }, 6 * 1.2, 1.2 * 1.2, (t * -50_deg), ColorF{ 1.0, 0.98, 0.9 }.removeSRGBCurve());

				font3D.drawCylindricalOuter(U"NIKKEI 225 30,248.81 +609.41", Vec3{ 0, 8, 0 }, 6, 1.0, (t * -72_deg), ColorF{ 0.6, 1.0, 0.8 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"HANG SENG 24,192,16 -318.82", Vec3{ 0, 7, 0 }, 6, 1.0, (t * -62_deg), ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"SHANGHAI 3,613.07 -29.15", Vec3{ 0, 6, 0 }, 6, 1.0, (t * -58_deg), ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"FTSE 7,051.48 -26.87", Vec3{ 0, 5, 0 }, 6, 1.0, (t * -70_deg), ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"CAC 6,638.46 -63.52", Vec3{ 0, 4, 0 }, 6, 1.0, (t * -60_deg), ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"DAX 15,531.75 -112.22", Vec3{ 0, 3, 0 }, 6, 1.0, (t * -66_deg), ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"NASDAQ 15,047.70 -4.54", Vec3{ 0, 2, 0 }, 6, 1.0, (t * -68_deg), ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());

				font3D.drawCylindricalOuter(U"NIKKEI 225 30,248.81 +609.41", Vec3{ 0, 8, 0 }, 6, 1.0, (t * -72_deg) + 180_deg, ColorF{ 0.6, 1.0, 0.8 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"HANG SENG 24,192,16 -318.82", Vec3{ 0, 7, 0 }, 6, 1.0, (t * -62_deg) + 180_deg, ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"SHANGHAI 3,613.07 -29.15", Vec3{ 0, 6, 0 }, 6, 1.0, (t * -58_deg) + 180_deg, ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"FTSE 7,051.48 -26.87", Vec3{ 0, 5, 0 }, 6, 1.0, (t * -70_deg) + 180_deg, ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"CAC 6,638.46 -63.52", Vec3{ 0, 4, 0 }, 6, 1.0, (t * -60_deg) + 180_deg, ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"DAX 15,531.75 -112.22", Vec3{ 0, 3, 0 }, 6, 1.0, (t * -66_deg) + 180_deg, ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
				font3D.drawCylindricalOuter(U"NASDAQ 15,047.70 -4.54", Vec3{ 0, 2, 0 }, 6, 1.0, (t * -68_deg) + 180_deg, ColorF{ 1.0, 0.6, 0.6 }.removeSRGBCurve());
			}
		}

		// 3D シーンを 2D シーンに描画する
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}
	}
}

3. 3D のボードゲームテンプレート

コード
# include <Siv3D.hpp>

// テーブル用のテクスチャを手続き的に生成する関数
Image CreateTableImage()
{
	PerlinNoise noise;
	return Image::Generate(Size{ 1024, 1024 }, [&](Point p)
	{
		const double x = Fraction(noise.octave2D0_1(p * Vec2{ 0.03, 0.0005 }, 2) * 25) * 0.3 + 0.55;
		return ColorF{ x, 0.85 * x, 0.7 * x }.removeSRGBCurve();
	}).gaussianBlurred(3);
}

// ブロック用のテクスチャを手続き的に生成する関数
Image CreateBlockImage()
{
	PerlinNoise noise;
	return Image::Generate(Size{ 256, 256 }, [&](Point p)
	{
		const double x = Fraction(noise.octave2D0_1(p * Vec2{ 0.05, 0.0005 }, 2) * 25) * 0.15 + 0.85;
		return ColorF{ x }.removeSRGBCurve();
	}).gaussianBlurred(2);
}

// テーブルを描画する関数
void DrawTable(const Texture& tableTexture)
{
	Plane{ Vec3{ 0, -0.4, 0 }, 24 }.draw(tableTexture);
}

// 盤を描画する関数
void DrawBoard(const Mesh& mesh)
{
	const ColorF BoardColor = ColorF{ 0.9, 0.85, 0.75 }.removeSRGBCurve();
	const ColorF LineColor = ColorF{ 0.3, 0.2, 0.0 }.removeSRGBCurve();

	mesh.draw(BoardColor);

	// 盤上の線
	for (int32 i = -4; i <= 4; ++i)
	{
		Line3D{ Vec3{ -4, 0.01, i }, Vec3{ 4, 0.01, i } }.draw(LineColor);
		Line3D{ Vec3{ i, 0.01, -4 }, Vec3{ i, 0.01, 4 } }.draw(LineColor);
	}
	Line3D{ Vec3{ -4.1, 0.01, -4.1 }, Vec3{ 4.1, 0.01, -4.1 } }.draw(LineColor);
	Line3D{ Vec3{ -4.1, 0.01, 4.1 }, Vec3{ 4.1, 0.01, 4.1 } }.draw(LineColor);
	Line3D{ Vec3{ -4.1, 0.01, 4.1 }, Vec3{ -4.1, 0.01, -4.1 } }.draw(LineColor);
	Line3D{ Vec3{ 4.1, 0.01, 4.1 }, Vec3{ 4.1, 0.01, -4.1 } }.draw(LineColor);
}

// 盤上のインデックス (x, y, z) から Box を作成する関数
Box MakeBox(int32 x, int32 y, int32 z)
{
	return Box::FromPoints(Vec3{ (x - 4), (y + 1), (4 - z) }, Vec3{ (x - 3), y, (3 - z) });
}

// ブロックを描く関数
void DrawBlock(int32 x, int32 y, int32 z, const ColorF& color, double scale = 1.0)
{
	MakeBox(x, y, z).scaled(scale).draw(color);
}

// ブロックを描く関数
void DrawBlock(int32 x, int32 y, int32 z, const ColorF& color, const Texture& blockTexture)
{
	MakeBox(x, y, z).draw(blockTexture, color);
}

// ゲームの状態
struct Game
{
	static constexpr int32 MaxY = 15;

	int32 s[8][(MaxY + 1)][8] = {};

	int32 getHeight(int32 x, int32 z) const
	{
		for (int32 y = MaxY; 0 <= y; --y)
		{
			if (s[x][y][z])
			{
				return (y + 1);
			}
		}

		return 0;
	}
};

// ゲームの状態に基づいてブロックを描く関数
void DrawGame(const Game& game, const Texture& blockTexture)
{
	const ColorF BlockColor1 = ColorF{ 1.0, 0.85, 0.6 }.removeSRGBCurve();
	const ColorF BlockColor2 = ColorF{ 0.4, 0.15, 0.15 }.removeSRGBCurve();

	for (int32 y = 0; y <= Game::MaxY; ++y)
	{
		for (int32 x = 0; x < 8; ++x)
		{
			for (int32 z = 0; z < 8; ++z)
			{
				const int32 s = game.s[x][y][z];

				if (s == 1)
				{
					DrawBlock(x, y, z, BlockColor1, blockTexture);
				}
				else if (s == 2)
				{
					DrawBlock(x, y, z, BlockColor2, blockTexture);
				}
			}
		}
	}
}

// カメラの制御クラス
class CameraController
{
public:

	explicit CameraController(const Size& sceneSize)
		: m_camera{ sceneSize, m_verticalFOV, m_eyePosition, m_focusPosition } {}

	void update()
	{
		const Ray ray = getMouseRay();

		// 盤のまわり部分
		const std::array<Box, 4> boxes =
		{
			Box::FromPoints(Vec3{ -5, 0.0, 5 }, Vec3{ 5, -0.4, 4 }),
			Box::FromPoints(Vec3{ 4, 0.0, 5 }, Vec3{ 5, -0.4, -5 }),
			Box::FromPoints(Vec3{ -5, 0.0, -4 }, Vec3{ 5, -0.4, -5 }),
			Box::FromPoints(Vec3{ -5, 0.0, 5 }, Vec3{ -4, -0.4, -5 })
		};

		if (MouseL.up())
		{
			m_grabbed = false;
		}

		if (m_grabbed)
		{
			const double before = (m_cursorPos - Scene::Center()).getAngle();
			const double after = (Cursor::Pos() - Scene::Center()).getAngle();
			m_phi -= (after - before);
			m_cursorPos = Cursor::Pos();
		}

		for (const auto& box : boxes)
		{
			if (box.intersects(ray))
			{
				// マウスカーソルを手のアイコンにする
				Cursor::RequestStyle(CursorStyle::Hand);

				if ((not m_grabbed) && MouseL.down())
				{
					m_grabbed = true;
					m_cursorPos = Cursor::Pos();
				}
			}
		}

		// 視点を球面座標系で計算する
		m_eyePosition = Spherical{ 24, m_theta, (270_deg - m_phi) };

		// カメラを更新する
		m_camera.setView(m_eyePosition, m_focusPosition);

		// シーンにカメラを設定する
		Graphics3D::SetCameraTransform(m_camera);
	}

	Ray getMouseRay() const
	{
		return m_camera.screenToRay(Cursor::PosF());
	}

	bool isGrabbing() const
	{
		return m_grabbed;
	}

private:

	// 縦方向の視野角(ラジアン)
	double m_verticalFOV = 25_deg;

	// カメラの視点の位置
	Vec3 m_eyePosition{ 16, 16, -16 };

	// カメラの注視点の位置
	Vec3 m_focusPosition{ 0, 0, 0 };

	double m_phi = -20_deg;

	double m_theta = 50_deg;

	// カメラ
	BasicCamera3D m_camera;

	bool m_grabbed = false;

	Vec2 m_cursorPos = Scene::Center();
};

void Main()
{
	// ウインドウとシーンを 1280x720 にリサイズする
	Window::Resize(1280, 720);

	// 環境光を設定する
	Graphics3D::SetGlobalAmbientColor(ColorF{ 0.75, 0.75, 0.75 });

	// 太陽光を設定する
	Graphics3D::SetSunColor(ColorF{ 0.5, 0.5, 0.5 });

	// 太陽の方向を設定する
	Graphics3D::SetSunDirection(Vec3{ 0, 1, -0.3 }.normalized());

	// 背景色 (3D 用の色は .removeSRGBCurve() で sRGB カーブを除去)
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();

	// 3D シーンを描く、マルチサンプリング対応レンダーテクスチャ
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	// テーブル用のテクスチャ
	const Texture tableTexture{ CreateTableImage(), TextureDesc::MippedSRGB };

	// ブロック用のテクスチャ
	const Texture blockTexture{ CreateBlockImage(), TextureDesc::MippedSRGB };

	// 盤用の 3D メッシュ
	const Mesh meshBoard{ MeshData::RoundedBox(0.1, Vec3{ 10, 0.4, 10 }, 5).translate(0, -0.2, 0) };

	// カメラ制御
	CameraController cameraController{ renderTexture.size() };

	// ゲームの状態
	Game game;
	game.s[0][0][0] = game.s[1][0][1] = 1;
	game.s[4][0][4] = game.s[5][0][4] = 2;

	while (System::Update())
	{
		// アクティブなボクセル
		Point activeVoxelXZ{ -1, -1 };

		////////////////////////////////
		//
		//	状態の更新
		//
		////////////////////////////////
		{
			if (not cameraController.isGrabbing())
			{
				const Ray ray = cameraController.getMouseRay();
				float minDistance = 99999.9f;

				for (int32 x = 0; x < 8; ++x)
				{
					for (int32 z = 0; z < 8; ++z)
					{
						const int32 height = game.getHeight(x, z);

						const Box box = MakeBox(x, height, z);

						if (Optional<float> distacne = box.intersects(ray))
						{
							if (*distacne < minDistance)
							{
								minDistance = *distacne;
								activeVoxelXZ.set(x, z);
							}
						}
					}
				}

				if (activeVoxelXZ != Point{ -1, -1 })
				{
					auto& voxel = game.s[activeVoxelXZ.x][game.getHeight(activeVoxelXZ.x, activeVoxelXZ.y)][activeVoxelXZ.y];

					if (MouseL.down())
					{
						voxel = 1;
					}
					else if (MouseR.down())
					{
						voxel = 2;
					}
				}
			}

			cameraController.update();
		}

		////////////////////////////////
		//
		//	3D 描画
		//
		////////////////////////////////
		{
			{
				// renderTexture を背景色で塗りつぶし、3D 描画のレンダーターゲットに
				const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

				DrawTable(tableTexture);
				DrawBoard(meshBoard);
				DrawGame(game, blockTexture);

				{
					// 半透明を有効に
					const ScopedRenderStates3D blend{ BlendState::OpaqueAlphaToCoverage };

					for (int32 x = 0; x < 8; ++x)
					{
						for (int32 z = 0; z < 8; ++z)
						{
							const int32 height = game.getHeight(x, z);
							DrawBlock(x, height, z, ColorF{ 0.2, 0.8, 0.8, 0.5 }, ((activeVoxelXZ == Point{ x, z }) ? 1.0 : 0.25));
						}
					}
				}
			}

			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		////////////////////////////////
		//
		//	2D 描画
		//
		////////////////////////////////
		{
			if (SimpleGUI::Button(U"片づける", Vec2{ 1100, 20 }, 160))
			{
				game = Game{};
			}
		}
	}
}

4. 低解像度風の 3D レンダリング

コード
# include <Siv3D.hpp>

void Main()
{
	constexpr Size SceneSize{ 256, 192 };
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();

	const Texture wiindmill{ Image{ U"example/windmill.png" }.clipped(200, 230, 64, 64), TextureDesc::UnmippedSRGB };
	const Texture siv3dKun{ Image{ U"example/spritesheet/siv3d-kun-16.png" }.clipped(0, 0, 20, 32), TextureDesc::UnmippedSRGB };

	const Mesh spriteMesh{ MeshData::TwoSidedPlane(SizeF{ 2.0, 3.2 }).rotate(Quaternion::RotateX(-90_deg)) };
	const RenderTexture renderTexture{ SceneSize, TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 2, -32 } };

	while (System::Update())
	{
		camera.update(2.0);
		Graphics3D::SetCameraTransform(camera);

		// [3D rendering]
		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

			Plane{ 64 }.draw(ColorF{ 0.7 }.removeSRGBCurve());
			Box::FromPoints(Vec3{ -4, 0, -4 }, Vec3{ -2, 4, 4 }).draw(ColorF{ 0.8, 0.6, 0.4 }.removeSRGBCurve());
			Plane{ Vec3{0, 4, 0 }, 64 }.draw(ColorF{ 0.5 }.removeSRGBCurve());

			{
				const ScopedRenderStates3D sampler{ SamplerState::ClampNearest };
				Box{ 4, 2, 0, 4 }.draw(wiindmill);
			}

			{
				const ScopedRenderStates3D sampler{ SamplerState::ClampNearest, BlendState::Default2D };
				spriteMesh.draw(Vec3{ 0, 1.6, -4 }, siv3dKun);
			}
		}

		// [2D rendering]
		{
			Graphics3D::Flush();

			// TextureFilter::Nearest
			Shader::LinearToScreen(renderTexture, TextureFilter::Nearest);
		}
	}
}

5. 3D オブジェクトの手動回転

コード
# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture uvChecker{ U"example/texture/uv.png", TextureDesc::MippedSRGB };
	const Texture earthTexture{ U"example/texture/earth.jpg", TextureDesc::MippedSRGB };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	const Sphere sphere{ 0,4,0,4 };
	Quaternion rotation;
	bool grabbedX = false;
	bool grabbedY = false;

	const Cylinder cY{ sphere.center, 4.6, 1 };
	const Cylinder cX{ sphere.center, 4.5, 1, Quaternion::RotateZ(90_deg) };

	while (System::Update())
	{
		ClearPrint();
		camera.update(2.0);
		Graphics3D::SetCameraTransform(camera);

		{
			const Ray ray = camera.screenToRay(Cursor::PosF());
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			Plane{ 64 }.draw(uvChecker);
			sphere.draw(rotation, earthTexture);

			cY.draw(ColorF{ (grabbedY ? 0.8 : 0.5), 0.0, 0.0 }.removeSRGBCurve());
			cX.draw(ColorF{ 0.0, (grabbedX ? 0.8 : 0.5), 0.0 }.removeSRGBCurve());

			if (grabbedX)
			{
				rotation *= Quaternion::RotateX(-1_deg * Cursor::DeltaF().y);
			}

			if (grabbedY)
			{
				rotation *= Quaternion::RotateY(-1_deg * Cursor::DeltaF().x);
			}

			const Optional<float> cyd = ray.intersects(cY);
			const Optional<float> cxd = ray.intersects(cX);

			Print << U"cyd: " << cyd;
			Print << U"cxd: " << cxd;

			if (cyd || cxd)
			{
				Cursor::RequestStyle(CursorStyle::Hand);

				if (MouseL.down())
				{
					if (cxd && cyd)
					{
						((cxd < cyd) ? grabbedX : grabbedY) = true;
					}
					else
					{
						((cxd) ? grabbedX : grabbedY) = true;
					}
				}
			}

			if (MouseL.up())
			{
				grabbedY = grabbedX = false;
			}
		}

		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}
	}
}

6. 高さマップの編集

コード
# include <Siv3D.hpp>

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

	const VertexShader vsTerrain = HLSL{ U"example/shader/hlsl/terrain_forward.hlsl", U"VS" }
		| GLSL{ U"example/shader/glsl/terrain_forward.vert", { { U"VSPerView", 1 }, { U"VSPerObject", 2 }, { U"VSPerMaterial", 3 } } };

	const PixelShader psTerrain = HLSL{ U"example/shader/hlsl/terrain_forward.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/terrain_forward.frag", { { U"PSPerFrame", 0 }, { U"PSPerView", 1 }, { U"PSPerMaterial", 3 } } };

	const PixelShader psNormal = HLSL{ U"example/shader/hlsl/terrain_normal.hlsl", U"PS" }
		| GLSL{ U"example/shader/glsl/terrain_normal.frag", { {U"PSConstants2D", 0} } };

	if ((not vsTerrain) || (not psTerrain) || (not psNormal))
	{
		return;
	}

	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const Texture terrainTexture{ U"example/texture/grass.jpg", TextureDesc::MippedSRGB };
	const Texture rockTexture{ U"example/texture/rock.jpg", TextureDesc::MippedSRGB };
	const Texture brushTexture{ U"example/particle.png" };
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	const Mesh gridMesh{ MeshData::Grid({ 128, 128 }, 128, 128) };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };
	RenderTexture heightmap{ Size{ 256, 256 }, ColorF{ 0.0 }, TextureFormat::R32_Float };
	RenderTexture normalmap{ Size{ 256, 256 }, ColorF{ 0.0, 0.0, 0.0 }, TextureFormat::R16G16_Float };

	while (System::Update())
	{
		camera.update(2.0);

		// 3D
		{
			Graphics3D::SetCameraTransform(camera);

			const ScopedCustomShader3D shader{ vsTerrain, psTerrain };
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			const ScopedRenderStates3D ss{ { ShaderStage::Vertex, 0, SamplerState::ClampLinear} };
			Graphics3D::SetVSTexture(0, heightmap);
			Graphics3D::SetPSTexture(1, normalmap);
			Graphics3D::SetPSTexture(2, rockTexture);

			gridMesh.draw(terrainTexture);
		}

		// RenderTexture を 2D シーンに描画
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		if (const bool gen = SimpleGUI::Button(U"Random", Vec2{ 270, 10 });
			(gen || (MouseL | MouseR).pressed())) // 地形を編集
		{
			// heightmap を編集
			if (gen)
			{
				const PerlinNoiseF perlin{ RandomUint64() };
				Grid<float> grid(256, 256);
				for (auto p : step(grid.size()))
				{
					grid[p] = perlin.octave2D0_1(p / 256.0f, 5) * 16.0f;
				}
				const RenderTexture noise{ grid };
				const ScopedRenderTarget2D target{ heightmap };
				noise.draw();
			}
			else
			{
				const ScopedRenderTarget2D target{ heightmap };
				const ScopedRenderStates2D blend{ BlendState::Additive };
				brushTexture.scaled(1.0 + MouseL.pressed()).drawAt(Cursor::PosF(), ColorF{ Scene::DeltaTime() * 15.0 });
			}

			// normal map を更新
			{
				const ScopedRenderTarget2D target{ normalmap };
				const ScopedCustomShader2D shader{ psNormal };
				const ScopedRenderStates2D blend{ BlendState::Opaque, SamplerState::ClampLinear };
				heightmap.draw();
			}
		}

		heightmap.draw(ColorF{ 0.1 });
		normalmap.draw(0, 260);
	}
}