feat(world): HDRI-Himmel „Nowhere Road" mit Umgebungslicht und scharfen Kaskaden-Schatten
- Himmel: 4k-HDRI von Poly Haven (CC0, tools/assets/hdri.mjs) statt Sky-Shader. Kuppel zeigt das Foto ab 12°, darunter weiches Band und Dunst in Nebelfarbe; Umgebungslicht und Spiegelungen per PMREM; Sonne aus dem Bild gekappt und als Licht übernommen (77°, Stärke 2,5) - Schatten: CSM mit vier Stufen (25/100/350/1000 m, je 2048²), Schutznetz für beleuchtete Materialien, Normal-Bias 1,5 Texel je Stufe gegen Streifen auf doppelseitigen Materialien (Figur, Autos) - Startfigur wirft Schatten, Palmen und Laternen empfangen Schatten, Wasser als ruhige See - Debug-Panel: Sonne, Himmelslicht, Himmelshelligkeit; Namensnennung um das HDRI ergänzt Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
034b12537a
commit
f76e97b69d
15 files changed
+762
-41
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AnimationClip, Quaternion, QuaternionKeyframeTrack, VectorKeyframeTrack } from "three";
|
||||
import { CHARACTERS, retargetClips, teleportTargets } from "../src/characters/index.js";
|
||||
import { AnimationClip, BoxGeometry, Group, Mesh, MeshStandardMaterial, Quaternion, QuaternionKeyframeTrack,
|
||||
VectorKeyframeTrack } from "three";
|
||||
import { CHARACTERS, createCharacterModule, retargetClips, teleportTargets } from "../src/characters/index.js";
|
||||
|
||||
const rest = (q) => new Map([["arm", { position: { x: 0, y: 1, z: 0 }, quaternion: q }]]);
|
||||
|
||||
@@ -64,3 +65,17 @@ describe("teleportTargets", () => {
|
||||
expect(targets[0].z).toBe(-20);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createCharacterModule", () => {
|
||||
// Befund (Schatten-Spec): enableModelShadows lief nur in applyCharacter — das Startmodell
|
||||
// (ual.glb, Mannequin) warf bis zum ersten Figurenwechsel keinen Schatten.
|
||||
it("schaltet Schatten schon für das Startmodell ein", () => {
|
||||
const body = new Mesh(new BoxGeometry(), new MeshStandardMaterial());
|
||||
const playerModel = new Group().add(body);
|
||||
|
||||
createCharacterModule({ playerModel, playerModelConfig: { animations: [] } });
|
||||
|
||||
expect(body.castShadow).toBe(true);
|
||||
expect(body.receiveShadow).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,12 @@ describe("buildAttribution", () => {
|
||||
expect(text).toMatch(/Quaternius[^.]*CC0/);
|
||||
});
|
||||
|
||||
// Himmel und Umgebungslicht kommen aus dem HDRI "Nowhere Road" (src/world/sky.js) — eine Quelle
|
||||
// mehr, die wie die Texturen mit Herkunft und Lizenz im selben Satz stehen soll.
|
||||
it("nennt das HDRI mit Poly Haven und CC0", () => {
|
||||
expect(buildAttribution(manifest)).toMatch(/HDRI[^.]*Poly Haven[^.]*CC0/);
|
||||
});
|
||||
|
||||
// Ruling (task-11-brief.md): keine Lizenz kürzen, keine zwei Lizenzen in einer Zeile
|
||||
// vermischen, kein CC0 für etwas behaupten, das CC BY ist — der SUV-Satz muss darum
|
||||
// isoliert bleiben und darf nicht in denselben Satz wie ein CC0-Eintrag rutschen.
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BoxGeometry, Color, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, MeshStandardMaterial,
|
||||
PerspectiveCamera, Scene, ShaderMaterial, Vector3 } from "three";
|
||||
import { CSM } from "three/examples/jsm/csm/CSM.js";
|
||||
import { cascadeBreaks, createSunShadows, prepareLitMaterials } from "../src/world/shadows.js";
|
||||
|
||||
describe("cascadeBreaks", () => {
|
||||
// Bruch: Meter nicht durch far geteilt (CSM erwartet Anteile) oder letzte Grenze nicht 1 —
|
||||
// dann endete die letzte Stufe vor far und ferne Gebäude verlören ihren Schatten.
|
||||
it("rechnet Meter in Anteile von far um, letzte Grenze immer 1", () => {
|
||||
expect(cascadeBreaks([25, 100, 350, 1000], 1000)).toEqual([0.025, 0.1, 0.35, 1]);
|
||||
});
|
||||
|
||||
it("kappt Grenzen jenseits von far auf 1", () => {
|
||||
expect(cascadeBreaks([25, 100, 350, 1000], 200)).toEqual([0.125, 0.5, 1, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("prepareLitMaterials", () => {
|
||||
const newCsm = () => new CSM({ camera: new PerspectiveCamera(55, 16 / 9, 0.5, 2500), parent: new Scene() });
|
||||
|
||||
// Bruch: ein beleuchtetes Material bleibt ohne USE_CSM — es summiert dann alle vier
|
||||
// Stufenlichter und leuchtet viermal zu hell; oder unbeleuchtete Materialien (Himmel, Linien)
|
||||
// werden unnötig umgebaut.
|
||||
it("richtet beleuchtete Materialien ein, auch in Material-Arrays, unbeleuchtete nicht", () => {
|
||||
const csm = newCsm();
|
||||
const root = new Scene();
|
||||
const lit = new MeshStandardMaterial();
|
||||
const litInArray = new MeshStandardMaterial();
|
||||
const basic = new MeshBasicMaterial();
|
||||
const shader = new ShaderMaterial();
|
||||
const line = new LineBasicMaterial();
|
||||
root.add(new Mesh(new BoxGeometry(), lit), new Mesh(new BoxGeometry(), [litInArray, basic]),
|
||||
new Mesh(new BoxGeometry(), shader), new Line(new BoxGeometry(), line));
|
||||
|
||||
const count = prepareLitMaterials(root, csm);
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(lit.defines.USE_CSM).toBe(1);
|
||||
expect(litInArray.defines.USE_CSM).toBe(1);
|
||||
expect(basic.defines?.USE_CSM).toBeUndefined();
|
||||
expect(shader.defines?.USE_CSM).toBeUndefined();
|
||||
expect(line.defines?.USE_CSM).toBeUndefined();
|
||||
});
|
||||
|
||||
// Bruch: jedes Frame neu einrichten — needsUpdate zwänge three.js dann in jedem Frame zum
|
||||
// Neuübersetzen aller Shader.
|
||||
it("richtet ein Material nur einmal ein", () => {
|
||||
const csm = newCsm();
|
||||
const root = new Scene();
|
||||
root.add(new Mesh(new BoxGeometry(), new MeshStandardMaterial()));
|
||||
prepareLitMaterials(root, csm);
|
||||
|
||||
expect(prepareLitMaterials(root, csm)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSunShadows", () => {
|
||||
const setup = () => {
|
||||
const scene = new Scene();
|
||||
const camera = new PerspectiveCamera(55, 16 / 9, 0.5, 2500);
|
||||
const sun = { direction: new Vector3(0, 0.8, 0.6), color: new Color(1, 0.9, 0.8), intensity: 2.5 };
|
||||
return { scene, camera, sun, shadows: createSunShadows({ scene, camera, sun }) };
|
||||
};
|
||||
|
||||
// Bruch: Vorzeichen vertauscht — CSM erwartet die Laufrichtung des Lichts, sky.js liefert die
|
||||
// Richtung zur Sonne; falsch herum käme das Licht von unten.
|
||||
it("stellt jedes Stufenlicht nach update() in Richtung der Sonne", () => {
|
||||
const { shadows } = setup();
|
||||
|
||||
shadows.update();
|
||||
|
||||
expect(shadows.csm.lights).toHaveLength(4);
|
||||
for (const light of shadows.csm.lights) {
|
||||
const toLight = light.position.clone().sub(light.target.position).normalize();
|
||||
expect(toLight.x).toBeCloseTo(0, 5);
|
||||
expect(toLight.y).toBeCloseTo(0.8, 5);
|
||||
expect(toLight.z).toBeCloseTo(0.6, 5);
|
||||
}
|
||||
});
|
||||
|
||||
// Bruch: der Regler "Sonne" träfe nur ein Stufenlicht — dann wechselte die Helligkeit mit der
|
||||
// Entfernung sprunghaft an den Stufengrenzen.
|
||||
it("gibt allen Stufenlichtern Farbe und Stärke der Sonne, auch nach dem Umstellen", () => {
|
||||
const { shadows } = setup();
|
||||
|
||||
for (const light of shadows.csm.lights) {
|
||||
expect(light.color.getHex()).toBe(new Color(1, 0.9, 0.8).getHex());
|
||||
expect(light.intensity).toBe(2.5);
|
||||
}
|
||||
shadows.intensity = 4;
|
||||
expect(shadows.csm.lights.map((light) => light.intensity)).toEqual([4, 4, 4, 4]);
|
||||
expect(shadows.intensity).toBe(4);
|
||||
});
|
||||
|
||||
// Bruch: eingebaute Aufteilung statt eigener Grenzen — die erste Stufe reichte bis ~127 m und
|
||||
// wäre ~270 m breit, die Figur bekäme nur 13 cm pro Texel.
|
||||
it("macht die erste Stufe so schmal, dass ein Texel etwa 2,6 cm misst", () => {
|
||||
// Kamera-far-Ecke auf maxFar = 1000 m: x = 1000 * tan 27,5° * 16/9 = 925,46, y = 520,57;
|
||||
// near-Ecke (0,5 m): x = 0,4627, y = 0,2603. Anteil 0,025 dazwischen: x = 23,588, y = 13,268.
|
||||
// Diagonale der Stufenfernebene: 2 * sqrt(23,588² + 13,268²) = 54,13 m -> / 2048 = 2,64 cm.
|
||||
const { shadows } = setup();
|
||||
const cam = shadows.csm.lights[0].shadow.camera;
|
||||
|
||||
expect(cam.right - cam.left).toBeCloseTo(54.13, 1);
|
||||
});
|
||||
|
||||
// Bruch: Schutznetz nur einmal beim Aufbau — nachgeladene Chunks, Autos und Figuren wären
|
||||
// viermal zu hell.
|
||||
it("richtet in update() auch nachträglich hinzugefügte Materialien ein", () => {
|
||||
const { scene, shadows } = setup();
|
||||
const material = new MeshStandardMaterial();
|
||||
scene.add(new Mesh(new BoxGeometry(), material));
|
||||
|
||||
shadows.update();
|
||||
|
||||
expect(material.defines.USE_CSM).toBe(1);
|
||||
});
|
||||
|
||||
// Befund (Nutzer-Screenshot): feine Streifen auf Figur und Auto. Doppelseitige Materialien
|
||||
// schreiben ihre Vorderseiten in die Schattenkarte und verschatten sich ohne Versatz selbst.
|
||||
// Bruch: Normal-Bias 0 (Streifen) oder fest statt je Stufe (ferne Stufen mit 1-m-Texeln
|
||||
// behielten ihre Akne) oder nach einer Projektionsänderung nicht nachgeführt.
|
||||
it("versetzt jede Stufe um 1,5 Texel ihrer Schattenbox entlang der Normalen, auch nach Nachführung", () => {
|
||||
// Stufe 1 bei 16:9: 54,13 m / 2048 * 1,5 = 0,03965 m; bei Seitenverhältnis 2: 59,34 m / 2048 * 1,5 = 0,04346 m.
|
||||
const { camera, shadows } = setup();
|
||||
expect(shadows.csm.lights[0].shadow.normalBias).toBeCloseTo(0.03965, 4);
|
||||
|
||||
camera.aspect = 2;
|
||||
camera.updateProjectionMatrix();
|
||||
shadows.update();
|
||||
|
||||
expect(shadows.csm.lights[0].shadow.normalBias).toBeCloseTo(0.04346, 4);
|
||||
for (const light of shadows.csm.lights) {
|
||||
const texel = (light.shadow.camera.right - light.shadow.camera.left) / light.shadow.mapSize.x;
|
||||
expect(light.shadow.normalBias).toBeCloseTo(1.5 * texel, 6);
|
||||
}
|
||||
});
|
||||
|
||||
// Bruch: Stufen bleiben nach Fenstergrößenänderung auf dem alten Seitenverhältnis — ein Teil
|
||||
// des Bildes läge dann außerhalb jeder Schattenbox.
|
||||
it("führt die Stufen nach, wenn sich das Seitenverhältnis ändert", () => {
|
||||
// Seitenverhältnis 2: x-far = 1000 * 0,52057 * 2 = 1041,14, x-near = 0,5206; Anteil 0,025
|
||||
// -> x = 26,536, y = 13,268 -> Diagonale 2 * sqrt(26,536² + 13,268²) = 59,34 m.
|
||||
const { camera, shadows } = setup();
|
||||
camera.aspect = 2;
|
||||
camera.updateProjectionMatrix();
|
||||
|
||||
shadows.update();
|
||||
|
||||
const cam = shadows.csm.lights[0].shadow.camera;
|
||||
expect(cam.right - cam.left).toBeCloseTo(59.34, 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DataUtils } from "three";
|
||||
import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
|
||||
import { extractSun, horizonBand, rowColor, sunLight, toneMapToDisplay } from "../src/world/sky.js";
|
||||
|
||||
// Winziges Radiance-Panorama, flach ohne Lauflängenkodierung geschrieben und durch den echten
|
||||
// HDRLoader gelesen — genau der Weg, auf dem sky.js das HDRI bekommt (Half-Float, Datenzeile 0
|
||||
// = oberste Dateizeile). Wert je Kanal laut HDRLoader: byte * 2^(e-128) / 255.
|
||||
function hdrFixture(width, height, pixel) {
|
||||
const header = `#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n-Y ${height} +X ${width}\n`;
|
||||
const bytes = new Uint8Array(header.length + width * height * 4);
|
||||
for (let i = 0; i < header.length; i++) bytes[i] = header.charCodeAt(i);
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
bytes.set(pixel(row, col), header.length + (row * width + col) * 4);
|
||||
}
|
||||
}
|
||||
return new HDRLoader().parse(bytes.buffer);
|
||||
}
|
||||
|
||||
const SKY = [26, 26, 26, 128]; // 26/255 = 0,102 je Kanal
|
||||
const SUN = [255, 255, 255, 136]; // 255 * 256/255 = 256 je Kanal
|
||||
|
||||
describe("extractSun", () => {
|
||||
// Bruch, den der Test fängt: Zeilen von unten gelesen (wie bei EXRLoader) oder Azimut
|
||||
// gespiegelt — das Sonnenlicht käme dann von woanders als die sichtbare Sonne.
|
||||
it("liest die Richtung wie equirectUv in three.js: erste Dateizeile oben, Spalte 4 von 8 nach +z", () => {
|
||||
// 8x4: Zeile 1 liegt bei 90° - 1,5/4 * 180° = 22,5° Höhe, Spalte 4 bei (4,5/8 - 0,5) * 360°
|
||||
// = 22,5° Azimut. Richtung = (cos²22,5°, sin 22,5°, cos 22,5° * sin 22,5°).
|
||||
const { data, width, height } = hdrFixture(8, 4, (row, col) => (row === 1 && col === 4 ? SUN : SKY));
|
||||
|
||||
const sun = extractSun(data, width, height, 50);
|
||||
|
||||
expect(sun.direction.x).toBeCloseTo(0.85355, 4);
|
||||
expect(sun.direction.y).toBeCloseTo(0.38268, 4);
|
||||
expect(sun.direction.z).toBeCloseTo(0.35355, 4);
|
||||
});
|
||||
|
||||
// Sonnenpixel (Zeile 1, Spalte 4), e=134 -> Faktor 64/255: R 64, G 57,7, B 25,1 — B liegt unter
|
||||
// der Schwelle 50. Kleiner Exponent mit Absicht: toHalfFloat schneidet die Mantisse ab, zwischen
|
||||
// 32 und 64 in Stufen von 1/32 statt 1/8 wie zwischen 128 und 256.
|
||||
const WARM_SUN = [255, 230, 100, 134];
|
||||
const sunAt = (row, col) => (row === 1 && col === 4 ? WARM_SUN : SKY);
|
||||
const SUN_INDEX = (1 * 8 + 4) * 4;
|
||||
|
||||
// Bruch: Sonne bleibt im Bild (sie hellte dann über das Umgebungslicht auch jeden Schatten auf)
|
||||
// oder ein Kanal unter der Schwelle wird mit auf die Schwelle gehoben.
|
||||
it("kappt nur die Kanäle über der Schwelle, und zwar im Bild selbst", () => {
|
||||
const { data, width, height } = hdrFixture(8, 4, sunAt);
|
||||
|
||||
extractSun(data, width, height, 50);
|
||||
|
||||
expect(DataUtils.fromHalfFloat(data[SUN_INDEX])).toBe(50);
|
||||
expect(DataUtils.fromHalfFloat(data[SUN_INDEX + 1])).toBe(50);
|
||||
expect(DataUtils.fromHalfFloat(data[SUN_INDEX + 2])).toBeCloseTo(25.1, 1);
|
||||
expect(DataUtils.fromHalfFloat(data[0])).toBeCloseTo(0.102, 3);
|
||||
});
|
||||
|
||||
// Bruch: Energie ohne Raumwinkel summiert, Schwelle nicht abgezogen, oder ein Kanal unter der
|
||||
// Schwelle zieht negativ ab — das Sonnenlicht hätte dann die falsche Stärke oder Farbe.
|
||||
it("gibt die abgezogene Energie als Einstrahlung zurück: Überschuss mal Raumwinkel des Pixels", () => {
|
||||
// Raumwinkel eines Pixels in Zeile 1 von 4 bei 8 Spalten: cos 22,5° * (π/4) * (2π/8) = 0,56989 sr.
|
||||
// Überschuss über 50: R 14, G 7,73, B 0 -> 7,98 / 4,40 / 0.
|
||||
const { data, width, height } = hdrFixture(8, 4, sunAt);
|
||||
|
||||
const sun = extractSun(data, width, height, 50);
|
||||
|
||||
expect(sun.irradiance[0]).toBeCloseTo(7.98, 1);
|
||||
expect(sun.irradiance[1]).toBeCloseTo(4.40, 1);
|
||||
expect(sun.irradiance[2]).toBe(0);
|
||||
});
|
||||
|
||||
// Bruch: ungewichteter Schwerpunkt — die schwachen Pixel am Rand (Linsenstrahlen um die echte
|
||||
// Sonne) zögen die Lichtrichtung dann genauso stark zu sich wie der helle Kern.
|
||||
it("gewichtet die Richtung mit der abgezogenen Energie", () => {
|
||||
// Zeile 1: Spalte 3 (Azimut -22,5°) mit 64 -> Überschuss 14, Spalte 4 (+22,5°) mit 256 -> 206.
|
||||
// Summe: x = cos²22,5° * 220 = 187,78, y = sin 22,5° * 220 = 84,19,
|
||||
// z = cos 22,5° * sin 22,5° * (206 - 14) = 67,88; Länge 216,70 -> (0,86656, 0,38851, 0,31326).
|
||||
const { data, width, height } = hdrFixture(8, 4, (row, col) => {
|
||||
if (row === 1 && col === 3) return [255, 255, 255, 134];
|
||||
if (row === 1 && col === 4) return SUN;
|
||||
return SKY;
|
||||
});
|
||||
|
||||
const sun = extractSun(data, width, height, 50);
|
||||
|
||||
expect(sun.direction.x).toBeCloseTo(0.86656, 4);
|
||||
expect(sun.direction.y).toBeCloseTo(0.38851, 4);
|
||||
expect(sun.direction.z).toBeCloseTo(0.31326, 4);
|
||||
});
|
||||
|
||||
// Bruch: Ein Panorama ohne Sonne liefert still eine Nullrichtung — updateShadow() setzte das
|
||||
// Licht dann genau auf den Spieler, und die Schattenkamera blickte in keine Richtung.
|
||||
it("wirft, wenn kein Pixel über der Schwelle liegt", () => {
|
||||
const { data, width, height } = hdrFixture(8, 4, () => SKY);
|
||||
|
||||
expect(() => extractSun(data, width, height, 50)).toThrow(/Sonne/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rowColor", () => {
|
||||
// Bruch: Zeilen von unten gezählt oder die falsche Zeile gewählt — die Nebelfarbe käme dann aus
|
||||
// dem Boden des Fotos oder aus dem Zenit statt aus dem Himmel knapp über dem Horizont.
|
||||
it("mittelt die Zeile, deren Mitte der gewünschten Höhe am nächsten liegt", () => {
|
||||
// 8x4, Zeilenmitten bei 67,5° / 22,5° / -22,5° / -67,5°; 20° -> Zeile 1. Bei e=128 gilt byte/255:
|
||||
// Zeile 1 halb (1; 0,2; 0), halb (0,2; 1; 0,4) -> Mittel (0,6; 0,6; 0,2).
|
||||
const { data, width, height } = hdrFixture(8, 4, (row, col) => {
|
||||
if (row === 0) return [0, 0, 255, 128];
|
||||
if (row === 1) return col < 4 ? [255, 51, 0, 128] : [51, 255, 102, 128];
|
||||
return [0, 255, 0, 128];
|
||||
});
|
||||
|
||||
const color = rowColor(data, width, height, 20);
|
||||
|
||||
expect(color[0]).toBeCloseTo(0.6, 3);
|
||||
expect(color[1]).toBeCloseTo(0.6, 3);
|
||||
expect(color[2]).toBeCloseTo(0.2, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("horizonBand", () => {
|
||||
// Bruch: Fenster wickelt nicht über Azimut ±180° (an der Bildnaht der Kuppel entstünde ein
|
||||
// Helligkeitssprung) oder mittelt über die falsche Zeile / falsche Breite — Wolken zögen dann
|
||||
// wieder als harte Streifen zum Horizont.
|
||||
it("mittelt die Zeile je Azimut über ±radiusDeg, über die Bildnaht hinweg", () => {
|
||||
// 8x4, Zeile 1 (22,5°): nur Spalte 0 hell (1,0). Pixel liegen 45° auseinander, ±50° erfasst
|
||||
// also Nachbar links, sich selbst, Nachbar rechts. Texel 0 und 1 sehen Spalte 0, Texel 7
|
||||
// ebenfalls — über die Naht (Spalten 6, 7, 0). Wert je Treffer 1/3, sonst 0; Alpha 1.
|
||||
const { data, width, height } = hdrFixture(8, 4, (row, col) =>
|
||||
(row === 1 && col === 0 ? [255, 255, 255, 128] : [0, 0, 0, 0]));
|
||||
|
||||
const band = horizonBand(data, width, height, 22.5, 50, 8);
|
||||
|
||||
const red = Array.from({ length: 8 }, (_, texel) => band[texel * 4]);
|
||||
expect(red.map((v) => +v.toFixed(4))).toEqual([0.3333, 0.3333, 0, 0, 0, 0, 0, 0.3333]);
|
||||
expect(band[3]).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sunLight", () => {
|
||||
// Bruch: Farbe nicht normiert (Licht doppelt so stark) oder auf den hellsten Kanal statt auf die
|
||||
// Helligkeit normiert — dann hieße "Sonne 1" im Panel je nach Farbton etwas anderes.
|
||||
it("teilt die Einstrahlung in eine Farbe mit Helligkeit 1 und die Helligkeit als Stärke", () => {
|
||||
// (2, 1, 1): Helligkeit 0,2126 * 2 + 0,7152 + 0,0722 = 1,2126 -> Farbe (1,64935; 0,82467; 0,82467).
|
||||
const light = sunLight([2, 1, 1]);
|
||||
|
||||
expect(light.intensity).toBeCloseTo(1.2126, 4);
|
||||
expect(light.color[0]).toBeCloseTo(1.64935, 4);
|
||||
expect(light.color[1]).toBeCloseTo(0.82467, 4);
|
||||
expect(light.color[2]).toBeCloseTo(0.82467, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toneMapToDisplay", () => {
|
||||
// Die Nebelfarbe wirkt in three.js erst NACH Tone Mapping und sRGB-Umrechnung (fog_fragment
|
||||
// kommt nach colorspace_fragment). Soll der Nebel am Horizont genau wie der Himmel aussehen, muss
|
||||
// die JS-Umrechnung dem Shader aufs Byte folgen. Erwartete Bytes: echter WebGLRenderer r184,
|
||||
// ACESFilmicToneMapping, eine MeshBasicMaterial-Fläche in dieser Farbe, per readPixels gelesen.
|
||||
// Bruch: 1/0,6-Faktor vergessen, Matrizen nicht transponiert, Belichtung ignoriert, falsche sRGB-Kurve.
|
||||
const MEASURED = [
|
||||
{ linear: [0.18, 0.18, 0.18], exposure: 1, bytes: [127, 127, 127] },
|
||||
{ linear: [0.3732, 0.498, 0.7192], exposure: 1, bytes: [183, 197, 213] },
|
||||
{ linear: [2.0, 1.0, 0.5], exposure: 1, bytes: [245, 227, 205] },
|
||||
{ linear: [0.05, 0.02, 0.01], exposure: 1, bytes: [46, 21, 10] },
|
||||
{ linear: [20, 20, 20], exposure: 1, bytes: [255, 255, 255] },
|
||||
{ linear: [0.18, 0.18, 0.18], exposure: 2, bytes: [177, 177, 177] },
|
||||
{ linear: [0, 0, 0], exposure: 1, bytes: [0, 0, 0] },
|
||||
];
|
||||
|
||||
for (const { linear, exposure, bytes } of MEASURED) {
|
||||
it(`bildet (${linear.join(", ")}) bei Belichtung ${exposure} auf die Renderer-Bytes (${bytes.join(", ")}) ab`, () => {
|
||||
const display = toneMapToDisplay(linear, exposure);
|
||||
|
||||
// Auf 2 Stellen = ±1,3 Byte: Readback rundet auf 8 Bit.
|
||||
for (let channel = 0; channel < 3; channel++) expect(display[channel]).toBeCloseTo(bytes[channel] / 255, 2);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user