UNPKG

@takram/three-clouds

Version:
1,275 lines (1,122 loc) 114 kB
"use strict";const w=require("postprocessing"),n=require("three"),z=require("@takram/three-atmosphere"),u=require("@takram/three-geospatial"),P=require("@takram/three-atmosphere/shaders/bruneton"),g=require("@takram/three-geospatial/shaders");class b{constructor(e=0,t=0,a=0,i=0){this.expTerm=e,this.exponent=t,this.linearTerm=a,this.constantTerm=i}set(e=0,t=0,a=0,i=0){return this.expTerm=e,this.exponent=t,this.linearTerm=a,this.constantTerm=i,this}clone(){return new b(this.expTerm,this.exponent,this.linearTerm,this.constantTerm)}copy(e){return this.expTerm=e.expTerm,this.exponent=e.exponent,this.linearTerm=e.linearTerm,this.constantTerm=e.constantTerm,this}}const me=["channel","altitude","height","densityScale","shapeAmount","shapeDetailAmount","weatherExponent","shapeAlteringBias","coverageFilterWidth","shadow","densityProfile"];function ve(o,e){if(e!=null)for(const t of me){const a=e[t];a!=null&&(o[t]instanceof b?o[t].copy(a):o[t]=a)}}const I=class I{constructor(e){this.channel="r",this.altitude=0,this.height=0,this.densityScale=.2,this.shapeAmount=1,this.shapeDetailAmount=1,this.weatherExponent=1,this.shapeAlteringBias=.35,this.coverageFilterWidth=.6,this.densityProfile=new b(0,0,.75,.25),this.shadow=!1,this.set(e)}set(e){return ve(this,e),this}clone(){return new I(this)}copy(e){return this.channel=e.channel,this.altitude=e.altitude,this.height=e.height,this.densityScale=e.densityScale,this.shapeAmount=e.shapeAmount,this.shapeDetailAmount=e.shapeDetailAmount,this.weatherExponent=e.weatherExponent,this.shapeAlteringBias=e.shapeAlteringBias,this.coverageFilterWidth=e.coverageFilterWidth,this.densityProfile.copy(e.densityProfile),this.shadow=e.shadow,this}};I.DEFAULT=new I;let S=I;const O=Array.from({length:8},()=>({value:0,flag:0})),L=Array.from({length:3},()=>({min:0,max:0}));function ge(o,e){return o.value!==e.value?o.value-e.value:o.flag-e.flag}const M=class M extends Array{constructor(e){super(new S(e?.[0]),new S(e?.[1]),new S(e?.[2]),new S(e?.[3]))}set(e){return this[0].set(e?.[0]),this[1].set(e?.[1]),this[2].set(e?.[2]),this[3].set(e?.[3]),this}reset(){return this[0].copy(S.DEFAULT),this[1].copy(S.DEFAULT),this[2].copy(S.DEFAULT),this[3].copy(S.DEFAULT),this}clone(){return new M(this)}copy(e){return this[0].copy(e[0]),this[1].copy(e[1]),this[2].copy(e[2]),this[3].copy(e[3]),this}get localWeatherChannels(){return this[0].channel+this[1].channel+this[2].channel+this[3].channel}packValues(e,t){return t.set(this[0][e],this[1][e],this[2][e],this[3][e])}packSums(e,t,a){return a.set(this[0][e]+this[0][t],this[1][e]+this[1][t],this[2][e]+this[2][t],this[3][e]+this[3][t])}packDensityProfiles(e,t){return t.set(this[0].densityProfile[e],this[1].densityProfile[e],this[2].densityProfile[e],this[3].densityProfile[e])}packIntervalHeights(e,t){for(let s=0;s<4;++s){const l=this[s];let h=O[s];h.value=l.altitude,h.flag=0,h=O[s+4],h.value=l.altitude+l.height,h.flag=1}O.sort(ge);let a=0,i=0;for(let s=0;s<O.length;++s){const{value:l,flag:h}=O[s];if(i===0&&s>0){const d=L[a++];d.min=O[s-1].value,d.max=l}i+=h===0?1:-1}for(;a<3;++a){const s=L[a];s.min=0,s.max=0}let r=L[0];e.x=r.min,t.x=r.max,r=L[1],e.y=r.min,t.y=r.max,r=L[2],e.z=r.min,t.z=r.max}};M.DEFAULT=new M([{channel:"r",altitude:750,height:650,densityScale:.2,shapeAmount:1,shapeDetailAmount:1,weatherExponent:1,shapeAlteringBias:.35,coverageFilterWidth:.6,shadow:!0},{channel:"g",altitude:1e3,height:1200,densityScale:.2,shapeAmount:1,shapeDetailAmount:1,weatherExponent:1,shapeAlteringBias:.35,coverageFilterWidth:.6,shadow:!0},{channel:"b",altitude:7500,height:500,densityScale:.003,shapeAmount:.4,shapeDetailAmount:0,weatherExponent:1,shapeAlteringBias:.35,coverageFilterWidth:.5},{channel:"a"}]);let W=M;var Se=process.env.NODE_ENV==="production",J="Invariant failed";function D(o,e){if(!o){if(Se)throw new Error(J);var t=J;throw new Error(t)}}class N{constructor(e,t){this.near=[new n.Vector3,new n.Vector3,new n.Vector3,new n.Vector3],this.far=[new n.Vector3,new n.Vector3,new n.Vector3,new n.Vector3],e!=null&&t!=null&&this.setFromCamera(e,t)}clone(){return new N().copy(this)}copy(e){for(let t=0;t<4;++t)this.near[t].copy(e.near[t]),this.far[t].copy(e.far[t]);return this}setFromCamera(e,t){const a=e.isOrthographicCamera===!0,i=e.projectionMatrixInverse;this.near[0].set(1,1,-1),this.near[1].set(1,-1,-1),this.near[2].set(-1,-1,-1),this.near[3].set(-1,1,-1);for(let r=0;r<4;++r)this.near[r].applyMatrix4(i);this.far[0].set(1,1,1),this.far[1].set(1,-1,1),this.far[2].set(-1,-1,1),this.far[3].set(-1,1,1);for(let r=0;r<4;++r){const s=this.far[r];s.applyMatrix4(i);const l=Math.abs(s.z);a?s.z*=Math.min(t/l,1):s.multiplyScalar(Math.min(t/l,1))}return this}split(e,t=[]){for(let a=0;a<e.length;++a){const i=t[a]??=new N;if(a===0)for(let r=0;r<4;++r)i.near[r].copy(this.near[r]);else for(let r=0;r<4;++r)i.near[r].lerpVectors(this.near[r],this.far[r],e[a-1]);if(a===e.length-1)for(let r=0;r<4;++r)i.far[r].copy(this.far[r]);else for(let r=0;r<4;++r)i.far[r].lerpVectors(this.near[r],this.far[r],e[a])}return t.length=e.length,t}applyMatrix4(e){for(let t=0;t<4;++t)this.near[t].applyMatrix4(e),this.far[t].applyMatrix4(e);return this}}const ye={uniform:(o,e,t,a,i=[])=>{for(let r=0;r<o;++r)i[r]=(e+(t-e)*(r+1)/o)/t;return i.length=o,i},logarithmic:(o,e,t,a,i=[])=>{for(let r=0;r<o;++r)i[r]=e*(t/e)**((r+1)/o)/t;return i.length=o,i},practical:(o,e,t,a=.5,i=[])=>{for(let r=0;r<o;++r){const s=(e+(t-e)*(r+1)/o)/t,l=e*(t/e)**((r+1)/o)/t;i[r]=u.lerp(s,l,a)}return i.length=o,i}};function xe(o,e,t,a,i,r=[]){return ye[o](e,t,a,i,r)}const Q=new n.Vector3,ee=new n.Vector3,we=new n.Matrix4,te=new n.Matrix4,Ce=new N,Te=new n.Box3,De={maxFar:null,farScale:1,splitMode:"practical",splitLambda:.5,margin:0,fade:!0};class Ee{constructor(e){this.cascades=[],this.mapSize=new n.Vector2,this.cameraFrustum=new N,this.frusta=[],this.splits=[],this._far=0;const{cascadeCount:t,mapSize:a,maxFar:i,farScale:r,splitMode:s,splitLambda:l,margin:h,fade:d}={...De,...e};this.cascadeCount=t,this.mapSize.copy(a),this.maxFar=i,this.farScale=r,this.splitMode=s,this.splitLambda=l,this.margin=h,this.fade=d}get cascadeCount(){return this.cascades.length}set cascadeCount(e){if(e!==this.cascadeCount){for(let t=0;t<e;++t)this.cascades[t]??={interval:new n.Vector2,matrix:new n.Matrix4,inverseMatrix:new n.Matrix4,projectionMatrix:new n.Matrix4,inverseProjectionMatrix:new n.Matrix4,viewMatrix:new n.Matrix4,inverseViewMatrix:new n.Matrix4};this.cascades.length=e}}get far(){return this._far}updateIntervals(e){const t=this.cascadeCount,a=this.splits,i=this.far;xe(this.splitMode,t,e.near,i,this.splitLambda,a),this.cameraFrustum.setFromCamera(e,i),this.cameraFrustum.split(a,this.frusta);const r=this.cascades;for(let s=0;s<t;++s)r[s].interval.set(a[s-1]??0,a[s]??0)}getFrustumRadius(e,t){const a=t.near,i=t.far;let r=Math.max(i[0].distanceTo(i[2]),i[0].distanceTo(a[2]));if(this.fade){const s=e.near,l=this.far,h=i[0].z/(l-s);r+=.25*h**2*(l-s)}return r*.5}updateMatrices(e,t,a=1){const i=we.lookAt(Q.setScalar(0),ee.copy(t).multiplyScalar(-1),n.Object3D.DEFAULT_UP),r=te.multiplyMatrices(te.copy(i).invert(),e.matrixWorld),s=this.frusta,l=this.cascades;D(s.length===l.length);const h=this.margin,d=this.mapSize;for(let p=0;p<s.length;++p){const f=s[p],y=l[p],C=this.getFrustumRadius(e,s[p]),A=-C,_=C,Z=C,q=-C;y.projectionMatrix.makeOrthographic(A,_,Z,q,-this.margin,C*2+this.margin);const{near:pe,far:fe}=Ce.copy(f).applyMatrix4(r),H=Te.makeEmpty();for(let F=0;F<4;F++)H.expandByPoint(pe[F]),H.expandByPoint(fe[F]);const T=H.getCenter(Q);T.z=H.max.z+h;const K=(_-A)/d.width,X=(Z-q)/d.height;T.x=Math.round(T.x/K)*K,T.y=Math.round(T.y/X)*X,T.applyMatrix4(i);const $=ee.copy(t).multiplyScalar(a).add(T);y.inverseViewMatrix.lookAt(T,$,n.Object3D.DEFAULT_UP).setPosition($)}}update(e,t,a){this._far=this.maxFar!=null?Math.min(this.maxFar,e.far*this.farScale):e.far*this.farScale,this.updateIntervals(e),this.updateMatrices(e,t,a);const i=this.cascades,r=this.cascadeCount;for(let s=0;s<r;++s){const{matrix:l,inverseMatrix:h,projectionMatrix:d,inverseProjectionMatrix:p,viewMatrix:f,inverseViewMatrix:y}=i[s];p.copy(d).invert(),f.copy(y).invert(),l.copy(d).multiply(f),h.copy(y).multiply(p)}}}const ne=[0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5],ie=ne.reduce((o,e,t)=>{const a=new n.Vector2;for(let i=0;i<16;++i)if(ne[i]===t){a.set((i%4+.5)/4,(Math.floor(i/4)+.5)/4);break}return[...o,a]},[]),Ae={resolutionScale:1,lightShafts:!0,shapeDetail:!0,turbulence:!0,haze:!0,clouds:{multiScatteringOctaves:8,accurateSunSkyLight:!0,accuratePhaseFunction:!1,maxIterationCount:500,minStepSize:50,maxStepSize:1e3,maxRayDistance:2e5,perspectiveStepScale:1.01,minDensity:1e-5,minExtinction:1e-5,minTransmittance:.01,maxIterationCountToGround:3,maxIterationCountToSun:2,minSecondaryStepSize:100,secondaryStepScale:2,maxShadowLengthIterationCount:500,minShadowLengthStepSize:50,maxShadowLengthRayDistance:2e5},shadow:{cascadeCount:3,mapSize:new n.Vector2(512,512),maxIterationCount:50,minStepSize:100,maxStepSize:1e3,minDensity:1e-5,minExtinction:1e-5,minTransmittance:1e-4}},c=Ae,_e={low:{...c,lightShafts:!1,shapeDetail:!1,turbulence:!1,clouds:{...c.clouds,accurateSunSkyLight:!1,maxIterationCount:200,minStepSize:100,maxRayDistance:1e5,minDensity:1e-4,minExtinction:1e-4,minTransmittance:.1,maxIterationCountToGround:0,maxIterationCountToSun:1},shadow:{...c.shadow,maxIterationCount:25,minDensity:1e-4,minExtinction:1e-4,minTransmittance:.01,cascadeCount:2,mapSize:new n.Vector2(256,256)}},medium:{...c,lightShafts:!1,turbulence:!1,clouds:{...c.clouds,minDensity:1e-4,minExtinction:1e-4,accurateSunSkyLight:!1,maxIterationCountToSun:2,maxIterationCountToGround:1},shadow:{...c.shadow,minDensity:1e-4,minExtinction:1e-4,mapSize:new n.Vector2(256,256)}},high:c,ultra:{...c,clouds:{...c.clouds,minStepSize:10},shadow:{...c.shadow,mapSize:new n.Vector2(1024,1024)}}},Pe=`precision highp float; precision highp sampler3D; precision highp sampler2DArray; #include <common> #include <packing> #include "core/depth" #include "core/math" #include "core/turbo" #include "core/generators" #include "core/raySphereIntersection" #include "core/cascadedShadowMaps" #include "core/interleavedGradientNoise" #include "core/vogelDisk" #include "atmosphere/bruneton/definitions" 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 sampler2D irradiance_texture; uniform sampler3D single_mie_scattering_texture; uniform sampler3D higher_order_scattering_texture; #include "atmosphere/bruneton/common" #include "atmosphere/bruneton/runtime" #include "types" #include "parameters" #include "clouds" #if !defined(RECIPROCAL_PI4) #define RECIPROCAL_PI4 0.07957747154594767 #endif // !defined(RECIPROCAL_PI4) uniform sampler2D depthBuffer; uniform mat4 viewMatrix; uniform mat4 reprojectionMatrix; uniform mat4 viewReprojectionMatrix; uniform float cameraNear; uniform float cameraFar; uniform float cameraHeight; uniform vec2 temporalJitter; uniform vec2 targetUvScale; uniform float mipLevelScale; // Scattering const vec2 scatterAnisotropy = vec2(SCATTER_ANISOTROPY_1, SCATTER_ANISOTROPY_2); const float scatterAnisotropyMix = SCATTER_ANISOTROPY_MIX; uniform float skyLightScale; uniform float groundBounceScale; uniform float powderScale; uniform float powderExponent; // Primary raymarch uniform int maxIterationCount; uniform float minStepSize; uniform float maxStepSize; uniform float maxRayDistance; uniform float perspectiveStepScale; // Secondary raymarch uniform int maxIterationCountToSun; uniform int maxIterationCountToGround; uniform float minSecondaryStepSize; uniform float secondaryStepScale; // Beer shadow map uniform sampler2DArray shadowBuffer; uniform vec2 shadowTexelSize; uniform vec2 shadowIntervals[SHADOW_CASCADE_COUNT]; uniform mat4 shadowMatrices[SHADOW_CASCADE_COUNT]; uniform float shadowFar; uniform float maxShadowFilterRadius; // Shadow length #ifdef SHADOW_LENGTH uniform int maxShadowLengthIterationCount; uniform float minShadowLengthStepSize; uniform float maxShadowLengthRayDistance; #endif // SHADOW_LENGTH in vec2 vUv; in vec3 vCameraPosition; in vec3 vCameraDirection; // Direction to the center of screen in vec3 vRayDirection; // Direction to the texel in vec3 vViewPosition; in GroundIrradiance vGroundIrradiance; in CloudsIrradiance vCloudsIrradiance; layout(location = 0) out vec4 outputColor; layout(location = 1) out vec3 outputDepthVelocity; #ifdef SHADOW_LENGTH layout(location = 2) out float outputShadowLength; #endif // SHADOW_LENGTH float readDepth(const vec2 uv) { #if DEPTH_PACKING == 3201 return unpackRGBAToDepth(texture(depthBuffer, uv)); #else // DEPTH_PACKING == 3201 return texture(depthBuffer, uv).r; #endif // DEPTH_PACKING == 3201 } float getViewZ(const float depth) { #ifdef PERSPECTIVE_CAMERA return perspectiveDepthToViewZ(depth, cameraNear, cameraFar); #else // PERSPECTIVE_CAMERA return orthographicDepthToViewZ(depth, cameraNear, cameraFar); #endif // PERSPECTIVE_CAMERA } vec3 ecefToWorld(const vec3 positionECEF) { return (ecefToWorldMatrix * vec4(positionECEF - altitudeCorrection, 1.0)).xyz; } vec2 getShadowUv(const vec3 worldPosition, const int cascadeIndex) { vec4 clip = shadowMatrices[cascadeIndex] * vec4(worldPosition, 1.0); clip /= clip.w; return clip.xy * 0.5 + 0.5; } float getDistanceToShadowTop(const vec3 rayPosition) { // Distance to the top of the shadows along the sun direction, which matches // the ray origin of BSM. return raySphereSecondIntersection( rayPosition, sunDirection, vec3(0.0), bottomRadius + shadowTopHeight ); } #ifdef DEBUG_SHOW_CASCADES const vec3 cascadeColors[4] = vec3[4]( vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(1.0, 1.0, 0.0) ); vec3 getCascadeColor(const vec3 rayPosition) { vec3 worldPosition = ecefToWorld(rayPosition); int cascadeIndex = getCascadeIndex( viewMatrix, worldPosition, shadowIntervals, cameraNear, shadowFar ); vec2 uv = getShadowUv(worldPosition, cascadeIndex); if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { return vec3(1.0); } return cascadeColors[cascadeIndex]; } vec3 getFadedCascadeColor(const vec3 rayPosition, const float jitter) { vec3 worldPosition = ecefToWorld(rayPosition); int cascadeIndex = getFadedCascadeIndex( viewMatrix, worldPosition, shadowIntervals, cameraNear, shadowFar, jitter ); return cascadeIndex >= 0 ? cascadeColors[cascadeIndex] : vec3(1.0); } #endif // DEBUG_SHOW_CASCADES float readShadowOpticalDepth( const vec2 uv, const float distanceToTop, const float distanceOffset, const int cascadeIndex ) { // r: frontDepth, g: meanExtinction, b: maxOpticalDepth, a: maxOpticalDepthTail // Also see the discussion here: https://x.com/shotamatsuda/status/1885322308908442106 vec4 shadow = texture(shadowBuffer, vec3(uv, float(cascadeIndex))); float distanceToFront = max(0.0, distanceToTop - distanceOffset - shadow.r); return min(shadow.b + shadow.a, shadow.g * distanceToFront); } float sampleShadowOpticalDepthPCF( const vec3 worldPosition, const float distanceToTop, const float distanceOffset, const float radius, const int cascadeIndex ) { vec2 uv = getShadowUv(worldPosition, cascadeIndex); 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, distanceToTop, distanceOffset, cascadeIndex); } float sum = 0.0; vec2 offset; #pragma unroll_loop_start for (int i = 0; i < 16; ++i) { #if UNROLLED_LOOP_INDEX < SHADOW_SAMPLE_COUNT offset = vogelDisk( UNROLLED_LOOP_INDEX, SHADOW_SAMPLE_COUNT, interleavedGradientNoise(gl_FragCoord.xy + temporalJitter * resolution) * PI2 ); sum += readShadowOpticalDepth( uv + offset * radius * shadowTexelSize, distanceToTop, distanceOffset, cascadeIndex ); #endif // UNROLLED_LOOP_INDEX < SHADOW_SAMPLE_COUNT } #pragma unroll_loop_end return sum / float(SHADOW_SAMPLE_COUNT); } float sampleShadowOpticalDepth( const vec3 rayPosition, const float distanceOffset, const float radius, const float jitter ) { float distanceToTop = getDistanceToShadowTop(rayPosition); if (distanceToTop <= 0.0) { return 0.0; } vec3 worldPosition = ecefToWorld(rayPosition); int cascadeIndex = getFadedCascadeIndex( viewMatrix, worldPosition, shadowIntervals, cameraNear, shadowFar, jitter ); return cascadeIndex >= 0 ? sampleShadowOpticalDepthPCF( worldPosition, distanceToTop, distanceOffset, radius, cascadeIndex ) : 0.0; } #ifdef DEBUG_SHOW_SHADOW_MAP vec4 getCascadedShadowMaps(vec2 uv) { vec4 coord = vec4(vUv, vUv - 0.5) * 2.0; vec4 shadow = vec4(0.0); if (uv.y > 0.5) { if (uv.x < 0.5) { shadow = texture(shadowBuffer, vec3(coord.xw, 0.0)); } else { #if SHADOW_CASCADE_COUNT > 1 shadow = texture(shadowBuffer, vec3(coord.zw, 1.0)); #endif // SHADOW_CASCADE_COUNT > 1 } } else { if (uv.x < 0.5) { #if SHADOW_CASCADE_COUNT > 2 shadow = texture(shadowBuffer, vec3(coord.xy, 2.0)); #endif // SHADOW_CASCADE_COUNT > 2 } else { #if SHADOW_CASCADE_COUNT > 3 shadow = texture(shadowBuffer, vec3(coord.zy, 3.0)); #endif // SHADOW_CASCADE_COUNT > 3 } } #if !defined(DEBUG_SHOW_SHADOW_MAP_TYPE) #define DEBUG_SHOW_SHADOW_MAP_TYPE 0 #endif // !defined(DEBUG_SHOW_SHADOW_MAP_TYPE const float frontDepthScale = 1e-5; const float meanExtinctionScale = 10.0; const float maxOpticalDepthScale = 0.01; vec3 color; #if DEBUG_SHOW_SHADOW_MAP_TYPE == 1 color = vec3(shadow.r * frontDepthScale); #elif DEBUG_SHOW_SHADOW_MAP_TYPE == 2 color = vec3(shadow.g * meanExtinctionScale); #elif DEBUG_SHOW_SHADOW_MAP_TYPE == 3 color = vec3((shadow.b + shadow.a) * maxOpticalDepthScale); #else // DEBUG_SHOW_SHADOW_MAP_TYPE color = (shadow.rgb + vec3(0.0, 0.0, shadow.a)) * vec3(frontDepthScale, meanExtinctionScale, maxOpticalDepthScale); #endif // DEBUG_SHOW_SHADOW_MAP_TYPE return vec4(color, 1.0); } #endif // DEBUG_SHOW_SHADOW_MAP vec2 henyeyGreenstein(const vec2 g, const float cosTheta) { vec2 g2 = g * g; // prettier-ignore return RECIPROCAL_PI4 * ((1.0 - g2) / max(vec2(1e-7), pow(1.0 + g2 - 2.0 * g * cosTheta, vec2(1.5)))); } #ifdef ACCURATE_PHASE_FUNCTION float draine(float u, float g, float a) { float g2 = g * g; // prettier-ignore return (1.0 - g2) * (1.0 + a * u * u) / (4.0 * (1.0 + a * (1.0 + 2.0 * g2) / 3.0) * PI * pow(1.0 + g2 - 2.0 * g * u, 1.5)); } // Numerically-fitted large particles (d=10) phase function It won't be // plausible without a more precise multiple scattering. // Reference: https://research.nvidia.com/labs/rtr/approximate-mie/ float phaseFunction(const float cosTheta, const float attenuation) { const float gHG = 0.988176691700256; // exp(-0.0990567/(d-1.67154)) const float gD = 0.5556712547839497; // exp(-2.20679/(d+3.91029) - 0.428934) const float alpha = 21.995520856274638; // exp(3.62489 - 8.29288/(d+5.52825)) const float weight = 0.4819554318404214; // exp(-0.599085/(d-0.641583)-0.665888) return mix( henyeyGreenstein(vec2(gHG) * attenuation, cosTheta).x, draine(cosTheta, gD * attenuation, alpha), weight ); } #else // ACCURATE_PHASE_FUNCTION float phaseFunction(const float cosTheta, const float attenuation) { const vec2 g = scatterAnisotropy; const vec2 weights = vec2(1.0 - scatterAnisotropyMix, scatterAnisotropyMix); // A similar approximation is described in the Frostbite's paper, where phase // angle is attenuated instead of anisotropy. return dot(henyeyGreenstein(g * attenuation, cosTheta), weights); } #endif // ACCURATE_PHASE_FUNCTION float phaseFunction(const float cosTheta) { return phaseFunction(cosTheta, 1.0); } float marchOpticalDepth( const vec3 rayOrigin, const vec3 rayDirection, const int maxIterationCount, const float mipLevel, const float jitter, out float rayDistance ) { int iterationCount = int( max(0.0, remap(mipLevel, 0.0, 1.0, float(maxIterationCount + 1), 1.0) - jitter) ); if (iterationCount == 0) { // Fudge factor to approximate the mean optical depth. // TODO: Remove it. return 0.5; } float stepSize = minSecondaryStepSize / float(iterationCount); float nextDistance = stepSize * jitter; float opticalDepth = 0.0; for (int i = 0; i < iterationCount; ++i) { rayDistance = nextDistance; vec3 position = rayDistance * rayDirection + rayOrigin; vec2 uv = getGlobeUv(position); float height = length(position) - bottomRadius; WeatherSample weather = sampleWeather(uv, height, mipLevel); MediaSample media = sampleMedia(weather, position, uv, mipLevel, jitter); opticalDepth += media.extinction * stepSize; nextDistance += stepSize; stepSize *= secondaryStepScale; } return opticalDepth; } float marchOpticalDepth( const vec3 rayOrigin, const vec3 rayDirection, const int maxIterationCount, const float mipLevel, const float jitter ) { float rayDistance; return marchOpticalDepth( rayOrigin, rayDirection, maxIterationCount, mipLevel, jitter, rayDistance ); } float approximateMultipleScattering(const float opticalDepth, const float cosTheta) { // Multiple scattering approximation // See: https://fpsunflower.github.io/ckulla/data/oz_volumes.pdf // a: attenuation, b: contribution, c: phase attenuation vec3 coeffs = vec3(1.0); // [a, b, c] const vec3 attenuation = vec3(0.5, 0.5, 0.5); // Should satisfy a <= b float scattering = 0.0; float beerLambert; #pragma unroll_loop_start for (int i = 0; i < 12; ++i) { #if UNROLLED_LOOP_INDEX < MULTI_SCATTERING_OCTAVES beerLambert = exp(-opticalDepth * coeffs.y); scattering += coeffs.x * beerLambert * phaseFunction(cosTheta, coeffs.z); coeffs *= attenuation; #endif // UNROLLED_LOOP_INDEX < MULTI_SCATTERING_OCTAVES } #pragma unroll_loop_end return scattering; } // TODO: Construct spherical harmonics of degree 2 using 2 sample points // positioned near the horizon occlusion points on the sun direction plane. vec3 getGroundSunSkyIrradiance( const vec3 position, const vec3 surfaceNormal, const float height, out vec3 skyIrradiance ) { #ifdef ACCURATE_SUN_SKY_LIGHT return GetSunAndSkyIrradiance( (position - surfaceNormal * height) * METER_TO_LENGTH_UNIT, surfaceNormal, sunDirection, skyIrradiance ); #else // ACCURATE_SUN_SKY_LIGHT skyIrradiance = vGroundIrradiance.sky; return vGroundIrradiance.sun; #endif // ACCURATE_SUN_SKY_LIGHT } vec3 getCloudsSunSkyIrradiance(const vec3 position, const float height, out vec3 skyIrradiance) { #ifdef ACCURATE_SUN_SKY_LIGHT return GetSunAndSkyScalarIrradiance(position * METER_TO_LENGTH_UNIT, sunDirection, skyIrradiance); #else // ACCURATE_SUN_SKY_LIGHT float alpha = remapClamped(height, minHeight, maxHeight); skyIrradiance = mix(vCloudsIrradiance.minSky, vCloudsIrradiance.maxSky, alpha); return mix(vCloudsIrradiance.minSun, vCloudsIrradiance.maxSun, alpha); #endif // ACCURATE_SUN_SKY_LIGHT } #ifdef GROUND_BOUNCE vec3 approximateRadianceFromGround( const vec3 position, const vec3 surfaceNormal, const float height, const float mipLevel, const float jitter ) { float opticalDepthToGround = marchOpticalDepth( position, -surfaceNormal, maxIterationCountToGround, mipLevel, jitter ); vec3 skyIrradiance; vec3 sunIrradiance = getGroundSunSkyIrradiance(position, surfaceNormal, height, skyIrradiance); const float groundAlbedo = 0.3; vec3 groundIrradiance = skyIrradiance + (1.0 - coverage) * sunIrradiance; vec3 bouncedRadiance = groundAlbedo * RECIPROCAL_PI * groundIrradiance; return bouncedRadiance * exp(-opticalDepthToGround); } #endif // GROUND_BOUNCE vec4 marchClouds( const vec3 rayOrigin, const vec3 rayDirection, const vec2 rayNearFar, const float cosTheta, const float jitter, const float rayStartTexelsPerPixel, out float frontDepth, out ivec3 sampleCount ) { vec3 radianceIntegral = vec3(0.0); float transmittanceIntegral = 1.0; float weightedDistanceSum = 0.0; float transmittanceSum = 0.0; float maxRayDistance = rayNearFar.y - rayNearFar.x; float stepSize = minStepSize + (perspectiveStepScale - 1.0) * rayNearFar.x; // I don't understand why spatial aliasing remains unless doubling the jitter. float rayDistance = stepSize * jitter * 2.0; for (int i = 0; i < maxIterationCount; ++i) { if (rayDistance > maxRayDistance) { break; // Termination } vec3 position = rayDistance * rayDirection + rayOrigin; float height = length(position) - bottomRadius; float mipLevel = log2(max(1.0, rayStartTexelsPerPixel + rayDistance * 1e-5)); #if !defined(DEBUG_MARCH_INTERVALS) if (insideLayerIntervals(height)) { stepSize *= perspectiveStepScale; rayDistance += mix(stepSize, maxStepSize, min(1.0, mipLevel)); continue; } #endif // !defined(DEBUG_MARCH_INTERVALS) // Sample rough weather. vec2 uv = getGlobeUv(position); WeatherSample weather = sampleWeather(uv, height, mipLevel); #ifdef DEBUG_SHOW_SAMPLE_COUNT ++sampleCount.x; #endif // DEBUG_SHOW_SAMPLE_COUNT if (!any(greaterThan(weather.density, vec4(minDensity)))) { // Step longer in empty space. // TODO: This produces banding artifacts. // Possible improvement: Binary search refinement stepSize *= perspectiveStepScale; rayDistance += mix(stepSize, maxStepSize, min(1.0, mipLevel)); continue; } // Sample detailed participating media. MediaSample media = sampleMedia(weather, position, uv, mipLevel, jitter, sampleCount); if (media.extinction > minExtinction) { vec3 skyIrradiance; vec3 sunIrradiance = getCloudsSunSkyIrradiance(position, height, skyIrradiance); vec3 surfaceNormal = normalize(position); // March optical depth to the sun for finer details, which BSM lacks. float sunRayDistance = 0.0; float opticalDepth = marchOpticalDepth( position, sunDirection, maxIterationCountToSun, mipLevel, jitter, sunRayDistance ); if (height < shadowTopHeight) { // Obtain the optical depth from BSM at the ray position. opticalDepth += sampleShadowOpticalDepth( position, // Take account of only positions further than the marched ray // distance. sunRayDistance, // Apply PCF only when the sun is close to the horizon. maxShadowFilterRadius * remapClamped(dot(sunDirection, surfaceNormal), 0.1, 0.0), jitter ); } vec3 radiance = sunIrradiance * approximateMultipleScattering(opticalDepth, cosTheta); #ifdef GROUND_BOUNCE // Fudge factor for the irradiance from ground. if (height < shadowTopHeight && mipLevel < 0.5) { vec3 groundRadiance = approximateRadianceFromGround( position, surfaceNormal, height, mipLevel, jitter ); radiance += groundRadiance * RECIPROCAL_PI4 * groundBounceScale; } #endif // GROUND_BOUNCE // Crude approximation of sky gradient. Better than none in the shadows. float skyGradient = dot(weather.heightFraction * 0.5 + 0.5, media.weight); radiance += skyIrradiance * RECIPROCAL_PI4 * skyGradient * skyLightScale; // Finally multiply by scattering. radiance *= media.scattering; #ifdef POWDER radiance *= 1.0 - powderScale * exp(-media.extinction * powderExponent); #endif // POWDER #ifdef DEBUG_SHOW_CASCADES if (height < shadowTopHeight) { radiance = 1e-3 * getFadedCascadeColor(position, jitter); } #endif // DEBUG_SHOW_CASCADES // Energy-conserving analytical integration of scattered light // See 5.6.3 in https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf float transmittance = exp(-media.extinction * stepSize); float clampedExtinction = max(media.extinction, 1e-7); vec3 scatteringIntegral = (radiance - radiance * transmittance) / clampedExtinction; radianceIntegral += transmittanceIntegral * scatteringIntegral; transmittanceIntegral *= transmittance; // Aerial perspective affecting clouds // See 5.9.1 in https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf weightedDistanceSum += rayDistance * transmittanceIntegral; transmittanceSum += transmittanceIntegral; } if (transmittanceIntegral <= minTransmittance) { break; // Early termination } // Take a shorter step because we've already hit the clouds. stepSize *= perspectiveStepScale; rayDistance += stepSize; } // The final product of 5.9.1 and we'll evaluate this in aerial perspective. frontDepth = transmittanceSum > 0.0 ? weightedDistanceSum / transmittanceSum : -1.0; return vec4(radianceIntegral, remapClamped(transmittanceIntegral, 1.0, minTransmittance)); } #ifdef SHADOW_LENGTH float marchShadowLength( const vec3 rayOrigin, const vec3 rayDirection, const vec2 rayNearFar, const float jitter ) { float shadowLength = 0.0; float maxRayDistance = rayNearFar.y - rayNearFar.x; float stepSize = minShadowLengthStepSize; float rayDistance = stepSize * jitter; const float attenuationFactor = 1.0 - 5e-4; float attenuation = 1.0; // TODO: This march is closed, and sample resolution can be much lower. // Refining the termination by binary search will make it much more efficient. for (int i = 0; i < maxShadowLengthIterationCount; ++i) { if (rayDistance > maxRayDistance) { break; // Termination } vec3 position = rayDistance * rayDirection + rayOrigin; float opticalDepth = sampleShadowOpticalDepth(position, 0.0, 0.0, jitter); shadowLength += (1.0 - exp(-opticalDepth)) * stepSize * attenuation; stepSize *= perspectiveStepScale; rayDistance += stepSize; } return shadowLength; } #endif // SHADOW_LENGTH #ifdef HAZE vec4 approximateHaze( const vec3 rayOrigin, const vec3 rayDirection, const float maxRayDistance, const float cosTheta, const float shadowLength ) { float modulation = remapClamped(coverage, 0.2, 0.4); if (cameraHeight * modulation < 0.0) { return vec4(0.0); } float density = modulation * hazeDensityScale * exp(-cameraHeight * hazeExponent); if (density < 1e-7) { return vec4(0.0); // Prevent artifact in views from space } // Blend two normals by the difference in angle so that normal near the // ground becomes that of the origin, and in the sky that of the horizon. vec3 normalAtOrigin = normalize(rayOrigin); vec3 normalAtHorizon = (rayOrigin - dot(rayOrigin, rayDirection) * rayDirection) / bottomRadius; float alpha = remapClamped(dot(normalAtOrigin, normalAtHorizon), 0.9, 1.0); vec3 normal = mix(normalAtOrigin, normalAtHorizon, alpha); // Analytical optical depth where density exponentially decreases with height. // Based on: https://iquilezles.org/articles/fog/ float angle = max(dot(normal, rayDirection), 1e-5); float exponent = angle * hazeExponent; float linearTerm = density / hazeExponent / angle; // Derive the optical depths separately for with and without shadow length. float expTerm = 1.0 - exp(-maxRayDistance * exponent); float shadowExpTerm = 1.0 - exp(-min(maxRayDistance, shadowLength) * exponent); float opticalDepth = expTerm * linearTerm; float shadowOpticalDepth = max((expTerm - shadowExpTerm) * linearTerm, 0.0); float transmittance = saturate(1.0 - exp(-opticalDepth)); float shadowTransmittance = saturate(1.0 - exp(-shadowOpticalDepth)); vec3 skyIrradiance = vGroundIrradiance.sky; vec3 sunIrradiance = vGroundIrradiance.sun; vec3 inscatter = sunIrradiance * phaseFunction(cosTheta) * shadowTransmittance; inscatter += skyIrradiance * RECIPROCAL_PI4 * skyLightScale * transmittance; inscatter *= hazeScatteringCoefficient / (hazeAbsorptionCoefficient + hazeScatteringCoefficient); return vec4(inscatter, transmittance); } #endif // HAZE void applyAerialPerspective( const vec3 cameraPosition, const vec3 frontPosition, const float shadowLength, inout vec4 color ) { vec3 transmittance; vec3 inscatter = GetSkyRadianceToPoint( cameraPosition * METER_TO_LENGTH_UNIT, frontPosition * METER_TO_LENGTH_UNIT, shadowLength * METER_TO_LENGTH_UNIT, sunDirection, transmittance ); color.rgb = color.rgb * transmittance + inscatter * color.a; } bool rayIntersectsGround(const vec3 cameraPosition, const vec3 rayDirection) { float r = length(cameraPosition); float mu = dot(cameraPosition, rayDirection) / r; return mu < 0.0 && r * r * (mu * mu - 1.0) + bottomRadius * bottomRadius >= 0.0; } struct IntersectionResult { bool ground; vec4 first; vec4 second; }; IntersectionResult getIntersections(const vec3 cameraPosition, const vec3 rayDirection) { IntersectionResult intersections; intersections.ground = rayIntersectsGround(cameraPosition, rayDirection); raySphereIntersections( cameraPosition, rayDirection, bottomRadius + vec4(0.0, minHeight, maxHeight, shadowTopHeight), intersections.first, intersections.second ); return intersections; } vec2 getRayNearFar(const IntersectionResult intersections) { vec2 nearFar; if (cameraHeight < minHeight) { // View below the clouds if (intersections.ground) { nearFar = vec2(-1.0); // No clouds to the ground } else { nearFar = vec2(intersections.second.y, intersections.second.z); nearFar.y = min(nearFar.y, maxRayDistance); } } else if (cameraHeight < maxHeight) { // View inside the total cloud layer if (intersections.ground) { nearFar = vec2(cameraNear, intersections.first.y); } else { nearFar = vec2(cameraNear, intersections.second.z); } } else { // View above the clouds nearFar = vec2(intersections.first.z, intersections.second.z); if (intersections.ground) { // Clamp the ray at the min height. nearFar.y = intersections.first.y; } } return nearFar; } #ifdef SHADOW_LENGTH vec2 getShadowRayNearFar(const IntersectionResult intersections) { vec2 nearFar; if (cameraHeight < shadowTopHeight) { if (intersections.ground) { nearFar = vec2(cameraNear, intersections.first.x); } else { nearFar = vec2(cameraNear, intersections.second.w); } } else { nearFar = vec2(intersections.first.w, intersections.second.w); if (intersections.ground) { // Clamp the ray at the ground. nearFar.y = intersections.first.x; } } nearFar.y = min(nearFar.y, maxShadowLengthRayDistance); return nearFar; } #endif // SHADOW_LENGTH #ifdef HAZE vec2 getHazeRayNearFar(const IntersectionResult intersections) { vec2 nearFar; if (cameraHeight < maxHeight) { if (intersections.ground) { nearFar = vec2(cameraNear, intersections.first.x); } else { nearFar = vec2(cameraNear, intersections.second.z); } } else { nearFar = vec2(cameraNear, intersections.second.z); if (intersections.ground) { // Clamp the ray at the ground. nearFar.y = intersections.first.x; } } return nearFar; } #endif // HAZE float getRayDistanceToScene(const vec3 rayDirection, out float viewZ) { float depth = readDepth(vUv * targetUvScale + temporalJitter); if (depth < 1.0 - 1e-7) { depth = reverseLogDepth(depth, cameraNear, cameraFar); viewZ = getViewZ(depth); return -viewZ / dot(rayDirection, vCameraDirection); } viewZ = 0.0; return 0.0; } void main() { #ifdef DEBUG_SHOW_SHADOW_MAP outputColor = getCascadedShadowMaps(vUv); outputDepthVelocity = vec3(0.0); #ifdef SHADOW_LENGTH outputShadowLength = 0.0; #endif // SHADOW_LENGTH return; #endif // DEBUG_SHOW_SHADOW_MAP vec3 cameraPosition = vCameraPosition + altitudeCorrection; vec3 rayDirection = normalize(vRayDirection); float cosTheta = dot(sunDirection, rayDirection); IntersectionResult intersections = getIntersections(cameraPosition, rayDirection); vec2 rayNearFar = getRayNearFar(intersections); #ifdef SHADOW_LENGTH vec2 shadowRayNearFar = getShadowRayNearFar(intersections); #endif // SHADOW_LENGTH #ifdef HAZE vec2 hazeRayNearFar = getHazeRayNearFar(intersections); #endif // HAZE float sceneViewZ; float rayDistanceToScene = getRayDistanceToScene(rayDirection, sceneViewZ); if (rayDistanceToScene > 0.0) { rayNearFar.y = min(rayNearFar.y, rayDistanceToScene); #ifdef SHADOW_LENGTH shadowRayNearFar.y = min(shadowRayNearFar.y, rayDistanceToScene); #endif // SHADOW_LENGTH #ifdef HAZE hazeRayNearFar.y = min(hazeRayNearFar.y, rayDistanceToScene); #endif // HAZE } bool intersectsGround = any(lessThan(rayNearFar, vec2(0.0))); bool intersectsScene = rayNearFar.y < rayNearFar.x; float stbn = getSTBN(); vec4 color = vec4(0.0); float frontDepth = rayNearFar.y; vec3 depthVelocity = vec3(0.0); float shadowLength = 0.0; bool hitClouds = false; if (!intersectsGround && !intersectsScene) { vec3 rayOrigin = rayNearFar.x * rayDirection + cameraPosition; vec2 globeUv = getGlobeUv(rayOrigin); #ifdef DEBUG_SHOW_UV outputColor = vec4(vec3(checker(globeUv, localWeatherRepeat + localWeatherOffset)), 1.0); outputDepthVelocity = vec3(0.0); #ifdef SHADOW_LENGTH outputShadowLength = 0.0; #endif // SHADOW_LENGTH return; #endif // DEBUG_SHOW_UV float mipLevel = getMipLevel(globeUv * localWeatherRepeat) * mipLevelScale; mipLevel = mix(0.0, mipLevel, min(1.0, 0.2 * cameraHeight / maxHeight)); float marchedFrontDepth; ivec3 sampleCount = ivec3(0); color = marchClouds( rayOrigin, rayDirection, rayNearFar, cosTheta, stbn, pow(2.0, mipLevel), marchedFrontDepth, sampleCount ); #ifdef DEBUG_SHOW_SAMPLE_COUNT outputColor = vec4(vec3(sampleCount) / vec3(500.0, 5.0, 5.0), 1.0); outputDepthVelocity = vec3(0.0); #ifdef SHADOW_LENGTH outputShadowLength = 0.0; #endif // SHADOW_LENGTH return; #endif // DEBUG_SHOW_SAMPLE_COUNT // Front depth will be -1.0 when no samples are accumulated. hitClouds = marchedFrontDepth >= 0.0; if (hitClouds) { frontDepth = rayNearFar.x + marchedFrontDepth; #ifdef SHADOW_LENGTH // Clamp the shadow length ray at the clouds. shadowRayNearFar.y = mix( shadowRayNearFar.y, min(frontDepth, shadowRayNearFar.y), color.a // Interpolate by the alpha for smoother edges. ); // Shadow length must be computed before applying aerial perspective. if (all(greaterThanEqual(shadowRayNearFar, vec2(0.0)))) { shadowLength = marchShadowLength( shadowRayNearFar.x * rayDirection + cameraPosition, rayDirection, shadowRayNearFar, stbn ); } #endif // SHADOW_LENGTH #ifdef HAZE // Clamp the haze ray at the clouds. hazeRayNearFar.y = mix( hazeRayNearFar.y, min(frontDepth, hazeRayNearFar.y), color.a // Interpolate by the alpha for smoother edges. ); #endif // HAZE // Apply aerial perspective. vec3 frontPosition = cameraPosition + frontDepth * rayDirection; applyAerialPerspective(cameraPosition, frontPosition, shadowLength, color); // Velocity for temporal resolution. vec3 frontPositionWorld = ecefToWorld(frontPosition); vec4 prevClip = reprojectionMatrix * vec4(frontPositionWorld, 1.0); prevClip /= prevClip.w; vec2 prevUv = prevClip.xy * 0.5 + 0.5; vec2 velocity = vUv - prevUv; depthVelocity = vec3(frontDepth, velocity); } } if (!hitClouds) { #ifdef SHADOW_LENGTH if (all(greaterThanEqual(shadowRayNearFar, vec2(0.0)))) { shadowLength = marchShadowLength( shadowRayNearFar.x * rayDirection + cameraPosition, rayDirection, shadowRayNearFar, stbn ); } #endif // SHADOW_LENGTH // Velocity for temporal resolution. Here reproject in the view space for // greatly reducing the precision errors. frontDepth = sceneViewZ < 0.0 ? -sceneViewZ : cameraFar; vec3 frontView = vViewPosition * frontDepth; vec4 prevClip = viewReprojectionMatrix * vec4(frontView, 1.0); prevClip /= prevClip.w; vec2 prevUv = prevClip.xy * 0.5 + 0.5; vec2 velocity = vUv - prevUv; depthVelocity = vec3(frontDepth, velocity); } #ifdef DEBUG_SHOW_FRONT_DEPTH outputColor = vec4(turbo(frontDepth / maxRayDistance), 1.0); outputDepthVelocity = vec3(0.0); #ifdef SHADOW_LENGTH outputShadowLength = 0.0; #endif // SHADOW_LENGTH return; #endif // DEBUG_SHOW_FRONT_DEPTH #ifdef HAZE vec4 haze = approximateHaze( cameraNear * rayDirection + cameraPosition, rayDirection, hazeRayNearFar.y - hazeRayNearFar.x, cosTheta, shadowLength ); color.rgb = mix(color.rgb, haze.rgb, haze.a); color.a = color.a * (1.0 - haze.a) + haze.a; #endif // HAZE outputColor = color; outputDepthVelocity = depthVelocity; #ifdef SHADOW_LENGTH outputShadowLength = shadowLength * METER_TO_LENGTH_UNIT; #endif // SHADOW_LENGTH } `,re=`float getSTBN() { ivec3 size = textureSize(stbnTexture, 0); vec3 scale = 1.0 / vec3(size); return texture(stbnTexture, vec3(gl_FragCoord.xy, float(frame % size.z)) * scale).r; } // Straightforward spherical mapping vec2 getSphericalUv(const vec3 position) { vec2 st = normalize(position.yx); float phi = atan(st.x, st.y); float theta = asin(normalize(position).z); return vec2(phi * RECIPROCAL_PI2 + 0.5, theta * RECIPROCAL_PI + 0.5); } vec2 getCubeSphereUv(const vec3 position) { // Cube-sphere relaxation by: http://mathproofs.blogspot.com/2005/07/mapping-cube-to-sphere.html // TODO: Tile and fix seams. // Possible improvements: // https://iquilezles.org/articles/texturerepetition/ // https://gamedev.stackexchange.com/questions/184388/fragment-shader-map-dot-texture-repeatedly-over-the-sphere // https://github.com/mmikk/hextile-demo vec3 n = normalize(position); vec3 f = abs(n); vec3 c = n / max(f.x, max(f.y, f.z)); vec2 m; if (all(greaterThan(f.yy, f.xz))) { m = c.y > 0.0 ? vec2(-n.x, n.z) : n.xz; } else if (all(greaterThan(f.xx, f.yz))) { 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(-24.0 * m2.x + q2)) * (m.x > 0.0 ? 1.0 : -1.0); uv.y = sqrt(6.0 / (3.0 - uv.x * uv.x)) * m.y; return uv * 0.5 + 0.5; } vec2 getGlobeUv(const vec3 position) { return getCubeSphereUv(position); } float getMipLevel(const vec2 uv) { const float mipLevelScale = 0.1; vec2 coord = uv * resolution; vec2 ddx = dFdx(coord); vec2 ddy = dFdy(coord); float deltaMaxSqr = max(dot(ddx, ddx), dot(ddy, ddy)) * mipLevelScale; return max(0.0, 0.5 * log2(max(1.0, deltaMaxSqr))); } bool insideLayerIntervals(const float height) { bvec3 gt = greaterThan(vec3(height), minIntervalHeights); bvec3 lt = lessThan(vec3(height), maxIntervalHeights); return any(bvec3(gt.x && lt.x, gt.y && lt.y, gt.z && lt.z)); } struct WeatherSample { vec4 heightFraction; // Normalized height of each layer vec4 density; }; vec4 shapeAlteringFunction(const vec4 heightFraction, const vec4 bias) { // Apply a semi-circle transform to round the clouds towards the top. vec4 biased = pow(heightFraction, bias); vec4 x = clamp(biased * 2.0 - 1.0, -1.0, 1.0); return 1.0 - x * x; } WeatherSample sampleWeather(const vec2 uv, const float height, const float mipLevel) { WeatherSample weather; weather.heightFraction = remapClamped(vec4(height), minLayerHeights, maxLayerHeights); vec4 localWeather = pow( textureLod( localWeatherTexture, uv * localWeatherRepeat + localWeatherOffset, mipLevel ).LOCAL_WEATHER_CHANNELS, weatherExponents ); #ifdef SHADOW localWeather *= shadowLayerMask; #endif // SHADOW vec4 heightScale = shapeAlteringFunction(weather.heightFraction, shapeAlteringBiases); // Modulation to control weather by coverage parameter. // Reference: https://github.com/Prograda/Skybolt/blob/master/Assets/Core/Shaders/Clouds.h#L63 vec4 factor = 1.0 - coverage * heightScale; weather.density = remapClamped( mix(localWeather, vec4(1.0), coverageFilterWidths), factor, factor + coverageFilterWidths ); return weather; } vec4 getLayerDensity(const vec4 heightFraction) { // prettier-ignore return densityProfile.expTerms * exp(densityProfile.exponents * heightFraction) + densityProfile.linearTerms * heightFraction + densityProfile.constantTerms; } struct MediaSample { float density; vec4 weight; float scattering; float extinction; }; MediaSample sampleMedia( const WeatherSample weather, const vec3 position, const vec2 uv, const float mipLevel, const float jitter, out ivec3 sampleCount ) { vec4 density = weather.density; // TODO: Define in physical length. vec3 surfaceNormal = normalize(position); float localWeatherSpeed = length(localWeatherOffset); vec3 evolution = -surfaceNormal * localWeatherSpeed * 2e4; vec3 turbulence = vec3(0.0); #ifdef TURBULENCE vec2 turbulenceUv = uv * localWeatherRepeat * turbulenceRepeat; turbulence = turbulenceDisplacement * (texture(turbulenceTexture, turbulenceUv).rgb * 2.0 - 1.0) * dot(density, remapClamped(weather.heightFraction, vec4(0.3), vec4(0.0))); #endif // TURBULENCE vec3 shapePosition = (position + evolution + turbulence) * shapeRepeat + shapeOffset; float shape = texture(shapeTexture, shapePosition).r; density = remapClamped(density, vec4(1.0 - shape) * shapeAmounts, vec4(1.0)); #ifdef DEBUG_SHOW_SAMPLE_COUNT ++sampleCount.y; #endif // DEBUG_SHOW_SAMPLE_COUNT #ifdef SHAPE_DETAIL if (mipLevel * 0.5 + (jitter - 0.5) * 0.5 < 0.5) { vec3 detailPosition = (position + turbulence) * shapeDetailRepeat + shapeDetailOffset; float detail = texture(shapeDetailTexture, detailPosition).r; // Fluffy at the top and whippy at the bottom. vec4 modifier = mix( vec4(pow(detail, 6.0)), vec4(1.0 - detail), remapClamped(weather.heightFraction, vec4(0.2), vec4(0.4)) ); modifier = mix(vec4(0.0), modifier, shapeDetailAmounts); density = remapClamped(density * 2.0, vec4(modifier * 0.5), vec4(1.0)); #ifdef DEBUG_SHOW_SAMPLE_COUNT ++sampleCount.z; #endif // DEBUG_SHOW_SAMPLE_COUNT } #endif // SHAPE_DETAIL // Apply the density profiles. density = saturate(density * densityScales * getLayerDensity(weather.heightFraction)); MediaSample media; float densitySum = density.x + density.y + density.z + density.w; media.weight = density / densitySum; media.scattering = densitySum * scatteringCoefficient; media.extinction = densitySum * absorptionCoefficient + media.scattering; return media; } MediaSample sampleMedia( const WeatherSample weather, const vec3 position, const vec2 uv, const float mipLevel, const float jitter ) { ivec3 sampleCount; return sampleMedia(weather, position, uv, mipLevel, jitter, sampleCount); } `,Oe=`precision highp float; precision highp sampler3D; #include "atmosphere/bruneton/definitions" 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 sampler2D irradiance_texture; uniform sampler3D single_mie_scattering_texture; uniform sampler3D higher_order_scattering_texture; #include "atmosphere/bruneton/common" #include "atmosphere/bruneton/runtime" #include "types" uniform mat4 inverseProjectionMatrix; uniform mat4 inverseViewMatrix; uniform vec3 cameraPosition; uniform mat4 worldToECEFMatrix; uniform vec3 altitudeCorrection; // Atmosphere uniform float bottomRadius; uniform vec3 sunDirection; // Cloud layers uniform float minHeight; uniform float maxHeight; layout(location = 0) in vec3 position; out vec2 vUv; out vec3 vCameraPosition; out vec3 vCameraDirection; // Direction to the center of screen out vec3 vRayDirection; // Direction to the texel out vec3 vViewPosition; out GroundIrradiance vGroundIrradiance; out CloudsIrradiance vCloudsIrradiance; void sampleSunSkyIrradiance(const vec3 positionECEF) { vGroundIrradiance.sun = GetSunAndSkyScalarIrradiance( positionECEF * METER_TO_LENGTH_UNIT, sunDirection, vGroundIrradiance.sky ); vec3 surfaceNormal = normalize(positionECEF); vec2 radii = (bottomRadius + vec2(minHeight, maxHeight)) * METER_TO_LENGTH_UNIT; vCloudsIrradiance.minSun = GetSunAndSkyScalarIrradiance( surfaceNormal * radii.x, sunDirection, vCloudsIrradiance.minSky ); vCloudsIrradiance.maxSun = GetSunAndSkyScalarIrradiance( surfaceNormal * radii.y, sunDirection, vCloudsIrradiance.maxSky ); } void main() { vUv = position.xy * 0.5 + 0.5; vec3 viewPosition = (inverseProjectionMatrix * vec4(position, 1.0)).xyz; vec3 worldDirection = (inverseViewMatrix * vec4(viewPosition.xyz, 0.0)).xyz; vec3 cameraDirection = normalize((inverseViewMatrix * vec4(0.0, 0.0, -1.0, 0.0)).xyz); vCameraPosition = (worldToECEFMatrix * vec4(cameraPosition, 1.0)).xyz; vCameraDirection = (worldToECEFMatrix * vec4(cameraDirection, 0.0)).xyz