ShaderTips

シェーダーTips

主にUnityシェーダーについての記事を書いています。

リニアワークフロー 概念の説明とUnityにおける実装と設定

視覚のガンマ

下の画像を見てください。

左から右に行くにつれて、白から黒へとグラデーションしており、人間の目で見た輝度はリニア(線形)になっています。 つまり黒の明るさを0、白の明るさを1とすると、中間の明るさは0.5に見えます。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210231541.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

しかし、実際の輝度は中間の明るさは約0.22です。 見た目と実際の輝度には差があり、人間の目に届くまで約0.45乗されています。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210232617.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

見た目の輝度と実際の輝度のグラフです。 リニア(直線)ではなく、ガンマカーブと呼ばれる曲線が描かれます。 f:id:Ny_Program:20210819104636p:plain

ディスプレイガンマ

ディスプレイには受け取った輝度を2.2乗してしまう性質があります。 これをガンマ補正と言います。

また、ガンマ補正する数値(2.2)のことをガンマ値と言います。

https://tech.cygames.co.jp/wp-content/uploads/2015/08/2015-08-21_gamma1.png 物理ベースレンダリング -リニアワークフロー編 (1)- | Cygames Engineers' Blog引用

このままだと暗くなってしまうので、予めガンマ値の逆数を累乗(1/2.2=約0.45乗)しておくことで、ガンマ補正を打ち消します。 これをデガンマと言います。

https://tech.cygames.co.jp/wp-content/uploads/2015/08/2015-08-21_gamma2.png 物理ベースレンダリング -リニアワークフロー編 (1)- | Cygames Engineers' Blog引用

リニア色空間

リニア色空間とは中間の明るさが0.5の色空間です。 数値的にリニアですが、物理的には間違えている色空間です。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210231541.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

sRGB色空間

sRGB色空間とは中間の明るさが約0.22の色空間です。 輝度が1/2.2=約0.45乗されているので、数値的にノンリニアですが、物理的に正しい色空間です。

https://cdn-ak.f.st-hatena.com/images/fotolife/h/halya_11/20200210/20200210232617.png リニアワークフロー入門 簡単な説明とその意義 - LIGHT11引用

デザイナーが見ているモニターはsRGB色空間なので、デザイナーが手書きした画像もsRGB色空間です。

https://4.bp.blogspot.com/-3ShSfgxoPsU/UnyHW6sfF8I/AAAAAAAAaic/qVLrZIciaUE/s800/illustrator.png

sRGB色空間の画像は輝度が1/2.2乗されているので(デガンマされている)、ディスプレイガンマで2.2乗されることで結果的にリニア色空間になります。

https://tech.cygames.co.jp/wp-content/uploads/2015/09/sRGB_Linear.png 物理ベースレンダリング -リニアワークフロー編 (2)- | Cygames Engineers' Blog引用 しかし、人間の目に届く時には視覚のガンマで約0.45乗されます。

エンジニアが意識すること

デザイナーが作成した画像などのsRGB色空間はディスプレイガンマと相殺させることができるという説明をしました。 逆にリニア色空間のライトはディスプレイガンマが掛かって色が暗くなってしまうので、対策をしなければなりません。

また、sRGB色空間とリニア色空間は色空間が違うため、[sRGB色空間の画像カラー * リニア色空間のライトカラー]のように一緒に計算できません。 sRGB色空間をリニア色空間に変換してから、計算する必要があります。 どちらの色空間に合わせても良いのですが、計算しやすいようにリニア色空間にします。 その上で、計算結果をディスプレイガンマと相殺させるために、sRGB色空間に戻す必要があります。 これがリニアワークフローです。

物理ベースレンダリングでどれだけ物理的にライティング計算を行っても、結果を正しく出力できなければ意味がありません。

実装と解説

Unityでリニアワークフローを実装しました。 球体にテクスチャを貼り付け、簡単なライティングを行なっています。 左がガンマワークフロー(従来の何もしない手法)で右がリニアワークフローです。 ガンマワークフローではディスプレイガンマがかかっていて、暗くなっていますが、リニアワークフローではデガンマしているので、暗くなっていません。 f:id:Ny_Program:20210828122335p:plain

以下、シェーダーコードです。

Shader "LinearWorkflow"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Intensity("Intensity", Range(0,1)) = 0
        [Toggle] _IS_LINEAR("Is Linear", Float) = 0
        [Toggle] _USE_FAST("Use Fast", Float) = 0
    }
    
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
            #pragma shader_feature _ _USE_FAST_ON
            #pragma shader_feature _ _IS_LINEAR_ON

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

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

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.normal = UnityObjectToWorldNormal(v.normal);
                return o;
            }

            sampler2D _MainTex;
            half _Intensity;

            #define FLT_EPSILON     1.192092896e-07 // Smallest positive number, such that 1.0 + FLT_EPSILON != 1.0
            
            
            float3 PositivePow(float3 base, float3 power)
            {
                return pow(max(abs(base), float3(FLT_EPSILON, FLT_EPSILON, FLT_EPSILON)), power);
            }


            half3 sRGBToLinear(half3 c)
            {
                half3 linearRGB;
                #ifdef _USE_FAST_ON
                    linearRGB = pow(c,2.2);
                #else 
                    half3 linearRGBLo = c / 12.92;
                    half3 linearRGBHi = PositivePow((c + 0.055) / 1.055, half3(2.4, 2.4, 2.4));
                    linearRGB = (c <= 0.04045) ? linearRGBLo : linearRGBHi;
                #endif
                return linearRGB;
            }

            
            half4 sRGBToLinear(half4 c)
            {
                return half4(sRGBToLinear(c.rgb), c.a);
            }


            half3 LinearTosRGB(half3 c)
            {
                half3 sRGB;
                #ifdef _USE_FAST_ON
                    sRGB = pow(c, 1 / 2.2);
                #else
                    half3 sRGBLo = c * 12.92;
                    half3 sRGBHi = (PositivePow(c, half3(1.0 / 2.4, 1.0 / 2.4, 1.0 / 2.4)) * 1.055) - 0.055;
                    sRGB = (c <= 0.0031308) ? sRGBLo : sRGBHi;
                #endif
                return sRGB;
            }

            half4 LinearTosRGB(half4 c)
            {
                return half4(LinearTosRGB(c.rgb), c.a);
            }


            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 albedo = tex2D(_MainTex, i.uv);
                #if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON
                    albedo = sRGBToLinear(albedo);
                #endif
                half lambert = max(0,dot(i.normal,WorldSpaceLightDir(i.vertex))) * _Intensity;
                fixed4 retColor = albedo * lambert;
                #if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON
                    retColor = LinearTosRGB(retColor);
                #endif            
                return retColor;
            }
            ENDCG
        }
    }
}
Propertiesの中身

_Intensity ライトの強度

_IS_LINEAR オンにするとリニアワークフローが有効。オフにするとガンマワークフローになります。

_USE_FAST オンにするとリニアワークフローの近似式を使用します。

sRGB色空間 → リニア色空間

紫の枠の部分です。

f:id:Ny_Program:20210828131311p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

      fixed4 albedo = tex2D(_MainTex, i.uv);
#if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON
      albedo = sRGBToLinear(albedo);
#endif

sRGB色空間のテクスチャをリニア色空間に変換しています。

UNITY_COLORSPACE_GAMMAはEdit>ProjectSetting>OtherSetting>Color Spaceで、ガンマ(デフォルト)にすると有効になり、リニアを選択すると無効になります。

f:id:Ny_Program:20210827135855p:plain:w500

コードを載せていますが、実はUnityの設定だけでリニアワークフローは実現できます。

