【Unity】RGBをHSVに変換して明るさとかを変えるシェーダー

はじめに

こんにちは。ソーシャルゲーム事業部の小笠原です。
この記事はカヤックUnityアドベントカレンダー2018の15日目の記事です。
今回はRGBをHSVに変換して明るさとかを変えるシェーダーの話です。

概要

オブジェクトを暗くしたり、彩度を低くしたい時、HSV(色相、彩度、明度)を直接変更できるのは非常に便利です。 Unityスクリプト上でHSVを変更するにはColor.RGBToHSV,Color.HSVToRGBを使う必要があります。
しかし今回はマテリアルのRGBとHSVを相互変換する仕組みを入れたシェーダーを作って、Inspectorやスクリプト上で直接マテリアルのHSVを変更できるようにしてみます。

1.シェーダー作成

まずAssets->Create->Shader->Standard Surface Shaderからサーフェイスシェーダーを作成します。 これを改修してHSVを弄れるようにします。

f:id:ogasawara-keita:20181214181621p:plain

次にAssets->Create->Materialから今作ったシェーダーを入れるマテリアルも作っておきます。

f:id:ogasawara-keita:20181214181712p:plain

マテリアルのInspector内にあるShaderからCustom/SufaceShaderを選ぶことでマテリアルにシェーダーがセットされます。 Custom/SufaceShader部分はシェーダーのコード内一番上にある

Shader "Custom/SurfaceShader" 

を書き換えることで名称を変更出来ます。

以下が改修した後のソースコードです。 改修した部分をピックアップして説明します。

Shader "Custom/SurfaceShader-HSV" {
    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
        
        _Hue ("Hue", Float) = 0
        _Sat ("Saturation", Float) = 1
        _Val ("Value", Float) = 1
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        half _Hue, _Sat, _Val;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
        // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)
        
        // RGB->HSV変換
        float3 rgb2hsv(float3 rgb)
        {
            float3 hsv;

            // RGBの三つの値で最大のもの
            float maxValue = max(rgb.r, max(rgb.g, rgb.b));
            // RGBの三つの値で最小のもの
            float minValue = min(rgb.r, min(rgb.g, rgb.b));
            // 最大値と最小値の差
            float delta = maxValue - minValue;
            
            // V(明度)
            // 一番強い色をV値にする
            hsv.z = maxValue;
            
            // S(彩度)
            // 最大値と最小値の差を正規化して求める
            if (maxValue != 0.0){
                hsv.y = delta / maxValue;
            } else {
                hsv.y = 0.0;
            }
            
            // H(色相)
            // RGBのうち最大値と最小値の差から求める
            if (hsv.y > 0.0){
                if (rgb.r == maxValue) {
                    hsv.x = (rgb.g - rgb.b) / delta;
                } else if (rgb.g == maxValue) {
                    hsv.x = 2 + (rgb.b - rgb.r) / delta;
                } else {
                    hsv.x = 4 + (rgb.r - rgb.g) / delta;
                }
                hsv.x /= 6.0;
                if (hsv.x < 0)
                {
                    hsv.x += 1.0;
                }
            }
            
            return hsv;
        }
        
        // HSV->RGB変換
        float3 hsv2rgb(float3 hsv)
        {
            float3 rgb;

            if (hsv.y == 0){
                // S(彩度)が0と等しいならば無色もしくは灰色
                rgb.r = rgb.g = rgb.b = hsv.z;
            } else {
                // 色環のH(色相)の位置とS(彩度)、V(明度)からRGB値を算出する
                hsv.x *= 6.0;
                float i = floor (hsv.x);
                float f = hsv.x - i;
                float aa = hsv.z * (1 - hsv.y);
                float bb = hsv.z * (1 - (hsv.y * f));
                float cc = hsv.z * (1 - (hsv.y * (1 - f)));
                if( i < 1 ) {
                    rgb.r = hsv.z;
                    rgb.g = cc;
                    rgb.b = aa;
                } else if( i < 2 ) {
                    rgb.r = bb;
                    rgb.g = hsv.z;
                    rgb.b = aa;
                } else if( i < 3 ) {
                    rgb.r = aa;
                    rgb.g = hsv.z;
                    rgb.b = cc;
                } else if( i < 4 ) {
                    rgb.r = aa;
                    rgb.g = bb;
                    rgb.b = hsv.z;
                } else if( i < 5 ) {
                    rgb.r = cc;
                    rgb.g = aa;
                    rgb.b = hsv.z;
                } else {
                    rgb.r = hsv.z;
                    rgb.g = aa;
                    rgb.b = bb;
                }
            }
            return rgb;
        }
        
        float3 shift_col(float3 rgb, half3 shift)
        {
            // RGB->HSV変換
            float3 hsv = rgb2hsv(rgb);
            
            // HSV操作
            hsv.x += shift.x;
            if (1.0 <= hsv.x)
            {
                hsv.x -= 1.0;
            }
            hsv.y *= shift.y;
            hsv.z *= shift.z;
            
            // HSV->RGB変換
            return hsv2rgb(hsv);
        }

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            half3 shift = half3(_Hue, _Sat, _Val);
            fixed4 shiftColor = fixed4(shift_col(c.rgb, shift), c.a);
            o.Albedo = shiftColor.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = shiftColor.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

まずPropertiesです。

    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
        
        _Hue ("Hue", Float) = 0
        _Sat ("Saturation", Float) = 1
        _Val ("Value", Float) = 1
    }

