エンジンを改造してLUTテクスチャを使ったトゥーンシェーダを実装する方法

前の記事

[UE4] エンジンのシェーダコードを変更する 基礎編

の応用編と言っていいのかな?という内容です。

前回の記事ではシェーダコードのみを変更してトゥーンシェーダを実現しましたが、完全な2値化しか実装できていません。

トゥーンシェーダを実装する場合、影の強さを何階調にするのか、境界部分でリニア補間をするのかなどの調整をアーティストさんに任せるのが一般的だと思いますが、前回の方法ではアーティストさんにシェーダを修正させる必要があったり、調整時にシェーダを何度もコンパイルし直す必要がありました。

この階調で行く!というのが完全に決まってしまえば前回の実装でも問題ないのでしょうけど、そこに行くまでに十分な検証をしづらいというのも事実です。

たいてい、トゥーンシェーダを実現する場合はルックアップテーブル(LUT)テクスチャを利用します。

LUTテクスチャというのは主に1次元、もしくは2次元のテクスチャで、UV座標をシェーダで計算した何らかの数値で代用してテクスチャをサンプリング、そしてその結果を利用する手法です。

UE4ではカラコレのLUTがありますが、あんな感じですね。

トゥーンシェーダではたいてい、256階調の1次元テクスチャ(横256、高さ4くらいの2次元テクスチャが一般的?)を用います。

以下のようなテクスチャですね。

ToonLut.png

このようなテクスチャであればアーティストさんは簡単に作成し、調整もできます。

複雑なカーブを用いるような色調でもテクスチャに落とし込むならサンプリング1回程度の手間で済むので簡単です。

しかし、ライティング時にこのテクスチャを読み込み、利用するようにさせるのはシェーダコードの修正だけではどうしようもありません。

そのため、今回はC++コードの修正を行います。

残念ながら、非プログラマの方にはチンプンカンプンかもしれませんが、手順に従えば大丈夫です。

ただし、UE4のバージョンが上がった場合はどうなるかわかりませんのでご注意ください。

まずはコードを修正する前の下準備として、GitHubからエンジンコードを取得、セットアップを行ってVisual Studioでエンジンのビルドを行ってください。

前回も紹介したヒストリアさんのブログを参考にしましょう。

[UE4] エンジンのソースコード取得とビルド手順のまとめ

ビルドが終わりましたか?

では起動して、何かプロジェクトを作成しましょう。

プロジェクトはC++プロジェクトでなくても構いません。

プロジェクトを作成したら立ち上げて、コンテンツブラウザでエンジン側のコンテンツを見られるようにしましょう。

ue317.jpg

チェックを入れると[エンジン コンテンツ]フォルダが表示されるので、この中の[EngineMaterials]というフォルダを開きましょう。

このフォルダに先ほど提示したLUTテクスチャをドラッグ&ドロップします。

これによってエンジン側のコンテンツにLUTテクスチャを追加することが出来ます。

ue318.jpg

エンジン改造後はドラッグ&ドロップした画像ファイルを編集し、再インポートをすれば調整は簡単にできます。

では、続きからでエンジンの修正を行ってみましょう。

まずは.iniファイルに追加を行います。

追加を行う.iniファイルは2つで、以下のように修正します。

Engine/Config/BaseEditor.ini (353付近)

+VersionedIniParams=Engine:/Script/Engine.Engine:PreIntegratedSkinBRDFTextureName

+VersionedIniParams=Engine:/Script/Engine.Engine:ToonLutTextureName

+VersionedIniParams=Engine:/Script/Engine.Engine:MiniFontTextureName

Engine/Config/BaseEngine.ini (62付近)

PreIntegratedSkinBRDFTextureName=/Engine/EngineMaterials/PreintegratedSkinBRDF.PreintegratedSkinBRDF

ToonLutTextureName=/Engine/EngineMaterials/ToonLut.ToonLut

MiniFontTextureName=/Engine/EngineMaterials/MiniFont.MiniFont

エンジンにLUTテクスチャの名前とリソースの位置を提示しています。

次にエンジン初期化時にこれらのテクスチャを読み込むようにしましょう。

Engine/Source/Runtime/Engine/Classes/Engine/Engine.h (1027付近)