ColorSpaceをリニアにしておき、テクスチャ設定でsRGBにしておけば、UNITY_COLORSPACE_GAMMA内の処理と同じようなことをやってくれます。 f:id:Ny_Program:20210828132127p:plain

今回はリニアワークフローを理解するためにColorSpaceをガンマにした状態でも、リニアワークフローが効くようにします。

            half3 sRGBToLinear(half3 c)
            {
                half3 linearRGB;
                #ifdef _USE_FAST_ON
                    linearRGB = pow(c,2.2);
                #else 
                    half3 linearRGBLo = c / 12.92;
                    half3 linearRGBHi = PositivePow((c + 0.055) / 1.055, half3(2.4, 2.4, 2.4));
                    linearRGB = (c <= 0.04045) ? linearRGBLo : linearRGBHi;
                #endif
                return linearRGB;
            }

            
            half4 sRGBToLinear(half4 c)
            {
                return half4(sRGBToLinear(c.rgb), c.a);
            }

sRGBToLinearの中身です。

sRGB→リニア変換を行なっています。

_USE_FAST_ONでは近似式を使用しています。 引数のカラーに対して、2.2乗しています。

#elseの中身はこちらのリポジトリから引用しました。 ご参考にどうぞ。 github.com

リニア色空間でライティング計算

f:id:Ny_Program:20210828134848p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

half lambert = max(0,dot(i.normal,WorldSpaceLightDir(i.vertex))) * _Intensity;
fixed4 retColor = albedo * lambert;

リニア色空間でライティングの計算を行います。 ライトはリニア色空間なのでそのまま計算に使います。

リニア色空間→sRGB色空間

f:id:Ny_Program:20210828135215p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

#if UNITY_COLORSPACE_GAMMA && _IS_LINEAR_ON
      retColor = LinearTosRGB(retColor);
#endif            
      return retColor;

シェーダー最後のフローです。 リニア色空間からsRGB色空間に変換します。 sRGB色空間にしておくことで、ディスプレイガンマがかかった時にリニアになります。

ここでも、ColorSpaceをリニアにすることで、UNITY_COLORSPACE_GAMMA内の処理と同じようなことをやってくれます。

            half3 LinearTosRGB(half3 c)
            {
                half3 sRGB;
                #ifdef _USE_FAST_ON
                    sRGB = pow(c, 1 / 2.2);
                #else
                    half3 sRGBLo = c * 12.92;
                    half3 sRGBHi = (PositivePow(c, half3(1.0 / 2.4, 1.0 / 2.4, 1.0 / 2.4)) * 1.055) - 0.055;
                    sRGB = (c <= 0.0031308) ? sRGBLo : sRGBHi;
                #endif
                return sRGB;
            }

            half4 LinearTosRGB(half4 c)
            {
                return half4(LinearTosRGB(c.rgb), c.a);
            }

LinearTosRGBの中身です。

リニア→sRGB変換を行なっています。

引数のカラーに対して、1/2.2乗しています。

ディスプレイガンマ

f:id:Ny_Program:20210828141311p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク 輝度を2.2乗して出力します。

sRGB色空間で出力したので、リニア色空間になります。

視覚のガンマ

f:id:Ny_Program:20210828141118p:plain 分かる!リニアワークフローのコンポジット - コンポジゴク

最初に紹介し、置き去りにされた視覚のガンマがかかります。

輝度が1/2.2乗されて目に届きます。

普段私たちは視覚のガンマがかかった状態なので、視覚のガンマに対して、デガンマは行いません。

参考

compojigoku.blog.fc2.com tech.cygames.co.jp tech.cygames.co.jp docs.unity3d.com r-ngtm.hatenablog.com

www.slideshare.net

【Unity】ComputeShader + Graphics.DrawMeshInstancedIndirectで大量のオブジェクトを描画する

今回はComputeShaderで大量のオブジェクトの座標を計算して、それをGPUインスタンシングを使って描画します。

オブジェクトの移動アルゴリズムBoidアルゴリズムを使用していますが、今回こちらの解説は行いません。 youtu.be

Step1 : ComputeShaderに必要な情報を渡し実行する

Boid.cs

using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using Random = Unity.Mathematics.Random;

public class Boids : MonoBehaviour
{
    public struct BoidState
    {
        public Vector3 Position;
        public Vector3 Forward;
    }

    [Serializable]
    public class BoidConfig
    {
        public float moveSpeed = 1f;
        // 分離
        [Range(0f, 1f)] public float separationWeight = .5f;
        // 整列
        [Range(0f, 1f)] public float alignmentWeight = .5f;
        // 結合
        [Range(0f, 1f)] public float targetWeight = .5f;

        public Transform boidTarget;
    }

    public int boidCount = 10000;

    public float3 boidExtent = new(32f, 32f, 32f);

    public ComputeShader BoidComputeShader;

    public BoidConfig boidConfig;

    GraphicsBuffer _boidBuffer;
    GraphicsBuffer _argsBuffer;
    int _kernelIndex;

    [SerializeField]
    Mesh mesh;
    [SerializeField]
    Material drawMaterial;

    void Start()
    {
        InitializeArgsBuffer();
        InitializeBoidsBuffer();
    }

    private void InitializeArgsBuffer()
    {
        var args = new uint[] { 0, 0, 0, 0, 0 };

        args[0] = mesh.GetIndexCount(0);
        args[1] = (uint)boidCount;

e        _argsBuffer.SetData(args);
    }

    private void InitializeBoidsBuffer()
    {
        var random = new Random(256);
        var boidArray = new NativeArray<BoidState>(boidCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
        for (var i = 0; i < boidArray.Length; i++)
        {
            boidArray[i] = new BoidState
            {
                Position = random.NextFloat3(-boidExtent, boidExtent),
                Forward = math.rotate(random.NextQuaternionRotation(), Vector3.forward),
            };
        }
        // GraphicsBuffer生成
        _boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
        // boidBufferに値を設定
        _boidBuffer.SetData(boidArray);
    }

    void Update()
    {
        UpdateBoids();
        RenderMesh();
    }

    void UpdateBoids()
    {
        var boidTarget = boidConfig.boidTarget != null
            ? boidConfig.boidTarget.position
            : transform.position;
        BoidComputeShader.SetFloat("deltaTime", Time.deltaTime);
        BoidComputeShader.SetFloat("separationWeight", boidConfig.separationWeight);
        BoidComputeShader.SetFloat("alignmentWeight", boidConfig.alignmentWeight);
        BoidComputeShader.SetFloat("targetWeight", boidConfig.targetWeight);
        BoidComputeShader.SetFloat("moveSpeed", boidConfig.moveSpeed);
        BoidComputeShader.SetVector("targetPosition", boidTarget);
        // ComputeShaderに生成するインスタンスの数をセット
        BoidComputeShader.SetInt("numBoids", boidCount);
        _kernelIndex = BoidComputeShader.FindKernel("CSMain");

        BoidComputeShader.GetKernelThreadGroupSizes(_kernelIndex, out var x, out var y, out var z);

        // ComputeShaderにboidBufferをセット
        BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
        // ComputeShaderを実行
        BoidComputeShader.Dispatch(_kernelIndex, (int)(boidCount / x), 1, 1);
    }

    void RenderMesh()
    {
        if (!SystemInfo.supportsInstancing)
        {
            return;
        }
        drawMaterial.SetBuffer("_BoidDataBuffer", _boidBuffer);
        BoidState[] data = new BoidState[10];
        _boidBuffer.GetData(data);
        Debug.Log(data);
        Graphics.DrawMeshInstancedIndirect
        (
            mesh,
            0,
            drawMaterial,
            new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)),
            _argsBuffer
        );
    }

    void OnDisable()
    {
        _boidBuffer?.Dispose();
        _argsBuffer?.Dispose();
    }
}

