Open5

Siv3D v0.6.12 新規追加・更新サンプル

Ryo SuzukiRyo Suzuki

Rect / RectF から角度を指定して平行四辺形を作成

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	while (System::Update())
	{
		const double angle = (Periodic::Sine1_1(6s) * 45_deg);

		for (int32 i = 0; i < 6; ++i)
		{
			Rect{ (50 + i * 120), (100 + i * 40), (20 + i * 20), (100 + i * 40)}
				.draw()
				.skewedX(angle)
				.draw(HSV{ (i * 60), 0.6, 0.9 });
		}
	}
}
Ryo SuzukiRyo Suzuki

Morton Order

2D

# include <Siv3D.hpp>

void DFS(const Morton32 morton, const uint32 currentLevel, const uint32 maxLevel)
{
	// 最大レベルにおける 1 辺のセルの数
	const int32 baseGridCount = (1 << maxLevel);

	// 現在のレベルにおける 1 辺のセルの数
	const uint32 n = (1 << currentLevel);

	// 現在のレベルにおけるセルのサイズ
	const int32 currentCellSize = ((baseGridCount / n) * (720 / baseGridCount));

	// モートン順序から二次元のインデックスを取得
	const Point index = Morton::Decode2D32(morton);

	// 現在のセルの正方形
	const Rect rect{ (index * currentCellSize), currentCellSize };

	if (not rect.mouseOver())
	{
		return;
	}

	rect.draw(Colormap01F(currentLevel / (maxLevel + 1.0)));

	// 最大レベルに達したら終了
	if (currentLevel == maxLevel)
	{
		return;
	}

	// 子のレベル
	const uint32 childLevel = (currentLevel + 1);

	// 子のモートン順序
	const Morton32 childMortonBase = (morton << 2);

	for (uint32 i = 0; i < 4; ++i)
	{
		DFS((childMortonBase + i), childLevel, maxLevel);
	}
}

void Main()
{
	Window::Resize(1280, 720);
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	size_t maxLevel = 3;
	LineString points;

	while (System::Update())
	{
		const int32 baseCellSize = (720 / (1 << maxLevel));
		const double circleRadius = (baseCellSize / 2.8);
		const double fontSize = (baseCellSize / 3.0);
		const double lineThickness = (baseCellSize / 12.0);

		if (not points)
		{
			for (Morton32 morton = 0; morton < (1u << (maxLevel * 2)); ++morton)
			{
				const Point index = Morton::Decode2D32(morton);
				points << ((index * baseCellSize) + (Point{ baseCellSize, baseCellSize } / 2));
			}
		}

		DFS(0, 0, static_cast<uint32>(maxLevel));

		points.draw(lineThickness);

		for (const auto& point : points)
		{
			point.asCircle(circleRadius).draw();
		}

		{
			Morton32 morton = 0;

			for (const auto& point : points)
			{
				font(morton++).drawAt(fontSize, point, ColorF{ 0.11 });
			}
		}

		if (SimpleGUI::RadioButtons(maxLevel, { U"0", U"1", U"2", U"3", U"4", U"5" }, Vec2{ 760, 40 }))
		{
			points.clear();
		}
	}
}

3D

# include <Siv3D.hpp>

