rn.log

備忘録など

【Unity】シャドウマッピングについて

環境

Unity2020.2.0f1
Universal RP 10.2.2

シーンに光源を置いてみる

シーンに緑色のDirectional Lightを置くと、以下のようになります。

f:id:r-ngtm:20210126050256p:plain:w480
緑のDirectional Lightを配置

光が当たった領域は緑色になりますが、影の領域には色を与えません ( = 真っ黒になります)

シャドウマップ

Unityでは、「ある領域が影になっているかどうか」の判定にシャドウマップを利用します。

f:id:r-ngtm:20210126072742p:plain:w640
シャドウマップの深度と描画点の深度を比較

シャドウマップのベイク

点光源を例に見てみます。
点光源から、シーンのオブジェクトに最も近い点までの距離を求め、それをテクスチャ(シャドウマップ)に保存します。
テクスチャは0 ~ 1 までの値しか保存できないので、範囲[0, maxDistance] を [0, 1] へ変換したものをテクスチャに保存します。

f:id:r-ngtm:20210126072044p:plain:w640
光源から見て最も近いオブジェクトまでの距離をシャドウマップへ保存

影の最大距離(maxDistance) はURP の Pipelineアセットの Shadows の部分から設定できます。

f:id:r-ngtm:20210126072552p:plain:w320
影のmaxDistance

シャドウマップを利用した影の判定

カメラで描画しようとしている点とライト間の距離D、シャドウマップに保存されている距離dを比較して、影かどうかを判定します。

f:id:r-ngtm:20210126072742p:plain:w640
シャドウマップの深度と描画点の深度を比較


ちなみに、MainLightのシャドウマッピングは Universal RP パッケージ内部の MainLightShadowCasterPass.cs にて実装されており、実装の内容を見ることができます。

FrameDebugger で シャドウマップを見てみる

以下のようなシーンを作成して、シャドウマップを見てみます。

f:id:r-ngtm:20210126080022p:plain
仮のシーン

DrawOpaqueObjects パスからシャドウマップを Ctrl + 左クリックすることで、シャドウマップを確認することができます。

f:id:r-ngtm:20210126075939p:plain
DrawOpaqueObjects

シャドウマップ

今回は以下のようなシャドウマップが確認できました。

f:id:r-ngtm:20210126080150p:plain
シャドウマップ

このシャドウマップは、カスケードシャドウマップになっています。

f:id:r-ngtm:20210126081532p:plain
シャドウマップの詳細
f:id:r-ngtm:20210126081731p:plain
カスケードの最大距離

おまけ : シャドウマップの実装場所

Universal RP の 中を見ると、シャドウマップを利用している処理を見ることができます。

Shadows.hlsl

Shadows.hlsl の中身を見ると、シャドウマップをサンプリングしている関数があります。

half MainLightRealtimeShadow(float4 shadowCoord)
{
#if !defined(MAIN_LIGHT_CALCULATE_SHADOWS)
    return 1.0h;
#endif

    ShadowSamplingData shadowSamplingData = GetMainLightShadowSamplingData();
    half4 shadowParams = GetMainLightShadowParams();
    return SampleShadowmap(TEXTURE2D_ARGS(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture), shadowCoord, shadowSamplingData, shadowParams, false);
}

Lighting.hlsl

上記のMainLightRealtimeShadow関数は、 Lighting.hlsl にて利用されています。

Light GetMainLight(float4 shadowCoord)
{
    Light light = GetMainLight();
    light.shadowAttenuation = MainLightRealtimeShadow(shadowCoord);
    return light;
}

上記のGetMainLight関数は URP の PBRシェーディングを行う関数 UniversalFragmentPBRの中で利用されています。(場所はLighting.hlslです)

    Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask);

    #if defined(_SCREEN_SPACE_OCCLUSION)
        AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(inputData.normalizedScreenSpaceUV);
        mainLight.color *= aoFactor.directAmbientOcclusion;
        surfaceData.occlusion = min(surfaceData.occlusion, aoFactor.indirectAmbientOcclusion);
    #endif

    MixRealtimeAndBakedGI(mainLight, inputData.normalWS, inputData.bakedGI);
    half3 color = GlobalIllumination(brdfData, brdfDataClearCoat, surfaceData.clearCoatMask,
                                     inputData.bakedGI, surfaceData.occlusion,
                                     inputData.normalWS, inputData.viewDirectionWS);
    color += LightingPhysicallyBased(brdfData, brdfDataClearCoat,
                                     mainLight,
                                     inputData.normalWS, inputData.viewDirectionWS,
                                     surfaceData.clearCoatMask, specularHighlightsOff);

