/** * GdPostFx — пост-обработка для GD-уровней (этап G9). * * Подключает Babylon DefaultRenderingPipeline с: * - Bloom — свечение ярких объектов (куб с glow, неон-края, конфетти) * - Vignette — мягкое затемнение углов * - ImageProcessing tone-mapping + лёгкий тёплый сдвиг * * Также подкручивает hemi/sun свет: тёплый свет сверху, прохладный снизу. * * Использование: * const fx = new GdPostFx(); * fx.attach(scene, camera, scene3d); * fx.dispose(); */ import { DefaultRenderingPipeline, Color3, Color4, ImageProcessingConfiguration, } from '@babylonjs/core'; export class GdPostFx { constructor() { this.scene = null; this._scene3d = null; this._pipeline = null; // Backup света (восстанавливаем при dispose) this._lightBackup = null; } attach(scene, camera, scene3d) { if (!scene || !camera) return; this.scene = scene; this._scene3d = scene3d; // Качество: 'high' (default) | 'low' — из localStorage const quality = (typeof localStorage !== 'undefined' && localStorage.getItem('gd_graphics_quality')) || 'high'; // === DefaultRenderingPipeline === const pipeline = new DefaultRenderingPipeline( 'gd_post_pipeline', quality === 'high', // HDR только в high scene, [camera], ); // Bloom — только в high quality (самый дорогой эффект) pipeline.bloomEnabled = quality === 'high'; if (pipeline.bloomEnabled) { pipeline.bloomThreshold = 0.85; pipeline.bloomWeight = 0.35; pipeline.bloomKernel = 64; pipeline.bloomScale = 0.5; } // Image processing — лёгкая коррекция (без затемнения) pipeline.imageProcessingEnabled = true; if (pipeline.imageProcessing) { // Tone mapping отключаем — он сжимает яркие участки и общая // картинка темнеет. Без него bloom+vignette не «съедают» свет. pipeline.imageProcessing.toneMappingEnabled = false; pipeline.imageProcessing.exposure = 1.0; pipeline.imageProcessing.contrast = 1.0; // Лёгкий vignette — только по краям, не глобальное затемнение pipeline.imageProcessing.vignetteEnabled = true; pipeline.imageProcessing.vignetteWeight = 0.6; pipeline.imageProcessing.vignetteColor = new Color4(0, 0, 0, 0); pipeline.imageProcessing.vignetteStretch = 0.3; pipeline.imageProcessing.vignetteCameraFov = 0.5; pipeline.imageProcessing.vignetteBlendMode = ImageProcessingConfiguration.VIGNETTEMODE_MULTIPLY; // Чуть-чуть насыщенности (без сдвига hue) pipeline.imageProcessing.colorCurvesEnabled = true; try { const curves = pipeline.imageProcessing.colorCurves; if (curves) { curves.globalSaturation = 8; } } catch (e) {} } // FXAA — антиалиасинг (бесплатно с pipeline) pipeline.fxaaEnabled = true; pipeline.samples = 4; this._pipeline = pipeline; // Свет дефолтный движка не трогаем — наш предыдущий «тёплый» сдвиг // делал картинку темнее (intensity 1.1 + diffuse 0.82B не компенсирует). console.log('[GdPostFx] подключен (bloom + лёгкий vignette)'); } dispose() { if (this._pipeline) { try { this._pipeline.dispose(); } catch (e) {} this._pipeline = null; } if (this._lightBackup && this._scene3d) { try { const sun = this._scene3d._sunLight; const hemi = this._scene3d._hemiLight; if (sun && this._lightBackup.sunDiffuse) { sun.diffuse = this._lightBackup.sunDiffuse; sun.specular = this._lightBackup.sunSpecular; sun.intensity = this._lightBackup.sunIntensity; } if (hemi && this._lightBackup.hemiDiffuse) { hemi.diffuse = this._lightBackup.hemiDiffuse; hemi.groundColor = this._lightBackup.hemiGroundColor; hemi.intensity = this._lightBackup.hemiIntensity; } } catch (e) {} this._lightBackup = null; } this._scene3d = null; this.scene = null; } }