void Main()
{
	Window::Resize(1280, 720);
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };

	BasicCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };
	Stopwatch stopwatch{ StartImmediately::Yes };
	Timer timer{ 16s };

	while (System::Update())
	{
		ClearPrint();

		if (KeySpace.down())
		{
			stopwatch.restart();
			timer.reset();
		}

		const Vec3 eyePos = Cylindrical{ 18, -160_deg + stopwatch.sF() * 3_deg, 11 } + Vec3{ 4, 0, -6 };
		const Vec3 focusPos{ 4, 4.5, -6 };
		camera.setView(eyePos, focusPos);

		Graphics3D::SetCameraTransform(camera);

		{
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			Plane{ 64 }.draw();

			constexpr uint32 N = 3;

			if ((not timer.isStarted()) && 36s < stopwatch)
			{
				timer.start();
			}

			if (timer.isStarted())
			{
				const double t = ((timer.sF() * 2.0) / 0.06);

				for (uint32 morton = 0; morton < Min<int64>((1 << (N * 3)), int32(t)); ++morton)
				{
					const Vec3 center = ((Morton::Decode3D32(morton) + Vec3{ 0.5, 0.5, 0.5 }) * Vec3 { 1, 1, -1 });
					const double scale = EaseInQuart(Min((t - morton) * 0.04, 1.0));
					Box{ center, scale }.draw(HSV{ morton * 2.0, 0.6, 0.85 }.removeSRGBCurve());
				}
			}
			else
			{
				const double t = ((stopwatch.sF() - 2.0) / 0.06);

				for (uint32 morton = 0; morton < Min<int64>((1 << (N * 3)), int32(t)); ++morton)
				{
					const Vec3 center = ((Morton::Decode3D32(morton) + Vec3{ 0.5, 0.5, 0.5 }) * Vec3 { 1, 1, -1 });
					const double scale = EaseOutExpo(Min((t - morton) * 0.05, 1.0));
					Box{ center, scale }.draw(HSV{ morton * 2.0, 0.6, 0.85 }.removeSRGBCurve());
				}
			}
		}

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

Terrain

# include <Siv3D.hpp>

class Terrain
{
public:

	static constexpr int32 MaxLevel = 8;

	static constexpr int32 GridCount = (1 << (MaxLevel));

	static constexpr s3d::Size MapSize{ GridCount, GridCount };

	static constexpr double TerrainSize = 256.0;

	static constexpr float HeightScale = 16.0f;

	Terrain() = default;

	const RenderTexture& getHeightmapTexture() const noexcept
	{
		return m_heightmapTexture;
	}

	const RenderTexture& getNormalMapTexture() const noexcept
	{
		return m_normalMapTexture;
	}

	Vec3 calculateTileCenter(Point index) const noexcept
	{
		const double x = (index.x - (GridCount / 2) + 0.5) * (TerrainSize / GridCount);
		const double y = m_heightmap[index.y][index.x];
		const double z = -(index.y - (GridCount / 2) + 0.5) * (TerrainSize / GridCount);
		return{ x, y, z };
	}

	Box calculateBoundingBox(uint32 level, Morton32 morton) const noexcept
	{
		// 現在のレベルのボックスの数 (1, 4, 16, 64, 256, ... )	
		const uint32 numIndices = (1 << (2 * level));
		const uint32 N = (1 << level);

		assert(level <= MaxLevel);
		assert(morton < numIndices);

		const auto [xi, yi] = Morton::Decode2D32(morton);
		const int32 x = (xi * (GridCount / N));
		const int32 y = (yi * (GridCount / N));

		float minY = Largest<float>;
		float maxY = Smallest<float>;

		for (uint32 yy = y; yy < (y + (GridCount / N)); ++yy)
		{
			for (uint32 xx = x; xx < (x + (GridCount / N)); ++xx)
			{
				const float h = m_heightmap[yy][xx];
				minY = Min(minY, h);
				maxY = Max(maxY, h);
			}
		}

		const double boxSize = (TerrainSize / N);
		const double minX = (-TerrainSize / 2) + (xi * boxSize);
		const double maxX = (minX + boxSize);
		const double minZ = (-TerrainSize / 2) + (yi * boxSize);
		const double maxZ = (minZ + boxSize);

		return Box::FromPoints({ minX, 0.0, -minZ }, { maxX, maxY, -maxZ });
	}

	void clear()
	{
		m_heightmap.fill(0.0f);
		m_heightmapTexture.clear(ColorF{ 0.0 });
		m_normalMapTexture.clear(ColorF{ 0.0 });
	}

	void fillRandomHeightmap()
	{
		GenerateRandomHeightmap(m_heightmap);
		{
			const RenderTexture noise{ m_heightmap };
			const ScopedRenderTarget2D target{ m_heightmapTexture };
			noise.draw();
		}
	}

	void readHeightmapFromHeightmapTexture()
	{
		m_heightmapTexture.read(m_heightmap);
	}

private:

	Grid<float> m_heightmap{ MapSize, 0.0f };

	RenderTexture m_heightmapTexture{ MapSize, ColorF{ 0.0 }, TextureFormat::R32_Float };

	RenderTexture m_normalMapTexture{ MapSize, ColorF{ 0.0 }, TextureFormat::R16G16_Float };

	static void GenerateRandomHeightmap(Grid<float>& heightmap)
	{
		constexpr int32 Octaves = 5;
		const PerlinNoiseF noise{ RandomUint64() };

		for (int32 y = 0; y < heightmap.height(); ++y)
		{
			for (int32 x = 0; x < heightmap.width(); ++x)
			{
				const Float2 pos{ x, y };
				const float h = noise.octave2D0_1((pos / 256.0f), Octaves);
				heightmap[y][x] = (h * HeightScale);
			}
		}
	}
};

class TerrainRenderer
{
public:

	TerrainRenderer() = default;

	bool init()
	{
		m_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 }} };

		m_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 }} };

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

		if ((not m_vsTerrain) || (not m_psTerrain) || (not m_psNormal))
		{
			return false;
		}

		m_gridMesh = Mesh{ MeshData::Grid(SizeF{ Terrain::TerrainSize, Terrain::TerrainSize }, 128, 128) };

		return true;
	}

	void draw(const Terrain& terrain, const Texture& grassTexture, const Texture& rockTexture) const
	{
		const ScopedCustomShader3D shader{ m_vsTerrain, m_psTerrain };
		const ScopedRenderStates3D ss{ { ShaderStage::Vertex, 0, SamplerState::ClampLinear} };
		Graphics3D::SetVSTexture(0, terrain.getHeightmapTexture());
		Graphics3D::SetPSTexture(1, terrain.getNormalMapTexture());
		Graphics3D::SetPSTexture(2, rockTexture);
		m_gridMesh.draw(grassTexture);
	}

	void updateNormalMapTexture(const Terrain& terrain)
	{
		const ScopedRenderTarget2D target{ terrain.getNormalMapTexture() };
		const ScopedCustomShader2D shader{ m_psNormal };
		const ScopedRenderStates2D blend{ BlendState::Opaque, SamplerState::ClampLinear };
		terrain.getHeightmapTexture().draw();
	}

