【Unity】HSV用のColor型を作成する

Unity の色指定ができる Color はインスペクター上からだと RBG(byteで0-255, float0~1.0fの2種類)、HSVの値の指定ができますがスクリプト上で color プロパティから Color オブジェクトを取得した場合中に入っている値は RBG となっています。

この Color クラスは RBG しか扱えないため HSV変換したいときは Color クラスの「(static)Color.RGBToHSV」メソッドで HSV に変換を行いますが得られる結果は floatx3 となる値は分解されバラバラになります。この後特定の操作を行ってまた color プロパティに戻すときにこの3つの値を「Color.HSVToRGB」を用いて RBG に戻して color プロパティに再設定する操作は、定型的かつ繰り返しの実装になりがちです。

色相をスライドしたり明るさを変更する操作は RBG だと少し複雑ですが HSV だとかなり簡単にできるため積極的に使用したいところなのでこの記述を簡単にするために HSV を表す専用の HsvColor 型を以前紹介した ValuoObject と絡めて作成しようと思います。

確認環境

今回実装・確認を行う環境は以下の通りです。

  • Unity 2020.3.14f1
  • VisualStudio 2019

Editor上のみで確認しています。

実装コード

ではさっそく実装の紹介です。

ColorHsvクラス

このクラスは構造体(値型)として作成し immutable を保証し readonly struct として宣言しします。中身の値を変更するときは新しいインスタンスを返すようにします。構造体としてスタックに値をとることで、ヒープ領域を確保せず GC によるゲームの進行を邪魔しないように注意します。また引数は(この実装の場合は念のためですが) in をつけて受け書き換え禁止の値型の参照渡しを明示します。あと最後の方によくあるユーティリティ操作を追記します。

Color クラスと違いオブジェクト同士の足し引きなどのオペレーターは実装しません。HSV 同士の足し算とか滅多に使用しないと思いますがもし使うときはオペレーターを追記します。余談ですがこのクラスはインスペクターからの操作や JsonUtility などでのシリアライズを考慮していません。

using System;
using System.Runtime.CompilerServices;

/// <summary>
/// HSVのValueObject
/// </summary>
public readonly struct ColorHsv : IEquatable<ColorHsv>
{
    // Fields
    public readonly float H; // H : 0-360
    public readonly float S; // S : 0-100
    public readonly float V; // V : 0-100
    public readonly float A; // A : 0-100

    // Constructors
    public ColorHsv(float h, float s, float v)
    {
        this.H = Mathf.Clamp(h, 0, 360);
        this.S = Mathf.Clamp(s, 0, 100);
        this.V = Mathf.Clamp(v, 0, 100);
        this.A = 100; // default is max
    }
    public ColorHsv(float h, float s, float v, float a) : this(h, s, v)
    {
        this.A = Mathf.Clamp(a, 0, 100);
    }

    // 演算子のオーバーライド
    public static bool operator ==(in ColorHsv a, in ColorHsv b) => Equals(a, b);
    public static bool operator !=(in ColorHsv a, in ColorHsv b) => !Equals(a, b);

    // 3つの組み合わせからHSVオブジェクトを作成する
    public static implicit operator ColorHsv((float h, float s, float v) hsv)
    {
        return new ColorHsv(hsv.h, hsv.s, hsv.v);
    }
    // 4つの組み合わせからHSVオブジェクトを作成する
    public static implicit operator ColorHsv((float h, float s, float v, float a) hsva)
    {
        return new ColorHsv(hsva.h, hsva.s, hsva.v, hsva.a);
    }
    public static implicit operator Color(in ColorHsv hsv)
    {
        return hsv.ToRgb();
    }

    // 等値比較演算子の実装
    public override bool Equals(object obj) => (obj is ColorHsv _obj) && this.Equals(_obj);

    // IEquatable<T> の implement
    public bool Equals(ColorHsv other)
    {
        // 個別に記述する
        return ReferenceEquals(this, other) ||
               this.H == other.H &&
               this.S == other.S &&
               this.V == other.V &&
               this.A == other.A;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = this.H.GetHashCode();
            hashCode = (hashCode * 397) ^ this.S.GetHashCode();
            hashCode = (hashCode * 397) ^ this.V.GetHashCode();
            hashCode = (hashCode * 397) ^ this.A.GetHashCode();
            return hashCode;
        }
    }

    // -- Utils ---

    /// <summary>
    /// RGB から HSV を作成します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ColorHsv FromColorRgb(in Color rgbColor)
    {
        Color.RGBToHSV(rgbColor, out float h, out float s, out float v);
        return new ColorHsv(h, s, v);
    }

    /// <summary>
    /// HSV オブジェクトから RGB オブジェクトを取得します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Color ToRgb() => Color.HSVToRGB(this.H, this.S, this.V);

    /// <summary>
    /// 指定した値にHを変更します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ColorHsv SetH(float newH)
    {
        float h = Mathf.Clamp(newH, 0, 360);
        return (h, this.S, this.V);
    }
    /// <summary>
    /// 指定した値をHに加算します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ColorHsv AddH(float value)
    {
        float h = Mathf.Clamp(this.H + value, 0, 360);
        return (h, this.S, this.V);
    }

    /// <summary>
    /// 指定した値にSを変更します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ColorHsv SetS(float newS)
    {
        float s = Mathf.Clamp(newS, 0, 100);
        return (this.H, s, this.V);
    }
    /// <summary>
    /// 指定した値をSに加算します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ColorHsv AddS(float value)
    {
        float s = Mathf.Clamp(this.S + value, 0, 100);
        return (this.H, s, this.V);
    }

    /// <summary>
    /// 指定した値にVを変更します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ColorHsv SetV(float newV)
    {
        float v = Mathf.Clamp(newV, 0, 100);
        return (this.H, this.S, v);
    }
    /// <summary>
    /// 指定した値をVに加算します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ColorHsv AddV(float value)
    {
        float v = Mathf.Clamp(this.V + value, 0, 100);
        return (this.H, this.S, v);
    }
}

