はじめに
今回は、3DCGの半透明表現がバグりやすい理由と、その対処法を紹介します。

解説画面にはBlenderとUnityを使います。

※説明のため、ペイントソフトで加筆したモノもあり
実際の3DCGの動作とは画面とは少し異なる所があります。
半透明の表示バグの仕組み概要
バグの発生原因は「モノを重ねる処理を数式で表現してるから」です。
このオプジェクトの重なりによってバグが発生します。

この、なんてことない “重なり” を3DCGで表現しようすると…
特に、半透明を中心にさまざまな問題が発生します。

この陰面処理の問題が、半透明エラーに繋がっていきます。

この重なって見えない部分を「陰面」とよびます。
そして、この重なりに関する処理は「陰面処理」や「陰面消去」と呼ばれます。
まず、この半透明エラーが発生する理由を理解するために必要な…
不透明の物体での「前後関係の描画方法」を解説します。
基本的な前後関係の描画方法
まずは、3DCGの普通の前後関係の描画方法を紹介します。

このような前後関係は、様々な書き方があります。
ここでは代表的な、下記の描画方法を紹介します。
・Zソート法
・Zバッファ法(主流)
Zソート法
視点からオプジェクトの座標で距離を計算 → 順番の遠い順に書く方法です。
「普通の人ならコレで良くない?」と真っ先に思いつく方法。


オプジェクトの座標はBlenderで言う所の原点です。
だいたいは中心点になります。
奥から順に、見えない所も描いて重ねる形になります。

絵描き的な言い方をすれば…
「奥から前に描く」です。


見えないところの形を意識し、破綻を減らせる。
王道デッサンスタイル。
奥から描くことで、重なって見えない部分を隠せます。


「これで良くない…?」
と思うかもしれませんが良くないのです。
なので、ここから話がややこしくなっていきます。
このZソート法は、交差した面などが上手く描画できない問題が発生します。

下図の様な視点で3Dモデルを見たことを想定し、交差した時の挙動を解説していきます。

Zソート法はオプジェクトの座標、Blenderで言う所の原点で位置を取ります。
そして、奥から順に描画します。
なので… オレンジ色 → 黄色の面の “順” に描画されます。

その結果…
↓のGifのような重なり方をします。

そうです。
Zソート法はモデルの “交差” を表現できません。
(あと、複雑な重なりも)

残念ながら、理想のような交差はしません。

これで「3DCGのモノを重ねる処理を数式で表現する」難しさ。
モノを重ねることで起こる問題理解への第1歩が進めたと思います。

[現実] 面の図で境界がちょっとギザギザしてるのは…
単純に、私の切り抜きが甘いだけです。
以上が、Zソート法です。
Zバッファ法(現在の主流)
Zソート法が視点から面の中心位置で距離を計算して失敗しました。
そこ登場するのがZバッファ法です。
これは、一言で言うと「奥行きの情報をピクセルごと計算して記録する処理」です。

Zバッファ法は下記の2つの処理を同時に行ってます。
・ビューに色の描画
・メモリーに奥行き情報の描画
(白黒で表現、このメモリーに書かれた奥行き情報をZバッファと呼びます)
この「色」と「奥行き」情報を画面の1px単位で描画+保存します。
そして、この2つの結果をうまいこと組み合わせて、裏側の面を消してます。


ちなみに奥行きの色は…
「手前が黒、奥が白」と「手前が白、奥が黒」の2パターンがあります。
これは、内部の処理の仕方、プログラムの考え方が違うだけなので…
そこまで気にしなくて大丈夫です。
やってる事は同じです。
(他の説明だと、手前が白い事がある事だけ覚えてください)
メモリーに格納される奥行き情報はグレースケールの「16bit ,24bit ,32bit」などです。
一般的な画像の8bitより多くの情報が使われます。
【Bit数によって入る情報量の違い】
・8 Bit → 256(2の8乗)
・16Bit → 65,536(2の16乗)
・24Bit → 16,777,216(2の24乗)
・32Bit → 4,294,967,296(2の32乗)
この数字の数が、作れる奥行きの差です。
あと、どのオプジェクトを順に描くかは…
①と②の順は特に指定は無さそうです。

