Просмотр исходного кода

增加体积云、天气系统

WQQ 18 часов назад
Родитель
Сommit
615fa75852
43 измененных файлов с 4778 добавлено и 139 удалено
  1. 1 1
      RuoYi-Vue3/.env.development
  2. 1 1
      RuoYi-Vue3/.env.production
  3. 1 1
      RuoYi-Vue3/.env.staging
  4. 82 0
      RuoYi-Vue3/analyze_cesium_obfuscation.cjs
  5. 31 0
      RuoYi-Vue3/check_syntax.cjs
  6. 26 0
      RuoYi-Vue3/find_uniform_patch.cjs
  7. 3 1
      RuoYi-Vue3/index.html
  8. 1 0
      RuoYi-Vue3/package.json
  9. 110 0
      RuoYi-Vue3/patch_cesium_sampler3d.cjs
  10. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/higher_order_scattering.bin
  11. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/irradiance.bin
  12. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/scattering.bin
  13. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/single_mie_scattering.bin
  14. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/transmittance.bin
  15. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/local_weather.png
  16. 0 0
      RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/shape.bin
  17. 0 0
      RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/shape_detail.bin
  18. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/stbn.bin
  19. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/turbulence.png
  20. BIN
      RuoYi-Vue3/public/cesium-clouds-assets/data/noisePic/noisergba256.png
  21. 54 0
      RuoYi-Vue3/public/cesium-patch.js
  22. 170 0
      RuoYi-Vue3/src/cesium-clouds-atmosphere/loadBinThreeGeospatial.js
  23. 754 0
      RuoYi-Vue3/src/cesium-clouds-atmosphere/simpleClouds.js
  24. 6 0
      RuoYi-Vue3/src/supermap-cesium-module/components/components.js
  25. 124 102
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/scene-attribute/scene-attribute.js
  26. 7 0
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/time-slider/index.js
  27. 105 0
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/time-slider/time-slider.js
  28. 68 0
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/time-slider/time-slider.vue
  29. 7 0
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/weather-select/index.js
  30. 55 0
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/weather-select/weather-select.js
  31. 208 0
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/weather-select/weather-select.vue
  32. 7 0
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/volumetric-cloud/index.js
  33. 121 0
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/volumetric-cloud/volumetric-cloud.js
  34. 151 0
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/volumetric-cloud/volumetric-cloud.vue
  35. 27 28
      RuoYi-Vue3/src/supermap-cesium-module/components/viewer/viewer.js
  36. 4 1
      RuoYi-Vue3/src/supermap-cesium-module/components/viewer/viewer.vue
  37. 571 0
      RuoYi-Vue3/src/supermap-cesium-module/js/common/dayPeriod.js
  38. 3 0
      RuoYi-Vue3/src/supermap-cesium-module/style/components.scss
  39. 1 0
      RuoYi-Vue3/src/supermap-cesium-module/style/globle.scss
  40. 1 1
      RuoYi-Vue3/src/views/front/PageHeader.vue
  41. 1 1
      RuoYi-Vue3/tests/basic.spec.ts
  42. 6 2
      RuoYi-Vue3/vite.config.js
  43. 2071 0
      ThreeGeospatialPipeline_temp.mjs

+ 1 - 1
RuoYi-Vue3/.env.development

@@ -1,5 +1,5 @@
 # 页面标题
-VITE_APP_TITLE = 若依管理系统
+VITE_APP_TITLE = 可视化模型平台
 
 # 开发环境配置
 VITE_APP_ENV = 'development'

+ 1 - 1
RuoYi-Vue3/.env.production

@@ -1,5 +1,5 @@
 # 页面标题
-VITE_APP_TITLE = 若依管理系统
+VITE_APP_TITLE = 可视化模型平台
 
 # 生产环境配置
 VITE_APP_ENV = 'production'

+ 1 - 1
RuoYi-Vue3/.env.staging

@@ -1,5 +1,5 @@
 # 页面标题
-VITE_APP_TITLE = 若依管理系统
+VITE_APP_TITLE = 可视化模型平台
 
 # 生产环境配置
 VITE_APP_ENV = 'staging'

+ 82 - 0
RuoYi-Vue3/analyze_cesium_obfuscation.cjs

@@ -0,0 +1,82 @@
+const fs = require('fs');
+
+// Analyze Cesium.js obfuscation to find the uniform switch-case
+const cesium = fs.readFileSync('public/Cesium/Cesium.js', 'utf8');
+
+// Find the SAMPLER_3D constant: 'SAMPLER_3D':0x8b5f
+const s3Idx = cesium.indexOf("'SAMPLER_3D':0x8b5f");
+if (s3Idx < 0) {
+    console.error('SAMPLER_3D constant not found');
+    process.exit(1);
+}
+console.log('SAMPLER_3D constant at byte:', s3Idx);
+console.log('Context:', cesium.substring(Math.max(0, s3Idx - 80), s3Idx + 40));
+
+// Find the object that contains the WebGL constants
+// Looking backward for the object opening
+const objStart = cesium.lastIndexOf('{', s3Idx - 100);
+console.log('\nObject definition starting at:', objStart);
+console.log('Object start context:', cesium.substring(objStart, Math.min(cesium.length, objStart + 200)));
+
+// The obfuscated code uses an object like:
+// var _0x... = {'SAMPLER_2D':0x8b5e,'SAMPLER_3D':0x8b5f,'SAMPLER_CUBE':0x8b60,...}
+// This object is assigned to a variable and used in switch cases
+
+// Find the variable name being assigned
+const varMatch = cesium.substring(Math.max(0, objStart - 100), objStart).match(/var\s+(\w+)\s*=\s*$/);
+if (varMatch) {
+    const varName = varMatch[1];
+    console.log('\nVariable name:', varName);
+    
+    // Search for usage of this variable with SAMPLER_2D in a case context
+    const searchStr = varName + '.SAMPLER_2D';
+    let pos = 0;
+    let count = 0;
+    const results = [];
+    while ((pos = cesium.indexOf(searchStr, pos)) >= 0 && count < 10) {
+        results.push(pos);
+        pos++;
+        count++;
+    }
+    
+    console.log('\nFound', results.length, 'occurrences of', searchStr);
+    for (const r of results) {
+        const ctx = cesium.substring(Math.max(0, r - 40), Math.min(cesium.length, r + 80));
+        console.log('  byte', r, ':', ctx.substring(0, 120));
+        
+        // Check if this is in a switch-case
+        if (ctx.includes('case')) {
+            console.log('  *** IN SWITCH CASE ***');
+            
+            // Check if SAMPLER_3D is also in the same switch
+            const switchStart = cesium.lastIndexOf('switch', r);
+            if (switchStart >= 0) {
+                const blockEnd = cesium.indexOf('}', r);
+                const switchBlock = cesium.substring(switchStart, Math.min(cesium.length, blockEnd + 100));
+                const hasS3 = switchBlock.includes('SAMPLER_3D');
+                console.log('  SAMPLER_3D in same switch:', hasS3);
+                
+                if (!hasS3) {
+                    console.log('  *** SAMPLER_3D MISSING! Need to patch here ***');
+                    // Extract the full case block
+                    const caseStart = switchBlock.indexOf('case');
+                    console.log('  Full switch block (first 500 chars):', switchBlock.substring(caseStart, Math.min(switchBlock.length, caseStart + 500)));
+                }
+            }
+        }
+    }
+} else {
+    console.log('Could not find variable assignment pattern');
+    
+    // Alternative: search for SAMPLER_2D used in nearby context
+    // Try finding by searching for this pattern
+    const patterns = ['.SAMPLER_2D', "['SAMPLER_2D']"];
+    for (const p of patterns) {
+        let pos = 0;
+        while ((pos = cesium.indexOf(p, 100000)) >= 0) {
+            const ctx = cesium.substring(Math.max(0, pos - 60), Math.min(cesium.length, pos + 80));
+            console.log('\nFound', p, 'at byte', pos, ':', ctx.substring(0, 150));
+            pos++;
+        }
+    }
+}

+ 31 - 0
RuoYi-Vue3/check_syntax.cjs

@@ -0,0 +1,31 @@
+const fs = require('fs');
+const code = fs.readFileSync('src/cesium-clouds-atmosphere/simpleClouds.js', 'utf8');
+
+const lines = code.split('\n');
+console.log('Total lines:', lines.length);
+
+// Count backticks
+const btCount = code.split('`').length - 1;
+console.log('Backtick count:', btCount, '(should be even)');
+
+// Count braces
+let openB = 0, closeB = 0;
+for (const ch of code) {
+  if (ch === '{') openB++;
+  if (ch === '}') closeB++;
+}
+console.log('Braces: open=', openB, 'close=', closeB, 'diff=', openB - closeB);
+
+// Count parens
+let openP = 0, closeP = 0;
+for (const ch of code) {
+  if (ch === '(') openP++;
+  if (ch === ')') closeP++;
+}
+console.log('Parens: open=', openP, 'close=', closeP, 'diff=', openP - closeP);
+
+// check last few lines
+console.log('\nLast 8 lines:');
+for (let i = Math.max(0, lines.length - 8); i < lines.length; i++) {
+  console.log(i+1 + ': ' + lines[i]);
+}

+ 26 - 0
RuoYi-Vue3/find_uniform_patch.cjs

@@ -0,0 +1,26 @@
+const fs = require('fs');
+const content = fs.readFileSync('public/Cesium/Cesium.js', 'utf8');
+
+// Search for pattern near the error string
+const idx1 = content.indexOf('Unrecognized');
+console.log('Unrecognized at byte:', idx1);
+if (idx1 >= 0) {
+    const start = Math.max(0, idx1 - 100);
+    const end = Math.min(content.length, idx1 + 100);
+    console.log('Context:', content.substring(start, end));
+}
+
+// Also search for the string parts
+const searchTerms = ['unrecognized', 'uniform type'];
+for (const term of searchTerms) {
+    let pos = 0;
+    let count = 0;
+    while ((pos = content.indexOf(term, pos)) >= 0 && count < 3) {
+        const start = Math.max(0, pos - 60);
+        const end = Math.min(content.length, pos + 60);
+        console.log('\nFound "' + term + '" at byte', pos);
+        console.log('  Context:', content.substring(start, end));
+        pos++;
+        count++;
+    }
+}

+ 3 - 1
RuoYi-Vue3/index.html

@@ -209,8 +209,10 @@
       <div class="load_title">正在加载系统资源,请耐心等待</div>
     </div>
   </div>
-  <!-- 引入 Cesium -->
+  <!-- 引入 Cesium(包含超图扩展) -->
   <script src="/Cesium/Cesium.js"></script>
+  <!-- Cesium 运行时补丁:让 PostProcessStage 支持 sampler3D uniform -->
+  <script src="/cesium-patch.js"></script>
   <script>
     console.log('=== Cesium 初始化检查 ===');
     console.log('Cesium 版本:', Cesium.CESIUM_VERSION);

+ 1 - 0
RuoYi-Vue3/package.json

@@ -32,6 +32,7 @@
     "cesium": "^1.140.0",
     "cesium-transform-gizmo": "^0.1.1",
     "clipboard": "2.0.11",
+    "dat.gui": "^0.7.9",
     "echarts": "5.6.0",
     "element-plus": "2.10.7",
     "file-saver": "2.0.5",

+ 110 - 0
RuoYi-Vue3/patch_cesium_sampler3d.cjs

@@ -0,0 +1,110 @@
+/**
+ * 修补 Cesium.js:在 uniform 类型 switch-case 中添加 SAMPLER_3D (35679) 支持。
+ * 
+ * SuperMap 定制的 Cesium.js 在 PostProcessStage 的 uniform setter 创建逻辑中
+ * 缺少对 SAMPLER_3D 分支的处理,导致 sampler3D 类型的 uniform 绑定失败。
+ *
+ * 本脚本通过分析 obfuscated 代码中的 SAMPLER constants 定义,找到对应的
+ * WebGL 常量值,并在 switch-case 中插入缺失的 SAMPLER_3D 分支。
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const filePath = path.join(__dirname, 'public/Cesium/Cesium.js');
+let content = fs.readFileSync(filePath, 'utf8');
+
+// Step 1: Find SAMPLER_3D constant definition
+// Looking for: 'SAMPLER_3D':0x8b5f
+const sampler3DConstIdx = content.indexOf("'SAMPLER_3D'");
+if (sampler3DConstIdx < 0) {
+    console.error('SAMPLER_3D constant not found in Cesium.js');
+    process.exit(1);
+}
+console.log('SAMPLER_3D constant found at byte', sampler3DConstIdx);
+
+// Step 2: Find SAMPLER_2D constant definition to find its numeric value
+const sampler2DConstIdx = content.indexOf("'SAMPLER_2D'");
+if (sampler2DConstIdx < 0) {
+    console.error('SAMPLER_2D constant not found');
+    process.exit(1);
+}
+console.log('SAMPLER_2D constant found at byte', sampler2DConstIdx);
+
+// Step 3: Find the switch-case that handles uniform types
+// Looking for the pattern: case ... SAMPLER_2D ... case ... SAMPLER_CUBE
+// These are used in the uniform setter creation function
+
+// Search for SAMPLER_CUBE in the context of a switch case
+const switchPattern = /case\s+[^;]*?SAMPLER_2D[^;]*?case\s+[^;]*?SAMPLER_CUBE/g;
+let match;
+let found = false;
+
+while ((match = switchPattern.exec(content)) !== null) {
+    const fullMatch = match[0];
+    console.log('\nFound SAMPLER_2D..SAMPLER_CUBE sequence at byte', match.index);
+    console.log('Match:', fullMatch.substring(0, 200));
+    
+    // Check if SAMPLER_3D is already present between them
+    if (fullMatch.includes('SAMPLER_3D')) {
+        console.log('SAMPLER_3D already present - no patch needed!');
+        found = true;
+        continue;
+    }
+    
+    // Find the pattern between SAMPLER_2D and SAMPLER_CUBE
+    const s2Idx = fullMatch.indexOf('SAMPLER_2D');
+    const scIdx = fullMatch.indexOf('SAMPLER_CUBE');
+    const between = fullMatch.substring(s2Idx + 'SAMPLER_2D'.length, scIdx);
+    
+    console.log('Between SAMPLER_2D and SAMPLER_CUBE:', between.substring(0, 100));
+    
+    // Get the property access pattern: e.g., `_0x2cbe(0x...)` or `this.SAMPLER_2D`
+    // We need to find how SAMPLER_2D's value is obtained
+    const s2Access = fullMatch.substring(0, s2Idx).trim();
+    const s2AccessClean = s2Access.replace(/^case\s+/, '').trim();
+    console.log('SAMPLER_2D access pattern:', s2AccessClean.substring(0, 80));
+    
+    // The SAMPLER_2D access is via a property on an object
+    // The SAMPLER_3D should use the SAME object but with SAMPLER_3D
+    // e.g. if SAMPLER_2D is accessed as `_0x4b6e38.SAMPLER_2D`
+    // then SAMPLER_3D should be `_0x4b6e38.SAMPLER_3D`
+    
+    const objMatch = s2AccessClean.match(/^([\w.]+)\.SAMPLER_2D$/);
+    if (!objMatch) {
+        console.log('Cannot parse access pattern, trying alternative...');
+        // Could also be an array access like _0x...['SAMPLER_2D']
+        continue;
+    }
+    
+    const obj = objMatch[1];
+    const sampler3DAccess = obj + ".SAMPLER_3D";
+    const insertStr = "case " + sampler3DAccess + ":";
+    
+    console.log('Will insert:', insertStr);
+    
+    // Insert SAMPLER_3D case after SAMPLER_2D case
+    const insertPos = match.index + s2Idx + 'SAMPLER_2D'.length + between.length;
+    const insertion = "case " + sampler3DAccess + ":";
+    
+    content = content.substring(0, insertPos) + insertion + content.substring(insertPos);
+    console.log('Inserted at byte', insertPos);
+    
+    found = true;
+    break;
+}
+
+if (!found) {
+    console.log('\nCould not find the switch-case pattern. Trying alternative approach...');
+    
+    // Alternative: Search for "SAMPLER_2D:SAMPLER_CUBE" or similar sequences
+    // The obfuscated code might have these constant strings next to each other
+    const altPattern = /SAMPLER_2D['"`:;,.\]]+SAMPLER_CUBE/g;
+    while ((match = altPattern.exec(content)) !== null) {
+        console.log('Alt pattern found at byte', match.index, ':', match[0].substring(0, 100));
+    }
+}
+
+// Save the patched file
+fs.writeFileSync(filePath, content, 'utf8');
+console.log('\nPatch complete');

BIN
RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/higher_order_scattering.bin


BIN
RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/irradiance.bin


BIN
RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/scattering.bin


BIN
RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/single_mie_scattering.bin


BIN
RuoYi-Vue3/public/cesium-clouds-assets/atmosphere-assets/transmittance.bin


BIN
RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/local_weather.png


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/shape.bin


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/shape_detail.bin


BIN
RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/stbn.bin


BIN
RuoYi-Vue3/public/cesium-clouds-assets/clouds-assets/turbulence.png


BIN
RuoYi-Vue3/public/cesium-clouds-assets/data/noisePic/noisergba256.png


+ 54 - 0
RuoYi-Vue3/public/cesium-patch.js

@@ -0,0 +1,54 @@
+/**
+ * Cesium 运行时补丁:为 PostProcessStage 添加 sampler3D uniform 支持。
+ *
+ * SuperMap 定制的 Cesium.js 缺少对 sampler3D (GL_SAMPLER_3D = 35679) 类型的
+ * uniform 处理,导致含有 3D 纹理的 PostProcessStage 渲染崩溃。
+ *
+ * 修复策略:
+ *   包装 WebGL2RenderingContext.prototype.getActiveUniform,
+ *   将 SAMPLER_3D (35679) 报告为 SAMPLER_2D (35678),
+ *   让 Cesium 的正常 uniform 处理流程接管。
+ *   实际的 3D 纹理绑定由自定义纹理对象的 bind() 方法(使用 GL_TEXTURE_3D)完成。
+ *
+ * 必须在 Cesium 加载之后、任何 PostProcessStage 创建之前执行。
+ */
+
+(function patchCesiumSampler3D() {
+  if (typeof Cesium === 'undefined') {
+    console.warn('[CesiumPatch] Cesium 未加载,跳过补丁');
+    return;
+  }
+
+  const SAMPLER_3D = 35679;
+  const SAMPLER_2D = 35678;
+  let patchedCount = 0;
+
+  // ── 包装 getActiveUniform ──────────────────────────────────────
+  // 让 Cesium 把所有 sampler3D uniform 当作 sampler2D 处理。
+  // 实际 3D 纹理绑定由纹理对象的自定义 bind() 方法自动处理。
+  function wrapGetActiveUniform(proto) {
+    if (!proto || !proto.getActiveUniform) return false;
+    const orig = proto.getActiveUniform;
+    proto.getActiveUniform = function (program, index) {
+      const info = orig.call(this, program, index);
+      if (info && info.type === SAMPLER_3D) {
+        info.type = SAMPLER_2D;
+        patchedCount++;
+      }
+      return info;
+    };
+    return true;
+  }
+
+  // WebGL2 上下文继承自 WebGLRenderingContext
+  const patched2 = wrapGetActiveUniform(WebGL2RenderingContext.prototype);
+  const patched1 = wrapGetActiveUniform(WebGLRenderingContext.prototype);
+
+  if (patched2 || patched1) {
+    console.log('[CesiumPatch] sampler3D → sampler2D 补丁已应用 ✅');
+    console.log('[CesiumPatch] WebGL2RenderingContext:', patched2);
+    console.log('[CesiumPatch] WebGLRenderingContext:', patched1);
+  } else {
+    console.warn('[CesiumPatch] 无法找到 getActiveUniform 进行补丁');
+  }
+})();

+ 170 - 0
RuoYi-Vue3/src/cesium-clouds-atmosphere/loadBinThreeGeospatial.js

@@ -0,0 +1,170 @@
+/**
+ * 与 three-geospatial 完全一致的 .bin 加载方式:
+ * 使用 DataTextureLoader(Data3DTexture, parseUint8Array, options).load(url),
+ * 解析结果为 Three.js 的 Data3DTexture。解析阶段不涉及 Cesium。
+ *
+ * 参考:Clouds.tsx useLoad3DTexture、DataTextureLoader.ts
+ */
+import {
+  Data3DTexture,
+  FileLoader,
+  LinearFilter,
+  NoColorSpace,
+  RedFormat,
+  RepeatWrapping,
+} from "three";
+
+// --- 与 packages/core 一致:parseUint8Array、ArrayBufferLoader、TypedArrayLoader、DataTextureLoader 逻辑 ---
+
+const parseUint8Array = (buffer) => new Uint8Array(buffer);
+
+/** 使用 fetch 加载 ArrayBuffer(替代 Three.js FileLoader,避免 SuperMap Cesium 兼容问题) */
+async function fetchArrayBuffer(url) {
+  const resp = await fetch(url);
+  if (!resp.ok) throw new Error(`Failed to load ${url}: ${resp.status}`);
+  return resp.arrayBuffer();
+}
+
+/** 使用 fetch 加载并解析 TypedArray */
+async function loadTypedArray(url, parser) {
+  const buf = await fetchArrayBuffer(url);
+  return parser(buf);
+}
+
+/**
+ * 与 Clouds.tsx useLoad3DTexture 相同:DataTextureLoader(Data3DTexture, parseUint8Array, options).load(input)
+ * 返回 Promise<Data3DTexture>,解析仅在此完成,不涉及 Cesium。
+ */
+export async function loadBinThreeGeospatial(url, size) {
+  const texture = new Data3DTexture();
+  const options = {
+    width: size,
+    height: size,
+    depth: size,
+    format: RedFormat,
+    minFilter: LinearFilter,
+    magFilter: LinearFilter,
+    wrapS: RepeatWrapping,
+    wrapT: RepeatWrapping,
+    wrapR: RepeatWrapping,
+    colorSpace: NoColorSpace,
+  };
+  const array = await loadTypedArray(url, parseUint8Array);
+  texture.image.data = array;
+  texture.image.width = options.width;
+  texture.image.height = options.height;
+  texture.image.depth = options.depth;
+  texture.format = options.format;
+  texture.minFilter = options.minFilter;
+  texture.magFilter = options.magFilter;
+  texture.wrapS = options.wrapS;
+  texture.wrapT = options.wrapT;
+  texture.wrapR = options.wrapR;
+  texture.colorSpace = options.colorSpace;
+  texture.needsUpdate = true;
+  return texture;
+}
+
+/**
+ * 将已解析好的 Three.js Data3DTexture 绑定到 Cesium 上下文并返回可供着色器使用的纹理。
+ * 与 three-geospatial 一致使用单通道(RedFormat → Cesium PixelFormat.RED),直接传 data,不扩成 RGBA。
+ * 此处不做解析,仅做「解析结果 → 传给着色器」的绑定。Cesium 由调用方传入,避免全局依赖。
+ */
+export function bindData3DTextureToCesiumContext(viewer, data3DTexture, Cesium) {
+  const raw = data3DTexture.image.data;
+  const w = data3DTexture.image.width;
+  const h = data3DTexture.image.height;
+  const d = data3DTexture.image.depth;
+  return new Cesium.Texture3D({
+    context: viewer.scene.context,
+    width: w,
+    height: h,
+    depth: d,
+    pixelFormat: Cesium.PixelFormat.RED,
+    pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
+    flipY: false,
+    source: {
+      arrayBufferView: raw,
+      width: w,
+      height: h,
+      depth: d,
+    },
+    sampler: new Cesium.Sampler({
+      minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
+      magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
+      wrapS: Cesium.TextureWrap.REPEAT,
+      wrapT: Cesium.TextureWrap.REPEAT,
+      wrapR: Cesium.TextureWrap.REPEAT,
+    }),
+  });
+}
+
+/**
+ * 将 3D 体积数据打包成 2D 图集(按 slice 平铺),用于 Cesium PostProcessStage(仅支持 sampler2D)。
+ * 布局:depth 个 slice,每 slice 为 size×size,按 cols 列×rows 行平铺。
+ * @param {Uint8Array} data - size×size×size 的线性数据,索引 z*size*size + y*size + x
+ * @param {number} size - 每维尺寸(width=height=depth=size)
+ * @returns {{ data: Uint8Array, atlasWidth: number, atlasHeight: number, cols: number, rows: number, volumeSize: number }}
+ */
+export function pack3DTo2DAtlas(data, size) {
+  const depth = size;
+  const cols = Math.min(16, size);
+  const rows = Math.ceil(depth / cols);
+  const atlasWidth = cols * size;
+  const atlasHeight = rows * size;
+  const atlasData = new Uint8Array(atlasWidth * atlasHeight);
+
+  for (let z = 0; z < depth; z++) {
+    const col = z % cols;
+    const row = Math.floor(z / cols);
+    const baseAx = col * size;
+    const baseAy = row * size;
+    for (let y = 0; y < size; y++) {
+      for (let x = 0; x < size; x++) {
+        const i3 = z * size * size + y * size + x;
+        const ax = baseAx + x;
+        const ay = baseAy + y;
+        const i2 = ay * atlasWidth + ax;
+        atlasData[i2] = data[i3];
+      }
+    }
+  }
+
+  return { data: atlasData, atlasWidth, atlasHeight, cols, rows, volumeSize: size };
+}
+
+/**
+ * 加载 .bin 并转为 Cesium 可用的 2D 图集纹理(供 PostProcessStage 使用,因其不支持 Texture3D)。
+ * @param {object} viewer - Cesium Viewer
+ * @param {string} url - .bin 地址
+ * @param {number} size - 体积尺寸(如 128、32)
+ * @param {object} Cesium - Cesium 命名空间
+ * @returns {Promise<{ texture: Cesium.Texture, atlasCols: number, atlasRows: number, volumeSize: number }>}
+ */
+export async function loadBinAs2DAtlasForCesium(viewer, url, size, Cesium) {
+  const data3D = await loadBinThreeGeospatial(url, size);
+  const raw = data3D.image.data;
+  const { data, atlasWidth, atlasHeight, cols, rows, volumeSize } = pack3DTo2DAtlas(raw, size);
+
+  const texture = new Cesium.Texture({
+    context: viewer.scene.context,
+    width: atlasWidth,
+    height: atlasHeight,
+    pixelFormat: Cesium.PixelFormat.RED,
+    pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
+    flipY: true,
+    source: {
+      arrayBufferView: data,
+      width: atlasWidth,
+      height: atlasHeight,
+    },
+    sampler: new Cesium.Sampler({
+      minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
+      magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
+      wrapS: Cesium.TextureWrap.REPEAT,
+      wrapT: Cesium.TextureWrap.REPEAT,
+    }),
+  });
+
+  return { texture, atlasCols: cols, atlasRows: rows, volumeSize };
+}

+ 754 - 0
RuoYi-Vue3/src/cesium-clouds-atmosphere/simpleClouds.js

