diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index a640b26..2e98317 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -1293,7 +1293,7 @@ export class BabylonScene { } this.dynamics = new DynamicsManager(this); this.environment = new Environment(this.scene, this._hemiLight, this._sunLight); - this.skybox = new SkyboxManager(this.scene); // задача 16 — кастомное небо + this.skybox = new SkyboxManager(this.scene, this._hemiLight, this._sunLight); // задача 16 — кастомное небо (единый источник света) this.audioManager = new AudioManager(); this.assetManager = new AssetManager(); // PrimitiveManager должен уметь брать dataURL картинки по id ассета, diff --git a/src/editor/engine/Environment.js b/src/editor/engine/Environment.js index 2d69514..590bd02 100644 --- a/src/editor/engine/Environment.js +++ b/src/editor/engine/Environment.js @@ -91,10 +91,15 @@ export class Environment { this.fogEnabled = false; this.fogColor = [0.7, 0.8, 0.9]; this.fogDensity = 0.01; - // Видимые тела на небе (солнце и луна) — создаём по запросу + // Видимые тела на небе (солнце и луна). + // ВАЖНО (задача 16): единое небо рисует SkyboxManager (купол + солнечный + // диск + облака). Environment больше НЕ рисует свою жёлтую сферу/луну/фон — + // иначе на небе два солнца. Environment теперь отвечает ТОЛЬКО за свет + // (направление/яркость солнца, ambient). Флаг ниже отключает небесные тела. + this._drawSkyBodies = false; this._sunMesh = null; this._moonMesh = null; - this._createSkyBodies(); + if (this._drawSkyBodies) this._createSkyBodies(); this._applyTime(); } diff --git a/src/editor/engine/SkyboxManager.js b/src/editor/engine/SkyboxManager.js index 0e2428d..d148cab 100644 --- a/src/editor/engine/SkyboxManager.js +++ b/src/editor/engine/SkyboxManager.js @@ -99,6 +99,7 @@ const PRESETS = { mountains: false, clouds: { enabled: true, cover: 0.25, color: '#ffffff', speed: 0.015 }, fog: { color: '#cfe2f2', density: 0.0035 }, + light: { sunIntensity: 1.0, sunColor: '#fff6e0', hemiIntensity: 0.7, ambient: '#b9d4ee' }, }, 'lowpoly-roblox': { top: '#4a93e6', horizon: '#cfe6f5', bottom: '#e6f1fa', @@ -106,6 +107,7 @@ const PRESETS = { mountains: true, clouds: { enabled: true, cover: 0.45, color: '#ffffff', speed: 0.012 }, fog: { color: '#e2eef7', density: 0.005 }, + light: { sunIntensity: 0.95, sunColor: '#fff7e0', hemiIntensity: 0.75, ambient: '#cfe6f5' }, }, 'cloudy': { top: '#8fa6bd', horizon: '#c2ccd6', bottom: '#d8dde2', @@ -113,6 +115,7 @@ const PRESETS = { mountains: false, clouds: { enabled: true, cover: 0.8, color: '#e8ebef', speed: 0.02 }, fog: { color: '#cfd6dd', density: 0.008 }, + light: { sunIntensity: 0.5, sunColor: '#dfe4ea', hemiIntensity: 0.8, ambient: '#c2ccd6' }, }, 'sunset': { top: '#2a3a6b', horizon: '#f5915a', bottom: '#f7c98a', @@ -120,6 +123,7 @@ const PRESETS = { mountains: true, clouds: { enabled: true, cover: 0.35, color: '#ffd9b3', speed: 0.01 }, fog: { color: '#f0b483', density: 0.006 }, + light: { sunIntensity: 0.7, sunColor: '#ff9a52', hemiIntensity: 0.5, ambient: '#9a6a78' }, }, 'starry-night': { top: '#070b1f', horizon: '#1b2547', bottom: '#243056', @@ -127,6 +131,7 @@ const PRESETS = { mountains: true, stars: true, clouds: { enabled: false }, fog: { color: '#141c38', density: 0.004 }, + light: { sunIntensity: 0.18, sunColor: '#9aa8d8', hemiIntensity: 0.25, ambient: '#1b2547' }, }, 'space': { top: '#02030a', horizon: '#06070f', bottom: '#0a0c18', @@ -134,12 +139,15 @@ const PRESETS = { mountains: false, stars: true, clouds: { enabled: false }, fog: { enabled: false }, + light: { sunIntensity: 0.6, sunColor: '#ffffff', hemiIntensity: 0.2, ambient: '#10121c' }, }, }; export class SkyboxManager { - constructor(scene) { + constructor(scene, hemiLight, sunLight) { this.scene = scene; + this.hemiLight = hemiLight || null; // ambient + this.sunLight = sunLight || null; // directional (тени) this._dome = null; this._mat = null; this._mountains = null; @@ -160,6 +168,7 @@ export class SkyboxManager { mountains: false, stars: false, clouds: { enabled: false, cover: 0.4, color: '#ffffff', speed: 0.012 }, fog: { enabled: false, color: '#dde8f2', density: 0.005 }, + light: { sunIntensity: 0.95, sunColor: '#fff7e0', hemiIntensity: 0.75, ambient: '#cfe6f5' }, }; } @@ -352,6 +361,25 @@ export class SkyboxManager { } } + // ── Освещение (единый источник: небо управляет светом сцены) ───────────── + /** Выставить направление/яркость солнца и ambient под текущее небо. */ + _applyLighting(light, sunDir) { + if (this.sunLight && sunDir) { + // DirectionalLight.direction указывает КУДА падает свет → от солнца вниз. + const d = new Vector3(-sunDir[0], -sunDir[1], -sunDir[2]); + if (d.lengthSquared() > 0) { d.normalize(); this.sunLight.direction = d; } + } + if (!light) return; + if (this.sunLight) { + if (typeof light.sunIntensity === 'number') this.sunLight.intensity = light.sunIntensity; + if (light.sunColor) this.sunLight.diffuse = Color3.FromArray(hexToRgb(light.sunColor)); + } + if (this.hemiLight) { + if (typeof light.hemiIntensity === 'number') this.hemiLight.intensity = light.hemiIntensity; + if (light.ambient) this.hemiLight.groundColor = Color3.FromArray(hexToRgb(light.ambient)); + } + } + // ── Public API ─────────────────────────────────────────────────────────── /** Применить пресет или ручные опции gradient. */ @@ -365,6 +393,8 @@ export class SkyboxManager { s.haze = preset.haze; s.mountains = !!preset.mountains; s.stars = !!preset.stars; s.clouds = { ...(preset.clouds || { enabled: false }) }; s.fog = { enabled: preset.fog?.enabled !== false, ...(preset.fog || {}) }; + s.light = preset.light || null; + this._applyLighting(preset.light, preset.sunDir); } else { // Ручной gradient: { topColor, bottomColor, horizonColor, sunDirection, sunColor, sunSize } if (opts.topColor) s.top = opts.topColor; @@ -428,13 +458,27 @@ export class SkyboxManager { s.mountains = !!target.mountains; s.stars = !!target.stars; s.clouds = { ...(target.clouds || { enabled: false }) }; s.fog = { enabled: target.fog?.enabled !== false, ...(target.fog || {}) }; + s.light = target.light || null; this._rebuildExtras(); + // Запоминаем стартовые/целевые значения света для плавной анимации. + if (target.light) { + this._fade.lightFrom = { + sunInt: this.sunLight?.intensity ?? 1, + hemiInt: this.hemiLight?.intensity ?? 0.7, + }; + this._fade.lightTo = { + sunInt: target.light.sunIntensity ?? 1, + hemiInt: target.light.hemiIntensity ?? 0.7, + sunColor: target.light.sunColor, ambient: target.light.ambient, + }; + } } - /** Пересобрать всё (купол-uniforms + горы + звёзды + облака + туман). */ + /** Пересобрать всё (купол-uniforms + горы + звёзды + облака + туман + свет). */ _rebuildAll() { this._applyShaderUniforms(); this._rebuildExtras(); + this._applyLighting(this._state.light, this._state.sunDir); } _rebuildExtras() { @@ -471,6 +515,23 @@ export class SkyboxManager { m.setVector3('sunDir', new Vector3(sd[0], sd[1], sd[2]).normalize()); m.setFloat('sunSize', f.sunSize + (t.sunSize - f.sunSize) * k); m.setFloat('horizonHaze', f.haze + (t.haze - f.haze) * k); + // Плавно ведём направление солнца (свет) к целевому (используем sd выше). + if (this.sunLight) { + const d = new Vector3(-sd[0], -sd[1], -sd[2]); + if (d.lengthSquared() > 0) { d.normalize(); this.sunLight.direction = d; } + } + } + // Плавно ведём яркость/ambient света. + if (this._fade.lightFrom && this._fade.lightTo) { + const lf = this._fade.lightFrom, lt = this._fade.lightTo; + if (this.sunLight) { + this.sunLight.intensity = lf.sunInt + (lt.sunInt - lf.sunInt) * k; + if (lt.sunColor) this.sunLight.diffuse = Color3.FromArray(hexToRgb(lt.sunColor)); + } + if (this.hemiLight) { + this.hemiLight.intensity = lf.hemiInt + (lt.hemiInt - lf.hemiInt) * k; + if (lt.ambient) this.hemiLight.groundColor = Color3.FromArray(hexToRgb(lt.ambient)); + } } if (k >= 1) { // Зафиксировать целевое состояние в _state (как hex).