現在作成中の3Dディスプレイ(3D POV)用の表示データはゲームエンジンのUnityを使っています。理想はUnityの3D空間上に自由にオブジェクトを置いたらスキャンをして3Dディスプレイ用のデータとして出力してくれる、というものなので、実際に実装した方法の説明です。
なお、この実装はAI(Claude Code)がほぼやってくれました(スキャン方法についてはこちらで指示をしています)。説明もAIがやってくれたので、普段よりも読みやすくてわかりやすいと感じたらAIのせいです。
1. 考え方・概要
このプロジェクトは、3Dモデル(キャラクターなど)を回転式の円柱型POVディスプレイ(※1)用のボクセル(※2)データに変換するツールです。
※1 POV(Persistence of Vision)ディスプレイ: 「残像現象」を利用したディスプレイ。LEDが並んだ平面ディスプレイを高速回転させ、回転角ごとに点灯パターンを切り替えることで、人の目には円柱状の立体映像として見える仕組み。
※2 ボクセル(Voxel): 3D版のピクセル。空間を格子状に区切った1マスのこと。
従来はこの変換を、円柱の各格子点(ボクセル)から6方向にRay(レイ、光線のように伸びる当たり判定)を飛ばし、モデルに当たるかどうかで「そこに表面があるか」を判定する方式で行っていました。この方式は、Rayが当たり判定を取るためにMeshCollider(※3)をモデルに用意する必要があり、特に服やスカートなど、ボーンで動くSkinnedMeshRenderer(※4)のパーツはポーズごとに形が変わるため、その都度コライダーを作り直す必要があるという大きな手間がありました。ちなみに判定するボクセルの数は64x64x192=786,432個もあります。
※3 MeshCollider: メッシュの形そのものを当たり判定として使うためのUnityのコンポーネント。
※4 SkinnedMeshRenderer: ボーン(骨格)に追従して変形するメッシュを描画するコンポーネント。人型キャラクターの体はほぼこれで作られている。
そこで発想を転換し、「格子点からモデルに当たり判定を飛ばす」のではなく、「カメラでモデルを取り囲んで撮影し、写った内容から直接『どこに・何色の表面があるか』を読み取る」方式に作り替えました。当たり判定を一切使わないため、コライダーの用意やベイクが完全に不要になります。