@@ -0,0 +1,754 @@
+/**
+ * 精简体积云 PostProcessStage(仅云层,无 Bruneton 大气)。
+ *
+ * 核心射线相交逻辑参考 three-geospatial / cesium-clouds-atmosphere-main,
+ * 适配 SuperMap Cesium(使用 2D 图集替代 sampler3D)。
+ *
+ * 关键改进:
+ *  1. 使用 4 半径 (bottom, minHeight, maxHeight, shadowTop) 计算射线交点
+ *  2. 加入地面交点检测,正确处理相机在云层下方时的射线区间
+ *  3. 使用 second 交点(外球远交点 = 从外进入云底的入口)跳跃到云层入口
+ *  4. 合理距离衰减参数 (11000/51000) 避免地面视角云被裁剪
+ *  5. 多散射近似 (approximateMultipleScattering) 增强体积感
+ */
+
+import { pack3DTo2DAtlas } from "./loadBinThreeGeospatial.js";
+
+// ─── 3D→2D 图集采样辅助 GLSL ─────────────────────────────────
+const ATLAS_SAMPLE_GLSL = `
+// 从 2D 图集中采样 3D 纹理(线性插值 z 层)
+vec4 sampleAtlas3D(sampler2D atlas, vec3 uvw, vec3 atlasInfo) {
+  float depth = atlasInfo.z;
+  float cols = atlasInfo.x;
+  float rows = atlasInfo.y;
+  float sliceF = uvw.z * (depth - 1.0);
+  float slice0 = floor(sliceF);
+  float fracZ = fract(sliceF);
+  float slice1 = min(slice0 + 1.0, depth - 1.0);
+  
+  vec2 tileUV = uvw.xy;
+  
+  // Slice 0
+  float c0 = mod(slice0, cols);
+  float r0 = floor(slice0 / cols);
+  vec2 atlasUV0 = (tileUV + vec2(c0, r0)) / vec2(cols, rows);
+  vec4 s0 = texture(atlas, atlasUV0);
+  
+  // Slice 1
+  float c1 = mod(slice1, cols);
+  float r1 = floor(slice1 / cols);
+  vec2 atlasUV1 = (tileUV + vec2(c1, r1)) / vec2(cols, rows);
+  vec4 s1 = texture(atlas, atlasUV1);
+  
+  return mix(s0, s1, fracZ);
+}
+
+float sampleAtlas3DNearest(sampler2D atlas, vec3 uvw, vec3 atlasInfo) {
+  float depth = atlasInfo.z;
+  float cols = atlasInfo.x;
+  float rows = atlasInfo.y;
+  float sliceF = uvw.z * (depth - 1.0);
+  float sliceIdx = floor(sliceF + 0.5);
+  sliceIdx = clamp(sliceIdx, 0.0, depth - 1.0);
+  
+  float c = mod(sliceIdx, cols);
+  float r = floor(sliceIdx / cols);
+  vec2 atlasUV = (uvw.xy + vec2(c, r)) / vec2(cols, rows);
+  return texture(atlas, atlasUV).r;
+}
+`;
+
+// ─── 云着色器 ──────────────────────────────────────────────────
+function getCloudFragmentShader() {
+  return /* glsl */ `
+out vec4 out_FragColor;
+const float RECIPROCAL_PI4 = 0.07957747154594767;
+const float EVOLUTION_SCALE = 2e4;
+
+uniform sampler2D colorTexture;
+uniform sampler2D depthTexture;
+
+// 所有纹理均使用 sampler2D(3D 纹理已打包为 2D 图集)
+uniform sampler2D u_shapeTexture;
+uniform sampler2D u_shapeDetailTexture;
+uniform sampler2D u_stbnTexture;
+uniform sampler2D u_weatherTexture;
+uniform sampler2D u_turbulenceTexture;
+uniform sampler2D u_blueNoise;
+
+// 图集元信息:x=cols, y=rows, z=depth
+uniform vec3 u_shapeAtlasInfo;
+uniform vec3 u_shapeDetailAtlasInfo;
+uniform vec3 u_stbnAtlasInfo;
+
+uniform float u_blueNoiseScale;
+uniform float u_jitterStrength;
+uniform vec3 u_cameraPosition;
+uniform float u_cameraHeight;
+uniform float u_bottomRadius;
+uniform float u_minHeight;
+uniform float u_maxHeight;
+uniform vec4 u_minLayerHeights;
+uniform vec4 u_maxLayerHeights;
+uniform vec4 u_densityScales;
+uniform vec4 u_shapeAmounts;
+uniform vec4 u_shapeDetailAmounts;
+uniform vec4 u_weatherExponents;
+uniform vec4 u_shapeAlteringBiases;
+uniform vec4 u_coverageFilterWidths;
+uniform float u_maxSteps;
+uniform float u_maxStepsToSun;
+uniform float u_minStepSize;
+uniform float u_maxStepSize;
+uniform float u_maxRayDistance;
+uniform float u_cameraNear;
+uniform float u_skyLightScale;
+uniform float u_weatherRepeat;
+uniform vec2 u_localWeatherOffset;
+uniform float u_shapeRepeat;
+uniform vec3 u_shapeOffset;
+uniform float u_shapeDetailRepeat;
+uniform vec3 u_shapeDetailOffset;
+uniform float u_turbulenceRepeat;
+uniform float u_turbulenceDisplacement;
+uniform vec4 u_coverages;
+uniform float u_scatteringCoefficient;
+uniform float u_absorptionCoefficient;
+uniform float u_scatterG1;
+uniform float u_scatterG2;
+uniform float u_scatterMix;
+uniform float u_sunIntensity;
+uniform float u_skyToSunRatio;
+uniform vec3 u_sunColor;
+uniform vec3 u_skyColor;
+uniform float u_powderScale;
+uniform float u_powderExponent;
+uniform float u_cloudExposure;
+uniform float u_edgeAlphaCutoff;
+uniform vec2 u_resolution;
+uniform float u_mipLevelScale;
+uniform float u_perspectiveStepScale;
+uniform float u_minDensity;
+uniform float u_minExtinction;
+uniform float u_minTransmittance;
+uniform float u_distFadeStart;
+uniform float u_distFadeEnd;
+uniform float u_minSecondaryStepSize;
+uniform float u_secondaryStepScale;
+uniform int u_multiScatteringOctaves;
+uniform float u_lowLayerDensityBoost;
+uniform vec4 u_densityProfileExpTerms;
+uniform vec4 u_densityProfileExponents;
+uniform vec4 u_densityProfileLinearTerms;
+uniform vec4 u_densityProfileConstantTerms;
+uniform vec3 u_minIntervalHeights;
+uniform vec3 u_maxIntervalHeights;
+uniform vec3 u_sunDirection;
+uniform float u_frame;
+uniform float u_debugMode;
+
+in vec2 v_textureCoordinates;
+
+${ATLAS_SAMPLE_GLSL}
+
+vec3 ACESFilmic(vec3 x) {
+  float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
+  return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
+}
+
+float saturate(float x) { return clamp(x, 0.0, 1.0); }
+vec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); }
+float remap(float v, float a, float b, float c, float d) { return c + (v - a) * (d - c) / (b - a); }
+float remapClamped(float v, float a, float b, float c, float d) { return clamp(remap(v, a, b, c, d), min(c, d), max(c, d)); }
+float remapClamped1(float v, float a, float b) { return clamp((v - a) / (b - a), 0.0, 1.0); }
+vec4 remapClampedVec(vec4 v, vec4 a, vec4 b, vec4 c, vec4 d) { return clamp(c + (v - a) * (d - c) / (b - a), min(c, d), max(c, d)); }
+vec4 remapClamped3(vec4 v, vec4 a, vec4 b) { return clamp((v - a) / (b - a), 0.0, 1.0); }
+
+vec3 reduceMagenta(vec3 color, float strength) {
+  float magenta = max(0.0, min(color.r, color.b) - color.g);
+  float fix = clamp(magenta * 5.0 * max(strength, 0.0), 0.0, 1.0);
+  color.r = mix(color.r, color.g, fix);
+  color.b = mix(color.b, color.g, fix);
+  return color;
+}
+
+void reconstructRay(out vec3 ro, out vec3 rd) {
+  ro = u_cameraPosition;
+  vec2 uv = v_textureCoordinates * 2.0 - 1.0;
+  // 标准 Cesium PostProcess 射线重建:使用投影矩阵的焦距分量
+  vec3 eyeDir = vec3(
+    czm_inverseProjection[0][0] * uv.x,
+    czm_inverseProjection[1][1] * uv.y,
+    -1.0
+  );
+  rd = normalize((czm_inverseView * vec4(eyeDir, 0.0)).xyz);
+}
+
+float getSTBN() {
+  ivec3 size = ivec3(128, 128, 64);
+  float cols = u_stbnAtlasInfo.x;
+  float rows = u_stbnAtlasInfo.y;
+  float depth = u_stbnAtlasInfo.z;
+  float sliceF = mod(u_frame, 64.0);
+  sliceF = clamp(sliceF, 0.0, depth - 1.0);
+  float sliceIdx = floor(sliceF + 0.5);
+  float c = mod(sliceIdx, cols);
+  float r = floor(sliceIdx / cols);
+  vec2 atlasUV = (gl_FragCoord.xy / u_resolution + vec2(c, r)) / vec2(cols, rows);
+  float mip = log2(max(u_resolution.x, u_resolution.y)) - 3.0;
+  return textureLod(u_stbnTexture, atlasUV, mip).r;
+}
+
+vec2 getGlobeUv(vec3 position) {
+  vec3 n = normalize(position);
+  return vec2(atan(n.y, n.x) * 0.159154943 + 0.5, asin(n.z) * 0.318309886 + 0.5);
+}
+
+float getMipLevel(vec2 uv) {
+  vec2 coord = uv * u_resolution;
+  vec2 ddx_v = dFdx(coord);
+  vec2 ddy_v = dFdy(coord);
+  float deltaMaxSqr = max(dot(ddx_v, ddx_v), dot(ddy_v, ddy_v)) * 0.1;
+  return max(0.0, 0.5 * log2(max(1.0, deltaMaxSqr)));
+}
+
+bool inEmptySpace(float height) {
+  bvec3 gt = greaterThan(vec3(height), u_minIntervalHeights);
+  bvec3 lt = lessThan(vec3(height), u_maxIntervalHeights);
+  return gt.x && lt.x || gt.y && lt.y || gt.z && lt.z;
+}
+
+vec4 getLayerDensity(vec4 hf) {
+  return u_densityProfileExpTerms * exp(u_densityProfileExponents * hf)
+       + u_densityProfileLinearTerms * hf
+       + u_densityProfileConstantTerms;
+}
+
+vec4 getHeightFractions(float height) {
+  vec4 range = u_maxLayerHeights - u_minLayerHeights;
+  return clamp((vec4(height) - u_minLayerHeights) / max(range, vec4(0.0001)), 0.0, 1.0);
+}
+
+vec4 shapeAlteringFunction(vec4 hf, vec4 bias) {
+  vec4 biased = pow(hf, bias);
+  vec4 x = clamp(biased * 2.0 - 1.0, -1.0, 1.0);
+  return 1.0 - x * x;
+}
+
+// ── 天气采样 ────────────────────────────────────────────────────
+vec4 sampleWeather(vec2 uv, float height, float mipLevel) {
+  vec4 hf = getHeightFractions(height);
+  vec2 wUv = uv * u_weatherRepeat + u_localWeatherOffset;
+  vec4 localW = pow(textureLod(u_weatherTexture, wUv, mipLevel).rgba, u_weatherExponents);
+  vec4 hs = shapeAlteringFunction(hf, u_shapeAlteringBiases);
+  vec4 factor = 1.0 - u_coverages * hs;
+  return remapClamped3(mix(localW, vec4(1.0), u_coverageFilterWidths), factor, factor + u_coverageFilterWidths);
+}
+
+float henyeyGreenstein(float g, float cosTheta) {
+  float g2 = g * g;
+  return RECIPROCAL_PI4 * (1.0 - g2) / pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5);
+}
+
+float phaseFunction(float cosTheta) {
+  return mix(henyeyGreenstein(u_scatterG1, cosTheta),
+             henyeyGreenstein(u_scatterG2, cosTheta), u_scatterMix);
+}
+
+// ── 参考 three-geospatial:4 半径射线交点 ──────────────────────
+void raySphereIntersections(vec3 origin, vec3 direction, vec4 radius, out vec4 i1, out vec4 i2) {
+  float b = 2.0 * dot(direction, origin);
+  vec4 c = dot(origin, origin) - radius * radius;
+  vec4 disc = b * b - 4.0 * c;
+  vec4 mask = step(disc, vec4(0.0));
+  vec4 Q = sqrt(max(vec4(0.0), disc));
+  i1 = mix((-b - Q) * 0.5, vec4(-1.0), mask);
+  i2 = mix((-b + Q) * 0.5, vec4(-1.0), mask);
+}
+
+void getIntersections(vec3 camPos, vec3 rd, out vec4 first, out vec4 second) {
+  vec4 radii = u_bottomRadius + vec4(0.0, u_minHeight, u_maxHeight, u_maxHeight);
+  raySphereIntersections(camPos, rd, radii, first, second);
+}
+
+vec2 getRayNearFar(vec4 first, vec4 second) {
+  vec2 nearFar = vec2(-1.0);
+  if (u_cameraHeight < u_minHeight) {
+    // 云层下方:从云底入口到云顶出口
+    nearFar = vec2(second.y, second.z);
+    nearFar.y = min(nearFar.y, u_maxRayDistance);
+  } else if (u_cameraHeight < u_maxHeight) {
+    // 云层内部:从近裁面到最远出口
+    float farExit = max(max(first.y, second.y), max(first.z, second.z));
+    if (farExit <= 0.0) farExit = u_maxRayDistance;
+    farExit = min(farExit, u_maxRayDistance);
+    farExit = max(farExit, u_cameraNear + u_minStepSize * 0.5);
+    nearFar = vec2(u_cameraNear, farExit);
+  } else {
+    // 云层上方:从云顶入口到最远出口
+    float nearEntry = first.z;
+    if (nearEntry > 0.0) {
+      float farExit = max(max(first.y, second.y), max(first.z, second.z));
+      farExit = min(farExit, u_maxRayDistance);
+      farExit = max(farExit, nearEntry + u_minStepSize * 0.5);
+      nearFar = vec2(nearEntry, farExit);
+    }
+  }
+  return nearFar;
+}
+
+// ── 多散射近似 ──────────────────────────────────────────────────
+float approximateMultipleScattering(float opticalDepth, float cosTheta) {
+  vec3 coeffs = vec3(1.0);
+  const vec3 attenuation = vec3(0.5);
+  float scattering = 0.0;
+  for (int i = 0; i < 12; i++) {
+    if (i >= u_multiScatteringOctaves) break;
+    scattering += coeffs.x * exp(-opticalDepth * coeffs.y) * phaseFunction(cosTheta);
+    coeffs *= attenuation;
+  }
+  return scattering;
+}
+
+// ── 光线步进到太阳方向计算光学深度 ────────────────────────────
+float marchOpticalDepthToSun(vec3 rayOrigin, vec3 rayDirection, float mipLevel, float jitter) {
+  // 高分辨率(mip=0)时步数多,低分辨率(mip=1)时步数少
+  float iterCount = max(0.0, mix(float(u_maxStepsToSun) + 1.0, 1.0, clamp(mipLevel, 0.0, 1.0)) - jitter);
+  int ic = int(iterCount);
+  if (ic == 0) return 0.5;
+  float stepSize = u_minSecondaryStepSize / max(iterCount, 1.0);
+  float nextDist = stepSize * jitter;
+  float od = 0.0;
+  for (int i = 0; i < 8; i++) {
+    if (i >= ic) break;
+    vec3 pos = rayDirection * nextDist + rayOrigin;
+    vec2 uv = getGlobeUv(pos);
+    float h = length(pos) - u_bottomRadius;
+    vec4 weather = sampleWeather(uv, h, mipLevel);
+    if (any(greaterThan(weather, vec4(u_minDensity)))) {
+      vec3 sn = normalize(pos);
+      vec3 evolution = -sn * length(u_localWeatherOffset) * EVOLUTION_SCALE;
+      vec2 tUv = uv * u_weatherRepeat * u_turbulenceRepeat;
+      vec3 turb = u_turbulenceDisplacement * (texture(u_turbulenceTexture, tUv).rgb * 2.0 - 1.0)
+          * dot(weather, remapClampedVec(getHeightFractions(h), vec4(0.3), vec4(0.0), vec4(0.0), vec4(1.0)));
+      vec3 sp = (pos + evolution + turb) * u_shapeRepeat + u_shapeOffset;
+      float shapeTex = sampleAtlas3D(u_shapeTexture, fract(sp), u_shapeAtlasInfo).r;
+      vec4 density = remapClamped3(weather, vec4(1.0 - shapeTex) * u_shapeAmounts, vec4(1.0));
+      density = saturate(density * u_densityScales * getLayerDensity(getHeightFractions(h)));
+      float ds = density.x + density.y + density.z + density.w;
+      float extinction = ds * u_absorptionCoefficient + ds * u_scatteringCoefficient;
+      od += extinction * stepSize;
+    }
+    nextDist += stepSize;
+    stepSize *= u_secondaryStepScale;
+  }
+  return od;
+}
+
+void main() {
+  // [强制测试] 输出红色验证 PostProcessStage 是否在执行
+  // out_FragColor = vec4(1.0, 0.0, 0.0, 1.0); return;
+
+  vec4 sceneColor = texture(colorTexture, v_textureCoordinates);
+  float depth = czm_readDepth(depthTexture, v_textureCoordinates);
+  vec3 ro, rd;
+  reconstructRay(ro, rd);
+
+  float jitter = getSTBN();
+  vec3 sunDir = normalize(u_sunDirection);
+
+  // ── 计算射线云层交点 ────────────────────────────────────
+  vec4 first, second;
+  getIntersections(ro, rd, first, second);
+  vec2 rayNearFar = getRayNearFar(first, second);
+
+  // ── 调试模式 ──────────────────────────
+  if (u_debugMode > 0.5) {
+    // 用 reconstructRay 重建的射线测试球体求交
+    // 红色 = first.z>0(云顶入口), 绿色 = second.y>0(云底出口)
+    float hasTop = step(0.0, first.z);
+    float hasBottom = step(0.0, second.y);
+    out_FragColor = mix(sceneColor, vec4(hasTop, hasBottom, 0.0, 1.0), 0.8);
+    return;
+  }
+  
+  if (rayNearFar.x < 0.0 || rayNearFar.y <= rayNearFar.x + 1.0) {
+    out_FragColor = sceneColor;
+    return;
+  }
+  
+  float tMin = rayNearFar.x;
+  float tMax = rayNearFar.y;
+
+  // ── 深度截断 ────────────────────────────────────────────
+  float rayDistToScene = 0.0;
+  if (depth < 1.0 - 1e-7) {
+    vec4 eyePos = czm_windowToEyeCoordinates(vec4(gl_FragCoord.xy, depth, 1.0));
+    if (abs(eyePos.w) > 1e-6) {
+      eyePos /= eyePos.w;
+      if (eyePos.z < 0.0) {
+        vec4 worldPos4 = czm_inverseView * eyePos;
+        rayDistToScene = max(0.0, dot(worldPos4.xyz - ro, rd));
+      }
+    }
+  }
+  
+  const float DEPTH_SKY = 1.0 - 1e-7;
+  bool skipDepthClamp = (depth >= DEPTH_SKY);
+  if (rayDistToScene > 0.0 && !skipDepthClamp) {
+    tMax = min(tMax, rayDistToScene);
+  }
+  if (tMin >= tMax) {
+    out_FragColor = sceneColor;
+    return;
+  }
+
+  float maxDist = min(tMax - tMin, u_maxRayDistance);
+  float stepSize = u_minStepSize;
+  float rayDist = stepSize * jitter * 2.0;
+
+  vec3 radInt = vec3(0.0);
+  float transInt = 1.0;
+  float cosTheta = dot(rd, sunDir);
+  vec3 sunColor = u_sunColor * u_sunIntensity;
+  vec3 skyColor = u_skyColor * u_sunIntensity * u_skyToSunRatio;
+
+  // ── 主光线步进循环 ──────────────────────────────────────
+  for (int i = 0; i < 512; i++) {
+    if (float(i) >= u_maxSteps) break;
+    if (rayDist > maxDist) break;
+    if (transInt <= u_minTransmittance) break;
+
+    vec3 pos = ro + rd * (tMin + rayDist);
+    float height = length(pos) - u_bottomRadius;
+
+    if (inEmptySpace(height)) {
+      stepSize *= u_perspectiveStepScale;
+      rayDist += min(stepSize, u_maxStepSize);
+      continue;
+    }
+
+    vec2 uv = getGlobeUv(pos);
+    float mipLevel = getMipLevel(uv * u_weatherRepeat) * u_mipLevelScale;
+    mipLevel = mix(0.0, mipLevel, min(1.0, 0.2 * u_cameraHeight / max(u_maxHeight, 1.0)));
+
+    vec4 weather = sampleWeather(uv, height, mipLevel);
+    if (!any(greaterThan(weather, vec4(u_minDensity)))) {
+      stepSize *= u_perspectiveStepScale;
+      rayDist += min(stepSize, u_maxStepSize);
+      continue;
+    }
+
+    weather.xy *= u_lowLayerDensityBoost;
+
+    // 采样形状噪声
+    vec3 sn = normalize(pos);
+    vec3 evolution = -sn * length(u_localWeatherOffset) * EVOLUTION_SCALE;
+    vec2 tUv = uv * u_weatherRepeat * u_turbulenceRepeat;
+    vec3 turb = u_turbulenceDisplacement * (texture(u_turbulenceTexture, tUv).rgb * 2.0 - 1.0)
+        * dot(weather, remapClampedVec(getHeightFractions(height), vec4(0.3), vec4(0.0), vec4(0.0), vec4(1.0)));
+    vec3 sp = (pos + evolution + turb) * u_shapeRepeat + u_shapeOffset;
+    float shapeTex = sampleAtlas3D(u_shapeTexture, fract(sp), u_shapeAtlasInfo).r;
+    vec4 density = remapClamped3(weather, vec4(1.0 - shapeTex) * u_shapeAmounts, vec4(1.0));
+
+    if (mipLevel * 0.5 + (jitter - 0.5) * 0.5 < 0.5) {
+      vec3 dp = (pos + turb) * u_shapeDetailRepeat + u_shapeDetailOffset;
+      float detail = sampleAtlas3D(u_shapeDetailTexture, dp, u_shapeDetailAtlasInfo).r;
+      vec4 modifier = mix(vec4(pow(detail, 6.0)), vec4(1.0 - detail),
+          remapClampedVec(getHeightFractions(height), vec4(0.2), vec4(0.4), vec4(0.0), vec4(1.0)));
+      modifier = mix(vec4(0.0), modifier, u_shapeDetailAmounts);
+      density = remapClamped3(density * 2.0, vec4(modifier * 0.5), vec4(1.0));
+    }
+
+    density = saturate(density * u_densityScales * getLayerDensity(getHeightFractions(height)));
+    float ds = density.x + density.y + density.z + density.w;
+    float scattering = ds * u_scatteringCoefficient;
+    float extinction = ds * u_absorptionCoefficient + scattering;
+
+    if (extinction > u_minExtinction) {
+      float heightAlpha = clamp((height - u_minHeight) / max(u_maxHeight - u_minHeight, 1.0), 0.0, 1.0);
+      vec3 colSun = mix(sunColor * 0.85, sunColor, heightAlpha);
+      vec3 colSky = mix(skyColor * 0.6, skyColor, heightAlpha);
+
+      // 多散射近似增强体积感
+      float opticalDepthToSun = marchOpticalDepthToSun(pos, sunDir, mipLevel, jitter);
+      float multiScat = approximateMultipleScattering(opticalDepthToSun, cosTheta);
+
+      // 引入 heightFraction 加权天空光梯度,增强体积感
+      float skyGradient = dot(getHeightFractions(height) * 0.5 + 0.5, density / max(ds, 1e-7));
+      vec3 radiance = colSun * multiScat + colSky * RECIPROCAL_PI4 * skyGradient * u_skyLightScale;
+      radiance *= scattering * (1.0 - u_powderScale * exp(-extinction * u_powderExponent));
+
+      float transmittance = exp(-extinction * stepSize);
+      vec3 scatInt = (radiance - radiance * transmittance) / max(extinction, 1e-7);
+      radInt += transInt * scatInt;
+      transInt *= transmittance;
+    }
+
+    stepSize *= u_perspectiveStepScale;
+    rayDist += stepSize;
+  }
+
+  // ── 远处云透明度距离衰减 ──────────────────────────────
+  // 用"相机到云层入口距离"(tMin)衰减,而非云内穿行厚度。
+  // 天顶云入口近不衰减,天际线云入口远衰减——只压远处透明度,不影响各层密度。
+  // 这解决斜射时云层路径长导致 alpha 堆积的问题。
+  float entryFade = 1.0 - smoothstep(u_distFadeStart, u_distFadeEnd, tMin);
+  float cloudAlpha = saturate(remapClamped1(transInt, 1.0, u_minTransmittance)) * entryFade;
+  vec3 cloudRad = radInt * entryFade;
+
+  // 色调映射
+  vec3 cloudColor = ACESFilmic(cloudRad * u_cloudExposure);
+  cloudColor = pow(cloudColor, vec3(1.0 / 2.2));
+
+  // 边缘裁剪
+  if (cloudAlpha < u_edgeAlphaCutoff) {
+    out_FragColor = sceneColor;
+    return;
+  }
+
+  // 合成
+  vec3 finalColor = sceneColor.rgb * (1.0 - cloudAlpha) + cloudColor * cloudAlpha;
+  finalColor = reduceMagenta(finalColor, 2.0);
+
+  out_FragColor = vec4(finalColor, 1.0);
+}
+`;
+}
+
+// ─── 加载 .bin 为 2D 图集纹理 ────────────────────────────────
+async function loadBinAsAtlas(viewer, url, size, Cesium) {
+  const resp = await fetch(url);
+  if (!resp.ok) throw new Error(`Failed to load ${url}: ${resp.status}`);
+  const buffer = await resp.arrayBuffer();
+  const raw = new Uint8Array(buffer);
+  const { data, atlasWidth, atlasHeight, cols, rows, volumeSize } = pack3DTo2DAtlas(raw, size);
+
+  const rgbaData = new Uint8Array(atlasWidth * atlasHeight * 4);
+  for (let i = 0; i < atlasWidth * atlasHeight; i++) {
+    rgbaData[i * 4] = data[i];
+    rgbaData[i * 4 + 1] = data[i];
+    rgbaData[i * 4 + 2] = data[i];
+    rgbaData[i * 4 + 3] = 255;
+  }
+
+  const tex = new Cesium.Texture({
+    context: viewer.scene.context,
+    width: atlasWidth,
+    height: atlasHeight,
+    pixelFormat: Cesium.PixelFormat.RGBA,
+    pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
+    flipY: true,
+    source: {
+      arrayBufferView: rgbaData,
+      width: atlasWidth,
+      height: atlasHeight,
+    },
+    sampler: new Cesium.Sampler({
+      minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
+      magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
+      wrapS: Cesium.TextureWrap.REPEAT,
+      wrapT: Cesium.TextureWrap.REPEAT,
+    }),
+  });
+
+  return { texture: tex, cols, rows, depth: volumeSize };
+}
+
+async function load2DTexture(viewer, url, Cesium) {
+  const img = await Cesium.Resource.fetchImage(url);
+  return new Cesium.Texture({
+    context: viewer.scene.context,
+    source: img,
+    sampler: new Cesium.Sampler({
+      minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
+      magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
+      wrapS: Cesium.TextureWrap.REPEAT,
+      wrapT: Cesium.TextureWrap.REPEAT,
+    }),
+  });
+}
+
+// ─── 初始化 ───────────────────────────────────────────────────
+export async function addSimpleClouds(viewer, options = {}) {
+  const Cesium = window.Cesium;
+  const base = '/cesium-clouds-assets/clouds-assets/';
+
+  // 可外部控制的云参数(通过 options 传入或修改 stage.cloudParams)
+  const params = {
+    show: true,
+    minHeight: 1800,
+    maxHeight: 8000,
+    densityScale: 1.0,
+    coverage: 0.3,
+    sunIntensity: 50.0,
+    cloudExposure: 4.0,
+    skyLightScale: 1.0,
+    powderScale: 0.8,
+    // 阳光与天光颜色(随时间变化,白天/日落/夜晚)
+    sunColor: [1.0, 0.95, 0.9],
+    skyColor: [0.4, 0.6, 1.0],
+    // 云层缓慢漂移速度(天气纹理 UV/秒,越小越慢,0 表示静止)
+    driftSpeed: 0.0005,
+    // 云体形态漂移倍率(相对 driftSpeed,决定肉眼可见的云层运动速度)
+    shapeDriftScale: 30,
+  };
+  Object.assign(params, options);
+
+  // 云层漂移动画的起始时间(真实时间驱动,保证持续缓慢运动)
+  const driftStart = performance.now();
+
+  const [shapeAtlas, detailAtlas, stbnAtlas, weatherTex, turbTex, noiseTex] = await Promise.all([
+    loadBinAsAtlas(viewer, base + 'shape.bin', 128, Cesium).catch(e => { console.warn('[Clouds] shape 加载失败:', e); return null; }),
+    loadBinAsAtlas(viewer, base + 'shape_detail.bin', 32, Cesium).catch(e => { console.warn('[Clouds] detail 加载失败:', e); return null; }),
+    loadBinAsAtlas(viewer, base + 'stbn.bin', 128, Cesium).catch(e => { console.warn('[Clouds] stbn 加载失败:', e); return null; }),
+    load2DTexture(viewer, base + 'local_weather.png', Cesium).catch(e => { console.warn('[Clouds] weather 加载失败:', e); return null; }),
+    load2DTexture(viewer, base + 'turbulence.png', Cesium).catch(e => { console.warn('[Clouds] turbulence 加载失败:', e); return null; }),
+    load2DTexture(viewer, '/cesium-clouds-assets/data/noisePic/noisergba256.png', Cesium).catch(e => { console.warn('[Clouds] blueNoise 加载失败:', e); return null; }),
+  ]);
+
+  if (!shapeAtlas) {
+    console.warn('[Clouds] 缺少核心纹理 shape.bin,跳过云初始化');
+    return null;
+  }
+
+  const fragmentShader = `precision highp float;\n${getCloudFragmentShader()}`;
+
+  const uniforms = {
+    u_shapeTexture: () => shapeAtlas.texture,
+    u_shapeDetailTexture: () => detailAtlas?.texture || shapeAtlas.texture,
+    u_stbnTexture: () => stbnAtlas?.texture || shapeAtlas.texture,
+    u_weatherTexture: () => weatherTex || shapeAtlas.texture,
+    u_turbulenceTexture: () => turbTex || shapeAtlas.texture,
+    u_blueNoise: () => noiseTex || shapeAtlas.texture,
+    u_shapeAtlasInfo: () => new Cesium.Cartesian3(shapeAtlas.cols, shapeAtlas.rows, shapeAtlas.depth),
+    u_shapeDetailAtlasInfo: () => detailAtlas ? new Cesium.Cartesian3(detailAtlas.cols, detailAtlas.rows, detailAtlas.depth) : new Cesium.Cartesian3(1, 1, 1),
+    u_stbnAtlasInfo: () => stbnAtlas ? new Cesium.Cartesian3(stbnAtlas.cols, stbnAtlas.rows, stbnAtlas.depth) : new Cesium.Cartesian3(1, 1, 1),
+    u_blueNoiseScale: () => 1.0,
+    u_jitterStrength: () => 1.0,
+    u_cameraPosition: () => viewer.camera.positionWC,
+    u_cameraHeight: () => {
+      // 用 ECEF 距离减去赤道半径,地面≈0
+      const pos = viewer.camera.positionWC;
+      return Math.max(0, Cesium.Cartesian3.magnitude(pos) - 6378137);
+    },
+    u_bottomRadius: () => 6378137,
+    u_minHeight: () => params.minHeight,
+    u_maxHeight: () => params.maxHeight,
+
+    // 云层参数(参考 three-geospatial 层结构)
+    u_minLayerHeights: () => new Cesium.Cartesian4(params.minHeight, params.minHeight + 600, params.maxHeight - 500, 0),
+    u_maxLayerHeights: () => new Cesium.Cartesian4(params.minHeight + 650, params.minHeight + 1800, params.maxHeight, 0),
+    u_densityScales: () => {
+      const s = params.densityScale;
+      return new Cesium.Cartesian4(0.01 * s, 0.01 * s, 0.0005 * s, 0);
+    },
+    u_shapeAmounts: () => new Cesium.Cartesian4(0.7, 0.7, 0.3, 0),
+    u_shapeDetailAmounts: () => new Cesium.Cartesian4(0.8, 0.8, 0.0, 0),
+    u_weatherExponents: () => new Cesium.Cartesian4(1.0, 1.0, 1.0, 1.0),
+    u_shapeAlteringBiases: () => new Cesium.Cartesian4(0.35, 0.35, 0.35, 0.35),
+    u_coverageFilterWidths: () => new Cesium.Cartesian4(0.6, 0.6, 0.5, 0),
+    u_coverages: () => new Cesium.Cartesian4(params.coverage, params.coverage, Math.min(0.2, params.coverage), 0),
+
+    // 密度剖面
+    u_densityProfileExpTerms: () => Cesium.Cartesian4.ZERO,
+    u_densityProfileExponents: () => Cesium.Cartesian4.ZERO,
+    u_densityProfileLinearTerms: () => new Cesium.Cartesian4(0.75, 0.75, 0.75, 0.25),
+    u_densityProfileConstantTerms: () => new Cesium.Cartesian4(0.25, 0.25, 0.25, 0.25),
+
+    // 禁用 inEmptySpace:依赖天气纹理和形状噪声自然塑造云形态
+    u_minIntervalHeights: () => Cesium.Cartesian3.ZERO,
+    u_maxIntervalHeights: () => Cesium.Cartesian3.ZERO,
+
+    // 步进参数(参考 three-geospatial)
+    u_maxSteps: () => 500,
+    u_maxStepsToSun: () => 8,
+    u_minStepSize: () => 20.0,
+    u_maxStepSize: () => 5000.0,
+    u_maxRayDistance: () => 1000000.0,
+    u_perspectiveStepScale: () => 1.01,
+
+    // 距离衰减——基于相机到云层入口距离,远处透明度线性衰减
+    u_distFadeStart: () => 50000.0,
+    u_distFadeEnd: () => 300000.0,
+
+    u_cameraNear: () => viewer.camera.frustum?.near || 1,
+    u_skyLightScale: () => params.skyLightScale,
+    u_weatherRepeat: () => 100.0,
+    // 天气纹理缓慢偏移 → 云层整体缓慢漂移(主运动)
+    u_localWeatherOffset: () => {
+      const t = (performance.now() - driftStart) / 1000;
+      return new Cesium.Cartesian2(t * params.driftSpeed, t * params.driftSpeed * 0.3);
+    },
+    u_shapeRepeat: () => 3.0 / 1e4,
+    // 形状噪声缓慢偏移 → 云体形态可见漂移(主运动,与天气偏移方向一致)
+    u_shapeOffset: () => {
+      const t = (performance.now() - driftStart) / 1000;
+      const s = t * params.driftSpeed * params.shapeDriftScale;
+      return new Cesium.Cartesian3(s, s * 0.3, 0);
+    },
+    u_shapeDetailRepeat: () => 0.0005,
+    u_shapeDetailOffset: () => Cesium.Cartesian3.ZERO,
+    u_turbulenceRepeat: () => 2.0,
+    u_turbulenceDisplacement: () => 400.0,
+
+    // 光照和散射
+    u_scatteringCoefficient: () => 1.0,
+    u_absorptionCoefficient: () => 0.0,
+    u_scatterG1: () => 0.8,
+    u_scatterG2: () => -0.1,
+    u_scatterMix: () => 0.8,
+    u_sunIntensity: () => params.sunIntensity,
+    u_skyToSunRatio: () => 0.4,
+    u_powderScale: () => params.powderScale,
+    u_sunColor: () => new Cesium.Cartesian3(params.sunColor[0], params.sunColor[1], params.sunColor[2]),
+    u_skyColor: () => new Cesium.Cartesian3(params.skyColor[0], params.skyColor[1], params.skyColor[2]),
+    u_powderExponent: () => 150.0,
+    u_cloudExposure: () => params.cloudExposure,
+    u_edgeAlphaCutoff: () => 0.005,
+
+    u_resolution: () => new Cesium.Cartesian2(
+      viewer.scene.context.drawingBufferWidth || 1,
+      viewer.scene.context.drawingBufferHeight || 1
+    ),
+    u_mipLevelScale: () => 0.8,
+    u_minDensity: () => 1e-5,
+    u_minExtinction: () => 1e-5,
+    u_minTransmittance: () => 0.01,
+    u_minSecondaryStepSize: () => 100.0,
+    u_secondaryStepScale: () => 2.0,
+    u_multiScatteringOctaves: () => 8,
+    u_lowLayerDensityBoost: () => 1.0,
+
+    u_sunDirection: () => {
+      const s = viewer.scene.context.uniformState.sunDirectionWC;
+      return s || new Cesium.Cartesian3(1, 0, 0);
+    },
+    u_frame: () => Math.floor((performance.now() - driftStart) / 1000) % 64,
+    u_debugMode: () => 0, // 设为 1 开启调试模式:红=云顶入口,绿=云底出口,黄=两者都有
+  };
+
+  const stage = new Cesium.PostProcessStage({
+    name: 'SimpleClouds',
+    fragmentShader,
+    uniforms,
+  });
+  stage.enabled = params.show;
+  // 暴露参数对象,便于外部组件控制
+  stage.cloudParams = params;
+
+  viewer.scene.postProcessStages.add(stage);
+
+  console.log('[Clouds] 体积云 PostProcessStage 已添加 ✅');
+  const wgsH = viewer.camera.positionCartographic?.height?.toFixed(0) || '?';
+  const sphereH = uniforms.u_cameraHeight().toFixed(0);
+  const ecefPos = viewer.camera.positionWC;
+  const ecefMag = Cesium.Cartesian3.magnitude(ecefPos).toFixed(0);
+  console.log('[Clouds] WGS84高度:', wgsH + 'm, ECEF半径:', ecefMag + 'm');
+  console.log('[Clouds] 球体参考高度(bottomR=6371860):', sphereH + 'm (与WGS84差~' + (uniforms.u_cameraHeight() - parseFloat(wgsH || '0')).toFixed(0) + 'm)');
+  console.log('[Clouds] 云层: minHeight=1800m, maxHeight=8000m, 三层: 1800-2450/2400-3600/7500-8000');
+  console.log('[Clouds] 密度:', uniforms.u_densityScales().toString(), ', boost:', uniforms.u_lowLayerDensityBoost());
+  console.log('[Clouds] 覆盖度:', uniforms.u_coverages().toString(), ', 散射:', uniforms.u_scatteringCoefficient());
+  console.log('[Clouds] ⚠ 调试模式已开启: 绿=云有效 橙=无交点 蓝=地面 亮度/色调反映高度');
+  return stage;
+}

+ 6 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/components.js

@@ -40,6 +40,8 @@ import modelFlood from "./layer/model-flood/index.js"
 import photography from "./layer/oblique-photography/index.js"
 import imglayerAttribute from "./layer/imglayer-attribute/index.js"
 import sceneAttribute from "./scene/scene-attribute/index.js"
+import timeSlider from "./scene/time-slider/index.js"
+import weatherSelect from "./scene/weather-select/index.js"
 import facade from "./scene/facade/index.js"
 import compass from "./scene/compass/index.js"
 import flyRoute from "./fly/fly-route/index.js"
