凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で円形に曲がるスクリーンをシェーダで作成するアセットを作ってみた

はじめに

uDesktopDuplication に用意されているカーブドスクリーンを uWindowCapture でも使いたいとの要望を頂いたので、単体のアセットとして作成してみました。

デモ

f:id:hecomi:20181017220808g:plain

ダウンロード

github.com

Release から最新の .unitypackage をダウンロードしてプロジェクトへ展開してください。

使い方

メッシュ

使えるメッシュは付属の CurvedScreen のみになります。Z軸方向を向き X 方向に曲げ用に分割され、片側の XY 平面が Z=0 になっている形です。スケールは X = Y = 1 m、Z = 0.01 m です。

f:id:hecomi:20181017223539p:plain

f:id:hecomi:20181017223707p:plain

シェーダ

CurvedScreen/Unlit および CurvedScreen/Standard の 2 つのシェーダが用意されています。それぞれ、Unlit/Texture および Surface Standard ShaderRadius および Thickness プロパティが追加されたものになっています。

f:id:hecomi:20181017221634p:plain

f:id:hecomi:20181017221647p:plain

Radius は曲げの半径、Thickness は厚み(1 が 1 cm)です。横幅は transform.localScale.x がそのまま使われます。

解説

頂点変形

曲げる部分は以下のように入力で渡ってきたローカル座標の頂点を関数を通して変形します。

#include "./CurvedScreen.cginc"

float _Radius;
float _Thickness;

v2f vert(appdata v)
{
    v2f o;
    CurvedScreenVertex(v.vertex.xyz, _Radius, CurvedScreenGetWidth(), _Thickness);
    o.vertex = UnityObjectToClipPos(v.vertex);
    ...
    return o;
}

CurvedScreen.cginc にここでの変形が書かれています。

inline float CurvedScreenGetWidth()
{
    return length(float3(unity_ObjectToWorld[0].x, unity_ObjectToWorld[1].x, unity_ObjectToWorld[2].x));
}

inline void CurvedScreenVertex(inout float3 v, float radius, float width, float thickness)
{
    float angle = width * v.x / radius;
    v.z *= thickness;
    radius += v.z;
    v.z -= radius * (1.0 - cos(angle));
    v.x = radius * sin(angle) / width;
}

X 方向(画面横幅)のスケールは unity_ObjectToWorld から以下のフォーラムのスレッドを参考に抽出しています。

移動成分と違ってスケール成分は回転成分と混ざってるので、モデル行列から取り出すのにちょっとだけ計算が必要です。こうして得られた横幅を使いながら、内側の真ん中の点を中心に曲がるように三角関数を適当に使いながらローカル座標位置を動かしています。

法線変形

CurvedScreen/Standard シェーダの方ではサーフェスシェーダを使って記述しています。こちらはライティングが影響するため、法線も曲げてあげなければいけません。

void vert(inout appdata_full v)
{
    float width = CurvedScreenGetWidth();
    CurvedScreenNormal(v.vertex.x, v.normal, _Radius, width);
    v.normal.x *= width; // for UnityObjectToWorldNormal()
    CurvedScreenVertex(v.vertex.xyz, _Radius, width, _Thickness);
}

CurvedScreenNormal() の中では法線を曲げに応じて回転させる処理が入っています。

inline float3 CurvedScreenRotateY(float3 n, float angle)
{
    float c = cos(angle);
    float s = sin(angle);
    return float3(c * n.x - s * n.z, n.y, s * n.x + c * n.z);
}

inline void CurvedScreenNormal(float x, inout float3 n, float radius, float width)
{
    float angle = -width * x / radius;
    n = CurvedScreenRotateY(n, angle);
}

ただし、法線は UnityObjectToWorldNormal() を通る際にスケーリングを考慮した計算が行われます。頂点は円形に曲げているのでスケーリングの考慮は要らず、単に回転させるだけで良いはずなのでこのままでは都合がよくありません。そこでこの考慮を打ち消すために、width を X 方向に掛ける計算を挟んでいます。詳しくは Catlike Coding さんにて解説されているので興味がある方は読んでみてください。

catlikecoding.com

影の対応

頂点変形をした結果を ShadowCaster パスへ反映させるためには、addshadow というキーワードを pragma 文に追加します。

addshadow の説明は次になります。

Generate a shadow caster pass. Commonly used with custom vertex modification, so that shadow casting also gets any procedural vertex animation. Often shaders don’t need any special shadows handling, as they can just use shadow caster pass from their fallback.

#pragma surface surf Standard addshadow fullforwardshadows vertex:vert

ちなみに、以前の記事では解説しませんでしたが、サーフェスシェーダが頂点・フラグメントシェーダへ展開されると、この vert 関数は頂点シェーダの早い段階で呼ばれるように差し込まれます。

v2f_surf vert_surf (appdata_full v) 
{
  UNITY_SETUP_INSTANCE_ID(v);
  v2f_surf o;
  UNITY_INITIALIZE_OUTPUT(v2f_surf,o);
  UNITY_TRANSFER_INSTANCE_ID(v,o);
  UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
  vert (v);
  ...

自作シェーダへの組み込み

自身のシェーダへ組み込みたい場合は、CurvedScreen.cginc を include して、上記で説明した CurvedScreenVertex() および CurvedScreenNormal() を頂点シェーダに差し込んでください。

おわりに

コリジョンが面倒だったりと問題はあるのですが、動的に曲げないといけないようなユースケースで使えると思います(ちなみに uDesktopDuplication では同様の方法で曲げていて、専用のレイキャストを用意しています)。現在は簡単のために Y 軸周りのカーブしか実装していませんが、X 軸側のカーブをつけても面白いかも知れません。