2019年9月のシェーダーを紹介する

f:id:kaneta1011:20190929202951p:plain
サムネ用

はじめに

毎月やっている月毎に制作したシェーダーの紹介第8回目です。

今月はShadertoyで一つシェーダーを作ったので簡単な解説をします。

Energy Lab

インスピレーション

9月頭あたりにこのシェーダーを見て、金色のケージっぽいオブジェクトかっこいいなと思ったので今回登場させました。

www.shadertoy.com f:id:kaneta1011:20190929204742p:plain

また以前からパイプをエネルギーが伝っているようなシーンを作ってみたかったので合わせて今回のシェーダーになりました。

形状

f:id:kaneta1011:20190929224827p:plain

ケージの形状はトーラスをpmodで二重に複製しています。

vec2 cage(vec3 p) {
    p.y += sin(time) * 0.1;
    p.xy *= rot(-pi*0.5);
    p.yz *= rot(time * 0.5);
    p.yz = pmod(p.yz, 7.0);
    p.yx = pmod(p.yx, 7.0);
    return vec2(torus(p, 0.025, 0.55), MAT_CAGE);
}

ステージのモデリングには例に漏れず、Mercuryのhg_sdfを活用させていただいています。 mercury.sexy

特にパイプは fOpPipe を使用して、元のステージとboxを組み合わせて這うパイプを表現しています。

f:id:kaneta1011:20190929231717p:plain
fOpPipe前

f:id:kaneta1011:20190929231748p:plain
fOpPipe後

ライティング

f:id:kaneta1011:20190929202951p:plain

n kbデモを意識してコンパクトでそれなりの見た目になる物を用意して使っています。

Diffuseに正規化Lambert、Specularに正規化BlinPhongを使用したもので、反射率はフレネルを考慮した方が良い結果になりますが、マテリアル毎に一定としています。

vec3 light(vec3 p, vec3 n, vec3 v, vec3 lp, vec3 baseColor, float roughness, float reflectance, float metallic, vec3 radiance) {
    vec3 ref = mix(vec3(reflectance), baseColor, metallic);

    vec3 l = lp - p;
    float len = length(l);
    l /= len;
    
    vec3 h = normalize(l + v);
    
    vec3 diffuse = mix(1.0 - ref, vec3(0.0), metallic) * baseColor / pi;
    
    float m = roughnessToExponent(roughness);
    vec3 specular = ref * pow( max( 0.0, dot( n, h ) ), m ) * ( m + 9.0 ) / ( 9.0 * pi );
    return (diffuse + specular) * radiance * max(0.0, dot(l, n)) / (len*len);
}

今まで、複数回のリフレクションに対応するシェーダーを書いたことが無かったので、今回はそれに対応しました。

シェーダー内では再帰関数を利用できないので、依存関係にならないように処理を小さい粒度で関数化して、レンダリングの後方でトレースをループすることで実現しました。

vec3 shade(vec3 p, vec3 ray, vec2 mat) {
    vec3 baseColor, emission;
    float roughness, metallic, reflectance;
    
    getSurfaceParams(p, mat, baseColor, emission, roughness, reflectance, metallic);
    vec3 n = normal(p);
    
    vec3 result = evalLights(p, n, ray, baseColor, roughness, reflectance, metallic) + emission;
    vec3 f0 = vec3(1.0);
    for(int i=0; i<1; i++) {
        f0 *= mix(vec3(reflectance), baseColor, metallic);
        vec3 secondPos;
        vec2 secondMat;
        float depth;
        ray = reflect(ray, n);
        trace(p + n * 0.001, ray, 100.0, 24, secondPos, secondMat, depth);
        getSurfaceParams(p, secondMat, baseColor, emission, roughness, reflectance, metallic);
        n = normal(secondPos);
        p = secondPos;
        result += (evalLights(secondPos, n, ray, baseColor, roughness, reflectance, metallic) + emission) * f0;
    }
    
    return result;
}

ポストエフェクト

過去に紹介したものばかりなので箇条書きで書きます

さいごに

来月はGLSLスクールがあるので頑張って資料作ります!

10月は何を作るかまだ未定ですが、もし何か作ったら紹介します。

2019年8月の成果物を紹介する

f:id:kaneta1011:20190831142630p:plain
サムネ用

はじめに

毎月やっている月毎に制作したシェーダーの紹介第7回目です。

今月はShadertoyでシェーダーを書きませんでしたが、最近触り始めたUnityのさらにHDRPで色々やっていたので紹介します!

新幹線でライブコーディング

会社の出張で大阪に向かった時にライブコーディングをやりました。

以前Shadertoyでトンネルを二つ組み合わせた面白い表現を見たので、ボリュームライトと組み合わせてライブコーディングでやってみました。

Unity HDRP

