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.jsonresources[].digestwasmModules[].moduleDigest で同梱または外部リソースを検証します。

単位系(units

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

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

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

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

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

参考