[OpenSiv3D]DrawTrianglesを使って🐟をいっぱい泳がそう

# はじめに
水族館いきたくないですか？？
僕は水族館が好きなので定期的に行きたい気持ちになります。
(一緒に行く人がいないので、ぼっちでは行きません)

そしてふと思いました。

せや、
$\huge{🐟🐟🐟お魚さんを100万体泳がそう🐟🐟🐟}$

# 安易な実装

```cpp:Main.cpp
# include <Siv3D.hpp>

void Main()
{
	// 🐟お魚さんを泳がすぞ～～🐟
	Texture tex(Emoji(U"🐟"));
	auto scaledTex = tex.scaled(0.3);
	const auto& texSize = scaledTex.size;

	// 🐟 100万体泳がすぞ～🐟
	constexpr size_t fishCount = 1000000;
	constexpr double sqrtFishCount = 1000.0;

	// 🐟 適当に座標計算🐟
	Array<Vec2> positions;
	positions.reserve(fishCount);
	for (size_t count = 0; count < fishCount; ++count) {
		positions << static_cast<Vec2>(texSize) * Vec2{
			Math::Fmod(count, sqrtFishCount),
			count / sqrtFishCount
		};
	}

	while (System::Update())
	{
		ClearPrint();
		Print << texSize;
		Print << Profiler::FPS();

		// 🐟 お魚さん描画！！🐟
		for (const auto& pos : positions) {
			scaledTex.drawAt(pos);
		}
	}
}

```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104703/82d28579-7979-d86a-e0d5-e07b08c78f26.png" width="80%">

お気づきでしょうか？
FPSが10まで落ちて、無事死亡しました。
(なお、リリースビルドでも同様です。)

----
※実際には、このやり方だとしても画面外のお魚さんは描画しないようにするなどの対応をしたほうがよいと思います🐟

```cpp
		auto screenRect = Scene::Rect();
		for (const auto& pos : positions) {
			if (!screenRect.intersects(scaledTex.regionAt(pos))) {
				// 画面外のものは無視
				continue;
			}
			scaledTex.drawAt(pos);
		}
```

ではどうすれば、お魚さんを100万体泳がすことができるのか

# DrawTriangles

ボトルネックはどこがありそうでしょうか…
そもそも100万ループも軽くはないと思いますが
100万回描画命令をしているのはとても重そうです。
(1個画像を画面に出すだけでも裏ではいろいろな処理をやっているのです。)

この悩みを解決してくれるのが、今回ご紹介するメソッド
🐟**`s3d::Graphics2D::DrawTriangles(uint32 count)`**🐟
です。

この関数は、
**引数で渡した数だけ三角形を描画する関数**です。
と、言いましたが実際にこのメソッドを呼んでも三角形△は描画されないでしょう。
三角形の座標や、色などの頂点情報を何一つ設定していないからです。

では、そういった頂点情報をどうやって設定するのかというと、**頂点シェーダー**を使います。


  
(余談ですが、旧Siv3Dにも類似の関数がありましたが、この時は三角形の数ではなく頂点数を渡すメソッドだったので、少しだけ使い方が変わっています。)

```cpp:Main.cpp
# include <Siv3D.hpp>

void Main()
{
	// 🐟お魚さんを泳がすぞ～～🐟
	Texture tex(Emoji(U"🐟"));

	// 🐟 100万体泳がすぞ～🐟
	constexpr size_t fishCount = 1000000;

	// シェーダー
	const FilePath shaderPath{ U"osakana_san.hlsl" };
	VertexShader vs = HLSL{ shaderPath };
	PixelShader ps = HLSL{ shaderPath };

	while (System::Update())
	{
		ClearPrint();
		Print << Profiler::FPS();

		// 🐟 お魚さん描画！！🐟
		{
			// シェーダー設定
			ScopedCustomShader2D scopedShder{ vs, ps };

			// PSで使うようにテクスチャをセット
			s3d::Graphics2D::SetPSTexture(0, tex);

			// 100万 * 2個の三角形描画
			// 四角形の描画には三角形が2ついるので2倍にしてる
			s3d::Graphics2D::DrawTriangles(fishCount * 2);
		}
	}
}
```
**四角形の画像を描画するためには三角形が2つ**必要なので、描画したい数に2倍をかける必要があります。

