Skip to content

ガウシアンスプラッティング (.ply .spz .sog .splat .ksplat) の表示に対応した - #18

Merged
malken21 merged 19 commits into
mainfrom
splat
Oct 20, 2026
Merged

malken21 merged 19 commits into
mainfrom
splat

Conversation

@malken21

@malken21 malken21 commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

概要

ガウシアンスプラッティング (3DGS) のファイルを、3D モデルと同じように看板に表示できるようにしました。

対応する形式:

  • .ply (3DGS の標準形式 (バイナリ / ASCII) と、PlayCanvas / SuperSplat の圧縮 PLY)
  • .spz (バージョン 2 / 3 / 4。4 は ZSTD のストリーム形式で、既存の ZstdDecoder を使う)
  • .sog (PlayCanvas SOG バージョン 1 / 2。WebP は既存の WebpDecoder を使う)
  • .splat
  • .ksplat (圧縮レベル 0 / 1 / 2)

デコーダはすべて JDK だけで書いたもので、外部のコードは取り込んでいません。

仕組み

  • core/splat: 形式ごとの読み込み (PlyReader SpzReader SogReader KsplatReader、.splat は SplatLoader) と、描画用の SplatScene
  • 1 つ 1 つの分布を、3 次元の共分散を視線に垂直な面に射影した楕円として、カメラの方を向いた四角形で描きます。
    カメラから遠い順に並べ、球面調和関数 (3 次まで) で見る向きによる色も計算します。
  • 並べ替えと頂点の生成は別のスレッドで行い、2 つの Batch を交互に使って、描画中の配列には書き込みません。
  • そのために SignImage.batches() にカメラの位置 (ブロックの角が原点) を渡す引数を足し、各バージョンの描画処理 (tools/gen_adapters.py の生成元を含む) から渡しています。
  • 読み込み: 分布が 15 万個を超えるファイルは、目立つもの (不透明度 × 表面積の平方根) から 15 万個を残します。
    撮影したシーンの空のドームなどで被写体が隠れないよう、中心 (各軸の中央値) からの距離が、距離の中央値の 4 倍を超える分布は省きます。
    省メモリのため、1 回目の走査で目立つ度合いと位置だけを集め、選んだものだけを展開します (300 万個・675 MB の PLY を 2 GB のヒープで読めます)。
  • 座標: .spz は Niantic の仕様どおり +Y が上 (RUB) として、ほかの形式は +Y が下 (COLMAP の RDF) として扱います。
    splat-transform は SPZ を PLY と同じ空間として扱っていますが、Niantic のサンプル (hornedlizard.spz racoonfamily.spz) は仕様どおりに読むと正しい向きになることを確認しました。
  • .splat と .ksplat は中身に目印がないため、拡張子で判別します。短縮 URL でも判別できるよう、Http.download がリダイレクトを自分でたどり、最後の URL か Content-Disposition の名前を渡すようにしました。
  • ゲーム内のウィンドウ: ファイルの選択肢に 5 形式を追加。プレビューの説明に新しいキー ui.preview.splat を使い、116 個すべての言語ファイルに訳を足しました (英語と日本語以外はネイティブの確認なし)。
  • 上限: ファイルは 200 MB まで (動画のアップロードと同じ)。ほかの形式の上限は変えていません。
  • 1.7 - 1.13 の描画経路では、色付きの面に裏面カリングをかけていました。スプラットの四角形はカメラの方を向けて作るので不要で、
    1.7.10 のインベントリでは全部が裏向きとして消えていたため、Batch.doubleSided を足してカリングしないようにしました。

