知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】投影テクスチャマッピング #109

前回の成果

オブジェクトの影を受け取った。

soramamenatan.hatenablog.com


今回やること

投影テクスチャマッピングを実装します。

light11.hatenadiary.com


事前準備

Scene上にGameObjectを配置し、今回制作したスクリプトをアタッチします。
f:id:soramamenatan:20210711184305p:plain

スクリーンとなるオブジェクトには、Materialをアタッチします。

f:id:soramamenatan:20210711184301p:plain


ソースコード

スクリプト

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
        }
    }
}


投影テクスチャマッピング

テクスチャをプロジェクターで写したかのように描画する技術のことです。

f:id:soramamenatan:20210707084730j:plain wgld.org | WebGL: 射影テクスチャマッピング |:より引用

射影テクスチャマッピングとも呼ばれます。


ProjectionType

カメラがシーンを投影する際に、どのような方法で投影するかを決めるものになります。
投影の手法は2つ存在します。

透視投影(perspective)

近くのオブジェクトを大きく、遠くのオブジェクトを小さく描画する手法になります。
主に3Dゲームで用いられる手法になります。

f:id:soramamenatan:20210708091928j:plain

Unity - Manual: Cameras:より引用

正投影(orthographic)

オブジェクトの距離では大きさを変えない投影手法になります。
主に2Dゲームで用いられる手法になります。

f:id:soramamenatan:20210708092110j:plain

Unity - Manual: Cameras:より引用

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

f:id:soramamenatan:20210707091854p:plain

Camera.worldToCameraMatrix

f:id:soramamenatan:20210707091850p:plain


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の範囲となっています。
そこでsignstepを組み合わせて0~1の範囲外は描画しないようにしています。

グラフ
赤 : (x-0.5) * sign(x)
青 : step((x-0.5) * sign(x), 0.5)

f:id:soramamenatan:20210711182917p:plain

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

f:id:soramamenatan:20210711184045p:plain

Orthographic

f:id:soramamenatan:20210711184050p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。

参考サイト様

wgld.org

light11.hatenadiary.com

【UnityShader】オブジェクトの影を受け取る #108

前回の成果

オブジェクトの影を落とした。

soramamenatan.hatenablog.com


今回やること

前回のコードのままですと、他のオブジェクトからの影を受け取ることができません。
なので、その部分を修正します。

影を受け取れていない

手前のオブジェクトが前回、影を落としたオブジェクトになります。
奥のオブジェクトはUnityのデフォルトのCubeになります。

f:id:soramamenatan:20210613143028p:plain


事前準備

オブジェクトを配置し、配置したオブジェクトの影が当たる位置にオブジェクトを配置します。
影が当たる位置のオブジェクトに今回制作するマテリアルをアタッチします。

f:id:soramamenatan:20210613143140p:plain


ソースコード

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、ライトマップを適応するものになります。
詳しくは以下の記事を参考にしてください。

soramamenatan.hatenablog.com


シェーダーバリアント

#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
の手順で確認できます。

f:id:soramamenatan:20210615084646p:plain

今回のシェーダーですと、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 フォワードレンダリングで、ライトプローブと頂点ライティングを適応しない

他にもいくつかあります。
詳しくは、公式のリファレンスを参照してください。

docs.unity3d.com


組み込みシェーダーのinclude

#include "Lighting.cginc"
#include "AutoLight.cginc"

Lighting.cginc

サーフェスシェーダーにおける標準的な照明モデルになります。
サーフェスシェーダーを書く時には自動的に含まれます。
今回の場合、_LightColor0を使用するためにincludeしています。

AutoLight.cginc

ライティングとシャドウの機能になります。
サーフェスシェーダーの場合、この組み込みシェーダーを内部的に使用しています。
今回の場合、SHADOW_COORDSTRANSFER_SHADOWSHADOW_ATTENUATIONを使用するためにincludeしています。


v2f構造体のSV_POSITION

struct v2f {
    // 省略
    float4 pos : SV_POSITION;
};

影を使用する際には、SV_POSITION名をposにしないとエラーが出てしまいます。

f:id:soramamenatan:20210616090145p:plain

これはvertexからfragmentへ変換を行う際のマクロでposが決め打ちとなっているからになります。
今回ですとTRANSFER_SHADOWでposが決め打ちとなっています。

forum.unity.com

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値で深度を参照するからになります。

シャドウマップの例

f:id:soramamenatan:20210618090034p:plain