private:

	VertexShader m_vsTerrain;

	PixelShader m_psTerrain;

	PixelShader m_psNormal;

	Mesh m_gridMesh;
};


void DFS(const Terrain& terrain, const Ray& mouseRay, Morton32 morton, uint32 level, uint32 maxLevel, Array<Morton32>& results)
{
	const Box boundingBox = terrain.calculateBoundingBox(level, morton);

	if (not boundingBox.intersects(mouseRay))
	{
		return;
	}

	if (level == maxLevel)
	{
		//boundingBox.drawFrame();
		results.push_back(morton);
		return;
	}

	const uint32 childLevel = (level + 1);
	const Morton32 childMorton = (morton << 2);

	for (uint32 i = 0; i < 4; ++i)
	{
		DFS(terrain, mouseRay, (childMorton + i), childLevel, maxLevel, results);
	}
}


void Main()
{
	Window::Resize(1280, 720);
	const ColorF backgroundColor = ColorF{ 0.4, 0.6, 0.8 }.removeSRGBCurve();
	const MSRenderTexture renderTexture{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
	DebugCamera3D camera{ renderTexture.size(), 30_deg, Vec3{ 10, 16, -32 } };

	const Texture grassTexture{ U"example/texture/grass.jpg", TextureDesc::MippedSRGB };
	const Texture rockTexture{ U"example/texture/rock.jpg", TextureDesc::MippedSRGB };
	const Texture brushTexture{ U"example/particle.png" };

	TerrainRenderer terrainRenderer;
	if (not terrainRenderer.init())
	{
		return;
	}

	Terrain terrain;

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

		// 3D シーンへの描画
		{
			Graphics3D::SetCameraTransform(camera);
			const ScopedRenderTarget3D target{ renderTexture.clear(backgroundColor) };
			terrainRenderer.draw(terrain, grassTexture, rockTexture);

			{
				const Ray mouseRay = camera.screenToRay(Cursor::PosF());
				Array<Morton32> results;
				DFS(terrain, mouseRay, 0, 0, Terrain::MaxLevel, results);

				Array<std::pair<Morton32, Vec3>> tiles;

				for (const Morton32 morton : results)
				{
					const Point pos = Morton::Decode2D32(morton);
					const Vec3 center = terrain.calculateTileCenter(pos);
					tiles.emplace_back(morton, center);
				}

				Print << tiles.size() << U" tiles";

				if (tiles)
				{
					const auto it = std::min_element(tiles.begin(), tiles.end(), [cameraXZ = camera.getEyePosition().xz()](const auto& a, const auto& b)
					{
						return (a.second.xz().distanceFrom(cameraXZ) < b.second.xz().distanceFrom(cameraXZ));
					});

					Print << Morton::Decode2D32(it->first);
					Sphere{ it->second, 0.5 }.draw(Linear::Palette::Red);
				}
			}
		}

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

		if (SimpleGUI::Button(U"Random", Vec2{ 270, 10 }, 120))
		{
			// heightmap および heightmapTexture をランダムに生成する
			terrain.fillRandomHeightmap();

			// normalMap を更新する
			terrainRenderer.updateNormalMapTexture(terrain);
		}

		if (Rect{ Terrain::MapSize }.mouseOver())
		{
			Cursor::RequestStyle(CursorStyle::Hand);

			// 地形をブラシで編集する
			if ((MouseL | MouseR).pressed())
			{
				// ブラシの中心座標
				const Vec2 pos = Cursor::PosF();

				// ブラシの大きさ
				const double radiusScale = (MouseL.pressed() ? 2.0 : 1.0);

				// ブラシの強さ
				const double intensity = (Scene::DeltaTime() * 15.0);

				// heightmapTexture を編集する
				{
					const ScopedRenderTarget2D target{ terrain.getHeightmapTexture() };
					const ScopedRenderStates2D blend{ BlendState::Additive };
					brushTexture.scaled(radiusScale).drawAt(pos, ColorF{ intensity });
				}

				// heightmap を更新する(heightmapTexture から読み込む重い処理なので、頻度を減らすことが望ましい)
				{
					Graphics2D::Flush();
					terrain.readHeightmapFromHeightmapTexture();
				}

				// normalMap を更新する
				terrainRenderer.updateNormalMapTexture(terrain);
			}
		}

		if (SimpleGUI::Button(U"Clear", Vec2{ 270, 50 }, 120))
		{
			terrain.clear();
		}

		terrain.getHeightmapTexture().draw(ColorF{ 0.1 });
		terrain.getNormalMapTexture().draw(0, 260);
	}
}
Ryo SuzukiRyo Suzuki

