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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
|
#define _USE_MATH_DEFINES
#define _XOPEN_SOURCE 700
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <math.h>
#include "color.h"
/*
* Illuminant D, or daylight locus, is is a "standard illuminant" used to
* describe natural daylight as we perceive it, and as such is how we expect
* bright, cold white light sources to look. This is different from the
* planckian locus due to the effects of the atmosphere on sunlight travelling
* through it.
*
* It is on this locus that D65, the whitepoint used by most monitors and
* assumed by display servers, is defined.
*
* This approximation is strictly speaking only well-defined between 4000K and
* 25000K, but we stretch it a bit further down for transition purposes.
*/
static int illuminant_d(int temp, double *x, double *y) {
// https://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D
if (temp >= 2500 && temp <= 7000) {
*x = 0.244063 +
0.09911e3 / temp +
2.9678e6 / pow(temp, 2) -
4.6070e9 / pow(temp, 3);
} else if (temp > 7000 && temp <= 25000) {
*x = 0.237040 +
0.24748e3 / temp +
1.9018e6 / pow(temp, 2) -
2.0064e9 / pow(temp, 3);
} else {
errno = EINVAL;
return -1;
}
*y = (-3 * pow(*x, 2)) + (2.870 * (*x)) - 0.275;
return 0;
}
/*
* Planckian locus, or black body locus, describes the color of a black body at
* a certain temperatures directly at its source, rather than observed through
* a thick atmosphere.
*
* While we are used to bright light coming from afar and going through the
* atmosphere, we are used to seeing dim incandescent light sources from close
* enough for the atmosphere to not affect its perception, dictating how we
* expect dim, warm light sources to look.
*
* This approximation is only valid from 1667K to 25000K.
*/
static int planckian_locus(int temp, double *x, double *y) {
// https://en.wikipedia.org/wiki/Planckian_locus#Approximation
if (temp >= 1667 && temp <= 4000) {
*x = -0.2661239e9 / pow(temp, 3) -
0.2343589e6 / pow(temp, 2) +
0.8776956e3 / temp +
0.179910;
if (temp <= 2222) {
*y = -1.1064814 * pow(*x, 3) -
1.34811020 * pow(*x, 2) +
2.18555832 * (*x) -
0.20219683;
} else {
*y = -0.9549476 * pow(*x, 3) -
1.37418593 * pow(*x, 2) +
2.09137015 * (*x) -
0.16748867;
}
} else if (temp > 4000 && temp < 25000) {
*x = -3.0258469e9 / pow(temp, 3) +
2.1070379e6 / pow(temp, 2) +
0.2226347e3 / temp +
0.240390;
*y = 3.0817580 * pow(*x, 3) -
5.87338670 * pow(*x, 2) +
3.75112997 * (*x) -
0.37001483;
} else {
errno = EINVAL;
return -1;
}
return 0;
}
static double clamp(double value) {
if (value > 1.0) {
return 1.0;
} else if (value < 0.0) {
return 0.0;
} else {
return value;
}
}
static struct rgb xyz_to_rgb(const struct xyz *xyz) {
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
return (struct rgb) {
.r = pow(clamp(3.2404542 * xyz->x - 1.5371385 * xyz->y - 0.4985314 * xyz->z), 1.0 / 2.2),
.g = pow(clamp(-0.9692660 * xyz->x + 1.8760108 * xyz->y + 0.0415560 * xyz->z), 1.0 / 2.2),
.b = pow(clamp(0.0556434 * xyz->x - 0.2040259 * xyz->y + 1.0572252 * xyz->z), 1.0 / 2.2)
};
}
static void rgb_normalize(struct rgb *rgb) {
double maxw = fmax(rgb->r, fmax(rgb->g, rgb->b));
rgb->r /= maxw;
rgb->g /= maxw;
rgb->b /= maxw;
}
struct rgb calc_whitepoint(int temp) {
if (temp == 6500) {
return (struct rgb) {.r = 1.0, .g = 1.0, .b = 1.0};
}
// We are not trying to calculate the accurate whitepoint, but rather
// an expected observed whitepoint. We generally expect dim and warm
// light sources to follow the planckian locus, while we expect bright
// and cold light sources to follow the daylight locus. There is no
// "correct" way to transition between these two curves, and so the
// goal is purely to be subjectively pleasant/non-jarring.
//
// A smooth transition between the two in the range between 2500K and
// 4000K seems to do the trick for now.
struct xyz wp;
if (temp >= 25000) {
illuminant_d(25000, &wp.x, &wp.y);
} else if (temp >= 4000) {
illuminant_d(temp, &wp.x, &wp.y);
} else if (temp >= 2500) {
double x1, y1, x2, y2;
illuminant_d(temp, &x1, &y1);
planckian_locus(temp, &x2, &y2);
double factor = (4000. - temp) / 1500.;
double sinefactor = (cos(M_PI*factor) + 1.0) / 2.0;
wp.x = x1 * sinefactor + x2 * (1.0 - sinefactor);
wp.y = y1 * sinefactor + y2 * (1.0 - sinefactor);
} else {
planckian_locus(temp >= 1667 ? temp : 1667, &wp.x, &wp.y);
}
wp.z = 1.0 - wp.x - wp.y;
struct rgb wp_rgb = xyz_to_rgb(&wp);
rgb_normalize(&wp_rgb);
return wp_rgb;
}
|