InitializeBoidsBufferでComputeShaderに渡すGraphicsBufferを作成しています。 BoidState(位置と進行方向を表す情報)を格納します。 boidBufferと名付けました。

// GraphicsBuffer生成
_boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
// boidBufferに値を設定
_boidBuffer.SetData(boidArray);

UpdateBoidsで必要な情報やboidBufferをComputeShaderに渡して、実行しています。

// ComputeShaderにboidBufferをセット
BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
// ComputeShaderを実行
BoidComputeShader.Dispatch(_kernelIndex, (int)(boidCount / x), 1, 1);

Step2 : ComputeShaderでオブジェクトの座標と進行方向を計算する

ComputeShaderはこちらを使用させていただきました。 github.com 先ほど送ったboidBufferはRWStructuredBufferで宣言されています。 これは読み書き可能なBuffferであることを意味しています。 今回は解説しませんが、Boid.csから送ったboidBufferの情報を使いつつ、計算結果をboidBufferに格納しています。

RWStructuredBuffer<BoidState> boidBuffer;

Step3 : GPUインスタンシングで描画を行う

Boid.cs

using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Rendering;
using Random = Unity.Mathematics.Random;

public class Boids : MonoBehaviour
{
    public struct BoidState
    {
        public Vector3 Position;
        public Vector3 Forward;
    }

    [Serializable]
    public class BoidConfig
    {
        public float moveSpeed = 1f;
        // 分離
        [Range(0f, 1f)] public float separationWeight = .5f;
        // 整列
        [Range(0f, 1f)] public float alignmentWeight = .5f;
        // 結合
        [Range(0f, 1f)] public float targetWeight = .5f;

        public Transform boidTarget;
    }

    public int boidCount = 10000;

    public float3 boidExtent = new(32f, 32f, 32f);

    public ComputeShader BoidComputeShader;

    public BoidConfig boidConfig;

    GraphicsBuffer _boidBuffer;
    GraphicsBuffer _argsBuffer;
    int _kernelIndex;

    [SerializeField]
    Mesh mesh;
    [SerializeField]
    Material drawMaterial;

    void Start()
    {
        InitializeArgsBuffer();
        InitializeBoidsBuffer();
    }

    private void InitializeArgsBuffer()
    {
        var args = new uint[] { 0, 0, 0, 0, 0 };

        args[0] = mesh.GetIndexCount(0);
        args[1] = (uint)boidCount;

e        _argsBuffer.SetData(args);
    }

    private void InitializeBoidsBuffer()
    {
        var random = new Random(256);
        var boidArray = new NativeArray<BoidState>(boidCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
        for (var i = 0; i < boidArray.Length; i++)
        {
            boidArray[i] = new BoidState
            {
                Position = random.NextFloat3(-boidExtent, boidExtent),
                Forward = math.rotate(random.NextQuaternionRotation(), Vector3.forward),
            };
        }
        // GraphicsBuffer生成
        _boidBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, boidArray.Length, Marshal.SizeOf<BoidState>());
        // boidBufferに値を設定
        _boidBuffer.SetData(boidArray);
    }

    void Update()
    {
        UpdateBoids();
        RenderMesh();
    }

    void UpdateBoids()
    {
        var boidTarget = boidConfig.boidTarget != null
            ? boidConfig.boidTarget.position
            : transform.position;
        BoidComputeShader.SetFloat("deltaTime", Time.deltaTime);
        BoidComputeShader.SetFloat("separationWeight", boidConfig.separationWeight);
        BoidComputeShader.SetFloat("alignmentWeight", boidConfig.alignmentWeight);
        BoidComputeShader.SetFloat("targetWeight", boidConfig.targetWeight);
        BoidComputeShader.SetFloat("moveSpeed", boidConfig.moveSpeed);
        BoidComputeShader.SetVector("targetPosition", boidTarget);
        // ComputeShaderに生成するインスタンスの数をセット
        BoidComputeShader.SetInt("numBoids", boidCount);
        _kernelIndex = BoidComputeShader.FindKernel("CSMain");

        BoidComputeShader.GetKernelThreadGroupSizes(_kernelIndex, out var x, out var y, out var z);

        // ComputeShaderにboidBufferをセット
        BoidComputeShader.SetBuffer(_kernelIndex, "boidBuffer", _boidBuffer);
        // ComputeShaderを実行
        BoidComputeShader.Dispatch(_kernelIndex, (int)(boidCount / x), 1, 1);
    }

    void RenderMesh()
    {
        if (!SystemInfo.supportsInstancing)
        {
            return;
        }
        drawMaterial.SetBuffer("_BoidDataBuffer", _boidBuffer);
        BoidState[] data = new BoidState[10];
        _boidBuffer.GetData(data);
        Debug.Log(data);
        Graphics.DrawMeshInstancedIndirect
        (
            mesh,
            0,
            drawMaterial,
            new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)),
            _argsBuffer
        );
    }

    void OnDisable()
    {
        _boidBuffer?.Dispose();
        _argsBuffer?.Dispose();
    }
}

描画するマテリアルにboidBufferを渡しています。 Graphics.DrawMeshInstancedIndirectは指定したマテリアルをGPUインスタンシングで描画することができます。

Graphics.DrawMeshInstancedIndirect(Mesh instanceMesh, int subMeshIndex, Material instanceMaterial, Vector3 bounds, ComputeBuffer argsBuffer);

Mesh instanceMesh : InstansingしたいMesh

int subMeshIndex : 何番目のSubMeshを使用するか

Material instanceMaterial : InstansingするMeshに適応させるMaterial

Vector3 bounds : 描画するインスタンスを囲む境界ボリューム

GraphicsBuffer argsBuffer : InstancingするためのMesh情報

 void RenderMesh()
    {
        if (!SystemInfo.supportsInstancing)
        {
            return;
        }
        drawMaterial.SetBuffer("_BoidDataBuffer", _boidBuffer);
        BoidState[] data = new BoidState[10];
        _boidBuffer.GetData(data);
        Debug.Log(data);
        Graphics.DrawMeshInstancedIndirect
        (
            mesh,
            0,
            drawMaterial,
            new Bounds(Vector3.zero, new Vector3(1000.0f, 1000.0f, 1000.0f)),
            _argsBuffer
        );
    }

argsBufferはuint型の配列を格納するBufferです。 Meshのインデックスと、Meshの数を持たせます。

    private void InitializeArgsBuffer()
    {
        var args = new uint[] { 0, 0, 0, 0, 0 };

        args[0] = mesh.GetIndexCount(0);
        args[1] = (uint)boidCount;

        _argsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, args.Length, sizeof(uint));
        _argsBuffer.SetData(args);
    }

BoidBufferの情報を使って描画する

Graphics.DrawMeshInstancedIndirec

Boids.shader

