🖼️

隠れたオブジェクトをスキャンで表示する(URP/Render Objects)

に公開

はじめに

この記事は サイバーエージェント26卒内定者 Advent Calendarの2日目として投稿しています。
2日目も投稿できるなんて光栄ですね。前世もいいことをしたのでしょう

この記事でできること

Unity URP環境で、
最終的に以下のようなスキャン表現を実装します!

(Ap○xの某キャラクターのスキルを参考にしています)

この記事の対象

基本的なUnityの操作ができることを前提としています。
新たなプロジェクトを開いた状態からこの記事はスタートします。

作成環境

Unity 6000.0.58f2 URP 3D

まずはRender Objectsを利用してオブジェクトに隠れるものを表示する

今回はまず隠れるオブジェクトと隠すオブジェクトを適当に用意します。

ちなみにこの記事ではこのアセットのキャラクターを隠します

https://assetstore.unity.com/packages/3d/characters/creatures/dino-shark-211520

このようにシーン内に2つのオブジェクトを配置します。

マテリアル、レイヤーの設定を行う

今回はLayerに新しくSeeBehindを追加し、隠した際に影にしたいメッシュを持ったオブジェクトを選択して設定します。

マテリアルは困ったら Universal Render PipelineのUnlit か Litにしておきましょう

影用のマテリアルを作成する

適当に新しいマテリアルを作成します。本記事内では「Shadow」と名付けて、Universal Render PipelineのUnlit でBasemapを青色にしています。

Render Objectsを使用して描画順を設定する

Render Objects は、URP の RendererFeature の一種で、特定のオブジェクトだけを特定の設定で描き直すための仕組みです。

つまり、レイヤーで対象オブジェクトを絞って、どのタイミングで描くか(RenderPassEvent)を選んで、特定の書き方で描きこむ、それをGUIで設定できるって感じっぽいです。

今回はOpaqueの描画の後に、SeeBehindを描画します

RendererとProjectビューで検索すると何かしら出てくるかと思います。
今回はUnityEditor上での実装のため、PC_Rendererを編集します。

Add Renderer Feature
を2回押して、

以下の画像のように設定します。

具体的には、一つ目では SeeBehind レイヤーのオブジェクトに対して、Opaque を描画した後、深度テスト(Greater)の結果 手前のオブジェクトに隠れて見えるはずの部分 だけを、指定したマテリアルで描いています。

隠れた部分だけを描くだけでは本来の表示が欠けてしまうため、通常の描画も行うために、2つ目の RenderFeature を追加しています。

スキャン表現を実装する

これで壁の裏側のオブジェクトを透けて見える影のように映すことができましたね。
どうせなら普段は写っていなくて、何か操作をすると映るような挙動にしたかったのでそこまで作ってみます。

影用のマテリアルをアップデートする

新たにScanShadow.shaderを追加します

Shader "Custom/ScanShadow"
{
    Properties
    {
        _Color("Color", Color) = (0, 0.7, 1, 1)
        _Radius("Radius", Range(0, 1)) = 0
        _EdgeWidth("Edge Width", Range(0, 0.5)) = 0.05
    }

    SubShader
    {
        Tags
        {
            "RenderType"="Transparent"
            "Queue"="Transparent"
            "RenderPipeline"="UniversalRenderPipeline"
        }

        Blend SrcAlpha OneMinusSrcAlpha
        ZWrite Off

        Pass
        {
            Name "ScanShadowPass"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex   vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float4 screenPos  : TEXCOORD0;
            };

            CBUFFER_START(UnityPerMaterial)
                float4 _Color;
                float  _Radius;
                float  _EdgeWidth;
            CBUFFER_END

            Varyings vert (Attributes IN)
            {
                Varyings OUT;

                float4 posCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.positionCS = posCS;
                OUT.screenPos  = ComputeScreenPos(posCS);
                return OUT;
            }

            half4 frag (Varyings IN) : SV_Target
            {
                // 0〜1 のスクリーン UV
                float2 uv = IN.screenPos.xy / IN.screenPos.w;

                // 画面中心からの距離
                float2 center = float2(0.5, 0.5);
                float dist = distance(uv, center);

                // 中心から _Radius までが「表示範囲」
                // _Radius - _EdgeWidth まではフルで表示、それ以降でフェードアウト
                float edgeStart = max(_Radius - _EdgeWidth, 0.0);
                float edgeEnd   = _Radius;
                
                float mask = 1.0 - smoothstep(edgeStart, edgeEnd, dist);
                mask = saturate(mask);

                if (mask <= 0.001)
                    discard;

                float3 col   = _Color.rgb;
                float  alpha = _Color.a * mask;

                return half4(col, alpha);
            }
            ENDHLSL
        }
    }

    FallBack Off
}

画面の中心にどのくらい近いかで描画するかを変えられるShaderです。

このShaderを先ほど作ったShadowマテリアルに適用します

C#スクリプトを追加する

using System.Collections;
using UnityEngine;

public class ScanShadowController : MonoBehaviour
{
    [SerializeField] private Material scanMaterial;
    [SerializeField] private float duration = 1.5f;
    [SerializeField] private float edgeWidth = 0.05f;
    [SerializeField] private KeyCode triggerKey = KeyCode.Space;

    private static readonly int RadiusID    = Shader.PropertyToID("_Radius");
    private static readonly int EdgeWidthID = Shader.PropertyToID("_EdgeWidth");

    private bool isPlaying;

    private void Start()
    {
        if (scanMaterial == null) return;
        scanMaterial.SetFloat(EdgeWidthID, edgeWidth);
        scanMaterial.SetFloat(RadiusID, 0f); // 最初は非表示
    }

    private void Update()
    {
        if (scanMaterial == null) return;

        if (Input.GetKeyDown(triggerKey) && !isPlaying)
        {
            StartCoroutine(PlayScan());
        }
    }

    private IEnumerator PlayScan()
    {
        isPlaying = true;

        // 表示
        float t = 0f;
        while (t < duration)
        {
            t += Time.deltaTime;
            float n = Mathf.Clamp01(t / duration);
            scanMaterial.SetFloat(RadiusID, n);
            yield return null;
        }

        // 非表示
        t = 0f;
        while (t < duration)
        {
            t += Time.deltaTime;
            float n = 1f - Mathf.Clamp01(t / duration);
            scanMaterial.SetFloat(RadiusID, n);
            yield return null;
        }

        isPlaying = false;
    }
}

こちらはマテリアルをアタッチすることで、指定のキーを押すと先ほど作成したマテリアルの値を変更するものになります。

完成

参考

https://unity.com/ja/resources/introduction-to-urp-advanced-creators-unity-6

Discussion