確認したこと

  • 読み込みの数値の比較 (./gradlew :core:splatCheck -PsplatArgs=<データ>): 同じシーンを各形式に変換し、分布ごとに位置・大きさ・回転・不透明度・色・SH を比較。
    PlayCanvas の @playcanvas/splat-transform 3.10.1 に各ファイルを PLY に戻させたものとの差は、すべての形式で量子化と float の丸めの範囲
    (位置の最大誤差 2.1e-4 (SOG)、SH の誤差 0、.splat の回転だけ量子化の式の違いで平均 4.6e-3) でした。
    SPZ v4 は、Niantic のサンプル (78 万個) を splat-transform で v4 にしたものでも一致しました。
    壊れたファイル (途中で切れたもの、gzip / zip / ZSTD の爆弾、NaN、範囲外の目次など) は IOException になることも確かめています。
  • オフラインの描画 (./gradlew :core:splatPreview): 物体のスキャン (nike.splat plush.splat) と、撮影したシーン (hornedlizard.spz racoonfamily.spz train.splat) を正面・斜め・横から描いて目で確認しました。
  • 15 万個のときの並べ替えと頂点生成: 平均 30 ms (別スレッドで実行)
  • 実機 (python tools/verify/matrix.py --media splat、バニラのサーバーに接続): Forge 1.7.10 / Forge 1.12.2 / Fabric 1.20.1 / Fabric 26.3 で、看板と看板アイテムの表示を確認し、ログに MarumaSign 由来の警告・エラーなし。
    1.7.10 と 1.12.2 は、カリングの修正のあとに画像・3D モデルも確認し直しました。
  • python tools/check_linkage.py: 112 の組み合わせで問題なし
  • ./gradlew :dist:assemble: コンパイラの警告は main にもある 2 件の非推奨 API の注記 (変更していない SignItems.java) だけで、この変更で増えたものはありません。

確認していないこと・制限

  • 実機で確認したのは上の 4 つの組み合わせだけです (ほかのバージョン・ローダー、影 Mod との組み合わせは未確認)。docs/verification.md はまだ更新していません。
  • Minecraft の描画では不透明度 0.1 未満の部分が捨てられるため、分布の薄い縁が描かれず、ほかのビューアより輪郭がはっきりします。
  • 同じ文字列の看板が複数あると、同じ描画状態を交互に別の視点から使うので、並べ替えのやり直しが続きます (CPU の負荷が上がります)。
  • 背景の切り落としは中心からの距離だけで判断するので、被写体が端にあるシーンでは被写体の一部が省かれることがあります。

統合したプルリクエスト

次の 2 つをこのブランチに取り込みました (それぞれのプルリクエストは閉じています)。

Closes #19
Closes #20

ガウシアンスプラッティング (3DGS) を看板に描画するためのパイプラインと各世代 adapter へのカメラ座標伝達を実装した。

- SplatScene: 1〜99パーセンタイル座標による bounds 計算、3D 共分散の看板座標変換、視線方向平面への 2D 射影と固有値分解による楕円ビルボード生成、16bit 深度ソート、球面調和関数 (SH) 評価、非同期スレッド計算
- SignImage: 看板描画時にブロック原点基準のカメラ座標を伝達、看板アイテム時は固定視点計算
- 各 adapter / tools/gen_adapters.py: SignRendererBase / SignDraw にカメラ座標引数を伝達
- ImageLoader / Http / SignManager / TextureEntry: 200MB 上限、多段階リダイレクト追跡、ガウステクスチャ連携
- UI / MainPage / FileBrowserPage: .ply / .spz / .sog / .splat / .ksplat 拡張子追加、Catbox 拡張子保持
- SplatPreview / SplatBenchmark / tools: プレビュー出力、性能測定、EWA 比較検証、テスト用スプラット作成スクリプト
- README.md: ガウシアンスプラッティングの対応形式・仕様・制限事項を追記
# Conflicts:
#	core/build.gradle
- SpzReader: 先頭 32 バイト平文ヘッダ、TOC、各ストリームの ZSTD 展開に対応
- SpzReader: 未知の拡張ブロックのスキップ処理および ZSTD 爆弾防止の展開サイズ上限検証を追加
- SpzReader: SH 次数 4 が入力された場合の 3 次への縮退処理を実装
- ZstdDecoder: decompress メソッドを public static に公開し、外部モジュールから再利用可能に変更
- SplatCheck: RFC 8878 Zstandard Raw Block を用いた SPZ v4 単体テスト・異常系・実データ・相互精度比較テストを追加
カメラの方を向けて作る四角形は裏面を省く必要がないので、Batch に doubleSided を足して、古い描画経路で裏面カリングをしないようにした (1.7.10 の GUI では面が裏向きとして扱われ、すべて消えていた)。README の説明を利用者向けに書き直した。
- 1.8 - 1.12.2: カメラの位置に足元の高さを使っていたため、ガウス分布が縦につぶれて縞模様になっていた。変換行列からカメラの位置を求めるようにした
- 1.15 以降: ゲーム側が四角形を中心との距離で並べ替えると、奥行きの記録と合わずにすき間や網目ができていた。四角形をそれぞれの中心からカメラへ向け、距離で並べ替えるようにした
- 確認ツール: 絵にすき間がないかも調べるようにした。4 つ目のファイルを確かめる場所が古いバージョンで地形の届かない遠さになっていたので、四角形を描くように移動させるようにした。報告書にガウシアンスプラットを加えた
音量は、その tick に描画された看板のうち最も近いものまでの距離で決めている。
看板に背を向けると、看板は視界の外になって描画されなくなり、距離が分からない tick ができる。
そのときに距離を 0 (目の前) と見なしていたため、再生が止まるまでの約 3 秒間、最大の音量になっていた。

