【Unity】【URP】【物理カメラ】カメラについて解説
今回はURPのカメラについて解説していきます。
UniversalAdditionalCameraDataコンポーネント
URPにはCameraコンポーネントをつけると自動でUniversal Additional CameraDataというコンポーネントが追加されます。 このコンポーネントはURPで拡張された機能に関するデータを管理します。 それらのパラメータはCameraコンポーネントのインスペクタ に表示されており、UniversalAdditionalCameraDataコンポーネントのインスペクタには何も表示されません。
このコンポーネントはカメラから以下のインスタンスで取得できます。
var cameraData = camera.GetUniversalAdditionalCameraData();
詳しくはリファレンスでご確認ください。 docs.unity3d.com
Camera コンポーネント
RenderType
BaseカメラかOverlayカメラかを選択します。 これはカメラスタックという複数のカメラをレンダリングする機能の設定です。 Baseカメラの描画が初めに行われ、その後にOverlayカメラの描画が行われます。 詳しい解説は後ほどカメラスタックの設定があるのでそこで説明します。
Projection
Projection
投影手法の設定を行います。 透視投影(Perspective)か平行投影(Orthographic)を選択します。
FieldofViewAxis
次に説明するFieldofViewパラメータに設定される値を、スクリーンの垂直方向(Vertical)か水平方向(Horizontal)で設定します。
FieldofView
視野角を設定します。
カメラはレンズが受け取った光をカメラセンサーに当たることで、写真を作ります。また、レンズからセンサーまでの距離を焦点距離と言います。焦点距離を変更すると、カメラセンサーに取り込む外光の範囲を変更できます。この範囲のことを視野角と言います。
以下の二枚の画像は同じシーンに同じカメラを置き、視野角のみを変えています。 二枚目のように視野角を狭くすると映る範囲が狭くなり、画面に表示されているCubeが占める割合が大きくなっています。
fovに60(初期値)を設定
fovに30に設定
Clipping Planes
カメラのNearとFarを設定します。
Physical Camera
チェックを入れると物理カメラが有効になり、物理カメラの設定が表示されるようになります。
CameraBody
SensorType
センサータイプには実際に使用されている代表的なフィルムサイズがあり、設定したセンサータイプからSensor Sizeを自動で設定します。
SensorSize
カメラセンサーのサイズを設定します。
GateFit
センサーサイズで設定されたアスペクト比をフィルムゲートと言います。
現在のスクリーンのアスペクト比を解像度ゲートと言います。
GateFitではフィルムゲートと解像度ゲートが異なる際に整合させる基準を設定します。
Vertical
フィルムゲートを解像度ゲートの高さ(Y軸)に合わせます。 センサーサイズの幅を変更しても、影響はありません。 フィルムゲートが解像度ゲートより大きい場合は、レンダリングされた画像の側面をトリミングします。
フィルムゲートが解像度ゲートよりも小さい場合は、レンダリングされた画像の両側にオーバースキャンします。
オーバースキャンは、テレビ画面に画像が収まらず、端が切れてしまう現象です。
Mac、Apple TV、またはその他のディスプレイでのオーバースキャンとアンダースキャンについて - Apple サポート (日本)引用
Horizontal
フィルムゲートを解像度ゲートの幅 (X軸)に合わせます。 センサーサイズの高さを変更しても、影響はありません。 フィルムゲートが解像度ゲートより大きい場合は、レンダリングされた画像の上下にオーバースキャンします。
フィルムゲートが解像度ゲートよりも小さい場合は、レンダリングされた画像の上下をトリミングします。
Fill
フィルムゲートを解像度ゲートの短い方の軸に合わせます。
Overscan
フィルムゲートを解像度ゲートの長い方の軸に合わせます。
None
解像度ゲートをフィルムゲートの幅と高さ (X 軸と Y 軸) に合わせます。レンダリングされた画像をゲームビューのアスペクト比に合わせて伸縮させます。
Lens
Focal Legth
焦点距離を設定します。
Shift
シフトレンズの設定を行います。
シフトレンズは、カメラのレンズをセンサーから水平方向と垂直方向にオフセットします。
例えば巨大なビルを地上から撮影する際にビル全体をカメラに収めようとするとカメラを上方向に向けます。 建物の側面に引いた2本の赤い線は本来、平行になるはずですが、カメラを傾けたため、上の方に行くほど収束するようになっています。
この歪みを解消するために、シフトレンズを使用します。 カメラの向きを変える代わりにレンズを上に使用することで2本の直線を平行に保ったまま、撮影することができます。
Rendering
Renderer
UniversalRenderPipelineアセットのRendererListに登録したScriptableRendererを継承したクラスを選択します。
PostProcessing
ポストエフェクトを有効にするか設定します。 デフォルトがオフなので、気をつけましょう。
Anti-aliasing
使用するアンチエイリアスを選択します。
アンチエイリアスに関してはこちらで解説しているのでよろしければどうぞ。
Stop Nans
レンダーターゲットにNaNが入っている状態を回避するためにStopNaN.shaderというシェーダーを実行し、NaNのピクセルを黒(0,0,0)に置き換えます。
Dithering(ディザリング)
ディザリングを有効にするか設定します。
色はレンダーターゲットに書き込まれる際に浮動小数点数から0 ~ 255 の整数に変換されます。このとき、誤差が切り捨てられるので、色がグラデーションしている部分が縞模様になってしまいます。 この現象をカラーバンディングと言います。
以下の画像の真ん中の大きなCubeをよく見ると縞模様になっています。
これを解消するためにランダムで色を増減させます。 以下の画像はディザリングを行なった状態ですが、先程の縞模様が解消されているのが分かります。
Render Shadows
シャドウをレンダリングするか設定します。
Priority
シーン内に配置されたBaseカメラが複数ある場合、描画順の優先度を設定します。 値が小さい方から順に描画されます。
OpaqueTexture
不透明オブジェクト群とスカイボックスが描画された時点でのテンポラリーなテクスチャを作成します。 このテクスチャは_CameraOpaqueTextureという名前で、グローバルテクスチャ変数として定義されます。
UsePipleneSettingsに設定した場合はUniversalRenderPipelineAssetの設定値を使用します。
DepthTexture
OpaqueTextureのデプス版です。 _CameraDepthTextureというグローバルテクスチャ変数に深度バッファが書き込まれます。
CullingMask
このカメラが描画するオブジェクトをレイヤーで指定します。
OcclusionCulling
オクルージョンカリングを有効にするか設定します。
Stack
シーン内に配置されたOverlayカメラをカメラスタックに追加できます。
RenderTypeで前述した通り、カメラはBaseカメラとOverlayカメラの2種類あり、これは複数のカメラを描画するための設定します。 カメラスタックは複数のカメラをグループ化する機能で、1個のBaseカメラと0個以上のOverlayカメラで構成されます。
レンダリング実行時には、Baseカメラを描画後、Stackに追加された順にOverlayカメラを描画します。
Enviroment
BackgroundType
カメラスタックの描画終了時にレンダリングされていなかったピクセルの扱いを設定します。
Volumes
ここからはVolume Frameworkに関する設定です。 Volume Frameworkはポストエフェクトで使用するパラメーターを管理するためのフレームワークです。 Volumeというポストエフェクトとコリジョンなどの情報を持つコンポーネントがあり、GlobalVolumeとLocalVolumeの2つあります。 LocalVolumeは設定したコリジョンにカメラがヒットした際にポストエフェクトが反映されるのに対し、GlobalVolumeは必ず反映されます。
UpdateMode
VolumeStack クラスインスタンス の更新を毎フレーム要求するかどうかを設定します。
VolumeMask
このカメラに適用するVolumeコンポーネントが所属するレイヤーを管理します。
VolumeTrigger
LocalVolumeのコライダーとヒットしているか判定するためのTransformを設定します。未設定時の場合カメラコンポーネントがついているオブジェクトのTransformを使用します。
Output
OutputTexture
描画先のレンダーテクスチャを設定します。 設定した場合、カメラターゲットには描画されません。
TargetDisplay
マルチディスプレイで描画する際にこのカメラが描画するディスプレイを設定します。
マルチディスプレイについてはリファレンスをご確認ください。 docs.unity3d.com
Target Eye
VRの時、このカメラで右目か左目どちらにレンダリングを行うか設定します。
ViewportRect
カメラがレンダリングするスクリーンの範囲を設定します。 スクリーンの範囲は左下が [0,0]、右上が[1,1]です。
HDR
カメラスタックごとのHDRの有効無効を設定します。 Offの時は常にLDRで処理されます。 Use Pipeline Settingsの時はUniversalRenderPipelineAssetアセットのHDRパラメータを参照します。
HDRはこちらの記事で解説しています。 ny-program.hatenablog.com
MSAA
カメラスタックごとのMSAAの有効無効を設定します。 Offの時はMSAAを適用しません。 Use Pipeline Settingsの時はUniversalRenderPipelineAssetアセットのAnti-Ailiathing ( MSAA ) パラメータ( 下 図 )
AllowDynamicResolution
動的解像度と言って、個々のレンダーターゲットを動的にスケーリングし、GPU の負荷を軽減できるカメラ設定です。
動的解像度についてはリファレンスをご確認ください。 docs.unity3d.com
参考
docs.unity3d.com note.com note.com docs.unity3d.com developers.wonderpla.net www.youtube.com
【Unity】【URP】 Chromatic Aberration (色収差)
Chromatic Aberration (色収差) エフェクト
光の屈折率は色によって異なるため、レンズを通過した際に焦点距離に差が発生し、ずれて見えます。
実装
実装方法としてはRチャンネルとBチャンネルを_Intensityに応じて拡大しています。
Shader "Hidden/ChromaticAberration" { Properties { _MainTex ("Texture", 2D) = "white" {} _Intensity ("Intensity", Range(0.0, 1.0)) = 0.1 } SubShader { // No culling or depth Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } sampler2D _MainTex; half _Intensity; fixed4 frag (v2f i) : SV_Target { half4 col = tex2D(_MainTex, i.uv); // uvを-0.5〜0.5にする half2 uvBase = i.uv - 0.5h; // R値を拡大する half2 uvR = uvBase * (1.0h - _Intensity * 2.0h) + 0.5h; col.r = tex2D(_MainTex, uvR).r; // G値を拡大する half2 uvG = uvBase * (1.0h - _Intensity) + 0.5h; col.g = tex2D(_MainTex, uvG).g; return col; } ENDCG } } }
色収差なし
色収差あり
SwapBuffer
今回URPのプロジェクトでポストエフェクトを実装するにあたってURP12の新機能であるSwapBufferを使って実装したので、解説します。
従来のやり方だとカメラのカラーバッファをテンポラリーなRenderTextureに描画しておき、ポストエフェクトをかけながらRenderTextureをカメラのカラーバファーに適応させます。
SwapBufferではURPが裏でテンポラリーなRenderTextureを用意してくれて、ユーザーがマテリアルを渡すだけでBlit処理を行ってくれます。
using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public sealed class ChromaticAberrationRenderPass : ScriptableRenderPass { private const string RenderPassName = nameof(ChromaticAberrationRenderPass); private readonly Material _material; public ChromaticAberrationRenderPass(Shader shader) { if (shader == null) return; _material = new Material(shader); // このレンダーパスをポストプロセスのタイミングで実行 renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; } public override void Execute(ScriptableRenderContext context, ref RenderingData data) { if (_material == null) return; var cmd = CommandBufferPool.Get(RenderPassName); // 一回Blitするだけで良くなりました。 Blit(cmd, ref data, _material); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }
using System; using UnityEngine; using UnityEngine.Rendering.Universal; [Serializable] public sealed class ChromaticAberrationRendererFeature : ScriptableRendererFeature { [SerializeField] private Shader _shader; private ChromaticAberrationRenderPass _postProcessPass; public override void Create() { _postProcessPass = new ChromaticAberrationRenderPass(_shader); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(_postProcessPass); } }
参考
【Unity】GPU インスタンシング、Static Batching、Dynamic Batching、SRP Batcherを解説
BatchとDraw CallとSetPass Call
Batchは同一マテリアルである等のバッチング条件を満たした結合メッシュ単位の描画処理を指します。
SetPass Callはマテリアルの設定値をCPUがGPUに送る処理です。
Draw CallはCPUがGPUに、描画を命令することです。
CPUからGPUへの命令は負荷が高いため、SetPass CallとDraw Callはなるべく減らしたいところです。
GPU インスタンシング
GPU インスタンシングを使用すると複数の同一メッシュ・同一マテリアルを1回のDrawCallで描画することができます。
Static Batching
シーン内の静的オブジェクトをゲーム実行時にコンバイン(複数のオブジェクトを一つのオブジェクトとして合体)して、ドローコールを削減します。 ゲーム実行時にコンバインを行うので、処理負荷が軽減できます。
Dynamic Batching
Dynamic Batchingは同じマテリアルを適用のオブジェクトをリアルタイムでコンバインします。 これによりドローコールとセットパスコールを1回で行うので、処理負荷が軽減できます。
しかし、コンバイン自体に負荷がかかるため、CPU負荷がかかるデメリットがあります。 また、頂点属性900以下かつ頂点数300以下のメッシュにしか使用できないなど他にも細かい条件があるため、デフォルトでオフになっています。
SRP Batcher
SRP BatcherはURPで使用できるバッチング機能です。 Dynamic Bachingとは異なりドローコールは描画する数だけ発行されますが、セットパスコールは一回のみです。 SRP Batcherの素晴らしい点はSRP Batcherを適用できる条件の緩さです。 以下が条件です。
・シェーダーが同一でシェーダキーワードが完全一致している場合、メッシュやマテリアルが異なっていても動作する
・オブジェクトはメッシュ内に存在しなければならない。パーティクルやスキンメッシュでは要件を満たせません。
・シェーダが対応している必要がある
・ほとんどすべてのプラットフォームで動作する OpenGL ESは3.1以上
設定方法
Scriptable Render Pipeline AssetのSRP BatcherをONにします。 (デフォルトでONになっています)
シェーダーのSRP Batcher対応
プロパティをCBUFFER_START(UnityPerMaterial)〜CBUFFER_ENDで囲います。
CBUFFER_START(UnityPerMaterial) float4 _MainTex; half4 _Color; half _RimPower; CBUFFER_END
必要なプロパティ全てを必ず含めるようにしてください。 余計なプロパティが混ざっていたとしても、そのPass内で参照されていないならコンパイラが自動で削除してくれるので気にする必要はありません。
参考
【Unity】【URP】Litシェーダーのライティング処理を見る
UPRのLitシェーダーのライティング処理を一通り解説します。
ライティングの流れ
ライティングの流れは大まかに以下のようになっています。
最終ライトカラー値 = ①メインライト
+ ②追加ライト(ピクセル単位)
+ ③追加ライト(頂点単位)
+ ④大域照明ライト
+ ⑤エミッションライト
メインライト
常にintensityが一番高いディレクショナルライトが割り当てられ、ピクセル単位で処理されます。 また、メインライトのみシャドウマップ使用時にカスケードシャドウを使用できます。
追加ライト
メインライトを除く、ディレクショナルライト、ポイントライト、スポットライトが設定したライトの最大数(上限8)まで割り当てられます。 設定した最大数を超えた分のライトは計算されません。
下の画像はライトの最大数を4個に設定して、ディレクショナルライト1個(メインライト)とポイントライトを5個配置しました。 上限が4個なので青色のポイントライトが無視されています。 どのライトが反映されるかはそのオブジェクトへの影響度で決まります。
今度は水色のポイントライトを床に当たらないように調整しました。 今度は青いポイントライトが反映されていますが、球体には水色のポイントライトが反映されており、合計5個のライトが反映されています。 これは1つのオブジェクトに反映される上限が4個だからです。
また、追加ライトは設定により頂点シェーダーかピクセルシェーダーのどちらでライティング処理を行うか選択ができます。
URPのライティングはURPアセットで設定できます。 ますは各設定項目について説明します。
ライティング処理を頂点シェーダーかフラグメントシェーダーのどちらで行うかURPアセットのAdditional Lightで設定できます。
Lit シェーダーのコードを見る
ここからは実際のシェーダーの中を追って、ライティング処理がどのように行われているか見ていきます。
頂点シェーダー
Varyings LitPassVertex(Attributes input) { // 出力用構造体の初期化 Varyings output = (Varyings)0; // インスンスIDの初期化 UNITY_SETUP_INSTANCE_ID(input); // インスタンス ID を頂点シェーダの入力構造体から出力構造体にコピー UNITY_TRANSFER_INSTANCE_ID(input, output); // 立体視初期化 UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); // 頂点座標変換 VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); // 法線ベクトル座標変換 VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS); // 視線ベクトル座標変換 half3 viewDirWS = GetWorldSpa // 頂点シェーダーの追加ライトの計算 half3 vertexLight = VertexLighting(vertexInput.positionWS, normalInput.normalWS); // フォグの強度の計算 half fogFactor = ComputeFogFactor(vertexInput.positionCS.z); // テクスチャのスケールオフセットを対応させたUV取得 output.uv = TRANSFORM_TEX(input.texcoord, _BaseMap); output.normalWS = normalInput.normalWS; output.viewDirWS = viewDirWS; #if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) || defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR) real sign = input.tangentOS.w * GetOddNegativeScale(); half4 tangentWS = half4(normalInput.tangentWS.xyz, sign); #endif #if defined(REQUIRES_WORLD_SPACE_TANGENT_INTERPOLATOR) output.tangentWS = tangentWS; #endif #if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR) half3 viewDirTS = GetViewDirectionTangentSpace(tangentWS, output.normalWS, viewDirWS); output.viewDirTS = viewDirTS; #endif // ライトマップのUV OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV); // SHライトカラー OUTPUT_SH(output.normalWS.xyz, output.vertexSH); output.fogFactorAndVertexLight = half4(fogFactor, vertexLight); #if defined(REQUIRES_WORLD_SPACE_POS_INTERPOLATOR) output.positionWS = vertexInput.positionWS; #endif #if defined(REQUIRES_VERTEX_SHADOW_COORD_INTERPOLATOR) output.shadowCoord = GetShadowCoord(vertexInput); #endif output.positionCS = vertexInput.positionCS; return output; }
頂点シェーダーの追加ライトの計算
頂点シェーダーで、追加ライトの計算を行なっている関数を見ていきます。
half3 VertexLighting(float3 positionWS, half3 normalWS) { half3 vertexLightColor = half3(0.0, 0.0, 0.0); // 追加ライトの計算を頂点シェーダーで行うとき有効 #ifdef _ADDITIONAL_LIGHTS_VERTEX // 計算対象とする追加ライトの数 uint lightsCount = GetAdditionalLightsCount(); // 計算対象の追加ライトをforで回しながら合算カラーを求める for (uint lightIndex = 0u; lightIndex < lightsCount; ++lightIndex) { // ライトのIndex(lightIndex)から対応するライト情報をLight構造体に格納 Light light = GetAdditionalLight(lightIndex, positionWS); // 光の減衰 half3 lightColor = light.color * light.distanceAttenuation; vertexLightColor += LightingLambert(lightColor, light.direction, normalWS); } #endif return vertexLightColor; }
_ADDITIONAL_LIGHTS_VERTEXはURPアセットのAdditional LightをPer Vertexにすると有効になります。 その下のfor文ではGetAdditionalLightsCountの数だけ、ライティング処理を行なっています。
int GetAdditionalLightsCount() { return min(_AdditionalLightsCount.x, unity_LightData.y); }
GetAdditionalLightsCountは計算対象とするライトの数を返しています。 これを以下の2つのパラメータの最小値をとることで求めています。
_AdditionalLightsCount.x・・・URPアセットのPer Object Limitの値が入ります。ただし、上限は8です。(グラフィックス APIがOpenGL ES 2.0の場合は4)
unity_LightData.y・・・プリミティブに光が届いているライトの個数
Light GetAdditionalLight(uint i, float3 positionWS) { int perObjectLightIndex = GetPerObjectLightIndex(i); return GetAdditionalPerObjectLight(perObjectLightIndex, positionWS); }
GetAdditionalLightでは計算するライトのIndex(lightIndex)から対応するライト情報をLight構造体に格納します。
まずはforのIndexから対応する追加ライトのIndexを返します。 (影響度が高い順のライトのIndexから、純粋なライトのIndexを返す)
int GetPerObjectLightIndex(uint index) { //一部のプラットフォームではStructuredBuffersに問題があるため、このバリアントは無効になります。 // RenderingUtils.useStructuredBufferで必ずfalseを返しています。 #if USE_STRUCTURED_BUFFER_FOR_LIGHT_DATA uint offset = unity_LightData.x; return _AdditionalLightsIndices[offset + index]; // SHADER_API_GLESはOpenGL ES 2.0の時に有効になります。 // OpenGL ES 2.0ではライト上限が4個なので処理を分けています。 // unity_LightIndicesはfloat4 x 2の2次元配列でライト(最大8個)のインデックスが格納されています #elif !defined(SHADER_API_GLES) return unity_LightIndices[index / 4][index % 4]; #else half2 lightIndex2 = (index < 2.0h) ? unity_LightIndices[0].xy : unity_LightIndices[0].zw; half i_rem = (index < 2.0h) ? index : index - 2.0h; return (i_rem < 1.0h) ? lightIndex2.x : lightIndex2.y; #endif }
ライトのインデックスから、Lgiht構造体に必要な情報を渡します。
// Fills a light struct given a perObjectLightIndex Light GetAdditionalPerObjectLight(int perObjectLightIndex, float3 positionWS) { ////一部のプラットフォームではStructuredBuffersに問題があるため、このバリアントは無効になります。 // RenderingUtils.useStructuredBufferで必ずfalseを返しています。 #if USE_STRUCTURED_BUFFER_FOR_LIGHT_DATA float4 lightPositionWS = _AdditionalLightsBuffer[perObjectLightIndex].position; half3 color = _AdditionalLightsBuffer[perObjectLightIndex].color.rgb; half4 distanceAndSpotAttenuation = _AdditionalLightsBuffer[perObjectLightIndex].attenuation; half4 spotDirection = _AdditionalLightsBuffer[perObjectLightIndex].spotDirection; #else // ForwardLightsから渡された配列から追加ライトの情報を収集する。 float4 lightPositionWS = _AdditionalLightsPosition[perObjectLightIndex]; half3 color = _AdditionalLightsColor[perObjectLightIndex].rgb; half4 distanceAndSpotAttenuation = _AdditionalLightsAttenuation[perObjectLightIndex]; half4 spotDirection = _AdditionalLightsSpotDir[perObjectLightIndex]; #endif // ライトベクトル // LightPositionWS.wはディレクショナルライトの場合に0になります。 float3 lightVector = lightPositionWS.xyz - positionWS * lightPositionWS.w; // 距離減衰と角度減衰をかける float distanceSqr = max(dot(lightVector, lightVector), HALF_MIN); half3 lightDirection = half3(lightVector * rsqrt(distanceSqr)); half attenuation = DistanceAttenuation(distanceSqr, distanceAndSpotAttenuation.xy) * AngleAttenuation(spotDirection.xyz, lightDirection, distanceAndSpotAttenuation.zw); // 計算した情報をLight構造体に入れる Light light; light.direction = lightDirection; light.distanceAttenuation = attenuation; light.shadowAttenuation = 1.0; light.color = color; return light; }
_AdditionalLightsXXXXから情報を受け取り、必要な計算をして、LIght構造体に入れています。 ここでは距離減衰と角度減衰について説明します。
距離減衰
スポットライトとポイントライトでは距離減衰を考慮します。 距離衰退とはライトとオブジェクトの距離が離れるほど、光が弱まることで、距離rの2乗に反比例して減衰します。
DistanceAttenuation関数は距離減衰を考慮した光の強さを返します。
// UnityVanilaの減衰に一致します // 減衰は光の範囲までスムーズに減少します。 float DistanceAttenuation(float distanceSqr, half2 distanceAttenuation) { // rcp(distanceSqr)は、1/distanceSqrの近似です。 // 近似なので1/distanceするより早いです。 float lightAtten = rcp(distanceSqr); #if SHADER_HINT_NICE_QUALITY half factor = distanceSqr * distanceAttenuation.x; half smoothFactor = saturate(1.0h - factor * factor); smoothFactor = smoothFactor * smoothFactor; #else half smoothFactor = saturate(distanceSqr * distanceAttenuation.x + distanceAttenuation.y); #endif return lightAtten * smoothFactor; }
モバイルプラットフォームでは以下のような計算が行われています。
lightRange | 光が届く範囲 |
distance | 光が当たる物体までの距離 |
この式で以下のような条件を満たしています。
・range が 80% までは光量は減衰しない(1以上になる)
・range が 80% から100% の間で線形に減衰する(1未満になる)
・range が 100% で光量がゼロになる
実際にはこの式を変形させて、CPUで事前計算した値をdistanceAttenuationに入れています コード上では以下のようにsaturateで0〜1の範囲にclampしています。
half smoothFactor = saturate(distanceSqr * distanceAttenuation.x + distanceAttenuation.y);
角度減衰
スポットライトでは角度減衰も考慮します。
角度減衰は光が衰退を始める半影角度(画像のinner-corn)と、光が届かなくなる本影角度(画像のouter-corn)が存在します。
half AngleAttenuation(half3 spotDirection, half3 lightDirection, half2 spotAttenuation) { half SdotL = dot(spotDirection, lightDirection); half atten = saturate(SdotL * spotAttenuation.x + spotAttenuation.y); return atten * atten; }
S | スポットライトの方向ベクトル |
L | スポットライトから物体への方向ベクトル |
本影角度 | |
半影角度 |
この式で以下のような条件を満たしています。
・半影角度までは光量は減衰しない
・半影角度から本影角度の間で線形に減衰する
・本影角度で光量がゼロになる
距離減衰と同じように実際にはこの式を変形させて、CPUで事前計算した値をdistanceAttenuationに入れています。
ランバートライティング
half3 VertexLighting(float3 positionWS, half3 normalWS) { half3 vertexLightColor = half3(0.0, 0.0, 0.0); // 追加ライトの計算を頂点シェーダーで行うとき有効 #ifdef _ADDITIONAL_LIGHTS_VERTEX // 計算対象とする追加ライトの数 uint lightsCount = GetAdditionalLightsCount(); // 計算対象の追加ライトをforで回しながら合算カラーを求める for (uint lightIndex = 0u; lightIndex < lightsCount; ++lightIndex) { // ライトのIndex(lightIndex)から対応するライト情報をLight構造体に格納 Light light = GetAdditionalLight(lightIndex, positionWS); // 光の減衰 half3 lightColor = light.color * light.distanceAttenuation; vertexLightColor += LightingLambert(lightColor, light.direction, normalWS); } #endif return vertexLightColor; }
GetAdditionalLightで必要なライト情報を格納したら、先ほど計算した光の減衰をライトカラーに乗算して、適応します。
その下のLightingLambertは法線とライトベクトルの内積をとって、ライトカラーに乗算しています
half3 LightingLambert(half3 lightColor, half3 lightDir, half3 normal)
{
half NdotL = saturate(dot(normal, lightDir));
return lightColor * NdotL;
}
頂点シェーダーでおこなう追加ライトのライティング処理はこれで終わりです。
グローバルイルミネーション
続いて、グローバルイルミネーションの処理が行われます。 グローバルイルミネーションはGIや大域照明とも呼ばれます。 ライトから直接、当たる光(直接光)だけではなく、反射を繰り返した光(間接光)を考慮することで、上の画像のようにオブジェクトが互いの外観に影響を与えるので、よりリアルに見せることができます。
これを実現するにはリアルタイムで計算すると非常に高負荷なため、現実的ではありません。
Unityではライトマップまたはライトプローブでグローバルイルミネーションを低負荷で実装することができます。
ライトマップ
ライトマップとは高負荷な光や影の計算をゲーム実行前に行い陰影情報を記録したテクスチャで、実行時にライトマップ専用のUVからライトマップを参照して、描画する技術です。 staticなオブジェクトやライトにしか適応できませんが、低負荷でリッチな描画ができます。 また、ライトマップ分のメモリを消費します。
ライトプローブ
ライトプローブではDytamicなオブジェクトに対しても、近い効果を得られます。 (光源が移動するものには使用できません) ライトプローブをシーン上に配置します。 ライトプローブにはその場所の色の情報を持っていおり、Dynamicなオブジェクトは周辺のライトプローブの色と位置関係から、自身に当たる光を計算します。
ライトマップUVまたはSHライトカラーの取得
VertexLightingの下を見ていくとライトマップとSHライトカラーのマクロが呼び出されています。
OUTPUT_LIGHTMAP_UV(input.lightmapUV, unity_LightmapST, output.lightmapUV); OUTPUT_SH(output.normalWS.xyz, output.vertexSH);
#ifdef LIGHTMAP_ON #define DECLARE_LIGHTMAP_OR_SH(lmName, shName, index) float2 lmName : TEXCOORD##index #define OUTPUT_LIGHTMAP_UV(lightmapUV, lightmapScaleOffset, OUT) OUT.xy = lightmapUV.xy * lightmapScaleOffset.xy + lightmapScaleOffset.zw; #define OUTPUT_SH(normalWS, OUT) #else #define DECLARE_LIGHTMAP_OR_SH(lmName, shName, index) half3 shName : TEXCOORD##index #define OUTPUT_LIGHTMAP_UV(lightmapUV, lightmapScaleOffset, OUT) #define OUTPUT_SH(normalWS, OUT) OUT.xyz = SampleSHVertex(normalWS) #endif
ライトマップが設定されている場合は、ライトマップのUV座標を取得し、ライトマップが無効のときはライトプローブのSH(球面調和関数)ライトからライトカラー値を取得します。
これらの値をそれぞれ、output.lightmapUV、output.vertexSHに格納しています。
フラグメントシェーダー
half4 LitPassFragment(Varyings input) : SV_Target { // インスンスIDの初期化 UNITY_SETUP_INSTANCE_ID(input); // 立体視初対応 UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input); #if defined(_PARALLAXMAP) #if defined(REQUIRES_TANGENT_SPACE_VIEW_DIR_INTERPOLATOR) half3 viewDirTS = input.viewDirTS; #else half3 viewDirTS = GetViewDirectionTangentSpace(input.tangentWS, input.normalWS, input.viewDirWS); #endif ApplyPerPixelDisplacement(viewDirTS, input.uv); #endif SurfaceData surfaceData; // テクスチャから必要なデータを取得 InitializeStandardLitSurfaceData(input.uv, surfaceData); // 必要なデータを構造体にInputData格納 InputData inputData; InitializeInputData(input, surfaceData.normalTS, inputData); // PBRの計算 half4 color = UniversalFragmentPBR(inputData, surfaceData); // FOG適応 color.rgb = MixFog(color.rgb, inputData.fogCoord); // アルファを適応 color.a = OutputAlpha(color.a, _Surface); return color; }
フラグメントシェーダーのライティング処理
フラグメントシェーダーのライティング処理はUniversalFragmentPBRに集約されています。 InitializeBRDFData、InitializeBRDFDataClearCoat、ではライティングに必要な情報の準備をしています。 その下ではshadowMaskの値を必要に応じて取得しています。 GetMainLightではライトの情報を取得しています。
half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData) { #ifdef _SPECULARHIGHLIGHTS_OFF bool specularHighlightsOff = true; #else bool specularHighlightsOff = false; #endif BRDFData brdfData; // BRDFdata構造体を構築 InitializeBRDFData(surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData); BRDFData brdfDataClearCoat = (BRDFData)0; #if defined(_CLEARCOAT) || defined(_CLEARCOATMAP) // base brdfData is modified here, rely on the compiler to eliminate dead computation by InitializeBRDFData() InitializeBRDFDataClearCoat(surfaceData.clearCoatMask, surfaceData.clearCoatSmoothness, brdfData, brdfDataClearCoat); #endif // ライトマップが有効でShadowMaskを使用している場合はinputData構造体に入れておいたShadowMaskの情報を格納 #if defined(SHADOWS_SHADOWMASK) && defined(LIGHTMAP_ON) half4 shadowMask = inputData.shadowMask; // ライトマップが無効な時 #elif !defined (LIGHTMAP_ON) half4 shadowMask = unity_ProbesOcclusion; #else half4 shadowMask = half4(1, 1, 1, 1); #endif // メインライトの情報を取得 Light mainLight = GetMainLight(inputData.shadowCoord, inputData.positionWS, shadowMask); // SSAO #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); // GI 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); // 追加ライト #ifdef _ADDITIONAL_LIGHTS uint pixelLightCount = GetAdditionalLightsCount(); for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex) { Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask); #if defined(_SCREEN_SPACE_OCCLUSION) light.color *= aoFactor.directAmbientOcclusion; #endif color += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff); } #endif // 追加のライトのライティング処理を頂点シェーダーで行った場合はそれを使う #ifdef _ADDITIONAL_LIGHTS_VERTEX color += inputData.vertexLighting * brdfData.diffuse; #endif // 発光 color += surfaceData.emission; return half4(color, surfaceData.alpha); }
SSAO
更にその下にSSAOの処理があります。 処理を見る前にSSAOについて解説します。
SSAO(スクリーンスペース・アンビエント・オクルージョンの略称で、アンビエントオルクルージョン)は動的にAOを実行するポストエフェクトです。 AOとは光源や周辺の物体の間にある遮蔽物によって、どのくらい物体に届く直接光や間接光が減衰するかを表したものです。 下のSSAOありの画像では隣接するオブジェクトによって光が減衰し、影が入っていることが分かります。
SSAO なし
SSAO あり
AOを発生させるために、AOを発生させる位置とどのくらい濃く陰が出るかを表したSSAOテクスチャを使用します。
SSAOテクスチャを作成する方法はいくつかあり、URPのSSAOではAlchemy Ambient Obscuranceというアルゴリズムを採用しています。
SSAOテクスチャはスクリーンスペースで求めます。 注目しているピクセルPの法線方向を向いた半球の範囲内でサンプリングします。 Pと複数のサンプリングポイントのベクトルVと法線nから、内積をとり、足し合わせます。 内積はコサインなので値が大きくなるほど、P周辺の遮蔽度が高いということになります。 (Pが凹んでいる)
URPでSSAOを実装するにはRendererFeatureから、追加できます。
SSAOの処理を見ます。
// SSAO // SSAOが有効なとき #if defined(_SCREEN_SPACE_OCCLUSION) AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(inputData.normalizedScreenSpaceUV); mainLight.color *= aoFactor.directAmbientOcclusion; surfaceData.occlusion = min(surfaceData.occlusion, aoFactor.indirectAmbientOcclusion); #endif
_SCREEN_SPACE_OCCLUSIONはUniversalRenderPipeline.csのShaderKeywordStrings.ScreenSpaceOcclusionとして定義されており、 先ほど追加したRendererFeature(ScreenSpaceAmbientOcclusion)で、レンダーキューに追加される際にオンになります。
GetScreenSpaceAmbientOcclusion関数により、AOの衰退量を計算しています。 引数にはスクリーン上の座標を渡します。
AmbientOcclusionFactor GetScreenSpaceAmbientOcclusion(float2 normalizedScreenSpaceUV) { AmbientOcclusionFactor aoFactor; aoFactor.indirectAmbientOcclusion = SampleAmbientOcclusion(normalizedScreenSpcaceUV); aoFactor.directAmbientOcclusion = lerp(1.0, aoFactor.indirectAmbientOcclusion, _AmbientOcclusionParam.w); return aoFactor; }
SampleAmbientOcclusion関数によって、SSAOテクスチャから間接光の値をサンプリングします。
half SampleAmbientOcclusion(float2 normalizedScreenSpaceUV)
{
float2 uv = UnityStereoTransformScreenSpaceTex(normalizedScreenSpaceUV);
return SAMPLE_TEXTURE2D_X(_ScreenSpaceOcclusionTexture, sampler_ScreenSpaceOcclusionTexture, uv).x;
}
UnityStereoTransformScreenSpaceTex関数はVR用の関数なので、無視します。 重要なのはその下で、_ScreenSpaceOcclusionTextureからサンプリングして、その結果を返しています。
aoFactor.directAmbientOcclusion = lerp(1.0, aoFactor.indirectAmbientOcclusion, _AmbientOcclusionParam.w); return aoFactor;
先ほどの関数に戻ります。
AmbientOcclusionParam.wはRendererFeatureのDirect Lighting Strengthで設定された値が入っています。 Direct Lighting Strengthはアンビエントオクルージョンに対する直接照明の影響を設定するので、先AmbientOcclusionParam.wの値でほどの間接光によるAOと1を補完しています。
その下では直接光のaoを決定しています。
#if defined(_SCREEN_SPACE_OCCLUSION) AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(inputData.normalizedScreenSpaceUV); mainLight.color *= aoFactor.directAmbientOcclusion; surfaceData.occlusion = min(surfaceData.occlusion, aoFactor.indirectAmbientOcclusion); #endif
計算した直接光のAOはメインライトに乗算し、間接光のAOはsurfaceData.occlusionより小さかった場合はsurfaceData.occlusionに格納します。 surfaceData.occlusionはマテリアルで設定したオクルージョンマップのサンプリング結果が入っており、間接照明の影響を受けるかをマスクする役割があります。
混合ライティング
次は混合ライティングの処理が行われます。
下の画像では赤色の動的オブジェクトと青色の静的オブジェクトの影が加算されて、濃くなってしまいます。
混合ライティングとはライトマップやライトプローブなどの静的な影と動的な影を併用する際に、生合成を保つ手法です。
有効にするにはライトコンポーネントのModeをMixedにする必要があります。
混合ライティングには3種類あります。 順番に説明します。
Baked Indirect
Baked Indirect モード: 間接光だけが事前計算されます 混合ライティング - Unity マニュアル引用
直接光はリアルタイムで計算され、間接光のみライトマップに焼かれます。
ただしShadow Distance (Edit > Project Settings > Quality > Shadows > Shadow Distance) の範囲外では影を作りません。 範囲外の部分をFogを使用することで隠すことができます。
Shadowmask
Shadowmask と Distance Shadowmask モード: 間接光と直接オクルージョンが事前計算されます
直接光の影と間接光の影をShadowMaskに書き込みます。 ShadowMaskはあくまでMaskなので動的に影色を変更することができます。
Distance ShadowMakを使用すると設定した距離まではリアルタイムで 計算し、それ以降はShadowMaskを使用します。
Subtractive
Subtractive モード: すべてのライトパスが事前計算されます とにかく軽くしたいときはSubtractiveにします。 LightmapとDirectional Lightmapが生成され、Directional ModeをNon-Directionalに変更するとLightmapのみが生成されます。
メインライトのみでリアルタイムに影をレンダリングし、その結果をベイクした直接光と間接光を合成します。 他のモードのいずれも使用できない場合使用します。
Subtractiveには、ライトマップの影にリアルタイムシャドウの影が二重に落ちてしまう弱点があります。Environment > Realtime Shadow Colorを調整して同じような(溶け込む)色にする必要があります。
void MixRealtimeAndBakedGI(inout Light light, half3 normalWS, inout half3 bakedGI) { #if defined(LIGHTMAP_ON) && defined(_MIXED_LIGHTING_SUBTRACTIVE) bakedGI = SubtractDirectMainLightFromLightmap(light, normalWS, bakedGI); #endif }
UniversalFragmentPBRで呼び出されているこの関数は ライトマップが有効で、Lighting Mode が Subtractiveが有効な時にSubtractDirectMainLightFromLightmapを呼び出します。
Shadowmask/Distance Shadowmaskモードは非対応で、今後対応予定です。
大照明ライトカラー
大照明ライトカラーは間接ディフューズライトカラーと間接スペキュラライトカラー値を乗算することで求まります。
half3 GlobalIllumination(BRDFData brdfData, BRDFData brdfDataClearCoat, float clearCoatMask, half3 bakedGI, half occlusion, half3 normalWS, half3 viewDirectionWS) { // 反射ベクトル half3 reflectVector = reflect(-viewDirectionWS, normalWS); half NoV = saturate(dot(normalWS, viewDirectionWS)); // フレネル項 half fresnelTerm = Pow4(1.0 - NoV); // 間接ディフューズライトカラー値の算出 half3 indirectDiffuse = bakedGI * occlusion; // 間接スペキュラライトカラー値の算出 half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, brdfData.perceptualRoughness, occlusion); half3 color = EnvironmentBRDF(brdfData, indirectDiffuse, indirectSpecular, fresnelTerm); // クリアコートが有効な時 #if defined(_CLEARCOAT) || defined(_CLEARCOATMAP) half3 coatIndirectSpecular = GlossyEnvironmentReflection(reflectVector, brdfDataClearCoat.perceptualRoughness, occlusion); half3 coatColor = EnvironmentBRDFClearCoat(brdfDataClearCoat, clearCoatMask, coatIndirectSpecular, fresnelTerm); half coatFresnel = kDielectricSpec.x + kDielectricSpec.a * fresnelTerm; return color * (1.0 - coatFresnel * clearCoatMask) + coatColor; #else return color; #endif }
// 間接ディフューズライトカラー値の算出
half3 indirectDiffuse = bakedGI * occlusion;
間接ディフューズライトカラー値は、ライトマップカラー 値(bakedGI)にオクルージョン量(occlusion)を乗算した値になります。
// 間接スペキュラライトカラー値の算出
half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, brdfData.perceptualRoughness, occlusion);
間接スペキュラライトカラーはGlossyEnvironmentReflectionで計算しています。
half3 GlossyEnvironmentReflection(half3 reflectVector, half perceptualRoughness, half occlusion) { // リフレクションプローブが有効な時 #if !defined(_ENVIRONMENTREFLECTIONS_OFF) half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness); half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip); #if defined(UNITY_USE_NATIVE_HDR) || defined(UNITY_DOTS_INSTANCING_ENABLED) half3 irradiance = encodedIrradiance.rgb; #else half3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR); #endif return irradiance * occlusion; #endif // GLOSSY_REFLECTIONS return _GlossyEnvironmentColor.rgb * occlusion; }
half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness); half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_ SpecCube0, reflectVector, mip);
PerceptualRoughnessToMipmapLevel 関数は、キューブマップからサンプリングする際のミップマップ レベル(mip)を計算します。 キューブマップからライトプローブカラーを取得しています。 PerceptualRoughnessToMipmapLevelの引数にperceptualRoughness(設定したラフネス量)にしているのは、ラフネス量が多くなるほど、mipmapレベルが上がり、風景の映り込みはぼやけていきます。
#if defined(UNITY_USE_NATIVE_HDR) || defined(UNITY_DOTS_INSTANCING_ENABLED) half3 irradiance = encodedIrradiance.rgb; #else half3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR); #endif
ライトマップ系ではHDRでレンダリングすることができます。 これを低ダイナミックレンジのテクスチャにエンコードできます。 それをDecodeHDREnvironment関数でデコードしたライトカラー値をイラジアンス(irradiance 放射照度)としています。
return irradiance * occlusion;
反射マップからサンプリングした周辺光のライトカラー値(irradiance)に、オクルージョンマップからサンプリングしたオクルージョン量(occlusion)を乗算して戻り値とします。
EnvironmentBRDF関数は、EnvironmentBRDFClearCoat 環境ディヒューズカラー(indirectDiffuse)とマテリアルディヒューズカラー(brdfData.diffuse)を乗算し、環境スペキュラーカラー(indirectSpecular)とマテリアルスペキュラカラー(EnvironmentBRDFSpecular関数)を乗算した値を加算しています。
half3 EnvironmentBRDF(BRDFData brdfData, half3 indirectDiffuse, half3 indirectSpecular, half fresnelTerm)
{
half3 c = indirectDiffuse * brdfData.diffuse;
c += indirectSpecular * EnvironmentBRDFSpecular(brdfData, fresnelTerm);
return c;
}
EnvironmentBRDFSpecular関数ではマテリアルスペキュラカラーを計算します・
half3 EnvironmentBRDFSpecular(BRDFData brdfData, half fresnelTerm) { float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0); return surfaceReduction * lerp(brdfData.specular, brdfData.grazingTerm, fresnelTerm); }
float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0);
間接光の幾何減衰を簡易的に算出します。
return surfaceReduction * lerp(brdfData.specular, brdfData.grazingTerm, fresnelTerm);
lerpの部分ではフレネル反射率を表しています。
【Unity】【シェーダー】 フレネル反射 - シェーダーTips
メインライトのライトカラー値
half3 LightingPhysicallyBased(BRDFData brdfData, BRDFData brdfDataClearCoat, half3 lightColor, half3 lightDirectionWS, half lightAttenuation, half3 normalWS, half3 viewDirectionWS, half clearCoatMask, bool specularHighlightsOff) { // ラジアンス(放射輝度) half NdotL = saturate(dot(normalWS, lightDirectionWS)); half3 radiance = lightColor * (lightAttenuation * NdotL); // 5-2:BRDF項を計算し、最終的なライトカラー値を算出する。 return return DirectBDRF(brdfData,normalWS,light.direction, viewDirectionWS) * radiance; }
half NdotL = saturate(dot(normalWS, lightDirectionWS));
ランバートの計算を行なっています。
half3 radiance = lightColor * (lightAttenuation * NdotL);
ライトの距離と角度減衰(light.distanceAttenuation) と影減衰(light.shadowAttenuation)を乗算して最終的な減衰値(lightAttenuation)ととランバート(NdotL)とライト自体に設定されたライトカラー値(lightColor)を乗算し、ラジアンスとします。
スペキュラBRDF(双方向反射率分布関数)
DirectBDRFでは直接スペキュラライトカラー値の反射率を求めるために、BRDFを使用します。
LitシェーダーのBRDF式はUnityがCook-Torranceモデルの簡易版をベースに開発したMinimalist CookTorrance BRDFが採用されています。
記号は以下です。
D項 | 法線分布関数 |
G項 | 幾何減衰 |
F項 | フレネル |
N | 法線ベクトル |
V | 視点方向ベクトル |
L | ライト方向ベクト ル |
roughness | ラフネス |
specColor | マテリアルスペキュラカラー |
D項、G項、F項については以下のサイトでそれぞれの解説しているので、必要であればご覧ください。
Cook-Torranceモデルの式は以下になります。
ここでをV項(Visibility term 可視度項)とし、式を変更します。
D項にGGXモデル、V項にKSK近似モデル、F項にChristian Schüler による近似式を使います。
roughness は表面のざらつきを表すパラメータです。specColorはマテリアルスペキュラカラー値です。Hはハーフベクトルです。 これらの項の中で、V項とF項に採用した近似式は、どちらもかなり大胆な近似で、高速ですが、物理的な正しさは不十分です。。そこで、V項とF項を1 つの式とし、さらにパラメータを追加して以下のように改良します。
この式は、モバイルでも高速に動作し、物理的な再現性が比較的高い式になっているようです。 これらのG項とV・F項を元の式にあてはめると、最終的に以下の式になります。また、specColorは最終的にカラー値を算出する時に乗算するので、BRDF式では以後記述を省略します。
この式が DirectBDRF関数内で評価され、スペキュラ反射率を算出します。ただし、式の中のいくつかの項は事前に計算され、BRDFData構造体にあらかじめ格納されます。 BRDFData構造体の要素名を以下に示します。
roughness | 見た目上のラフネス量 |
roughness2 | roughnessの2乗 |
roughness2MinusOne | roughness2-1 |
normalizationTerm | roughness * 4.0h + 2.0h |
half3 DirectBDRF(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS) { // マテリアルインスペクタのAdvanced/Specular HighLithgtsをオンにすると、鏡面光沢が反映されるようになります。 #ifndef _SPECULARHIGHLIGHTS_OFF float3 halfDir = SafeNormalize( float3(lightDirectionWS) + float3(viewDirectionWS)); float NoH = saturate(dot(normalWS, halfDir)); half LoH = saturate(dot(lightDirectionWS, halfDir)); float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f; half LoH2 = LoH * LoH; half specularTerm = brdfData.roughness2 / ((d * d) * max(0.1h, LoH2) * brdfData.normalizationTerm); #if defined (SHADER_API_MOBILE) || defined (SHADER_API_SWITCH) specularTerm = specularTerm - HALF_MIN; specularTerm = clamp(specularTerm, 0.0, 100.0); #endif half3 color = specularTerm * brdfData.specular + brdfData.diffuse; // D return color; #else return brdfData.diffuse; // E #endif }
// ハーフベクトル float3 halfDir = SafeNormalize( float3(lightDirectionWS) + float3(viewDirectionWS)); // ハーフベクトルと法線の内積 float NoH = saturate(dot(normalWS, halfDir)); // ハーフベクトルとライトベクトルの内積 half LoH = saturate(dot(lightDirectionWS, halfDir)); float d = NoH * NoH * brdfData.roughness2MinusOne + 1.00001f; half LoH2 = LoH * LoH; half specularTerm = brdfData.roughness2 / ((d * d) * max(0.1h, LoH2) * brdfData.normalizationTerm);
ここでスペキュラBRDFの式を計算しています。 変数dの計算時に1.0ではなく1.00001を加算しているのは後の式でゼロ除算を回避するためです。
#if defined (SHADER_API_MOBILE) || defined (SHADER_API_SWITCH) specularTerm = specularTerm - HALF_MIN; specularTerm = clamp(specularTerm, 0.0, 100.0); #endif half3 color = specularTerm * brdfData.specular + brdfData.diffuse; // D return color; #else return brdfData.diffuse; // E #endif
マテリアルスペキュラカラー値(brdfData.specular)に、BRDF式で計算した反射率 (specularTerm)を乗算し、それにマテリアルディフューズカラー値(brdfData.diffuse)を加算した値を、メインライトの反射による直接ライトカラー値とします。
追加ライト&発光
UniversalFragmentPBRの残りの処理を見ていきます。
half4 UniversalFragmentPBR(InputData inputData, SurfaceData surfaceData) { ・ ・ ・ 省略 // 追加ライト #ifdef _ADDITIONAL_LIGHTS uint pixelLightCount = GetAdditionalLightsCount(); for (uint lightIndex = 0u; lightIndex < pixelLightCount; ++lightIndex) { Light light = GetAdditionalLight(lightIndex, inputData.positionWS, shadowMask); #if defined(_SCREEN_SPACE_OCCLUSION) light.color *= aoFactor.directAmbientOcclusion; #endif color += LightingPhysicallyBased(brdfData, brdfDataClearCoat, light, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff); } #endif // 追加のライトのライティング処理を頂点シェーダーで行った場合はそれを使う #ifdef _ADDITIONAL_LIGHTS_VERTEX color += inputData.vertexLighting * brdfData.diffuse; #endif // 発光 color += surfaceData.emission; return half4(color, surfaceData.alpha); }
追加ライトを頂点シェーダーで計算してた場合(_ADDITIONAL_LIGHTS_VERTEXが有効)はその値を使用します。 そうでなければフラグメントシェーダーで処理します。
color += surfaceData.emission;
HDRカラーを加算して発光表現を行います。 詳しくはこちらをご参考ください。
以上でLitシェーダーのライティング処理について一通り解説し終えました。 最後までご覧いただきありがとうございました。
参考
https://casual-effects.com/research/McGuire2011AlchemyAO/VV11AlchemyAO.pdf
【Unity】【シェーダー】描画するオブジェクトの深度値と深度バッファの取得方法
Shader "Zpos" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; float4 projPos : TEXCOORD1; }; // デプステクスチャの宣言 UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture); v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); // 深度バッファの値を取得するためにスクリーンスペースでの位置を求める o.projPos = ComputeScreenPos(o.vertex); // ビュー座標系での深度値を求める COMPUTE_EYEDEPTH(o.projPos.z); return o; } fixed4 frag (v2f i) : SV_Target { // 描画するピクセルの深度バッファの値 float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,i.projPos)); // 描画するピクセルの深度値 float partZ = i.projPos.z; return fixed4(1,1,1,1); } ENDCG } } }
深度バッファの取得
ComputeScreenPos
定義は以下。
inline float4 ComputeScreenPos(float4 pos) { float4 o = ComputeNonStereoScreenPos(pos); #if defined(UNITY_SINGLE_PASS_STEREO) o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w); #endif return o; } inline float4 ComputeNonStereoScreenPos(float4 pos) { float4 o = pos * 0.5f; o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w; o.zw = pos.zw; return o; }
UNITY_SINGLE_PASS_STEREO
VRChatなどのUnityのVRアプリはシングルパスステレオレンダリングという方法でレンダリングされており、その際に有効となります。 そのため、無視してください。
ComputeNonStereoScreenPos
_ProjectionParams.x にはプラットフォーム毎の y の向きの扱いを吸収するために +1.0 または -1.0 が入っています。これを y 座標に掛けると y の向きを統一することができます。 ny-program.hatenablog.com
1行目と2行目でクリップ座標に対して、0.5を乗算し、w成分を加算しています。 これでスクリーン座標に変換できる原理を説明します。
正規デバイス座標系はX(幅)とY(高さ)が2で-1〜1の範囲をとります。
クリップ空間から正規デバイス座標系に変換するにはwで除算します。
ということはクリップ空間のXYは-w〜wの範囲をとることになります。
posに0.5を乗算することでo.xy は -0.5w ~ +0.5wの範囲をとり、wは0.5wになります。
そして、-0.5w ~ +0.5wに0.5wを足すことで、0 ~ wに変換しています。 これは後で w で除算して 0 ~ 1 となる UV 座標として使いたいためです。
求めたスクリーン座標から深度バッファにアクセスしてz値を算出します。
// 描画するピクセルの深度バッファの値 float sceneZ = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture,i.projPos));
SAMPLE_DEPTH_TEXTURE_PROJ
# define SAMPLE_DEPTH_TEXTURE_PROJ(sampler, uv) (tex2Dproj(sampler, uv).r)
tex2Dprojの結果からr成分を返しているだけです。 深度バッファはモノトーンなので、r成分だけ返せば充分ということですね。
tex2Dproj()は与えられた座標のxyをw除算をしてからtex2D()のuvとして使うという処理をします。 先述したとおり、ComputeScreenPosで求めた0~wの値をw除算して、uv値(0 ~ 1 )として、使用するためです。
inline float LinearEyeDepth(float z) { return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w); }
深度バッファには正規デバイス座標系のz値が格納(0〜1)されていますが、実際のz値に対して、線形ではありません。 near付近の正規デバイス座標系のz値の範囲は大きいですが、far付近の正規デバイス座標系のz値の範囲はとても小さいです。 そのため、これを線形にしています。
この関数は純粋な深度バッファの値が欲しければ必要ありませんが、ソフトパーティクルや他のオブジェクトと重なっている部分を判定したいときなどに描画するオブジェクトの深度値と比較したい場合は必要になります。
描画するオブジェクトの深度値の取得
COMPUTE_EYEDEPTH
#define COMPUTE_EYEDEPTH(o) o = -UnityObjectToViewPos( v.vertex ).z inline float3 UnityObjectToViewPos( in float3 pos ) { return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz; }
defineにより、引数に渡した値にUnityObjectToViewPosで求めた座標のz値を代入しています。
UnityObjectToViewPos
UnityObjectToViewPosの引数にはv.vertexが指定されています。 これは頂点シェーダーの引数であるローカル座標です。
ローカル座標に対して、unity_ObjectToWorld(ワールド行列)とUNITY_MATRIX_V(ビュー行列)を乗算しています。
つまり、COMPUTE_EYEDEPTHはローカル座標をビュー座標に変換し、そのz値を引数oに対して代入しています。
float partZ = i.projPos.z;
あとはフラグメントシェーダーでそのまま値を使えばOKです!
関連
参考
【Unity】Disney Diffuse BRDF
記号 | 説明 |
---|---|
視線方向を表す単位ベクトル | |
入射光の方向を表す単位ベクトル | |
法線方向を表す単位ベクトル | |
ハーフベクトル | |
BRDFの拡散反射成分 | |
拡散反射率(ディフューズアルベド) | |
法線方向から入射する光の反射率 | |
グレージング角から入射する光の反射率 |
Disney Diffuse BRDFとは?
Disney Diffuse BRDFとはBurley Diffuseとも呼ばれ、エネルギー保存の法則を厳密に守っているわけではなく、発案者の経験則に基づいて提案されました。 特徴的なのは、DiffuseモデルにFresnelの効果が組み込まれている点です。
はディフューズアルベドに円周率を除算しています。 これは古典的なランバート反射です。 その後に続くはFresnelのSchlickによる近似式で、以下のような式です。
これを最初の式に代入した式がこちらです。
式の考察
の係数であるフレネル部分について考えていきます。
の値が大きくなるほどフレネルの値が大きくなることは明白です。
roughneesは粗さを表しており、金属に近い質感にしたいほど大きな値をデザイナーが設定します。 金属はフレネル反射が強くなるため、値が大きくなるほど、の値も大きくなるようになっています。
はLightとViewの角度が同一方向の時にHdotLが1となり、角度が離れるほど値が小さくなります。 そのため、拡散反射というよりは、指向性を持った再帰反射モデルに近いものだと思います。
指向性 ・・・太陽光ように表面の輝度がどの角度から見ても一定である拡散反射とは異なり、光源の方向により、表面の輝度が異なる性質
再帰反射・・・反射光が入射光と同一方向に反射する現象
実装
さきほどの公式をシェーダーで書くとこうなります。
Shader "DisneyDiffuseBRDF" { Properties { _Roughness("Roughness", Range(0.0, 1.0)) = 0.5 } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; half3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; half3 worldNormal : TEXCOORD2; half3 viewDir : TEXCOORD3; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.viewDir = UnityWorldSpaceViewDir(worldPos); return o; } sampler2D _MainTex; float _Roughness; float3 _LightColor0; inline half3 F_Schlick(half3 f0, half3 f90, half cos) { return f0 + (f90 - f0) * pow(1 - cos, 5); } inline half Fd_Burley(half ndotv, half ndotl, half ldoth, half roughness) { half fd90 = 0.5 + 2 * ldoth * ldoth * roughness; half lightScatter = F_Schlick(1,fd90,ndotl); half viewScatter = F_Schlick(1,fd90,ndotv); half diffuse = lightScatter * viewScatter / UNITY_PI; return diffuse; } fixed4 frag (v2f i) : SV_Target { half3 normal = normalize(i.worldNormal); half3 viewDir = normalize(i.viewDir); half ndotv = abs(dot(normal, viewDir)); float ndotl = max(0, dot(normal, _WorldSpaceLightPos0.xyz)); float3 halfDir = normalize(_WorldSpaceLightPos0.xyz + viewDir); half ldoth = max(0, dot(_WorldSpaceLightPos0.xyz, halfDir)); half diffuse = Fd_Burley(ndotv,ndotl,ldoth,_Roughness); return fixed4(diffuse * _LightColor0.rgb,1); } ENDCG } } }
【Unity】幾何減衰
マイクロファセット(微細表面)
マイクロファセットとは物体の表面には目では見えない凹凸のことです。
幾何減衰
マイクロファセットにより入射光が遮断される(シャドウイング)ことがあります。
同様に反射光もマイクロファセットにより遮断されます(マスキング)。 幾何減衰はこのマイクロファセットによる光の遮断による反射光の成分の減衰を計算するものです。
幾何減衰にはいくつか種類があるようですが、今回はHeight-Correlated Smithモデルを使用します。
実装
Shader "SmithGGXCorrelated" { Properties { _MainTex ("Texture", 2D) = "white" {} _Roughness("Roughness", Range(0.0, 1.0)) = 0.5 } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; half3 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; half3 worldNormal : TEXCOORD2; half3 viewDir : TEXCOORD3; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal = UnityObjectToWorldNormal(v.normal); o.viewDir = UnityWorldSpaceViewDir(worldPos); return o; } sampler2D _MainTex; float _Roughness; float3 _LightColor0; // 幾何減衰(V項) マイクロファセットの凹凸に遮れた反射光 inline float V_SmithGGXCorrelated(float ndotl, float ndotv, float alpha) { float lambdaV = ndotl * (ndotv * (1 - alpha) + alpha); float lambdaL = ndotv * (ndotl * (1 - alpha) + alpha); return 0.5f / (lambdaV + lambdaL + 0.0001); } fixed4 frag (v2f i) : SV_Target { half3 normal = normalize(i.worldNormal); half3 viewDir = normalize(i.viewDir); half ndotv = abs(dot(normal, viewDir)); float ndotl = max(0, dot(normal, _WorldSpaceLightPos0.xyz)); float alpha = _Roughness * _Roughness; half V = V_SmithGGXCorrelated(_Roughness,ndotv,alpha); return fixed4(V * _LightColor0.rgb,1); } ENDCG } } }
白色のキューブに緑色のライトを当てています。 右に行くほど_Roughness(粗さ)をあげています。 マイクロファセットにより入射光と反射光が遮断される為、右に行くほどライトの色(緑色)が消えています。