Defoldが完全3D化:テクニカル解説とアーキテクチャ・チュートリアル
要点まとめ
Defoldが軽量な2Dエンジンから、フル機能の3D game engineへと進化した背景と実装手法を詳しく解説します。Luaによるカスタムのrender_scriptや3D Cameraの制御、GLSLを用いたShader最適化など、開発者が直接Rendering Pipelineを制御できる強みを深掘りしています。また、Multiplayer化に伴うインフラの課題を解決するhorizOnとの統合についても触れ、実用的な開発ガイドとなっています。
すべてのインディー開発者は、ゲームエンジンに裏切られる瞬間を経験したことがあるでしょう。最初は軽量な2Dコンセプトから始まり、スコープクリープによって3D要素が導入され、気づけばエンジンのビルドサイズは膨れ上がり、ロード時間は遅延し、Webビルドはメモリエラーでクラッシュします。UnityやUnrealのような巨大なエンジンはAAA級の品質を実現するには驚異的ですが、WebGLやモバイルといった摩擦のないクロスプラットフォーム配信を目指す個人開発者にとっては、スーパーへの買い物に戦車で行くような過剰さを感じることが多々あります。
そこでDefoldの登場です。歴史的には、単一のコードベースでHTML5からNintendo Switchまであらゆるプラットフォームに対応できる、超高速で無駄のない2Dエンジンとして支持されてきたDefoldが、公式に進化を遂げました。現在、Defoldは完全な機能を備えた3D game engineとなっています。以前から技術的には内部で3Dコンテキストとしてレンダリングされていましたが(正投影カメラを介して平面を投影)、最近のアップデートにより、専用の3Dツール、完全なglTF meshサポート、そして本格的な3D開発のための効率的なワークフローが導入されました。
これはエコシステムにとって大きな転換点です。数秒でコンパイルでき、1桁メガバイト単位のバイナリを生成しながら、ダイナミックな3D世界をレンダリングするパワーを求めているなら、Defoldは今やトップクラスの選択肢です。このDefold 3D game engineチュートリアルとアーキテクチャ解説では、DefoldのRendering Pipelineがどのように動作するのか、カスタム3D Cameraをゼロからスクリプトする方法、そして軽量3Dゲームをネットワーク化する際のBackend的な影響について深く掘り下げていきます。
内部構造:Defoldにおける3D Pipelineの実態
Defoldで3Dをマスターするには、そのレンダリング哲学を理解する必要があります。Defoldは、Unreal Engineのように事前設定されたPBR (Physically Based Rendering) pipelineをすぐに提供してくれるわけではありません。その代わり、Luaで記述された、高度に最適化されたデータ駆動型のrender_scriptを提供します。
Defoldで画面に描画されるすべてのものは、render_scriptによって処理されます。デフォルトでは、このスクリプトは2D向けに構成されています。正投影行列(orthographic projection matrix)をセットアップし、スプライトをZ値(深度)でソートし、奥から手前へと描画します。Defoldの3D機能を解放するには、このスクリプトを書き換えて透視投影行列(perspective projection matrix)を利用し、ハードウェアの深度テストを有効にし、3Dモデル用のカスタムレンダリング述語(render predicates)を定義する必要があります。
このローレベルなアクセスは諸刃の剣です。一方で、行列計算の数学を少し書く必要があります。しかしもう一方では、Draw Callを完全に制御できるため、モノリシックなエンジンでは非常に困難な方法で、超低スペックのハードウェア向けにレンダリングを最適化することが可能になります。
カスタム3D Render Scriptの設計
描画順序に基づいて3Dモデルが誤って重ならないように正しくレンダリングするには、深度バッファ(Z-buffer)を有効にする必要があります。以下は、Defoldのデフォルトのパイプラインを置き換える基礎的な3D Render Scriptです。
-- main/3d_pipeline.render_script
function init(self)
-- 背景のクリアカラーを定義 (RGBA)
self.clear_color = vmath.vector4(0.1, 0.1, 0.12, 1.0)
self.clear_buffers = {
[render.BUFFER_COLOR_BIT] = self.clear_color,
[render.BUFFER_DEPTH_BIT] = 1.0,
[render.BUFFER_STENCIL_BIT] = 0
}
-- render predicatesを作成。'model'は3Dメッシュのデフォルトタグです
self.predicates = {
model = render.predicate({"model"}),
gui = render.predicate({"gui"}),
text = render.predicate({"text"})
}
end
function update(self)
-- 1. フレームのレンダリング状態をセットアップ
render.set_depth_mask(true)
render.set_stencil_mask(0xff)
render.clear(self.clear_buffers)
-- 2. 3D Camera Projectionを構成
local window_width = render.get_window_width()
local window_height = render.get_window_height()
if window_width == 0 or window_height == 0 then return end
local aspect_ratio = window_width / window_height
local fov = math.rad(60) -- 視野角60度
local near_z = 0.1
local far_z = 1000.0
local proj_matrix = vmath.matrix4_perspective(fov, aspect_ratio, near_z, far_z)
render.set_projection(proj_matrix)
-- 3. 深度テストを有効にして3Dモデルを描画
render.set_depth_test(render.COMPARE_LEQUAL)
render.set_cull_face(render.FACE_BACK)
render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
-- ビュー行列はカメラ本体のスクリプトからメッセージ経由で渡されます
if self.view_matrix then
render.set_view(self.view_matrix)
render.draw(self.predicates.model)
end
-- 4. 3Dシーンの上にGUIを描画 (Orthographic)
render.set_depth_mask(false)
render.set_depth_test(render.COMPARE_ALWAYS)
local gui_proj = vmath.matrix4_orthographic(0, window_width, 0, window_height, -1, 1)
render.set_projection(gui_proj)
render.set_view(vmath.matrix4())
render.draw(self.predicates.gui)
render.draw(self.predicates.text)
end
function on_message(self, message_id, message)
if message_id == hash("set_view_matrix") then
self.view_matrix = message.matrix
end
end
ここでの状態管理がいかに明示的であるかに注目してください。深度バッファをクリアし、現在のウィンドウサイズに基づいて透視投影行列を計算し、背面カリング(back-face culling)を適用してラスタライズのサイクルを節約し、最後にGUIをレンダリングする前に投影行列を正投影(orthographic)に戻しています。これにより、堅牢なセパレートPipelineのレンダリングアーキテクチャが実現します。
First-Person 3D Camera Controllerの構築
空間内を移動するカメラがなければ、Render Scriptだけでは何も見えません。DefoldはMessage Passingアーキテクチャを重視しています。カメラが直接transform.Translate()を呼び出すようなオブジェクト指向型のエンジンとは異なり、DefoldではカメラのスクリプトがView Matrixを計算し、先ほど作成したRender Scriptに送信します。
マウスによる視点操作(ピッチとヨー)とキーボード移動(WASD)を処理する、標準的なFirst-Personカメラを構築してみましょう。
-- scripts/camera_controller.script
go.property("mouse_sensitivity", 0.2)
go.property("move_speed", 10.0)
function init(self)
msg.post(".", "acquire_input_focus")
self.pitch = 0
self.yaw = 0
-- マウスカーソルを非表示にしてキャプチャ
window.set_mouse_lock(true)
self.forward = vmath.vector3(0, 0, -1)
self.right = vmath.vector3(1, 0, 0)
self.up = vmath.vector3(0, 1, 0)
self.velocity = vmath.vector3(0)
end
function update(self, dt)
local pos = go.get_position()
-- 移動速度を適用
if vmath.length_sqr(self.velocity) > 0 then
local move_dir = vmath.normalize(self.velocity)
pos = pos + move_dir * self.move_speed * dt
go.set_position(pos)
end
-- ビュー行列を計算
local rotation = go.get_rotation()
self.forward = vmath.rotate(rotation, vmath.vector3(0, 0, -1))
local target = pos + self.forward
local view_matrix = vmath.matrix4_look_at(pos, target, self.up)
-- 計算したビュー行列をレンダリングパイプラインに送信
msg.post("@render:", "set_view_matrix", { matrix = view_matrix })
-- 次のフレームのために速度をリセット
self.velocity = vmath.vector3(0)
end
function on_input(self, action_id, action)
if action_id == hash("mouse_moved") then
self.yaw = self.yaw - action.dx * self.mouse_sensitivity
self.pitch = self.pitch + action.dy * self.mouse_sensitivity
-- カメラが反転しないようにピッチをクランプ
self.pitch = math.max(-89, math.min(89, self.pitch))
-- オイラー角をクォータニオンに変換
local rot_y = vmath.quat_rotation_y(math.rad(self.yaw))
local rot_x = vmath.quat_rotation_x(math.rad(self.pitch))
go.set_rotation(rot_y * rot_x)
elseif action_id == hash("move_forward") then
self.velocity = self.velocity + self.forward
elseif action_id == hash("move_backward") then
self.velocity = self.velocity - self.forward
elseif action_id == hash("move_left") then
self.right = vmath.cross(self.forward, self.up)
self.velocity = self.velocity - self.right
elseif action_id == hash("move_right") then
self.right = vmath.cross(self.forward, self.up)
self.velocity = self.velocity + self.right
end
end
このスクリプトは、マウスのデルタ移動をキャプチャしてピッチとヨーを調整し、それらのオイラー角を堅牢なQuaternion回転に変換します。その後、その回転からフォワードベクトルとライトベクトルを直接導出することで、「W」キーを押したときに常に現在向いている方向に移動できるようにしています。
3D Assetの統合とカスタムShader
Defoldの最近のアップデートにより、プロジェクトへの3D Assetの取り込みは非常に簡単になりました。エンジンは、Webおよび軽量ゲーム開発の業界標準となった.gltfおよび.glb形式をネイティブにサポートしています。
ただし、メッシュをレンダリングするにはMaterialが必要であり、MaterialにはShaderが必要です。デフォルトでDefoldには基本的なMaterialが含まれていますが、独自のGLSL Shaderを記述することで、他と差別化するために必要な視覚的な独自性を得ることができます。モバイルやHTML5ターゲットに最適化された、高速なUnlit Textured Shaderを書いてみましょう。
Vertex Shader (model.vp)
// model.vp
uniform highp mat4 view_proj;
uniform highp mat4 world;
attribute highp vec4 position;
attribute mediump vec2 texcoord0;
varying mediump vec2 var_texcoord0;
void main()
{
// 頂点の最終的なスクリーン空間位置を計算
vec4 p = view_proj * world * vec4(position.xyz, 1.0);
var_texcoord0 = texcoord0;
gl_Position = p;
}
Fragment Shader (model.fp)
// model.fp
varying mediump vec2 var_texcoord0;
uniform lowp sampler2D texture_sampler;
uniform lowp vec4 tint;
void main()
{
// テクスチャをサンプリングし、カラーティントを乗算
lowp vec4 tex_color = texture2D(texture_sampler, var_texcoord0.xy);
gl_FragColor = tex_color * tint;
}
DefoldのMaterialファイルにおいて、view_proj uniformをエンジンの組み込みビュー投影行列にマッピングし、texture_samplerをメッシュのディフューズテクスチャにマッピングします。これらのShaderは動的なライティングやシャドウマップを計算しないため、非常に高速に動作し、低スペックのハードウェアでも容易に60 FPSを維持できます。
3D MultiplayerとState Synchronizationの処理
2Dのグリッドベースのゲームから完全な3D環境に移行すると、ネットワークアーキテクチャの複雑さは飛躍的に増大します。2Dから3Dへの移行は、State SynchronizationにおいてZ軸の深度、物理エンジン間の浮動小数点の誤差、そして完全なQuaternion回転を考慮しなければならないことを意味します。これの処理を誤ると、深刻な視覚的スタッタリングが発生します。これは、How To Fix Player Location Desync In Uefn And Unreal Engine Multiplayerの分析でも詳しく掘り下げた共通の課題です。
Defoldは軽量なエンジンのため、Unreal EngineのRPCのような重厚な組み込みレプリケーションシステムは備えていません。開発者自身が状態データを効率的にパッキングする責任があります。
3D座標の同期をREST APIに頼ると、ゲームループは瞬時にボトルネックに陥ります。代わりに、永続的な双方向接続が必要です。以前のガイドではDitch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backendsを取り上げましたが、全く同じアーキテクチャの原則が、DefoldのLuaベースのWebSocket拡張機能にも直接適用されます。
以下は、WebSocketを介したペイロードサイズを最小限に抑えるための、3D transformデータのシリアライズ例です。
-- scripts/network_sync.lua
local json = require "builtins.scripts.json"
function serialize_transform(go_id)
local pos = go.get_position(go_id)
local rot = go.get_rotation(go_id)
-- ペイロードを必要最小限のデータに圧縮
-- 帯域幅を節約するため、座標を小数点第2位で丸める
local payload = {
id = hash_to_hex(go_id),
x = math.floor(pos.x * 100) / 100,
y = math.floor(pos.y * 100) / 100,
z = math.floor(pos.z * 100) / 100,
-- クォータニオンの正確な復元には4つのコンポーネントすべてが必要
qx = rot.x,
qy = rot.y,
qz = rot.z,
qw = rot.w
}
return json.encode(payload)
end
浮動小数点を丸めて必須データのみをパッキングすることで、急激な移動更新中のネットワークバッファのオーバーフローを防ぐことができます。このフローを管理することが、レスポンシブなクロスプラットフォームプレイの鍵となります。
クロスプラットフォーム3Dにおけるインフラの負荷
Defoldで高度に最適化された3Dゲームクライアントを書くことは、非常に満足度の高い作業です。エンジンは開発の邪魔をせず、ミリ秒単位でコンパイルでき、ロジックのみに集中させてくれます。
しかし、その3DゲームをMultiplayerにしたり、クラウドセーブを追加したり、クロスプラットフォームのリーダーボードを実装しようと決めた瞬間、焦点はゲーム開発からサーバーのオーケストレーションへと移ってしまいます。突然、Dockerfilesを書き、Kubernetesクラスターを構成し、セッション状態のためにRedisインスタンスと格闘し、DDoS攻撃からWebSocketゲートウェイを保護する方法を模索することになります。
これらをすべて自前で構築するには、ロードバランサー、データベースのシャーディング、SSL証明書の管理などが必要になり、信頼性の高いプロトタイプを動かすだけで4〜6週間の作業が費やされてしまいます。
horizOnを使用すれば、これらのBackendサービスはあらかじめ構成されているため、インフラではなくゲームのリリースに集中できます。horizOnは、ユーザー認証、リアルタイムのデータベース同期、サーバーオーソリティ(server-authoritative)ロジックのためのネイティブ統合を提供し、独自のBackendエコシステムを持たないDefoldのような軽量エンジンのギャップを完璧に埋めてくれます。クライアントエンジンのスピードを維持しながら、サーバーアーキテクチャの重労働をアウトソーシングできるのです。
Defold 3D開発のベストプラクティス
Defoldで商業的に成立する3Dプロジェクトを計画している場合は、以下のアーキテクチャガイドラインを厳守してください。
- ジオメトリを高度に最適化する: Defoldはスピードを重視して設計されています。軽量であるという利点を維持するために、特にHTML5/WebGLをターゲットにする場合は、シーン全体のポリゴン数を10万以下に抑えてください。ディテールを表現するには、高密度メッシュではなく、ベイクされた法線マップ(normal maps)を使用します。
- Frustum CullingにRender Predicatesを活用する: Defoldはデフォルトでは、3D空間においてカメラの視界外にあるオブジェクトを自動的にカリングしません。LuaでカスタムのFrustum Cullingロジックを記述し、範囲外にあるオブジェクトのモデルコンポーネントを動的に無効化してラスタライズ時間を節約する必要があります。
- アトラシングによるDraw Callの統合: 固有のMaterialやテクスチャごとに、GPUへの個別のDraw Callが必要になります。テクスチャを大きなテクスチャアトラスに統合してください。10個の異なる3Dモデルが全く同じMaterialとアトラスを共有していれば、Defoldは内部でより効率的にバッチ処理を行うことができます。
- 複雑な計算を事前に計算する: 行列の乗算やクォータニオン変換は、Luaにおいて非常にコストのかかる操作です。フォワードベクトルやライトベクトルをキャッシュし、毎フレーム無条件に計算するのではなく、プレイヤーの回転が実際に変化したときにのみ再計算するようにしてください。
- ロジックをレンダリング頻度から分離する: ゲームロジック(
update)は60 FPSで動作するかもしれませんが、カスタム物理やネットワークのステップは30 FPSで更新されるかもしれません。3Dの視覚的な位置を、最新の状態に直接スナップさせるのではなく、速度に基づいて補間(interpolation)することで、多様なモニターリフレッシュレートで滑らかなレンダリングを実現します。 - LuaのGarbage Collectionを管理する: 動的な3D環境では、ベクトルオブジェクトや行列を頻繁に作成・破棄することになります。LuaのGarbage Collectorは、適切に管理しないと顕著なフレームスパイクを引き起こす可能性があります。
updateループ内で新しいローカル変数をインスタンス化する代わりに、既存のvmath.vector3やvmath.matrix4インスタンスの内部値を直接更新して再利用してください。弾丸やエンティティのためにメモリプールを事前に割り当てておきましょう。 - ライティングを外部でベイクする: カスタムGLSL Shaderでの動的ライティングは、モバイルデバイスのパフォーマンス予算をすぐに使い果たしてしまいます。glTFモデルをエクスポートする前に、BlenderやMayaを使用してグローバルイルミネーション(GI)やアンビエントオクルージョン(AO)をテクスチャに直接ベイクしてください。美しくベイクされたライティングを備えたシンプルなUnlit Shaderは、モバイルWebブラウザにおいて、常に複雑な動的Shaderを凌駕します。
結論
Defoldが堅牢な3D game engineへと進化したことは、独立系開発者にとって大きな勝利です。超高速なコンパイル時間と驚異的に小さなバイナリサイズを維持しつつ、広大で魅力的な3D世界を構築するために必要な数学的基盤とツールを提供しています。カスタムRender Scriptをマスターし、行列演算を理解し、ネットワークデータを効率的にシリアライズすることで、より巨大で肥大化したエンジンと技術的に競合できるクロスプラットフォームタイトルを構築できます。
高度に最適化された3Dクライアントをオンライン化し、インフラ管理の煩わしさなしにMultiplayerのBackendをスケールさせる準備ができたら、horizOnを無料でお試しください。また、API docsをチェックして、次回のDefoldプロジェクトにいかに素早くリアルタイムサービスを統合できるかを確認してみてください。