okayurisotto.net

私が好きでやったことが他の人のためにもなったらお得かも!

Minecraftでのブロックの見た目の水増しについての技術的な愚痴

はじめに

Minecraftでは、ブロックを回転させることでその見た目を水増しし、見た目の違和感をなくそうという努力がされています。しかしこれは完璧ではなく、非常に残念なクオリティになってしまっています。この記事ではそれについて解決策を模索しつつ、愚痴ります。

水増しとその実装の問題点について

Minecraftでは、草ブロックや土ブロックや砂ブロックなどにおいて、ブロックモデルをy軸基準で90度ずつ回転させることで見た目の数を水増ししています。これによって、例えば高いところから草原を見下ろしたときに草ブロックのテクスチャが一様なことからパターンを感じ取ってしまうことがなくなります。

しかしながらバニラのリソースパックでは、それらに対する回転はy軸基準のものしか行っていません。天面や底面が存在する草ブロックの場合はx軸やz軸で回転させると問題が生じてしまうため仕方がないところはあるでしょうが、土ブロックや砂ブロックの場合はどうでしょうか。土ブロックや砂ブロックで壁を作ってみると、どうしてもその見た目にパターンが感じ取れてしまいます。特に土ブロックの場合がわかりやすく、普段プレイしていても目に付くときがあります。

注意深く考えてみると、ブロックの見た目がまったく同じというのは不自然な話です。地形を形作るブロックであればなおのこと。Mojangもそう思って、一部のブロックに関しては見た目を水増ししているのでしょう。しかしそれは不十分です。

「正しい水増し」について

土ブロックは全面に同じテクスチャが貼り付けられた立方体ブロックです。y軸基準での90度ずつの回転で見た目は4通り。なぜx軸やz軸を基準とする回転はしないのでしょうか。立方体を回転させたときには24通りの見た目を取り得ます。このうちの4通りしか存在しないとはいささか不自然です。

もっと言うと、土ブロックの各面におけるテクスチャをそれぞれ独立して回転させるべきです。各面で4通り、それが6面集まって4096通り。また、回転させるだけでなく裏返すこともひとつの手でしょう。実際、石ブロックなどではテクスチャを左右反転させて使っています。表裏があるので各面で8通り、それが6面集まって262144通り。このうちのたった4通りしか定義しなかったがために不自然さが生まれてしまっているとは、もったいなさすら感じてしまいます。

どうにかできないのか?

Minecraftにおいて、これらテクスチャやモデルなどを管理しているのはデフォルトのリソースパックです。そしてリソースパックはユーザーによる拡張や上書きが可能……。つまりMojangの不十分な努力を、私の努力によって挽回できる可能性があるということです。

Minecraftのプログラムがゲーム内のブロックに見た目を与える処理の流れは次のようになると考えられています。

  1. ブロックに対応したブロック状態ファイルを参照する
  2. ブロック状態ファイルの記述に従い、ブロックモデルファイルを参照する
  3. ブロックモデルファイルの記述に従い、テクスチャファイルを参照しつつモデルを構築する

この中の「ブロック状態ファイル」と「ブロックモデルファイル」と「テクスチャファイル」がリソースパックに含まれています。それぞれ、blockstatesディレクトリにJSONファイルとして、models/blockディレクトリにJSONファイルとして、textures/blockディレクトリにPNGファイルとして置かれています。

ブロック状態ファイルでのブロックモデルファイルの参照は次のように行います。

{
  "variants": {
    "": [
      { "model": "minecraft:block/dirt" },
      { "model": "minecraft:block/dirt", "y": 90 },
      { "model": "minecraft:block/dirt", "y": 180 },
      { "model": "minecraft:block/dirt", "y": 270 }
    ]
  }
}

これはdirt.jsonとしてデフォルトのリソースパックに含まれるブロック状態ファイルで、y軸を基準に90度ずつ回転させた4通りのモデル定義オブジェクトがあり、その4つの要素を持つ配列の中からおそらくランダムに1つが選ばれるだろうことが見てわかるでしょう。

ブロックに対し262144通りの見た目を持たせるには、ブロックモデルファイルを262144個作成し、それらを上の例のようにブロック状態ファイルから参照する必要があります。しかしそんなにも大量のモデルを作成するのは無理があるでしょう。せめて1面ずつに分解することができたら、そしてそれをランダムにピックアップしていくようなことができたら……。

