diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md index 30e9832..0b3a145 100644 --- a/v2/CLAUDE.md +++ b/v2/CLAUDE.md @@ -1500,15 +1500,17 @@ юнита): фиксированный список капель размазывался по нему в субпиксельную пыль (капля 0.5 юнита при 3.3 px/юнит — 1.7 px). Wrap и edgeAlpha гасят частицы у края (как у пепла: без «появления из воздуха»). - **Многослойность** (2026-09-12, фидбек «сильно гуще, как туман»): - RAIN_LAYERS/SNOW_LAYERS = 3 слоя частиц, каждый в своей полосе глубины - (ближний 0.5..1.0 редкий и крупный, дальний 0..0.34 частый и мелкий — - стена дождя в глубине; полосы перекрываются — слоистость не видна - линиями). Суммарная плотность ~3× одиночного слоя: дождь 0.044 - (ливень ~6100 капель у игры), снег 0.022 (~3100 хлопьев). Глобальный + **Многослойность** (2026-09-12, фидбеки «сильно гуще, как туман» и + «количество слоёв удвоим»): RAIN_LAYERS/SNOW_LAYERS = 6 слоёв частиц, + каждый в своей полосе глубины (дальний 0..0.3 частый и мелкий, + ближний 0.75..1.0 редкий и крупный — стена дождя в глубине; полосы + перекрываются — слоистость не видна линиями). Суммарная плотность ~5× + одиночного слоя: дождь 0.077 (ливень ~10 700 капель у игры), снег + 0.0405 (~5 800 хлопьев). Глобальный номер gi = i + k·7919/6271 разводит колонки слоёв — без него слои легли бы в одни x. `rainPoints`/`snowPoints(spec, t, win, layer?)` — - без layer все слои разом (тесты/счётчики). + без layer все слои разом (тесты/счётчики). Потолки RAIN_MAX/SNOW_MAX — + по-СЛОЙНЫЕ (макс. плотность отдельного слоя не менялась при удвоении). `WeatherSystem(renderer)` — RAIN_LAYERS/SNOW_LAYERS слоёв через `renderer.addParticles()` (спрайты-билборды, lit: ambient-тинт и свет источников применяются слоями сами); `set(spec)` клампит и сразу гонит diff --git a/v2/packages/engine/src/render/__tests__/weather.test.ts b/v2/packages/engine/src/render/__tests__/weather.test.ts index 8b80cf4..6201f86 100644 --- a/v2/packages/engine/src/render/__tests__/weather.test.ts +++ b/v2/packages/engine/src/render/__tests__/weather.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { cloudDim } from '../daynight'; import { - DEFAULT_WEATHER, WeatherSystem, clampWeather, rainPoints, snowPoints, windVector, + DEFAULT_WEATHER, RAIN_LAYERS, WeatherSystem, clampWeather, rainPoints, snowPoints, windVector, } from '../weather'; const WIN = { minX: 0, maxX: 48, minZ: 0, maxZ: 32 }; @@ -51,20 +51,21 @@ }); describe('rain/snow points', () => { - // окно 48×32 = 1536 кв. юнитов; дождь многослойный (3 слоя, плотности - // 0.020/0.014/0.010 → 31+22+15 = 68 капель × 3 точки штриха = 204); - // снег (0.010/0.007/0.005 → 15+11+8 = 34 хлопьев) + // окно 48×32 = 1536 кв. юнитов; дождь многослойный (6 слоёв, плотности + // 0.020/0.016/0.013/0.011/0.009/0.008 → 31+25+20+17+14+12 = 119 капель + // × 3 точки штриха = 357); снег (0.010/0.008/0.007/0.006/0.006/0.0045 + // → 15+12+11+9+9+7 = 63 хлопьев) it('число точек = сумма слоёв (плотность × площадь × интенсивность); ноль — пусто', () => { const spec = { ...DEFAULT_WEATHER, rain: 1, snow: 0 }; - expect(rainPoints(spec, 0, WIN).length).toBe(204); - expect(snowPoints({ ...spec, snow: 1 }, 0, WIN).length).toBe(34); + expect(rainPoints(spec, 0, WIN).length).toBe(357); + expect(snowPoints({ ...spec, snow: 1 }, 0, WIN).length).toBe(63); expect(rainPoints({ ...spec, rain: 0 }, 0, WIN).length).toBe(0); expect(snowPoints({ ...spec, snow: 0 }, 0, WIN).length).toBe(0); }); it('слои живут в своих полосах глубины и не совпадают колонками', () => { const spec = { ...DEFAULT_WEATHER, rain: 1 }; const a = rainPoints(spec, 0, WIN, 0); // дальний слой - const b = rainPoints(spec, 0, WIN, 2); // ближний + const b = rainPoints(spec, 0, WIN, RAIN_LAYERS - 1); // ближний expect(a.length).toBeGreaterThan(0); expect(b.length).toBeGreaterThan(0); const sizes = (pts: ReturnType) => pts.filter((p) => p.size !== undefined).map((p) => p.size!); @@ -76,7 +77,9 @@ expect(a.every((p) => !keysB.has(key(p)))).toBe(true); // сумма слоёв = вызов без слоя const all = rainPoints(spec, 0, WIN); - expect(all.length).toBe(a.length + b.length + rainPoints(spec, 0, WIN, 1).length); + let sum = 0; + for (let k = 0; k < RAIN_LAYERS; k++) sum += rainPoints(spec, 0, WIN, k).length; + expect(all.length).toBe(sum); }); it('детерминизм: тот же t — те же позы', () => { const spec = { ...DEFAULT_WEATHER, rain: 0.4, snow: 0.6, windStrength: 0.3 }; @@ -195,8 +198,9 @@ sys.set({ rain: 1 }); sys.update(1); // окно погоды — след кадра + маржа 16: (48+32)×(32+32) = 5120 кв. юнитов; - // 3 слоя дождя: 0.020/0.014/0.010 × 5120 → 102+72+51 = 225 капель - expect(sys.info()).toEqual({ rainDrops: 225, snowFlakes: 0 }); + // 6 слоёв дождя: 0.020/0.016/0.013/0.011/0.009/0.008 × 5120 + // → 102+82+67+56+46+41 = 394 капли + expect(sys.info()).toEqual({ rainDrops: 394, snowFlakes: 0 }); sys.set({ rain: 0, snow: 0 }); // пустая погода прячет слои и счётчики expect(sys.info()).toEqual({ rainDrops: 0, snowFlakes: 0 }); }); diff --git a/v2/packages/engine/src/render/weather.ts b/v2/packages/engine/src/render/weather.ts index 7a84f71..7cf08ce 100644 --- a/v2/packages/engine/src/render/weather.ts +++ b/v2/packages/engine/src/render/weather.ts @@ -16,7 +16,7 @@ * (фидбек «сильно гуще, тоже многослойным, как туман»): RAIN_LAYERS/SNOW_LAYERS * слоёв частиц, каждый живёт в своей узкой полосе глубины — ближний слой * редкий и крупный, дальний частый и мелкий (стена дождя в глубине); - * суммарная плотность в ~3× выше одиночного слоя. Респавн + * суммарная плотность в ~5× выше одиночного слоя (6 полос). Респавн * наверху — ПОЛНЫЙ реролл колонки (sin-хэш от номера цикла падения * умножается на большой период): сдвиг в окрестности якоря оставляет каплю * падать в той же полосе — «трек остаётся на экране», а поле колонн @@ -95,31 +95,38 @@ const SNOW_COLOR = '#eef3f6'; const SNOW_ALPHA = 0.85; -// ---- Многослойность осадков (фидбек «сильно гуще, как туман») ---- +// ---- Многослойность осадков (фидбек «сильно гуще, как туман»; 2026-09-12: +// «количество слоёв удвоим» — 3 → 6) ---- // Каждый слой живёт в своей полосе глубины [from..to] (0 — дальний план, -// 1 — ближний) со своей плотностью: ближний слой редкий и крупный, -// дальний частый и мелкий — стена дождя читается в глубине, отдельные -// капли вблизи. Полосы перекрываются — слоистость не видна как линии. +// 1 — ближний) со своей плотностью: ближние слои реже и крупнее, дальние +// чаще и мельче — стена дождя читается в глубине, отдельные капли вблизи. +// Полосы перекрываются — слоистость не видна как линии. /** Число слоёв дождя. */ -export const RAIN_LAYERS = 3; +export const RAIN_LAYERS = 6; /** Число слоёв снега. */ -export const SNOW_LAYERS = 3; +export const SNOW_LAYERS = 6; /** Полосы глубины слоёв (от дальнего к ближнему) и их плотность - * (капель на кв. юнит окна при rain = 1). Сумма 0.044 против 0.016 + * (капель на кв. юнит окна при rain = 1). Сумма 0.077 против 0.016 * одиночного слоя — «сильно гуще». */ const RAIN_BANDS: readonly { from: number; to: number; density: number }[] = [ - { from: 0.0, to: 0.34, density: 0.020 }, // дальний: стена мелкой мороси - { from: 0.24, to: 0.62, density: 0.014 }, // средний - { from: 0.5, to: 1.0, density: 0.010 }, // ближний: редкие крупные капли + { from: 0.0, to: 0.3, density: 0.020 }, // дальний: стена мелкой мороси + { from: 0.15, to: 0.45, density: 0.016 }, + { from: 0.3, to: 0.58, density: 0.013 }, + { from: 0.45, to: 0.72, density: 0.011 }, + { from: 0.6, to: 0.86, density: 0.009 }, + { from: 0.75, to: 1.0, density: 0.008 }, // ближний: редкие крупные капли ]; -/** Полосы глубины слоёв снега и плотности (сумма 0.022 против 0.007). */ +/** Полосы глубины слоёв снега и плотности (сумма 0.0405 против 0.007). */ const SNOW_BANDS: readonly { from: number; to: number; density: number }[] = [ - { from: 0.0, to: 0.34, density: 0.010 }, - { from: 0.24, to: 0.62, density: 0.007 }, - { from: 0.5, to: 1.0, density: 0.005 }, + { from: 0.0, to: 0.3, density: 0.010 }, + { from: 0.15, to: 0.45, density: 0.008 }, + { from: 0.3, to: 0.58, density: 0.007 }, + { from: 0.45, to: 0.72, density: 0.006 }, + { from: 0.6, to: 0.86, density: 0.006 }, + { from: 0.75, to: 1.0, density: 0.0045 }, ]; const MARGIN = 16; // перекрытие за краем кадра (как у пепла)