Shader "Boids"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _ObjectScale ("Scale", Vector) = (1,1,1,1)
    }
    SubShader
    {
        Pass
        {
            Tags { "RenderType"="Opaque" }
            LOD 200
            
            HLSLPROGRAM
            #pragma instancing_options procedural:setup
            #pragma multi_compile_instancing
            #pragma target 4.5

            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            struct Attributes
            {
                float4 positionOS   : POSITION;
                float3 normalOS     : NORMAL;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float2 uv                       : TEXCOORD0;
                float4 positionCS               : SV_POSITION;
                half3 data : TEXCOORD1;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            // Boidの構造体
            struct BoidState
            {
                float3 position; // 位置
                float3 forward; // 速度
            };

           #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
                // Boidデータの構造体バッファ
                StructuredBuffer<BoidState> _BoidDataBuffer;
           #endif

            sampler2D _MainTex; // テクスチャ
            half3 _Color;      // カラー
            float3 _ObjectScale; // Boidオブジェクトのスケール

            // オイラー角(ラジアン)を回転行列に変換
            float4x4 eulerAnglesToRotationMatrix(float3 angles)
            {
                float ch = cos(angles.y); float sh = sin(angles.y); // heading
                float ca = cos(angles.z); float sa = sin(angles.z); // attitude
                float cb = cos(angles.x); float sb = sin(angles.x); // bank

                // Ry-Rx-Rz (Yaw Pitch Roll)
                return float4x4(
                ch * ca + sh * sb * sa, -ch * sa + sh * sb * ca, sh * cb, 0,
                cb * sa, cb * ca, -sb, 0,
                -sh * ca + ch * sb * sa, sh * sa + ch * sb * ca, ch * cb, 0,
                0, 0, 0, 1
                );
            }

            // 頂点シェーダ
            Varyings vert(Attributes input, uint instanceID : SV_InstanceID)
            {

                Varyings output = (Varyings)0;
                #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED

                // インスタンスIDからBoidのデータを取得
                BoidState boidData = _BoidDataBuffer[instanceID]; 

                float3 pos = boidData.position.xyz; // Boidの位置を取得
                float3 scl = _ObjectScale;          // Boidのスケールを取得

                // ワールド行列
                float4x4 object2world = (float4x4)0; 
                // スケール値を代入
                object2world._11_22_33_44 = float4(scl.xyz, 1.0);

                // 速度からY軸についての回転を算出
                float rotY = atan2(boidData.forward.x, boidData.forward.z);
                // 速度からX軸についての回転を算出
                float rotX = -asin(boidData.forward.y / (length(boidData.forward.xyz) + 1e-8));
                // オイラー角(ラジアン)から回転行列を求める
                float4x4 rotMatrix = eulerAnglesToRotationMatrix(float3(rotX, rotY, 0));
                // 行列に回転を適用
                object2world = mul(rotMatrix, object2world);
                // 行列に位置(平行移動)を適用
                object2world._14_24_34 += pos.xyz;

                // 頂点を座標変換
                float4 positionWS = mul(object2world, input.positionOS);
                output.positionCS = TransformWorldToHClip(positionWS);
                #endif
                return output;
            }
            
            void setup()
            {
            }

            half4 frag(Varyings input) : SV_Target
            {
                return half4(_Color,1);
            }
            ENDHLSL
        }
    }
}

instancing_options proceduralはGraphics.DrawMeshInstancedIndirectと一緒に使用されます。 setupの部分は任意の関数名を指定できます。

ここで、指定した関数は頂点シェーダーが実行される前に各インスタンスごとに呼ばれます。

公式リファレンスではBufferの回転値から、変換行列を変更しているようですが、今回はsetup関数に処理を入れず頂点シェーダーで行います。

Unity - Scripting API: Graphics.DrawMeshInstancedIndirect

#pragma instancing_options procedural:setup

UNITY_PROCEDURAL_INSTANCING_ENABLEDはGraphics.DrawMeshInstancedIndirect(Graphics.DrawMeshInstancedも)を使用して描画するときに有効になります。

BoidComputeShader.SetBufferで、送られたデータが、StructuredBufferで宣言した_BoidDataBufferに格納されます。

// Boidの構造体
struct BoidState
{
   float3 position; // 位置
   float3 forward; // 速度
};

#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
     // Boidデータの構造体バッファ
     StructuredBuffer<BoidState> _BoidDataBuffer;
#endif

_BoidDataBuffer[instanceID]で、処理中の頂点のインスタンスからBoidDataを取得して、位置や回転の計算しています。

Varyings vert(Attributes input, uint instanceID : SV_InstanceID)
{
    Varyings output = (Varyings)0;
    #ifdef UNITY_PROCEDURAL_INSTANCING_ENABL
    // インスタンスIDからBoidのデータを取得
    BoidState boidData = _BoidDataBuffer[instanceID]; 
・
・
・

参考

edom18.hateblo.jp

www.youtube.com

【Unity】ディザリングを実装してみた

ディザリングとは

一定間隔で穴を空けることで、不透明だけど半透明に見せることができます。 通常の半透明で発生する不具合や、処理負荷の問題が発生しないのが利点です。 youtu.be

穴を開けるパターン

現在描画するピクセルがを開けるか判定する方法の一つとして、しきい値マップという数値の配列を使用します。 ピクセルの位置に対応する閾値マップの値と設定した閾値を比較して、ピクセルの破棄を行うか判定します。

https://wikimedia.org/api/rest_v1/media/math/render/svg/3c62838dbbd378c058444a60b9c803b9bb4ee09c

配列ディザリング - Wikipedia引用

実装

Shader "Dither"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _DitherLevel("DitherLevel", Range(0, 16)) = 1
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;
                float4 positionSS : TEXCOORD1;
            };
            
            // しきい値マップ
            static const float4x4 pattern =
            {
                0,8,2,10,
                12,4,14,6,
                3,11,1,9,
                15,7,13,565
            };
            static const int PATTERN_ROW_SIZE = 4;

            sampler2D _MainTex;
            sampler2D _DitherTex;
            float _Alpha;
            half _DitherLevel;

            v2f vert (appdata v)
            {
                v2f o;
                o.positionCS = UnityObjectToClipPos(v.positionOS);
                o.positionSS = ComputeScreenPos(o.positionCS);
                o.uv = v.uv;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // ①
                // スクリーン座標
                float2 screenPos = i.positionSS.xy / i.positionSS.w;
                // 画面サイズを乗算して、ピクセル単位に
                float2 screenPosInPixel = screenPos.xy * _ScreenParams.xy;

                // ②
                // ディザリングテクスチャ用のUVを作成
                int ditherUV_x = (int)fmod(screenPosInPixel.x, PATTERN_ROW_SIZE);
                int ditherUV_y = (int)fmod(screenPosInPixel.y, PATTERN_ROW_SIZE);
                float dither = pattern[ditherUV_x, ditherUV_y];
                  
                 // ③
                 // 閾値が0以下なら描画しない
                clip(dither - _DitherLevel);

                //メインテクスチャからサンプリング`
                float4 color = tex2D(_MainTex, i.uv); 
                return color;
            }
            ENDCG
        }
    }
}

ComputeScreenPosで求めた値を正規化すると0〜1のスクリーン座標になります。

そこに_ScreenParams.xy=スクリーンの幅と高さ(ピクセル単位)を乗算することで、0〜スクリーンの幅、高さの値にしています。

詳しくはこちら ny-program.hatenablog.com

しきい値マップは4x4の行列なので①で計算したスクリーン座標を4で除算した余りを求め、しきい値マップの成分を取り出し、それを閾値とします。 これにより、スクリーン上を縦横4x4分割し、分割された単位ごとにしきい値マップの成分が割り当てられることになります。

しきい値マップから取得した閾値よりマテリアルプロパティから設定した閾値が大きければそのピクセルの描画を破棄して穴を開けています。

参考

3dcg-school.pro

【Unity】Compute Shaderの基礎

ComputeShaderとは

GPUで実行するプログラムです 画像処理、ポストエフェクト、物理シミュレーション、アニメーション、水面シミュレーション 、パーティクル演算などCPUよりもGPUの並列計算を使用したい時に使われます。

カーネル

カーネルとは、GPUで実行される関数のことです。

#pragma kernel CSMain