実はブロック状態ファイルのデータ構造は上の例がすべてではありません。次の例は、オークのフェンスのためのブロック状態ファイルです。

{
  "multipart": [
    {
      "apply": { "model": "minecraft:block/oak_fence_post" }
    },
    {
      "apply": { "model": "minecraft:block/oak_fence_side", "uvlock": true },
      "when": { "north": "true" }
    },
    {
      "apply": { "model": "minecraft:block/oak_fence_side", "uvlock": true, "y": 90 },
      "when": { "east": "true" }
    },
    {
      "apply": { "model": "minecraft:block/oak_fence_side", "uvlock": true, "y": 180 },
      "when": { "south": "true" }
    },
    {
      "apply": { "model": "minecraft:block/oak_fence_side", "uvlock": true, "y": 270 },
      "when": { "west": "true" }
    }
  ]
}

フェンスのモデルは、直立した部分と、周辺のブロックとの繋がりの部分とに分けられます。直立した部分は常に表示され、繋がりの部分は状況に応じてそれに重ね合わせるような形で描画されます。multipart機能はそのような状況に応じた重ね合わせを可能にするものです。もちろんその状況とは未定義でもよく、未定義だった場合、状況に関わらず描画されることになります。

そして、multipartではapplyとして配列を取ることができます。これは、先程のvariantsを使う例のような挙動をします。配列の中からランダムに1つが選ばれるのです。……しかし残念なことに、ここで言う「ランダム」とは各applyで再計算されるものではありませんでした。よって、各面でランダムにピックアップしてはくれません。

そういったわけで、今回の用途——8通りからランダムにピックアップしていくことを6面分繰り返す——というようなことはできません。では諦めるしかないのでしょうか?

各面には8通りの見た目があるから、1面ずつモデルを分割したとき、モデルの総数は48になります。モデルの作成自体の難易度は低く、スクリプトでも組んでしまえば一瞬です。それらをmultipartを使ってまとめあげるブロック状態ファイルの方が問題となります。愚直にすべての組み合わせを列挙するとなると、そのファイル中には8^6 * 6 = 1572864という途方もない数のモデル定義オブジェクトを含むことになります。

ひとつひとつブロックモデルファイルを作成するのは不可能……。各面でモデルを分けても、その分けたモデルの組み合わせの列挙が不可能……。ここまで悲惨だとMojangを恨みたくもなってきます。

Mojangへの恨み

実際、リソースパックの仕組みには無理があります。ブロック状態ファイルにおけるブロックモデルの読み込みと統合について、なぜandorの概念がないのでしょうか。下のようなTypeScriptで示すModel型ではダメだったのでしょうか。(循環しているとダメなのでしょうか?)

type Model = {
  model: string | Or | And;
  when?: { [key: string]: string };
  x?: 0 | 90 | 180 | 270;
  y?: 0 | 90 | 180 | 270;
  z?: 0 | 90 | 180 | 270;
  uvlock?: boolean;
};
type Or = { type: "or"; model: Model[] };
type And = { type: "and"; model: Model[] };

また、ブロックモデルファイルとテクスチャファイルの間にもう1層の抽象化が欲しいところです。石ブロックはテクスチャを左右反転させているという話をしましたが、これはcube_mirrored.jsonというブロックモデルファイルを祖先に持つstone_mirrored.jsonによって実現されています。普通の石ブロックのブロックモデルファイルはcube.jsonを祖先として持っていて、この2つのブロックモデルファイルは共通点が多いです。というより、テクスチャのuv座標の違いしかありません。にも関わらず、この2つのブロックモデルファイルはまったく別のものとして定義されていて、共通の親を持ちません。PNG画像としてのテクスチャとは別に、それをuv座標を指定して切り出したり裏返したりする存在がいたらよかったでしょう。

なんとかする。してみせる。

さて。8通りの面をmultipartで6つ集めて立方体の土ブロックとしたところで、ブロック状態ファイルの書き方を工夫しなければ見た目は8通りにしかなりません。しかしそのファイルのデータ構造は工夫した書き方ができるほど幅があるものではなく、よく言えばミニマルでシンプルな、悪く言えば行き当たりばったりで汎用性に欠くようなものです。この方法は苦痛が大きいです。

では初心に帰って、完成形のモデルをx軸、y軸、z軸で90度ずつ回転させることを考えてみましょう。どの面を底面とするかで6通り、底面を変えずにy軸で回転させることで4通り。よって24通りの見た目を取り得るのです。24という数は262144には遠く及びませんが、これでも見た目の違和感を減らすという目的は達せられるでしょう。

