@takram/three-clouds
Version:
A Three.js and R3F implementation of geospatial volumetric clouds
1,275 lines (1,122 loc) • 114 kB
JavaScript
"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