Skip to content

hoscene アセットフォーマット v1(草案) ​

com.hoshiboshi.asset.hoscene は、Hoshiboshi ワールドの静的データ(シーングラフ、リソース、WASM モジュール)を 1 つの .hoscene パッケージにまとめた配布フォーマットです。Nocturne プロトコルの information.assetKinds に記載し、QUIC 直送または CDN 経由でクライアントに届けます。

.hoscene の配布 MIME 型は application/hoscene です。内部コンテナは ZIP 互換ですが、Nocturne や CDN のトップレベル contentType では application/zip ではなく application/hoscene を使います。

ステータス ​

本稿は草案です。フィールド名、必須/任意の区別、WASM sandbox の詳細は今後の稿で固定します。

ZIP レイアウト ​

example-world.hoscene
├── manifest.json
├── Scenes/
│   ├── main.scene.json
│   └── lobby.scene.json
├── Resources/
│   ├── textures/
│   │   ├── wall_diffuse.png
│   │   └── poster.png
│   ├── audio/
│   │   ├── bgm_ambient.opus
│   │   └── click.ogg
│   ├── models/
│   │   ├── lobby.glb
│   │   └── props/
│   │       └── chair.glb
│   └── physics/
│       └── collision_lobby.glb
├── WASM/
│   ├── door_logic.wasm
│   └── bgm_controller.wasm
└── Extensions/
    ├── godot/
    └── unity/
パス必須説明
manifest.jsonはいパッケージ全体のメタデータとリソース索引。
Scenes/はい1 つ以上のシーン定義ファイル(.scene.json)。シーンの内部構造は hoscene scene を参照。
Resources/いいえテクスチャ、音声、モデル、物理コリジョン等の静的データ。
WASM/いいえクライアントで実行する WebAssembly モジュール。
Extensions/いいえエンジン固有の補助データ。読めなくてもワールドは立ち上がること。

ZIP 内のパスは大文字小文字を区別します。パッケージ作成ツールは、ファイルシステム間の差異を避けるため、パスを記載どおりに格納する必要があります。

manifest.json のスキーマ ​

json
{
  "assetKind": "com.hoshiboshi.asset.hoscene",
  "formatVersion": "1.0.0",
  "worldId": "medi:world:ed25519:base64url-world-public-key",
  "name": "Starter Lobby",
  "summary": "A small social lobby world.",
  "entryScene": "Scenes/main.scene.json",
  "units": {
    "distance": "meter",
    "mass": "kilogram",
    "time": "second"
  },
  "physics": {
    "gravity": [0.0, -9.80665, 0.0],
    "defaultMaterial": {
      "staticFriction": 0.6,
      "dynamicFriction": 0.6,
      "restitution": 0.0
    }
  },
  "scenes": [
    {
      "id": "main",
      "path": "Scenes/main.scene.json"
    },
    {
      "id": "lobby",
      "path": "Scenes/lobby.scene.json"
    }
  ],
  "resources": [
    {
      "id": "model.lobby",
      "kind": "model",
      "storage": "embedded",
      "path": "Resources/models/lobby.glb",
      "mimeType": "model/gltf-binary",
      "digest": "sha256:1111...",
      "bytes": 8243201
    },
    {
      "id": "audio.bgm",
      "kind": "audio",
      "storage": "external",
      "path": "Resources/audio/bgm_ambient.opus",
      "mimeType": "audio/opus",
      "digest": "sha256:2222...",
      "bytes": 512004
    },
    {
      "id": "texture.poster",
      "kind": "image",
      "storage": "embedded",
      "path": "Resources/textures/poster.png",
      "mimeType": "image/png",
      "digest": "sha256:3333...",
      "bytes": 248000
    },
    {
      "id": "collision.lobby",
      "kind": "collision",
      "storage": "embedded",
      "path": "Resources/physics/collision_lobby.glb",
      "mimeType": "model/gltf-binary",
      "digest": "sha256:4444...",
      "bytes": 340122
    }
  ],
  "wasmModules": [
    {
      "path": "WASM/door_logic.wasm",
      "entry": true,
      "tickRate": 30,
      "permissions": {
        "rpc": true,
        "objectWrite": true,
        "objectRead": true,
        "log": true
      },
      "requiredCapabilities": [
        "core.object",
        "core.rpc"
      ],
      "optionalCapabilities": [
        "client.godot.xr"
      ],
      "sandbox": {
        "network": false,
        "filesystem": false,
        "worldMutation": false,
        "maxMemoryBytes": 16777216,
        "maxExecutionMillis": 5
      },
      "moduleDigest": "sha256:abcd..."
    },
    {
      "path": "WASM/bgm_controller.wasm",
      "entry": false,
      "tickRate": 10,
      "permissions": {
        "rpc": false,
        "objectWrite": false,
        "objectRead": true,
        "log": true
      },
      "requiredCapabilities": [
        "core.object",
        "core.log"
      ],
      "optionalCapabilities": [],
      "sandbox": {
        "network": false,
        "filesystem": false,
        "worldMutation": false,
        "maxMemoryBytes": 8388608,
        "maxExecutionMillis": 3
      },
      "moduleDigest": "sha256:ef01..."
    }
  ]
}

主要フィールド ​

フィールド必須説明
assetKindはい固定値 "com.hoshiboshi.asset.hoscene"。クライアントはこの値でフォーマットを識別する。
formatVersionはいこの manifest スキーマ自体のセマンティックバージョン。現在は "1.0.0"。
worldIdはいこのワールドを識別する VirMesh の world ID。クライアントは接続先の world と一致することを検証する。
nameはい表示名。
summaryいいえ短い説明文。
entrySceneはい最初にロードするシーンファイルのパス(manifest.json からの相対パス)。
unitsはい単位系宣言。
physicsいいえワールド共通の物理既定値。省略時は上記例の値を使用。
scenesはいシーン一覧。各エントリは id(シーン識別子)と path(ファイルパス)を持つ。
resourcesはいリソース索引。空配列も可。
wasmModulesいいえWASM モジュール一覧。空配列も可。