【Unity】Linearワークフロー と Gammaワークフローについてまとめてみる

はじめに

UnityのLinearワークフローGammaワークフローについて、軽くまとめてみようと思います。

環境

Unity2020.2.0f1
Universal RP 10.2.2

問題

突然ですが、ここで問題です。
フラグメントシェーダーで 0.5 ( RGB = (128, 128, 128 ) ) という色を返した場合、
画面にはどんな色が表示されるでしょうか? (Linearワークフロー)

fixed4 frag (v2f i) : SV_Target
{
    return 0.5;
}

答え

RGB = (188, 188, 188) が表示されます。(明るく表示されます)


表示色が異なる理由はLinearワークフローにあります。

Linearワークフロー

通常のUnityでは、Linearワークフローが採用されており、以下のような計算を行います。

  • シェーダー内では色の計算をLinear空間で行う
  • 計算結果はガンマ補正してから画面に表示する

f:id:r-ngtm:20210120235736p:plain
色はガンマ補正してから画面に表示する

sRGBテクスチャが画面に表示されるまで (Linearワークフロー)

sRGBテクスチャが画面に表示される流れは以下のようになります。
1. sRGBテクスチャはサンプリング時にLinear色空間へ変換される (逆ガンマ補正)
2. シェーダーではLinear色空間で計算を行う
3. 色の計算結果はsRGB色空間へ変換してから画面に表示する(ガンマ補正)

f:id:r-ngtm:20210121000255p:plain
sRGBテクスチャがスクリーンに表示されるまで

sRGBテクスチャとLinearテクスチャの切り替え

テクスチャは、デフォルトではsRGBにチェックが入った状態になっています。
sRGBのチェックが入っていると、そのテクスチャはsRGB色空間にあるものとして扱われます。

f:id:r-ngtm:20210121003421p:plain
sRGBのチェックが入っていると、sRGB色空間として扱われる

テクスチャのsRGBのチェックを外すと、Linear色空間のテクスチャとして扱われるようになります。

f:id:r-ngtm:20210121002928p:plain
sRGBのチェックを外すとLinear色空間として扱われる

Linearテクスチャが画面に表示されるまで (Linearワークフロー)

Linear色空間のテクスチャは、テクスチャサンプリング時に逆ガンマ補正がかからなくなります。
画面に表示される際にはガンマ補正がかかります。
結果として、テクスチャは明るく表示されるようになります。

f:id:r-ngtm:20210121002814p:plain
非sRGBテクスチャが画面に表示されるまで(Linearワークフロー)

Linearワークフロー と Gammaワークフローの切り替え

UnityのPlayerSettingsには Color Space という設定項目があります。

ここがLinearの場合、UnityはLinearワークフローレンダリングを行うようになります。

f:id:r-ngtm:20210121001007p:plain
Linearワークフロー

Gammaだと、UnityはGammaワークフローレンダリングを行うようになります。。

f:id:r-ngtm:20210121000837p:plain
Gammaワークフロー

Gammaワークフロー

Gammaワークフローでは、色が画面にそのまま表示されるようになります。(Linearワークフローだとガンマ補正がかかっていました)

f:id:r-ngtm:20210121004008p:plain
Gammaワークフローでは色がそのまま表示される

テクスチャが画面に表示されるまで (Gammaワークフロー)

Gamma ワークフローでは、テクスチャの色はそのままシェーダーに取り込まれ、シェーダーの計算結果もそのまま画面に表示されます。

f:id:r-ngtm:20210121001336p:plain
Gammaワークフロー

まとめ

  1. Linearワークフローでは、ガンマ補正や逆ガンマ補正が入る
  2. Gammaワークフローでは、ガンマ補正や逆ガンマ補正は入らない

