Give every star a limb-darkened surface in its own colour, and light its planets with it
Every star but the Sun was a flat disc of one colour, and the Sun wore the texture pack's orange photograph, lit by the same white light as every other star's planets. Every star now shares one TSL material (starSurfaceMaterial in the scene): the Sun's map in grey, times the colour of a blackbody at the star's temperature, times a linear limb-darkening law, 1 - 0.6 (1 - mu), the Sun's coefficient in the visible. The temperature is the one the radius uses: the archive's st_teff for a host, else the dwarf sequence at its colour or type, else the Sun's. The colour comes from blackbodyColor (stellar.ts): Kim et al.'s cubic fit to the Planckian locus, then CIE XYZ to linear sRGB, brightest channel 1. At D65 it gives 2 900 K (255, 180, 103), 5 800 K (255, 241, 235) and 9 600 K (208, 219, 255), against (255, 182, 98), (255, 241, 231) and (211, 221, 255) in Charity's integrated blackbody table. The tint is a uniform, so the shader is built once and not per system. The star's PointLight takes the same colour against the Sun's, since the planets' photographs were taken in sunlight: the Sun's light stays white at pi, TRAPPIST-1's (2 566 K) is (1, 0.44, 0.10) and Proxima's (2 900 K) (1, 0.52, 0.17), Sirius's (0.52, 0.67, 1). The intensity stays pi. No halo comes back. sun.jpg was 2048 by 1024 and 822 427 bytes for a disc that reaches 216 px across at the Sun's closest approach on a 1080-line screen. It is now 1024 by 512 in grey, 31 306 bytes, which covers the disc to a 1440-line screen. Its brightness varied by 56 % rms, which made every star a mottled rock; it is rescaled to 14 % rms about the display's white, of the order of the Sun's granulation contrast, the brighter half clipped as in a photograph exposed for the disc (6 % rms remains). Measured on the dev server (1600 by 1000, the camera at its closest approach): - the disc's brightness against the law, from r/R 0.52 to 0.97: Sun 0.970/0.841/0.763/0.657/0.560 against 0.927/0.829/0.755/0.645/0.554, ups And and Sirius the same to within 0.05; - the disc's centre in sRGB: Sun (245, 233, 226), ups And (F8V, 6 157 K) (249, 239, 239), Sirius (199, 209, 243); - the first system entry of a fresh page, three runs each in alternating blocks against the previous commit: Sol's longest task 90 ms before, 86 ms after, two tasks over 50 ms in every run either way; Proxima Centauri's none over 50 ms after, one run of six with a 53 ms task before. The long tasks on Sol's first entry predate this change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
absoluteMagnitude,
|
||||
blackbodyColor,
|
||||
bolometricCorrection,
|
||||
effectiveTemperatureK,
|
||||
luminositySolar,
|
||||
@@ -164,3 +165,29 @@ describe('radiusFromLuminositySolar', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('blackbodyColor', () => {
|
||||
/** As the display shows it: sRGB-encoded, 0 to 255. */
|
||||
const displayed = (rgb: readonly number[]) => rgb.map((v) => Math.round(255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055)));
|
||||
|
||||
it('gives the colours of the stars against the display white', () => {
|
||||
// Charity's blackbody colour table (CIE 1931 2°, D65): 2 900 K #ffb662, 5 800 K #fff1e7, 9 600 K #d3ddff.
|
||||
for (const [temperatureK, expected] of [[2900, [255, 182, 98]], [5800, [255, 241, 231]], [9600, [211, 221, 255]]] as const) {
|
||||
displayed(blackbodyColor(temperatureK)).forEach((channel, i) => expect(Math.abs(channel - expected[i])).toBeLessThanOrEqual(5));
|
||||
}
|
||||
});
|
||||
|
||||
it("is white at the white point it is given, and an M dwarf's light orange-red against the Sun's", () => {
|
||||
expect(blackbodyColor(SOLAR_EFFECTIVE_TEMPERATURE_K, SOLAR_EFFECTIVE_TEMPERATURE_K)).toEqual([1, 1, 1]);
|
||||
const [r, g, b] = blackbodyColor(2566, SOLAR_EFFECTIVE_TEMPERATURE_K);
|
||||
expect(r).toBe(1);
|
||||
expect(g).toBeCloseTo(0.44, 2);
|
||||
expect(b).toBeCloseTo(0.1, 2);
|
||||
});
|
||||
|
||||
it('holds the ends of the fit, and never goes negative', () => {
|
||||
expect(blackbodyColor(800)).toEqual(blackbodyColor(1667));
|
||||
expect(blackbodyColor(60000)).toEqual(blackbodyColor(25000));
|
||||
expect(Math.min(...blackbodyColor(1667))).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,3 +159,41 @@ export function effectiveTemperatureK(star: StellarPhotometry): number | null {
|
||||
export function radiusFromLuminositySolar(luminositySolar: number, temperatureK: number): number {
|
||||
return Math.sqrt(luminositySolar) / (temperatureK / SOLAR_EFFECTIVE_TEMPERATURE_K) ** 2;
|
||||
}
|
||||
|
||||
/** The range Kim et al.'s fit to the Planckian locus covers; a temperature outside it is clamped. */
|
||||
const PLANCKIAN_LOCUS_MIN_K = 1667;
|
||||
const PLANCKIAN_LOCUS_MAX_K = 25000;
|
||||
|
||||
/**
|
||||
* The colour of a blackbody at `temperatureK`, in linear sRGB with its brightest channel at 1:
|
||||
* its chromaticity off the Planckian locus (Kim et al. 2002, the cubic fit to CIE 1931), then
|
||||
* CIE XYZ to sRGB. Against the display's own white, D65, unless `whitePointK` names the blackbody
|
||||
* that is to read as white — as the Sun's does for the photographs of its planets, which were
|
||||
* taken in its light.
|
||||
*
|
||||
* At D65, a 2 900 K M dwarf is sRGB (255, 180, 103), the Sun (255, 241, 234), a 9 600 K A star
|
||||
* (208, 219, 255): Charity's table, which integrates the Planck spectrum, gives (255, 182, 98),
|
||||
* (255, 241, 231) at 5 800 K and (211, 221, 255).
|
||||
*/
|
||||
export function blackbodyColor(temperatureK: number, whitePointK?: number): [number, number, number] {
|
||||
const rgb = blackbodyLinearSrgb(temperatureK);
|
||||
const white = whitePointK === undefined ? [1, 1, 1] : blackbodyLinearSrgb(whitePointK);
|
||||
const relative = rgb.map((channel, i) => channel / white[i]);
|
||||
const brightest = Math.max(...relative);
|
||||
return relative.map((channel) => channel / brightest) as [number, number, number];
|
||||
}
|
||||
|
||||
function blackbodyLinearSrgb(temperatureK: number): number[] {
|
||||
const t = 1000 / Math.min(Math.max(temperatureK, PLANCKIAN_LOCUS_MIN_K), PLANCKIAN_LOCUS_MAX_K);
|
||||
const x =
|
||||
t >= 0.25 ? -0.2661239 * t ** 3 - 0.2343589 * t ** 2 + 0.8776956 * t + 0.17991 : -3.0258469 * t ** 3 + 2.1070379 * t ** 2 + 0.2226347 * t + 0.24039;
|
||||
const y =
|
||||
t >= 1000 / 2222
|
||||
? -1.1063814 * x ** 3 - 1.3481102 * x ** 2 + 2.18555832 * x - 0.20219683
|
||||
: t >= 0.25
|
||||
? -0.9549476 * x ** 3 - 1.37418593 * x ** 2 + 2.09137015 * x - 0.16748867
|
||||
: 3.081758 * x ** 3 - 5.8733867 * x ** 2 + 3.75112997 * x - 0.37001483;
|
||||
const [X, Y, Z] = [x / y, 1, (1 - x - y) / y];
|
||||
// Below 1 920 K the locus leaves the sRGB gamut, and blue comes out negative.
|
||||
return [3.2406 * X - 1.5372 * Y - 0.4986 * Z, -0.9689 * X + 1.8758 * Y + 0.0415 * Z, 0.0557 * X - 0.204 * Y + 1.057 * Z].map((channel) => Math.max(channel, 0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user