📝

【Unityシェーダーグラフ】円柱を変形させてスポットライト表現を作る

2021/02/11に公開

はじめに

ShaderGraph(シェーダーグラフ)でスポットライト表現を作ってみました。
Unity標準のCylinderをShaderGraphの頂点シェーダーで変形させることでスポットライト表現を作っています。

スポットライト表現

環境

Unity 2020.2.0f1
UniversalRP 10.2.2

頂点シェーダーを使ったモデル変形

3Dモデルのバウンディングボックス(モデルを囲む仮想の箱)を考えます。

箱の頂点を動かして四角錐にすると、内側のモデルが変形します。

HoudiniのTaperを使った変形

円柱に対してこの変形を適用すると円錐型のモデルになります。

円柱を円錐に変形

UnityのShaderGraphには、箱を変形するという演算は用意されていないので、
頂点座標の変換計算を自分で書くことになります。

§1 : 円柱を円錐に変形する

STEP1 : HLSLファイルの作成

以下のHLSLファイルをUnityプロジェクトに追加します。
これは、3Dモデルを囲む箱を四角錘へ変形させるような演算を行います。

bounding_box_convert.hlsl
void BoundingBoxConvert_float(
    float3 Position, // モデルの頂点座標
    float3 BoundingMin, // バウンディングボックスの最小座標
    float3 BoundingMax, // バウンディングボックスの最大座標
    out float3 Output // 変換後の頂点座標
    ) 
{
    // モデル頂点のy座標が 0 ~ 1 の範囲に収まるように座標変換
    float y = (Position.y - BoundingMin.y) / (BoundingMax.y - BoundingMin.y);

    // バウンディングボックスを四角錐に変形
    float2 centerXZ = (BoundingMin.xz + BoundingMax.xz) / 2.0; // 中心座標
    Position.xz = lerp(Position.xz, centerXZ, y); // yが1に近づくにつれて、XZ座標は中心座標に近づく

    // 変換後の頂点を出力
    Output = Position;
}

上記HLSLのBouningMin, BoundingMax には、3Dモデルを覆う箱の最小座標、最大座標を指定します。

STEP2 : ShaderGraph 作成

以下のShaderGraphを作成します。

Custom Function は 以下のように設定します (Graph Inspector)
先ほど作成したHLSLファイルはCustom Functionに指定しています。

BoundingMin = (-1, -1, -1), BoundingMax = (1, 1, 1) を指定して、
Cylinderをぴったり覆うような箱を指定します。

結果

このShaderGraphからマテリアルを作成し、Unity標準のCylinderモデルにアタッチすると円錐の形になります。

§2 : スポットライト表現を作る

円錐にグラデーションをつけてスポットライトっぽい見た目にします。

ShaderGraphの変更

Unity標準のCylinderモデルは高さ方向にUVのy成分が割り当てられています。
これを利用して縦方向のグラデーションをつけてみたいと思います。

ShaderGraphに以下のようなノードを追加します。

UV.y を Colorとして出力


Shader Graph は Transparent / Additive に設定

結果

§3 : スポットライトを動かせるようにする

座標を指定して、そこをスポットライトが照らすようにします。

指定した座標をスポットライトが照らす

STEP1 : プロパティの追加

ShaderGraphのプロパティに ライトを向けたい目標位置 TargetPosition を追加します。

このTargetPositionは、モデルの足元を原点(0,0,0)とした座標になるため、
円柱モデルの足元をシーンの座標(0,0,0)に合わせておく必要があります。

STEP2 : Custom Functionの変更

Custom Function にも Target Position というパラメータを追加します。

ShaderGraphに追加した Target Position プロパティをCustom Functionへつなぎます。

STEP3 : HLSLの変更

先ほど作成したHLSLファイルは以下のように変更します。

bounding_box_convert.hlsl
void BoundingBoxConvert_float(
    float3 Position, // モデルの頂点座標
    float3 BoundingMin, // バウンディングボックスの最小座標
    float3 BoundingMax, // バウンディングボックスの最大座標
    float3 TargetPosition, // モデルを追従
    out float3 Output // 変換後の頂点座標
    ) 
{
    // モデル頂点のy座標が 0 ~ 1 の範囲に収まるように座標変換
    float y = (Position.y - BoundingMin.y) / (BoundingMax.y - BoundingMin.y);

    // バウンディングボックスを四角錐に変形
    float2 centerXZ = (BoundingMin.xz + BoundingMax.xz) / 2.0; // 中心座標
    Position.xz = lerp(Position.xz, centerXZ, y); // yが1に近づくにつれて、XZ座標は中心座標に近づく

    // 位置をずらす
    float3 ShiftPosition = Position + TargetPosition;
    
    // yが0に近づくほど、頂点座標はShiftPositionに近づく
    Output = lerp(ShiftPosition, Position, y);
}

結果

ShaderGraphのパラメータを変更すると、円柱も変形します。

§4 : スポットライトをオブジェクトに追従させる

C#を使ってシェーダーのプロパティを更新し、スポットライトをオブジェクトに追従させます。

STEP1 : パラメータの設定

先ほど追加したプロパティTargetPositionの_Referenceの部分を _TargetPosition に設定します。

C#からは _TargetPosition という文字列を指定してシェーダーパラメータを更新することになります。

STEP2 : C#を書く

シェーダーの_TargetPositionを更新するC#スクリプト(SpotLightController.cs) を作成します。