Linearワークフロー

f:id:r-ngtm:20210121000255p:plain:w500
Linearワークフロー

Gammaワークフロー

f:id:r-ngtm:20210121001336p:plain:w500
Gammaワークフロー

【Compute Shader メモ 2】中心差分法を利用してノーマルマップを作る

はじめに

Compute Shader で何かを作るシリーズ 第二弾!

今回は、Compute Shader で ノーマルマップ(法線マップ)を作ってみたいと思います。

f:id:r-ngtm:20210120000231p:plain:h240
実行結果 (1024x1024のハイトマップからノーマルマップを計算)

ノーマルマップ(法線マップ)とは

ノーマルマップとは、3Dモデルの法線情報を画像に保存したものです。

f:id:r-ngtm:20210119214419p:plain
ノーマルマップ

平面メッシュにノーマルマップを適用することで、あたかも凹凸が付いているように陰影をつけることができます。

f:id:r-ngtm:20210119214336p:plain
ノーマルマップを適用することで、凹凸が付いて見える

ノーマルマップによって陰影がつく原理

Unityは物体をレンダリングするときに、物体表面の法線を見て凹んでいるかどうかを判断しています。
(法線は黄色の線で表示しています)

f:id:r-ngtm:20210119221045p:plain:w480
凹凸のある面の法線はバラバラになる

ノーマルマップを使うと、物体の法線を再現することができ、陰影をつけることができます。

f:id:r-ngtm:20210119221358p:plain:w480
法線によって陰影がつく

Unityは物体をレンダリングするときに、物体の法線を見て凹んでいるかどうかを判断しています。
そのため、法線がバラつくとあたかもその面には凹凸が付いているかのように陰影が付くわけです。

Heightマップからノーマルマップを作る

今回、以下のような高低マップ(Heightマップ)から法線マップを作りたいと思います。

f:id:r-ngtm:20210119232127p:plain:w240
パーリンノイズ

以下のような法線マップを作ります。 (下記テクスチャはSubstance Designer で作ったノーマルマップになります)

f:id:r-ngtm:20210119232316p:plain:w240
ノーマルマップ

Heightマップからノーマルマップを求める方法

今回は中心差分法を利用して、Heightマップから勾配を求め、そこから法線を計算します。

f:id:r-ngtm:20210119211927p:plain:w320
法線ベクトルnは勾配の外積で計算

求めた法線情報はテクスチャ(ノーマルマップ)に保存します。



中心差分法で勾配を求める

ここで、連続な曲線 f(x) 上の接線(勾配)を求めることを考えます。

f:id:r-ngtm:20210119215214p:plain:w400
ある点での接線の傾きは、両隣の点の座標の差分で近似できる

ある点での接線の傾き(勾配)は、以下の式で近似できます。(中心差分法)
 \dfrac{ \partial f }{ \partial x } = \dfrac{ y_{i + 1} - y_{i - 1} }{ 2 \Delta x } = \dfrac{ f(x_{i + 1}) - f(x_{i - 1}) }{ 2 \Delta x }

参考 : http://www.icehap.chiba-u.jp/activity/SS2016/textbook/SS2016_miyoshi_FD.pdf



勾配ベクトルを求める

Hiehgtマップの座標(x,y)に保存されている高さ情報をf(x,y)とすると、以下のような図になります。

f:id:r-ngtm:20210119210754p:plain:w640
あるピクセルとその隣接ピクセルを可視化

中心差分法を利用すると二つの勾配ベクトル(\Delta x, 0, \Delta f_x ) , (0, \Delta y, \Delta f_y ) を求めることができます。

f:id:r-ngtm:20210119231430p:plain:w640
差分から勾配を求めることができる

外積を利用して法線を求める

法線ベクトル \vec{n} はこれら2つの勾配の外積を利用すると計算することができます。
 \vec{n} (- \Delta y \Delta f_x,  - \Delta x \Delta f_y,  \Delta x \Delta y ) に平行になります。

f:id:r-ngtm:20210119211927p:plain:w640
法線ベクトルnは外積で計算

 \Delta x = 2, \Delta y = 2と置いたとき、
 \vec{n} (- \Delta f_x,  - \Delta f_y,  2 ) に平行になります。