8月上旬から中旬はずっとこれをやっていました。

凄い人が作ったであろうHDRPを実際に触って改造してみることで色々な知見を得られるのではないかという作戦で進めました。

実際に今まで知らなかった手法やテクニックがたくさん使われており大変勉強になっております!

HDRPのキャッチアップ

まず手始めにHDRPを改造できる環境を整えました。

以下にその時のメモが残っています。

nanka.hateblo.jp

また改造の練習にHDRPにレンダリングパスを追加してアウトラインを描画するということもやりました。

nanka.hateblo.jp

レイマーチング

HDRPでレイマーチングをするというのが今月の目標でした。

HDRPの改造方法も大体わかってきたのでこの辺りから詩作を始めました。

レンダリングパスを追加できるようにしておく等入念に準備していましたが、意外とHDRP側の修正は少なく済みました。

この辺りからはHDRPの素敵なライティングでスクショを取るのが楽しすぎて、作業よりもスクショを取ってる時間の方が長かったです。

そして完成したものがこちらです。

この時に得た知見等はこちらの記事で解説しています。(まだ途中だけど...)

nanka.hateblo.jp

さいごに

今月はやったことをほとんど別記事で書いてしまったので内容が薄くなってしまいました...

もうしばらくUnity + HDRPを触っていろいろやりたいなと考えています。

また、10月に@h_doxas氏主催のGLSLスクールでプラスワン講義を担当することになったので、来月はそちらの資料作りに時間を取りたいとも考えています!

webgl.souhonzan.org

来月ももし何か作ったら紹介します。

Unity HDRPのLitシェーダーを改造してレイマーチングする(GBuffer編)

f:id:kaneta1011:20190826231443p:plain
サムネ用

なにをやったか

今回はUnityのHDRPのシェーダーを改造して、HDRPのシーン上にレイマーチングオブジェクトを表示しました!

がむさんによる作例もあります

UnityとHDRPのバージョン

  • Unity 2019.2.0f1
  • HDRP 6.9.0-preview

今回作ったプロジェクトはこちらのリポジトリで公開しています!

もしかしたらHDRPでカスタムシェーダーを作るときに何かの足しになるかもしれないです!!

github.com

仕組み

まずどうやってUnityの世界にレイマーチングオブジェクトを表示するのかの仕組みを簡単に説明します。

3Dではフォワードやディファードレンダリングという言葉をよく聞くと思います。HDRPではディファードをさらに発展させたものをハイブリットに使用してレンダリングしています。

ディファードレンダリングでは、GBufferと呼ばれるライティングに必要な情報を収集するパスと、そのGBufferを利用して実際にライティングを行うパスに分かれて世界を表現します。

今回はこのGBufferを生成するパスにレイマーチングによる情報で上書きすることで、ライティングをHDRPの強力なライティングソリューションにお任せするということをしています。

既にUnityでレイマーチングをするのは偉大な先駆者の方々がいらっしゃるので、詳しくはそちらの方々の解説を参照してください。

この記事ではHDRPでレイマーチングするにあたっての知識にフォーカスします。

i-saint.hatenablog.com

tips.hecomi.com

gam0022.net

レイマーチング用シェーダーファイルの構成

レイマーチングオブジェクトのマテリアルは、以下の6つのユーザーシェーダーで構成されます。

もし独自のレイマーチングオブジェクトをこのプロジェクトに追加したければ、 DF.hlslRaymarching.shader をコピーして、 DF.hlsldistanceFunctiongetDistanceFunctionSurfaceData を実装すればOKです。

GBufferパス

HDRPではマテリアルタイプ毎にGBufferに格納する内容が異なるっぽい(GBuffer0.aとGBuffer2が異なるみたい)ですが、今回のレイマーチングはHDRPのStandardマテリアルのみで描画しています。(今後別マテリアルにも対応したいです)

シェーダー内のコメントによると、特に追加設定の無いStandardマテリアルのGBufferの内容は以下のようになっています。(バージョンアップで変わる可能性もあります)

f:id:kaneta1011:20190817235544p:plain

最終的にこのフォーマットでレンダリングすればHDRPの強力なライティングソリューションで描画してくれます!

既存のLit.shaderで使用されているShaderPassGBuffer.hlslのGBufferパスを覗いてみると、こちらの一行でGBufferに情報を書き込んでいました。

SurfaceData surfaceData;
BuiltinData builtinData;
GetSurfaceAndBuiltinData(input, V, posInput, surfaceData, builtinData);

ENCODE_INTO_GBUFFER(surfaceData, builtinData, posInput.positionSS, outGBuffer);

このインターフェースに乗っ取って、SurfaceDataとBuiltinDataをレイマーチングで生成できれば上手くGBufferに情報を書き込むことが出来そうです。

GBufferパスでレイマーチングを行うように改造する

