From afe89c4c7962399798e3d201c8ad88b0adcae12d Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 11 Dec 2022 13:41:30 +0200 Subject: [PATCH] atmosphere --- src/atmosphere.ts | 67 ++++++++++++++++++++++++++ src/globe.ts | 61 +++-------------------- src/main.ts | 2 +- src/shaders/atmosphere.frag | 28 +++++++++++ src/shaders/atmosphere.vert | 96 +++++++++++++++++++++++++++++++++++++ src/shaders/earth.frag | 7 ++- src/shaders/earth.vert | 83 +++++++++++++++++++++++++++++++- 7 files changed, 285 insertions(+), 59 deletions(-) create mode 100644 src/atmosphere.ts create mode 100644 src/shaders/atmosphere.frag create mode 100644 src/shaders/atmosphere.vert diff --git a/src/atmosphere.ts b/src/atmosphere.ts new file mode 100644 index 0000000..14fee5b --- /dev/null +++ b/src/atmosphere.ts @@ -0,0 +1,67 @@ +import { + BackSide, + DirectionalLight, + Mesh, + Object3D, + ShaderMaterial, + SphereGeometry, +} from 'three'; + +import { plainText as vertexShader } from './shaders/atmosphere.vert'; +import { plainText as fragmentShader } from './shaders/atmosphere.frag'; + +export class Atmosphere extends Object3D { + public Rayleigh = 0.0025; + public Mie = 0.0005; + public Exposure = 15.0; + public Scale = 1; + public ScaleDepth = 0.25; + public G = -0.95; + public Wavelength = [0.65, 0.57, 0.475]; + + public geom?: SphereGeometry; + public mesh?: Mesh; + public shader = new ShaderMaterial({ + vertexShader, + fragmentShader, + side: BackSide, + transparent: true, + }); + + constructor(public innerRadius: number, public outerRadius: number) { + super(); + this.Scale = 1 / (this.outerRadius - this.innerRadius); + this.setUniforms(this.shader); + this.initialize(); + } + + setUniforms(shader: ShaderMaterial) { + shader.uniforms.invWavelength = { + value: [ + 1 / Math.pow(this.Wavelength[0], 4), + 1 / Math.pow(this.Wavelength[1], 4), + 1 / Math.pow(this.Wavelength[2], 4), + ], + }; + shader.uniforms.outerRadius = { value: this.outerRadius }; + shader.uniforms.innerRadius = { value: this.innerRadius }; + shader.uniforms.Kr = { value: this.Rayleigh }; + shader.uniforms.Km = { value: this.Mie }; + shader.uniforms.ESun = { value: this.Exposure }; + shader.uniforms.scale = { value: this.Scale }; + shader.uniforms.scaleDepth = { value: this.ScaleDepth }; + shader.uniforms.g = { value: this.G }; + } + + setLight(light: DirectionalLight) { + this.shader.uniforms.lightDirection = { + value: light.position.clone().normalize(), + }; + } + + initialize() { + this.geom = new SphereGeometry(this.outerRadius, 128, 128); + this.mesh = new Mesh(this.geom, this.shader); + this.add(this.mesh); + } +} diff --git a/src/globe.ts b/src/globe.ts index a4227fb..c8be1bd 100644 --- a/src/globe.ts +++ b/src/globe.ts @@ -2,20 +2,20 @@ import { SphereGeometry, Texture, Mesh, - MeshPhongMaterial, Object3D, TextureLoader, Color, DirectionalLight, - Shader, ShaderMaterial, } from 'three'; import { plainText as vertexShader } from './shaders/earth.vert'; import { plainText as fragmentShader } from './shaders/earth.frag'; +import { Atmosphere } from './atmosphere'; export class Globe extends Object3D { - private sphere = new SphereGeometry(1, 32, 32); + private sphere = new SphereGeometry(1, 128, 128); + private atmosphere = new Atmosphere(1, 1.05); private earthMaterial?: ShaderMaterial; public earthMesh?: Mesh; private loader = new TextureLoader(); @@ -53,60 +53,11 @@ export class Globe extends Object3D { }, }); - // this.earthMaterial = new MeshPhongMaterial(); - // this.earthMaterial.onBeforeCompile = (shader) => { - // let fragment = shader.fragmentShader; - // let vertex = shader.vertexShader; - // vertex = injectBefore( - // vertex, - // 'void main() {', - // `uniform vec3 lightDirection;\nvarying vec3 surfaceToLight;\n` - // ); - // vertex = injectBefore( - // vertex, - // '#include ', - // 'surfaceToLight = mat3(modelViewMatrix) * lightDirection;\n' - // ); - // fragment = injectBefore( - // fragment, - // 'void main() {', - // `varying vec3 surfaceToLight;\nuniform sampler2D nightmap;\n` - // ); - // fragment = replaceWith(fragment, '#include ', ''); - // fragment = injectBefore( - // fragment, - // '#include ', - // ` - // vec4 sampledDiffuseColor = texture2D(map, vUv); - // vec4 sampledDiffuseColorNight = texture2D(nightmap, vUv); - // diffuseColor *= sampledDiffuseColor; - // ` - // ); - // fragment = injectBefore( - // fragment, - // '#include ', - // `float cosineAngleSunToNormal = dot(geometryNormal, surfaceToLight); - // cosineAngleSunToNormal = clamp( cosineAngleSunToNormal * 1.0, -1.0, 1.0); - // float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5; - // outgoingLight = mix(sampledDiffuseColorNight.xyz, outgoingLight, mixAmount); - // ` - // ); - // console.log(fragment); - // shader.uniforms.nightmap = { value: textures[1] }; - // shader.uniforms.lightDirection = { - // value: light.position.clone(), - // }; - // shader.fragmentShader = fragment; - // shader.vertexShader = vertex; - // }; - - // this.earthMaterial.map = textures[0]; - // this.earthMaterial.normalMap = textures[3]; - // this.earthMaterial.specularMap = textures[4]; - // this.earthMaterial.specular = new Color('grey'); - + this.atmosphere.setUniforms(this.earthMaterial); + this.atmosphere.setLight(light); this.earthMesh = new Mesh(this.sphere, this.earthMaterial); this.add(this.earthMesh); + this.add(this.atmosphere); } public tick() {} diff --git a/src/main.ts b/src/main.ts index 4880906..ac92996 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,7 +22,7 @@ const control = new OrbitControls(main.camera, main.canvas); function tick() { requestAnimationFrame(tick); control.update(); - globe.rotateY(Math.PI * 0.0005); + // globe.rotateY(Math.PI * 0.0005); main.render(); globe.tick(); } diff --git a/src/shaders/atmosphere.frag b/src/shaders/atmosphere.frag new file mode 100644 index 0000000..76d4a47 --- /dev/null +++ b/src/shaders/atmosphere.frag @@ -0,0 +1,28 @@ +#include + +uniform vec3 lightDirection; +uniform float g; + +varying vec3 v3Direction; +varying vec3 c0; +varying vec3 c1; + +// Calculates the Mie phase function +float getMiePhase(float fCos, float fCos2, float g, float g2){ + return 1.5 * ((1.0 - g2) / (2.0 + g2)) * (1.0 + fCos2) / pow(1.0 + g2 - 2.0 * g * fCos, 1.5); +} + +// Calculates the Rayleigh phase function +float getRayleighPhase(float fCos2) { + // return 0.75 + 0.75 * fCos2; + return 0.75 * (2.0 + 0.5 * fCos2); +} + +void main (void) { + float fCos = dot(lightDirection, v3Direction) / length(v3Direction); + float fCos2 = fCos * fCos; + vec3 color = getRayleighPhase(fCos2) * c0 + + getMiePhase(fCos, fCos2, g, pow(g, 2.0)) * c1; + gl_FragColor = vec4(color, 1.0); + gl_FragColor.a = gl_FragColor.b; +} diff --git a/src/shaders/atmosphere.vert b/src/shaders/atmosphere.vert new file mode 100644 index 0000000..4e70bd3 --- /dev/null +++ b/src/shaders/atmosphere.vert @@ -0,0 +1,96 @@ +#include +uniform vec3 lightDirection; // The direction vector to the light source +uniform vec3 invWavelength; // 1 / pow(wavelength, 4) for the red, green, and blue channels +uniform float outerRadius; // The outer (atmosphere) radius +uniform float innerRadius; // The inner (planetary) radius +uniform float ESun; // Sun exposure +uniform float Km; // Mie coefficient +uniform float Kr; // Rayleigh coefficient +uniform float scale; // 1 / (outerRadius - innerRadius) +uniform float scaleDepth; // The scale depth (i.e. the altitude at which the atmosphere's average density is found) + +const int nSamples = 2; +const float fSamples = 1.0; + +varying vec3 v3Direction; +varying vec3 c0; +varying vec3 c1; + +float dscale(float fCos) { + float x = 1.0 - fCos; + return scaleDepth * exp(-0.00287 + x * (0.459 + x * (3.83 + x * (-6.80 + x * 5.25)))); +} + +float calculateNearScatter(vec3 v3CameraPosition, vec3 v3Ray, float fCameraHeight, float fOuterRadius) { + float B = 2.0 * dot(v3CameraPosition, v3Ray); + float C = pow(fCameraHeight, 2.0) - pow(fOuterRadius, 2.0); + float fDet = max(0.0, B * B - 4.0 * C); + return 0.5 * (-B - sqrt(fDet)); +} + +void main(void) { + vec3 mvPosition = (modelMatrix * vec4(position, 1.0)).xyz; + + // Initialize variables + float cameraHeight = length(vec3(0.0, 0.0, 0.0) - cameraPosition); + float KmESun = Km * ESun; + float KrESun = Kr * ESun; + float Kr4PI = Kr * 4.0 * PI; + float Km4PI = Km * 4.0 * PI; + float scaleOverScaleDepth = scale / scaleDepth; + + // Get the ray from the camera to the vertex and its length (which is the far point of the ray passing through the atmosphere) + vec3 v3Ray = mvPosition - cameraPosition; + float fFar = length(v3Ray); + v3Ray /= fFar; + + vec3 v3Start; + float fStartAngle; + float fStartDepth; + float fStartOffset; + + if (cameraHeight > outerRadius) { + // Sky from space + // Calculate the closest intersection of the ray with the outer atmosphere (which is the near point of the ray passing through the atmosphere) + float fNear = calculateNearScatter(cameraPosition, v3Ray, cameraHeight, outerRadius); + + // Calculate the ray's starting position, then calculate its scattering offset + v3Start = cameraPosition + v3Ray * fNear; + fFar -= fNear; + fStartAngle = dot(v3Ray, v3Start) / outerRadius; + fStartDepth = exp(-1.0 / scaleDepth); + fStartOffset = fStartDepth * dscale(fStartAngle); + } else { + // Sky from within the atmosphere + v3Start = cameraPosition; + fStartDepth = exp(scaleOverScaleDepth * (innerRadius - cameraHeight)); + fStartAngle = dot(v3Ray, v3Start) / length(v3Start); + fStartOffset = fStartDepth * dscale(fStartAngle); + } + + // Initialize the scattering loop variables + float fSampleLength = fFar / fSamples; + float scaledLength = fSampleLength * scale; + vec3 v3SampleRay = v3Ray * fSampleLength; + vec3 v3SamplePoint = v3Start + v3SampleRay * 0.5; + + // Now loop through the sample rays + vec3 v3FrontColor = vec3(0.0, 0.0, 0.0); + for(int i=0; i #include +varying vec3 c0; +varying vec3 c1; + void main() { vec4 sampledDiffuseColor = texture2D(texture0, vUv); vec4 sampledDiffuseColorNight = texture2D(texture1, vUv); @@ -37,10 +40,10 @@ void main() { vec4 texelSpecular = texture2D(specularMap, vUv); float specularStrength = texelSpecular.r; - vec3 outgoingLight = mix(sampledDiffuseColorNight.xyz, sampledDiffuseColor.xyz, mixAmount); + vec3 outgoingLight = mix(sampledDiffuseColorNight.xyz, c0 + sampledDiffuseColor.xyz * c1, mixAmount); float specularAmount = max(0.0, specularAngle); specularAmount = pow(specularAmount, 32.0 * shininess) * specularStrength; - gl_FragColor = vec4( outgoingLight + specular * specularAmount, 1.0 ); + gl_FragColor = vec4( (outgoingLight + specular * specularAmount), 1.0 ); } diff --git a/src/shaders/earth.vert b/src/shaders/earth.vert index da8af72..384cf90 100644 --- a/src/shaders/earth.vert +++ b/src/shaders/earth.vert @@ -6,15 +6,96 @@ varying vec3 vEyeNormal; #include #include uniform vec3 light; + +uniform vec3 invWavelength; // 1 / pow(wavelength, 4) for the red, green, and blue channels +uniform float outerRadius; // The outer (atmosphere) radius +uniform float innerRadius; // The inner (planetary) radius +uniform float ESun; // Sun exposure +uniform float Km; // Mie coefficient +uniform float Kr; // Rayleigh coefficient +uniform float scale; // 1 / (outerRadius - innerRadius) +uniform float scaleDepth; // The scale depth (i.e. the altitude at which the atmosphere's average density is found) + +const int nSamples = 2; +const float fSamples = 1.0; + +varying vec3 v3Direction; +varying vec3 c0; +varying vec3 c1; + +float dscale(float fCos) { + float x = 1.0 - fCos; + return scaleDepth * exp(-0.00287 + x * (0.459 + x * (3.83 + x * (-6.80 + x * 5.25)))); +} + +float calculateNearScatter(vec3 v3CameraPosition, vec3 v3Ray, float fCameraHeight, float fOuterRadius) { + float B = 2.0 * dot(v3CameraPosition, v3Ray); + float C = pow(fCameraHeight, 2.0) - pow(fOuterRadius, 2.0); + float fDet = max(0.0, B * B - 4.0 * C); + return 0.5 * (-B - sqrt(fDet)); +} + void main() { #include #include #include #include #include + + vLightDirection = (viewMatrix * vec4(light, 0.)).xyz; + + vec3 modelPosition = (modelMatrix * vec4(position, 1.0)).xyz; + float cameraHeight = length(vec3(0.0, 0.0, 0.0) - cameraPosition); + float KmESun = Km * ESun; + float KrESun = Kr * ESun; + float Kr4PI = Kr * 4.0 * PI; + float Km4PI = Km * 4.0 * PI; + float scaleOverScaleDepth = scale / scaleDepth; + + // Get the ray from the camera to the vertex and its length (which is the far point of the ray passing through the atmosphere) + vec3 v3Ray = modelPosition - cameraPosition; + float fFar = length(v3Ray); + v3Ray /= fFar; + + // Calculate the closest intersection of the ray with the outer atmosphere (which is the near point of the ray passing through the atmosphere) + float fNear = calculateNearScatter(cameraPosition, v3Ray, cameraHeight, outerRadius); + + // Calculate the ray's starting position, then calculate its scattering offset + vec3 v3Start = cameraPosition + v3Ray * fNear; + fFar -= fNear; + float fDepth = exp((innerRadius - outerRadius) / scaleDepth); + float fCameraAngle = dot(-v3Ray, modelPosition) / length(modelPosition); + float fLightAngle = dot(normalize(light), modelPosition) / length(modelPosition); + + float fCameraScale = dscale(fCameraAngle); + float fLightScale = dscale(fLightAngle); + float fCameraOffset = fDepth*fCameraScale; + float fTemp = (fLightScale + fCameraScale); + + // Initialize the scattering loop variables + float fSampleLength = fFar / fSamples; + float fScaledLength = fSampleLength * scale; + vec3 v3SampleRay = v3Ray * fSampleLength; + vec3 v3SamplePoint = v3Start + v3SampleRay * 0.5; + + // Now loop through the sample rays + vec3 v3FrontColor = vec3(0.0, 0.0, 0.0); + vec3 v3Attenuate; + for(int i=0; i