Compute Shader で実装

#pragma kernel CSMain

RWTexture2D<float4> Result; // ノーマルマップの書き込み先
Texture2D<float4> HeightMap; // Heightマップ

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{    
    // Heightマップの差分
    float dfx = (HeightMap[id.xy + uint2(1, 0)].r - HeightMap[id.xy + uint2(-1, 0)].r);
    float dfy = (HeightMap[id.xy + uint2(0, 1)].r - HeightMap[id.xy + uint2(0, -1)].r);
    
    // 法線
    float3 n = float3(-dfx, -dfy, 2.0);
    n = normalize(n);
    
    // 範囲[-a,a]を[0,1]に変換してからテクスチャに書き込む    
    float a = 0.02;
    n = (n / a + 1.0)  * 0.5;

    Result[id.xy] = float4(n, 1.0);
}

Compute Shader を実行する C#スクリプト

using UnityEngine;
using UnityEngine.UI;

public class ComputeNormal : MonoBehaviour
{
    [SerializeField] private ComputeShader shader; // 実行するComputeShader
    [SerializeField] private Texture _texture; // 入力画像
    [SerializeField] private RawImage targetImage; // RenderTextureを割り当てる対象のRawImage
    private RenderTexture _renderTexture;
    private int kernel;
    
    private void Start()
    {
        // RenderTexture作成
        _renderTexture = new RenderTexture(_texture.width, _texture.height, 0);
        _renderTexture.enableRandomWrite = true;
        _renderTexture.Create();
        
        // 実行したいカーネル(関数のようなもの)
        kernel = shader.FindKernel("CSMain"); 
        
        // ComputeShaderへデータを渡す
        shader.SetTexture(kernel, "HeightMap", _texture); // 元となるテクスチャ
        shader.SetTexture(kernel, "Result", _renderTexture); // 書き込み先のテクスチャ
        
        // スレッドグループサイズの取得
        uint sizeX, sizeY, sizeZ;
        shader.GetKernelThreadGroupSizes(kernel, out sizeX, out sizeY, out sizeZ);
        
        shader.Dispatch(kernel, _texture.width / (int)sizeX, _texture.height / (int)sizeY, 1 / (int)sizeZ);

        targetImage.texture = _renderTexture;
    }
}

実行結果

f:id:r-ngtm:20210120000231p:plain
実行結果 (1024x1024のハイトマップからノーマルマップを計算)

【Unity】メッシュの頂点カラーにColorを設定するとParticleSystemで色が壊れる話

はじめに

Unity C# で作成したメッシュをParticleSystemで使用したとき、頂点カラーの表示が壊れました。
この現象の原因の考察・および回避方法をまとめようと思います。

f:id:r-ngtm:20210119040423p:plain:h320
ParticleSystemでメッシュを表示

以下のような頂点カラーを設定すると、ParticleSystemで表示したときに頂点カラーが壊れます。

mesh.colors = colors; // Color[]



環境

Unity 2020.2.0f1
Universal RP 10.2.2


mesh.colorに頂点カラーを設定

mesh.colorsに頂点カラーを設定してメッシュを作成したとします。

mesh.colors = colors; // Color[]

頂点カラー情報は メッシュに16byte カラーとして保存されます。

f:id:r-ngtm:20210119052726p:plain
colorsは16byteカラーとしてメッシュに保存されます。

MeshRenderer で表示した場合

このメッシュをMeshRendererで描画した際は、色が正常に表示されます。

f:id:r-ngtm:20210119040441p:plain:h320
MeshRendererで表示

メッシュのマテリアルにはシェーダーグラフを使用しており、頂点カラーをMasterノードとして出力しています。

f:id:r-ngtm:20210119040750p:plain:h320
頂点カラー(Vertex Color)を出力するShaderGraph

ParticleSystem で表示した場合

このメッシュをParticleSystemで表示すると色が壊れます。

f:id:r-ngtm:20210119040423p:plain:h320
ParticleSystemでメッシュを表示

mesh.colors は Color であるのに対して、Particle Systemでは頂点カラーは Color32 として扱われていることが理由だと考えられます。