しかしこれは実はうまくいきません。というのも、Minecraftではz軸を基準とした回転がサポートされていないのです。これはMC-70636としてバグ報告されていますが、2014年に報告されてから一向に修正されていません。x軸やy軸を基準とした回転はできますが、それでは16通りが限界です。

妥協

妥協に妥協を重ねるようで気が滅入りますが、仕方がありません。16通りの見た目を持たせるだけで満足することにしましょう。実際、普段のプレイでは16通りでも十分かもしれないのですから。

それでは、具体的にどのブロックに対して修正を行わなければならないのか調べてみます。最終的な選別は目視でやるとして、ざっくりとスクリプトで、デフォルトのリソースパックに含まれるブロック状態ファイルのうち、JSONが次のような構造をしているものを列挙してみました。

{ variants: { "": unknown[] } }

すると次に挙げるファイルがそのような構造をしているとわかりました。なお注釈は、目視確認での分類を経た上で書き加えたものです。

# y軸基準回転しかしてないもの。今回のターゲット
black_concrete_powder.json
blue_concrete_powder.json
brown_concrete_powder.json
cyan_concrete_powder.json
dirt.json
gray_concrete_powder.json
green_concrete_powder.json
light_blue_concrete_powder.json
light_gray_concrete_powder.json
lime_concrete_powder.json
magenta_concrete_powder.json
orange_concrete_powder.json
pink_concrete_powder.json
purple_concrete_powder.json
red_concrete_powder.json
red_sand.json
rooted_dirt.json
sand.json
white_concrete_powder.json
yellow_concrete_powder.json

# 左右反転とy軸基準の180度回転をしているもの
bedrock.json
infested_stone.json
sculk.json
stone.json

# 無闇に回転させるべきではないもの
dirt_path.json
lily_pad.json

# 謎
netherrack.json

今回のターゲットは、土ブロックと根付いた土ブロック、砂ブロックと赤い砂ブロック、コンクリートパウダー各色の20種類でした。たしかにコンクリートパウダーに関しても、並べてみたときの側面は一様です。

石ブロックと虫食い石ブロック、岩盤に関しては左右反転(前述のcube_mirrored.jsonによる)とy軸基準での180度回転を行っているようでした。そしてスカルクブロックについても、なぜか、より複雑な回転はさせていないようでした。

土の道ブロックとスイレンの葉ブロックについてはy軸基準での回転しかすべきではないため、今回のターゲットではありません。

ネザーラックブロックを「謎」としたのは、(整形すると)次のようなJSONだったからです。

{
  "variants": {
    "": [
      { "model": "minecraft:block/netherrack" },
      { "model": "minecraft:block/netherrack", "y": 90 },
      { "model": "minecraft:block/netherrack", "y": 180 },
      { "model": "minecraft:block/netherrack", "y": 270 },

      { "model": "minecraft:block/netherrack", "x": 90 },
      { "model": "minecraft:block/netherrack", "x": 90, "y": 90 },
      { "model": "minecraft:block/netherrack", "x": 90, "y": 180 },
      { "model": "minecraft:block/netherrack", "x": 90, "y": 270 },

      { "model": "minecraft:block/netherrack", "x": 180 },
      { "model": "minecraft:block/netherrack", "x": 180, "y": 90 },
      { "model": "minecraft:block/netherrack", "x": 180, "y": 180 },
      { "model": "minecraft:block/netherrack", "x": 180, "y": 270 },

      { "model": "minecraft:block/netherrack", "x": 270 },
      { "model": "minecraft:block/netherrack", "x": 270, "y": 90 },
      { "model": "minecraft:block/netherrack", "x": 270, "y": 180 },
      { "model": "minecraft:block/netherrack", "x": 270, "y": 270 }
    ]
  }
}

土系ブロックなどについてはされていないx軸基準での回転が、私がいまとても求めている設定が、なぜかネザーラックブロックについてはデフォルトで行われているのです。……しかしなぜネザーラックブロックでだけ?

  1. Mojangは意図してネザーラックブロックにのみx軸基準での回転を設定している(土系ブロックなどには必要ないと思っている)
  2. Mojangは土系ブロックなどへのx軸基準での回転の設定を忘れている

おわりに

おのれもやん。お前は何がしたいんだ。私にはわからない。