のりまき日記

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

unity)AdobeのIllustratorみたいにべジュ曲線でマスクしたい

Unityのみでプロジェクションマッピングを行うときに画面をマスクすることがあるのですが、べジュ曲線でマスクできると便利だな!と思って作ってみました。

できた!

べジュ曲線とは

ベジェ曲線 - Wikipedia を参考にイメージをUnityのuGUIで再現してみました。赤い点が曲線を描いています。

考えた人すごい!

コード

public class Emulate : MonoBehaviour
{
    [SerializeField]
    RectTransform m_Point0;

    [SerializeField]
    RectTransform m_Point1;

    [SerializeField]
    RectTransform m_Point2;

    [SerializeField]
    RectTransform m_Point3;

    [SerializeField]
    RectTransform m_Point4;

    [SerializeField]
    RectTransform m_Point5;

    [SerializeField]
    RectTransform m_Point6;

    [SerializeField]
    RectTransform m_Point7;

    [SerializeField]
    RectTransform m_Point8;

    [SerializeField]
    RectTransform m_Point9;

    [SerializeField]
    RectTransform m_Line0;

    [SerializeField]
    RectTransform m_Line1;

    [SerializeField]
    RectTransform m_Line2;

    [SerializeField]
    RectTransform m_Line3;

    [SerializeField, Range(0f, 1f)]
    float m_NormalizePosition;

    [SerializeField, Range(1, 10)]
    int m_Speed = 1;

    private void Update()
    {
        m_NormalizePosition = Time.time / m_Speed % 1f;
        Draw();
    }

    private void OnValidate()
    {
        Draw();
    }

    private void Draw()
    {
        m_Point4.position = Vector3.Lerp(m_Point0.position, m_Point1.position, m_NormalizePosition);
        m_Point5.position = Vector3.Lerp(m_Point2.position, m_Point3.position, m_NormalizePosition);
        m_Point6.position = Vector3.Lerp(m_Point1.position, m_Point2.position, m_NormalizePosition);
        m_Point7.position = Vector3.Lerp(m_Point0.position, m_Point6.position, m_NormalizePosition);
        m_Point8.position = Vector3.Lerp(m_Point6.position, m_Point3.position, m_NormalizePosition);
        m_Point9.position = Vector3.Lerp(m_Point7.position, m_Point8.position, m_NormalizePosition);

        DrawLine(m_Line0, m_Point1, m_Point2);
        DrawLine(m_Line1, m_Point0, m_Point6);
        DrawLine(m_Line2, m_Point6, m_Point3);
        DrawLine(m_Line3, m_Point7, m_Point8);
    }

    private void DrawLine(RectTransform l, RectTransform a, RectTransform b)
    {
        var vec = b.position - a.position;
        l.position = Vector3.Lerp(a.position, b.position, .5f);
        l.rotation = Quaternion.FromToRotation(Vector3.up, vec.normalized);
        l.sizeDelta = new Vector2(2, vec.magnitude);
    }
}

note

        m_Point4.position = Vector3.Lerp(m_Point0.position, m_Point1.position, m_NormalizePosition);
        m_Point5.position = Vector3.Lerp(m_Point2.position, m_Point3.position, m_NormalizePosition);
        m_Point6.position = Vector3.Lerp(m_Point1.position, m_Point2.position, m_NormalizePosition);
        m_Point7.position = Vector3.Lerp(m_Point0.position, m_Point6.position, m_NormalizePosition);
        m_Point8.position = Vector3.Lerp(m_Point6.position, m_Point3.position, m_NormalizePosition);
        m_Point9.position = Vector3.Lerp(m_Point7.position, m_Point8.position, m_NormalizePosition);

Lerpがいっぱい出てきます。補間を繰り返すことで表現できるようです。

Vector Graphicsを使う

上記で曲線は描けるようになりましたがパス内を塗ったり、画像に保存するのは大変そうです。調べてみると「Vector Graphics」というパッケージをUnityが作成していました。SVGをインポートできるようになるパッケージのようですが、ベクターファイルを操作するAPIも含まれているみたいです。今回はこのAPIを使ってみます。

Vector Graphicsをインポート