以下のように32bitカラーを設定すれば、色が壊れる現象は回避できます。

mesh.colors32 = colors32; // Color32[]

Colors32 は 4byteカラー(32bitカラー)としてメッシュに保存されます。

f:id:r-ngtm:20210119055540p:plain
Colors32 は 4byteカラーとしてメッシュに保存される

色が壊れる理由について

ParticleSystemのカラーはColors32

Unityの ParticleSystemは頂点カラーをColor32として持っているようです。

たとえばParticleSystem.csの中を見ると、ParticleSystem.startColorはColor32型で定義されています。

/// <summary>
///   <para>The initial color of the particle. The current color of the particle is calculated procedurally based on this value and the active color modules.</para>
/// </summary>
public Color32 startColor

ドキュメントが見つからなかったので断言はできませんが、ParticleSystemはメモリ領域からColor32[ ]を読んでいると考えられます。
そして、メッシュの頂点カラー領域にColor[ ] が設定されていた場合、色が壊れます

16byte カラーを設定したときのメモリ領域

RGBA = (0.5, 0.5, 0.5 ,1.0) という灰色を メッシュのColorに設定したとします。

f:id:r-ngtm:20210119054337p:plain
RGB = (0.5, 0.5, 0.5)

メモリ上では、以下のような32bit Float値が4つ並びます。

f:id:r-ngtm:20210119045127p:plain
Color型は16byteカラー (RGBAがそれぞれ32bit float)

ParticleSystemは32bitカラーとして解釈

ParticleSystemは頂点カラーのことを Color32 だと思っているので、カラー情報を以下のように読み取ります。

f:id:r-ngtm:20210119045904p:plain
32bit color として解釈

1つの16byte カラーは 4つの32bit カラーとして解釈されます。

0.5を 32bit カラーとして解釈された場合

0.5という32bit float のビット列は、以下のような32ケタの2進数になります。

f:id:r-ngtm:20210119050221p:plain
0.5のビット列

ParticleSystemはこれをbyte が4つ並んだRGBAカラーとして解釈し、RGBA = (63, 0, 0, 0) になります。

f:id:r-ngtm:20210119051003p:plain
0.5という値は RGBA = (63, 0, 0, 0) として認識される

RGBA = (63, 0, 0, 0) は画面上では以下のような色として表示されます。

f:id:r-ngtm:20210119052512p:plain
RGB = (63, 0, 0)

0.6 を32bitカラーとして認識した場合

Rチャンネルが0.6だった場合も考えてみます。

0.6という32bit floatのビット列は以下のようになります。

f:id:r-ngtm:20210119051924p:plain
0.6のビット列

このビット列を32bit RGBAカラーとして解釈すると、RGBA = (63, 25, 153, 154) になります。

f:id:r-ngtm:20210119052302p:plain
0.6を32bit RGBAカラーとして解釈した場合

RGBA = (63, 25, 153, 154)は画面上では以下のような色として表示されます。

f:id:r-ngtm:20210119052434p:plain
RGB = (63, 25, 153)


まとめ

Color[] を 頂点カラーに設定すると、想定とは異なる色が表示されてしまうことがわかりました。

メッシュを作成するときは以下のように32bitカラーを設定すれば、色が壊れる現象を回避できます。

mesh.colors32 = colors32; // Color32[]
f:id:r-ngtm:20210119055540p:plain
Colors32 は 4byteカラー(32bitカラー)としてメッシュに保存される

【ComputeShader メモ 1】平均化フィルタで画像をぼかす

はじめに

Compute Shader を理解したいと思い、簡単に実装できそうな 平均化フィルタ をCompute Shaderで実装してみました。

環境

Unity 2020.2.0f1
Universal RP 10.2.2

平均化フィルタの効果

平均化フィルタを適用することで、画像がぼやけるようなエフェクトをかけることができます。

f:id:r-ngtm:20210117162402p:plain
平均化フィルタの適用

平均化フィルタの仕組み

画像のあるピクセルに注目し、その値を周辺のピクセルの平均値に置き換えるような処理を考えます。