ここにはHSVにあたる変数を書くことで、Inspecterやスクリプトで値にアクセスできるようになります。

次にSubShaderです。

half _Hue, _Sat, _Val;

Propertiesに追記したHSVの変数と同名のものをこちらでも定義します。

        // RGB->HSV変換
        float3 rgb2hsv(float3 rgb)
        {
            float3 hsv;

            // RGBの三つの値で最大のもの
            float maxValue = max(rgb.r, max(rgb.g, rgb.b));
            // RGBの三つの値で最小のもの
            float minValue = min(rgb.r, min(rgb.g, rgb.b));
            // 最大値と最小値の差
            float delta = maxValue - minValue;
            
            // V(明度)
            // 一番強い色をV値にする
            hsv.z = maxValue;
            
            // S(彩度)
            // 最大値と最小値の差を正規化して求める
            if (maxValue != 0.0){
                hsv.y = delta / maxValue;
            } else {
                hsv.y = 0.0;
            }
            
            // H(色相)
            // RGBのうち最大値と最小値の差から求める
            if (hsv.y > 0.0){
                if (rgb.r == maxValue) {
                    hsv.x = (rgb.g - rgb.b) / delta;
                } else if (rgb.g == maxValue) {
                    hsv.x = 2 + (rgb.b - rgb.r) / delta;
                } else {
                    hsv.x = 4 + (rgb.r - rgb.g) / delta;
                }
                hsv.x /= 6.0;
                if (hsv.x < 0)
                {
                    hsv.x += 1.0;
                }
            }
            
            return hsv;
        }
        
        // HSV->RGB変換
        float3 hsv2rgb(float3 hsv)
        {
            float3 rgb;

            if (hsv.y == 0){
                // S(彩度)が0と等しいならば無色もしくは灰色
                rgb.r = rgb.g = rgb.b = hsv.z;
            } else {
                // 色環のH(色相)の位置とS(彩度)、V(明度)からRGB値を算出する
                hsv.x *= 6.0;
                float i = floor (hsv.x);
                float f = hsv.x - i;
                float aa = hsv.z * (1 - hsv.y);
                float bb = hsv.z * (1 - (hsv.y * f));
                float cc = hsv.z * (1 - (hsv.y * (1 - f)));
                if( i < 1 ) {
                    rgb.r = hsv.z;
                    rgb.g = cc;
                    rgb.b = aa;
                } else if( i < 2 ) {
                    rgb.r = bb;
                    rgb.g = hsv.z;
                    rgb.b = aa;
                } else if( i < 3 ) {
                    rgb.r = aa;
                    rgb.g = hsv.z;
                    rgb.b = cc;
                } else if( i < 4 ) {
                    rgb.r = aa;
                    rgb.g = bb;
                    rgb.b = hsv.z;
                } else if( i < 5 ) {
                    rgb.r = cc;
                    rgb.g = aa;
                    rgb.b = hsv.z;
                } else {
                    rgb.r = hsv.z;
                    rgb.g = aa;
                    rgb.b = bb;
                }
            }
            return rgb;
        }
        
        float3 shift_col(float3 rgb, half3 shift)
        {
            // RGB->HSV変換
            float3 hsv = rgb2hsv(rgb);
            
            // HSV操作
            hsv.x += shift.x;
            if (1.0 <= hsv.x)
            {
                hsv.x -= 1.0;
            }
            hsv.y *= shift.y;
            hsv.z *= shift.z;
            
            // HSV->RGB変換
            return hsv2rgb(hsv);
        }