#pragma kernelの後にCSMainという関数名を指定しています。

スレッド

スレッドとは、カーネルを実行する単位で、1スレッドが1カーネルを実行します。コンピュートシェーダーではカーネルを複数のスレッドで並行して同時に実行することができます。

[numthreads(10,8,3)]
void CSMain ()
{
}

スレッド数はカーネルの上に[numthreads(X,Y,Z)]と定義します。 X * Y * Zのスレッドが生成されます。 今回の例では10 * 8 * 3 = 240スレッド生成されます。

スレッドグループ

スレッドグループとは、スレッドを実行する単位で、1スレッドグループで、指定したスレッド数のカーネルを実行します。

int kernelIndex = shader.FindKernel("CSMain");
shader.Dispatch(kernelIndex, 5, 3, 2);

スレッドグループはコンピュートシェーダーを実行する shader.Dispatchの引数で指定します。 第一引数はshader.FindKernelで取得したカーネルのインデックスを指定します。

残りの引数で、スレッドグループ数を指定します。 スレッド数と同じように、5 * 3 * 2 = 30グループとなります。

これは[numthreads(10,8,3)](下の図)で合計240スレッドが指定されたカーネルDispatch(5,3,2)(上の図)で合計30グループのスレッドグループからなるスレッド群を実行している図です。(つまり合計7,200スレッド)

セマンティクス

カーネルの引数にはセマンティクスを指定できます。 上の画像を見ながら、読んでください。

SV_GroupThreadID

実行しているグループ内でのスレッドIDです。

SV_GroupID

実行しているスレッドグループのIDです。

SV_DispatchThreadID

実行しているスレッドが、どのスレッドグループのどのスレッドかを識別できるIDで、以下の式によって求められます。

SV_GroupID * numthreads + SV_GroupThreadID

SV_GroupID * numthreadsで、実行しているスレッドグループまでの合計スレッド数を求め、+ SV_GroupThreadIDで実行しているスレッドIDを加算しています。

SV_GroupIndex

SV_GroupThreadIDを数値を一つにして返します。

Compute Shaderに値を渡す

Compute Shaderで、変数を定義

float hoge;

スクリプトから、値を渡す

shader.SetFloat("hoge", 1.0f);

ComputeShaderにバッファを渡す

Compute Shaderで、バッファを定義

RWがついている型は読み書きが可能な変数です。 ついていない型は読み込みのみです。

struct Data
{
    float3 position;
    floar3 rotation;
};
RWStructuredBuffer<Data> Result;

スクリプトから、バッファを渡す

public struct Data
{
    public Vector3 Position;
    public Vector3 Rotation;
}
Data[] dataList = new Data[100];

// dataListに値を入れる

//GraphicsBuffer作成
GraphicsBuffer buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,dataList.Length, sizeof(Data)));
// GraphicsBufferにデータを設定
buffer.SetData(dataList);
// ComputeShaderにバッファを渡す
shader.SetBuffer(kernelIndex, "dataList", buffer);

ComputeShaderからバッファを受け取る

スクリプトで、GraphicsBufferのGetDataを使えば簡単に取得できます。

buffer.GetData(dataList);

参考

edom18.hateblo.jp

edom18.hateblo.jp

shitakami.hatenablog.com

【Unity】【URP】LUT (ルックアップテーブル)

カラーグレーディング(Color Grading) とは

画面の色合いを調整するポストエフェクトです。

LUTとは

ルックアップテーブル(Look Up Table)の略称で、入力されたRGBを指定した別のRGBに変換するための情報が格納されてたテクスチャです。 カラーグレーディングで使われます。

URPにおけるカラーグレーディング

設定

Grading Mode

LUTテクスチャのグラフィックフォーマットをLDR/HDRのいずれにするかを指定します。

LUT size

LUTテクスチャのサイズを設定します。設定した値Aに対し「高さA、幅A×A」 サイズのテクスチャが生成されます。

LUTの作成

ColorGradingLutPassでLUTを生成するシェーダーを実行します。 以下はExecute関数です。 各カラーグレーディングの設定をシェーダーに渡し、LUTテクスチャをレンダーターゲットとして描画の実行をしています。

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    var cmd = CommandBufferPool.Get();
    using (new ProfilingScope(cmd, ProfilingSampler.Get(URPProfileId.ColorGradingLUT)))
    {
        // 各カラーグレーディングの設定はVolumeで行います
        // 追加したVolumeを取得します
        var stack = VolumeManager.instance.stack;
        var channelMixer = stack.GetComponent<ChannelMixer>();
        var colorAdjustments = stack.GetComponent<ColorAdjustments>();
        var curves = stack.GetComponent<ColorCurves>();
        var liftGammaGain = stack.GetComponent<LiftGammaGain>();
        var shadowsMidtonesHighlights = stack.GetComponent<ShadowsMidtonesHighlights>();
        var splitToning = stack.GetComponent<SplitToning>();
        var tonemapping = stack.GetComponent<Tonemapping>();
        var whiteBalance = stack.GetComponent<WhiteBalance>();
        // UniversalRenderPipelineAssetで設定したGrading Modeを取得します
        // Grading ModeはLUTテクスチャのグラフィックフォーマットをLDRまたはHDRのどちらにするかを設定します
        ref var postProcessingData = ref renderingData.postProcessingData;
        bool hdr = postProcessingData.gradingMode == ColorGradingMode.HighDynamicRange;
        // UniversalRenderPipelineAssetで設定したLUTsizeを取得
        // 高さ = LUTsize
        int lutHeight = postProcessingData.lutSize;
        // 幅 = LUTsize^2
        int lutWidth = lutHeight * lutHeight;
        // LDRかHDRによってテクスチャフォーマットを変更
        // LDR : R8G8B8A8_UNorm
        // HDR : R16G16B16A16_SFloat サポートされていなければ、B10G11R11_UFloatPack32
        var format = hdr ? m_HdrLutFormat : m_LdrLutFormat;
        // LDRかHDRによってマテリアルを変更します
        // LDR : LutBuilderLdr.shaderが設定されているマテリアル
        // HDR : LutBuilderHdr.shaderが設定されているマテリアル
        var material = hdr ? m_LutBuilderHdr : m_LutBuilderLdr;
        // LUTを一時テクスチャをして生成します
        var desc = new RenderTextureDescriptor(lutWidth, lutHeight, format, 0);
        desc.vrUsage = VRTextureUsage.None; // We only need one for both eyes in VR
        cmd.GetTemporaryRT(m_InternalLut.id, desc, FilterMode.Bilinear);
        // 各カラーグレーディングの設定から、シェーダーパラメータを設定します
        var lmsColorBalance = ColorUtils.ColorBalanceToLMSCoeffs(whiteBalance.temperature.value, whiteBalance.tint.value);
        var hueSatCon = new Vector4(colorAdjustments.hueShift.value / 360f, colorAdjustments.saturation.value / 100f + 1f, colorAdjustments.contrast.value / 100f + 1f, 0f);
        var channelMixerR = new Vector4(channelMixer.redOutRedIn.value / 100f, channelMixer.redOutGreenIn.value / 100f, channelMixer.redOutBlueIn.value / 100f, 0f);
        var channelMixerG = new Vector4(channelMixer.greenOutRedIn.value / 100f, channelMixer.greenOutGreenIn.value / 100f, channelMixer.greenOutBlueIn.value / 100f, 0f);
        var channelMixerB = new Vector4(channelMixer.blueOutRedIn.value / 100f, channelMixer.blueOutGreenIn.value / 100f, channelMixer.blueOutBlueIn.value / 100f, 0f);
        var shadowsHighlightsLimits = new Vector4(
            shadowsMidtonesHighlights.shadowsStart.value,
            shadowsMidtonesHighlights.shadowsEnd.value,
            shadowsMidtonesHighlights.highlightsStart.value,
            shadowsMidtonesHighlights.highlightsEnd.value
        );
        var (shadows, midtones, highlights) = ColorUtils.PrepareShadowsMidtonesHighlights(
            shadowsMidtonesHighlights.shadows.value,
            shadowsMidtonesHighlights.midtones.value,
            shadowsMidtonesHighlights.highlights.value
        );
        var (lift, gamma, gain) = ColorUtils.PrepareLiftGammaGain(
            liftGammaGain.lift.value,
            liftGammaGain.gamma.value,
            liftGammaGain.gain.value
        );
        var (splitShadows, splitHighlights) = ColorUtils.PrepareSplitToning(
            splitToning.shadows.value,
            splitToning.highlights.value,
            splitToning.balance.value
        );
        var lutParameters = new Vector4(lutHeight, 0.5f / lutWidth, 0.5f / lutHeight,
            lutHeight / (lutHeight - 1f));
        material.SetVector(ShaderConstants._Lut_Params, lutParameters);
        material.SetVector(ShaderConstants._ColorBalance, lmsColorBalance);
        material.SetVector(ShaderConstants._ColorFilter, colorAdjustments.colorFilter.value.linear);
        material.SetVector(ShaderConstants._ChannelMixerRed, channelMixerR);
        material.SetVector(ShaderConstants._ChannelMixerGreen, channelMixerG);
        material.SetVector(ShaderConstants._ChannelMixerBlue, channelMixerB);
        material.SetVector(ShaderConstants._HueSatCon, hueSatCon);
        material.SetVector(ShaderConstants._Lift, lift);
        material.SetVector(ShaderConstants._Gamma, gamma);
        material.SetVector(ShaderConstants._Gain, gain);
        material.SetVector(ShaderConstants._Shadows, shadows);
        material.SetVector(ShaderConstants._Midtones, midtones);
        material.SetVector(ShaderConstants._Highlights, highlights);
        material.SetVector(ShaderConstants._ShaHiLimits, shadowsHighlightsLimits);
        material.SetVector(ShaderConstants._SplitShadows, splitShadows);
        material.SetVector(ShaderConstants._SplitHighlights, splitHighlights);
        material.SetTexture(ShaderConstants._CurveMaster, curves.master.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveRed, curves.red.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveGreen, curves.green.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveBlue, curves.blue.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveHueVsHue, curves.hueVsHue.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveHueVsSat, curves.hueVsSat.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveLumVsSat, curves.lumVsSat.value.GetTexture());
        material.SetTexture(ShaderConstants._CurveSatVsSat, curves.satVsSat.value.GetTexture());
        // Tonemapping
        if (hdr)
        {
            material.shaderKeywords = null;
            switch (tonemapping.mode.value)
            {
                case TonemappingMode.Neutral: material.EnableKeyword(ShaderKeywordStrings.TonemapNeutral); break;
                case TonemappingMode.ACES: material.EnableKeyword(m_AllowColorGradingACESHDR ? ShaderKeywordStrings.TonemapACES : ShaderKeywordStrings.TonemapNeutral); break;
                default: break; 
            }
        }
        // XR系処理
        renderingData.cameraData.xr.StopSinglePass(cmd);
        // 描画実行
        cmd.Blit(null, m_InternalLut.id, material);
        // XR系処理
        renderingData.cameraData.xr.StartSinglePass(cmd);
    }
    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
}

