UnrealEngine改造 カスタムシェーディングへ ~ShadingModelの追加~

概要

UnrealEngineで独自のシェーディング表現を使いたいとき、UnealEngineの改造を行うという手法をとれます。 Unityを使用していた時と比べるとパッとRenderingFeatureを追加してパスを増やしたりシェーダーを追加したりすることができる感覚がありましたがUnrealEngineではそうはいきません。

シェーディングモデルを追加すると独自のカスタム表現を行えますし、マテリアルピンの追加・削除を行うことができます。 今回はUE5.4.4からエンジンを改造し、独自のシェーディングモデルを追加していきましょう。

前提条件

この記事では

として改造を行っていきます。

環境整備

まずUnrealEngineのソースコードを取得しなければいけません。以下にアクセスします。

https://www.unrealengine.com/ja/ue-on-github

ここに書いてある通り進めていけば大丈夫です。

EpicとGitHubのアカウント連携が終了すれば以下のようにリポジトリを閲覧することができます。 ここからリポジトリをクローンします。

この後の操作をする前に(容量が大きくなる前に)ブランチは改造を行うブランチに変更しておきましょう。(今回は5.4ブランチにチェックアウト)

その後、Setup.batを叩いて、その後にGenerateProjectFiles.batをたたきます。

VisualStudioで開いた後、UE5をスタートアッププロジェクトに設定し、DevelopmentEditor>Win64を選択した後ソリューションのリビルドを行います。

ビルドは自分のPCでは2-3時間かかったので気長に待ちましょう、、

ビルドを成功したらテンプレートの画面が出てきます。

プロジェクトを新しく作る場合はここから選びますが基本的にエンジンが置かれているディレクトリに配置したほうがパスを通すのが楽です。 プロジェクトを作成してからVisualStudioに戻るとファイルの変更検知が入るので「再読み込み」を選択します。

ソリューションエクスプローラーにプロジェクトが追加されていたらそちらを「スタートアッププロジェクトに設定」します。

ビルドエラー解消

普通にビルドが通ればいいのですが、自分の場合はエラーが出て失敗してしまいました。

いろいろ疑うところは結構多いのですが、一番最初はVisualStudioのバージョンを見てみてください。 自分はVisualStudioが2022 17.11.2を使っていてかなりのエラーが出たのですが、2022 17.11.3にアップデートしたらたくさんのエラーが消えました。

その次にWindowsSDKでWindows11 SDK 10.0.22621.2428をクリーンインストールすることで定数オーバーフローの問題を解決できました。

コードベースでいうと VisualStudioでビルド -> 構成マネージャ で AutoRTFMTests のチェックをすべてはずした状態にしてから以下のコードを追加します。

Engine\Source\Programs\AutoRTFMTests\AutoRTFMTests.Target.cs

// ~~~ (中略) ~~~
    public AutoRTFMTestsTarget(TargetInfo Target) : base(Target)
    {
// ~~~ (中略) ~~~
        bFNameOutlineNumber = true;
        // ビルドエラー解消のため追加
        bBuildInSolutionByDefault = false;
    }
}

参考は以下です。 zenn.dev

ビルドエラーに関しては環境の問題が多いかと思いますが参考になった記事を列挙します。

UE5.4 エンジンビルド時のエラー対応

UE5エラー解決集

5.4 source build errors - #7 by monstercoo - Platform & Builds - Epic Developer Community Forums

Plugin Build fails only on UE5.4 - Platform & Builds - Epic Developer Community Forums

UnrealEngineではまったもの

UE5.1 VS2022 C++ Build Error With MSB3073 - #24 by wr.lex - Platform & Builds - Epic Developer Community Forums

DatasmithAssetUserData.h / UE 5.4.3 / Visual Studio 2022 / Compile error - Programming & Scripting - Epic Developer Community Forums

エンジン改造

ではエンジンを改造していきましょう。

まずはシェーディングモデルの追加です。MaterialShadingModelの略称でMSMをつけるのが慣習となっています。

Runtime/Engine/Classes/Engine/EngineTypes.h

UENUM()
enum EMaterialShadingModel : int
{
    MSM_Unlit                     UMETA(DisplayName = "Unlit"),
    MSM_DefaultLit               UMETA(DisplayName = "Default Lit"),
    MSM_Subsurface               UMETA(DisplayName = "Subsurface"),
    MSM_PreintegratedSkin        UMETA(DisplayName = "Preintegrated Skin"),
    MSM_ClearCoat                UMETA(DisplayName = "Clear Coat"),
    MSM_SubsurfaceProfile        UMETA(DisplayName = "Subsurface Profile"),
    MSM_TwoSidedFoliage           UMETA(DisplayName = "Two Sided Foliage"),
    MSM_Hair                     UMETA(DisplayName = "Hair"),
    MSM_Cloth                    UMETA(DisplayName = "Cloth"),
    MSM_Eye                      UMETA(DisplayName = "Eye"),
    MSM_SingleLayerWater         UMETA(DisplayName = "SingleLayerWater"),
    MSM_ThinTranslucent           UMETA(DisplayName = "Thin Translucent"),
    MSM_Strata                   UMETA(DisplayName = "Substrate", Hidden),

    //// 2025/01/14 @EguchiDaiki
    MSM_ToonLit                  UMETA(DisplayName = "Toon Lit"),
    //// 2025/01/14 @EguchiDaiki

    /** Number of unique shading models. */
    MSM_NUM                      UMETA(Hidden),

    /** Shading model will be determined by the Material Expression Graph,
        by utilizing the 'Shading Model' MaterialAttribute output pin. */
    MSM_FromMaterialExpression   UMETA(DisplayName = "From Material Expression"),

    MSM_MAX
};

シェーディングモデルを取得する関数にも追加したシェーディングモデルを追加しましょう。

Runtime/Engine/Private/Materials/MaterialShader.cpp

FString GetShadingModelString(EMaterialShadingModel ShadingModel)
{
    FString ShadingModelName;
    switch(ShadingModel)
    {
        case MSM_Unlit:                ShadingModelName = TEXT("MSM_Unlit"); break;
        case MSM_DefaultLit:        ShadingModelName = TEXT("MSM_DefaultLit"); break;
        case MSM_Subsurface:        ShadingModelName = TEXT("MSM_Subsurface"); break;
        case MSM_PreintegratedSkin:    ShadingModelName = TEXT("MSM_PreintegratedSkin"); break;
        case MSM_ClearCoat:            ShadingModelName = TEXT("MSM_ClearCoat"); break;
        case MSM_SubsurfaceProfile:    ShadingModelName = TEXT("MSM_SubsurfaceProfile"); break;
        case MSM_TwoSidedFoliage:    ShadingModelName = TEXT("MSM_TwoSidedFoliage"); break;
        case MSM_Hair:                ShadingModelName = TEXT("MSM_Hair"); break;
        case MSM_Cloth:                ShadingModelName = TEXT("MSM_Cloth"); break;
        case MSM_Eye:                ShadingModelName = TEXT("MSM_Eye"); break;
        case MSM_SingleLayerWater:    ShadingModelName = TEXT("MSM_SingleLayerWater"); break;
        case MSM_ThinTranslucent:    ShadingModelName = TEXT("MSM_ThinTranslucent"); break;
    //// 2025/01/14 @EguchiDaiki
        case MSM_ToonLit:            ShadingModelName = TEXT("MSM_ToonLit"); break;
    //// 2025/01/14 @EguchiDaiki
        default: ShadingModelName = TEXT("Unknown"); break;
    }
    return ShadingModelName;
}

1934行に追加です。シェーディングモデルにToonLitが追加されている場合はSetDefineによって定数を立てます。

Runtime/Engine/Private/Materials/HLSLMaterialTranslator.cpp

void FHLSLMaterialTranslator::GetMaterialEnvironment(EShaderPlatform InPlatform, FShaderCompilerEnvironment& OutEnvironment)
{
    if (ShadingModels.IsLit())
    {    
  //// 2025/01/14 @EguchiDaiki
        if (ShadingModels.HasShadingModel(MSM_ToonLit))
  {
     OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_TOON_LIT"), TEXT("1"));
     NumSetMaterials++;
    }
  //// 2025/01/14 @EguchiDaiki
    }
}

Runtime/Engine/Private/Materials/MaterialHLSLEmitter.cpp

ここではTranslatorと同様に処理を行います。

