UNPKG

webgpu-sky-atmosphere

Version:

A WebGPU implementation of Hillaire's atmosphere model. Renders the sky as a post-process.

407 lines (382 loc) 212 kB
/* webgpu-sky-atmosphere@1.2.0, license MIT */ /* * Copyright (c) 2024 Lukas Herzberger * Copyright (c) 2020 Epic Games, Inc. * SPDX-License-Identifier: MIT */ /** * Create a default atmosphere that corresponds to earth's atmosphere. * * @param center The center of the atmosphere. Defaults to `upDirection * -{@link Atmosphere.bottomRadius}` (`upDirection` depends on `yUp`). * @param yUp If true, the up direction for the default center will be `[0, 1, 0]`, otherwise `[0, 0, 1]` will be used. * @param useHenyeyGreenstein If this is true, {@link Mie.phaseParam} will be set to a value suitable for the Cornette-Shanks approximation (`0.8`), otherwise it is set to `3.4` for use with the Henyey-Greenstein + Draine approximation. * * @returns Atmosphere parameters corresponding to earth's atmosphere. */ function makeEarthAtmosphere(center, yUp = true, useHenyeyGreenstein = true) { const rayleighScaleHeight = 8.0; const mieScaleHeight = 1.2; const bottomRadius = 6360.0; return { center: center ?? [0.0, yUp ? -bottomRadius : 0.0, yUp ? 0.0 : -bottomRadius], bottomRadius, height: 100.0, rayleigh: { densityExpScale: -1.0 / rayleighScaleHeight, scattering: [0.005802, 0.013558, 0.033100], }, mie: { densityExpScale: -1.0 / mieScaleHeight, scattering: [0.003996, 0.003996, 0.003996], extinction: [0.004440, 0.004440, 0.004440], phaseParam: useHenyeyGreenstein ? 0.8 : 3.4, }, absorption: { layer0: { height: 25.0, constantTerm: -2.0 / 3.0, linearTerm: 1.0 / 15.0, }, layer1: { constantTerm: 8.0 / 3.0, linearTerm: -1.0 / 15.0, }, extinction: [0.000650, 0.001881, 0.000085], }, groundAlbedo: [0.4, 0.4, 0.4], multipleScatteringFactor: 1.0, }; } /* * Copyright (c) 2024 Lukas Herzberger * SPDX-License-Identifier: MIT */ /** * A helper class for textures. */ class LookUpTable { texture; view; constructor(texture) { this.texture = texture; this.view = texture.createView({ label: texture.label, }); } } /** * A helper class for compute passes */ class ComputePass { pipeline; bindGroups; dispatchDimensions; constructor(pipeline, bindGroups, dispatchDimensions) { this.pipeline = pipeline; this.bindGroups = bindGroups; this.dispatchDimensions = dispatchDimensions; } encode(computePassEncoder, resetBindGroups = false) { computePassEncoder.setPipeline(this.pipeline); for (let i = 0; i < this.bindGroups.length; ++i) { computePassEncoder.setBindGroup(i, this.bindGroups[i]); } computePassEncoder.dispatchWorkgroups(...this.dispatchDimensions); if (resetBindGroups) { for (let i = 0; i < this.bindGroups.length; ++i) { computePassEncoder.setBindGroup(i, null); } } } replaceBindGroup(index, bindGroup) { this.bindGroups[index] = bindGroup; } replaceDispatchDimensions(dispatchDimensions) { this.dispatchDimensions[0] = dispatchDimensions[0]; this.dispatchDimensions[1] = dispatchDimensions[1]; this.dispatchDimensions[2] = dispatchDimensions[2]; } } /** * A helper class for render passes */ class RenderPass { pipeline; bindGroups; constructor(pipeline, bindGroups) { this.pipeline = pipeline; this.bindGroups = bindGroups; } encode(passEncoder, resetBindGroups = false) { passEncoder.setPipeline(this.pipeline); for (let i = 0; i < this.bindGroups.length; ++i) { passEncoder.setBindGroup(i, this.bindGroups[i]); } passEncoder.draw(3); if (resetBindGroups) { for (let i = 0; i < this.bindGroups.length; ++i) { passEncoder.setBindGroup(i, null); } } } replaceBindGroup(index, bindGroup) { this.bindGroups[index] = bindGroup; } } function makeLutSampler(device) { return device.createSampler({ label: 'LUT sampler', addressModeU: 'clamp-to-edge', addressModeV: 'clamp-to-edge', addressModeW: 'clamp-to-edge', minFilter: 'linear', magFilter: 'linear', mipmapFilter: 'linear', lodMinClamp: 0, lodMaxClamp: 32, maxAnisotropy: 1, }); } /* * Copyright (c) 2024-2025 Lukas Herzberger * SPDX-License-Identifier: MIT */ const DEFAULT_TRANSMITTANCE_LUT_SIZE = [256, 64]; const DEFAULT_MULTISCATTERING_LUT_SIZE = 32; const DEFAULT_SKY_VIEW_LUT_SIZE = [192, 108]; const DEFAULT_AERIAL_PERSPECTIVE_LUT_SIZE = [32, 32, 32]; const TRANSMITTANCE_LUT_FORMAT = 'rgba16float'; const MULTI_SCATTERING_LUT_FORMAT = TRANSMITTANCE_LUT_FORMAT; const SKY_VIEW_LUT_FORMAT = TRANSMITTANCE_LUT_FORMAT; const AERIAL_PERSPECTIVE_LUT_FORMAT = TRANSMITTANCE_LUT_FORMAT; const ATMOSPHERE_BUFFER_SIZE = 128; const UNIFORMS_BUFFER_SIZE = 224; class SkyAtmosphereResources { /** * A name that is propagated to the WebGPU resources. */ label; /** * The WebGPU device the resources are allocated from. */ device; /** * A uniform buffer of size {@link ATMOSPHERE_BUFFER_SIZE} storing the {@link Atmosphere}'s parameters. */ atmosphereBuffer; /** * A uniform buffer of size {@link UNIFORMS_BUFFER_SIZE} storing parameters set through {@link Uniforms}. * * If custom uniform buffers are used, this is undefined (see {@link CustomUniformsSourceConfig}). */ uniformsBuffer; /** * A linear sampler used to sample the look up tables. */ lutSampler; /** * The transmittance look up table. * Stores the medium transmittance toward the sun. * * Parameterized by the view / zenith angle in x and the altitude in y. */ transmittanceLut; /** * The multiple scattering look up table. * Stores multiple scattering contribution. * * Paramterized by the sun / zenith angle in x (range: [π, 0]) and the altitude in y (range: [0, top], where top is the height of the atmosphere). */ multiScatteringLut; /** * The sky view lookup table. * Stores the distant sky around the camera with respect to its altitude within the atmosphere. * * The lookup table is parameterized by a longitude mapping along the X-axis and a latitude mapping along the Y-axis. * The latitude range is always [-π/2, π/2], where 0 represents the horizon. * There are two supported longitude mappings: * - **Default (Sun-centric)**: Longitude is mapped with respect to the sun direction, assuming symmetric contributions. * This mapping uses a range of [0, π], where 0 is directly toward the sun and π is directly opposite. * This mode is optimized for scenes with a single dominant light source. * - **Uniform**: Longitude is uniformly mapped around the zenith with a full range of [0, 2π]. * This mode supports multiple light sources but comes at the cost of angular resolution. * * To enable the uniform longitude mapping, {@link SkyViewLutConfig.uniformParameterizationConfig} must be defined. * Note that this mapping assumes that light and view directions are given in a right-handed Y-up coordinate system. This is configured * by {@link SkyViewUniformParameterizationConfig.isYUp} and {@link SkyViewUniformParameterizationConfig.isRightHanded}. */ skyViewLut; /** * The aerial perspective look up table. * Stores the aerial perspective in a volume fit to the view frustum. * * Parameterized by x and y corresponding to the image plane and z being the view depth (range: [0, {@link AerialPerspectiveLutConfig.size}[2] * {@link AerialPerspectiveLutConfig.distancePerSlice}]). */ aerialPerspectiveLut; /** * {@link Atmosphere} parameters. * * Set using {@link updateAtmosphere}. * * @see {@link updateAtmosphere} */ #atmosphere; constructor(device, config, lutSampler) { this.label = config.label ?? 'atmosphere'; this.device = device; this.#atmosphere = config.atmosphere ?? makeEarthAtmosphere(); this.atmosphereBuffer = device.createBuffer({ label: `atmosphere buffer [${this.label}]`, size: ATMOSPHERE_BUFFER_SIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); this.updateAtmosphere(this.#atmosphere); if (config.customUniformsSource) { this.uniformsBuffer = undefined; } else { this.uniformsBuffer = device.createBuffer({ label: `config buffer [${this.label}]`, size: UNIFORMS_BUFFER_SIZE, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); } this.lutSampler = lutSampler || makeLutSampler(device); this.transmittanceLut = new LookUpTable(device.createTexture({ label: `transmittance LUT [${this.label}]`, size: config.lookUpTables?.transmittanceLut?.size ?? DEFAULT_TRANSMITTANCE_LUT_SIZE, format: config.lookUpTables?.transmittanceLut?.format ?? TRANSMITTANCE_LUT_FORMAT, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, })); this.multiScatteringLut = new LookUpTable(device.createTexture({ label: `multi scattering LUT [${this.label}]`, size: config.lookUpTables?.multiScatteringLut?.size ?? [DEFAULT_MULTISCATTERING_LUT_SIZE, DEFAULT_MULTISCATTERING_LUT_SIZE], format: config.lookUpTables?.multiScatteringLut?.format ?? MULTI_SCATTERING_LUT_FORMAT, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, })); this.skyViewLut = new LookUpTable(device.createTexture({ label: `sky view LUT [${this.label}]`, size: config.lookUpTables?.skyViewLut?.size ?? DEFAULT_SKY_VIEW_LUT_SIZE, format: config.lookUpTables?.skyViewLut?.format ?? SKY_VIEW_LUT_FORMAT, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, })); this.aerialPerspectiveLut = new LookUpTable(device.createTexture({ label: `aerial perspective LUT [${this.label}]`, size: config.lookUpTables?.aerialPerspectiveLut?.size ?? DEFAULT_AERIAL_PERSPECTIVE_LUT_SIZE, format: config.lookUpTables?.aerialPerspectiveLut?.format ?? AERIAL_PERSPECTIVE_LUT_FORMAT, dimension: '3d', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING, })); } get atmosphere() { return this.#atmosphere; } /** * Updates the {@link SkyAtmosphereResources.atmosphereBuffer} using a given {@link Atmosphere}. * * Overwrites this instance's internal {@link Atmosphere} parameters. * * @param atmosphere the {@link Atmosphere} to write to the {@link atmosphereBuffer}. * @see atmosphereToFloatArray Internally call {@link atmosphereToFloatArray} to convert the {@link Atmosphere} to a `Float32Array`. */ updateAtmosphere(atmosphere) { this.#atmosphere = atmosphere; this.device.queue.writeBuffer(this.atmosphereBuffer, 0, atmosphereToFloatArray(this.#atmosphere)); } /** * Updates the {@link SkyAtmosphereResources.uniformsBuffer} using a given {@link Uniforms}. * @param uniforms the {@link Uniforms} to write to the {@link atmosphereBuffer}. * @see uniformsToFloatArray Internally call {@link uniformsToFloatArray} to convert the {@link Uniforms} to a `Float32Array`. */ updateUniforms(uniforms) { if (this.uniformsBuffer) { this.device.queue.writeBuffer(this.uniformsBuffer, 0, uniformsToFloatArray(uniforms)); } } } /** * Converts an {@link Atmosphere} to a tightly packed `Float32Array` of size {@link ATMOSPHERE_BUFFER_SIZE}. * @param atmosphere the {@link Atmosphere} to convert. * @returns a `Float32Array` containing the {@link Atmosphere} parameters. */ function atmosphereToFloatArray(atmosphere) { return new Float32Array([ atmosphere.rayleigh.scattering[0], atmosphere.rayleigh.scattering[1], atmosphere.rayleigh.scattering[2], atmosphere.rayleigh.densityExpScale, atmosphere.mie.scattering[0], atmosphere.mie.scattering[1], atmosphere.mie.scattering[2], atmosphere.mie.densityExpScale, atmosphere.mie.extinction[0], atmosphere.mie.extinction[1], atmosphere.mie.extinction[2], atmosphere.mie.phaseParam, Math.max(atmosphere.mie.extinction[0] - atmosphere.mie.scattering[0], 0.0), Math.max(atmosphere.mie.extinction[1] - atmosphere.mie.scattering[1], 0.0), Math.max(atmosphere.mie.extinction[2] - atmosphere.mie.scattering[2], 0.0), atmosphere.absorption.layer0.height, atmosphere.absorption.layer0.constantTerm, atmosphere.absorption.layer0.linearTerm, atmosphere.absorption.layer1.constantTerm, atmosphere.absorption.layer1.linearTerm, atmosphere.absorption.extinction[0], atmosphere.absorption.extinction[1], atmosphere.absorption.extinction[2], atmosphere.bottomRadius, atmosphere.groundAlbedo[0], atmosphere.groundAlbedo[1], atmosphere.groundAlbedo[2], atmosphere.bottomRadius + Math.max(atmosphere.height, 0.0), ...atmosphere.center, atmosphere.multipleScatteringFactor, ]); } /** * Converts an {@link Uniforms} to a tightly packed `Float32Array` of size {@link UNIFORMS_BUFFER_SIZE}. * @param uniforms the {@link Uniforms} to convert. * @returns a `Float32Array` containing the {@link Uniforms} parameters. */ function uniformsToFloatArray(uniforms) { return new Float32Array([ ...uniforms.camera.inverseProjection, ...uniforms.camera.inverseView, ...uniforms.camera.position, uniforms.frameId ?? 0.0, ...uniforms.screenResolution, uniforms.rayMarchMinSPP ?? 14.0, uniforms.rayMarchMaxSPP ?? 30.0, ...(uniforms.sun.illuminance ?? [1.0, 1.0, 1.0]), uniforms.sun.diskAngularDiameter ?? (0.545 * (Math.PI / 180.0)), ...uniforms.sun.direction, uniforms.sun.diskLuminanceScale ?? 1.0, ...(uniforms.moon?.illuminance ?? [1.0, 1.0, 1.0]), uniforms.moon?.diskAngularDiameter ?? (0.568 * Math.PI / 180.0), ...(uniforms.moon?.direction ?? uniforms.sun.direction.map(d => d * -1)), uniforms.moon?.diskLuminanceScale ?? 1.0, ]); } var aerialPerspectiveWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\noverride AP_SLICE_COUNT: f32 = 32.0;\noverride AP_DISTANCE_PER_SLICE: f32 = 4.0;\n\noverride AP_INV_DISTANCE_PER_SLICE: f32 = 1.0 / AP_DISTANCE_PER_SLICE;\n\nfn aerial_perspective_depth_to_slice(depth: f32) -> f32 {\n\treturn depth * AP_INV_DISTANCE_PER_SLICE;\n}\nfn aerial_perspective_slice_to_depth(slice: f32) -> f32 {\n\treturn slice * AP_DISTANCE_PER_SLICE;\n}\n"; var blendWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\nfn blend(pix: vec2<u32>, src: vec4<f32>) {\n\tlet dst = textureLoad(backbuffer, pix, 0);\n\t// blend op: src*1 + dst * (1.0 - srcA)\n\t// alpha blend op: src * 0 + dst * 1\n\tlet rgb = src.rgb + dst.rgb * (1.0 - saturate(src.a));\n\tlet a = dst.a;\n\ttextureStore(render_target, pix, vec4<f32>(rgb, a));\n}\n\nfn dual_source_blend(pix: vec2<u32>, src0: vec4<f32>, src1: vec4<f32>) {\n\tlet dst = textureLoad(backbuffer, pix, 0);\n\t// blend op: src0 * 1 + dst * src1\n\t// alpha blend op: src * 0 + dst * 1\n\tlet rgb = src0.rgb + dst.rgb * src1.rgb;\n\tlet a = dst.a;\n\ttextureStore(render_target, pix, vec4<f32>(rgb, a));\n}\n"; var constantsWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\nconst pi: f32 = radians(180.0);\nconst tau: f32 = pi * 2.0;\nconst golden_ratio: f32 = (1.0 + sqrt(5.0)) / 2.0;\n\nconst u32_max: f32 = 4294967296.0;\n\nconst sphere_solid_angle: f32 = 4.0 * pi;\n\nconst t_max_max: f32 = 9000000.0;\nconst planet_radius_offset: f32 = 0.01;\n\n"; var customUniformsWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\nfn get_uniforms() -> Uniforms {\n\tUniforms uniforms;\n\tuniforms.inverse_projection = get_inverse_projection();\n\tuniforms.inverse_view = get_inverse_view();\n\tuniforms.camera_world_position = get_camera_world_position();\n\tuniforms.frame_id = get_frame_id();\n\tuniforms.screen_resolution = get_screen_resolution();\n\tuniforms.ray_march_min_spp = get_ray_march_min_spp();\n\tuniforms.ray_march_max_spp = get_ray_march_max_spp();\n\tuniforms.sun.illuminance = get_sun_illuminance();\n\tuniforms.sun.direction = get_sun_direction();\n\tuniforms.sun.disk_diameter = get_sun_disk_diameter();\n\tuniforms.sun.disk_luminance_scale = get_sun_disk_luminance_scale();\n\tuniforms.moon.illuminance = get_moon_illuminance();\n\tuniforms.moon.direction = get_moon_direction();\n\tuniforms.moon.disk_diameter = get_moon_disk_diameter();\n\tuniforms.moon.disk_luminance_scale = get_moon_disk_luminance_scale();\n\treturn uniforms;\n}\n"; var coordinateSystemWgsl = "/*\n * Copyright (c) 2024-2025 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\noverride IS_Y_UP: bool = true;\noverride IS_RIGHT_HANDED: bool = true;\noverride IS_REVERSE_Z: bool = true;\n\noverride FROM_KM_SCALE: f32 = 1.0;\noverride TO_KM_SCALE: f32 = 1.0 / FROM_KM_SCALE;\n\nfn depth_max() -> f32 {\n\tif IS_REVERSE_Z {\n\t\treturn 0.0000001;\n\t} else {\n\t\treturn 1.0;\n\t}\n}\n\nfn is_valid_depth(depth: f32) -> bool {\n\tif IS_REVERSE_Z {\n\t\treturn depth > 0.0 && depth <= 1.0;\n\t} else {\n\t\treturn depth < 1.0 && depth >= 0.0;\n\t}\n}\n\nfn uv_to_world_dir(uv: vec2<f32>, inv_proj: mat4x4<f32>, inv_view: mat4x4<f32>) -> vec3<f32> {\n\tlet hom_view_space = inv_proj * vec4<f32>(vec3<f32>(uv * vec2<f32>(2.0, -2.0) - vec2<f32>(1.0, -1.0), depth_max()), 1.0);\n\treturn normalize((inv_view * vec4<f32>(hom_view_space.xyz / hom_view_space.w, 0.0)).xyz);\n}\n\nfn uv_and_depth_to_world_pos(uv: vec2<f32>, inv_proj: mat4x4<f32>, inv_view: mat4x4<f32>, depth: f32) -> vec3<f32> {\n\tlet hom_view_space = inv_proj * vec4<f32>(vec3<f32>(uv * vec2<f32>(2.0, -2.0) - vec2<f32>(1.0, -1.0), depth), 1.0);\n\treturn (inv_view * vec4<f32>(hom_view_space.xyz / hom_view_space.w, 1.0)).xyz * TO_KM_SCALE;\n}\n\nfn to_z_up_left_handed(v: vec3<f32>) -> vec3<f32> {\n if IS_Y_UP {\n if IS_RIGHT_HANDED {\n return vec3<f32>(v.x, v.z, v.y);\n } else {\n return vec3<f32>(v.x, v.z, -v.y);\n }\n } else {\n if IS_RIGHT_HANDED {\n return vec3<f32>(v.x, v.y, -v.z);\n } else {\n return v;\n }\n }\n}\n"; var fullScreenVertexShaderWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\n@vertex\nfn vertex(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {\n\treturn vec4<f32>(vec2<f32>(f32((vertex_index << 1) & 2), f32(vertex_index & 2)) * 2 - 1, 0, 1);\n}\n"; var hgDraineConstWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\noverride HG_DRAINE_ALPHA_THIRDS = HG_DRAINE_ALPHA / 3.0;\noverride HG_DRAINE_G_HG_2 = HG_DRAINE_G_HG * HG_DRAINE_G_HG;\noverride HG_DRAINE_G_D_2 = HG_DRAINE_G_D * HG_DRAINE_G_D;\noverride HG_DRAINE_CONST_DENOM = 1.0 / (1.0 + (HG_DRAINE_ALPHA * (1.0 / 3.0) * (1.0 + (2.0 * HG_DRAINE_G_D_2))));\n\nfn draine_phase_hg(cos_theta: f32) -> f32 {\n return one_over_four_pi *\n ((1.0 - HG_DRAINE_G_HG_2) / pow((1.0 + HG_DRAINE_G_HG_2 - (2.0 * HG_DRAINE_G_HG * cos_theta)), 1.5));\n}\n\nfn draine_phase_d(cos_theta: f32) -> f32 {\n return one_over_four_pi *\n ((1.0 - HG_DRAINE_G_D_2) / pow((1.0 + HG_DRAINE_G_D_2 - (2.0 * HG_DRAINE_G_D * cos_theta)), 1.5)) *\n ((1.0 + (HG_DRAINE_ALPHA * cos_theta * cos_theta)) * HG_DRAINE_CONST_DENOM);\n}\n\nfn hg_draine_phase(cos_theta: f32) -> f32 {\n return mix(draine_phase_hg(cos_theta), draine_phase_d(cos_theta), HG_DRAINE_W_D);\n}\n"; var hgDraineLargeWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\n// 5 µm ≤ 𝑑 ≤ 50 µm\noverride HG_DRAINE_G_HG = exp(-(0.0990567 / (HG_DRAINE_DROPLET_DIAMETER - 1.67154)));\noverride HG_DRAINE_G_D = exp(-(2.20679 / (HG_DRAINE_DROPLET_DIAMETER + 3.91029)) - 0.428934);\noverride HG_DRAINE_ALPHA = exp(3.62489 - (8.29288 / (HG_DRAINE_DROPLET_DIAMETER + 5.52825)));\noverride HG_DRAINE_W_D = exp(-(0.599085 / (HG_DRAINE_DROPLET_DIAMETER - 0.641583)) - 0.665888);\n"; var hgDraineMid2Wgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\n// 1.5 µm <= 𝑑 < 5 µm\noverride HG_DRAINE_G_HG = 0.0604931 * log(log(HG_DRAINE_DROPLET_DIAMETER)) + 0.940256;\noverride HG_DRAINE_G_D = 0.500411 - 0.081287 / (-2.0 * log(HG_DRAINE_DROPLET_DIAMETER) + tan(log(HG_DRAINE_DROPLET_DIAMETER)) + 1.27551);\noverride HG_DRAINE_ALPHA = 7.30354 * log(HG_DRAINE_DROPLET_DIAMETER) + 6.31675;\noverride HG_DRAINE_W_D = 0.026914 * (log(HG_DRAINE_DROPLET_DIAMETER) - cos(5.68947 * (log(log(HG_DRAINE_DROPLET_DIAMETER)) - 0.0292149))) + 0.376475;\n"; var hgDraineMid1Wgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\n// 0.1 µm < 𝑑 < 1.5 µm\noverride HG_DRAINE_G_HG = 0.862 - 0.143 * log(HG_DRAINE_DROPLET_DIAMETER) * log(HG_DRAINE_DROPLET_DIAMETER);\noverride HG_DRAINE_G_D = 0.379685 * cos(1.19692 * cos(((log(HG_DRAINE_DROPLET_DIAMETER) - 0.238604) * (log(HG_DRAINE_DROPLET_DIAMETER) + 1.00667)) / (0.507522 - 0.15677 * log(HG_DRAINE_DROPLET_DIAMETER))) + 1.37932 * log(HG_DRAINE_DROPLET_DIAMETER) + 0.0625835) + 0.344213;\noverride HG_DRAINE_ALPHA = 250.0;\noverride HG_DRAINE_W_D = 0.146209 * cos(3.38707 * log(HG_DRAINE_DROPLET_DIAMETER) + 2.11193) + 0.316072 + 0.0778917 * log(HG_DRAINE_DROPLET_DIAMETER);\n"; var hgDraineSmallWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\n// 𝑑 <= 0.1 µm\noverride HG_DRAINE_G_HG = 13.8 * HG_DRAINE_DROPLET_DIAMETER * HG_DRAINE_DROPLET_DIAMETER;\noverride HG_DRAINE_G_D = 1.1456 * HG_DRAINE_DROPLET_DIAMETER * sin(9.29044 * HG_DRAINE_DROPLET_DIAMETER);\noverride HG_DRAINE_ALPHA = 250.0;\noverride HG_DRAINE_W_D = 0.252977 - pow(312.983 * HG_DRAINE_DROPLET_DIAMETER, 4.3);\n"; var intersectionWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\n// If there are no positive real solutions, returns -1.0\nfn solve_quadratic_for_positive_reals(a: f32, b: f32, c: f32) -> f32 {\n\tlet delta = b * b - 4.0 * a * c;\n\tif delta < 0.0 || a == 0.0 {\n\t\treturn -1.0;\n\t}\n\tlet solution0 = (-b - sqrt(delta)) / (2.0 * a);\n\tlet solution1 = (-b + sqrt(delta)) / (2.0 * a);\n\tif solution0 < 0.0 && solution1 < 0.0 {\n\t\treturn -1.0;\n\t}\n\tif solution0 < 0.0 {\n\t\treturn max(0.0, solution1);\n\t}\n\telse if solution1 < 0.0 {\n\t\treturn max(0.0, solution0);\n\t}\n\treturn max(0.0, min(solution0, solution1));\n}\n\nfn quadratic_has_positive_real_solutions(a: f32, b: f32, c: f32) -> bool {\n\tlet delta = b * b - 4.0 * a * c;\n\treturn (delta >= 0.0 && a != 0.0) && (((-b - sqrt(delta)) / (2.0 * a)) >= 0.0 || ((-b + sqrt(delta)) / (2.0 * a)) >= 0.0);\n}\n\nfn find_closest_ray_sphere_intersection(o: vec3<f32>, d: vec3<f32>, c: vec3<f32>, r: f32) -> f32 {\n\tlet dist = o - c;\n\treturn solve_quadratic_for_positive_reals(dot(d, d), 2.0 * dot(d, dist), dot(dist, dist) - (r * r));\n}\n\nfn ray_intersects_sphere(o: vec3<f32>, d: vec3<f32>, c: vec3<f32>, r: f32) -> bool {\n\tlet dist = o - c;\n\treturn quadratic_has_positive_real_solutions(dot(d, d), 2.0 * dot(d, dist), dot(dist, dist) - (r * r));\n}\n\nfn compute_planet_shadow(o: vec3<f32>, d: vec3<f32>, c: vec3<f32>, r: f32) -> f32 {\n\treturn f32(!ray_intersects_sphere(o, d, c, r));\n}\n\nfn find_atmosphere_t_max(t_max: ptr<function, f32>, o: vec3<f32>, d: vec3<f32>, c: vec3<f32>, bottom_radius: f32, top_radius: f32) -> bool {\n\tlet t_bottom = find_closest_ray_sphere_intersection(o, d, c, bottom_radius);\n\tlet t_top = find_closest_ray_sphere_intersection(o, d, c, top_radius);\n\tif t_bottom < 0.0 {\n\t\tif t_top < 0.0 {\n\t\t\t*t_max = 0.0;\n\t\t\treturn false;\n\t\t} else {\n\t\t\t*t_max = t_top;\n\t\t}\n\t} else {\n\t\tif t_top > 0.0 {\n\t\t\t*t_max = min(t_top, t_bottom);\n\t\t} else {\n\t\t\t*t_max = t_bottom;\n\t\t}\n\t}\n\treturn true;\n}\n\nfn find_atmosphere_t_max_t_bottom(t_max: ptr<function, f32>, t_bottom: ptr<function, f32>, o: vec3<f32>, d: vec3<f32>, c: vec3<f32>, bottom_radius: f32, top_radius: f32) -> bool {\n\t*t_bottom = find_closest_ray_sphere_intersection(o, d, c, bottom_radius);\n\tlet t_top = find_closest_ray_sphere_intersection(o, d, c, top_radius);\n\tif *t_bottom < 0.0 {\n\t\tif t_top < 0.0 {\n\t\t\t*t_max = 0.0;\n\t\t\treturn false;\n\t\t} else {\n\t\t\t*t_max = t_top;\n\t\t}\n\t} else {\n\t\tif t_top > 0.0 {\n\t\t\t*t_max = min(t_top, *t_bottom);\n\t\t} else {\n\t\t\t*t_max = *t_bottom;\n\t\t}\n\t}\n\treturn true;\n}\n\nfn move_to_atmosphere_top(world_pos: ptr<function, vec3<f32>>, world_dir: vec3<f32>, top_radius: f32) -> bool {\n\tlet view_height = length(*world_pos);\n\tif view_height > top_radius {\n\t\tlet t_top = find_closest_ray_sphere_intersection(*world_pos, world_dir, vec3<f32>(), top_radius * 0.9999);\n\t\tif t_top >= 0.0 {\n\t\t\t*world_pos = *world_pos + world_dir * t_top;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n"; var mediumWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\nstruct Atmosphere {\n\t// Rayleigh scattering coefficients\n\trayleigh_scattering: vec3<f32>,\n\t// Rayleigh scattering exponential distribution scale in the atmosphere\n\trayleigh_density_exp_scale: f32,\n\n\t// Mie scattering coefficients\n\tmie_scattering: vec3<f32>,\n\t// Mie scattering exponential distribution scale in the atmosphere\n\tmie_density_exp_scale: f32,\n\t// Mie extinction coefficients\n\tmie_extinction: vec3<f32>,\n\t// Mie phase parameter (Cornette-Shanks excentricity or Henyey-Greenstein-Draine droplet diameter)\n\tmie_phase_param: f32,\n\t// Mie absorption coefficients\n\tmie_absorption: vec3<f32>,\n\t\n\t// Another medium type in the atmosphere\n\tabsorption_density_0_layer_height: f32,\n\tabsorption_density_0_constant_term: f32,\n\tabsorption_density_0_linear_term: f32,\n\tabsorption_density_1_constant_term: f32,\n\tabsorption_density_1_linear_term: f32,\n\t// This other medium only absorb light, e.g. useful to represent ozone in the earth atmosphere\n\tabsorption_extinction: vec3<f32>,\n\n\t// Radius of the planet (center to ground)\n\tbottom_radius: f32,\n\n\t// The albedo of the ground.\n\tground_albedo: vec3<f32>,\n\n\t// Maximum considered atmosphere height (center to atmosphere top)\n\ttop_radius: f32,\n\n\t// planet center in world space (z up)\n\t// used to transform the camera's position to the atmosphere's object space\n\tplanet_center: vec3<f32>,\n\t\n\tmulti_scattering_factor: f32,\n}\n\nstruct MediumSample {\n\tscattering: vec3<f32>,\n\textinction: vec3<f32>,\n\n\tmie_scattering: vec3<f32>,\n\trayleigh_scattering: vec3<f32>,\n}\n\n/*\n * origin is the planet's center\n */\nfn sample_medium_extinction(height: f32, atmosphere: Atmosphere) -> vec3<f32> {\n\tlet mie_density: f32 = exp(atmosphere.mie_density_exp_scale * height);\n\tlet rayleigh_density: f32 = exp(atmosphere.rayleigh_density_exp_scale * height);\n\tvar absorption_density: f32;\n\tif height < atmosphere.absorption_density_0_layer_height {\n\t\tabsorption_density = saturate(atmosphere.absorption_density_0_linear_term * height + atmosphere.absorption_density_0_constant_term);\n\t} else {\n\t\tabsorption_density = saturate(atmosphere.absorption_density_1_linear_term * height + atmosphere.absorption_density_1_constant_term);\n\t}\n\n\tlet mie_extinction = mie_density * atmosphere.mie_extinction;\n\tlet rayleigh_extinction = rayleigh_density * atmosphere.rayleigh_scattering;\n\tlet absorption_extinction = absorption_density * atmosphere.absorption_extinction;\n\n\treturn mie_extinction + rayleigh_extinction + absorption_extinction;\n}\n\nfn sample_medium(height: f32, atmosphere: Atmosphere) -> MediumSample {\n\tlet mie_density: f32 = exp(atmosphere.mie_density_exp_scale * height);\n\tlet rayleigh_density: f32 = exp(atmosphere.rayleigh_density_exp_scale * height);\n\tvar absorption_density: f32;\n\tif height < atmosphere.absorption_density_0_layer_height {\n\t\tabsorption_density = saturate(atmosphere.absorption_density_0_linear_term * height + atmosphere.absorption_density_0_constant_term);\n\t} else {\n\t\tabsorption_density = saturate(atmosphere.absorption_density_1_linear_term * height + atmosphere.absorption_density_1_constant_term);\n\t}\n\n\tvar s: MediumSample;\n\ts.mie_scattering = mie_density * atmosphere.mie_scattering;\n\ts.rayleigh_scattering = rayleigh_density * atmosphere.rayleigh_scattering;\n\ts.scattering = s.mie_scattering + s.rayleigh_scattering;\n\n\tlet mie_extinction = mie_density * atmosphere.mie_extinction;\n\tlet rayleigh_extinction = s.rayleigh_scattering;\n\tlet absorption_extinction = absorption_density * atmosphere.absorption_extinction;\n\ts.extinction = mie_extinction + rayleigh_extinction + absorption_extinction;\n\n\treturn s;\n}\n"; var multipleScatteringWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\noverride MULTI_SCATTERING_LUT_RES_X: f32 = 32.0;\noverride MULTI_SCATTERING_LUT_RES_Y: f32 = MULTI_SCATTERING_LUT_RES_X;\n\nfn get_multiple_scattering(atmosphere: Atmosphere, scattering: vec3<f32>, extinction: vec3<f32>, worl_pos: vec3<f32>, cos_view_zenith: f32) -> vec3<f32> {\n\tvar uv = saturate(vec2<f32>(cos_view_zenith * 0.5 + 0.5, (length(worl_pos) - atmosphere.bottom_radius) / (atmosphere.top_radius - atmosphere.bottom_radius)));\n\tuv = vec2<f32>(from_unit_to_sub_uvs(uv.x, MULTI_SCATTERING_LUT_RES_X), from_unit_to_sub_uvs(uv.y, MULTI_SCATTERING_LUT_RES_Y));\n\treturn textureSampleLevel(multi_scattering_lut, lut_sampler, uv, 0).rgb;\n}\n"; var phaseWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\noverride MIE_USE_HG_DRAINE: bool = false;\noverride MIE_USE_HG_DRAINE_DYNAMIC: bool = false;\n\n// https://research.nvidia.com/labs/rtr/approximate-mie/publications/approximate-mie.pdf\n// cloud water droplet diameter in µm (should be 5 µm < d < 50 µm)\noverride HG_DRAINE_DROPLET_DIAMETER: f32 = 3.4;\n// include hg_draine_size\n// include hg_draine_const\n\nconst one_over_four_pi = 1.0 / (2.0 * tau);\n\nconst isotropic_phase: f32 = 1.0 / sphere_solid_angle;\n\nfn draine_phase_dynamic(alpha: f32, g: f32, cos_theta: f32) -> f32 {\n let g2 = g * g;\n return one_over_four_pi *\n ((1.0 - g2) / pow((1.0 + g2 - (2.0 * g * cos_theta)), 1.5)) *\n ((1.0 + (alpha * cos_theta * cos_theta)) / (1.0 + (alpha * (1.0 / 3.0) * (1.0 + (2.0 * g2)))));\n}\n\nfn hg_draine_phase_dynamic(cos_theta: f32, g_hg: f32, g_d: f32, alpha: f32, w_d: f32) -> f32 {\n return mix(draine_phase_dynamic(0, g_hg, cos_theta), draine_phase_dynamic(alpha, g_d, cos_theta), w_d);\n}\n\nfn hg_draine_phase_dynamic_dispatch(cos_theta: f32, diameter: f32) -> f32 {\n if diameter >= 5.0 {\n return hg_draine_phase_dynamic(\n cos_theta,\n exp(-(0.0990567 / (diameter - 1.67154))),\n exp(-(2.20679 / (diameter + 3.91029)) - 0.428934),\n exp(3.62489 - (8.29288 / (diameter + 5.52825))),\n exp(-(0.599085 / (diameter - 0.641583)) - 0.665888),\n );\n } else if diameter >= 1.5 {\n return hg_draine_phase_dynamic(\n cos_theta,\n 0.0604931 * log(log(diameter)) + 0.940256,\n 0.500411 - 0.081287 / (-2.0 * log(diameter) + tan(log(diameter)) + 1.27551),\n 7.30354 * log(diameter) + 6.31675,\n 0.026914 * (log(diameter) - cos(5.68947 * (log(log(diameter)) - 0.0292149))) + 0.376475,\n );\n } else if diameter > 0.1 {\n return hg_draine_phase_dynamic(\n cos_theta,\n 0.862 - 0.143 * log(diameter) * log(diameter),\n 0.379685 * cos(1.19692 * cos(((log(diameter) - 0.238604) * (log(diameter) + 1.00667)) / (0.507522 - 0.15677 * log(diameter))) + 1.37932 * log(diameter) + 0.0625835) + 0.344213,\n 250.0,\n 0.146209 * cos(3.38707 * log(diameter) + 2.11193) + 0.316072 + 0.0778917 * log(diameter),\n );\n } else {\n return hg_draine_phase_dynamic(\n cos_theta,\n 13.8 * diameter * diameter,\n 1.1456 * diameter * sin(9.29044 * diameter),\n 250.0,\n 0.252977 - pow(312.983 * diameter, 4.3),\n );\n }\n}\n\nfn cornette_shanks_phase(cos_theta: f32, g: f32) -> f32 {\n\tlet k: f32 = 3.0 / (8.0 * pi) * (1.0 - g * g) / (2.0 + g * g);\n\treturn k * (1.0 + cos_theta * cos_theta) / pow(1.0 + g * g - 2.0 * g * -cos_theta, 1.5);\n}\n\nfn mie_phase(cos_theta: f32, g_or_d: f32) -> f32 {\n if MIE_USE_HG_DRAINE {\n if MIE_USE_HG_DRAINE_DYNAMIC {\n return hg_draine_phase_dynamic_dispatch(cos_theta, g_or_d);\n } else {\n return hg_draine_phase(cos_theta);\n }\n } else {\n return cornette_shanks_phase(-cos_theta, g_or_d);\n }\n}\n\nfn rayleigh_phase(cos_theta: f32) -> f32 {\n\tlet factor: f32 = 3.0f / (16.0f * pi);\n\treturn factor * (1.0f + cos_theta * cos_theta);\n}\n"; var sampleSegmentWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\noverride RANDOMIZE_SAMPLE_OFFSET: bool = true;\n\nfn pcg_hash(seed: u32) -> u32 {\n\tlet state = seed * 747796405u + 2891336453u;\n\tlet word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n\treturn (word >> 22u) ^ word;\n}\n\nfn pcg_hashf(seed: u32) -> f32 {\n\treturn f32(pcg_hash(seed)) / 4294967296.0;\n}\n\nfn pcg_hash3(x: u32, y: u32, z: u32) -> f32 {\n\treturn pcg_hashf((x * 1664525 + y) + z);\n}\n\nfn get_sample_segment_t(uv: vec2<f32>, config: Uniforms) -> f32 {\n\tif RANDOMIZE_SAMPLE_OFFSET {\n\t\tlet seed = vec3<u32>(\n\t\t\tu32(uv.x * config.screen_resolution.x),\n\t\t\tu32(uv.y * config.screen_resolution.y),\n\t\t\tpcg_hash(u32(config.frame_id)),\n\t\t);\n\t\treturn pcg_hash3(seed.x, seed.y, seed.z);\n\t} else {\n\t\treturn 0.3;\n\t}\n}\n"; var shadowBaseWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\nfn get_sample_shadow(atmosphere: Atmosphere, sample_position: vec3<f32>, light_index: u32) -> f32 {\n\treturn get_shadow((sample_position + atmosphere.planet_center) * FROM_KM_SCALE, light_index);\n}\n"; var skyViewWgsl = "/*\n * Copyright (c) 2024-2025 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\noverride SKY_VIEW_LUT_RES_X: f32 = 192.0;\noverride SKY_VIEW_LUT_RES_Y: f32 = 108.0;\n\noverride USE_UNIFORM_LONGITUDE_PARAMETERIZATION: bool = false;\n\nfn sky_view_lut_params_to_v(atmosphere: Atmosphere, intersects_ground: bool, cos_view_zenith: f32, view_height: f32) -> f32 {\n let v_horizon = sqrt(max(view_height * view_height - atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0));\n\tlet ground_to_horizon = acos(v_horizon / view_height);\n\tlet zenith_horizon_angle = pi - ground_to_horizon;\n\n\tif !intersects_ground {\n\t\tlet coord = 1.0 - sqrt(max(1.0 - acos(cos_view_zenith) / zenith_horizon_angle, 0.0));\n\t\treturn coord * 0.5;\n\t} else {\n\t\tlet coord = (acos(cos_view_zenith) - zenith_horizon_angle) / ground_to_horizon;\n\t\treturn sqrt(max(coord, 0.0)) * 0.5 + 0.5;\n\t}\n}\n\nfn sky_view_lut_params_to_uv(atmosphere: Atmosphere, intersects_ground: bool, cos_view_zenith: f32, cos_light_view: f32, view_height: f32) -> vec2<f32> {\n\treturn vec2<f32>(\n\t from_unit_to_sub_uvs(sqrt(max(-cos_light_view * 0.5 + 0.5, 0.0)), SKY_VIEW_LUT_RES_X),\n\t from_unit_to_sub_uvs(sky_view_lut_params_to_v(atmosphere, intersects_ground, cos_view_zenith, view_height), SKY_VIEW_LUT_RES_Y)\n\t);\n}\n\nfn sky_view_lut_params_to_u_uniform(view_dir: vec3<f32>) -> f32 {\n var azimuth = 0.0;\n if IS_Y_UP {\n azimuth = atan2(view_dir.x, view_dir.z);\n\t} else {\n azimuth = atan2(view_dir.y, view_dir.x);\n\t}\n\tif IS_RIGHT_HANDED {\n\t azimuth = -azimuth;\n\t}\n\tif azimuth < 0.0 {\n return (azimuth + tau) / tau;\n } else {\n return azimuth / tau;\n }\n}\n\nfn sky_view_lut_params_to_uv_uniform(atmosphere: Atmosphere, intersects_ground: bool, cos_view_zenith: f32, view_dir: vec3<f32>, view_height: f32) -> vec2<f32> {\n\treturn vec2<f32>(\n\t from_unit_to_sub_uvs(sky_view_lut_params_to_u_uniform(view_dir), SKY_VIEW_LUT_RES_X),\n\t from_unit_to_sub_uvs(sky_view_lut_params_to_v(atmosphere, intersects_ground, cos_view_zenith, view_height), SKY_VIEW_LUT_RES_Y)\n\t);\n}\n\nfn compute_sky_view_lut_uv(view_height: f32, world_pos: vec3<f32>, world_dir: vec3<f32>, sun_dir: vec3<f32>, atmosphere: Atmosphere, config: Uniforms) -> vec2<f32> {\n\tlet zenith = normalize(world_pos);\n\tlet cos_view_zenith = dot(world_dir, zenith);\n\tlet intersects_ground = ray_intersects_sphere(world_pos, world_dir, vec3<f32>(), atmosphere.bottom_radius);\n\n if USE_UNIFORM_LONGITUDE_PARAMETERIZATION {\n return sky_view_lut_params_to_uv_uniform(atmosphere, intersects_ground, cos_view_zenith, world_dir, view_height);\n } else {\n let side = normalize(cross(zenith, world_dir));\t// assumes non parallel vectors\n let forward = normalize(cross(side, zenith));\t// aligns toward the sun light but perpendicular to up vector\n let cos_light_view = normalize(vec2<f32>(dot(sun_dir, forward), dot(sun_dir, side))).x;\n return sky_view_lut_params_to_uv(atmosphere, intersects_ground, cos_view_zenith, cos_light_view, view_height);\n }\n}\n"; var sunDiskWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * SPDX-License-Identifier: MIT\n */\n\noverride RENDER_SUN_DISK: bool = true;\noverride RENDER_MOON_DISK: bool = true;\noverride LIMB_DARKENING_ON_SUN: bool = true;\noverride LIMB_DARKENING_ON_MOON: bool = false;\n\nfn limb_darkeining_factor(center_to_edge: f32) -> vec3<f32> {\n\tlet u = vec3<f32>(1.0);\n\tlet a = vec3<f32>(0.397 , 0.503 , 0.652);\n\tlet inv_center_to_edge = 1.0 - center_to_edge;\n\tlet mu = sqrt(max(1.0 - inv_center_to_edge * inv_center_to_edge, 0.0));\n\treturn 1.0 - u * (1.0 - pow(vec3<f32>(mu), a));\n}\n\nfn sun_disk_luminance(world_pos: vec3<f32>, world_dir: vec3<f32>, atmosphere: Atmosphere, light: AtmosphereLight, apply_limb_darkening: bool) -> vec3<f32> {\n\tlet cos_view_sun = dot(world_dir, light.direction);\n\tlet cos_disk_radius = cos(0.5 * light.disk_diameter);\n\t\n\tif cos_view_sun <= cos_disk_radius || ray_intersects_sphere(world_pos, world_dir, vec3<f32>(), atmosphere.bottom_radius) {\n\t\treturn vec3<f32>();\n\t}\n\n\tlet disk_solid_angle = tau * cos_disk_radius;\n\tlet l_outer_space = (light.illuminance / disk_solid_angle) * light.disk_luminance_scale;\n\n\tlet height = length(world_pos);\n\tlet zenith = world_pos / height;\n\tlet cos_view_zenith = dot(world_dir, zenith);\n\tlet uv = transmittance_lut_params_to_uv(atmosphere, height, cos_view_zenith);\n\tlet transmittance_sun = textureSampleLevel(transmittance_lut, lut_sampler, uv, 0).rgb;\n\n\tif apply_limb_darkening {\n\t\tlet center_to_edge = 1.0 - ((2.0 * acos(cos_view_sun)) / light.disk_diameter);\n\t\treturn transmittance_sun * l_outer_space * limb_darkeining_factor(center_to_edge);\n\t} else {\n\t\treturn transmittance_sun * l_outer_space;\n\t}\n}\n\nfn get_sun_luminance(world_pos: vec3<f32>, world_dir: vec3<f32>, atmosphere: Atmosphere, uniforms: Uniforms) -> vec3<f32> {\n\tvar sun_luminance = vec3<f32>();\n\tif RENDER_SUN_DISK {\n\t\tsun_luminance += sun_disk_luminance(world_pos, world_dir, atmosphere, uniforms.sun, LIMB_DARKENING_ON_SUN);\n\t}\n\tif RENDER_MOON_DISK && USE_MOON {\n\t\tsun_luminance += sun_disk_luminance(world_pos, world_dir, atmosphere, uniforms.moon, LIMB_DARKENING_ON_MOON);\n\t}\n\treturn sun_luminance;\n}\n"; var uniformsWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\nstruct AtmosphereLight {\n\t// Sun light's illuminance\n\tilluminance: vec3<f32>,\n\t\n\t// Sun disk's angular diameter in radians\n\tdisk_diameter: f32,\n\t\n\t// Sun light's direction (direction pointing to the sun)\n\tdirection: vec3<f32>,\n\n\t// Sun disk's luminance\n\tdisk_luminance_scale: f32,\n}\n\nstruct Uniforms {\n\t// Inverse projection matrix for the current camera view\n\tinverse_projection: mat4x4<f32>,\n\n\t// Inverse view matrix for the current camera view\n\tinverse_view: mat4x4<f32>,\n\n\t// World position of the current camera view\n\tcamera_world_position: vec3<f32>,\n\n\t// Resolution of the multiscattering LUT (width = height)\n\tframe_id: f32,\n\n\t// Resolution of the output texture\n\tscreen_resolution: vec2<f32>,\n\n\t// Minimum number of ray marching samples per pixel\n\tray_march_min_spp: f32,\n\n\t// Maximum number of ray marching samples per pixel\n\tray_march_max_spp: f32,\n\n\t// Sun parameters\n\tsun: AtmosphereLight,\n\n\t// Moon / second sun parameters \n\tmoon: AtmosphereLight,\n}\n\n"; var uvWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\nfn from_sub_uvs_to_unit(u: f32, resolution: f32) -> f32 {\n\treturn (u - 0.5 / resolution) * (resolution / (resolution - 1.0));\n}\n\nfn from_unit_to_sub_uvs(u: f32, resolution: f32) -> f32 {\n\treturn (u + 0.5 / resolution) * (resolution / (resolution + 1.0));\n}\n\nfn transmittance_lut_params_to_uv(atmosphere: Atmosphere, view_height: f32, cos_view_zenith: f32) -> vec2<f32> {\n\tlet height_sq = view_height * view_height;\n\tlet bottom_radius_sq = atmosphere.bottom_radius * atmosphere.bottom_radius;\n\tlet top_radius_sq = atmosphere.top_radius * atmosphere.top_radius;\n\tlet h = sqrt(max(0.0, top_radius_sq - bottom_radius_sq));\n\tlet rho = sqrt(max(0.0, height_sq - bottom_radius_sq));\n\n\tlet discriminant = height_sq * (cos_view_zenith * cos_view_zenith - 1.0) + top_radius_sq;\n\tlet distance_to_boundary = max(0.0, (-view_height * cos_view_zenith + sqrt(max(discriminant, 0.0))));\n\n\tlet min_distance = atmosphere.top_radius - view_height;\n\tlet max_distance = rho + h;\n\tlet x_mu = (distance_to_boundary - min_distance) / (max_distance - min_distance);\n\tlet x_r = rho / h;\n\n\treturn vec2<f32>(x_mu, x_r);\n}\n"; var renderTransmittanceLutWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n\noverride SAMPLE_COUNT: u32 = 40;\n\noverride WORKGROUP_SIZE_X: u32 = 16;\noverride WORKGROUP_SIZE_Y: u32 = 16;\n\n@group(0) @binding(0) var<uniform> atmosphere_buffer: Atmosphere;\n@group(0) @binding(1) var transmittance_lut : texture_storage_2d<rgba16float, write>;\n\nfn find_closest_ray_circle_intersection(o: vec2<f32>, d: vec2<f32>, r: f32) -> f32 {\n\treturn solve_quadratic_for_positive_reals(dot(d, d), 2.0 * dot(d, o), dot(o, o) - (r * r));\n}\n\nfn find_atmosphere_t_max_2d(t_max: ptr<function, f32>, o: vec2<f32>, d: vec2<f32>, bottom_radius: f32, top_radius: f32) -> bool {\n\tlet t_bottom = find_closest_ray_circle_intersection(o, d, bottom_radius);\n\tlet t_top = find_closest_ray_circle_intersection(o, d, top_radius);\n\tif t_bottom < 0.0 {\n\t\tif t_top < 0.0 {\n\t\t\t*t_max = 0.0;\n\t\t\treturn false;\n\t\t} else {\n\t\t\t*t_max = t_top;\n\t\t}\n\t} else {\n\t\tif t_top > 0.0 {\n\t\t\t*t_max = min(t_top, t_bottom);\n\t\t} else {\n\t\t\t*t_max = 0.0;\n\t\t}\n\t}\n\treturn true;\n}\n\nfn uv_to_transmittance_lut_params(uv: vec2<f32>, atmosphere: Atmosphere) -> vec2<f32> {\n\tlet x_mu: f32 = uv.x;\n\tlet x_r: f32 = uv.y;\n\n\tlet bottom_radius_sq = atmosphere.bottom_radius * atmosphere.bottom_radius;\n\tlet h_sq = atmosphere.top_radius * atmosphere.top_radius - bottom_radius_sq;\n\tlet h: f32 = sqrt(h_sq);\n\tlet rho: f32 = h * x_r;\n\tlet rho_sq = rho * rho;\n\tlet view_height = sqrt(rho_sq + bottom_radius_sq);\n\n\tlet d_min: f32 = atmosphere.top_radius - view_height;\n\tlet d_max: f32 = rho + h;\n\tlet d: f32 = d_min + x_mu * (d_max - d_min);\n\n\tvar cos_view_zenith = 1.0;\n\tif d != 0.0 {\n\t\tcos_view_zenith = clamp((h_sq - rho_sq - d * d) / (2.0 * view_height * d), -1.0, 1.0);\n\t}\n\n\treturn vec2<f32>(view_height, cos_view_zenith);\n}\n\n@compute\n@workgroup_size(WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, 1)\nfn render_transmittance_lut(@builtin(global_invocation_id) global_id: vec3<u32>) {\n\tlet output_size = vec2<u32>(textureDimensions(transmittance_lut));\n\tif output_size.x <= global_id.x || output_size.y <= global_id.y {\n\t\treturn;\n\t}\n\n\tlet pix = vec2<f32>(global_id.xy) + 0.5;\n\tlet uv = pix / vec2<f32>(output_size);\n\n\tlet atmosphere = atmosphere_buffer;\n\n\t// Compute camera position from LUT coords\n\tlet lut_params = uv_to_transmittance_lut_params(uv, atmosphere);\n\tlet view_height = lut_params.x;\n\tlet cos_view_zenith = lut_params.y;\n\tlet world_pos = vec2<f32>(0.0, view_height);\n\tlet world_dir = vec2<f32>(sqrt(max(1.0 - cos_view_zenith * cos_view_zenith, 0.0)), cos_view_zenith);\n\n\tvar transmittance = vec3<f32>();\n\n\t// Compute next intersection with atmosphere or ground\n\tvar t_max: f32 = 0.0;\n\tif find_atmosphere_t_max_2d(&t_max, world_pos, world_dir, atmosphere.bottom_radius, atmosphere.top_radius) {\n\t\tt_max = min(t_max, t_max_max);\n\n\t\t// Sample count\n\t\tlet sample_count = f32(SAMPLE_COUNT);\t// Can go a low as 10 sample but energy lost starts to be visible.\n\t\tlet sample_segment_t: f32 = 0.3f;\n\t\tlet dt = t_max / sample_count;\n\n\t\t// Ray march the atmosphere to integrate optical depth\n\t\tvar t = 0.0f;\n\t\tvar dt_exact = 0.0f;\n\t\tfor (var s: f32 = 0.0f; s < sample_count; s += 1.0f) {\n\t\t\tlet t_new = (s + sample_segment_t) * dt;\n\t\t\tdt_exact = t_new - t;\n\t\t\tt = t_new;\n\n\t\t\tlet sample_height = length(world_pos + t * world_dir) - atmosphere.bottom_radius;\n\t\t\ttransmittance += sample_medium_extinction(sample_height, atmosphere) * dt_exact;\n\t\t}\n\n\t\ttransmittance = exp(-transmittance);\n\t}\n\n\ttextureStore(transmittance_lut, global_id.xy, vec4<f32>(transmittance, 1.0));\n}\n"; var renderMultiScatteringLutWgsl = "/*\n * Copyright (c) 2024 Lukas Herzberger\n * Copyright (c) 2020 Epic Games, Inc.\n * SPDX-License-Identifier: MIT\n */\n \noverride SAMPLE_COUNT: u32 = 20;\n\n@group(0) @binding(0) var<uniform> atmosphere_buffer: Atmosphere;\n@group(0) @binding(1) var lut_sampler: sampler;\n@group(0) @binding(2) var transmittance_lut: texture_2d<f32>;\n@group(0) @binding(3) var multi_scattering_lut: texture_storage_2d<rgba16float, write>;\n\nconst direction_sample_count: f32 = 64.0;\nconst workgroup_size_z: u32 = 64;\n\nvar<workgroup> shared_multi_scattering: array<vec3<f32>, workgroup_size_z>;\nvar<workgroup> shared_luminance: array<vec3<f32>, workgroup_size_z>;\n\nfn get_transmittance_to_sun(sun_dir: vec3<f32>, zenith: vec3<f32>, atmosphere: Atmosphere, sample_height: f32) -> vec3<f32> {\n\tlet cos_sun_zenith = dot(sun_dir, zenith);\n\tlet uv = transmittance_lut_params_to_uv(atmosphere, sample_height, cos_sun_zenith);\n\treturn textureSampleLevel(transmittance_lut, lut_sampler, uv, 0).rgb;\n}\n\nstruct IntegrationResults {\n\tluminance: vec3<f32>,\n\tmulti_scattering: vec3<f32>,\n}\n\nfn integrate_scattered_luminance(world_pos: vec3<f32>, world_dir: vec3<f32>, sun_dir: vec3<f32>, atmosphere: Atmosphere) -> IntegrationResults {\n\tvar result = IntegrationResults();\n\n\tlet planet_center = vec3<f32>();\n\tvar t_max: f32 = 0.0;\n\tvar t_bottom: f32 = 0.0;\n\tif !find_atmosphere_t_max_t_bottom(&t_max, &t_bottom, world_pos, world_dir, planet_center, atmosphere.bottom_radius, atmosphere.top_radius) {\n\t\treturn result;\n\t}\n\tt_max = min(t_max, t_max_max);\n\n\tlet sample_count = f32(SAMPLE_COUNT);\n\tlet sample_segment_t = 0.3;\n\tlet dt = t_max / sample_count;\n\n\tvar throughput = vec3<f32>(1.0);\n\tvar t = 0.0;\n\tvar dt_exact = 0.0;\n\tfor (var s = 0.0; s < sample_count; s += 1.0) {\n\t\tlet t_new = (s + sample_segment_t) * dt;\n\t\tdt_exact = t_new - t;\n\t\tt = t_new;\n\n\t\tlet sample_pos = world_pos + t * world_dir;\n\t\tlet sample_height = length(sample_pos);\n\n\t\tlet zenith = sample_pos / sample_height;\n\t\tlet transmittance_to_sun = get_transmittance_to_sun(sun_dir, zenith, atmosphere, sample_height);\n\n\t\tlet medium = sample_medium(sample_height - atmosphere.bottom_radius, atmosphere);\n\t\tlet sample_transmittance = exp(-medium.extinction * dt_exact);\n\n\t\tlet planet_shadow = compute_planet_shadow(sample_pos, sun_dir, planet_center + planet_radius_offset * zenith, atmosphere.bottom_radius);\n\t\tlet scattered_luminance = planet_shadow * transmittance_to_sun * (medium.scattering * isotropic_phase);\n\n\t\tresult.multi_scattering += throughput * (medium.scattering - medium.scattering * sample_transmittance) / medium.extinction;\n\t\tresult.luminance += throughput * (scattered_luminance - scattered_luminance * sample_transmittance) / medium.extinction;\n\n\t\tthroughput *= sample_transmittance;\n\t}\n\n\t// Account for light bounced off the planet\n\tif t_max == t_bottom && t_bottom > 0.0 {\n\t\tlet t = t_bottom;\n\t\tlet sample_pos = world_pos + t