Grading Modeの設定(HDRLDR)によって、LutBuilderHdr.shaderLutBuilderLdr.shaderのどちらかが実行されます。

今回はLutBuilderLdr.shaderの処理を見ていきます。

SubShader
{
    Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
    LOD 100
    ZTest Always ZWrite Off Cull Off
    Pass
    {
        Name "LutBuilderLdr"
        HLSLPROGRAM
            // Fullscreeen.hlslに定義されている汎用頂点シェーダー
            #pragma vertex FullscreenVert
            #pragma fragment Frag
        ENDHLSL
    }
}

フラグメントシェーダーの定義

half4 Frag(Varyings input) : SV_Target
{
    // ① LUT生成
    float3 colorLinear = GetLutStripValue(input.uv, _Lut_Params);
    float3 colorLMS = LinearToLMS(colorLinear);
    colorLMS *= _ColorBalance.xyz;
    colorLinear = LMSToLinear(colorLMS);
    float3 colorLog = LinearToLogC(colorLinear);
    colorLog = (colorLog - ACEScc_MIDGRAY) * _HueSatCon.z + ACEScc_MIDGRAY;
    colorLinear = LogCToLinear(colorLog);
    // ②Color AdjustmentsのColor filterを適応
    colorLinear *= _ColorFilter.xyz;
    colorLinear = max(0.0, colorLinear);
    // Split toning
    // As counter-intuitive as it is, to make split-toning work the same way it does in Adobe
    // products we have to do all the maths in gamma-space...
    float balance = _SplitShadows.w;
    float3 colorGamma = PositivePow(colorLinear, 1.0 / 2.2);
    float luma = saturate(GetLuminance(saturate(colorGamma)) + balance);
    float3 splitShadows = lerp((0.5).xxx, _SplitShadows.xyz, 1.0 - luma);
    float3 splitHighlights = lerp((0.5).xxx, _SplitHighlights.xyz, luma);
    colorGamma = SoftLight(colorGamma, splitShadows);
    colorGamma = SoftLight(colorGamma, splitHighlights);
    colorLinear = PositivePow(colorGamma, 2.2);
    // Channel mixing (Adobe style)
    colorLinear = float3(
        dot(colorLinear, _ChannelMixerRed.xyz),
        dot(colorLinear, _ChannelMixerGreen.xyz),
        dot(colorLinear, _ChannelMixerBlue.xyz)
    );
    // Shadows, midtones, highlights
    luma = GetLuminance(colorLinear);
    float shadowsFactor = 1.0 - smoothstep(_ShaHiLimits.x, _ShaHiLimits.y, luma);
    float highlightsFactor = smoothstep(_ShaHiLimits.z, _ShaHiLimits.w, luma);
    float midtonesFactor = 1.0 - shadowsFactor - highlightsFactor;
    colorLinear = colorLinear * _Shadows.xyz * shadowsFactor
        + colorLinear * _Midtones.xyz * midtonesFactor
        + colorLinear * _Highlights.xyz * highlightsFactor;
    // Lift, gamma, gain
    colorLinear = colorLinear * _Gain.xyz + _Lift.xyz;
    colorLinear = sign(colorLinear) * pow(abs(colorLinear), _Gamma.xyz);
    // HSV operations
    float satMult;
    float3 hsv = RgbToHsv(colorLinear);
    {
        // Hue Vs Sat
        satMult = EvaluateCurve(_CurveHueVsSat, hsv.x) * 2.0;
        // Sat Vs Sat
        satMult *= EvaluateCurve(_CurveSatVsSat, hsv.y) * 2.0;
        // Lum Vs Sat
        satMult *= EvaluateCurve(_CurveLumVsSat, Luminance(colorLinear)) * 2.0;
        // Hue Shift & Hue Vs Hue
        float hue = hsv.x + _HueSatCon.x;
        float offset = EvaluateCurve(_CurveHueVsHue, hue) - 0.5;
        hue += offset;
        hsv.x = RotateHue(hue, 0.0, 1.0);
    }
    colorLinear = HsvToRgb(hsv);
    // Global saturation
    luma = GetLuminance(colorLinear);
    colorLinear = luma.xxx + (_HueSatCon.yyy * satMult) * (colorLinear - luma.xxx);
    // YRGB curves
    {
        const float kHalfPixel = (1.0 / 128.0) / 2.0;
        float3 c = colorLinear;
        // Y (master)
        c += kHalfPixel.xxx;
        float mr = EvaluateCurve(_CurveMaster, c.r);
        float mg = EvaluateCurve(_CurveMaster, c.g);
        float mb = EvaluateCurve(_CurveMaster, c.b);
        c = float3(mr, mg, mb);
        // RGB
        c += kHalfPixel.xxx;
        float r = EvaluateCurve(_CurveRed, c.r);
        float g = EvaluateCurve(_CurveGreen, c.g);
        float b = EvaluateCurve(_CurveBlue, c.b);
        colorLinear = float3(r, g, b);
    }
    return half4(saturate(colorLinear), 1.0);
}