static void GetMaterialEnvironment(EShaderPlatform InPlatform,
    const FMaterial& InMaterial,
    const UE::HLSLTree::FEmitContext& EmitContext,
    const FMaterialCompilationOutput& MaterialCompilationOutput,
    FShaderCompilerEnvironment& OutEnvironment)
{
///// 中略
if (ShadingModels.IsLit())
    {
//// 中略
//// @EguchiDaiki
        if (ShadingModels.HasShadingModel(MSM_ToonLit))
        {
            OutEnvironment.SetDefine(TEXT("MATERIAL_SHADINGMODEL_TOON_LIT"), TEXT("1"));
            NumSetMaterials++;
        }
}
//// @EguchiDaiki

Runtime/RenderCore/Public/ShaderMaterial.h

こちらではToonLitに対応したシェーダーのバリエーションの判定変数を立てます。

struct FShaderMaterialPropertyDefines
{
    //DECLARE_TYPE_LAYOUT(FShaderMaterialPropertyDefines, NonVirtual);
    //void ModifyEnvironment(FShaderCompilerEnvironment& OutEnvironment) const;
    //void WriteFrozenVertexFactoryParameters(FMemoryImageWriter& Writer, const TMemoryImagePtr<FShaderMaterialPropertyDefines>& InPropDefines) const;
    uint8 MATERIAL_ENABLE_TRANSLUCENCY_FOGGING : 1;
    uint8 MATERIALBLENDING_ANY_TRANSLUCENT : 1;
    uint8 MATERIAL_USES_SCENE_COLOR_COPY : 1;
    uint8 MATERIALBLENDING_MASKED_USING_COVERAGE : 1;
    uint8 MATERIAL_COMPUTE_FOG_PER_PIXEL : 1;
    uint8 MATERIAL_SHADINGMODEL_UNLIT : 1;
    uint8 MATERIAL_SHADINGMODEL_DEFAULT_LIT : 1;
    uint8 MATERIAL_SHADINGMODEL_SUBSURFACE : 1;
    uint8 MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN : 1;
    uint8 MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE : 1;
    uint8 MATERIAL_SHADINGMODEL_CLEAR_COAT : 1;
    uint8 MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE : 1;
    uint8 MATERIAL_SHADINGMODEL_HAIR : 1;
    uint8 MATERIAL_SHADINGMODEL_CLOTH : 1;
    uint8 MATERIAL_SHADINGMODEL_EYE : 1;
    uint8 MATERIAL_SHADINGMODEL_SINGLELAYERWATER : 1;
    uint8 SINGLE_LAYER_WATER_SEPARATED_MAIN_LIGHT : 1;
    uint8 MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT : 1;
//// @EguchiDaiki
    uint8 MATERIAL_SHADINGMODEL_TOON_LIT : 1;
//// @EguchiDaiki

Runtime/Engine/Private/ShaderCompiler/ShaderGenerationUtil.cpp

TranslatorやEmitterでSetDefineした場合、ShaderMaterial.hに定義したMATERIAL_SHADING_MODEL_LITにSetDefineした値が格納されます。 マクロであるFETCH_COMPILE_BOOLを立てます。

void FShaderCompileUtilities::ApplyFetchEnvironment(FShaderMaterialPropertyDefines& SrcDefines, FShaderCompilerEnvironment& OutEnvironment)
{
    FETCH_COMPILE_BOOL(MATERIAL_ENABLE_TRANSLUCENCY_FOGGING);
    FETCH_COMPILE_BOOL(MATERIALBLENDING_ANY_TRANSLUCENT);
    FETCH_COMPILE_BOOL(MATERIAL_USES_SCENE_COLOR_COPY);
    FETCH_COMPILE_BOOL(MATERIALBLENDING_MASKED_USING_COVERAGE);
    FETCH_COMPILE_BOOL(MATERIAL_COMPUTE_FOG_PER_PIXEL);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_UNLIT);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_DEFAULT_LIT);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SUBSURFACE);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_CLEAR_COAT);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_HAIR);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_CLOTH);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_EYE);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_SINGLELAYERWATER);
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT);
//// @EguchiDaikoi
    FETCH_COMPILE_BOOL(MATERIAL_SHADINGMODEL_TOON_LIT);
//// @EguchiDaiki

Runtime/Engine/Private/ShaderCompiler/ShaderGenerationUtil.cpp

バリエーションに対応してシェーディングモデルが使用するGBUFFERスロットを指定します。Slotに格納した情報をもとにGBUFFERのエンコード、デコードしたシェーダコードが生成されます。

static void DetermineUsedMaterialSlots(
    bool Slots[],
    const FShaderMaterialDerivedDefines& Dst,
    const FShaderMaterialPropertyDefines& Mat,
    const FShaderLightmapPropertyDefines& Lightmap,
    const FShaderGlobalDefines& SrcGlobal,
    const FShaderCompilerDefines& Compiler,
    ERHIFeatureLevel::Type FEATURE_LEVEL)
{
    //// 中略
    // doesn't write to GBuffer
    if (Mat.MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT)
    {
    }
//// @EguchiDaiki
    if (Mat.MATERIAL_SHADINGMODEL_TOON_LIT)
    {
        SetStandardGBufferSlots(Slots, bWriteEmissive, bHasTangent, bHasVelocity, bHasStaticLighting, bIsStrataMaterial);
        Slots[GBS_CustomData] = bUseCustomData;
    }
//// @EguchiDaiki

Shaders/Private/Definitions.usf

定義ファイルに未定義の場合の処理を担保させるために追記します。

#ifndef MATERIAL_SHADINGMODEL_UNLIT
#define    MATERIAL_SHADINGMODEL_UNLIT                        0
#endif
#ifndef MATERIAL_SHADINGMODEL_SINGLELAYERWATER
#define MATERIAL_SHADINGMODEL_SINGLELAYERWATER            0
#endif
//// @EguchiDaiki
#ifndef MATERIAL_SHADINGMODEL_TOON_LIT
#define MATERIAL_SHADINGMODEL_TOON_LIT                    0
#endif
//// @EguchiDaiki

Shaders/Private/ShadingCommon.ush

シェーディングモデルIDのナンバリングをEngineTypes.hで定義したEMaterialShadingModelと一致させます。

// SHADINGMODELID_* occupy the 4 low bits of an 8bit channel and SKIP_* occupy the 4 high bits
#define SHADINGMODELID_UNLIT                0
#define SHADINGMODELID_DEFAULT_LIT            1
#define SHADINGMODELID_SUBSURFACE            2
#define SHADINGMODELID_PREINTEGRATED_SKIN    3
#define SHADINGMODELID_CLEAR_COAT            4
#define SHADINGMODELID_SUBSURFACE_PROFILE    5
#define SHADINGMODELID_TWOSIDED_FOLIAGE        6
#define SHADINGMODELID_HAIR                    7
#define SHADINGMODELID_CLOTH                8
#define SHADINGMODELID_EYE                    9
#define SHADINGMODELID_SINGLELAYERWATER        10
#define SHADINGMODELID_THIN_TRANSLUCENT        11
#define SHADINGMODELID_STRATA                12        // Temporary while we convert everything to Strata
//// @EguchiDaiki
#if 0
#define SHADINGMODELID_NUM                    13
#else
#define SHADINGMODELID_TOON_LIT                13
#define SHADINGMODELID_NUM                    14
#endif
//// @ EguchiDaiki
#define SHADINGMODELID_MASK                    0xF        // 4 bits reserved for ShadingModelID    
//// @EguchiDaiki

次にデバッグ用途で使用されるシェーディングモデルIDの色を取得する関数にも色の定義を行います。 PS4ではswitch文の最適化が不適切なためにPS4用にif文の処理とそれ以外で使用するswitchの2種類を用意します。

#define SSS_PROFILE_ID_INVALID 256
#define SSS_PROFILE_ID_PERPIXEL 512
// for debugging and to visualize
float3 GetShadingModelColor(uint ShadingModelID)
{
    // TODO: PS4 doesn't optimize out correctly the switch(), so it thinks it needs all the Samplers even if they get compiled out
    //    This will get fixed after launch per Sony...
#if PS4_PROFILE
         if (ShadingModelID == SHADINGMODELID_UNLIT) return float3(0.1f, 0.1f, 0.2f); // Dark Blue
    else if (ShadingModelID == SHADINGMODELID_DEFAULT_LIT) return float3(0.1f, 1.0f, 0.1f); // Green
    else if (ShadingModelID == SHADINGMODELID_SUBSURFACE) return float3(1.0f, 0.1f, 0.1f); // Red
    else if (ShadingModelID == SHADINGMODELID_PREINTEGRATED_SKIN) return float3(0.6f, 0.4f, 0.1f); // Brown
    else if (ShadingModelID == SHADINGMODELID_CLEAR_COAT) return float3(0.1f, 0.4f, 0.4f);
    else if (ShadingModelID == SHADINGMODELID_SUBSURFACE_PROFILE) return float3(0.2f, 0.6f, 0.5f); // Cyan
    else if (ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE) return float3(0.2f, 0.2f, 0.8f); // Blue
    else if (ShadingModelID == SHADINGMODELID_HAIR) return float3(0.6f, 0.1f, 0.5f);
    else if (ShadingModelID == SHADINGMODELID_CLOTH) return float3(0.7f, 1.0f, 1.0f);
    else if (ShadingModelID == SHADINGMODELID_EYE) return float3(0.3f, 1.0f, 1.0f);
    else if (ShadingModelID == SHADINGMODELID_SINGLELAYERWATER) return float3(0.5f, 0.5f, 1.0f);
    else if (ShadingModelID == SHADINGMODELID_THIN_TRANSLUCENT) return float3(1.0f, 0.8f, 0.3f);
    else if (ShadingModelID == SHADINGMODELID_STRATA) return float3(1.0f, 1.0f, 0.0f);
//// @EguchiDaiki
    else if (ShadingModelID == SHADINGMODELID_TOON_LIT) return float3(1.0f, 0.4f, 0.0f);
//// @EguchiDaiki
    else return float3(1.0f, 1.0f, 1.0f); // White
#else
    switch(ShadingModelID)
    {
        case SHADINGMODELID_UNLIT: return float3(0.1f, 0.1f, 0.2f); // Dark Blue
        case SHADINGMODELID_DEFAULT_LIT: return float3(0.1f, 1.0f, 0.1f); // Green
        case SHADINGMODELID_SUBSURFACE: return float3(1.0f, 0.1f, 0.1f); // Red
        case SHADINGMODELID_PREINTEGRATED_SKIN: return float3(0.6f, 0.4f, 0.1f); // Brown
        case SHADINGMODELID_CLEAR_COAT: return float3(0.1f, 0.4f, 0.4f); // Brown
        case SHADINGMODELID_SUBSURFACE_PROFILE: return float3(0.2f, 0.6f, 0.5f); // Cyan
        case SHADINGMODELID_TWOSIDED_FOLIAGE: return float3(0.2f, 0.2f, 0.8f); // Cyan
        case SHADINGMODELID_HAIR: return float3(0.6f, 0.1f, 0.5f);
        case SHADINGMODELID_CLOTH: return float3(0.7f, 1.0f, 1.0f);
        case SHADINGMODELID_EYE: return float3(0.3f, 1.0f, 1.0f);
        case SHADINGMODELID_SINGLELAYERWATER: return float3(0.5f, 0.5f, 1.0f);
        case SHADINGMODELID_THIN_TRANSLUCENT: return float3(1.0f, 0.8f, 0.3f);
        case SHADINGMODELID_STRATA: return float3(1.0f, 1.0f, 0.0f);
    //// @EguchiDaiki
        case SHADINGMODELID_TOON_LIT: return float3(1.0f, 0.4f, 0.0f);
    //// @EguchiDaiki
        default: return float3(1.0f, 1.0f, 1.0f); // White
    }
#endif
}

Shaders/Private/ShadingModels.ush

ToonLitの独自シェーディングモデルを使用している際のライティング処理を追加します。

独自のライティング処理はこの新しく追加した関数にいろいろと定義していきますが一旦は何もせず黒を返します。

FDirectLighting PreintegratedSkinBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
    FDirectLighting Lighting = DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
    
    half3 SubsurfaceColor = ExtractSubsurfaceColor(GBuffer);
    half Opacity = GBuffer.CustomData.a;
    half3 PreintegratedBRDF = Texture2DSampleLevel(View.PreIntegratedBRDF, View.PreIntegratedBRDFSampler, float2(saturate(dot(N, L) * .5 + .5), 1 - Opacity), 0).rgb;
    Lighting.Transmission = AreaLight.FalloffColor * Falloff * PreintegratedBRDF * SubsurfaceColor;
    return Lighting;
}
//// @EguchiDaiki
FDirectLighting ToonLitBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow)
{
    FDirectLighting Lighting = (FDirectLighting)0;
    return Lighting;
}
//// @EguchiDaiki

