/** * 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; } }