SpotLightController.cs
using UnityEngine;

public class SpotLightController : MonoBehaviour
{
    // パラメータを更新したいマテリアル
    [SerializeField] private Material material; 
    
    // スポットライトを追従させたいTransform
    [SerializeField] private Transform lookTarget;  
    
    // 更新するシェーダープロパティのID
    private readonly int shaderID_TargetPosition = Shader.PropertyToID("_TargetPosition");

    private void Update()
    {
        if (material == null) return;
        if (lookTarget == null) return;
        
        material.SetVector(shaderID_TargetPosition, lookTarget.position);
    }
}

STEP3 : SpotLightControllerをスポットライトにアタッチ

円柱に設定し、 SpotLightControllerをアタッチします。

円柱のPositionは (0, 1, 0) にしておきます。(円柱の根元を座標(0, 0, 0)に一致させます)

コンポーネントのMaterialの部分には今回作成したマテリアルを、
Look Target には追従させたいオブジェクトを設定します。

※ Scale を (1, 1, 1)以外に設定すると位置がずれます

結果

スポットライトが対象を追従するようになります。

いかがでしたでしょうか。
モデルを変形させる表現は応用の幅が広いじゃないかと思います。

おまけ : バウンディングボックス変形を数式で解説してみる(数式)

その他 : バウンディングボックス変形を補間で表現する

今回、バウンディングボックスの変形を実装しましたが、
これを数式を使って解説してみたいと思います。

座標 (0, y) は 2点の補間で表現できる

xy空間の座標 P(0, y) を考えます。

P は 点A(0, 1) と 点B(0,0) を結ぶ線分を p : 1-pに内分する点と考えることができます。

P = (1 - y) \cdot (0, 0) + y \cdot (0, 1)

シェーダーコードで書くと以下のようになります。

float2 A = float2(0, 0);
float2 B = float2(0, 1);
float2 P = lerp(A, B, y);


座標 (x, y) は 4点の補間で表現できる

次にxy空間上の 点 P (x, y) を考えてみます。

P(x, y)は 線分 ABx : 1-x に内分する点としてみることができます。

\begin{aligned} P &= (1 - x) \cdot A + x \cdot B \\ A &= (1 - y) \cdot (0,0) + y \cdot (0,1) \\ B &= (1 - y) \cdot (1,0) + y \cdot (1,1) \end{aligned}

シェーダーコードで書くと以下のようになります。

float2 A = lerp(float2(0, 0), float2(0, 1), y);
float2 B = lerp(float2(1, 0), float2(1, 1), y);
float2 P = lerp(A, B, x);

これはバイリニア補間という名前がついています。


座標 (x, y, z) は 8点の補間で表現できる

次にxyz空間上の 点 P (x, y, z) を考えてみます。

Pは線分ABz:1-zに内分する点と考えることができます。

Aは 4点 (0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0) のバイリニア補間、
Bは 4点 (0, 0, 1), (1, 0, 1), (0, 1, 1), (1, 1, 1) のバイリニア補間
で得られる座標になります。

シェーダーコードで書くと以下のようになります。
(x, y, z)はバウンディングボックス変形前の頂点座標になります。

float3 A = lerp(
	lerp(float3(0, 0, 0), float3(0, 1, 0), y),
	lerp(float3(1, 0, 0), float3(1, 1, 0), y),
	x);
	
float3 B = lerp(
	lerp(float3(0, 0, 1), float3(0, 1, 1), y),
	lerp(float3(1, 0, 1), float3(1, 1, 1), y),
	x);
	
float3 P = lerp(A, B, z); // 箱を変形した後の頂点座標

これはトリリニア補間という名前がついています。


バウンディングボックスの変形

バウンディングボックスの上側の4つの点を動かして、四角錐にすることを考えます。
バウンディングボックスの内側のモデルの形状も変化します。


変形前の点Pの座標

モデル変形前の、モデル頂点P(x, y, z) の座標は8個の点
(0, 0, 0), (0, 1, 0), (1, 0, 0), (1, 1, 0), (0, 0, 1), (0, 1, 1), (1, 0, 1), (1, 1, 1)
のトリリニア補間によってあらわすことができます。

float3 A = lerp(
	lerp(float3(0, 0, 0), float3(0, 1, 0), y),
	lerp(float3(1, 0, 0), float3(1, 1, 0), y),
	x);
	
float3 B = lerp(
	lerp(float3(0, 0, 1), float3(0, 1, 1), y),
	lerp(float3(1, 0, 1), float3(1, 1, 1), y),
	x);
	
float3 P = lerp(A, B, z); // 座標(x, y, z)に一致


変形後の点Pの座標

変形後の点Pの座標は8個の点
(0, 0, 0), (a, b, c), (1, 0, 0), (a, b, c), (0, 0, 1), (a, b, c), (1, 0, 1), (a, b, c)
のトリリニア補間によってあらわすことができます。

変形後の点Pのシェーダーコードで書くと、以下のようになります。

float3 A = lerp(
	lerp(float3(0, 0, 0), float3(a, b, c), y),
	lerp(float3(1, 0, 0), float3(a, b, c), y),
	x);
	
float3 B = lerp(
	lerp(float3(0, 0, 1), float3(a, b, c), y),
	lerp(float3(1, 0, 1), float3(a, b, c), y),
	x);
	
float3 P = lerp(A, B, z); // バウンディングボックスを変形した後のモデル頂点座標

Discussion