その独自のライティング関数を使用するため以下のように処理を追加します。

FDirectLighting IntegrateBxDF( FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, half NoL, FAreaLight AreaLight, FShadowTerms Shadow )
{
    switch( GBuffer.ShadingModelID )
    {
        case SHADINGMODELID_DEFAULT_LIT:
        case SHADINGMODELID_SINGLELAYERWATER:
        case SHADINGMODELID_THIN_TRANSLUCENT:
            return DefaultLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_SUBSURFACE:
            return SubsurfaceBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_PREINTEGRATED_SKIN:
            return PreintegratedSkinBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_CLEAR_COAT:
            return ClearCoatBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_SUBSURFACE_PROFILE:
            return SubsurfaceProfileBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_TWOSIDED_FOLIAGE:
            return TwoSidedBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_HAIR:
            return HairBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_CLOTH:
            return ClothBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
        case SHADINGMODELID_EYE:
            return EyeBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
    //// @EguchiDaiki
        case SHADINGMODELID_TOON_LIT:
            return ToonLitBxDF( GBuffer, N, V, L, Falloff, NoL, AreaLight, Shadow );
    //// @EguchiDaiki
        default:
            return (FDirectLighting)0;
    }
}

Shaders/Private/ReflectionEnvironmentPixelShader.usf

一旦環境光や反射表現の計算をスキップするようにします。 またシェーダー内部の仕様が決まったらこちらも詰めて書いていきます。

void ReflectionEnvironmentSkyLighting(
    in float4 SvPosition : SV_Position,
    out float4 OutColor : SV_Target0
#if STRATA_OPAQUE_ROUGH_REFRACTION_ENABLED
    , out float3 OutOpaqueRoughRefractionSceneColor : SV_Target1
    , out float3 OutSubSurfaceSceneColor : SV_Target2
#endif
    )
{
    uint2 PixelPos = SvPosition.xy;
    float2 BufferUV = SvPositionToBufferUV(SvPosition);
    float2 ScreenPosition = SvPositionToScreenPosition(SvPosition).xy;
    
    OutColor = 0.0f;
#if STRATA_OPAQUE_ROUGH_REFRACTION_ENABLED
    OutOpaqueRoughRefractionSceneColor = 0.0f;
    OutSubSurfaceSceneColor = 0.0f;
#endif
#if STRATA_ENABLED
//// 中略
#else // STRATA_ENABLED
    // Sample scene textures.
    FGBufferData GBuffer = GetGBufferDataFromSceneTextures(BufferUV);
    uint ShadingModelID = GBuffer.ShadingModelID;
//// @EguchiDaiki
#if 0
    const bool bUnlitMaterial = ShadingModelID == SHADINGMODELID_UNLIT;
#else
    const bool bUnlitMaterial = ShadingModelID == SHADINGMODELID_UNLIT || ShadingModelID == SHADINGMODELID_TOON_LIT;
#endif
//// @EguchiDaiki
    float3 DiffuseColor = GBuffer.DiffuseColor;
    float3 SpecularColor = GBuffer.SpecularColor;
    RemapClearCoatDiffuseAndSpecularColor(GBuffer, ScreenPosition, DiffuseColor, SpecularColor);

結果

上記を行ってリビルドを実行します。

ビルドが成功した後、プロジェクトを開きMaterialEditorからシェーディングモデルを確認するとToonLitが追加されていたことを確認します。

ただ、これを使用してもライティングは未実装なので真っ黒になるかと思います。

次はライティングの内部、そしてトゥーンシェーディングの実装をどんどん行っていきます。

参考文献

cgworld.jp

UnityでSRPからデプスシャドウを描く ~複数のDirectionalLightの影を描く~

概要

前記事ではUnityでステンシルボリュームシャドウを描画する方法について書きました。

daiki-eguchi.hatenablog.com

昨今のGPUではテクセルフィルレートが劇的に向上したことで昨今主流となったのがデプスシャドウ技法のようです。 参考: 3Dグラフィックス・マニアックス(24) 影の生成(5)~デプスシャドウ技法 | マイナビニュース

テクセルフィルレートというのはテクスチャを構成する単位のテクセルの処理速度をいい、処理速度が昨今早くなったことから主流になった手法ということですね。

デプスシャドウ技法とは

光源を仮想の視点と捉えて光源から光が当たっているかどうかをシャドウマップに書き込む。 その後、シャドウマップを投影テクスチャマッピングの用法でオブジェクトに貼り付けるイメージで描画することで影が描画される仕組みです。

光源から光が届いているかどうかをバッファに書き込む

ではどのようにして「光が当たっているかどうか」を書き込むかです。

カメラから見て、レンダリングしようとしているピクセルと光源までの距離をdとして、光源からそのピクセルまで飛ばしたRayが衝突したところまでの距離をsとします。

以下のように2点考えると、s=dとなるところはレンダリングしようとしているピクセルと光源の間に遮蔽物がなかったことになるので「影ではない」ということになります。

d>sとなるところではレンダリングしようとしているピクセルと光源の間に遮蔽物があったことになるので「影」ということになります。

Unityでデプスシャドウ技法を行う

Unityでデプスシャドウ技法を行うとき、シャドウマップの描画はUnityがよしなにやってくれます。ただ今回はせっかくなのでSRPからシャドウマップの描画自体も行ってみましょう。

SRPからデプスシャドウ技法を行う

SRPからデプスシャドウ技法を行っていこうと思いますが、素晴らしい記事があるのでこちらを参照してください。 この記事を実装するとDirectionalLight1つのシャドウを描画することができます。

今回は、僭越ながらこの記事を実装した前提で複数のDirectionalLightの影を描くように実装してみます。

qiita.com

qiita.com

成果物

まずは今回の成果物です。DirectionalLightを4つおいて様々な方向を向かせていますがどのライトからのシャドウも描画されています。

シャドウマップの描画

では複数のDirectionalLightの影を落とすときはどのようにしたらよいのでしょうか。

今回、シャドウマップは以下のように描画されています。(シャドウオンのライトが4つの時)

これは、大きなシャドウマップに対して以下のようにそれぞれのライトのシャドウを分割して描画されています。

今回、影を落とせるライトの最大数を4つとしたので4つにシャドウマップを分けています。しかし、最大数が4だとしてもシーンにDirectionalLightが1つのときにこの分割数だとシャドウの品質が下がってしまいます。デプスシャドウによるシャドウの品質はシャドウマップの解像度と直結するからです。

そのため、DirectionalLightが1つの時は以下のように分けずにシャドウを描画します。

実装

コード全文は長いので折りたたみました。大事なところは抽出して説明いたします。

複数のDirectionalLightの影を描画するため、C#側は大きく二つのことを行います。ひとつは上記でお話ししたシャドウマップ描画のタイリング。そしてもう一つはライトのパラメーターを複数シェーダーに渡すために行う配列化です。

シャドウマップのタイリング

上記で説明したようにライトが1つだったときと2~4つだった時でシャドウのタイリングが変わります。Render関数内でその判定を行っています。

ライトの個数が1つの時はRenderPipelineAsset側で指定したシャドウ解像度が分割後のタイルサイズと一致するように。つまりシャドウマップ用のバッファ全体に影を描画できるようにします。

ライトの個数が2つ以上の時はRenderPipelineAsset側で指定したシャドウ解像度を2分割し、タイルのサイズとします。これによって、一枚のシャドウマップ用バッファに4つのライトシャドウ情報を入れることができます。

シャドウのサイズを2分の1にすれば4つのライト情報をシャドウマップ1枚に収めることができる

var lightIndexes = SearchLightIndexes(cullingResults, LightType.Directional);
int splitCount = lightIndexes.Count <= 1 ? 1 : 2;
float tileSize = shadowResolution / splitCount;

シャドウマップへの描画はDrawShadowで行っています。上記で説明したシャドウマップの分割を以下のSetTileViewport(cmd, index, split, tileSize, shadowResolution);で行っています。

private void DrawShadow(ScriptableRenderContext context, CommandBuffer cmd, CullingResults cullingResults,
        int lightIndex, int index, int split, float tileSize, Light light, int shadowResolution, float shadowDistance)
{
    cmd.Clear();
    cmd.SetRenderTarget(LightShadowId);

    // この関数でシャドウマップの描画のタイリングを行っている
    SetTileViewport(cmd, index, split, tileSize, shadowResolution);
   // 以下略
}

SetTileViewportの中身は大事なTile分割です。

各ライトのシャドウに対してoffsetと名付けたVector2をフィールド変数に保存しています。各ライトがシャドウマップのどこに描画しているかを表すものです。これが必要なのはShader側も、どのライトのシャドウ情報が一枚のシャドウマップのどこからどこまでにあるのかを知る必要があるためです。

実際にタイリングしているのはcmd.SetViewport(new Rect(offset.x * tileSize, offset.y * tileSize, tileSize, tileSize));の部分です。

private Vector2 SetTileViewport(CommandBuffer cmd, int index, int split, float tileSize, int shadowResolution)
{
    Vector2 offset = new Vector2(index % split, index / split);
    Vector2 tileOffset = offset * tileSize;
    Offsets[index] = tileOffset / shadowResolution;
    cmd.SetViewport(new Rect(offset.x * tileSize, offset.y * tileSize, tileSize, tileSize));

    return offset;
}

ライト情報の配列化

参考の記事でfloat定義であったLightのパラメーターを配列に変更します。複数のライトがあり、それぞれにBias等の設定があるのでそれを反映するためになります。

private readonly float[] LightShadowBiases = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly float[] LightShadowNormalBiases = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];
private readonly float[] LightShadowDistanceSqrts = new float[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT];