英語版Wikiに書かれていた、サンプルコードには…
処理は「オプジェクトごとに行う」とだけ書かれてました。
(これは、読み飛ばしてもokです)
The following pseudocode demonstrates the process of z-buffering:
// First of all, initialize the depth of each pixel.
d(i, j) = infinite // Max length// Initialize the color value for each pixel to the background color
c(i, j) = background color// For each polygon, do the following steps :
引用:Z-buffering「Algorithmics」より
for (each pixel in polygon’s projection)
{
// Find depth i.e, z of polygon
// at (x, y) corresponding to pixel (i, j)
if (z < d(i, j))
{
d(i, j) = z;
c(i, j) = color;
}
}
↑のpolygonは、blenderで言う所の「オプジェクト」を指してると解釈しました。

内容を要約すると下記。
① → 色を初期化する
d(i, j) = infinite // Max length)
② → 背景色で塗る(黒か白で塗りつぶし)
c(i, j) = background color
➂ → オプジェクトごとに④の処理を繰り返す
for (each pixel in polygon’s projection)
④ → ピクセルごとに「奥行き(z)」を計算して比較し…
元の奥行きより近ければ⑤の処理を実行する
if (z < d(i, j))
⑤ → オプジェクトの「奥行き(z)」と「色(color)」情報を描く。
{ d(i, j) = z; c(i, j) = color; }
⑥ → オプジェクトの数だけこれを繰り返す。(➂~から)
Blenderだと、視点から近い方(原点位置)で決定。
原点位置が同じなら、アウトライナーの順番で決まってるようでした。

そしたら、元の問題に戻ります。
重なりの表現で問題になるのはこの部分です。

ここで、メモリーに書かれた奥行き情報を使います。

この、重なった部分の色の明るさで奥行きを判定します。

現在、この説明では手前の方が黒くなる処理を行ってます。
なので、後で描画オプジェクトの奥行き情報が下記のようになります。
・手前の部分 → 元のZバッファ情報より暗い色になる
・後ろの部分 → 元のZバッファ情報より明るい色になる
その結果、オプジェクトが交差していても前後関係を判定できます。

そして、この奥行き情報の比較結果を使って…
「色」と「奥行き情報」を描画するところを決めます。


この “元のZバッファ” から “新しく描画したオプジェクト” の奥行きを比較する処理は…
「Zテスト」や「デプステスト」などと呼ばれます。
この処理を、下図のような手順で行う事で3DCGの前後関係を表現してます。

これが、Zバッファ法です。

奥になってる所は奥行き情報も描き込まれません。
なので、Zバッファも青色の所は描画されず、正しい奥行きの形を表現できます。
一見、重そうに見えるこの方法ですが… 重なってる部分は完全に描画しません。
なので、その分の処理を軽くできます。

なので、下記のメリットがあり現在、Zバッファ法が多くの3DCGツールで使われてます。
【Zバッファ法のメリット】
・処理が軽く、ゲームなどでリアルタイムで動かせる
・交差などを表現できる
・現在主流で、多くのツールやハードウェアで対応してる
基本、3DCGの重なり表現は「Zバッファ法」で考えて良いと思います。
Zバッファ法の描画順について
さきほど、2つのオプジェクトをどの順番に描くかの指定は無いと解説しました。

ですが、描画順は3DCGソフトによって様々なルールが決められてます。
セオリーとして「前から後ろのオプジェクトの順に描く」があります。

Unityは「前 → 後ろ」で描いてます。
Blenderもおそらく同じです。(オプジェクトの原点位置で距離を決定)

Zソート的な要領で、視点からオプジェクトの原点位置で距離を計算してると思います。
というのも、後ろから描くと無駄の部分の描画が発生します。
なので、手前から描いた方が無駄な描画を行う可能性が少なく負荷を減らせる可能性が高いです。

なので、ここまでの事をまとめると…
Zバッファの基本は絵描き的な言い方をすれば「前から順に描く」です。


超上手い絵描きさんが、作業を効率化するために使う描き方。
そして、Zバッファ法の最大強みがこの描画順が適当でもOKという所です。

絵描き的な言い方をすれば「適当に描いて良い感じにする」です。
この、描画順管理が不要な事が最大のメリット。