このプロジェクトでは3つのレイマーチングオブジェクトがありますが、その中のMengerEmitを例に解説します。

GBufferパスのフラグメントシェーダーでレイマーチングを行いたいので、該当の処理を見てみます。

HDRPのシェーダーは各処理毎にシェーダーファイルが分かれていて、includeを差し替えることでいろいろなパスに切り替えれるようになっています。

なのでGBufferのフラグメントシェーダーも実際は.shaderファイルには無くて、ShaderPassGBuffer.hlslに切り出されています。

このhlslファイルを丸々コピーしてレイマーチング用のGBufferパスにカスタマイズしていきます。

GBufferパス改造の本質と言える部分は以下の箇所です。

GBufferPass.hlslの該当箇所

float3 ray = normalize(input.positionRWS);
float3 pos = GetRayOrigin(input.positionRWS);

DistanceFunctionSurfaceData surface = Trace(pos, ray, GBUFFER_MARCHING_ITERATION);

SurfaceData surfaceData;
BuiltinData builtinData;
ToHDRPSurfaceAndBuiltinData(input, V, posInput, surface, surfaceData, builtinData);

float depth = WorldPosToDeviceDepth(surface.Position);

ENCODE_INTO_GBUFFER(surfaceData, builtinData, posInput.positionSS, outGBuffer);

上記のコードを一つずつ解説していきます。

RayOriginとRayDirectionについて

レイマーチングを行うにはレイマーチを始める点である RayOrigin と進む方向である RayDirection が必要です。

今回は球を描画する際にレイマーチングを行うので、本来ならばカメラのワールド座標をRayOriginに、カメラ位置からフラグメントの位置へのベクトルをRayDirectionにするはずです。

しかし私は今回以下のように実装しました。

// RaymarchingUtility.hlsl
float3 GetRayOrigin(float3 positionRWS) {
    float3 pos = float3(0.0, 0.0, 0.0);
    return pos;
}

float3 ray = normalize(input.positionRWS);
float3 pos = GetRayOrigin(input.positionRWS);

RayOriginが原点...?なんだこれは?と思いますよね...

実はHDRPではカメラの平行移動による座標値の精度低下を抑えるために、MVP行列のMに当たるモデル変換行列の位置成分から既にカメラ座標が差し引かれています。カメラ空間レンダリングというらしいです。

該当箇所はこちらです

#define UNITY_MATRIX_M     ApplyCameraTranslationToMatrix(GetRawUnityObjectToWorld())

float4x4 ApplyCameraTranslationToMatrix(float4x4 modelMatrix)
{
    // To handle camera relative rendering we substract the camera position in the model matrix
#if (SHADEROPTIONS_CAMERA_RELATIVE_RENDERING != 0)
    modelMatrix._m03_m13_m23 -= _WorldSpaceCameraPos;
#endif
    return modelMatrix;
}

そしてその変換行列でObject to World変換を行ったフラグメント座標が input.positionRWS に格納されています。(RWSは Relative World Space の略だと思います)

つまり、HDRPではフラグメントシェーダーの時点でカメラ位置が原点の空間に変換されているので、カメラ位置を表すRayOriginが原点になります!

RayDirectionも本来ならカメラ座標からフラグメントのワールド座標へのベクトルを求める必要がありますが、今回はカメラ位置が原点の空間なので input.positionRWS をnormalizeするだけで求めることができます。

カメラ空間レンダリングを行わないとどうなるのかと言ったことはこちらの記事で詳しく解説してくれています。(こちらはレイトレーサーの話なので少し文脈が異なりそうですが...)

pharr.org

実際にHDRPではどういう理由で採用されて、どう動いているかはこちらのHDRPのマニュアルに記載されています。

docs.unity3d.com

サーフェースデータをHDRPのデータに変換してGBufferに書き込む

DistanceFunctionSurfaceData surface = Trace(pos, ray, GBUFFER_MARCHING_ITERATION);

SurfaceData surfaceData;
BuiltinData builtinData;
ToHDRPSurfaceAndBuiltinData(input, V, posInput, surface, surfaceData, builtinData);

ENCODE_INTO_GBUFFER(surfaceData, builtinData, posInput.positionSS, outGBuffer);

