← 返回数学手册

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