Newer
Older
rpg / v2 / packages / engine / src / render / __tests__ / fog.test.ts
/**
 * Тесты тумана: чистые функции цвета (fogColor из env-света) и глубин слоёв
 * (fogBandStart — от камеры вдоль взгляда), кламп fog в WeatherSpec. Композит
 * и шейдер тестируются пробами с WebGL — здесь только математика.
 */
import { describe, expect, it } from 'vitest';
import { fogColor, fogBandStart, fogBandLayout, FOG_ENV_SUN, FOG_BRIGHT, FOG_BAND_START, FOG_BAND_STEP, PERSP_FOG_FAR_K, FOG_BANDS } from '../fog';
import { PERSP_FOV } from '../cameraMath';
import { clampWeather } from '../weather';

describe('fogColor (цвет тумана из env-света)', () => {
    it('ambient целиком + доля солнца, умноженные на FOG_BRIGHT', () => {
        const [r, g, b] = fogColor(
            { r: 0.5, g: 0.6, b: 0.7, intensity: 0.5 },
            { r: 0, g: 0, b: 0, intensity: 0 },
        );
        expect(r).toBeCloseTo(0.25 * FOG_BRIGHT);
        expect(g).toBeCloseTo(0.3 * FOG_BRIGHT);
        expect(b).toBeCloseTo(0.35 * FOG_BRIGHT);
    });

    it('солнце входит с долей FOG_ENV_SUN (больше, чем у частиц)', () => {
        const [r] = fogColor(
            { r: 0, g: 0, b: 0, intensity: 0 },
            { r: 1, g: 1, b: 1, intensity: 2 },
        );
        expect(r).toBeCloseTo(Math.min(1, 2 * FOG_ENV_SUN * FOG_BRIGHT));
        expect(FOG_ENV_SUN).toBeGreaterThan(0.3);
    });

    it('полдень не выбеливает: кламп 0..1, ночь — тёмный туман', () => {
        const day = fogColor(
            { r: 0.58, g: 0.58, b: 0.62, intensity: 0.55 },
            { r: 1, g: 0.9, b: 0.8, intensity: 2 },
        );
        for (const c of day) expect(c).toBeLessThanOrEqual(1);
        const night = fogColor(
            { r: 0.04, g: 0.04, b: 0.07, intensity: 0.55 },
            { r: 1, g: 1, b: 1, intensity: 0.08 },
        );
        const luma = night[0]! * 0.3 + night[1]! * 0.6 + night[2]! * 0.1;
        expect(luma).toBeLessThan(0.1);
    });
});

describe('fogBandStart (глубины слоёв по дальности)', () => {
    it('старт и шаг в долях viewSize, строго по возрастанию', () => {
        expect(fogBandStart(0, 16)).toBeCloseTo(16 * FOG_BAND_START);
        expect(fogBandStart(1, 16) - fogBandStart(0, 16)).toBeCloseTo(16 * FOG_BAND_STEP);
        for (let k = 1; k < 6; k++) expect(fogBandStart(k, 16)).toBeGreaterThan(fogBandStart(k - 1, 16));
    });

    it('полоса слоёв накрывает кадр: камера на 4·viewSize от цели, полукадр ~1.12', () => {
        // ближний край кадра ~2.9·viewSize, дальний ~5.1 — первый слой до него,
        // последний внутри дальнего края
        expect(fogBandStart(0, 10)).toBeLessThan(29);
        expect(fogBandStart(5, 10)).toBeLessThan(51);
    });
});

describe('fogBandLayout (разметка слоёв под камеру)', () => {
    it('изо — легаси-масштаб viewSize·констант ТОЧНО (регрессионный замок)', () => {
        const l = fogBandLayout({ mode: 'iso', viewSize: 80, pitch: 0.5, eyeH: 30, fov: PERSP_FOV });
        expect(l.start).toBe(80 * FOG_BAND_START);
        expect(l.step).toBe(80 * FOG_BAND_STEP);
    });

    it('перспектива: стопка на следе земли фрустума [nearG, farG]', () => {
        const l = fogBandLayout({ mode: 'third', viewSize: 80, pitch: 0.5, eyeH: 30, fov: PERSP_FOV });
        expect(l.nearG).toBeCloseTo(30 / Math.tan(0.5 + PERSP_FOV / 2));
        const farUncapped = 30 / Math.tan(Math.max(0.5 - PERSP_FOV / 2, 0.05));
        expect(l.farG).toBeCloseTo(Math.min(farUncapped, 80 * PERSP_FOG_FAR_K));
        expect(l.start).toBeCloseTo(l.nearG * 0.9);
        expect(l.start + l.step * (FOG_BANDS - 1)).toBeCloseTo(l.farG);
        expect(l.step).toBeGreaterThan(0);
    });

    it('взгляд в небо (1st person): кап farG работает, разметка конечна', () => {
        const l = fogBandLayout({ mode: 'first', viewSize: 80, pitch: -1.2, eyeH: 14, fov: PERSP_FOV });
        expect(Number.isFinite(l.start + l.step)).toBe(true);
        expect(l.farG).toBeLessThanOrEqual(80 * PERSP_FOG_FAR_K);
        expect(l.step).toBeGreaterThan(0);
    });
});

describe('clampWeather: fog клампится как остальные поля', () => {
    it('недостающее поле — дефолт 0, лишние значения — кламп', () => {
        expect(clampWeather({}).fog).toBe(0);
        expect(clampWeather({ fog: 0.42 }).fog).toBe(0.42);
        expect(clampWeather({ fog: 5 }).fog).toBe(1);
        expect(clampWeather({ fog: -1 }).fog).toBe(0);
    });
});