2. 実際のスキャンの流れ
ステップ1: 6方向からオルソカメラで囲む
モデル全体を包む範囲を計算し、その6面(+X, -X, +Y, -Y, +Z, -Z)それぞれの外側にカメラを配置し、中心に向けて撮影します。カメラはOrthographic(平行投影、※5)に設定します。
※5 Orthographic(平行投影)カメラ: 通常のカメラ(透視投影)は遠近感があり遠くのものほど小さく写るが、Orthographicは遠近感のない「設計図の投影図」のような写り方をする。画面上のピクセル位置がワールド座標に比例するため、「このピクセルは実世界のどの位置に対応するか」を単純な計算で逆算できる。
ステップ2: デプスピーリング ── 奥の層まで剥がして撮る
1回の撮影では、各ピクセルに写るのはそのカメラから見て一番手前の面だけです。しかしキャラクターは、シャツの奥にお腹の肌があったりと、同じ方向から見て複数の層が重なっています。これを漏れなく取得するためにデプスピーリング(Depth Peeling、※6)という手法を使います。
※6 デプスピーリング: 「一番手前の層を1枚撮ったら、その層を除外してもう一度撮る」を繰り返し、奥の層を1枚ずつ剥がすように取得していく手法。玉ねぎの皮を1枚ずつ剥くイメージに近い。
- 撮影し、一番手前の層の位置と深さを記録する。
- 「前回記録した深さより手前にある面は描画しない」という条件を加えて、同じ方向からもう一度撮影する。すると前回の面は無視され、その奥の面が新たに写る。
- そのピクセルに何も写らなくなるまで繰り返す。
これを6方向すべてに行い、モデル表面のほぼ全層の位置と色を収集します。
ステップ3: 位置と色を同時に記録する専用シェーダー
撮影中は、モデル本来のシェーダー(照明やトゥーン処理込みの複雑なもの)ではなく、全オブジェクト共通のスキャン専用シェーダーに一時的に差し替えて描画します。デプスピーリングの層判定などの処理を、モデルごとに違う複雑なシェーダーの上に乗せるのは難しいため、シンプルな共通シェーダー1枚に統一する必要があるためです。
このシェーダーは、1回の描画で色(RGB)と、ワールド座標+カメラからの距離を、それぞれ別の画像に同時に書き出します(MRT: Multiple Render Targetsという仕組み)。撮影後、この2枚の画像をCPU側で読み取り、「位置」と「色」のペアをサンプルとして集めます。
ただし全オブジェクトが同じシェーダー(マテリアル)を使うため、何も対策しないと色の情報が失われて全員同じ色になってしまいます。これを避けるため、撮影前に各パーツ本来のテクスチャ・色をMaterialPropertyBlock(※7)という仕組みで“注入”しておきます。
※7 MaterialPropertyBlock: 共有のマテリアルを複製せずに、Renderer単位でテクスチャや色などのプロパティだけを個別に上書きできるUnityの仕組み。
このとき重要なのが、1つのRendererが複数のマテリアル(サブメッシュ)を持つケース ── 肌・髪・服などが1つのメッシュにまとまっているモデル ── への対応です。“注入”はサブメッシュのインデックスを指定して行う必要があり、これによって「肌には肌のテクスチャ、髪には髪のテクスチャ」を、マテリアル数やパーツ構成に関わらず正しく反映できます。(著者注:ちょっとここがの説明がよくわかっていないですが大事なことのようです)
ステップ4: 円柱グリッドの座標へ変換する
集まった大量の「ワールド座標+色」サンプルを、POVディスプレイのボクセル配列(半径・高さ・回転角の3次元グリッド)のどのマス目に対応するか計算します。円柱型ディスプレイなので、単純なXYZ格子ではなく円柱座標系でボクセルが並んでおり、ワールド座標をこの座標系のインデックスへ変換する逆算式で処理します。該当ボクセルが決まったら色を積算(複数サンプルがあれば平均化)し、「占有されている」フラグを立てます。
全方向・全レイヤー分の処理が終わると、ボクセル配列の「どこが占有され、何色か」が完成し、旧方式と同じファイル形式で書き出せます。
3. モデルを追加する時の注意点・必要なこと
新しいモデルをスキャン対象として追加する際に必要な作業は、次の2点のみです。
- スキャン対象レイヤーを設定する: モデルのRendererを、スキャンが参照するレイヤーに割り当てる。
- シェーダーがUnityの一般的な命名慣習に沿っていること: テクスチャ・色のプロパティが
_MainTex/_Color、または_BaseMap/_BaseColorといった標準的な名前で定義されていること。Material.mainTexture/.colorがこの慣習を見て自動的に「本来のテクスチャ・色」を解決してくれるため、シェーダーの中身がまったく違っていても共通のコードで扱える。
4. Ray方式との比較・メリット
Ray方式の原理: あらかじめ用意したボクセルの格子点それぞれから、6方向に当たり判定(BoxCast/Raycast)を飛ばし、MeshColliderに当たった位置のUV座標からテクスチャの色を読み取る。
新方式の原理: 当たり判定を使わず、モデルを取り囲むカメラで撮影し、描画結果の画像から直接「位置」と「色」を読み取る。
| Ray方式 | カメラ+デプスピーリング方式 | |
|---|---|---|
| 当たり判定 | MeshColliderが必須 | 不要 |
| アニメーション・ポーズ変更への対応 | ポーズが変わるたびコライダーの再ベイクが必要 | 再ベイク不要、そのままスキャン可能 |
| 新モデル追加時の作業 | コライダーのセットアップが必要 | レイヤー設定のみ |
| 複数マテリアル(サブメッシュ)への対応 | 部位ごとの当たり判定・色取得を個別に作り込む必要 | サブメッシュ単位の処理が最初から汎用的 |
最大のメリットは、当たり判定というワンクッションを完全に排除し、「見えているものをそのまま読む」ことでモデル追加のハードルを大きく下げられた点です。
↑ここまでAI
つまり、3D空間に配置したオブジェクトを見たままの色でスキャンすることができるようになったということです。まだ静止画専用なので、動画もできるように改良します。UnityのGame空間上で0.1秒ずつ動かし、スキャンを繰り返すことで動画にも対応しました。任意の名前のフォルダを作成し、中に00000.bin, 00001.bin, 00002.bin,…と連番のスキャンファイルを保存します。3D POVは一周ごとに時間経過に基づいた連番ファイルを検索し、表示することで一連の3D動画として再生することができます。


コメント