titleコマンドで、最高にアツいGUIを作ろう

この記事は、[Minecraft Command Advent Calendar 2024](https://qiita.com/advent-calendar/2024/mccmd) 21日目の記事です。:wolf:

# はじめに

突然ですがみなさん、コマンドで<strong>「GUI」</strong>作ったことありますか？？
今回のテーマは <font color="#FF3333"><strong>画面表示</strong></font> 関連のお話。特に <font color="#FF3333"><strong>titleコマンド</strong></font> を使った方法について解説していきます。
こういった演出は何かを作るうえで必須ではないかもしれませんが、ここをちょっとこだわるだけで作品全体のクオリティがぐっと上がると思っています。プレイヤーが思わず唸ってしまうような <font color="#FF3333"><strong>アツい演出</strong></font> 、一緒に作ってみませんか？

ちなみに、おそらくこの記事のテーマ自体は中～上級者向けだと思いますが、かなり分かりやすいように書いたつもりなので「そんなの作ったことないよ！」という方も大歓迎です。

それでは、やっていきましょう！！

:::note
この記事は Minecraft Java Edition 1.21.3 を時点の情報をもとに書いています。
:::

# 何ができるのさ！

![reversi_gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/77cd3893-03d3-e810-c749-20bddb82369b.gif)

まずはあなたの心に住まう「これ作りたい！！！」を刺激するために、実際に作ったものをお見せしましょう。このリザルト画面は全てtitleコマンド（とシェーダーなどなど）で作っています。

さて、今回はこういうのを作っていこう！という記事なんですがその前に...

# え、ディスプレイエンティティでよくね？
最近、item_displayやtext_displayのような革命的なシステムが実装されましたね。<font color="DarkGray">~~（2年前、、？）~~</font>\
比較的直感的に操作できるし結構なんでもできるし、titleコマンドよりそっちの方がいいんじゃねーの！と思ったそこのあなた、ちょっと待ってください。

## 1. ディスプレイエンティティは半透明がニガテ
どういうことかは実際に見た方が早いと思います。\
⇩ ということで画像 ⇩

![displayentity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/60b5fc62-3599-e9f3-d5f7-765649df58dc.png)

手前がtext_display、奥のTNTがblock_displayです。こんな感じで <strong>重なってる部分が描画されなくなっちゃう</strong> 。
これはバグ報告もされてるみたいなので一応修正待ちではあるんですけど、これは結構きつい。

## 2. 動き回るプレイヤーもニガテ

![textdisplay.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/c819a606-7c2e-2f63-da85-3a8fd6c3a657.gif)

「ワールド内の特定の場所に表示しておく」という用途ならディスプレイエンティティが最強なんですが、例えばHPバーのように「常に画面上に表示しておくUI」なんかを作るのは少し苦手です。\
上の動画のように、常時tpコマンドで追従させていても明らかに動いているのがわかってしまいます。F5の視点変更？にもなかなか弱いです。\
そしてこのラグなどに関しては、今後改善されることは無いんじゃないかなーと思います。

## 3. titleは意外となんでもできる
titleコマンドってなかなか癖が強いですよね。仕様も割とややこしいし、何種類ものUIを同時に表示したいと思うとかなーりめんどくさい。\
でも！そんな難しそうだしめんどくさいしややこしいtitleコマンドくんですが、色々な「ワザ」を使えば意外とどんなUIでも作れちゃうんです！
簡単だけど課題のあるディスプレイエンティティではなく、難しいけどより柔軟なtitleコマンドを選んだ、というわけです。

# titleを最強にしていこう
というわけで、ここからはその「ワザ」たちを紹介していこうと思います。ただ全部解説してたらとんでもないボリュームになってしまうので、この記事では最低限のことに絞って解説しようと思います。

:::note
これからtitleコマンドをたくさん使うので、あらかじめ`/title @s times 0 100 0`などを実行しておくと良いと思います！
:::

## 1. スペース
まずは一度titleコマンドにおける<strong>「スペース」</strong>の仕様について解説していきます。なんのこっちゃと思うかもしれませんがしばしお付き合いください。

では、titleコマンドを使って「a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b」という文字を表示する例を考えてみます。
```
/title @s title "a         b"
```
実際にこのコマンドを打つとわかると思いますが、表示される文字は以下の画像ように
「a b」になるはずです。あれー、スペースが足りない。

![space1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/b7414fba-f90c-8331-de2f-a70fb0571c6e.png)

これは、「titleコマンドなどのテキストに含まれる <strong>複数個繋がった空白文字</strong> は、<font color="#FF3333"><strong>一つの半角空白として扱われる</strong></font> 」ということが原因です。

さて、これを踏まえた上で本題に戻りましょう。
まず<strong>「titleコマンドでUIはどうやって作っているのか」</strong>ですが、そもそもtitleコマンドで表示できるものは<strong>「たった一つの文字列」</strong>です。「あああ」とか「abcde」とか。

では、例えば画面左と画面右にそれぞれ異なるものを表示したいとしたらどうすれば良いでしょうか？それぞれを「左」「右」という文字で簡略化して考えてみます。

まず思いつくのは「左&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;右」のようにたくさんスペースを開ける方法ですが、このやり方ではさっき説明した通りスペースがまとまって「左 右」のようになってしまいます。
そこで登場するのが、いわゆる<strong>「カスタムスペース」</strong>といった方法です。

まずはリソースパックを作って、その`minecraft\font\`内に、こんな感じのjsonファイルを追加します。ここでは`test.json`という名前にしておきます。
```json:minecraft\font\test.json
{
    "providers": [
        {
            "type": "space",
            "advances": {
                "あ": 30,
                "い": 60
            }
        },
        {
            "type": "reference",
            "id": "default"
        }
    ]
}
```

リソースパックがきちんと読み込めたら、このコマンドを実行してみましょう。
```mcfunction
/title @s title {"text":"aあbいc","font":"test"}
```
そうすると、こんな感じで表示されるはずです。
![space2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/11718655-a555-6f07-1387-d2bcb8523211.png)

ひとまず何をしたのか解説していきます。
まずリソースパックに作ったjsonファイルですが、これの役割は端的に言うと<font color="#FF3333"><strong>「翻訳」</strong></font>です。今作ったjsonには、「testというフォントを適応した時、<strong>『あ』</strong>という文字は<strong>『幅30の空白』</strong>に、<strong>『い』</strong>という文字は<strong>『幅60の空白』</strong>に置き換えてね」ということが書かれています。
`"type":"reference"`と書いてある部分には、「その他の文字は全て <strong>デフォルトのフォントを参照</strong> してね！」という意味になります。

そしてさっき打ったコマンドは、「testというフォントのルールに則って <strong>aあbいc</strong> という文字列を表示してね。」ということです。

これが<strong>「カスタムスペース」</strong>なわけですが、ここで一つ疑問が湧いてきたんじゃないでしょうか！

<strong>「これって負の数を入れたらどうなるんだろう？」</strong>

では、やってみましょう。

先程のjsonファイルを少し変えて、
```json:minecraft\font\test.json
{
    "providers": [
        {
            "type": "space",
            "advances": {
                "あ": 30,
                "い": -50
            }
        },
        {
            "type": "reference",
            "id": "default"
        }
    ]
}
```
こんな感じにしてみました。リソパの再読み込みをして、この状態でさっきと同じコマンドを実行してみると...？？

![space3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/519b727b-f908-e58f-eaa9-6c0d06212672.png)

！！！！！
負の数を入れたら、cがかなり左に移動しましたね。つまり、正のスペースは通常通り **右にずらす効果** 、負のスペースは逆に **左にずらす効果** があるということです。

これを使えば、例えば下の画像のように **背景を表示してからその上に文字を表示する** なんてこともできます！（背景、負のスペース、文字の順で表示してるよ）

![space4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/4e1afe61-da75-a1bc-a502-a82baabebdbc.png)

## 2. シェーダー
さて、いよいよお待ちかね<strong>「シェーダー」</strong>のお話です。
シェーダーは本当に優秀で、マジで何でもできると言っても割と過言ではありません。できることが多すぎて全ては解説しきれないので、特に使う技術に絞って一つずつ順番に解説していきます。

### 2-1. 上下を調節してみる
カスタムスペースを覚えたことによって左右方向は自由に調節できるようになりましたけど、なら今度は上下も調節したいですよね。というわけで！今度は上下の調節のやり方について書いていきます。

自分で用意した画像をtitleで表示するためには、基本的にリソースパックを使ってフォントを自作すると思います。`type:bitmap`では、「 **height**（高さ）」と「 **ascent**（上下シフト）」の2つのパラメータがありますが、こいつらには面倒くさい縛りがあって、<font color="#FF3333"><strong>ascent < height</strong></font> である必要があります。
まぁつまり、標準の機能では<strong>「下にはいくらでも下げれるけど上にはあまり上げられない」</strong>ということです。これで何が困るかと言うと、例えば画面の上端に表示するようなUIが作れなかったりします。わぁそれは困る。

そしてもう一つ、heightやascentで管理する方法だと表示位置や文字のサイズごとに全てフォント側で登録する必要があり、例えばスコアボードの値に応じて動的に高さを変える...というような柔軟な実装が難しくなってしまいます。

ここで登場するのが<strong>「シェーダー」</strong>です！！ <strong>上下に自由に動かせて、コマンドから動的に高さも変更できるような方法</strong> を紹介していきます。ただこれも全てを解説していたらとんでもない文量になってしまうので重要なところだけ。とりあえずかなり長いですがいきなり例を載せます。まずは核となるシェーダー部分のファイルから。

```vsh:minecraft\shaders\core\rendertype_text.vsh
#version 150

#moj_import <fog.glsl>

in vec3 Position;
in vec4 Color;
in vec2 UV0;
in ivec2 UV2;

uniform sampler2D Sampler2;

uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
uniform int FogShape;

uniform float GameTime;

out float vertexDistance;
out vec4 vertexColor;
out vec2 texCoord0;

out float depthLevel;

out vec3 ColorValue;

void main() {
    depthLevel = Position.z;
    vec3 pos = Position;

    // 透明度だけ引き継いだ白色
    vec4 white = vec4(1.0, 1.0, 1.0, 1.0);
    white.w = Color.w;

    // RGBを整数値で取得
    ColorValue = vec3(round(Color.r * 255.0), round(Color.g * 255.0), round(Color.b * 255.0));

    if (ColorValue.r == 1.0 && depthLevel == 2400.12) {
        // title
        pos.y -= 10.0;
        vertexColor = white * texelFetch(Sampler2, UV2 / 16, 0);
    } else {
        // その他
        vertexColor = Color * texelFetch(Sampler2, UV2 / 16, 0);
    }

    gl_Position = ProjMat * ModelViewMat * vec4(pos, 1.0);

    vertexDistance = fog_distance(pos, FogShape);
    texCoord0 = UV0;
}
```

はい、これです。

えーっと、、、何がなんだかわからないと思いますが、とりあえず実際にやってみましょうか。リソースパックの`minecraft\shaders\core\`に`rendertype_text.vsh`というファイルを作り、中にこのめちゃくちゃ長いやつをコピペしてください。

リソースパックの再読み込みをしたら、このコマンドを実行してみましょう！

```mcfunction
/title @s title [{"text":"a"},{"text":"a","color":"#01FFFF"}]
```

![shader1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/c6da0fc2-50ef-47e4-5948-4d6c5dd6d17b.png)

こんな感じになったでしょうか。<font color="DarkGray">~~なっててくれ～っ！~~</font>

とりあえずコマンドの解説から。左右のaで何が異なるのかですが、右の方のみ色指定がされています。他にもいくつかやり方はあると思いますが、今回使うのは<strong>​「文字の色」</strong>を使ってシェーダー側に値を送り、それに応じた処理をシェーダー側で行うという方法です。

そしてシェーダー側の処理ですが、最初の方と最後の方は一旦ほぼ全部おまじないだと思っていただいて、、真ん中の部分の解説をしていきます。

```
// 透明度だけ引き継いだ白色
vec4 white = vec4(1.0, 1.0, 1.0, 1.0);
white.w = Color.w;
```
⇧まずこれは、文字の色で指定したtitleのテキストを、透明度を維持したまま白色に戻すための処理（の準備）です。これもおまじないです。

```
// RGBを整数値で取得
ColorValue = vec3(round(Color.r * 255.0), round(Color.g * 255.0), round(Color.b * 255.0));
```
⇧これは小数値で入っている色の値を扱いやすいように整数値にする処理です。まぁこれもおまじないです。

```
if (ColorValue.r == 1.0 && depthLevel == 2400.12) {
    // title
    pos.y -= 10.0;
    vertexColor = white * texelFetch(Sampler2, UV2 / 16, 0);
} else {
    // その他
    vertexColor = Color * texelFetch(Sampler2, UV2 / 16, 0);
}
```

⇧まず`depthLevel == 2400.12`というのは、titleコマンドのtitle（`/title @s title`で表示されるテキスト）の影ではない部分を絞り込むための条件です。おまじないです。\
そして、`ColorValue.r == 1.0`というのは、文字の色のR値（#01FFFFの01の部分）が1.0であるという条件です。\
その中にある`pos.y -= 10.0;`は、pos.y（文字のY座標）の値を-10する（10だけ画面上方向に上げる）という処理、`vertexColor = white * texelFetch(Sampler2, UV2 / 16, 0);`は文字の色を白に戻す処理です。
つまり今回の例では、<strong>​「文字の色のR値が1のときに、その文字を10だけ画面上方向に上げる」​</strong>というわけです。

ここらへんに関しては数値とかを実際に自分でいじって色々試してみるのが一番理解しやすいと思います。`pos.y += 20.0;`とかにすれば下方向に20動くし、`pos.x += 100.0;`とかにすれば右方向に動きます。`pos.y -= ColorValue.g;`とかにすれば、文字のG値に応じて動的に動かすこともできます。カラーコードで表現できる値しか入力できないですが、割と直感的に操作できるしそんなに複雑なものを作らなければ範囲に困ることも無いので、結構良い方法だと思います。

### 2-2. 影を操作してみる
さて、さっき色々と操作していて気付いたかもしれないですが、文字の「影」は動いていなかったと思います。影だけ消したい？影も一緒に動かしたい？やっちゃいましょう！\
あ、今回はめっちゃ軽めの内容なのでご安心を、、。

さっき「titleのtitle」を絞り込む時に、`depthLevel == 2400.12`という条件を書きました。これはこのテキストのZ座標（画面の奥行き、レイヤー）が2400.12であることを条件にしているということです。同じような要領で、Z座標を使って文字の種類を絞り込むことができます。以下に以前Ver1.20.5で検証したZ座標の一覧表を載せておきます！

| タイプ | Z座標 |
|:------|:------|
| title | 2400.12 |
| subtitle | 2400.06 |
| title、subtitleの影 | 2400.00 |
| actionbar | 2200.03 |
| actionbarの影 | 2200.00 |

titleの影を操作したいのであれば、`depthLevel == 2400.00`とすればできそうですね。
移動はもうさっきやったし、せっかくなので今度は「削除」をやってみましょう。

```fsh:minecraft\shaders\core\rendertype_text.fsh
#version 150

#moj_import <fog.glsl>

uniform sampler2D Sampler0;

uniform vec4 ColorModulator;
uniform float FogStart;
uniform float FogEnd;
uniform vec4 FogColor;

in float vertexDistance;
in float depthLevel;
in vec4 vertexColor;
in vec2 texCoord0;

in vec3 ColorValue;

out vec4 fragColor;

void main() {
    vec4 color = texture(Sampler0, texCoord0) * vertexColor * ColorModulator;

	if (color.a < 0.1) {
		discard;
	}

	// titleの影を消す
	if (ColorValue.b == 2 && depthLevel == 2400.00) {
		discard;
	}

    fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
}
```

まずは`minecraft\shaders\core\`にこんな感じで`rendertype_text.fsh`というファイルを作ってください。

:::note warn
さっきは.vshファイルだったのに対して、今度は.fshファイルです！今`minecraft\shaders\core\`の中にはファイルが2種類ある状態になっています。
:::

今回もそこそこ長いですね。ただ今回大事なのはこの部分だけです、先に解説しちゃいましょう。
```fsh
// titleの影を消す
if (ColorValue.b == 2 && depthLevel == 2400.00) {
	discard;
}
```

まず`discard;`は「非表示にする」という処理です。ということは、B値が2でZ座標が2400.00のテキストを非表示に、、となりそうですが、実はちょっと違くて。影の色は、<strong>RGBそれぞれの値が元のテキストの色の4分の1になる</strong> ので、今回検知できるのはB値が「8,9,A,B（16進数）」のテキストになります。
例えば「#000408」という色の文字の影を処理したいとき、影の色はRGBの各値が4分の1になった「#000102」という色になっているので、使う条件は`if (ColorValue.b == 2)`になるということです。
つまり、今書いた処理は<strong>「titleかsubtitleの影で、B値が8~Bのものを非表示にする」</strong>ということになるのです！

:::note
ちょっとややこしい話になってしまいましたが、「影の部分のRGBは元のテキストの4分の1になるので、色の指定に気をつける必要がある」ことと、「`discard;`で非表示にできる」ということだけ分かっていれば大丈夫です！
:::

ではそれを踏まえて下のコマンドを実行したら、どうなるでしょうか？
```
/title @s title [{"text":"a"},{"text":"a","color":"#01FFFF"},{"text":"a","color":"#01FF08"}]
```
 
![shader2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/1657d6b8-9935-181f-a8b2-7a9a6dffe8ce.png)

おおお！！きちんと一番右のaだけ影が消えましたね！

### 2-3. 基準点を中心からずらしてみる
上下左右に動かせて、影と本体部分も別々に操作できるようになった。これで全てのテクニックを紹介した...と思いました？いいえ、まだあります。

titleコマンドの挙動をよーく見てみると、<strong>「テキストの表示位置が画面中央である」</strong>ことがわかります。これ、変えれちゃうんです。
だからなんだよ！って思った方、例えば画面の隅に表示したいUIがあると考えてください。

![FKRC1EbakAYA1tx.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/0990ddc0-80c9-710c-fba1-8973537cd981.jpeg)

例えばこんな感じ。画面右上には敵のHPとかを表示するUI、左上には方角とか時間とか天気とかを表示しています（ついでに座標も）。
でもこれ、マイクラのウィンドウのサイズが変わったりすることで <strong>表示位置が画面の隅じゃなくなったりする</strong> んです。

せっかくなのでちょっと試してみましょう！
とりあえず`rendertype_text.vsh`のpos.yの値を変更していた部分を`pos.y -= 55.0;`に変更して、`minecraft\font\test2.json`を作ってみてください。中身は以下の通り。

```json:minecraft\font\test2.json
{
    "providers": [
        {
            "type": "space",
            "advances": {
                "あ": -100
            }
        },
        {
            "type": "bitmap",
            "file": "block/stone.png",
            "ascent": 16,
            "height": 16,
            "chars": ["石"]
        },
        {
            "type": "reference",
            "id": "default"
        }
    ]
}
```

vshの方、確認できるように全文も一応置いておきます。

<details><summary>rendertype_text.vsh（クリックで開閉）</summary>

```
#version 150

#moj_import <fog.glsl>

in vec3 Position;
in vec4 Color;
in vec2 UV0;
in ivec2 UV2;

uniform sampler2D Sampler2;

uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
uniform int FogShape;

uniform float GameTime;

out float vertexDistance;
out vec4 vertexColor;
out vec2 texCoord0;

out float depthLevel;

out vec3 ColorValue;

void main() {
    depthLevel = Position.z;
    vec3 pos = Position;

    // 透明度だけ引き継いだ白色
    vec4 white = vec4(1.0, 1.0, 1.0, 1.0);
    white.w = Color.w;

    // RGBを整数値で取得
    ColorValue = vec3(round(Color.r * 255.0), round(Color.g * 255.0), round(Color.b * 255.0));

    if (ColorValue.r == 1.0 && depthLevel == 2400.12) {
        // title
        pos.y -= 55.0;  //変更したのはここだけ!!
        vertexColor = white * texelFetch(Sampler2, UV2 / 16, 0);
    } else {
        // その他
        vertexColor = Color * texelFetch(Sampler2, UV2 / 16, 0);
    }

    gl_Position = ProjMat * ModelViewMat * vec4(pos, 1.0);

    vertexDistance = fog_distance(pos, FogShape);
    texCoord0 = UV0;
}
```

</details>

そしたらこの状態でこんな感じのコマンドを打ってみてください。リソパの再読み込みも忘れずに！

```
/title @s title {"text":"あ石","font":"test2","color":"#01FF08"}
```

石の画像は画面のどこに表示されましたか？

![layout1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/5a26d6e7-0117-9f4f-4d38-e014e9a0d1f2.png)

⇧こんな感じで綺麗に画面左隅に表示された？

![layout2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/b27d3a88-700c-fad8-88d2-fb0c254d3129.png)

⇧それともこんな感じ？

![layout3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/52eb7200-83be-2c3e-9388-70138147bcbf.png)

⇧もしかしたらこんなんだったり？

さっきのコマンドを実行しながらウィンドウのサイズを変えたり、ビデオ設定の「GUIの大きさ」などを変えたりするとわかると思いますが、つまり<strong>「ユーザー側の設定によって表示位置が変わってしまう」</strong>んです。
画面の角っこに表示したいUIなのに、全然違うところに表示されてしまったり、画面外にはみ出したりしてしまったら嫌ですよね。
そこでテキストの基準点を真ん中から変更しよう！！となるわけです。

てことで方法。まず`rendertype_text.vsh`を一部書き換えましょう。

<details><summary>rendertype_text.vsh（クリックで開閉）</summary>

```vsh:minecraft\shaders\core\rendertype_text.vsh
#version 150

#moj_import <fog.glsl>

in vec3 Position;
in vec4 Color;
in vec2 UV0;
in ivec2 UV2;

uniform sampler2D Sampler2;

uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
uniform int FogShape;

uniform float GameTime;

out float vertexDistance;
out vec4 vertexColor;
out vec2 texCoord0;

out float depthLevel;

out vec3 ColorValue;

void main() {
    depthLevel = Position.z;
    vec3 pos = Position;

    // 透明度だけ引き継いだ白色
    vec4 white = vec4(1.0, 1.0, 1.0, 1.0);
    white.w = Color.w;

    // RGBを整数値で取得
    ColorValue = vec3(round(Color.r * 255.0), round(Color.g * 255.0), round(Color.b * 255.0));

    if (ColorValue.r == 1.0 && depthLevel == 2400.12) {
        // title
        vertexColor = white * texelFetch(Sampler2, UV2 / 16, 0);

        gl_Position = ProjMat * ModelViewMat * vec4(pos.x, pos.y + 40.0, pos.z, 1.0);
        if (ColorValue.b == 8.0) {
            gl_Position.x -= 1.0;
            gl_Position.y += 1.0;
        }
    } else {
        // その他
        vertexColor = Color * texelFetch(Sampler2, UV2 / 16, 0);
        gl_Position = ProjMat * ModelViewMat * vec4(pos, 1.0);
    }

    vertexDistance = fog_distance(pos, FogShape);
    texCoord0 = UV0;
}
```

</details>

ここらへんは割と難しいと思うので、自分でいじるために必要そうな最低限の説明だけ。
さっきまでいじっていたposなどの情報を使ってgl_Positionという変数に値を入れています。gl_Positionっていうのは超ざっくり言うと<strong>「ウィンドウのどのへんに配置するか」</strong>を表現するための変数です。
-1~1の範囲で表していて、gl_Position.xが1なら右端、gl_Position.yが1なら上端になります。
pos.yに40.0を足しているのは、マイクラのタイトル（/title @s title ""）は画面の中心から上方向に40ピクセルだけずれているので、その補正です。
つまり、pos.yに40.0を足して画面中心に戻したあとに、gl_Position.xを-1とgl_Position.yを+1（<strong>画面左上に基準点をずらしている</strong>）ということになります。

それじゃあこの状態でこのコマンドを実行してみましょうか！！シェーダーで画面左にずらしたので、さっき「あ」で入れていた負のスペースは消してます！

```
/title @s title {"text":"石","font":"test2","color":"#01FF08"}
```

![layout4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/a36ce4f6-cd93-3dd9-a59e-832227c6262d.png)
![layout5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/6c35c7a8-b329-70db-986a-2f662266d4b5.png)
![layout6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/d780351c-4951-3d5a-eeec-190efa9683b2.png)

<strong>おおおー！！！どんな状態でも常に左上基準になってるー！！！やったーー！！！！！</strong>

ちなみにvshをよく見ると分かると思いますが、今回は文字色のB値が8であることを条件に左上基準にずらしてみました。例えばこれに追加で「B値が9なら右上基準にする」といった処理を書くこともできます！ <strong>テキストごとに基準点は変えられる</strong> のでご安心を！

### 2-4. その他小技
シェーダーには、この記事では紹介しきれないほどまだまだ様々な機能が備わっています。詳しい解説は省きますが、こんなのも作れるんだよ！というものをいくつか紹介してみます。

シェーダーに使われている.vshや.fshでは、sinやcosのような三角関数やGameTimeという変数など、色々なものを使うことができます。
GameTimeは、「ワールドができてからのtick数が記録されている「gametime」を24000で割った余りをさらに24000で割ったもの」です。つまり、<strong>1200秒周期で0.0から1.0まで増加する変数</strong> ということ。

![other1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/86e0b3f2-9156-abbc-6a3a-13b2fc64a7ce.gif)

ということで、sinとGameTimeの２つを使ってこんなものを作ってみました。
今回はY座標に代入しているのでそのまま石がsin波を描いていますが、例えば文字の透明度に代入すれば、冒頭の勝利演出にあった「＜ スニークで結果を閉じる ＞」のように <strong>滑らかに文字を点滅させる</strong> ことができます。

また、シェーダーを使って<strong>「ノイズ」</strong>を作ることもできます。

![other2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/552df164-c2dc-6e5a-098e-1ec713968f4c.png)
⇧ノイズあり。表面がザラザラしてます。

![other3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/24cf1463-3147-9dec-1173-c73c132b77a4.png)
⇧ノイズなし。つやつや。


具体的なコードが見たい方がいるかもしれないので、念の為載せておきます。

<details><summary>リソースパック（クリックで開閉）</summary>

```vsh:minecraft\shaders\core\rendertype_text.vsh
#version 150

#moj_import <fog.glsl>

in vec3 Position;
in vec4 Color;
in vec2 UV0;
in ivec2 UV2;

uniform sampler2D Sampler2;

uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
uniform int FogShape;

uniform float GameTime;

out float vertexDistance;
out vec4 vertexColor;
out vec2 texCoord0;

out float depthLevel;

out vec3 ColorValue;

void main() {
    depthLevel = Position.z;
    vec3 pos = Position;

    // 透明度だけ引き継いだ白色
    vec4 white = vec4(1.0, 1.0, 1.0, 1.0);
    white.w = Color.w;

    // RGBを整数値で取得
    ColorValue = vec3(round(Color.r * 255.0), round(Color.g * 255.0), round(Color.b * 255.0));

    if (ColorValue.r == 1.0 && depthLevel == 2400.12) {
        // title
        vertexColor = white * texelFetch(Sampler2, UV2 / 16, 0);

        gl_Position = ProjMat * ModelViewMat * vec4(pos.x, pos.y + 40.0, pos.z, 1.0);
        if (ColorValue.b == 8.0) {
            gl_Position.x -= 1.0;
            gl_Position.y += 1.0;
        } else if (ColorValue.b == 9.0) {
            // GameTimeを3秒周期で0~239の変数に変換
            int i = int(GameTime * 96000) % 240;
            // offset.xは-1.0~0.99ぐらい、offset.yは0~4πのsinを3秒周期で
            vec2 offset = vec2((i / 120.0 - 1.0), sin(i * 3.14159265 / 60));
            gl_Position.x += offset.x;
            gl_Position.y += offset.y * 0.8;
        }
    } else {
        // その他
        vertexColor = Color * texelFetch(Sampler2, UV2 / 16, 0);
        gl_Position = ProjMat * ModelViewMat * vec4(pos, 1.0);
    }

    vertexDistance = fog_distance(pos, FogShape);
    texCoord0 = UV0;
}
```

```fsh:minecraft\shaders\core\rendertype_text.fsh
#version 150

#moj_import <fog.glsl>

uniform sampler2D Sampler0;

uniform vec4 ColorModulator;
uniform float FogStart;
uniform float FogEnd;
uniform vec4 FogColor;

uniform vec2 ScreenSize;

in float vertexDistance;
in float depthLevel;
in vec4 vertexColor;
in vec2 texCoord0;

in vec3 ColorValue;

out vec4 fragColor;

void main() {
    vec4 color = texture(Sampler0, texCoord0) * vertexColor * ColorModulator;

	if (color.a < 0.1) {
		discard;
	}

	// titleの影を消す
	if (ColorValue.b == 2 && depthLevel == 2400.00) {
		discard;
	}

	if (ColorValue.g == 1 && depthLevel == 2400.12) {
		// ノイズを生成して、元のテクスチャに足す
		vec2 uv = gl_FragCoord.xy / ScreenSize.xy / 10.0;
    	float noise = fract(sin(dot(uv.xy, vec2(100.001, 77.7777))) * 12345.6789012);
		color = color + vec4(vec3(noise) * 0.5, 1.0);
	}

	fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
}
```

```json:minecraft\shaders\core\rendertype_text.json
{
    "vertex": "minecraft:core/rendertype_text",
    "fragment": "minecraft:core/rendertype_text",
    "samplers": [
        { "name": "Sampler0" },
        { "name": "Sampler2" }
    ],
    "uniforms": [
        { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
        { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] },
        { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] },
        { "name": "FogStart", "type": "float", "count": 1, "values": [ 0.0 ] },
        { "name": "FogEnd", "type": "float", "count": 1, "values": [ 1.0 ] },
        { "name": "FogColor", "type": "float", "count": 4, "values": [ 0.0, 0.0, 0.0, 0.0 ] },
        { "name": "FogShape", "type": "int", "count": 1, "values": [ 0 ] },
        { "name": "GameTime", "type": "float", "count": 1, "values": [1.0] },
        { "name": "ScreenSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }
    ]
}

```

⇧しれっと新しいファイルが出てきましたが、これは特に解説することも無いのでそのままコピペして使ってください。GameTimeとノイズ生成の際に使ったScreenSizeを使うために必要なので、
```json
{ "name": "GameTime", "type": "float", "count": 1, "values": [1.0] },
{ "name": "ScreenSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }
```
というのを追記しています。

```json:minecraft\font\test2.json
{
    "providers": [
        {
            "type": "space",
            "advances": {
                "あ": -100
            }
        },
        {
            "type": "bitmap",
            "file": "block/stone.png",
            "ascent": 16,
            "height": 16,
            "chars": ["石"]
        },
        {
            "type": "bitmap",
            "file": "block/tnt_side.png",
            "ascent": 16,
            "height": 16,
            "chars": ["T"]
        },
        {
            "type": "reference",
            "id": "default"
        }
    ]
}
```
⇧そしてしれっとTNTのテクスチャも追記してます。

</details>

<details><summary>実行したコマンド（クリックで開閉）</summary>

```:sin波を描く石
/title @s title {"text":"石","font":"test2","color":"#01FF09"}
```

```:TNT：ノイズあり
/title @s title {"text":"T","font":"test2","color":"#01010A"}
```

```:TNT：ノイズなし
/title @s title {"text":"T","font":"test2","color":"#01000A"}
```

</details>

## 3. なんか作ってみようぜ！
これで技術の解説は終わりです。ということで、今得た知識を使って何か作ってみましょう！！！今回は例として<strong>「画面左上に時計を表示するデータパック」</strong>を作ってみようと思います！

<strong>できました。</strong>完成図がこちら⇩
![example1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/ac0d65f6-00a6-68da-55a4-c98bfd7f1ef3.png)

もちろんビデオ設定を変えたりしてもへっちゃら。
![example2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3924291/2c0cdf54-1a63-839c-f4e4-36e1d232b1be.png)

細かい解説はここではしませんが、データパックとリソースパックはダウンロードできるようにしたので、ぜひダウンロードして色々いじってみてください。CC0ライセンスで配布しているので、<strong>改造も再配布もなんでも大丈夫</strong> です。

⇩ ダウンロードリンク ⇩
\>> [データパック](https://github.com/Yuru-Yurufu/AdventCalendar2024-Datapack/archive/refs/tags/v1.0.0.zip) <<
\>> [リソースパック](https://github.com/Yuru-Yurufu/AdventCalendar2024-Resourcepack/archive/refs/tags/v1.0.0.zip) <<

:::note warn
時計がチカチカする場合は、`/title @s times 0 100 0`を再度実行してみてください！
:::

ただやっぱりここに一切解説を書かないのもどうなんだ？と思ったので、本当にざっくりですが何をやったのか書いておきます。

リソースパック：シェーダー部分はさっきとほぼ同じで、画面左上に移動する処理。フォントに各時計の画像に対応するように文字を登録。

データパック：あらかじめ時計の画像に対応する文字を入れたリストをstorageで用意しておく。現在時刻を取得（/time query daytime）して、マクロを使ってリストの何番目をtitleで表示するかを指定。

...という感じです。ちゃんと解説するってなるとマクロの解説とかからしなきゃいけなくなって収集つかなくなるのでこのくらいで、、。

余談ですがマイクラの「時計」ってテクスチャが切り替わる間隔均等じゃないんですね、、。まぁでもそこは今回の趣旨と違うと思うので、均等だってことにして作っちゃいました。

# おわりに
長い長いこの記事もようやく終わりです。ここまで読んでくれてありがとうございました。ホント、読む方も疲れたよね。最後なのですこ～しだけ喋りたいことを喋ろうと思います。

titleコマンド、意外と奥が深かったんじゃないでしょうか？？私が始めてtitleコマンドでGUI的なものを作ったのは、過去のツイートを見る限り約4年前、、、え。4年前？？？？まぁ、良いでしょう。ともかく約4年間のんびりとコマンドで遊び続けて得た知識とか技術とか、title関連のそういったものはほとんどこの記事に書いたつもりです。まぁそのせいでこの文量なわけですが、、。結構削ってこの文量なので許してほしいです。

ここまでこの記事を読んで、実践してくれたあなたはきっとGUIマスターになっていることでしょう。この記事の中で、何か一つでも新しい発見があればとても嬉しいです。
それではまた、どこかでお会いしましょう！！

.........最後に宣伝とかしても許されるかな。冒頭に載せたリバーシの配布マップが気になるよ～！って方はぜひ！！！！！！！⇩⇩⇩
<iframe width="560" height="315" src="https://www.youtube.com/embed/gD1s4s_Kjo0?si=y27nJfn2jpUkGbdF" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>