具体的に言うと下記の2つのアプローチを組み合わせて使ってます。
・手前で隠れてる所を書かない
・奥にある、手間のモデルで隠れた所を消す
そして3DCGソフトの描画順ですが…
Unityの場合は、描画順をマテリアルのRender Queueで強制的に決定できます。
大体の不透明オプジェクトは「2000」で設定されてます。

ほとんどのオプジェクトのRender Queueは2000です。
共通する2000の中で、視点からの距離などから描画順を決めてます。

Render Queueの値が同じ場合は…
視点からオプジェクトの原点や中心点までの距離で描画順を決めます。
※Render Queueの決定は、視点からの距離より優先度が高い
試しに、2つのオプジェクトとマテリアルを用意。
手前のマテリアルのRender Queue「2001」と「1999」に設定。
すると、見た目に変化が起こりません。

実際の挙動は↓のGifの通り。
※どのマテリアルか分かりやすいよう、色を変えてから動かしてます

以上が、Zバッファ法の描画順についての解説です。
Zバッファ法とZソート法どちらが軽いか
Zバッファ法に比べれば単純で、メモリーも消費しないZソート法ですが…
こちらは、Zソート法は見えない部分を描いてます。

一方、Zバッファ法はメモリーを消費しますが…
見えない部分は描きません。

なので、処理的にどちらが軽いかは…
「パソコン(ハードウェア)」がこの処理に最適化してるかという問題になってきます。

Zバッファ法のサポートが無いパソコンであれば、Zソートの方が早いです。
が、Zバッファ法のサポートがあるパソコンならZバッファの方が高速らしいです。
(詳細はこちらの「47ページ」参照)
そして、現在主流のZバッファ法は多くのパソコンでサポートがあります。
なので多くの場合で「Zバッファ法」の方が軽く最適解になります。
以上が、基本的な前後関係の描画方法です。
ここからは、多くの3DCGは「Zバッファ法」で動いてる事を前提に進めます。

ここまで来て、ようやく半透明のバクに挑めます。
Zバッファ法は半透明に弱い
Zバッファは、描画順が適当でも良い感じに重なりを表現できる。
その上で、セオリーとして視点から近いオプジェクトの順に描く事を紹介しました。

そしたら問題です。
①のマテリアルを半透明にすると何が起こるでしょうか…?
…
…
↓正解はこちら

何が起こったかは下記。
① → 多くの3DCGツールはZバッファ法でも、描画順は「前から後ろ」の順
② → 前の半透明オプジェクトを先に描画する
➂ → Zバッファ法で前のオプジェクトに重なった後ろのオプジェクトの一部が消える
④ → 前のオプジェクトが半透明なので、後ろのモデルが消えた部分が見える
↓の水色の部分が、先ほどの半透明と考えて見てください。
Zバッファ法で前から順に描かれたので、後ろのモデルの重なった部分が消えました。

↓分かりやすく、青い箱の透明度を下げて、背景に模様を入れたモノがこちら。
このような仕組みで、3DCGの半透明は透過した先が消えるなどの不具合を発生させます。

また、ソフトの設定によっては奥のモデルが手前のモデルを貫通することもあります。

Unityを使った際、奥のオプジェクトが、見た目は不透明でも…
データ的に半透明の設定だった場合に、この挙動になります。

Unityの半透明設定はZバッファによる描画省略を行わないようです。
なので「手前 → 後ろ」の順で描くと…
そのまま、後ろのオプジェクトが描画されて手前の描画を貫通します。
これが、Zバッファ法が半透明に弱い理由です。
Zバッファ法で半透明を描画する方法
Zバッファは描画順が適当でも何とかなると解説しました。
なので、先ほどと逆の「後ろ → 前」の順でも正しく前後関係が描画されます。
(重なった部分の描画は無駄になりますが…)

そしたら問題です。
②のマテリアルを半透明にすると何が起こるでしょうか…?
…
…
↓正解はこちら

そうです。
正しく見えます。

UnityでlilToonというシェーダーを使用。
2つのマテリアルのレンダーキューを「後ろ:2460」と「前:2461」で設定。
先ほどとは逆の「後ろ → 前」の順で描かれた場合…
見えない部分の描画が残るので、正しく半透明を表現できます。

②の半透明な物体は、ただ単純に上から順に重ねて描かれただけです。