【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_PSP2PlayStation 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のデフォルトシェーダーのオブジェクトになります。
手前のオブジェクトにも影が落ちるようになりました。

f:id:soramamenatan:20210623091736p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。


参考サイト様

qiita.com

【UnityShader】オブジェクトの影を落とす #107

前回の成果

Bloomシェーダーについて学んだ。

soramamenatan.hatenablog.com


今回やること

シェーダーでの影について学んでいきます。


事前準備

Scene上にCubeを配置します。
Cubeの影が出るようにPlaneを配置して準備は完了となります。

f:id:soramamenatan:20210612151136p:plain


Unityのデフォルトのマテリアルを使用する際には特に意識せずとも、オブジェクトの影が表示されています。
ですが、自分でシェーダーを使用し、そのシェーダーを元にしたマテリアルを使用すると影の表示がなくなってしまいます。

影が出ない例

UnlitShaderを制作し、そのマテリアルをアタッチしています。
Planeに影が出ていません。

f:id:soramamenatan:20210612151443p:plain

なので、シェーダーで影を描画していきます。


シェーダー

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" }

RenderTypeQueueと同じTagになります。
このタグは、ライティングに使用されるTagになっています。
組み込みのレンダリングパイプラインで、LightModeTagを設定しない場合には、Unityはライティングや影なしで描画しようとします。
ですので、UnlitShaderだと影やライティングが描画されません。
今回は、ShadowCasterを指定しているので、オブジェクトの深度をシャドウマップから深度テクスチャにレンダリングすることをUnityに伝えています。

LightMode Tagの一例

Tag名 意味
Always 常にレンダリングされ、ライティングは適応されない
デフォルト値
ForwardBase Forwardレンダリングで使用
アンビエント、メインのDirectionalLight、Vertex/SHLight、ライトマップを適応
Deferred DeferredShading(遅延シェーディング)で使用
g-Bufferをレンダリング
ShadowCaster オブジェクトの深度をシャドウマップから深度テクスチャにレンダリング

他にもLightMode Tagは種類があるのですが、一部割愛させていただきます。
以下リファレンスに他の種類が記載されています。

docs.unity3d.com


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コンポーネント

f:id:soramamenatan:20210613113832p:plain

もう片方の条件となっている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


シャドウアクネ

少し脱線してしまいますが、シャドウアクネについて解説します。

以下の画像でシャドウアクネが発生していることが確認できます。
オブジェクトの影が描画されているのですが、影の他に縦方向の縞模様のようなものができてしまっています。
それがシャドウアクネと呼ばれるものになります。

f:id:soramamenatan:20210613121053j:plain 影 - Unity マニュアル:より引用


発生の原因

シャドウマップで指定された距離のピクセルが遠くにあるように計算されてしまう場合に発生します。


以下の画像のように本来ポリゴンは頂点AからBにかけてなめらかになっています。
それがシャドウマップのテクセルによってZ値でまとめられ、一定の間隔で区切られてしまいます。

f:id:soramamenatan:20210613121459p:plain ☆PROJECT ASURA☆ [Direct3D 11] 『シャドウマッピングの基本』:より引用

シャドウマップを使用し影をつけることは、この一定間隔に区切られたポリゴンとの比較をすることになります。
つまり以下画像の緑色の箇所が影と判定されてしまいます。
なので縞模様ができてしまっています。

f:id:soramamenatan:20210613121502p:plain ☆PROJECT ASURA☆ [Direct3D 11] 『シャドウマッピングの基本』:より引用

対策として最も良いのは、1テクセルの大きさを小さくしてシャドウマップの解像度を上げることになります。
ですがこの方法ですとパフォーマンスが著しく落ちてしまいます。
なので、バイアスをかけてあげることによって対策をしています。

f:id:soramamenatan:20210613121506p:plain ☆PROJECT ASURA☆ [Direct3D 11] 『シャドウマッピングの基本』:より引用

ただし、バイアスをかけすぎてしまうとオブジェクトと影が離れてしまうピーターパン現象と呼ばれるものが発生します。

ピーターパン現象の例

f:id:soramamenatan:20210613121958j:plain 影 - Unity マニュアル:より引用


試してみる

実際にUnity上で試してみたいと思います。

特になにも考えずにオブジェクトをScene上に配置します。
違和感なく影が出ているのがわかるかと思います。

通常の影

f:id:soramamenatan:20210613122132p:plain

