diff --git a/v2/CLAUDE.md b/v2/CLAUDE.md index 30d1fdb..6000b26 100644 --- a/v2/CLAUDE.md +++ b/v2/CLAUDE.md @@ -1447,18 +1447,27 @@ `setSun` (туда сходятся все изменения света) чистой функцией `particleEnv(amb, sun)` = ambient·intensity + солнце·`PARTICLE_ENV_SUN` (0.3 — у билборда нет нормали). Шейдер один, ветка `#if LIT`: - полученный свет = env + точечные источники; видимость (альфа) = - clamp(max-канал(received)·`PARTICLE_LIT_VIS` (1.6), 0, 1) — ночью в - темноте частица исчезает, у лампы проявляется; днём vis насыщается в 1 — - вид как раньше. Излучающий режим — прежняя формула (tint + источники). + полученный свет = env + точечные источники; видимость + vis = clamp((max-канал(received)·`PARTICLE_LIT_VIS` (1.6))^`PARTICLE_LIT_GAMMA` + (2.0), 0, 1) (зеркало для тестов — `litVisibility(maxChannel)`), и vis гасит + И цвет, и альфу: `vColor·(uTint·vis + received)` — фидбек «в темноте ещё + темнее»: линейная кривая оставляла vis ~0.1 и полупрозрачные СЕРЫЕ точки + (цвет оставался полным — гасилась только альфа), квадрат уводит слабый + свет в невидимость, а день/лампа по-прежнему насыщают vis в 1. Замер + (пара кадров ночь «дождь вкл/выкл», px ярче фона): 815 → 10. Излучающий + режим — прежняя формула (tint + источники). - **Игра**: lit-слои — дождь и снег (`weather.ts`, метод setTint удалён — слои сами читают день/ночь из env-света), пепел (`ash.ts`), листья/пыль (`airFx.ts`); tint остался самосветящимся: мотыльки, дымка прудов, блики воды (main.ts `particleTint` теперь красит только их). - Грабли замера: тёмные грани самого кастера загрязняют маску «тени» по яркости кадра — свет/тень делить по паре кадров (с кастером и без); - `setClock` моста принимает МИНУТЫ (720 = полдень, 1200 = 20:00 — ночь). - Замер пробы: дождь день 101 / ночь 14; мотыльки ночью светятся. + `setClock` моста принимает МИНУТЫ (720 = полдень, 1200 = 20:00 — ночь); + средняя яркость большой зоны НЕ видит одиночные капли — мерить пару + кадров «дождь вкл/выкл» и считать пиксели ярче фона. Гейт weather + «облачность темнит кадр» ослаблен до ×0.93: зенитный свет пасмурного + дня (OVERCAST_TOP_LIGHT) намеренно добавляет освещённости. + Замер пробы: дождь день 100 / ночь 14; мотыльки ночью светятся. ## Погода (2026-09-12): дождь, снег, ветер, облачность — часть движка diff --git a/v2/apps/game/tools/checks/weather.mjs b/v2/apps/game/tools/checks/weather.mjs index 768c886..1ddc250 100644 --- a/v2/apps/game/tools/checks/weather.mjs +++ b/v2/apps/game/tools/checks/weather.mjs @@ -102,8 +102,11 @@ await client.call('setWeather', { cloud: 1 }); await client.call('stepTicks', 3); const cloudBright = brightness(await shot(client, '/tmp/v2_weather_cloud.png')); + // порог 0.93: зенитный свет пасмурного дня (OVERCAST_TOP_LIGHT — + // «свет переотражается, словно есть доп. свет прямо сверху») добавляет + // освещённости, и кадр темнит слабее, чем до зенита (~0.89 против ~0.75) check('облачность темнит кадр (полная — заметно темнее)', - cloudBright < clearBright * 0.8, { clearBright, cloudBright }); + cloudBright < clearBright * 0.93, { clearBright, cloudBright }); // ---- setAsh(false): гейт-режим глушит и осадки с облачностью ---- await client.call('setWeather', null); // ручная правка переживает смену области — снять diff --git a/v2/packages/engine/src/index.ts b/v2/packages/engine/src/index.ts index ae8226d..6109a1f 100644 --- a/v2/packages/engine/src/index.ts +++ b/v2/packages/engine/src/index.ts @@ -23,7 +23,7 @@ export { meshChunk, chunkBounds } from './world/worldMesher'; export { Chunk, CHUNK_SX, CHUNK_SZ } from './world/chunk'; export { VoxelRenderer, worldTileAtlas } from './render/voxelRenderer'; -export { createSpriteLayer, createQuadLayer, makeParticleLightUniforms, packParticleLights, particleEnv, PARTICLE_LIGHT_MAX, PARTICLE_ENV_SUN, PARTICLE_LIT_VIS } from './render/particles'; +export { createSpriteLayer, createQuadLayer, makeParticleLightUniforms, packParticleLights, particleEnv, litVisibility, PARTICLE_LIGHT_MAX, PARTICLE_ENV_SUN, PARTICLE_LIT_VIS, PARTICLE_LIT_GAMMA } from './render/particles'; export { postPass } from './render/postShader'; export type { PostOptions, PostPassSpec } from './render/postShader'; export type { PostPassHandle } from './render/voxelRenderer'; diff --git a/v2/packages/engine/src/render/__tests__/particleEnv.test.ts b/v2/packages/engine/src/render/__tests__/particleEnv.test.ts index 5ba84c9..add7ce7 100644 --- a/v2/packages/engine/src/render/__tests__/particleEnv.test.ts +++ b/v2/packages/engine/src/render/__tests__/particleEnv.test.ts @@ -4,7 +4,7 @@ * столько света, сколько даёт сцена, — рендер пересчитывает при тиках. */ import { describe, expect, it } from 'vitest'; -import { particleEnv, PARTICLE_ENV_SUN, PARTICLE_LIT_VIS } from '../particles'; +import { particleEnv, PARTICLE_ENV_SUN, litVisibility } from '../particles'; describe('particleEnv (env-свет освещаемых частиц)', () => { it('ambient входит целиком с интенсивностью', () => { @@ -40,6 +40,24 @@ { r: 1, g: 0.9, b: 0.8, intensity: 2.0 }, ); const luma = r * 0.3 + g * 0.6 + b * 0.1; - expect(luma * PARTICLE_LIT_VIS).toBeGreaterThan(1); // vis = 1 — как раньше + expect(litVisibility(luma)).toBe(1); // vis = 1 — как раньше + }); +}); + +describe('litVisibility (кривая видимости lit-частиц)', () => { + it('ночь в темноте: слабый свет давится квадратом — частица невидима', () => { + // линейная кривая давала vis ~0.1 (серые точки), квадрат — в ~10 раз меньше + expect(litVisibility(0.06)).toBeLessThan(0.015); + expect(litVisibility(0.09)).toBeLessThan(0.03); + }); + + it('лампа/день: яркий свет насыщает vis в 1', () => { + expect(litVisibility(0.7)).toBe(1); + expect(litVisibility(5)).toBe(1); + }); + + it('монотонность и клампы: больше света — виднее, минус не ломает', () => { + expect(litVisibility(0.3)).toBeGreaterThan(litVisibility(0.1)); + expect(litVisibility(-0.5)).toBe(0); }); }); \ No newline at end of file diff --git a/v2/packages/engine/src/render/particles.ts b/v2/packages/engine/src/render/particles.ts index d4c15d9..ddca40e 100644 --- a/v2/packages/engine/src/render/particles.ts +++ b/v2/packages/engine/src/render/particles.ts @@ -89,10 +89,24 @@ export const PARTICLE_ENV_SUN = 0.3; /** Насколько полученный свет проявляет освещаемую частицу: видимость — - * clamp(max-канал(полученный свет)·GAIN, 0, 1) — множитель альфы. - * Ночью в темноте received ~0 — частица невидима, у лампы — видна. */ + * clamp((max-канал(полученный свет)·GAIN)^GAMMA, 0, 1) — множитель альфы + * и цвета. Ночью в темноте received ~0 — частица невидима, у лампы — видна. */ export const PARTICLE_LIT_VIS = 1.6; +/** Нелинейность кривой видимости: степень давит СЛАБЫЙ свет — в полутьме + * (received ~0.05–0.1) линейная кривая оставляла vis ~0.1 и серые точки + * на тёмном фоне, квадрат уводит их в невидимость; яркий свет (день, + * лампа) по-прежнему насыщает vis в 1. */ +export const PARTICLE_LIT_GAMMA = 2.0; + +/** Зеркало шейдерной кривой видимости освещаемых частиц (чистая функция — + * тесты сверяют поведение без WebGL): maxChannel — max-канал полученного + * света, результат — множитель альфы и цвета. */ +export function litVisibility(maxChannel: number): number { + const v = Math.pow(Math.max(0, maxChannel) * PARTICLE_LIT_VIS, PARTICLE_LIT_GAMMA); + return Math.min(1, Math.max(0, v)); +} + /** Источник света для particleEnv: цветовые компоненты 0..1 + яркость * (подходит THREE.Color/свет three — поля r/g/b/intensity). */ export interface RgbIntensity { @@ -230,11 +244,13 @@ float vis = 1.0; #if LIT // освещаемый режим: полученный свет (env + источники) правит и цветом, - // и видимостью — ночью в темноте частица исчезает, у лампы проявляется + // и видимостью — ночью в темноте частица исчезает, у лампы проявляется. + // tint тоже гасится vis: иначе ночью цвет остаётся полным и полупрозрачные + // СЕРЫЕ точки читаются на тёмном фоне (запрос «в темноте ещё темнее») received += uEnv; - vis = clamp(max(received.r, max(received.g, received.b)) * ${PARTICLE_LIT_VIS}, 0.0, 1.0); + vis = clamp(pow(max(received.r, max(received.g, received.b)) * ${PARTICLE_LIT_VIS}, ${PARTICLE_LIT_GAMMA}), 0.0, 1.0); #endif - gl_FragColor = vec4(vColor * (uTint + received), vAlpha * vis); + gl_FragColor = vec4(vColor * (uTint * vis + received), vAlpha * vis); }`; const QUAD_VERT = `