のりまき日記

Unityなどの活用リファレンスブログ。「こうしたい時どうする」をまとめたい

unity)映画「インセプション」みたいな表現をしたい ~ shaderでmeshを折り曲げる ~

こういう表現

はじめに

調べてみましたがアルゴリズムがみつからなかったので自分で考えて実装してみます。shaderで頂点をいじってmeshを折り曲げることにしました。

完成図

実装方法

3次元で考えるとむずかしいので2次元で考えます。折り曲げる基準点(以下「重力点」と呼ぶ)を中心に平面を回転させます。回転量を補間することでスムースに折り畳みます。

考え方

shaderの準備

光や影の影響を受けたいのでデフォルトのsurface shaderを改造します。

shaderの作成方法

Shader "Custom/MySurfaceShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // 頂点シェーダを追加
        #pragma surface surf Standard fullforwardshadows vertex:vert
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // 頂点シェーダを追加
        void vert (inout appdata_full v)
        {
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

note

#pragma surface surf Standard fullforwardshadows vertex:vert

...

void vert (inout appdata_full v)
{
}

頂点をいじりたいのでvertexオプションを追加し、頂点シェーダの関数を指定します。

スクリプト準備

シーン

わかりやすいように重力点をシリンダーで表現しました。この円筒に巻き付くように世界を折り曲げます。以下は重力点用のスクリプトです。

// 重力点
[ExecuteInEditMode]
public class GravityPole : MonoBehaviour
{
    [SerializeField]
    GameObject m_Cylinder;

    // どのくらい空間を曲げるか
    [SerializeField]
    int m_Curve = 90;

    // シェーダに渡す情報
    int m_WorldToGravityId;
    int m_GravityToWorldId;
    int m_GravityRadiusId;
    int m_GravityCurveId;

    void Start()
    {
        // キャッシュ
        m_WorldToGravityId = Shader.PropertyToID("_WorldToGravity");
        m_GravityToWorldId = Shader.PropertyToID("_GravityToWorld");
        m_GravityRadiusId = Shader.PropertyToID("_GravityRadius");
        m_GravityCurveId = Shader.PropertyToID("_GravityCurve");
    }

    void Update()
    {
        Shader.SetGlobalMatrix(m_WorldToGravityId, transform.worldToLocalMatrix);
        Shader.SetGlobalMatrix(m_GravityToWorldId, transform.localToWorldMatrix);
        // シリンダーの半径を使う
        Shader.SetGlobalFloat(m_GravityRadiusId, m_Cylinder.transform.localScale.x / 2f);
        Shader.SetGlobalFloat(m_GravityCurveId, m_Curve);
    }
}

シェーダーで必要な情報を渡しています。

note

Shader.SetGlobalMatrix(m_WorldToGravityId, transform.worldToLocalMatrix);
Shader.SetGlobalMatrix(m_GravityToWorldId, transform.localToWorldMatrix);

重力点から見た座標に変換するのに使います。

Shader.SetGlobalFloat(m_GravityRadiusId, m_Cylinder.transform.localScale.x / 2f);
Shader.SetGlobalFloat(m_GravityCurveId, m_Curve);

わかりやすくするため折り曲げる半径はシリンダーの半径にしています。あとは折り曲げる角度も渡しています。

shaderを改造する

まずは重力点の周りを回転するようにしてみます。

回転している様子

void vert (inout appdata_full v)
{
    // ワールド座標にする
    float3 wpos = mul(unity_ObjectToWorld, v.vertex).xyz;

    // 重力点からみた座標にする
    float3 gpos = mul(_WorldToGravity,float4(wpos,1)).xyz;

    // 円筒座標する
    float r = sqrt(gpos.z * gpos.z + gpos.y * gpos.y);
    float t = sign(gpos.y) * acos(gpos.z / r);

    // 指定角度曲げる
    float rad = radians(_GravityCurve);
    t += rad;

    // 直交座標に戻す
    gpos.z = r * cos(t);
    gpos.y = r * sin(t);

    // ワールド座標に戻す
    wpos = mul(_GravityToWorld, float4(gpos, 1)).xyz;

    // オブジェクト座標に戻す
    v.vertex.xyz = mul(unity_WorldToObject, float4(wpos,1)).xyz;
}

note

// 円筒座標する
float r = sqrt(gpos.z * gpos.z + gpos.y * gpos.y);
float t = sign(gpos.y) * acos(gpos.z / r);

座標を回転する計算方法はいろいろありますが、今回は円筒座標を使ってみました。xyzではなく距離(r)と角度(t)から座標を指定できるようになります。tをいじることで簡単に回転させることができます。

円筒座標系 - Wikipediaを参考に実装しました。

距離に応じて回転させる

距離に応じて補間することで滑らかに曲げてみます。

void vert (inout appdata_full v)
{
    // ワールド座標にする
    float3 wpos = mul(unity_ObjectToWorld, v.vertex).xyz;

    // 重力点からみた座標にする
    float3 gpos = mul(_WorldToGravity,float4(wpos,1)).xyz;

    // 円筒座標する
    float r = sqrt(gpos.z * gpos.z + gpos.y * gpos.y);
    float t = sign(gpos.y) * acos(gpos.z / r);

    // 指定角度曲げる
    float rad = radians(_GravityCurve);
    float arc = _GravityRadius * rad;// 円弧の長さ
    float rate = saturate(gpos.z / arc);// 補間量
    float ratedRad = rad * rate;
    t += ratedRad;

    // 直交座標に戻す
    gpos.z = r * cos(t);
    gpos.y = r * sin(t);

    // ワールド座標に戻す
    wpos = mul(_GravityToWorld, float4(gpos, 1)).xyz;

    // オブジェクト座標に戻す
    v.vertex.xyz = mul(unity_WorldToObject, float4(wpos,1)).xyz;
}

note

float rad = radians(_GravityCurve);
float arc = _GravityRadius * rad;// 円弧の長さ
float rate = saturate(gpos.z / arc);// 補間量
float ratedRad = rad * rate;
t += ratedRad;

半径と角度から折り曲げるのに必要な距離を計算して、奥行きで割ることで0〜1の値を算出しています。

ここまでの結果

遠くにあるほどrの値が大きいのでその分膨らんでしまっています。また回転させた結果、同じ部分を描画してしまうので絵が伸びてしまっています。思ってたのと違うので修正していきます。

shaderを修正する

回転させる前に現在の座標を回転の原点に持ってきてから回転させます。rが一定値になるので膨らまず、同じ部分を描画しないので絵が伸びなくなります。

こうしたい

void vert (inout appdata_full v)
{
    // ワールド座標にする
    float3 wpos = mul(unity_ObjectToWorld, v.vertex).xyz;

    // 重力点からみた座標にする
    float3 gpos = mul(_WorldToGravity,float4(wpos,1)).xyz;

    // いろいろ計算
    float rad = radians(_GravityCurve);
    float arc = _GravityRadius * rad;// 円弧の長さ
    float rate = saturate(gpos.z / arc);// 補間量
    float ratedRad = rad * rate;

    // 回転の原点に移動させる
    gpos.z -= _GravityRadius * ratedRad;

    // 円筒座標する
    float r = sqrt(gpos.z * gpos.z + gpos.y * gpos.y);
    float t = sign(gpos.y) * acos(gpos.z / r);

    // 指定角度曲げる
    t += ratedRad;

    // 直交座標に戻す
    gpos.z = r * cos(t);
    gpos.y = r * sin(t);

    // ワールド座標に戻す
    wpos = mul(_GravityToWorld, float4(gpos, 1)).xyz;

    // オブジェクト座標に戻す
    v.vertex.xyz = mul(unity_WorldToObject, float4(wpos,1)).xyz;
}

note

// いろいろ計算
float rad = radians(_GravityCurve);
float arc = _GravityRadius * rad;// 円弧の長さ
float rate = saturate(gpos.z / arc);// 補間量
float ratedRad = rad * rate;

// 回転の原点に移動させる
gpos.z -= _GravityRadius * ratedRad;

現在の座標の移動量分(円弧の長さ分)奥行きを戻すことで原点に戻しています。

できた!

memo

伸びる例

シリンダーの半径と同じ位置にある座標は完璧に等倍になります。半径より遠かったり近かったりする場合は、カーブを描く時に伸びたり縮んだりします。

おわり

気持ちよい!

参考にさせていただいた記事・サイト