自己紹介
Life is Tech! でMinecraftプログラミングコースのメンターをしています、ゆるゆるです。
Minecraftプログラミングコースの他にiPhoneアプリプログラミングコースとメディアアートコースのメンターもしているので、せっかくなら3つのコースを横断した何かで記事を書けないかと思い、執筆してみました。
作ったもの
Minecraft風の世界をSwiftでレンダリングするリアルタイムパストレーサーエンジンです。
MinecraftコースのMOD制作技術、iPhoneコースで使っているプログラミング言語のSwift、メディアアートコースで学べるシェーダーの技術を使っています。
まずはMinecraftの世界からブロック情報を切り出す
Minecraft Forge 1.18.1用のModを作ります。
以下のコードを使って、実際のMinecraftの世界からこのような形式のテキストファイルを作成します。
-15,-15,-15 minecraft:air
-15,-15,-14 minecraft:air
-15,-15,-13 minecraft:air
-15,-15,-12 minecraft:air
-15,-15,-11 minecraft:stone
-15,-15,-10 minecraft:stone
-15,-15,-9 minecraft:stone
-15,-15,-8 minecraft:stone
-15,-15,-7 minecraft:stone
-15,-15,-6 minecraft:stone
-15,-15,-5 minecraft:stone
-15,-15,-4 minecraft:stone
-15,-15,-3 minecraft:stone
-15,-15,-2 minecraft:stone
-15,-15,-1 minecraft:stone
-15,-15,0 minecraft:diorite
-15,-15,1 minecraft:diorite
-15,-15,2 minecraft:diorite
-15,-15,3 minecraft:diorite
-15,-15,4 minecraft:diorite
-15,-15,5 minecraft:diorite
...
自作コマンドの作り方はこちらの記事が参考になるかと思います。
private static int execute(CommandContext<CommandSourceStack> ctx, int r) {
if (ctx.getSource().getEntity() instanceof Player player) {
BlockPos center = player.blockPosition();
StringBuilder result = new StringBuilder("\n");
for (int x = center.getX() - r; x < center.getX() + r; x++) {
for (int y = center.getY() - r; y < center.getY() + r; y++) {
for (int z = center.getZ() - r; z < center.getZ() + r; z++) {
Block block = player.level.getBlockState(new BlockPos(x, y, z)).getBlock();
String name = block.getRegistryName().toString();
BlockPos rel = new BlockPos(
x - center.getX(),
y - center.getY(),
z - center.getZ()
);
result.append(rel.getX()).append(',')
.append(rel.getY()).append(',')
.append(rel.getZ()).append(' ')
.append(name).append('\n');
}
}
}
writeDump(result.toString());
}
return Command.SINGLE_SUCCESS;
}
private static void writeDump(String text) {
Path out = Paths.get(
System.getProperty("user.home"),
"Desktop",
"block_dump.txt"
);
try {
Files.createDirectories(out.getParent());
Files.writeString(
out,
text,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
);
} catch (IOException e) {
e.printStackTrace();
}
}
テクスチャの準備
XcodeでmacOS用のプロジェクトを作成します。
Minecraftのテクスチャをどこかから拾ってきて(あくまで個人利用・MOD制作の範囲内で)、Asset.xcassetsにこのようにテクスチャを配置します。
今回はパストレーサーの作成ということでPBR(物理ベースレンダリング)をしたいので、以下のパラメーター用のテクスチャがそれぞれ必要になります。
- albedo ベースの色
- bump bumpと名付けてしまっていますが、法線マップのことです
- emission 発光の色
- metallic 金属さ
- opacity 透明さ(opacityと名付けていますが今回は0で不透明、1で透明です)
- roughness 表面の粗さ
例として、鉄ブロックのroughnessはこのようにしています。