お魚さんのテクスチャを張り付けたいので、ピクセルシェーダーも使っています。

※注意：この記事ではHLSLでシェーダーを書きますが、プラットフォームに応じてはGLSLを使用する必要があります。

# 頂点シェーダーで計算

計算しました。(完)

```hlsl:osakana_san.hlsl
Texture2D		g_texture0 : register(t0);
SamplerState	g_sampler0 : register(s0);

namespace s3d
{
    //
    //	VS Input (nothing)
    //

    //
    //	VS Output
    //
    struct PSInput
    {
        float4 position	: SV_POSITION;
        float4 color	: COLOR0;
        float2 uv		: TEXCOORD0;
    };

    //
    //	Siv3D Functions
    //
    float4 Transform2D(float2 pos, float2x4 t)
    {
        return float4((t._13_14 + (pos.x * t._11_12) + (pos.y * t._21_22)), t._23_24);
    }
}

//
//	Constant Buffer
//
cbuffer VSConstants2D : register(b0)
{
    row_major float2x4 g_transform;
    float4 g_colorMul;
}

cbuffer PSConstants2D : register(b0)
{
    float4 g_colorAdd;
    float4 g_sdfParam;
    float4 g_sdfOutlineColor;
    float4 g_sdfShadowColor;
    float4 g_internal;
}

//-------------------------------------------
// 頂点シェーダー

float2 toUV(uint mod6)
{
    if (mod6 == 0) {
        return float2(0, 0);
    } else if (mod6 == 1) {
        return float2(1, 0);
    } else if (mod6 == 2) {
        return float2(0, 1);
    } else if (mod6 == 3) {
        return float2(0, 1);
    } else if (mod6 == 4) {
        return float2(1, 0);
    } else {
        return float2(1, 1);
    }
}

float2 toQuad(float2 uv, float2 size)
{
    float2 halfSize = size / 2.0;
    float2 ret = lerp(-halfSize, halfSize, uv);
    return ret;
}

s3d::PSInput VS(uint id: SV_VERTEXID)
{
    s3d::PSInput result;
    // 一匹のお魚さんに必要な頂点は 6 
    const uint quadId = id / 6;
    const uint mod6 = id % 6;

    // 座標を適当に計算
    float2 pos = float2(0, 0);
    const uint sqrtFishCount = 1000;
    const int xId = (int)(quadId % sqrtFishCount);
    const int yId = (int)(quadId / sqrtFishCount);

    float2 size = float2(40.8, 38.4);
    pos += float2(xId * size.x, yId * size.y);

    // uv座標を計算
    float2 uv = toUV(mod6);
    // 四角形にする
    pos += toQuad(uv, size);

    result.position = s3d::Transform2D(pos, g_transform);
    result.uv = uv;
    result.color = g_colorMul;

    return result;
}

//-------------------------------------------
// ピクセルシェーダー

float4 PS(s3d::PSInput input) : SV_TARGET
{
    const float4 texColor = g_texture0.Sample(g_sampler0, input.uv);
    return (texColor * input.color) + g_colorAdd;
}
```

…
もう少し細かく見ていきます。