①GetLutStripValueはcolor.hlslに定義されています。

UVとparams = (lut_height, 0.5 / lut_width, 0.5 / lut_height, lut_height / lut_height - 1)を引数として受け取っています。

real3 GetLutStripValue(float2 uv, float4 params)
{
    uv -= params.yz;
    real3 color;
    color.r = frac(uv.x * params.x);
    color.b = uv.x - color.r / params.x;
    color.g = uv.y;
    return color * params.w;
}

ぱっと見では分かりにくいので、GetLutStripValueの値をそのまま出力してみます。

half4 Frag(Varyings input) : SV_Target
{
    float3 colorLinear = GetLutStripValue(input.uv, _Lut_Params);
    return half4(colorLinear.r,colorLinear.g,colorLinear.b,1);
}

RGB値 2次元の画像が横に並んでいます。

R値 各画像のX値と一致します。

G値 各画像のY値と一致します。

B値 各画像のインデックス値と一致します。

LUTは入力されたRGBを指定した別のRGBに変換するための情報が格納されてたテクスチャと前述しましたが、変換前のカラーRをX、GをY, Bをインデックスと見立てて、LUTのテクスチャをフェッチすることで、色を変換します。 GetLutStripValueの段階では入力カラーは色が変化しません。 その後の処理でLUTを書き換え、後ほど使用します。

②Color AdjustmentsのColor filterで画面を赤くした際はLUTの色が赤くなっています。

LUTを使ってカラーグレーディング

カラーグレーディングはUberPost.shaderで反映されます。 UberPost.shaderUberという名の通り、色んなポストエフェクトがひとつのシェーダーで実行されます。

SubShader
{
    Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
    LOD 100
    ZTest Always ZWrite Off Cull Off
    Pass
    {
        Name "UberPost"
        HLSLPROGRAM
            // Fullscreeen.hlslに定義されている汎用頂点シェーダー
            #pragma vertex FullscreenVert
            #pragma fragment Frag
        ENDHLSL
    }
}

フラグメントシェーダーの定義

half4 Frag(Varyings input) : SV_Target
{
・
・ // 色んなポストエフェクトの処理// カラーグレーディングの適応
   color = ApplyColorGrading(color, PostExposure, TEXTURE2D_ARGS(_InternalLut, sampler_LinearClamp), LutParams, TEXTURE2D_ARGS(_UserLut, sampler_LinearClamp), UserLutParams, UserLutContribution);
・
・ // 色んなポストエフェクトの処理return half4(color, 1.0);
}

ApplyColorGradingの定義

half3 ApplyColorGrading(half3 input, float postExposure, TEXTURE2D_PARAM(lutTex, lutSampler), float3 lutParams, TEXTURE2D_PARAM(userLutTex, userLutSampler), float3 userLutParams, float userLutContrib)
{
    // Color AdjustmentsのPost Exposure 画面の明るさを調整します 2のn乗が入力カラー値の要素に乗算します
    input *= postExposure;

    // Grading ModeでHDRを選択
    #if _HDR_GRADING
    {
        // ①リニア色空間からLogc色空間に変換
        float3 inputLutSpace = saturate(LinearToLogC(input));
        // ②Lutを使ってカラー変換
        input = ApplyLut2D(TEXTURE2D_ARGS(lutTex, lutSampler), inputLutSpace, lutParams);
        
        // ③Color Look Up(ユーザー定義のLUT)の適応率が0以上
        UNITY_BRANCH
        if (userLutContrib > 0.0)
        {
            input = saturate(input);
            input.rgb = GetLinearToSRGB(input.rgb);
            half3 outLut = ApplyLut2D(TEXTURE2D_ARGS(userLutTex, userLutSampler), input, userLutParams);
            input = lerp(input, outLut, userLutContrib);
            input.rgb = GetSRGBToLinear(input.rgb);
        }
    }

    // Grading ModeでLDRを選択
    #else
    {
        // ToneMapの適応
        input = ApplyTonemap(input);

        // ③Color Look Up(ユーザー定義のLUT)の適応率が0以上
        UNITY_BRANCH
        if (userLutContrib > 0.0)
        {
            input.rgb = GetLinearToSRGB(input.rgb);
            half3 outLut = ApplyLut2D(TEXTURE2D_ARGS(userLutTex, userLutSampler), input, userLutParams);
            input = lerp(input, outLut, userLutContrib);
            input.rgb = GetSRGBToLinear(input.rgb);
        }
        //  ②Grading ModeがLDRの場合はLDRでLutに保存されているので、そのままLutを使ってカラー変換
        input = ApplyLut2D(TEXTURE2D_ARGS(lutTex, lutSampler), input, lutParams);
    }
    #endif

    return input;
}

①LUTは基本リニア色空間ですが、Grading ModeでHDRを選択した場合はLUT作成時にLogC色空間(HDR形式のフォーマット)に変換されてから保存されます。 そのため、入力カラーをLogC色空間に変換しています。

②入力カラーからLUTを使って色変換を行います。 ApplyLut2Dは以下のように定義されています。

real3 ApplyLut2D(TEXTURE2D_PARAM(tex, samplerTex), float3 uvw, float3 scaleOffset)
{
    uvw.z *= scaleOffset.z;
    float shift = floor(uvw.z);
    uvw.xy = uvw.xy * scaleOffset.z * scaleOffset.xy + scaleOffset.xy * 0.5;
    uvw.x += shift * scaleOffset.y;
    uvw.xyz = lerp(
        SAMPLE_TEXTURE2D_LOD(tex, samplerTex, uvw.xy, 0.0).rgb,
        SAMPLE_TEXTURE2D_LOD(tex, samplerTex, uvw.xy + float2(scaleOffset.y, 0.0), 0.0).rgb,
        uvw.z - shift
    );
    return uvw;
}

③Color Look UpというLUTをユーザーが用意して使うための機能の処理です。推奨されていないそうです。

【Unity】半透明描画について

半透明とブレンドモード

半透明を描画するときに背景の色とブレンドさせるにはブレンドモードという機能を使用します。
ブレンドモードは以下のように記述します。

Blend SrcFactor DstFactor

SrcFactorとDstFactorはそのまま記述するのではなく、決められたパラメータがあるので、用途に合わせて指定します。

指定したSrcFactorとDstFactorのキーワードを使って、以下のように最終的な色を決定ます。

最終的な色 = SrcFactor * 現在出力した色 + DstFactor * フレームバッファの色

一般的な半透明の描画では以下のように指定します。

Blend SrcAlpha OneMinusSrcAlpha

これで、不透明に近いほど現在出力した色が優先され、透明に近いほど、フレームバッファの色が優先されて合成されます。

最終的な色 = α * 現在出力した色 + (1 - α)  * フレームバッファの色

半透明とデプスバッファ

