要在代码里画一个 HSL/HSV/HSB 调色盘非常容易,不过如果这个调色盘需要实时变化,那么频繁绘制需要在 CPU 上大量创建或者修改位图,性能不太好。本文将使用 HLSL 来完成这一任务。
HLSL 入门
如果你对 WPF 使用像素着色器还不太了解,那么可以阅读入门文章:
HSL/HSV/HSB
为了让后面的代码容易看懂,我们需要先简单了解一下 HSL/HSV/HSB。
- HSL:hue 色相, saturation 饱和度, lightness 亮度
- HSV/HSB:hue 色相, saturation 饱和度, value/brighness 明度
这是两个不同但类似的,符合人眼感知的颜色表示方法,其中后两者只是名称不同,实际上是完全相同的意思。
关于 HSL 和 HSV/HSB 的更多资料,可以参考 HSL and HSV - Wikipedia
▲ HSL
▲ HSV
HSL 和 HSV/HSB 的 HLSL 代码
版本一:初步实现
由于 HSL 和 HSV/HSB 到 RGB 的转换是非常广泛被使用的,所以网上的代码非常丰富,我们只需要让 GPT-4 帮我们生成一个就可以了:
这是 HSL 调色盘的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
sampler2D input : register(s0);
/// <summary>Background Color outside of the Circle</summary>
/// <type>Color</type>
/// <minValue>0,0,0,1</minValue>
/// <maxValue>1,1,1,1</maxValue>
/// <defaultValue>1,1,1,1</defaultValue>
float4 BackColor : register(C0);
/// <summary>Hue Initial Angle</summary>
/// <minValue>0</minValue>
/// <maxValue>360</maxValue>
/// <defaultValue>0</defaultValue>
float HueInitialAngle : register(C1);
/// <summary>Lightness</summary>
/// <minValue>0</minValue>
/// <maxValue>1</maxValue>
/// <defaultValue>0.5</defaultValue>
float Lightness : register(C2);
float3 HSLtoRGB(float H, float S, float L)
{
float C = (1.0f - abs(2.0f * L - 1.0f)) * S;
float X = C * (1.0f - abs(fmod(H / 60.0f, 2.0f) - 1.0f));
float m = L - C / 2.0f;
float3 RGB;
if (0 <= H && H < 60)
RGB = float3(C, X, 0);
else if (60 <= H && H < 120)
RGB = float3(X, C, 0);
else if (120 <= H && H < 180)
RGB = float3(0, C, X);
else if (180 <= H && H < 240)
RGB = float3(0, X, C);
else if (240 <= H && H < 300)
RGB = float3(X, 0, C);
else
RGB = float3(C, 0, X);
return RGB + m;
}
float4 main(float2 uv : TEXCOORD) : COLOR
{
float2 pos = uv * 2.0f - 1.0f;
float dist = length(pos);
if(dist > 1.0f)
return BackColor;
float h = atan2(pos.y, pos.x) * (180.0f / 3.1415926f) + HueInitialAngle;
if(h < 0)
h += 360;
else if(h > 360)
h -= 360;
float s = dist;
float l = Lightness;
float3 color = HSLtoRGB(h, s, l);
return float4(color, 1.0f);
}
这是 HSB 调色盘的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
sampler2D input : register(s0);
/// <summary>Background Color outside of the Circle</summary>
/// <type>Color</type>
/// <minValue>0,0,0,1</minValue>
/// <maxValue>1,1,1,1</maxValue>
/// <defaultValue>1,1,1,1</defaultValue>
float4 BackColor : register(C0);
/// <summary>Hue Initial Angle</summary>
/// <minValue>0</minValue>
/// <maxValue>360</maxValue>
/// <defaultValue>0</defaultValue>
float HueInitialAngle : register(C1);
/// <summary>Brightness</summary>
/// <minValue>0</minValue>
/// <maxValue>1</maxValue>
/// <defaultValue>1</defaultValue>
float Brightness : register(C2);
float3 HSBtoRGB(float H, float S, float B)
{
float C = B * S;
float X = C * (1.0f - abs(fmod(H / 60.0f, 2.0f) - 1.0f));
float m = B - C;
float3 RGB;
if (0 <= H && H < 60)
RGB = float3(C, X, 0);
else if (60 <= H && H < 120)
RGB = float3(X, C, 0);
else if (120 <= H && H < 180)
RGB = float3(0, C, X);
else if (180 <= H && H < 240)
RGB = float3(0, X, C);
else if (240 <= H && H < 300)
RGB = float3(X, 0, C);
else
RGB = float3(C, 0, X);
return RGB + m;
}
float4 main(float2 uv : TEXCOORD) : COLOR
{
float2 pos = uv * 2.0f - 1.0f;
float dist = length(pos);
if(dist > 1.0f)
return BackColor;
float h = atan2(pos.y, pos.x) * (180.0f / 3.1415926f) + HueInitialAngle;
if (h < 0)
h += 360;
else if (h > 360)
h -= 360;
float s = dist;
float b = Brightness;
float3 color = HSBtoRGB(h, s, b);
return float4(color, 1.0f);
}
这两个调色盘都支持三个参数:
- 背景色,用于指定显示圆盘外面显示什么颜色
- 色相旋转角度,用于按照你的需要将起始的色相转到对应的位置(右、上等)
- 亮度或明度,当指定这个值时,整个调色盘的最大亮度或明度就被限制到了这个值
通常,1 和 2 直接在代码中设好就可以了,3 则通常是在界面中额外显示一个滑块了整体调节。
版本二:精简指令
需要注意的是,上述代码都是超过了 PS_2 的最大 64 条指令的,也就是说只能以 PS_3 作为目标框架。不过,PS_3 不支持部分显卡(例如 Windows 远程桌面 RDP 所虚拟的显卡)。所以,如果你希望上述像素着色器能够在这样的情况下工作,则需要放弃 PS_3 转而使用 PS_2,或者在不满足要求的情况下自己用其他方式进行软渲染。
那么,上述代码能将指令数优化到 64 以内吗?我们去问问 GPT-4。
被 GPT-4 精简后的代码如下,现在已经可以完全在 PS_2 的目标框架下完成编译并使用了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
sampler2D input : register(s0);
/// <summary>Hue Initial Angle</summary>
/// <minValue>0</minValue>
/// <maxValue>360</maxValue>
/// <defaultValue>0</defaultValue>
float HueInitialAngle : register(C0);
/// <summary>Brightness</summary>
/// <minValue>0</minValue>
/// <maxValue>1</maxValue>
/// <defaultValue>1</defaultValue>
float Brightness : register(C1);
float3 HSBToRGB(float3 hsb)
{
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(hsb.xxx + K.xyz) * 6.0 - K.www);
return hsb.z * lerp(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsb.y);
}
float4 main(float2 uv : TEXCOORD) : COLOR
{
float2 pos = uv * 2.0f - 1.0f;
float dist = length(pos);
float h = (atan2(pos.y, pos.x) / (2.0f * 3.1415926f) + HueInitialAngle / 360.0f) % 1.0f;
float s = dist;
float b = Brightness;
float3 hsb = float3(h, s, b);
float3 color = HSBToRGB(hsb);
return float4(color, 1.0f);
}
版本三:带有完整功能的精简指令
既然可以把指令精简到如此程度,那么我们把前面删除的 BackColor
功能加回来能否继续保证在 64 指令数以内呢?
既然 GPT-4 那么强大,那么就劳烦一下它吧,经过反复询问以及我的调试下,HSL 调色盘和 HSV/HSB 调色盘的精简指令全功能版本就出来啦,代码如下,大家可复制参考。
至于全功能是哪写全功能呢?
- 支持使用 HueInitialAngle 参数控制色相的旋转角度
- 支持设置 HSL 中的 L(Lightness)或 HSV/HSB 中的 B(Brighness)
- 支持 Gamma 校正(设置为 1.0 则不校正,如果希望用户看起来更自然一些,可以设置为 2.2)
- 支持 OutsideColor 参数设置调色盘圆外的颜色,且支持设置半透明色
如下图是这四个参数的设置效果,其中圆外设置成了半透明黑色。
▲ 全功能的 HSB 调色盘
HSL 调色盘:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
sampler2D input : register(s0);
/// <summary>Hue Initial Angle</summary>
/// <type>Single</type>
/// <minValue>0</minValue>
/// <maxValue>360</maxValue>
/// <defaultValue>0</defaultValue>
float HueInitialAngle : register(C1);
/// <summary>Lightness</summary>
/// <type>Single</type>
/// <minValue>0</minValue>
/// <maxValue>1</maxValue>
/// <defaultValue>0.5</defaultValue>
float Lightness : register(C2);
/// <summary>Gamma Correction</summary>
/// <type>Single</type>
/// <minValue>1.0</minValue>
/// <maxValue>2.4</maxValue>
/// <defaultValue>1.0</defaultValue>
float Gamma : register(C3);
/// <summary>Background Color outside of the Circle</summary>
/// <type>Color</type>
/// <minValue>0,0,0,1</minValue>
/// <maxValue>1,1,1,1</maxValue>
/// <defaultValue>1,1,1,1</defaultValue>
float4 OutsideColor : register(C4);
float3 HUEtoRGB(float H)
{
float R = abs(H * 6 - 3) - 1;
float G = 2 - abs(H * 6 - 2);
float B = 2 - abs(H * 6 - 4);
return saturate(float3(R,G,B));
}
float3 HSLtoRGB(in float3 HSL)
{
float3 RGB = HUEtoRGB(HSL.x);
float C = (1 - abs(2 * HSL.z - 1)) * HSL.y;
return (RGB - 0.5) * C + HSL.z;
}
float4 main(float2 uv : TEXCOORD) : COLOR
{
float2 pos = uv * 2.0f - 1.0f;
float dist = length(pos);
dist = pow(dist, Gamma);
float h = (atan2(pos.y, pos.x) / (2.0f * 3.1415926f) + 1.0f + HueInitialAngle / 360.0f) % 1.0f;
float s = dist;
float l = Lightness;
float3 hsl = float3(h, s, l);
float3 color = HSLtoRGB(hsl);
float4 finalColor = float4(color, 1.0f);
if(dist > 1.0f)
finalColor = float4(OutsideColor.rgb, 1.0f) * OutsideColor.a + finalColor * (1 - OutsideColor.a);
return finalColor;
}
HSV/HSB 调色盘:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
sampler2D input : register(s0);
/// <summary>Hue Initial Angle</summary>
/// <type>Single</type>
/// <minValue>0</minValue>
/// <maxValue>360</maxValue>
/// <defaultValue>0</defaultValue>
float HueInitialAngle : register(C0);
/// <summary>Brightness</summary>
/// <type>Single</type>
/// <minValue>0</minValue>
/// <maxValue>1</maxValue>
/// <defaultValue>1</defaultValue>
float Brightness : register(C1);
/// <summary>Gamma Correction</summary>
/// <type>Single</type>
/// <minValue>1.0</minValue>
/// <maxValue>2.4</maxValue>
/// <defaultValue>2.2</defaultValue>
float Gamma : register(C2);
/// <summary>Color outside of the Circle</summary>
/// <type>Color</type>
/// <minValue>0,0,0,1</minValue>
/// <maxValue>1,1,1,1</maxValue>
/// <defaultValue>1,1,1,1</defaultValue>
float4 OutsideColor : register(C3);
float3 HSBToRGB(float3 hsb)
{
float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
float3 p = abs(frac(hsb.xxx + K.xyz) * 6.0 - K.www);
return hsb.z * lerp(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsb.y);
}
float4 main(float2 uv : TEXCOORD) : COLOR
{
float2 pos = uv * 2.0f - 1.0f;
float dist = length(pos);
dist = pow(dist, Gamma);
float h = (atan2(pos.y, pos.x) / (2.0f * 3.1415926f) + HueInitialAngle / 360.0f) % 1.0f;
float s = dist;
float b = Brightness;
float3 hsb = float3(h, s, b);
float3 color = HSBToRGB(hsb);
float4 finalColor = float4(color, 1.0f);
if(dist > 1.0f)
finalColor = float4(OutsideColor.rgb, 1.0f) * OutsideColor.a + finalColor * (1 - OutsideColor.a);
return finalColor;
}
附:GPT-4 提示词
考虑到有些小伙伴可能对我的 GPT-4 提示词感兴趣,那么我就把我的询问过程贴出来。
参考资料
本文会经常更新,请阅读原文: https://blog.walterlv.com/post/wpf-draw-a-hsl-hsb-palette-using-hlsl ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected]) 。