
前言
这次呢,继续再来看一个iq大神的简单作品,作品虽简单,但是却包含了很多知识点,先放上最终效果:

ShaderToy地址:https://www.shadertoy.com/view/MsS3Wc
不过本篇改动较大,最终效果与ShaderToy上的已不太一致,这点请注意,不要因此产生困扰,本篇的核心主要在于探讨几种不同的HSV转换到RGB的方法。
色彩模式
色彩模式有很多种,每一种都有各自的用途,比如RGB用于计算机显示器的显示,CMYK用于纸张的印刷等等。
- RGB
RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
- HSV/HSL
但是,RGB的方式对于人类来说,并不友好,比如我问你R(104)G(185)B(166)是个什么颜色,可能你需要想很久,甚至最后也不一定能正确回答出来,这不怪你,都是RGB的错〜
于是呢,计算机图形学研究人员在20世纪70年代设计出了一种更符合人类感知的色彩模式,这就是HSV与HSL,这两者类似但又有些区别。
HSV有时也被叫做HSB,其中的V与B表示的是亮度的意思。

共同点:
- 两者的色彩模型都可以用一个圆柱体来表示
- 每个色调的颜色(色相)被排列在圆柱中呈放射状的圆形横截面上
- 以圆柱中心为轴,从下到上为黑色到白色的过渡
不同点:
- HSV表示法模拟了不同颜色混合在一起的方式,Saturation(饱和度)类似于当前颜色纯度与最大纯度的比值,范围[0,1],0时为灰色,Value(亮度)则是表示颜色与不同数量的黑色或白色的混合物。
- HSL模型尝试类似于更感性的颜色模型,例如Natural Color System(NCS)或Munsell color system颜色系统,将完全饱和的颜色置于亮度值为1/2的圆周围,其中亮度值0或1分别为完全黑色或白色。
本文的重点,HSV,即H(hue色相)S(saturation饱和度)V(value亮度),通过定义一个颜色的色相,然后饱和度多少,最后亮度又是多少,即可得到我们所想要的颜色值。
有人问了,那既然HSV更符合人类感知,那为什么还要用RGB呢,能不能把RGB去掉呢?
那肯定是不能啦,因为显示器本身的硬件是通过RGB(红绿蓝)三色发光极来生成色彩的,HSV就相当于为了使我们人类更易懂而产生的一种中间语言!
效果实现
顶点着色器
顶点着色器,一始既往,没什么好说的,仅仅正确显示模型以及获取UV值即可。
v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
片断着色器
fixed4 frag (v2f i) : SV_Target
{
fixed3 hsv=fixed3(i.uv.x,1,i.uv.y);
//return fixed4(hsv2rgb_01(hsv),1);
//return fixed4(hsv2rgb_02(hsv), 1);
return fixed4(hsv2rgb_03(hsv), 1);
}
这个实例的片断着色器还是比较清晰易懂的,大体分析如下:
- 通过uv定义出hsv
- 分别利用此hsv实现三种HSV2RGB的效果
- 注释了前面两个,仅显示最后一个效果
HSV UV
大致理清思路后,我们来看细节实现
fixed3 hsv=fixed3(i.uv.x,1,i.uv.y);
定义三维向量hsv,返回的值是由屏幕uv坐标组合而来HSV
- H=i.uv.x
- S=1
- V=i.uv.y
也就是说屏幕的横向坐标表示的是色相H,垂直坐标表示的是亮度V,而饱和度S假设都是1的情况。
这个我们可以打开Unity中的拾色器,将色板切换到HSV模式下就可以直观的感受到这样分UV的用途了。

HSV2RGB(标准版)
fixed3 rgb_o = hsv2rgb_01( hsv );
根据我们上面的大体分析,这里主要是将我们的hsv通过自定义的函数hsv2rgb_01来转换成RGB颜色后的效果。
Wiki上的公式:
https://en.wikipedia.org/wiki/HSL_and_HSV
首先需先满足条件:
H ∈ [0º,360º]
S ∈ [0,1]
V ∈ [0,1]
然后直接上公式:

翻译后的Shader代码如下:
fixed3 hsv2rgb_01(fixed3 hsv)
{
fixed R,G,B = hsv.z;
fixed h = hsv.x;
fixed s = hsv.y;
fixed v = hsv.z;
h *= 6;
fixed i = floor(h);
fixed f = h - i;
fixed p = v * (1 - s);
fixed q = v * (1 - s * f);
fixed t = v * (1 - s * (1 - f));
switch(i)
{
case 0:
R = v; G = t; B = p;
break;
case 1:
R = q; G = v; B = p;
break;
case 2:
R = p; G = v; B = t;
break;
case 3:
R = p; G = q; B = v;
break;
case 4:
R = t; G = p; B = v;
break;
default:
R = v; G = p; B = q;
break;
}
return fixed3(R,G,B);
}
注:公式中的h/60在代码中变成了h*6,原因是因为我们传进来的h并不是[0º,360º],而是[0,1],所以才需要*6来限定最终的结果在[0,5]之间.
注:switch仅支持#pragma target 3.5及以上
hsv2rgb_01返回的结果如下:

效果刚好与我们上面在拾色器中看到的一样,横向表示色相,竖向表示亮度,而饱合度是1的情况。
HSV2RGB(替代版)
以上的版本虽然效果实现上没有问题,但是会产生一些性能与兼容性的问题,于是,在Wiki上还有一种替代方案,公式如下:

套用公式,翻译成js语言:
function hsv2rgb(h,s,v)
{
let f= (n,k=(n+h/60)%6) => v-v*s*Math.max(Math.min(k,4-k,1),0);
return [f(5),f(3),f(1)];
}
转换成shader后的代码:
fixed3 hsv2rgb_02(fixed3 c)
{
float3 k = fmod(float3(5, 3, 1) + c.x * 6, 6);
return c.z - c.z * c.y * max(min(min(k, 4 - k), 1), 0);
}
返回的效果如下:

与hsv2rgb_01效果一致,但是运算量更少,支持更低的SM版本。
HSV2RGB(优化版)
然而,我在ASE中发现了一版更优的版本,效果同样一致,运算量更少,代码如下:
float3 hsv2rgb_03(float3 c)
{
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y);
}
总结
那么HSV转换RGB功能我们可以用在什么地方呢?
比如,我们可以用在后处理上,暴露HSV参数来方便直观的调节,然后内部转换成RGB,同样也可以用在一些角色材质的换色上等等。
最后
欢迎大家关注更多干货的公众号:Unity技术美术 ( ID:gh_8b69cca044dc )

Unity技术美术QQ交流分享群:19470667(1群已满)、763506271
