【UnityShader】投影テクスチャマッピング #109
前回の成果
オブジェクトの影を受け取った。
今回やること
投影テクスチャマッピングを実装します。
事前準備
Scene上にGameObjectを配置し、今回制作したスクリプトをアタッチします。
スクリーンとなるオブジェクトには、Materialをアタッチします。
ソースコード
スクリプト側
using UnityEngine; public class TextureProjector : MonoBehaviour { [SerializeField, Range(0.0001f, 180)] private float _fieldOfView = 60; [SerializeField, Range(0.2f, 5.0f)] private float _aspect = 1.0f; [SerializeField, Range(0.0001f, 1000.0f)] private float _nearClipPlane = 0.01f; [SerializeField, Range(0.0001f, 1000.0f)] private float _farClipPlane = 100.0f; [SerializeField] private ProjectionType projectionType; [SerializeField] private float _orthographicSize = 1.0f; [SerializeField] private Texture2D _texture; [SerializeField] private Material _material; private enum ProjectionType { PERSPECTIVE, ORTHOGRAPHIC, } private int _matrixVpId; private int _textureId; private int _posId; private void Awake() { SetPropertyId(); } /// <summary> /// PropertyIdの設定 /// </summary> private void SetPropertyId() { _matrixVpId = Shader.PropertyToID("_ProjectorMatrixVP"); _textureId = Shader.PropertyToID("_ProjectorTexture"); _posId = Shader.PropertyToID("_ProjectorPos"); } private void Update() { SetMaterialParam(); } /// <summary> /// マテリアルのパラメータ設定 /// </summary> private void SetMaterialParam() { if (_texture == null) { Debug.LogError("Not Setting Material."); return; } var viewMatrix = Matrix4x4.Scale(new Vector3(1, 1, -1)) * transform.worldToLocalMatrix; var projectionMatrix = GetProjectionMatrix(); _material.SetMatrix(_matrixVpId, projectionMatrix * viewMatrix); _material.SetTexture(_textureId, _texture); // プロジェクターの位置を渡す // _ObjectSpaceLightPosのような感じでwに0が入っていたらOrthographicの前方方向とみなす _material.SetVector(_posId, GetProjectorPos()); } /// <summary> /// Projection行列の取得 /// </summary> /// <returns></returns> private Matrix4x4 GetProjectionMatrix() { Matrix4x4 projectionMatrix; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectionMatrix = Matrix4x4.Perspective(_fieldOfView, _aspect, _nearClipPlane, _farClipPlane); break; case ProjectionType.ORTHOGRAPHIC : var orthographicWidth = _orthographicSize * _aspect; projectionMatrix = Matrix4x4.Ortho(-orthographicWidth, orthographicWidth, -_orthographicSize, _orthographicSize, _nearClipPlane, _farClipPlane); break; default : Debug.LogError("Not Expected CameraType"); return Matrix4x4.identity; } return GL.GetGPUProjectionMatrix(projectionMatrix, true); } /// <summary> /// プロジェクターの座標取得 /// </summary> /// <returns></returns> private Vector4 GetProjectorPos() { Vector4 projectorPos; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectorPos = transform.position; projectorPos.w = 1; break; case ProjectionType.ORTHOGRAPHIC : projectorPos = transform.forward; projectorPos.w = 0; break; default : Debug.LogError("Not Expected CameraType"); return Vector4.zero; } return projectorPos; } private void OnDrawGizmos() { var gizmosMatrix = Gizmos.matrix; Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one); switch (projectionType) { case ProjectionType.ORTHOGRAPHIC : var orthographicWidth = _orthographicSize * _aspect; var length = _farClipPlane - _nearClipPlane; var start = _nearClipPlane + length / 2; Gizmos.DrawWireCube(Vector3.forward * start, new Vector3(orthographicWidth * 2, _orthographicSize * 2, length)); break; case ProjectionType.PERSPECTIVE : Gizmos.DrawFrustum(Vector3.zero, _fieldOfView, _farClipPlane, _nearClipPlane, _aspect); break; default : Debug.LogError("Not Expected CameraType"); break; } Gizmos.matrix = gizmosMatrix; } }
シェーダー側
Shader "Unlit/TextureProjecor" { SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float4 normal : NORMAL; }; struct v2f { float4 vertex : SV_POSITION; float4 projectorSpacePos : TEXCOORD0; float3 worldPos : TEXCOORD1; float3 worldNormal : TEXCOORD2; }; sampler2D _ProjectorTexture; float4x4 _ProjectorMatrixVP; float4 _ProjectorPos; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.projectorSpacePos = ComputeScreenPos(mul(mul(_ProjectorMatrixVP, unity_ObjectToWorld), v.vertex)); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // Projection座標に変換 // tex2DProj(_ProjectorTexture, i.projectorSpacePos)と同義 i.projectorSpacePos.xyz /= i.projectorSpacePos.w; float4 projectorTex = tex2D(_ProjectorTexture, 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 : _ProjectorPos.xyz - i.worldPos // Orthographic : -_ProjectorPos.xyz alpha *= step(-dot(lerp(-_ProjectorPos.xyz, _ProjectorPos.xyz - i.worldPos, _ProjectorPos.w), i.worldNormal), 0); return projectorTex * alpha; } ENDCG } } }
投影テクスチャマッピング
テクスチャをプロジェクターで写したかのように描画する技術のことです。
射影テクスチャマッピングとも呼ばれます。
ProjectionType
カメラがシーンを投影する際に、どのような方法で投影するかを決めるものになります。
投影の手法は2つ存在します。
透視投影(perspective)
近くのオブジェクトを大きく、遠くのオブジェクトを小さく描画する手法になります。
主に3Dゲームで用いられる手法になります。
正投影(orthographic)
オブジェクトの距離では大きさを変えない投影手法になります。
主に2Dゲームで用いられる手法になります。
View行列の作成
var viewMatrix = Matrix4x4.Scale(new Vector3(1, 1, -1)) * transform.worldToLocalMatrix;
今回はこのスクリプトをアタッチしたオブジェクトをカメラのように扱います。
transform.worldToLocalMatrix
は、ワールド座標からローカル座標へと変換した行列を取得するものになっているので、これを元にView行列を取得します。
ScaleのZ座標
Matrix4x4.Scale(new Vector3(1, 1, -1)) * transform.worldToLocalMatrix
transform.worldToLocalMatrix
のZ座標を反転させています。
これはC#は左手座標系、シェーダーは右手座標系なので、その差を吸収するためにZ座標を反転させています。
似たメソッドにCamera.worldToCameraMatrix
があり、こちらは予め右手座標系にしてくれています。
transform.worldToLocalMatrix
Camera.worldToCameraMatrix
Projection行列の作成
private Matrix4x4 GetProjectionMatrix() { Matrix4x4 projectionMatrix; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectionMatrix = Matrix4x4.Perspective(_fieldOfView, _aspect, _nearClipPlane, _farClipPlane); break; case ProjectionType.ORTHOGRAPHIC : var orthographicWidth = _orthographicSize * _aspect; projectionMatrix = Matrix4x4.Ortho(-orthographicWidth, orthographicWidth, -_orthographicSize, _orthographicSize, _nearClipPlane, _farClipPlane); break; default : Debug.LogError("Not Expected CameraType"); return Matrix4x4.identity; } projectionMatrix = GL.GetGPUProjectionMatrix(projectionMatrix, true); return projectionMatrix; }
PERSPECTIVE
Matrix4x4.Perspective(_fieldOfView, _aspect, _nearClipPlane, _farClipPlane);
でProjection行列を計算しています。
Matrix4x4.Perspective
はPerspectiveなProjection行列を生成する関数になっています。
定義
/// <summary> /// PerspectiveなProjection行列を生成 /// </summary> /// <param name="fov">垂直な視野角(度)</param> /// <param name="aspect">アスペクト比</param> /// <param name="zNear">Near Clip</param> /// <param name="zFar">Far Clip</param> /// <returns></returns> public static Matrix4x4 Perspective(float fov, float aspect, float zNear, float zFar);
ORTHOGRAPHIC
Matrix4x4.Ortho(-orthographicWidth, orthographicWidth, -_orthographicSize, _orthographicSize, _nearClipPlane, _farClipPlane);
でProjection行列を計算しています。
Matrix4x4.Ortho
はOrthographicなProjection行列を生成する関数になっています。
定義
/// <summary> /// OrthographicなProjection行列を生成 /// </summary> /// <param name="left">左のx座標</param> /// <param name="right">右のx座標</param> /// <param name="bottom">下のy座標</param> /// <param name="top">上のy座標</param> /// <param name="zNear">Near Clip</param> /// <param name="zFar">Far Clip</param> /// <returns></returns> public static Matrix4x4 Ortho(float left, float right, float bottom, float top, float zNear, float zFar);
プラットフォームの違いを吸収
projectionMatrix = GL.GetGPUProjectionMatrix(projectionMatrix, true);
取得したProjection行列をProjection変換に使用する際に、
View空間にあるxyzを-1~1に収めるような座標に変換する必要があります。
これを正規化デバイス座標と呼びます。
この際にwで除算する必要があるのですが、現在取得したProjection行列はC#側で生成したので正規化した際にShader側とz値を収める範囲が異なります。
その差を吸収してくれるのがGL.GetGPUProjectionMatrix
となります。
定義
/// <summary> /// Projection行列のプラットフォームの違いを吸収する /// </summary> /// <param name="proj">Projection行列</param> /// <param name="renderIntoTexture">Projection行列をRenderTextureで使用するか</param> /// <returns></returns> public static Matrix4x4 GetGPUProjectionMatrix(Matrix4x4 proj, bool renderIntoTexture);
プロジェクターの座標を取得
private Vector4 GetProjectorPos() { Vector4 projectorPos; switch (projectionType) { case ProjectionType.PERSPECTIVE : projectorPos = transform.position; projectorPos.w = 1; break; case ProjectionType.ORTHOGRAPHIC : projectorPos = transform.forward; projectorPos.w = 0; break; default : Debug.LogError("Not Expected CameraType"); return Vector4.zero; } return projectorPos; }
シェーダーの定義済の値に_ObjectSpaceLightPos
があり、
wの値が0の場合は指向性ライト、1の場合はその他のライトとなっています。
それに習い、wの値をPerspectiveかOrthographicによって変化させています。
範囲外の描画をしない
カメラ外
fixed3 isOut = step((i.projectorSpacePos - 0.5) * sign(i.projectorSpacePos), 0.5);
i.projectorSpacePos
はクリップスペース空間に変換されているので、0~1の範囲となっています。
そこでsign
とstep
を組み合わせて0~1の範囲外は描画しないようにしています。
グラフ
赤 : (x-0.5) * sign(x) 青 : step((x-0.5) * sign(x), 0.5)
sign(x)
xが正なら+1.0、0.0なら0.0、負なら-1.0を返す関数になります。
裏麺
step(-dot(lerp(-_ProjectorPos.xyz, _ProjectorPos.xyz - i.worldPos, _ProjectorPos.w), i.worldNormal), 0);
lerp
を使用しているのは、スクリプト側から_ProjectorPosを渡した際のwで投影法を切り替えるためになります。
その後、視線ベクトルと法線ベクトルの内積で裏面かを判断しています。
結果
Perspective
Orthographic
今回は以上となります。
ここまでご視聴ありがとうございました。
参考サイト様
【UnityShader】オブジェクトの影を受け取る #108
前回の成果
オブジェクトの影を落とした。
今回やること
前回のコードのままですと、他のオブジェクトからの影を受け取ることができません。
なので、その部分を修正します。
影を受け取れていない
手前のオブジェクトが前回、影を落としたオブジェクトになります。
奥のオブジェクトはUnityのデフォルトのCubeになります。
事前準備
オブジェクトを配置し、配置したオブジェクトの影が当たる位置にオブジェクトを配置します。
影が当たる位置のオブジェクトに今回制作するマテリアルをアタッチします。
ソースコード
Shader "Unlit/ShadowDefault" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // 通常の描画 Pass { Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; fixed4 diff : COLOR0; float4 pos : SV_POSITION; SHADOW_COORDS(1) }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); half3 worldNormal = UnityObjectToWorldNormal(v.normal); half NdotL = saturate(dot(worldNormal, _WorldSpaceLightPos0.xyz)); o.diff = NdotL * _LightColor0; TRANSFER_SHADOW(o) return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); fixed4 shadow = SHADOW_ATTENUATION(i); return col * i.diff * shadow; } ENDCG } // 影の描画 Pass { Tags { "LightMode"="ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { // 深度テクスチャではないキューブマップの場合 // float3 vec : TEXCOORD0; // float4 pos : SV_POSITION // それ以外の場合 // float4 pos : SV_POSITION V2F_SHADOW_CASTER; }; v2f vert (appdata v) { v2f o; // 深度テクスチャではないキューブマップの場合 // o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; // o.pos = UnityObjectToClipPos(v.vertex); // それ以外の場合 // o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); // o.pos = UnityApplyLinearShadowBias(o.pos); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } fixed4 frag (v2f i) : SV_Target { // 深度テクスチャではないキューブマップの場合 // UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); // それ以外の場合 // return 0 SHADOW_CASTER_FRAGMENT(i) } ENDCG } } }
LightMode Tag
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
今回はForwardBase
を指定しています。
このキーワードはアンビエント、メインのDirectionalLight、Vertex/SHLight、ライトマップを適応するものになります。
詳しくは以下の記事を参考にしてください。
シェーダーバリアント
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights
シェーダーバリアントとは
コンパイル時に複数のシェーダーコードを自動的に生成したい命令をコンパイラに伝えるものになります。
また、multi_compile_fwdbase
のようなmulti_compile
で始まるものは複数のシェーダーバリアントをコンパイルしています。
今回のmulti_compile_fwdbase
では、コンパイラにフォワードレンダリングベースに必要な全てのバリアントをコンパイルしたいことを伝えています。
multi_compileの例
名前 | コンパイルするバリアント |
---|---|
multi_compile_fwdbase | フォワードレンダリングのベース処理 |
multi_compile_fwdadd | フォワードレンダリングのライト加算処理 |
multi_compile_fwdadd_fullshadows | multi_compile_fwdaddとライトのリアルタイムシャドウ |
multi_compile_fog | フォグの処理 |
使用されているバリアントの確認
UnityのInspectorから、使用されているバリアントを確認することができます。
シェーダーを選択肢、Inspectorから
Compile code -> ▼ -> Show
の手順で確認できます。
今回のシェーダーですと、8つのバリアントが使用されているようです。
// Total snippets: 2 // ----------------------------------------- // Snippet #0 platforms ffffffff: Builtin keywords used: DIRECTIONAL LIGHTPROBE_SH SHADOWS_SHADOWMASK SHADOWS_SCREEN LIGHTMAP_SHADOW_MIXING VERTEXLIGHT_ON 8 keyword variants used in scene: DIRECTIONAL DIRECTIONAL LIGHTPROBE_SH DIRECTIONAL SHADOWS_SCREEN DIRECTIONAL LIGHTPROBE_SH SHADOWS_SCREEN DIRECTIONAL VERTEXLIGHT_ON DIRECTIONAL LIGHTPROBE_SH VERTEXLIGHT_ON DIRECTIONAL SHADOWS_SCREEN VERTEXLIGHT_ON DIRECTIONAL LIGHTPROBE_SH SHADOWS_SCREEN VERTEXLIGHT_ON // ----------------------------------------- // Snippet #1 platforms ffffffff: Builtin keywords used: SHADOWS_DEPTH SHADOWS_CUBE 2 keyword variants used in scene: SHADOWS_DEPTH SHADOWS_CUBE
シェーダーバリアントのコード生成オプション
フォワードレンダリングのバリアントを指定したあとに、いくつかのキーワードが指定されています。
#pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights
これは、コード生成オプションと呼ばれるものになります。
multi_compileでは複数のバリアントをコンパイルしてしまうため、不要なものが出てきてしまう可能性があります。
予め、不要なものがわかっている場合は、コード生成オプションを指定することで対象のコンパイルをスキップすることができます。
今回使用しているコード生成オプション
名前 | 意味 |
---|---|
nolightmap | このシェーダーの全てのライトマッピングのサポートを無効にする |
nodirlightmap | このシェーダーのDirectional Lightmapのサポートを無効にする |
nodynlightmap | このシェーダーのDynamic GIを無効にする |
novertexlights | フォワードレンダリングで、ライトプローブと頂点ライティングを適応しない |
他にもいくつかあります。
詳しくは、公式のリファレンスを参照してください。
組み込みシェーダーのinclude
#include "Lighting.cginc" #include "AutoLight.cginc"
Lighting.cginc
サーフェスシェーダーにおける標準的な照明モデルになります。
サーフェスシェーダーを書く時には自動的に含まれます。
今回の場合、_LightColor0
を使用するためにincludeしています。
AutoLight.cginc
ライティングとシャドウの機能になります。
サーフェスシェーダーの場合、この組み込みシェーダーを内部的に使用しています。
今回の場合、SHADOW_COORDS
、TRANSFER_SHADOW
、SHADOW_ATTENUATION
を使用するためにincludeしています。
v2f構造体のSV_POSITION
struct v2f { // 省略 float4 pos : SV_POSITION; };
影を使用する際には、SV_POSITION名をpos
にしないとエラーが出てしまいます。
これはvertexからfragmentへ変換を行う際のマクロでpos
が決め打ちとなっているからになります。
今回ですとTRANSFER_SHADOW
でposが決め打ちとなっています。
SHADOW_COORDS
struct v2f { // 省略 SHADOW_COORDS(1) };
SHADOW_COORDSの定義
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
引数に応じたunityShadowCoord4
型のTEXCOORDを定義しています。
すでに使用しているTEXCOORDは使用できないので注意してください。
今回の場合、uvでTEXCOORD0を指定しているので、SHADOW_COORDSの引数は1にしています。
unityShadowCoord4の定義
#define unityShadowCoord4 float4
float4型となっており、ここに影の情報を入れています。
TRANSFER_SHADOW
v2f vert (appdata v) {
// 省略
TRANSFER_SHADOW(o)
}
TRANSFER_SHADOWの定義
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
SHADOW_COORDS
で定義した、_ShadowCoord
にスクリーンスペースの座標を入れています。
ここでposが決め打ちとなっているので、v2f構造体のSV_POSITIONの名前はposである必要があります。
SHADOW_ATTENUATION
fixed4 frag (v2f i) : SV_Target {
// 省略
fixed4 shadow = SHADOW_ATTENUATION(i);
}
SHADOW_ATTENUATIONの定義
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
バーテックスシェーダーでTRANSFER_SHADOW
マクロを用いてスクリーンスペース座標を計算した_ShadowCoord
を引数としていれています。
unitySampleShadowの定義
inline fixed unitySampleShadow (float4 shadowCoord) { fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord) ).r; return shadow; }
tex2Dproj
を使用して、オブジェクトにシャドウマップのテクセルを参照しています。
R値を返しているのは、シャドウマップはR値で深度を参照するからになります。
シャドウマップの例
【Unity】シャドウマップを自前で作成する(ライトからの深度をRenderTextureに描画する) - LIGHT11:より引用
tex2Dproj
tex2Dprojは、投影テクスチャマッピングの際に必要なtex2D処理となります。
これは仮想視点での射影変換をしているので、wでの除算が必要になるためになります。
// 元定義 fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord) ).r; return shadow; // tex2Dprojの置き換え float2 uv = shadowCoord.xy / shadowCoord.w; fixed shadow = tex2D(_ShadowMapTexture, uv).r return shadow
UNITY_PROJ_COORD
#if defined(SHADER_API_PSP2) #define UNITY_BUGGY_TEX2DPROJ4 #define UNITY_PROJ_COORD(a) (a).xyw #else #define UNITY_PROJ_COORD(a) a #endif
SHADER_API_PSP2
はPlayStation Vitaかを判断するマクロになります。
ですので、ほとんどのプラットフォームの場合、aをそのまま返すだけになります。
シェーダーにコメントを付与
まとめとして、コメントを記載しました。
Shader "Unlit/ShadowDefault" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // 通常の描画 Pass { Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlights #include "UnityCG.cginc" #include "Lighting.cginc" #include "AutoLight.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; fixed4 diff : COLOR0; // 変数名は"pos"固定 float4 pos : SV_POSITION; // unityShadowCoord4型(float4)を定義 // 影の情報を入れる SHADOW_COORDS(1) }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); half3 worldNormal = UnityObjectToWorldNormal(v.normal); half NdotL = saturate(dot(worldNormal, _WorldSpaceLightPos0.xyz)); o.diff = NdotL * _LightColor0; // SHADOW_COORDSにスクリーンスペース座標を入れる TRANSFER_SHADOW(o) return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); // シャドウマップのテクセルを反映 fixed4 shadow = SHADOW_ATTENUATION(i); return col * i.diff * shadow; } ENDCG } // 影の描画 Pass { Tags { "LightMode"="ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { // 深度テクスチャではないキューブマップの場合 // float3 vec : TEXCOORD0; // float4 pos : SV_POSITION // それ以外の場合 // float4 pos : SV_POSITION V2F_SHADOW_CASTER; }; v2f vert (appdata v) { v2f o; // 深度テクスチャではないキューブマップの場合 // o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; // o.pos = UnityObjectToClipPos(v.vertex); // それ以外の場合 // o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); // o.pos = UnityApplyLinearShadowBias(o.pos); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } fixed4 frag (v2f i) : SV_Target { // 深度テクスチャではないキューブマップの場合 // UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); // それ以外の場合 // return 0 SHADOW_CASTER_FRAGMENT(i) } ENDCG } } }
結果
手前が今回制作したシェーダーのオブジェクト
奥がUnityのデフォルトシェーダーのオブジェクトになります。
手前のオブジェクトにも影が落ちるようになりました。
今回は以上となります。
ここまでご視聴ありがとうございました。
参考サイト様
【UnityShader】オブジェクトの影を落とす #107
前回の成果
Bloomシェーダーについて学んだ。
今回やること
シェーダーでの影について学んでいきます。
事前準備
Scene上にCubeを配置します。
Cubeの影が出るようにPlaneを配置して準備は完了となります。
影
Unityのデフォルトのマテリアルを使用する際には特に意識せずとも、オブジェクトの影が表示されています。
ですが、自分でシェーダーを使用し、そのシェーダーを元にしたマテリアルを使用すると影の表示がなくなってしまいます。
影が出ない例
UnlitShaderを制作し、そのマテリアルをアタッチしています。
Planeに影が出ていません。
なので、シェーダーで影を描画していきます。
シェーダー
Shader "Unlit/ShadowDef" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // 通常の描画 Pass { Tags { "RenderType"="Opaque" } 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; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; 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 } // 影の描画 Pass { Tags { "LightMode"="ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { V2F_SHADOW_CASTER; }; v2f vert (appdata v) { v2f o; TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } fixed4 frag (v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) } ENDCG } } }
1Pass目でオブジェクトの描画をし、2Pass目で影の描画をしていきます。
影自体の箇所は短いのですが、組み込みの関数を使用しているので中身について解説していきます。
LightMode Tag
以下の箇所で使用しています。
Tags { "LightMode"="ShadowCaster" }
RenderType
やQueue
と同じTagになります。
このタグは、ライティングに使用されるTagになっています。
組み込みのレンダリングパイプラインで、LightModeTagを設定しない場合には、Unityはライティングや影なしで描画しようとします。
ですので、UnlitShaderだと影やライティングが描画されません。
今回は、ShadowCasterを指定しているので、オブジェクトの深度をシャドウマップから深度テクスチャにレンダリングすることをUnityに伝えています。
LightMode Tagの一例
Tag名 | 意味 |
---|---|
Always | 常にレンダリングされ、ライティングは適応されない デフォルト値 |
ForwardBase | Forwardレンダリングで使用 アンビエント、メインのDirectionalLight、Vertex/SHLight、ライトマップを適応 |
Deferred | DeferredShading(遅延シェーディング)で使用 g-Bufferをレンダリング |
ShadowCaster | オブジェクトの深度をシャドウマップから深度テクスチャにレンダリング |
他にもLightMode Tagは種類があるのですが、一部割愛させていただきます。
以下リファレンスに他の種類が記載されています。
V2F_SHADOW_CASTER
以下の箇所で使用しています。
struct v2f {
V2F_SHADOW_CASTER;
};
V2F_SHADOW_CASTERの定義
#define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS UNITY_POSITION(pos)
V2F_SHADOW_CASTER_NOPOS
のあとにUNITY_POSITION(pos)
を呼んでいるのと同義です。
更に展開します。
V2F_SHADOW_CASTER_NOPOSの定義
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX) // Rendering into point light (cubemap) shadows #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0; #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define SHADOW_CASTER_FRAGMENT(i) return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); #else // Rendering into directional or spot light shadows #define V2F_SHADOW_CASTER_NOPOS // Let embedding code know that V2F_SHADOW_CASTER_NOPOS is empty; so that it can workaround // empty structs that could possibly be produced. #define V2F_SHADOW_CASTER_NOPOS_IS_EMPTY #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) \ opos = UnityObjectToClipPos(v.vertex.xyz); \ opos = UnityApplyLinearShadowBias(opos); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #define SHADOW_CASTER_FRAGMENT(i) return 0; #endif
V2F_SHADOW_CASTER_NOPOSの解説
defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
は、シャドウマップにキューブマップを使うポイントライトかを見ているifになります。
また、SHADOWS_CUBE
はLightコンポーネントのTypeとShadowTypeによって定義されているものになります。
Lightコンポーネント
もう片方の条件となっているSHADOWS_CUBE_IN_DEPTH_TEX
の定義は以下になります。
SHADOWS_CUBE_IN_DEPTH_TEXの定義
#if defined(SHADER_API_D3D11) || defined(SHADER_API_PSSL) || defined(SHADER_API_METAL) || defined(SHADER_API_GLCORE) || defined(SHADER_API_GLES3) || defined(SHADER_API_VULKAN) || defined(SHADER_API_SWITCH) // D3D11, D3D12, XB1, PS4, iOS, macOS, tvOS, glcore, gles3, webgl2.0, Switch // Real-support for depth-format cube shadow map. #define SHADOWS_CUBE_IN_DEPTH_TEX #endif
基本的なプラットフォームは記載されているので、あまり気にする必要はなさそうです。
UNITY_POSITION(pos)の定義
// On D3D reading screen space coordinates from fragment shader requires SM3.0 #define UNITY_POSITION(pos) float4 pos : SV_POSITION
こちらは単純で、SV_POSITIONの定義だけになります。
V2F_SHADOW_CASTER_NOPOSのまとめ
少し長くなってしまったので、まとめます。
深度テクスチャではないキューブマップ
float3 vec : TEXCOORD0; float4 pos : SV_POSITION
それ以外(基本的にはこちら)
float4 pos : SV_POSITION
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
以下の箇所で使用しています。
v2f vert (appdata v) {
v2f o;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o);
return o;
}
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)の定義
TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
に置き換わっているだけとなります。
// Vertex shader part, with support for normal offset shadows. Requires // position and normal to be present in the vertex input. #define TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) TRANSFER_SHADOW_CASTER_NOPOS(o,o.pos)
TRANSFER_SHADOW_CASTER_NOPOSの定義
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX) // Rendering into point light (cubemap) shadows #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0; #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define SHADOW_CASTER_FRAGMENT(i) return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); #else // Rendering into directional or spot light shadows #define V2F_SHADOW_CASTER_NOPOS // Let embedding code know that V2F_SHADOW_CASTER_NOPOS is empty; so that it can workaround // empty structs that could possibly be produced. #define V2F_SHADOW_CASTER_NOPOS_IS_EMPTY #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) \ opos = UnityObjectToClipPos(v.vertex.xyz); \ opos = UnityApplyLinearShadowBias(opos); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #define SHADOW_CASTER_FRAGMENT(i) return 0; #endif
最初の条件式はV2F_SHADOW_CASTER
と同じく深度テクスチャではないキューブマップかを見ています。
TRANSFER_SHADOW_CASTER_NOPOSの解説(深度テクスチャではないキューブマップ)
レガシーな方は割愛させていただきます。
定義
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex);
_LightPositionRange
の定義はこちらです。
float4 _LightPositionRange; // xyz = pos, w = 1/range
つまり、TRANSFER_SHADOW_CASTER_NOPOSはこうなります。
o.vec = 頂点のワールド座標 - ライトの座標 opos = 頂点のクリップ座標
TRANSFER_SHADOW_CASTER_NOPOSの解説(それ以外の場合)
深度テクスチャではないキューブマップ以外の場合になります。
こちらもレガシーなものは割愛します。
定義
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos);
UnityClipSpaceShadowCasterPos(v.vertex, v.normal)
と
UnityApplyLinearShadowBias(opos)
に展開されて処理されています。
UnityClipSpaceShadowCasterPos
定義
float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal) { float4 wPos = mul(unity_ObjectToWorld, vertex); if (unity_LightShadowBias.z != 0.0) { float3 wNormal = UnityObjectToWorldNormal(normal); float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz)); // apply normal offset bias (inset position along the normal) // bias needs to be scaled by sine between normal and light direction // (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/) // // unity_LightShadowBias.z contains user-specified normal offset amount // scaled by world space texel size. float shadowCos = dot(wNormal, wLight); float shadowSine = sqrt(1-shadowCos*shadowCos); float normalBias = unity_LightShadowBias.z * shadowSine; wPos.xyz -= wNormal * normalBias; } return mul(UNITY_MATRIX_VP, wPos); } // Legacy, not used anymore; kept around to not break existing user shaders float4 UnityClipSpaceShadowCasterPos(float3 vertex, float3 normal) { return UnityClipSpaceShadowCasterPos(float4(vertex, 1), normal); }
unity_LightShadowBias.z
はユーザーが指定した法線のオフセット量になります。
基本的には、オブジェクトのクリップ座標を計算しているものになります。
法線方向にnormalBias
を乗算しているのは、シャドウアクネを軽減させるためになります。
このバイアスのスケールは法線とライトのアークコサインのサインに比例します。
ですので、
float shadowSine = sqrt(1-shadowCos*shadowCos);
で求めています。
このバイアスの意味合いは以下サイト様に記載されています。
Shadow Mapping Summary – Part 1 – The Witness
シャドウアクネ
少し脱線してしまいますが、シャドウアクネについて解説します。
以下の画像でシャドウアクネが発生していることが確認できます。
オブジェクトの影が描画されているのですが、影の他に縦方向の縞模様のようなものができてしまっています。
それがシャドウアクネと呼ばれるものになります。
影 - Unity マニュアル:より引用
発生の原因
シャドウマップで指定された距離のピクセルが遠くにあるように計算されてしまう場合に発生します。
以下の画像のように本来ポリゴンは頂点AからBにかけてなめらかになっています。
それがシャドウマップのテクセルによってZ値でまとめられ、一定の間隔で区切られてしまいます。
シャドウマップを使用し影をつけることは、この一定間隔に区切られたポリゴンとの比較をすることになります。
つまり以下画像の緑色の箇所が影と判定されてしまいます。
なので縞模様ができてしまっています。
対策として最も良いのは、1テクセルの大きさを小さくしてシャドウマップの解像度を上げることになります。
ですがこの方法ですとパフォーマンスが著しく落ちてしまいます。
なので、バイアスをかけてあげることによって対策をしています。
ただし、バイアスをかけすぎてしまうとオブジェクトと影が離れてしまうピーターパン現象と呼ばれるものが発生します。
ピーターパン現象の例
影 - Unity マニュアル:より引用
試してみる
実際にUnity上で試してみたいと思います。
特になにも考えずにオブジェクトをScene上に配置します。
違和感なく影が出ているのがわかるかと思います。
通常の影
LightコンポーネントのBias
とNormalBias
の値を0にしてみます。
そうすると、オブジェクトの影自体は問題なく描画されていますが、Planeに縞模様ができてしまっています。
シャドウアクネ
逆にLightコンポーネントのBias
とNormalBias
の値を最大値にしてみます。
そうすると、オブジェクトと影との距離が離れてしまっています。
ピーターパン現象
オブジェクトの大きさがそのままだと現象が発生していることが確認しにくかったので、縦方向にスケールさせています。
UnityApplyLinearShadowBiasの定義
float4 UnityApplyLinearShadowBias(float4 clipPos) { // For point lights that support depth cube map, the bias is applied in the fragment shader sampling the shadow map. // This is because the legacy behaviour for point light shadow map cannot be implemented by offseting the vertex position // in the vertex shader generating the shadow map. #if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX)) #if defined(UNITY_REVERSED_Z) // We use max/min instead of clamp to ensure proper handling of the rare case // where both numerator and denominator are zero and the fraction becomes NaN. clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0)); #else clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w); #endif #endif #if defined(UNITY_REVERSED_Z) float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE); #else float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE); #endif clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y); return clipPos; }
#if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX))
で分岐されていますが、UnityApplyLinearShadowBias
を呼ぶ際にこの条件式を通らないと呼ばないので、分岐の中は実行されています。
UnityApplyLinearShadowBiasの解説
unity_LightShadowBias .x
とunity_LightShadowBias .y
にはこのブログを書いている段階では情報がありませんでした。
ですが、おそらくLightコンポーネントのBias
の値を元に処理をしているものだと思われます。
Bias
深度値のものであるZをバイアスに応じて増加させて、その値をClampしています。
UNITY_REVERSED_Z
UNITY_REVERSED_Z
はプラットフォーム毎のZの向きに対応するものになります。
UNITY_REVERSED_Zの値 | プラッフォーム | Zバッファの範囲 |
---|---|---|
1 | DX11/12 PS4 XboxOne Metal |
1 ~ 0 |
0 | その他のプラットフォーム | 0 ~ 1 |
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)のまとめ
こちらもまとめます。
深度テクスチャではないキューブマップ
o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; o.pos = UnityObjectToClipPos(v.vertex);
それ以外(基本的にはこちら)
o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); o.pos = UnityApplyLinearShadowBias(o.pos);
SHADOW_CASTER_FRAGMENT
使用箇所は以下になります。
fixed4 frag (v2f i) : SV_Target { SHADOW_CASTER_FRAGMENT(i) }
SHADOW_CASTER_FRAGMENTの定義
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX) // Rendering into point light (cubemap) shadows #define V2F_SHADOW_CASTER_NOPOS float3 vec : TEXCOORD0; #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; opos = UnityObjectToClipPos(v.vertex); #define SHADOW_CASTER_FRAGMENT(i) return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); #else // Rendering into directional or spot light shadows #define V2F_SHADOW_CASTER_NOPOS // Let embedding code know that V2F_SHADOW_CASTER_NOPOS is empty; so that it can workaround // empty structs that could possibly be produced. #define V2F_SHADOW_CASTER_NOPOS_IS_EMPTY #define TRANSFER_SHADOW_CASTER_NOPOS_LEGACY(o,opos) \ opos = UnityObjectToClipPos(v.vertex.xyz); \ opos = UnityApplyLinearShadowBias(opos); #define TRANSFER_SHADOW_CASTER_NOPOS(o,opos) \ opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); \ opos = UnityApplyLinearShadowBias(opos); #define SHADOW_CASTER_FRAGMENT(i) return 0; #endif
ここも他と同様に深度テクスチャではないキューブマップとそれ以外を見ています。
それ以外の場合は0を返しています。
これは深度を利用しているので、色は0で問題ないからになります。
UnityEncodeCubeShadowDepth
定義
// Encoding/decoding [0..1) floats into 8 bit/channel RGBA. Note that 1.0 will not be encoded properly. inline float4 EncodeFloatRGBA( float v ) { float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 16581375.0); float kEncodeBit = 1.0/255.0; float4 enc = kEncodeMul * v; enc = frac (enc); enc -= enc.yzww * kEncodeBit; return enc; } float4 UnityEncodeCubeShadowDepth (float z) { #ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS return EncodeFloatRGBA (min(z, 0.999)); #else return z; #endif }
floatの精度を必要に応じて、rgbaに入れています。
必要なプラットフォームの分岐はUNITY_USE_RGBA_FOR_POINT_SHADOWS
で見ています。
これは、
// SHADER_API_GLES : OpenGL ES 2.0 // SHADER_API_GLES3 : OpenGL ES 3.0/3.1 defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)
の場合となります。
SHADOW_CASTER_FRAGMENTのまとめ
深度テクスチャではないキューブマップ
return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
それ以外(基本的にはこちら)
return 0
シェーダーにコメントを付与
全体のまとめとして、コメントに記載しました。
Shader "Unlit/ShadowDef" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // 通常の描画 Pass { Tags { "RenderType"="Opaque" } 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; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; 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 } // 影の描画 Pass { Tags { "LightMode"="ShadowCaster" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_shadowcaster #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { // 深度テクスチャではないキューブマップの場合 // float3 vec : TEXCOORD0; // float4 pos : SV_POSITION // それ以外の場合 // float4 pos : SV_POSITION V2F_SHADOW_CASTER; }; v2f vert (appdata v) { v2f o; // 深度テクスチャではないキューブマップの場合 // o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz; // o.pos = UnityObjectToClipPos(v.vertex); // それ以外の場合 // o.pos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal); // o.pos = UnityApplyLinearShadowBias(o.pos); TRANSFER_SHADOW_CASTER_NORMALOFFSET(o); return o; } fixed4 frag (v2f i) : SV_Target { // 深度テクスチャではないキューブマップの場合 // UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w); // それ以外の場合 // return 0 SHADOW_CASTER_FRAGMENT(i) } ENDCG } } }
結果
右が通常のオブジェクト、左が今回制作したシェーダーのオブジェクトになります。
左のオブジェクトでも影がPlaneに落ちていることが確認できます。
今回は以上となります。
ここまでご視聴ありがとうございました。
参考サイト様
【UnityShader】Bloomシェーダー #106
前回の成果
3Dモデルを液体のようにするシェーダーについて学んだ。
今回やること
以下のサイト様を参考に、Bloomシェーダーを実装します。
事前準備
Scene上にSpriteRendererを配置します。
そして、MainCameraに今回使用するScriptをアタッチして事前準備完了となります。
ソースコード
Script
using UnityEngine; [ExecuteInEditMode] public class BloomTexture : MonoBehaviour { [SerializeField] private Shader _shader; // 全体の強度 [SerializeField, Range(0, 1f)] public float _strength = 0.3f; // ブラーの強度 [SerializeField, Range(1, 64)] public int _blur = 20; // 明るさのしきい値 [SerializeField, Range(0, 1f)] public float _threshold = 0.3f; // RenderTextureサイズの分母 [SerializeField, Range(1,12)] public int _ratio = 1; [SerializeField, Range(1f, 10f)] private float _offset = 1f; private Material _material; private float[] _weights = new float[10]; void OnRenderImage(RenderTexture src, RenderTexture dest) { if (_material == null) { _material = new Material(_shader); _material.hideFlags = HideFlags.DontSave; } _OnRenderImage(src, dest); } private void _OnRenderImage (RenderTexture src, RenderTexture dest) { int renderTextureX = src.width / _ratio; int renderTextureY = src.height / _ratio; RenderTexture tmp = CreateRenderTexture(renderTextureX, renderTextureY); RenderTexture tmp2 = CreateRenderTexture(renderTextureX, renderTextureY); // Bloom _material.SetFloat ("_Strength", _strength); _material.SetFloat ("_Threshold", _threshold); _material.SetFloat ("_Blur", _blur); _material.SetTexture ("_Tmp", tmp); Graphics.Blit (src, tmp, _material, 0); // ガウシアンブラー UpdateWeights(); _material.SetFloatArray("_Weights", _weights); float x = _offset / tmp2.width; float y = _offset / tmp2.height; _material.SetVector("_Offset", new Vector4(x, 0, 0, 0)); Graphics.Blit(src, tmp2, _material, 1); _material.SetVector("_Offset", new Vector4(0, y, 0, 0)); Graphics.Blit(tmp2, dest, _material, 1); RenderTexture.ReleaseTemporary(tmp); RenderTexture.ReleaseTemporary(tmp2); } /// <summary> /// RenderTextureの生成 /// </summary> /// <param name="width"></param> /// <param name="height"></param> /// <returns></returns> private RenderTexture CreateRenderTexture(int width, int height) { RenderTexture renderTexture = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32); renderTexture.filterMode = FilterMode.Bilinear; return renderTexture; } /// <summary> /// 重みの計算 /// </summary> private void UpdateWeights() { float total = 0; float d = _blur * _blur * 0.01f; for (int i = 0; i < _weights.Length; i++) { float x = 1.0f + i * 2f; float w = Mathf.Exp(-0.5f * (x * x) / d); _weights[i] = w; // xとyがあるので2倍 if (i > 0) { w *= 2.0f; } total += w; } // 正規化 for (int i = 0; i < _weights.Length; i++) { _weights[i] /= total; } } }
シェーダー
Shader "Unlit/BloomTexture" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment fragBright ENDCG } Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment fragGauss ENDCG } CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; sampler2D _Tmp; float _Strength; float _Blur; float _Threshold; half4 _Offset; static const int samplingCount = 10; half _Weights[samplingCount]; fixed4 fragBright (v2f_img i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); // ピクセルの明るさ float bright = (col.r + col.g + col.b) / 3; // しきい値によって明るさを決める float tmp = step(_Threshold, bright); return col * tmp * _Strength; } fixed4 fragGauss (v2f_img i) : SV_Target { fixed4 col = 0; // メモリサイズを大きくする代わりに高速にする [unroll] // 左右へのサンプリング for (int j = samplingCount - 1; j > 0; j--) { col += tex2D(_Tmp, i.uv - (_Offset.xy * j)) * _Weights[j]; } [unroll] // 上下へのサンプリング for (int j = 0; j < samplingCount; j++) { col += tex2D(_Tmp, i.uv + (_Offset.xy * j)) * _Weights[j]; } return col + tex2D(_MainTex, i.uv); } ENDCG } }
Bloom
Bloomとは
ブルームとは、光源から光があふれ出るようなエフェクトのことを指します。ゲームでは光源は数えきれないほど存在します。太陽の光、反射するミラーやガラス、スポットライトやサーチライトといったオブジェクトの光だけでなく、シューティングゲームの弾幕や、スキルエフェクトなども光源として扱われます。
とのことです。
このエフェクトを使用することで、オブジェクトにライティングを付与させてリッチな表現にすることができます。
Bloom適応前
Bloom - Unity マニュアル:より引用
Bloom適応後
Bloom - Unity マニュアル:より引用
BloomはPostProcessVolumeコンポーネントから追加することもできます。
光らせる箇所の取得
フラグメントシェーダーでテクセルの色を取得し、スクリプトで渡したしきい値とstep()します。
そうすることで、しきい値以上の明るさを箇所を2Pass目に渡して処理することができます。
// ピクセルの明るさ float bright = (col.r + col.g + col.b) / 3; // しきい値によって明るさを決める float tmp = step(_Threshold, bright);
光らせる箇所
しきい値が0.8の時の2Pass目にわたす箇所になります。
2Pass目で元画像に合成するため、黒くしておくことで計算を楽にできるようにしています。
ブラー処理
1Pass目で取得した箇所にブラー処理をかけて、元の画像に加算合成します。
fixed4 col = 0; // メモリサイズを大きくする代わりに高速にする [unroll] // 左右へのサンプリング for (int j = samplingCount - 1; j > 0; j--) { col += tex2D(_Tmp, i.uv - (_Offset.xy * j)) * _Weights[j]; } [unroll] // 上下へのサンプリング for (int j = 0; j < samplingCount; j++) { col += tex2D(_Tmp, i.uv + (_Offset.xy * j)) * _Weights[j]; } return col + tex2D(_MainTex, i.uv);
今回は以前勉強した、ガウシアンブラーを使用しています。
このブラーを使用することで、より軽量な計算でブラー処理を行うことが出来ます。
詳しくは以下を参考にしてください。
ガウシアンブラーの横方向サンプリング結果
ガウシアンブラーの縦方向サンプリング結果
スクリプト側
備忘録程度に記載します。
OnRenderImage
カメラのレンダリングが完了した時に呼ばれる関数になります。
第一引数には、入力画像が、第二引数には出力画像が渡されます。
ExecuteInEditMode
UnityをPlayモードにせず、Editモードのままで実行することが出来ます。
ただし、実行できるのは以下の3つのみになります。
- Update
- OnGUI
- OnRenderObject
Playモード
PlayモードはUnityの右三角が押されている状態のことを指します。
結果
SpriteRendererにBloomエフェクトがかかれば成功です。
また、普通のオブジェクトに対しても使用することができます。
今回は以上となります。
ここまでご視聴ありがとうございました。
この作品はユニティちゃんライセンス条項の元に提供されています
【UnityShader】3Dモデルを液体のようにする #105
前回の成果
その他のオペレータについてまとめた。
今回やること
以下のサイト様を参考に、3Dモデルを液体のようにするシェーダーを制作します。
事前準備
Scene上に適当な3Dモデルを配置します。
そして、今回制作するScriptとMaterialをアタッチしてください。
ソースコード
Shader
Shader "Unlit/Liquid" { Properties { _MainTex ("Texture", 2D) = "white" {} [Header(Shape)] _HeightMax ("Height Max", Float) = 1.0 _HeightMin ("Height Min", Float) = 0.0 _TopColor ("Top Color", Color) = (0.5, 0.75, 1,1) _BottomColor ("Bottom Color", Color) = (0, 0.25, 1, 1) [Header(Wave)] _WaveSpeed ("Wave Speed", Float) = 1.0 _WavePower ("Wave Power", Float) = 0.1 _WaveLength ("Wave Length", Float) = 1.0 [Header(Rim)] _RimColor ("Rim Light Color", Color) = (1, 1, 1, 1) _RimPower("Rim Light Power", Float) = 3 [Header(Surface)] _SurfaceColor ("Surface Color", Color) = (1, 1, 1, 1) [HideInInspector] _TransformPositionY ("Transform Position Y", float) = 0 } SubShader { Tags { "RenderType"="Opaque"} LOD 100 Pass { ZWrite On ZTest LEqual Blend Off Cull Back CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float3 viewDir : TEXCOORD1; float4 worldPos : TEXCOORD2; float3 normal : NORMAL; float4 vertex : POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; half4 _TopColor; half4 _BottomColor; half4 _RimColor; float _RimPower; float _HeightMax; float _HeightMin; float _WaveSpeed; float _WavePower; float _WaveLength; float _TransformPositionY; v2f vert (appdata v) { v2f o; o.worldPos = mul(unity_ObjectToWorld, v.vertex); o.vertex = mul(UNITY_MATRIX_VP,o.worldPos); o.normal = UnityObjectToWorldNormal(v.normal); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.viewDir = normalize(_WorldSpaceCameraPos - o.worldPos.xyz); return o; } half4 frag (v2f i) : SV_Target { float heightMax = _HeightMax + _TransformPositionY; float heightMin = _HeightMin + _TransformPositionY; float height = heightMax + sin((i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower; clip(height - i.worldPos.y); half4 col = tex2D(_MainTex, i.uv); float rate = saturate((i.worldPos.y - heightMin) / (heightMax - heightMin)); col.rgb *= lerp(_BottomColor.rgb, _TopColor.rgb, rate); float rim = 1 - saturate(dot(i.normal, i.viewDir)); col.rgb += pow(rim, _RimPower) * _RimColor; return col; } ENDCG } Pass { ZWrite On ZTest LEqual Blend Off Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { UNITY_FOG_COORDS(0) float4 worldPos : TEXCOORD3; float4 vertex : SV_POSITION; }; half4 _SurfaceColor; float _HeightMax; float _WaveSpeed; float _WavePower; float _WaveLength; float _TransformPositionY; v2f vert (appdata v) { v2f o; o.worldPos = mul(unity_ObjectToWorld, v.vertex ); o.vertex = mul(UNITY_MATRIX_VP,o.worldPos); return o; } half4 frag (v2f i) : SV_Target { float heightMax = _HeightMax + _TransformPositionY; float height = heightMax + sin( (i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower; clip(height - i.worldPos.y); half4 col = _SurfaceColor; return col; } ENDCG } } }
Script
using UnityEngine; public class Liquid : MonoBehaviour { [SerializeField] private Material _material; private int _propertyId; void Start() { _propertyId = Shader.PropertyToID("_TransformPositionY"); } void Update() { _material.SetFloat(_propertyId, transform.localPosition.y); transform.localPosition = new Vector3(0, (Mathf.Sin((Time.time)) * 2), 0); } }
Fragment側に渡す頂点情報
Fragment Shader側に渡す頂点情報は特に考えずUnityObjectToClipPos
を使用してしまっている節がありました。
o.vertex = UnityObjectToClipPos(v.vertex);
ですが、今回の場合事前に
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
でモデル座標を取得しています。
UnityObjectToClipPos
の定義は以下となっています。
// Tranforms position from object to homogenous space inline float4 UnityObjectToClipPos(in float3 pos) { #if defined(STEREO_CUBEMAP_RENDER_ON) return UnityObjectToClipPosODS(pos); #else // 今回はこちらを通る // More efficient than computing M*VP matrix product return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0))); #endif }
STEREO_CUBEMAP_RENDER_ON
は360度画像の時に使用されるもので、主にVRで使用されています。
ですので、今回の場合はelseを通ります。
else以降の文でモデル行列を計算してからビューとプロジェクションを計算していることがわかります。
今回の場合モデル行列を無駄に計算してしまうのでUnityObjectToClipPos
ではなくUNITY_MATRIX_VP
で頂点座標を計算しています。
波の計算
オブジェクトのローカル座標
Script側でShaderにオブジェクトのローカル座標を渡してあげることで、位置によって描画結果が変化してしまうのを防いでいます。
float heightMax = _HeightMax + _TransformPositionY; float heightMin = _HeightMin + _TransformPositionY;
描画結果が変わってしまう例
左側のオブジェクトは、y座標が変化しても形は変わっていません。
右のオブジェクトは、y座標が変化すると描画結果が変わってしまっています。。
波の揺れ
パラメータで乗算等していますが、重要なのは以下になります。
sin(i.worldPos.x + i.worldPos.z)
この計算で以下の画像のような揺れを表現しており、パラメータで揺れの強さを調整しています。
sin(i.worldPos.x + i.worldPos.z)のグラフ
波の切り抜き
Clip
は引数の値が0より小さい場合は描画を行わない関数となっています。
これを利用することで、波の揺れで計算したsinを用いた式を表現することができます。
clip(height - i.worldPos.y);
Clip前後
右がClip前、左がClip後となります。
波のリムライト
リムライトとは、モデルの後方からライトが当たっているような表現をすることです。
詳しくは以下で解説しています。
float rim = 1 - saturate(dot(i.normal, i.viewDir)); col.rgb += pow(rim, _RimPower) * _RimColor;
リムライト前後
左がリムライトを適応しているオブジェクトになります。
リムライトを適応しているオブジェクトの方が、明るくなっています。
波の背面を描画
今までは波の前面を描画していました。
前面の計算を応用し、背面を計算することより立体感をつけます。
また、Passを分けることによって軽量化に繋がります。
波の背面
左が背面あり、右が背面無しのオブジェクトになります。
ソースコードにコメントを付与
Shader "Unlit/Liquid" { Properties { _MainTex ("Texture", 2D) = "white" {} [Header(Shape)] _HeightMax ("Height Max", Float) = 1.0 _HeightMin ("Height Min", Float) = 0.0 _TopColor ("Top Color", Color) = (0.5, 0.75, 1,1) _BottomColor ("Bottom Color", Color) = (0, 0.25, 1, 1) [Header(Wave)] _WaveSpeed ("Wave Speed", Float) = 1.0 _WavePower ("Wave Power", Float) = 0.1 _WaveLength ("Wave Length", Float) = 1.0 [Header(Rim)] _RimColor ("Rim Light Color", Color) = (1, 1, 1, 1) _RimPower("Rim Light Power", Float) = 3 [Header(Surface)] _SurfaceColor ("Surface Color", Color) = (1, 1, 1, 1) [HideInInspector] _TransformPositionY ("Transform Position Y", float) = 0 } SubShader { Tags { "RenderType"="Opaque"} LOD 100 // 波の全面の描画 Pass { ZWrite On ZTest LEqual Blend Off Cull Back CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float3 viewDir : TEXCOORD1; float4 worldPos : TEXCOORD2; float3 normal : NORMAL; float4 vertex : POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; half4 _TopColor; half4 _BottomColor; half4 _RimColor; float _RimPower; float _HeightMax; float _HeightMin; float _WaveSpeed; float _WavePower; float _WaveLength; float _TransformPositionY; v2f vert (appdata v) { v2f o; o.worldPos = mul(unity_ObjectToWorld, v.vertex); // o.worldPosでモデル行列を出している // 最適化として、UnityObjectToClipPos()を使用せずに頂点を計算 o.vertex = mul(UNITY_MATRIX_VP,o.worldPos); o.normal = UnityObjectToWorldNormal(v.normal); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.viewDir = normalize(_WorldSpaceCameraPos - o.worldPos.xyz); return o; } half4 frag (v2f i) : SV_Target { // Script側からオブジェクトのローカル座標を渡す float heightMax = _HeightMax + _TransformPositionY; float heightMin = _HeightMin + _TransformPositionY; // sin(i.worldPos.x + i.worldPos.z)で波w表現 float height = heightMax + sin((i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower; // 0以下は描画しない clip(height - i.worldPos.y); half4 col = tex2D(_MainTex, i.uv); //グラデーション float rate = saturate((i.worldPos.y - heightMin) / (heightMax - heightMin)); col.rgb *= lerp(_BottomColor.rgb, _TopColor.rgb, rate); //リムライト float rim = 1 - saturate(dot(i.normal, i.viewDir)); col.rgb += pow(rim, _RimPower) * _RimColor; return col; } ENDCG } // 波の背面の描画 Pass { ZWrite On ZTest LEqual Blend Off Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; }; struct v2f { UNITY_FOG_COORDS(0) float4 worldPos : TEXCOORD3; float4 vertex : SV_POSITION; }; half4 _SurfaceColor; float _HeightMax; float _WaveSpeed; float _WavePower; float _WaveLength; float _TransformPositionY; v2f vert (appdata v) { v2f o; o.worldPos = mul(unity_ObjectToWorld, v.vertex ); o.vertex = mul(UNITY_MATRIX_VP,o.worldPos); return o; } half4 frag (v2f i) : SV_Target { float heightMax = _HeightMax + _TransformPositionY; float height = heightMax + sin( (i.worldPos.x + i.worldPos.z) * _WaveLength + _Time.w * _WaveSpeed) * _WavePower; clip(height - i.worldPos.y); half4 col = _SurfaceColor; return col; } ENDCG } } }
結果
液体のように揺れれば成功です。
今回は以上となります。
ここまでご視聴ありがとうございました。
【UniRx】その他のオペレータ #104
前回の成果
Observableが完了したときに処理を行うオペレータについてまとめた。
今回やること
その他のオペレータについてまとめます。
- 前回の成果
- 今回やること
- 購読時に指定したメッセージを最初の値として発行する
- 発行されたOnNextをキャッシュし、OnCompleted時にArrayとして1つのメッセージに変換
- 発行されたOnNextをキャッシュし、OnCompleted時にListとして1つのメッセージに変換
- メッセージの加工はせずに、ストリームのメッセージを用いて処理を行う
- メッセージの処理を行い、一番最後にUnitを流す
- 参考サイト様
購読時に指定したメッセージを最初の値として発行する
StartWith
購読時に引数に指定したメッセージを最初の値として発行します。
値だけではなく、関数も引数に指定することができます。
サンプルコード
/// <summary> /// Subscribe時に初期値を流す /// </summary> private void ExcuteStartWith() { Observable .Range(0, 3) .Select(x => x.ToString()) // Rangeより先に呼ばれる .StartWith("Red", "Green", "Blue") .Subscribe(x => { Debug.Log("StartWith OnNext : " + x); }, () => { Debug.Log("StartWith OnCompleted"); }); }
結果
先に呼ばれるはずのRangeの値より先に、引数で指定した"Red", "Green", "Blue"が呼ばれています。
その後、Rangeの値が発行されます。
発行されたOnNextをキャッシュし、OnCompleted時にArrayとして1つのメッセージに変換
ToArray
発行されたOnNextの値を全てキャッシュします。
OnCompleted時に、キャッシュされた値を1つのOnNextのArrayとして変換して、発行します。
サンプルコード
/// <summary> /// 発行されたOnNextをキャッシュし、OnCompleted時にArrayとして1つのメッセージに変換 /// </summary> private void ExcuteToArray() { Observable .Range(0, 3) .ToArray() .Subscribe(x => { Debug.Log("ToArray OnNext : " + x); foreach (var value in x) { Debug.Log("Value : " + value); } }, () => { Debug.Log("ToArray OnCompleted"); }); }
結果
Arrayに変換されています。
また、Arrayの中身がOnNextでキャッシュしたRangeの値となっています。
発行されたOnNextをキャッシュし、OnCompleted時にListとして1つのメッセージに変換
ToList
発行されたOnNextの値を全てキャッシュします。 OnCompleted時に、キャッシュされた値を1つのOnNextのListとして変換して、発行します。
サンプルコード
/// <summary> /// 発行されたOnNextをキャッシュし、OnCompleted時にListとして1つのメッセージに変換 /// </summary> private void ExcuteToList() { Observable .Range(0, 3) .ToList() .Subscribe(x => { Debug.Log("ToList OnNext : " + x); foreach (var value in x) { Debug.Log("Value : " + value); } }, () => { Debug.Log("ToList OnCompleted"); }); }
結果
Listに変換されています。 また、Listの中身がOnNextでキャッシュしたRangeの値となっています。
メッセージの加工はせずに、ストリームのメッセージを用いて処理を行う
Do〇〇
Do〇〇系のオペレータが該当します。
それぞれの呼ばれるタイミングは以下になります。
オペレータ名 | 呼ばれるタイミング |
---|---|
Do | OnNext |
DoOnCompleted | OnCompleted |
DoOnError | OnError |
DoOnSubscribe | Subscribe |
DoOnCancel | Dispose |
DoOnTerminate | OnCompletedとOnError |
ソースコード
/// <summary> /// Do系のメッセージ /// ストリームのメッセージを用いて処理を行う /// メッセージ自体の加工は行わない /// </summary> private void ExcuteDoOperators() { Observable .Range(0, 1) // OnNext時に呼ばれる .Do (x => Debug.Log("Call Do")) // OnCompleted時に呼ばれる .DoOnCompleted(() => Debug.Log("Call DoOnCompleted")) // 購読されたときに呼ばれる .DoOnSubscribe(() => Debug.Log("Call DoOnSubscribe")) // OnCompleted時かOnError時に呼ばれる .DoOnTerminate(() => Debug.Log("Call DoOnTerminate")) .Subscribe(); Subject<int> cancelSubject = new Subject<int>(); IDisposable dispose = cancelSubject // Dispose時に呼ばれる .DoOnCancel(() => Debug.Log("Call DoOnCancel")) .Subscribe(); dispose.Dispose(); Subject<int> errorSubject = new Subject<int>(); errorSubject // OnError時に呼ばれる .DoOnError(e => Debug.Log("Call DoOnError")) .Subscribe(); errorSubject.OnError(new Exception()); }
結果
上記に記載した表のタイミングで各オペレータが呼ばれています。
メッセージの処理を行い、一番最後にUnitを流す
ForEachAsync
OnNextが流れてきたときに処理を挟みたいけど、最終的なストリームにはOnNextとOnCompletedが1回だけ同時に呼ばれるようにしたい場合に使用します。
主にファイルのロード等の非同期処理に使用します。
また、行っていることはDo()とLast()とAsUnitObservable()を組み合わせたものと同じになります。
ソースコード
/// <summary> /// メッセージの処理を行い、一番最後にUnitを流す /// </summary> private void ExcuteForEachAsync() { Observable .Range(0, 3) .ForEachAsync(x => Debug.Log("ForEachAsync : " + x)) .Subscribe(x => { Debug.Log("Subscribe OnNext : " + x); }, () => { Debug.Log("Subscribe OnCompleted"); }); Debug.Log("--------Same--------"); // ForEachAsyncを使用しない場合 Observable .Range(0, 3) .Do(x => Debug.Log("Do : " + x)) .Last() .AsUnitObservable() .Subscribe(x => { Debug.Log("Subscribe OnNext : " + x); }, () => { Debug.Log("Subscribe OnCompleted"); }); }
結果
最終的なストリームには、OnNextとOnCompletedが1回しか流れていません。
また、ForEachAsyncを使用した場合とDo()Last()AsUnitObservable()を組み合わせた場合でも結果が同じです。
今回は以上となります。
ここまでご視聴ありがとうございました。
参考サイト様
【UniRx】Observableが完了時に処理を行うオペレータ #103
前回の成果
エラーハンドリングのオペレータをまとめた。
今回やること
Observableが完了時に処理を行うオペレータをまとめます。
- 前回の成果
- 今回やること
- ストリームのOnCompletedが呼ばれたら、同じストリームを生成する
- 同じストリームを指定回数生成する
- 短時間にOnCompletedが呼ばれた場合、Repeatを止める
- ストリームのOnCompletedが呼ばれたら、同じストリームを生成し、指定したGameObjectが非表示になったら、Repeatを中止する
- ストリームのOnCompletedが呼ばれたら、同じストリームを生成し、指定したGameObjectが破棄されたら、Repeatを中止する
- ストリームが完了時、例外発生時、破棄時に処理を行う
- 参考サイト様
ストリームのOnCompletedが呼ばれたら、同じストリームを生成する
Repeat
OnCompletedが呼ばれた際に、同じストリームを生成してSubscribeします。
ただし、このオペレータは無限ループが発生するので注意してください。
可能ならば、下記で紹介するRepeatUntilDisableやRepeatUntilDestoryを使用した方が良いケースが多いです。
ソースコード
/// <summary> /// ストリームのOnCompletedが呼ばれたら、同じストリームを生成する /// </summary> private void ExcuteRepeat() { Observable .Timer(TimeSpan.FromSeconds(1)) .Do(_ => Debug.Log("Do")) .DoOnCompleted(() => Debug.Log("DoOnCompleted")) .Repeat() .Subscribe(); }
結果
Timerで1秒後にメッセージを発行し、OnCompletedが発行されています。
OnCompletedが発行されたので、再びSubscribeをしています。
こちらの例では、無限ループしています。
同じストリームを指定回数生成する
Repeat
Repeatの第一引数に発行する値を、第二引数に回数を指定することが出来ます。
定義は以下のようになっています。
/// <summary> /// 回数指定のRepeat /// </summary> /// <param name="value">発行する値</param> /// <param name="repeatCount">Repeatの回数</param> public static IObservable<T> Repeat<T>(T value, int repeatCount) { return Repeat(value, repeatCount, Scheduler.DefaultSchedulers.Iteration); }
ソースコード
/// <summary> /// 同じストリームを指定回数生成する /// </summary> private void ExcuteRepeatCount() { Observable .Repeat("Repeat", 3) .Subscribe(x => { Debug.Log("OnNext : " + x); }, () => { // OnCompletedは1回しか呼ばれない Debug.Log("OnCompleted"); }); }
結果
OnNextが第二引数で指定した回数呼ばれ、その後OnCompletedが呼ばれています。
引数無しのRepeatと異なり、OnCompletedは一度しか呼ばれません。
短時間にOnCompletedが呼ばれた場合、Repeatを止める
RepeatSafe
OnCompletedが短時間で呼ばれるRepeatは無限ループが挙げられます。
そのようなケースが発生した場合に、このオペレータを使用することで防止することができます。
ただし、意図的に制御することができません。
ですので、以下で紹介するRepeatUntilDisable やRepeatUntilDestoryを使用するのをおすすめします。
ソースコード
/// <summary> /// 短時間にOnCompletedが呼ばれた場合、Repeatを止める /// 意図的に制御できない /// </summary> private void ExcuteRepeatSafe() { Subject<Unit> subject = new Subject<Unit>(); subject .Do(_ => { Debug.Log("Do"); }) .DoOnCompleted(() => { Debug.Log("DoOnCompleted"); }) // Repeatだと無限ループしてしまう //.Repeat() .RepeatSafe() .Subscribe(x => { Debug.Log("OnNext : " + x); }, () => { Debug.Log("OnCompleted"); }); subject.OnCompleted(); }
結果
Repeatですと無限ループしてしまうのを防ぐことができます。
ストリームのOnCompletedが呼ばれたら、同じストリームを生成し、指定したGameObjectが非表示になったら、Repeatを中止する
RepeatUntilDisable
引数で指定したGameObjectが非表示になった際に、Repeatを中止することができます。
RepeatSafeと違い、Repeatを中止するタイミングを制御できます。
ソースコード
/// <summary> /// ストリームのOnCompletedが呼ばれたら、同じストリームを生成する /// 指定したGameObjectが非表示になったら、Repeatを中止する /// </summary> private void ExcuteRepeatUntilDisable() { this.UpdateAsObservable() .Where(_ => Input.anyKeyDown) .Subscribe(_ => { Debug.Log("anyKeyDown"); gameObject.SetActive(false); }); Observable .Timer(TimeSpan.FromSeconds(1)) .Do(_ => Debug.Log("Do")) .RepeatUntilDisable(gameObject) .Subscribe(x => { Debug.Log("OnNext : " + x); }, () => { Debug.Log("OnCompleted"); }); }
結果
指定したGameObject(今回の場合、ObservableCompleted)が非表示になった場合にRepeatが中止されています。
ストリームのOnCompletedが呼ばれたら、同じストリームを生成し、指定したGameObjectが破棄されたら、Repeatを中止する
RepeatUntilDestroy
引数で指定したGameObjectが破棄された際に、Repeatを中止することができます。 RepeatSafeと違い、Repeatを中止するタイミングを制御できます。
ソースコード
/// <summary> /// ストリームのOnCompletedが呼ばれたら、同じストリームを生成する /// 指定したGameObjectが破棄されたら、Repeatを中止する /// </summary> private void ExcuteRepeatUntilDestory() { this.UpdateAsObservable() .Where(_ => Input.anyKeyDown) .Subscribe(_ => { Debug.Log("anyKeyDown"); Destroy(gameObject); }); Observable .Timer(TimeSpan.FromSeconds(1)) .Do(_ => Debug.Log("Do")) .RepeatUntilDestroy(gameObject) .Subscribe(x => { Debug.Log("OnNext : " + x); }, () => { Debug.Log("OnCompleted"); }); }
結果
指定したGameObject(今回の場合、ObservableCompleted)が破棄された場合にRepeatが中止されています。
ストリームが完了時、例外発生時、破棄時に処理を行う
DoOnTerminate / Finally
ストリームのOnCompleted、OnError、Dispose時に処理を行うことができます。
DoOnTerminateとFinallyの違いは以下になります。
\ | 完了時 | 例外時 | 破棄時 |
---|---|---|---|
DoOnTerminate | 呼ばれる | 呼ばれる | 呼ばれない |
Finally | 呼ばれる | 呼ばれる | 呼ばれる |
ソースコード
/// <summary> /// ストリーム完了時のDoOnTerminateとFinallyの違い /// </summary> private void ExcuteDiffCompleted() { // DoOnTerminateとFinallyの両方が呼ばれる CreateSubject().OnCompleted(); } /// <summary> /// 例外発生時のDoOnTerminateとFinallyの違い /// </summary> private void ExcuteDiffError() { // DoOnTerminateとFinallyの両方が呼ばれる CreateSubject().OnError(new Exception()); } /// <summary> /// ストリーム破棄時のDoOnTerminateとFinallyの違い /// </summary> private void ExcuteDiffDispose() { // Finallyのみ呼ばれる CreateDisposable().Dispose(); } /// <summary> /// DoOnTerminateとFinallyの違いのSubject生成 /// </summary> private Subject<Unit> CreateSubject() { Subject<Unit> subject = new Subject<Unit>(); subject // 例外発生時 .DoOnError (e => Debug.Log("DoOnError")) // ストリーム完了時 .DoOnCompleted(() => Debug.Log("DoOnCompleted")) // ストリーム破棄時 .DoOnCancel (() => Debug.Log("DoOnCancel")) // ストリーム完了、例外発生時 .DoOnTerminate(() => Debug.Log("DoOnTerminate")) // ストリーム完了、ストリーム破棄、例外発生時 .Finally (() => Debug.Log("Finally")).Subscribe(); return subject; } /// <summary> /// DoOnTerminateとFinallyの違いのDisposable生成 /// </summary> private IDisposable CreateDisposable() { Subject<Unit> subject = new Subject<Unit>(); return subject // 例外発生時 .DoOnError (e => Debug.Log("DoOnError")) // ストリーム完了時 .DoOnCompleted(() => Debug.Log("DoOnCompleted")) // ストリーム破棄時 .DoOnCancel (() => Debug.Log("DoOnCancel")) // ストリーム完了、例外発生時 .DoOnTerminate(() => Debug.Log("DoOnTerminate")) // ストリーム完了、ストリーム破棄、例外発生時 .Finally (() => Debug.Log("Finally")).Subscribe(); }
ストリーム完了時の結果
DoOnTerminateもFinallyも呼ばれています。
ストリーム例外発生時の結果
DoOnTerminateもFinallyも呼ばれています。
ストリーム破棄時の結果
Finallyのみ呼ばれています。
今回は以上となります。
ここまでご視聴ありがとうございました。