```hlsl
namespace s3d
{
    //
    //	VS Input (nothing)
    //

    //
    //	VS Output
    //
    struct PSInput
    {
        float4 position	: SV_POSITION;
        float4 color	: COLOR0;
        float2 uv		: TEXCOORD0;
    };

    //
    //	Siv3D Functions
    //
    float4 Transform2D(float2 pos, float2x4 t)
    {
        return float4((t._13_14 + (pos.x * t._11_12) + (pos.y * t._21_22)), t._23_24);
    }
}

//
//	Constant Buffer
//
cbuffer VSConstants2D : register(b0)
{
    row_major float2x4 g_transform;
    float4 g_colorMul;
}

cbuffer PSConstants2D : register(b0)
{
    float4 g_colorAdd;
    float4 g_sdfParam;
    float4 g_sdfOutlineColor;
    float4 g_sdfShadowColor;
    float4 g_internal;
}
```
まず、上のほうのこのあたりのコードは、Siv3Dで2D描画する上で必要な入力レイアウトや定数バッファの型定義なのでSiv3Dでカスタムシェーダーを使う時のコピペのような部分です。
(というか基本はコピペしたほうがよいです)
(PSを使わない場合はPSConstants2D は不要です)

ただし、注目すべきところがあります。
それは`VS Input (nothing)`とコメントされているように、
頂点の入力型定義がないことです。

本来`Texture.draw`などで描画する際にカスタム頂点シェーダーを使う際には
各頂点の座標やuv、色情報を設定して描画するから
以下のような型定義が必要です。

```hlsl
	struct VSInput
	{
		float2 position : POSITION;
		float2 uv : TEXCOORD0;
		float4 color : COLOR0;
	};
```

しかし、`DrawTriangles`は一切頂点情報を設定せずにただ、頂点数だけ(三角形の数だけ)指定して描画する関数だったので、ここではVSInputが不要になります。

---

次にピクセルシェーダー側に関するコードです。
特に特殊なことは何もしていませんので、スルーします。

```hlsl
Texture2D       g_texture0 : register(t0);
SamplerState    g_sampler0 : register(s0);
------------------------

float4 PS(s3d::PSInput input) : SV_TARGET
{
    const float4 texColor = g_texture0.Sample(g_sampler0, input.uv);
    return (texColor * input.color) + g_colorAdd;
}
```
補足：C++側でお魚さんのテクスチャを設定しているので↑のところでアクセスできる感じです
(サンプラーはもともと設定されているもので構わないので特に明示的に設定していません。)

```cpp
            // PSで使うようにテクスチャをセット
            s3d::Graphics2D::SetPSTexture(0, tex);
```
---
さて、いよいよ頂点シェーダーの中を見ていきます。

```hlsl
s3d::PSInput VS(uint id: SV_VERTEXID)
```
ここのidに、描画する頂点数の分だけ数値が入ってきます。
つまりidは
`0 ~ DrawTrianglesに渡した引数の数*3 - 1`
まで数字が入ってきます。(三角形の頂点数は3なので*3されます。)

```hlsl
    // 一匹のお魚さんに必要な頂点は 6 
    const uint quadId = id / 6;
```
四角形を描画するのに頂点が6つ必要なので一匹のお魚さんを表現するのに6頂点使います。
なので、ここで6で割っておくことでお魚さん一匹のidに変換しています。

```hlsl
    // 座標を適当に計算
    float2 pos = float2(0, 0);
    const uint sqrtFishCount = 1000;
    const int xId = (int)(quadId % sqrtFishCount);
    const int yId = (int)(quadId / sqrtFishCount);

    float2 size = float2(40.8, 38.4);
    pos += float2(xId * size.x, yId * size.y);
```
この辺は最初に安易な実装した時と同じように適当に並べてるだけでそこまで重要なところではないです。

```hlsl
float2 toUV(uint mod6)
{
    if (mod6 == 0) {
        return float2(0, 0);
    } else if (mod6 == 1) {
        return float2(1, 0);
    } else if (mod6 == 2) {
        return float2(0, 1);
    } else if (mod6 == 3) {
        return float2(0, 1);
    } else if (mod6 == 4) {
        return float2(1, 0);
    } else {
        return float2(1, 1);
    }
}

-----
    const uint mod6 = id % 6;
    // uv座標を計算
    float2 uv = toUV(mod6);
```
お魚さんのテクスチャを張り付けるためには各頂点にUV座標を設定する必要があります。
そのあたりの計算をここでしています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104703/a41df31a-501d-71a9-6803-cdacc72e003d.png" width="40%">