/** @todo document */

UPROPERTY(globalconfig)

FStringAssetReference PreIntegratedSkinBRDFTextureName;

UPROPERTY()

class UTexture2D* ToonLutTexture;

UPROPERTY(globalconfig)

FStringAssetReference ToonLutTextureName;

UPROPERTY()

class UTexture2D* MiniFontTexture;

Engine/Source/Runtime/Engine/Private/UnrealEngine.cpp (1331付近)

if( PreIntegratedSkinBRDFTexture == NULL )

{

    PreIntegratedSkinBRDFTexture = LoadObject<UTexture2D>(NULL, *PreIntegratedSkinBRDFTextureName.ToString(), NULL, LOAD_None, NULL);

}

if( ToonLutTexture == NULL )

{

    ToonLutTexture = LoadObject<UTexture2D>(NULL, *ToonLutTextureName.ToString(), NULL, LOAD_None, NULL);

}

if( MiniFontTexture == NULL )

{

    MiniFontTexture = LoadObject<UTexture2D>(NULL, *MiniFontTextureName.ToString(), NULL, LOAD_None, NULL);

}

これでエンジン初期化時にテクスチャが読み込まれます。

テクスチャが読み込まれてもシェーダ側に使用することを通知する必要があります。

今回はディファードレンダリングを行っている部分のみなので、修正するファイルは1つです。

Engine/Source/Runtime/Renderer/Private/LightRendering.cpp

(64付近)

PreIntegratedBRDF.Bind(Initializer.ParameterMap, TEXT("PreIntegratedBRDF"));

PreIntegratedBRDFSampler.Bind(Initializer.ParameterMap, TEXT("PreIntegratedBRDFSampler"));

ToonLut.Bind(Initializer.ParameterMap, TEXT("ToonLut"));

ToonLutSampler.Bind(Initializer.ParameterMap, TEXT("ToonLutSampler"));

IESTexture.Bind(Initializer.ParameterMap, TEXT("IESTexture"));

IESTextureSampler.Bind(Initializer.ParameterMap, TEXT("IESTextureSampler"));

(96付近)

Ar << PreIntegratedBRDF;

Ar << PreIntegratedBRDFSampler;

Ar << ToonLut;

Ar << ToonLutSampler;

Ar << IESTexture;

Ar << IESTextureSampler;

(130付近)

SetTextureParameter(

    RHICmdList,

    ShaderRHI,

    PreIntegratedBRDF,

    PreIntegratedBRDFSampler,

    TStaticSamplerState<SF_Bilinear,AM_Clamp,AM_Clamp,AM_Clamp>::GetRHI(),

    GEngine->PreIntegratedSkinBRDFTexture->Resource->TextureRHI

    );

SetTextureParameter(

    RHICmdList,

    ShaderRHI,

    ToonLut,

    ToonLutSampler,

    TStaticSamplerState<SF_Bilinear,AM_Clamp,AM_Clamp,AM_Clamp>::GetRHI(),

    GEngine->ToonLutTexture->Resource->TextureRHI

    );

