【UnityShader】ライティングの基礎シェーダー #115
はじめに
今回は複数ライトに対応した基本的なシェーダーについて勉強します。
シェーダー
Shader "Day3/Practice1" { Properties { _MainTex ("Texture", 2D) = "white" {} _SpecularPow ("Speclar Pow", float) = 5 } SubShader { Tags { "RenderType"="Opaque"} LOD 100 CGINCLUDE sampler2D _MainTex; float4 _MainTex_ST; // ライトの色を取得する half4 _LightColor0; #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; half3 normal : NORMAL; }; ENDCG Pass { Tags { "LightMode"="ForwardBase" } CGPROGRAM struct v2f { float2 uv : TEXCOORD0; half3 normal : NORMAL; float3 viewDir : TEXCOORD1; float3 lightDir : TEXCOORD2; float4 vertex : SV_POSITION; }; half _SpecularPow; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float4 worldPos = mul(unity_ObjectToWorld, v.vertex); o.viewDir = normalize(_WorldSpaceCameraPos - worldPos.xyz); o.uv = TRANSFORM_TEX(v.uv, _MainTex); float isDirectional = step(1, _WorldSpaceLightPos0.w); o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional)); o.normal = UnityObjectToWorldNormal(v.normal); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 texCol = tex2D(_MainTex, i.uv); // 拡散反射 float3 diffuse = saturate(dot(i.normal, i.lightDir)) * _LightColor0; // 鏡面反射 float3 reflectVec = reflect(-i.lightDir, i.normal); float specular = pow(saturate(dot(reflectVec, i.viewDir)), _SpecularPow); // 環境光 float3 ambient = ShadeSH9(float4(i.normal, 1)); return fixed4(ambient, 1); fixed4 col = fixed4(texCol.rgb * (ambient + diffuse) + specular, texCol.a); return col; } ENDCG } Pass { Tags { "LightMode"="ForwardAdd" } Blend One One CGPROGRAM struct v2f { float2 uv : TEXCOORD0; half3 normal : NORMAL; float3 lightDir : TEXCOORD1; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); float4 worldPos = mul(unity_ObjectToWorld, v.vertex); float isDirectional = step(1, _WorldSpaceLightPos0.w); o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional)); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.normal = UnityObjectToWorldNormal(v.normal); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 texCol = tex2D(_MainTex, i.uv); float3 lightCol = saturate(dot(i.normal, i.lightDir)) * _LightColor0; return fixed4(texCol.rgb * lightCol, 1); } ENDCG } } }
拡散反射光
非金属表面付近で起きる光の反射のうち、界面で発生する鏡面反射を除いた成分のことである
拡散反射 - Wikipedia:より引用
つまり、光があたっている部分を明るく、あたっていない部分は暗くなるものになります。
以下の画像のdiffuse reflection
がそれにあたります。
拡散反射 - Wikipedia:より引用
求め方
光が一番当たる部分は、光の向きとオブジェクトの法線が逆ベクトルに
当たらない部分は垂直、もしくはそれ以上と考えれば求められそうです。
以下の画像のようにイメージして頂ければわかりやすいかと思います。
つまり、光の向きと法線の内積を求めることで光の強さを計算することができます。
それをコードに起こしたものが以下になります。
// vertex float isDirectional = step(1, _WorldSpaceLightPos0.w); o.lightDir = normalize(_WorldSpaceLightPos0.xyz - (worldPos.xyz * isDirectional)); // fragment float3 diffuse = saturate(dot(i.normal, i.lightDir)) * _LightColor0;
_WorldSpaceLightPos0
ディレクショナルライトとその他のライトで取得できるものが異なります。
ライト | 値 |
---|---|
ディレクショナルライト | float4(ワールド空間の向き, 0) |
その他のライト | float4(ワールド空間の位置, 1) |
その他のライトの場合は、位置が取得できるので向きベクトルへと変換しています。
鏡面反射光
二物質の界面において発生する反射である。鏡面反射では反射の法則が成り立ち、入射角と反射角を等しくする。
鏡面反射 - Wikipedia:より引用
つまり、どの程度光を反射させるかを示すものになります。
求め方
まずは、光が入ってくるベクトルの反射ベクトルを求めます。
そのベクトルと視線のベクトルの内積を求めることで、反射の強さを求めることができそうです。
以下の画像のようにイメージして頂ければわかりやすいかと思います。
コードに起こしたものが以下になります。
// vertex o.viewDir = normalize(_WorldSpaceCameraPos - worldPos.xyz); // fragment float3 reflectVec = reflect(-i.lightDir, i.normal); float specular = pow(saturate(dot(reflectVec, i.viewDir)), _SpecularPow);
reflect
はhlslに定義済の反射ベクトルを返すものになります。
また、計算結果にpow
をしているのは物体によって反射率が異なるからになります。
環境光
物体間や物体内部における光の反射が生み出す間接光を簡易的に表現したい場合に用いる。
とのこと。
求め方
0.3f辺りを加算する手法もありますが、今回はShadeSH9
を使用します。
コードに起こしたものが以下になります。
float3 ambient = ShadeSH9(float4(i.normal, 1));
ShadeSH9
引数に法線を入れることで、環境光を取得するものになります。
wは1.0とコメントに記載してあります。
定義は以下となります。
// normal should be normalized, w=1.0 // output in active color space half3 ShadeSH9 (half4 normal) { // Linear + constant polynomial terms half3 res = SHEvalLinearL0L1 (normal); // Quadratic polynomials res += SHEvalLinearL2 (normal); # ifdef UNITY_COLORSPACE_GAMMA res = LinearToGammaSpace (res); # endif return res; }
深堀り
ShadeSH9の中身を深堀りしてみましたが、結論から述べると理解できませんでした。
// normal should be normalized, w=1.0 half3 SHEvalLinearL0L1 (half4 normal) { half3 x; // Linear (L1) + constant (L0) polynomial terms x.r = dot(unity_SHAr,normal); x.g = dot(unity_SHAg,normal); x.b = dot(unity_SHAb,normal); return x; } // normal should be normalized, w=1.0 half3 SHEvalLinearL2 (half4 normal) { half3 x1, x2; // 4 of the quadratic (L2) polynomials half4 vB = normal.xyzz * normal.yzzx; x1.r = dot(unity_SHBr,vB); x1.g = dot(unity_SHBg,vB); x1.b = dot(unity_SHBb,vB); // Final (5th) quadratic (L2) polynomial half vC = normal.x*normal.x - normal.y*normal.y; x2 = unity_SHC.rgb * vC; return x1 + x2; }
unity_SH
系は、球面調和関数によって求められたGIのようで、それと法線情報をごにょごにょして環境光を求めているっぽいです。
使わなかったもの
UNITY_LIGHTMODEL_AMBIENT
は公式にLegacy variableとあるので、使用しませんでした。
UNITY_LIGHTMODEL_AMBIENT
をそのまま返したものは以下になります。
各光の描画結果
拡散反射光
鏡面反射光
環境光
上記3つを合わせたもの
他のライトの実装
基本的には、拡散反射光と同様に法線とライトの向きの内積によって求めます。
float3 lightCol = saturate(dot(i.normal, i.lightDir)) * _LightColor0;
また、Unityにライトのパスを追加していることを伝えるためにForwardAdd
をTagsに追加します。
今回は、色を追加するのでBlendはOne One
にしています。
Tags { "LightMode"="ForwardAdd" } Blend One One
他のライトを追加した描画結果
ポイントライトを追加しました。
参考サイト様
【UnityShader】Deferred Shading #114
はじめに
Deffered Shadingについて勉強していきます。
Forward Rendering
各オブジェクトを1つ、または複数のパスで描画するものになります。
パスの数は、オブジェクトに作用するライトによって決まります。
ライティング
Forward Rendertingには以下の3つのライティングが存在します。
ライティング名 | 意味 |
---|---|
ピクセルライティング(Per-pixel) | ピクセルごとに計算される高価なもの |
頂点ライティング(Per-vertex) | 頂点ごとに計算される安価なもの |
球面調和ライティング(SH) | 頂点で複数のライトの球面調和による計算コストが実質0だが、あくまで近似なので精度は低い |
これらをUnityが自動で割り振ってくれます。
パス
パスは以下の2種類があります。
パス名 | 意味 |
---|---|
ベースパス | 1つのディレクショナルライトと全ての頂点ライティング、球面調和ライティングでオブジェクトをレンダリングする |
加算パス | 追加されたピクセルライティングでオブジェクトをレンダリングする |
問題点
ディレクショナルライト以外のピクセルライトは、加算パスでレンダリングされます。
これはひとつのライトごとにひとつのパスを使用するものになります。
つまり、1つのオブジェクトに対して複数のライトが当たる場面等で非常に負荷がかかってしまいます。
また、ピクセルライティングから頂点や球面調和ライティングに変更させても、これらは精度が低いものとなるので、不自然な描画結果になってしまう可能性があります。
Differd Rendering
こちらは2パスで描画する手法になります。
パス
パスは以下の2種類があります。
パス名 | 意味 |
---|---|
G-Bufferパス | 各オブジェクトを1回レンダリングするもの 描画に必要なデータをG-Bufferとしてテクスチャに書き込む |
ライティングパス | G-Bufferと深度に基づいてライティングを計算するパス |
G-Buffer
G-Bufferはテクスチャとして各データを保持しているものになります。
1パス目で、描画に必要なデータをG-Bufferに書き込きます。
2パス目以降で、このテクスチャを使用して描画していきます。
これらのテクスチャはグローバルシェーダーのプロパティとして、設定されます。
詳しくは以下となります。
プロパティ名 | 意味 | 備考 |
---|---|---|
_CameraGBufferTexture0 | Diffuse Color(RBG), occlusion(A) | |
_CameraGBufferTexture1 | Specular Color(RBG), roughness(A) | roughness = 1.0 - smoothness |
_CameraGBufferTexture2 | 法線ベクトル(RBG), 未使用(A) | |
_CameraGBufferTexture3 | Emission + lighting + lightmaps + reflection probes | カメラがHDRレンダリングを使用している場合、Emission + lightingのRenderTargetsは生成されない 代わりに、カメラがレンダリングするターゲットがRT3として使用される |
_CameraGBufferTexture4 | Light occlusion(RGBA) | ShadowmaskかDistanceShadowmaskのライトモードを使用している場合に使える |
_CameraDepthTexture | Depth + Stencil |
メリット
ライティングを2パス目でまとめて行うため、ライトを増やしても負荷がかかりにくいことが挙げられます。
また、オブジェクトに影響を与えられるライトの数に制限が無く、ピクセル毎に評価冴えるため正しく描画されます。
デメリット
半透明のオブジェクトを正しく描画できない
これはG-Bufferパスで情報を2次元のテクスチャに書き込んでしまうからになります。
このパス段階ではブレンドができず、G-Bufferには1ピクセルにつき1つのデータしか保存できないため不透明と半透明の情報を同時に保持できません。
これにより、オブジェクト同士の前後関係が無くなってしまい、半透明が正しく描画されなくなってしまいます。
MSAAが使えない
G-Bufferを全てMSAA用に変更する必要があります。
また、各サブピクセルでライティングを行う必要があり結果的にSSAAと負荷がそこまで変わらなくなってしまうからになります。
MSAA
1ピクセルをより小さなサブピクセルと呼ばれるもの二分割します。
この分割数はMSAAの手前に付いている値に応じて変化し、4xなら4分割、8xなら8分割になります。
このサブピクセルの深度を求め、深度の差が大きい箇所をエッジとして色を求める手法になります。
実装
適当にScene上にオブジェクトを配置します。
CameraのRendering PathをDeferred
に設定して終了になります。
結果
フレームデバッグ上に各Textureが出ています。
CameraDepthTexture
ShadowMapTexture
GBufferTexture0
GBufferTexture1
GBufferTexture2
参考サイト様
【UnityShader】Cascade Shadow Maps【3】 #114
はじめに
前回、CSMsのスクリプト部分について解説しました。
今回は、シェーダー部分について解説します。
深度を渡すシェーダー
Shader "CustomShadow/Caster" { SubShader { Tags { "RenderType" = "Opaque" } CGINCLUDE #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; float2 depth:TEXCOORD0; }; uniform float _gShadowBias; v2f vert (appdata_full v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.pos.z += _gShadowBias; o.depth = o.pos.zw; return o; } fixed4 frag (v2f i) : COLOR { // 正規化デバイス空間に float depth = i.depth.x / i.depth.y; // プラットフォーム毎の違いを吸収 // shaderの言語がGLSLならtrue #if defined (SHADER_TARGET_GLSL) // (-1, 1)-->(0, 1) depth = depth * 0.5 + 0.5; // 深度値が反転しているか // DirectX 11, DirectX 12, PS4, Xbox One, Metal: 逆方向 #elif defined (UNITY_REVERSED_Z) //(1, 0)-->(0, 1) depth = 1 - depth; #endif return depth; } ENDCG Pass { Cull front CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } }
影を受け取るシェーダー
Shader "CustomShadow/Receiver" { SubShader { Tags { "RenderType"="Opaque" } Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; float4 worldPos : TEXCOORD0; float eyeZ : TEXCOORD1; }; uniform float4x4 _gWorldToShadow; uniform sampler2D _gShadowMapTexture; uniform float4 _gShadowMapTexture_TexelSize; uniform float4 _gLightSplitsNear; uniform float4 _gLightSplitsFar; uniform float4x4 _gWorld2Shadow[4]; uniform sampler2D _gShadowMapTexture0; uniform sampler2D _gShadowMapTexture1; uniform sampler2D _gShadowMapTexture2; uniform sampler2D _gShadowMapTexture3; uniform float _gShadowStrength; v2f vert(appdata_full v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex); o.eyeZ = o.pos.w; return o; } // Cascadeの重みを色として表示 fixed4 GetCascadeWeights(float z) { fixed4 zNear = float4(z >= _gLightSplitsNear); fixed4 zFar = float4(z < _gLightSplitsFar); // 仮に2番目の視錐台の場合、(0,1,0,0) fixed4 weights = zNear * zFar; return weights; } // 影の計算 float CulcShadow(float4x4 w2Shadow, float4 wPos, fixed cascadeWeight, sampler2D shadowMapTex) { // WVP行列に変換し、0~1の範囲に float4 shadowCoord = mul(w2Shadow, wPos); shadowCoord.xy /= shadowCoord.w; shadowCoord.xy = shadowCoord.xy * 0.5 + 0.5; float4 sampleDepth = tex2D(shadowMapTex, shadowCoord.xy); // 深度 float depth = shadowCoord.z / shadowCoord.w; #if defined (SHADER_TARGET_GLSL) depth = depth * 0.5 + 0.5; #elif defined (UNITY_REVERSED_Z) depth = 1 - depth; #endif float shadow = sampleDepth < depth ? _gShadowStrength : 1; // どの分割した視錐台のものの影かを返す return shadow * cascadeWeight; } // ShadowTextureのサンプリング float4 SampleShadowTexture(float4 wPos, fixed4 cascadeWeights) { float cascadeShadow = CulcShadow(_gWorld2Shadow[0], wPos, cascadeWeights[0], _gShadowMapTexture0) + CulcShadow(_gWorld2Shadow[1], wPos, cascadeWeights[1], _gShadowMapTexture1) + CulcShadow(_gWorld2Shadow[2], wPos, cascadeWeights[2], _gShadowMapTexture2) + CulcShadow(_gWorld2Shadow[3], wPos, cascadeWeights[3], _gShadowMapTexture3); return cascadeShadow;// * cascadeWeights; } fixed4 frag (v2f i) : COLOR0 { fixed4 weights = GetCascadeWeights(i.eyeZ); fixed4 col = SampleShadowTexture(i.worldPos, weights); return col; } #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest ENDCG } } }
解説
深度を渡すシェーダー
頂点シェーダー
v2f vert (appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.pos.z += _gShadowBias;
o.depth = o.pos.zw;
return o;
}
スクリプトから渡してきた_gShadowBias
をクリップ座標のZに加算することでシャドウアクネを防いでいます。
また、このシェーダーは深度を渡すだけですので、ZWのみをdepth
に入れています。
フラグメントシェーダー
fixed4 frag (v2f i) : COLOR { // 正規化デバイス空間に float depth = i.depth.x / i.depth.y; // プラットフォーム毎の違いを吸収 // shaderの言語がGLSLならtrue #if defined (SHADER_TARGET_GLSL) // (-1, 1)-->(0, 1) depth = depth * 0.5 + 0.5; // 深度値が反転しているか // DirectX 11, DirectX 12, PS4, Xbox One, Metal: 逆方向 #elif defined (UNITY_REVERSED_Z) //(1, 0)-->(0, 1) depth = 1 - depth; #endif return depth; }
Z値をWで除算することにより、正規化デバイス空間へと変換しています。
Z値はプラットフォームによって異なるのでその違いを吸収しています。
影を受け取るシェーダー
頂点シェーダー
特に何もしていないので、割愛させて頂きます。
フラグメントシェーダー
fixed4 frag (v2f i) : COLOR0
{
fixed4 weights = GetCascadeWeights(i.eyeZ);
fixed4 col = SampleShadowTexture(i.worldPos, weights);
return col;
}
影の重み
// Cascadeの重みを色として表示 fixed4 GetCascadeWeights(float z) { fixed4 zNear = float4(z >= _gLightSplitsNear); fixed4 zFar = float4(z < _gLightSplitsFar); // 仮に2番目の視錐台の場合、(0,1,0,0) fixed4 weights = zNear * zFar; return weights; }
UnityObjectToClipPos()
で取得したクリップ空間のw成分を引数としてどのシャドウマップを使用するかを決めています。
w成分はクリップ空間においてz成分の逆ベクトルとなっています。
クリップ空間は右手座標系ですので、カメラの前方にあるオブジェクトのz成分が負になっています。
そこでz成分の逆ベクトルであるw成分を使用することによって正の値を求めています。
例えば、カメラのNearが0.3でFarが100のときの各値は以下となります。
Near
Far
そして、w成分が仮に30だとすると
fiexd4 zNear = (1, 1, 1, 0); fiexd4 zFar = (0, 0, 1, 1); // (0, 0, 1, 0) fixed4 weights = zNear * zFar;
となるので3番目のシャドウマップが使われることになります。
参考
https://answers.unity.com/questions/1364663/z-and-w-output-of-unityobjecttoclippos.html
影の計算
// 影の計算 float CulcShadow(float4x4 w2Shadow, float4 wPos, fixed cascadeWeight, sampler2D shadowMapTex) { // WVP行列に変換し、0~1の範囲に float4 shadowCoord = mul(w2Shadow, wPos); shadowCoord.xy /= shadowCoord.w; shadowCoord.xy = shadowCoord.xy * 0.5 + 0.5; float4 sampleDepth = tex2D(shadowMapTex, shadowCoord.xy); // 深度 float depth = shadowCoord.z / shadowCoord.w; #if defined (SHADER_TARGET_GLSL) depth = depth * 0.5 + 0.5; #elif defined (UNITY_REVERSED_Z) depth = 1 - depth; #endif float shadow = sampleDepth < depth ? _gShadowStrength : 1; // どの分割した視錐台のものの影かを返す return shadow * cascadeWeight; }
シャドウマップのサンプリングを行う関数となります。
スクリプト側から渡されたVP行列を元にシャドウマップのサンプリングを行い、重みに応じて描画するかの有無を決めています。
結果
影が描画されれば成功です。
今回は以上となります。
ここまでご視聴ありがとうございました。
【UnityShader】Cascade Shadow Maps【2】 #113
はじめに
前回で、CSMsについて解説したので、今回は実装していきます。
実装
Unity上
Scene上に以下のオブジェクトを配置します。
- カメラ
- ライト
- カメラから近いオブジェクト(Sphere)
- カメラから遠いオブジェクト(Cube)
- Plane
カメラのInspector
カメラに今回制作するスクリプトを刺します。
DirLightとMainCameraは先程のステップで配置したもの、ShadowCasterは影を描画するシェーダーを刺します。
PlaneのInspector
Planeには影を受け取るシェーダーのマテリアルを刺します。
ソースコード
スクリプト
using System.Collections.Generic; using UnityEngine; public class CascadedShadowMapping : MonoBehaviour { [SerializeField] private Light _dirLight; [SerializeField] private Shader _shadowCaster; [SerializeField] private Camera _mainCamera; // 分割数 private const int SplitsNum = 4; private Camera _dirLightCamera; // 影行列 private readonly List<Matrix4x4> _world2ShadowMats = new List<Matrix4x4>(SplitsNum); // 分割したDirectionalLightカメラ private readonly GameObject[] _dirLightCameraSplits = new GameObject[SplitsNum]; private readonly RenderTexture[] _depthTextures = new RenderTexture[SplitsNum]; // ------------------視錐台関連------------------ private float[] _lightSplitsNear; private float[] _lightSplitsFar; // 視錐台構造体 private struct FrustumCorners { public Vector3[] NearCorners; public Vector3[] FarCorners; } private FrustumCorners[] _mainCameraSplitsFcs; private FrustumCorners[] _lightCameraSplitsFcs; // ------------------視錐台関連------------------ private void Awake() { InitFrustumCorners(); _dirLightCamera = CreateDirLightCamera(); CreateRenderTexture(); } /// <summary> /// 視錐台の初期化 /// </summary> private void InitFrustumCorners() { _mainCameraSplitsFcs = new FrustumCorners[SplitsNum]; _lightCameraSplitsFcs = new FrustumCorners[SplitsNum]; for (var i = 0; i < SplitsNum; i++) { _mainCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum]; _mainCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum]; _lightCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum]; _lightCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum]; } } /// <summary> /// DirectionalLightカメラを生成 /// </summary> /// <returns>生成したライト</returns> private Camera CreateDirLightCamera() { var goLightCamera = new GameObject("Directional Light Camera"); var lightCamera = goLightCamera.AddComponent<Camera>(); lightCamera.cullingMask = 1 << LayerMask.NameToLayer("Caster"); lightCamera.backgroundColor = Color.white; lightCamera.clearFlags = CameraClearFlags.SolidColor; lightCamera.orthographic = true; lightCamera.enabled = false; for (var i = 0; i < SplitsNum; i++) { _dirLightCameraSplits[i] = new GameObject("dirLightCameraSplits" + i); } return lightCamera; } /// <summary> /// RenderTextureの生成 /// </summary> private void CreateRenderTexture() { var rtFormat = RenderTextureFormat.ARGB32; if (!SystemInfo.SupportsRenderTextureFormat(rtFormat)) { rtFormat = RenderTextureFormat.Default; } for (var i = 0; i < SplitsNum; i++) { // Stencilに書き込めるようにするので,Depthに24を指定 _depthTextures[i] = new RenderTexture(1024, 1024, 24, rtFormat); Shader.SetGlobalTexture("_gShadowMapTexture" + i, _depthTextures[i]); } } private void Update() { // mainCameraを視錐台用に分割する CalcMainCameraSplitsFrustumCorners(); // Light用Cameraの計算 CalcLightCamera(); if (!_dirLight || !_dirLightCamera) { return; } Shader.SetGlobalFloat("_gShadowBias", 0.005f); Shader.SetGlobalFloat("_gShadowStrength", 0.5f); // 影行列の計算 CalcShadowMats(); Shader.SetGlobalMatrixArray("_gWorld2Shadow", _world2ShadowMats); } /// <summary> /// mainCameraを視錐台用に分割する /// </summary> private void CalcMainCameraSplitsFrustumCorners() { var near = _mainCamera.nearClipPlane; var far = _mainCamera.farClipPlane; // UnityのCascadeSplitsの値 // 0 : 6.7%, 1 : 13.3%, 2 : 26.7%, 4 : 53.5%, float[] nears = { near, far * 0.067f + near, far * 0.133f + far * 0.067f + near, far * 0.267f + far * 0.133f + far * 0.067f + near }; float[] fars = { far * 0.067f + near, far * 0.133f + far * 0.067f + near, far * 0.267f + far * 0.133f + far * 0.067f + near, far }; _lightSplitsNear = nears; _lightSplitsFar = fars; Shader.SetGlobalVector("_gLightSplitsNear", new Vector4(_lightSplitsNear[0], _lightSplitsNear[1], _lightSplitsNear[2], _lightSplitsNear[3])); Shader.SetGlobalVector("_gLightSplitsFar", new Vector4(_lightSplitsFar[0], _lightSplitsFar[1], _lightSplitsFar[2], _lightSplitsFar[3])); // 視錐台Cameraの頂点を計算 for (var k = 0; k < SplitsNum; k++) { // near // ビューポート座標を指定して、指定したカメラ深度の4つの視錐台の頂点を指すビュー空間ベクトルを計算する。 // 第4引数に視錐台ベクトルの配列が出力される _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsNear[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].NearCorners); for (var i = 0; i < SplitsNum; i++) { // 出力されたmainCameraの視錐コーナー座標をワールド座標に変換して、代入 _mainCameraSplitsFcs[k].NearCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]); } // far _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsFar[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].FarCorners); for (var i = 0; i < SplitsNum; i++) { _mainCameraSplitsFcs[k].FarCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]); } } } /// <summary> /// Light用Cameraの計算 /// </summary> private void CalcLightCamera() { if (_dirLightCamera == null) { return; } for (var k = 0; k < SplitsNum; k++) { for (var i = 0; i < SplitsNum; i++) { // mainCameraの視錐コーナー座標をローカルに変換し、視錐台Lightカメラに入れている _lightCameraSplitsFcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]); _lightCameraSplitsFcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]); } // 分割した視錐台の各座標で一番小さいもの、一番大きいものを計算 float[] xs = { _lightCameraSplitsFcs[k].NearCorners[0].x, _lightCameraSplitsFcs[k].NearCorners[1].x, _lightCameraSplitsFcs[k].NearCorners[2].x, _lightCameraSplitsFcs[k].NearCorners[3].x, _lightCameraSplitsFcs[k].FarCorners[0].x, _lightCameraSplitsFcs[k].FarCorners[1].x, _lightCameraSplitsFcs[k].FarCorners[2].x, _lightCameraSplitsFcs[k].FarCorners[3].x }; float[] ys = { _lightCameraSplitsFcs[k].NearCorners[0].y, _lightCameraSplitsFcs[k].NearCorners[1].y, _lightCameraSplitsFcs[k].NearCorners[2].y, _lightCameraSplitsFcs[k].NearCorners[3].y, _lightCameraSplitsFcs[k].FarCorners[0].y, _lightCameraSplitsFcs[k].FarCorners[1].y, _lightCameraSplitsFcs[k].FarCorners[2].y, _lightCameraSplitsFcs[k].FarCorners[3].y }; float[] zs = { _lightCameraSplitsFcs[k].NearCorners[0].z, _lightCameraSplitsFcs[k].NearCorners[1].z, _lightCameraSplitsFcs[k].NearCorners[2].z, _lightCameraSplitsFcs[k].NearCorners[3].z, _lightCameraSplitsFcs[k].FarCorners[0].z, _lightCameraSplitsFcs[k].FarCorners[1].z, _lightCameraSplitsFcs[k].FarCorners[2].z, _lightCameraSplitsFcs[k].FarCorners[3].z }; var minX = Mathf.Min(xs); var maxX = Mathf.Max(xs); var minY = Mathf.Min(ys); var maxY = Mathf.Max(ys); var minZ = Mathf.Min(zs); var maxZ = Mathf.Max(zs); // 視錐台Lightカメラに視錐台の情報を入れる _lightCameraSplitsFcs[k].NearCorners[0] = new Vector3(minX, minY, minZ); _lightCameraSplitsFcs[k].NearCorners[1] = new Vector3(maxX, minY, minZ); _lightCameraSplitsFcs[k].NearCorners[2] = new Vector3(maxX, maxY, minZ); _lightCameraSplitsFcs[k].NearCorners[3] = new Vector3(minX, maxY, minZ); _lightCameraSplitsFcs[k].FarCorners[0] = new Vector3(minX, minY, maxZ); _lightCameraSplitsFcs[k].FarCorners[1] = new Vector3(maxX, minY, maxZ); _lightCameraSplitsFcs[k].FarCorners[2] = new Vector3(maxX, maxY, maxZ); _lightCameraSplitsFcs[k].FarCorners[3] = new Vector3(minX, maxY, maxZ); // near平面の中心点 var pos = _lightCameraSplitsFcs[k].NearCorners[0] + (_lightCameraSplitsFcs[k].NearCorners[2] - _lightCameraSplitsFcs[k].NearCorners[0]) * 0.5f; // ローカルからワールドに変換 _dirLightCameraSplits[k].transform.TransformPoint(pos); _dirLightCameraSplits[k].transform.rotation = _dirLight.transform.rotation; } } /// <summary> /// 影行列の計算 /// </summary> private void CalcShadowMats() { _world2ShadowMats.Clear(); for (var i = 0; i < SplitsNum; i++) { // DirectionalLightカメラの設定 ConstructLightCameraSplits(i); _dirLightCamera.targetTexture = _depthTextures[i]; // shaderの設定でカメラのレンダリングを行う _dirLightCamera.RenderWithShader(_shadowCaster, ""); // カメラでレンダリングを行うので、false var projectionMatrix = GL.GetGPUProjectionMatrix(_dirLightCamera.projectionMatrix, false); // VP行列 _world2ShadowMats.Add(projectionMatrix * _dirLightCamera.worldToCameraMatrix); } } /// <summary> /// DirectionalLightカメラの設定 /// </summary> /// <param name="index">分割したDirectionalLightカメラのindex</param> private void ConstructLightCameraSplits(int index) { var cameraTransform = _dirLightCamera.transform; cameraTransform.position = _dirLightCameraSplits[index].transform.position; cameraTransform.rotation = _dirLightCameraSplits[index].transform.rotation; _dirLightCamera.nearClipPlane = _lightCameraSplitsFcs[index].NearCorners[0].z; _dirLightCamera.farClipPlane = _lightCameraSplitsFcs[index].FarCorners[0].z; _dirLightCamera.aspect = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[0] - _lightCameraSplitsFcs[index].NearCorners[1]) / Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]); _dirLightCamera.orthographicSize = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]) * 0.5f; } /// <summary> /// Gizmoの描画 /// </summary> private void OnDrawGizmos() { if (_dirLightCamera == null) { return; } var fcs = new FrustumCorners[SplitsNum]; for (var k = 0; k < SplitsNum; k++) { // mainCameraから見た、視錐台の分割線 Gizmos.color = Color.white; Gizmos.DrawLine(_mainCameraSplitsFcs[k].NearCorners[1], _mainCameraSplitsFcs[k].NearCorners[2]); fcs[k].NearCorners = new Vector3[SplitsNum]; fcs[k].FarCorners = new Vector3[SplitsNum]; for (var i = 0; i < SplitsNum; i++) { fcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.TransformPoint(_lightCameraSplitsFcs[k].NearCorners[i]); fcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.TransformPoint(_lightCameraSplitsFcs[k].FarCorners[i]); } // 分割したDirectionalLightカメラの視錐台を描画 // ライトのrotateを0にすれば、上のものと一致する Gizmos.color = Color.red; Gizmos.DrawLine(fcs[k].NearCorners[0], fcs[k].NearCorners[1]); Gizmos.DrawLine(fcs[k].NearCorners[1], fcs[k].NearCorners[2]); Gizmos.DrawLine(fcs[k].NearCorners[2], fcs[k].NearCorners[3]); Gizmos.DrawLine(fcs[k].NearCorners[3], fcs[k].NearCorners[0]); Gizmos.color = Color.green; Gizmos.DrawLine(fcs[k].FarCorners[0], fcs[k].FarCorners[1]); Gizmos.DrawLine(fcs[k].FarCorners[1], fcs[k].FarCorners[2]); Gizmos.DrawLine(fcs[k].FarCorners[2], fcs[k].FarCorners[3]); Gizmos.DrawLine(fcs[k].FarCorners[3], fcs[k].FarCorners[0]); Gizmos.DrawLine(fcs[k].NearCorners[0], fcs[k].FarCorners[0]); Gizmos.DrawLine(fcs[k].NearCorners[1], fcs[k].FarCorners[1]); Gizmos.DrawLine(fcs[k].NearCorners[2], fcs[k].FarCorners[2]); Gizmos.DrawLine(fcs[k].NearCorners[3], fcs[k].FarCorners[3]); } } private void OnDestroy() { _dirLightCamera = null; for (var i = 0; i < SplitsNum; i++) { if (_depthTextures[i]) { DestroyImmediate(_depthTextures[i]); } } } }
影を描画するシェーダー
Shader "CustomShadow/Caster" { SubShader { Tags { "RenderType" = "Opaque" } CGINCLUDE #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; float2 depth:TEXCOORD0; }; uniform float _gShadowBias; v2f vert (appdata_full v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.pos.z += _gShadowBias; o.depth = o.pos.zw; return o; } fixed4 frag (v2f i) : COLOR { float depth = i.depth.x / i.depth.y; // プラットフォーム毎の違いを吸収 // shaderの言語がGLSLならtrue #if defined (SHADER_TARGET_GLSL) // (-1, 1)-->(0, 1) depth = depth * 0.5 + 0.5; // 深度値が反転しているか // DirectX 11, DirectX 12, PS4, Xbox One, Metal: 逆方向 #elif defined (UNITY_REVERSED_Z) //(1, 0)-->(0, 1) depth = 1 - depth; #endif return depth; } ENDCG Pass { Cull front CGPROGRAM #pragma vertex vert #pragma fragment frag ENDCG } } }
影を受け取るシェーダー
Shader "CustomShadow/Receiver" { SubShader { Tags { "RenderType"="Opaque" } Pass { Tags{ "LightMode" = "ForwardBase" } CGPROGRAM #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; float4 worldPos : TEXCOORD3; float eyeZ : TEXCOORD2; }; uniform float4x4 _gWorldToShadow; uniform sampler2D _gShadowMapTexture; uniform float4 _gShadowMapTexture_TexelSize; uniform float4 _gLightSplitsNear; uniform float4 _gLightSplitsFar; uniform float4x4 _gWorld2Shadow[4]; uniform sampler2D _gShadowMapTexture0; uniform sampler2D _gShadowMapTexture1; uniform sampler2D _gShadowMapTexture2; uniform sampler2D _gShadowMapTexture3; uniform float _gShadowStrength; v2f vert(appdata_full v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex); o.eyeZ = o.pos.w; return o; } // Cascadeの重みを色として表示 fixed4 GetCascadeWeights(float z) { fixed4 zNear = float4(z >= _gLightSplitsNear); fixed4 zFar = float4(z < _gLightSplitsFar); // 仮に2番目の視錐台の場合、(0,1,0,0) fixed4 weights = zNear * zFar; return weights; } // 影の計算 float CulcShadow(float4x4 w2Shadow, float4 wPos, fixed cascadeWeight, sampler2D shadowMapTex) { // WVP行列に変換し、0~1の範囲に float4 shadowCoord = mul(w2Shadow, wPos); shadowCoord.xy /= shadowCoord.w; shadowCoord.xy = shadowCoord.xy * 0.5 + 0.5; float4 sampleDepth = tex2D(shadowMapTex, shadowCoord.xy); // 深度 float depth = shadowCoord.z / shadowCoord.w; #if defined (SHADER_TARGET_GLSL) depth = depth * 0.5 + 0.5; #elif defined (UNITY_REVERSED_Z) depth = 1 - depth; #endif float shadow = sampleDepth < depth ? _gShadowStrength : 1; // どの分割した視錐台のものの影かを返す return shadow * cascadeWeight; } // ShadowTextureのサンプリング float4 SampleShadowTexture(float4 wPos, fixed4 cascadeWeights) { float cascadeShadow = CulcShadow(_gWorld2Shadow[0], wPos, cascadeWeights[0], _gShadowMapTexture0) + CulcShadow(_gWorld2Shadow[1], wPos, cascadeWeights[1], _gShadowMapTexture1) + CulcShadow(_gWorld2Shadow[2], wPos, cascadeWeights[2], _gShadowMapTexture2) + CulcShadow(_gWorld2Shadow[3], wPos, cascadeWeights[3], _gShadowMapTexture3); return cascadeShadow * cascadeWeights; } fixed4 frag (v2f i) : COLOR0 { fixed4 weights = GetCascadeWeights(i.eyeZ); fixed4 col = SampleShadowTexture(i.worldPos, weights); return col; } #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest ENDCG } } }
解説
初期化
視錐台
private void InitFrustumCorners() { _mainCameraSplitsFcs = new FrustumCorners[SplitsNum]; _lightCameraSplitsFcs = new FrustumCorners[SplitsNum]; for (var i = 0; i < SplitsNum; i++) { _mainCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum]; _mainCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum]; _lightCameraSplitsFcs[i].NearCorners = new Vector3[SplitsNum]; _lightCameraSplitsFcs[i].FarCorners = new Vector3[SplitsNum]; } }
複数の視錐台に分割するための配列の初期化を行っています。
DirectionalLightカメラ
private Camera CreateDirLightCamera() { var goLightCamera = new GameObject("Directional Light Camera"); var lightCamera = goLightCamera.AddComponent<Camera>(); lightCamera.cullingMask = 1 << LayerMask.NameToLayer("Caster"); lightCamera.backgroundColor = Color.white; lightCamera.clearFlags = CameraClearFlags.SolidColor; lightCamera.orthographic = true; lightCamera.enabled = false; for (var i = 0; i < SplitsNum; i++) { _dirLightCameraSplits[i] = new GameObject("dirLightCameraSplits" + i); } return lightCamera; }
DirectionalLight用のカメラと、視錐台用のDirectionalLightカメラを生成しています。
Update
MainCameraを4分割した、視錐台の計算
private void CalcMainCameraSplitsFrustumCorners() { var near = _mainCamera.nearClipPlane; var far = _mainCamera.farClipPlane; // UnityのCascadeSplitsの値 // 0 : 6.7%, 1 : 13.3%, 2 : 26.7%, 4 : 53.5%, float[] nears = { near, far * 0.067f + near, far * 0.133f + far * 0.067f + near, far * 0.267f + far * 0.133f + far * 0.067f + near }; float[] fars = { far * 0.067f + near, far * 0.133f + far * 0.067f + near, far * 0.267f + far * 0.133f + far * 0.067f + near, far }; _lightSplitsNear = nears; _lightSplitsFar = fars; Shader.SetGlobalVector("_gLightSplitsNear", new Vector4(_lightSplitsNear[0], _lightSplitsNear[1], _lightSplitsNear[2], _lightSplitsNear[3])); Shader.SetGlobalVector("_gLightSplitsFar", new Vector4(_lightSplitsFar[0], _lightSplitsFar[1], _lightSplitsFar[2], _lightSplitsFar[3])); // 視錐台Cameraの頂点を計算 for (var k = 0; k < SplitsNum; k++) { // near // ビューポート座標を指定して、指定したカメラ深度の4つの錐台コーナーを指すビュー空間ベクトルを計算する。 // 第4引数に視錐台ベクトルの配列が出力される _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsNear[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].NearCorners); for (var i = 0; i < SplitsNum; i++) { // 出力されたmainCameraの視錐コーナー座標をワールド座標に変換して、代入 _mainCameraSplitsFcs[k].NearCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]); } // far _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsFar[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].FarCorners); for (var i = 0; i < SplitsNum; i++) { _mainCameraSplitsFcs[k].FarCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]); } } }
各視錐台のnearとfar
float[] nears = { near, far * 0.067f + near, far * 0.133f + far * 0.067f + near, far * 0.267f + far * 0.133f + far * 0.067f + near }; float[] fars = { far * 0.067f + near, far * 0.133f + far * 0.067f + near, far * 0.267f + far * 0.133f + far * 0.067f + near, far };
マジックナンバーは、CascadeSplitsの値となっています。
これにより、各視錐台のnearとfarを計算しています。
実際の視錐台の値は以下の表となります。
※MainCameraのnearが0.3、farが100の場合
\ | 1番目 | 2番目 | 3番目 | 4番目 |
---|---|---|---|---|
near | 0.3 | 7 | 20.3 | 47 |
far | 7 | 20.3 | 47 | 100 |
表を見ても分かる通り、各視錐台にMainCameraのnearとfarを分割した値を割り当てられています。
各視錐台の各頂点の計算
// 視錐台Cameraの頂点を計算 for (var k = 0; k < SplitsNum; k++) { // near // ビューポート座標を指定して、指定したカメラ深度の4つの錐台コーナーを指すビュー空間ベクトルを計算する。 // 第4引数に視錐台ベクトルの配列が出力される _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsNear[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].NearCorners); for (var i = 0; i < SplitsNum; i++) { // 出力されたmainCameraの視錐コーナー座標をワールド座標に変換して、代入 _mainCameraSplitsFcs[k].NearCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]); } // far _mainCamera.CalculateFrustumCorners(new Rect(0, 0, 1, 1), _lightSplitsFar[k], Camera.MonoOrStereoscopicEye.Mono, _mainCameraSplitsFcs[k].FarCorners); for (var i = 0; i < SplitsNum; i++) { _mainCameraSplitsFcs[k].FarCorners[i] = _mainCamera.transform.TransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]); } }
CalculateFrustumCornersはビューポート座標を指定して、指定した深度の視錐台の4つの頂点を取得するものになります。
この座標はローカル座標で返ってくるので、TransformPointを使用してワールド座標へと変換します。
CalculateFrustumCorners
/// <summary> /// ビューポート座標を指定して、指定したカメラ深度の4つの視錐台の頂点を指すビュー空間ベクトルを計算する。 /// </summary> /// <param name="viewport">視錐台の計算に使用する正規化されたビューポート座標</param> /// <param name="z">カメラの原点からの深度</param> /// <param name="eye">カメラアイ</param> /// <param name="outCorners">視錐台の頂点ベクトル出力配列, Lengthは4以上</param> public void CalculateFrustumCorners( Rect viewport, float z, Camera.MonoOrStereoscopicEye eye, Vector3[] outCorners)
Light用Cameraの計算
/// <summary> /// Light用Cameraの計算 /// </summary> private void CalcLightCamera() { if (_dirLightCamera == null) { return; } for (var k = 0; k < SplitsNum; k++) { for (var i = 0; i < SplitsNum; i++) { // mainCameraの視錐コーナー座標をローカルに変換し、視錐台Lightカメラに入れている _lightCameraSplitsFcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]); _lightCameraSplitsFcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]); } // 分割した視錐台の各座標で一番小さいもの、一番大きいものを計算 float[] xs = { _lightCameraSplitsFcs[k].NearCorners[0].x, _lightCameraSplitsFcs[k].NearCorners[1].x, _lightCameraSplitsFcs[k].NearCorners[2].x, _lightCameraSplitsFcs[k].NearCorners[3].x, _lightCameraSplitsFcs[k].FarCorners[0].x, _lightCameraSplitsFcs[k].FarCorners[1].x, _lightCameraSplitsFcs[k].FarCorners[2].x, _lightCameraSplitsFcs[k].FarCorners[3].x }; float[] ys = { _lightCameraSplitsFcs[k].NearCorners[0].y, _lightCameraSplitsFcs[k].NearCorners[1].y, _lightCameraSplitsFcs[k].NearCorners[2].y, _lightCameraSplitsFcs[k].NearCorners[3].y, _lightCameraSplitsFcs[k].FarCorners[0].y, _lightCameraSplitsFcs[k].FarCorners[1].y, _lightCameraSplitsFcs[k].FarCorners[2].y, _lightCameraSplitsFcs[k].FarCorners[3].y }; float[] zs = { _lightCameraSplitsFcs[k].NearCorners[0].z, _lightCameraSplitsFcs[k].NearCorners[1].z, _lightCameraSplitsFcs[k].NearCorners[2].z, _lightCameraSplitsFcs[k].NearCorners[3].z, _lightCameraSplitsFcs[k].FarCorners[0].z, _lightCameraSplitsFcs[k].FarCorners[1].z, _lightCameraSplitsFcs[k].FarCorners[2].z, _lightCameraSplitsFcs[k].FarCorners[3].z }; var minX = Mathf.Min(xs); var maxX = Mathf.Max(xs); var minY = Mathf.Min(ys); var maxY = Mathf.Max(ys); var minZ = Mathf.Min(zs); var maxZ = Mathf.Max(zs); // 視錐台Lightカメラに視錐台の情報を入れる _lightCameraSplitsFcs[k].NearCorners[0] = new Vector3(minX, minY, minZ); _lightCameraSplitsFcs[k].NearCorners[1] = new Vector3(maxX, minY, minZ); _lightCameraSplitsFcs[k].NearCorners[2] = new Vector3(maxX, maxY, minZ); _lightCameraSplitsFcs[k].NearCorners[3] = new Vector3(minX, maxY, minZ); _lightCameraSplitsFcs[k].FarCorners[0] = new Vector3(minX, minY, maxZ); _lightCameraSplitsFcs[k].FarCorners[1] = new Vector3(maxX, minY, maxZ); _lightCameraSplitsFcs[k].FarCorners[2] = new Vector3(maxX, maxY, maxZ); _lightCameraSplitsFcs[k].FarCorners[3] = new Vector3(minX, maxY, maxZ); // near平面の中心点 var pos = _lightCameraSplitsFcs[k].NearCorners[0] + (_lightCameraSplitsFcs[k].NearCorners[2] - _lightCameraSplitsFcs[k].NearCorners[0]) * 0.5f; // ローカルからワールドに変換 _dirLightCameraSplits[k].transform.TransformPoint(pos); _dirLightCameraSplits[k].transform.rotation = _dirLight.transform.rotation; } }
MainCameraを分割し各視錐台を計算しました。
その視錐台をもとに、対応したLightカメラを分割した視錐台を計算します。
各視錐台のnearとfarを計算
for (var i = 0; i < SplitsNum; i++) { // mainCameraの視錐コーナー座標をローカルに変換し、視錐台Lightカメラに入れている _lightCameraSplitsFcs[k].NearCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].NearCorners[i]); _lightCameraSplitsFcs[k].FarCorners[i] = _dirLightCameraSplits[k].transform.InverseTransformPoint(_mainCameraSplitsFcs[k].FarCorners[i]); }
MainCamera視錐台にはワールド座標に変換したnearとfarの値があるので、DirectionalLightカメラのtransformでローカル座標へと変換し、DirectionalLightカメラ視錐台のnearとfarとします。
計算結果をDirectionalLightカメラに渡す
// near平面の中心点 var pos = _lightCameraSplitsFcs[k].NearCorners[0] + (_lightCameraSplitsFcs[k].NearCorners[2] - _lightCameraSplitsFcs[k].NearCorners[0]) * 0.5f; // ローカルからワールドに変換 _dirLightCameraSplits[k].transform.TransformPoint(pos); _dirLightCameraSplits[k].transform.rotation = _dirLight.transform.rotation;
各Lightカメラで計算したnear座標の中心を元に、DirectionalLightカメラに渡しています。
最初の計算のときにローカル座標に変換しているので、ワールド座標に戻すために再び変換する必要があります。
影の行列の計算
/// <summary> /// 影行列の計算 /// </summary> private void CalcShadowMats() { _world2ShadowMats.Clear(); for (var i = 0; i < SplitsNum; i++) { // DirectionalLightカメラの設定 ConstructLightCameraSplits(i); _dirLightCamera.targetTexture = _depthTextures[i]; // shaderの設定でカメラのレンダリングを行う _dirLightCamera.RenderWithShader(_shadowCaster, ""); // カメラでレンダリングを行うので、false var projectionMatrix = GL.GetGPUProjectionMatrix(_dirLightCamera.projectionMatrix, false); // VP行列 _world2ShadowMats.Add(projectionMatrix * _dirLightCamera.worldToCameraMatrix); } }
DirectionalLightカメラの設定
/// <summary> /// DirectionalLightカメラの設定 /// </summary> /// <param name="index">分割したDirectionalLightカメラのindex</param> private void ConstructLightCameraSplits(int index) { var cameraTransform = _dirLightCamera.transform; cameraTransform.position = _dirLightCameraSplits[index].transform.position; cameraTransform.rotation = _dirLightCameraSplits[index].transform.rotation; _dirLightCamera.nearClipPlane = _lightCameraSplitsFcs[index].NearCorners[0].z; _dirLightCamera.farClipPlane = _lightCameraSplitsFcs[index].FarCorners[0].z; _dirLightCamera.aspect = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[0] - _lightCameraSplitsFcs[index].NearCorners[1]) / Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]); _dirLightCamera.orthographicSize = Vector3.Magnitude(_lightCameraSplitsFcs[index].NearCorners[1] - _lightCameraSplitsFcs[index].NearCorners[2]) * 0.5f; }
CalcLightCamera()
で計算した、分割DirectionalLightCameraを元に、大元のirectionalLightCameraに情報を渡しています。
アスペクト比は、nearの視錐台から縦横のベクトルの長さを取得することで計算しています。
正投影時のサイズも、同様にnearの視錐台から計算しています。
orthographic モードの場合、カメラの半分のサイズ。 https://docs.unity3d.com/ja/560/ScriptReference/Camera-orthographicSize.html
とあるので、半分にしています。
行列の計算
// shaderの設定でカメラのレンダリングを行う _dirLightCamera.RenderWithShader(_shadowCaster, ""); // カメラでレンダリングを行うので、false var projectionMatrix = GL.GetGPUProjectionMatrix(_dirLightCamera.projectionMatrix, false); // VP行列 _world2ShadowMats.Add(projectionMatrix * _dirLightCamera.worldToCameraMatrix);
RenderWithShader
レンダリングに使うシェーダーを差し替え、そのシェーダーでレンダリングを行う関数になります。
差し替えだけですと、SetReplacementShader()
というメソッドもあります。
GL.GetGPUProjectionMatrix
Projection行列のプラットフォーム毎の違いを吸収してくれるメソッドとなります。
この変換を正規化デバイス座標と呼びます。
最後に、Projection行列とDirectionalLightCameraのView行列を計算して終了となります。
今回は以上となります。
ここまでご視聴ありがとうございました。
参考サイト様
【UnityShader】Cascade Shadow Maps【1】 #112
Cascade Shadow Mapsとは
カメラに近い位置の影はジャギーが目立って表示されています。
これを解決する手法の1つがCascade Shadow Maps(CSMs)になります。
ジャギーが目立ってしまっている例
奥の立方体は影のジャギーが特に目立たないのに対して、
手前の球は影のジャギーが目立っています。
発生してしまう原因
このジャギーが発生してしまう原因は、シャドウマップの各領域がカメラの透視によって想定よりも拡大されているからになります。
以下の画像を見て頂けるとわかりやすいです。
視錐の遠い方の端は20ピクセルのシャドウマップに対し、
視錐の近い方の端は4ピクセルのシャドウマップになっています。
画面上では、どちらの端も同じサイズで表示されるので、近い方はシャドウマップの解像度が低くなってしまうのでジャギーが発生します。
考え方
CSMsの考え方は、以下の手順になります。
この手順を踏むことにより手前の影は詳細なものを、奥の影は広域にシャドウマッピングすることができます。
この時、シャドウマップの解像度はすべて同じで手前のものほどサイズが小さくなっています。
CSMsでのシャドウマップイメージ
ソースコード無しでの実装
オブジェクトの配置
Scene上に以下のオブジェクトを配置します。
- カメラ
- ライト
- カメラから近いオブジェクト(Sphere)
- カメラから遠いオブジェクト(Cube)
- Plane
ライトの設定
ジャギーを目立たせやすくするために、ShadowTypeをHardShadows
にします。
影をレンダリングするカメラの距離を伸ばす
Project Settingsを開き、Quality
を選びます。
Shadow Distanceの数値を250にします。
下のShadow CascadesをNo Cascades
にします。
設定後
GameViewに戻ると、手前のオブジェクトの影が荒くなっています。
Cascadeの追加
Project Settingsに戻り、Shadow CascadesをFour Cascades
にします。
Cascade後
GameViewに戻ると、手前のオブジェクトの影のジャギーが減っています。
解説
Shadow CascadesをFour Cascadesにしたことにより、
Cascadeを4つにしているので影がジャギーなく描画できています。
各設定の詳細
Shadow Distance
カメラからどのくらい離れたところまで影を計算するかの値になります。
Shadow Cascades
Cascadeを用意する数を決めます。
問題点
Unityの公式に以下のように記載されています。
モバイルプラットホームでは、シャドウカスケードをサポートしていない
ですので、モバイルの場合は自作する必要があります。
今回は以上となります。
ご視聴ありがとうございました。
参考サイト様
【UnityShader】オブジェクトに軌跡をつける #111
はじめに
UnityのTrailRendererに似た、オブジェクトに追従する軌跡を制作していきます。
実装
Unity上
Scene上に適当なオブジェクトを配置します。
オブジェクト
x座標を少しズラしてください。
また、今回制作するマテリアルを刺してください。
マテリアル
マテリアルには適当なノイズ用テクスチャを刺します。
スクリプト
using UnityEngine; public class Afterimage : MonoBehaviour { [SerializeField] private Material _material; [SerializeField] private float _trailSpeed = 10f; private Vector3 _trailPos; private int _dirId; private void Awake() { _trailPos = transform.position; _dirId = Shader.PropertyToID("_TrailDir"); } private void Update() { Trail(); Rotate(); } /// <summary> /// 原点を中心に回転させる /// </summary> private void Rotate() { var tr = transform; var angleAxis = Quaternion.AngleAxis(180 * Time.deltaTime, Vector3.forward); var pos = tr.position; tr.position = angleAxis * pos; } /// <summary> /// 軌跡 /// </summary> private void Trail() { var time = Mathf.Clamp01(Time.deltaTime * _trailSpeed); var tr = transform.position; _trailPos = Vector3.Lerp(_trailPos, tr, time); _material.SetVector(_dirId, transform.InverseTransformDirection(_trailPos - tr)); } }
シェーダー
Shader "Unlit/Afterimage" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} _NoiseTex ("Noise", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; float4 normal : NORMAL; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _NoiseTex; float4 _NoiseTex_ST; fixed4 _TrailDir; v2f vert (appdata v) { v2f o; fixed weight = saturate((dot(v.normal, _TrailDir))); fixed noise = tex2Dlod(_NoiseTex, float4(v.uv.xy, 0, 0)).r; fixed4 trail = _TrailDir * weight * noise; v.vertex.xyz = float3(v.vertex.x + trail.x, v.vertex.y + trail.y, v.vertex.z + trail.z); o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); return col; } ENDCG } } }
解説
Trail()
軌跡用に頂点をどのくらい伸ばすかをシェーダー側に渡しているメソッドになります。
private void Trail() { var time = Mathf.Clamp01(Time.deltaTime * _trailSpeed); var tr = transform.position; _trailPos = Vector3.Lerp(_trailPos, tr, time); _material.SetVector(_dirId, transform.InverseTransformDirection(_trailPos - tr)); }
InverseTransformDirection
引数に入れた方向ベクトルを、ワールド空間からローカル空間へと変更してくれます。
方向ではなく、位置の場合にはTransform.InverseTransformPoint
を使用します。
バーテックスシェーダー
頂点を伸ばしている箇所になります。
fixed weight = saturate((dot(v.normal, _TrailDir))); fixed noise = tex2Dlod(_NoiseTex, float4(v.uv.xy, 0, 0)).r; fixed4 trail = _TrailDir * weight * noise; v.vertex.xyz = float3(v.vertex.x + trail.x, v.vertex.y + trail.y, v.vertex.z + trail.z);
weight
自身の法線方向と、スクリプトから渡された方向ベクトルの内積を取得することで、重みを出しています。
今回は、角度差が小さいほど重みが大きくなります。
tex2Dlod
バーテックスシェーダー版のtex2D()
になります。
定義は以下となります。
tex2Dlod(テクスチャ, float4(u値, v値, 0, lodの値の指定(0 ~ 7)))
tex2Dlodの詳しい解説
テクスチャのlodの値がDepth値(カメラからの距離)によって決定されます。
Depth値が計算されるのはバーテックスシェーダーの処理が終わり、フラグメントシェーダーに値を渡すときになります。
ですので、バーテックスシェーダー内ではlodの値が自動で割り当てられないので、tex2Dlod()
を使用して明示的にlod値を指定して上げる必要があります。
MipMap等でも同様の処理が必要になります。
結果
オブジェクトに軌跡が追従しています。
参考サイト様
【UnityShader】投影テクスチャシャドウ #110
投影テクスチャシャドウとは
前回行った投影テクスチャマッピングの応用技術となります。
影用のカメラを用意し、光源と同じ方向に設置します。
そして、ターゲットとなるオブジェクトの影だけを描画しカメラのtergetTextureとします。
そうすることにより、影のみ描画することができます。
実装
Unity上
Scene上に以下のオブジェクトを配置します。
- 影用のカメラ
- 影を落とすオブジェクト
- 影を受けるオブジェクト
影用のカメラのInspector
オブジェクト
影を落とすオブジェクトには、何もしなくて良いです。
影を受けるオブジェクトには、今回制作したマテリアルを刺してください。
Scene
スクリプト
using UnityEngine; [RequireComponent(typeof(Camera))] public class ProjectionTextureShadow : MonoBehaviour { [Header ("Setting")] [SerializeField] private Camera _camera; [SerializeField] private int _renderTextureSize = 512; [SerializeField] private Material _material; [SerializeField] private Transform _lightTransform; private int _matrixVpId; private int _textureId; private int _posId; private RenderTexture _renderTexture; private void Start() { SetPropertyId(); CameraSettings(); } /// <summary> /// PropertyIdの設定 /// </summary> private void SetPropertyId() { _matrixVpId = Shader.PropertyToID("_ShadowProjectorMatrixVP1"); _textureId = Shader.PropertyToID("_ShadowProjectorTexture1"); _posId = Shader.PropertyToID("_ShadowProjectorPos1"); } /// <summary> /// カメラの設定 /// </summary> private void CameraSettings() { // 先にレンダリングさせるために、小さい値に _camera.depth = -10000; _camera.clearFlags = CameraClearFlags.Color; _camera.backgroundColor = Color.white; // 点滅を防ぐ _camera.allowHDR = false; } /// <summary> /// カメラがシーンのレンダリングを開始する前に呼ばれる /// </summary> private void OnPreRender() { if (_renderTexture == null) { UpdateSettings(); } SetMaterialParam(); } /// <summary> /// 設定の更新 /// </summary> private void UpdateSettings() { ReleaseTexture(); SetLightPosition(); UpdateRenderTexture(); } /// <summary> /// テクスチャの解放 /// </summary> private void ReleaseTexture() { if (_renderTexture == null) { return; } _material.SetTexture(_textureId, null); RenderTexture.ReleaseTemporary(_renderTexture); _renderTexture = null; _camera.targetTexture = null; } /// <summary> /// 位置の設定 /// </summary> private void SetLightPosition() { var objTransform = transform; _lightTransform.position = objTransform.position; _lightTransform.rotation = objTransform.rotation; } /// <summary> /// RenderTextureの更新 /// </summary> private void UpdateRenderTexture() { _renderTexture = RenderTexture.GetTemporary(_renderTextureSize, _renderTextureSize, 16, RenderTextureFormat.ARGB32); _camera.targetTexture = _renderTexture; } /// <summary> /// マテリアルのパラメータ設定 /// </summary> private void SetMaterialParam() { var viewMatrix = _camera.worldToCameraMatrix; var projectionMatrix = GL.GetGPUProjectionMatrix(_camera.projectionMatrix, true); _material.SetMatrix(_matrixVpId, projectionMatrix * viewMatrix); _material.SetTexture(_textureId, _renderTexture); _material.SetVector(_posId, GetProjectorPos()); } /// <summary> /// プロジェクターの座標取得 /// </summary> /// <returns></returns> private Vector4 GetProjectorPos() { Vector4 projectorPos; // Orthographic // _ObjectSpaceLightPosを参考に、wに0が入っていたらOrthographicの前方方向とみなす if (_camera.orthographic) { projectorPos = transform.forward; projectorPos.w = 0; } // Perspective else { projectorPos = transform.position; projectorPos.w = 1; } return projectorPos; } private void OnDestroy() { ReleaseTexture(); } }
シェーダー
Shader "Unlit/ProjectionTextureShadow" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float4 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float4 projectorSpacePos : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; float2 uv : TEXCOORD3; }; sampler2D _ShadowProjectorTexture1; float4x4 _ShadowProjectorMatrixVP1; float4 _ShadowProjectorPos1; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.projectorSpacePos = ComputeScreenPos(mul(mul(_ShadowProjectorMatrixVP1, unity_ObjectToWorld), v.vertex)); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { // Projection座標に変換 // tex2DProj(_ProjectorTexture, i.projectorSpacePos)と同義 i.projectorSpacePos.xyz /= i.projectorSpacePos.w; float4 projectorTex = tex2D(_ShadowProjectorTexture1, i.projectorSpacePos.xy); // カメラの範囲外(0 ~ 1)は描画しない fixed3 isOut = step((i.projectorSpacePos - 0.5) * sign(i.projectorSpacePos), 0.5); float alpha = isOut.x * isOut.y * isOut.z; // プロジェクターから見て、裏側の面には描画しない // Perspective : _ShadowProjectorPos.xyz - i.worldPos // Orthographic : -_ShadowProjectorPos.xyz alpha *= step(-dot(lerp(-_ShadowProjectorPos1.xyz, _ShadowProjectorPos1.xyz - i.worldPos, _ShadowProjectorPos1.w), i.worldNormal), 0); fixed4 col = tex2D(_MainTex, i.uv); return lerp(1, 1 - projectorTex.a, alpha) * col; } ENDCG } } }
解説
基本は前回のものと同じですので、違う箇所のみ解説します。
CameraSettings()
カメラの初期設定を行っているメソッドになります。
private void CameraSettings() { // 先にレンダリングさせるために、小さい値に _camera.depth = -10000; _camera.clearFlags = CameraClearFlags.Color; _camera.backgroundColor = Color.white; // 点滅を防ぐ _camera.allowHDR = false; }
depth
depthは深度という意味ですが、カメラですと描画順で使用されています。
今回複数のカメラがある可能性を考慮して低い値にしています。
allowHDR
HDRを無効にしています。
HDRを有効にしていると、Zファイティングのようなものが起きてしまいます。
原因は調べても不明でしたので、詳しい方がいらっしゃれば、教えてくださると幸いです。
HDRに関しては以下で解説しています。
UpdateRenderTexture()
RenderTextureの更新を行っているメソッドになります。
private void UpdateRenderTexture() { _renderTexture = RenderTexture.GetTemporary(_renderTextureSize, _renderTextureSize, 16, RenderTextureFormat.ARGB32); _camera.targetTexture = _renderTexture; }
今回、シャドウマップとしてRenderTextureを生成するので第三引数のdepthBuffer
に値を入れています。
フラグメントシェーダー
最後のreturn箇所になります。
return lerp(1, 1 - projectorTex.a, alpha) * col;
alphaはカメラの投影手法によって切り替わるため0or1になります。
ですのでlerpで何もなしかRenderTextureを描画するかを決めています。
結果
影を受けるオブジェクトに、影が落ちているのがわかります。
また、Camera Previewを見るとシャドウマップに書き込まれている内容がわかります。