そして、ループを回してすべてのライトのShadowMapを描いた後にパラメーターをShader側に渡すように変更しています。ライトの情報をすべて配列に格納し終えてからShaderに渡すためです。

if (existValidLight)
{
    SetupLightRT(context, cmd, shadowResolution, lightIndexes.Count);
                
    for (int i = 0; i < lightIndexes.Count; i++)
    {
        var lightIndex = lightIndexes[i];
        var light = cullingResults.visibleLights[lightIndex];

        var lightVP = CalcLightViewProjection(cullingResults, lightIndex);
                    
        SetupLightVP(context, cmd, lightVP);
        DrawShadow(context, cmd, cullingResults, lightIndex, i, splitCount, tileSize, light.light, shadowResolution, shadowDistance);   
    }
                
    SetupLightParameters(context, cmd);
}

オブジェクトシェーダー側

オブジェクトにアタッチするシェーダー側の変更点もC#側の変更に合わせて大きく二つです。

一つ目はDirectionalLightが複数あるのでフラグメントシェーダーで複数のライトの処理を行えるようにループ処理させることです。以下はフラグメントシェーダー内でループ処理している部分の抽出です。

for (int t = 0; t < _DirectionalLightCount; t++)
{
    float dotNL = max(0, dot(normalize(i.normalWS), _LightDirs[t]));

    half3 diffuse = _LightColors[t] * dotNL;
    float shadowAttenuation = GetShadowAttenuation(i.positionWS, dotNL, t, _DirectionalLightCount);

    color *= diffuse * shadowAttenuation;
}

そしてもう一つはシャドウマップのサンプリング位置の調整です。以下のコードはシャドウを求めているところですが、参考の記事と比べてライトのパラメーターが配列化しているのでその変更部分。そして各ライトのシャドウがシャドウマップ内のどこにあるのかをC#側からもらっているのでサンプリング位置の調整が入っています。

float GetShadowAttenuation(float4 positionWS, float dotNL, int tileIndex, int split)
{
    // ワールド空間からLVP空間へと変換する
    float4 positionLVP = TransformWorldToLightViewProjection(positionWS, tileIndex);
    // LVP空間からシャドウマップのUV座標へと変換する
    float2 samplingUv = 0;
    float2 uv = positionLVP.xy / positionLVP.w * float2(0.5f, -0.5f) + 0.5f;

    float scale = split == 1 ? 1 : 0.5;
    samplingUv = Offsets[tileIndex] + uv * scale;

    float3 positionVS = mul(unity_MatrixV, positionWS);
    float distanceSqrt = dot(positionVS.xyz, positionVS.xyz);

    float zInLVP = CalcLightViewProjectionDepth(positionLVP);
    float zInShadow = tex2D(_LightShadow, samplingUv.xy).r;
    float bias = _ShadowNormalBiases[tileIndex] * tan(acos(dotNL)) + _ShadowBiases[tileIndex];
    float attenuation = zInLVP - bias > zInShadow ? 0 : 1;

    return samplingUv.x > 0 && samplingUv.x < 1 && samplingUv.y > 0 && samplingUv.y < 1 && distanceSqrt <= _ShadowDistanceSqrts[tileIndex] ? attenuation : 1;
}

サンプリング位置の調整は以下の部分です。uvをだしたところは投影テクスチャマッピングと同じように算出していますが、その値にscaleをかけることとオフセットをつけることでサンプリング位置を調整しています。

float2 samplingUv = 0;
float2 uv = positionLVP.xy / positionLVP.w * float2(0.5f, -0.5f) + 0.5f;

float scale = split == 1 ? 1 : 0.5;
samplingUv = Offsets[tileIndex] + uv * scale;

最後に

上記によって以下のように複数のDirectionalLightのシャドウを描くことができました。

1つのDirectionalLightのシャドウを描くときも同様ですが、うまくシャドウが描画できないときは各ライトコンポーネントのBiasやStrength等を調整してみてください。

参考

catlikecoding.com

news.mynavi.jp

qiita.com

qiita.com

鈴木直美の「PC Watch先週のキーワード」

HDRPでカスタムShader -レイヤーが一致するDirectionalLightの影響を受ける・レイヤーで指定したオブジェクトの影を落とす-

はじめに

以前HDRPでカスタムシェーダーを作成し、DirectionalLightの影を落とす・受け取る方法について書きました。

daiki-eguchi.hatenablog.com

今回はHDRPで複数のDirectionalLightがあった際にレイヤーを設定することでレイヤーが合っているものだけ影響を受ける方法についてです。

開発環境

  • Unity2022.3.4f1
  • HDRP14.08

注意点

まず、HDRPではカスケードシャドウを使用している時、複数のDirectionalLightの影を落とすことはできなさそうです。 なので一灯のDirectionalLightとは別角度の影を落としたいときはSpotLightを使用したりしています。(SpotLightでは複数方向の影を落とすことができる)

DirectionalLightを複数Shadowオンにすると出るエラー

Lightのレイヤー設定

DIrectionalLightにはLayerを設定することができ、そのライトの影響を及ぼすレイヤー分けを行うことができます。 このレイヤーを設定することでどのライトがどのオブジェクトに影響を及ぼすか。ということを設定することができます。

LightのLayerを設定する

このレイヤーはRendererのAdditionalSettings欄にあるRenderingLayerMaskと対応しています。

オブジェクト側のRenderingLayer

もし、Light側のLayer設定がない場合は以下のように「Show All Additional Properties」をクリックし、開いたPreferenceの「CoreRenderPipeline」からVisibilityをAllVisibleに設定することで確認できるかと思います。

