studio/src/editor/engine/Environment.js
min a4881ee5ce 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>
2026-06-05 00:44:25 +03:00

277 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 больше НЕ рисует свою жёлтую сферу/луну/фон —
// иначе на небе два солнца. 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();
}
}