記事を書いてる現在ではパッケージマネージャーからはインポートできませんでした。Previewパッケージを有効にしても出てきません。以下の記事によるとExperimentalパッケージというのに分類されるみたいでした。

shibuya24.info

「com.unity.vectorgraphics」を指定するとインポートできました。

ここからインポートする

Vector Graphicsの使い方を探す

以下の記事で大体の使い方を知ることができました。いくつか引用させていただきます。

learning.unity3d.jp

こちらのマニュアルも参考にしました。

docs.unity3d.com

memo

上記マニュアルには「Path」や「IDrawable」クラスなどが出てきますがインポートしたパッケージには含まれていなく「Shape」クラスになっていました。参考にしつつ使ってみます。

Vector Graphicsでメッシュやスプライト・画像を作るまで

❶Sceneデータを作る

データ構造(引用)

実際には以下のようになっていました。

  • SceneがあってRootとしてSceneNodeを保持。
  • SceneNodeSceneNodeを子として複数持てる。
  • SceneNodeShapeを子として複数持てる。
  • ShapeBezierContourを子として複数持てる。Contourは「輪郭」という意味。
  • BezierContourBezierPathSegmentを複数持てる。これが先ほどのP0やP1やP2といった点の位置データ。

輪郭を作ってくれるユーティリティー(引用)

BezierPathSegmentを作ってくれるユーティリティーもありました。位置や大きさを指定して、単純な円や四角や線を作成できます。

VectorUtils.Make***

❷Sceneデータをジオメトリに変換する

ジオメトリに変換(引用)

VectorUtils.TessellateScene

❸ジオメトリをメッシュやスプライトに変換

メッシュに変換(引用)

VectorUtils.FillMesh
VectorUtils.BuildSprite

❹スプライトを画像に変換

テクスチャに書き出すかマテリアルに直接渡します。

VectorUtils.RenderSpriteToTexture2D
VectorUtils.RenderSprite

エディタを作る

なんとかしてP0・P1・P2を設定できればよさそうなので作ります。Illustratorを参考にuGUIで作成しました。

こんな感じ

反映する

作ったエディタからBezierContourを作成していろいろVectorUtilsに渡せばメッシュや画像を作ってくれます。P0・P1・P2といったデータの並び方がよくわからなかったので、以下を参考にコンバートしました。

公式マニュアルより(引用)

using System.Collections;
using System.Collections.Generic;
using Unity.VectorGraphics;
using UnityEngine;

public class BezierEdit : MonoBehaviour
{
    // VectorUtils.TessellateSceneで使うオプション
    static readonly VectorUtils.TessellationOptions TessellationOptions = new VectorUtils.TessellationOptions()
    {
        // Tessellationの細かさ
        StepDistance = .1f,
        // 以下はデフォルト値
        MaxCordDeviation = float.MaxValue,
        MaxTanAngleDeviation = Mathf.PI / 2.0f,
        SamplingStepSize = 0.01f
    };

    // ここはいい感じに作る
    [SerializeField]
    UIBezierEdit m_UIBezierEdit;

    // スプライトのレンダリング先
    [SerializeField]
    SpriteRenderer m_SpriteRenderer;

    Scene m_Scene;

    private void Awake()
    {
        // Shapeを準備
        var shape = new Shape()
        {
            // ここをエディタで更新する
            Contours = new BezierContour[] { new BezierContour() },
            // 塗り方
            Fill = new SolidFill()
            {
                Color = Color.green,
                Mode = FillMode.OddEven
            }
        };

        // Sceneを準備
        m_Scene = new Scene()
        {
            Root = new SceneNode()
            {
                Shapes = new List<Shape>() { shape }
            }
        };
    }

    private void Update()
    {
        // 変更があれば再構築
        if (m_UIBezierEdit.hasUpdate)
        {
            BuildSprite();
        }
    }

    void BuildSprite()
    {
        // エディタからSegmentsを更新
        m_Scene.Root.Shapes[0].Contours[0].Segments = m_UIBezierEdit.ToSegments();

        // ジオメトリ作成
        var geoms = VectorUtils.TessellateScene(m_Scene, TessellationOptions);

        // ジオメトリからスプライト作成
        m_SpriteRenderer.sprite = VectorUtils.BuildSprite(geoms, 1, VectorUtils.Alignment.SVGOrigin, Vector2.zero, 128);
    }
}

