リニアワークフロー 概念の説明とUnityにおける実装と設定
視覚のガンマ
下の画像を見てください。
左から右に行くにつれて、白から黒へとグラデーションしており、人間の目で見た輝度はリニア(線形)になっています。 つまり黒の明るさを0、白の明るさを1とすると、中間の明るさは0.5に見えます。
しかし、実際の輝度は中間の明るさは約0.22です。 見た目と実際の輝度には差があり、人間の目に届くまで約0.45乗されています。
見た目の輝度と実際の輝度のグラフです。 リニア(直線)ではなく、ガンマカーブと呼ばれる曲線が描かれます。
ディスプレイガンマ
ディスプレイには受け取った輝度を2.2乗してしまう性質があります。 これをガンマ補正と言います。
また、ガンマ補正する数値(2.2)のことをガンマ値と言います。
このままだと暗くなってしまうので、予めガンマ値の逆数を累乗(1/2.2=約0.45乗)しておくことで、ガンマ補正を打ち消します。 これをデガンマと言います。
リニア色空間
リニア色空間とは中間の明るさが0.5の色空間です。 数値的にリニアですが、物理的には間違えている色空間です。
sRGB色空間
sRGB色空間とは中間の明るさが約0.22の色空間です。 輝度が1/2.2=約0.45乗されているので、数値的にノンリニアですが、物理的に正しい色空間です。
デザイナーが見ているモニターはsRGB色空間なので、デザイナーが手書きした画像もsRGB色空間です。
sRGB色空間の画像は輝度が1/2.2乗されているので(デガンマされている)、ディスプレイガンマで2.2乗されることで結果的にリニア色空間になります。
物理ベースレンダリング -リニアワークフロー編 (2)- | Cygames Engineers' Blog引用 しかし、人間の目に届く時には視覚のガンマで約0.45乗されます。
エンジニアが意識すること
デザイナーが作成した画像などのsRGB色空間はディスプレイガンマと相殺させることができるという説明をしました。 逆にリニア色空間のライトはディスプレイガンマが掛かって色が暗くなってしまうので、対策をしなければなりません。
また、sRGB色空間とリニア色空間は色空間が違うため、[sRGB色空間の画像カラー * リニア色空間のライトカラー]のように一緒に計算できません。 sRGB色空間をリニア色空間に変換してから、計算する必要があります。 どちらの色空間に合わせても良いのですが、計算しやすいようにリニア色空間にします。 その上で、計算結果をディスプレイガンマと相殺させるために、sRGB色空間に戻す必要があります。 これがリニアワークフローです。
物理ベースレンダリングでどれだけ物理的にライティング計算を行っても、結果を正しく出力できなければ意味がありません。
実装と解説
Unityでリニアワークフローを実装しました。 球体にテクスチャを貼り付け、簡単なライティングを行なっています。 左がガンマワークフロー(従来の何もしない手法)で右がリニアワークフローです。 ガンマワークフローではディスプレイガンマがかかっていて、暗くなっていますが、リニアワークフローではデガンマしているので、暗くなっていません。
以下、シェーダーコードです。
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色空間 → リニア色空間
紫の枠の部分です。
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で、ガンマ(デフォルト)にすると有効になり、リニアを選択すると無効になります。
コードを載せていますが、実はUnityの設定だけでリニアワークフローは実現できます。
ColorSpaceをリニアにしておき、テクスチャ設定でsRGBにしておけば、UNITY_COLORSPACE_GAMMA内の処理と同じようなことをやってくれます。
今回はリニアワークフローを理解するために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
リニア色空間でライティング計算
half lambert = max(0,dot(i.normal,WorldSpaceLightDir(i.vertex))) * _Intensity;
fixed4 retColor = albedo * lambert;
リニア色空間でライティングの計算を行います。 ライトはリニア色空間なのでそのまま計算に使います。
リニア色空間→sRGB色空間
#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乗しています。
ディスプレイガンマ
分かる!リニアワークフローのコンポジット - コンポジゴク 輝度を2.2乗して出力します。
sRGB色空間で出力したので、リニア色空間になります。
視覚のガンマ
最初に紹介し、置き去りにされた視覚のガンマがかかります。
輝度が1/2.2乗されて目に届きます。
普段私たちは視覚のガンマがかかった状態なので、視覚のガンマに対して、デガンマは行いません。
参考
compojigoku.blog.fc2.com tech.cygames.co.jp tech.cygames.co.jp docs.unity3d.com r-ngtm.hatenablog.com
【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]; ・ ・ ・
参考
【Unity】ディザリングを実装してみた
ディザリングとは
一定間隔で穴を空けることで、不透明だけど半透明に見せることができます。 通常の半透明で発生する不具合や、処理負荷の問題が発生しないのが利点です。 youtu.be
穴を開けるパターン
現在描画するピクセルがを開けるか判定する方法の一つとして、しきい値マップという数値の配列を使用します。 ピクセルの位置に対応する閾値マップの値と設定した閾値を比較して、ピクセルの破棄を行うか判定します。
実装
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分割し、分割された単位ごとにしきい値マップの成分が割り当てられることになります。
③
しきい値マップから取得した閾値よりマテリアルプロパティから設定した閾値が大きければそのピクセルの描画を破棄して穴を開けています。
参考
【Unity】Compute Shaderの基礎
ComputeShaderとは
GPUで実行するプログラムです 画像処理、ポストエフェクト、物理シミュレーション、アニメーション、水面シミュレーション 、パーティクル演算などCPUよりも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);
参考
【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の設定(HDRかLDR)によって、LutBuilderHdr.shader
かLutBuilderLdr.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.shader
はUberという名の通り、色んなポストエフェクトがひとつのシェーダーで実行されます。
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以上は半透明とみなされ、奥から手前に描画されます。
これにより後ろのオブジェクトの方が先に描画されるようになるため、深度テストによるピクセルの削除が行われず、正常に描画されるようになりました。
半透明を綺麗に描画する
モデルの形状が複雑だと、デプスバッファへの書き込みを行わないと裏面のポリゴンが透けて、汚く見えてしまいます。
これを解決するためには先にデプスバッファにモデルのZ値(デプス値)のみを書き込みます。この時モデルは描画しません。
ただ、半透明なオブジェクト同士が重なると前のオブジェクトしか表示されなくなります。
半透明の負荷は高い
半透明の描画はデプステストによるピクセルの破棄が行われないので、オーバードローが発生するのと、ブレンドが必要なため、負荷が高いです。
参考
【Unity】【URP】接空間まとめ
接空間とは
ポリゴンの表面を基準とした座標空間です。
ローカル空間の法線を全て面に対して垂直にした時、ポリゴンの向きによって法線の値が変わりますが、接空間ではポリゴンの向きに影響されません。
行列変換
NormalMapの法線は接空間に存在するので、ローカルやワールド空間にあるベクトルと計算する際は同じ座標空間に合わせる必要があります。
接空間の座標変換には以下の3つベクトルを使用します。
- 法線(normal) 正面方向
- 接線(tangent) 右手方向
- 従法線(binormal) 頭上方向
接空間からワールド(ローカル)空間への変換行列
ワールド(ローカル)空間から接空間への変換行列(逆行列にしただけ)
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が入っているので、これを乗算します。
GetOddNegativeScale
TransformのScaleによって反転するケースがあります。 unity_WorldTransformParams.wにはスケールのXYZのいずれかが反転していた場合は -1 が入ります。
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);
}
参考
タイルベースレンダリングとは
タイルベースレンダリングとは?
スマートフォンやタブレットで採用されているレンダリング手法で、GPU画面を複数のタイルに分割して、描画します。メモリの容量を節約し、描画速度を上げてくれます。
PowerVR - embedded graphics processors powering iconic products より引用
タイルベースレンダリング仕組み
タイルベースレンダリングでは、まずシーン内の全てのジオメトリの計算が終了したとき、各タイルごとに描画するポリゴンの情報(ポリゴンリスト)を作成します。
その後、ラスタライズで、各タイルごとにポリゴンリストから描画するポリゴンをレンダリングします。
タイルベースレンダリングの利点
GPU専用では大きなメモリを積むことができません。その為、CPUとメモリを共有していますが、このメモリとGPUをつなぐバスが多くの場合、非常に細いという特徴があります。
※バスとはデータをやり取りするための信号線の束です。バス幅が細いということは1クロックでやりとりできるデータ量が少ないということです。
モバイルGPUは、細いバスで繋がったRAMから、頻繁にデータを読み書きしないで済むように、太いバスで繋がったSRAMを内蔵しています。ただし、SRAMの容量はとても小さいです。
一般的なレンダリング手法で描画で使用する画面サイズ分のカラーバッファ、深度バッファ、ステンシルバッファなどを必要するため、SRAMは使用できません。細いバスで繋がったRAMに置かれてしまいます。
タイルベースレンダリングでは、1つのタイルをレンダリングするのに必要なデータだけをSRAMに持っていき、計算を行います。これにより計算の途中で細いバスにデータを流さずにレンダリングすることができるので、高速ですし、メモリも節約することができます。
タイルベースはPowerVR、Adreno、Maliなどで採用されているそうです。