Skip to content

hoscene シーン定義 v1(草案)

この文書は、com.hoshiboshi.asset.hoscene パッケージの Scenes/*.scene.json ファイルの内部構造と全コンポーネントの仕様を定義します。

シーンファイル全体構造

json
{
  "sceneId": "main",
  "displayName": "Main Lobby",
  "physicsOverrides": {
    "gravity": [0.0, -9.80665, 0.0]
  },
  "nodes": [
    {
      "id": "root",
      "name": "Root",
      "parentId": null,
      "components": []
    }
  ]
}
フィールド必須説明
sceneIdはいシーン識別子。manifest.jsonscenes[].id と一致する。
displayNameいいえエディタ・デバッグ用の表示名。
physicsOverridesいいえこのシーン固有の物理パラメータ。manifest.jsonphysics を上書きする。
nodesはいノード配列。1 つ以上必須。parentId: null のルートノードを必ず 1 つ含む。

ノード

各ノードはシーングラフの 1 要素です。親子関係は parentId で表現します。

json
{
  "id": "floor_01",
  "name": "LobbyFloor",
  "parentId": "root",
  "components": []
}
フィールド必須説明
idはいシーン内で一意なノード識別子。
nameいいえエディタ・デバッグ用の表示名。
parentIdroot 以外は必須親ノードの id。ルートノードのみ null
componentsいいえコンポーネント配列。空配列可。

ノードの挙動

  • 各シーンにルートノード(parentId: null)は 1 つだけ。
  • 子ノードの Transform は親からの相対変換。ワールド行列の合成はホストエンジンが担当。
  • 親が削除または非表示になると、すべての子孫に波及する。
  • 親子関係に循環がないことをパース時に検証する。
  • 未知のコンポーネントは無視し、ワーニングを出力する。既知のコンポーネント内の未知フィールドも無視する。
  • ルートノードが存在しないシーンは無効とし、ロードを拒否する。

座標系・単位系

項目
座標系右手系
上方向Y+
前方向Z-
距離meter
質量kilogram
時間second
回転クォータニオン [x, y, z, w]
スケール各軸独立

Transform の適用順: scale → rotation → translation。


コンポーネント一覧

全コンポーネントは component+com.hoshiboshi.* 名前空間に属します。type フィールドで種別を宣言します。

シーン内の type は常に具体的なコンポーネント ID にします。component+dev.yoking.specialComponents.* のような末尾 wildcard は、Nocturne の componentKinds や client runtime capability など、対応範囲を宣言するメタデータでのみ使用できます。実際のシーンファイルに wildcard type を書いてはいけません。

1. Transform — component+com.hoshiboshi.transform

ノードの位置・回転・スケールを定義します。

json
{
  "type": "component+com.hoshiboshi.transform",
  "position": [0.0, 1.2, 2.0],
  "rotation": [0.0, 0.0, 0.0, 1.0],
  "scale": [1.0, 1.0, 1.0]
}
フィールド既定値説明
positionfloat[3][0, 0, 0]位置(m)。
rotationfloat[4][0, 0, 0, 1]クォータニオン(x, y, z, w)。
scalefloat[3][1, 1, 1]各軸スケール。

挙動:

  • クォータニオンはロード時に正規化する。単位長でない場合はワーニング。
  • スケール 0 は有効(隠蔽用途)。負スケールはミラーリングに使用可能。
  • 親ノードの Transform と合成したワールド行列はエンジン側で計算する。

2. StaticMesh — component+com.hoshiboshi.staticMesh

静的メッシュを表示します。頂点変形を伴わないモデルに使用します。

json
{
  "type": "component+com.hoshiboshi.staticMesh",
  "resourceId": "model.lobby",
  "visible": true,
  "castShadows": true,
  "receiveShadows": true
}
フィールド既定値説明
resourceIdstring必須manifest.jsonresources[] 内の id。kind は "model"
visiblebooltrue初期表示状態。
castShadowsbooltrue影を落とすか。
receiveShadowsbooltrue影を受けるか。

挙動:

  • resourceId から GLB メッシュをロード。解決失敗時は非表示(エラーログのみ。クラッシュしない)。
  • 「静的」とは頂点変形なしの意味。Transform による移動・回転・拡縮は可能。
  • 複数ノードが同一 resourceId を参照可能(インスタンシング最適化の余地)。
  • visible はランタイムにスクリプトから切り替え可能。castShadowsreceiveShadows も動的変更可。

3. SkinnedMesh — component+com.hoshiboshi.skinnedMesh

ボーン変形を伴うメッシュを表示します。アバターやアニメーション付きオブジェクトに使用します。

json
{
  "type": "component+com.hoshiboshi.skinnedMesh",
  "resourceId": "model.avatar",
  "skeletonRoot": "avatar_root",
  "visible": true,
  "castShadows": true
}
フィールド既定値説明
resourceIdstring必須スキンメッシュ GLB の resources[]id
skeletonRootstring自身の idスケルトン階層のルートノード id
visiblebooltrue
castShadowsbooltrue

挙動:

  • スケルトン階層はシーングラフのノードとして定義する(GLB 内に埋め込まない)。
  • skeletonRoot 省略時は自身のノードをスケルトンルートとみなす。
  • ボーンの Transform がスキニングに反映される。ボーン位置はスクリプトまたはサーバー RPC で操作可能。
  • スキンデータがない GLB を指定した場合、StaticMesh 相当でレンダリングしワーニングを出力。

4. Collider — component+com.hoshiboshi.collider

物理衝突の形状を定義します。単体では静的コライダー(動かない)として機能し、RigidBody と併用すると動的物理の形状を提供します。

json
{
  "type": "component+com.hoshiboshi.collider",
  "shape": {
    "type": "box",
    "halfExtents": [0.5, 0.5, 0.5]
  },
  "isTrigger": false,
  "layer": 0,
  "material": {
    "staticFriction": 0.6,
    "dynamicFriction": 0.6,
    "restitution": 0.0
  }
}

Shape バリエーション

Box:

json
{ "type": "box", "halfExtents": [0.5, 1.0, 0.5] }

Sphere:

json
{ "type": "sphere", "radius": 1.0 }

Capsule:

json
{ "type": "capsule", "radius": 0.5, "height": 1.8 }

Mesh:

json
{ "type": "mesh", "resourceId": "collision.lobby" }
フィールド既定値説明
shape.typestring必須"box" | "sphere" | "capsule" | "mesh"
shape.halfExtentsfloat[3]box 時必須各軸の半分のサイズ(m)。
shape.radiusfloatsphere/capsule 時必須半径(m)。
shape.heightfloatcapsule 時必須中心の円柱部の高さ(m)。カプセル全体の高さは height + 2 × radius
shape.resourceIdstringmesh 時必須コリジョン用 GLB の resources[]id。kind は "collision"
isTriggerboolfalsetrue で物理応答なし、進入/退出イベントのみ発火。
layerint0衝突レイヤーマスク。
material.staticFrictionfloat0.6静止摩擦係数。
material.dynamicFrictionfloat0.6動摩擦係数。
material.restitutionfloat0.0反発係数(0 = 跳ねない、1 = 完全弾性)。

挙動:

  • isTrigger: true: 物理衝突なし。進入/退出イベントのみ。検出ゾーン・ゲート通過判定用。
  • isTrigger: false: 物理衝突 + イベント両方。
  • 同一ノードに複数の Collider を配置して複合形状にできる。
  • Mesh Collider はレンダリングメッシュと別物。必ず簡略化コリジョン用 GLB を推奨。
  • Box: ノード原点中心に halfExtents 分拡張。
  • Sphere: ノード原点中心。
  • Capsule: ノード原点中心、Y 軸方向にカプセルを配置。
  • RigidBody がないノードの Collider は静的(不動物)。動的な力を受けない。
  • layer で衝突相手をフィルタリングする。

5. RigidBody — component+com.hoshiboshi.rigidBody

ノードに物理シミュレーション対象としての挙動を与えます。同じノードに Collider が必要です。

json
{
  "type": "component+com.hoshiboshi.rigidBody",
  "mass": 1.0,
  "isKinematic": false,
  "freezePosition": { "x": false, "y": false, "z": false },
  "freezeRotation": { "x": false, "y": false, "z": false },
  "linearDamping": 0.0,
  "angularDamping": 0.05,
  "useGravity": true
}
フィールド既定値説明
massfloat1.0質量(kg)。0 で無限質量(static 相当)。
isKinematicboolfalsetrue で物理シミュレーション対象外。スクリプト変更のみで動く。
freezePositionfalse位置軸のロック。
freezeRotationfalse回転軸のロック。
linearDampingfloat0.0速度減衰(空気抵抗)。
angularDampingfloat0.05回転減衰。
useGravitybooltrueワールド重力の影響を受けるか。

挙動:

  • 同じノードに Collider がない場合はワーニング(衝突判定なしで落下し続ける)。
  • isKinematic: true: 重力・衝突力を無視。動く床・エレベーター・ドア用。Transform をスクリプトで操作。
  • isKinematic: false: 完全物理シミュレーション。重力・力積・衝突を計算。
  • freezePosition/Rotation: 該当軸は物理で変化せず、スクリプト変更のみ可。
  • 質量比で衝突応答: 重い物体ほど軽い物体を押す。
  • 静止時のスリープ最適化はエンジン任せ。

6. Light — component+com.hoshiboshi.light

シーンに光源を配置します。

json
{
  "type": "component+com.hoshiboshi.light",
  "lightType": "point",
  "color": [1.0, 0.9, 0.8],
  "intensity": 1.5,
  "range": 10.0,
  "spotAngle": 30.0,
  "innerSpotAngle": 25.0,
  "castShadows": false
}
フィールド既定値説明
lightTypestring必須"directional" | "point" | "spot"
colorfloat[3][1, 1, 1]RGB(HDR 可、1.0 超も許容)。
intensityfloat1.0光度。
rangefloatpoint: 10, spot: 20減衰距離(m)。Directional では無視。
spotAnglefloat30.0スポットライト外側コーン角(度)。Point/Directional では無視。
innerSpotAnglefloatspotAngle と同じスポットライト内側フル照度角。外側との間で減衰。
castShadowsboolfalse影を落とすか(高コスト)。

挙動:

  • Directional: 位置無視・回転のみ有効。無限遠光源(太陽/月)。シーン全体に影響。
  • Point: 位置から全方向に放射。逆二乗距離減衰。range で完全に消灯。
  • Spot: 位置からコーン状に放射。方向はノードの前方(Z-)。
  • 最終色 = color × intensity
  • 影を落とす Directional Light はシーン内 1 つ推奨。クライアントハードウェアで上限あり。

7. AudioSource — component+com.hoshiboshi.audioSource

音声を再生します。3D 空間オーディオまたは 2D グローバルオーディオとして使用します。

json
{
  "type": "component+com.hoshiboshi.audioSource",
  "resourceId": "audio.bgm",
  "loop": true,
  "autoplay": true,
  "spatial": false,
  "volume": 0.7,
  "pitch": 1.0,
  "minDistance": 1.0,
  "maxDistance": 50.0,
  "rolloffMode": "inverse",
  "priority": 128
}
フィールド既定値説明
resourceIdstring必須kind が "audio"resources[]id
loopboolfalseループ再生。
autoplayboolfalseシーンロード時に自動再生。
spatialbooltruetrue で 3D 空間オーディオ。false で 2D グローバル。
volumefloat1.00.0–1.0。
pitchfloat1.0再生速度兼ピッチ。
minDistancefloat1.0最大音量の距離(m)。
maxDistancefloat50.0音量 0 になる距離(m)。
rolloffModestring"inverse""inverse" | "linear" | "logarithmic"
priorityint128小さいほど優先度高。同時発音数超過時に低優先度から停止。

挙動:

  • spatial: true: 3D オーディオ。リスナーからの距離・角度で音量・パンを変化。
  • spatial: false: 2D グローバル。BGM・UI 音用。位置無視。
  • 距離減衰: < minDistance = 100% 音量。min–max 間 = rolloffMode に従う。> maxDistance = 無音。
  • 同一ノードに複数 AudioSource を配置してレイヤー再生可能。

8. SpawnPoint — component+com.hoshiboshi.spawnPoint

プレイヤーの出現地点を定義します。視覚表現を持たないメタデータコンポーネントです。

json
{
  "type": "component+com.hoshiboshi.spawnPoint",
  "spawnTag": "default",
  "spawnRadius": 0.5,
  "spawnRotation": [0.0, 0.0, 0.0, 1.0],
  "priority": 0,
  "playerLimit": 0
}
フィールド既定値説明
spawnTagstring"default"スポーン地点カテゴリ("teamA", "respawn" など)。
spawnRadiusfloat0.0XZ 平面ランダムオフセット半径(m)。プレイヤー重なり防止。
spawnRotationfloat[4]Transform の値上書き回転。省略時はノードの Transform.rotation を使用。
priorityint0大きいほど優先。
playerLimitint0この地点の最大利用人数。0 = 無制限。

挙動:

  • サーバーがプレイヤー配置時に参照する。視覚表現なし。
  • spawnTag 一致する中から priority 降順で選択。同 priority はラウンドロビン。
  • playerLimit 到達済みの地点は選択対象外。
  • spawnRadius > 0: XZ 平面でランダム位置加算。
  • spawnRotation 指定時はそちらを優先。

9. Visibility — component+com.hoshiboshi.visibility

ノードとその子孫のレンダリング設定を制御します。物理やコリジョンには影響しません。

json
{
  "type": "component+com.hoshiboshi.visibility",
  "visible": true,
  "castShadows": true,
  "receiveShadows": true,
  "renderLayer": 0
}
フィールド既定値説明
visiblebooltrueノードの表示状態。
castShadowsbooltrue影を落とすか。
receiveShadowsbooltrue影を受けるか。
renderLayerint0レンダリングレイヤーマスク。

挙動:

  • visible: false → 全子孫も非表示。parent.visible: false, child.visible: true でも child は非表示(親優先)。
  • 非表示でも Collider・RigidBody は機能し続ける。レンダリングのみの制御。
  • renderLayer: カメラごとに描画レイヤーをフィルタ(防犯カメラ視点 / 通常視点の分離など)。
  • StaticMesh/SkinnedMesh 側の同名フィールドと併用時は AND 条件。

10. Script — component+com.hoshiboshi.script

ノードに WASM モジュールを紐付けます。このコンポーネント自体はロジックを持たず、どの WASM がどのノードを担当するかのマーカーです。

json
{
  "type": "component+com.hoshiboshi.script",
  "modulePath": "WASM/door_logic.wasm",
  "interactable": true,
  "parameters": {
    "openSpeed": 2.0,
    "requireKey": false
  }
}
フィールド既定値説明
modulePathstring必須対応する WASM ファイルのパス。manifest.jsonwasmModules[].path と一致する。
interactableboolfalseプレイヤーのインタラクト対象か。
parametersobject{}WASM に渡す読み取り専用の初期パラメータ。

挙動:

  • 同じ modulePath を持つ全ノードが 1 つの WASM インスタンスに束ねられる。
  • WASM は runtime.init で担当オブジェクトの handle 一覧とパラメータを受け取る。
  • interactable: true のノードにプレイヤーが近づいてインタラクト操作をすると、host から WASM に rpc.event.object_interacted が配送される。
  • パラメータはランタイムに読み取り専用で渡され、変更不可。
  • WASM がクラッシュしても、host は他の WASM インスタンスやワールド全体に影響させない。
  • WASM からは host API(core.object.*, core.rpc.*, core.log.*)経由でオブジェクト操作・RPC・ログ出力を行う。

WASM の実行モデル:

text
WorldServer / InstanceServer
          ^
          | RPC (host bridge)
          v
Client host runtime  <---->  Engine main thread (render, physics)
          ^
          | message bridge
          v
Dedicated WASM worker (per modulePath)
  • WASM は main thread ではなく専用 worker thread で実行される。
  • WASM から engine object への直接アクセスは不可。すべて host API 経由。
  • ネットワークアクセスは host bridge の RPC のみ。raw socket 不可。
  • 1 tick あたりの実行時間上限を超過したモジュールは停止される。

11. Camera — component+com.hoshiboshi.camera

クライアントの初期視点を定義します。SpawnPoint が「どこに出るか」なら、Camera は「そこからどう見るか」です。

json
{
  "type": "component+com.hoshiboshi.camera",
  "fov": 90.0,
  "nearPlane": 0.1,
  "farPlane": 1000.0,
  "mode": "firstPerson",
  "priority": 0
}
フィールド既定値説明
fovfloat90.0垂直視野角(度)。
nearPlanefloat0.1近クリップ面(m)。
farPlanefloat1000.0遠クリップ面(m)。
modestring"firstPerson""firstPerson" | "thirdPerson"
priorityint0複数 Camera がある場合の優先度。大きいほど優先。

挙動:

  • シーン内に Camera がない場合、クライアントは自身の既定カメラ設定を使用する。
  • 複数の Camera がある場合、priority が最大のものを選択する。同 priority の場合は最初に見つかったものを使用。
  • mode: "firstPerson": カメラはプレイヤーの頭部位置から前方を向く。
  • mode: "thirdPerson": カメラはプレイヤーの後方上空からプレイヤーを追跡する。オフセット距離は実装依存。
  • fovnearPlanefarPlane はクライアントの設定やユーザー選択で上書き可能。シーン定義はあくまで初期値。
  • Camera の Transform はカメラ自体の位置ではなく、注視点の基準位置として扱う。実際のカメラ位置は mode に従ってオフセットが計算される。

共通ルール

未知のコンポーネント・フィールド

  • 未知の type を持つコンポーネントは無視し、ワーニングを出力する。シーンロードは継続する。
  • 既知のコンポーネント内の未知フィールドも無視する。前方互換性のため。
  • 未知の shape.type(Collider)、未知の lightType(Light)、未知の mode(Camera)は、そのコンポーネントを無効化する。

リソース解決失敗

コンポーネントが参照する resourceIdmanifest.jsonresources[] に存在しない、またはリソースの digest 検証に失敗した場合:

  • StaticMesh: 非表示(エラーログのみ)。
  • SkinnedMesh: 非表示(エラーログのみ)。
  • Collider (mesh): その Collider を無効化。他の Collider は維持。
  • AudioSource: 無音(エラーログのみ)。
  • Script: modulePathwasmModules[] に存在しない場合、その Script コンポーネントを無効化(該当ノードは WASM に割り当てられない)。

いずれもクラッシュしない。 ワールドは可能な限りレンダリングを継続する。

コンポーネントの重複

  • 1 ノードに Transform は 1 つだけ。重複時は最初の 1 つを使用し、残りは無視。
  • StaticMesh と SkinnedMesh は同じノードに共存不可。両方ある場合は SkinnedMesh を優先。
  • Collider は同一ノードに複数可(複合形状)。
  • RigidBody は 1 ノードに 1 つ。重複時は最初の 1 つを使用。
  • Script は同一ノードに複数可(複数 WASM が同じオブジェクトを担当できる)。

拡張コンポーネント

component+com.hoshiboshi.* 以外の名前空間(component+com.example.* など)のコンポーネントは拡張として扱います。未知の拡張コンポーネントは無視し、ワーニングを出力します。拡張コンポーネントがないとワールドが成立しない設計は避けてください。

拡張コンポーネントの対応宣言では、名前空間末尾の .* を wildcard として使えます。たとえば component+dev.yoking.specialComponents.* は、component+dev.yoking.specialComponents. で始まるすべての具体コンポーネント ID に対応していることを示します。wildcard は末尾の .* のみ有効で、シーン内の components[].type には使いません。

シーン完全な例

json
{
  "sceneId": "main",
  "displayName": "Main Lobby",
  "physicsOverrides": {
    "gravity": [0.0, -9.80665, 0.0]
  },
  "nodes": [
    {
      "id": "root",
      "name": "Root",
      "parentId": null,
      "components": []
    },
    {
      "id": "lobby_floor",
      "name": "LobbyFloor",
      "parentId": "root",
      "components": [
        {
          "type": "component+com.hoshiboshi.transform",
          "position": [0.0, 0.0, 0.0],
          "rotation": [0.0, 0.0, 0.0, 1.0],
          "scale": [1.0, 1.0, 1.0]
        },
        {
          "type": "component+com.hoshiboshi.staticMesh",
          "resourceId": "model.lobby"
        },
        {
          "type": "component+com.hoshiboshi.collider",
          "shape": { "type": "mesh", "resourceId": "collision.lobby" }
        },
        {
          "type": "component+com.hoshiboshi.visibility",
          "visible": true,
          "castShadows": true,
          "receiveShadows": true
        }
      ]
    },
    {
      "id": "spawn_default",
      "name": "DefaultSpawn",
      "parentId": "root",
      "components": [
        {
          "type": "component+com.hoshiboshi.transform",
          "position": [0.0, 1.2, 2.0],
          "rotation": [0.0, 0.0, 0.0, 1.0],
          "scale": [1.0, 1.0, 1.0]
        },
        { "type": "component+com.hoshiboshi.spawnPoint", "spawnTag": "default" },
        {
          "type": "component+com.hoshiboshi.camera",
          "fov": 90.0,
          "mode": "firstPerson",
          "priority": 0
        }
      ]
    },
    {
      "id": "bgm_source",
      "name": "BgmSource",
      "parentId": "root",
      "components": [
        {
          "type": "component+com.hoshiboshi.transform",
          "position": [0.0, 2.0, 0.0],
          "rotation": [0.0, 0.0, 0.0, 1.0],
          "scale": [1.0, 1.0, 1.0]
        },
        {
          "type": "component+com.hoshiboshi.audioSource",
          "resourceId": "audio.bgm",
          "loop": true,
          "autoplay": true,
          "spatial": false,
          "volume": 0.7
        },
        {
          "type": "component+com.hoshiboshi.script",
          "modulePath": "WASM/bgm_controller.wasm",
          "interactable": false
        }
      ]
    },
    {
      "id": "door_main",
      "name": "MainDoor",
      "parentId": "root",
      "components": [
        {
          "type": "component+com.hoshiboshi.transform",
          "position": [3.0, 0.0, -1.0],
          "rotation": [0.0, 0.0, 0.0, 1.0],
          "scale": [1.0, 1.0, 1.0]
        },
        {
          "type": "component+com.hoshiboshi.staticMesh",
          "resourceId": "model.door"
        },
        {
          "type": "component+com.hoshiboshi.collider",
          "shape": { "type": "box", "halfExtents": [1.0, 1.5, 0.2] }
        },
        {
          "type": "component+com.hoshiboshi.rigidBody",
          "mass": 10.0,
          "isKinematic": true
        },
        {
          "type": "component+com.hoshiboshi.script",
          "modulePath": "WASM/door_logic.wasm",
          "interactable": true,
          "parameters": { "openSpeed": 2.0 }
        }
      ]
    }
  ]
}

参考