つまり、3DCGの半透明は「後ろから前」の順に描けば生成できます。
これが、Zバッファ法で半透明を描画する方法です。
半透明のエラー対処法
先ほど、Zバッファ法で半透明を描画する方法を紹介しました。
内容をまとめると下記。
・奥にある不透明を先に描く
・半透明の手前に不透明がある場合はZバッファ法により描画を無効化される
・手前に不透明が無い場所の半透明を奥から順に描く
重要なのは、半透明の領域は「後ろ → 前」の順に描画する事です。
これで、陰面の描画が残り、半透明を表現できます。

そして、ほとんどの半透明エラーは…
この描画順が正しく設定されなかった事が原因で発生します。
なので、半透明のエラー対処法は…
「半透明の描画順をなるべく正しく設定する」になります。

半透明の正しい描画順は「後ろを先 → 前を後」です。

これを設定できれば、半透明に勝てます。

そして、この設定を行うためには…
ソフトによって違う「描画順の決まり方を知る」必要があります。

↓Blenderでの、描画順の決まり方と設定方法はこちらで解説。
(「半透明バグの対処法」の所から)

↓Unityでの、描画順の決まり方と設定方法はこちらで解説。

こちらを見ながら “半透明” のオプジェクトで…
正しい描画順になるよう調整してください。

前が不透明 → 後ろが半透明の重なりは…
Zバッファ法が良い感じに消してくれるので何とかなります。
問題なのは半透明が前に来た時です。
以上が、半透明のエラー対処法です。

ちなみに、この方法は細かな「反射」や「屈折」の挙動は表現できません。
次以降の内容は、これらを表現したい人向けの情報になります。
非リアルタイムでの半透明のエラー解決法
非リアルタイムであれば、他の方法でも半透明を表現できます。
俗にいう、画像を出すための計算時間(レンダリング)が必要な方法です。
代表的なモノは下記の3つ。
・スキャンライン法を使う
・レイトレーシングを使う
・パストレーシングを使う
この3つについて解説します。

ここからの処理は、負荷が高い処理になります。
なのでゲームや3DCGソフトのビューといったリアルタイムが必要な場面では…
まず使われません。
レンダリングが行われることが前提になります。
スキャンライン法
まず、視点(カメラ)を配置。
そして見える範囲を決定します。

この範囲で何が見えるかを…
左上のピクセルから順に処理する方法です。

視点側から物体を検知し、スライスするように奥行きを計算します。
なので、正確なモノの重なりを描けます。
また、半透明にも対応。

これが、スキャンライン法です。

ただ、処理が重いらしいです。
リアルタイムグラフィックスではほぼ、使われません。

個人的にZバッファとやってる事ほぼ同じじゃない…?
と思うのですが、ハードや内部処理の関係なのか “重い” らしいです。
レイトレーシング
イメージとしては、スキャンライン法の精度を上げたモノです。
スキャンライン法の容量で、視点(カメラ)を配置。
同じ要領で、視点から何が見えるかを処理していきます。

大きな違いは…
視点から出てる、何が見えてるかを表す線が “物体に当たった後” の状態まで追いかける事です。

これで、モノの “色” が見える挙動を逆方向から追うことができます。

ちなみに、この線を「レイ」と呼びます。
ray(光線)の意。
レイが物体に当たった後の状態まで追うので「トレーシング」。
合わせて「レイトレーシングです。」
現実世界で色を見るには「目orカメラ」と「モノ」と「光源」の3要素が必要です。

現実世界で色が見える仕組みは下記。
① → 光源から光が出る
② → 光源の光が物体に当たる
➂ → 物体がその光の一部を吸収する
④ → 物体に当たって吸収されなかった光は反射される
⑤ → その反射された光が目に届く
⑥ → 目などのセンサーに光が届くと "色として認識" される。
まとめると、下図のようになります。

※上の図では、色成分を光の3原色による加法混色で表示してます。
(右側のバー見たいモノの話です)

これが、現実世界で色が見える仕組みです。

ちなみに吸収された光は、熱エネルギーに変換されます。
なので黒い服は光の吸収量が多い → 暑く感じる。
白い服は光の吸収量が少ない → 暑くなりにくいです。
そして、レイトレーシングはこれの逆をやります。
視点側から見える範囲(画像として書き出す範囲)を決めて、1pxごとに何が見えてるかを検知。
レイが物体に当たれば、反射してその先にある光源を探して色を決定。