手前の緑色の球体は半透明ですが赤色の球体の重なっている部分が消えてしまっています。

これはオブジェクトを手前から描画するため、手前のオブジェクトの深度値を後ろの球体の描画時に深度バッファから参照して、消えてしまうからです。

(カメラから離れているオブジェクトから描画すると、手前の方のオブジェクトを描画する時に、既に描画したオブジェクトのピクセルカラーは破棄されます。破棄されるピクセルの描画をするのは無駄になるので、手前から描画されます。)

この問題を解決するには不透明の描画が終わった後に半透明の描画をカメラから離れているオブジェクトから描画していきます。

Unityで描画順を指定するにはQueueというキーワードを使用します。半透明の場合はQueueにTransparentを指定します。

Tags {"Queue"="Transparent"}

RenderQueueの数値が低いシェーダー(マテリアル)から描画されます。 Transparentには列挙型のように3000という数値が入っていおり、 2500までは不透明とみなされ、手前から奥に描画され、2501以上は半透明とみなされ、奥から手前に描画されます。

これにより後ろのオブジェクトの方が先に描画されるようになるため、深度テストによるピクセルの削除が行われず、正常に描画されるようになりました。

半透明を綺麗に描画する

モデルの形状が複雑だと、デプスバッファへの書き込みを行わないと裏面のポリゴンが透けて、汚く見えてしまいます。

【Unityシェーダ入門】綺麗に半透明のモデルが表示できるシェーダを作る - おもちゃラボ引用

これを解決するためには先にデプスバッファにモデルのZ値(デプス値)のみを書き込みます。この時モデルは描画しません。

ただ、半透明なオブジェクト同士が重なると前のオブジェクトしか表示されなくなります。

半透明の負荷は高い

半透明の描画はデプステストによるピクセルの破棄が行われないので、オーバードローが発生するのと、ブレンドが必要なため、負荷が高いです。

参考

www.youtube.com

nn-hokuson.hatenablog.com

【Unity】【URP】接空間まとめ

接空間とは

ポリゴンの表面を基準とした座標空間です。

ローカル空間の法線を全て面に対して垂直にした時、ポリゴンの向きによって法線の値が変わりますが、接空間ではポリゴンの向きに影響されません。

うにばな(ノーマル(法線)基本 と ノーマルマップ合成) : Yaminabe引用

行列変換

NormalMapの法線は接空間に存在するので、ローカルやワールド空間にあるベクトルと計算する際は同じ座標空間に合わせる必要があります。

接空間の座標変換には以下の3つベクトルを使用します。

  • 法線(normal) 正面方向
  • 接線(tangent) 右手方向
  • 従法線(binormal) 頭上方向

うにばな(ノーマル(法線)基本 と ノーマルマップ合成) : Yaminabe引用

接空間からワールド(ローカル)空間への変換行列

【Unity】接空間について - コポうぇぶろぐ引用

ワールド(ローカル)空間から接空間への変換行列(逆行列にしただけ)

【Unity】接空間について - コポうぇぶろぐ引用

NormalMapを取得する流れ

①法線、接線、従法線をワールド空間に変換

VertexNormalInputs normalInput = GetVertexNormalInputs(input.normal, input.tangent);
VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS)
{
    VertexNormalInputs tbn;

    real sign = tangentOS.w * GetOddNegativeScale();
    tbn.normalWS = TransformObjectToWorldNormal(normalOS);
    tbn.tangentWS = TransformObjectToWorldDir(tangentOS.xyz);
    tbn.binormal WS = cross(tbn.normalWS, tbn.tangentWS) * sign;
    return tbn;
}

従法線の変換は、法線と接線の外積で求められますが、反転するケースがあるので、必要に応じて-1を乗算します。

real sign = tangentOS.w * GetOddNegativeScale();
tangentOS.w

外積の性質上、左手系のプラットフォームだと頭上方向とは反対を向いてしまいます。

tangentOS.wはプラットフォームの右手系/左手系を示す1or-1が入っているので、これを乗算します。

marupeke296.com

GetOddNegativeScale

TransformのScaleによって反転するケースがあります。 unity_WorldTransformParams.wにはスケールのXYZのいずれかが反転していた場合は -1 が入ります。

https://cdn-ak.f.st-hatena.com/images/fotolife/c/coposuke/20201221/20201221055351.gif

【Unity】接空間について - コポうぇぶろぐ引用

real GetOddNegativeScale()
{
    return unity_WorldTransformParams.w >= 0.0 ? 1.0 : -1.0;
}

②ノーマルマップ取得

half3 normal = UnpackNormal(tex2D(_NormalMap, i.uv));

テクスチャに0〜1で書き込まれている法線情報を -1〜1 の値に変換します。

inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
    return packednormal.xyz * 2 - 1;
#else
    return UnpackNormalDXT5nm(packednormal);
#endif
}

③ノーマルマップの法線をワールド空間に変換

前述した接線、従法線、法線で作った変換行列を使います。

normalWS = TransformTangentToWorld(normal, half3x3(tangentWS, binormalWS, normalWS));
real3 TransformTangentToWorld(real3 dirTS, real3x3 tangentToWorld)
{
    return mul(dirTS, tangentToWorld);
}

参考

coposuke.hateblo.jp

タイルベースレンダリングとは

タイルベースレンダリングとは?

スマートフォンタブレットで採用されているレンダリング手法で、GPU画面を複数のタイルに分割して、描画します。メモリの容量を節約し、描画速度を上げてくれます。

PowerVR - embedded graphics processors powering iconic products より引用

タイルベースレンダリング仕組み

タイルベースレンダリングでは、まずシーン内の全てのジオメトリの計算が終了したとき、各タイルごとに描画するポリゴンの情報(ポリゴンリスト)を作成します。


その後、ラスタライズで、各タイルごとにポリゴンリストから描画するポリゴンをレンダリングします。

https://hardzone.es/app/uploads-hardzone.es/2020/11/tile-rendering.jpg タイルレンダリング、非常に低消費のGPUのアーキテクチャ| ITIGIC引用

タイルベースレンダリングの利点

GPU専用では大きなメモリを積むことができません。その為、CPUとメモリを共有していますが、このメモリとGPUをつなぐバスが多くの場合、非常に細いという特徴があります。


※バスとはデータをやり取りするための信号線の束です。バス幅が細いということは1クロックでやりとりできるデータ量が少ないということです。


モバイルGPUは、細いバスで繋がったRAMから、頻繁にデータを読み書きしないで済むように、太いバスで繋がったSRAMを内蔵しています。ただし、SRAMの容量はとても小さいです。


一般的なレンダリング手法で描画で使用する画面サイズ分のカラーバッファ、深度バッファ、ステンシルバッファなどを必要するため、SRAMは使用できません。細いバスで繋がったRAMに置かれてしまいます。

https://img.logmi.jp/article_images/KpuZdQdTujdnZGjuc72JzY.png レンダリング結果を画面に表示してみよう Vulkanで画面に3Dグラフィックスを描く流れ - ログミーTech引用

タイルベースレンダリングでは、1つのタイルをレンダリングするのに必要なデータだけをSRAMに持っていき、計算を行います。これにより計算の途中で細いバスにデータを流さずにレンダリングすることができるので、高速ですし、メモリも節約することができます。

https://img.logmi.jp/article_images/5hFXSxiAnfK7vhfzc9Txt1.png レンダリング結果を画面に表示してみよう Vulkanで画面に3Dグラフィックスを描く流れ - ログミーTech引用

タイルベースはPowerVR、Adreno、Maliなどで採用されているそうです。

参考

www.4gamer.net

itigic.com

logmi.jp

monobook.org