From ac44d096ccb51718f0d1a4b13117e77a0d57a1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=CC=81te=CC=81=20Homolya?= Date: Tue, 13 Aug 2024 23:58:33 -0700 Subject: [PATCH] feat(atmosphere): Atmosphere package initial commit --- .../component/AtmosphericComponent.ts | 227 +++++++++ packages/atmosphere/index.ts | 3 + packages/atmosphere/package.json | 26 ++ packages/atmosphere/renderer/SkyRenderer.ts | 104 +++++ .../atmosphere/shader/AtmosphereEarth.wgsl | 111 +++++ .../atmosphere/shader/AtmosphereUniforms.wgsl | 16 + .../AtmosphericScatteringIntegration.wgsl | 429 ++++++++++++++++++ .../shader/AtmosphericScatteringSky.wgsl | 258 +++++++++++ .../shader/AtmosphericScatteringSky_shader.ts | 24 + packages/atmosphere/shader/CloudNoise.wgsl | 45 ++ .../atmosphere/shader/NewMultiScattCS.wgsl | 127 ++++++ .../shader/RenderSkyRayMarching.wgsl | 75 +++ .../shader/RenderTransmittanceLutPS.wgsl | 76 ++++ packages/atmosphere/shader/SkyViewLutPS.wgsl | 121 +++++ .../textures/AtmosphericScatteringSky.ts | 252 ++++++++++ packages/atmosphere/tsconfig.json | 30 ++ packages/atmosphere/vite.config.js | 26 ++ samples/ext/Sample_AtmosphericSky.ts | 130 ++++++ 18 files changed, 2080 insertions(+) create mode 100644 packages/atmosphere/component/AtmosphericComponent.ts create mode 100644 packages/atmosphere/index.ts create mode 100644 packages/atmosphere/package.json create mode 100644 packages/atmosphere/renderer/SkyRenderer.ts create mode 100644 packages/atmosphere/shader/AtmosphereEarth.wgsl create mode 100644 packages/atmosphere/shader/AtmosphereUniforms.wgsl create mode 100644 packages/atmosphere/shader/AtmosphericScatteringIntegration.wgsl create mode 100644 packages/atmosphere/shader/AtmosphericScatteringSky.wgsl create mode 100644 packages/atmosphere/shader/AtmosphericScatteringSky_shader.ts create mode 100644 packages/atmosphere/shader/CloudNoise.wgsl create mode 100644 packages/atmosphere/shader/NewMultiScattCS.wgsl create mode 100644 packages/atmosphere/shader/RenderSkyRayMarching.wgsl create mode 100644 packages/atmosphere/shader/RenderTransmittanceLutPS.wgsl create mode 100644 packages/atmosphere/shader/SkyViewLutPS.wgsl create mode 100644 packages/atmosphere/textures/AtmosphericScatteringSky.ts create mode 100644 packages/atmosphere/tsconfig.json create mode 100644 packages/atmosphere/vite.config.js create mode 100644 samples/ext/Sample_AtmosphericSky.ts diff --git a/packages/atmosphere/component/AtmosphericComponent.ts b/packages/atmosphere/component/AtmosphericComponent.ts new file mode 100644 index 00000000..73ce98fb --- /dev/null +++ b/packages/atmosphere/component/AtmosphericComponent.ts @@ -0,0 +1,227 @@ +import { AtmosphericScatteringSky, AtmosphericScatteringSkySetting } from "../textures/AtmosphericScatteringSky"; +import { SkyRenderer } from "../renderer/SkyRenderer"; +import { ShaderLib, Transform } from "@orillusion/core"; +import { AtmosphericScatteringSky_shader } from "../shader/AtmosphericScatteringSky_shader"; + +class HistoryData { + public rotateX: number; + public rotateY: number; + + public sunX: number; + public sunY: number; + + constructor() { + this.reset(); + } + + public reset(): this { + this.rotateX = this.rotateY = this.sunX = this.sunY = Number.MAX_VALUE; + return this; + } + + public isRotateChange(rx: number, ry: number): boolean { + return Math.abs(this.rotateX - rx) >= 0.001 || Math.abs(this.rotateY - ry) >= 0.001; + } + + public isSkyChange(x: number, y: number): boolean { + return Math.abs(this.sunX - x) >= 0.001 || Math.abs(this.sunY - y) >= 0.001; + } + + public save(x: number, y: number, rx: number, ry: number): this { + this.sunX = x; + this.sunY = y; + this.rotateX = rx; + this.rotateY = ry; + + return this; + } +} + +/** + * + * Atmospheric Sky Box Component + * @group Components + */ +export class AtmosphericComponent extends SkyRenderer { + + private _atmosphericScatteringSky: AtmosphericScatteringSky; + private _onChange: boolean = true; + private _relatedTransform: Transform; + private _historyData: HistoryData; + public get sunX() { + return this._atmosphericScatteringSky.setting.sunX; + } + + public set sunX(value) { + if (this._atmosphericScatteringSky.setting.sunX != value) { + this._atmosphericScatteringSky.setting.sunX = value; + if (this._relatedTransform) { + this._relatedTransform.rotationY = value * 360 - 90; + } + this._onChange = true; + } + } + + public get sunY() { + return this._atmosphericScatteringSky.setting.sunY; + } + + public set sunY(value) { + if (this._atmosphericScatteringSky.setting.sunY != value) { + this._atmosphericScatteringSky.setting.sunY = value; + if (this._relatedTransform) { + this._relatedTransform.rotationX = (value - 0.5) * 180; + } + this._onChange = true; + } + } + + public get eyePos() { + return this._atmosphericScatteringSky.setting.eyePos; + } + + public set eyePos(value) { + if (this._atmosphericScatteringSky.setting.eyePos != value) { + this._atmosphericScatteringSky.setting.eyePos = value; + this._onChange = true; + } + } + + public get sunRadius() { + return this._atmosphericScatteringSky.setting.sunRadius; + } + + public set sunRadius(value) { + if (this._atmosphericScatteringSky.setting.sunRadius != value) { + this._atmosphericScatteringSky.setting.sunRadius = value; + this._onChange = true; + } + } + + public get sunRadiance() { + return this._atmosphericScatteringSky.setting.sunRadiance; + } + + public set sunRadiance(value) { + if (this._atmosphericScatteringSky.setting.sunRadiance != value) { + this._atmosphericScatteringSky.setting.sunRadiance = value; + this._onChange = true; + } + } + + public get sunBrightness() { + return this._atmosphericScatteringSky.setting.sunBrightness; + } + + public set sunBrightness(value) { + if (this._atmosphericScatteringSky.setting.sunBrightness != value) { + this._atmosphericScatteringSky.setting.sunBrightness = value; + this._onChange = true; + } + } + + public get displaySun() { + return this._atmosphericScatteringSky.setting.displaySun; + } + + public set displaySun(value) { + if (this._atmosphericScatteringSky.setting.displaySun != value) { + this._atmosphericScatteringSky.setting.displaySun = value; + this._onChange = true; + } + } + + public get enableClouds() { + return this._atmosphericScatteringSky.setting.enableClouds; + } + + public set enableClouds(value) { + if (this._atmosphericScatteringSky.setting.enableClouds != value) { + this._atmosphericScatteringSky.setting.enableClouds = value; + this._onChange = true; + } + } + + public get showV1() { + return this._atmosphericScatteringSky.setting.showV1; + } + + public set showV1(value) { + if (this._atmosphericScatteringSky.setting.showV1 != value) { + this._atmosphericScatteringSky.setting.showV1 = value; + this._onChange = true; + } + } + + public get hdrExposure() { + return this._atmosphericScatteringSky.setting.hdrExposure; + } + + public set hdrExposure(value) { + if (this._atmosphericScatteringSky.setting.hdrExposure != value) { + this._atmosphericScatteringSky.setting.hdrExposure = value; + this._onChange = true; + } + } + + + public init(): void { + super.init(); + this._historyData = new HistoryData(); + ShaderLib.register('AtmosphericScatteringIntegration', AtmosphericScatteringSky_shader.integration); + ShaderLib.register('AtmosphereEarth', AtmosphericScatteringSky_shader.earth); + ShaderLib.register('AtmosphereUniforms', AtmosphericScatteringSky_shader.uniforms); + this._atmosphericScatteringSky = new AtmosphericScatteringSky(new AtmosphericScatteringSkySetting()); + + let view3D = this.transform.view3D; + let scene = this.transform.scene3D; + this.map = this._atmosphericScatteringSky; + scene.envMap = this._atmosphericScatteringSky; + scene.envMap.isHDRTexture = true; + this.onUpdate(view3D); + } + + public start(view?: any): void { + let scene = this.transform.scene3D; + this.map = this._atmosphericScatteringSky; + scene.envMap = this._atmosphericScatteringSky; + scene.envMap.isHDRTexture = true; + super.start(); + } + + public get relativeTransform() { + return this._relatedTransform; + } + + public set relativeTransform(value: Transform) { + this._relatedTransform = value; + this._historyData.reset(); + } + + public onUpdate(view?: any) { + if (this._relatedTransform) { + this._relatedTransform.rotationZ = 0; + if (this._historyData.isRotateChange(this._relatedTransform.rotationX, this._relatedTransform.rotationY)) { + this.sunX = (this._relatedTransform.rotationY + 90) / 360// + this.sunY = this._relatedTransform.rotationX / 180 + 0.5; + } else if (this._historyData.isSkyChange(this.sunX, this.sunY)) { + this._relatedTransform.rotationY = this.sunX * 360 - 90; + this._relatedTransform.rotationX = (this.sunY - 0.5) * 180; + } + this._historyData.save(this.sunX, this.sunY, this._relatedTransform.rotationX, this._relatedTransform.rotationY); + } + + if (this._onChange) { + this._onChange = false; + this._atmosphericScatteringSky.apply(view); + } + + } + + public destroy(force?: boolean): void { + super.destroy(force); + this._atmosphericScatteringSky.destroy(); + this._atmosphericScatteringSky = null; + this._onChange = null; + } +} diff --git a/packages/atmosphere/index.ts b/packages/atmosphere/index.ts new file mode 100644 index 00000000..582de297 --- /dev/null +++ b/packages/atmosphere/index.ts @@ -0,0 +1,3 @@ +export * from './component/AtmosphericComponent'; +export * from './renderer/SkyRenderer'; +export * from './textures/AtmosphericScatteringSky'; \ No newline at end of file diff --git a/packages/atmosphere/package.json b/packages/atmosphere/package.json new file mode 100644 index 00000000..d2fa6266 --- /dev/null +++ b/packages/atmosphere/package.json @@ -0,0 +1,26 @@ +{ + "name": "@orillusion/atmosphere", + "version": "0.1.0", + "author": "Orillusion", + "description": "Orillusion Atmosphere Plugin", + "main": "./dist/atmosphere.umd.js", + "module": "./dist/atmosphere.es.js", + "module:dev": "./index.ts", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "vite build && npm run build:types && npm run build:clean", + "build:types": "tsc --emitDeclarationOnly -p tsconfig.json", + "build:clean": "mv dist/packages/atmosphere/* dist && rm -rf dist/src && rm -rf dist/packages" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Orillusion/orillusion.git" + }, + "dependencies": { + "@orillusion/core": "^0.8.0" + } +} diff --git a/packages/atmosphere/renderer/SkyRenderer.ts b/packages/atmosphere/renderer/SkyRenderer.ts new file mode 100644 index 00000000..fc9bf5b2 --- /dev/null +++ b/packages/atmosphere/renderer/SkyRenderer.ts @@ -0,0 +1,104 @@ +import { + Engine3D, + View3D, + MeshRenderer, + BoundingBox, + Texture, + EntityCollect, + ClusterLightingBuffer, + RendererMask, + RendererPassState, + PassType, + SkyMaterial, + Vector3, + SphereGeometry +} from "@orillusion/core"; + +/** + * + * Sky Box Renderer Component + * @group Components + */ +export class SkyRenderer extends MeshRenderer { + /** + * The material used in the Sky Box. + */ + public skyMaterial: SkyMaterial; + + + public init(): void { + super.init(); + this.castShadow = false; + this.castGI = true; + this.addRendererMask(RendererMask.Sky); + this.alwaysRender = true; + + this.object3D.bound = new BoundingBox(Vector3.ZERO.clone(), Vector3.MAX); + this.geometry = new SphereGeometry(Engine3D.setting.sky.defaultFar, 20, 20); + this.skyMaterial ||= new SkyMaterial(); + } + + public onEnable(): void { + if (!this._readyPipeline) { + this.initPipeline(); + } else { + this.castNeedPass(); + + if (!this._inRenderer && this.transform.scene3D) { + EntityCollect.instance.sky = this; + this._inRenderer = true; + } + } + } + + public onDisable(): void { + if (this._inRenderer && this.transform.scene3D) { + this._inRenderer = false; + EntityCollect.instance.sky = null; + } + super.onDisable(); + } + + public renderPass2(view: View3D, passType: PassType, rendererPassState: RendererPassState, clusterLightingBuffer: ClusterLightingBuffer, encoder: GPURenderPassEncoder, useBundle: boolean = false) { + // this.transform.updateWorldMatrix(); + super.renderPass2(view, passType, rendererPassState, clusterLightingBuffer, encoder, useBundle); + // this.transform.localPosition = Camera3D.mainCamera.transform.localPosition ; + } + + /** + * set environment texture + */ + public set map(texture: Texture) { + this.skyMaterial.baseMap = texture; + if (this.skyMaterial.name == null) { + this.skyMaterial.name = 'skyMaterial'; + } + this.material = this.skyMaterial; + } + + /** + * get environment texture + */ + public get map(): Texture { + return this.skyMaterial.baseMap; + } + + public get exposure() { + return this.skyMaterial.exposure; + } + + public set exposure(value) { + if (this.skyMaterial) + this.skyMaterial.exposure = value; + } + + public get roughness() { + return this.skyMaterial.roughness; + } + + public set roughness(value) { + if (this.skyMaterial) + this.skyMaterial.roughness = value; + } + +} diff --git a/packages/atmosphere/shader/AtmosphereEarth.wgsl b/packages/atmosphere/shader/AtmosphereEarth.wgsl new file mode 100644 index 00000000..ee8c6f78 --- /dev/null +++ b/packages/atmosphere/shader/AtmosphereEarth.wgsl @@ -0,0 +1,111 @@ +fn GetAtmosphereParameters() -> AtmosphereParameters { + var info: AtmosphereParameters; + + let EarthBottomRadius: f32 = 6360.0; + var scalar: f32 = 1.0; // TODO: control with uniform + var EarthTopRadius: f32 = EarthBottomRadius + 100.0 * scalar; + var EarthRayleighScaleHeight: f32 = 8.0 * scalar; + var EarthMieScaleHeight: f32 = 1.2 * scalar; + + info.BottomRadius = EarthBottomRadius; + info.TopRadius = EarthTopRadius; + info.GroundAlbedo = vec3(1.0, 1.0, 1.0); + + info.RayleighDensityExpScale = -1.0 / EarthRayleighScaleHeight; + info.RayleighScattering = vec3(0.005802, 0.013558, 0.033100); + + info.MieDensityExpScale = -1.0 / EarthMieScaleHeight; + info.MieScattering = vec3(0.003996, 0.003996, 0.003996); + info.MieExtinction = vec3(0.004440, 0.004440, 0.004440); + info.MieAbsorption = info.MieExtinction - info.MieScattering; + info.MiePhaseG = 0.8; + + info.AbsorptionDensity0LayerWidth = 25.0 * scalar; + info.AbsorptionDensity0ConstantTerm = -2.0 / 3.0; + info.AbsorptionDensity0LinearTerm = 1.0 / (15.0 * scalar); + info.AbsorptionDensity1ConstantTerm = 8.0 / 3.0; + info.AbsorptionDensity1LinearTerm = -1.0 / (15.0 * scalar); + info.AbsorptionExtinction = vec3(0.000650, 0.001881, 0.000085); + + // Cloud parameters + info.CloudBaseHeight = 3.0; // Base height of clouds in km + info.CloudTopHeight = 5.0; // Top height of clouds in km + info.CloudScattering = vec3(0.9, 0.9, 0.9); // Albedo of clouds + info.CloudAbsorption = vec3(0.001, 0.001, 0.001); + info.CloudPhaseG = 0.8; + info.CloudK = 0.5; + + return info; +} + +fn getAlbedo(scattering: vec3, extinction: vec3) -> vec3 { + return vec3( + scattering.x / max(0.001, extinction.x), + scattering.y / max(0.001, extinction.y), + scattering.z / max(0.001, extinction.z) + ); +} + +fn sampleMediumRGB(WorldPos: vec3, Atmosphere: AtmosphereParameters) -> MediumSampleRGB { + var viewHeight: f32 = length(WorldPos) - Atmosphere.BottomRadius; + + var densityMie: f32 = exp(Atmosphere.MieDensityExpScale * viewHeight); + var densityRay: f32 = exp(Atmosphere.RayleighDensityExpScale * viewHeight); + var clampVal: f32 = Atmosphere.AbsorptionDensity1LinearTerm * viewHeight + Atmosphere.AbsorptionDensity1ConstantTerm; + if viewHeight < Atmosphere.AbsorptionDensity0LayerWidth { + clampVal = Atmosphere.AbsorptionDensity0LinearTerm * viewHeight + Atmosphere.AbsorptionDensity0ConstantTerm; + } + var densityOzo: f32 = clamp(clampVal, 0.0, 1.0); + + var s: MediumSampleRGB; + + s.scatteringMie = densityMie * Atmosphere.MieScattering; + s.absorptionMie = densityMie * Atmosphere.MieAbsorption; + s.extinctionMie = densityMie * Atmosphere.MieExtinction; + + s.scatteringRay = densityRay * Atmosphere.RayleighScattering; + s.absorptionRay = vec3(0.0, 0.0, 0.0); + s.extinctionRay = s.scatteringRay + s.absorptionRay; + + s.scatteringOzo = vec3(0.0, 0.0, 0.0); + s.absorptionOzo = densityOzo * Atmosphere.AbsorptionExtinction; + s.extinctionOzo = s.scatteringOzo + s.absorptionOzo; + + if uniformBuffer.enableClouds > .5 { + var cloudDensity: f32 = sampleCloudDensity(WorldPos, Atmosphere); + s.scatteringCloud = cloudDensity * Atmosphere.CloudScattering; + s.absorptionCloud = cloudDensity * Atmosphere.CloudAbsorption; + s.extinctionCloud = s.scatteringCloud + s.absorptionCloud; + } + + s.scattering = s.scatteringMie + s.scatteringRay + s.scatteringOzo + s.scatteringCloud; + s.absorption = s.absorptionMie + s.absorptionRay + s.absorptionOzo + s.absorptionCloud; + s.extinction = s.extinctionMie + s.extinctionRay + s.extinctionOzo + s.extinctionCloud; + s.albedo = getAlbedo(s.scattering, s.extinction); + + return s; +} + +fn sigmoid(x: f32) -> f32 { + return 1.0 / (1.0 + exp(-x)); +} + +fn sampleCloudDensity(WorldPos: vec3, Atmosphere: AtmosphereParameters) -> f32 { + var x: f32 = length(WorldPos) - Atmosphere.BottomRadius; + var ymin: f32 = Atmosphere.CloudBaseHeight; + var ymax: f32 = Atmosphere.CloudTopHeight; + var yt: f32 = 1.0; + var baseVal: f32 = smoothstep(ymin, ymin + yt, x) * (1. - smoothstep(ymax - yt, ymax, x)); + var noiseScale: f32 = 1. / 11000.; + var uv0: vec2 = WorldPos.xz * noiseScale; + var noiseValue: f32 = sampleCloudTexture(uv0); + var noiseExpression: f32 = 12.; + var noiseFactor: f32 = sigmoid(noiseValue * 2.0 * noiseExpression - noiseExpression); + return baseVal * noiseFactor; +} + +// sample the cloud texture +fn sampleCloudTexture(uv: vec2) -> f32 { + var coords = vec2(uv * vec2(textureDimensions(cloudTexture, 0))); + return textureSampleLevel(cloudTexture, cloudTextureSampler, coords, 0).r; +} diff --git a/packages/atmosphere/shader/AtmosphereUniforms.wgsl b/packages/atmosphere/shader/AtmosphereUniforms.wgsl new file mode 100644 index 00000000..c59896be --- /dev/null +++ b/packages/atmosphere/shader/AtmosphereUniforms.wgsl @@ -0,0 +1,16 @@ +struct UniformData { + width: f32, + height: f32, + sunU: f32, + sunV: f32, + eyePos: f32, + sunRadius: f32, // = 500.0; + sunRadiance: f32, // = 20.0; + mieG: f32, // = 0.76; + mieHeight: f32, // = 1200; + sunBrightness: f32, // = 1.0; + displaySun: f32, // > 0.5: true + enableClouds: f32, // > 0.5: true + hdrExposure: f32, // = 1.0; + skyColor: vec4, // sky color +}; \ No newline at end of file diff --git a/packages/atmosphere/shader/AtmosphericScatteringIntegration.wgsl b/packages/atmosphere/shader/AtmosphericScatteringIntegration.wgsl new file mode 100644 index 00000000..8fa1ca2d --- /dev/null +++ b/packages/atmosphere/shader/AtmosphericScatteringIntegration.wgsl @@ -0,0 +1,429 @@ +// the max distance to ray march in meters +var defaultTMaxMax: f32 = 9000000.0; +var PLANET_RADIUS_OFFSET: f32 = 0.01; + +// Sample per pixel for ray marching +// 16.0 without clouds +// 128.0 for thicker atmosphere +// 256.0 for clouds +var RayMarchMinMaxSPP: vec2 = vec2(1.0, 16.0); +var RayMarchMinMaxSPPCloud: vec2 = vec2(1.0, 256.0); +var MULTI_SCATTERING_POWER_SERIE: u32 = 1; +var MULTISCATAPPROX_ENABLED: u32 = 1; +var SHADOWMAP_ENABLED: u32 = 0; +var VOLUMETRIC_SHADOW_ENABLED: u32 = 1; +var MultiScatteringLUTRes: f32 = 32.0; + +struct SingleScatteringResult { + L: vec3, // Scattered light (luminance) + OpticalDepth: vec3, // Optical depth (1/m) + Transmittance: vec3, // Transmittance in [0,1] (unitless) + MultiScatAs1: vec3, + NewMultiScatStep0Out: vec3, + NewMultiScatStep1Out: vec3, +}; + +var SunLuminance: vec3 = vec3(1000000.0); // arbitrary. But fine, not use when comparing the models + +struct AtmosphereParameters { + BottomRadius: f32, + TopRadius: f32, + + RayleighDensityExpScale: f32, + RayleighScattering: vec3, + + MieDensityExpScale: f32, + MieScattering: vec3, + MieExtinction: vec3, + MieAbsorption: vec3, + MiePhaseG: f32, + + AbsorptionDensity0LayerWidth: f32, + AbsorptionDensity0ConstantTerm: f32, + AbsorptionDensity0LinearTerm: f32, + AbsorptionDensity1ConstantTerm: f32, + AbsorptionDensity1LinearTerm: f32, + AbsorptionExtinction: vec3, + + GroundAlbedo: vec3, + + CloudBaseHeight: f32, + CloudTopHeight: f32, + CloudScattering: vec3, + CloudAbsorption: vec3, + CloudPhaseG: f32, + CloudK: f32, +}; + +struct MediumSampleRGB { + scattering: vec3, + absorption: vec3, + extinction: vec3, + + scatteringMie: vec3, + absorptionMie: vec3, + extinctionMie: vec3, + + scatteringRay: vec3, + absorptionRay: vec3, + extinctionRay: vec3, + + scatteringOzo: vec3, + absorptionOzo: vec3, + extinctionOzo: vec3, + + scatteringCloud: vec3, + absorptionCloud: vec3, + extinctionCloud: vec3, + + albedo: vec3, +}; + +fn ComputeSphereNormal(coord: vec2, phiStart: f32, phiLength: f32, thetaStart: f32, thetaLength: f32) -> vec3 { + var normal: vec3; + normal.x = -sin(thetaStart + coord.y * thetaLength) * sin(phiStart + coord.x * phiLength); + normal.y = -cos(thetaStart + coord.y * thetaLength); + normal.z = -sin(thetaStart + coord.y * thetaLength) * cos(phiStart + coord.x * phiLength); + return normalize(normal); +} + +fn raySphereIntersectNearest(r0: vec3, rd: vec3, s0: vec3, sR: f32) -> f32 { + var a: f32 = dot(rd, rd); + var s0_r0: vec3 = r0 - s0; + var b: f32 = 2.0 * dot(rd, s0_r0); + var c: f32 = dot(s0_r0, s0_r0) - (sR * sR); + var delta: f32 = b * b - 4.0 * a * c; + if delta < 0.0 || a == 0.0 { + return -1.0; + } + var sol0: f32 = (-b - sqrt(delta)) / (2.0 * a); + var sol1: f32 = (-b + sqrt(delta)) / (2.0 * a); + if sol0 < 0.0 && sol1 < 0.0 { + return -1.0; + } + if sol0 < 0.0 { + return max(0.0, sol1); + } else if sol1 < 0.0 { + return max(0.0, sol0); + } + return max(0.0, min(sol0, sol1)); +} + +fn CornetteShanksMiePhaseFunction(g: f32, cosTheta: f32) -> f32 { + var k: f32 = 3.0 / (8.0 * PI) * (1.0 - g * g) / (2.0 + g * g); + return k * (1.0 + cosTheta * cosTheta) / pow(1.0 + g * g - 2.0 * g * -cosTheta, 1.5); +} + +fn RayleighPhase(cosTheta: f32) -> f32 { + var factor: f32 = 3.0 / (16.0 * PI); + return factor * (1.0 + cosTheta * cosTheta); +} + +fn hgPhase(g: f32, cosTheta: f32) -> f32 { + return CornetteShanksMiePhaseFunction(g, cosTheta); +} + +// dual-lobe hg phase +fn dualLobeHgPhase(g: f32, cosTheta: f32, k: f32) -> f32 { + var phase1: f32 = hgPhase(g, cosTheta); + var phase2: f32 = hgPhase(-g, cosTheta); + return mix(phase1, phase2, k); +} + +fn LutTransmittanceParamsToUv(Atmosphere: AtmosphereParameters, viewHeight: f32, viewZenithCosAngle: f32) -> vec2 { + var H: f32 = sqrt(max(0.0, Atmosphere.TopRadius * Atmosphere.TopRadius - Atmosphere.BottomRadius * Atmosphere.BottomRadius)); + var rho: f32 = sqrt(max(0.0, viewHeight * viewHeight - Atmosphere.BottomRadius * Atmosphere.BottomRadius)); + + var discriminant: f32 = viewHeight * viewHeight * (viewZenithCosAngle * viewZenithCosAngle - 1.0) + Atmosphere.TopRadius * Atmosphere.TopRadius; + var d: f32 = max(0.0, (-viewHeight * viewZenithCosAngle + sqrt(discriminant))); // Distance to atmosphere boundary + + var d_min: f32 = Atmosphere.TopRadius - viewHeight; + var d_max: f32 = rho + H; + var x_mu: f32 = (d - d_min) / (d_max - d_min); + var x_r: f32 = rho / H; + + return vec2(x_mu, x_r); +} + +fn fromUnitToSubUvs(u: f32, resolution: f32) -> f32 { + return (u + 0.5 / resolution) * (resolution / (resolution + 1.0)); +} + +fn fromSubUvsToUnit(u: f32, resolution: f32) -> f32 { + return (u - 0.5 / resolution) * (resolution / (resolution - 1.0)); +} + +fn GetSunLuminance(WorldPos: vec3, WorldDir: vec3, PlanetRadius: f32) -> vec3 { + var sun_direction: vec3 = normalize(getSunDirection()); + if dot(WorldDir, sun_direction) > cos(0.5 * 0.505 * PI / 180.0) { + var t: f32 = raySphereIntersectNearest(WorldPos, WorldDir, vec3(0.0, 0.0, 0.0), PlanetRadius); + if t < 0.0 { // no intersection + return SunLuminance; // arbitrary. But fine, not use when comparing the models + } + } + return vec3(0.0); +} + +fn MoveToTopAtmosphere(WorldPos: ptr>, WorldDir: vec3, AtmosphereTopRadius: f32) -> bool { + var viewHeight: f32 = length(*WorldPos); + if viewHeight > AtmosphereTopRadius { + var tTop: f32 = raySphereIntersectNearest(*WorldPos, WorldDir, vec3(0.0, 0.0, 0.0), AtmosphereTopRadius); + if tTop >= 0.0 { + var UpVector: vec3 = *WorldPos / viewHeight; + var UpOffset: vec3 = UpVector * -0.01; + *WorldPos = *WorldPos + WorldDir * tTop + UpOffset; + } else { + // Ray is not intersecting the atmosphere + return false; + } + } + return true; // ok to start tracing +} + +fn getSunDirection() -> vec3 { + var sun = vec2(uniformBuffer.sunU, uniformBuffer.sunV); + var L: vec3 = ComputeSphereNormal(vec2(sun.x, sun.y), 0.0, PI_2, 0.0, PI); + return L; +} + +fn GetTransmittanceToSun(Atmosphere: AtmosphereParameters, P: vec3) -> vec3 { + var pHeight: f32 = length(P); + var UpVector: vec3 = P / pHeight; + var SunZenithCosAngle: f32 = dot(getSunDirection(), UpVector); + var uv = LutTransmittanceParamsToUv(Atmosphere, pHeight, SunZenithCosAngle); + return textureSampleLevel(transmittanceTexture, transmittanceTextureSampler, uv, 0).rgb; +} + +fn GetMultipleScattering(Atmosphere: AtmosphereParameters, scattering: vec3, extinction: vec3, worlPos: vec3, viewZenithCosAngle: f32) -> vec3 { + var uv = saturate(vec2(viewZenithCosAngle * 0.5 + 0.5, (length(worlPos) - Atmosphere.BottomRadius) / (Atmosphere.TopRadius - Atmosphere.BottomRadius))); + uv = vec2(fromUnitToSubUvs(uv.x, MultiScatteringLUTRes), fromUnitToSubUvs(uv.y, MultiScatteringLUTRes)); + + var multiScatteredLuminance: vec3 = textureSampleLevel(multipleScatteringTexture, multipleScatteringTextureSampler, uv, 0).rgb; + return multiScatteredLuminance; +} + +fn getShadow(Atmosphere: AtmosphereParameters, P: vec3) -> f32 { + // TODO: sample cascading shadow map + return 1.0; +} + +fn computeVolumetricShadow(WorldPos: vec3, LightDir: vec3, Atmosphere: AtmosphereParameters) -> f32 { + var shadow: f32 = 1.0; + var stepSize: f32 = 0.3; // Adjust based on scene scale + var pos: vec3 = WorldPos; + for (var i: f32 = 0.0; i < 10.0; i += 1.0) { // Number of steps can be adjusted + pos += stepSize * LightDir; + shadow *= 1.0 - sampleCloudDensity(pos, Atmosphere); + if (shadow < 0.05) { + break; // Early exit for low shadow values + } + } + return shadow; +} + +// near: 0.01, far: 10000 +fn linearizeDepth(depth: f32, near: f32, far: f32) -> f32 { + var z: f32 = depth * 2.0 - 1.0; // Back to NDC + return (2.0 * near * far) / (far + near - z * (far - near)); +} + +fn IntegrateScatteredLuminance( + pixPos: vec2, + WorldPos: vec3, + WorldDir: vec3, + SunDir: vec3, + Atmosphere: AtmosphereParameters, + ground: bool, + SampleCountIni: f32, + DepthBufferValue: f32, + VariableSampleCount: bool, + MieRayPhase: bool, + tMaxMax: f32, + resolution: vec2 +) -> SingleScatteringResult { + var debugEnabled: bool = false; + var result: SingleScatteringResult = SingleScatteringResult(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0)); + + var ClipSpace: vec3 = vec3((pixPos / resolution) * vec2(2.0, 2.0) - vec2(1.0, 1.0), 1.0); + + // Compute next intersection with atmosphere or ground + var earthO: vec3 = vec3(0.0, 0.0, 0.0); + var tBottom: f32 = raySphereIntersectNearest(WorldPos, WorldDir, earthO, Atmosphere.BottomRadius); + var tTop: f32 = raySphereIntersectNearest(WorldPos, WorldDir, earthO, Atmosphere.TopRadius); + var tMax: f32 = 0.0; + if tBottom < 0.0 { + if tTop < 0.0 { + tMax = 0.0; // No intersection with earth nor atmosphere: stop right away + return result; + } else { + tMax = tTop; + } + } else { + if tTop > 0.0 { + tMax = min(tTop, tBottom); + } + } + + if DepthBufferValue >= 0.0 { + ClipSpace.z = DepthBufferValue; + if ClipSpace.z < 1.0 { + // TODO: use uniformBuffer.invViewProjMatrix to get the world position + var DepthBufferWorldPos: vec4 = mat4x4() * vec4(ClipSpace, 1.0); + DepthBufferWorldPos /= DepthBufferWorldPos.w; + + var tDepth: f32 = length(DepthBufferWorldPos.xyz - (WorldPos + vec3(0.0, -Atmosphere.BottomRadius, 0.0))); // apply earth offset to go back to origin as top of earth mode. + tMax = min(tMax, tDepth); + } + } + tMax = min(tMax, tMaxMax); + + // Sample count + var SampleCount: f32 = SampleCountIni; + var SampleCountFloor: f32 = SampleCountIni; + var tMaxFloor: f32 = tMax; + if VariableSampleCount { + var spp: vec2 = RayMarchMinMaxSPP; + if uniformBuffer.enableClouds > 0.5 { + spp = RayMarchMinMaxSPPCloud; + } + SampleCount = mix(spp.x, spp.y, clamp(tMax * 0.01, 0.0, 1.0)); + SampleCountFloor = floor(SampleCount); + tMaxFloor = tMax * SampleCountFloor / SampleCount; // rescale tMax to map to the last entire step segment. + } + var dt: f32 = tMax / SampleCount; + + // Phase functions + var uniformPhase: f32 = 1.0 / (4.0 * PI); + var wi: vec3 = SunDir; + var wo: vec3 = WorldDir; + var cosTheta: f32 = dot(wi, wo); + var MiePhaseValue: f32 = hgPhase(Atmosphere.MiePhaseG, -cosTheta); // negate cosTheta because WorldDir is an "in" direction. + var RayleighPhaseValue: f32 = RayleighPhase(cosTheta); + var CloudPhaseValue: f32 = dualLobeHgPhase(Atmosphere.CloudPhaseG, cosTheta, Atmosphere.CloudK); + + // #ifdef ILLUMINANCE_IS_ONE + var globalL: vec3 = vec3(1.0); + // #else + // var globalL: vec3 = iSunIlluminance; + // #endif + + // Ray march the atmosphere to integrate optical depth + var L: vec3 = vec3(0.0); + var throughput: vec3 = vec3(1.0); + var OpticalDepth: vec3 = vec3(0.0); + var t: f32 = 0.0; + var tPrev: f32 = 0.0; + var SampleSegmentT: f32 = 0.3; + + // TODO: improve sampling and performance inside of the cloud layer + // compute the intersection points pointing in WorldDir direction + var tCloudBottom: f32 = raySphereIntersectNearest(WorldPos, WorldDir, earthO, Atmosphere.BottomRadius + Atmosphere.CloudBaseHeight); + var tCloudTop: f32 = raySphereIntersectNearest(WorldPos, WorldDir, earthO, Atmosphere.BottomRadius + Atmosphere.CloudTopHeight); + + // Ray marching loop + for (var s: f32 = 0.0; s < SampleCount; s += 1.0) { + if VariableSampleCount { + var t0: f32 = s / SampleCountFloor; + var t1: f32 = (s + 1.0) / SampleCountFloor; + t0 = t0 * t0; + t1 = t1 * t1; + t0 = tMaxFloor * t0; + if t1 > 1.0 { + t1 = tMax; + } else { + t1 = tMaxFloor * t1; + } + t = t0 + (t1 - t0) * SampleSegmentT; + dt = t1 - t0; + } else { + var NewT: f32 = tMax * (s + SampleSegmentT) / SampleCount; + dt = NewT - t; + t = NewT; + } + var P: vec3 = WorldPos + t * WorldDir; + + var medium: MediumSampleRGB = sampleMediumRGB(P, Atmosphere); + var SampleOpticalDepth: vec3 = medium.extinction * dt; + var SampleTransmittance: vec3 = exp(-SampleOpticalDepth); + OpticalDepth += SampleOpticalDepth; + + var pHeight: f32 = length(P); + var UpVector: vec3 = P / pHeight; + var SunZenithCosAngle: f32 = dot(SunDir, UpVector); + var uv = LutTransmittanceParamsToUv(Atmosphere, pHeight, SunZenithCosAngle); + var transmittanceTextureSize = vec2(textureDimensions(transmittanceTexture, 0)); + var transmittanceTextureCoord = vec2(transmittanceTextureSize * uv); + var TransmittanceToSun: vec3 = textureLoad(transmittanceTexture, transmittanceTextureCoord, 0).rgb; + + var PhaseTimesScattering: vec3; + if MieRayPhase { + PhaseTimesScattering = medium.scatteringMie * MiePhaseValue + medium.scatteringRay * RayleighPhaseValue + medium.scatteringCloud * MiePhaseValue; + } else { + PhaseTimesScattering = medium.scattering * uniformPhase; + } + + // Earth shadow + var tEarth: f32 = raySphereIntersectNearest(P, SunDir, earthO + PLANET_RADIUS_OFFSET * UpVector, Atmosphere.BottomRadius); + var earthShadow: f32 = 1.0; + if tEarth >= 0.0 { + earthShadow = 0.0; + } + + // Dual scattering for multi scattering + var multiScatteredLuminance: vec3 = vec3(0.0); + if MULTISCATAPPROX_ENABLED == 1 { + multiScatteredLuminance = GetMultipleScattering(Atmosphere, medium.scattering, medium.extinction, P, SunZenithCosAngle); + } + + var shadow: f32 = 1.0; + if SHADOWMAP_ENABLED == 1 { + shadow = getShadow(Atmosphere, P); + } + if VOLUMETRIC_SHADOW_ENABLED == 1 && uniformBuffer.enableClouds > 0.5 { + shadow = computeVolumetricShadow(P, SunDir, Atmosphere); + } + + var S: vec3 = globalL * (earthShadow * shadow * TransmittanceToSun * PhaseTimesScattering + multiScatteredLuminance * medium.scattering); + + if MULTI_SCATTERING_POWER_SERIE == 0 { + result.MultiScatAs1 += throughput * medium.scattering * 1.0 * dt; + } else { + var MS: vec3 = medium.scattering * 1.0; + var MSint: vec3 = (MS - MS * SampleTransmittance) / medium.extinction; + result.MultiScatAs1 += throughput * MSint; + } + + // Evaluate input to multi scattering + { + var newMS: vec3; + + newMS = earthShadow * TransmittanceToSun * medium.scattering * uniformPhase * 1.0; + result.NewMultiScatStep0Out += throughput * (newMS - newMS * SampleTransmittance) / medium.extinction; + + newMS = medium.scattering * uniformPhase * multiScatteredLuminance; + result.NewMultiScatStep1Out += throughput * (newMS - newMS * SampleTransmittance) / medium.extinction; + } + + var Sint: vec3 = (S - S * SampleTransmittance) / medium.extinction; + L += throughput * Sint; + throughput *= SampleTransmittance; + + tPrev = t; + } + + if ground && tMax == tBottom && tBottom > 0.0 { + // Account for bounced light off the earth + var P: vec3 = WorldPos + tBottom * WorldDir; + var pHeight: f32 = length(P); + var UpVector: vec3 = P / pHeight; + var NdotL: f32 = clamp(dot(normalize(UpVector), normalize(SunDir)), 0.0, 1.0); + var albedo: vec3 = Atmosphere.GroundAlbedo; + var TransmittanceToSun: vec3 = GetTransmittanceToSun(Atmosphere, P); + L += globalL * TransmittanceToSun * throughput * NdotL * albedo / PI; + } + + result.L = L; + result.OpticalDepth = OpticalDepth; + result.Transmittance = throughput; + return result; +} diff --git a/packages/atmosphere/shader/AtmosphericScatteringSky.wgsl b/packages/atmosphere/shader/AtmosphericScatteringSky.wgsl new file mode 100644 index 00000000..2bede056 --- /dev/null +++ b/packages/atmosphere/shader/AtmosphericScatteringSky.wgsl @@ -0,0 +1,258 @@ +#include 'ColorUtil_frag' +#include 'AtmosphereUniforms' + +@group(0) @binding(0) var uniformBuffer: UniformData; +@group(0) @binding(1) var outTexture : texture_storage_2d; + +var uv01: vec2; +var fragCoord: vec2; +var texSizeF32: vec2; + +var PI:f32 = 3.1415926535; +var PI_2:f32 = 0.0; +var EPSILON:f32 = 0.0000001; +var SAMPLES_NUMS:i32 = 16; + +var transmittance:vec3; +var insctrMie:vec3; +var insctrRayleigh:vec3; + +@compute @workgroup_size( 8 , 8 , 1 ) +fn CsMain(@builtin(workgroup_id) workgroup_id: vec3, @builtin(global_invocation_id) globalInvocation_id: vec3) { + fragCoord = vec2(globalInvocation_id.xy); + texSizeF32 = vec2(uniformBuffer.width, uniformBuffer.height); + uv01 = vec2(globalInvocation_id.xy) / texSizeF32; + uv01.y = 1.0 - uv01.y - EPSILON; + PI_2 = PI * 2.0; + textureStore(outTexture, fragCoord, mainImage(uv01));//vec4(uv01, 0.0, 1.0)); +} + +struct ScatteringParams { + sunRadius: f32, + sunRadiance: f32, + + mieG: f32, + mieHeight: f32, + + rayleighHeight: f32, + + waveLambdaMie: vec3, + waveLambdaOzone: vec3, + waveLambdaRayleigh: vec3, + + earthRadius: f32, + earthAtmTopRadius: f32, + earthCenter: vec3, +} + +fn ComputeSphereNormal(coord: vec2, phiStart: f32, phiLength: f32, thetaStart: f32, thetaLength: f32) -> vec3 { + var normal: vec3; + normal.x = -sin(thetaStart + coord.y * thetaLength) * sin(phiStart + coord.x * phiLength); + normal.y = -cos(thetaStart + coord.y * thetaLength); + normal.z = -sin(thetaStart + coord.y * thetaLength) * cos(phiStart + coord.x * phiLength); + return normalize(normal); +} + +fn ComputeRaySphereIntersection(position: vec3, dir: vec3, center: vec3, radius: f32) -> vec2 { + var origin: vec3 = position - center; + var B = dot(origin, dir); + var C = dot(origin, origin) - radius * radius; + var D = B * B - C; + + var minimaxIntersections: vec2; + if D < 0.0 { + minimaxIntersections = vec2(-1.0, -1.0); + } else { + D = sqrt(D); + minimaxIntersections = vec2(-B - D, -B + D); + } + + return minimaxIntersections; +} + +fn ComputeWaveLambdaRayleigh(lambda: vec3) -> vec3 { + var n: f32 = 1.0003; + var N: f32 = 2.545E25; + var pn: f32 = 0.035; + var n2: f32 = n * n; + var pi3: f32 = PI * PI * PI; + var rayleighConst: f32 = (8.0 * pi3 * pow(n2 - 1.0, 2.0)) / (3.0 * N) * ((6.0 + 3.0 * pn) / (6.0 - 7.0 * pn)); + return vec3(rayleighConst) / (lambda * lambda * lambda * lambda); +} + +fn ComputePhaseMie(theta: f32, g: f32) -> f32 { + var g2 = g * g; + return (1.0 - g2) / pow(1.0 + g2 - 2.0 * g * saturate(theta), 1.5) / (4.0 * PI); +} + +fn ComputePhaseRayleigh(theta: f32) -> f32 { + var theta2 = theta * theta; + return (theta2 * 0.75 + 0.75) / (4.0 * PI); +} + +fn ChapmanApproximation(X: f32, h: f32, cosZenith: f32) -> f32 { + var c = sqrt(X + h); + var c_exp_h = c * exp(-h); + + if cosZenith >= 0.0 { + return c_exp_h / (c * cosZenith + 1.0); + } else { + var x0 = sqrt(1.0 - cosZenith * cosZenith) * (X + h); + var c0 = sqrt(x0); + + return 2.0 * c0 * exp(X - x0) - c_exp_h / (1.0 - c * cosZenith); + } +} + +fn GetOpticalDepthSchueler(h: f32, H: f32, earthRadius: f32, cosZenith: f32) -> f32 { + return H * ChapmanApproximation(earthRadius / H, h / H, cosZenith); +} + +fn GetTransmittance(setting: ScatteringParams, L: vec3, V: vec3) -> vec3 { + var ch = GetOpticalDepthSchueler(L.y, setting.rayleighHeight, setting.earthRadius, V.y); + return exp(-(setting.waveLambdaMie + setting.waveLambdaRayleigh) * ch); +} + +fn ComputeOpticalDepth(setting: ScatteringParams, samplePoint: vec3, V: vec3, L: vec3, neg: f32) -> vec2 { + var rl = length(samplePoint); + var h = rl - setting.earthRadius; + var r: vec3 = samplePoint / rl; + + var cos_chi_sun = dot(r, L); + var cos_chi_ray = dot(r, V * neg); + + var opticalDepthSun = GetOpticalDepthSchueler(h, setting.rayleighHeight, setting.earthRadius, cos_chi_sun); + var opticalDepthCamera = GetOpticalDepthSchueler(h, setting.rayleighHeight, setting.earthRadius, cos_chi_ray) * neg; + + return vec2(opticalDepthSun, opticalDepthCamera); +} + +fn AerialPerspective(setting: ScatteringParams, start: vec3, end: vec3, V: vec3, L: vec3, infinite: i32) { + var inf_neg: f32 = 1.0; + if infinite == 0 { + inf_neg = -1.0; + } + + var sampleStep: vec3 = (end - start) / f32(SAMPLES_NUMS); + var samplePoint: vec3 = end - sampleStep; + var sampleLambda: vec3 = setting.waveLambdaMie + setting.waveLambdaRayleigh + setting.waveLambdaOzone; + + var sampleLength: f32 = length(sampleStep); + + var scattering: vec3 = vec3(0.0); + var lastOpticalDepth: vec2 = ComputeOpticalDepth(setting, end, V, L, inf_neg); + + for (var i: i32 = 1; i < SAMPLES_NUMS; i = i + 1) { + var opticalDepth: vec2 = ComputeOpticalDepth(setting, samplePoint, V, L, inf_neg); + + var segment_s: vec3 = exp(-sampleLambda * (opticalDepth.x + lastOpticalDepth.x)); + var segment_t: vec3 = exp(-sampleLambda * (opticalDepth.y - lastOpticalDepth.y)); + + transmittance *= segment_t; + + scattering = scattering * segment_t; + scattering += exp(-(length(samplePoint) - setting.earthRadius) / setting.rayleighHeight) * segment_s; + + lastOpticalDepth = opticalDepth; + samplePoint = samplePoint - sampleStep; + } + + insctrMie = scattering * setting.waveLambdaMie * sampleLength; + insctrRayleigh = scattering * setting.waveLambdaRayleigh * sampleLength; +} + +fn ComputeSkyboxChapman(setting: ScatteringParams, eye: vec3, V: vec3, L: vec3) -> f32 { + var neg: i32 = 1; + var outerIntersections: vec2 = ComputeRaySphereIntersection(eye, V, setting.earthCenter, setting.earthAtmTopRadius); + if outerIntersections.y < 0.0 { + return 0.0; + } + var innerIntersections: vec2 = ComputeRaySphereIntersection(eye, V, setting.earthCenter, setting.earthRadius); + if innerIntersections.x > 0.0 { + neg = 0; + outerIntersections.y = innerIntersections.x; + } + + let eye0 = eye - setting.earthCenter; + + var start: vec3 = eye0 + V * max(0.0, outerIntersections.x); + var end: vec3 = eye0 + V * outerIntersections.y; + + AerialPerspective(setting, start, end, V, L, neg); + + //bool intersectionTest = innerIntersections.x < 0.0 && innerIntersections.y < 0.0; + //return intersectionTest ? 1.0 : 0.0; + + if innerIntersections.x < 0.0 && innerIntersections.y < 0.0 { + return 1.0; + } + return 0.0; +} + +fn ComputeSkyInscattering(setting: ScatteringParams, eye: vec3, V: vec3, L: vec3) -> vec4 { + transmittance = vec3(1.0); + insctrMie = vec3(0.0); + insctrRayleigh = vec3(0.0); + var intersectionTest: f32 = ComputeSkyboxChapman(setting, eye, V, L); + + var phaseTheta = dot(V, L); + var phaseMie = ComputePhaseMie(phaseTheta, setting.mieG); + var phaseRayleigh = ComputePhaseRayleigh(phaseTheta); + var phaseNight = 1.0 - saturate(transmittance.x * EPSILON); + + var insctrTotalMie: vec3 = insctrMie * phaseMie; + var insctrTotalRayleigh: vec3 = insctrRayleigh * phaseRayleigh; + + var sky: vec3 = (insctrTotalMie + insctrTotalRayleigh) * setting.sunRadiance; + if uniformBuffer.displaySun > 0.5 { + var angle: f32 = saturate((1.0 - phaseTheta) * setting.sunRadius); + var cosAngle: f32 = cos(angle * PI * 0.5); + var edge: f32 = 0.0; + if angle >= 0.9 { + edge = smoothstep(0.9, 1.0, angle); + } + + var limbDarkening: vec3 = GetTransmittance(setting, -L, V); + limbDarkening *= pow(vec3(cosAngle), vec3(0.420, 0.503, 0.652)) * mix(vec3(1.0), vec3(1.2, 0.9, 0.5), edge) * intersectionTest; + sky += limbDarkening * uniformBuffer.sunBrightness; + } + return vec4(sky, phaseNight * intersectionTest); +} + +fn noise(uv: vec2) -> f32 { + return fract(dot(sin(vec3(uv.xyx) * vec3(uv.xyy) * 1024.0), vec3(341896.483, 891618.637, 602649.7031))); +} + +fn mainImage(uv: vec2) -> vec4 { + let eyePosition = uniformBuffer.eyePos; + var sun = vec2(uniformBuffer.sunU, uniformBuffer.sunV); + var V: vec3 = ComputeSphereNormal(uv, 0.0, PI_2, 0.0, PI); + var L: vec3 = ComputeSphereNormal(vec2(sun.x, sun.y), 0.0, PI_2, 0.0, PI); + + var setting: ScatteringParams; + setting.sunRadius = uniformBuffer.sunRadius;//500.0; + setting.sunRadiance = uniformBuffer.sunRadiance;//20.0; + setting.mieG = uniformBuffer.mieG;//0.76; + setting.mieHeight = uniformBuffer.mieHeight;// 1200.0; + setting.rayleighHeight = 8000.0; + setting.earthRadius = 6360000.0; + setting.earthAtmTopRadius = 6420000.0; + setting.earthCenter = vec3(0, -setting.earthRadius, 0); + setting.waveLambdaMie = vec3(0.0000002); + + // wavelength with 680nm, 550nm, 450nm + setting.waveLambdaRayleigh = ComputeWaveLambdaRayleigh(vec3(0.000000680, 0.000000550, 0.000000450)); + + // see https://www.shadertoy.com/view/MllBR2 + setting.waveLambdaOzone = vec3(1.36820899679147, 3.31405330400124, 0.13601728252538) * 0.0000006 * 2.504; + + var eye: vec3 = vec3(0, eyePosition, 0); + var sky0: vec4 = ComputeSkyInscattering(setting, eye, V, L); + var sky = vec3(sky0.rgb); + + sky = ACESToneMapping(sky.rgb, uniformBuffer.hdrExposure); + sky = pow(sky.rgb, vec3(1.0 / 1.2)); // gamma + + var fragColor: vec4 = vec4((sky.rgb), 1.0); + return fragColor; +} \ No newline at end of file diff --git a/packages/atmosphere/shader/AtmosphericScatteringSky_shader.ts b/packages/atmosphere/shader/AtmosphericScatteringSky_shader.ts new file mode 100644 index 00000000..77b87792 --- /dev/null +++ b/packages/atmosphere/shader/AtmosphericScatteringSky_shader.ts @@ -0,0 +1,24 @@ +import source from "./AtmosphericScatteringSky.wgsl?raw"; +import transmittance from "./RenderTransmittanceLutPS.wgsl?raw"; +import integration from "./AtmosphericScatteringIntegration.wgsl?raw"; +import multiscatter from "./NewMultiScattCS.wgsl?raw"; +import earth from "./AtmosphereEarth.wgsl?raw"; +import uniforms from "./AtmosphereUniforms.wgsl?raw"; +import raymarch from "./RenderSkyRayMarching.wgsl?raw"; +import skyview from "./SkyViewLutPS.wgsl?raw"; +import cloud from "./CloudNoise.wgsl?raw"; + +/** + * @internal + */ +export class AtmosphericScatteringSky_shader { + public static cs: string = source; + public static transmittance_cs: string = transmittance; + public static multiscatter_cs: string = multiscatter; + public static integration: string = integration; + public static raymarch_cs: string = raymarch; + public static skyview_cs: string = skyview; + public static earth: string = earth; + public static uniforms: string = uniforms; + public static cloud_cs: string = cloud; +} diff --git a/packages/atmosphere/shader/CloudNoise.wgsl b/packages/atmosphere/shader/CloudNoise.wgsl new file mode 100644 index 00000000..b624359a --- /dev/null +++ b/packages/atmosphere/shader/CloudNoise.wgsl @@ -0,0 +1,45 @@ +#include 'AtmosphereUniforms' + +@group(0) @binding(0) var uniformBuffer: UniformData; +@group(0) @binding(1) var outTexture: texture_storage_2d; + +var uv01: vec2; +var fragCoord: vec2; +var texSizeF32: vec2; +var PI: f32 = 3.1415926535897932384626433832795; +var PI_2: f32 = 0.0; +var EPSILON:f32 = 0.0000001; + +@compute @workgroup_size(8, 8, 1) +fn CsMain(@builtin(workgroup_id) workgroup_id: vec3, @builtin(global_invocation_id) globalInvocation_id: vec3) { + fragCoord = vec2(globalInvocation_id.xy); + texSizeF32 = vec2(uniformBuffer.width, uniformBuffer.height); + uv01 = vec2(globalInvocation_id.xy) / texSizeF32; + uv01.y = 1.0 - uv01.y - EPSILON; + PI_2 = PI * 2.0; + uv01 *= vec2(10., 10.); + var coord = vec3(uv01, 1.0); + var noiseValue = 1. - worleyNoise3D(coord); + var outColor = vec4(noiseValue, noiseValue, noiseValue, 1.); + textureStore(outTexture, fragCoord, outColor); +} + +fn worleyNoise3D(pos: vec3) -> f32 { + var minDist: f32 = 1.0; // Large number to start with + for (var z: i32 = -1; z <= 1; z++) { + for (var y: i32 = -1; y <= 1; y++) { + for (var x: i32 = -1; x <= 1; x++) { + var cell = floor(pos) + vec3(f32(x), f32(y), f32(z)); + var point = cell + hash3D(cell); // hash3D is a function you need to define + var dist = distance(pos, point); + minDist = min(minDist, dist); + } + } + } + return minDist; +} + +fn hash3D(p: vec3) -> vec3 { + // Simple hash function, replace with a better one if needed + return fract(sin(vec3(dot(p, vec3(127.1, 311.7, 74.7)), dot(p, vec3(269.5, 183.3, 246.1)), dot(p, vec3(113.5, 271.9, 124.6)))) * 43758.5453); +} \ No newline at end of file diff --git a/packages/atmosphere/shader/NewMultiScattCS.wgsl b/packages/atmosphere/shader/NewMultiScattCS.wgsl new file mode 100644 index 00000000..d653d5d2 --- /dev/null +++ b/packages/atmosphere/shader/NewMultiScattCS.wgsl @@ -0,0 +1,127 @@ +#include 'AtmosphereEarth' +#include 'AtmosphericScatteringIntegration' +#include 'AtmosphereUniforms' + +@group(0) @binding(0) var uniformBuffer: UniformData; +@group(0) @binding(1) var outTexture: texture_storage_2d; +@group(0) @binding(auto) var transmittanceTexture: texture_2d; +@group(0) @binding(auto) var transmittanceTextureSampler: sampler; +@group(0) @binding(auto) var multipleScatteringTexture: texture_2d; +@group(0) @binding(auto) var multipleScatteringTextureSampler: sampler; +@group(0) @binding(auto) var cloudTextureSampler: sampler; +@group(0) @binding(auto) var cloudTexture: texture_2d; + +var MultiScatAs1SharedMem: array, 64>; +var LSharedMem: array, 64>; + +var PI: f32 = 3.1415926535897932384626433832795; +var PI_2: f32 = 0.0; +var EPSILON: f32 = 0.0000001; +// var MULTI_SCATTERING_POWER_SERIE: u32 = 1; +var SQRTSAMPLECOUNT: u32 = 8; + +var MultipleScatteringFactor: f32 = 1.0; // change to 50 to see the texture + +@compute @workgroup_size(1, 1, 64) +fn CsMain(@builtin(global_invocation_id) ThreadId: vec3) { + var texSize = vec2(uniformBuffer.width, uniformBuffer.height); + var pixPos: vec2 = vec2(ThreadId.xy) + 0.5; + var uv: vec2 = pixPos / MultiScatteringLUTRes; + + uv = vec2(fromSubUvsToUnit(uv.x, MultiScatteringLUTRes), fromSubUvsToUnit(1. - uv.y, MultiScatteringLUTRes)); + + var Atmosphere: AtmosphereParameters = GetAtmosphereParameters(); + + var cosSunZenithAngle: f32 = uv.x * 2.0 - 1.0; + var sunDir: vec3 = vec3(0.0, sqrt(saturate(1.0 - cosSunZenithAngle * cosSunZenithAngle)), cosSunZenithAngle); + var viewHeight: f32 = Atmosphere.BottomRadius + saturate(uv.y + PLANET_RADIUS_OFFSET) * (Atmosphere.TopRadius - Atmosphere.BottomRadius - PLANET_RADIUS_OFFSET); + + var WorldPos: vec3 = vec3(0.0, 0.0, viewHeight); + var WorldDir: vec3 = vec3(0.0, 0.0, 1.0); + + const ground: bool = true; + const SampleCountIni: f32 = 20.0; + const DepthBufferValue: f32 = -1.0; + const VariableSampleCount: bool = false; + const MieRayPhase: bool = false; + + var SphereSolidAngle: f32 = 4.0 * PI; + var IsotropicPhase: f32 = 1.0 / SphereSolidAngle; + + + var sqrtSample: f32 = f32(SQRTSAMPLECOUNT); + var i: f32 = 0.5 + f32(ThreadId.z / SQRTSAMPLECOUNT); + var j: f32 = 0.5 + f32(ThreadId.z - u32(f32(ThreadId.z / SQRTSAMPLECOUNT) * f32(SQRTSAMPLECOUNT))); + { + var randA: f32 = i / sqrtSample; + var randB: f32 = j / sqrtSample; + var theta: f32 = 2.0 * PI * randA; + var phi: f32 = acos(1.0 - 2.0 * randB); + var cosPhi: f32 = cos(phi); + var sinPhi: f32 = sin(phi); + var cosTheta: f32 = cos(theta); + var sinTheta: f32 = sin(theta); + WorldDir.x = cosTheta * sinPhi; + WorldDir.y = sinTheta * sinPhi; + WorldDir.z = cosPhi; + var result: SingleScatteringResult = IntegrateScatteredLuminance(pixPos, WorldPos, WorldDir, sunDir, Atmosphere, ground, SampleCountIni, DepthBufferValue, VariableSampleCount, MieRayPhase, defaultTMaxMax, texSize); + + MultiScatAs1SharedMem[ThreadId.z] = result.MultiScatAs1 * SphereSolidAngle / (sqrtSample * sqrtSample); + LSharedMem[ThreadId.z] = result.L * SphereSolidAngle / (sqrtSample * sqrtSample); + } + + workgroupBarrier(); + + if ThreadId.z < 32 { + MultiScatAs1SharedMem[ThreadId.z] += MultiScatAs1SharedMem[ThreadId.z + 32]; + LSharedMem[ThreadId.z] += LSharedMem[ThreadId.z + 32]; + } + workgroupBarrier(); + + if ThreadId.z < 16 { + MultiScatAs1SharedMem[ThreadId.z] += MultiScatAs1SharedMem[ThreadId.z + 16]; + LSharedMem[ThreadId.z] += LSharedMem[ThreadId.z + 16]; + } + workgroupBarrier(); + + if ThreadId.z < 8 { + MultiScatAs1SharedMem[ThreadId.z] += MultiScatAs1SharedMem[ThreadId.z + 8]; + LSharedMem[ThreadId.z] += LSharedMem[ThreadId.z + 8]; + } + workgroupBarrier(); + if ThreadId.z < 4 { + MultiScatAs1SharedMem[ThreadId.z] += MultiScatAs1SharedMem[ThreadId.z + 4]; + LSharedMem[ThreadId.z] += LSharedMem[ThreadId.z + 4]; + } + workgroupBarrier(); + if ThreadId.z < 2 { + MultiScatAs1SharedMem[ThreadId.z] += MultiScatAs1SharedMem[ThreadId.z + 2]; + LSharedMem[ThreadId.z] += LSharedMem[ThreadId.z + 2]; + } + workgroupBarrier(); + if ThreadId.z < 1 { + MultiScatAs1SharedMem[ThreadId.z] += MultiScatAs1SharedMem[ThreadId.z + 1]; + LSharedMem[ThreadId.z] += LSharedMem[ThreadId.z + 1]; + } + workgroupBarrier(); + if ThreadId.z > 0 { + return; + } + + var MultiScatAs1: vec3 = MultiScatAs1SharedMem[0] * IsotropicPhase; + var InScatteredLuminance: vec3 = LSharedMem[0] * IsotropicPhase; + + var L: vec3 = vec3(0.0, 0.0, 0.0); + if MULTI_SCATTERING_POWER_SERIE == 0 { + var MultiScatAs1SQR: vec3 = MultiScatAs1 * MultiScatAs1; + L = InScatteredLuminance * (1.0 + MultiScatAs1 + MultiScatAs1SQR + MultiScatAs1 * MultiScatAs1SQR + MultiScatAs1SQR * MultiScatAs1SQR); + } else { + var r: vec3 = MultiScatAs1; + var SumOfAllMultiScatteringEventsContribution: vec3 = 1.0 / (1.0 - r); + L = InScatteredLuminance * SumOfAllMultiScatteringEventsContribution; + } + + var fragColor = vec4(MultipleScatteringFactor * L, 1.0); + var fragCoord = vec2(ThreadId.xy); + textureStore(outTexture, fragCoord, fragColor); +} diff --git a/packages/atmosphere/shader/RenderSkyRayMarching.wgsl b/packages/atmosphere/shader/RenderSkyRayMarching.wgsl new file mode 100644 index 00000000..ee3ad08e --- /dev/null +++ b/packages/atmosphere/shader/RenderSkyRayMarching.wgsl @@ -0,0 +1,75 @@ +#include 'AtmosphereEarth' +#include 'AtmosphericScatteringIntegration' +#include 'AtmosphereUniforms' +#include 'ColorUtil_frag' + +@group(0) @binding(0) var uniformBuffer: UniformData; +@group(0) @binding(1) var outTexture: texture_storage_2d; + +@group(0) @binding(auto) var multipleScatteringTexture: texture_2d; +@group(0) @binding(auto) var multipleScatteringTextureSampler: sampler; + +@group(0) @binding(auto) var transmittanceTexture: texture_2d; +@group(0) @binding(auto) var transmittanceTextureSampler: sampler; + +@group(0) @binding(auto) var cloudTextureSampler: sampler; +@group(0) @binding(auto) var cloudTexture: texture_2d; + +var uv01: vec2; +var fragCoord: vec2; +var texSize: vec2; + +var PI: f32 = 3.1415926535897932384626433832795; +var PI_2: f32 = 0.0; +var EPSILON: f32 = 0.0000001; +var IS_HDR_SKY = false; + +@compute @workgroup_size(8, 8, 1) +fn CsMain(@builtin(global_invocation_id) ThreadId: vec3) { + fragCoord = vec2(ThreadId.xy); + texSize = vec2(uniformBuffer.width, uniformBuffer.height); + uv01 = vec2(ThreadId.xy) / texSize; + uv01.y = 1.0 - uv01.y - EPSILON; + PI_2 = PI * 2.0; + textureStore(outTexture, fragCoord, mainImage(uv01, vec2(fragCoord))); +} + +fn mainImage(uv: vec2, pixPos: vec2) -> vec4 { + var coords = vec2(uv * vec2(textureDimensions(multipleScatteringTexture, 0))); + var sampleA = textureSampleLevel(multipleScatteringTexture, multipleScatteringTextureSampler, uv, 0).rgb; + + coords = vec2(uv * vec2(textureDimensions(transmittanceTexture, 0))); + var sampleB = textureSampleLevel(transmittanceTexture, transmittanceTextureSampler, uv, 0).rgb; + + // sample the cloud texture + coords = vec2(uv * vec2(textureDimensions(cloudTexture, 0))); + var sampleC = textureSampleLevel(cloudTexture, cloudTextureSampler, uv, 0).rgb; + + var Atmosphere: AtmosphereParameters = GetAtmosphereParameters(); + + var eyePosition = uniformBuffer.eyePos; + var sun = vec2(uniformBuffer.sunU, uniformBuffer.sunV); + var WorldDir: vec3 = ComputeSphereNormal(uv, 0.0, PI_2, 0.0, PI); + var SunDir: vec3 = ComputeSphereNormal(vec2(sun.x, sun.y), 0.0, PI_2, 0.0, PI); + var WorldPos = vec3(0, Atmosphere.BottomRadius + eyePosition/1000.0 + 0.01, 0); + + const ground = false; + const SampleCountIni = 30.0; + const DepthBufferValue = -1.0; + const VariableSampleCount = true; + const MieRayPhase = true; + var result: SingleScatteringResult = IntegrateScatteredLuminance(pixPos, WorldPos, WorldDir, SunDir, Atmosphere, ground, SampleCountIni, DepthBufferValue, VariableSampleCount, MieRayPhase, defaultTMaxMax, texSize); + + // for HDR lighting + var sky: vec3; + if (IS_HDR_SKY) { + sky = LinearToGammaSpace(result.L) * uniformBuffer.hdrExposure; + } else { + // for LDR lighting + sky = result.L; + sky = ACESToneMapping(sky.rgb, uniformBuffer.hdrExposure); + sky = pow(sky.rgb, vec3(1.0/1.2)); // gamma + } + + return vec4(sky, 1.0); +} \ No newline at end of file diff --git a/packages/atmosphere/shader/RenderTransmittanceLutPS.wgsl b/packages/atmosphere/shader/RenderTransmittanceLutPS.wgsl new file mode 100644 index 00000000..0ad8c1bf --- /dev/null +++ b/packages/atmosphere/shader/RenderTransmittanceLutPS.wgsl @@ -0,0 +1,76 @@ +#include 'AtmosphereEarth' +#include 'AtmosphericScatteringIntegration' +#include 'AtmosphereUniforms' + +@group(0) @binding(0) var uniformBuffer: UniformData; +@group(0) @binding(1) var outTexture: texture_storage_2d; +@group(0) @binding(auto) var transmittanceTexture: texture_2d; +@group(0) @binding(auto) var transmittanceTextureSampler: sampler; +@group(0) @binding(auto) var multipleScatteringTexture: texture_2d; +@group(0) @binding(auto) var multipleScatteringTextureSampler: sampler; +@group(0) @binding(auto) var cloudTextureSampler: sampler; +@group(0) @binding(auto) var cloudTexture: texture_2d; + +var uv01: vec2; +var fragCoord: vec2; +var texSizeF32: vec2; +var PI: f32 = 3.1415926535897932384626433832795; +var PI_2: f32 = 0.0; +var EPSILON:f32 = 0.0000001; + +@compute @workgroup_size(8 , 8 , 1) +fn CsMain(@builtin(workgroup_id) workgroup_id: vec3, @builtin(global_invocation_id) globalInvocation_id: vec3) { + fragCoord = vec2(globalInvocation_id.xy); + texSizeF32 = vec2(uniformBuffer.width, uniformBuffer.height); + uv01 = vec2(globalInvocation_id.xy) / texSizeF32; + uv01.y = 1.0 - uv01.y - EPSILON; + PI_2 = PI * 2.0; + textureStore(outTexture, fragCoord, RenderTransmittanceLutPS(vec2(fragCoord), uv01));//vec4(uv01, 0.0, 1.0)); +} + +struct UvToLutResult { + viewHeight: f32, + viewZenithCosAngle: f32, +}; + +fn UvToLutTransmittanceParams(Atmosphere: AtmosphereParameters, uv: vec2) -> UvToLutResult { + var result: UvToLutResult; + + var x_mu: f32 = uv.x; + var x_r: f32 = uv.y; + + var H: f32 = sqrt(Atmosphere.TopRadius * Atmosphere.TopRadius - Atmosphere.BottomRadius * Atmosphere.BottomRadius); + var rho: f32 = H * x_r; + result.viewHeight = sqrt(rho * rho + Atmosphere.BottomRadius * Atmosphere.BottomRadius); + + var d_min: f32 = Atmosphere.TopRadius - result.viewHeight; + var d_max: f32 = rho + H; + var d: f32 = d_min + x_mu * (d_max - d_min); + result.viewZenithCosAngle = (H * H - rho * rho - d * d) / (2.0 * result.viewHeight * d); + if d == 0.0 { + result.viewZenithCosAngle = 1.0; + } + result.viewZenithCosAngle = clamp(result.viewZenithCosAngle, -1.0, 1.0); + + return result; +} + +fn RenderTransmittanceLutPS(pixPos: vec2, uv: vec2) -> vec4 { + var Atmosphere: AtmosphereParameters = GetAtmosphereParameters(); + var transmittanceParams: UvToLutResult = UvToLutTransmittanceParams(Atmosphere, uv); + + var WorldPos: vec3 = vec3(0.0, 0.0, transmittanceParams.viewHeight); + var WorldDir: vec3 = vec3(0.0, sqrt(1.0 - transmittanceParams.viewZenithCosAngle * transmittanceParams.viewZenithCosAngle), transmittanceParams.viewZenithCosAngle); + + var ground = false; + var SampleCountIni = 40.0; // Can go a low as 10 sample but energy lost starts to be visible. + var DepthBufferValue = -1.0; + var VariableSampleCount = false; + var MieRayPhase = false; + + var scatteringResult: SingleScatteringResult = IntegrateScatteredLuminance(pixPos, WorldPos, WorldDir, getSunDirection(), Atmosphere, ground, SampleCountIni, DepthBufferValue, VariableSampleCount, MieRayPhase, defaultTMaxMax, texSizeF32); + var transmittance: vec3 = exp(-scatteringResult.OpticalDepth); + + // Optical depth to transmittance + return vec4(transmittance, 1.0); +} diff --git a/packages/atmosphere/shader/SkyViewLutPS.wgsl b/packages/atmosphere/shader/SkyViewLutPS.wgsl new file mode 100644 index 00000000..10228577 --- /dev/null +++ b/packages/atmosphere/shader/SkyViewLutPS.wgsl @@ -0,0 +1,121 @@ +#include 'AtmosphereEarth' +#include 'AtmosphericScatteringIntegration' +#include 'AtmosphereUniforms' + +@group(0) @binding(0) var uniformBuffer: UniformData; +@group(0) @binding(1) var outTexture: texture_storage_2d; +@group(0) @binding(auto) var transmittanceTexture: texture_2d; +@group(0) @binding(auto) var transmittanceTextureSampler: sampler; +@group(0) @binding(auto) var multipleScatteringTexture: texture_2d; +@group(0) @binding(auto) var multipleScatteringTextureSampler: sampler; +@group(0) @binding(auto) var cloudTextureSampler: sampler; +@group(0) @binding(auto) var cloudTexture: texture_2d; + +var uv01: vec2; +var fragCoord: vec2; +var texSize: vec2; +var PI: f32 = 3.1415926535897932384626433832795; +var PI_2: f32 = 0.0; +var EPSILON:f32 = 0.0000001; +var NONLINEARSKYVIEWLUT: bool = true; + +@compute @workgroup_size(8, 8, 1) +fn CsMain(@builtin(workgroup_id) workgroup_id: vec3, @builtin(global_invocation_id) globalInvocation_id: vec3) { + fragCoord = vec2(globalInvocation_id.xy); + texSize = vec2(uniformBuffer.width, uniformBuffer.height); + uv01 = vec2(globalInvocation_id.xy) / texSize; + uv01.y = 1.0 - uv01.y - EPSILON; + PI_2 = PI * 2.0; + textureStore(outTexture, fragCoord, SkyViewLutPS(vec2(fragCoord), uv01));//vec4(uv01, 0.0, 1.0)); +} + +fn UvToSkyViewLutParams(Atmosphere: AtmosphereParameters, viewHeight: f32, uv0: vec2) -> vec2 { + // Constrain uvs to valid sub texel range (avoid zenith derivative issue making LUT usage visible) + var uv = vec2(fromSubUvsToUnit(uv0.x, 192.0), fromSubUvsToUnit(uv0.y, 108.0)); + + var Vhorizon: f32 = sqrt(viewHeight * viewHeight - Atmosphere.BottomRadius * Atmosphere.BottomRadius); + var CosBeta: f32 = Vhorizon / viewHeight; // GroundToHorizonCos + var Beta: f32 = acos(CosBeta); + var ZenithHorizonAngle: f32 = PI - Beta; + var viewZenithCosAngle: f32; + + if uv.y < 0.5 { + var coord: f32 = 2.0 * uv.y; + coord = 1.0 - coord; + if NONLINEARSKYVIEWLUT { + coord = coord * coord; + } + coord = 1.0 - coord; + viewZenithCosAngle = cos(ZenithHorizonAngle * coord); + } else { + var coord: f32 = uv.y * 2.0 - 1.0; + if NONLINEARSKYVIEWLUT { + coord = coord * coord; + } + viewZenithCosAngle = cos(ZenithHorizonAngle + Beta * coord); + } + + var coord: f32 = uv.x; + coord = coord * coord; + var lightViewCosAngle: f32 = -(coord * 2.0 - 1.0); + + return vec2(viewZenithCosAngle, lightViewCosAngle); +} + + +fn SkyViewLutPS(pixPos: vec2, uv: vec2) -> vec4 { + var Atmosphere: AtmosphereParameters = GetAtmosphereParameters(); + + // var ClipSpace: vec3 = vec3((pixPos / vec2(192.0, 108.0)) * vec2(2.0, -2.0) - vec2(1.0, -1.0), 1.0); + // var HViewPos: vec4 = uniformBuffer.skyInvProjMat * vec4(ClipSpace, 1.0); + // var m = uniformBuffer.skyInvViewMat; + // var WorldDir: vec3 = normalize((mat3x3(m[0].xyz, m[1].xyz, m[2].xyz) * HViewPos.xyz) / HViewPos.w); + var WorldPos: vec3 = vec3(0, Atmosphere.BottomRadius + uniformBuffer.eyePos + 0.01, 0); + + var viewHeight: f32 = length(WorldPos); + + var lutParams = UvToSkyViewLutParams(Atmosphere, viewHeight, uv); + var viewZenithCosAngle: f32 = lutParams.x; + var lightViewCosAngle: f32 = lutParams.y; + + var sun = vec2(uniformBuffer.sunU, uniformBuffer.sunV); + var sun_direction: vec3 = ComputeSphereNormal(vec2(sun.x, sun.y), 0.0, PI_2, 0.0, PI); + + var SunDir: vec3; + { + var UpVector: vec3 = WorldPos / viewHeight; + var sunZenithCosAngle: f32 = dot(UpVector, sun_direction); + SunDir = normalize(vec3(sqrt(1.0 - sunZenithCosAngle * sunZenithCosAngle), 0.0, sunZenithCosAngle)); + } + + WorldPos = vec3(0.0, 0.0, viewHeight); + + var viewZenithSinAngle: f32 = sqrt(1.0 - viewZenithCosAngle * viewZenithCosAngle); + var WorldDir = vec3( + viewZenithSinAngle * lightViewCosAngle, + viewZenithSinAngle * sqrt(1.0 - lightViewCosAngle * lightViewCosAngle), + viewZenithCosAngle + ); + + // Move to top atmosphere + if !MoveToTopAtmosphere(&WorldPos, WorldDir, Atmosphere.TopRadius) { + // Ray is not intersecting the atmosphere + return vec4(0.0, 0.0, 0.0, 1.0); + } + + const ground: bool = false; + const SampleCountIni: f32 = 30.0; + const DepthBufferValue: f32 = -1.0; + const VariableSampleCount: bool = true; + const MieRayPhase: bool = true; + var texSize = vec2(uniformBuffer.width, uniformBuffer.height); + var ss: SingleScatteringResult = IntegrateScatteredLuminance( + pixPos, WorldPos, WorldDir, SunDir, Atmosphere, + ground, SampleCountIni, DepthBufferValue, VariableSampleCount, + MieRayPhase, defaultTMaxMax, texSize + ); + + var L: vec3 = ss.L; + + return vec4(L, 1.0); +} diff --git a/packages/atmosphere/textures/AtmosphericScatteringSky.ts b/packages/atmosphere/textures/AtmosphericScatteringSky.ts new file mode 100644 index 00000000..995c85a4 --- /dev/null +++ b/packages/atmosphere/textures/AtmosphericScatteringSky.ts @@ -0,0 +1,252 @@ +import { AtmosphericScatteringSky_shader } from '../shader/AtmosphericScatteringSky_shader'; +import { + Engine3D, + Vector3, + Color, + HDRTextureCube, + Texture, + ComputeShader, + UniformGPUBuffer, + VirtualTexture, + GPUTextureFormat, + GPUContext, + GPUAddressMode +} from "@orillusion/core"; + +/** + * AtmosphericScattering Sky Setting + * @group Texture + */ +export class AtmosphericScatteringSkySetting { + public sunRadius: number = 500.0; + public sunRadiance: number = 11.0; + public mieG: number = 0.76; + public mieHeight: number = 1200; + public eyePos: number = 1500; + public sunX: number = 0.71; + public sunY: number = 0.56; + public sunBrightness: number = 1.0; + public displaySun: boolean = true; + public enableClouds: boolean = true; + public showV1: boolean = false; + public defaultTextureCubeSize: number = 512; + public defaultTexture2DSize: number = 1024; + public skyColor: Color = new Color(1, 1, 1, 1); + public hdrExposure: number = 2; +} + +/** + * Atmospheric Scattering Sky Texture + * @group Texture + */ +export class AtmosphericScatteringSky extends HDRTextureCube { + private _transmittanceLut: TransmittanceTexture2D; + private _multipleScatteringLut: MultipleScatteringTexture2D; + private _skyTexture: SkyTexture2D; + private _skyViewLut: SkyViewTexture2D; + private _cloudNoiseTexture: CloudNoiseTexture2D; + private _cubeSize: number; + public readonly setting: AtmosphericScatteringSkySetting; + private _internalTexture: AtmosphericTexture; + + /** + * @constructor + * @param setting AtmosphericScatteringSkySetting + * @returns + */ + constructor(setting: AtmosphericScatteringSkySetting) { + super(); + this.setting = setting; + this.isHDRTexture = true; + this._internalTexture = new AtmosphericTexture(setting.defaultTexture2DSize, setting.defaultTexture2DSize * 0.5); + this._internalTexture.updateUniforms(this.setting); + this._internalTexture.update(); + this._internalTexture.isHDRTexture = true; + this._cubeSize = setting.defaultTextureCubeSize; + this._cloudNoiseTexture = new CloudNoiseTexture2D(64, 64); + this._cloudNoiseTexture.updateUniforms(this.setting); + this._cloudNoiseTexture.update(); + this._transmittanceLut = new TransmittanceTexture2D(256, 64); + this._transmittanceLut.updateUniforms(this.setting); + this._transmittanceLut.update(); + this._multipleScatteringLut = new MultipleScatteringTexture2D(32, 32); + this._multipleScatteringLut.updateUniforms(this.setting); + this._multipleScatteringLut.updateTransmittance(this._transmittanceLut, this._cloudNoiseTexture); + this._multipleScatteringLut.update(); + this._skyViewLut = new SkyViewTexture2D(192, 108); + this._skyViewLut.updateUniforms(this.setting); + this._skyViewLut.updateTextures(this._transmittanceLut, this._multipleScatteringLut, this._cloudNoiseTexture); + this._skyViewLut.update(); + this._skyTexture = new SkyTexture2D(setting.defaultTexture2DSize, setting.defaultTexture2DSize * 0.5); + this._skyTexture.updateUniforms(this.setting); + this._skyTexture.updateTextures(this._transmittanceLut, this._multipleScatteringLut, this._skyViewLut, this._cloudNoiseTexture); + this._skyTexture.isHDRTexture = true; + this._skyTexture.update(); + this.createFromTexture(this._cubeSize, this._skyTexture); + return this; + } + + public get texture2D(): Texture { + return this._skyTexture; + } + + /** + * @internal + * @returns + */ + public apply(view?: any): this { + if (this.setting.showV1) { + this._internalTexture.updateUniforms(this.setting); + this._internalTexture.update(); + this._faceData.uploadErpTexture(this._internalTexture); + } else { + this._skyTexture.updateUniforms(this.setting); + this._skyTexture.updateTextures(this._transmittanceLut, this._multipleScatteringLut, this._skyViewLut, this._cloudNoiseTexture); + this._skyTexture.update(); + this._faceData.uploadErpTexture(this._skyTexture); + } + return this; + } +} + +/** + * @internal + */ +class AtmosphericTexture extends VirtualTexture { + protected _computeShader: ComputeShader; + private _uniformBuffer: UniformGPUBuffer; + private _workerSize: Vector3 = new Vector3(8, 8, 1); + + set workerSize(value: Vector3) { + this._workerSize = value; + } + + get workerSize(): Vector3 { + return this._workerSize; + } + + constructor(width: number, height: number, numberLayer: number = 1) { + super(width, height, GPUTextureFormat.rgba16float, false, GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING, numberLayer); + this._computeShader = new ComputeShader(AtmosphericScatteringSky_shader.cs); + this._computeShader.entryPoint = 'CsMain'; + this.magFilter = 'linear'; + this.minFilter = 'linear'; + this.initCompute(width, height, numberLayer); + } + + protected initCompute(w: number, h: number, d: number = 1): void { + this._uniformBuffer = new UniformGPUBuffer(16 * 4); + this._uniformBuffer.apply(); + + this._computeShader.setUniformBuffer('uniformBuffer', this._uniformBuffer); + this._computeShader.setStorageTexture(`outTexture`, this); + this._computeShader.setSamplerTexture(`transmittanceTexture`, Engine3D.res.blackTexture); + this._computeShader.setSamplerTexture(`multipleScatteringTexture`, Engine3D.res.blackTexture); + this._computeShader.setSamplerTexture(`cloudTexture`, Engine3D.res.blackTexture); + this._computeShader.workerSizeX = w / this._workerSize.x; + this._computeShader.workerSizeY = h / this._workerSize.y; + this._computeShader.workerSizeZ = this._workerSize.z; + } + + public updateUniforms(setting: AtmosphericScatteringSkySetting): this { + this._uniformBuffer.setFloat('width', this.width); + this._uniformBuffer.setFloat('height', this.height); + this._uniformBuffer.setFloat('sunU', setting.sunX); + this._uniformBuffer.setFloat('sunV', setting.sunY); + this._uniformBuffer.setFloat('eyePos', setting.eyePos); + this._uniformBuffer.setFloat('sunRadius', setting.sunRadius); + this._uniformBuffer.setFloat('sunRadiance', setting.sunRadiance); + this._uniformBuffer.setFloat('mieG', setting.mieG); + this._uniformBuffer.setFloat('mieHeight', setting.mieHeight); + this._uniformBuffer.setFloat('sunBrightness', setting.sunBrightness); + this._uniformBuffer.setFloat('displaySun', setting.displaySun ? 1 : 0); + this._uniformBuffer.setFloat('enableClouds', setting.enableClouds ? 1 : 0); + this._uniformBuffer.setFloat('hdrExposure', setting.hdrExposure); + this._uniformBuffer.setColor('skyColor', setting.skyColor); + this._uniformBuffer.apply(); + return this; + } + + public update(): this { + let command = GPUContext.beginCommandEncoder(); + GPUContext.computeCommand(command, [this._computeShader]); + GPUContext.endCommandEncoder(command); + return this; + } +} + +/** + * @internal + */ +class TransmittanceTexture2D extends AtmosphericTexture { + constructor(width: number, height: number) { + super(width, height); + this._computeShader = new ComputeShader(AtmosphericScatteringSky_shader.transmittance_cs); + this.initCompute(width, height); + } +} + +/** + * @internal + */ +class MultipleScatteringTexture2D extends AtmosphericTexture { + constructor(width: number, height: number) { + super(width, height); + this._computeShader = new ComputeShader(AtmosphericScatteringSky_shader.multiscatter_cs); + this.workerSize.set(1, 1, 64); + this.initCompute(width, height); + } + + public updateTransmittance(transmittanceTexture: TransmittanceTexture2D, cloudTexture: CloudNoiseTexture2D) { + this._computeShader.setSamplerTexture(`transmittanceTexture`, transmittanceTexture); + this._computeShader.setSamplerTexture(`cloudTexture`, cloudTexture); + } +} + +/** + * @internal + */ +class SkyViewTexture2D extends AtmosphericTexture { + constructor(width: number, height: number) { + super(width, height); + this._computeShader = new ComputeShader(AtmosphericScatteringSky_shader.skyview_cs); + this.initCompute(width, height); + } + + public updateTextures(transmittanceTexture: TransmittanceTexture2D, multipleScatteringTexture: MultipleScatteringTexture2D, cloudTexture: CloudNoiseTexture2D) { + this._computeShader.setSamplerTexture(`transmittanceTexture`, transmittanceTexture); + this._computeShader.setSamplerTexture(`multipleScatteringTexture`, multipleScatteringTexture); + this._computeShader.setSamplerTexture(`cloudTexture`, cloudTexture); + } +} + +/** + * @internal + */ +class SkyTexture2D extends AtmosphericTexture { + constructor(width: number, height: number) { + super(width, height); + this._computeShader = new ComputeShader(AtmosphericScatteringSky_shader.raymarch_cs); + this.initCompute(width, height); + } + + public updateTextures(transmittanceTexture: TransmittanceTexture2D, multipleScatteringTexture: MultipleScatteringTexture2D, skyViewTexture: SkyViewTexture2D, cloudTexture: CloudNoiseTexture2D) { + this._computeShader.setSamplerTexture(`transmittanceTexture`, transmittanceTexture); + this._computeShader.setSamplerTexture(`multipleScatteringTexture`, multipleScatteringTexture); + this._computeShader.setSamplerTexture(`skyTexture`, skyViewTexture); + this._computeShader.setSamplerTexture(`cloudTexture`, cloudTexture); + } +} + +/** + * @internal + */ +class CloudNoiseTexture2D extends AtmosphericTexture { + constructor(width: number, height: number) { + super(width, height); + this._computeShader = new ComputeShader(AtmosphericScatteringSky_shader.cloud_cs); + this.addressModeU = GPUAddressMode.repeat; + this.addressModeV = GPUAddressMode.repeat; + this.initCompute(width, height); + } +} diff --git a/packages/atmosphere/tsconfig.json b/packages/atmosphere/tsconfig.json new file mode 100644 index 00000000..99cc83e4 --- /dev/null +++ b/packages/atmosphere/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "sourceMap": false, + "resolveJsonModule": true, + "esModuleInterop": true, + "types": ["vite/client", "@webgpu/types"], + // for build + // "noEmit": true, + "declaration": true, + "declarationDir": "dist", + "outDir": "dist", + "skipLibCheck": true, + "noImplicitAny": false, + "noUnusedLocals": false, + "noUnusedParameters":false, + "noImplicitReturns": false, + "strictPropertyInitialization": false, + "strictNullChecks": false, + "strict": false, + "paths": { + "@orillusion/core": ["../../src"], + "@orillusion/*": ["../*"] + } + } +} \ No newline at end of file diff --git a/packages/atmosphere/vite.config.js b/packages/atmosphere/vite.config.js new file mode 100644 index 00000000..4b1f0712 --- /dev/null +++ b/packages/atmosphere/vite.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +const path = require('path') +export default defineConfig({ + resolve: { + alias: { + '@orillusion/core': path.resolve(__dirname, '../../src'), + '@orillusion': path.resolve(__dirname, '../') + } + }, + build: { + target: 'esnext', + lib: { + entry: path.resolve('index.ts'), + name: 'Atmosphere', + fileName: (format) => `atmosphere.${format}.js` + }, + rollupOptions: { + external: ['@orillusion/core'], + output: { + globals: { + '@orillusion/core': 'Orillusion' + } + } + } + } +}) \ No newline at end of file diff --git a/samples/ext/Sample_AtmosphericSky.ts b/samples/ext/Sample_AtmosphericSky.ts new file mode 100644 index 00000000..19d7c30e --- /dev/null +++ b/samples/ext/Sample_AtmosphericSky.ts @@ -0,0 +1,130 @@ +import { GUIHelp } from "@orillusion/debug/GUIHelp"; +import { createExampleScene } from "@samples/utils/ExampleScene"; +import { Engine3D, GPUCullMode, MeshRenderer, Object3D, PlaneGeometry, Scene3D, UnLitMaterial, Vector3 } from "@orillusion/core"; +import { AtmosphericComponent } from "@orillusion/atmosphere"; + +// sample of AtmosphericSky +class Sample_AtmosphericSky { + async run() { + // init engine + await Engine3D.init({}); + // init scene + let example = createExampleScene(); + let scene: Scene3D = example.scene; + let camera = example.camera; + camera.fov = 90; + // start renderer + Engine3D.startRenderView(scene.view); + // add atmospheric sky + let sky = scene.addComponent(AtmosphericComponent); + sky.sunX = 0.25; + let y = 100; + const settings = { + displayTextures: true, + } + let objs: Object3D[] = []; + { + let texture = sky['_atmosphericScatteringSky']['_transmittanceLut']; + let ulitMaterial = new UnLitMaterial(); + ulitMaterial.baseMap = texture; + ulitMaterial.cullMode = GPUCullMode.none; + let obj = new Object3D(); + let r = obj.addComponent(MeshRenderer); + r.material = ulitMaterial; + r.geometry = new PlaneGeometry(50, 25, 1, 1, Vector3.Z_AXIS); + obj.y = y; + y -= 25; + scene.addChild(obj); + objs.push(obj); + } + { + let texture = sky['_atmosphericScatteringSky']['_multipleScatteringLut']; + let ulitMaterial = new UnLitMaterial(); + ulitMaterial.baseMap = texture; + ulitMaterial.cullMode = GPUCullMode.none; + let obj = new Object3D(); + let r = obj.addComponent(MeshRenderer); + r.material = ulitMaterial; + r.geometry = new PlaneGeometry(25, 25, 1, 1, Vector3.Z_AXIS); + obj.y = y; + y -= 25; + scene.addChild(obj); + objs.push(obj); + } + { + let texture = sky['_atmosphericScatteringSky']['_skyViewLut']; + let ulitMaterial = new UnLitMaterial(); + ulitMaterial.baseMap = texture; + ulitMaterial.cullMode = GPUCullMode.none; + let obj = new Object3D(); + let r = obj.addComponent(MeshRenderer); + r.material = ulitMaterial; + r.geometry = new PlaneGeometry(50, 25, 1, 1, Vector3.Z_AXIS); + obj.y = y; + y -= 25; + scene.addChild(obj); + objs.push(obj); + } + { + // _cloudNoiseTexture + let texture = sky['_atmosphericScatteringSky']['_cloudNoiseTexture']; + let ulitMaterial = new UnLitMaterial(); + ulitMaterial.baseMap = texture; + ulitMaterial.cullMode = GPUCullMode.none; + let obj = new Object3D(); + let r = obj.addComponent(MeshRenderer); + r.material = ulitMaterial; + r.geometry = new PlaneGeometry(25, 25, 1, 1, Vector3.Z_AXIS); + obj.y = y; + y -= 25; + scene.addChild(obj); + objs.push(obj); + } + + { + let texture = sky['_atmosphericScatteringSky']['_skyTexture']; + let ulitMaterial = new UnLitMaterial(); + ulitMaterial.baseMap = texture; + ulitMaterial.cullMode = GPUCullMode.none; + let obj = new Object3D(); + let r = obj.addComponent(MeshRenderer); + r.material = ulitMaterial; + r.geometry = new PlaneGeometry(50, 25, 1, 1, Vector3.Z_AXIS); + obj.y = y; + y -= 25; + scene.addChild(obj); + objs.push(obj); + } + + + // gui + GUIHelp.init(); + const name = 'AtmosphericSky'; + GUIHelp.addFolder(name); + GUIHelp.add(sky, 'sunX', 0, 1, 0.01); + GUIHelp.add(sky, 'sunY', 0.4, 1.6, 0.01); + GUIHelp.add(sky, 'eyePos', 0, 7000, 1); + GUIHelp.add(sky, 'sunRadius', 0, 1000, 0.01); + GUIHelp.add(sky, 'sunRadiance', 0, 100, 0.01); + GUIHelp.add(sky, 'sunBrightness', 0, 10, 0.01); + GUIHelp.add(sky, 'hdrExposure', 0, 5, 0.01); + GUIHelp.add(sky, 'displaySun', 0, 1, 0.01); + GUIHelp.add(sky, 'enable'); + // bool whether to display the textures + GUIHelp.add(settings, 'displayTextures').onChange((v) => { + objs.forEach(obj => { + obj.transform.enable = v; + }); + }); + GUIHelp.open(); + GUIHelp.endFolder(); + + // add folder for camera + GUIHelp.addFolder('Camera'); + GUIHelp.add(camera, 'fov', 1, 180, 1); + GUIHelp.open(); + GUIHelp.endFolder(); + } +} + +new Sample_AtmosphericSky().run(); \ No newline at end of file