- 描画されなかった tick は、最後に分かった距離のまま音量を決める
- 動作確認 (SelfTest): 動画・音声の看板では、正面を向いたときと、真上を向いて看板が視界から外れたときの
  音の大きさを比べ、外れたときの方が大きければ失敗にする
- 1.7.10: GUI の看板アイテムで、面の表の向きを射影行列の上下反転を数えずに決めていたため、3D モデルの正面が省かれて裏の面が見えていた。射影行列と変換行列の両方から表の向きを決めるようにした
- 確認ツール: 看板アイテムを画像・3D モデル・ガウシアンスプラット・動画の 4 種類渡し、ホットバーの枠ごとに判定するようにした (これまでは画像しか確かめていなかった)
- 確認ツール: 撮影の前に Minecraft が一番手前にあるかを確かめ、他の画面が写り込まないようにした。クライアントとサーバーの優先度を上げ、同じ PC のビルドで起動が時間切れにならないようにした
- 確認ツール: すき間の判定で、画面下の体力の表示を絵の範囲に含めないようにした。やり直す前に、失敗した回の画面とログを残すようにした
- 読み込みと描画は Apache PDFBox 2.0 (Java 6 以降で動く)。配布する jar に同梱し、他の Mod とぶつからないよう
  dist の shadeCore でパッケージ名を marumasa.marumasa_sign.lib の下に変える (中のファイルと、それを指す文字列も)。
  ログの出力先 (Commons Logging) は同梱せず、MarumaSign のロガーに書く代わりのクラスにつなぐ
- ページは 1 枚ずつ、長い辺 1024 ピクセルで別のスレッドで描き、前後のページを先に描いておく
- 看板の絵に照準が合っている間だけ、絵の下側に帯 (戻る・進むボタンと「現在 / 総ページ数」) を重ねて描く。
  見た目は MarumaSign の画面と同じテーマ。照準が外れると消える。照準は各バージョンの Platform.crosshair で受け取る
- 照準が絵に合っている間の使用キー (右クリック) はページ送りに使い、ゲームには渡さない。
  ゲームの使用キーを、先に MarumaSign.useClicked を呼ぶものに置き換える (1.14 以降は KeyMapping、それより前は KeyBinding)
- 画面: PDF のファイルを選んでアップロードでき、プレビューに 1 ページ目とページ数を出す
- 動作確認: tools/verify に 2 ページの test-pdf.pdf を追加。SelfTest は進むボタンに照準を合わせて帯が出ること、
  クリックで 2 ページ目に進むこと、照準を外すと帯が消えることを確かめ、verify.py は各場面の画面を記録して 2 ページ目の絵を判定する
