【Unity】LineRendererを使ってお絵かきソフトにある機能を実装する その2

概要

LineRendererを用いての対称定規や円、螺旋の描き方について
実装方法を記載します。

綺麗な円の描き方等がなかなか見つからなかったのですが、
誰かに需要あると信じてまとめます。

実装したもの

f:id:coffee_ryo:20181014005607g:plain

サンプルとして以下を作りました。

  • フリーハンド
  • 対称定規
  • 螺旋
  • 正方形っぽい形

ドラッグで線の描画、右クリックでUndoができます。

処理の流れ

マウスの位置情報と線のタイプをインプットとします。
線のタイプによって、線の数の決定や座標変換を行い、
最終的にできた座標をLineRendererに渡して描画します。
ここでの線のタイプは、円とかフリーハンドか対称定規か、などを指します。

f:id:coffee_ryo:20181014011728p:plain

実装サンプルについて

Materialの作成

描画の色を設定する用途で、Projectタブ上にMaterialを作成します。

f:id:coffee_ryo:20181014012258p:plain

Prefabの作成

空のオブジェクトを作成し、LineRendererをアタッチ。
LineRendererのMaterialsに先ほど作成したMaterialを設定したら
オブジェクトをPrefab化します。

f:id:coffee_ryo:20181014012334p:plain

お絵描き用のオブジェクトを作成する

空のオブジェクトを作成し、後述するIllustDrawerコンポーネントをアタッチ。
インスペクター上で先ほど作成したPrefabを設定をしたら、準備完了です。

f:id:coffee_ryo:20181014012932p:plain

ソースコード

