webgpu-sky-atmosphere
Version:
A WebGPU implementation of Hillaire's atmosphere model. Renders the sky as a post-process.
1 lines • 120 kB
JavaScript
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).webgpuSkyAtmosphere={})}(this,(function(e){"use strict";function t(e,t=!0,n=!0){const r=6360;return{center:e??[0,t?-6360:0,t?0:-6360],bottomRadius:r,height:100,rayleigh:{densityExpScale:-1/8,scattering:[.005802,.013558,.0331]},mie:{densityExpScale:-1/1.2,scattering:[.003996,.003996,.003996],extinction:[.00444,.00444,.00444],phaseParam:n?.8:3.4},absorption:{layer0:{height:25,constantTerm:-2/3,linearTerm:1/15},layer1:{constantTerm:8/3,linearTerm:-1/15},extinction:[65e-5,.001881,85e-6]},groundAlbedo:[.4,.4,.4],multipleScatteringFactor:1}}class n{texture;view;constructor(e){this.texture=e,this.view=e.createView({label:e.label})}}class r{pipeline;bindGroups;dispatchDimensions;constructor(e,t,n){this.pipeline=e,this.bindGroups=t,this.dispatchDimensions=n}encode(e,t=!1){e.setPipeline(this.pipeline);for(let t=0;t<this.bindGroups.length;++t)e.setBindGroup(t,this.bindGroups[t]);if(e.dispatchWorkgroups(...this.dispatchDimensions),t)for(let t=0;t<this.bindGroups.length;++t)e.setBindGroup(t,null)}replaceBindGroup(e,t){this.bindGroups[e]=t}replaceDispatchDimensions(e){this.dispatchDimensions[0]=e[0],this.dispatchDimensions[1]=e[1],this.dispatchDimensions[2]=e[2]}}class i{pipeline;bindGroups;constructor(e,t){this.pipeline=e,this.bindGroups=t}encode(e,t=!1){e.setPipeline(this.pipeline);for(let t=0;t<this.bindGroups.length;++t)e.setBindGroup(t,this.bindGroups[t]);if(e.draw(3),t)for(let t=0;t<this.bindGroups.length;++t)e.setBindGroup(t,null)}replaceBindGroup(e,t){this.bindGroups[e]=t}}const a=[256,64],s=32,o=[192,108],_=[32,32,32],u="rgba16float",l=u,c=u,m=u,d=128,p=224;class h{label;device;atmosphereBuffer;uniformsBuffer;lutSampler;transmittanceLut;multiScatteringLut;skyViewLut;aerialPerspectiveLut;#e;constructor(e,r,i){this.label=r.label??"atmosphere",this.device=e,this.#e=r.atmosphere??t(),this.atmosphereBuffer=e.createBuffer({label:`atmosphere buffer [${this.label}]`,size:d,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.updateAtmosphere(this.#e),r.customUniformsSource?this.uniformsBuffer=void 0:this.uniformsBuffer=e.createBuffer({label:`config buffer [${this.label}]`,size:p,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.lutSampler=i||function(e){return e.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})}(e),this.transmittanceLut=new n(e.createTexture({label:`transmittance LUT [${this.label}]`,size:r.lookUpTables?.transmittanceLut?.size??a,format:r.lookUpTables?.transmittanceLut?.format??u,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.STORAGE_BINDING})),this.multiScatteringLut=new n(e.createTexture({label:`multi scattering LUT [${this.label}]`,size:r.lookUpTables?.multiScatteringLut?.size??[s,s],format:r.lookUpTables?.multiScatteringLut?.format??l,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.STORAGE_BINDING})),this.skyViewLut=new n(e.createTexture({label:`sky view LUT [${this.label}]`,size:r.lookUpTables?.skyViewLut?.size??o,format:r.lookUpTables?.skyViewLut?.format??c,usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.STORAGE_BINDING})),this.aerialPerspectiveLut=new n(e.createTexture({label:`aerial perspective LUT [${this.label}]`,size:r.lookUpTables?.aerialPerspectiveLut?.size??_,format:r.lookUpTables?.aerialPerspectiveLut?.format??m,dimension:"3d",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.STORAGE_BINDING}))}get atmosphere(){return this.#e}updateAtmosphere(e){this.#e=e,this.device.queue.writeBuffer(this.atmosphereBuffer,0,f(this.#e))}updateUniforms(e){this.uniformsBuffer&&this.device.queue.writeBuffer(this.uniformsBuffer,0,g(e))}}function f(e){return new Float32Array([e.rayleigh.scattering[0],e.rayleigh.scattering[1],e.rayleigh.scattering[2],e.rayleigh.densityExpScale,e.mie.scattering[0],e.mie.scattering[1],e.mie.scattering[2],e.mie.densityExpScale,e.mie.extinction[0],e.mie.extinction[1],e.mie.extinction[2],e.mie.phaseParam,Math.max(e.mie.extinction[0]-e.mie.scattering[0],0),Math.max(e.mie.extinction[1]-e.mie.scattering[1],0),Math.max(e.mie.extinction[2]-e.mie.scattering[2],0),e.absorption.layer0.height,e.absorption.layer0.constantTerm,e.absorption.layer0.linearTerm,e.absorption.layer1.constantTerm,e.absorption.layer1.linearTerm,e.absorption.extinction[0],e.absorption.extinction[1],e.absorption.extinction[2],e.bottomRadius,e.groundAlbedo[0],e.groundAlbedo[1],e.groundAlbedo[2],e.bottomRadius+Math.max(e.height,0),...e.center,e.multipleScatteringFactor])}function g(e){return new Float32Array([...e.camera.inverseProjection,...e.camera.inverseView,...e.camera.position,e.frameId??0,...e.screenResolution,e.rayMarchMinSPP??14,e.rayMarchMaxSPP??30,...e.sun.illuminance??[1,1,1],e.sun.diskAngularDiameter??Math.PI/180*.545,...e.sun.direction,e.sun.diskLuminanceScale??1,...e.moon?.illuminance??[1,1,1],e.moon?.diskAngularDiameter??.568*Math.PI/180,...e.moon?.direction??e.sun.direction.map((e=>-1*e)),e.moon?.diskLuminanceScale??1])}var v="/*\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",y="/*\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",b="/*\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",w="/*\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",L="/*\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",S="/*\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",R="/*\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",x="/*\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",E="/*\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",k="/*\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",T="/*\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",P="/*\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",I="/*\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",A="/*\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",D="/*\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",U="/*\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",G="/*\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",z="/*\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",M="/*\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",N="/*\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",O="/*\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";function C(e){const t=D.replace("// include hg_draine_const",R);return!e||e>=5?t.replace("// include hg_draine_size",x):e>=1.5?t.replace("// include hg_draine_size",E):e>.1?t.replace("// include hg_draine_size",k):t.replace("// include hg_draine_size",T)}function B(e="rgba16float"){return`${b}\n${P}\n${I}\n/*\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`.replace("rgba16float",e)}function $(e="rgba16float"){return`${b}\n${P}\n${I}\n${C()}\n${O}\n/*\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 * 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 n_dot_l = saturate(dot(zenith, sun_dir));\n\t\tresult.luminance += transmittance_to_sun * throughput * n_dot_l * atmosphere.ground_albedo / pi;\n\t}\n\n\treturn result;\n}\n\nfn compute_sample_direction(direction_index: u32) -> vec3<f32> {\n\tlet sample = f32(direction_index);\n\tlet theta = tau * sample / golden_ratio;\n\tlet phi = acos(1.0 - 2.0 * (sample + 0.5) / direction_sample_count);\n\tlet cos_phi = cos(phi);\n\tlet sin_phi = sin(phi);\n\tlet cos_theta = cos(theta);\n\tlet sin_theta = sin(theta);\n\treturn vec3<f32>(\n\t\tcos_theta * sin_phi,\n\t\tsin_theta * sin_phi,\n\t\tcos_phi\n\t);\n}\n\n@compute\n@workgroup_size(1, 1, workgroup_size_z)\nfn render_multi_scattering_lut(@builtin(global_invocation_id) global_id: vec3<u32>) {\n\tlet output_size = textureDimensions(multi_scattering_lut);\n\tlet direction_index = global_id.z;\n\n\tlet pix = vec2<f32>(global_id.xy) + 0.5;\n\tvar uv = pix / vec2<f32>(output_size);\n\tuv = vec2<f32>(from_sub_uvs_to_unit(uv.x, f32(output_size.x)), from_sub_uvs_to_unit(uv.y, f32(output_size.y)));\n\n\tlet atmosphere = atmosphere_buffer;\n\n\tlet cos_sun_zenith = uv.x * 2.0 - 1.0;\n\tlet sun_dir = vec3<f32>(0.0, sqrt(saturate(1.0 - cos_sun_zenith * cos_sun_zenith)), cos_sun_zenith);\n\tlet view_height = atmosphere.bottom_radius + saturate(uv.y + planet_radius_offset) * (atmosphere.top_radius - atmosphere.bottom_radius - planet_radius_offset);\n\n\tlet world_pos = vec3<f32>(0.0, 0.0, view_height);\n\tlet world_dir = compute_sample_direction(direction_index);\n\n\tlet scattering_result = integrate_scattered_luminance(world_pos, world_dir, normalize(sun_dir), atmosphere);\n\n\tshared_multi_scattering[direction_index] = scattering_result.multi_scattering / direction_sample_count;\n\tshared_luminance[direction_index] = scattering_result.luminance / direction_sample_count;\n\n\tworkgroupBarrier();\n\n\t// reduce samples - the last remaining thread publishes the result\n\tfor (var i = 32u; i > 0; i = i >> 1) {\n\t\tif direction_index < i {\n\t\t\tshared_multi_scattering[direction_index] += shared_multi_scattering[direction_index + i];\n\t\t\tshared_luminance[direction_index] += shared_luminance[direction_index + i];\n\t\t}\n\t\tworkgroupBarrier();\n\t}\n\tif direction_index > 0 {\n\t\treturn;\n\t}\n\n\tlet luminance = shared_luminance[0] * (1.0 / (1.0 - shared_multi_scattering[0]));\n\n\ttextureStore(multi_scattering_lut, global_id.xy, vec4<f32>(atmosphere.multi_scattering_factor * luminance, 1.0));\n}\n\n`.replace("rgba16float",e)}function H(e){return`${e??"fn get_shadow(p: vec3<f32>, i: u32) -> f32 { return 1.0; }"}\n${G}`}function V(e="rgba16float",t,n,r){const i=`${b}\n${P}\n${I}\n${C(r)}\n${O}\n${N}\n${n?`${n}\n${w}\n`:""}${L}\n${A}\n`;let a="/*\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 INV_DISTANCE_TO_MAX_SAMPLE_COUNT: f32 = 1.0 / 100.0;\n\noverride USE_UNIFORM_LONGITUDE_PARAMETERIZATION: bool = false;\noverride USE_MOON: bool = false;\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<uniform> config_buffer: Uniforms;\n@group(0) @binding(2) var lut_sampler: sampler;\n@group(0) @binding(3) var transmittance_lut: texture_2d<f32>;\n@group(0) @binding(4) var multi_scattering_lut: texture_2d<f32>;\n@group(0) @binding(5) var sky_view_lut : texture_storage_2d<rgba16float, write>;\n\nstruct SingleScatteringResult {\n\tluminance: vec3<f32>,\t\t\t\t// Scattered light (luminance)\n\ttransmittance: vec3<f32>,\t\t\t// transmittance in [0,1] (unitless)\n}\n\nfn integrate_scattered_luminance(world_pos: vec3<f32>, world_dir: vec3<f32>, sun_dir: vec3<f32>, moon_dir: vec3<f32>, atmosphere: Atmosphere, config: Uniforms) -> SingleScatteringResult {\n\tvar result = SingleScatteringResult();\n\n\tlet planet_center = vec3<f32>();\n\tvar t_max: f32 = 0.0;\n\tif !find_atmosphere_t_max(&t_max, 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 = mix(config.ray_march_min_spp, config.ray_march_max_spp, saturate(t_max * INV_DISTANCE_TO_MAX_SAMPLE_COUNT));\n\tlet sample_count_floored = floor(sample_count);\n\tlet inv_sample_count_floored = 1.0 / sample_count_floored;\n\tlet t_max_floored = t_max * sample_count_floored / sample_count;\n\tlet sample_segment_t = 0.3;\n\n\tlet sun_direction = normalize(sun_dir);\n\tlet sun_illuminance = config.sun.illuminance;\n\n\tlet cos_theta = dot(sun_dir, world_dir);\n\tlet mie_phase_val = mie_phase(cos_theta, atmosphere.mie_phase_param);\n\tlet rayleigh_phase_val = rayleigh_phase(cos_theta);\n\n\tvar moon_direction = moon_dir;\n\tvar moon_illuminance = config.moon.illuminance;\n\n\tvar cos_theta_moon = 0.0;\n\tvar mie_phase_val_moon = 0.0;\n\tvar rayleigh_phase_val_moon = 0.0;\n\n\tif USE_MOON {\n\t\tmoon_direction = normalize(moon_direction);\n\t\tmoon_illuminance = config.moon.illuminance;\n\n\t\tcos_theta_moon = dot(moon_direction, world_dir);\n\t\tmie_phase_val_moon = mie_phase(cos_theta_moon, atmosphere.mie_phase_param);\n\t\trayleigh_phase_val_moon = rayleigh_phase(cos_theta_moon);\n\t}\n\n\tresult.luminance = vec3<f32>(0.0);\n\tresult.transmittance = vec3<f32>(1.0);\n\tvar t = 0.0;\n\tvar dt = t_max / sample_count;\n\tfor (var s = 0.0; s < sample_count; s += 1.0) {\n\t\tvar t0 = s * inv_sample_count_floored;\n\t\tvar t1 = (s + 1.0) * inv_sample_count_floored;\n\t\tt0 = (t0 * t0) * t_max_floored;\n\t\tt1 = t1 * t1;\n\t\tif t1 > 1.0 {\n\t\t\tt1 = t_max;\n\t\t} else {\n\t\t\tt1 = t_max_floored * t1;\n\t\t}\n\t\tdt = t1 - t0;\n\t\tt = t0 + dt * sample_segment_t;\n\n\t\tlet sample_pos = world_pos + t * world_dir;\n\t\tlet sample_height = length(sample_pos);\n\n\t\tlet medium = sample_medium(sample_height - atmosphere.bottom_radius, atmosphere);\n\t\tlet sample_transmittance = exp(-medium.extinction * dt);\n\n\t\tlet zenith = sample_pos / sample_height;\n\n\t\tlet cos_sun_zenith = dot(sun_direction, zenith);\n\t\tlet transmittance_to_sun = textureSampleLevel(transmittance_lut, lut_sampler, transmittance_lut_params_to_uv(atmosphere, sample_height, cos_sun_zenith), 0).rgb;\n\t\tlet phase_times_scattering = medium.mie_scattering * mie_phase_val + medium.rayleigh_scattering * rayleigh_phase_val;\n\t\tlet multi_scattered_luminance = get_multiple_scattering(atmosphere, medium.scattering, medium.extinction, sample_pos, cos_sun_zenith);\n\t\tlet planet_shadow = compute_planet_shadow(sample_pos, sun_direction, planet_center + planet_radius_offset * zenith, atmosphere.bottom_radius);\n\t\tlet shadow = get_sample_shadow(atmosphere, sample_pos, 0);\n\n\t\tvar scattered_luminance = sun_illuminance * (planet_shadow * shadow * transmittance_to_sun * phase_times_scattering + multi_scattered_luminance * medium.scattering);\n\n\t\tif USE_MOON {\n\t\t\tlet cos_moon_zenith = dot(moon_direction, zenith);\n\t\t\tlet transmittance_to_moon = textureSampleLevel(transmittance_lut, lut_sampler, transmittance_lut_params_to_uv(atmosphere, sample_height, cos_moon_zenith), 0).rgb;\n\t\t\tlet phase_times_scattering_moon = medium.mie_scattering * mie_phase_val_moon + medium.rayleigh_scattering * rayleigh_phase_val_moon;\n\t\t\tlet multi_scattered_luminance_moon = get_multiple_scattering(atmosphere, medium.scattering, medium.extinction, sample_pos, cos_moon_zenith);\n\t\t\tlet planet_shadow_moon = compute_planet_shadow(sample_pos, moon_direction, planet_center + planet_radius_offset * zenith, atmosphere.bottom_radius);\n\t\t\tlet shadow_moon = get_sample_shadow(atmosphere, sample_pos, 1);\n\n\t\t\tscattered_luminance += moon_illuminance * (planet_shadow_moon * shadow_moon * transmittance_to_moon * phase_times_scattering_moon + multi_scattered_luminance_moon * medium.scattering);\n\t\t}\n\n\t\tlet intergrated_luminance = (scattered_luminance - scattered_luminance * sample_transmittance) / medium.extinction;\n\t\tresult.luminance += result.transmittance * intergrated_luminance;\n\t\tresult.transmittance *= sample_transmittance;\n\t}\n\n\treturn result;\n}\n\nfn compute_world_dir(uv_in: vec2<f32>, sky_view_res: vec2<f32>, view_height: f32, atmosphere: Atmosphere) -> vec3<f32> {\n\tlet uv = vec2<f32>(from_sub_uvs_to_unit(uv_in.x, sky_view_res.x), from_sub_uvs_to_unit(uv_in.y, sky_view_res.y));\n\n\tlet v_horizon = sqrt(max(view_height * view_height - atmosphere.bottom_radius * atmosphere.bottom_radius, 0.0));\n\tlet ground_to_horizon_angle = acos(v_horizon / view_height);\n\tlet zenith_horizon_angle = pi - ground_to_horizon_angle;\n\n\tvar cos_view_zenith: f32;\n\tif uv.y < 0.5 {\n\t\tlet coord = 1.0 - (2.0 * uv.y);\n\t\tcos_view_zenith = cos(zenith_horizon_angle * (1.0 - (coord * coord)));\n\t} else {\n\t\tlet coord = (uv.y * 2.0) - 1.0;\n\t\tcos_view_zenith = cos(zenith_horizon_angle + ground_to_horizon_angle * (coord * coord));\n\t}\n\tlet sin_view_zenith = sqrt(max(1.0 - cos_view_zenith * cos_view_zenith, 0.0));\n\n if USE_UNIFORM_LONGITUDE_PARAMETERIZATION {\n \tlet azimuth = fract(uv.x + 0.25) * tau;\n \treturn vec3<f32>(\n \t\tsin_view_zenith * cos(azimuth),\n \t\tsin_view_zenith * sin(azimuth),\n \t\tcos_view_zenith\n \t);\n } else {\n let cos_light_view = -((uv.x * uv.x) * 2.0 - 1.0);\n return vec3<f32>(\n sin_view_zenith * cos_light_view,\n sin_view_zenith * sqrt(max(1.0 - cos_light_view * cos_light_view, 0