- :core:pdfCheck で、Minecraft を起動せずに表示とページ送りを確かめられる
- 1.7.10 - 1.14.4: スプラットは奥行きを記録せずに描くため、後から描かれる空や雲 (OptiFine のシェーダーで目立つ) に上書きされていた。色を描いた後に、ほぼ不透明な部分の奥行きだけを書くようにした
- 確認ツール: Iris が jar の中で必須にしている Sodium の版を入れるようにした (Modrinth の依存の情報と食い違う版があった)。「a || b」の版の条件を読めるようにし、必要な Mod がそのバージョンにない新しい版の Iris は古い版に替えるようにした
- 確認ツール: 影 Mod がシェーダーパックを読み込めずに通常の描画へ戻ったときは、有効とみなさず対象外とするようにした。Forge 1.14.3 は OptiFine を読み込まないので、影 Mod なしとして扱うようにした
- 確認ツール: タスクバーの縮小表示など、常に手前に出るウィンドウが重なっていたら、ウィンドウを動かしてから撮るようにした。失敗した基準は消して、次の確認で対象外として記録し直せるようにした
feature/20-pdf を splat に取り込んだ。衝突は両方の変更を残して解決した。

- SignImage.batches: スプラット用のカメラの位置と、PDF のページ送り用の看板の位置を、両方受け取る形にした
- ImageLoader / MainPage: 内容の判定に PDF を加えた (上限は PdfFile.MAX_SIZE の 64 MB)
- 言語ファイル・README・ファイル選択の拡張子: 両方の項目を残した
- 確認ツール: 確かめるものに PDF を加え、報告書の説明を 5 種類にした
- 帯の当たり判定で使う絵の表の向きが逆で、どのバージョンでも照準を合わせても帯が出ず、
  出たとしても絵の奥に隠れていた。表の向きを、表と裏の頂点の位置の差ではなく、絵の面の向き (v × u) から求めるようにした
- 埋め込まれていないフォントの代わりを探す一覧を自前で作るようにした (SystemFonts)。
  PDFBox の標準の一覧はすべてのフォントファイルを最後まで読むため、フォントが 6000 個ほどのパソコンで 53 秒かかっていた。
  必要な表 (name, OS/2, head と CFF の先頭) だけを読み、PDFBox の一覧と同じ内容 (6028 個すべて一致) を 1 - 2 秒で作る。
  一覧は marumasign/pdf-fonts.cache に保存し、次からは変わったファイルだけを読み直す
- 確認ツール: 5 つ目の看板を、最初の看板と同じ場所に置こうとして置けなかった問題を直した (移動の順番)。
  帯の出た画面を記録し終えてからページを送るようにした。帯が出なかったときは、照準の計算の値を結果に書く
- SignPdfCheck: 看板の文字が読める側 (向き 0 では南) からだけ確かめるようにした
- 使用キーを置き換えたあとも、元の使用キーがキーの割り当ての一覧 (1.11.2 まではリスト) に残っていた。
  Forge 1.9 - 1.11 は同じキーの割り当てのうち先に見つかったものにだけ押されたことを伝えるので、
  置き換えた使用キーが押されず、看板を置けなかった。置き換えるときに、元のものを一覧から除くようにした
- matrix.py --jobs N: 組み合わせを別々のプロセスで N 個ずつ同時に確認する。サーバーのポートとフォルダはプロセスごとに分け、
  結果のファイルは書く直前に読み直して、その組み合わせだけを書き換える。ファイルを揃える処理は 1 つずつ行う
- 画面は、確認しているクライアントのウィンドウを PrintWindow で撮る。手前に出さなくてよいので、ウィンドウが重なっても撮れる
  (撮れなかったときは、これまでどおり手前に出して画面から撮る)
- PDF の表示の判定では、絵の下の帯を見ない (照準が絵に合うとページ送りの帯が重なるため、すき間ありと誤って判定していた)
- 既知の警告に、バニラの「Item entity N has no item?!」を加えた
@malken21
malken21 merged commit a6cd66e into main Oct 20, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PDF対応 後ろ向くと音量上がる

1 participant