これが、レイトレーシングの仕組みです。
このレイは曲げたり増やしたりできるので、反射、屈折、半透明すべて問題無く表現できます。


※物体や光源が無い場合は背景の色が反映されます。
そして、リアルな反射や屈折を描けます。
これがレイトレースです。

ただ、スキャンライン法より処理が重いです。
圧倒的に重いです。

ゲームなどのリアルタイム表示モノでは、基本使いません。
綺麗な映像や静止画1枚を作るためのモノと思ってください。
とはいえ、近年は「ハードウェア―の進歩」と「レイトレースの軽量化」で…
リアルタイムレイトレーシングなるモノが出てきました。
もしかしたら、今後はレイトレーシングで動く時代が来るのかもしれません。

とはいえ、重い事には変わりなく、まだまだ実験的な技術なので…
リアルタイムレイトレーシングが世間一般に広がっていくのは、もう少し先かな…
パストレーシングを使う
イメージとしては、レイトレーシング法の精度を上げたモノ。
レイトレーシングで拡散反射なども計算できるようにした技術です。
これを計算する事で、よりリアルな光を表現できます。

まず、物体の表面は粒子レベルで見てください。
そこには細かな凹凸があります。

イメージできない方は、Eames氏制作の「Poers of Ten」の6分ぐらいの所を見てください。
物体は拡大すると、無限にボコボコしてる事が分かります。

最後は分子、元素レベルでボコボコしてます。
なので、拡散反射は基本、全ての物体で発生します。
つまりレイトレースが再現したような…
1方向だけのシンプルな反射は現実には存在しません。

物体表面には細かな凹凸があり、この凹凸によって光が拡散反射します。

どんなツルツルな物体でも、それが “分子” で出来てる限りボコボコします。
なので、粗さ/滑らかさに関係無く、全ての物体で拡散反射は起こります。

この拡散反射を3DCGで再現しようとしたのがパストレーシングです。
視点から出るレイを、物体に当たったらランダムに分散させる処理で拡散反射を再現してます。

このレイの分散を「パス」と呼ぶようです。
レイトレーシングにパスの分散を入れたモノ → パストレーシングです。

このレイの分散は、物体に当たるたびに発生します。
なので、えげつないぐらい重いです。


このように、レイトレーシングが追えなかった光まで追尾してるので…
パストレーシングは「フルレイトレーシング」とも呼ばれる事があります。
えげつないぐらい重いですが…
最も綺麗な絵を生成できます。

Blenderでは「Cyclesレンダラー」で設定できます。
Cyclesはデフォルトでパストレーシングです。
そして、ライトパスの「ディフューズ」の値がパスの分散数だと思います。

「ディフューズ = 32」はパスが32本、出るという意味と思います。
(生成物の挙動を見て判断)

なので…Cyclesレンダラーでディフューズ0にすると、レイトレースの見た目を再現できます。
その見た目比較の結果がこちら。

パストレースの方が透明の影部分の表現などのクォリティアガがってます。
以上がパストレーシングです。

あとは、視点と光源の双方向からレイを出したりする
「双方向レイトレーシング」などの考え方による差がありますが…
このあたりはフォトリアル系の専門家でなければ、
今は覚えて無くても良いモノだと思うので、省きます。
リアルタイムでの「屈折」と「反射」表現
屈折と反射はレイ/パストレーシングでしか作れませんでした。
が、この処理は重すぎて基本的にゲームなどには使えません。

ではBlenderのプレビュー画面やVRChatなどの屈折や反射はどのように再現してるのか?
という話をします。

「屈折」は、透過した部分の画像を歪めて表現してます。
歪み方はモデルの形で決定します。(頂点法線の形)

ただ歪めてるだけです。
なので、正確な透過屈折ではありません。
あくまで、疑似的にそう見えてるだけです。


ちゃんと見ると間違ってますが…
イラスト的にはこっちの方が自然に見える事もあり、
このような描かれ方をしている絵もいくつかあります。
「反射」は、周囲の反射状況などを書いた球体の画像を使って再現してます。
代表的なのは下図のような、MatCapと呼ばれる球体を描いた画像です。