LightコンポーネントBiasNormalBiasの値を0にしてみます。
そうすると、オブジェクトの影自体は問題なく描画されていますが、Planeに縞模様ができてしまっています。

シャドウアクネ

f:id:soramamenatan:20210613122146p:plain

逆にLightコンポーネントBiasNormalBiasの値を最大値にしてみます。
そうすると、オブジェクトと影との距離が離れてしまっています。

ピーターパン現象

オブジェクトの大きさがそのままだと現象が発生していることが確認しにくかったので、縦方向にスケールさせています。

f:id:soramamenatan:20210613122159p:plain


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 .xunity_LightShadowBias .yにはこのブログを書いている段階では情報がありませんでした。
ですが、おそらくLightコンポーネントBiasの値を元に処理をしているものだと思われます。

Bias

f:id:soramamenatan:20210613123237p:plain

深度値のものである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に落ちていることが確認できます。

f:id:soramamenatan:20210613135346p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。


参考サイト様

www.project-asura.com

qiita.com

UnityでForwardのライトに対応したLambert反射モデルのシェーダを作成する | 測度ゼロの抹茶チョコ

【UnityShader】Bloomシェーダー #106

前回の成果

3Dモデルを液体のようにするシェーダーについて学んだ。

soramamenatan.hatenablog.com


今回やること

以下のサイト様を参考に、Bloomシェーダーを実装します。

www.shibuya24.info

事前準備

Scene上にSpriteRendererを配置します。

f:id:soramamenatan:20210605182519p:plain

そして、MainCameraに今回使用するScriptをアタッチして事前準備完了となります。

f:id:soramamenatan:20210605182529p:plain

ソースコード

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とは

ブルームとは、光源から光があふれ出るようなエフェクトのことを指します。ゲームでは光源は数えきれないほど存在します。太陽の光、反射するミラーやガラス、スポットライトやサーチライトといったオブジェクトの光だけでなく、シューティングゲーム弾幕や、スキルエフェクトなども光源として扱われます。

グラフィック設定のブルームとは | ゲーミングPCなう:より引用

とのことです。
このエフェクトを使用することで、オブジェクトにライティングを付与させてリッチな表現にすることができます。

Bloom適応前

f:id:soramamenatan:20210605183030j:plain

Bloom - Unity マニュアル:より引用

Bloom適応後

f:id:soramamenatan:20210605183037j:plain Bloom - Unity マニュアル:より引用

BloomはPostProcessVolumeコンポーネントから追加することもできます。

光らせる箇所の取得

フラグメントシェーダーでテクセルの色を取得し、スクリプトで渡したしきい値とstep()します。
そうすることで、しきい値以上の明るさを箇所を2Pass目に渡して処理することができます。

// ピクセルの明るさ
float bright = (col.r + col.g + col.b) / 3;
// しきい値によって明るさを決める
float tmp = step(_Threshold, bright);
光らせる箇所

しきい値が0.8の時の2Pass目にわたす箇所になります。
2Pass目で元画像に合成するため、黒くしておくことで計算を楽にできるようにしています。

f:id:soramamenatan:20210605191123p:plain


ブラー処理

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);

今回は以前勉強した、ガウシアンブラーを使用しています。
このブラーを使用することで、より軽量な計算でブラー処理を行うことが出来ます。
詳しくは以下を参考にしてください。

soramamenatan.hatenablog.com

ガウシアンブラーの横方向サンプリング結果

f:id:soramamenatan:20210605191836p:plain

ガウシアンブラーの縦方向サンプリング結果

f:id:soramamenatan:20210605191847p:plain


スクリプト

備忘録程度に記載します。

OnRenderImage

カメラのレンダリングが完了した時に呼ばれる関数になります。
第一引数には、入力画像が、第二引数には出力画像が渡されます。

ExecuteInEditMode

UnityをPlayモードにせず、Editモードのままで実行することが出来ます。
ただし、実行できるのは以下の3つのみになります。

  • Update
  • OnGUI
  • OnRenderObject
Playモード

PlayモードはUnityの右三角が押されている状態のことを指します。

f:id:soramamenatan:20210605192401p:plain


結果

SpriteRendererにBloomエフェクトがかかれば成功です。

f:id:soramamenatan:20210605192608p:plain

また、普通のオブジェクトに対しても使用することができます。

f:id:soramamenatan:20210605192621p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

【UnityShader】3Dモデルを液体のようにする #105

前回の成果

その他のオペレータについてまとめた。

soramamenatan.hatenablog.com


今回やること