座標変換等少し特殊なことをしていますが、いつものようにスフィアトレーシングをしてサーフェースデータを計算します。(該当の処理はこの辺

その後 ToHDRPSurfaceAndBuiltinData でHDRPのライティングに必要なデータに変換します。

関数の中身はこのようになっています。

void ToHDRPSurfaceAndBuiltinData(FragInputs input, float3 V, inout PositionInputs posInput, DistanceFunctionSurfaceData surface, out SurfaceData surfaceData, out BuiltinData builtinData) {
    surfaceData = (SurfaceData)0;
    surfaceData.materialFeatures = MATERIALFEATUREFLAGS_LIT_STANDARD;
    surfaceData.normalWS = surface.Normal;
    surfaceData.ambientOcclusion = surface.Occlusion;
    surfaceData.perceptualSmoothness = surface.Smoothness;
    surfaceData.specularOcclusion = GetSpecularOcclusionFromAmbientOcclusion(ClampNdotV(dot(surfaceData.normalWS, V)), surfaceData.ambientOcclusion, PerceptualSmoothnessToRoughness(surfaceData.perceptualSmoothness));
    surfaceData.baseColor = surface.Albedo;
    surfaceData.metallic = surface.Metallic;
    input.positionRWS = surface.Position;
    posInput.positionWS = surface.Position;

    float alpha = 1.0;
#if HAVE_DECALS
    if (_EnableDecals)
    {
        DecalSurfaceData decalSurfaceData = GetDecalSurfaceData(posInput, alpha);
        ApplyDecalToSurfaceData(decalSurfaceData, surfaceData);
    }
#endif

    GetBuiltinData(input, V, posInput, surfaceData, alpha, surface.BentNormal, 0.0, builtinData);
    builtinData.emissiveColor = surface.Emissive;
}

HDRPは描画の計算に SurfaceDataBuiltinData が必要で、既存のGBufferパスもこの二つを計算する処理が書かれています。

SurfaceData はレイマーチングによって簡単にデータを作ることができますが、 BuiltinData はGI等HDRPの仕組みに依存したデータが必要です。

これは GetBuiltinData というLitBuiltinData.hlslに定義されている関数で SurfaceData 等の情報を使って取得してくれるのでそちらにお任せします。

Emissiveのみ取得後に上書きする必要があるので注意です。

この関数を拡張すれば、SSSマテリアル等にも対応できるのではないかと思っているので今後検証していきたいです。

この関数で取得した SurfaceDataBuiltinDataENCODE_INTO_GBUFFER を用いてGBufferに書き込みます。

レイマーチングオブジェクトの深度で上書きする

このままだとポリゴンの形状のままの深度値で計算されてしまうので、レイマーチングによって計算された深度で上書きします。

既にGBufferパスのフラグメントシェーダーには、深度を書き込む処理が実装されていますが、プリプロセッサで分岐しているので #define _DEPTHOFFSET_ON を定義します。

GBufferPass.hlslの該当箇所

+ #define _DEPTHOFFSET_ON

void Frag(  PackedVaryingsToPS packedInput,
            OUTPUT_GBUFFER(outGBuffer)
            #ifdef _DEPTHOFFSET_ON
            , out float outputDepth : SV_Depth
            #endif
            )
{

・・・

#ifdef _DEPTHOFFSET_ON
    outputDepth = depth;
#endif

HDRPのカスタマイズの内容

今回レイマーチングをHDRPで動かすにあたってHDRP本体に二行ほどの修正を行いました。

-           RenderDBuffer(hdCamera, cmd, renderContext, cullingResults);
-           // We can call DBufferNormalPatch after RenderDBuffer as it only affect forward material and isn't affected by RenderGBuffer
-           // This reduce lifteime of stencil bit
-           DBufferNormalPatch(hdCamera, cmd, renderContext, cullingResults);
#if ENABLE_RAYTRACING
            bool validIndirectDiffuse = m_RaytracingIndirectDiffuse.ValidIndirectDiffuseState();
            cmd.SetGlobalInt(HDShaderIDs._RaytracedIndirectDiffuse, validIndirectDiffuse ? 1 : 0);
#endif

            RenderGBuffer(cullingResults, hdCamera, renderContext, cmd);
+           // This will bind the depth buffer if needed for DBuffer)
+           RenderDBuffer(hdCamera, cmd, renderContext, cullingResults);
+           // We can call DBufferNormalPatch after RenderDBuffer as it only affect forward material and isn't affected by RenderGBuffer
+           // This reduce lifteime of stencil bit
+           DBufferNormalPatch(hdCamera, cmd, renderContext, cullingResults);

具体的に言うとGBufferパスの後にD(ecal)Bufferパスを描画するようにしました。

また Lit.shader をコピーして作成した Raymarching.shader から DepthOnlyPass を消去しました。

なぜこのような変更を加えたかというと、デプステクスチャのコピーのタイミングに関係があります。

HDRenderPipeline.csを除くとデプステクスチャーがコピーされる可能性があるタイミングが以下の二か所にあります。

Decalは設定によってON/OFFを切り替えれるため、DBufferパスを実行する場合はその直前に、しなかった場合はGBufferパスの直後にデプステクスチャーがコピーされるという実装になっているようです。

このままだと何が問題かというと、Decalが有効の場合、GBufferパスの前に既にデプステクスチャがコピーされてしまっているので、レイマーチングで上書きした深度がその後のパスに反映されないという状態になります。

f:id:kaneta1011:20190826143732p:plain
カスタム無し

f:id:kaneta1011:20190826143829p:plain
カスタム有り

このように前後関係がめちゃくちゃになります。

そのため、GBufferによる深度をコピー前に上書きするために、このような変更を加えました。

この時3つの解決策を思いつきましたが、結局GとDBufferの順番を入れ替えるという方法で解決することにしました。

  • ①DBufferとGBufferを入れ替える
    • メリット
      • 入れ替えるだけで簡単に解決できる
      • 追加のコピーや描画が不要なのでコストが安い
    • デメリット
      • GBufferパスでDBufferを参照しているため、入れ替えることによって1フレーム前のDBufferが参照されてしまう
  • ➁Depth Only Passでもレイマーチングを行って正しい深度を計算する
    • メリット
      • HDRP本体に変更を加える必要がないためクリーンに解決できる
    • デメリット
      • 追加のレイマーチングが必要なため負荷が非常に高い
  • ➂DBuffer前とGBuffer後、どちらもデプスをコピーするように変更する
    • メリット
      • 追加のレイマーチングをするのに比べてテクスチャのコピーなので負荷が少ない
    • デメリット
      • HDRP側の変更が入れ替えるものより少し多い
      • 入れ替えるものに比べて少し負荷が高い

今回は①の入れ替える方法を採用しましたが、個人的には負荷や正確性を考えて➂が良いのかなと思います。

さいごに

筆が遅すぎて、このプロジェクトを作ってから結構時間が空いてしまいましたが、ここまでの情報でHDRP上でレイマーチングできます!

HDRPのカスタムシェーダーを手書きするのはあまり賢い手段ではないのですが、何かの足しになれば幸いです。

まだ影とモーションベクターのパスの解説ができていないので、来月ぐらいにはそちらも書きたいです。

Unity HDRPをカスタマイズしてアウトラインパスを追加する

HDRPを弄れる環境はこちらで既に整えました

nanka.hateblo.jp

今回は実際にカスタマイズして周作に最適なアウトラインパスを追加してみます。(Render Graphがリリースされたらここの情報は無になります(多分...))

使用するバージョン

  • Unity 2019.2.0f1
  • HDRP 6.9.0-preview

今回のプロジェクトはこちらです。 github.com

Lit.shaderをカスタマイズしてアウトラインパスを追加する

大元となるのはHDRPのLit.shaderなので丸々コピーしてきてアウトラインパスを追加しましょう。

今回使うバージョンである6.9.0-previewLit.shader はこちらです。

github.com

Lit.shader にはいくつかのパスが既に記述されているので、それを参考にアウトラインを描画するパスを追加します。

Pass
{
    Name "Outline"
    Tags { "LightMode" = "Outline" }

    // アウトライン用のパスのためカリング設定を反対にする
    Cull Front

    ZTest On
    ZWrite On

    HLSLPROGRAM

    #define SHADERPASS SHADERPASS_FORWARD
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Material.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Lit/Lit.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Lit/ShaderPass/LitSharePass.hlsl"
    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Lit/LitData.hlsl"

    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/VertMesh.hlsl"

    PackedVaryingsType Vert(AttributesMesh inputMesh)
    {
        VaryingsType varyingsType;

        // オブジェクトスペースで法線方向に頂点を膨らませる
        inputMesh.positionOS += inputMesh.normalOS * 0.02;

        varyingsType.vmesh = VertMesh(inputMesh);
        return PackVaryingsType(varyingsType);
    }

    #ifdef TESSELLATION_ON

    PackedVaryingsToPS VertTesselation(VaryingsToDS input)
    {
        VaryingsToPS output;
        output.vmesh = VertMeshTesselation(input.vmesh);
        return PackVaryingsToPS(output);
    }

    #include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/ShaderPass/TessellationShare.hlsl"

    #endif // TESSELLATION_ON

    void Frag(  PackedVaryingsToPS packedInput,
                out float4 outColor : SV_Target
                )
    {
        // アウトラインは黒色
        outColor = float4(0.0, 0.0, 0.0, 1.0);
    }

    #pragma vertex Vert
    #pragma fragment Frag

    ENDHLSL
}

該当箇所 https://github.com/kaneta1992/HDRPExperiments/blob/6b432dbe8ed167e9315ef1fce6f2fc16a75e6953/Assets/Outline/Shaders/outline.shader#L767-L818

HDRPのコードを書き換えてOutlineパスを実行する

最近HDRPのコードを読んでいて描画の大部分は HDRenderPipeline.cs に記述されていそうな感じがします。

少なくともパスを追加して呼び出すということがしたいだけであれば、ここを起点に読み進めればできそうです!

https://github.com/Unity-Technologies/ScriptableRenderPipeline/blob/6.9.0-preview/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/HDRenderPipeline.cs

というわけでOutlineを呼び出す処理を書いていきます。

Outlineパス差し込み

実装は置いておいて HDRenderPipeline.cs にパスを差し込みます。

今回はアウトラインということなので、DefferdやForwardのライティング処理が終わってから実行したいです。

空描画の次あたりに差し込んでみます。

RenderForwardEmissive(cullingResults, hdCamera, renderContext, cmd);

RenderSky(hdCamera, cmd);
++ RenderOutline(cullingResults, hdCamera, renderContext, cmd);

RenderTransparentDepthPrepass(cullingResults, hdCamera, renderContext, cmd);

RenderOutlineを実装する

差し込みましたが実装がまだないのでその他のRender関数を参考に作ります。

今回は RenderForwardOpaque を参考にしました。

void RenderOutline(CullingResults cullResults, HDCamera hdCamera, ScriptableRenderContext renderContext, CommandBuffer cmd)
{
    bool debugDisplay = m_CurrentDebugDisplaySettings.IsDebugDisplayEnabled();
    using (new ProfilingSample(cmd, "Render Outline", CustomSamplerId.Outline.GetSampler()))
    {
        bool msaa = hdCamera.frameSettings.IsEnabled(FrameSettingsField.MSAA);

        RenderTargetIdentifier[] renderTarget = null;

        renderTarget = mMRTSingle;
        renderTarget[0] = msaa ? m_CameraColorMSAABuffer : m_CameraColorBuffer;

        HDUtils.SetRenderTarget(cmd, renderTarget, m_SharedRTManager.GetDepthStencilBuffer(msaa));
        var rendererList = RendererList.Create(CreateOpaqueRendererListDesc(cullResults, hdCamera.camera, HDShaderPassNames.s_OutlineName));
        DrawOpaqueRendererList(renderContext, cmd, hdCamera.frameSettings, rendererList);
    }
}

CustomSamplerIdとHDShaderPassNamesにOutlineを追加

RenderOutlineで使用する変数等を追加します。

HDCustomSamplerId.cs にプロファイリング用に使用する列挙体を追加します

ForwardPassName,
++ Outline,
ForwardTransparentDepthPrepass,

HDStringConstants.cs にパス名の文字列とIDを追加します。

public static readonly string s_ForwardStr = "Forward";
++ public static readonly string s_OutlineStr = "Outline";
public static readonly string s_DepthOnlyStr = "DepthOnly";
public static readonly ShaderTagId s_ForwardName = new ShaderTagId(s_ForwardStr);
++ public static readonly ShaderTagId s_OutlineName = new ShaderTagId(s_OutlineStr);
public static readonly ShaderTagId s_DepthOnlyName = new ShaderTagId(s_DepthOnlyStr);

動作確認

これでアウトラインパスが動作するHDRPとシェーダーの準備ができたので、作成したシェーダーをアサインしたマテリアルをメッシュに割り当てて動作確認します。 f:id:kaneta1011:20190804023946p:plain

問題なく動作していそうです!

単純なパスなら意外とさっくりと追加できました、もっと良い方法や考慮すべきことが残っていそうな気がするので何か情報をお持ちの方は是非教えていただきたいです。

Unity HDRPのコードを弄れる環境を整える

カスタマイズするか不明ですが、UPMでインストールしたパッケージはカスタマイズしずらいので、公式リポジトリをforkした物を使うプロジェクトを準備しておく。

フォークする

後で使うのでHDRPが含まれているこちらのSRP公式リポジトリをforkする。 github.com

f:id:kaneta1011:20190803161910p:plain

HDRPプロジェクト作る

今回は最近正式リリースされたUnity2019.2.0f1を使用してHDRPプロジェクトを作る。(Unity Hub等でインストールしておく)

この後forkしたSRPをサブモジュール設定するために、作ったプロジェクトは適当にgit管理しておく。

f:id:kaneta1011:20190803162557p:plain

ついでに Packages/manifest.json を覗いて使用しているHDRPのバージョンを確認しておく。

f:id:kaneta1011:20190803163126p:plain

このUnityバージョンで使用するHDRPは 6.9.0-preview らしい

プロジェクトでforkしたSRPを使用するためにサブモジュール設定をする。

プロジェクトのディレクトリに移動して以下のコマンドで設定(kaneta1992の部分はそれぞれのユーザー名に合わせる。)

kanet@DESKTOP-82NLVQ1 MINGW64 ~/HDRPExperiments (master)
$ git submodule add git@github.com:kaneta1992/ScriptableRenderPipeline.git ScriptableRenderPipeline

設定したら ScriptableRenderPipeline ディレクトリにリポジトリがクローンされるので、中に入って使用するバージョンをチェックアウトする

kanet@DESKTOP-82NLVQ1 MINGW64 ~/HDRPExperiments/ScriptableRenderPipeline (master)
$ git checkout 6.9.0-preview

manifest.jsonを書き換えて認識させる

プロジェクトはまだ公式パッケージを使用しているので、 manifest.json にサブモジュール設定したディレクトリを設定する。

    "com.unity.render-pipelines.high-definition": "file:../ScriptableRenderPipeline/com.unity.render-pipelines.high-definition",

パッケージのインポートが走って特にエラーが発生していなければ準備完了。 f:id:kaneta1011:20190803165006p:plain

今回準備したリポジトリ github.com

2019年7月に作ったシェーダーを紹介する(VRChatライブコーディングもやったぞ!)

f:id:kaneta1011:20190719164311p:plain
サムネ用

はじめに

毎月やっている月毎に制作したシェーダーの紹介第6回目です。

今月は先月までと比べて忙しい時期だった(後先月頑張りすぎて燃え尽きていた...)ので、あまり活動できませんでしたがいくつかシェーダーを作ったので紹介します。

7/3

f:id:kaneta1011:20190719164250p:plain

仕事から帰ってきて友人と通話している間にのんびりシェーダーライブコーディングの練習をしていてできたものです。

Travelerに登場した物体と似たようなものを出そうという意思と、bloom表現をボリューメトリックにしようと考えてできました。

一度のマーチングループで衝突判定とボリューム累積を行っているので、結構いい加減な見た目になっていますが、これはこれでありだと思ってます。

動くものはこちらから glslfan.com

7/16

会社の同僚にこちらのページを教えてもらい、インスパイアされて作り始めました。 www.reddit.com

Glow表現

レイマーチングでGlow表現をする時はボリューム累積を行うのが一般的なので、今回も例に漏れずそのようにしています。

自分が今までやっていたボリューム累積と異なるところとして

  • マーチ数は変更せずにシーンの深度を見てマーチ距離を決定する
  • ディザパターンを使用して見た目の情報量を増やす

を取り入れてみました。

f:id:kaneta1011:20190720230436p:plain
ピクセル毎にマーチ距離が一定の特に工夫なしのボリューム累積

f:id:kaneta1011:20190720230627p:plain
ディザパターンあり

f:id:kaneta1011:20190720230716p:plain
深度からマーチ距離を決定する

深度を見てマーチ距離を決定することで、距離が近い箇所のボリューム累積が正確になり何もしないよりかは良い見た目になったと思います。

ディザパターンはこちらのShadertoyで実装されていたベイヤー配列というパターンを使わせていただきました。

www.shadertoy.com

ja.wikipedia.org

特殊な距離関数合成

距離関数に関する情報はiq氏のページがとても有名ですが、MERCURYというデモグループのこちらのページにもとても有益な距離関数の情報が載っています(こちらは主に距離関数同士の合成に関するメソッドが多数あります)

mercury.sexy

f:id:kaneta1011:20190720233227p:plain
minによる合成

f:id:kaneta1011:20190720233310p:plain
fOpUnionStairsによる合成

スパイクブラー

f:id:kaneta1011:20190720233614p:plain
ブラー

このブラーをスパイクブラーと呼ぶか知りませんが、トゲトゲになるので僕はそのように読んでいます。

実は全然特殊なことをしていなくて、スクリーン中央から放射状にノイズを作成してアニメーション変数iTimeのオフセットとして利用しています。

iTimeにオフセットしてモーションブラーは過去にTraveler 2で利用したテクニックですが、これを放射状なノイズに変更するだけで全然違った印象になりました!

f:id:kaneta1011:20190720234038p:plain
放射状ノイズ

7/17

帰宅直前、玄関の前あたりでブタジエン氏に誘われて2度目のVRChatライブコーディングをやりました!

今回はなんと坪倉氏制作の4人ライブコーディングバトル特化の闘技場風のワールドで、私含めて4名のライブコーダーで二時間弱で同時にコーディングを実現できました!!

当日の様子はこちらにアーカイブされています。

www.youtube.com

私はアイデアが思いつかないときは過去に見た映像などを再現しようと頑張る傾向にあります。

コーディングが終わった後に、「元ネタを忘れてしまったがTwitterで見た映像を再現しようとした」という話をしたら、観覧者の一人だったfotfla氏が元ネタを知っていて、まさかの裏ライブコーディングで同じネタで作っていました...!

こんなこともあるのかと盛り上がっていました。

さいごに

7月も何とかシェーダーを作ることができました。

3日坊主で有名な僕が、7ヵ月も続けられていることが今でも驚きです...(それだけシェーダーで遊ぶのは楽しい)

最近はGLSLやシェーダーライブコーディングを始める人が多くて、TLを眺めるだけでもとても楽しいですし、VRChatというプラットフォームでみんなでワイワイシェーダーを書いてボイチャするという空間が楽しすぎました!

8月は何を作るかまだ未定ですが、もし何か作ったら紹介します。

LWRP学習 レンダリングパスを追加する

そろそろUnityも触らないとなぁと思ったので、LWRPを少しずつ学習していこうと思います。

手始めに、目標を達成するためにレンダリングパスを追加してみます。

今回はいずれシャドウマップを使ってエフェクトを作成したかったので、Screen Space Shadowパスをコピーして追加したパスでもう一度シャドウを生成するということをやったので雑なメモを残します。

大体こちらの記事の通りに進めればできました!

connect.unity.com

ScriptableRenderPassを作る

Scriptable Render Pipelineのリポジトリから該当の処理を探してほぼ丸コピしました。

github.com

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.LWRP;


public sealed class MyShadowPass : ScriptableRenderPass
{
    Material m_ScreenSpaceShadowsMaterial;
    RenderTargetHandle m_ScreenSpaceShadowmap;
    RenderTextureDescriptor m_RenderTextureDescriptor;
    const string m_ProfilerTag = "Kaneta Shadows";

    public MyShadowPass(RenderPassEvent evt, Material screenspaceShadowsMaterial)
    {
        m_ScreenSpaceShadowsMaterial = screenspaceShadowsMaterial;
        m_ScreenSpaceShadowmap.Init("_ScreenSpaceShadowmapTexture");
        renderPassEvent = evt;
    }

    public void Setup(RenderTextureDescriptor baseDescriptor)
    {
        m_RenderTextureDescriptor = baseDescriptor;
        m_RenderTextureDescriptor.depthBufferBits = 0;
        m_RenderTextureDescriptor.msaaSamples = 1;
        m_RenderTextureDescriptor.colorFormat = true
            ? RenderTextureFormat.R8
            : RenderTextureFormat.ARGB32;
    }

    public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    {
        cmd.GetTemporaryRT(m_ScreenSpaceShadowmap.id, m_RenderTextureDescriptor, FilterMode.Bilinear);

        RenderTargetIdentifier screenSpaceOcclusionTexture = m_ScreenSpaceShadowmap.Identifier();
        ConfigureTarget(screenSpaceOcclusionTexture);
        ConfigureClear(ClearFlag.All, Color.white);
    }

    /// <inheritdoc/>
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (m_ScreenSpaceShadowsMaterial == null)
        {
            Debug.LogErrorFormat("Missing {0}. {1} render pass will not execute. Check for missing reference in the renderer resources.", m_ScreenSpaceShadowsMaterial, GetType().Name);
            return;
        }

        if (renderingData.lightData.mainLightIndex == -1)
            return;

        Camera camera = renderingData.cameraData.camera;
        bool stereo = renderingData.cameraData.isStereoEnabled;

        CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);
        if (!stereo)
        {
            cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.identity);
            cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, m_ScreenSpaceShadowsMaterial);
            cmd.SetViewProjectionMatrices(camera.worldToCameraMatrix, camera.projectionMatrix);
        }
        else
        {
            // Avoid setting and restoring camera view and projection matrices when in stereo.
            RenderTargetIdentifier screenSpaceOcclusionTexture = m_ScreenSpaceShadowmap.Identifier();
            Blit(cmd, screenSpaceOcclusionTexture, screenSpaceOcclusionTexture, m_ScreenSpaceShadowsMaterial);
        }

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    /// <inheritdoc/>
    public override void FrameCleanup(CommandBuffer cmd)
    {

        cmd.ReleaseTemporaryRT(m_ScreenSpaceShadowmap.id);
    }
}

ScriptableRendererFeatureを作る

元のシャドウパス生成処理を真似てScriptableRendererFeatureを作ります。

github.com

using UnityEngine;
using UnityEngine.Rendering.LWRP;

[CreateAssetMenu(fileName = "MyShadowPassFeature",
    menuName = "Sample/MyShadowPassFeature", order = 1)]
public sealed class MyShadowPassFeature : ScriptableRendererFeature
{
    private MyShadowPass currentPass;

    public override void Create()
    {
        var screenspaceShadowsMaterial = new Material(Shader.Find("Hidden/Lightweight Render Pipeline/ScreenSpaceShadows"));
        if (currentPass == null)
            currentPass = new MyShadowPass(RenderPassEvent.BeforeRenderingPrepasses, screenspaceShadowsMaterial);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        RenderTextureDescriptor cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
        currentPass.Setup(cameraTargetDescriptor);
        renderer.EnqueuePass(currentPass);
    }
}

多分動いてるはず... f:id:kaneta1011:20190630234549p:plain