IllustDrawer.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace CoffeeR.Paint{
    public class IllustDrawer : MonoBehaviour {

        [Header("LineRendererがついたPrefab指定")]
        [SerializeField]
        LineRenderer lineRendererPrefab;

        [Header("線の太さを指定")]
        [SerializeField]
        [Range(0.05f, 1.0f)]
        float lineWidth;

        [Header("線のタイプを指定")]
        [SerializeField]
        EnumLineType lineType;

        [Header("円等を描く時の中心点を設定")]
        [SerializeField]
        Vector3 centerPosition;

        /// <summary>
        /// 描画コンポーネント群
        /// </summary>
        List<List<LineRenderer>> lineRendererMultipleList;

        void Start () {
            lineRendererMultipleList = new List<List<LineRenderer>>();
        }

        void Update () {
            if(Input.GetMouseButtonDown(1)){
                UndoLine();
            }
            if(Input.GetMouseButtonDown(0)){
                CreateLineRendererObject(lineType);
            }
            if(Input.GetMouseButton(0)){
                var mousePosition = GetPostionOfInput();
                DrawingLine(mousePosition);
            }
        }

        /// <summary>
        /// レンダラー付きのオブジェクトを作成する
        /// </summary>
        void CreateLineRendererObject (EnumLineType type) {
            lineRendererMultipleList.Add(new List<LineRenderer>());

            // 線を作成する個数を設定
            int lineCount = 0;
            switch(type){
                case EnumLineType.FreeHand:
                case EnumLineType.Circle:
                case EnumLineType.Spiral:
                    lineCount = 1;
                    break;
                case EnumLineType.LikeSquare:
                    lineCount = 4;
                    break;
                case EnumLineType.Symmetry:
                    lineCount = 12;
                    break;
                default:
                    Debug.LogError(type.ToString() + "での線の本数が指定されていません。");
                    break;
            }

            for(int i = 0; i < lineCount; i++){
                // 描画コンポーネントがついたオブジェクトを作成
                LineRenderer line = Instantiate(lineRendererPrefab);

                // 太さを設定する
                line.startWidth = lineWidth;
                line.endWidth   = lineWidth;

                // 子オブジェクトに設定
                line.transform.parent = this.transform;

                // 作成した描画コンポーネントをこのクラスにキャッシュする
                lineRendererMultipleList.Last().Add(line);
            }
        }

        void DrawingLine (Vector3 mousePosition){

            int positionIndex = 0;

            switch(lineType){
                case EnumLineType.FreeHand:
                    lineRendererMultipleList.Last().Last().positionCount++;
                    positionIndex = lineRendererMultipleList.Last().Last().positionCount;
                    lineRendererMultipleList.Last().Last().SetPosition(positionIndex - 1, mousePosition);
                    break;
                
                case EnumLineType.Circle:
                    lineRendererMultipleList.Last().Last().positionCount++;
                    positionIndex = lineRendererMultipleList.Last().Last().positionCount;

                    if(positionIndex == 1){
                        lineRendererMultipleList.Last().Last().SetPosition(positionIndex - 1, centerPosition + mousePosition);
                    }
                    else{
                        var firstPosition = lineRendererMultipleList.Last().Last().GetPosition(0);
                        lineRendererMultipleList.Last().Last().SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler (0f, 0f, positionIndex * 5) * (firstPosition - centerPosition));
                    }
                    break;
                
                case EnumLineType.Spiral:
                    lineRendererMultipleList.Last().Last().positionCount++;
                    positionIndex = lineRendererMultipleList.Last().Last().positionCount;

                    if(positionIndex == 1){
                        lineRendererMultipleList.Last().Last().SetPosition(positionIndex - 1, centerPosition + mousePosition);
                    }
                    else{
                        var firstPosition = lineRendererMultipleList.Last().Last().GetPosition(0);
                        lineRendererMultipleList.Last().Last().SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler (0f, 0f, positionIndex * 5) * (firstPosition  - centerPosition) / (1 + positionIndex * 0.025f));
                    }
                    break;
                
                case EnumLineType.Symmetry:
                    var degreeIndex = 0;
                    foreach(var line in lineRendererMultipleList.Last()){
                        line.positionCount++;
                        positionIndex = line.positionCount;
                        line.SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler (0f,  0f, degreeIndex * 360 / 12) * (mousePosition - centerPosition));
                        degreeIndex++;
                    }
                    break;
                
                case EnumLineType.LikeSquare:
                    for(int i = 0; i < 4; i++){
                        lineRendererMultipleList.Last()[i].positionCount++;
                    }
                    positionIndex = lineRendererMultipleList.Last().Last().positionCount;


                    if(positionIndex == 1){
                        for(int i = 0; i < 4; i++){
                            lineRendererMultipleList.Last()[i].SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler(0f, 0f,   (i - 1) * 90) * mousePosition);
                        }
                    }else{
                        var vertexPos = lineRendererMultipleList.Last()[0].GetPosition(0) - centerPosition;
                        lineRendererMultipleList.Last()[0].SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler(0f, 0f,   0f)  * vertexPos + positionIndex * Vector3.down  * 0.1f);
                        lineRendererMultipleList.Last()[1].SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler(0f, 0f,  90f)  * vertexPos + positionIndex * Vector3.right * 0.1f);
                        lineRendererMultipleList.Last()[2].SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler(0f, 0f,  180f) * vertexPos + positionIndex * Vector3.up    * 0.1f);
                        lineRendererMultipleList.Last()[3].SetPosition(positionIndex - 1, centerPosition + Quaternion.Euler(0f, 0f,  270f) * vertexPos + positionIndex * Vector3.left  * 0.1f);
                    }
                    break;

                default:
                    Debug.LogError(lineType.ToString() + "での線の描き方が指定されていません。");
                    break;
            }
        }

        /// <summary>
        /// 一つ前の状態に戻す
        /// </summary>
        void UndoLine(){
            try{
                var lastLineRendererList = lineRendererMultipleList.Last();
                foreach(var line in lastLineRendererList){
                    Destroy(line.gameObject);
                }
                lineRendererMultipleList.Remove(lastLineRendererList);
            }catch(System.InvalidOperationException){
                Debug.Log("線がないためUndoされませんでした");
            }
        }

        /// <summary>
        /// 入力位置を返却する
        /// </summary>
        /// <returns></returns>
        Vector2 GetPostionOfInput(){
            Vector3 position = new Vector3(Input.mousePosition.x, Input.mousePosition.y, Camera.main.nearClipPlane + 1.0f);
            return Camera.main.ScreenToWorldPoint(position);
        }
    }

    /// <summary>
    /// 線のタイプ
    /// </summary>
    internal enum EnumLineType{

        /// <summary>
        /// 自由線
        /// </summary>
        FreeHand,
        /// <summary>
        /// 対称定規
        /// </summary>
        Symmetry,
        /// <summary>
        /// 円
        /// </summary>
        Circle,
        /// <summary>
        /// 螺旋
        /// </summary>
        Spiral,
        /// <summary>
        /// 正方形状
        /// </summary>
        LikeSquare
    }
}

それぞれの線について

Quaternionを使って座標変換をするのがポイントになります。

マウス入力してからの1フレーム目の、マウスの位置と中心点との間を半径とします。
フレームを進めるごとに、「1フレーム目のマウス位置」を中心点から回転させた位置を描画していきます。

f:id:coffee_ryo:20181014032908p:plain

螺旋

円と類似していますが、フレームを進めるごとに半径を小さくしていきます。

対称定規

複数のLineRendererを動かします。
マウス入力位置を、中心点から一定数回転させた位置をLineRendererに設定します。
ソースコードにてList<List>という構造を使っているのは、これのため。

正方形状

複数のLineRendererを動かします。
マウス入力位置を、中心点から90度ずつ回転させた位置をLineRendererに設定します。