IME の変換候補ウィンドウの描画(Windows 11)

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	TextEditState state;

	while (System::Update())
	{
		SimpleGUI::TextBox(state, Vec2{ 40, 40 }, 600);

		if (state.active)
		{
			SimpleGUI::IMECandidateWindow(Vec2{ 40, 80 });
		}
	}
}
Ryo SuzukiRyo Suzuki

Point / Vec2 ::rotate90()

# include <Siv3D.hpp>

void Main()
{
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	Vec2 v{ 120, 200 };

	while (System::Update())
	{
		ClearPrint();
		Print << v;

		if (MouseL.down())
		{
			v.rotate90(-1);
		}
		else if (MouseR.down())
		{
			v.rotate90(1);
		}

		Line{ Scene::Center(), (Scene::Center() + v) }.drawArrow(10, SizeF{ 20, 20 }, ColorF{ 0.25 });
	}
}
Ryo SuzukiRyo Suzuki

SceneManager で最初のフェードイン時間を指定

# include <Siv3D.hpp>

using App = SceneManager<String>;

// タイトルシーン
class Title : public App::Scene
{
public:

	Title(const InitData& init)
		: IScene{ init }
	{
		Print << U"Title::Title()";
	}

	~Title()
	{
		Print << U"Title::~Title()";
	}

	void update() override
	{
		if (MouseL.down())
		{
			// ゲームシーンに遷移
			changeScene(U"Game");
		}
	}

	void draw() const override
	{
		Scene::SetBackground(ColorF{ 0.3, 0.4, 0.5 });

		FontAsset(U"TitleFont")(U"My Game").drawAt(400, 100);

		Circle{ Cursor::Pos(), 50 }.draw(Palette::Orange);
	}
};

// ゲームシーン
class Game : public App::Scene
{
public:

	Game(const InitData& init)
		: IScene{ init }
		, m_texture{ U"🐈"_emoji }
	{
		Print << U"Game::Game()";
	}

	~Game()
	{
		Print << U"Game::~Game()";
	}

	void update() override
	{
		if (MouseL.down())
		{
			// タイトルシーンに遷移
			changeScene(U"Title");
		}
	}

	void draw() const override
	{
		Scene::SetBackground(ColorF(0.2, 0.8, 0.6));

		m_texture.drawAt(Cursor::Pos());
	}

private:

	Texture m_texture;
};

void Main()
{
	FontAsset::Register(U"TitleFont", 60, Typeface::Heavy);

	// シーンマネージャーを作成
	App manager;
	manager.add<Title>(U"Title");
	manager.add<Game>(U"Game");

	// "Title" シーンから開始。フェードインなし(0.0 秒)
	manager.init(U"Title", 0.0s);

	while (System::Update())
	{
		if (not manager.update())
		{
			break;
		}
	}
}