マテリアルを色々調整し、MatCapの所にこの画像を入力します。

すると、MatCapに描かれた球体の金属光沢がモデルの形に合わせて表示されます。
(頂点法線の形に依存)

あくまで画像データなので…
実際の光沢に合った感じで描かなくてもOKです。

こちらのようなMatCapを設定すると、ちょっと変わった金属光沢まで表現できます。

↓詳細はこちらでまとめてます。
BlenderのマテリアルプレビューはHDRIという360°の風景が記録された画像を使ってます。
↓の画像のようなイメージです。

3Dビューの右上の所から、付属のHDRI画像を設定できます。
プレビュー画面はHDRIを、MatCapに変換したモノになってます。

この、HDRI変換で作られたMacCapの入力を使って反射を再現されます。

HDRIは360どの画像情報があるので、視点によってMacCapの描画を変えれます。
また、透過もMatCapの裏側の見えない部分の情報を使ってちょっとリアルに作れます。

あと、BlenderのEEVEEレンダラーは「スクリーンスペース反射」を有効化すると…
簡易レイトレーシングみたいな処理が入り、周囲の物体の影響も計算されます。


レイトレーシングとは違うらしいですが…
それでも、かなり精度の高い “それっぽく見える” 処理が入ってます。
ここまでをまとめると…
・リアルタイムモノの透過は「歪めてる」だけ
・反射は「球体の画像データからモデルに合わせて表示してる」だけ
以上が、リアルタイムでの「屈折」と「反射」表現の解説です。
おまけ:透過がぶつ切りで良いなら「アルファクリップ」を使う
透過素材を扱う上で「アルファクリップ」という考え方があります。
半透明には使えないので “おまけ” として解説します。
これは、透明な部分をぶつ切りで消す処理です。
↓の黒い所が透明部分です。

これに、アルファクリップを使うと…
綺麗に切り抜けます。
そして、最大の特徴が、これまで紹介した「半透明エラー」が発生しません。
描画順を気にせず、透過素材を設定できます。

この方法は、アルファクリップは半透明を表現できないという弱点があります。
↓試しに下図の様な半透明の素材を用意。

そしてアルファクリップを設定。
すると「しきい値」で、どこからを透過、どこから表示という事は調整できます。
が… 半透明部分は下図のように、ぶつ切りになります。

半透明は表現できませんが… 描画順によるエラーが出ないのが最大の魅力。
なので、透明/不透明がぶつ切りで良いなら「アルファクリップ」を使う。
半透明の表現を使うなら「アルファブレンド」を使うのがおすすめ。

あと、処理的な事を言えば…
「アルファブレンド」より「アルファクリップ」の方が高負荷です。
が、半透明の描画順エラーが出ない事のメリットが大きいので…
普通に使って良いと思います。
(使いすぎ注意、透過が無いなら不透明に設定するのが一番軽量です)
あと、Unityで使えるlilToonシェーダーでは下記のような表記になってます。
・アルファクリップ → カットアウト
・アルファブレンド → 半透明
このような表記の違いはありますが…
基本的な中の構造や仕組みは同じです。
以上が、半透明表現がバグりやすい理由と対処法です。
まとめ
今回は、3DCGで半透明表現がバグりやすい理由と対処法を紹介しました。
・原因は、モノの重なりを数学的に表現する事の難しさにより発生する
・ほとんどのリアルタイム3DCGはZバッファで関係を描く
・Zバッファ法は、視点から近い → 遠い順にオプジェクトを描画する
・Zバッファ法は、前のオプジェクトが重なり、隠れた部分を省いて描画する
・すべてが不透明のモノであればZバッファ法は何も問題が起こらない
・Zバッファ法は半透明のモノ描画した時に問題が発生する
・Zバッファ法は重なった部分の描画を省く
・半透明のモノを手前で重ねると、この省かれた描画が見えてしまう
・対処法は半透明部分を後から描く事
・半透明は奥 → 前の順に描画することでエラーは解決する
・なので、対処法は3DCGツールの描画順設定を知り、半透明は奥 → 前の順になるよう設定する事
・完全な透明と不透明の2つであれば「アルファクリップ」を使うのがおすすめ
また、他にも3DCGについて解説してます。
ぜひ、こちらもご覧ください。
コメント