RGB<->HSVの変換用関数です。

HSVは環状や円錐の視覚モデルで表現することができ、H(色相)は色環の角度から決定されます。
f:id:ogasawara-keita:20181214181522j:plain
(著作権者:Wapcaplet、ライセンス:CC by-sa 3.0、HSV色空間 - Wikipedia

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            half3 shift = half3(_Hue, _Sat, _Val);
            fixed4 shiftColor = fixed4(shift_col(c.rgb, shift), c.a);
            o.Albedo = shiftColor.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = shiftColor.a;
        }

Surf内を改修します。

RGB<->HSVの変換式は下記を参考にしています。
https://answers.unity.com/questions/600199/hsv-shader-with-alpha-1.html
https://beesbuzz.biz/code/16-hsv-color-transforms
https://t-pot.com/program/112_HSV/index.html

また、RGB<->HSVの変換関数は下記のように短縮できます。

        // RGB->HSV変換
        float3 rgb2hsv(float3 rgb)
        {
            float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
            float4 p = rgb.g < rgb.b ? float4(rgb.bg, K.wz) : float4(rgb.gb, K.xy);
            float4 q = rgb.r < p.x ? float4(p.xyw, rgb.r) : float4(rgb.r, p.yzx);

            float d = q.x - min(q.w, q.y);
            float e = 1.0e-10;
            return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
        }

        // HSV->RGB変換
        float3 hsv2rgb(float3 hsv)
        {
            float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
            float3 p = abs(frac(hsv.xxx + K.xyz) * 6.0 - K.www);
            return hsv.z * lerp(K.xxx, saturate(p - K.xxx), hsv.y);
        }

参考:
http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv
http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl

これでシェーダーの準備は終わりです。

スクリプトでHSVを変えてみる

早速マテリアルをオブジェクトにアタッチしてみます。 今回は適当に作ったSphereのMesh Rendererにマテリアルをアタッチします。

f:id:ogasawara-keita:20181214181730p:plain

Inspectorのマテリアルが切り替わりHSVのプロパティが追加されています。 テクスチャをセットする場合は白黒のものを避けるとHSVの変化が分かりやすいです。

f:id:ogasawara-keita:20181214181746p:plain

インスペクターから直接マテリアルの操作も可能ですが、
今回はスクリプト上でマテリアルを操作したいので簡単なコントローラーのスクリプトを作り、Sphereにアタッチします。

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

public class HSVColorController : MonoBehaviour
{
    private Material material = null;
    [Range(0f, 1f)]
    public float hue = 0f;
    [Range(0f, 1f)]
    public float sat = 1f;
    [Range(0f, 1f)]
    public float val = 1f;

    // Use this for initialization
    void Start ()
    {
        this.material = gameObject.GetComponent<Renderer>().material;
    }

    // Update is called once per frame
    void Update ()
    {
        this.material.SetFloat("_Hue", hue);
        this.material.SetFloat("_Sat", sat);
        this.material.SetFloat("_Val", val);
    }
}

これでスクリプトからマテリアルのHSVを弄れるようになりました。 f:id:ogasawara-keita:20181214181801g:plain

ついでに

少しソースコードを改修してユニティちゃんの服の色を変えてみます。

f:id:ogasawara-keita:20181214181818g:plain:w400

スマ◯ラのような色変えしたり、

f:id:ogasawara-keita:20181214181827p:plain

うぉっまぶしっ。

おわりに

HSVはRGBと違い直感的に明度彩度を変更できるのが利点です。用途に合わせて使い分けてみてください。
以上「RGBをHSVに変換して明るさとかを変えるシェーダー」の話でした。 今回の話が何かのお役に立てれば幸いです。

明日は魏さんによる『Unityエディター拡張のカスタムプレビュー』です。

© UTJ/UCL