読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

リングセレクトシステムとモーフォング

はじめに

今回はリングセレクトシステム(と呼んでいきます。正式名称とかあるのかは知らないです。)とモーフィングについて書いてみます。

f:id:es_program:20160707205712g:plain:w500

Unityで実装しています。

リングセレクトシステムはアイコンがくるくる回って、その中の1つを選択するみたいなアレです。

モーフィングは、上記のgif中央の風船アイコンに注目してもらえればわかると思います。

この2つ、見た感じすごくイイんですが、実装はめちゃくちゃ簡単です。

リングシステム

極座標で座標計算すればいいだけです。選択しているアイテムのインデックスを保持して、インデックスをmodの世界に持っていけば完成! あとはUIなど凝りつつアニメーションつけたり好きなようにごにょごにょしていきます。

極座標による座標計算はこんな感じ。

private Vector2 CalclatePosition(int index)
{
    float x = Mathf.Cos((angle + progressAngle * index) * Mathf.Deg2Rad) * radius;
    float y = Mathf.Sin((angle + progressAngle * index) * Mathf.Deg2Rad) * radius;
    return new Vector2(x, y);
}

回転の処理はコルーチン使ってこんな感じ。

private IEnumerator RotateCoroutine(RotateDirection dir)
{
    isRotate = true;
    var dirCoef = dir == RotateDirection.Next ? 1 : -1;
    selectItemIndex += dirCoef;
    if(selectItemIndex < 0)
        selectItemIndex = items.Count - 1;
    selectItemIndex %= items.Count;

    for(int i = 0; i < progressAngle; i += rotateSpeed)
    {
        this.angle -= rotateSpeed * dirCoef;
        Reposition();
        yield return new WaitForEndOfFrame();
    }
    angle %= 360;
    isRotate = false;
}

$360/アイテム数=progressAngle$で、回転速度をrotateSpeedとしています。 Repositionメソッドは先程の極座標から位置を計算するメソッドでそれぞれのアイテムの位置を変更します。

基本的にはこれだけです。ものの5分で出来るお手軽さですが、ユーザー視点だと結構凝ったシステムに見えるかも?

モーフィング

リングシステムはuGUIで作っています。なのでこのモーフィングの実装はuGUIの機能を継承し、なるべく簡単に実装しました。 まずモーフィングを実現するためのImageMorphing.csです。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// UNITY UIのRawImageをモーフィングさせるためのクラス
/// </summary>
public class ImageMorphing : RawImage
{
    private const string COLOR_PROPERTY = "_Color";
    private const string MAIN_IMAGE_PROPERTY = "_MainTex";
    private const string NEW_IMAGE_PROPERTY = "_NewImage";
    private const string MORPHING_COEFFICIENT_PROPERTY = "_MorphingCoefficient";

    private int colorID;
    private int mainImageID;
    private int newImageID;
    private int morphingCoeffID;

   #region UnityEventMethod

    protected override void Awake()
    {
        base.Awake();
        colorID = Shader.PropertyToID(COLOR_PROPERTY);
        mainImageID = Shader.PropertyToID(MAIN_IMAGE_PROPERTY);
        newImageID = Shader.PropertyToID(NEW_IMAGE_PROPERTY);
        morphingCoeffID = Shader.PropertyToID(MORPHING_COEFFICIENT_PROPERTY);
    }

    private void Update()
    {
        m_Material.SetColor(colorID, color);
    }

   #endregion UnityEventMethod

    /// <summary>
    /// 新しいイメージにモーフィング
    /// </summary>
    /// <param name="newImage">新しいイメージ</param>
    /// <param name="duration">モーフィング時間</param>
    public void Morphing(Texture newImage, float duration)
    {
        StartCoroutine(MorphingCoroutine(newImage, duration));
    }

    /// <summary>
    /// モーフィング用のコルーチン
    /// </summary>
    /// <param name="newImage">新しいイメージ</param>
    /// <param name="duration">モーフィング時間</param>
    /// <returns></returns>
    private IEnumerator MorphingCoroutine(Texture newImage, float duration)
    {
        m_Material.SetTexture(newImageID, newImage);
        var coef = -m_Material.GetFloat(morphingCoeffID);

        float time = 0f;
        yield return new WaitUntil(() =>
        {
            time += Time.deltaTime / duration;
            m_Material.SetFloat(morphingCoeffID, Mathf.Abs(coef - time));
            if(time >= 1f)
                return true;
            return false;
        });

        texture = newImage;
        m_Material.SetTexture(mainImageID, newImage);
        m_Material.SetFloat(morphingCoeffID, 0);
    }
}

Spriteだと勝手が悪いのでRawImageを継承して実装しています。ただ単にRawImageにモーフィング用のメソッドを追加しただけですが、モーフィングを実現するために専用にシェーダーを書いています。このシェーダーに必要なパラメーターを揃えて渡すのがImageMorphingクラスの役割です。

モーフィング用のシェーダーですが、これも超絶簡単です。

Shader "Hidden/ImageMorphing"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _NewImage("NewImage", 2D) = "white" {}
        _MorphingCoefficient("MorphingCoef", Range(0,1)) = 0
        _CullAlpha("Culling Alpha", Range(0,1)) = 0.1
        _Color("Color", COLOR) = (0,0,0,1)
    }
    SubShader
    {
        // No culling or depth
        Blend SrcAlpha OneMinusSrcAlpha
        Cull Off ZWrite Off ZTest Always

        Tags{"Queue" = "Transparent" "PreviewType" = "Plane"}

        Pass
        {
            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag

           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;
            sampler2D _NewImage;
            float _MorphingCoefficient;
            float _CullAlpha;
            fixed4 _Color;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 tex1 = tex2D(_MainTex, i.uv);
                fixed4 tex2 = tex2D(_NewImage, i.uv);

                fixed4 col = tex1 * (1 - _MorphingCoefficient) + tex2 * _MorphingCoefficient;

                clip(col.a - _CullAlpha);
                col.a = _Color.a;
                return col;
            }
            ENDCG
        }
    }
}

重要なのはフラグメントシェーダーですが、特に目新しい物はないかと思います。 適当にそれっぽくなりそうな計算させてるだけです。お手軽ですね。

このモーフィング処理をリングセレクトシステムの回転時に呼び出してやることで次第にイメージが変化していきます。