{

(160付近)

FShaderResourceParameter PreIntegratedBRDF;

FShaderResourceParameter PreIntegratedBRDFSampler;

FShaderResourceParameter ToonLut;

FShaderResourceParameter ToonLutSampler;

FShaderResourceParameter IESTexture;

FShaderResourceParameter IESTextureSampler;

C++コードの修正はここまでです。

修正を行ったらビルドして、正常にビルドが終了するのを確認しましょう。

最後にシェーダコードの修正を行います。

Engine/Shaders/DeferredLightingCommon.usf

(520付近)

Texture2D  ToonLut;

SamplerState ToonLutSampler;

/** Calculates lighting for a given position, normal, etc with a fully featured lighting model designed for quality. */

float4 GetDynamicLighting(float3 WorldPosition, float3 CameraVector, float2 InUV, FScreenSpaceData ScreenSpaceData, uint ShadingModelID, FDeferredLightData LightData, float4 LightAttenuation, uint2 Random)

{

(532付近)

float3 V = -CameraVector;

float3 N = ScreenSpaceData.GBuffer.WorldNormal;

float3 ToLight = LightData.LightDirection;

float3 L = ToLight; // no need to normalize

float NoL = saturate(dot(N, L) * 0.5f + 0.5f);

float DistanceAttenuation = 1;

float LightRadiusMask = 1;

float SpotFalloff = 1;

(620付近)

// accumulate diffuse and specular

{

    float ToonNoL = LightData.bRadialLight ?

        NoL * SurfaceAttenuation :

        Texture2DSampleLevel(ToonLut, ToonLutSampler, float2(NoL, 0.5f), 0).r;

#if 1 // for testing if there is a perf impact

    // correct screen space subsurface scattering

    float3 SurfaceLightingDiff = SurfaceShading(ScreenSpaceData.GBuffer, LobeRoughness, LobeEnergy, L, V, N, float2(1, 0));

    float3 SurfaceLightingSpec = SurfaceShading(ScreenSpaceData.GBuffer, LobeRoughness, LobeEnergy, L, V, N, float2(0, 1));

    LightAccumulator_Add(LightAccumulator, SurfaceLightingDiff, SurfaceLightingSpec, LightColor * ToonNoL);

#else

以上です。

何をやっているのか簡単に解説しますと、

520行付近の処理はテクスチャとサンプラを定義しています。

532行付近の処理は法線とライトの内積を修正しました。

通常、法線とライトの内積は -1~1 の範囲を取りますが、UE4のデフォルトでは -1~0 を 0 として扱います。

トゥーンシェーディングでもこのやり方で問題ないのですが、今回は折角なので -1~0 にも意味を持たせられるようにしました。

例えば、-0.5 までは光が入るようにすることが可能になっているというわけです。

最終的には -1~1 の値がLUTテクスチャをサンプリングするためのU座標として利用するため、これを 0~1 の値に圧縮しているのがこの処理です。

最後は先の法線とライトの内積からLUTテクスチャをサンプリングするのですが、この時に LightData.bRadialLight フラグが立っていたらサンプリングせずにそのままの内積値を利用するようにしています。

このフラグは、現在処理を行っているライトが平行光源かそれ以外(ポイントライト、スポットライト)かを示すフラグで、フラグが立っている場合は平行光源ではありません。

個人的な印象として、トゥーンシェーディングとポイントライトはあまり相性が良くないと感じています。

距離減衰が存在するポイントライトやスポットライトでは、減衰前にLUTサンプリングを行うと減衰によってトゥーンっぽくならないし、減衰後にLUTサンプリングを行うと距離の2乗減衰のためかほとんどライティング効果が見込めなくなります。

という、割と個人的な理由で、トゥーンシェーディングが有効になるのは平行光源の時のみとしました。

ここまで終わったらプロジェクトを立ち上げて見ましょう。

ライティングがトゥーンぽくなっているはずです。

LUTテクスチャは先にも述べたとおり、元ファイルを編集して再インポートすれば変更することが出来ます。

LUTを変更してみた結果をいくつか提示します。

ue319.jpg 

今回の実装方法の弱点としては、LUTをアプリで固定してしまう点です。

例えば、LUTをレベルによって変更したい、モデルごとに変更したい、ライトごとに変更したいという場合には対応ができません。

レベルによる変更はもう少し修正すれば出来るんじゃないかとは思いますが、今回ほど簡単ではありません。

ライトごとも多分そこまで難しいことはないと思いますが、多分あまり使わないでしょう。

モデルごとやマテリアルごとはかなり難しいですね。

ディファードレンダリングではライティングを行う段階でどのマテリアルか、という情報は通常は埋め込んでいません。

そのため、このマテリアルはこのLUTを使う、という情報がライティング段階で欠落してしまっています。

ただ、同じようなことを Subsurface Profile でも行っているので、何らかの方法で実装は可能と思われます。

多分こんな感じで実装しているんだろうな、という予想はついていますが、トゥーンでそれをやるには結構な修正が必要になると考えられます。

まあ、経験上、LUTをモデルやマテリアルごとに変更するようなことはほとんど行いません。

やるのはレベルごとと、背景とキャラで分けるくらいでしょうか。

背景とキャラで分ける、というのもディファードでは難しいですが。

というわけで今回はここまで。