以下のサイト様を参考に、3Dモデルを液体のようにするシェーダーを制作します。


qiita.com


事前準備

Scene上に適当な3Dモデルを配置します。 そして、今回制作するScriptとMaterialをアタッチしてください。

f:id:soramamenatan:20210522124010p:plain

f:id:soramamenatan:20210522124019p:plain

ソースコード

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座標が変化すると描画結果が変わってしまっています。。

f:id:soramamenatan:20210522150243g:plain

波の揺れ

パラメータで乗算等していますが、重要なのは以下になります。

sin(i.worldPos.x + i.worldPos.z)

この計算で以下の画像のような揺れを表現しており、パラメータで揺れの強さを調整しています。

sin(i.worldPos.x + i.worldPos.z)のグラフ

f:id:soramamenatan:20210522150639p:plain

波の切り抜き

Clipは引数の値が0より小さい場合は描画を行わない関数となっています。
これを利用することで、波の揺れで計算したsinを用いた式を表現することができます。

clip(height - i.worldPos.y);
Clip前後

右がClip前、左がClip後となります。

f:id:soramamenatan:20210522152224p:plain

波のリムライト

リムライトとは、モデルの後方からライトが当たっているような表現をすることです。
詳しくは以下で解説しています。

soramamenatan.hatenablog.com

float rim = 1 - saturate(dot(i.normal, i.viewDir));
col.rgb += pow(rim, _RimPower) * _RimColor;
リムライト前後

左がリムライトを適応しているオブジェクトになります。
リムライトを適応しているオブジェクトの方が、明るくなっています。

f:id:soramamenatan:20210522153029p:plain

波の背面を描画

今までは波の前面を描画していました。
前面の計算を応用し、背面を計算することより立体感をつけます。
また、Passを分けることによって軽量化に繋がります。

波の背面

左が背面あり、右が背面無しのオブジェクトになります。

f:id:soramamenatan:20210522153537p:plain


ソースコードにコメントを付与

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
        }
    }
}

結果

液体のように揺れれば成功です。

f:id:soramamenatan:20210522154045g:plain

今回は以上となります。
ここまでご視聴ありがとうございました。
f:id:soramamenatan:20210522154045g:plain

【UniRx】その他のオペレータ #104

前回の成果

Observableが完了したときに処理を行うオペレータについてまとめた。

soramamenatan.hatenablog.com

今回やること

その他のオペレータについてまとめます。


購読時に指定したメッセージを最初の値として発行する

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の値が発行されます。

f:id:soramamenatan:20210515151641p:plain


発行された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の値となっています。

f:id:soramamenatan:20210515152317p:plain


発行された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の値となっています。

f:id:soramamenatan:20210515152637p:plain


メッセージの加工はせずに、ストリームのメッセージを用いて処理を行う

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());
}
結果

上記に記載した表のタイミングで各オペレータが呼ばれています。

f:id:soramamenatan:20210515153418p:plain


メッセージの処理を行い、一番最後に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()を組み合わせた場合でも結果が同じです。

f:id:soramamenatan:20210515153802p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。


参考サイト様

nobollel-tech.hatenablog.com

light11.hatenadiary.com

light11.hatenadiary.com

【UniRx】Observableが完了時に処理を行うオペレータ #103

前回の成果

エラーハンドリングのオペレータをまとめた。

soramamenatan.hatenablog.com


今回やること

Observableが完了時に処理を行うオペレータをまとめます。



ストリームの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をしています。
こちらの例では、無限ループしています。

f:id:soramamenatan:20210513091617p:plain


同じストリームを指定回数生成する

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は一度しか呼ばれません。

f:id:soramamenatan:20210513092050p:plain


短時間に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ですと無限ループしてしまうのを防ぐことができます。

f:id:soramamenatan:20210515132244p:plain


ストリームの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が中止されています。

f:id:soramamenatan:20210515132627p:plain


ストリームの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が中止されています。

f:id:soramamenatan:20210515132912p:plain


ストリームが完了時、例外発生時、破棄時に処理を行う

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も呼ばれています。

f:id:soramamenatan:20210515133553p:plain

ストリーム例外発生時の結果

DoOnTerminateもFinallyも呼ばれています。

f:id:soramamenatan:20210515133610p:plain

ストリーム破棄時の結果

Finallyのみ呼ばれています。

f:id:soramamenatan:20210515133627p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。

参考サイト様

light11.hatenadiary.com

light11.hatenadiary.com