Skip to content

Commit b70f7f9

Browse files
committed
Verify color conversion parity, fix a bunch of issues
1 parent 22ac50e commit b70f7f9

File tree

7 files changed

+164
-70
lines changed

7 files changed

+164
-70
lines changed

docs/color.md

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ relatively high smoothing factor), the color evenly blends as the shapes blend.
2121

2222
This tool processes color internally within the shader in the CIE XYZ
2323
colorspace. This is makes the math that operates on color values easier in the
24-
shader pipeline, because the color values are in a perceptually uniform space,
24+
shader pipeline, because the color values are in a linear space,
2525
but it can be harder to reason about them.
2626

2727
To demonstrate this, the following example colors a cube with a blend of red →
@@ -61,12 +61,81 @@ second values according to the third value, which clamped between 0 and 1. The
6161
box in the example has been positioned to ensure that the x-coordinate is 0 on
6262
the left hand edge and 1 on the right hand edge.
6363

64+
## Supported Color Spaces
65+
66+
### sRGB
67+
68+
[sRGB](https://en.wikipedia.org/wiki/sRGB) is the standard color space used for
69+
web presentation. The three color channels represent red, green, and blue, and
70+
have ranges from 0→1. Every rgb value where each channel is between 0 and 1 is
71+
displayable. The other colorspaces can represent colors outside of these bounds,
72+
some of which would be considered visible, but cannot be represented on screen,
73+
and others are outside the gamut of human vision.
74+
75+
### CIE XYZ
76+
77+
[CIE 1931 XYZ](https://en.wikipedia.org/wiki/CIEXYZ) is the color space used
78+
internally in the shader. Colors are converted to sRGB only at the final stage
79+
of setting the pixel value. The three color values X, Y, Z can have ranges from 0 -> (0.950489, 1, 1.08884) respectively. These limits are derived from the
80+
D65 standard illuminant. Any color in the XYZ color space that is within these
81+
bounds can be considered ‘visible‛, but not all of those colors will be
82+
displayable.
83+
84+
### CIE L\*a\*b\*
85+
86+
[CIE L\*a\*b\*](https://en.wikipedia.org/wiki/CIELAB_color_space) is an
87+
approximately perceptually uniform color space derived from CIE XYZ.
88+
89+
- The L\* (L-star) value is a lightness value between 0–black, and
90+
100–white.
91+
- The a\* value represents a green↔red axis, with negative values adding green,
92+
and positive values adding red.
93+
- The b\* value represents a blue↔yellow axis, with negative values adding blue,
94+
and positive values adding yellow.
95+
96+
The a\* and b\* values aren't formally bounded, but useful values are typically
97+
between ±150
98+
99+
### CIE L\*C\*h
100+
101+
[CIE
102+
L\*C\*](https://en.wikipedia.org/wiki/CIELAB_color_space#Cylindrical_model) is
103+
a transformation of the CIE L\*a\*b\* color space into cylindrical polar
104+
coordinates.
105+
106+
- The L\* value has the same interpretation as in LAB space, lightness between 0–black and 100–white.
107+
- C\* is the length of the vector <a\*, b\*>, and represents chromaticity
108+
- h° is an angle in degrees, where 0°&ndash;red, 90°&ndash;yellow,
109+
180°&ndash;green, 270°&ndash;blue. Note that this is similar to, but
110+
not the same as the H value in HSL or HSV color models.
111+
112+
```example
113+
#|start-interactive-values
114+
view.z = 0.3
115+
c = 75 [0:100:1]
116+
l = 50 [0:100:1]
117+
end-interactive-values|#
118+
119+
(union
120+
(color (saturate-xyz (lch-xyz #<:l :c 0>))
121+
(sphere #<-3 1 0> 1))
122+
(color (saturate-xyz (lch-xyz #<:l :c 90>))
123+
(sphere #<-1 1 0> 1))
124+
(color (saturate-xyz (lch-xyz #<:l :c 180>))
125+
(sphere #<1 1 0> 1))
126+
(color (saturate-xyz (lch-xyz #<:l :c 270>))
127+
(sphere #<3 1 0> 1)))
128+
```
129+
130+
This shows four spheres each with the same lightness and chrominance values at
131+
the four cardinal color directions in the L\*C\*h° color space.
132+
64133
## Conversion Functions
65134

66135
This tool processes color internally within the shader in the CIE XYZ
67-
colorspace. This is makes the math that operates on color values easier in the
68-
shader pipeline, because the color values are in a perceptually uniform space,
69-
but it is harder to reason about them.
136+
color space. This is makes the math that operates on color values easier in the
137+
shader pipeline, because the color values are in a linear space, but it is
138+
harder to reason about them.
70139

71140
The following conversion functions are provided which can make manipulating colors easier.
72141

@@ -85,8 +154,8 @@ Or to adjust the hue by a fixed value without changing saturation or lightness
85154
(lch-xyz (+ (xyz-lch a) #<0 0 20>))
86155
```
87156

88-
This converts to the CIE LCH colorspace (via CIE LAB) (where the H is a hue value in degrees),
89-
adds 20° and then converts back.
157+
This converts to the CIE LCH color space (via CIE LAB) (where the H is a hue
158+
value in degrees), adds 20° and then converts back.
90159

91160
![RGB → XYZ](rgb-xyz.doc)
92161
![XYZ → RGB](xyz-rgb.doc)

docs/faq.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ The code editing font is Nikita Prokopov's "[Fira
8989
Code](https://github.com/tonsky/FiraCode)" as packaged by
9090
[fontsource](https://npmjs.com/@fontsource-variable/fira-code)
9191

92+
The evaluation time color space conversions were derived from the
93+
[colorspaces.js](https://github.com/boronine/colorspaces.js) library by Alexei
94+
Boronine.
95+
9296
# Acknowledgments
9397

9498
It is pretty much impossible to play with SDFs without recognizing the contribution that Inigo Quillez has made in this space.

src/app.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,9 @@ export const App: React.FC = () => {
315315
fontVariantLigatures: "discretionary-ligatures",
316316
}}
317317
>
318-
<pre style={{ fontFamily: "unset", whiteSpace: "pre-wrap" }}>
319-
{generated}
320-
</pre>
318+
<pre style={{ whiteSpace: "pre-wrap" }}>{generated}</pre>
321319
<pre
322320
style={{
323-
fontFamily: "unset",
324321
whiteSpace: "pre-wrap",
325322
color: currTheme.red,
326323
}}

src/builtins.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,7 +1435,7 @@ const kBuiltins: Internal[] = [
14351435
const vec = coerce(args[0], "vec");
14361436
return {
14371437
type: "vec",
1438-
code: `clamp(vec3<f32>(0), kReferenceD65 * 0.01, ${vec.code})`,
1438+
code: `clamp(vec3<f32>(0), kReferenceD65, ${vec.code})`,
14391439
};
14401440
},
14411441
docs: [
@@ -1520,8 +1520,9 @@ const kBuiltins: Internal[] = [
15201520
},
15211521
docs: [
15221522
"(**rgb-xyz** *rgb*)",
1523-
"Converts the *rgb* vector representing a color in sRgb space, into a vector " +
1524-
"representing the same color in CIE 1931 XYZ color space, using the D65 reference illuminant",
1523+
"Converts the *rgb* vector representing a color in sRgb space, into a " +
1524+
"vector representing the same color in CIE 1931 XYZ color space, using " +
1525+
"the D65 reference illuminant",
15251526
],
15261527
insertText: "rgb-xyz ${1:rgb}",
15271528
},
@@ -1544,8 +1545,9 @@ const kBuiltins: Internal[] = [
15441545
},
15451546
docs: [
15461547
"(**xyz-rgb** *xyz*)",
1547-
"Converts the *xyz* vector representing a color in CIE 1931 XYZ color space, into a vector " +
1548-
"representing the same color in sRgb color space, using the D65 reference illuminant",
1548+
"Converts the *xyz* vector representing a color in CIE 1931 XYZ color " +
1549+
"space, into a vector representing the same color in sRgb color space, " +
1550+
"using the D65 reference illuminant",
15491551
],
15501552
insertText: "xyz-rgb ${1:xyz}",
15511553
},
@@ -1629,7 +1631,7 @@ const kBuiltins: Internal[] = [
16291631
requireArity("lch-lab", 1, args);
16301632
requireVector("lch-lab", 0, args[0]);
16311633
const lch = args[0].value as Vector;
1632-
const lab = make_color("CIEXYZ", [lch.x, lch.y, lch.z]).as(
1634+
const lab = make_color("CIELCH", [lch.x, lch.y, lch.z]).as(
16331635
"CIELAB"
16341636
) as ColorTuple;
16351637

@@ -1718,7 +1720,7 @@ const kLambdas: MacroDef[] = [
17181720
"space, into a vector representing the same color in CIE 1931 XYZ color " +
17191721
"space, using the D65 reference illuminant",
17201722
],
1721-
insertText: "lch-xyz ${1:xyz}",
1723+
insertText: "lch-xyz ${1:lch}",
17221724
},
17231725
{
17241726
name: "step",

src/colorspaces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,8 @@ const polar_to_scalar = (tuple: ColorValue): ColorValue => {
259259
const _C = tuple[1];
260260
const _h = tuple[2];
261261
const _h_rad = (_h / 360) * 2 * Math.PI;
262-
const var1 = Math.cos(_h_rad) * _C;
263-
const var2 = Math.sin(_h_rad) * _C;
262+
const var1 = Math.sin(_h_rad) * _C;
263+
const var2 = Math.cos(_h_rad) * _C;
264264
return [_L, var1, var2];
265265
};
266266
conv["CIELCH"]["CIELAB"] = polar_to_scalar;

src/sdf/colors.wgsl

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,107 @@
11
const kRgbToXyzMat = mat3x3<f32>(
2-
vec3<f32>(0.4124, 0.2126, 0.0193),
3-
vec3<f32>(0.3576, 0.7152, 0.1192),
4-
vec3<f32>(0.1805, 0.0722, 0.9505));
2+
vec3<f32>(0.4124, 0.2126, 0.0193),
3+
vec3<f32>(0.3576, 0.7152, 0.1192),
4+
vec3<f32>(0.1805, 0.0722, 0.9505)
5+
);
56

67
fn colRgbToXyz(rgb: vec3<f32>) -> vec3<f32> {
7-
return kRgbToXyzMat * rgb;
8+
return kRgbToXyzMat * rgb;
89
}
910

1011
fn colSRgbToXyz(sRgb: vec3<f32>) -> vec3<f32> {
11-
var rgb = select(sRgb / 12.92,
12-
pow((sRgb + 0.055) / 1.055, vec3<f32>(2.4)),
13-
sRgb > vec3<f32>(0.04045));
14-
return kRgbToXyzMat * rgb;
12+
var rgb = select(sRgb / 12.92,
13+
pow((sRgb + 0.055) / 1.055, vec3<f32>(2.4)),
14+
sRgb > vec3<f32>(0.04045));
15+
return kRgbToXyzMat * rgb;
1516
}
1617

1718
const kXyzToRgbMat = mat3x3<f32>(
18-
vec3<f32>(3.2046, -0.9689, 0.0577),
19-
vec3<f32>(-1.5372, 1.8758, -0.2040),
20-
vec3<f32>(-0.4986, 0.0415, 1.0570));
19+
vec3<f32>(3.2046, -0.9689, 0.0577),
20+
vec3<f32>(-1.5372, 1.8758, -0.2040),
21+
vec3<f32>(-0.4986, 0.0415, 1.0570)
22+
);
2123

2224
fn colXyzToSRgb(xyz: vec3<f32>) -> vec3<f32> {
23-
var rgb = kXyzToRgbMat * xyz;
25+
var rgb = kXyzToRgbMat * xyz;
2426

25-
rgb = select(rgb * 12.92,
26-
1.055 * (pow(rgb, vec3<f32>(1/2.4)) - 0.055),
27-
rgb > vec3<f32>(0.0031308));
27+
rgb = select(rgb * 12.92,
28+
1.055 * (pow(rgb, vec3<f32>(1 / 2.4)) - 0.055),
29+
rgb > vec3<f32>(0.0031308));
2830

29-
return rgb;
31+
return rgb;
3032
}
3133

32-
const kReferenceD65 = vec3<f32>(95.0489, 100, 108.884);
34+
const kReferenceD65 = vec3<f32>(0.950489, 1, 1.08884);
35+
36+
37+
// matrix
38+
// ┌ ┐
39+
// │ 0 116 0 -16 │
40+
// │ 500 -500 0 0 │
41+
// │ 0 200 -200 0 │
42+
// └ ┘
43+
3344

3445
const kXyzToLabMat = mat4x3<f32>(
35-
vec3<f32>(0, 500, 0),
36-
vec3<f32>(116, -500, 200),
37-
vec3<f32>(0, 0, -200),
38-
vec3<f32>(-16, 0, 0));
46+
vec3<f32>(0, 500, 0),
47+
vec3<f32>(116, -500, 200),
48+
vec3<f32>(0, 0, -200),
49+
vec3<f32>(-16, 0, 0)
50+
);
3951

4052
fn colXyzToCIELab(xyz: vec3<f32>) -> vec3<f32> {
41-
var s = xyz / kReferenceD65;
53+
var s = xyz / kReferenceD65;
4254

43-
const d3 = (6.0 * 6.0 * 6.0) / (29.0 * 29.0 * 29.0);
44-
const i3d2 = (29.0 * 29.0) / (6.0 * 6.0 * 3);
55+
const d3 = (6.0 * 6.0 * 6.0) / (29.0 * 29.0 * 29.0);
56+
const i3d2 = (29.0 * 29.0) / (6.0 * 6.0 * 3);
4557

46-
s = select(s * i3d2 + vec3<f32>(4.0/29.0),
47-
pow(s, vec3<f32>(1/3)),
48-
s > vec3<f32>(d3));
58+
s = select(s * i3d2 + vec3<f32>(4.0 / 29.0),
59+
pow(s, vec3<f32>(1.0 / 3.0)),
60+
s > vec3<f32>(d3));
4961

50-
return kXyzToLabMat * vec4<f32>(s, 1);
62+
return kXyzToLabMat * vec4<f32>(s, 1);
5163
}
5264

65+
// matrix
66+
// ┌ ┐
67+
// │ 1/116 1/500 0 16/116 │
68+
// │ 1/116 0 0 16/116 │
69+
// │ 1/116 0 -1/200 16/116 │
70+
// └ ┘
5371
const kLabToXyzMat = mat4x3<f32>(
54-
vec3<f32>(1.0/116.0),
55-
vec3<f32>(1.0/500.0, 0, 0),
56-
vec3<f32>(0, 0, -1.0/200.0),
57-
vec3<f32>(16.0/116.0));
72+
vec3<f32>(1.0 / 116.0, 1.0 / 116.0, 1.0 / 116.0),
73+
vec3<f32>(1.0 / 500.0, 0, 0),
74+
vec3<f32>(0, 0, -1.0 / 200.0),
75+
vec3<f32>(16.0 / 116.0, 16.0 / 116.0, 16.0 / 116.0)
76+
);
5877

5978
fn colCIELabToXyz(lab: vec3<f32>) -> vec3<f32> {
60-
var xyz = kLabToXyzMat * vec4<f32>(lab, 1);
79+
var xyz = kLabToXyzMat * vec4<f32>(lab, 1);
6180

62-
const d = vec3<f32>(6.0/29.0);
63-
const t3d2 = vec3<f32>((3.0 * 6.0 * 6.0) / (29.0 * 29.0));
81+
// δ = 6/29 24/116
82+
const d = vec3<f32>(6.0 / 29.0);
83+
// 3δ²
84+
const t3d2 = vec3<f32>((3.0 * 6.0 * 6.0) / (29.0 * 29.0));
6485

65-
xyz = select(t3d2 * (xyz - vec3<f32>(4.0/29.0)),
66-
xyz * xyz * xyz,
67-
xyz > d);
86+
xyz = select(t3d2 * (xyz - vec3<f32>(4.0 / 29.0)), // t ≤ δ → 3δ²(t - 4/29)
87+
xyz * xyz * xyz, // t > δ → t³
88+
xyz > d);
6889

69-
return xyz * kReferenceD65;
90+
return xyz * kReferenceD65;
7091
}
7192

7293
fn colCIELabToLch(lab: vec3<f32>) -> vec3<f32> {
73-
var h = degrees(atan2(lab.z, lab.y));
74-
h = select(h + 360, h, h >= 0);
75-
76-
return vec3<f32>(
77-
lab.x,
78-
length(lab.yz),
79-
h);
94+
var h = degrees(atan2(lab.z, lab.y));
95+
h = select(h + 360, h, h >= 0);
96+
97+
return vec3<f32>(
98+
lab.x,
99+
length(lab.yz),
100+
h
101+
);
80102
}
81103

82104
fn colCIELchToLab(lch: vec3<f32>) -> vec3<f32> {
83-
var h = radians(lch.z);
84-
return vec3<f32>(lch.x, lch.y * cos(h), lch.y * sin(h));
105+
var h = radians(lch.z);
106+
return vec3<f32>(lch.x, lch.y * sin(h), lch.y * cos(h));
85107
}

src/sdf/map.wgsl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
fn map(p: vec3<f32>) -> vec4<f32> {
2-
var k: f32 = 0;
3-
var col = kReferenceD65 * 0.01;
4-
var res4 = vec4<f32>(0);
2+
var k: f32 = 0;
3+
var col = kReferenceD65;
4+
var res4 = vec4<f32>(0);
55
//MAP-CODE//
66
}

0 commit comments

Comments
 (0)