f:id:r-ngtm:20210117155047p:plain
平均化フィルタ(3x3)を利用した画素の置き換え

この平均化処理を画像のすべてのピクセルに対して適用することで、画像がぼやけたような効果が得られます。
先ほどの例ではフィルタサイズ3x3で説明していましたが、以下の例では64x64という大きいサイズのフィルタを適用しています。

f:id:r-ngtm:20210117162402p:plain
平均化フィルタの適用

平均化フィルタの実装 (Compute Shader)

ComputeShaderによる平均化フィルタの実装例を以下に示します。

ここでは[numthreads(1,1,1)] と書いてしまっていますが、これはあまり良いコードではありません(理由は後述します)

#pragma kernel CSMain

int FilterSize; // フィルタサイズ
Texture2D<float4> Texture; // 入力画像
RWTexture2D<float4> RenderTexture; // 平均化フィルタを書けた画像の書き込み先

[numthreads(1,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // 周辺ピクセルを合計する
    float4 total = 0.0; 
    for (int y = -FilterSize/2; y <= FilterSize/2; y++) {
        for (int x = -FilterSize/2; x <= FilterSize/2; x++) {
            total += Texture[id.xy + int2(x, y)]; // sRGBテクスチャから取得する色には逆ガンマ補正がかかる(リニア色空間)
        }
    }
    // フィルタの大きさで割る (平均値をとる)
    total = total / FilterSize / FilterSize;
    
    // 書き込み先のRenderTextureはsRGBなので、ガンマ補正をかける (LinearカラーをsRGBカラーにする)
    total = pow(total, 1.0/2.2);
    
    // RenderTexture へ書き込み
    RenderTexture[id.xy] = total;
}

ComputeShader を実行するC#スクリプト

using UnityEngine;
using UnityEngine.UI;

public class AverageFilter : MonoBehaviour
{
    [SerializeField] private ComputeShader shader; // 実行するComputeShader
    [SerializeField] private Texture _texture; // 入力画像
    [SerializeField] private RawImage targetImage; // RenderTextureを割り当てる対象のRawImage
    [SerializeField] private int filterSize = 64; // フィルタサイズ
    private RenderTexture _renderTexture;

    private void Start()
    {
        // RenderTexture作成
        _renderTexture = new RenderTexture(_texture.width, _texture.height, 0);
        _renderTexture.enableRandomWrite = true;
        _renderTexture.Create();
        
        // 実行したいカーネル(関数のようなもの)
        int kernel = shader.FindKernel("CSMain"); 

        // ComputeShaderへデータを渡す
        shader.SetInt("FilterSize", filterSize); // フィルタサイズ
        shader.SetTexture(kernel, "Texture", _texture); // 元となるテクスチャ
        shader.SetTexture(kernel, "RenderTexture", _renderTexture); // 書き込み先のテクスチャ
        
        // テクスチャの画素数ぶんのスレッドを回す
        shader.Dispatch(kernel, _texture.width, _texture.height, 1);

        targetImage.texture = _renderTexture;
    }
}

結果

元のテクスチャ(左側)と、ComputeShaderでぼかした結果のレンダーテクスチャを割り当てたものを並べてみました。
両方ともRawImageにテクスチャを割り当てて表示しています。

f:id:r-ngtm:20210117162402p:plain
平均化フィルタの適用

スレッドグループ数は1を指定しないほうが良い

先ほどはスレッドグループ数 [numthreads(1,1,1)] を指定していました。
下記リンクによれば、スレッドグループ数には32や64といった数を指定した方が良いようです。
www.reddit.com


スレッドグループ数に1を指定した場合でも、nVidiaGPUでは32スレッドのグループが確保されてしまい、結果としてGPUの3%ぶんしか活用されないようです。

E.G. nVidia's hardware is organized in groups of 32 threads, any threadgroups of less than 32 threads is still using 32 hardware threads. The extra hardware threads are just prevented from writing to memory so they have no effect. A 1080ti has about 28 compute clusters, each with 128 threads. If you do (1,1,1) you can run at most 112 threads (28CUs * 4 simd's per CU) out of the 3584 (28*128) potential threads that the gpu can run. Achieving about 3% gpu utilization.

