Crystal Renderer 技術解説③

概要 この記事について

UE5 向けのプラグインである Crystal Renderer についての技術解説です。
全3回+αに分けての解説を予定しています(これはボリューム等を見て変更する可能性があります)。

第1回:Crystal Renderer の原理
第2回:光の反射と屈折
第3回:形状をマテリアルに取り込む(本記事)
補講:色収差について

本記事は第3回です。先に前回までの記事を読んで頂くことをお勧めします。

前置き

前回、レイが平面(無限平面)に衝突したときの計算について解説しました。
しかし、「衝突させるまで」の部分についてが問題として残っています。
視点から発射されたレイが結晶表面に衝突する位置については、ピクセルシェーダーにデフォルトで渡されていますので問題ありません(UE5 の場合は WorldPosition としてマテリアル内でアクセスが可能)。
しかしそこから先、結晶内部を進むレイがどの位置で物体の表面に到達するかを知るには、形状のデータが不可欠です。
マテリアルを扱う方ならお分かりかと思いますが、ここでメッシュ自体を参照するわけにはいかないという点が問題になります。

形状データの表現

結論から提示しますが、形状はテクスチャとして表現されます。
例えばブリリアント・カット(いわゆるダイヤモンドの形状)のメッシュは Crystal Renderer のマテリアル内では以下のようなテクスチャになっています。

勘の良い人であれば何が行われているかを想像できるでしょうか。
今回はこれについての話です。

形状をマテリアルに取り込むにあたって、以下二点の状況・性質を利用します。

  1. 宝石の形状は多くの場合において凸多面体である(正確には曲面を含む場合でも、ポリゴンでは多面体で近似される)
  2. 凸多面体は平面の集合として記述できる

宝石の形状は多くの場合凸多面体である

字面の通りではありますが、凸多面体とは凹みのない多面体のことです。
宝石のカットという工程を動画などで見ると分かると思いますが(検索例)、
平坦なやすりで表面を削る操作によって面を作り出すことを繰り返すのが基本的な操作です。
この操作で凹みが発生することはありませんので、宝石の形状は多くが凸型になります(例外はあります)。
また、ブリリアントカットの周辺部分(ガードルと呼ばれます)など実物の宝石には曲面も存在することがありますが、
ゲーム用のCGにおいては多面体(ポリゴン)に落とし込まれるため曲面ではなくなります。

凸多面体は平面の集合として記述できる

「平面で削り取る」操作を繰り返して作られた形状は、削り取った平面の集合によって充分に表現できるはずです。 「同じ組み合わせの平面で削れば同じ形状になる」と表現すればより納得しやすいでしょうか。
前回の記事で少し触れたように、境界のない無限平面は四次元ベクトルで記述できます。
四次元ベクトルはRGBAカラーに変換できますので、一面あたり1ピクセルのテクスチャとして平面の集合を表現できることになります。
注意点として、ベクトルの成分は負の数や程度大きい数を含むことになります。
したがってそれを許容するようなフォーマットを用いるか、何らかの正規化によって0.0~1.0の範囲内に値が収まるようにする必要があります。 Crystal Renderer では RGBA16F(16ビット浮動小数)フォーマットのテクスチャを用いています。

メッシュを読み取って得た三角形の集合を平面の集合に直し、以下のような関数によってテクスチャ化しています。