実装

今回、以下のようにDirectionalLightを複数用意し、RenderingLayerをそれぞれ「0」と「1」に設定します。

前記事ではDirectionalLightは一つと考えていたので以下のように指定していました。DirectionalLightやスポットライトの情報はStructuredBufferとして登録されています。

DirectionalLightData directionalLightData = _DirectionalLightDatas[0];

DirectionalLightの数はDirectionalLightCountとして設定されているため、DirectionalLightDatasを順番にfor分で回し、レイヤーを判定します。

Shaderのフラグメントシェーダー部分を以下のようにします。

float4 frag (v2f i) : SV_Target
{
    float4 mainColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                
    for (int lightCount = 0; lightCount < _DirectionalLightCount; lightCount++)
    {
        if(IsMatchingLightLayer(_DirectionalLightDatas[lightCount].lightLayers, 1 << 1))
        {
            DirectionalLightData directionalLightData = _DirectionalLightDatas[lightCount];

            float NdotL = saturate(dot(i.normal, -directionalLightData.forward));

            HDShadowContext shadowContext = InitShadowContext();
            float3 shadow = GetDirectionalShadowAttenuation(
                        shadowContext,
                        i.positionSS.xy,
                        i.positionWS,
                        i.normal,
                        directionalLightData.shadowIndex,
                       -directionalLightData.forward);

             mainColor.rgb *= directionalLightData.color * NdotL * shadow;
         }
    }
                
    return mainColor;
}

まず、for文でDirectionalLightを回します。最大数は_DirectionalLightCountになります。

for (int lightCount = 0; lightCount < _DirectionalLightCount; lightCount++)

そして、IsMatchingLightLayerメソッドでLightレイヤーとオブジェクトのRenderingLayerとあっているかを判定します。

LightLayerは_DirectionalLightDatas[lightCount].lightLayersになり、複数を設定することが可能です。以下のように書くとレイヤーが1のもののみ影響を受けるようになります。

if(IsMatchingLightLayer(_DirectionalLightDatas[lightCount].lightLayers, 1 << 1))

この関数を使用するときは以下のインクルードを行う必要があることに注意してください。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonLighting.hlsl"

上記によってLayerが0に設定しているライトを緑、Layerを1に設定しているライトを黄色にしていますが、Layerを1の黄色の影響をうけています。

レイヤーが1の黄色の影響を受けている

しかし、影が落ちていません。

影が落ちていない

影を落とす対象オブジェクトのレイヤーを設定する

これはデフォルトではLightLayerと一致しているレイヤーの影を落とそうとするので影が落ちていないのが原因になります。

HDRPには影を落とす対象のオブジェクトのレイヤーを設定することができるのですが、これがなにもしないとLightLayerの設定値になるので(試してみた感じ)、今LightLayerを1に設定しているためにRenderingLayerが1の影を落とそうとしています。 現在、CubeのRenderingLayerは0なので、0の影を落とす必要があります。

そのため、以下のようにLightコンポーネントのShadow欄にある「CustomShadowLayer」をオンにして影を落としたいオブジェクトのレイヤーを指定します。 今回はCubeやPlaneのレイヤーは0なのでCustomShadowLayerは0にします。

この設定を行うと以下のようにしっかりとシャドウが描画されます。

無事に影が描画された

最後に

これによって複数のDirectionalLightがあったときにレイヤー指定したライトの影響を受けるように。そしてレイヤー分けで狙った影を描画することができました。

次はHDRPのカスタムシェーダーで記載するDepthPrepassについて記事を書こうかと思っています。

HDRPでカスタムShader -影を落とす・影を受け取る-

はじめに

以前HDRPでカスタムシェーダーを作成する方法をUnlit形式で紹介しました。

daiki-eguchi.hatenablog.com

今回はHDRPで影を出す、影を受け取る方法についてです。

最小限ということで影響を受ける光はまず「1つのみ」とします。

開発環境

  • Unity2022.3.4f1
  • HDRP14.08

成果物

最終的に以下のような見た目になります。PlaneとCube二つには同じShaderを使っており、落ち影やほかの落ち影の影響をうけることが確認できます。

Shader

まずはShader全文です。見慣れないincludeファイルがありますが一つ一つ解説していきます。

Shader "Custom/SampleLit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags 
        {
            "RenderType"="Opaque"
            "RenderPipeline"="HDRenderPipeline"
        }
        LOD 100

        Pass
        {
            Tags {"LightMode" = "Forward"}
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/HDShadow.hlsl"

            #pragma multi_compile SHADOW_LOW SHADOW_MEDIUM SHADOW_HIGH SHADOW_VERY_HIGH
            #pragma multi_compile_fragment AREA_SHADOW_MEDIUM AREA_SHADOW_HIGH
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/NormalBuffer.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
                float4 positionSS : TEXCOORD1;
                float3 positionWS : TEXCOORD2;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.normal = TransformObjectToWorldNormal(v.normal);
                
                float4 ss = o.vertex * 0.5f;
                ss.xy = float2(ss.x, ss.y * _ProjectionParams.x) + ss.w;
                ss.zw = o.vertex.zw;
                o.positionSS = ss;
                
                o.positionWS = TransformObjectToWorld(v.vertex);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                DirectionalLightData directionalLightData = _DirectionalLightDatas[0];

                HDShadowContext shadowContext = InitShadowContext();
                float3 shadow = GetDirectionalShadowAttenuation(
                    shadowContext,
                    i.positionSS.xy,
                    i.positionWS,
                    i.normal,
                    directionalLightData.shadowIndex,
                    -directionalLightData.forward);

                float4 mainColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
                mainColor.rgb *= shadow.r;
                return mainColor;
            }
            ENDHLSL
        }
        Pass
        {
            Name "ShadowCaster"
            Tags {"LightMode" = "ShadowCaster"}
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
            };

            v2f vert(appdata i)
            {
                v2f o;
                o.pos = TransformObjectToHClip(i.vertex);
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                return 0;
            }
            
            ENDHLSL
        }
    }
}

影を落とす

影を落とすのはURPなどと同様に影を落とすようのパスを追加します。

LightModeをShadowCasterにしないと判別されないので注意です。この辺はURPと同様ですね。

Pass
{
    Name "ShadowCaster"
    Tags {"LightMode" = "ShadowCaster"}
            
     HLSLPROGRAM
     #pragma vertex vert
     #pragma fragment frag
     #pragma multi_compile_shadowcaster
     #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
     #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

     struct appdata
     {
         float4 vertex : POSITION;
         float3 normal : NORMAL;
         float2 texcoord : TEXCOORD0;
      };

      struct v2f
      {
          float4 pos : SV_POSITION;
      };

      v2f vert(appdata i)
      {
          v2f o;
          o.pos = TransformObjectToHClip(i.vertex);
          return o;
      }

      float4 frag(v2f i) : SV_Target
      {
          return 0;
      }
            
      ENDHLSL
}

普段よく使うような関数は以下のインクルードファイルの中に入っていました。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

影を受け取る

影を受け取るのが少し大変だったのですが一つ一つ見てみます。

URPであったようなGetMainLight()GetAdditionalLight()というような便利関数がHDRPにはありません。

まずはDirectionalLightの取得ですがHDRPでは_DirectionalLightDatasというStructuredBufferにシーン内のDirectionalLightが入ってきます。 実装されているのは以下で示すようなShaderVariablesLightLoop.hlslです。

#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightDefinition.cs.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Core/Utilities/GeometryUtils.cs.hlsl"

// don't support Buffer yet in unity
StructuredBuffer<uint>  g_vBigTileLightList;

StructuredBuffer<uint>  g_vLightListTile;
StructuredBuffer<uint>  g_vLightListCluster;

StructuredBuffer<uint>  g_vLayeredOffsetsBuffer;
StructuredBuffer<float> g_logBaseBuffer;

#ifdef USE_INDIRECT
    StructuredBuffer<uint> g_TileFeatureFlags;
#endif

StructuredBuffer<DirectionalLightData> _DirectionalLightDatas;
StructuredBuffer<LightData>            _LightDatas;
StructuredBuffer<EnvLightData>         _EnvLightDatas;

// Used by directional and spot lights
TEXTURE2D(_CookieAtlas);

// Used by cube and planar reflection probes
TEXTURE2D_ARRAY(_ReflectionAtlas);

// Contact shadows
TEXTURE2D_X_UINT(_ContactShadowTexture);

// Screen space shadows
TEXTURE2D_ARRAY(_ScreenSpaceShadowsTexture);

// Indirect Diffuse Texture
TEXTURE2D_X(_IndirectDiffuseTexture);

今回はDirectionalLightが一つということで進めていくので配列の0番目を取得します。

DiectionalLightが複数の時の実装方法はまた別途書こうと思います。

float4 frag (v2f i) : SV_Target
{
    DirectionalLightData directionalLightData = _DirectionalLightDatas[0];

    HDShadowContext shadowContext = InitShadowContext();
    float3 shadow = GetDirectionalShadowAttenuation(
                    shadowContext,
                    i.positionSS.xy,
                    i.positionWS,
                    i.normal,
                    directionalLightData.shadowIndex,
                    -directionalLightData.forward);
}

