Skip to content

3D Drawing Samples

1. Plot3D

Code
# 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);
		}

		// Draw RenderTexture to 2D scene
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		if (SimpleGUI::TextBox(te, Vec2{ 20,20 }, 1240))
		{
			// Math expression parser
			MathParser parser{ (te.text ? te.text : U"0") };

			// Set variables x, y
			double x = 0;
			double z = 0;
			parser.setVariable(U"x", &x);
			parser.setVariable(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
				{
					// Do not process if expression is invalid
					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
				{
					// Do not process if expression is invalid
					break;
				}
			}

			// Calculate normals
			meshDataFront.computeNormals();
			meshDataBack.computeNormals();

			// Update DynamicTexture
			meshFront.fill(meshDataFront);
			meshBack.fill(meshDataBack);
		}
	}
}

2. 3D text

Code
# 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 drawing
		{
			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
			{
				// Enable double-sided drawing
				const ScopedRenderStates3D rasterizer{ RasterizerState::SolidCullNone, BlendState::Additive };

				// Disable lighting
				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());
			}
		}

		// Draw 3D scene to 2D scene
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}
	}
}

3. 3D board game template

Code
# include <Siv3D.hpp>

// Function to procedurally generate table texture
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);
}

// Function to procedurally generate block texture
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);
}

// Function to draw table
void DrawTable(const Texture& tableTexture)
{
	Plane{ Vec3{ 0, -0.4, 0 }, 24 }.draw(tableTexture);
}

// Function to draw board
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);

	// Lines on the board
	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);
}

// Function to create Box from board index (x, y, z)
Box MakeBox(int32 x, int32 y, int32 z)
{
	return Box::FromPoints(Vec3{ (x - 4), (y + 1), (4 - z) }, Vec3{ (x - 3), y, (3 - z) });
}

// Function to draw block
void DrawBlock(int32 x, int32 y, int32 z, const ColorF& color, double scale = 1.0)
{
	MakeBox(x, y, z).scaled(scale).draw(color);
}

// Function to draw block
void DrawBlock(int32 x, int32 y, int32 z, const ColorF& color, const Texture& blockTexture)
{
	MakeBox(x, y, z).draw(blockTexture, color);
}

// Game state
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;
	}
};

// Function to draw blocks based on game state
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);
				}
			}
		}
	}
}

// Camera control class
class CameraController
{
public:

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

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

		// Areas around the board
		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))
			{
				// Change mouse cursor to hand icon
				Cursor::RequestStyle(CursorStyle::Hand);

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

		// Calculate viewpoint in spherical coordinates
		m_eyePosition = Spherical{ 24, m_theta, (270_deg - m_phi) };

		// Update camera
		m_camera.setView(m_eyePosition, m_focusPosition);

		// Set camera to scene
		Graphics3D::SetCameraTransform(m_camera);
	}

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

	bool isGrabbing() const
	{
		return m_grabbed;
	}

private:

	// Vertical field of view (radians)
	double m_verticalFOV = 25_deg;

	// Camera viewpoint position
	Vec3 m_eyePosition{ 16, 16, -16 };

	// Camera focus position
	Vec3 m_focusPosition{ 0, 0, 0 };

	double m_phi = -20_deg;

	double m_theta = 50_deg;

	// Camera
	BasicCamera3D m_camera;

	bool m_grabbed = false;

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

void Main()
{
	// Resize window and scene to 1280x720
	Window::Resize(1280, 720);

	// Set ambient light
	Graphics3D::SetGlobalAmbientColor(ColorF{ 0.75, 0.75, 0.75 });

	// Set sunlight
	Graphics3D::SetSunColor(ColorF{ 0.5, 0.5, 0.5 });

	// Set sun direction
	Graphics3D::SetSunDirection(Vec3{ 0, 1, -0.3 }.normalized());

	// Background color (remove sRGB curve with .removeSRGBCurve() for 3D colors)
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();

	// Multisample render texture for drawing 3D scene
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	// Table texture
	const Texture tableTexture{ CreateTableImage(), TextureDesc::MippedSRGB };

	// Block texture
	const Texture blockTexture{ CreateBlockImage(), TextureDesc::MippedSRGB };

	// 3D mesh for board
	const Mesh meshBoard{ MeshData::RoundedBox(0.1, Vec3{ 10, 0.4, 10 }, 5).translate(0, -0.2, 0) };

	// Camera control
	CameraController cameraController{ renderTexture.size() };

	// Game state
	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())
	{
		// Active voxel
		Point activeVoxelXZ{ -1, -1 };

		////////////////////////////////
		//
		//	State update
		//
		////////////////////////////////
		{
			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 drawing
		//
		////////////////////////////////
		{
			{
				// Fill renderTexture with background color and set as 3D render target
				const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };

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

				{
					// Enable transparency
					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 drawing
		//
		////////////////////////////////
		{
			if (SimpleGUI::Button(U"Clean up", Vec2{ 1100, 20 }, 160))
			{
				game = Game{};
			}
		}
	}
}

4. Low-resolution style 3D rendering

Code
# 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. Manual rotation of 3D objects

Code
# 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. Height map editing

Code
# 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);
		}

		// Draw RenderTexture to 2D scene
		{
			Graphics3D::Flush();
			renderTexture.resolve();
			Shader::LinearToScreen(renderTexture);
		}

		if (const bool gen = SimpleGUI::Button(U"Random", Vec2{ 270, 10 });
			(gen || (MouseL | MouseR).pressed())) // Edit terrain
		{
			// Edit 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 });
			}

			// Update 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);
	}
}