HsvExtensionクラス

基本的に色が RGB で扱われているため HSV を取得するときに繰り返し同じコードを記述することになるので以下のような拡張メソッドを用意して使いやすいようにします。

using System;
using System.Runtime.CompilerServices;

/// <summary>
/// <see cref="ColorHsv"/> 用の便利な拡張機能を定義します。
/// </summary>
public static class HsvExtension
{
    /// <summary>
    /// <see cref="ColorHsv"/> を取得します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ColorHsv GetColorHsv(this Image img)
    {
        Color.RGBToHSV(img.color, out float h, out float s, out float v);
        return (h, s, v);
    }

    /// <summary>
    /// <see cref="ColorHsv"/> を取得します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ColorHsv GetColorHsv(this SpriteRenderer sp)
    {
        Color.RGBToHSV(sp.color, out float h, out float s, out float v);
        return (h, s, v);
    }

    /// <summary>
    /// <see cref="ColorHsv"/> を取得します。
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static ColorHsv ToHsv(this in Color c)
    {
        Color.RGBToHSV(c, out float h, out float s, out float v);
        return (h, s, v, c.a);
    }
}

使い方

使い方は以下の通りです。拡張メソッドのおかげで便利に使えます。

public void Foo()
{
    // 通常のインスタンスの作成
    ColorHsv hsv = new ColorHsv(200, 100, 100);

    // SpriteRendererオブジェクトからHSVを取得する 
    SpriteRenderer sr = this.sr;
    ColorHsv hsv2 = sr.GetColorHsv();

    // ColorをHSVに変換して取り出す
    ColorHsv hsv3 = sr.color.ToHsv();

    // Imageオブジェクトから直接HSVを取得する
    Image img = this.img;
    ColorHsv hsv4 = img.GetColorHsv();

    // RGBに戻す
    Color rgb = hsv4.ToRgb();
    // ColorHsvをColorに代入したときはRGBに暗黙的に変換される
    //  → この挙動が気に入らなければ暗黙の変換コンストラクタを削除する
    sr.color = hsv4;
}

インスペクターと若干相性が悪い以外はいい感じかと思います。基本的にここに書いた内容を少し変更すればほかの型も作成できるので、ColorRGB を作成して RGB と HSV を明確に分離したり、角度の Degree と Radian を分けても面白いかもしれません。

関連記事

takap-tech.com

以上です。