```hlsl
float2 toQuad(float2 uv, float2 size)
{
    float2 halfSize = size / 2.0;
    float2 ret = lerp(-halfSize, halfSize, uv);
    return ret;
}
-------
    // 四角形にする
    pos += toQuad(uv, size);
```
上のほうで適当に計算した座標が中心座標となる四角形になるように計算しています。
(お魚さん一匹を操作するために6で割って座標計算をしていたので、ココを忘れると6頂点全部同じ座標になって点になってしまいます。)


```hlsl
    s3d::PSInput result;
    result.position = s3d::Transform2D(pos, g_transform);
    result.uv = uv;
    result.color = g_colorMul;
```
最後に、今までいろいろ計算したものをピクセルシェーダーが扱う入力の形に変換して終わりです。

# お魚さんが100万体泳いだ
🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104703/847b2e59-5fd0-e813-ac17-b4a26448a9ff.png" width="80%">
🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟

全く画面に収まるわけがないのですが、100万体泳いでてもFPSも維持できていそうです。
🐟**やったね!**🐟

# まて、泳いでないやないか

そうです、お魚さん、まだ泳いでません…！！
現状、停止しちゃっています。
どうやって泳がすのだ…？？🐟

### 定数バッファ
C++側からシェーダーにパラメータを送りたいときに使えるのが定数バッファです。

```cpp:Main.cpp
# include <Siv3D.hpp>

/// 定数バッファの型定義
struct OsakanaSanParam
{
	float time{};
};

void Main()
{
	// 🐟お魚さんを泳がすぞ～～🐟
	Texture tex(Emoji(U"🐟"));

	// 🐟 100万体泳がすぞ～🐟
	constexpr size_t fishCount = 1000000;

	// シェーダー
	const FilePath shaderPath{ U"osakana_san.hlsl" };
	VertexShader vs = HLSL{ shaderPath };
	PixelShader ps = HLSL{ shaderPath };

	ConstantBuffer<OsakanaSanParam> cb;

	while (System::Update())
	{
		ClearPrint();
		Print << Profiler::FPS();
		Print << cb->time;

		// 🐟 お魚さん描画！！🐟
		{
			// シェーダー設定
			ScopedCustomShader2D scopedShder{ vs, ps };

			// 定数バッファで時間を送る
			cb->time = static_cast<float>(Scene::Time());
			s3d::Graphics2D::SetVSConstantBuffer(1, cb);

			// PSで使うようにテクスチャをセット
			s3d::Graphics2D::SetPSTexture(0, tex);

			// 100万 * 2個の三角形描画
			// 四角形の描画には三角形が2ついるので2倍にしてる
			s3d::Graphics2D::DrawTriangles(fishCount * 2);
		}
	}
}
```

```hlsl:osakana_san.hlsl
cbuffer OsakanasanParam : register(b1)
{
    float g_time;
}

s3d::PSInput VS(uint id: SV_VERTEXID)
{
    s3d::PSInput result;
    const uint quadId = id / 6;
    const uint mod6 = id % 6;

    float2 pos = float2(0, 0);
    const uint sqrtFishCount = 1000;
    const int xId = (int)(quadId % sqrtFishCount);
    const int yId = (int)(quadId / sqrtFishCount);

    float2 size = float2(40.8, 38.4);
    pos += float2(xId * size.x, yId * size.y);

    // 泳げお魚さん！
    pos += float2(-100, -100) * g_time;

    float2 uv = toUV(mod6);
    pos += toQuad(uv, size);

    result.position = s3d::Transform2D(pos, g_transform);
    result.uv = uv;
    result.color = g_colorMul;

    return result;
}
```