DirectionalLightを取得しましたがそこからまた呪文が続いています。

HDShadowContext shadowContext = InitShadowContext();
float3 shadow = GetDirectionalShadowAttenuation(
                    shadowContext,
                    i.positionSS.xy,
                    i.positionWS,
                    i.normal,
                    directionalLightData.shadowIndex,
                    -directionalLightData.forward);

ここがShadowMapから影の情報を取得し、shadowAttenuationを取得する関数です。実はHDRPのLit.shaderの中身での影の受け取りはほかにもいろいろやっているのですが、最小限ではこのぐらいでよさそうです。

InitShadowContext()はHDShadowContext.hlslで定義されている関数でシャドウデータを取得する初期化のような関数です。

以下はHDShadowContext.hlslの切り抜きです。

///////// 中略

StructuredBuffer<HDShadowData>              _HDShadowDatas;
// Only the first element is used since we only support one directional light
StructuredBuffer<HDDirectionalShadowData>   _HDDirectionalShadowData;

HDShadowContext InitShadowContext()
{
    HDShadowContext         sc;

    sc.shadowDatas = _HDShadowDatas;
    sc.directionalShadowData = _HDDirectionalShadowData[0];

    return sc;
}

////// 中略

これらを使用するために必要なファイルをインクルードしています。

#include "Packages/com.unity.render-pipelines.high-definition/Runtime/Lighting/LightLoop/HDShadow.hlsl"

また、以下のマクロを使用しています。

#pragma multi_compile SHADOW_LOW SHADOW_MEDIUM SHADOW_HIGH SHADOW_VERY_HIGH
#pragma multi_compile_fragment AREA_SHADOW_MEDIUM AREA_SHADOW_HIGH

GetDirectionalShadowAttenuationという関数の引数に必要な変数がpositionSS(スクリーンスペース)、positionWS(ワールドスペース)、normal(法線情報)が必要なので頂点シェーダーのほうで格納しています。

v2f vert (appdata v)
{
    v2f o;
    o.vertex = TransformObjectToHClip(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    o.normal = TransformObjectToWorldNormal(v.normal);
                
    float4 ss = o.vertex * 0.5f;
    ss.xy = float2(ss.x, ss.y * _ProjectionParams.x) + ss.w;
    ss.zw = o.vertex.zw;
    o.positionSS = ss;
                
    o.positionWS = TransformObjectToWorld(v.vertex);
    return o;
}

最後にShadowの色味をテクスチャの色味に乗算することで今回はシャドウを表現しました。

float4 mainColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
mainColor.rgb *= shadow.r;
return mainColor;

設定

最後にUnityの設定を行います。 上記のシェーダーをCubeとPlaneにアタッチすると以下のようにシャドウが汚く見えます。

これを改善するには、、、!と設定周りを探してみると方法が二つありました。

一つ目はDirectionalLight側の設定を調整する方法です。実はこのシャドウの汚さはShadow Filtering Qualityに由来するものでした。

このShadow Filtering Qualityは以下にドキュメントがありました。

docs.unity3d.com

Highに設定するとパーセンテージ クローザー ソフト シャドウ (PCSS)が働くのでそこが悪さしているのか、、、という感じです。ソフトシャドウの質はAngular DiameterとSampleCountによると記載があるのでそちらを調整します。

スクリーンスペースによるものなのでサンプリング回数を上げれば軽減します。サンプリング回数はライトのほうの設定のShadow欄にある「Filter Sample Count」なのでこちらを上げれば軽減します。

ただ今回はMaxのサンプリング数64にしても軽減しなかったのでAngular Diameter Scale For Softnessを下げました。

もちろんサンプリング回数を上げるということは負荷も上がるということだと思うのでこの辺は二つの変数をいいかんじに調整する必要がありそうですね。

2つ目はProjectSettingsでQuality>HDRP>Shadowsの「Shadow Filtering Quality」をHighからMidiumに変更することです。

HighからMediumにするとそもそもPCSSではなくなるので汚く見えなくなった。というだけになります。

これらの方法のどちらかをとると今回のような見た目に見えます。

おわりに

HDRPで影を落とし、影を受ける方法について書いてみました。

次はライトの影響を受けてシェーディングできる方法について書こうと思います。

Unityでステンシルシャドウボリューム

はじめに

前回、投影テクスチャマッピングシャドウを取り上げました。しかし、投影テクスチャマッピングシャドウは影生成の単位がキャラ単位なので自身の部位から受ける影「セルフシャドウ」を描画することができません。

daiki-eguchi.hatenablog.com

そこでセルフシャドウ表現まで行える技法で比較的互換性が高く、汎用性が高い技法がステンシルシャドウボリュームのようです。

ステンシルシャドウボリュームの理論

まず、視点方向からの深度情報(Zバッファ)を保持しておきます。

その後、以下のようにオブジェクトの輪郭を光源方向(光源からオブジェクトの方向)に伸ばします。

そのあと、以下のように視点からみてシャドウボリュームの表面(前面)をステンシルバッファに+1を書き込みます。 光源が複数あったり、オブジェクトが重なっているときはステンシルバッファでの+1演算が複数になります。

ステンシルバッファは算術処理用のフレームバッファです。

ステンシルバッファはあらかじめ0に初期化しておきます。

そして、視点から見てシャドウボリュームの裏面でステンシルバッファを-1します。

すると影を書く以外が0に戻り、影が生成される場所のステンシルは1以上になります。そのため、ステンシルが1以上の場所に影色つけていきます。

これによってセルフシャドウ表現もでき、モデル同士が影を落としあうこともできます。

成果物

以下のように落ち影が描画できています。

Unityでの実装

今回はURPで描画してみるのでRendererFeatureを使用してみます。 ProjectSettingsで現在使用しているRenderPipelineAssetを探します。

Stencil値の初期化

メニューの下からAdd RendererFeatureをクリックし、RenderObjectsを選択します。

名前をStencilInitializeにしてOverrides内のStencilをオン、ValueをONにしました。 これによって、画面全体のStencilが0になります。

オブジェクトの前面を伸ばし、ステンシル値をあげる

同様にRenderObjectsのRendererFeatureをもう一つ追加します。 命名をStencilIncreaseにして、新しく作成したStencilIncreaseというMaterialをOverrideの欄に入れます。

このMaterialにアサインするShaderは以下のように作成しました。

Shader "Custom/StencilIncrease"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor ("Shadow Color", COlor) = (0, 0, 0, 0.5)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            NAME "StencilFront"
            
            Tags { "LightMode" = "UniversalForward" }
            //カラーチャンネルに書き込むレンダーターゲットを設定する
            //0の場合、全てのカラーチャンネルが無効化され何も書き込まれない
            ColorMask 0
            ZWrite Off
            Cull Back
            ZTest LEqual

            Stencil
            {
                Ref 1
                Comp Always
                Pass IncrSat
            }
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _ShadowLength;
            half4 _ShadowColor;

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                // ライトの取得
                Light light = GetMainLight();

                // 法線をワールド空間に変換
                float3 worldNormal = TransformObjectToWorldNormal(v.normal);
                float nDotL = dot(worldNormal, light.direction);

                // 頂点を押し出す
                float3 vertex2 = v.vertex + TransformWorldToObjectDir(-light.direction) * _ShadowLength;

                o.vertex = TransformObjectToHClip(lerp(v.vertex, vertex2, 1 - step(-0.3, nDotL)));
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }
    }
}

ステンシルと頂点シェーダーがポイントになります。

まずはCullをBack指定し表面を伸ばすようにします。

Cull Back

Stencilを記述するだけなのでカラーチャンネルには何も書き込まれないように設定します。

ColorMask 0

ステンシルは以下のように記述し、必ずステンシルの値を1あげるようにしています。

Stencil
{
    Ref 1
    Comp Always
    Pass IncrSat
}

頂点シェーダー内では自分の頂点からライトの方向に対して伸ばしています。

まずは伸びた先の頂点位置を計算しています。

/ / 頂点を押し出す
float3 vertex2 = v.vertex + TransformWorldToObjectDir(-light.direction) * _ShadowLength;

その後、ライト方向と法線方向の内積から判断して頂点を伸ばしました。

o.vertex = TransformObjectToHClip(lerp(v.vertex, vertex2, 1 - step(-0.3, nDotL)));

オブジェクトの背面を伸ばし、ステンシル値を下げる

RendererFeatureを一つ追加します。RenderObjectsのRendererFeatureを追加し、名前をStencilDecreaseとしました。

Materialを新規で一つ追加し、アサインしたStencilを下げるShaderは以下のように記述しています。