パッケージ全体の digest ​

hoscene パッケージ全体の digest は、パッケージ内部の manifest.json ではなく、Nocturne の assetHash または World manifest の assets[].hash など、外側の配布メタデータで宣言します。

manifest.json 内に ZIP 全体の digest を含めると digest 対象が自己参照になるため、manifest.json は個別リソースと WASM モジュールの digest だけを持ちます。クライアントは、まず外側の配布メタデータで ZIP 全体を検証し、その後 manifest.json の resources[].digest と wasmModules[].moduleDigest で同梱または外部リソースを検証します。

単位系(units) ​

キー値説明
distance"meter"距離の単位。
mass"kilogram"質量の単位。
time"second"時間の単位。

回転はクォータニオン [x, y, z, w] で表現し、単位系の影響を受けません。座標系は右手系・Y 軸上方向です。

リソースレジストリ(resources[]) ​

シーンはリソースをファイルパスで直接参照せず、resourceId による間接参照を使います。実ファイルの所在は manifest.json の resources[] が解決します。

フィールド必須説明
idはいパッケージ内で一意なリソース識別子。シーンの各コンポーネントがこの ID で参照する。
kindはいリソース種別。"model" | "audio" | "image" | "collision"。
storageはい"embedded"(ZIP 内に同梱)または "external"(CDN 等の外部から取得)。
pathはいembedded の場合は ZIP 内パス。external の場合は package base URI からの相対パス。
mimeTypeはいMIME 型。
digestはいリソースファイル単体の SHA-256 ハッシュ("sha256:base64url")。
bytesはいファイルサイズ(バイト)。

storage: "external" のリソースは ZIP に実体を含みません。クライアントは hoscene パッケージを取得した URL、または Nocturne / World manifest が提供する asset URL の親ディレクトリを package base URI とし、path を相対解決して外部リソースを取得します。取得後は digest で個別に検証します。

QUIC 直送など package base URI が存在しない取得経路では、external resource を含む package はロード不可とするか、外側の asset metadata で package base URI を明示する必要があります。

推奨リソースフォーマット ​

kind推奨
modelGLB(glTF Binary)
audioOpus(.opus)または OGG Vorbis(.ogg)
imagePNG(.png)または JPEG(.jpg)
collision簡略化 GLB(レンダリング用とは別の低ポリゴンメッシュ)

WASM モジュール(wasmModules[]) ​

各 WASM モジュールの設定:

フィールド必須説明
pathはいWASM ファイルの ZIP 内パス。
entryはいtrue の場合、シーンロード時に自動起動する。false の場合は他の WASM またはスクリプトから明示的に起動される。
tickRateはい1 秒あたりの呼び出し回数(Hz)。30 なら 33ms 間隔。
permissionsはいこのモジュールに許可する操作。rpc(RPC 発行)、objectWrite(オブジェクト変更)、objectRead(オブジェクト参照)、log(ログ出力)。
requiredCapabilitiesはいこのモジュールの動作に必須の host capability 一覧。不足時は起動を拒否する。
optionalCapabilitiesいいえあれば使うが、なくても動作する capability。
sandboxはいWASM 実行制約。
moduleDigestはいWASM バイナリの SHA-256 ハッシュ。

Sandbox 制約 ​

フィールド説明
networkfalse 固定。WASM は直接ネットワークアクセス不可。RPC は host bridge 経由。
filesystemfalse 固定。WASM はローカルファイルシステムにアクセス不可。
worldMutationfalse 固定。WASM はワールドの永続状態を直接変更不可。変更は host API 経由。
maxMemoryBytesWASM インスタンスあたりの最大メモリ(バイト)。
maxExecutionMillis1 tick あたりの最大実行時間(ミリ秒)。超過時はモジュールを停止する。

WASM モジュールの実行モデルと host API の詳細は .mw WASM host API v1 を参照してください。

.mw との関係 ​

hoscene は .mw フォーマット(medi.world.mw.v1)から以下の設計を継承しています:

  • ZIP コンテナ、エンジン中立な scene graph JSON
  • resourceId による間接リソース参照
  • storage: embedded | external の 2 モード
  • digest ベースの検証(sha256:base64url)
  • WASM を host 管理の安全な非同期ランタイムとして扱う方針

hoscene が .mw から意図的に変更した点:

項目.mwhoscene
WASM の位置extensions/wasm/(拡張扱い)WASM/(トップレベル)
WASM 設定別ファイル runtime.jsonmanifest.json に統合
フォーマット識別WorldServer 側 worldFormatIdパッケージ内 assetKind + Nocturne protocol information.assetKinds
コンポーネント命名component+me.virmesh.* 予定component+com.hoshiboshi.*

セキュリティ ​

  • クライアントは manifest.json の worldId が接続先ワールドと一致することを確認する。
  • Nocturne の assetHash または World manifest の assets[].hash が存在する場合、受信した ZIP 全体の SHA-256 が一致することを検証する。
  • 各リソースの digest が実ファイルと一致することを検証する。不一致のリソースはロードしない。
  • WASM モジュールの moduleDigest を検証し、不一致の場合は起動しない。
  • Extensions/ 内のファイルは、読めなくてもワールドが最低限動作する前提で扱う。extension の読み込み失敗でワールド全体を拒否しない。
  • ZIP 展開時に path traversal(../、絶対パス、ドライブレター)を拒否する。
  • パッケージのファイル数・合計サイズ・ZIP 展開後サイズに上限を設ける。

参考 ​