@@ -54,6 +56,7 @@ import projection from "./scene/projection-image/index.js"
 import pickCoordinate from "./scene/pick-coordinate/index.js"
 import scanEffect from "./special-effects/scan-effect/index.js"
 import weather from "./special-effects/weather/index.js"
+import volumetricCloud from "./special-effects/volumetric-cloud/index.js"
 import geologicalBody from "./model/geological-body/index.js"
 import volume from "./scene/volume-render/index.js"
 import particle from "./special-effects/particle-system/index.js"
@@ -92,6 +95,8 @@ const components = [
     photography,
     imglayerAttribute,
     sceneAttribute,
+    timeSlider,
+    weatherSelect,
     facade,
     compass,
     flyRoute,
@@ -104,6 +109,7 @@ const components = [
     pickCoordinate,
     scanEffect,
     weather,
+    volumetricCloud,
     geologicalBody,
     volume,
     particle,

+ 124 - 102
RuoYi-Vue3/src/supermap-cesium-module/components/scene/scene-attribute/scene-attribute.js

@@ -3,6 +3,7 @@ import { watch, ref, reactive, toRefs, onBeforeUnmount, onMounted, nextTick } fr
 import { storeState } from '../../../js/store/store.js'   
 import tool from '../../../js/tool/tool.js'        
 import resource from '../../../js/local/lang.js'  
+import { applyDayPeriod } from '../../../js/common/dayPeriod.js'
 
 function s3mlayerAttribute(props) {
     const viewer = window.viewer;
@@ -15,11 +16,11 @@ function s3mlayerAttribute(props) {
         earthShow: true,
         shadows: false,
         cloud: false,
-        skyBox: true,
+        skyBox: false,
         sunShow: false,
         timeline: false,
         depthAgainst: true,
-        atomsphereRender: false,
+        atomsphereRender: true,
         fogEffect: true,
         underground: true,
         frameRate: false,
@@ -92,6 +93,7 @@ function s3mlayerAttribute(props) {
 
     let cloudBox = new Cesium.CloudBox({ url: state.cloudsUrl });
     let skyboxs = {}, defaultSkybox;
+    let initialRainParams = null, initialSnowParams = null;
     let savedRainParams = null, savedSnowParams = null;
     for (let key in state.skyboxSources) {
         let skybox = new Cesium.SkyBox({ sources: state.skyboxSources[key] });
@@ -115,7 +117,6 @@ function s3mlayerAttribute(props) {
         if (state.skyBox) {
             let h = viewer.scene.camera.positionCartographic.height;
             viewer.scene.skyBox.show = h < 22e4;
-            state.atomsphereRender = h >= 22e4;
         }
     }
 
@@ -126,7 +127,8 @@ function s3mlayerAttribute(props) {
         if (!viewer) return;
 
         viewer.scene.sun.show = state.sunShow;
-        viewer.scene.moon.show = true;
+        // 关闭月亮显示,避免调节时间时天上出现圆形贴图
+        viewer.scene.moon.show = false;
         viewer.scene.globe.enableLighting = state.sunShow;
 
         // 设置时间轴为今天的00:00到24:00
@@ -149,68 +151,28 @@ function s3mlayerAttribute(props) {
         viewer.clock.multiplier = 1;
         viewer.clock.shouldAnimate = false;
 
-        // 监听时间变化,更新天空效果
+        // 监听时间变化,仅在时钟动画播放时自动更新天空效果
         viewer.clock.onTick.addEventListener(function(clock) {
-            updateSkyEffect(clock.currentTime);
+            if (viewer.clock.shouldAnimate) {
+                updateSkyEffect(clock.currentTime);
+            }
             state.currentTime = Cesium.JulianDate.toDate(clock.currentTime);
         });
     }
 
-    // 根据时间更新天空效果
+    // ==============================================
+    // 全天时段效果系统已抽取到 js/common/dayPeriod.js,
+    // 供场景属性面板与地图右上角时间滑块共用
+    // ==============================================
+
+    // 根据时间更新天空效果(大气 + 雾 + 体积云一整套)
     function updateSkyEffect(currentTime) {
         if (!viewer) return;
-
-        // 获取当前时间的小时数(北京时间)
+        // 小时数(含分钟,保证平滑过渡)
         const date = Cesium.JulianDate.toDate(currentTime);
-        const hour = date.getHours();
-
-        // 根据小时数设置不同的天空效果
-        if (hour >= 0 && hour < 3) {
-            // 凌晨00点-3点:漆黑
-            viewer.scene.skyAtmosphere.hueShift = -0.3;
-            viewer.scene.skyAtmosphere.saturationShift = -0.6;
-            viewer.scene.skyAtmosphere.brightnessShift = -0.7;
-        } else if (hour >= 3 && hour < 6) {
-            // 凌晨3点-6点:天空逐渐转亮
-            const progress = (hour - 3) / 3;
-            viewer.scene.skyAtmosphere.hueShift = -0.3 + progress * 0.3;
-            viewer.scene.skyAtmosphere.saturationShift = -0.6 + progress * 0.6;
-            viewer.scene.skyAtmosphere.brightnessShift = -0.7 + progress * 0.7;
-        } else if (hour >= 6 && hour < 8) {
-            // 早上6点-8点:日出
-            const progress = (hour - 6) / 2;
-            viewer.scene.skyAtmosphere.hueShift = 0 + progress * 0.05;
-            viewer.scene.skyAtmosphere.saturationShift = 0 + progress * 0.15;
-            viewer.scene.skyAtmosphere.brightnessShift = 0 + progress * 0.15;
-        } else if (hour >= 8 && hour < 12) {
-            // 早上8点-12点:上午
-            const progress = (hour - 8) / 4;
-            viewer.scene.skyAtmosphere.hueShift = 0.05 + progress * 0.05;
-            viewer.scene.skyAtmosphere.saturationShift = 0.15 + progress * 0.05;
-            viewer.scene.skyAtmosphere.brightnessShift = 0.15 + progress * 0.05;
-        } else if (hour >= 12 && hour < 16) {
-            // 中午12点-16点:中午
-            viewer.scene.skyAtmosphere.hueShift = 0.1;
-            viewer.scene.skyAtmosphere.saturationShift = 0.2;
-            viewer.scene.skyAtmosphere.brightnessShift = 0.2;
-        } else if (hour >= 16 && hour < 18) {
-            // 下午16点-18点:日落
-            const progress = (hour - 16) / 2;
-            viewer.scene.skyAtmosphere.hueShift = 0.1 + progress * 0.1;
-            viewer.scene.skyAtmosphere.saturationShift = 0.2 + progress * 0.1;
-            viewer.scene.skyAtmosphere.brightnessShift = 0.2 - progress * 0.2;
-        } else if (hour >= 18 && hour < 21) {
-            // 傍晚18点-21点:天空逐渐转暗
-            const progress = (hour - 18) / 3;
-            viewer.scene.skyAtmosphere.hueShift = 0.2 + progress * 0.1;
-            viewer.scene.skyAtmosphere.saturationShift = 0.3 - progress * 0.4;
-            viewer.scene.skyAtmosphere.brightnessShift = 0 - progress * 0.5;
-        } else {
-            // 晚上21点-24点:漆黑
-            viewer.scene.skyAtmosphere.hueShift = -0.3;
-            viewer.scene.skyAtmosphere.saturationShift = -0.6;
-            viewer.scene.skyAtmosphere.brightnessShift = -0.7;
-        }
+        const hour = date.getHours() + date.getMinutes() / 60;
+        // 雾开关遵循面板设置
+        applyDayPeriod(hour, state.fogEffect);
     }
 
     // 处理时间轴时间变化
@@ -269,29 +231,27 @@ function s3mlayerAttribute(props) {
         }
     }
 
-    // 设置春季天气:阴天,小雨
+    // 雨雪效果 = 季节激活 OR 复选框勾选(两者独立)
+    function updateRainEffect() {
+        const shouldRain = state.summer || state.rain;
+        viewer.scene.postProcessStages.rain.enabled = shouldRain;
+    }
+    function updateSnowEffect() {
+        const shouldSnow = state.winter || state.snow;
+        viewer.scene.postProcessStages.snow.enabled = shouldSnow;
+    }
+
+    // 设置春季天气:阴天
     function setSpringWeather() {
         // 设置阴天效果
         viewer.scene.skyAtmosphere.hueShift = 0.1;
         viewer.scene.skyAtmosphere.saturationShift = -0.3;
         viewer.scene.skyAtmosphere.brightnessShift = -0.4;
-        
-        // 开启小雨效果(直接操作后处理阶段,不修改state参数)
-        viewer.scene.postProcessStages.snow.enabled = false;
-        viewer.scene.postProcessStages.rain.enabled = true;
-        
-        // 保存当前雨雪参数,用于恢复
-        savedRainParams = {
-            speed: viewer.scene.postProcessStages.rain.uniforms.speed,
-            angle: viewer.scene.postProcessStages.rain.uniforms.angle
-        };
-        
-        // 小雨:比默认雨更小的速度和角度(直接设置后处理参数,不修改state)
-        viewer.scene.postProcessStages.rain.uniforms.speed = 4;
-        viewer.scene.postProcessStages.rain.uniforms.angle = 20;
+        updateRainEffect();
+        updateSnowEffect();
     }
 
-    // 设置夏季天气:晴天,大气雾比较薄
+    // 设置夏季天气:晴天,大气雾比较薄,下雨
     function setSummerWeather() {
         // 设置晴天效果
         viewer.scene.skyAtmosphere.hueShift = 0.0;
@@ -303,9 +263,19 @@ function s3mlayerAttribute(props) {
         viewer.scene.fog.enabled = true;
         viewer.scene.fog.density = 0.0001;
         
-        // 关闭雨雪后处理阶段
-        viewer.scene.postProcessStages.rain.enabled = false;
-        viewer.scene.postProcessStages.snow.enabled = false;
+        // 保存当前雨参数(仅首次)
+        if (!savedRainParams) {
+            savedRainParams = {
+                speed: viewer.scene.postProcessStages.rain.uniforms.speed,
+                angle: viewer.scene.postProcessStages.rain.uniforms.angle
+            };
+        }
+        // 设置雨参数(小雨)
+        viewer.scene.postProcessStages.rain.uniforms.speed = 4;
+        viewer.scene.postProcessStages.rain.uniforms.angle = 20;
+        
+        updateRainEffect();
+        updateSnowEffect();
     }
 
     // 设置秋季天气:阴天
@@ -314,34 +284,32 @@ function s3mlayerAttribute(props) {
         viewer.scene.skyAtmosphere.hueShift = 0.1;
         viewer.scene.skyAtmosphere.saturationShift = -0.3;
         viewer.scene.skyAtmosphere.brightnessShift = -0.4;
-        
-        // 关闭雨雪后处理阶段
-        viewer.scene.postProcessStages.rain.enabled = false;
-        viewer.scene.postProcessStages.snow.enabled = false;
+        updateRainEffect();
+        updateSnowEffect();
     }
 
-    // 设置冬季天气:
+    // 设置冬季天气:阴天,下
     function setWinterWeather() {
         // 设置阴天效果
         viewer.scene.skyAtmosphere.hueShift = 0.1;
         viewer.scene.skyAtmosphere.saturationShift = -0.3;
         viewer.scene.skyAtmosphere.brightnessShift = -0.4;
         
-        // 开启大雪效果(直接操作后处理阶段,不修改state参数)
-        viewer.scene.postProcessStages.rain.enabled = false;
-        viewer.scene.postProcessStages.snow.enabled = true;
-        
-        // 保存当前雪参数,用于恢复
-        savedSnowParams = {
-            density: viewer.scene.postProcessStages.snow.uniforms.density,
-            speed: viewer.scene.postProcessStages.snow.uniforms.speed,
-            angle: viewer.scene.postProcessStages.snow.uniforms.angle
-        };
-        
-        // 大雪:比默认雪更大的密度和速度(直接设置后处理参数,不修改state)
+        // 保存当前雪参数(仅首次)
+        if (!savedSnowParams) {
+            savedSnowParams = {
+                density: viewer.scene.postProcessStages.snow.uniforms.density,
+                speed: viewer.scene.postProcessStages.snow.uniforms.speed,
+                angle: viewer.scene.postProcessStages.snow.uniforms.angle
+            };
+        }
+        // 设置雪参数(大雪)
         viewer.scene.postProcessStages.snow.uniforms.density = 20;
         viewer.scene.postProcessStages.snow.uniforms.speed = 10;
         viewer.scene.postProcessStages.snow.uniforms.angle = 10;
+        
+        updateRainEffect();
+        updateSnowEffect();
     }
 
     // 重置天气效果
@@ -356,22 +324,29 @@ function s3mlayerAttribute(props) {
         viewer.scene.fog.enabled = true;
         viewer.scene.fog.density = 0.0002;
         
-        // 关闭雨雪后处理阶段
-        viewer.scene.postProcessStages.rain.enabled = false;
-        viewer.scene.postProcessStages.snow.enabled = false;
-        
-        // 恢复雨雪参数到之前保存的值(如果有保存的话)
+        // 恢复雨参数到季节修改前的值
         if (savedRainParams) {
             viewer.scene.postProcessStages.rain.uniforms.speed = savedRainParams.speed;
             viewer.scene.postProcessStages.rain.uniforms.angle = savedRainParams.angle;
             savedRainParams = null;
+        } else if (initialRainParams) {
+            viewer.scene.postProcessStages.rain.uniforms.speed = initialRainParams.speed;
+            viewer.scene.postProcessStages.rain.uniforms.angle = initialRainParams.angle;
         }
+        // 恢复雪参数到季节修改前的值
         if (savedSnowParams) {
             viewer.scene.postProcessStages.snow.uniforms.density = savedSnowParams.density;
             viewer.scene.postProcessStages.snow.uniforms.speed = savedSnowParams.speed;
             viewer.scene.postProcessStages.snow.uniforms.angle = savedSnowParams.angle;
             savedSnowParams = null;
+        } else if (initialSnowParams) {
+            viewer.scene.postProcessStages.snow.uniforms.density = initialSnowParams.density;
+            viewer.scene.postProcessStages.snow.uniforms.speed = initialSnowParams.speed;
+            viewer.scene.postProcessStages.snow.uniforms.angle = initialSnowParams.angle;
         }
+        
+        updateRainEffect();
+        updateSnowEffect();
     }
 
     if (storeState.isViewer) {
@@ -381,6 +356,19 @@ function s3mlayerAttribute(props) {
         viewer.scene.postProcessStages.snow.uniforms.speed = parseFloat(state.snowSpeed);
         viewer.scene.postProcessStages.rain.uniforms.speed = parseFloat(state.rainSpeed);
         viewer.scene.debugShowFramesPerSecond = state.frameRate;
+        // 应用默认状态:大气渲染开启,天空盒关闭
+        viewer.scene.skyAtmosphere.show = state.atomsphereRender;
+        viewer.scene.skyBox.show = false;
+        // 保存雨雪初始参数,用于季节取消时恢复
+        initialRainParams = {
+            speed: viewer.scene.postProcessStages.rain.uniforms.speed,
+            angle: viewer.scene.postProcessStages.rain.uniforms.angle
+        };
+        initialSnowParams = {
+            density: viewer.scene.postProcessStages.snow.uniforms.density,
+            speed: viewer.scene.postProcessStages.snow.uniforms.speed,
+            angle: viewer.scene.postProcessStages.snow.uniforms.angle
+        };
         watchCameraHeight();
         initCorrectBeijingTime();
     }
@@ -393,6 +381,19 @@ function s3mlayerAttribute(props) {
             viewer.scene.postProcessStages.snow.uniforms.speed = parseFloat(state.snowSpeed);
             viewer.scene.postProcessStages.rain.uniforms.speed = parseFloat(state.rainSpeed);
             viewer.scene.debugShowFramesPerSecond = state.frameRate;
+            // 应用默认状态:大气渲染开启,天空盒关闭
+            viewer.scene.skyAtmosphere.show = state.atomsphereRender;
+            viewer.scene.skyBox.show = false;
+            // 保存雨雪初始参数,用于季节取消时恢复
+            initialRainParams = {
+                speed: viewer.scene.postProcessStages.rain.uniforms.speed,
+                angle: viewer.scene.postProcessStages.rain.uniforms.angle
+            };
+            initialSnowParams = {
+                density: viewer.scene.postProcessStages.snow.uniforms.density,
+                speed: viewer.scene.postProcessStages.snow.uniforms.speed,
+                angle: viewer.scene.postProcessStages.snow.uniforms.angle
+            };
             watchCameraHeight();
             initCorrectBeijingTime();
         }
@@ -433,7 +434,23 @@ function s3mlayerAttribute(props) {
     }, { immediate: true });
 
     watch(() => state.depthAgainst, val => { viewer.scene.globe.depthTestAgainstTerrain = val; });
-    watch(() => state.atomsphereRender, val => { viewer.scene.skyAtmosphere.show = val; });
+
+    // 控制体积云显隐(同时更新 cloudParams.show,保持与体积云组件同步)
+    function setVolumetricCloud(show) {
+        if (window.__volumetricCloudStage) {
+            window.__volumetricCloudStage.enabled = show;
+            if (window.__volumetricCloudStage.cloudParams) {
+                window.__volumetricCloudStage.cloudParams.show = show;
+            }
+        }
+    }
+
+    watch(() => state.atomsphereRender, val => {
+        viewer.scene.skyAtmosphere.show = val;
+        // 开启大气渲染时开启体积云,关闭时同步关闭
+        setVolumetricCloud(val);
+        if (val) state.skyBox = false;
+    });
     watch(() => state.fogEffect, val => { viewer.scene.fog.enabled = val; });
     watch(() => state.surfaceTransparency, val => { viewer.scene.globe.globeAlpha = parseFloat(val); });
     watch(() => state.underground, val => { viewer.scene.undergroundMode = val; });
@@ -452,11 +469,16 @@ function s3mlayerAttribute(props) {
 
     watch(() => state.skyBox, val => {
         if (val) {
+            state.atomsphereRender = false;
+            // 开启天空盒时关闭体积云
+            setVolumetricCloud(false);
             viewer.scene.postRender.addEventListener(watchCameraHeight);
             viewer.scene.skyBox = skyboxs[state.skyboxType];
             watchCameraHeight();
         } else {
             state.atomsphereRender = true;
+            // 关闭天空盒时随大气渲染开启体积云
+            setVolumetricCloud(true);
             viewer.scene.skyBox = defaultSkybox;
             viewer.scene.postRender.removeEventListener(watchCameraHeight);
         }
@@ -488,8 +510,8 @@ function s3mlayerAttribute(props) {
         else viewer.scene.mode = Cesium.SceneMode.COLUMBUS_VIEW;
     });
 
-    watch(() => state.rain, val => { viewer.scene.postProcessStages.rain.enabled = val; });
-    watch(() => state.snow, val => { viewer.scene.postProcessStages.snow.enabled = val; });
+    watch(() => state.rain, () => { updateRainEffect(); });
+    watch(() => state.snow, () => { updateSnowEffect(); });
     watch(() => state.rainAngle, val => { viewer.scene.postProcessStages.rain.uniforms.angle = parseFloat(val); });
     watch(() => state.rainSpeed, val => { viewer.scene.postProcessStages.rain.uniforms.speed = parseFloat(val); });
     watch(() => state.snowDesity, val => { viewer.scene.postProcessStages.snow.uniforms.density = parseFloat(val); });

+ 7 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/scene/time-slider/index.js

@@ -0,0 +1,7 @@
+import timeSlider from './time-slider.vue';
+
+timeSlider.install = function(app) {
+  app.component(timeSlider.name, timeSlider);
+};
+
+export default timeSlider;

+ 105 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/scene/time-slider/time-slider.js

@@ -0,0 +1,105 @@
+// 简易时间滑块逻辑:拖动调整场景时间并联动天空/大气/体积云效果
+import { ref, onMounted, onBeforeUnmount } from "vue";
+import { applyDayPeriod } from '../../../js/common/dayPeriod.js'
+
+function timeSlider(props) {
+    // 时间以分钟表示(0-1439)
+    const sliderValue = ref(600);
+    const timeText = ref('10:00');
+    const show = ref(true);
+
+    let viewer = null;
+    let syncing = false;
+
+    function formatTime(minutes) {
+        const h = Math.floor(minutes / 60).toString().padStart(2, '0');
+        const m = (minutes % 60).toString().padStart(2, '0');
+        return `${h}:${m}`;
+    }
+
+    // 从日期同步滑块位置
+    function syncFromDate(date) {
+        const minutes = date.getHours() * 60 + date.getMinutes();
+        if (sliderValue.value !== minutes) {
+            syncing = true;
+            sliderValue.value = minutes;
+            timeText.value = formatTime(minutes);
+            syncing = false;
+        }
+    }
+
+    function init() {
+        viewer = window.viewer;
+        if (!viewer || !viewer.scene) return;
+
+        // 初始化时钟范围为今天的 00:00-24:00
+        const today = new Date();
+        today.setHours(0, 0, 0, 0);
+        const tomorrow = new Date(today);
+        tomorrow.setDate(tomorrow.getDate() + 1);
+        viewer.clock.startTime = Cesium.JulianDate.fromDate(today);
+        viewer.clock.stopTime = Cesium.JulianDate.fromDate(tomorrow);
+        viewer.clock.multiplier = 1;
+        viewer.clock.shouldAnimate = false;
+
+        // 同步当前时间到滑块
+        const date = Cesium.JulianDate.toDate(viewer.clock.currentTime);
+        syncFromDate(date);
+
+        // 监听时钟变化,与场景属性面板等外部时间调整保持同步
+        viewer.clock.onTick.addEventListener(onTick);
+    }
+
+    // 应用滑块时间到场景(设置时钟 + 时段效果)
+    function applyMinutes(minutes) {
+        if (!viewer) return;
+        const date = new Date();
+        date.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
+        viewer.clock.currentTime = Cesium.JulianDate.fromDate(date);
+        applyDayPeriod(minutes / 60, true);
+    }
+
+    // 拖动过程中实时预览
+    function handleInput(val) {
+        timeText.value = formatTime(val);
+        applyMinutes(val);
+    }
+
+    // 松开滑块时应用
+    function handleChange(val) {
+        timeText.value = formatTime(val);
+        applyMinutes(val);
+    }
+
+    // 时钟 tick:外部修改时间时同步滑块位置
+    function onTick(clock) {
+        if (syncing) return;
+        const date = Cesium.JulianDate.toDate(clock.currentTime);
+        const minutes = date.getHours() * 60 + date.getMinutes();
+        if (Math.abs(minutes - sliderValue.value) >= 1) {
+            syncing = true;
+            sliderValue.value = minutes;
+            timeText.value = formatTime(minutes);
+            syncing = false;
+        }
+    }
+
+    onMounted(() => {
+        if (window.viewer) {
+            init();
+        } else {
+            // viewer 未就绪时延迟重试
+            setTimeout(init, 800);
+        }
+    });
+
+    onBeforeUnmount(() => {
+        if (window.viewer && window.viewer.clock) {
+            window.viewer.clock.onTick.removeEventListener(onTick);
+        }
+    });
+
+    return { sliderValue, timeText, show, handleInput, handleChange };
+}
+
+export default timeSlider;

+ 68 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/scene/time-slider/time-slider.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="time-slider-container" v-show="show">
+    <div class="time-slider-title">时间</div>
+    <el-slider
+      class="time-slider"
+      v-model="sliderValue"
+      :min="0"
+      :max="1439"
+      :step="1"
+      :show-tooltip="false"
+      size="small"
+      @input="handleInput"
+      @change="handleChange"
+    />
+    <div class="time-slider-value">{{ timeText }}</div>
+  </div>
+</template>
+
+<script>
+import timeSlider from "./time-slider.js";
+export default {
+  name: "Sm3dTimeSlider",
+  setup() {
+    let { sliderValue, timeText, show, handleInput, handleChange } = timeSlider();
+    return { sliderValue, timeText, show, handleInput, handleChange };
+  }
+};
+</script>
+
+<style scoped>
+.time-slider-container {
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  padding: 8px 12px;
+  background: rgba(20, 40, 70, 0.85);
+  border: 1px solid rgba(255, 255, 255, 0.15);
+  border-radius: 6px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(4px);
+}
+
+.time-slider-title {
+  color: #fff;
+  font-size: 13px;
+  font-weight: bold;
+  margin-right: 8px;
+  white-space: nowrap;
+}
+
+.time-slider {
+  width: 160px;
+  margin: 0 6px;
+}
+
+.time-slider-value {
+  color: #ffd04b;
+  font-size: 14px;
+  font-weight: bold;
+  font-family: "Consolas", monospace;
+  min-width: 44px;
+  text-align: center;
+  white-space: nowrap;
+}
+</style>

+ 7 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/scene/weather-select/index.js

@@ -0,0 +1,7 @@
+import weatherSelect from './weather-select.vue';
+
+weatherSelect.install = function(app) {
+  app.component(weatherSelect.name, weatherSelect);
+};
+
+export default weatherSelect;

+ 55 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/scene/weather-select/weather-select.js

@@ -0,0 +1,55 @@
+// 环境控制面板逻辑:时间滑块 + 天气(晴/多云/阴/雨/雪)+ 季节(默认/春夏秋冬)
+import { ref } from "vue";
+import { getWeatherType, setWeatherType, getSeasonType, setSeasonType } from '../../../js/common/dayPeriod.js'
+import timeSlider from "../time-slider/time-slider.js";
+
+function weatherSelect(props) {
+    // 面板展开状态(默认收起)
+    const expanded = ref(false);
+
+    // 时间滑块逻辑(复用 time-slider 组件)
+    const time = timeSlider();
+
+    // 天气
+    const weatherList = [
+        { type: 'sunny', label: '晴' },
+        { type: 'cloudy', label: '多云' },
+        { type: 'overcast', label: '阴' },
+        { type: 'rain', label: '雨' },
+        { type: 'snow', label: '雪' }
+    ];
+    const currentWeather = ref(getWeatherType());
+
+    // 季节
+    const seasonList = [
+        { type: 'default', label: '默认' },
+        { type: 'spring', label: '春' },
+        { type: 'summer', label: '夏' },
+        { type: 'autumn', label: '秋' },
+        { type: 'winter', label: '冬' }
+    ];
+    const currentSeason = ref(getSeasonType());
+
+    function selectWeather(type) {
+        setWeatherType(type);
+        currentWeather.value = getWeatherType();
+    }
+
+    function selectSeason(type) {
+        setSeasonType(type);
+        currentSeason.value = getSeasonType();
+    }
+
+    return {
+        expanded,
+        sliderValue: time.sliderValue,
+        timeText: time.timeText,
+        handleInput: time.handleInput,
+        handleChange: time.handleChange,
+        weatherList, seasonList,
+        currentWeather, currentSeason,
+        selectWeather, selectSeason
+    };
+}
+
+export default weatherSelect;

+ 208 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/scene/weather-select/weather-select.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="env-panel" v-if="expanded">
+    <div class="env-header">
+      <div class="env-header-title">环境控制</div>
+      <button class="env-toggle-btn" @click="expanded = false">收起 ▲</button>
+    </div>
+    <div class="env-row">
+      <div class="weather-select-title">时间</div>
+      <el-slider
+        class="env-slider"
+        v-model="sliderValue"
+        :min="0"
+        :max="1439"
+        :step="1"
+        :show-tooltip="false"
+        size="small"
+        @input="handleInput"
+        @change="handleChange"
+      />
+      <div class="env-time-value">{{ timeText }}</div>
+    </div>
+    <div class="env-row">
+      <div class="weather-select-title">天气</div>
+      <div class="weather-select-buttons">
+        <button
+          v-for="w in weatherList"
+          :key="w.type"
+          :class="['weather-btn', { active: currentWeather === w.type }]"
+          @click="selectWeather(w.type)"
+        >{{ w.label }}</button>
+      </div>
+    </div>
+    <div class="env-row">
+      <div class="weather-select-title">季节</div>
+      <div class="weather-select-buttons">
+        <button
+          v-for="s in seasonList"
+          :key="s.type"
+          :class="['weather-btn', { active: currentSeason === s.type }]"
+          @click="selectSeason(s.type)"
+        >{{ s.label }}</button>
+      </div>
+    </div>
+  </div>
+  <button v-else class="env-collapsed-btn" @click="expanded = true">环境 ▼</button>
+</template>
+
+<script>
+import weatherSelect from "./weather-select.js";
+export default {
+  name: "Sm3dWeatherSelect",
+  setup() {
+    let {
+      expanded,
+      sliderValue,
+      timeText,
+      handleInput,
+      handleChange,
+      weatherList,
+      seasonList,
+      currentWeather,
+      currentSeason,
+      selectWeather,
+      selectSeason
+    } = weatherSelect();
+    return {
+      expanded,
+      sliderValue,
+      timeText,
+      handleInput,
+      handleChange,
+      weatherList,
+      seasonList,
+      currentWeather,
+      currentSeason,
+      selectWeather,
+      selectSeason
+    };
+  }
+};
+</script>
+
+<style scoped>
+.env-panel {
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  padding: 8px 12px;
+  background: rgba(20, 40, 70, 0.85);
+  border: 1px solid rgba(255, 255, 255, 0.15);
+  border-radius: 6px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(4px);
+}
+
+.env-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 2px;
+}
+
+.env-header-title {
+  color: #fff;
+  font-size: 13px;
+  font-weight: bold;
+}
+
+.env-toggle-btn {
+  padding: 2px 10px;
+  font-size: 12px;
+  color: #cfe0ff;
+  background: rgba(255, 255, 255, 0.1);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.env-toggle-btn:hover {
+  background: rgba(255, 255, 255, 0.2);
+  color: #fff;
+}
+
+.env-collapsed-btn {
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  z-index: 1000;
+  padding: 6px 14px;
+  font-size: 13px;
+  color: #cfe0ff;
+  background: rgba(20, 40, 70, 0.85);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(4px);
+}
+
+.env-collapsed-btn:hover {
+  background: rgba(40, 70, 110, 0.9);
+  color: #fff;
+}
+
+.env-row {
+  display: flex;
+  align-items: center;
+}
+
+.weather-select-title {
+  color: #fff;
+  font-size: 13px;
+  font-weight: bold;
+  margin-right: 10px;
+  white-space: nowrap;
+}
+
+.env-slider {
+  width: 160px;
+  margin: 0 6px;
+}
+
+.env-time-value {
+  color: #ffd04b;
+  font-size: 14px;
+  font-weight: bold;
+  font-family: "Consolas", monospace;
+  min-width: 44px;
+  text-align: center;
+  white-space: nowrap;
+}
+
+.weather-select-buttons {
+  display: flex;
+  gap: 4px;
+}
+
+.weather-btn {
+  padding: 3px 12px;
+  font-size: 13px;
+  color: #cfe0ff;
+  background: rgba(255, 255, 255, 0.1);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.2s;
+  white-space: nowrap;
+}
+
+.weather-btn:hover {
+  background: rgba(255, 255, 255, 0.2);
+  color: #fff;
+}
+
+.weather-btn.active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+  font-weight: bold;
+}
+</style>
+

+ 7 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/volumetric-cloud/index.js

@@ -0,0 +1,7 @@
+import volumetricCloud from './volumetric-cloud.vue';
+
+volumetricCloud.install = function(app) {
+  app.component(volumetricCloud.name, volumetricCloud);
+};
+
+export default volumetricCloud;

+ 121 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/volumetric-cloud/volumetric-cloud.js

@@ -0,0 +1,121 @@
+// 引入依赖
+import { watch, reactive, toRefs, onBeforeUnmount, onActivated } from "vue";
+import { storeState } from '../../../js/store/store.js'
+import tool from '../../../js/tool/tool.js'
+import resource from '../../../js/local/lang.js'
+import { addSimpleClouds } from '@/cesium-clouds-atmosphere/simpleClouds.js'
+
+function volumetricCloud(props) {
+    const viewer = window.viewer;
+
+    let state = reactive({
+        show: true,
+        loading: false,
+        minHeight: 1800,
+        maxHeight: 8000,
+        density: 1.0,
+        coverage: 0.35,
+        sunIntensity: 50.0,
+        cloudExposure: 4.0,
+        skyLight: 1.0
+    });
+
+    // 传入props改变默认值
+    if (props) {
+        for (let key in props) {
+            if (state.hasOwnProperty(key)) {
+                if (props[key] !== undefined) state[key] = props[key];
+            } else {
+                tool.Message.errorMsg(resource.AttributeError + key);
+            }
+        }
+    }
+
+    let cloudStage = null;
+
+    function init() {
+        if (!viewer || !viewer.scene) return;
+        // 复用已初始化的体积云(viewer 或其它位置已创建)
+        if (window.__volumetricCloudStage) {
+            cloudStage = window.__volumetricCloudStage;
+            syncState();
+            return;
+        }
+        state.loading = true;
+        addSimpleClouds(viewer).then(stage => {
+            cloudStage = stage;
+            window.__volumetricCloudStage = stage;
+            syncState();
+            state.loading = false;
+        }).catch(err => {
+            console.warn('[体积云] 初始化失败:', err?.message || err);
+            state.loading = false;
+        });
+    }
+
+    // 将体积云当前参数同步到面板
+    function syncState() {
+        if (!cloudStage || !cloudStage.cloudParams) return;
+        const p = cloudStage.cloudParams;
+        state.show = p.show;
+        state.minHeight = p.minHeight;
+        state.maxHeight = p.maxHeight;
+        state.density = p.densityScale;
+        state.coverage = p.coverage;
+        state.sunIntensity = p.sunIntensity;
+        state.cloudExposure = p.cloudExposure;
+        state.skyLight = p.skyLightScale;
+    }
+
+    if (storeState.isViewer) {
+        init();
+    }
+    // viewer 初始化完成的监听
+    watch(() => storeState.isViewer, val => {
+        if (val) init();
+    });
+
+    // 监听
+    watch(() => state.show, val => {
+        if (cloudStage) {
+            cloudStage.enabled = val;
+            if (cloudStage.cloudParams) cloudStage.cloudParams.show = val;
+        }
+    });
+    watch(() => state.minHeight, val => {
+        if (cloudStage && cloudStage.cloudParams) cloudStage.cloudParams.minHeight = Number(val);
+    });
+    watch(() => state.maxHeight, val => {
+        if (cloudStage && cloudStage.cloudParams) cloudStage.cloudParams.maxHeight = Number(val);
+    });
+    watch(() => state.density, val => {
+        if (cloudStage && cloudStage.cloudParams) cloudStage.cloudParams.densityScale = Number(val);
+    });
+    watch(() => state.coverage, val => {
+        if (cloudStage && cloudStage.cloudParams) cloudStage.cloudParams.coverage = Number(val);
+    });
+    watch(() => state.sunIntensity, val => {
+        if (cloudStage && cloudStage.cloudParams) cloudStage.cloudParams.sunIntensity = Number(val);
+    });
+    watch(() => state.cloudExposure, val => {
+        if (cloudStage && cloudStage.cloudParams) cloudStage.cloudParams.cloudExposure = Number(val);
+    });
+    watch(() => state.skyLight, val => {
+        if (cloudStage && cloudStage.cloudParams) cloudStage.cloudParams.skyLightScale = Number(val);
+    });
+
+    // 销毁
+    onBeforeUnmount(() => {
+        // 体积云由 viewer 全局管理,这里仅移除控制,不销毁 stage,
+        // 避免其它面板/组件复用时报 stage 不存在
+    });
+
+    // 面板通过 keep-alive 复用,重新激活时同步外部对体积云的显隐控制
+    onActivated(() => {
+        syncState();
+    });
+
+    return { ...toRefs(state) };
+}
+
+export default volumetricCloud;

+ 151 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/volumetric-cloud/volumetric-cloud.vue

@@ -0,0 +1,151 @@
+<template>
+  <div id="volumetric-cloud-panel" class="sm-panel" v-drag>
+    <div class="sm-function-module-sub-section" style="margin:0" v-stopdrag>
+      <div class="sm-half-L">
+        <label style="width:100%">
+          <input type="checkbox" v-model="show" />
+          体积云
+        </label>
+      </div>
+      <div class="sm-half-L">
+        <label style="width:35%">云层高度(m)</label>
+        <el-slider
+          v-model="minHeight"
+          :min="500"
+          :max="20000"
+          :step="100"
+          input-size="mini"
+          :debounce="300"
+          tooltip-class="tooltip-class"
+          style="width:63%"
+        ></el-slider>
+      </div>
+      <div class="sm-half-L">
+        <label style="width:35%">云层厚度(m)</label>
+        <el-slider
+          v-model="maxHeight"
+          :min="2000"
+          :max="30000"
+          :step="100"
+          input-size="mini"
+          :debounce="300"
+          tooltip-class="tooltip-class"
+          style="width:63%"
+        ></el-slider>
+      </div>
+      <div class="sm-half-L">
+        <label style="width:35%">密度</label>
+        <el-slider
+          v-model="density"
+          :min="0"
+          :max="3"
+          :step="0.01"
+          input-size="mini"
+          :debounce="300"
+          tooltip-class="tooltip-class"
+          style="width:63%"
+        ></el-slider>
+      </div>
+      <div class="sm-half-L">
+        <label style="width:35%">云量</label>
+        <el-slider
+          v-model="coverage"
+          :min="0"
+          :max="1"
+          :step="0.01"
+          input-size="mini"
+          :debounce="300"
+          tooltip-class="tooltip-class"
+          style="width:63%"
+        ></el-slider>
+      </div>
+      <div class="sm-half-L">
+        <label style="width:35%">太阳强度</label>
+        <el-slider
+          v-model="sunIntensity"
+          :min="0"
+          :max="100"
+          :step="1"
+          input-size="mini"
+          :debounce="300"
+          tooltip-class="tooltip-class"
+          style="width:63%"
+        ></el-slider>
+      </div>
+      <div class="sm-half-L">
+        <label style="width:35%">云亮度</label>
+        <el-slider
+          v-model="cloudExposure"
+          :min="0.1"
+          :max="10"
+          :step="0.1"
+          input-size="mini"
+          :debounce="300"
+          tooltip-class="tooltip-class"
+          style="width:63%"
+        ></el-slider>
+      </div>
+      <div class="sm-half-L">
+        <label style="width:35%">天空光</label>
+        <el-slider
+          v-model="skyLight"
+          :min="0"
+          :max="3"
+          :step="0.01"
+          input-size="mini"
+          :debounce="300"
+          tooltip-class="tooltip-class"
+          style="width:63%"
+        ></el-slider>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import volumetricCloud from "./volumetric-cloud.js";
+export default {
+  name: "Sm3dVolumetricCloud",
+  props: {
+    // 是否显示体积云
+    show: { type: Boolean },
+    // 云层最低高度(米)
+    minHeight: { type: Number },
+    // 云层最高高度(米)
+    maxHeight: { type: Number },
+    // 密度系数
+    density: { type: Number },
+    // 云量(覆盖度)
+    coverage: { type: Number },
+    // 太阳强度
+    sunIntensity: { type: Number },
+    // 云亮度
+    cloudExposure: { type: Number },
+    // 天空光强度
+    skyLight: { type: Number }
+  },
+  setup(props) {
+    let {
+      show,
+      minHeight,
+      maxHeight,
+      density,
+      coverage,
+      sunIntensity,
+      cloudExposure,
+      skyLight
+    } = volumetricCloud(props);
+
+    return {
+      show,
+      minHeight,
+      maxHeight,
+      density,
+      coverage,
+      sunIntensity,
+      cloudExposure,
+      skyLight
+    };
+  }
+};
+</script>

+ 27 - 28
RuoYi-Vue3/src/supermap-cesium-module/components/viewer/viewer.js

@@ -5,8 +5,8 @@ import EventManager from '../../js/common/eventManager/EventManager.js'   //事
 import { onBeforeUnmount } from 'vue'
 import createTooltip from '../../js/tool/tooltip2.js'
 import resource from '../../js/local/lang.js'  //语言资源
+import { addSimpleClouds } from '@/cesium-clouds-atmosphere/simpleClouds.js'
 
-// Cesium 已经在 index.html 中通过 script 标签全局引入,无需再次导入
 //初始化地球 
 function initViewer(props, callback) {
   //初始化viewer
@@ -27,9 +27,9 @@ function initViewer(props, callback) {
       // geocoder: true,  //查询
       // skyBox: false, // 关闭天空盒会一同关闭太阳,场景会变暗
       navigation: false,
-      // contextOptions: {
-      //   requestWebgl2: true
-      // },
+      contextOptions: {
+        requestWebgl2: true
+      },
       scale: true // 添加比例尺
     });
     // 太阳光默认打开
@@ -76,25 +76,23 @@ function initViewer(props, callback) {
   viewer.scene.globe.baseColor = Cesium.Color.BLACK; // 没有影像图层时地球的底色
   // viewer.scene.globe.depthTestAgainstTerrain = false; //地形深度
   
-  // 完全禁用大气效果
+  // ── 体积云: 使用 Cesium 原生天空大气 + simpleClouds 云层 ──
+  // Cesium 内置天空大气效果
   if (viewer.scene.skyAtmosphere) {
-    viewer.scene.skyAtmosphere.show = false;
+    viewer.scene.skyAtmosphere.show = true;
   }
-  
-  // 禁用太阳和月亮
+  // 太阳保持显示(云渲染需要太阳方向)
   if (viewer.scene.sun) {
-    viewer.scene.sun.show = false;
+    viewer.scene.sun.show = true;
   }
   if (viewer.scene.moon) {
     viewer.scene.moon.show = false;
   }
-  
-  // 禁用光照效果,避免影响天空盒
   viewer.scene.globe.enableLighting = false;
   
-  // 使用与场景属性设置相同的方式创建天空盒
-  const skyboxSources = {
-    cloudy: {
+  // 初始化天空盒实例(默认不启用,勾选天空盒时使用)
+  viewer.scene.skyBox = new Cesium.SkyBox({ 
+    sources: {
       positiveX: '/img/skyboxs/cloudy/Right.png',
       negativeX: '/img/skyboxs/cloudy/Left.png',
       positiveY: '/img/skyboxs/cloudy/Front.png',
@@ -102,24 +100,25 @@ function initViewer(props, callback) {
       positiveZ: '/img/skyboxs/cloudy/Up.png',
       negativeZ: '/img/skyboxs/cloudy/Down.png'
     }
-  };
-  
-  // 创建天空盒实例
-  let skybox = new Cesium.SkyBox({ sources: skyboxSources.cloudy });
-  skybox.USpeed = 0;
-  skybox.VSpeed = 0;
-  skybox.WSpeed = 0.5;
-  
-  // 设置天空盒
-  viewer.scene.skyBox = skybox;
-  
-  // 启用天空盒
-  viewer.scene.skyBox.show = true;
+  });
+  viewer.scene.skyBox.USpeed = 0;
+  viewer.scene.skyBox.VSpeed = 0;
+  viewer.scene.skyBox.WSpeed = 0.5;
+  viewer.scene.skyBox.show = false;
   
   window.viewer = viewer;
   window.scene = viewer.scene;
   scene.moon.show = false;
   viewer.eventManager = new EventManager(viewer);  //添加事件管理派发
+
+  // 异步初始化体积云(不阻塞主流程),并挂载到全局供体积云组件复用
+  setTimeout(() => {
+    addSimpleClouds(viewer).then((stage) => {
+      window.__volumetricCloudStage = stage;
+    }).catch((err) => {
+      console.warn('[viewer] 体积云初始化失败:', err?.message || err);
+    });
+  }, 500);
   
   // ========== 关键修复:全局渲染错误捕获,防止渲染停止 ==========
   // 捕获渲染错误,返回true表示已处理,Cesium会继续渲染
@@ -155,7 +154,7 @@ function initViewer(props, callback) {
     destination: Cesium.Cartesian3.fromDegrees(119.290381, 26.10377, 50000), // 指定位置,高度50公里
     orientation: {
       heading: Cesium.Math.toRadians(0), // 方向角
-      pitch: Cesium.Math.toRadians(0), // 俯仰角
+      pitch: Cesium.Math.toRadians(-40), // 俯仰角(-40度朝下看,方便看到云层)
       roll: 0 // 翻滚角
     }
   });

+ 4 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/viewer/viewer.vue

@@ -4,6 +4,7 @@
     <ScaleBar v-if="viewerInstance" :viewer="viewerInstance" />
     <ModelTransformPanel v-if="viewerInstance" :viewer="viewerInstance" />
     <RectangleZoom v-if="viewerInstance" :viewer="viewerInstance" />
+    <WeatherSelect v-if="viewerInstance" />
     <slot></slot>
   </div>
 </template>
@@ -15,6 +16,7 @@ import SearchComponent from "../search/search.vue";
 import ScaleBar from "../scale-bar/scale-bar.vue";
 import ModelTransformPanel from "../model-transform/model-transform-panel.vue";
 import RectangleZoom from "../scene/rectangle-zoom/index.js";
+import WeatherSelect from "../scene/weather-select/index.js";
 // import {initViewer} from "../../../dist/vue-webgl2.min";
 
 export default {
@@ -23,7 +25,8 @@ export default {
     SearchComponent,
     ScaleBar,
     ModelTransformPanel,
-    RectangleZoom
+    RectangleZoom,
+    WeatherSelect
   },
   props: {
     sceneUrl: {

+ 571 - 0
RuoYi-Vue3/src/supermap-cesium-module/js/common/dayPeriod.js

@@ -0,0 +1,571 @@
+/**
+ * 全天时段效果系统:时间 → 大气 + 雾 + 体积云
+ * 供「场景属性设置」面板与地图右上角「时间滑块」共用
+ */
+
+function lerp(a, b, t) { return a + (b - a) * t; }
+
+// 颜色线性插值
+function lerpColor(c1, c2, t) {
+    return [
+        c1[0] + (c2[0] - c1[0]) * t,
+        c1[1] + (c2[1] - c1[1]) * t,
+        c1[2] + (c2[2] - c1[2]) * t
+    ];
+}
+
+// 关键帧:每个关键帧包含大气偏移、体积云参数、雾浓度。
+// 参数说明:
+//  - atmosphere: Cesium skyAtmosphere 的 hue/saturation/brightness 偏移
+//  - cloud.sunIntensity: 光源强度(正午基准 50)
+//  - cloud.cloudExposure: 云曝光(正午基准 4)
+//  - cloud.skyLightScale: 天光环境光强度(正午基准 1)
+//  - cloud.sunColor/skyColor: 阳光色 / 天光色(RGB)
+//  - cloud.minHeight/maxHeight: 云层底/顶高度(m)
+//  - cloud.densityScale/coverage: 云密度 / 云量
+//  - cloud.powderScale: 金边/边缘光强度
+//  - fog: Cesium 雾浓度
+const dayPeriodKeyframes = [
+    {
+        // 00:00 深夜:深蓝近黑、月光微弱、背光灰云
+        hour: 0,
+        atmosphere: { hueShift: -0.05, saturationShift: -0.35, brightnessShift: -0.8 },
+        cloud: {
+            sunIntensity: 1.0, skyLightScale: 0.07, cloudExposure: 0.32,
+            sunColor: [0.65, 0.7, 0.9], skyColor: [0.08, 0.1, 0.25],
+            minHeight: 1500, maxHeight: 3500, densityScale: 0.15, coverage: 0.1, powderScale: 0.2
+        },
+        fog: 0.00012
+    },
+    {
+        // 05:00 凌晨末:微光初现
+        hour: 5,
+        atmosphere: { hueShift: -0.02, saturationShift: -0.3, brightnessShift: -0.7 },
+        cloud: {
+            sunIntensity: 4, skyLightScale: 0.18, cloudExposure: 0.5,
+            sunColor: [0.7, 0.72, 0.92], skyColor: [0.12, 0.14, 0.3],
+            minHeight: 1500, maxHeight: 3500, densityScale: 0.18, coverage: 0.12, powderScale: 0.3
+        },
+        fog: 0.00015
+    },
+    {
+        // 06:00 日出拂晓:橙红霞光、云层金边、晨雾
+        hour: 6,
+        atmosphere: { hueShift: 0.05, saturationShift: -0.2, brightnessShift: 0.1 },
+        cloud: {
+            sunIntensity: 22, skyLightScale: 0.6, cloudExposure: 1.4,
+            sunColor: [1.0, 0.6, 0.3], skyColor: [0.6, 0.55, 0.85],
+            minHeight: 1200, maxHeight: 5500, densityScale: 0.35, coverage: 0.35, powderScale: 1.2
+        },
+        fog: 0.0006
+    },
+    {
+        // 09:00 上午:云量减少,天气转晴
+        hour: 9,
+        atmosphere: { hueShift: 0, saturationShift: 0.04, brightnessShift: 0.12 },
+        cloud: {
+            sunIntensity: 40, skyLightScale: 0.83, cloudExposure: 3.0,
+            sunColor: [1.0, 0.92, 0.9], skyColor: [0.42, 0.62, 0.98],
+            minHeight: 1000, maxHeight: 8000, densityScale: 0.5, coverage: 0.25, powderScale: 0.6
+        },
+        fog: 0.00018
+    },
+    {
+        // 12:00 正午:积云增多
+        hour: 12,
+        atmosphere: { hueShift: 0.04, saturationShift: 0.1, brightnessShift: 0.15 },
+        cloud: {
+            sunIntensity: 50, skyLightScale: 1.0, cloudExposure: 4.0,
+            sunColor: [1.0, 0.98, 0.95], skyColor: [0.4, 0.6, 1.0],
+            minHeight: 1000, maxHeight: 8000, densityScale: 0.55, coverage: 0.35, powderScale: 0.5
+        },
+        fog: 0.0002
+    },
+    {
+        // 16:00 午后:云量明显减少
+        hour: 16,
+        atmosphere: { hueShift: 0, saturationShift: 0.04, brightnessShift: 0.12 },
+        cloud: {
+            sunIntensity: 40, skyLightScale: 0.95, cloudExposure: 3.0,
+            sunColor: [1.0, 0.9, 0.75], skyColor: [0.5, 0.6, 0.9],
+            minHeight: 1000, maxHeight: 8000, densityScale: 0.4, coverage: 0.15, powderScale: 0.8
+        },
+        fog: 0.0002
+    },
+    {
+        // 17:00 晚霞前:天空仍保持蓝色(不提前变紫)
+        hour: 17,
+        atmosphere: { hueShift: 0, saturationShift: 0.02, brightnessShift: 0.12 },
+        cloud: {
+            sunIntensity: 33, skyLightScale: 0.8, cloudExposure: 2.4,
+            sunColor: [1.0, 0.75, 0.5], skyColor: [0.62, 0.52, 0.8],
+            minHeight: 1100, maxHeight: 8000, densityScale: 0.39, coverage: 0.25, powderScale: 1.0
+        },
+        fog: 0.0003
+    },
+    {
+        // 17:30 黄昏日落:暖橙晚霞(hue=0.42 将蓝色旋转为橙色,夏季叠加更浓郁)
+        hour: 17.5,
+        atmosphere: { hueShift: 0.42, saturationShift: -0.05, brightnessShift: 0.1 },
+        cloud: {
+            sunIntensity: 26, skyLightScale: 0.68, cloudExposure: 1.8,
+            sunColor: [1.0, 0.5, 0.2], skyColor: [0.75, 0.45, 0.65],
+            minHeight: 1200, maxHeight: 8000, densityScale: 0.38, coverage: 0.35, powderScale: 1.5
+        },
+        fog: 0.00045
+    },
+    {
+        // 18:30 晚霞持续:保持暖橙红色调,不提前变紫
+        hour: 18.5,
+        atmosphere: { hueShift: 0.4, saturationShift: -0.08, brightnessShift: -0.02 },
+        cloud: {
+            sunIntensity: 12, skyLightScale: 0.4, cloudExposure: 1.2,
+            sunColor: [0.9, 0.45, 0.2], skyColor: [0.9, 0.5, 0.5],
+            minHeight: 1400, maxHeight: 6000, densityScale: 0.34, coverage: 0.4, powderScale: 1.3
+        },
+        fog: 0.00035
+    },
+    {
+        // 19:30 暮色蓝调:冷蓝紫罗兰、云失去金边
+        hour: 19.5,
+        atmosphere: { hueShift: 0.08, saturationShift: -0.2, brightnessShift: -0.35 },
+        cloud: {
+            sunIntensity: 4, skyLightScale: 0.2, cloudExposure: 0.8,
+            sunColor: [0.75, 0.8, 1.0], skyColor: [0.45, 0.5, 0.9],
+            minHeight: 1500, maxHeight: 5000, densityScale: 0.3, coverage: 0.15, powderScale: 0.6
+        },
+        fog: 0.00025
+    },
+    {
+        // 22:00 夜晚过渡:渐入深夜
+        hour: 22,
+        atmosphere: { hueShift: 0, saturationShift: -0.4, brightnessShift: -0.6 },
+        cloud: {
+            sunIntensity: 2, skyLightScale: 0.1, cloudExposure: 0.4,
+            sunColor: [0.6, 0.65, 0.9], skyColor: [0.15, 0.18, 0.4],
+            minHeight: 1500, maxHeight: 3500, densityScale: 0.2, coverage: 0.1, powderScale: 0.2
+        },
+        fog: 0.00015
+    },
+    {
+        // 24:00 深夜(回到 0:00)
+        hour: 24,
+        atmosphere: { hueShift: -0.05, saturationShift: -0.35, brightnessShift: -0.8 },
+        cloud: {
+            sunIntensity: 1.0, skyLightScale: 0.07, cloudExposure: 0.32,
+            sunColor: [0.65, 0.7, 0.9], skyColor: [0.08, 0.1, 0.25],
+            minHeight: 1500, maxHeight: 3500, densityScale: 0.15, coverage: 0.1, powderScale: 0.2
+        },
+        fog: 0.00012
+    }
+];
+
+// 获取指定小时的插值配置
+function getDayPeriodConfig(hour) {
+    const kf = dayPeriodKeyframes;
+    let k0 = kf[0], k1 = kf[kf.length - 1];
+    for (let i = 0; i < kf.length - 1; i++) {
+        if (hour >= kf[i].hour && hour < kf[i + 1].hour) {
+            k0 = kf[i];
+            k1 = kf[i + 1];
+            break;
+        }
+    }
+    const span = (k1.hour - k0.hour) || 1;
+    const t = Math.min(1, Math.max(0, (hour - k0.hour) / span));
+    return {
+        atmosphere: {
+            hueShift: lerp(k0.atmosphere.hueShift, k1.atmosphere.hueShift, t),
+            saturationShift: lerp(k0.atmosphere.saturationShift, k1.atmosphere.saturationShift, t),
+            brightnessShift: lerp(k0.atmosphere.brightnessShift, k1.atmosphere.brightnessShift, t)
+        },
+        cloud: {
+            sunIntensity: lerp(k0.cloud.sunIntensity, k1.cloud.sunIntensity, t),
+            skyLightScale: lerp(k0.cloud.skyLightScale, k1.cloud.skyLightScale, t),
+            cloudExposure: lerp(k0.cloud.cloudExposure, k1.cloud.cloudExposure, t),
+            sunColor: lerpColor(k0.cloud.sunColor, k1.cloud.sunColor, t),
+            skyColor: lerpColor(k0.cloud.skyColor, k1.cloud.skyColor, t),
+            minHeight: lerp(k0.cloud.minHeight, k1.cloud.minHeight, t),
+            maxHeight: lerp(k0.cloud.maxHeight, k1.cloud.maxHeight, t),
+            densityScale: lerp(k0.cloud.densityScale, k1.cloud.densityScale, t),
+            coverage: lerp(k0.cloud.coverage, k1.cloud.coverage, t),
+            powderScale: lerp(k0.cloud.powderScale, k1.cloud.powderScale, t)
+        },
+        fog: lerp(k0.fog, k1.fog, t)
+    };
+}
+
+// 平滑阶梯函数(用于正午附近过渡)
+function smoothstep(edge0, edge1, x) {
+    const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
+    return t * t * (3 - 2 * t);
+}
+
+// 根据季节对查询时段做偏移:夏季亮得早黑得晚,冬季亮得晚黑得早
+function getSeasonShiftedHour(hour) {
+    const season = seasonPresets[currentSeason] || {};
+    if (!season.cloud) return hour;  // 默认季节不偏移
+    const morning = season.shiftMorning || 0;
+    const evening = season.shiftEvening || 0;
+    // 以正午为中心(10-14点)在早晨偏移与傍晚偏移之间平滑过渡
+    const t = smoothstep(10, 14, hour);
+    let h = hour + morning * (1 - t) + evening * t;
+    // 归一化到 0-24
+    h = ((h % 24) + 24) % 24;
+    return h;
+}
+
+// 应用指定小时的整体效果(大气 + 雾 + 体积云)
+export function applyDayPeriod(hour, fogEnabled = true) {
+    const viewer = window.viewer;
+    if (!viewer || !viewer.scene) return;
+    // 按季节偏移时段(日照时长差异)
+    const cfg = getDayPeriodConfig(getSeasonShiftedHour(hour));
+    // 叠加季节修正(先于天气)
+    applySeasonModifiers(cfg, seasonPresets[currentSeason]);
+    // 更新基准为时段+季节值,再叠加天气修正
+    baseCloud = {
+        coverage: cfg.cloud.coverage,
+        densityScale: cfg.cloud.densityScale,
+        sunIntensity: cfg.cloud.sunIntensity,
+        skyLightScale: cfg.cloud.skyLightScale,
+        cloudExposure: cfg.cloud.cloudExposure,
+        powderScale: cfg.cloud.powderScale,
+        sunColor: cfg.cloud.sunColor,
+        skyColor: cfg.cloud.skyColor,
+        minHeight: cfg.cloud.minHeight,
+        maxHeight: cfg.cloud.maxHeight
+    };
+    baseAtmosphere = { ...cfg.atmosphere };
+    baseFog = cfg.fog;
+    applyWeatherToScene();
+    viewer.scene.fog.enabled = fogEnabled;
+
+    // 场景暗化:夜晚整体压暗(地形/建筑统一变暗),白天保持原样
+    const daySun = cfg.cloud.sunIntensity;
+    dimFactor = Math.max(0.25, Math.min(1, 0.28 + 0.72 * Math.min(1, daySun / 18)));
+    ensureDimmerStage();
+}
+
+// ==============================================
+// 场景明暗调节后处理(夜晚压暗,白天不变)
+// ==============================================
+let dimmerStage = null;
+let dimFactor = 1.0;
+
+function ensureDimmerStage() {
+    const viewer = window.viewer;
+    if (!viewer || !viewer.scene || dimmerStage) return;
+    dimmerStage = new Cesium.PostProcessStage({
+        name: 'SceneDimmer',
+        fragmentShader: [
+            'in vec2 v_textureCoordinates;',
+            'uniform sampler2D colorTexture;',
+            'uniform float u_dim;',
+            'out vec4 out_FragColor;',
+            'void main() {',
+            '  vec4 color = texture(colorTexture, v_textureCoordinates);',
+            '  out_FragColor = vec4(color.rgb * u_dim, color.a);',
+            '}'
+        ].join('\n'),
+        uniforms: {
+            u_dim: () => dimFactor
+        }
+    });
+    viewer.scene.postProcessStages.add(dimmerStage);
+    dimmerStage.enabled = true;
+}
+
+// 设置场景时间并应用对应时段效果
+export function setSceneTime(date, fogEnabled = true) {
+    const viewer = window.viewer;
+    if (!viewer) return;
+    viewer.clock.currentTime = Cesium.JulianDate.fromDate(date);
+    const hour = date.getHours() + date.getMinutes() / 60;
+    applyDayPeriod(hour, fogEnabled);
+}
+
+// ==============================================
+// 天气预设系统(默认 = 多云 = 初始状态)
+// 天气只控制「云量」与「云亮度」:
+//  - 晴/多云 不修改大气渲染,保持与初始/当前时段一致
+//  - 阴/雨/雪 云量更多,云亮度更暗,天空灰暗
+// ==============================================
+const overcastBase = {
+    name: '阴天',
+    cloud: {
+        coverage: 0.55,            // 云量多(绝对控制,不随时段波动)
+        densityScale: 0.85,        // 云层密度 0.8~0.95
+        sunIntensityScale: 0.08,   // 太阳光 ×0.08 → 云变暗
+        skyLightScaleScale: 0.25,  // 天光 ×0.25 → 云不亮
+        exposureScale: 0.4,        // 曝光 ×0.4 → 更暗
+        powderScale: 0,            // 无高光金边
+        minHeightOffset: -200,     // 云层厚度拉满
+        maxHeightOffset: 2000
+    },
+    fogScale: 3,                     // 厚雾弥漫
+    atmosphere: {
+        saturationShift: -0.45,      // 天空灰白,无蓝天
+        brightnessShift: -0.4,       // 天空压暗
+        hueShift: 0
+    },
+    sunColor: [0.8, 0.82, 0.85],     // 6500K 冷灰白
+    skyColor: [0.55, 0.6, 0.72],
+    rain: false,
+    snow: false
+};
+
+const weatherPresets = {
+    // 晴天:云量少
+    sunny: {
+        name: '晴天',
+        cloud: {
+            coverage: 0.15
+        },
+        rain: false,
+        snow: false
+    },
+    // 多云(默认):与初始状态一致
+    cloudy: {
+        name: '多云',
+        cloud: {
+            coverage: 0.3
+        },
+        rain: false,
+        snow: false
+    },
+    overcast: {
+        ...overcastBase
+    },
+    rain: {
+        ...overcastBase,
+        name: '雨',
+        cloud: {
+            ...overcastBase.cloud,
+            coverage: 0.65,
+            densityScale: 0.9
+        },
+        rain: true
+    },
+    snow: {
+        ...overcastBase,
+        name: '雪',
+        cloud: {
+            ...overcastBase.cloud,
+            coverage: 0.5,
+            densityScale: 0.7
+        },
+        snow: true
+    }
+};
+
+// 当前天气类型(默认多云 = 初始状态)
+let currentWeather = 'cloudy';
+
+export function getWeatherType() {
+    return currentWeather;
+}
+
+// 天气修正基准(时段计算值;未拖时间时取初始场景值)
+let baseCloud = null;
+let baseAtmosphere = null;
+let baseFog = 0.0002;
+
+// 捕获当前场景值作为天气修正基准(初始状态)
+function captureBase() {
+    const viewer = window.viewer;
+    if (!viewer || !viewer.scene) return;
+    const stage = window.__volumetricCloudStage;
+    if (!stage || !stage.cloudParams) return;
+    const p = stage.cloudParams;
+    baseCloud = {
+        coverage: p.coverage,
+        densityScale: p.densityScale,
+        sunIntensity: p.sunIntensity,
+        skyLightScale: p.skyLightScale,
+        cloudExposure: p.cloudExposure,
+        powderScale: p.powderScale,
+        sunColor: p.sunColor ? [...p.sunColor] : [1.0, 0.95, 0.9],
+        skyColor: p.skyColor ? [...p.skyColor] : [0.4, 0.6, 1.0],
+        minHeight: p.minHeight,
+        maxHeight: p.maxHeight
+    };
+    baseAtmosphere = {
+        hueShift: viewer.scene.skyAtmosphere.hueShift,
+        saturationShift: viewer.scene.skyAtmosphere.saturationShift,
+        brightnessShift: viewer.scene.skyAtmosphere.brightnessShift
+    };
+    baseFog = viewer.scene.fog.density || 0.0002;
+}
+
+// 将当前天气修正应用到场景(基于基准值,避免叠加累积)
+function applyWeatherToScene() {
+    const viewer = window.viewer;
+    if (!viewer || !viewer.scene) return;
+    if (!baseCloud) captureBase();
+    if (!baseCloud) return;
+
+    const preset = weatherPresets[currentWeather] || weatherPresets.cloudy;
+    const w = preset.cloud || {};
+    const b = baseCloud;
+
+    // 体积云
+    const stage = window.__volumetricCloudStage;
+    if (stage && stage.cloudParams) {
+        const p = stage.cloudParams;
+        // 季节云量系数(春多 / 夏多 / 秋少 / 冬连片)
+        const season = seasonPresets[currentSeason] || {};
+        const seasonCov = (season.cloud && season.cloud.coverageScale) || 1.0;
+        // 云量:天气绝对控制 × 季节系数
+        p.coverage = Math.min(0.9, Math.max(0.02, (w.coverage !== undefined ? w.coverage : b.coverage) * seasonCov));
+        // 云密度
+        if (w.densityScale !== undefined) p.densityScale = w.densityScale;
+        else p.densityScale = b.densityScale;
+        // 云亮度
+        p.sunIntensity = b.sunIntensity * (w.sunIntensityScale !== undefined ? w.sunIntensityScale : 1.0);
+        p.skyLightScale = Math.min(2, Math.max(0, b.skyLightScale * (w.skyLightScaleScale !== undefined ? w.skyLightScaleScale : 1.0)));
+        p.cloudExposure = b.cloudExposure * (w.exposureScale !== undefined ? w.exposureScale : 1.0);
+        p.powderScale = (w.powderScale !== undefined) ? w.powderScale : b.powderScale;
+        // 云颜色:天气未指定时恢复基准
+        p.sunColor = preset.sunColor ? [...preset.sunColor] : (b.sunColor ? [...b.sunColor] : p.sunColor);
+        p.skyColor = preset.skyColor ? [...preset.skyColor] : (b.skyColor ? [...b.skyColor] : p.skyColor);
+        // 云高:天气未指定时恢复基准
+        p.minHeight = w.minHeightOffset ? Math.max(500, b.minHeight + w.minHeightOffset) : b.minHeight;
+        p.maxHeight = w.maxHeightOffset ? Math.min(12000, b.maxHeight + w.maxHeightOffset) : b.maxHeight;
+    }
+
+    // 大气渲染:始终恢复基准,再叠加天气偏移(晴/多云无偏移 → 与初始/时段一致)
+    const atm = viewer.scene.skyAtmosphere;
+    atm.hueShift = baseAtmosphere.hueShift + ((preset.atmosphere && preset.atmosphere.hueShift) || 0);
+    atm.saturationShift = baseAtmosphere.saturationShift + ((preset.atmosphere && preset.atmosphere.saturationShift) || 0);
+    atm.brightnessShift = baseAtmosphere.brightnessShift + ((preset.atmosphere && preset.atmosphere.brightnessShift) || 0);
+
+    // 雾浓度
+    viewer.scene.fog.density = Math.min(0.002, baseFog * (preset.fogScale || 1.0));
+
+    // 雨雪后处理
+    const stages = viewer.scene.postProcessStages;
+    if (stages.rain) stages.rain.enabled = !!preset.rain;
+    if (stages.snow) stages.snow.enabled = !!preset.snow;
+}
+
+// 切换天气:只应用天气修正,不重新计算时段(避免天空颜色变化)
+export function setWeatherType(type) {
+    if (!weatherPresets[type]) type = 'cloudy';
+    currentWeather = type;
+    applyWeatherToScene();
+    return currentWeather;
+}
+
+// ==============================================
+// 季节预设系统(默认 / 春 / 夏 / 秋 / 冬)
+// 季节在时段基准之上修正:大气蓝度、云层高度/密度/光照、雾浓度
+// ==============================================
+const seasonPresets = {
+    // 默认:当前基准,不做任何修正
+    'default': {
+        name: '默认',
+        cloud: null
+    },
+    // 春季:多浮尘、柔和淡蓝、碎云多、低云(日出稍早、日落稍晚)
+    spring: {
+        name: '春',
+        shiftMorning: 0.5,             // 日出提前 0.5h
+        shiftEvening: -0.5,            // 日落延后 0.5h
+        atmosphere: { hueShift: 0, saturationShift: -0.06, brightnessShift: -0.04 },
+        cloud: {
+            coverageScale: 1.15,         // 碎云多
+            minHeightOffset: -200,       // 低云 800-2500
+            maxHeightScale: 0.5,
+            densityScale: 0.8,           // 薄云透光
+            sunIntensityScale: 0.95
+        },
+        fogScale: 1.2                    // 浮尘雾,空气朦胧
+    },
+    // 夏季:饱和深蓝、厚积云、通透、霞光浓(亮得早、黑得晚)
+    summer: {
+        name: '夏',
+        shiftMorning: 1,                 // 日出提前 1h
+        shiftEvening: -1.5,              // 日落延后 1.5h
+        atmosphere: { hueShift: 0, saturationShift: 0.1, brightnessShift: 0.05 },
+        cloud: {
+            coverageScale: 1.2,
+            maxHeightOffset: 2000,       // 高空卷云至 10000m
+            densityScale: 1.1,           // 厚云立体
+            sunIntensityScale: 1.05      // 光照 1.05
+        },
+        fogScale: 0.6                    // 通透,远山清晰
+    },
+    // 秋季:高饱和淡蓝、轻薄高云、能见度最强(日出稍晚、日落稍早)
+    autumn: {
+        name: '秋',
+        shiftMorning: -0.5,              // 日出延后 0.5h
+        shiftEvening: 0.5,               // 日落提前 0.5h
+        atmosphere: { hueShift: 0, saturationShift: 0.06, brightnessShift: 0.08 },
+        cloud: {
+            coverageScale: 0.7,          // 云少
+            minHeightOffset: 3500,       // 高云为主
+            maxHeightScale: 1.1,
+            densityScale: 0.6,           // 轻薄透光
+            sunIntensityScale: 1.0
+        },
+        fogScale: 0.5                    // 秋高气爽
+    },
+    // 冬季:浅灰蓝、厚重层云、冷调寡淡(亮得晚、黑得早)
+    winter: {
+        name: '冬',
+        shiftMorning: -1.5,              // 日出延后 1.5h
+        shiftEvening: 1,                 // 日落提前 1h
+        atmosphere: { hueShift: 0, saturationShift: -0.12, brightnessShift: -0.1 },
+        cloud: {
+            coverageScale: 1.3,          // 连片覆盖
+            minHeightOffset: -600,       // 低云 600-2000
+            maxHeightScale: 0.35,
+            densityScale: 1.15,          // 厚重不透光
+            sunIntensityScale: 0.85      // 光照弱
+        },
+        fogScale: 1.4                    // 雾霾多发
+    }
+};
+
+// 当前季节类型(默认不修正)
+let currentSeason = 'default';
+
+export function getSeasonType() {
+    return currentSeason;
+}
+
+// 将季节修正叠加到时段配置上(默认季节不修正)
+function applySeasonModifiers(cfg, season) {
+    if (!season || !season.cloud) return;
+    // 大气(蓝度)
+    if (season.atmosphere) {
+        cfg.atmosphere.hueShift += season.atmosphere.hueShift || 0;
+        cfg.atmosphere.saturationShift += season.atmosphere.saturationShift || 0;
+        cfg.atmosphere.brightnessShift += season.atmosphere.brightnessShift || 0;
+    }
+    // 云
+    const c = cfg.cloud;
+    const m = season.cloud;
+    if (m.minHeightOffset) c.minHeight = Math.max(500, c.minHeight + m.minHeightOffset);
+    if (m.maxHeightOffset) c.maxHeight = Math.min(12000, c.maxHeight + m.maxHeightOffset);
+    if (m.maxHeightScale) c.maxHeight = Math.min(12000, c.maxHeight * m.maxHeightScale);
+    if (m.densityScale) c.densityScale = Math.min(1.5, c.densityScale * m.densityScale);
+    if (m.sunIntensityScale) c.sunIntensity = c.sunIntensity * m.sunIntensityScale;
+    // 云颜色由时段关键帧(日出/日落暖色)与天气预设决定,季节不覆盖
+    // 雾
+    if (season.fogScale) cfg.fog = Math.min(0.002, cfg.fog * season.fogScale);
+}
+
+// 切换季节:基于当前时钟时间重新计算 时段+季节+天气 整套效果
+export function setSeasonType(type) {
+    if (!seasonPresets[type]) type = 'default';
+    currentSeason = type;
+    const viewer = window.viewer;
+    if (viewer && viewer.clock) {
+        const date = Cesium.JulianDate.toDate(viewer.clock.currentTime);
+        const hour = date.getHours() + date.getMinutes() / 60;
+        applyDayPeriod(hour, true);
+    }
+    return currentSeason;
+}

+ 3 - 0
RuoYi-Vue3/src/supermap-cesium-module/style/components.scss

@@ -186,6 +186,9 @@
     #volume-panel{
       width:252px
     }
+    #volumetric-cloud-panel{
+      width: 280px;
+    }
   #mvtlayer-panel{
     width: 280px;
   }

+ 1 - 0
RuoYi-Vue3/src/supermap-cesium-module/style/globle.scss

@@ -18,6 +18,7 @@ body,
 }
 
 #cesiumContainer {
+  position: relative;
   width: 100%;
   height: 100%;
   margin: 0;

+ 1 - 1
RuoYi-Vue3/src/views/front/PageHeader.vue

@@ -6,7 +6,7 @@
           <img src="@/assets/images/水利logo.png" alt="水利logo" class="water-logo">
         </div>
         <div class="title-container">
-          <h1 class="site-title">水利部模型平台<span class="sub-title">可视化模型库</span></h1>
+          <h1 class="site-title">模型平台<span class="sub-title">可视化模型库</span></h1>
         </div>
       </div>
       

+ 1 - 1
RuoYi-Vue3/tests/basic.spec.ts

@@ -8,7 +8,7 @@ test.describe('基础页面测试', () => {
     await page.goto('/');
     
     // 检查页面标题
-    await expect(page).toHaveTitle(/若依管理系统/);
+    await expect(page).toHaveTitle(/可视化模型平台/);
     
     // 检查页面是否成功加载
     await expect(page.locator('body')).toBeVisible();

+ 6 - 2
RuoYi-Vue3/vite.config.js

@@ -38,10 +38,14 @@ export default defineConfig(({ mode, command }) => {
         'vue-router',
         'element-plus',
         '@element-plus/icons-vue',
-        'cesium'
+        'cesium',
+        'three',
+        'dat.gui'
       ],
-      exclude: ['@supermap/iclient3d-vue-for-webgl']
+      exclude: ['@supermap/icloud3d-vue-for-webgl']
     },
+    // 二进制文件作为静态资源处理
+    assetsInclude: ['**/*.bin'],
     // 打包配置
     build: {
       // https://vite.dev/config/build-options.html

+ 2071 - 0
ThreeGeospatialPipeline_temp.mjs

@@ -0,0 +1,2071 @@
+/**
+ * ThreeGeospatialPipeline - 精炼版体积云 + Bruneton 大气 + 空中透视一体化管线。
+ *
+ * 渲染顺序(与 three-geospatial 对齐):
+ *  1. PostProcessStage: 体积云 raymarch(含 BSM 采样、shadowLength、haze)
+ *  2. PostProcessStage: AtmospherePostProcess 天空
+ *  3. PostProcessStage: AerialPerspectiveEffect 几何透视 + tonemap
+ *
+ * BSM(Beer Shadow Map)和 TAA 通过原生 WebGL 在 preRender/postRender 执行。
+ * BSM 数据通过 setCloudShadow 同步到大气和 Aerial 两侧,实现丁达尔与地面云影。
+ */
+
+import * as dat from "dat.gui";
+import { AtmosphereParameters, PRECOMPUTE_CONSTANTS, getPrecomputeDefines, flattenAtmosphereUniform } from "./AtmosphereFromThreeGeospatial/AtmosphereParameters.js";
+import { AtmospherePostProcess } from "./AtmosphereFromThreeGeospatial/AtmospherePostProcess.js";
+import { AerialPerspectiveEffect } from "./AtmosphereFromThreeGeospatial/AerialPerspectiveEffect.js";
+import { loadBinThreeGeospatial, bindData3DTextureToCesiumContext } from "./loadBinThreeGeospatial.js";
+import { loadShaderSource } from "./shaderLoader.js";
+import {
+  DEFAULT_CLOUDS_ASSETS_BASE,
+  DEFAULT_BRUNETON_SHADER_BASE,
+  DEFAULT_BLUE_NOISE_URL,
+  DEFAULT_ATMOSPHERE_ASSETS_BASE,
+  DEFAULT_ATMOSPHERE_SHADER_BASE,
+} from "./assetPaths.js";
+
+const SHADOW_MAP_SIZE = 1024;
+const SHADOW_CASCADE_COUNT = 4;
+const SHADOW_RAY_FAR = 500000.0;
+const BSM_BLIT_SIZE = 1024;
+
+// ─── Cloud fragment shader (Bruneton integrated, no debug branches) ────────
+
+function getCloudFragmentShader() {
+  return /* glsl */ `
+const float RECIPROCAL_PI4 = 0.07957747154594767;
+const float EVOLUTION_SCALE = 2e4;
+
+uniform sampler2D colorTexture;
+uniform sampler2D depthTexture;
+uniform sampler3D u_shapeTexture;
+uniform sampler3D u_shapeDetailTexture;
+uniform sampler3D u_stbnTexture;
+uniform sampler2D u_weatherTexture;
+uniform sampler2D u_turbulenceTexture;
+uniform sampler2D u_blueNoise;
+uniform float u_blueNoiseScale;
+uniform float u_jitterStrength;
+
+uniform vec3 u_cameraPosition;
+uniform vec3 u_altitudeCorrection;
+uniform float u_cameraHeight;
+uniform float u_bottomRadius;
+uniform float u_minHeight;
+uniform float u_maxHeight;
+uniform vec4 u_minLayerHeights;
+uniform vec4 u_maxLayerHeights;
+uniform vec4 u_densityScales;
+uniform vec4 u_shapeAmounts;
+uniform vec4 u_shapeDetailAmounts;
+uniform vec4 u_weatherExponents;
+uniform vec4 u_shapeAlteringBiases;
+uniform vec4 u_coverageFilterWidths;
+uniform float u_maxSteps;
+uniform float u_maxStepsToSun;
+uniform float u_minStepSize;
+uniform float u_maxStepSize;
+uniform float u_maxRayDistance;
+uniform float u_cameraNear;
+uniform float u_shadowTopHeight;
+uniform int u_shadowLengthEnabled;
+uniform int u_hazeEnabled;
+uniform int u_maxShadowLengthIterationCount;
+uniform float u_minShadowLengthStepSize;
+uniform float u_maxShadowLengthRayDistance;
+uniform float u_hazeDensityScale;
+uniform float u_hazeExponent;
+uniform float u_hazeScatteringCoefficient;
+uniform float u_hazeAbsorptionCoefficient;
+uniform sampler2D u_shadowBuffer;
+uniform vec2 u_shadowTexelSize;
+uniform vec2 u_shadowIntervals[4];
+uniform mat4 u_shadowMatrices[4];
+uniform float u_shadowFar;
+uniform float u_maxShadowFilterRadius;
+uniform int u_useShadowBuffer;
+uniform float u_skyLightScale;
+uniform float u_weatherRepeat;
+uniform vec2 u_localWeatherOffset;
+uniform float u_shapeRepeat;
+uniform vec3 u_shapeOffset;
+uniform float u_shapeDetailRepeat;
+uniform vec3 u_shapeDetailOffset;
+uniform float u_turbulenceRepeat;
+uniform float u_turbulenceDisplacement;
+uniform vec4 u_coverages;
+uniform float u_coverageHaze;
+uniform float u_scatteringCoefficient;
+uniform float u_absorptionCoefficient;
+uniform float u_scatterG1;
+uniform float u_scatterG2;
+uniform float u_scatterMix;
+uniform float u_sunIntensity;
+uniform float u_skyToSunRatio;
+uniform float u_powderScale;
+uniform float u_powderExponent;
+uniform float u_aerialPerspectiveScale;
+uniform float u_cloudExposure;
+uniform float u_magentaFixStrength;
+uniform float u_edgeAlphaCutoff;
+uniform vec2 u_resolution;
+uniform float u_mipLevelScale;
+uniform float u_perspectiveStepScale;
+uniform float u_minDensity;
+uniform float u_minExtinction;
+uniform float u_minTransmittance;
+// 远处云密度距离衰减:从 u_distFadeStart(米)开始线性降到0,到 u_distFadeEnd 完全消失
+// 消除天际线附近云"堆在一起"的视觉拥挤
+uniform float u_distFadeStart;
+uniform float u_distFadeEnd;
+uniform float u_minSecondaryStepSize;
+uniform float u_secondaryStepScale;
+uniform int u_multiScatteringOctaves;
+uniform float u_lowLayerDensityBoost;
+uniform vec4 u_densityProfileExpTerms;
+uniform vec4 u_densityProfileExponents;
+uniform vec4 u_densityProfileLinearTerms;
+uniform vec4 u_densityProfileConstantTerms;
+uniform vec3 u_minIntervalHeights;
+uniform vec3 u_maxIntervalHeights;
+
+uniform sampler2D u_historyTexture;
+uniform mat4 u_prevViewProjection;
+uniform float u_temporalAlpha;
+uniform int u_temporalEnabled;
+uniform int u_frame;
+
+in vec2 v_textureCoordinates;
+
+vec3 ACESFilmic(vec3 x) {
+  float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
+  return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
+}
+
+float saturate(float x) { return clamp(x, 0.0, 1.0); }
+vec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); }
+float remap(float v, float a, float b, float c, float d) { return c + (v - a) * (d - c) / (b - a); }
+float remapClamped(float v, float a, float b, float c, float d) { return clamp(remap(v, a, b, c, d), min(c, d), max(c, d)); }
+float remapClamped(float v, float a, float b) { return clamp((v - a) / (b - a), 0.0, 1.0); }
+vec4 remap(vec4 v, vec4 a, vec4 b, vec4 c, vec4 d) { return c + (v - a) * (d - c) / (b - a); }
+vec4 remapClamped(vec4 v, vec4 a, vec4 b, vec4 c, vec4 d) { return clamp(remap(v, a, b, c, d), min(c, d), max(c, d)); }
+vec4 remapClamped(vec4 v, vec4 a, vec4 b) { return clamp((v - a) / (b - a), 0.0, 1.0); }
+
+vec3 reduceMagenta(vec3 color, float strength) {
+  float magenta = max(0.0, min(color.r, color.b) - color.g);
+  float fix = clamp(magenta * 5.0 * max(strength, 0.0), 0.0, 1.0);
+  float target = color.g;
+  color.r = mix(color.r, target, fix);
+  color.b = mix(color.b, target, fix);
+  return color;
+}
+
+vec2 raySphereIntersect(vec3 ro, vec3 rd, float radius) {
+  float b = dot(ro, rd);
+  float c = dot(ro, ro) - radius * radius;
+  float h = b * b - c;
+  if (h < 0.0) return vec2(-1.0);
+  h = sqrt(h);
+  return vec2(-b - h, -b + h);
+}
+
+void reconstructRay(out vec3 ro, out vec3 rd) {
+  ro = u_cameraPosition + u_altitudeCorrection;
+  vec2 uv = v_textureCoordinates * 2.0 - 1.0;
+  vec4 clipPos = vec4(uv, 1.0, 1.0);
+  vec4 viewPos = czm_inverseProjection * clipPos;
+  viewPos /= viewPos.w;
+  vec4 worldPos4 = czm_inverseView * viewPos;
+  vec3 worldPos = worldPos4.xyz + u_altitudeCorrection;
+  rd = normalize(worldPos - ro);
+}
+
+float getSTBN() {
+  // 与 three-geospatial 一致:按帧在 3D STBN 的 z 维切片轮换
+  ivec3 size = textureSize(u_stbnTexture, 0);
+  vec3 scale = 1.0 / vec3(size);
+  return texture(
+    u_stbnTexture,
+    vec3(gl_FragCoord.xy, float(u_frame % size.z)) * scale
+  ).r;
+}
+
+vec2 getCubeSphereUv(vec3 position) {
+  vec3 n = normalize(position);
+  vec3 f = abs(n);
+  vec3 c = n / max(f.x, max(f.y, f.z));
+  vec2 m;
+  if (f.y >= f.x && f.y >= f.z) { m = c.y > 0.0 ? vec2(-n.x, n.z) : n.xz; }
+  else if (f.x >= f.y && f.x >= f.z) { m = c.x > 0.0 ? n.yz : vec2(-n.y, n.z); }
+  else { m = c.z > 0.0 ? n.xy : vec2(n.x, -n.y); }
+  vec2 m2 = m * m;
+  float q = dot(m2.xy, vec2(-2.0, 2.0)) - 3.0;
+  float q2 = q * q;
+  vec2 uv;
+  uv.x = sqrt(1.5 + m2.x - m2.y - 0.5 * sqrt(max(0.0, -24.0 * m2.x + q2))) * (m.x > 0.0 ? 1.0 : -1.0);
+  uv.y = sqrt(6.0 / max(0.001, 3.0 - uv.x * uv.x)) * m.y;
+  return uv * 0.5 + 0.5;
+}
+vec2 getGlobeUv(vec3 position) { return getCubeSphereUv(position); }
+
+float getMipLevel(vec2 uv) {
+  vec2 coord = uv * u_resolution;
+  vec2 ddx_v = dFdx(coord);
+  vec2 ddy_v = dFdy(coord);
+  float deltaMaxSqr = max(dot(ddx_v, ddx_v), dot(ddy_v, ddy_v)) * 0.1;
+  return max(0.0, 0.5 * log2(max(1.0, deltaMaxSqr)));
+}
+
+bool inEmptySpace(float height) {
+  bvec3 gt = greaterThan(vec3(height), u_minIntervalHeights);
+  bvec3 lt = lessThan(vec3(height), u_maxIntervalHeights);
+  return gt.x && lt.x || gt.y && lt.y || gt.z && lt.z;
+}
+
+vec4 getLayerDensity(vec4 hf) {
+  return u_densityProfileExpTerms * exp(u_densityProfileExponents * hf) + u_densityProfileLinearTerms * hf + u_densityProfileConstantTerms;
+}
+
+vec4 getHeightFractions(float height) {
+  vec4 range = u_maxLayerHeights - u_minLayerHeights;
+  return clamp((vec4(height) - u_minLayerHeights) / max(range, vec4(0.0001)), 0.0, 1.0);
+}
+
+struct WeatherSample { vec4 heightFraction; vec4 density; };
+struct MediaSample { float density; vec4 weight; float scattering; float extinction; };
+
+vec4 shapeAlteringFunction(vec4 hf, vec4 bias) {
+  vec4 biased = pow(hf, bias);
+  vec4 x = clamp(biased * 2.0 - 1.0, -1.0, 1.0);
+  return 1.0 - x * x;
+}
+
+WeatherSample sampleWeather(vec2 uv, float height, float mipLevel) {
+  WeatherSample w;
+  w.heightFraction = getHeightFractions(height);
+  vec2 wUv = uv * u_weatherRepeat + u_localWeatherOffset;
+  vec4 localW = pow(textureLod(u_weatherTexture, wUv, mipLevel).rgba, u_weatherExponents);
+  vec4 hs = shapeAlteringFunction(w.heightFraction, u_shapeAlteringBiases);
+  vec4 factor = 1.0 - u_coverages * hs;
+  w.density = remapClamped(mix(localW, vec4(1.0), u_coverageFilterWidths), factor, factor + u_coverageFilterWidths);
+  return w;
+}
+
+MediaSample sampleMedia(WeatherSample weather, vec3 position, vec2 uv, float mipLevel, float jitter) {
+  vec4 density = weather.density;
+  vec3 sn = normalize(position);
+  vec3 evolution = -sn * length(u_localWeatherOffset) * EVOLUTION_SCALE;
+  vec2 tUv = uv * u_weatherRepeat * u_turbulenceRepeat;
+  vec3 turb = u_turbulenceDisplacement * (texture(u_turbulenceTexture, tUv).rgb * 2.0 - 1.0)
+      * dot(density, remapClamped(weather.heightFraction, vec4(0.3), vec4(0.0)));
+  vec3 sp = (position + evolution + turb) * u_shapeRepeat + u_shapeOffset;
+  float shapeTex = texture(u_shapeTexture, fract(sp)).r;
+  density = remapClamped(density, vec4(1.0 - shapeTex) * u_shapeAmounts, vec4(1.0));
+  if (mipLevel * 0.5 + (jitter - 0.5) * 0.5 < 0.5) {
+    vec3 dp = (position + turb) * u_shapeDetailRepeat + u_shapeDetailOffset;
+    float detail = texture(u_shapeDetailTexture, dp).r;
+    vec4 modifier = mix(vec4(pow(detail, 6.0)), vec4(1.0 - detail),
+        remapClamped(weather.heightFraction, vec4(0.2), vec4(0.4), vec4(0.0), vec4(1.0)));
+    modifier = mix(vec4(0.0), modifier, u_shapeDetailAmounts);
+    density = remapClamped(density * 2.0, vec4(modifier * 0.5), vec4(1.0));
+  }
+  density = saturate(density * u_densityScales * getLayerDensity(weather.heightFraction));
+  float ds = density.x + density.y + density.z + density.w;
+  MediaSample m;
+  m.density = ds;
+  m.weight = density / max(ds, 1e-7);
+  m.scattering = ds * u_scatteringCoefficient;
+  m.extinction = ds * u_absorptionCoefficient + m.scattering;
+  return m;
+}
+
+float henyeyGreenstein(float g, float cosTheta) {
+  float g2 = g * g;
+  return RECIPROCAL_PI4 * (1.0 - g2) / pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5);
+}
+
+float phaseFunction(float cosTheta, float attenuation) {
+  return mix(henyeyGreenstein(u_scatterG1 * attenuation, cosTheta),
+             henyeyGreenstein(u_scatterG2 * attenuation, cosTheta), u_scatterMix);
+}
+
+float approximateMultipleScattering(float opticalDepth, float cosTheta) {
+  vec3 coeffs = vec3(1.0);
+  const vec3 attenuation = vec3(0.5);
+  float scattering = 0.0;
+  for (int i = 0; i < 12; i++) {
+    if (i >= u_multiScatteringOctaves) break;
+    scattering += coeffs.x * exp(-opticalDepth * coeffs.y) * phaseFunction(cosTheta, coeffs.z);
+    coeffs *= attenuation;
+  }
+  return scattering;
+}
+
+float marchOpticalDepthToSun(vec3 rayOrigin, vec3 rayDirection, float mipLevel, float jitter, out float sunRayDist) {
+  float iterCount = max(0.0, remap(mipLevel, 0.0, 1.0, float(u_maxStepsToSun) + 1.0, 1.0) - jitter);
+  int ic = int(iterCount);
+  if (ic == 0) return 0.5;
+  float stepSize = u_minSecondaryStepSize / iterCount;
+  float nextDist = stepSize * jitter;
+  float od = 0.0;
+  sunRayDist = 0.0;
+  for (int i = 0; i < 8; i++) {
+    if (i >= ic) break;
+    sunRayDist = nextDist;
+    vec3 pos = rayDirection * nextDist + rayOrigin;
+    vec2 uv = getGlobeUv(pos);
+    float h = length(pos) - u_bottomRadius;
+    WeatherSample ws = sampleWeather(uv, h, mipLevel);
+    MediaSample ms = sampleMedia(ws, pos, uv, mipLevel, jitter);
+    od += ms.extinction * stepSize;
+    nextDist += stepSize;
+    stepSize *= u_secondaryStepScale;
+  }
+  return od;
+}
+
+bool rayIntersectsGround(vec3 camPos, vec3 rd) {
+  float r = length(camPos);
+  float mu = dot(camPos, rd) / r;
+  return mu < 0.0 && r * r * (mu * mu - 1.0) + u_bottomRadius * u_bottomRadius >= 0.0;
+}
+
+void raySphereIntersections(vec3 origin, vec3 direction, vec4 radius, out vec4 i1, out vec4 i2) {
+  float b = 2.0 * dot(direction, origin);
+  vec4 c = dot(origin, origin) - radius * radius;
+  vec4 disc = b * b - 4.0 * c;
+  vec4 mask = step(disc, vec4(0.0));
+  vec4 Q = sqrt(max(vec4(0.0), disc));
+  i1 = mix((-b - Q) * 0.5, vec4(-1.0), mask);
+  i2 = mix((-b + Q) * 0.5, vec4(-1.0), mask);
+}
+
+void getIntersections(vec3 camPos, vec3 rd, out bool ground, out vec4 first, out vec4 second) {
+  ground = rayIntersectsGround(camPos, rd);
+  vec4 radii = u_bottomRadius + vec4(0.0, u_minHeight, u_maxHeight, u_shadowTopHeight);
+  raySphereIntersections(camPos, rd, radii, first, second);
+}
+
+vec2 getRayNearFar(bool ground, vec4 first, vec4 second) {
+  vec2 nearFar;
+  if (u_cameraHeight < u_minHeight) {
+     if (ground) {
+        nearFar = vec2(-1.0);
+    } else {
+        nearFar = vec2(second.y, second.z);
+        nearFar.y = min(nearFar.y, u_maxRayDistance);
+    }
+  } else if (u_cameraHeight < u_maxHeight) {
+      if (ground) {
+          // 地面相交时,采样从相机近裁面到云层下边界
+          nearFar = vec2(u_cameraNear, first.y);
+          if (nearFar.y <= nearFar.x) nearFar = vec2(-1.0);
+      } else {
+          float farExit = max(max(first.y, second.y), max(first.z, second.z));
+          if (farExit <= 0.0) {
+          // 无有效远边界时,强制采样到最大射线距离
+          farExit = u_maxRayDistance;
+          }
+          farExit = min(farExit, u_maxRayDistance);
+          farExit = max(farExit, u_cameraNear + u_minStepSize * 0.5);
+          nearFar = vec2(u_cameraNear, farExit);
+      }
+  } else {
+      float farExit = max(max(first.y, second.y), max(first.z, second.z));
+      if (farExit > 0.0) {
+          farExit = min(farExit, u_maxRayDistance);
+          farExit = max(farExit, u_cameraNear + u_minStepSize * 0.5);
+          nearFar = vec2(u_cameraNear, farExit);
+      }
+  }
+  return nearFar;
+}
+
+vec2 getShadowRayNearFar(bool ground, vec4 first, vec4 second) {
+  vec2 nf;
+  if (u_cameraHeight < u_shadowTopHeight) {
+    nf = ground ? vec2(u_cameraNear, first.x) : vec2(u_cameraNear, second.w);
+  } else {
+    nf = vec2(first.w, second.w);
+    if (ground) nf.y = first.x;
+  }
+  nf.y = min(nf.y, u_maxShadowLengthRayDistance);
+  return nf;
+}
+
+vec2 getHazeRayNearFar(bool ground, vec4 first, vec4 second) {
+  vec2 nf;
+  if (u_cameraHeight < u_maxHeight) {
+    nf = ground ? vec2(u_cameraNear, first.x) : vec2(u_cameraNear, second.z);
+  } else {
+    nf = vec2(u_cameraNear, second.z);
+    if (ground) nf.y = first.x;
+  }
+  return nf;
+}
+
+// ── BSM sampling ──
+float sampleShadowOpticalDepth(vec3 rayPosition, float distanceOffset, float radius, float jitter);
+
+float getDistanceToShadowTop(vec3 rayPos) {
+  vec3 rd = czm_sunDirectionWC;
+  float R = u_bottomRadius + u_shadowTopHeight;
+  float b = dot(rayPos, rd);
+  float c = dot(rayPos, rayPos) - R * R;
+  float h = b * b - c;
+  if (h < 0.0) return -1.0;
+  return -b + sqrt(h);
+}
+
+// three.js / CloudShadowPass intervals=(d-near)/(far-near) 一致
+float viewZToOrthographicDepth(float viewZ, float near, float far) {
+  return (viewZ + near) / (near - far);
+}
+
+int getFadedCascadeIndex(mat4 viewMat, vec3 worldPos, vec2 intervals[4], float near, float far, float jitter) {
+  vec4 vp = viewMat * vec4(worldPos, 1.0);
+  float depth = viewZToOrthographicDepth(vp.z, near, far);
+  int nextIndex = -1, prevIndex = -1;
+  float alpha = 1.0;
+  for (int i = 0; i < 4; ++i) {
+    vec2 interval = intervals[i];
+    float intervalCenter = (interval.x + interval.y) * 0.5;
+    float closestEdge = depth < intervalCenter ? interval.x : interval.y;
+    float margin = closestEdge * closestEdge * 0.5;
+    interval += margin * vec2(-0.5, 0.5);
+    if (i < 3) {
+      if (depth >= interval.x && depth < interval.y) { prevIndex = nextIndex; nextIndex = i; alpha = saturate(min(depth - interval.x, interval.y - depth) / max(margin, 1e-6)); }
+    } else {
+      if (depth >= interval.x) { prevIndex = nextIndex; nextIndex = i; alpha = saturate((depth - interval.x) / max(margin, 1e-6)); }
+    }
+  }
+  return jitter <= alpha ? nextIndex : prevIndex;
+}
+
+vec2 getShadowUv(vec3 pos, int ci) { vec4 clip = u_shadowMatrices[ci] * vec4(pos, 1.0); clip /= clip.w; return clip.xy * 0.5 + 0.5; }
+vec2 getShadowAtlasOffset(int ci) { return vec2(mod(float(ci), 2.0) * 0.5, (ci < 2) ? 0.5 : 0.0); }
+
+float readShadowOpticalDepth(vec2 uv, int ci, float distToTop, float distOff) {
+  if (u_useShadowBuffer == 0) return 0.0;
+  vec2 atlasUv = getShadowAtlasOffset(ci) + uv * 0.5;
+  vec4 shadow = texture(u_shadowBuffer, atlasUv);
+  float distToFront = max(0.0, distToTop - distOff - shadow.r);
+  return min(shadow.b + shadow.a, shadow.g * distToFront);
+}
+
+float interleavedGradientNoise(vec2 coord) {
+  const vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189);
+  return fract(magic.z * fract(dot(coord, magic.xy)));
+}
+
+vec2 vogelDisk(int index, int count, float phi) {
+  const float goldenAngle = 2.39996322972865332;
+  float r = sqrt(float(index) + 0.5) / sqrt(float(count));
+  float theta = float(index) * goldenAngle + phi;
+  return r * vec2(cos(theta), sin(theta));
+}
+
+float sampleShadowOpticalDepthPCF(vec3 worldPos, float distToTop, float distOff, float radius, int ci) {
+  vec2 uv = getShadowUv(worldPos, ci);
+  if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return 0.0;
+  if (radius < 0.1) return readShadowOpticalDepth(uv, ci, distToTop, distOff);
+  float sum = 0.0;
+  float phi = interleavedGradientNoise(gl_FragCoord.xy) * 3.14159265 * 2.0;
+  for (int i = 0; i < 16; ++i) sum += readShadowOpticalDepth(uv + vogelDisk(i, 16, phi) * radius * u_shadowTexelSize, ci, distToTop, distOff);
+  return sum / 16.0;
+}
+
+float sampleShadowOpticalDepth(vec3 rayPos, float distOff, float radius, float jitter) {
+  float distToTop = getDistanceToShadowTop(rayPos);
+  if (distToTop <= 0.0) return 0.0;
+  int ci = getFadedCascadeIndex(czm_view, rayPos, u_shadowIntervals, u_cameraNear, u_shadowFar, jitter);
+  return ci >= 0 ? sampleShadowOpticalDepthPCF(rayPos, distToTop, distOff, radius, ci) : 0.0;
+}
+
+float marchShadowLength(vec3 rayOrigin, vec3 rayDir, vec2 rayNearFar, float jitter) {
+  float shadowLen = 0.0;
+  float maxDist = rayNearFar.y - rayNearFar.x;
+  float stepSize = u_minShadowLengthStepSize;
+  float rayDist = stepSize * jitter;
+  for (int i = 0; i < 512; i++) {
+    if (float(i) >= float(u_maxShadowLengthIterationCount)) break;
+    if (rayDist > maxDist) break;
+    vec3 pos = rayDir * rayDist + rayOrigin;
+    float od = sampleShadowOpticalDepth(pos, 0.0, 0.0, jitter);
+    shadowLen += (1.0 - exp(-od)) * stepSize;
+    stepSize *= u_perspectiveStepScale;
+    rayDist += stepSize;
+  }
+  return shadowLen;
+}
+
+#ifdef USE_ATMOSPHERE_IRRADIANCE
+void applyAerialPerspective(vec3 camPos, vec3 frontPos, float dist, float shadowLen, inout vec4 color) {
+  vec3 transmittance;
+  vec3 inscatter = GetSkyRadianceToPoint(camPos * METER_TO_LENGTH_UNIT, frontPos * METER_TO_LENGTH_UNIT, shadowLen * METER_TO_LENGTH_UNIT, sunDirection, transmittance);
+  float horizonBias = smoothstep(20.0, 80.0, dist * METER_TO_LENGTH_UNIT);
+  // 【新增】根据太阳高度计算可见度,晚上太阳沉下地平线时 sunVis 为 0
+  float sunVis = smoothstep(-0.02, 0.05, dot(normalize(camPos), sunDirection));
+  vec3 fakeHorizonColor = vec3(0.5, 0.6, 0.8) * 0.2 * sunVis;
+  inscatter = mix(inscatter, inscatter * 0.4 + fakeHorizonColor, horizonBias);
+  color.rgb = color.rgb * transmittance + inscatter * color.a * u_aerialPerspectiveScale;
+}
+#else
+void applyAerialPerspective(vec3 camPos, vec3 frontPos, float dist, float shadowLen, inout vec4 color) {
+  vec3 rayleigh = vec3(0.005802, 0.013558, 0.033100) * 0.001;
+  float h = length(frontPos) - u_bottomRadius;
+  float density = exp(-h / 8000.0);
+  vec3 transmittance = exp(-dist * rayleigh * density * u_aerialPerspectiveScale);
+  // 【新增】太阳可见度衰减
+  float sunVis = smoothstep(-0.05, 0.1, dot(normalize(camPos), sunDirection));
+  vec3 skyColor = vec3(0.4, 0.6, 1.0) * u_sunIntensity * 0.02 * sunVis;
+  color.rgb = color.rgb * transmittance + skyColor * (1.0 - transmittance) * color.a;
+}
+#endif
+
+vec4 approximateHaze(vec3 ro, vec3 rd, float maxDist, float cosTheta, float shadowLen) {
+  float modulation = remapClamped(u_coverageHaze, 0.2, 0.4);
+  if (u_cameraHeight * modulation < 0.0) return vec4(0.0);
+  float density = modulation * u_hazeDensityScale * exp(-u_cameraHeight * u_hazeExponent);
+  if (density < 1e-7) return vec4(0.0);
+  vec3 nOrigin = normalize(ro);
+  float sunHeight = dot(nOrigin, sunDirection);
+  float sunVis = smoothstep(-0.02, 0.05, sunHeight);
+  float viewZenith = abs(rd.y);
+  float horizonTaming = smoothstep(0.0, 0.15, viewZenith);
+  // 即使在天际线,也要保留一点点基础亮度,但不能是 1.0
+  horizonTaming = mix(0.3, 1.0, horizonTaming);
+  vec3 nHoriz = (ro - dot(ro, rd) * rd) / u_bottomRadius;
+  float alpha = remapClamped(dot(nOrigin, nHoriz), 0.9, 1.0);
+  vec3 normal = mix(nOrigin, nHoriz, alpha);
+  float angle = max(dot(normal, rd), 1e-5);
+  float exponent = angle * u_hazeExponent;
+  float linearTerm = density / u_hazeExponent / angle;
+  float expTerm = 1.0 - exp(-maxDist * exponent);
+  float shadowExpTerm = 1.0 - exp(-min(maxDist, shadowLen) * exponent);
+  float opticalDepth = expTerm * linearTerm;
+  float shadowOD = max((expTerm - shadowExpTerm) * linearTerm, 0.0);
+  float transmittance = saturate(1.0 - exp(-opticalDepth));
+  float shadowTransmittance = saturate(1.0 - exp(-shadowOD));
+  // 【修改】将硬编码的光源强度乘以太阳可见度
+  vec3 skyIrradiance = vec3(0.4, 0.6, 1.0) * u_sunIntensity * 0.04 * sunVis * horizonTaming;
+  vec3 sunIrradiance = vec3(1.0, 0.95, 0.9) * u_sunIntensity * sunVis;
+  float ph = henyeyGreenstein(u_scatterG1, cosTheta) * (1.0 - u_scatterMix) + henyeyGreenstein(u_scatterG2, cosTheta) * u_scatterMix;
+  vec3 inscatter = sunIrradiance * ph * shadowTransmittance + skyIrradiance * RECIPROCAL_PI4 * u_skyLightScale * transmittance;
+  inscatter *= u_hazeScatteringCoefficient / (u_hazeAbsorptionCoefficient + u_hazeScatteringCoefficient);
+  return vec4(inscatter, transmittance);
+}
+
+// ── Main raymarch ──
+vec4 marchClouds(vec3 rayOrigin, vec3 rd, vec2 rayNearFar, float cosTheta, float jitter, float rayStartTexels, out float frontDepth) {
+  float maxDist = min(rayNearFar.y - rayNearFar.x, u_maxRayDistance);
+  vec3 radInt = vec3(0.0);
+  float transInt = 1.0, wdSum = 0.0, tSum = 0.0;
+  float perspDist = min(rayNearFar.x, 3000.0);
+  float stepSize = u_minStepSize + (u_perspectiveStepScale - 1.0) * perspDist;
+  float rayDist = stepSize * jitter * 2.0;
+  #ifdef USE_ATMOSPHERE_IRRADIANCE
+  float refRadius = u_bottomRadius;
+  #else
+  vec3 sunColorBase = vec3(1.0, 0.95, 0.9) * u_sunIntensity;
+  vec3 skyColorBase = vec3(0.4, 0.6, 1.0) * u_sunIntensity * u_skyToSunRatio;
+  float refRadius = u_bottomRadius;
+  vec3 sunDirection = czm_sunDirectionWC;
+  #endif
+
+  for (int i = 0; i < 512; i++) {
+    if (float(i) >= u_maxSteps) break;
+    if (rayDist > maxDist) break;
+    if (transInt <= u_minTransmittance) break;
+    vec3 position = rayOrigin + rd * rayDist;
+    float height = length(position) - refRadius;
+    float mipLevel = log2(max(1.0, rayStartTexels + rayDist * 1e-5));
+    if (inEmptySpace(height)) { stepSize *= u_perspectiveStepScale; rayDist += mix(stepSize, u_maxStepSize, min(1.0, mipLevel)); continue; }
+    vec2 uv = getGlobeUv(position);
+    WeatherSample weather = sampleWeather(uv, height, mipLevel);
+    if (!any(greaterThan(weather.density, vec4(u_minDensity)))) { stepSize *= u_perspectiveStepScale; rayDist += mix(stepSize, u_maxStepSize, min(1.0, mipLevel)); continue; }
+    weather.density.xy *= u_lowLayerDensityBoost;
+    MediaSample media = sampleMedia(weather, position, uv, mipLevel, jitter);
+    if (media.extinction > u_minExtinction) {
+      #ifdef USE_ATMOSPHERE_IRRADIANCE
+      vec3 skyIrradiance;
+      vec3 sunIrradiance = GetSunAndSkyScalarIrradiance(position * METER_TO_LENGTH_UNIT, sunDirection, skyIrradiance);
+      float skyGradient = dot(weather.heightFraction * 0.5 + 0.5, media.weight);
+      vec3 sunColor = sunIrradiance * u_sunIntensity;
+      vec3 skyColor = skyIrradiance * u_sunIntensity * u_skyToSunRatio;
+      #else
+      float heightAlpha = clamp((height - u_minHeight) / max(u_maxHeight - u_minHeight, 1.0), 0.0, 1.0);
+      vec3 sunColor = mix(sunColorBase * 0.85, sunColorBase, heightAlpha);
+      vec3 skyColor = mix(skyColorBase * 0.6, skyColorBase, heightAlpha);
+      float skyGradient = dot(weather.heightFraction * 0.5 + 0.5, media.weight);
+      #endif
+      float sunRayDist;
+      float opticalDepth = marchOpticalDepthToSun(position, sunDirection, mipLevel, jitter, sunRayDist);
+      if (length(position) - refRadius < u_shadowTopHeight && u_useShadowBuffer == 1) {
+        vec3 sn = normalize(position);
+        float r = u_maxShadowFilterRadius * remapClamped(dot(sunDirection, sn), 0.1, 0.0);
+        opticalDepth += sampleShadowOpticalDepth(position, sunRayDist, r, jitter);
+      }
+      vec3 radiance = sunColor * approximateMultipleScattering(opticalDepth, cosTheta);
+      radiance += skyColor * RECIPROCAL_PI4 * skyGradient * u_skyLightScale;
+      radiance *= media.scattering * (1.0 - u_powderScale * exp(-media.extinction * u_powderExponent));
+      float transmittance = exp(-media.extinction * stepSize);
+      vec3 scatInt = (radiance - radiance * transmittance) / max(media.extinction, 1e-7);
+      radInt += transInt * scatInt;
+      transInt *= transmittance;
+      wdSum += rayDist * transInt;
+      tSum += transInt;
+    }
+    stepSize *= u_perspectiveStepScale;
+    rayDist += stepSize;
+  }
+  frontDepth = tSum > 0.0 ? wdSum / tSum : -1.0;
+  float alpha = saturate(remapClamped(transInt, 1.0, u_minTransmittance));
+  return vec4(radInt, alpha);
+}
+
+void main() {
+  vec4 sceneColor = texture(colorTexture, v_textureCoordinates);
+  float depth = czm_readDepth(depthTexture, v_textureCoordinates);
+  vec3 ro, rd;
+  reconstructRay(ro, rd);
+  #ifndef USE_ATMOSPHERE_IRRADIANCE
+  vec3 sunDirection = czm_sunDirectionWC;
+  #endif
+  float jitter = getSTBN();
+  bool ground; vec4 first, second;
+  getIntersections(ro, rd, ground, first, second);
+  vec2 rayNearFar = getRayNearFar(ground, first, second);
+  vec2 shadowNF = vec2(-1.0), hazeNF = vec2(-1.0);
+  if (u_shadowLengthEnabled == 1) shadowNF = getShadowRayNearFar(ground, first, second);
+  if (u_hazeEnabled == 1) hazeNF = getHazeRayNearFar(ground, first, second);
+
+  // depthTestAgainstTerrain 只影响 Globe/贴地物体与地形网格的深度关系,不能替后处理修正「沿像素射线」的距离。
+  // 此处必须用 inverseView 还原命中点,再沿 rd 求距离;用 -viewZ/dot(rd, forward) 在离轴像素上会偏大 → 云压在地形前。
+  float rayDistToScene = 0.0;
+  if (depth < 1.0 - 1e-7) {
+    vec4 eyePos = czm_windowToEyeCoordinates(vec4(gl_FragCoord.xy, depth, 1.0));
+    if (abs(eyePos.w) > 1e-6) {
+      eyePos /= eyePos.w;
+      if (eyePos.z < 0.0) {
+        vec4 worldPos4 = czm_inverseView * eyePos;
+        vec3 worldHit = worldPos4.xyz + u_altitudeCorrection;
+        rayDistToScene = max(0.0, dot(worldHit - ro, rd));
+      }
+    }
+  }
+  float tMax = rayNearFar.y;
+  // 原逻辑:低于云层且 !ground 时跳过深度钳位 —— 平视/看山体时 ground 常为 false,会整屏不钳位 → 云盖住地形。
+  // 仅当该像素无场景深度(天空)时才允许跳过;有地形/几何时必须用 rayDistToScene 截断射线。
+  const float DEPTH_SKY = 1.0 - 1e-7;
+  bool skipDepthClamp =
+    (depth >= DEPTH_SKY) && (u_cameraHeight < u_minHeight) && (!ground);
+  if (rayDistToScene > 0.0 && !skipDepthClamp) {
+    tMax = min(tMax, rayDistToScene);
+    if (u_shadowLengthEnabled == 1 && shadowNF.y > 0.0) shadowNF.y = min(shadowNF.y, rayDistToScene);
+    if (u_hazeEnabled == 1 && hazeNF.y > 0.0) hazeNF.y = min(hazeNF.y, rayDistToScene);
+  }
+  if (rayNearFar.x >= tMax) { gl_FragColor = sceneColor; return; }
+
+  float frontDepth;
+  float cosTheta = dot(rd, sunDirection);
+  vec2 globeUv = getGlobeUv(ro + rd * rayNearFar.x);
+  float mipLevel = getMipLevel(globeUv * u_weatherRepeat) * u_mipLevelScale;
+  mipLevel = mix(0.0, mipLevel, min(1.0, 0.2 * u_cameraHeight / max(u_maxHeight, 1.0)));
+  vec4 cloudColor = marchClouds(ro + rd * rayNearFar.x, rd, vec2(rayNearFar.x, tMax), cosTheta, jitter, pow(2.0, mipLevel), frontDepth);
+
+  // 远处云透明度距离衰减:用"相机到云层入口距离"(rayNearFar.x)衰减 alpha,
+  // 而非云内穿行距离。天顶云入口近不衰减,天际线云入口远衰减——只压远处透明度,不影响各层密度。
+  // 这解决斜射时云层路径长导致 alpha 堆积的问题,且不误伤高空稀疏层(层2 近处不衰减)。
+  float entryFade = 1.0 - smoothstep(u_distFadeStart, u_distFadeEnd, rayNearFar.x);
+  cloudColor.a *= entryFade;
+  cloudColor.rgb *= entryFade;
+
+  float shadowLen = 0.0;
+  bool hitClouds = frontDepth > 0.0 && cloudColor.a > max(u_edgeAlphaCutoff, 0.02);
+  float rayFrontT = rayNearFar.x + frontDepth;
+
+  if (hitClouds) {
+    if (u_shadowLengthEnabled == 1 && all(greaterThanEqual(shadowNF, vec2(0.0)))) {
+      shadowNF.y = mix(shadowNF.y, min(rayFrontT, shadowNF.y), cloudColor.a);
+      shadowLen = marchShadowLength(ro + rd * shadowNF.x, rd, shadowNF, jitter);
+    }
+    if (u_hazeEnabled == 1 && all(greaterThanEqual(hazeNF, vec2(0.0))))
+      hazeNF.y = mix(hazeNF.y, min(rayFrontT, hazeNF.y), cloudColor.a);
+      applyAerialPerspective(ro, ro + rd * rayFrontT, rayFrontT, shadowLen, cloudColor);
+    } else if (u_shadowLengthEnabled == 1 && all(greaterThanEqual(shadowNF, vec2(0.0)))) {
+    shadowLen = marchShadowLength(ro + rd * shadowNF.x, rd, shadowNF, jitter);
+  }
+
+  if (u_hazeEnabled == 1) {
+    float hazeDist = all(greaterThanEqual(hazeNF, vec2(0.0))) ? (hazeNF.y - hazeNF.x) : 0.0;
+    vec4 haze = approximateHaze(ro, rd, hazeDist, cosTheta, shadowLen);
+    cloudColor.rgb = mix(cloudColor.rgb, haze.rgb, haze.a);
+    cloudColor.a = cloudColor.a * (1.0 - haze.a) + haze.a;
+  }
+
+  // 边缘裁剪:低 alpha 区域直接清零,避免云边缘细碎噪点与闪烁
+  if (cloudColor.a < u_edgeAlphaCutoff) {
+    cloudColor = vec4(0.0);
+  }
+  // 边缘裁剪后再判一次:防止“已被裁掉的薄云像素”仍进入 TAA,导致底层模型抖动
+  hitClouds = hitClouds && (cloudColor.a > max(u_edgeAlphaCutoff, 0.02));
+
+  // 边缘稳噪:低 alpha 处直接除以 alpha 会把随机误差放大成亮点/闪点
+  float edgeSafeAlpha = max(cloudColor.a, 0.08);
+  vec3 cloudActual = cloudColor.rgb / edgeSafeAlpha;
+  cloudActual = ACESFilmic(cloudActual * u_cloudExposure);
+  cloudActual = pow(cloudActual, vec3(1.0 / 2.2));
+
+  vec4 composited = vec4(
+    sceneColor.rgb * (1.0 - cloudColor.a) + cloudActual * cloudColor.a,
+    // 让 history.a 表示“云覆盖度”,用于后续 TAA 历史有效性判定
+    cloudColor.a
+  );
+  // 在最终云合成色上去品红,按云覆盖度加权,确保无云区域不受影响
+  vec3 compositedNoMagenta = reduceMagenta(composited.rgb, u_magentaFixStrength);
+  float cloudW = smoothstep(0.02, 0.3, cloudColor.a);
+  composited.rgb = mix(composited.rgb, compositedNoMagenta, cloudW);
+
+  if (u_temporalEnabled > 0 && hitClouds) {
+    vec3 worldPos = ro + rd * rayFrontT - u_altitudeCorrection;
+    vec4 prevClip = u_prevViewProjection * vec4(worldPos, 1.0);
+    vec2 prevUv = (prevClip.xy / prevClip.w) * 0.5 + 0.5;
+    if (prevUv.x >= 0.0 && prevUv.x <= 1.0 && prevUv.y >= 0.0 && prevUv.y <= 1.0) {
+      vec4 history = texture(u_historyTexture, prevUv);
+      // TAA 仅对“云增量”做融合,底层模型保持当前帧,减少模型虚影
+      vec3 deltaNow = composited.rgb - sceneColor.rgb;
+      vec3 deltaHist = history.rgb - sceneColor.rgb;
+      float maxDiff = max(abs(deltaHist.r - deltaNow.r), max(abs(deltaHist.g - deltaNow.g), abs(deltaHist.b - deltaNow.b)));
+      float reject = max(
+        smoothstep(0.35, 0.75, maxDiff),
+        smoothstep(0.004, 0.03, length(prevUv - v_textureCoordinates))
+      );
+      // 让低透明边缘也参与历史融合,抑制云边缘噪点“跳闪”
+      float opacityW = smoothstep(0.015, 0.25, cloudColor.a);
+      float a = mix(1.0, mix(u_temporalAlpha, 1.0, reject), opacityW);
+      // 仅当“当前与历史”都存在足够云覆盖时才使用历史,避免把模型底色抖动带入
+      float historyCloudW = smoothstep(0.02, 0.12, history.a);
+      float currentCloudW = smoothstep(0.02, 0.12, cloudColor.a);
+      float cloudHistoryValidity = min(historyCloudW, currentCloudW);
+      a = mix(1.0, a, cloudHistoryValidity);
+      vec3 deltaFiltered = mix(deltaHist, deltaNow, a);
+      composited.rgb = sceneColor.rgb + deltaFiltered;
+      composited.a = cloudColor.a;
+    }
+  }
+  gl_FragColor = composited;
+}
+`;
+}
+
+// ─── Helper: compile & link GL program ─────────────────────────────────────
+
+function createGLProgram(gl, vsSource, fsSource, label) {
+  const vs = gl.createShader(gl.VERTEX_SHADER);
+  gl.shaderSource(vs, vsSource);
+  gl.compileShader(vs);
+  if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) { console.error(`[${label}] VS:`, gl.getShaderInfoLog(vs)); gl.deleteShader(vs); return null; }
+  const fs = gl.createShader(gl.FRAGMENT_SHADER);
+  gl.shaderSource(fs, fsSource);
+  gl.compileShader(fs);
+  if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) { console.error(`[${label}] FS:`, gl.getShaderInfoLog(fs)); gl.deleteShader(vs); gl.deleteShader(fs); return null; }
+  const prog = gl.createProgram();
+  gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog);
+  gl.deleteShader(vs); gl.deleteShader(fs);
+  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { console.error(`[${label}] link:`, gl.getProgramInfoLog(prog)); gl.deleteProgram(prog); return null; }
+  return prog;
+}
+
+// ─── Main pipeline class ──────────────────────────────────────────────────
+
+export class ThreeGeospatialPipeline {
+  constructor(viewer, options = {}) {
+    this.viewer = viewer;
+    this.atmosphereParams = options.atmosphereParams ?? new AtmosphereParameters();
+    this._frameCount = 0;
+    this._gui = null;
+
+    // 可配置的资源/shader 根路径(均带默认值,便于在任意部署路径下使用)
+    this.assetsBase = options.cloudsAssetsBase ?? DEFAULT_CLOUDS_ASSETS_BASE;
+    this.brunetonShaderBase = options.brunetonShaderBase ?? DEFAULT_BRUNETON_SHADER_BASE;
+    this.blueNoiseUrl = options.blueNoiseUrl ?? DEFAULT_BLUE_NOISE_URL;
+    this.atmosphereAssetsBase = options.atmosphereAssetsBase ?? DEFAULT_ATMOSPHERE_ASSETS_BASE;
+    this.atmosphereShaderBase = options.atmosphereShaderBase ?? DEFAULT_ATMOSPHERE_SHADER_BASE;
+
+    this.params = {
+      cloudsVisible: true,
+      bottomRadius: 6371860,
+      layers: [
+        { channel: 'r', altitude: 1800, height: 650, densityScale: 0.2, shapeAmount: 1.0, shapeDetailAmount: 1.0, weatherExponent: 1.0, shapeAlteringBias: 0.35, coverageFilterWidth: 0.6, coverage: 0.3, densityProfile: { expTerm: 0, exponent: 0, linearTerm: 0.75, constantTerm: 0.25 } },
+        { channel: 'g', altitude: 2400, height: 1200, densityScale: 0.2, shapeAmount: 1.0, shapeDetailAmount: 1.0, weatherExponent: 1.0, shapeAlteringBias: 0.35, coverageFilterWidth: 0.6, coverage: 0.3, densityProfile: { expTerm: 0, exponent: 0, linearTerm: 0.75, constantTerm: 0.25 } },
+        { channel: 'b', altitude: 7500, height: 500, densityScale: 0.003, shapeAmount: 0.4, shapeDetailAmount: 0.0, weatherExponent: 1.0, shapeAlteringBias: 0.35, coverageFilterWidth: 0.5, coverage: 0.3, densityProfile: { expTerm: 0, exponent: 0, linearTerm: 0.75, constantTerm: 0.25 } },
+        { channel: 'a' }
+      ],
+      maxSteps: 500, maxStepsToSun: 8, minStepSize: 20.0, maxStepSize: 1000.0, maxRayDistance: 200000.0,
+      perspectiveStepScale: 1.005, minDensity: 1e-5, minExtinction: 1e-5, minTransmittance: 0.01,
+      // 远处云距离衰减(米):天际线附近射线斜穿云层累积过密,从 distFadeStart 起线性衰减到 distFadeEnd 完全消失
+      distFadeStart: 11000.0, distFadeEnd: 51000.0,
+      minSecondaryStepSize: 100.0, secondaryStepScale: 2.0, multiScatteringOctaves: 8, lowLayerDensityBoost: 1.0,
+      shadowLengthEnabled: true, useShadowBuffer: true, hazeEnabled: false,
+      maxShadowLengthIterationCount: 500, minShadowLengthStepSize: 50.0, maxShadowLengthRayDistance: 200000.0,
+      hazeDensityScale: 3e-5, hazeExponent: 1e-3, hazeScatteringCoefficient: 0.9, hazeAbsorptionCoefficient: 0.5,
+      weatherRepeat: 100.0, shapeRepeat: 4.1, shapeDetailRepeat: 0.0005,
+      turbulenceRepeat: 2.0, turbulenceDisplacement: 400.0,
+      scatteringCoefficient: 1.0, absorptionCoefficient: 0.0,
+      scatterG1: 0.7, scatterG2: -0.2, scatterMix: 0.5,
+      sunIntensity: 20.0, skyLightScale: 1.0, skyToSunRatio: 0.28,
+      powderScale: 0.8, powderExponent: 150.0,
+      aerialPerspectiveScale: 1.3, cloudExposure: 3.0, magentaFixStrength: 2.0, edgeAlphaCutoff: 0.0, mipLevelScale: 0.35,
+      windSpeed: 0.0, evolutionSpeed: 0.005,
+      temporalEnabled: false, temporalAlpha: 0.1,
+      blueNoiseScale: 1.0, jitterStrength: 1.0,
+      // BSM cascade 几何:shadowFar 控制覆盖最远距离,splitLambda 控制近处分配,fadeScale 扩大 ortho radius 防切割
+      // fadeScale 提高以扩大 ortho 覆盖,避免 cascade 矩形外硬切;不再依赖 UV edgeFade
+      shadowFar: 40000, shadowSplitLambda: 1.0, shadowFadeScale: 5.0,
+    };
+
+    this.atmosphere = null;
+    this.aerial = null;
+    this.cloudStage = null;
+    this.textures = null;
+    this._ready = null;
+
+    // BSM state
+    this._bsm = { pass: null, resolve: null, blitFbo: null, blitProg: null, blitVbo: null };
+    // TAA state
+    this._taa = { texA: null, texB: null, current: 0, pbo: null, pboReady: false, w: 0, h: 0, frameCount: 0, prevVP: null, curVP: null };
+    // Wind offsets
+    this._weatherOffsetX = 0; this._weatherOffsetY = 0;
+    this._shapeOffsetX = 0; this._shapeOffsetY = 0; this._shapeOffsetZ = 0;
+    this._shapeDetailOffsetX = 0; this._shapeDetailOffsetY = 0; this._shapeDetailOffsetZ = 0;
+    this._lastFrameTime = undefined;
+    this._listeners = [];
+
+    // 原始 WebGL2 云渲染状态
+    this._raw = {
+      prog: null,        // 云渲染着色器程序
+      vao: null,         // 全屏四边形 VAO
+      vbo: null,         // 全屏四边形 VBO
+      sceneColor: null,  // 场景颜色拷贝纹理
+      depthTex: null,    // 场景深度拷贝纹理
+      depthFbo: null,    // 深度拷贝用 FBO
+      depthW: 0,
+      depthH: 0,
+      texUnits: {},      // 纹理 uniform 到纹理单元映射
+    };
+  }
+
+  // ── Raw WebGL2 云渲染片段着色器(czm_* 替换为原始 uniform)────────────
+
+  /**
+   * 返回一个自包含的 GLSL #version 100 片段着色器。
+   * 将 Cesium PostProcessStage 中的 czm_* 内置变量替换为显式 uniform,
+   * 移除了 USE_ATMOSPHERE_IRRADIANCE(改用简化 Rayleigh 天光模型)。
+   * 输出使用 gl_FragColor。
+   */
+  _getRawCloudFragmentShader() {
+    let src = getCloudFragmentShader();
+
+    // #version 100 兼容:in → varying
+    src = src.replace('in vec2 v_textureCoordinates;', 'varying vec2 v_textureCoordinates;');
+
+    // 在文件头部插入 extension 声明 + 新增 uniform
+    const header = `#extension GL_OES_texture_3D : enable
+precision highp float;
+precision highp sampler2D;
+precision highp sampler3D;
+
+uniform mat4 u_inverseProjection;
+uniform mat4 u_inverseView;
+uniform mat4 u_viewMatrix;
+uniform vec3 u_sunDirection;
+
+`;
+    // 找到第一个非注释行(跳过开头的 const 声明、main 之前的代码)
+    // 直接在所有代码之前插入 header
+    // getCloudFragmentShader() 以模板字面量 \` 开始,内容以 const float 开头
+    // 我们先移除 precision 和 extension 行(如果有的话),再插入
+    // 但原始 shader 在 _buildCloudFragmentShader 里才加 precision,这里裸的 getCloudFragmentShader 没有 precision 行
+    // 直接在开头插入即可
+
+    // 在第一个非空白、非注释行之前插入 header
+    // getCloudFragmentShader() 第一行是 `\nconst float RECIPROCAL_PI4...`
+    // 我们把 header 插在 const float RECIPROCAL_PI4 之前
+    src = header + src;
+
+    // #define USE_ATMOSPHERE_IRRADIANCE 相关代码不再需要,但 shader 默认没有 #define USE_ATMOSPHERE_IRRADIANCE
+    // (它仅在 _buildCloudFragmentShader 中被定义),所以 else 分支会走简化 Rayleigh 模型。
+    // 不需要额外操作。
+
+    // 替换 czm_* 为 uniform
+    // 注意替换顺序:先长后短,避免部分匹配
+    src = src.replace(/czm_windowToEyeCoordinates\(vec4\(gl_FragCoord\.xy,\s*depth,\s*1\.0\)\)/g,
+      '(u_inverseProjection * vec4(gl_FragCoord.x * 2.0 / u_resolution.x - 1.0, gl_FragCoord.y * 2.0 / u_resolution.y - 1.0, depth * 2.0 - 1.0, 1.0))');
+    src = src.replace(/czm_inverseProjection/g, 'u_inverseProjection');
+    src = src.replace(/czm_inverseView/g, 'u_inverseView');
+    src = src.replace(/czm_sunDirectionWC/g, 'u_sunDirection');
+    src = src.replace(/czm_view/g, 'u_viewMatrix');
+    src = src.replace(/czm_readDepth\(depthTexture,\s*v_textureCoordinates\)/g,
+      'texture2D(depthTexture, v_textureCoordinates).r');
+
+    return src;
+  }
+
+  // ── Texture loading ────────────────────────────────────────────────────
+
+  async _load3DTexture(url, size) {
+    const data3D = await loadBinThreeGeospatial(url, size);
+    return bindData3DTextureToCesiumContext(this.viewer, data3D, Cesium);
+  }
+
+  async _load3DTextureWHD(url, width, height, depth) {
+    const arrayBuffer = await Cesium.Resource.fetchArrayBuffer(url);
+    const raw = new Uint8Array(arrayBuffer);
+    const context = this.viewer.scene.context;
+    const gl = context._gl;
+    try {
+      return new Cesium.Texture3D({
+        context,
+        width,
+        height,
+        depth,
+        pixelFormat: Cesium.PixelFormat.RED,
+        pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
+        source: {
+          arrayBufferView: raw,
+          width,
+          height,
+          depth,
+        },
+        sampler: new Cesium.Sampler({
+          minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
+          magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
+          wrapS: Cesium.TextureWrap.REPEAT,
+          wrapT: Cesium.TextureWrap.REPEAT,
+          wrapR: Cesium.TextureWrap.REPEAT,
+        }),
+      });
+    } catch (e) {
+      console.warn('[Pipeline] Cesium.Texture3D 失败,使用原生 WebGL2 回退:', e.message);
+      const tex = gl.createTexture();
+      gl.bindTexture(gl.TEXTURE_3D, tex);
+      gl.texImage3D(gl.TEXTURE_3D, 0, gl.R8, width, height, depth, 0, gl.RED, gl.UNSIGNED_BYTE, raw);
+      gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+      gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+      gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.REPEAT);
+      gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.REPEAT);
+      gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.REPEAT);
+      gl.bindTexture(gl.TEXTURE_3D, null);
+      return {
+        _texture: tex,
+        _textureTarget: gl.TEXTURE_3D,
+        _target: gl.TEXTURE_3D,
+        width, height, depth,
+        bind: function(textureUnit) {
+          gl.activeTexture(gl.TEXTURE0 + textureUnit);
+          gl.bindTexture(gl.TEXTURE_3D, tex);
+        },
+        destroy: function() { gl.deleteTexture(tex); }
+      };
+    }
+  }
+
+  async _load2DTexture(url) {
+    const img = await Cesium.Resource.fetchImage(url);
+    if (!img || img.width <= 2 || img.height <= 2) throw new Error(`Invalid image: ${url}`);
+    return new Cesium.Texture({
+      context: this.viewer.scene.context, source: img,
+      sampler: new Cesium.Sampler({ minificationFilter: Cesium.TextureMinificationFilter.LINEAR, magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR, wrapS: Cesium.TextureWrap.REPEAT, wrapT: Cesium.TextureWrap.REPEAT })
+    });
+  }
+
+  async _loadTextures() {
+    const bp = this.assetsBase;
+    const [shape, detail, stbn, weather, turb, noise] = await Promise.all([
+      this._load3DTexture(bp + "shape.bin", 128).catch(() => null),
+      this._load3DTexture(bp + "shape_detail.bin", 32).catch(() => null),
+      this._load3DTextureWHD(bp + "stbn.bin", 128, 128, 64).catch(() => null),
+      this._load2DTexture(bp + "local_weather.png").catch(() => null),
+      this._load2DTexture(bp + "turbulence.png").catch(() => null),
+      this._load2DTexture(this.blueNoiseUrl).catch(() => null),
+    ]);
+    this.textures = { shape, shapeDetail: detail, stbn, weather, turbulence: turb, blueNoise: noise };
+    console.log("[Pipeline] textures:", Object.fromEntries(Object.entries(this.textures).map(([k, v]) => [k, !!v])));
+  }
+
+  // ── Shader loading for Bruneton prefix ─────────────────────────────────
+
+  async _loadShader(name) {
+    return loadShaderSource(name, { shaderBaseUrl: this.brunetonShaderBase });
+  }
+
+  async _buildCloudFragmentShader() {
+    const provider = this.atmosphere.getAtmosphereForClouds();
+    const [definitions, common, runtime] = await Promise.all([
+      this._loadShader("definitions.glsl"),
+      this._loadShader("common.glsl"),
+      this._loadShader("runtime.glsl"),
+    ]);
+    const defines = "precision highp float;\nprecision highp sampler2D;\nprecision highp sampler3D;\n"
+      + provider.constants.getShaderDefines()
+      + "\n#define METER_TO_LENGTH_UNIT 0.001\n#define USE_ATMOSPHERE_IRRADIANCE\n";
+    const globalU = `
+uniform vec3 sunDirection;
+uniform AtmosphereParameters ATMOSPHERE;
+uniform vec3 SUN_SPECTRAL_RADIANCE_TO_LUMINANCE;
+uniform vec3 SKY_SPECTRAL_RADIANCE_TO_LUMINANCE;
+uniform sampler2D transmittance_texture;
+uniform sampler3D scattering_texture;
+uniform sampler3D single_mie_scattering_texture;
+uniform sampler2D irradiance_texture;
+`;
+    return defines + definitions + "\n" + common + "\n" + globalU + "\n" + runtime + "\n" + getCloudFragmentShader();
+  }
+
+  // ── Wind animation ─────────────────────────────────────────────────────
+
+  _advanceOffsets() {
+    const now = performance.now() / 1000;
+    if (this._lastFrameTime !== undefined) {
+      const dt = now - this._lastFrameTime;
+      this._weatherOffsetX += (this.params.windSpeed || 0) * dt;
+      this._shapeOffsetX += (this.params.evolutionSpeed || 0) * dt;
+      this._shapeDetailOffsetX += (this.params.evolutionSpeed || 0) * 2 * dt;
+    }
+    this._lastFrameTime = now;
+  }
+
+  // ── Helpers ────────────────────────────────────────────────────────────
+
+  _getDensityProfileVec4(key) {
+    const ls = this.params.layers, def = k => k === "linearTerm" ? 0.75 : k === "constantTerm" ? 0.25 : 0;
+    return new Cesium.Cartesian4(...[0,1,2,3].map(i => {
+      const val = ls[i]?.densityProfile?.[key];
+      return val !== undefined ? Number(val) : def(key);
+    }));
+  }
+
+  _getIntervalHeights() {
+    const ls = this.params.layers, entries = [];
+    for (let i = 0; i < 4; i++) { const a = Number(ls[i]?.altitude) || 0, h = Number(ls[i]?.height) || 0; entries.push({ v: a, flag: 0 }, { v: a + h, flag: 1 }); }
+    entries.sort((a, b) => a.v !== b.v ? a.v - b.v : a.flag - b.flag);
+    const intervals = [{ min: 0, max: 0 }, { min: 0, max: 0 }, { min: 0, max: 0 }];
+    let idx = 0, balance = 0;
+    for (let i = 0; i < entries.length; i++) { if (balance === 0 && i > 0) { intervals[idx] = { min: entries[i - 1].v, max: entries[i].v }; idx++; } balance += entries[i].flag === 0 ? 1 : -1; }
+    return { min: new Cesium.Cartesian3(intervals[0].min, intervals[1].min, intervals[2].min), max: new Cesium.Cartesian3(intervals[0].max, intervals[1].max, intervals[2].max) };
+  }
+
+  _getLayerVec4(key, fallback = 0) {
+    const ls = this.params.layers;
+    return new Cesium.Cartesian4(...[0,1,2,3].map(i => {
+      const val = ls[i]?.[key];
+      return val !== undefined ? Number(val) : fallback;
+    }));
+  }
+
+  _getAltitudeCorrectionOffset(bottomRadius) {
+    const ellipsoid = this.viewer?.scene?.globe?.ellipsoid;
+    const cameraPos = this.viewer?.camera?.positionWC;
+    if (!ellipsoid || !cameraPos) return Cesium.Cartesian3.ZERO.clone();
+    const carto = Cesium.Cartographic.fromCartesian(cameraPos, ellipsoid);
+    if (!carto) return Cesium.Cartesian3.ZERO.clone();
+    const surface = Cesium.Cartesian3.fromRadians(
+      carto.longitude,
+      carto.latitude,
+      0.0,
+      ellipsoid
+    );
+    const normal = ellipsoid.geodeticSurfaceNormal(surface, new Cesium.Cartesian3());
+    const center = Cesium.Cartesian3.subtract(
+      surface,
+      Cesium.Cartesian3.multiplyByScalar(normal, Number(bottomRadius) || 0, new Cesium.Cartesian3()),
+      new Cesium.Cartesian3()
+    );
+    return Cesium.Cartesian3.negate(center, new Cesium.Cartesian3());
+  }
+
+  _getMinHeight() { const ls = this.params.layers; let m = Infinity; for (let i = 0; i < 4; i++) { if ((Number(ls[i]?.height) || 0) > 0) m = Math.min(m, Number(ls[i]?.altitude) || 0); } return Number.isFinite(m) ? m : 0; }
+  _getMaxHeight() { const ls = this.params.layers; let m = 0; for (let i = 0; i < 4; i++) { const h = Number(ls[i]?.height) || 0; if (h > 0) m = Math.max(m, (Number(ls[i]?.altitude) || 0) + h); } return m; }
+
+  // ── Cloud PostProcessStage uniform map ─────────────────────────────────
+
+  _buildCloudUniforms() {
+    const self = this, p = () => self.params, tex = () => self.textures;
+    const provider = this.atmosphere.getAtmosphereForClouds();
+    const atm = provider.getUniforms();
+    const u = {
+      u_shapeTexture: () => tex()?.shape,
+      u_shapeDetailTexture: () => tex()?.shapeDetail,
+      u_stbnTexture: () => tex()?.stbn || tex()?.shape,
+      u_weatherTexture: () => tex()?.weather,
+      u_turbulenceTexture: () => tex()?.turbulence,
+      u_blueNoise: () => tex()?.blueNoise,
+      u_blueNoiseScale: () => p().blueNoiseScale ?? 1.0,
+      u_jitterStrength: () => p().jitterStrength ?? 1.0,
+      u_cameraPosition: () => self.viewer.camera.positionWC,
+      u_altitudeCorrection: () => {
+        const br = Number(atm.bottomRadius()) || Number(p().bottomRadius) || 0;
+        return self._getAltitudeCorrectionOffset(br);
+      },
+      u_cameraHeight: () => {
+        const corr = u.u_altitudeCorrection();
+        const pos = Cesium.Cartesian3.add(self.viewer.camera.positionWC, corr, new Cesium.Cartesian3());
+        const br = Number(atm.bottomRadius()) || Number(p().bottomRadius) || 0;
+        return Math.max(0, Cesium.Cartesian3.magnitude(pos) - br);
+      },
+      u_bottomRadius: () => Number(p().bottomRadius),
+      u_minHeight: () => self._getMinHeight(),
+      u_maxHeight: () => self._getMaxHeight(),
+      u_minLayerHeights: () => self._getLayerVec4("altitude", 0),
+      u_maxLayerHeights: () => { const ls = p().layers; return new Cesium.Cartesian4(...[0,1,2,3].map(i => (Number(ls[i]?.altitude)||0)+(Number(ls[i]?.height)||0))); },
+      u_densityScales: () => self._getLayerVec4("densityScale", 0),
+      u_shapeAmounts: () => self._getLayerVec4("shapeAmount", 0),
+      u_shapeDetailAmounts: () => self._getLayerVec4("shapeDetailAmount", 0),
+      u_weatherExponents: () => self._getLayerVec4("weatherExponent", 1),
+      u_shapeAlteringBiases: () => self._getLayerVec4("shapeAlteringBias", 0.35),
+      u_coverageFilterWidths: () => self._getLayerVec4("coverageFilterWidth", 0.6),
+      u_maxSteps: () => p().maxSteps, u_maxStepsToSun: () => p().maxStepsToSun,
+      u_minStepSize: () => p().minStepSize, u_maxStepSize: () => p().maxStepSize,
+      u_maxRayDistance: () => p().maxRayDistance,
+      u_distFadeStart: () => Number(p().distFadeStart) || 30000.0,
+      u_distFadeEnd: () => Number(p().distFadeEnd) || 150000.0,
+      u_cameraNear: () => Number(self.viewer.camera.frustum?.near) || 0,
+      u_shadowTopHeight: () => self._getMaxHeight(),
+      u_shadowLengthEnabled: () => p().shadowLengthEnabled ? 1 : 0,
+      u_hazeEnabled: () => p().hazeEnabled ? 1 : 0,
+      u_maxShadowLengthIterationCount: () => p().maxShadowLengthIterationCount,
+      u_minShadowLengthStepSize: () => p().minShadowLengthStepSize,
+      u_maxShadowLengthRayDistance: () => p().maxShadowLengthRayDistance,
+      u_hazeDensityScale: () => p().hazeDensityScale, u_hazeExponent: () => p().hazeExponent,
+      u_hazeScatteringCoefficient: () => p().hazeScatteringCoefficient,
+      u_hazeAbsorptionCoefficient: () => p().hazeAbsorptionCoefficient,
+      u_shadowBuffer: () => {
+        if (p().useShadowBuffer && self._bsm.resolve) { 
+          const t = self._bsmResolveGetTexture(); // 直接调用对象的原生方法
+          if (t) return t; 
+        }
+        return tex()?.weather;
+      },
+      u_shadowTexelSize: () => { const tile = self._bsm.pass ? Math.floor(SHADOW_MAP_SIZE / 2) : 512; return new Cesium.Cartesian2(1 / tile, 1 / tile); },
+      u_shadowIntervals: () => { if (p().useShadowBuffer && self._bsm.pass) { 
+        const iv = self._bsm.pass.getShadowIntervals(); // 使用 Getter
+        return iv.map(a => new Cesium.Cartesian2(a[0], a[1])); 
+      } 
+      return Array(4).fill(null).map(() => new Cesium.Cartesian2(0, 0)); },
+      u_shadowMatrices: () => { if (p().useShadowBuffer && self._bsm.pass) return self._bsm.pass._shadowMatrices.map(m => Cesium.Matrix4.fromArray(m)); return Array(4).fill(null).map(() => Cesium.Matrix4.IDENTITY.clone()); },
+      u_shadowFar: () => self._bsm.pass ? self._bsm.pass._shadowFar : p().maxShadowLengthRayDistance,
+      u_maxShadowFilterRadius: () => 2.0,
+      u_useShadowBuffer: () => p().useShadowBuffer ? 1 : 0,
+      u_skyLightScale: () => p().skyLightScale,
+      u_weatherRepeat: () => p().weatherRepeat,
+      u_localWeatherOffset: () => { self._advanceOffsets(); return new Cesium.Cartesian2(self._weatherOffsetX, self._weatherOffsetY); },
+      u_shapeRepeat: () => (Number(p().shapeRepeat) || 3) / 1e4,
+      u_shapeOffset: () => { self._advanceOffsets(); return new Cesium.Cartesian3(self._shapeOffsetX, self._shapeOffsetY, self._shapeOffsetZ); },
+      u_shapeDetailRepeat: () => p().shapeDetailRepeat,
+      u_shapeDetailOffset: () => { self._advanceOffsets(); return new Cesium.Cartesian3(self._shapeDetailOffsetX, self._shapeDetailOffsetY, self._shapeDetailOffsetZ); },
+      u_turbulenceRepeat: () => p().turbulenceRepeat, u_turbulenceDisplacement: () => p().turbulenceDisplacement,
+      u_coverages: () => self._getLayerVec4("coverage", 0.3),
+      u_coverageHaze: () => { const ls = p().layers; return Math.max(Number(ls[0]?.coverage) ?? 0.3, Number(ls[1]?.coverage) ?? 0.3, Number(ls[2]?.coverage) ?? 0.3); },
+      u_scatteringCoefficient: () => p().scatteringCoefficient, u_absorptionCoefficient: () => p().absorptionCoefficient,
+      u_scatterG1: () => p().scatterG1, u_scatterG2: () => p().scatterG2, u_scatterMix: () => p().scatterMix,
+      u_sunIntensity: () => p().sunIntensity, u_skyToSunRatio: () => p().skyToSunRatio,
+      u_powderScale: () => p().powderScale, u_powderExponent: () => p().powderExponent,
+      u_aerialPerspectiveScale: () => p().aerialPerspectiveScale, u_cloudExposure: () => p().cloudExposure,
+      u_magentaFixStrength: () => p().magentaFixStrength ?? 0.8,
+      u_edgeAlphaCutoff: () => p().edgeAlphaCutoff ?? 0.03,
+      u_resolution: () => { const ctx = self.viewer.scene.context; return new Cesium.Cartesian2(ctx.drawingBufferWidth || 1, ctx.drawingBufferHeight || 1); },
+      u_mipLevelScale: () => Number(p().mipLevelScale) || 1.0,
+      u_perspectiveStepScale: () => p().perspectiveStepScale ?? 1.01,
+      u_minDensity: () => p().minDensity ?? 1e-5, u_minExtinction: () => p().minExtinction ?? 1e-5,
+      u_minTransmittance: () => p().minTransmittance ?? 0.01,
+      u_minSecondaryStepSize: () => p().minSecondaryStepSize ?? 100, u_secondaryStepScale: () => p().secondaryStepScale ?? 2,
+      u_multiScatteringOctaves: () => Math.min(12, Math.max(1, p().multiScatteringOctaves ?? 8)),
+      u_lowLayerDensityBoost: () => p().lowLayerDensityBoost ?? 1.0,
+      u_densityProfileExpTerms: () => self._getDensityProfileVec4("expTerm"),
+      u_densityProfileExponents: () => self._getDensityProfileVec4("exponent"),
+      u_densityProfileLinearTerms: () => self._getDensityProfileVec4("linearTerm"),
+      u_densityProfileConstantTerms: () => self._getDensityProfileVec4("constantTerm"),
+      u_minIntervalHeights: () => self._getIntervalHeights().min,
+      u_maxIntervalHeights: () => self._getIntervalHeights().max,
+      u_historyTexture: () => { const t = self._taaGetHistoryTexture(); return t || tex()?.blueNoise; },
+      u_prevViewProjection: () => self._taa.prevVP || Cesium.Matrix4.IDENTITY,
+      u_temporalAlpha: () => p().temporalAlpha ?? 0.1,
+      u_temporalEnabled: () => (p().temporalEnabled && self._taa.frameCount > 2 && self._taa.prevVP) ? 1 : 0,
+      u_frame: () => self._frameCount || 0,
+    };
+
+    Object.assign(u, atm);
+    u.u_cameraPosition = atm.cameraPosition;
+    u.u_bottomRadius = atm.bottomRadius;
+    return u;
+  }
+
+  // ── 原始 WebGL2 云渲染初始化 ────────────────────────────────────────────
+
+  /**
+   * 初始化原始 WebGL2 全屏四边形渲染管线。
+   * @param {WebGL2RenderingContext} gl
+   * @returns {boolean} 是否成功
+   */
+  _initRawWebGL(gl) {
+    // 顶点着色器(#version 100,兼容 gl_FragColor)
+    const vsSrc = `
+attribute vec2 a_position;
+varying vec2 v_textureCoordinates;
+void main() {
+    v_textureCoordinates = a_position * 0.5 + 0.5;
+    gl_Position = vec4(a_position, 0.0, 1.0);
+}`;
+
+    const fsSrc = this._getRawCloudFragmentShader();
+
+    this._raw.prog = createGLProgram(gl, vsSrc, fsSrc, 'RawCloud');
+    if (!this._raw.prog) {
+      console.error('[Pipeline] 原始 WebGL 云渲染程序编译失败');
+      return false;
+    }
+
+    // 全屏四边形 VAO
+    this._raw.vao = gl.createVertexArray();
+    gl.bindVertexArray(this._raw.vao);
+
+    this._raw.vbo = gl.createBuffer();
+    gl.bindBuffer(gl.ARRAY_BUFFER, this._raw.vbo);
+    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
+
+    const posLoc = gl.getAttribLocation(this._raw.prog, 'a_position');
+    if (posLoc >= 0) {
+      gl.enableVertexAttribArray(posLoc);
+      gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
+    }
+
+    gl.bindVertexArray(null);
+    gl.bindBuffer(gl.ARRAY_BUFFER, null);
+
+    // 场景颜色拷贝纹理
+    this._raw.sceneColor = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, this._raw.sceneColor);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+    gl.bindTexture(gl.TEXTURE_2D, null);
+
+    // 深度拷贝纹理 + FBO
+    this._raw.depthTex = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, this._raw.depthTex);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+    gl.bindTexture(gl.TEXTURE_2D, null);
+
+    this._raw.depthFbo = gl.createFramebuffer();
+
+    // 建立纹理 uniform → 纹理单元映射表
+    this._raw.texUnits = {
+      colorTexture: 0,
+      depthTexture: 1,
+      u_shapeTexture: 2,
+      u_shapeDetailTexture: 3,
+      u_stbnTexture: 4,
+      u_weatherTexture: 5,
+      u_turbulenceTexture: 6,
+      u_blueNoise: 7,
+      u_shadowBuffer: 8,
+      u_historyTexture: 9,
+    };
+
+    return true;
+  }
+
+  /**
+   * 为原始 WebGL 云渲染设置所有 uniform。
+   * 使用 _buildCloudUniforms() 获取值与 Cesium 一致的参数值。
+   */
+  _setRawCloudUniforms(gl, w, h) {
+    const prog = this._raw.prog;
+    if (!prog) return;
+
+    const u = this._buildCloudUniforms();
+
+    // 工具:尝试获取 uniform 位置,不存在时返回 -1
+    const uloc = (name) => gl.getUniformLocation(prog, name);
+
+    // ── 纹理绑定 ──
+    const texUnits = this._raw.texUnits;
+
+    // 场景颜色(从拷贝纹理读取)
+    gl.activeTexture(gl.TEXTURE0 + texUnits.colorTexture);
+    gl.bindTexture(gl.TEXTURE_2D, this._raw.sceneColor);
+    const locColor = uloc('colorTexture');
+    if (locColor) gl.uniform1i(locColor, texUnits.colorTexture);
+
+    // 场景深度(从拷贝纹理读取)
+    gl.activeTexture(gl.TEXTURE0 + texUnits.depthTexture);
+    gl.bindTexture(gl.TEXTURE_2D, this._raw.depthTex);
+    const locDepth = uloc('depthTexture');
+    if (locDepth) gl.uniform1i(locDepth, texUnits.depthTexture);
+
+    // 云 3D 纹理 + 2D 纹理
+    const texBindings = [
+      { name: 'u_shapeTexture', getter: u.u_shapeTexture, unit: texUnits.u_shapeTexture },
+      { name: 'u_shapeDetailTexture', getter: u.u_shapeDetailTexture, unit: texUnits.u_shapeDetailTexture },
+      { name: 'u_stbnTexture', getter: u.u_stbnTexture, unit: texUnits.u_stbnTexture },
+      { name: 'u_weatherTexture', getter: u.u_weatherTexture, unit: texUnits.u_weatherTexture },
+      { name: 'u_turbulenceTexture', getter: u.u_turbulenceTexture, unit: texUnits.u_turbulenceTexture },
+      { name: 'u_blueNoise', getter: u.u_blueNoise, unit: texUnits.u_blueNoise },
+      { name: 'u_shadowBuffer', getter: u.u_shadowBuffer, unit: texUnits.u_shadowBuffer },
+      { name: 'u_historyTexture', getter: u.u_historyTexture, unit: texUnits.u_historyTexture },
+    ];
+
+    for (const tb of texBindings) {
+      const texVal = tb.getter();
+      const loc = uloc(tb.name);
+      if (loc && texVal) {
+        gl.activeTexture(gl.TEXTURE0 + tb.unit);
+        if (texVal.bind) {
+          texVal.bind(tb.unit);
+        } else if (texVal._texture) {
+          const target = texVal._target || gl.TEXTURE_2D;
+          gl.bindTexture(target, texVal._texture);
+        }
+        gl.uniform1i(loc, tb.unit);
+      }
+    }
+
+    // ── 数值/向量 uniform ──
+    const uniformSetters = [
+      { name: 'u_cameraPosition', getter: u.u_cameraPosition },
+      { name: 'u_altitudeCorrection', getter: u.u_altitudeCorrection },
+      { name: 'u_cameraHeight', getter: u.u_cameraHeight },
+      { name: 'u_bottomRadius', getter: u.u_bottomRadius },
+      { name: 'u_minHeight', getter: u.u_minHeight },
+      { name: 'u_maxHeight', getter: u.u_maxHeight },
+      { name: 'u_minLayerHeights', getter: u.u_minLayerHeights },
+      { name: 'u_maxLayerHeights', getter: u.u_maxLayerHeights },
+      { name: 'u_densityScales', getter: u.u_densityScales },
+      { name: 'u_shapeAmounts', getter: u.u_shapeAmounts },
+      { name: 'u_shapeDetailAmounts', getter: u.u_shapeDetailAmounts },
+      { name: 'u_weatherExponents', getter: u.u_weatherExponents },
+      { name: 'u_shapeAlteringBiases', getter: u.u_shapeAlteringBiases },
+      { name: 'u_coverageFilterWidths', getter: u.u_coverageFilterWidths },
+      { name: 'u_maxSteps', getter: u.u_maxSteps },
+      { name: 'u_maxStepsToSun', getter: u.u_maxStepsToSun },
+      { name: 'u_minStepSize', getter: u.u_minStepSize },
+      { name: 'u_maxStepSize', getter: u.u_maxStepSize },
+      { name: 'u_maxRayDistance', getter: u.u_maxRayDistance },
+      { name: 'u_distFadeStart', getter: u.u_distFadeStart },
+      { name: 'u_distFadeEnd', getter: u.u_distFadeEnd },
+      { name: 'u_cameraNear', getter: u.u_cameraNear },
+      { name: 'u_shadowTopHeight', getter: u.u_shadowTopHeight },
+      { name: 'u_shadowLengthEnabled', getter: u.u_shadowLengthEnabled },
+      { name: 'u_hazeEnabled', getter: u.u_hazeEnabled },
+      { name: 'u_maxShadowLengthIterationCount', getter: u.u_maxShadowLengthIterationCount },
+      { name: 'u_minShadowLengthStepSize', getter: u.u_minShadowLengthStepSize },
+      { name: 'u_maxShadowLengthRayDistance', getter: u.u_maxShadowLengthRayDistance },
+      { name: 'u_hazeDensityScale', getter: u.u_hazeDensityScale },
+      { name: 'u_hazeExponent', getter: u.u_hazeExponent },
+      { name: 'u_hazeScatteringCoefficient', getter: u.u_hazeScatteringCoefficient },
+      { name: 'u_hazeAbsorptionCoefficient', getter: u.u_hazeAbsorptionCoefficient },
+      { name: 'u_shadowTexelSize', getter: u.u_shadowTexelSize },
+      { name: 'u_shadowFar', getter: u.u_shadowFar },
+      { name: 'u_maxShadowFilterRadius', getter: () => 2.0 },
+      { name: 'u_useShadowBuffer', getter: u.u_useShadowBuffer },
+      { name: 'u_skyLightScale', getter: u.u_skyLightScale },
+      { name: 'u_weatherRepeat', getter: u.u_weatherRepeat },
+      { name: 'u_localWeatherOffset', getter: u.u_localWeatherOffset },
+      { name: 'u_shapeRepeat', getter: u.u_shapeRepeat },
+      { name: 'u_shapeOffset', getter: u.u_shapeOffset },
+      { name: 'u_shapeDetailRepeat', getter: u.u_shapeDetailRepeat },
+      { name: 'u_shapeDetailOffset', getter: u.u_shapeDetailOffset },
+      { name: 'u_turbulenceRepeat', getter: u.u_turbulenceRepeat },
+      { name: 'u_turbulenceDisplacement', getter: u.u_turbulenceDisplacement },
+      { name: 'u_coverages', getter: u.u_coverages },
+      { name: 'u_coverageHaze', getter: u.u_coverageHaze },
+      { name: 'u_scatteringCoefficient', getter: u.u_scatteringCoefficient },
+      { name: 'u_absorptionCoefficient', getter: u.u_absorptionCoefficient },
+      { name: 'u_scatterG1', getter: u.u_scatterG1 },
+      { name: 'u_scatterG2', getter: u.u_scatterG2 },
+      { name: 'u_scatterMix', getter: u.u_scatterMix },
+      { name: 'u_sunIntensity', getter: u.u_sunIntensity },
+      { name: 'u_skyToSunRatio', getter: u.u_skyToSunRatio },
+      { name: 'u_powderScale', getter: u.u_powderScale },
+      { name: 'u_powderExponent', getter: u.u_powderExponent },
+      { name: 'u_aerialPerspectiveScale', getter: u.u_aerialPerspectiveScale },
+      { name: 'u_cloudExposure', getter: u.u_cloudExposure },
+      { name: 'u_magentaFixStrength', getter: u.u_magentaFixStrength },
+      { name: 'u_edgeAlphaCutoff', getter: u.u_edgeAlphaCutoff },
+      { name: 'u_resolution', getter: () => new Cesium.Cartesian2(w, h) },
+      { name: 'u_mipLevelScale', getter: u.u_mipLevelScale },
+      { name: 'u_perspectiveStepScale', getter: u.u_perspectiveStepScale },
+      { name: 'u_minDensity', getter: u.u_minDensity },
+      { name: 'u_minExtinction', getter: u.u_minExtinction },
+      { name: 'u_minTransmittance', getter: u.u_minTransmittance },
+      { name: 'u_minSecondaryStepSize', getter: u.u_minSecondaryStepSize },
+      { name: 'u_secondaryStepScale', getter: u.u_secondaryStepScale },
+      { name: 'u_multiScatteringOctaves', getter: u.u_multiScatteringOctaves },
+      { name: 'u_lowLayerDensityBoost', getter: u.u_lowLayerDensityBoost },
+      { name: 'u_densityProfileExpTerms', getter: u.u_densityProfileExpTerms },
+      { name: 'u_densityProfileExponents', getter: u.u_densityProfileExponents },
+      { name: 'u_densityProfileLinearTerms', getter: u.u_densityProfileLinearTerms },
+      { name: 'u_densityProfileConstantTerms', getter: u.u_densityProfileConstantTerms },
+      { name: 'u_minIntervalHeights', getter: u.u_minIntervalHeights },
+      { name: 'u_maxIntervalHeights', getter: u.u_maxIntervalHeights },
+      { name: 'u_prevViewProjection', getter: u.u_prevViewProjection },
+      { name: 'u_temporalAlpha', getter: u.u_temporalAlpha },
+      { name: 'u_temporalEnabled', getter: u.u_temporalEnabled },
+      { name: 'u_frame', getter: u.u_frame },
+      { name: 'u_blueNoiseScale', getter: u.u_blueNoiseScale },
+      { name: 'u_jitterStrength', getter: u.u_jitterStrength },
+      // 新增原始 uniform(替代 czm_* 内置变量)
+      { name: 'u_inverseProjection', getter: () => {
+        const p = this.viewer.camera.frustum;
+        const m = p.infiniteProjectionMatrix || p.projectionMatrix;
+        const inv = new Cesium.Matrix4();
+        return Cesium.Matrix4.inverse(m, inv) || Cesium.Matrix4.IDENTITY;
+      }},
+      { name: 'u_inverseView', getter: () => this.viewer.camera.inverseViewMatrix },
+      { name: 'u_viewMatrix', getter: () => this.viewer.camera.viewMatrix },
+      { name: 'u_sunDirection', getter: () => {
+        // czm_sunDirectionWC 等价于从相机到太阳的归一化方向
+        const sunPos = this.viewer.scene.sunPosition;
+        if (!sunPos) return new Cesium.Cartesian3(0.577, 0.577, 0.577);
+        return Cesium.Cartesian3.normalize(sunPos, new Cesium.Cartesian3());
+      }},
+    ];
+
+    // 处理 shadowIntervals(数组)和 shadowMatrices(数组)
+    // 特别注意 czm_view 和 czm_sunDirectionWC 已替换为 u_viewMatrix / u_sunDirection
+
+    for (const s of uniformSetters) {
+      const loc = uloc(s.name);
+      if (!loc) continue;
+
+      let val;
+      try {
+        val = s.getter();
+      } catch (e) {
+        // uniform getter 可能依赖未就绪的状态(如 TAA history),跳过
+        continue;
+      }
+      if (val === null || val === undefined) continue;
+
+      this._setRawGLUniform(gl, loc, val);
+    }
+
+    // ── 数组 uniform ──
+    // u_shadowIntervals: uniform vec2[4]
+    const locSI = uloc('u_shadowIntervals');
+    if (locSI) {
+      const intervals = u.u_shadowIntervals();
+      if (intervals && intervals.length === 4) {
+        const flat = [];
+        for (let i = 0; i < 4; i++) {
+          const iv = intervals[i];
+          if (iv && iv.x !== undefined) { flat.push(iv.x, iv.y); }
+          else { flat.push(0, 0); }
+        }
+        gl.uniform2fv(locSI, flat);
+      }
+    }
+
+    // u_shadowMatrices: uniform mat4[4]
+    const locSM = uloc('u_shadowMatrices');
+    if (locSM) {
+      const mats = u.u_shadowMatrices();
+      if (mats && mats.length === 4) {
+        for (let i = 0; i < 4; i++) {
+          const m = mats[i];
+          if (m && m.values) {
+            gl.uniformMatrix4fv(uloc(`u_shadowMatrices[${i}]`) || locSM, false, m.values);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * 将 Cesium 类型值转换为原始 WebGL uniform 调用。
+   */
+  _setRawGLUniform(gl, loc, val) {
+    if (val === null || val === undefined) return;
+
+    // Cesium 类型判断
+    const isCesiumObj = val.constructor && typeof val.constructor.name === 'string';
+    const typeName = val.constructor?.name;
+
+    if (typeof val === 'number') {
+      gl.uniform1f(loc, val);
+    } else if (typeof val === 'boolean') {
+      gl.uniform1i(loc, val ? 1 : 0);
+    } else if (typeName === 'Cartesian2' || (val.x !== undefined && val.y !== undefined && val.z === undefined && val.w === undefined)) {
+      gl.uniform2f(loc, Number(val.x), Number(val.y));
+    } else if (typeName === 'Cartesian3' || (val.x !== undefined && val.y !== undefined && val.z !== undefined && val.w === undefined)) {
+      gl.uniform3f(loc, Number(val.x), Number(val.y), Number(val.z));
+    } else if (typeName === 'Cartesian4' || (val.x !== undefined && val.y !== undefined && val.z !== undefined && val.w !== undefined)) {
+      gl.uniform4f(loc, Number(val.x), Number(val.y), Number(val.z), Number(val.w));
+    } else if (typeName === 'Matrix4') {
+      gl.uniformMatrix4fv(loc, false, val.values);
+    } else if (typeName === 'Matrix3') {
+      gl.uniformMatrix3fv(loc, false, val.values);
+    } else if (typeName === 'Matrix2') {
+      gl.uniformMatrix2fv(loc, false, val.values);
+    } else if (Array.isArray(val)) {
+      if (val.length === 2) gl.uniform2fv(loc, val);
+      else if (val.length === 3) gl.uniform3fv(loc, val);
+      else if (val.length === 4) gl.uniform4fv(loc, val);
+      else if (val.length === 9) gl.uniformMatrix3fv(loc, false, val);
+      else if (val.length === 16) gl.uniformMatrix4fv(loc, false, val);
+      else if (val.length === 1) gl.uniform1f(loc, val[0]);
+    } else if (val._texture) {
+      // 纹理 uniform 已在 _setRawCloudUniforms 中单独处理
+    } else if (typeof val === 'object' && val !== null) {
+      // 尝试作为 Cartesian 兼容对象处理
+      const keys = Object.keys(val).filter(k => !isNaN(Number(k)));
+      if (keys.length > 0) {
+        const arr = keys.map(k => Number(val[k]));
+        if (arr.length <= 4) {
+          if (arr.length === 1) gl.uniform1f(loc, arr[0]);
+          else if (arr.length === 2) gl.uniform2fv(loc, arr);
+          else if (arr.length === 3) gl.uniform3fv(loc, arr);
+          else if (arr.length === 4) gl.uniform4fv(loc, arr);
+        }
+      }
+    }
+  }
+
+  /**
+   * 在 postRender 中执行原始 WebGL2 云渲染。
+   * 拷贝当前帧缓冲区的颜色和深度,然后渲染全屏四边形。
+   */
+  _renderRawClouds(gl) {
+    if (!this._raw.prog || !this._raw.vao) return;
+
+    const canvas = this.viewer.scene.canvas;
+    const w = canvas.width;
+    const h = canvas.height;
+    if (w < 2 || h < 2) return;
+
+    // 如果画布尺寸变化,重新分配纹理
+    if (this._raw.depthW !== w || this._raw.depthH !== h) {
+      gl.bindTexture(gl.TEXTURE_2D, this._raw.depthTex);
+      gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT24, w, h, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_INT, null);
+      gl.bindTexture(gl.TEXTURE_2D, null);
+
+      gl.bindTexture(gl.TEXTURE_2D, this._raw.sceneColor);
+      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
+      gl.bindTexture(gl.TEXTURE_2D, null);
+
+      gl.bindFramebuffer(gl.FRAMEBUFFER, this._raw.depthFbo);
+      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, this._raw.depthTex, 0);
+      // 深度拷贝 FBO 仅有深度附着,无颜色附着——需设置 NONE 以避免 FRAMEBUFFER_INCOMPLETE
+      gl.drawBuffers([gl.NONE]);
+      gl.readBuffer(gl.NONE);
+      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+
+      this._raw.depthW = w;
+      this._raw.depthH = h;
+    }
+
+    // 保存 GL 状态
+    const prevFbo = gl.getParameter(gl.FRAMEBUFFER_BINDING);
+    const prevVp = gl.getParameter(gl.VIEWPORT);
+    const prevBlend = gl.getParameter(gl.BLEND);
+    const prevDepthTest = gl.getParameter(gl.DEPTH_TEST);
+    const prevCullFace = gl.getParameter(gl.CULL_FACE);
+    const prevProg = gl.getParameter(gl.CURRENT_PROGRAM);
+    const prevVAO = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
+
+    // 1. 拷贝场景颜色到纹理
+    gl.bindTexture(gl.TEXTURE_2D, this._raw.sceneColor);
+    gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 0, 0, w, h);
+
+    // 2. 拷贝场景深度到纹理
+    gl.bindFramebuffer(gl.READ_FRAMEBUFFER, prevFbo ? prevFbo : null);
+    gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this._raw.depthFbo);
+    try {
+      gl.blitFramebuffer(0, 0, w, h, 0, 0, w, h, gl.DEPTH_BUFFER_BIT, gl.NEAREST);
+    } catch (e) {
+      // 深度拷贝可能因 FBO 不兼容而失败,静默忽略
+    }
+
+    // 3. 渲染云层到当前帧缓冲
+    gl.bindFramebuffer(gl.FRAMEBUFFER, prevFbo ? prevFbo : null);
+    gl.viewport(0, 0, w, h);
+
+    gl.disable(gl.BLEND);
+    gl.disable(gl.DEPTH_TEST);
+    gl.disable(gl.CULL_FACE);
+
+    gl.useProgram(this._raw.prog);
+    gl.bindVertexArray(this._raw.vao);
+
+    this._setRawCloudUniforms(gl, w, h);
+
+    gl.drawArrays(gl.TRIANGLES, 0, 3);
+
+    // 4. 恢复状态
+    gl.bindVertexArray(prevVAO);
+    gl.useProgram(prevProg);
+    if (prevBlend) gl.enable(gl.BLEND); else gl.disable(gl.BLEND);
+    if (prevDepthTest) gl.enable(gl.DEPTH_TEST); else gl.disable(gl.DEPTH_TEST);
+    if (prevCullFace) gl.enable(gl.CULL_FACE); else gl.disable(gl.CULL_FACE);
+    gl.viewport(prevVp[0], prevVp[1], prevVp[2], prevVp[3]);
+  }
+
+  // ── BSM helpers ─────────────────────────────────────────────────────────
+
+  _bsmResolveGetTexture() {
+    const r = this._bsm.resolve;
+    const tex = r ? r._historyTex : (this._bsm.pass ? this._bsm.pass._colorTexture : null);
+    if (!tex) return null;
+    const gl = this.viewer.scene.context._gl;
+    return { 
+      _texture: tex, 
+      _textureTarget: gl.TEXTURE_2D, 
+      _target: gl.TEXTURE_2D,
+      // 【关键注入】Cesium 必须调用此方法才能把纹理挂载到 GPU
+      bind: function(textureUnit) {
+          gl.activeTexture(gl.TEXTURE0 + textureUnit);
+          gl.bindTexture(gl.TEXTURE_2D, this._texture);
+      }
+    };
+  }
+
+  _taaGetHistoryTexture() {
+    const gl = this.viewer.scene.context?._gl;
+    if (!gl) return null;
+    const tex = this._taa.current === 0 ? this._taa.texA : this._taa.texB;
+    if (!tex) return null;
+    return { _texture: tex, _textureTarget: gl.TEXTURE_2D, _target: gl.TEXTURE_2D };
+  }
+
+  // ── BSM blit to Cesium.Texture ─────────────────────────────────────────
+
+  _blitBSM(sourceTex, targetCesiumTex, scale) {
+    const gl = this.viewer.scene.context?._gl;
+    if (!gl || !sourceTex?._texture || !targetCesiumTex?._texture) return;
+    if (!this._bsm.blitFbo) {
+      this._bsm.blitFbo = gl.createFramebuffer();
+      this._bsm.blitProg = createGLProgram(gl,
+        `#version 300 es\nin vec2 a_pos;\nout vec2 v_uv;\nvoid main(){v_uv=a_pos*0.5+0.5;gl_Position=vec4(a_pos,0,1);}`,
+        `#version 300 es\nprecision highp float;\nuniform sampler2D u_src;\nuniform float u_scale;\nin vec2 v_uv;\nout vec4 o;\nvoid main(){vec4 raw=texture(u_src,v_uv);\n  // 编码:rgba *= scale。HALF_FLOAT(scale=1)等价透传;RGBA8(scale=0.02)压到0..1,消费端 /scale 还原。\n  o=vec4(raw.rgb*u_scale, raw.a*u_scale);}`,
+        "BSMBlit");
+      const vbo = gl.createBuffer();
+      gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
+      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
+      this._bsm.blitVbo = vbo;
+    }
+    const prevFbo = gl.getParameter(gl.FRAMEBUFFER_BINDING), prevVp = gl.getParameter(gl.VIEWPORT);
+    gl.bindFramebuffer(gl.FRAMEBUFFER, this._bsm.blitFbo);
+    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, targetCesiumTex._texture, 0);
+    if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) { gl.bindFramebuffer(gl.FRAMEBUFFER, prevFbo); gl.viewport(...prevVp); return; }
+    gl.viewport(0, 0, BSM_BLIT_SIZE, BSM_BLIT_SIZE);
+    gl.useProgram(this._bsm.blitProg);
+    gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, sourceTex._texture);
+    gl.uniform1i(gl.getUniformLocation(this._bsm.blitProg, "u_src"), 0);
+    gl.uniform1f(gl.getUniformLocation(this._bsm.blitProg, "u_scale"), scale);
+    gl.bindBuffer(gl.ARRAY_BUFFER, this._bsm.blitVbo);
+    const aloc = gl.getAttribLocation(this._bsm.blitProg, "a_pos");
+    if (aloc >= 0) { gl.enableVertexAttribArray(aloc); gl.vertexAttribPointer(aloc, 2, gl.FLOAT, false, 0, 0); }
+    gl.drawArrays(gl.TRIANGLES, 0, 3);
+    if (aloc >= 0) gl.disableVertexAttribArray(aloc);
+    gl.bindFramebuffer(gl.FRAMEBUFFER, prevFbo); gl.viewport(prevVp[0], prevVp[1], prevVp[2], prevVp[3]);
+  }
+
+  // ── BSM sync to Atmosphere + Aerial ────────────────────────────────────
+
+  _syncBSM() {
+    const sp = this._bsm.pass;
+    if (!sp || !this.params.useShadowBuffer) {
+      this.atmosphere?.setCloudShadow?.({ enabled: false });
+      this.aerial?.setCloudShadow?.({ enabled: false });
+      return;
+    }
+    sp.updateDynamicParams({
+      localWeatherOffset: [this._weatherOffsetX || 0, this._weatherOffsetY || 0],
+      shapeOffset: [this._shapeOffsetX || 0, this._shapeOffsetY || 0, this._shapeOffsetZ || 0],
+      shapeDetailOffset: [this._shapeDetailOffsetX || 0, this._shapeDetailOffsetY || 0, this._shapeDetailOffsetZ || 0],
+      bottomRadius: this.params.bottomRadius,
+      // 每帧同步 shadow cascade far,限制到云层相关距离(避免 Cesium frustum.far~8e8 导致矩阵 NaN)
+      shadowFar: Number(this.params.shadowFar) || Number(this.params.maxShadowLengthRayDistance) || 200000.0,
+      maxShadowLengthRayDistance: Number(this.params.maxShadowLengthRayDistance) || 200000.0,
+      shadowSplitLambda: Number(this.params.shadowSplitLambda) || 0.5,
+      shadowFadeScale: Number(this.params.shadowFadeScale) || 1.0,
+      // 同步 layer 参数(coverage/densityScale 等),否则 GUI 调 coverage 只影响主云,BSM 阴影不变
+      // 用普通数组(非 Cartesian4),因为 BSM 的 set4f 走原生 gl.uniform4fv 只接受数组/Float32Array
+      coverages: [0,1,2,3].map(i => { const v = this.params.layers[i]?.coverage; return v !== undefined ? Number(v) : 0.3; }),
+      densityScales: [0,1,2,3].map(i => { const v = this.params.layers[i]?.densityScale; return v !== undefined ? Number(v) : 0; }),
+      shapeAmounts: [0,1,2,3].map(i => { const v = this.params.layers[i]?.shapeAmount; return v !== undefined ? Number(v) : 0; }),
+      shapeDetailAmounts: [0,1,2,3].map(i => { const v = this.params.layers[i]?.shapeDetailAmount; return v !== undefined ? Number(v) : 0; }),
+      weatherExponents: [0,1,2,3].map(i => { const v = this.params.layers[i]?.weatherExponent; return v !== undefined ? Number(v) : 1; }),
+      shapeAlteringBiases: [0,1,2,3].map(i => { const v = this.params.layers[i]?.shapeAlteringBias; return v !== undefined ? Number(v) : 0.35; }),
+      coverageFilterWidths: [0,1,2,3].map(i => { const v = this.params.layers[i]?.coverageFilterWidth; return v !== undefined ? Number(v) : 0.6; }),
+      scatteringCoefficient: Number(this.params.scatteringCoefficient) ?? 0.9,
+      absorptionCoefficient: Number(this.params.absorptionCoefficient) ?? 1.0,
+    });
+
+    let tex = this._bsm.resolve ? this._bsmResolveGetTexture() : null;
+    if (!tex) tex = sp.getTexture();
+    if (!tex) { this.atmosphere?.setCloudShadow?.({ enabled: false }); this.aerial?.setCloudShadow?.({ enabled: false }); return; }
+
+    const provider = this.atmosphere?.getAtmosphereForClouds?.();
+    const targetTex = provider?.getCloudShadowTargetTexture?.();
+    const clamp01 = provider?.getCloudShadowClamp01?.() ?? true;
+    const scaleToPass = clamp01 ? 200.0 : 1.0;
+    let textureToPass = tex;
+
+    if (targetTex && tex._texture) { this._blitBSM(tex, targetTex, scaleToPass); textureToPass = targetTex; }
+
+    const intervals = sp.getShadowIntervals();
+    const mats = sp.getShadowMatrices();
+    const tile = sp.getTileSize?.() || Math.floor(SHADOW_MAP_SIZE / 2);
+    // 远距几何误差修正:相机越高/越远,越把 BSM 采样点拉向稳定球面(对齐 three-geospatial correctGeometricError)
+    const camH = this.viewer.camera.positionCartographic?.height ?? 0;
+    const geoAmt = Math.min(1, Math.max(0, (camH - 2000) / 25000));
+    const opts = {
+      enabled: true, texture: textureToPass, scale: scaleToPass,
+      decode: { x: 1, y: 1, z: 1, w: 1 },
+      near: sp.getShadowNear?.() ?? (Number(this.viewer.camera.frustum?.near) || 0.1),
+      far: sp.getShadowFar(),
+      topHeight: this._getMaxHeight(), bottomRadius: Number(this.params.bottomRadius) || 6371000,
+      intervals: intervals.map(a => new Cesium.Cartesian2(a[0], a[1])),
+      matrices: mats.map(m => Cesium.Matrix4.fromArray(m)),
+      texelSize: { x: 1 / tile, y: 1 / tile },
+      geometricErrorCorrectionAmount: geoAmt,
+    };
+    this.atmosphere?.setCloudShadow?.(opts);
+    this.aerial?.setCloudShadow?.(opts);
+  }
+
+  // ── TAA (inline CloudsResolvePass) ─────────────────────────────────────
+
+  _taaCapture() {
+    const gl = this.viewer.scene.context?._gl;
+    if (!gl) return;
+    const canvas = this.viewer.scene.canvas;
+    const w = canvas.width, h = canvas.height;
+    if (w !== this._taa.w || h !== this._taa.h) {
+      if (this._taa.texA) gl.deleteTexture(this._taa.texA);
+      if (this._taa.texB) gl.deleteTexture(this._taa.texB);
+      if (this._taa.pbo) gl.deleteBuffer(this._taa.pbo);
+      const mkTex = () => { const t = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, t); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.bindTexture(gl.TEXTURE_2D, null); return t; };
+      this._taa.texA = mkTex(); this._taa.texB = mkTex();
+      this._taa.pbo = gl.createBuffer(); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._taa.pbo); gl.bufferData(gl.PIXEL_PACK_BUFFER, w * h * 4, gl.STREAM_READ); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
+      this._taa.w = w; this._taa.h = h; this._taa.frameCount = 0; this._taa.pboReady = false;
+    }
+    const writeTex = this._taa.current === 0 ? this._taa.texB : this._taa.texA;
+    if (this._taa.pboReady) {
+      const prevTex = gl.getParameter(gl.TEXTURE_BINDING_2D);
+      const flipY = gl.getParameter(gl.UNPACK_FLIP_Y_WEBGL), premul = gl.getParameter(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL);
+      if (flipY) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
+      if (premul) gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
+      gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, this._taa.pbo);
+      gl.bindTexture(gl.TEXTURE_2D, writeTex);
+      gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, 0);
+      gl.bindTexture(gl.TEXTURE_2D, prevTex);
+      gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
+      if (flipY) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
+      if (premul) gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
+      this._taa.current = 1 - this._taa.current;
+      this._taa.frameCount++;
+    }
+    const prevFbo = gl.getParameter(gl.FRAMEBUFFER_BINDING);
+    gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+    gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._taa.pbo);
+    gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, 0);
+    gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
+    if (prevFbo) gl.bindFramebuffer(gl.FRAMEBUFFER, prevFbo);
+    this._taa.pboReady = true;
+  }
+
+  _taaUpdateVP() {
+    this._taa.prevVP = this._taa.curVP;
+    const cam = this.viewer.camera;
+    this._taa.curVP = Cesium.Matrix4.multiply(cam.frustum.projectionMatrix, cam.viewMatrix, new Cesium.Matrix4());
+  }
+
+  // ── GUI ────────────────────────────────────────────────────────────────
+
+  _setupGUI() {
+    if (this._gui) return;
+    this._gui = new dat.GUI({ name: "体积云管线" });
+    const p = this.params, ls = p.layers;
+    const f = this._gui.addFolder("云层");
+    f.add(ls[0], "altitude", 0, 20000, 50).name("层0底高(m)");
+    f.add(ls[0], "height", 0, 10000, 50).name("层0厚度(m)");
+    f.add(ls[0], "coverage", 0, 1, 0.01).name("层0覆盖度");
+    f.add(ls[1], "altitude", 0, 20000, 50).name("层1底高(m)");
+    f.add(ls[1], "height", 0, 10000, 50).name("层1厚度(m)");
+    f.add(ls[1], "coverage", 0, 1, 0.01).name("层1覆盖度");
+    f.add(ls[2], "altitude", 0, 20000, 50).name("层2底高(m)");
+    f.add(ls[2], "height", 0, 10000, 50).name("层2厚度(m)");
+    f.add(ls[2], "coverage", 0, 1, 0.01).name("层2覆盖度");
+    // 降高频相关参数(用于抑制边缘噪点/闪烁)
+    f.add(p, "shapeRepeat", 1.0, 8.0, 0.1).name("主体噪声频率");
+    f.add(p, "shapeDetailRepeat", 0.0005, 0.02, 0.0001).name("细节噪声频率");
+    f.add(ls[0], "shapeDetailAmount", 0.0, 1.5, 0.01).name("层0细节权重");
+    f.add(ls[1], "shapeDetailAmount", 0.0, 1.5, 0.01).name("层1细节权重");
+    f.add(ls[2], "shapeDetailAmount", 0.0, 1.5, 0.01).name("层2细节权重");
+    f.add(ls[0], "weatherExponent", 0.2, 2.0, 0.01).name("层0天气指数");
+    f.add(ls[1], "weatherExponent", 0.2, 2.0, 0.01).name("层1天气指数");
+    f.add(ls[2], "weatherExponent", 0.2, 2.0, 0.01).name("层2天气指数");
+    f.add(ls[0], "coverageFilterWidth", 0.1, 1.0, 0.01).name("层0覆盖过滤宽度");
+    f.add(ls[1], "coverageFilterWidth", 0.1, 1.0, 0.01).name("层1覆盖过滤宽度");
+    f.add(ls[2], "coverageFilterWidth", 0.1, 1.0, 0.01).name("层2覆盖过滤宽度");
+    f.open();
+    const l = this._gui.addFolder("光照");
+    l.add(p, "sunIntensity", 0, 150, 5).name("太阳强度");
+    l.add(p, "skyToSunRatio", 0.05, 0.6, 0.01).name("天空/太阳比");
+    l.add(p, "cloudExposure", 0.1, 5.0, 0.1).name("云曝光");
+    l.add(p, "magentaFixStrength", 0.0, 2.0, 0.05).name("落日去品红强度");
+    l.add(p, "edgeAlphaCutoff", 0.0, 0.2, 0.005).name("边缘Alpha裁剪");
+    l.add(p, "aerialPerspectiveScale", 0, 3, 0.1).name("大气透视");
+    const s = this._gui.addFolder("散射");
+    s.add(p, "scatterG1", 0, 0.99, 0.01).name("前向散射G");
+    s.add(p, "scatterG2", -0.99, 0, 0.01).name("后向散射G");
+    s.add(p, "multiScatteringOctaves", 1, 12, 1).name("多散射阶数");
+    const a = this._gui.addFolder("动画");
+    a.add(p, "windSpeed", 0, 1, 0.0001).name("风速");
+    a.add(p, "evolutionSpeed", 0, 0.0001, 0.000001).name("演化速度");
+    // 远处云距离衰减:调小 distFadeStart 让衰减更早开始(远处更疏),调大 distFadeEnd 让衰减更平缓
+    a.add(p, "distFadeStart", 5000, 100000, 1000).name("远处衰减起点(m)");
+    a.add(p, "distFadeEnd", 20000, 200000, 1000).name("远处衰减终点(m)");
+    const o = this._gui.addFolder("开关");
+    o.add(p, "cloudsVisible").name("显示云").onChange((v) => {
+      if (this.cloudStage) this.cloudStage.enabled = v;
+    });
+    o.add(p, "useShadowBuffer").name("BSM(云阴影)");
+    o.add(p, "shadowLengthEnabled").name("阴影长度(丁达尔)");
+    o.add(p, "hazeEnabled").name("雾效(HAZE)");
+    o.add(p, "temporalEnabled").name("TAA");
+    o.add(p, "maxSteps", 64, 1200, 1).name("主采样步数");
+    o.add(p, "minStepSize", 5.0, 200.0, 1.0).name("最小步长");
+    o.add(p, "blueNoiseScale", 0.25, 4.0, 0.05).name("噪声采样缩放");
+    o.add(p, "jitterStrength", 0.0, 1.0, 0.01).name("抖动强度");
+
+    // BSM OD 缩放联动:地面阴影实际在 AerialPerspectiveEffect stage 渲染,
+    // 但原 GUI 只绑了 AtmospherePostProcess 实例,调不动。这里统一驱动两侧。
+    const bsm = this._gui.addFolder("BSM 缩放");
+    const syncBsmScale = (key, val) => {
+      if (this.atmosphere) this.atmosphere[`_${key}`] = val;
+      if (this.aerial) this.aerial[`_${key}`] = val;
+    };
+    // 用 params 上的占位属性承载 GUI 值,初始与两侧默认(1.0)对齐
+    p._bsmGroundScale = 0.3; p._bsmTyndallScale = 1.0;
+    bsm.add(p, "_bsmGroundScale", 0.1, 20.0, 0.1).name("OD缩放(地面)").onChange((v) => syncBsmScale("bsmGroundOpticalDepthScale", v));
+    bsm.add(p, "_bsmTyndallScale", 0.1, 20.0, 0.1).name("OD缩放(光柱)").onChange((v) => syncBsmScale("bsmTyndallOpticalDepthScale", v));
+    // cascade 几何:调这三个解决"近处阴影被切割"。shadowFar=覆盖最远距离,splitLambda=近处分配(越大近处越多),fadeScale=ortho扩展
+    bsm.add(p, "shadowFar", 20000, 500000, 5000).name("阴影覆盖距离");
+    bsm.add(p, "shadowSplitLambda", 0.0, 1.0, 0.05).name("近处分配");
+    bsm.add(p, "shadowFadeScale", 0.0, 5.0, 0.1).name("边缘扩展");
+    // 关键:占位初始值不会自动触发 onChange,这里手动同步一次,否则启动时两侧 scale 仍是构造默认(1.0)
+    syncBsmScale("bsmGroundOpticalDepthScale", p._bsmGroundScale);
+    syncBsmScale("bsmTyndallOpticalDepthScale", p._bsmTyndallScale);
+  }
+
+  // ── BSM ShadowPass params (for CloudShadowPass) ────────────────────────
+
+  _getShadowPassParams() {
+    const ls = this.params.layers;
+    const minLayerHeights = [], maxLayerHeights = [], densityProfileLinear = [], densityProfileConstant = [];
+    const densityScales = [], shapeAmounts = [], shapeDetailAmounts = [], weatherExponents = [];
+    const shapeAlteringBiases = [], coverageFilterWidths = [], coverages = [];
+    let minAlt = 1e9, maxAltH = 0;
+    for (let i = 0; i < 4; i++) {
+      const a = Number(ls[i]?.altitude) || 0, h = Number(ls[i]?.height) || 0;
+      if (a + h > 0) { minAlt = Math.min(minAlt, a); maxAltH = Math.max(maxAltH, a + h); }
+      minLayerHeights[i] = a; maxLayerHeights[i] = a + h;
+      densityProfileLinear[i] = Number(ls[i]?.densityProfile?.linearTerm) ?? 0.75;
+      densityProfileConstant[i] = Number(ls[i]?.densityProfile?.constantTerm) ?? 0.25;
+      densityScales[i] = Number(ls[i]?.densityScale) || 0;
+      shapeAmounts[i] = Number(ls[i]?.shapeAmount) ?? 1;
+      shapeDetailAmounts[i] = Number(ls[i]?.shapeDetailAmount) ?? 1;
+      weatherExponents[i] = Number(ls[i]?.weatherExponent) ?? 1;
+      shapeAlteringBiases[i] = Number(ls[i]?.shapeAlteringBias) ?? 0.35;
+      coverageFilterWidths[i] = Number(ls[i]?.coverageFilterWidth) ?? 0.6;
+      coverages[i] = Number(ls[i]?.coverage) ?? 0.3;
+    }
+    const iv = this._getIntervalHeights();
+    const cBottom = Number.isFinite(minAlt) ? minAlt : 750;
+    return {
+      bottomRadius: Number(this.params.bottomRadius) || 6378137,
+      cloudBottomHeight: cBottom, cloudTopHeight: Math.max(0, maxAltH - cBottom) || 1500,
+      shadowBottomHeight: cBottom, shadowTopHeight: maxAltH || (cBottom + 1500),
+      // shadow cascade far 必须限制到云层相关距离,否则会取 Cesium 相机 frustum.far(~8e8),
+      // 导致 ortho proj radius 爆炸、数值精度崩坏产生 NaN、矩阵不可逆、BSM 全失效。
+      shadowFar: Number(this.params.shadowFar) || Number(this.params.maxShadowLengthRayDistance) || 200000.0,
+      maxShadowLengthRayDistance: Number(this.params.maxShadowLengthRayDistance) || 200000.0,
+      shadowSplitLambda: Number(this.params.shadowSplitLambda) || 0.5,
+      shadowFadeScale: Number(this.params.shadowFadeScale) || 1.0,
+      weatherRepeat: Number(this.params.weatherRepeat) || 100, windSpeed: Number(this.params.windSpeed) || 0,
+      shapeRepeat: (Number(this.params.shapeRepeat) || 3) / 1e4,
+      shapeDetailRepeat: Number(this.params.shapeDetailRepeat) || 0.006,
+      turbulenceRepeat: Number(this.params.turbulenceRepeat) || 2,
+      turbulenceDisplacement: Number(this.params.turbulenceDisplacement) || 400,
+      coverage: Math.max(...coverages), densityScale: Math.max(...densityScales),
+      scatteringCoefficient: Number(this.params.scatteringCoefficient) ?? 0.9,
+      absorptionCoefficient: Number(this.params.absorptionCoefficient) ?? 1.0,
+      startTime: performance.now() / 1000, evolutionSpeed: Number(this.params.evolutionSpeed) || 0.005,
+      maxSteps: this.params.maxSteps, minStepSize: this.params.minStepSize,
+      minDensity: this.params.minDensity ?? 1e-5, minExtinction: this.params.minExtinction ?? 1e-5,
+      minTransmittance: this.params.minTransmittance ?? 0.01, opticalDepthTailScale: 1.0,
+      minLayerHeights, maxLayerHeights, densityProfileLinear, densityProfileConstant,
+      densityProfileExpTerms: [0,0,0,0], densityProfileExponents: [0,0,0,0],
+      densityScales, shapeAmounts, shapeDetailAmounts, weatherExponents,
+      shapeAlteringBiases, coverageFilterWidths, coverages,
+      minIntervalHeights: [iv.min.x, iv.min.y, iv.min.z],
+      maxIntervalHeights: [iv.max.x, iv.max.y, iv.max.z],
+      localWeatherOffset: [0, 0], shapeOffset: [0, 0, 0], shapeDetailOffset: [0, 0, 0],
+    };
+  }
+
+  // ── Init ───────────────────────────────────────────────────────────────
+
+  async init() {
+    if (this._ready) return this._ready;
+    this._ready = (async () => {
+      const viewer = this.viewer;
+      viewer.scene.globe.depthTestAgainstTerrain = true;
+
+      // 1. Atmosphere
+      this.atmosphere = new AtmospherePostProcess(viewer, {
+        atmosphereParams: this.atmosphereParams, renderSky: true,
+        applyGroundAtmosphere: false, autoAddStage: false,
+        assetsBaseUrl: this.atmosphereAssetsBase, shaderBaseUrl: this.atmosphereShaderBase,
+      });
+      await this.atmosphere.init();
+
+      // 2. Aerial
+      this.aerial = new AerialPerspectiveEffect(viewer, {
+        atmosphereParams: this.atmosphereParams, autoAddStage: false,
+        assetsBaseUrl: this.atmosphereAssetsBase, shaderBaseUrl: this.atmosphereShaderBase,
+      });
+
+      // 3. Load cloud textures + build shader
+      await this._loadTextures();
+      const fragmentShader = await this._buildCloudFragmentShader();
+
+      // 4. BSM passes (import dynamically to avoid circular deps)
+      const { CloudShadowPass } = await import("./CloudShadowPass.js");
+      const { ShadowResolvePass } = await import("./ShadowResolvePass.js");
+      if (this.params.useShadowBuffer && this.textures) {
+        this._bsm.pass = new CloudShadowPass(viewer, { textures: this.textures, params: this._getShadowPassParams() });
+        this._bsm.pass.init();
+        // 静止 temporalAlpha 对齐 three-geospatial≈0.01;运动时 ShadowResolvePass 内会抬高 alpha
+        this._bsm.resolve = new ShadowResolvePass(viewer, { size: SHADOW_MAP_SIZE, temporalAlpha: 0.01 });
+        this._bsm.resolve.setInputTextures(this._bsm.pass.getTexture(), this._bsm.pass.getDepthVelocityTexture());
+        this._bsm.resolve.init();
+      }
+
+      // 5. Aerial init
+      await this.aerial.init();
+
+      // 6. 初始化原始 WebGL2 云渲染管线
+      // 注意:大气/空中透视仍使用 Cesium PostProcessStage(提供天空背景),
+      // 但它们的 sampler3D uniform 在 Cesium binding 中可能失败。
+      // 因此这里不注册任何 PostProcessStage,只渲染云层。
+      const gl = viewer.scene.context._gl;
+      const rawOk = gl ? this._initRawWebGL(gl) : false;
+      if (!rawOk) {
+        console.warn('[Pipeline] 原始 WebGL2 云渲染初始化失败,回退使用 Cesium PostProcessStage');
+        // 回退:使用原来的 PostProcessStage(但 sampler3D 可能失败)
+        const uniforms = this._buildCloudUniforms();
+        this.cloudStage = new Cesium.PostProcessStage({
+          name: "GeospatialVolumetricClouds", fragmentShader, uniforms,
+        });
+        this.cloudStage.enabled = this.params.cloudsVisible;
+        const stages = viewer.scene.postProcessStages;
+        if (this.atmosphere.stage) stages.add(this.atmosphere.stage);
+        if (this.aerial.stage) stages.add(this.aerial.stage);
+        stages.add(this.cloudStage);
+      } else {
+        console.log('[Pipeline] 使用原始 WebGL2 渲染云层(跳过 PostProcessStage)');
+        // 仅用 Cesium 的默认 skyBox 作为背景
+      }
+
+      // 7. preRender: BSM sync
+      this._listeners.push(viewer.scene.preRender.addEventListener(() => this._syncBSM()));
+
+      // 8. postRender: TAA capture + frame count + 原始 WebGL2 云渲染
+      this._listeners.push(viewer.scene.postRender.addEventListener(() => {
+        this._taaUpdateVP();
+        if (this.params.temporalEnabled) this._taaCapture();
+        this._frameCount++;
+        // 如果使用原始 WebGL2 渲染,在 postRender 中执行
+        if (rawOk && this.params.cloudsVisible) {
+          try { this._renderRawClouds(gl); } catch (e) {
+            console.warn('[Pipeline] 原始 WebGL2 云渲染失败:', e.message);
+          }
+        }
+      }));
+      this._listeners.push(viewer.camera.changed.addEventListener(() => {
+        const c = Cesium.Cartographic.fromCartesian(
+          viewer.camera.positionWC,
+          viewer.scene.globe.ellipsoid
+        );
+        const ellipsoidHeight = Number(c?.height) || 0;
+        const atmBottomRadius = Number(
+          this.atmosphere?.getAtmosphereForClouds?.()?.getUniforms?.()?.bottomRadius?.() ?? NaN
+        );
+        const usedBottomRadius = Number.isFinite(atmBottomRadius)
+          ? atmBottomRadius
+          : (Number(this.params.bottomRadius) || 0);
+        const corr = this._getAltitudeCorrectionOffset(usedBottomRadius);
+        const correctedPos = Cesium.Cartesian3.add(
+          viewer.camera.positionWC,
+          corr,
+          new Cesium.Cartesian3()
+        );
+        const correctedHeight = Math.max(
+          0,
+          Cesium.Cartesian3.magnitude(correctedPos) - usedBottomRadius
+        );
+      }));
+
+      this._setupGUI();
+      console.log("[Pipeline] ready: Cloud -> Atmosphere -> Aerial");
+    })();
+    return this._ready;
+  }
+
+  // ── Destroy ────────────────────────────────────────────────────────────
+
+  destroy() {
+    for (const remove of this._listeners) if (typeof remove === "function") remove();
+    this._listeners = [];
+    const stages = this.viewer?.scene?.postProcessStages;
+    if (stages && this.cloudStage) { try { stages.remove(this.cloudStage); } catch {} }
+    this.cloudStage = null;
+    try { this.aerial?.destroy(); } catch {} this.aerial = null;
+    try { this.atmosphere?.destroy(); } catch {} this.atmosphere = null;
+    try { this._bsm.pass?.destroy(); } catch {} this._bsm.pass = null;
+    try { this._bsm.resolve?.destroy(); } catch {} this._bsm.resolve = null;
+    const gl = this.viewer?.scene?.context?._gl;
+    if (gl) {
+      // 清理 BSM 资源
+      if (this._bsm.blitFbo) gl.deleteFramebuffer(this._bsm.blitFbo);
+      if (this._bsm.blitProg) gl.deleteProgram(this._bsm.blitProg);
+      if (this._bsm.blitVbo) gl.deleteBuffer(this._bsm.blitVbo);
+      // 清理 TAA 资源
+      if (this._taa.texA) gl.deleteTexture(this._taa.texA);
+      if (this._taa.texB) gl.deleteTexture(this._taa.texB);
+      if (this._taa.pbo) gl.deleteBuffer(this._taa.pbo);
+      // 清理原始 WebGL2 云渲染资源
+      if (this._raw.prog) gl.deleteProgram(this._raw.prog);
+      if (this._raw.vao) gl.deleteVertexArray(this._raw.vao);
+      if (this._raw.vbo) gl.deleteBuffer(this._raw.vbo);
+      if (this._raw.sceneColor) gl.deleteTexture(this._raw.sceneColor);
+      if (this._raw.depthTex) gl.deleteTexture(this._raw.depthTex);
+      if (this._raw.depthFbo) gl.deleteFramebuffer(this._raw.depthFbo);
+    }
+    this._bsm = { pass: null, resolve: null, blitFbo: null, blitProg: null, blitVbo: null };
+    this._taa = { texA: null, texB: null, current: 0, pbo: null, pboReady: false, w: 0, h: 0, frameCount: 0, prevVP: null, curVP: null };
+    this._raw = { prog: null, vao: null, vbo: null, sceneColor: null, depthTex: null, depthFbo: null, depthW: 0, depthH: 0, texUnits: {} };
+    if (this.textures) { for (const k in this.textures) { try { this.textures[k]?.destroy?.(); } catch {} } this.textures = null; }
+    if (this._gui) { this._gui.destroy(); this._gui = null; }
+    this._ready = null;
+  }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов