public/math/sh.mjs
与本次数学页面一起发布的源码快照。SHA-256: e2cb85a24dcd0426062b2918baaf9928e5192f3989329f07bea1c0a17918d593
1// Real, orthonormal SH. Ordering and signs follow Graphdeco's 3DGS convention.
2// Independent educational evaluator; the STG-Lite renderer stores direct RGB.
3export const SH_COUNT = 16;
4export const C0 = 1 / Math.sqrt(4 * Math.PI);
5
6export function unitDirection(direction) {
7 const length = Math.hypot(...direction);
8 if (direction.length !== 3 || !direction.every(Number.isFinite) || !(length > 0)) {
9 throw new RangeError('A finite, nonzero 3D direction is required');
10 }
11 return direction.map(value => value / length);
12}
13
14export function directionFromAngles(azimuth, polar) {
15 // Radians; theta is measured from +z, phi from +x towards +y.
16 return [Math.sin(polar) * Math.cos(azimuth), Math.sin(polar) * Math.sin(azimuth), Math.cos(polar)];
17}
18
19export function shBasis(direction, degree = 3) {
20 if (!Number.isInteger(degree) || degree < 0 || degree > 3) throw new RangeError('Degree must be 0..3');
21 const [x, y, z] = unitDirection(direction);
22 const pi = Math.PI, xx = x*x, yy = y*y, zz = z*z;
23 const a = Math.sqrt(3 / (4*pi)), b = Math.sqrt(15 / (4*pi));
24 const c = Math.sqrt(35 / (32*pi)), d = Math.sqrt(21 / (32*pi));
25 const values = [
26 C0,
27 -a*y, a*z, -a*x,
28 b*x*y, -b*y*z, Math.sqrt(5/(16*pi))*(2*zz-xx-yy),
29 -b*x*z, Math.sqrt(15/(16*pi))*(xx-yy),
30 -c*y*(3*xx-yy), Math.sqrt(105/(4*pi))*x*y*z,
31 -d*y*(4*zz-xx-yy), Math.sqrt(7/(16*pi))*z*(2*zz-3*xx-3*yy),
32 -d*x*(4*zz-xx-yy), Math.sqrt(105/(16*pi))*z*(xx-yy), -c*x*(xx-3*yy),
33 ];
34 return values.slice(0, (degree+1)**2);
35}
36
37export function sampleSH(coefficients, direction, degree = 3) {
38 const basis = shBasis(direction, degree);
39 if (coefficients.length < basis.length || coefficients.slice(0,basis.length).some(
40 rgb => rgb.length !== 3 || !rgb.every(Number.isFinite))) throw new RangeError('Missing finite RGB coefficients');
41 const contributions = basis.map((value,k) => coefficients[k].map(a => a*value));
42 const raw = [0,1,2].map(ch => contributions.reduce((sum,rgb) => sum+rgb[ch],0));
43 const shifted = raw.map(value => value+0.5);
44 // Graphdeco's forward.cu clamps below zero; it does NOT clamp above one here.
45 const shaderColor = shifted.map(value => Math.max(0,value));
46 // A separate SDR display clipping step for this canvas demo (no tone mapping).
47 const display = shaderColor.map(value => Math.min(1,value));
48 return { basis, contributions, raw, shifted, shaderColor, display };
49}
50
51export function blankCoefficients(rgb = [0.5,0.5,0.5]) {
52 const coefficients = Array.from({length:SH_COUNT}, () => [0,0,0]);
53 coefficients[0] = rgb.map(value => (value-0.5)/C0);
54 return coefficients;
55}
56
57export function presetCoefficients(name) {
58 const a = blankCoefficients([0.52,0.39,0.26]);
59 if (name === 'dc') return a;
60 a[2] = [0.50,0.22,-0.28];
61 a[3] = [-0.32,0.27,0.43];
62 if (name === 'first') return a;
63 a[4] = [0.32,-0.24,0.08];
64 a[6] = [-0.21,0.15,0.36];
65 if (name === 'second') return a;
66 a[10] = [0.18,0.06,-0.24];
67 a[15] = [0.22,-0.18,0.09];
68 return a;
69}
70
71export function sphereSamples(count) {
72 if (!Number.isInteger(count) || count < 1) throw new RangeError('Positive sample count required');
73 // Equal-area z strips, with a golden-angle rotation to avoid aligned columns.
74 const golden = Math.PI*(3-Math.sqrt(5));
75 return Array.from({length:count}, (_,i) => {
76 const z = 1-2*(i+0.5)/count, radius = Math.sqrt(1-z*z), phi = i*golden;
77 return [radius*Math.cos(phi), radius*Math.sin(phi), z];
78 });
79}
80
81export function targetColor(direction) {
82 const v = unitDirection(direction), axis = unitDirection([1,-1,1]);
83 const cosine = v.reduce((sum,value,i) => sum+value*axis[i],0);
84 const spot = Math.exp((cosine-1)/0.14);
85 return [0.12+0.84*spot, 0.16+0.65*spot, 0.24+0.28*spot];
86}
87
88export function projectColor(target = targetColor, degree = 3, count = 4096) {
89 const result = Array.from({length:(degree+1)**2}, () => [0,0,0]);
90 for (const v of sphereSamples(count)) {
91 const residual = target(v).map(value => value-0.5);
92 shBasis(v,degree).forEach((basis,k) => {
93 for (let ch=0;ch<3;ch++) result[k][ch] += residual[ch]*basis*4*Math.PI/count;
94 });
95 }
96 return result;
97}
98