Shader "Unlit/StencilDecrease"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor ("Shadow Color", COlor) = (0, 0, 0, 0.5)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            NAME "StencilBack"
            
            Tags { "LightMode" = "UniversalForward" }
            //カラーチャンネルに書き込むレンダーターゲットを設定する
            //0の場合、全てのカラーチャンネルが無効化され何も書き込まれない
            ColorMask 0
            ZWrite Off
            Cull Front
            ZTest LEqual
            
            Stencil
            {
                Ref 1
                Comp Always
                Pass DecrSat
            }
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            float3 _LightPosition;

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _ShadowLength;
            half4 _ShadowColor;

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                // ライトの取得
                Light light = GetMainLight();

                // 法線をワールド空間に変換
                float3 worldNormal = TransformObjectToWorldNormal(v.normal);
                float nDotL = dot(worldNormal, light.direction);

                // 頂点を押し出す
                float3 vertex2 = v.vertex + TransformWorldToObjectDir(-light.direction) * _ShadowLength;

                o.vertex = TransformObjectToHClip(lerp(v.vertex, vertex2, 1 - step(-0.3, nDotL)));
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }
    }
}

Stencilを上げるShaderと比較すると変更点は少ないです。

CullをFront指定することでオブジェクトの背面を伸ばします。

Cull Front

Stencilは以下のように記述してStencilを下げています。変更点はこれぐらいになります。

Stencil
{
    Ref 1
    Comp Always
    Pass DecrSat
}

Stencilをもとに影を描画する

Stencilは上記のRendererFeatureで操作できているので、あとはそれをもとに影を書いていきます。

RendererFeatureをまた一つ増やします。RenderObjectsを追加し、命名をDrawStencilShadowにしました。

Materialを新規で一つ作成して、それにアサインするShaderは以下のように記述しています。

Shader "Unlit/StencilShadowReceiver"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor ("Shadow Color", COlor) = (0, 0, 0, 0.5)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            NAME "StencilShadowReceiver"
            
            Tags { "LightMode" = "ShadowStencil" }
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            
            Stencil
            {
                Ref 1
                Comp Equal
                Pass Keep
            }
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            float _ShadowLength;
            half4 _ShadowColor;

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex.xyz);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                return _ShadowColor;
            }
            ENDHLSL
        }
    }
}

Stencilは以下のように記述しており今回は簡単なシーン設定のため、Stencilが1のところに影を書かせています。

Stencil
{
    Ref 1
    Comp Equal
    Pass Keep
 }

フラグメントシェーダーはMaterialから設定した色を描画するようにしています。

float4 frag (v2f i) : SV_Target
{
    return _ShadowColor;
}

最後に

上記によって以下のような影の描画を実現できました。

今回はステンシルシャドウボリュームを書いてみました。

ステンシルシャドウボリュームの弱点は影の生成単位がポリゴン単位なので例えば平面にTextureのアルファ抜きで模様や葉っぱを書いていた時、模様や葉っぱの影はできず平面の影ができてしまいます。

それを解決するために影生成用の引き延ばし用頂点を事前に仕込んでいたそうです。

ただ、これによって頂点シェーダーの負荷が上がるため、それを見越してポリゴン数を下げて品質が下がってしまうということもあったそうです。

参考

news.mynavi.jp

news.mynavi.jp

https://gameprogrammingunit.web.fc2.com/shadow/volume_simple.htm

zenn.dev

qiita.com

Unityで投影テクスチャマッピングシャドウ

はじめに

3Dゲームの影生成は90年代、キャラクターの足元に黒い丸を置いた「丸影」が主流だったようです。

しかし丸影は負荷は軽いですが「影」としてのリアリティはありませんでした。それを進化させたのが「投影テクスチャマッピング」です。

投影テクスチャマッピングの理論

投影テクスチャマッピングによるシャドウは以下のステップで実現できます。

まず、光源から見た(カメラに見立てて)キャラクターのシルエットをRenderTextureに書き出します。

そして、その作成したRenderTextureを影が生成される場所に貼る(マッピング)することで影が落ちているように見えます。

投影テクスチャマッピングの強みと弱み

この技法の強みはほぼすべてのGPUにサポートされることやキャラの影が別のキャラに投影されるようなことも実現する点になることです。

しかし、投影テクスチャマッピングの影の生成単位は「キャラ」であるため自分の部位の影が自分の体に映る「セルフシャドウ」は行えないことが弱点の一つになります。

また、光源からみた影のRenderTextureをどこにマッピングするのか(貼るのか)の判定が甘いと、ありえないところに影が発生してしまうことも弱点の一つのようです。

Unityでの実装例

まずは今回の成果物になります。

まず、光源と同じ位置、同じ回転値にしたカメラを用意します。

そしてこのカメラを以下のように

  • ProjectionをOrthographicに

  • CullingMaskを影を落としたいオブジェクトに設定しているLayerに

  • BackgroundColorをSolidColorにして背景色のアルファ値を0に

という風に設定しました。

スクリプト

そしてカメラにアタッチするC#スクリプトを以下のように作成します。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteAlways]
[RequireComponent(typeof(Camera))]
public class ShadowProjector : MonoBehaviour
{
    [SerializeField] private int renderTextureSize = 512;

    [SerializeField] private RenderTexture renderTexture;

    [SerializeField] private Light directionalLight;

    private Camera camera;

    private int textureSize;

    private void OnEnable()
    {
        camera = GetComponent<Camera>();
        camera.SetReplacementShader(Shader.Find("Custom/ShadowReciever"), null);
        camera.depth = -100000;
        renderTexture = new RenderTexture(renderTextureSize, renderTextureSize, 0, RenderTextureFormat.ARGB32, 0);
        camera.targetTexture = renderTexture;
        camera.clearFlags = CameraClearFlags.Color;
        camera.backgroundColor = Color.black;
    }

    private void OnDisable()
    {
        if (renderTexture == null)
        {
            return;
        }

        camera.targetTexture = null;
        DestroyImmediate(renderTexture);
    }

    private void LateUpdate()
    {
        textureSize = renderTexture.width;
        #if UNITY_EDITOR
        if (textureSize != renderTextureSize)
        {
            renderTexture.Release();
            renderTexture = new RenderTexture(renderTextureSize, renderTextureSize, 0, RenderTextureFormat.ARGB32, 0);
            camera.targetTexture = renderTexture;
        }
        #endif
        camera.transform.position = directionalLight.transform.position;
        camera.transform.rotation = directionalLight.transform.rotation;

        var viewMatrix = camera.worldToCameraMatrix;
        // プロジェクション行列のプラットフォームの差を吸収
        var projectionMatrix = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
        Shader.SetGlobalMatrix("_ShadowProjectorMatrixVP", projectionMatrix * viewMatrix);
        Shader.SetGlobalTexture("_ShadowProjectorTexture", renderTexture);

        // プロジェクターのポジションを渡す
        var projectPos = Vector4.zero;
        projectPos = camera.orthographic ? transform.forward : transform.position;
        projectPos.w = camera.orthographic ? 0 : 1;
        Shader.SetGlobalVector("_ShadowProjectorPos", projectPos);
    }
}

このC#ではステップ1である光源方向から見た影のRenderTextureを生成するところまでを行い、作成したRenderTextureをShader側に渡しています。

Enable処理では以下コードのようにCameraの設定、RenderTextureの生成、そしてCameraのoutputにあるTargetTextureにRenderTextureを設定するところまでを行っています。

影を描画するカメラは一番初めに描画したいためdepthをかなり小さい値にしています。

private void OnEnable()
{
    camera = GetComponent<Camera>();
    camera.SetReplacementShader(Shader.Find("Custom/ShadowReciever"), null);
    camera.depth = -100000;
    renderTexture = new RenderTexture(renderTextureSize, renderTextureSize, 0, RenderTextureFormat.ARGB32, 0);
    camera.targetTexture = renderTexture;
    camera.clearFlags = CameraClearFlags.Color;
    camera.backgroundColor = Color.black;
}

LateUpdate内ではShaderに必要な計算を行っています。Shader側でView行列とProjection行列が必要なのでShader側に渡しています。

カメラのView行列はcamera.worldToCameraMatrixで取得し、カメラのProjection行列はcamera.projectionMatrixでとれます。ただ、カメラ空間からクリッピング空間に座標変換(射影変換)はGPUに渡す際、OpenGLDirectXで差異があるためGL.GetGPUProjectionMatrixでプラットフォームの差異を吸収します。

これは、XYZ座標をW座標で割って正規化する際、Z値を収める範囲がOpenGLは-1~1、DirectXは0~1なのでこのような処理が必要になります。

var viewMatrix = camera.worldToCameraMatrix;
// プロジェクション行列のプラットフォームの差を吸収
var projectionMatrix = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
Shader.SetGlobalMatrix("_ShadowProjectorMatrixVP", projectionMatrix * viewMatrix);
Shader.SetGlobalTexture("_ShadowProjectorTexture", renderTexture);

// プロジェクターのポジションを渡す
var projectPos = Vector4.zero;
projectPos = camera.orthographic ? transform.forward : transform.position;
projectPos.w = camera.orthographic ? 0 : 1;
Shader.SetGlobalVector("_ShadowProjectorPos", projectPos);

この状態でカメラのOutputにあるtargetTextureをクリックしRenderTextureの情報を見ると以下のようになっています。

RenderTextureのアルファ値のみを確認すると以下のようになっています。

この白い部分が投影された時の影になる場所になります。