こんな感じ

note

// VectorUtils.TessellateSceneで使うオプション
static readonly VectorUtils.TessellationOptions TessellationOptions = new VectorUtils.TessellationOptions()
{
    // Tessellationの細かさ
    StepDistance = .1f,
    // 以下はデフォルト値
    MaxCordDeviation = float.MaxValue,
    MaxTanAngleDeviation = Mathf.PI / 2.0f,
    SamplingStepSize = 0.01f
};

ジオメトリを作成する時のオプションです。StepDistanceは分割する数なのでパフォーマンスに影響します。小さすぎると曲線がガタガタになるので、いい感じの値にしました。

// Shapeを準備
var shape = new Shape()
{
    // ここをエディタで更新する
    Contours = new BezierContour[] { new BezierContour() },
    // 塗り方
    Fill = new SolidFill()
    {
        Color = Color.green,
        Mode = FillMode.OddEven
    },
};

// Sceneを準備
m_Scene = new Scene()
{
    Root = new SceneNode()
    {
        Shapes = new List<Shape>() { shape }
    }
};

データの準備をしています。ややこしいです。

// エディタからSegmentsを更新
m_Scene.Root.Shapes[0].Contours[0].Segments = m_UIBezierEdit.ToSegments();

// ジオメトリ作成
var geoms = VectorUtils.TessellateScene(m_Scene, TessellationOptions);

// ジオメトリからスプライト作成
m_SpriteRenderer.sprite = VectorUtils.BuildSprite(geoms, 1, VectorUtils.Alignment.SVGOrigin, Vector2.zero, 128);

エディタの点の位置情報からスプライトを作成しています。

シーンの配置
作成されるのはスプライトなのでuGUIに貼り付けられません。CanvasRender ModeScreen Space - Cameraに設定し、UIの位置とワールドの位置が一致するようにスプライトを配置しています。ワールド座標の位置情報を使ってスプライトを作成しているのでSprite Rendererのスケールは1のまま真ん中に配置しています。

線を描く

Shapeの中身を変更します。PathPropsを指定すると線を描けるようです。Fillも一緒に指定すれば線も塗りも描画できます。

// Shapeを準備
var shape = new Shape()
{
    // ここをエディタで更新する
    Contours = new BezierContour[] { new BezierContour() },
    // 線の描き方
    PathProps = new PathProperties()
    {
        Stroke = new Stroke()
        {
            Color = Color.green,
            HalfThickness = .05f
        }
    }
};

線で描画

マスクにする

マスクしたいので黒い画像をパスで抜く必要があります。SVGの仕様とくり抜き方は以下を参考にしました。以下変更点です。

くり抜き方(引用)

// スクリーンサイズとカメラの大きさを合わせる
m_EditorCamera.orthographicSize = Screen.height / 100f / 2f;

// 四角い輪郭を準備
var size = new Vector2(Screen.width / 100f, Screen.height / 100f);
var rect = VectorUtils.BuildRectangleContour(new Rect(-size / 2f, size), Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero);

// Shapeを準備
var shape = new Shape()
{
    // ここをエディタで更新する
    Contours = new BezierContour[] { rect, new BezierContour() },
    // 塗り方
    Fill = new SolidFill()
    {
        Color = Color.black,
        Mode = FillMode.OddEven
    },
};

画面を覆い尽くす黒い四角を登録しています。

// エディタからSegmentsを更新
m_Scene.Root.Shapes[0].Contours[1].Segments = m_UIBezierEdit.ToSegments();

2つ目の輪郭に登録することでくり抜いています。

こんな感じになる

おわり

できた!

memo

最終的な結果を画像として保存して、ポストエフェクトなどでマスクを適応するといいと思います。

VectorUtils.RenderSpriteToTexture2D

他に参考にさせていただいた記事

setchi.hatenablog.com

tsubakit1.hateblo.jp

ありがとうございます。