Minecraft Moddingに詳しい方はこのroughnessテクスチャどこで入手するんだ!と思った方もいるかもしれませんが、手書きです。Photoshopとか使って自分で書きましょう。
roughnessは黒がツルツル、白になるにつれてざらざらになっていきます。
objファイル、mtlファイルの作成
次に、Swiftを使ってblock_dump.txtからobjファイルとmtlファイルを作成します。これらは3Dモデルを扱えるファイル形式ですね。
objファイル
さて、ここでobjファイルの中身について解説します。
実際に僕のエンジンで作成した1ブロック分のobjファイルはこちらです。
mtllib F2595D5D43CD448BA96A2003B6E354BD.mtl
o GeneratedObject
usemtl Main
v 0.5 0.5 -0.5
v 0.5 -0.5 -0.5
v 0.5 0.5 0.5
v 0.5 -0.5 0.5
v -0.5 0.5 -0.5
v -0.5 -0.5 -0.5
v -0.5 0.5 0.5
v -0.5 -0.5 0.5
vn -0.0 1.0 -0.0
vn -0.0 -0.0 1.0
vn -1.0 -0.0 -0.0
vn -0.0 -1.0 -0.0
vn 1.0 -0.0 -0.0
vn -0.0 -0.0 -1.0
vn 0.7071068 0.0 0.7071068
vn 0.7071068 0.0 -0.7071068
vt 0.0 0.0
vt 0.0625 0.0
vt 0.0 0.0625
vt 0.0625 0.0625
vt 0.0625 0.0
vt 0.125 0.0
vt 0.0625 0.0625
vt 0.125 0.0625
vt 0.125 0.0
vt 0.1875 0.0
vt 0.125 0.0625
vt 0.1875 0.0625
vt 0.1875 0.0
vt 0.25 0.0
vt 0.1875 0.0625
vt 0.25 0.0625
vt 0.25 0.0
vt 0.3125 0.0
vt 0.25 0.0625
vt 0.3125 0.0625
vt 0.3125 0.0
vt 0.375 0.0
vt 0.3125 0.0625
vt 0.375 0.0625
vt 0.375 0.0
vt 0.4375 0.0
vt 0.375 0.0625
vt 0.4375 0.0625
vt 0.4375 0.0
vt 0.5 0.0
vt 0.4375 0.0625
vt 0.5 0.0625
vt 0.5 0.0
vt 0.5625 0.0
vt 0.5 0.0625
vt 0.5625 0.0625
vt 0.5625 0.0
vt 0.625 0.0
vt 0.5625 0.0625
vt 0.625 0.0625
vt 0.625 0.0
vt 0.6875 0.0
vt 0.625 0.0625
vt 0.6875 0.0625
vt 0.6875 0.0
vt 0.75 0.0
vt 0.6875 0.0625
vt 0.75 0.0625
vt 0.75 0.0
vt 0.8125 0.0
vt 0.75 0.0625
vt 0.8125 0.0625
vt 0.8125 0.0
vt 0.875 0.0
vt 0.8125 0.0625
vt 0.875 0.0625
vt 0.875 0.0
vt 0.9375 0.0
vt 0.875 0.0625
vt 0.9375 0.0625
vt 0.9375 0.0
vt 1.0 0.0
vt 0.9375 0.0625
vt 1.0 0.0625
vt 0.0 0.0625
vt 0.0625 0.0625
vt 0.0 0.125
vt 0.0625 0.125
vt 0.0625 0.0625
vt 0.125 0.0625
vt 0.0625 0.125
vt 0.125 0.125
vt 0.125 0.0625
vt 0.1875 0.0625
vt 0.125 0.125
vt 0.1875 0.125
vt 0.1875 0.0625
vt 0.25 0.0625
vt 0.1875 0.125
vt 0.25 0.125
vt 0.25 0.0625
vt 0.3125 0.0625
vt 0.25 0.125
vt 0.3125 0.125
vt 0.3125 0.0625
vt 0.375 0.0625
vt 0.3125 0.125
vt 0.375 0.125
vt 0.375 0.0625
vt 0.4375 0.0625
vt 0.375 0.125
vt 0.4375 0.125
vt 0.4375 0.0625
vt 0.5 0.0625
vt 0.4375 0.125
vt 0.5 0.125
vt 0.5 0.0625
vt 0.5625 0.0625
vt 0.5 0.125
vt 0.5625 0.125
vt 0.5625 0.0625
vt 0.625 0.0625
vt 0.5625 0.125
vt 0.625 0.125
vt 0.625 0.0625
vt 0.6875 0.0625
vt 0.625 0.125
vt 0.6875 0.125
vt 0.6875 0.0625
vt 0.75 0.0625
vt 0.6875 0.125
vt 0.75 0.125
vt 0.75 0.0625
vt 0.8125 0.0625
vt 0.75 0.125
vt 0.8125 0.125
vt 0.8125 0.0625
vt 0.875 0.0625
vt 0.8125 0.125
vt 0.875 0.125
f 5/66/1 3/67/1 1/68/1
f 3/66/2 8/67/2 4/68/2
f 7/66/3 6/67/3 8/68/3
f 2/66/4 8/67/4 6/68/4
f 1/66/5 4/67/5 2/68/5
f 5/66/6 2/67/6 6/68/6
f 5/66/1 7/65/1 3/67/1
f 3/66/2 7/65/2 8/67/2
f 7/66/3 5/65/3 6/67/3
f 2/66/4 4/65/4 8/67/4
f 1/66/5 3/65/5 4/67/5
f 5/66/6 1/65/6 2/67/6
usemtlはテクスチャなどを指定しているmtlファイルの名前を指定します。
vはこの3Dオブジェクトで使う可能性のある頂点の座標一覧です。
vnはこの3Dオブジェクトで使う可能性のある法線の一覧です。
vtはこの3Dオブジェクトで使う可能性のあるテクスチャ座標(uv座標)の一覧です。
このv, vn, vtのデータを用いて、実際に3角形の面を定義しているのが、fです。
こちらの1行を解説します。
f 5/66/1 3/67/1 1/68/1
5/66/1の塊で1つの頂点を表します。それぞれの数字が、このobjファイル内にあるv/vn/vtの何番目の値を参照するかを表しています。つまり、
vの5番目の値/vnの66番めの値/vtの1番めの値 を使ってこの頂点を定義しています。
さあ、これでobjファイルの仕組みがわかったので、Swiftを使ってblock_dump.txtからこのようなファイルを作成することは簡単ですね。
このように全てのブロック座標を定義して、ワールド全体を1つの3Dモデルとして1つのobjファイルを作成します。
mtllib 53E9837F272D438CB12ECAF10441B866.mtl
o GeneratedObject
usemtl Main
v -9.5 -4.5 -7.5
v -9.5 -5.5 -7.5
v -9.5 -4.5 -6.5
v -9.5 -5.5 -6.5
v -10.5 -4.5 -7.5
v -10.5 -5.5 -7.5
v -10.5 -4.5 -6.5
v -10.5 -5.5 -6.5
v 7.5 -0.5 1.5
v 7.5 -1.5 1.5
v 7.5 -0.5 2.5
v 7.5 -1.5 2.5
v 6.5 -0.5 1.5
v 6.5 -1.5 1.5
v 6.5 -0.5 2.5
v 6.5 -1.5 2.5
v 7.5 -7.5 -12.5
v 7.5 -8.5 -12.5
v 7.5 -7.5 -11.5
v 7.5 -8.5 -11.5
v 6.5 -7.5 -12.5
v 6.5 -8.5 -12.5
v 6.5 -7.5 -11.5
v 6.5 -8.5 -11.5
...
MTLファイル
MTLファイルはobjファイルを1対1に対応している、テクスチャなどマテリアル類を定義しているファイルです。
先ほどxcassetsに置いた画像などはここで参照します。
ただ、自分が作ったパストレーサーエンジンのパフォーマンスの関係上、できるだけ読み込むテクスチャ数を少なくする必要があったので、xcassetsに入っている全ての画像ファイルをパースして1枚にまとめて軽量化しています。
以下のようなテクスチャにパースされるようにします。
MTLファイルの中身は簡単で、これだけ書いておけばOKです。
newmtl Main
map_Kd albedo.png
map_Ka emission.png
map_metallic metallic.png
map_roughness roughness.png
map_bump bump.png
map_d opacity.png
map_Kdとかmap_bumpとか書いてあるのが、それぞれmtlファイルのファイル形式として事前に定義されているタグみたいなものです。
パストレーサーの実装
パストレーシングってなに?って方はこちらの記事を読んでみてください。概要はわかると思います。
自分のPCがMacなので、今回はMetalというグラフィックスAPIを使ってエンジンを作っていきました。ちょっといろいろな作品・アプリでこのエンジンを使っているので詳しいコードとかは公開できないのですが、工夫点だけ共有できればと思います!
工夫1 ポリゴン数を減らす
先ほどobjファイルを作成する時に、dumpした全てのブロックの情報から1つのobjファイルを作成していると説明しました。しかし、愚直に全てのブロックを三角形に変換していると、絶対に見えることのない三角形がたくさん発生してしまうのがわかると思います。
例えば1チャンク分の土の3Dモデルを作成するとして、愚直に全ての三角形を書き出してしまうと
- 1ブロックあたり12個の三角形
- 1チャンクあたり4096ブロック
ということで49152個の三角形が必要になります。
しかし、1チャンク(16x16x16の立法体)のシーンを作る場合、真ん中の14x14x14ブロック分は絶対に見えないので書き出す必要がありません。また、隣り合っているブロックは面を共有するのでそれらも必要ありません。
つまり、最も三角形を少なくしようとすると、三角形は3072個で十分ということになります。
パストレーシングでは、1つの画面上のピクセルから複数本の光線を飛ばし、それぞれが複数回バウンスしながらピクセルの色を確定させていきます。
その際、全ての三角形との交差判定をする必要があるので、三角形数をできるだけ少なくすることがパフォーマンスの高速化に一番大事な要素となります。
そのため、僕のobj書き出しエンジンでは絶対に見えないブロック・面は全て書き出し対象から除外するようにプログラムを組んでいます。
工夫2 Next Event Estimation の実装
Next Event Estimation(NEE)とは、三角形とrayの交差点からバウンスごとに光源を直接サンプリングして影レイを飛ばす手法です。
NEEなしのスタンダードなパストレーシングでは、rayが光源にヒットするまで待つ必要がありますが、NEEを実装することで主に暗いシーン・光源面積が小さいシーンでのノイズの収束速度を大幅に改善することができます。
最後に
Metalでパストレーサーを作るのに興味ある方は僕に直接話しかけてもらっても大丈夫ですし、こちらのAppleの動画を見てみるのも良いと思います!






