Skip to main content

viewport_lib/resources/
matcap_data.rs

1/// Procedurally generated built-in matcap textures.
2///
3/// Each matcap is 256×256 RGBA.  The texture is stored with the upper hemisphere
4/// (normals pointing up in view space) at y = 0 (the first row in memory), so
5/// that the fragment shader sampling `uv = (nx * 0.5 + 0.5, -ny * 0.5 + 0.5)`
6/// produces the intuitively correct bright-at-top orientation.
7///
8/// Blendable matcaps use the alpha channel to blend with the base geometry color:
9///   `output = matcap.rgb + matcap.a * base_color`
10/// Static matcaps ignore alpha and override the color entirely.
11
12const SIZE: usize = 256;
13
14fn srgb_to_linear(c: f32) -> f32 {
15    if c <= 0.04045 {
16        c / 12.92
17    } else {
18        ((c + 0.055) / 1.055).powf(2.4)
19    }
20}
21
22fn to_u8(v: f32) -> u8 {
23    (v.clamp(0.0, 1.0) * 255.0).round() as u8
24}
25
26/// Diffuse Lambertian factor N·L, clamped to [0,1].
27fn diffuse(nx: f32, ny: f32, nz: f32, lx: f32, ly: f32, lz: f32) -> f32 {
28    (nx * lx + ny * ly + nz * lz).max(0.0)
29}
30
31/// Blinn-Phong specular N·H^shininess, view direction is (0,0,1) in view space.
32fn specular(nx: f32, ny: f32, nz: f32, lx: f32, ly: f32, lz: f32, shininess: f32) -> f32 {
33    // View direction: (0, 0, 1)
34    let hx = lx;
35    let hy = ly;
36    let hz = lz + 1.0;
37    let hlen = (hx * hx + hy * hy + hz * hz).sqrt();
38    if hlen < 1e-6 {
39        return 0.0;
40    }
41    let hx = hx / hlen;
42    let hy = hy / hlen;
43    let hz = hz / hlen;
44    let ndoth = (nx * hx + ny * hy + nz * hz).max(0.0);
45    ndoth.powf(shininess)
46}
47
48/// Generate a 256×256 RGBA matcap texture from a per-pixel closure.
49///
50/// `f(nx, ny, nz)` receives the view-space surface normal at each pixel and
51/// returns `[r, g, b, a]` in 0..=255.  Pixels outside the unit hemisphere
52/// (radius > 1) receive `[0, 0, 0, 0]`.
53fn generate<F: Fn(f32, f32, f32) -> [u8; 4]>(f: F) -> Vec<u8> {
54    let mut data = Vec::with_capacity(SIZE * SIZE * 4);
55    for y in 0..SIZE {
56        for x in 0..SIZE {
57            // Map pixel center to UV, then to view-space normal.
58            // Convention: y=0 -> ny=+1 (up-facing normals, top of image = bright).
59            let u = (x as f32 + 0.5) / SIZE as f32; // [0, 1]
60            let v = (y as f32 + 0.5) / SIZE as f32; // [0, 1], v=0 -> top
61            let nx = u * 2.0 - 1.0;
62            let ny = 1.0 - v * 2.0; // flip: v=0 -> ny=+1, v=1 -> ny=-1
63            let r2 = nx * nx + ny * ny;
64            if r2 > 1.0 {
65                // Outside the unit disc : no valid normal.
66                data.extend_from_slice(&[0, 0, 0, 0]);
67            } else {
68                let nz = (1.0 - r2).sqrt();
69                data.extend_from_slice(&f(nx, ny, nz));
70            }
71        }
72    }
73    data
74}
75
76// ---------------------------------------------------------------------------
77// Built-in matcap generators
78// ---------------------------------------------------------------------------
79
80/// Clay : warm orange-brown, RGBA blendable.
81///
82/// A single warm directional light from upper-left plus a cool fill from the right.
83/// Alpha encodes how strongly the matcap tints the underlying geometry color.
84pub fn clay() -> Vec<u8> {
85    // Main light: warm, upper-left
86    let (mlx, mly, mlz) = {
87        let len = (0.4f32 * 0.4 + 0.9 * 0.9 + 0.4 * 0.4).sqrt();
88        (-0.4 / len, 0.9 / len, 0.4 / len)
89    };
90    // Fill light: cool, lower-right, low intensity
91    let (flx, fly, flz) = {
92        let len = (0.6f32 * 0.6 + 0.2 * 0.2 + 0.3 * 0.3).sqrt();
93        (0.6 / len, -0.2 / len, 0.3 / len)
94    };
95
96    let base = [
97        srgb_to_linear(0.78),
98        srgb_to_linear(0.47),
99        srgb_to_linear(0.26),
100    ];
101    let fill_color = [
102        srgb_to_linear(0.45),
103        srgb_to_linear(0.58),
104        srgb_to_linear(0.72),
105    ];
106
107    generate(|nx, ny, nz| {
108        let d = diffuse(nx, ny, nz, mlx, mly, mlz);
109        let fd = diffuse(nx, ny, nz, flx, fly, flz) * 0.3;
110        let s = specular(nx, ny, nz, mlx, mly, mlz, 18.0) * 0.25;
111        let ambient = 0.18;
112
113        let r = base[0] * (ambient + d * 0.75) + fill_color[0] * fd + s;
114        let g = base[1] * (ambient + d * 0.75) + fill_color[1] * fd + s;
115        let b = base[2] * (ambient + d * 0.75) + fill_color[2] * fd + s;
116        // Alpha: blend weight : tints base geometry color at ~60 %
117        let a = 0.58_f32;
118        [to_u8(r), to_u8(g), to_u8(b), to_u8(a)]
119    })
120}
121
122/// Wax : warm peach with a wide soft specular, RGBA blendable.
123pub fn wax() -> Vec<u8> {
124    let (lx, ly, lz) = {
125        let len = (0.3f32 * 0.3 + 1.0 * 1.0 + 0.5 * 0.5).sqrt();
126        (0.3 / len, 1.0 / len, 0.5 / len)
127    };
128    let base = [
129        srgb_to_linear(0.95),
130        srgb_to_linear(0.78),
131        srgb_to_linear(0.65),
132    ];
133
134    generate(|nx, ny, nz| {
135        let d = diffuse(nx, ny, nz, lx, ly, lz);
136        // Wide specular (subsurface scattering approximation)
137        let s = specular(nx, ny, nz, lx, ly, lz, 8.0) * 0.55;
138        let ambient = 0.22;
139        let r = base[0] * (ambient + d * 0.68) + s * 0.95;
140        let g = base[1] * (ambient + d * 0.68) + s * 0.88;
141        let b = base[2] * (ambient + d * 0.68) + s * 0.78;
142        let a = 0.45_f32;
143        [to_u8(r), to_u8(g), to_u8(b), to_u8(a)]
144    })
145}
146
147/// Candy : vivid colorful gradient, RGBA blendable.
148///
149/// Cycles through hue as a function of the upper hemisphere angle, giving
150/// a rainbow-sphere appearance that blends with geometry color.
151pub fn candy() -> Vec<u8> {
152    let (lx, ly, lz) = {
153        let len = (0.0f32 * 0.0 + 1.0 * 1.0 + 0.6 * 0.6).sqrt();
154        (0.0 / len, 1.0 / len, 0.6 / len)
155    };
156
157    generate(|nx, ny, nz| {
158        let d = diffuse(nx, ny, nz, lx, ly, lz);
159        let s = specular(nx, ny, nz, lx, ly, lz, 32.0) * 0.7;
160        let ambient = 0.25;
161
162        // Hue from horizontal angle in view space
163        let angle = nx.atan2(nz) / std::f32::consts::PI; // -1..+1
164        let hue = (angle * 0.5 + 0.5 + ny * 0.12).fract(); // 0..1
165
166        // HSV -> RGB for vivid hue
167        let h6 = hue * 6.0;
168        let i = h6.floor() as u32 % 6;
169        let f = h6 - h6.floor();
170        let q = 1.0 - f;
171        let (hr, hg, hb) = match i {
172            0 => (1.0_f32, f, 0.0),
173            1 => (q, 1.0, 0.0),
174            2 => (0.0, 1.0, f),
175            3 => (0.0, q, 1.0),
176            4 => (f, 0.0, 1.0),
177            _ => (1.0, 0.0, q),
178        };
179
180        let lit = (ambient + d * 0.65).min(1.0);
181        let r = hr * lit + s;
182        let g = hg * lit + s;
183        let b = hb * lit + s;
184        let a = 0.28_f32;
185        [to_u8(r), to_u8(g), to_u8(b), to_u8(a)]
186    })
187}
188
189/// Flat : neutral gray Lambertian, RGBA blendable.
190///
191/// Simple diffuse-only shading, no specular, no color contribution.  The alpha
192/// channel controls how strongly the lighting gradient blends onto geometry.
193pub fn flat() -> Vec<u8> {
194    let (lx, ly, lz) = (0.0_f32, 1.0, 0.0);
195
196    generate(|nx, ny, nz| {
197        let d = diffuse(nx, ny, nz, lx, ly, lz);
198        let ambient = 0.20;
199        let gray = ambient + d * 0.78;
200        let a = 0.65_f32;
201        [to_u8(gray), to_u8(gray), to_u8(gray), to_u8(a)]
202    })
203}
204
205/// Ceramic : clean white with sharp specular, RGB static.
206pub fn ceramic() -> Vec<u8> {
207    let (lx, ly, lz) = {
208        let len = (-0.2f32 * -0.2 + 0.85 * 0.85 + 0.5 * 0.5).sqrt();
209        (-0.2 / len, 0.85 / len, 0.5 / len)
210    };
211    let base = [
212        srgb_to_linear(0.94),
213        srgb_to_linear(0.95),
214        srgb_to_linear(0.97),
215    ];
216
217    generate(|nx, ny, nz| {
218        let d = diffuse(nx, ny, nz, lx, ly, lz);
219        let s = specular(nx, ny, nz, lx, ly, lz, 80.0) * 0.85;
220        let ambient = 0.25;
221        let r = base[0] * (ambient + d * 0.72) + s;
222        let g = base[1] * (ambient + d * 0.72) + s;
223        let b = base[2] * (ambient + d * 0.72) + s;
224        [to_u8(r), to_u8(g), to_u8(b), 255]
225    })
226}
227
228/// Jade : deep translucent green, RGB static.
229pub fn jade() -> Vec<u8> {
230    let (lx, ly, lz) = {
231        let len = (0.3f32 * 0.3 + 0.9 * 0.9 + 0.4 * 0.4).sqrt();
232        (0.3 / len, 0.9 / len, 0.4 / len)
233    };
234    let surface = [
235        srgb_to_linear(0.15),
236        srgb_to_linear(0.58),
237        srgb_to_linear(0.36),
238    ];
239    let deep = [
240        srgb_to_linear(0.04),
241        srgb_to_linear(0.28),
242        srgb_to_linear(0.18),
243    ];
244    let highlight = [
245        srgb_to_linear(0.72),
246        srgb_to_linear(0.90),
247        srgb_to_linear(0.70),
248    ];
249
250    generate(|nx, ny, nz| {
251        let d = diffuse(nx, ny, nz, lx, ly, lz);
252        let s = specular(nx, ny, nz, lx, ly, lz, 40.0) * 0.6;
253        let rim = (1.0 - nz).powf(2.5) * 0.35; // rim light from behind
254        let ambient = 0.18;
255
256        let t = d * 0.80 + ambient;
257        let r = deep[0] * (1.0 - t) + surface[0] * t + highlight[0] * s + 0.12 * rim;
258        let g = deep[1] * (1.0 - t) + surface[1] * t + highlight[1] * s + 0.22 * rim;
259        let b = deep[2] * (1.0 - t) + surface[2] * t + highlight[2] * s + 0.14 * rim;
260        [to_u8(r), to_u8(g), to_u8(b), 255]
261    })
262}
263
264/// Mud : dark brownish, low specular, rough look, RGB static.
265pub fn mud() -> Vec<u8> {
266    let (lx, ly, lz) = {
267        let len = (0.1f32 * 0.1 + 0.8 * 0.8 + 0.3 * 0.3).sqrt();
268        (0.1 / len, 0.8 / len, 0.3 / len)
269    };
270    let base = [
271        srgb_to_linear(0.32),
272        srgb_to_linear(0.20),
273        srgb_to_linear(0.10),
274    ];
275    let dark = [
276        srgb_to_linear(0.08),
277        srgb_to_linear(0.05),
278        srgb_to_linear(0.02),
279    ];
280
281    generate(|nx, ny, nz| {
282        let d = diffuse(nx, ny, nz, lx, ly, lz);
283        let s = specular(nx, ny, nz, lx, ly, lz, 4.0) * 0.12;
284        let ambient = 0.12;
285
286        // Add coarse directional variation to simulate rough texture
287        let rough = 0.5 + 0.5 * ((nx * 7.0).sin() * (ny * 5.0).cos() * 0.5);
288        let t = (ambient + d * 0.65) * rough;
289        let r = dark[0] * (1.0 - t) + base[0] * t + s;
290        let g = dark[1] * (1.0 - t) + base[1] * t + s;
291        let b = dark[2] * (1.0 - t) + base[2] * t + s;
292        [to_u8(r), to_u8(g), to_u8(b), 255]
293    })
294}
295
296/// Normal : view-space normal visualization, RGB static.
297///
298/// R = nx mapped [−1, 1] -> [0, 1], G = ny, B = nz.
299pub fn normal() -> Vec<u8> {
300    generate(|nx, ny, nz| {
301        let r = nx * 0.5 + 0.5;
302        let g = ny * 0.5 + 0.5;
303        let b = nz * 0.5 + 0.5;
304        [to_u8(r), to_u8(g), to_u8(b), 255]
305    })
306}