【Unityシェーダーグラフ】円柱を変形させてスポットライト表現を作る
はじめに
ShaderGraph(シェーダーグラフ)でスポットライト表現を作ってみました。
Unity標準のCylinderをShaderGraphの頂点シェーダーで変形させることでスポットライト表現を作っています。
スポットライト表現
環境
Unity 2020.2.0f1
UniversalRP 10.2.2
頂点シェーダーを使ったモデル変形
3Dモデルのバウンディングボックス(モデルを囲む仮想の箱)を考えます。
箱の頂点を動かして四角錐にすると、内側のモデルが変形します。
HoudiniのTaperを使った変形
円柱に対してこの変形を適用すると円錐型のモデルになります。
円柱を円錐に変形
UnityのShaderGraphには、箱を変形するという演算は用意されていないので、
頂点座標の変換計算を自分で書くことになります。
§1 : 円柱を円錐に変形する
STEP1 : HLSLファイルの作成
以下のHLSLファイルをUnityプロジェクトに追加します。
これは、3Dモデルを囲む箱を四角錘へ変形させるような演算を行います。
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ファイルは以下のように変更します。
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) を作成します。
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空間の座標
点
シェーダーコードで書くと以下のようになります。
float2 A = float2(0, 0);
float2 B = float2(0, 1);
float2 P = lerp(A, B, y);
座標 (x, y) は 4点の補間で表現できる
次にxy空間上の 点
点
シェーダーコードで書くと以下のようになります。
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空間上の 点
点
点
点
で得られる座標になります。
シェーダーコードで書くと以下のようになります。
(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の座標
モデル変形前の、モデル頂点
のトリリニア補間によってあらわすことができます。
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のシェーダーコードで書くと、以下のようになります。
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