Shader

まずは全文になります。このShaderをアタッチしたMaterialを影を受け取る側のオブジェクトにアタッチすれば影が描画されます。

Shader "Custom/ShadowReciever"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _ShadowColor("ShadowColor", Color) = (0,0,0,0)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass
        {
            NAME "ShadowReceiver"
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            struct appdata
            {
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 projectorPos : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };
            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            TEXTURE2D(_ShadowProjectorTexture);
            SAMPLER(sampler_ShadowProjectorTexture);
            float4x4 _ShadowProjectorMatrixVP;
            float4 _ShadowProjectorPos;
            float4 _ShadowColor;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.projectorPos = mul(mul(_ShadowProjectorMatrixVP, unity_ObjectToWorld), v.vertex);
                o.projectorPos = ComputeScreenPos(o.projectorPos);
                o.worldNormal = TransformObjectToWorldNormal(v.normal);
                o.worldPos = TransformObjectToWorld(v.vertex);
                return o;
            }
            float4 frag (v2f i) : SV_Target
            {
                i.projectorPos.xyz /= i.projectorPos.w;
                float2 uv = i.projectorPos.xy;
                float4 projectorTex = SAMPLE_TEXTURE2D(_ShadowProjectorTexture, sampler_ShadowProjectorTexture, uv);
                float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
                // カメラの範囲外は適用しない
                float3 isOut = step((i.projectorPos - 0.5) * sign(i.projectorPos), 0.5);
                float alpha = isOut.x * isOut.y * isOut.z;

                // プロジェクターから見て裏面の面には適用しない
                alpha *= step(-dot(lerp(-_ShadowProjectorPos.xyz, _ShadowProjectorPos.xyz - i.worldPos, _ShadowProjectorPos.w), i.worldNormal), 0);;
                return lerp(col, 0 , alpha * projectorTex.a);
            }
            ENDHLSL
        }
    }
}

大事になるのは頂点シェーダーです。テクスチャマッピングを行うので流れとしては

  • VP行列を頂点座標にかけて座標変換
  • _ProjectionParams.xをかけることでプラットフォームの違いを吸収する
  • wで除算することで-1~1の範囲に
  • それを0-1に変換する

の流れになります。

(影描画用の)カメラから見たときのCubeやPlaneをTextureに描画させるのでモデル行列に対して影描画用のカメラのビュー行列とプロジェクション行列をかけます。

今回はC#側から渡ってきたVP行列を使って座標変換した後、ComputeScreenPos()を行うことで上の

  • _ProjectionParams.xをかけることでプラットフォームの違いを吸収する
  • それを0-1に変換する(0-wに変換し、後でwでわると0-1の範囲になるようにしている)

を解決しています。

v2f vert (appdata v)
{
    v2f o;
    o.vertex = TransformObjectToHClip(v.vertex);
    o.projectorPos = mul(mul(_ShadowProjectorMatrixVP, unity_ObjectToWorld), v.vertex);
    o.projectorPos = ComputeScreenPos(o.projectorPos);
    o.worldNormal = TransformObjectToWorldNormal(v.normal);
    o.worldPos = TransformObjectToWorld(v.vertex);
    return o;
}

ComputeScreenPos()は以下のようにxyが0~wに変換されてyの上下問題を解決してくれるもので、Unityが用意してくれている関数になっています。

inline float4 ComputeNonStereoScreenPos(float4 pos) {
    // この時点でxyは-wからwの値を取る(MVP変換後だから)
    // xyzwに0.5をかけるのでxyは-0.5w~0.5wになる
    float4 o = pos * 0.5f;

    // この処理でxyが0~wになる
    // yがプラットフォームにより上下反転する問題も吸収
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

inline float4 ComputeScreenPos(float4 pos) {
    // 通常はこの処理だけ
    float4 o = ComputeNonStereoScreenPos(pos);

// こっちはVR用の処理
#if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
    return o;
}

【Unity】【シェーダ】スクリーンに対してテクスチャをマッピングする方法を完全解説する - LIGHT11から引用

yがプラットフォームによって違うのはプラットフォームによって射影行列が反転しているためです。

フラグメントシェーダーでwによる除算を行なってからテクスチャサンプリングを行います。

float4 frag (v2f i) : SV_Target
{
     i.projectorPos.xyz /= i.projectorPos.w;
     float2 uv = i.projectorPos.xy;
     float4 projectorTex = SAMPLE_TEXTURE2D(_ShadowProjectorTexture, sampler_ShadowProjectorTexture, uv);
     float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
     // カメラの範囲外は適用しない
     float3 isOut = step((i.projectorPos - 0.5) * sign(i.projectorPos), 0.5);
     float alpha = isOut.x * isOut.y * isOut.z;

     // プロジェクターから見て裏面の面には適用しない
     alpha *= step(-dot(lerp(-_ShadowProjectorPos.xyz, _ShadowProjectorPos.xyz - i.worldPos, _ShadowProjectorPos.w), i.worldNormal), 0);
     return lerp(col, 0 , alpha * projectorTex.a);
}

カメラの範囲外かどうかの判定

カメラの範囲外かどうかの判定式は以下で行っています。

float3 isOut = step((i.projectorPos - 0.5) * sign(i.projectorPos), 0.5);
float alpha = isOut.x * isOut.y * isOut.z;

i.projectorPosはクリップスペース空間になるので、0-1の範囲外の場合はカメラの範囲外という判定になります。

sign(x)は以下の値を返す関数になります。

1.0 (if x>0)、0 (if x=0)、-1.0 (if x<0)を返す

裏面かどうかの判定

裏面かどうかは以下の処理で見ていますが、視線方向と放線方向のベクトルの内積をとり、負である場合は裏面ということになります。

alpha *= step(-dot(lerp(-_ShadowProjectorPos.xyz, _ShadowProjectorPos.xyz - i.worldPos, _ShadowProjectorPos.w), i.worldNormal), 0);

影の描画

最後に影を描画します。今回は影を0で書いているので真っ黒ですがPropertiesで定義してあげて影色を指定してあげるといいかと思います。

return lerp(col, 0 , alpha * projectorTex.a);

最後に

上記のShaderをアタッチしたMaterialを影を描画する側のオブジェクトにアタッチし、最終的には以下のような絵が作れます。

参考

news.mynavi.jp

light11.hatenadiary.com

tech.drecom.co.jp

soramamenatan.hatenablog.com

light11.hatenadiary.com

HDRPでカスタムShader - Unlit編

概要

HDRPはUnityが提供しているレンダーパイプラインの1つです。PBRや写実性が高いものを描画するのに長けている印象で、URPと同じくSRPの上に構築されたレンダーパイプラインの一つです。

しかし、URPと比較するとHDRPに関する記事は多いわけではなく内部のスクリプトも複雑になっているため敷居がたかいイメージがあります。

HDRPはShaderGraphを使用することや、HDRPに入っている標準のポストエフェクト等を使用することを目指して作られているような所感を受けましたが、元々作っているものをHDRPに移植したい時やHLSLを書いて表現を構築したい場合はあります。

本記事やこのブログで、HDRPにおいてカスタムシェーダーを作りたい方の少しでも助けになればと思います。

HDRPでカスタムUnlitシェーダーを書く

本記事では最低要素のUnlitシェーダーを書いてみます。

筆者の環境は

  • Unity2022.3.4f1
  • HDRP 14.08

になります。

Unity設定

まず、前提として今回はHDRPのRenderingのLitShaderModeを「Forward Only」に変更します。

以下のようにProjectSettingsからRenderingタブから変更してください。

Deferred Onlyの場合やBothの設定の場合はまた別の記事で書いてみようと思います。

Shader

ではUnlitShader全文です。

Shader "Custom/SampleUnLit"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags 
        {
            "RenderType"="Opaque"
            "RenderPipeline"="HDRenderPipeline"
        }
        LOD 100

        Pass
        {
            Tags {"LightMode" = "Forward"}
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
            float4 _MainTex_ST;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = TransformObjectToHClip(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            float4 frag (v2f i) : SV_Target
            {
                return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
            }
            ENDHLSL
        }
    }
}

解説

基本はURPで行うUnlitと同じ処理になります。ただ、HDRPでのincludeやTag指定は気を付けないと描画されません。

include

includeは基本的にこの2つかと思います。影の描画を行う場合は別のincludeが必要ですが、この2つがカスタムシェーダーを書く際のUnityが用意している関数やGlobal変数などを保持しています。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl"

Tags

Tag指定は以下のように行っています。もちろんRenderTypeはその時に応じて変更しますが、RenderPipelineはHDRenderPipelineを指定します。

Tags 
{
    "RenderType"="Opaque"
    "RenderPipeline"="HDRenderPipeline"
}

描画結果

上記のShaderをもとにMaterialを作成し、Cubeにアタッチすると以下のように描画されます。

最後に

これで最低要素のUnlitShaderを記述できました。これをもとにお好みのカスタマイズを行えます。

次はLitShader、特に影を落とす・他の影を受ける。というところを詳細に書こうと思います。

参考

docs.unity3d.com

tips.hecomi.com