/** * Environment — управление окружением сцены: время суток, скайбокс, освещение, * туман. Работает поверх существующих lights/clearColor BabylonScene. * * Время суток (timeOfDay): 0..24 часов. * 06:00 — рассвет (золотистый свет, оранжево-розовый горизонт) * 12:00 — день (яркое солнце, голубое небо) * 18:00 — закат (красно-оранжевое небо, длинные тени) * 00:00 — ночь (тёмно-синее небо, луна, приглушённый свет) * * Пресеты: * 'day' — фиксированный день (12:00) * 'sunset' — закат (18:30) * 'night' — ночь (00:00) * 'dawn' — рассвет (06:00) * 'cycle' — авто-цикл (timeSpeed контролирует скорость) * * Public API: * setPreset(preset) * setTimeOfDay(hour) — 0..24 * setTimeSpeed(seconds_per_hour) — для cycle-режима * tick(dt) — вызывать в render-loop * serialize() / load(data) */ import { Color3, Color4, Vector3, MeshBuilder, StandardMaterial, } from '@babylonjs/core'; /** * Цвета по часам — интерполируем между ключевыми моментами. * Каждая запись: [час, sky, fog, sunDir, sunIntensity, sunColor, ambientGround, hemiIntensity] */ const KEYFRAMES = [ // hour, sky color (RGB 0..1), sun direction, sunIntensity, sunColor, hemiGround, hemiIntensity { hour: 0, sky: [0.05, 0.05, 0.15], sunDir: [-0.3, -1, 0.2], sunInt: 0.05, sunCol: [0.4, 0.5, 0.7], hemiGround: [0.05, 0.05, 0.1], hemiInt: 0.15 }, { hour: 5, sky: [0.15, 0.12, 0.25], sunDir: [-0.6, -0.5, 0.3], sunInt: 0.15, sunCol: [0.6, 0.4, 0.4], hemiGround: [0.1, 0.08, 0.15], hemiInt: 0.25 }, { hour: 7, sky: [0.95, 0.7, 0.5], sunDir: [-0.8, -0.4, 0.2], sunInt: 0.6, sunCol: [1.0, 0.8, 0.5], hemiGround: [0.3, 0.25, 0.3], hemiInt: 0.5 }, { hour: 12, sky: [0.5, 0.7, 0.95], sunDir: [-0.3, -1, -0.2], sunInt: 0.9, sunCol: [1.0, 1.0, 0.95], hemiGround: [0.3, 0.3, 0.4], hemiInt: 0.65 }, { hour: 17, sky: [0.6, 0.75, 0.9], sunDir: [0.4, -0.6, -0.3], sunInt: 0.7, sunCol: [1.0, 0.9, 0.7], hemiGround: [0.3, 0.3, 0.35], hemiInt: 0.55 }, { hour: 19, sky: [0.95, 0.55, 0.4], sunDir: [0.7, -0.3, -0.2], sunInt: 0.5, sunCol: [1.0, 0.6, 0.4], hemiGround: [0.35, 0.25, 0.25], hemiInt: 0.4 }, { hour: 21, sky: [0.2, 0.18, 0.35], sunDir: [0.5, -0.6, 0.2], sunInt: 0.15, sunCol: [0.5, 0.5, 0.7], hemiGround: [0.1, 0.1, 0.2], hemiInt: 0.2 }, { hour: 24, sky: [0.05, 0.05, 0.15], sunDir: [-0.3, -1, 0.2], sunInt: 0.05, sunCol: [0.4, 0.5, 0.7], hemiGround: [0.05, 0.05, 0.1], hemiInt: 0.15 }, ]; const PRESETS = { day: { hour: 12, autoCycle: false }, sunset: { hour: 18.5, autoCycle: false }, night: { hour: 0, autoCycle: false }, dawn: { hour: 6, autoCycle: false }, cycle: { hour: 12, autoCycle: true }, }; function lerp(a, b, t) { return a + (b - a) * t; } function lerpVec3(a, b, t) { return [lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t)]; } /** Найти кадры до и после текущего часа и интерполировать. */ function sampleAt(hour) { let prev = KEYFRAMES[0], next = KEYFRAMES[KEYFRAMES.length - 1]; for (let i = 0; i < KEYFRAMES.length - 1; i++) { if (KEYFRAMES[i].hour <= hour && KEYFRAMES[i + 1].hour >= hour) { prev = KEYFRAMES[i]; next = KEYFRAMES[i + 1]; break; } } const span = next.hour - prev.hour; const t = span > 0 ? (hour - prev.hour) / span : 0; return { sky: lerpVec3(prev.sky, next.sky, t), sunDir: lerpVec3(prev.sunDir, next.sunDir, t), sunInt: lerp(prev.sunInt, next.sunInt, t), sunCol: lerpVec3(prev.sunCol, next.sunCol, t), hemiGround: lerpVec3(prev.hemiGround, next.hemiGround, t), hemiInt: lerp(prev.hemiInt, next.hemiInt, t), }; } export class Environment { constructor(scene, hemiLight, sunLight) { this.scene = scene; this.hemiLight = hemiLight; this.sunLight = sunLight; this.preset = 'day'; this.timeOfDay = 12; // часов 0..24 // Длительность фаз цикла day/night в МИНУТАХ реального времени. // День — с 06 до 18 (12 игровых часов). // Ночь — с 18 до 06 (тоже 12 игровых часов). this.dayDurationMin = 5; this.nightDurationMin = 3; this.fogEnabled = false; this.fogColor = [0.7, 0.8, 0.9]; this.fogDensity = 0.01; // Видимые тела на небе (солнце и луна). // ВАЖНО (задача 16): единое небо рисует SkyboxManager. Environment больше // НЕ рисует свою жёлтую сферу/луну — иначе на небе два солнца. Здесь // остаётся только управление светом (направление/яркость/ambient). this._drawSkyBodies = false; this._sunMesh = null; this._moonMesh = null; if (this._drawSkyBodies) this._createSkyBodies(); this._applyTime(); } _createSkyBodies() { // Солнце — жёлтая светящаяся сфера const sun = MeshBuilder.CreateSphere('skySun', { diameter: 8, segments: 12 }, this.scene); const sunMat = new StandardMaterial('skySunMat', this.scene); sunMat.diffuseColor = new Color3(0, 0, 0); sunMat.emissiveColor = new Color3(1, 0.95, 0.7); sunMat.specularColor = new Color3(0, 0, 0); sunMat.disableLighting = true; sun.material = sunMat; sun.isPickable = false; sun.renderingGroupId = 0; this._sunMesh = sun; // Луна — серовато-белая сфера const moon = MeshBuilder.CreateSphere('skyMoon', { diameter: 6, segments: 12 }, this.scene); const moonMat = new StandardMaterial('skyMoonMat', this.scene); moonMat.diffuseColor = new Color3(0, 0, 0); moonMat.emissiveColor = new Color3(0.85, 0.85, 0.95); moonMat.specularColor = new Color3(0, 0, 0); moonMat.disableLighting = true; moon.material = moonMat; moon.isPickable = false; moon.renderingGroupId = 0; this._moonMesh = moon; } /** Вычислить позицию небесного тела по фазе 0..1 (0=восход, 0.5=зенит, 1=закат). */ _skyBodyPosition(phase) { // Дуга по полусфере радиусом R, центр в (0, 0, 0). // phase=0 → восток (восход на горизонте); // phase=0.5 → зенит; // phase=1 → запад (заход). const R = 80; const angle = phase * Math.PI; // 0..π return new Vector3( Math.cos(angle) * R, // восход +X → закат -X Math.sin(angle) * R, // 0 → R → 0 0 ); } setPreset(preset) { if (!PRESETS[preset]) return; this.preset = preset; const cfg = PRESETS[preset]; this.timeOfDay = cfg.hour; this._applyTime(); } setTimeOfDay(hour) { this.timeOfDay = ((hour % 24) + 24) % 24; this._applyTime(); } /** Установить длительность фазы дня и ночи в минутах реального времени. */ setCycleDuration(dayMin, nightMin) { if (typeof dayMin === 'number' && dayMin > 0) this.dayDurationMin = dayMin; if (typeof nightMin === 'number' && nightMin > 0) this.nightDurationMin = nightMin; } /** Совместимость со старым setTimeSpeed (sec/hour). Игнорируется в новом API. */ setTimeSpeed() { /* deprecated */ } setFog(enabled, color, density) { this.fogEnabled = !!enabled; if (Array.isArray(color)) this.fogColor = color; if (density != null) this.fogDensity = density; this._applyFog(); } /** В render-loop вызвать каждый кадр (для авто-цикла). */ tick(dt) { if (this.preset !== 'cycle') return; // День: 06 → 18 (12 игровых часов) идёт за dayDurationMin минут реального. // Ночь: 18 → 06 (12 игровых часов) идёт за nightDurationMin минут реального. const isDayTime = this.timeOfDay >= 6 && this.timeOfDay < 18; const phaseSeconds = (isDayTime ? this.dayDurationMin : this.nightDurationMin) * 60; // За phaseSeconds игровое время продвигается на 12 часов. const hoursPerSecond = 12 / phaseSeconds; this.timeOfDay = (this.timeOfDay + dt * hoursPerSecond) % 24; this._applyTime(); } /** Применить текущее время суток к небу и освещению. */ _applyTime() { const s = sampleAt(this.timeOfDay); if (this.scene) { this.scene.clearColor = new Color4(s.sky[0], s.sky[1], s.sky[2], 1.0); } if (this.sunLight) { // Babylon DirectionalLight: direction указывает В сторону куда падает свет. const dir = new Vector3(s.sunDir[0], s.sunDir[1], s.sunDir[2]); dir.normalize(); this.sunLight.direction = dir; this.sunLight.intensity = s.sunInt; this.sunLight.diffuse = new Color3(s.sunCol[0], s.sunCol[1], s.sunCol[2]); } if (this.hemiLight) { this.hemiLight.intensity = s.hemiInt; this.hemiLight.groundColor = new Color3(s.hemiGround[0], s.hemiGround[1], s.hemiGround[2]); this.hemiLight.diffuse = new Color3(s.sky[0], s.sky[1], s.sky[2]); } // === Позиции солнца и луны === // Солнце видно с 06 до 18 (12 часов). phase = (hour - 6) / 12 → 0..1 // Луна видна с 18 до 06 (через полночь). phase = ((hour + 6) % 24) / 12 → 0..1 const h = this.timeOfDay; if (this._sunMesh) { if (h >= 6 && h <= 18) { const phase = (h - 6) / 12; this._sunMesh.position = this._skyBodyPosition(phase); this._sunMesh.setEnabled(true); } else { this._sunMesh.setEnabled(false); } } if (this._moonMesh) { // Луна видна когда солнце под горизонтом if (h <= 6 || h >= 18) { // hour 18→0 = phase 0 (восход), hour 0→6 = phase 0.5..1 const adj = (h + 6) % 24; const phase = adj / 12; this._moonMesh.position = this._skyBodyPosition(phase); this._moonMesh.setEnabled(true); } else { this._moonMesh.setEnabled(false); } } this._applyFog(); } _applyFog() { if (!this.scene) return; if (this.fogEnabled) { this.scene.fogMode = 2; // FOGMODE_EXP this.scene.fogColor = new Color3(this.fogColor[0], this.fogColor[1], this.fogColor[2]); this.scene.fogDensity = this.fogDensity; } else { this.scene.fogMode = 0; // FOGMODE_NONE } } serialize() { return { preset: this.preset, timeOfDay: this.timeOfDay, dayDurationMin: this.dayDurationMin, nightDurationMin: this.nightDurationMin, fogEnabled: this.fogEnabled, fogColor: this.fogColor, fogDensity: this.fogDensity, }; } load(data) { if (!data) return; if (data.preset) this.preset = data.preset; if (typeof data.timeOfDay === 'number') this.timeOfDay = data.timeOfDay; if (typeof data.dayDurationMin === 'number' && data.dayDurationMin > 0) { this.dayDurationMin = data.dayDurationMin; } if (typeof data.nightDurationMin === 'number' && data.nightDurationMin > 0) { this.nightDurationMin = data.nightDurationMin; } if (typeof data.fogEnabled === 'boolean') this.fogEnabled = data.fogEnabled; if (Array.isArray(data.fogColor)) this.fogColor = data.fogColor; if (typeof data.fogDensity === 'number') this.fogDensity = data.fogDensity; this._applyTime(); } }