修正ComputeShader

今回は1024x1024の画像に対して平均化フィルタを適用しているため、[numthreads(32,32,1)] を指定してみます。 (合計 32 * 32 * 1 = 1024スレッドが実行されます)

#pragma kernel CSMain

int FilterSize; // フィルタサイズ
Texture2D<float4> Texture; // 入力画像
RWTexture2D<float4> RenderTexture; // 平均化フィルタを書けた画像の書き込み先

[numthreads(32,32,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // 周辺ピクセルを合計する
    float4 total = 0.0; 
    for (int y = -FilterSize/2; y <= FilterSize/2; y++) {
        for (int x = -FilterSize/2; x <= FilterSize/2; x++) {
            total += Texture[id.xy + int2(x, y)];
        }
    }
    // フィルタの大きさで割る (平均値をとる)
    total = total / FilterSize / FilterSize;
    
    // RenderTextureはsRGBなので、リニアカラーにガンマ補正をかける
    total = pow(total, 1.0/2.2);
    
    // RenderTexture へ書き込み
    RenderTexture[id.xy] = total;
}

修正ComputeShaderを実行するC#スクリプト

Threadグループ数(numthread)を考慮して、Threadグループサイズを計算します。

 グループ数 \times グループサイズ = 合計スレッド数 


今回はテクスチャの画素数ぶんのスレッドを回すため、Threadグループサイズは以下の計算で求まります。

 グループサイズ  = \dfrac{テクスチャ画素数}{グループ数}


using UnityEngine;
using UnityEngine.UI;

public class AverageFilter : MonoBehaviour
{
    [SerializeField] private ComputeShader shader; // 実行するComputeShader
    [SerializeField] private Texture _texture; // 入力画像
    [SerializeField] private RawImage targetImage; // RenderTextureを割り当てる対象のRawImage
    [SerializeField] private int filterSize = 64; // フィルタサイズ
    private RenderTexture _renderTexture;

    private void Start()
    {
        // RenderTexture作成
        _renderTexture = new RenderTexture(_texture.width, _texture.height, 0);
        _renderTexture.enableRandomWrite = true;
        _renderTexture.Create();
        
        // 実行したいカーネル(関数のようなもの)
        int kernel = shader.FindKernel("CSMain"); 
        
        // ComputeShaderへデータを渡す
        shader.SetInt("FilterSize", filterSize); // フィルタサイズ
        shader.SetTexture(kernel, "Texture", _texture); // 元となるテクスチャ
        shader.SetTexture(kernel, "RenderTexture", _renderTexture); // 書き込み先のテクスチャ

        // スレッドグループサイズの取得
        uint sizeX, sizeY, sizeZ;
        shader.GetKernelThreadGroupSizes(kernel, out sizeX, out sizeY, out sizeZ);

        // スレッドグループサイズ * スレッドグループ数 = テクスチャ画素数 となるようなスレッドグループ数を指定して実行 
        shader.Dispatch(kernel, 
            _texture.width / (int)sizeX,
            _texture.height / (int)sizeY, 1 / (int)sizeZ);

        targetImage.texture = _renderTexture;
    }
}

結果

左側に元画像、右側に平均化フィルタを適用した画像を並べてみました。

f:id:r-ngtm:20210117171253p:plain
平均化フィルタサイズ 64 x 64 での実行結果
f:id:r-ngtm:20210117171337p:plain
平均化フィルタサイズ 256 x 256 での実行結果

【ガンマ補正】sRGBテクスチャをRenderTextureに書き込むと表示が暗くなる話【Unity】

環境

Unity2020.2.0f1
Universal RP 10.2.2

はじめに

sRGBのテクスチャをCompute Shaderでサンプリングし、
計算結果をRenderTexture(sRGB)へコピーすると、表示がなぜか暗くなるという不可解な現象に遭遇しました。

f:id:r-ngtm:20210117111100p:plain
sRGBテクスチャをRenderTexture(sRGB)にコピーすると暗くなる

シェーダーにてsRGBテクスチャをサンプリングした色がリニア色空間であり、これをsRGBのテクスチャに書き込んだことによって発生する現象だと考えられます。

sRGBテクスチャから取得する色

sRGBテクスチャの色空間はガンマ色空間です。
sRGBテクスチャ(ガンマ色空間)をシェーダーでサンプリングする時、逆ガンマ補正された値(リニア色空間)がシェーダーへ流れてきます。

f:id:r-ngtm:20210117103352p:plain
sRGBテクスチャをsRGBレンダーテクスチャにコピーすると暗くなる

sRGBレンダーテクスチャにリニアカラーを書き込んでしまうと、Unity側でガンマ補正が行われなくなるため、表示結果が暗くなります。

sRGBカラーが逆ガンマ補正される理由について

sRGBテクスチャをサンプリングする時、逆ガンマ補正がなぜ入るのかをまとめてみたいと思います。

sRGBカラーはそのままモニターに表示される

ここで、sRGBテクスチャに 0.2, 0.3 という色が保存されていたとします。

これらをそのままモニターに表示した場合、0.2, 0.3 という色が表示されます。
モニターに送信される色は、ディスプレイ側で逆ガンマ補正がかかります。

f:id:r-ngtm:20210117122257p:plain
sRGBテクスチャカラーが画面に表示されるまで

sRGBカラーをそのまま使うと表示がおかしくなる

sRGBカラー 0.2, 0.3 という値をシェーダー内でそのまま足し算してしまうと、モニターには 1.134... という値が表示されてしまい、 0.2 + 0.3 = 0.5 という計算結果に一致しません。

f:id:r-ngtm:20210117122956p:plain
sRGBテクスチャカラーをそのまま足すと結果がおかしくなる

sRGBカラーを逆ガンマ補正すると表示が正しくなる

sRGBカラー を最初に逆ガンマ補正してからシェーダーにて足し算を行い、
モニターに送信する前にガンマ補正をかけてやると、モニター上には 0.5 という色が表示されます。

sRGBカラーの  0.2 + 0.3 = 0.5 という計算結果と、モニター上の表示0.5 が一致するのでこれは都合が良い結果となります。

f:id:r-ngtm:20210117123053p:plain
sRGBカラーを逆ガンマ補正してから計算に使った場合

sRGBカラーの逆ガンマ補正はUnityが自動でやってくれるようです。

f:id:r-ngtm:20210117124325p:plain
sRGBのチェックを入れると、シェーダー利用時に逆ガンマ補正がかかるようになります

【シェーダー】バイリニア補間で4色グラデーションを作る

はじめに

シェーダーを使い、以下のような4色のグラデーションを作る方法を紹介します。

f:id:r-ngtm:20210116142756p:plain:w480
4色グラデーション

2色グラデーション

x座標を使って二つの色を線形補間した場合、以下のような二色のグラデーションを作ることができます。

f:id:r-ngtm:20210116153308p:plain:w480
2つの色を線形補間
vec3 c = mix(GREEN, BLUE, p.x);  // x座標で 緑と青を線形補間

4色グラデーション

x, y座標を使って4色を補完すると、以下のような4色のグラデーションを作ることができます。

f:id:r-ngtm:20210116152839p:plain:w480
x, y で4つの色をブレンド
vec3 tc = mix(YELLOW, RED, p.x); // top : 黄と赤の線形補間 (上側の色)
vec3 bc = mix(GREEN, BLUE, p.x); // bottom : 緑と青の線形補間 (下側の色)
vec3 c = mix(bc, tc, p.y); // y座標で top と bottom の線形補間

この補間はバイリニア補間(双一次補間)という名前がついています。


GLSLによる4色グラデーション実装

uniform vec2 resolution;

#define YELLOW vec3(1,1,0)
#define RED vec3(1,0,0)
#define GREEN vec3(0,1,0) 
#define BLUE vec3(0,0,1)

void main()
{
    vec2 p = gl_FragCoord.xy / resolution.xy;    
    vec3 tc = mix(YELLOW, RED, p.x); // top color
    vec3 bc = mix(GREEN, BLUE, p.x); // bottom color
    vec3 c = mix(bc, tc, p.y);
    gl_FragColor = vec4(c, 1);
}

シェーダーグラフによる実装

f:id:r-ngtm:20210116153835p:plain