fix(studio): единая система неба — убрать второе (жёлтое) солнце

Environment больше НЕ рисует свою жёлтую сферу-солнце/луну/фон (флаг
_drawSkyBodies=false) — иначе на небе было два солнца. Единое небо рисует
SkyboxManager (купол + солнечный диск + облака + горы). SkyboxManager стал
единым источником освещения: каждый пресет выставляет direction/intensity/
color солнца и ambient (lights переданы в конструктор), fadeTo плавно ведёт
и свет. Environment оставлен только для day/night cycle совместимости.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-05 00:44:25 +03:00
parent 71536668f2
commit a4881ee5ce
3 changed files with 71 additions and 5 deletions

View File

@ -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 ассета,

View File

@ -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();
}

View File

@ -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).