1. 今回やること
前々回の記事「#07 MMD party formation follow」では、Babylon.js と babylon-mmd を使って、MMD キャラクターが隊列を組んでランダム徘徊する仕組みを作りました。本稿はその続編として、同じ隊列システムに「焚き火休憩イベント」を追加していきます。
[Babylon.js #07] 地形上を複数MMDキャラが隊列移動する仕組みを作る
Babylon.js と babylon-mmd を使い、MMDキャラクターの複数体運用を一歩進めて、地形上での隊列移動(RPG風の縦列歩行)を実装します。TransformNode による制御、Ray による地面吸着、Yaw 補間、履歴追 …
https://humanxai.info/posts/babylonjs-07-mmd-party-formation-follow/具体的には、ノイズで生成した草原地形の上を MMD パーティが徘徊しているときに、
- しばらく通常のランダム移動をしたあと、
- 一定条件を満たすと、フィールド上の焚き火地点まで歩いて集合し、
- 焚き火を囲むように円陣を組んでしばらく休憩し、
- 休憩が終わったら再び隊列を組み直してランダム徘徊に戻る、
という一連の流れを、状態マシンとして実装します。
焚き火の 3D モデル自体は、Tencent の Hunyuan3D をローカル環境で動かし、手元で作成した焚き火画像からメッシュ+テクスチャを生成したものを glb でエクスポートし、Babylon.js から読み込んで利用します。生成 AI 製の小物モデルを、そのままゲーム内ギミックとして組み込むところまでを一通り見ていきます。
前回の記事:
[Babylon.js #08] メビウスの帯の上をMMDモデルが歩く
Babylon.js / babylon-mmd を使って、メビウス帯の上を MMD キャラが歩くシーンを構築。トラックフレーム、姿勢合わせ、MMDアニメーションの停止対策、Inspector UI によるリアルタイム調整までまとめて解説。
https://humanxai.info/posts/babylonjs-08-mmd-mobius-walker-inspector/■ VMDのポーズデータ制作者
- さわのつり 様
女の子座りポーズ集 - BowlRoll
さわのつりの作品です。
https://bowlroll.net/file/24316動画(Youtube):
Babylon.js × MMD #04|隊列に「焚き火休憩イベント」を追加🔥#BabylonJS #MMD #3DCG #WebGL #生成AI #Hunyuan3D
Babylon.js + babylon-mmd を使った MMD 隊列システムに、「焚き火位置へ集合 → 円陣で休憩 → 再び散開する」イベント演出を追加しました。焚き火モデルは Hunyuan3D で自作し、Babylon.js 上に配置しています。▼ 技術要素・Babylon.js(WebGL / WebG...
https://youtube.com/shorts/vI5oF7NB5n4?feature=share動画(PC):
2. 前回までのおさらい
まずは前回の記事「#07 MMD party formation follow」で実装した内容を軽く振り返ります。
隊列追従(FOLLOW_SPACING)
複数の MMD キャラクターに対して、先頭キャラの移動履歴(位置+向き)をサンプリングし、
FOLLOW_SPACING だけ距離をずらして過去の履歴を参照させることで、後続キャラが自然に隊列を維持しながら歩くパーティシステムを実装しました。
地面への吸着(Ray による高さ補正)
Babylon.js の Ray を使って、各キャラの足元から地形へ垂直レイキャストを行い、 ノイズ地形の凹凸に合わせてキャラクターの高さを補正する仕組みも組み込み済みです。
これにより、徘徊 AI としての最小セットはすでに完成しており、 キャラ数を増やしたり、フィールドを拡張したりすることも容易になっています。
#08 とのつながり
#08 では「メビウス帯の上を歩く MMD Walker」という別テーマを扱いましたが、 babylon-mmd の操作には慣れてきたため、今回はその流れで 「イベント的な動き(焚き火 → 休憩 → 再集合)」 といった 行動状態の切り替え(ステートマシン) を導入する回になります。
徘徊 → 集合 → 円陣 → 再出発 というループを作ることで、 単なる “歩くデモ” から、より “生活感のある MMD パーティ” に近づけていきます。
3. Hunyuan3D で焚き火モデルを作る
今回の焚き火モデルは、Hunyuan3D をローカル環境に入れて 画像 → 3D メッシュ(glb) を生成しました。
既存の minimal_demo.py から「画像入力〜glb出力」部分だけを抜き出し、焚き火専用のスクリプトにしています。
Hunyuan3D(ShapeGen & TexGen)を Windows + CUDA で実行するためのセットアップ(2026年2月版)
Windows 11 + CUDA 12.1 環境で Hunyuan3D の 3D 生成(ShapeGen/TexGen)を実行するまでの導入方法を詳細に解説。PyTorch 2.5.1+cu121、transformers 4.39.3、MSVC …
https://humanxai.info/posts/ai-hunyuan3d-install-windows-cuda-2026/3.1 minimal_demo.py のカスタマイズ
Hunyuan3D 本体のリポジトリに含まれている minimal_demo.py から、
形状生成(ShapeGen)+テクスチャ生成(Paint)に相当する処理だけを取り出して、
次のようなシンプルなスクリプトを用意しました。
from PIL import Image
from hy3dgen.rembg import BackgroundRemover
from hy3dgen.shapegen import Hunyuan3DDiTFlowMatchingPipeline
from hy3dgen.texgen import Hunyuan3DPaintPipeline
model_path = 'tencent/Hunyuan3D-2'
pipeline_shapegen = Hunyuan3DDiTFlowMatchingPipeline.from_pretrained(model_path)
pipeline_texgen = Hunyuan3DPaintPipeline.from_pretrained(model_path)
image_path = 'assets/takibi.png' # 自作の焚き火画像
image = Image.open(image_path).convert("RGBA")
if image.mode == 'RGB':
rembg = BackgroundRemover()
image = rembg(image)
mesh = pipeline_shapegen(image=image)[0]
mesh = pipeline_texgen(mesh, image=image)
mesh.export('takibi.glb')
ポイントは以下の通りです。
- image_path に 自作した焚き火イラスト(PNG)を指定するだけでよい
- 背景付きの画像を使う場合は、
BackgroundRemoverを通して 背景透過済みの RGBA にしてから ShapeGen に渡す - pipeline_shapegen で粗い 3D 形状を生成し、
pipeline_texgenで同じ画像を条件としてテクスチャを書き込む - 最後に
mesh.export('takibi.glb')で Babylon.js / Blender から扱いやすい glb 形式として出力
初回実行時は、tencent/Hunyuan3D-2 の重いモデルを Hugging Face からダウンロードするため、
GPU・回線ともにかなり時間がかかる点には注意してください(一度ダウンロードされればキャッシュから読み込まれます)。
3.2 Blender での確認
生成した takibi.glb は、そのまま Babylon.js に放り込む前に、一度 Blender で中身を確認しておきました。
-
glb をインポートすると、すでに ベースカラー用のテクスチャがノードに接続された状態 になっている
-
必要に応じて
- 全体スケールの調整
- 原点位置(Origin)の位置合わせ
- 余計なメッシュやマテリアルスロットの整理 などを軽く行っておくと、ゲーム側で扱いやすくなる
(ここに Blender のスクリーンショットを 1 枚)
この段階で「焚き火らしく見える」こと、マテリアルが正しく貼られていることが確認できたので、
あとは public/models/takibi.glb として Babylon.js 側に配置し、
シーン内のキャンプファイヤー位置にスポーンさせて使っていきます。
4. 焚き火モデルを Babylon.js から読み込む
4.1 public/models に配置する
生成した焚き火モデル(takibi.glb / bonfire2.glb など)は、Vite プロジェクトの
public/models/takibi.glb
# or
public/models/bonfire2.glb
のように public 配下 に置いておきます。
Vite の場合、public ディレクトリ以下はビルド時にそのままルートにコピーされるので、
public/models/xxx.glb→ 実行時のパスは/models/xxx.glb
として、そのまま SceneLoader.ImportMeshAsync から叩くことができます。
/src からの相対パスではなく、常にルート相対パス /models/... になる点だけ覚えておけば OK です。
4.2 campfireRoot の作成と SceneLoader.ImportMeshAsync
焚き火は「シーン内のある一点を中心に、モデルをまとめて置きたい」ので、
まずは TransformNode でルートノード campfireRoot を作り、焚き火の座標に配置します。
// 焚き火のルート
campfireRoot = new TransformNode("campfire_root", scene);
campfireRoot.position.copyFrom(CAMPFIRE_POS); // or campfirePos
campfireRoot.setEnabled(false); // 最初は非表示
実際のメッシュは SceneLoader.ImportMeshAsync で読み込み、
一番メインになりそうな Mesh を campfireRoot の子としてぶら下げます。
SceneLoader.ImportMeshAsync("", "/models/", "bonfire2.glb", scene)
.then((result) => {
const main = result.meshes.find((m) => m instanceof Mesh) as Mesh | undefined;
if (!main) return;
main.parent = campfireRoot!;
main.position.set(0, 5, 0); // 地面に少しめり込む場合は Y を持ち上げる
main.rotation.set(0, 0, 0);
main.scaling.scaleInPlace(campfireScale); // config 側でスケールを一元管理
})
.catch((e) => {
console.error("Failed to load bonfire.glb", e);
});
最初に読み込んだときはスケールが合っておらず、
草原ノイズ地形の中に“米粒サイズの焚き火”が埋もれていてほぼ見えない 状態になりました。
そこで一度 Babylon.js のインスペクタでスケールを確認し、campfireScale に倍率を入れてから
scaleInPlace で一気にスケーリングするようにしています。
コード上では最終的に
- 位置ベクトル:
campfirePos - スケール:
campfireScale
のように config.ts 側で管理するようにしておくと、
- 草原のサイズを変えたときに焚き火の位置だけ差し替える
- VR 用シーンと PC 用シーンで焚き火のスケールを変える
といった調整が楽になります。
5. MMD隊列ロジックを「焚き火イベント付き」に拡張する
ここからは、#07 で作った「ランダム徘徊+隊列追従」のロジックに対して、
- たまに焚き火位置へ集合して
- 円陣でしばらく休憩し
- 再び隊列を組み直して散開する
というイベントを差し込むための拡張内容を書いていきます。
5.1 PartyMode enum で 4 状態を定義する
まずは「今パーティがどんな状態なのか」を表現するために、シンプルなステートマシンを enum で用意します。
enum PartyMode {
Roam, // 普段のランダム徘徊
GoToFire, // 焚き火位置へ集合中
AtFire, // 焚き火を囲んで待機
Regroup, // 再び隊列を組み直す
}
各状態での振る舞いはおおまかに次のようになります。
-
Roam
- これまで通り、リーダーはランダムなターゲットへ歩き続ける
- 後続は
FOLLOW_SPACINGぶん距離をあけて、履歴を使って追従する
-
GoToFire
- 各キャラクターに「焚き火の円周上のスロット」を 1 つずつ割り当て
- そこへ最短ルートで直行する(隊列追従はいったん止める)
-
AtFire
- 焚き火を中心に向き直り、ポーズに切り替えてしばらく待機
- 焚き火モデルを有効化して表示
-
Regroup
- リーダーの背後に再び隊列を組み直す
- 整列し終わったら Roam に戻り、ランダム徘徊を再開
この 4 状態を scene.onBeforeRenderObservable の中で switch していく形です。
5.2 焚き火周りのスロット計算
焚き火イベントでは、パーティ全員を「焚き火を囲む円周上の座標」にきれいに配置したいので、 まずはスロットとなる位置を計算する関数を用意します。
function computeFireSlots() {
fireSlots = [];
const n = roots.length;
for (let i = 0; i < n; i++) {
const angle = (2 * Math.PI * i) / n;
const x = CAMPFIRE_POS.x + Math.cos(angle) * CAMPFIRE_CIRCLE_RADIUS;
const z = CAMPFIRE_POS.z + Math.sin(angle) * CAMPFIRE_CIRCLE_RADIUS;
const pos = projectOnGroundXZ(x, z);
fireSlots.push(pos);
}
}
単純な円周配置ですが、ここでポイントになるのが projectOnGroundXZ です。
function projectOnGroundXZ(x: number, z: number): Vector3 {
const rayOrigin = new Vector3(x, RAY_FROM_Y, z);
const ray = new Ray(rayOrigin, Vector3.Down(), RAY_FROM_Y + 50);
const hit = scene.pickWithRay(ray, (m) => m === ground);
if (hit?.hit && hit.pickedPoint) {
return new Vector3(x, hit.pickedPoint.y + FOOT_OFFSET_Y, z);
}
return new Vector3(x, 0 + FOOT_OFFSET_Y, z);
}
- XZ 平面上で理想的な円を作ったあと、
- Ray を下向きに飛ばして実際の地形(ノイズ地形)にスナップする
ことで、「草原ノイズでうねっている地面の上」に、きれいに円陣を組めるようにしています。 #07 でやった「歩行キャラの地面吸着」を、焚き火のスロット計算にも再利用しているイメージです。
5.3 Roam → GoToFire への遷移条件
どのタイミングで焚き火イベントに入るかは、完全なランダムではなく、
- 一定回数ランダムターゲットに到達したあとで
- そのたびに一定確率でトリガーする
という 2 段階の条件にしています。
const MIN_ROAM_LOOPS_BEFORE_FIRE = 3; // 焚き火許可までの最低周回数
const FIRE_TRIGGER_PROB_PER_ARRIVAL = 0.4; // 到達ごとの発火確率(40%)
Roam 状態のターゲット到達処理はこうなります。
if (dist < ARRIVE_DIST) {
// ターゲット到達
roamLoopsSinceLastFire++;
const canTriggerFire = roamLoopsSinceLastFire >= MIN_ROAM_LOOPS_BEFORE_FIRE;
const shouldTriggerFire = canTriggerFire && Math.random() < FIRE_TRIGGER_PROB_PER_ARRIVAL;
if (shouldTriggerFire) {
startGoToFire();
return; // このフレームはここで終了
}
// 焚き火に行かない場合は次のランダムターゲットへ
target = randomTarget(HALF);
} else {
moveToward(leader, target, WALK_SPEED);
}
- 最低 3 回は必ず普通に徘徊させる
- そのあとでターゲット到達するたびに 40% の確率で焚き火イベントへ
という設計にしていて、 「常に焚き火に向かってしまう」のを避けつつ、時々ふっと休憩が挟まる感じを狙っています。
5.4 AtFire / Regroup の処理フロー
startAtFire : 焚き火円陣へ遷移
焚き火スロットに全員が到達すると startAtFire に入り、ここで
- MMD モデルを歩きアニメからポーズアニメに切り替え
- 焚き火モデルを有効化して表示
- 全員を焚き火の方向へ向ける
といった処理を行います。
function startAtFire() {
partyMode = PartyMode.AtFire;
fireTimer = 0;
setPoseAnimations(); // VPD ポーズへ切り替え
if (bonfire) bonfire.setEnabled(true); // 焚き火モデルを表示
roots.forEach((root) => {
faceToFire(root); // 全員を焚き火の方向へ
});
}
向きの調整は faceToFire でやっています。
function faceToFire(root: TransformNode) {
const pos = root.position;
const dir = CAMPFIRE_POS.subtract(pos);
dir.y = 0;
if (dir.lengthSquared() < 1e-6) return;
dir.normalize();
const yaw = Math.atan2(dir.x, dir.z) + MODEL_FORWARD_OFFSET;
const dt = scene.getEngine().getDeltaTime() / 1000;
root.rotation.y += shortestAngleDelta(root.rotation.y, yaw) * Math.min(1, dt * YAW_LERP_SPEED);
}
updateAtFire 一定時間待機
AtFire 状態の updateAtFire では、特に移動はさせず
- タイマー
fireTimerを進める - 向き・地面吸着だけは毎フレーム維持
という最小限の処理にしています。
function updateAtFire(dt: number) {
fireTimer += dt;
roots.forEach((root) => {
faceToFire(root);
snapToGround(root);
});
if (fireTimer >= CAMPFIRE_WAIT_SECONDS) {
startRegroup();
}
}
CAMPFIRE_WAIT_SECONDS(今回は 8 秒)経過で、隊列再構成フェーズに進みます。
Regroup → Roam への復帰
startRegroup では、リーダーの背後に等間隔で並ぶようなスロットを計算しておきます。
function computeRegroupSlots() {
regroupSlots = [];
const { forward } = getLeaderForwardRight();
const leaderPos = leader.position.clone();
for (let i = 0; i < roots.length; i++) {
const backOffset = forward.scale(-FOLLOW_SPACING * i);
const base = leaderPos.add(backOffset);
const pos = projectOnGroundXZ(base.x, base.z);
regroupSlots.push(pos);
}
}
あとは updateRegroup で全員をそれぞれのスロットに移動させ、
全員到達したら resumeRoam で元のランダム徘徊に戻します。
function updateRegroup(dt: number) {
let allArrived = true;
for (let i = 0; i < roots.length; i++) {
const root = roots[i];
const dst = regroupSlots[i];
moveToward(root, dst, WALK_SPEED);
snapToGround(root);
const dist = Vector3.Distance(root.position, dst);
if (dist > ARRIVE_DIST) {
allArrived = false;
}
}
if (allArrived) {
resumeRoam();
}
}
resumeRoam の中では
- 履歴
historyをリセットし直す - 焚き火モデルを非表示に戻す
- 歩行アニメを再セットして
Roamに復帰
という後片付けを一括で行っています。
function resumeRoam() {
partyMode = PartyMode.Roam;
history.length = 0;
history.push({
pos: leader.position.clone(),
yaw: leader.rotation.y,
});
target = randomTarget(HALF);
roamLoopsSinceLastFire = 0;
if (bonfire) bonfire.setEnabled(false); // 焚き火モデルを消す
setWalkAnimations(); // 再び歩きアニメへ
}
5.5 MMDランタイムとの連携
最後に、この焚き火イベントを MMD アニメーション とどう繋いでいるかを整理しておきます。
歩きモーションの登録
BVMD からロードした「歩きモーション」は、applyMotionToModel で各キャラの mmdModel に登録しつつ、
あとで状態切り替えに使えるよう、名前やハンドルを __walk* として保存しています。
function applyMotionToModel(mmdModel: any, bvmd: any) {
if (typeof mmdModel.addAnimation === "function") {
mmdModel.addAnimation(bvmd);
mmdModel.setAnimation(bvmd.name);
mmdModel.__walkAnimName = bvmd.name;
} else {
const handle = mmdModel.createRuntimeAnimation(bvmd);
mmdModel.setRuntimeAnimation(handle);
mmdModel.__walkRuntimeHandle = handle;
}
}
VPD ポーズの登録
ポーズ用 VPD は、VpdLoader から読み込んで __pose* として保持します。
if (spec.poseFilePath) {
const poseAnimName = `${spec.name}_pose`;
const poseAnim = await vpdLoader.loadAsync(poseAnimName, spec.poseFilePath);
if (typeof anyModel.addAnimation === "function") {
anyModel.addAnimation(poseAnim);
anyModel.__poseAnimName = poseAnim.name;
} else if (typeof anyModel.createRuntimeAnimation === "function") {
const handle = anyModel.createRuntimeAnimation(poseAnim);
anyModel.__poseRuntimeHandle = handle;
}
}
歩き/ポーズの切り替え
焚き火の状態遷移に合わせて、setWalkAnimations / setPoseAnimations で全員のアニメーションを切り替えます。
function setWalkAnimations() {
if (!mmdRuntime) return;
for (const model of models) {
const anyModel = model as any;
if (anyModel.__walkAnimName && typeof model.setAnimation === "function") {
model.setAnimation(anyModel.__walkAnimName);
} else if (anyModel.__walkRuntimeHandle && typeof anyModel.setRuntimeAnimation === "function") {
anyModel.setRuntimeAnimation(anyModel.__walkRuntimeHandle);
}
}
if (typeof (mmdRuntime as any).seekAnimation === "function") {
(mmdRuntime as any).seekAnimation(0, false);
}
if (typeof (mmdRuntime as any).playAnimation === "function") {
(mmdRuntime as any).playAnimation();
}
}
function setPoseAnimations() {
if (!mmdRuntime) return;
for (const model of models) {
const anyModel = model as any;
if (anyModel.__poseAnimName && typeof model.setAnimation === "function") {
model.setAnimation(anyModel.__poseAnimName);
} else if (anyModel.__poseRuntimeHandle && typeof anyModel.setRuntimeAnimation === "function") {
anyModel.setRuntimeAnimation(anyModel.__poseRuntimeHandle);
}
}
if (typeof (mmdRuntime as any).seekAnimation === "function") {
(mmdRuntime as any).seekAnimation(0, false);
}
if (typeof (mmdRuntime as any).playAnimation === "function") {
(mmdRuntime as any).playAnimation();
}
}
Roam / GoToFire / Regroupでは 歩きアニメAtFireでは ポーズアニメ
という切り替えが入ることで、 「ノイズ地形を行軍しているパーティが、たまに焚き火に集まって一息つく」 という MMD × Babylon.js のイベントシーンが完成します。
6. 実行結果スクショと所感
最後に、今回の実行結果を軽く振り返っておきます。
6.1 スクリーンショット
この記事では、以下のようなスクショを載せています。
-
ランダム徘徊中の隊列
- ノイズ地形の上を、#07 と同じ要領でリーダー+フォロワーが行軍している状態
- 遠目に見ると、草原の中をパーティがふらふら散歩している感じになる
-
焚き火集合中の円陣シーン
CAMPFIRE_POS付近に全員が集まり、円周上のスロットにそれぞれ配置された状態- VPD ポーズに切り替わって焚き火の方を向き、中央に Hunyuan3D で自作した焚き火モデルが置かれている
MMD モデル自体は歩きモーションのままでも成立しますが、 ポーズ用 VPD を挟むことで「ちゃんと休んでいる感」が出て、スクショ映えがだいぶ良くなりました。
6.2 パラメータ調整メモ
実際に動かしてみて、よく触ったパラメータをいくつかメモしておきます。
-
CAMPFIRE_CIRCLE_RADIUS
- 焚き火を中心にした円陣の半径
- 小さすぎるとキャラ同士が密集しすぎて、モデル同士がめり込みやすい
- 大きくしすぎると、焚き火から離れた「囲んでいる感」の薄い配置になる
- 使用している MMD モデルのスケールや人数に合わせて、10〜20 あたりで調整するのが良さそうでした
-
焚き火モデルのスケールと位置
- 草原ノイズがそこそこ密に生えているので、最初は焚き火が「草の中の米粒」状態になりがち
campfireScale(スケーリング)とcampfireRoot.position(特に Y)をかなりシビアに調整する必要があった- 草より少しだけ高い位置に持ち上げる (
yにオフセットを足す) と、遠目でも炎のシルエットが分かりやすくなる
-
待機時間
CAMPFIRE_WAIT_SECONDS- 短すぎると「来たと思ったらすぐ散開してしまう」ので、雰囲気が出ない
- 長すぎると、徘徊 → 焚き火 → 徘徊… のサイクルが間延びして見える
- 今回は 8 秒にして、YouTube ショート向けの尺でも 1 サイクルがしっかり入るようにしている
6.3 作ってみた所感
- #07 で作った隊列ロジックをそのまま活かしつつ、「状態マシン」として整理し直すことで
- 焚き火イベントのような「一時的な振る舞い」を割と素直に差し込めた
- Hunyuan3D で自作した焚き火モデルを実際に Babylon.js 上で使ってみると
- 「画像 → 3D メッシュ → Vite プロジェクトに組み込み」という一連のパイプラインが手になじんできた感触がある
- 今回の仕組みは、焚き火以外にも
- ベンチで座る
- 拠点に帰って寝る
- ボス部屋前で集合する といったイベントに横展開しやすいので、今後の MMD × Babylon.js シリーズのベースとしても使えそうです。
次は、焚き火シーンを VR から眺めたり、座り/寝ポーズのバリエーションを増やして もう少し「キャンプ感」を出していきたいところです。
7. おまけ / 今後やりたいこと
最後に、今回の焚き火シーンをベースに「ここから先に広げたいところ」をメモしておきます。
7.1 炎アニメーションをちゃんと作りたい
今回の焚き火モデルは「静止メッシュ+マテリアル」止まりなので、炎の表現はまだ最低限です。 今後やってみたい案としては:
-
ライトのゆらぎ
- 焚き火用の PointLight / SpotLight を追加して、
- フレームごとに強度 (
intensity) や色味を少しずつランダム変動させる - 低コストで「炎っぽい陰影」が出るので、まずはここからが現実的
-
パーティクルで火の粉を飛ばす
- ParticleSystem でオレンジ〜黄色の小さなパーティクルを上方向に飛ばす
- 寿命を短くして、焚き火の上にだけひらひら舞うような見た目にする
-
フリップブック / シェーダーで燃え方を表現
- テクスチャアニメ(フリップブック)で、数枚の炎テクスチャを切り替える
- もしくはノイズを使ったシェーダーで、UV を歪ませて炎の揺らぎを出す
- ここまでやると本格的な「焚き火シェーダー」になってくるので、別エピソードで腰を据えてやりたいところ
7.2 VR モード連動の「焚き火イベント」
現状は「隊列の状態マシン」が焚き火イベントを管理していますが、 今後は VR モード側のインタラクション とも絡めてみたいです。
たとえば:
- プレイヤー(VR カメラ)が焚き火付近 (
CAMPFIRE_POS周辺) に入ったら- パーティの
PartyModeを強制的にGoToFireにする - プレイヤーが「キャンプ地に戻る」と、メンバーも帰ってきてくれるイメージ
- パーティの
- 一定時間、焚き火のそばにいたら
- パーティが自動で円陣を組み、全員ポーズ
- その状態をそのまま VR スクリーンショットや動画撮影に使う
「プレイヤーが焚き火の主で、パーティがそこに集合してくる」ような構図にできるので、 VR 映えするシーン作りにも使えそうです。
7.3 Hunyuan3D でキャンプ場セットを量産したい
焚き火が 1 つできたので、次の小物も Hunyuan3D で量産していきたいです。
- テント
- キャンプ用チェア
- 折りたたみテーブル
- マグカップやケトル
- ランタン など
これらを個別の glb として作り:
public/models/以下に配置- Babylon.js 側でそれぞれ専用の
TransformNodeを用意 - 好きなレイアウトで「キャンプ場」を組んでいく
という形にすれば、MMDパーティ+キャンプ場+ノイズ地形+VR という かなり「世界観のある一枚絵/一動画」を量産できる土台になりそうです。
今回の焚き火イベントは、
「隊列ロジックにイベントを1つ挟むと、世界が一気に“生活感”に寄る」
という感触があったので、今後も キャンプ・拠点・街・ボス前など、いろいろな「イベント地点」を増やしていきたいところです。
💬 コメント