UTexture2D *UCrystalRendererBPLibrary::CreateConvexMeshFromSurfaces(TArray<FVector4> &Surfaces, int &PlaneNum, int &TextureSizeW, UObject *TextureOwner)
{
    if (Surfaces.Num() <= 0)
    {
        UE_LOG(LogTemp, Error, TEXT("Invalid surface list"));
        return nullptr;
    }

    // merge surfaces with (apploximately) same plane
    {
        TArray<int> RemoveList;
        for (int BaseIndex = 0; BaseIndex < Surfaces.Num() - 1; ++BaseIndex)
        {
            for (int AnotherIndex = BaseIndex + 1; AnotherIndex < Surfaces.Num(); ++AnotherIndex)
            {
                if (Surfaces[BaseIndex].Equals(Surfaces[AnotherIndex], 0.01f))
                {
                    RemoveList.AddUnique(AnotherIndex);
                }
            }
        }
        RemoveList.Sort();

        for (int i = RemoveList.Num() - 1; i >= 0; --i)
        {
            Surfaces.RemoveAt(RemoveList[i]);
        }
        PlaneNum = Surfaces.Num();
    }

    // decide texture size
    {
        int Width = 4;
        while (Width*Width < PlaneNum)
        {
            Width *= 2;
        }
        TextureSizeW = Width;
    }

    UTexture2D* Retval = CreateTexture(TextureOwner, TextureSizeW);
    
    TArray<FFloat16Color> Pixels;
    Pixels.SetNum(TextureSizeW*TextureSizeW);

    for (int i = 0; i < Surfaces.Num(); ++i)
    {
        Pixels[i] = FLinearColor(Surfaces[i]);
    }

    void *BulkData = Retval->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
    FMemory::Memcpy(BulkData, Pixels.GetData(), sizeof(FFloat16Color)*PlaneNum);
    Retval->GetPlatformData()->Mips[0].BulkData.Unlock();
#if WITH_EDITORONLY_DATA
    Retval->Source.Init(TextureSizeW, TextureSizeW, 1, 1, ETextureSourceFormat::TSF_RGBA16F, (const uint8*)Pixels.GetData());
#endif
    Retval->UpdateResource();

#if WITH_EDITOR
    Retval->PostEditChange();
#endif

    Retval->MarkPackageDirty();

    return Retval;
}

多角形の面は同一平面上のトライアングルを含みますので、これを除外する処理をついでに行っています。
これは衝突検出時の無駄な繰り返しを減らし、シェーダーの実行速度を有意に向上させる効果があります
(もちろん形状によりますが、宝石の面には四角形がけっこう多いためそれなりの効果が期待できます)。

形状データと衝突検出

さて、平面の集合として表現された形状とレイとの衝突をどのように計算すればよいでしょうか。
ふつうのポリゴンの場合は三角形とレイとの衝突判定を繰り返すことになりますが、
四次元ベクトルで表される(無限)平面の集合は頂点やエッジの情報を持ちません。
計算によりそれらを得ることも理屈上は可能でしょうが、死ぬほど効率が悪いということは想像に難くありません。
さいわい、これに関しては頓智めいた方法が存在します。
レイが形状の内部より開始するという条件が付くものの、各平面とレイとの衝突のうちもっとも早いものを採用するだけで凸多面体自体とレイとの衝突を求めることができます。
次の図を見ればこのことが直観的に分かるかと思います(もちろん実際には三次元空間を扱いますが、理屈は同じことです)。

(茶色い線が実際の形状データ、黒い線が元の形状、白い線がレイ、青グレーの丸がレイと平面との交点、黄色い丸がレイと多面体との交点を表します)

まとめ

レイの軌跡を計算するために形状をマテリアルに取り込む方法、及び取り込んだ形状の利用方法について説明しました。
なかなかに都合の良い話なのですが、凸多面体という条件下においてはこのような計算が成り立ちます。
Crystal Renderer の核となる理論についてはこれでおしまいです。
次は補講と称して色収差オプションについて少し書きます。

(おまけ)凸型でない形状

その場合においては軌跡の計算が正確ではなくなるため、ストアでの説明上は「凸型形状のメッシュに結晶風のマテリアルを適用するもの」としてあります。
正確でない計算の結果どうなるかと言えば、例えば結晶内部に透明な境界があるように見えたりします。
劇的に色が変わるわけではないので遠目にて気になる可能性は低いと思われますが、よく観察すれば違和感に気付くでしょう。