このあたりのコードを追加しました。

```cpp
/// 定数バッファの型定義
struct OsakanaSanParam
{
	float time{};
};

------------

// 定数バッファで時間を送る
cb->time = static_cast<float>(Scene::Time());
s3d::Graphics2D::SetVSConstantBuffer(1, cb);
```

```hlsl
cbuffer OsakanasanParam : register(b1)
{
    float g_time;
}
--------

// 泳げお魚さん！
pos += float2(-100, -100) * g_time;
```
C++側の引数`1`とシェーダー側の`b1`の数字を合わせる必要があります。

せっかくなので、おまけにお魚さんの向きも少しかえましょう

```hlsl
float2 rot(float2 xy, float theta)
{
    float cosTheta = cos(theta);
    float sinTheta = sin(theta);
    return mul(float2x2(cosTheta, -sinTheta, sinTheta, cosTheta), xy);
}

float2 toQuad(float2 uv, float2 size)
{
    float2 halfSize = size / 2.0;
    float2 ret = lerp(-halfSize, halfSize, uv);

    // 回転
    {
        float theta = atan2(100.0, 100.0);
        ret = rot(ret, theta);
    }
    return ret;
}
```

🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104703/48e05b52-1090-9838-d152-db7855471100.gif" width="80%">
🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟🐟

🐟**お魚さん泳いだよ！やったね!**🐟

# ⚠️100万体描画できるとは言ってない
100万体のお魚さんを泳がせることができました、が、実は、100万体のお魚さんが描画できるわけではありません。
これはクリッピング処理により、画面に映らないものは捨てられているおかげで処理が高速化されているためです。

試しに100万匹すべておなじ座標にすると自分の環境ではめちゃくちゃ処理落ちしました。
(それでも安易に100万回描画命令するよりは処理負荷が少なかった)

とはいえ、100万は無理でも10万匹くらいなら普通に描画できちゃいました!
(10万回描画命令した場合も処理落ちはしていた)

# おまけ：🐟私のお魚さんを紹介🐟

今回紹介したソースコードは、めちゃくちゃ綺麗に整列したお魚さんが淡々と移動していくだけでしたが
もっといろいろ味付けをしていくと、楽しい感じになっていきます。
移動処理自体もル―プするようにしたりすると無限にお魚さんが泳いでくれます。
(さっきの実装だと時間経過に連れて座標がどんどん更新されるので、いつかお魚さんがいなくなっちゃうからね)

https://twitter.com/i/status/1459239656772730880

https://twitter.com/i/status/1459880560814624768

https://twitter.com/i/status/1459896056200974336

※なお、さすがに100万体もおよがしていません。🐟

# 参考
カスタムシェーダー周りはOpenSiv3Dのチュートリアルにあるものを読むのがいいと思います。

- [チュートリアル 35 | 2D カスタムシェーダ](https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-shader2d)
  - カスタムシェーダー周りの基礎的な使い方がまとまっています。 
- [サンプル集 | ビジュアル表現 > GPU での頂点生成](https://zenn.dev/reputeless/books/siv3d-documentation/viewer/sample-visual#gpu-%E3%81%A7%E3%81%AE%E9%A0%82%E7%82%B9%E7%94%9F%E6%88%90)
  - 上記サンプルは、今回の用途とは全く違ったサンプルですが、同じようにDrawTrianglesを使って頂点計算している例です。

# まとめ

- `s3d::Graphics2D::DrawTriangles(uint32 count)`を使うと、処理負荷をおさえつつ沢山の頂点を扱うことができる
  - ただし頂点シェーダーで自前に全て計算する必要がある
  - 複雑な形状を描画したい際の頂点計算に便利です
  - 今回のように同じテクスチャなどを大量に画面に出したいときなどには低コストでたくさん描画できて便利です
- お魚さんがいっぱい泳いでて可愛い🐟
- 水族館に行きたい