pub const FOG: &str = "// Volumetric fog: single source for every backend, both halves in one file.\n//\n// Frostbite-style. Each frame `fog_froxel_kernel` populates a screen-aligned 3D\n// RGBA16F volume of (scattered_rgb, 1 - T) across the view frustum, and the\n// fullscreen `fog_fragment` samples it by (screen_uv, view_z) instead of\n// marching per pixel. The scatter integral is the same for every pixel inside a\n// froxel column, so the per-slice work (density + CSM shadow tap +\n// Henyey-Greenstein phase) amortises across many pixels -- which is also what\n// buys the per-slice sun shadowing an inline ray-march could not afford at 32\n// shadow taps per pixel.\n//\n// Z distribution is linear from z_near to z_far (= fog.max_distance). Log-Z\n// would put more samples near the camera; that is a follow-up.\n//\n// The two halves share `FogParams` and `FogFroxelParams`, which is why they\n// move as one unit: splitting them across languages would leave two\n// unguarded copies of both structs. One entry per compile (FOG_FROXEL /\n// FOG_FRAGMENT) so each variant declares only the resources it binds.\n//\n// Divergences the port settles:\n//\n// * The depth texture is fetched by integer coordinate on all three backends\n// and never sampled, so it is declared as a plain texture and Metal keeps\n// binding no sampler for it. The froxel volume IS filtered, so it is a\n// combined texture-sampler and the host now supplies that sampler state\n// where Metal previously baked a constexpr one into the shader -- the\n// other two backends already bound theirs from the host. Vulkan\'s\n// COMBINED_IMAGE_SAMPLER descriptors are unchanged either way: a combined\n// descriptor satisfies a plain sampled-image declaration.\n// * USE_MSAA is a HOST difference, not a target one -- Vulkan reads the\n// multisampled main depth while Metal always reads the resolved\n// single-sample copy -- so it is a define rather than a __target_switch.\n// * The fullscreen vertex is the shared `fullscreen_vertex`, which settles\n// the triangle winding Metal\'s own fog vertex wound the other way (the\n// pass does no culling, so this was always harmless).\n//\n// FogParams (176 B) and FogFroxelParams (96 B) mirror gfx::render_types; their\n// vec3 + scalar pairs are spelled as float4 / uint4 because MSL sizes a\n// constant-buffer float3 at 16 bytes, so a literal transcription would shift\n// every following field on Metal alone.\n\nstatic const uint NUM_SHADOW_CASCADES = 4u;\n\nstruct FogParams\n{\n // Inverse view-projection: reconstructs world position from depth.\n float4x4 inv_vp;\n // Linear-space fog tint (RGB); alpha unused.\n float4 color;\n // xyz = world-space camera position, w = pad.\n float4 cam_pos;\n // xyz = first directional light\'s world-space direction (toward the light),\n // w = pad.\n float4 sun_dir;\n // xyz = that light\'s colour pre-multiplied with its intensity, w = pad.\n float4 sun_color;\n // Base density at height_reference, per world unit.\n float density;\n // Exponential height-falloff rate; 0 = homogeneous medium.\n float height_falloff;\n // World-space Y at which density equals `density`.\n float height_reference;\n float max_distance;\n // Henyey-Greenstein anisotropy in (-0.95, 0.95).\n float phase_g;\n // Ambient (sky-side) scattering, added isotropically each slab.\n float ambient;\n // Width / height of the HDR resolve in pixels.\n float2 viewport;\n float inv_max_distance;\n float _pad3a;\n float _pad3b;\n float _pad3c;\n};\n\nstruct FogFroxelParams\n{\n // World -> view, for view-space depth.\n float4x4 view;\n // xyz = volume extents along screen-x / screen-y / view-z, w = pad.\n uint4 froxel_dims;\n // Camera near plane, in view units.\n float z_near;\n // Far edge of the volume: FogSettings.max_distance.\n float z_far;\n float _pad0;\n float _pad1;\n};\n\n#ifdef FOG_FROXEL\n\nstruct ShadowUniforms\n{\n float4x4 light_vps[NUM_SHADOW_CASCADES];\n float4 cascade_splits;\n uint active_cascades;\n uint _pad0;\n uint _pad1;\n uint _pad2;\n};\n\n[[vk::binding(0, 0)]] ConstantBuffer<FogParams> fog : register(b0);\n[[vk::binding(1, 0)]] ConstantBuffer<FogFroxelParams> froxel : register(b1);\n[[vk::binding(2, 0)]] ConstantBuffer<ShadowUniforms> shadow_uni : register(b2);\n// Cascaded shadow depth array, sampled with depth compare for the per-slab tap.\n// Combined where the descriptor model fuses the pair (Vulkan\'s layout uses\n// COMBINED_IMAGE_SAMPLER; Metal lowers the pair itself), split on DXIL, where\n// SampleCmp on a combined type mis-lowers -- the same split main_bindless.slang\n// carries for the same reason.\n#ifdef DXIL_SPLIT\n[[vk::binding(3, 0)]] Texture2DArray<float4> shadow_map_t;\nSamplerComparisonState shadow_map_s;\n#else\n[[vk::binding(3, 0)]] Sampler2DArray<float4> shadow_map;\n#endif\n// Destination volume: camera->slice integrated (scattered, 1 - T). Write-only,\n// so Metal gets access::write rather than a read_write qualifier RGBA16Float\n// would need read-write texture support for.\n[[vk::binding(4, 0)]] [format(\"rgba16f\")] WTexture3D<float4> fog_volume;\n\n#else\n\n[[vk::binding(0, 0)]] ConstantBuffer<FogParams> fog : register(b0);\n[[vk::binding(2, 0)]] ConstantBuffer<FogFroxelParams> froxel : register(b1);\n#if USE_MSAA\n[[vk::binding(1, 0)]] Texture2DMS<float> scene_depth;\n#else\n[[vk::binding(1, 0)]] Texture2D<float> scene_depth;\n#endif\n[[vk::binding(3, 0)]] Sampler3D<float4> fog_volume;\n\n#endif\n\n#ifdef FOG_FROXEL\n\n// Closed-form Henyey-Greenstein phase function. `cos_theta` is the cosine of\n// the angle between the view ray and the direction toward the sun; positive `g`\n// gives forward scattering.\nfloat henyey_greenstein(float cos_theta, float g)\n{\n float g2 = g * g;\n float denom = 1.0 + g2 - 2.0 * g * cos_theta;\n return (1.0 - g2) / (4.0 * 3.14159265358979 * pow(max(denom, 1e-5), 1.5));\n}\n\n// Cascade-aware single-sample shadow tap. No PCF: the trilinear sample at\n// fragment-shader time smooths the result. Returns 1.0 (fully lit) outside every\n// cascade, matching the main shader\'s fall-through.\nfloat fog_shadow_factor(float3 world_pos, float view_depth)\n{\n uint cascade = NUM_SHADOW_CASCADES;\n if (view_depth < shadow_uni.cascade_splits[0]) cascade = 0u;\n else if (view_depth < shadow_uni.cascade_splits[1]) cascade = 1u;\n else if (view_depth < shadow_uni.cascade_splits[2]) cascade = 2u;\n else if (view_depth < shadow_uni.cascade_splits[3]) cascade = 3u;\n if (cascade >= shadow_uni.active_cascades)\n {\n return 1.0;\n }\n\n float4 light_clip = mul(shadow_uni.light_vps[cascade], float4(world_pos, 1.0));\n float3 ndc = light_clip.xyz / light_clip.w;\n // Flip Y: the shadow pass rasterises through a negative-height viewport on\n // Vulkan and has a top-left origin on Metal / DirectX, so the sampled UV\n // mirrors Y on every backend alike.\n float2 uv = float2(ndc.x * 0.5 + 0.5, -ndc.y * 0.5 + 0.5);\n if (any(uv < 0.0) || any(uv > 1.0) || ndc.z < 0.0 || ndc.z > 1.0)\n {\n return 1.0;\n }\n float bias = 0.0015 * (1.0 + float(cascade) * 0.7);\n float3 uv_layer = float3(uv, float(cascade));\n // Explicit LOD, not the implicit one the hand-written shaders used. A\n // compute kernel has no fragment quad to derive a mip from -- neighbouring\n // threads are unrelated froxel columns -- so an implicit-LOD compare makes\n // slangc declare SPV_KHR_compute_shader_derivatives, which MoltenVK does not\n // support. The cascade array has one mip, so level zero is the same tap.\n#ifdef DXIL_SPLIT\n return shadow_map_t.SampleCmpLevelZero(shadow_map_s, uv_layer, ndc.z - bias);\n#else\n return shadow_map.SampleCmpLevelZero(uv_layer, ndc.z - bias);\n#endif\n}\n\n// World-space position at a froxel centre. `z_slice` is a floating-point slab\n// index; the caller offsets it by an interleaved-gradient-noise jitter.\nfloat3 froxel_to_world(uint x, uint y, float z_slice)\n{\n float2 uv = float2(float(x) + 0.5, float(y) + 0.5)\n / float2(float(froxel.froxel_dims.x), float(froxel.froxel_dims.y));\n float2 ndc_xy = float2(uv.x * 2.0 - 1.0, -(uv.y * 2.0 - 1.0));\n\n // Linear-Z distribution across [z_near, z_far].\n float view_z = lerp(froxel.z_near, froxel.z_far,\n (z_slice + 0.5) / float(froxel.froxel_dims.z));\n\n // Un-project a far-plane direction, then walk that ray to the requested\n // view-space z. Cheaper than inverting a per-froxel matrix and correct for\n // any perspective projection.\n float4 clip_far = float4(ndc_xy, 1.0, 1.0);\n float4 world_far = mul(fog.inv_vp, clip_far);\n world_far /= world_far.w;\n float3 ray = normalize(world_far.xyz - fog.cam_pos.xyz);\n\n // Projection of `ray` onto the view-forward axis. `view` is world->view and\n // positive view depth is -z, so view-forward in world space is the negated\n // third ROW -- `view[2].xyz` under Slang\'s row-first subscript, which is the\n // (view[0][2], view[1][2], view[2][2]) the column-indexed MSL and GLSL\n // spell.\n float3 view_fwd = -froxel.view[2].xyz;\n float forward = max(dot(ray, view_fwd), 1e-4);\n return fog.cam_pos.xyz + ray * (view_z / forward);\n}\n\n[shader(\"compute\")]\n[numthreads(8, 8, 1)]\nvoid fog_froxel_kernel(uint3 tid : SV_DispatchThreadID)\n{\n if (tid.x >= froxel.froxel_dims.x || tid.y >= froxel.froxel_dims.y)\n {\n return;\n }\n\n // Interleaved gradient noise: a per-(x, y) tile offset so neighbouring\n // columns sample density and shadows at slightly different Z. Trilinear\n // filtering at sample time plus TAA smear it into smooth illumination.\n float2 tile_xy = float2(float(tid.x), float(tid.y));\n float ign = frac(52.9829189 * frac(dot(tile_xy, float2(0.06711056, 0.00583715))));\n\n // Ray direction at the (x, y, 0) froxel. Within a column the direction is\n // approximately constant across Z (small-FOV approximation), so the phase\n // term is evaluated once.\n float3 col_world = froxel_to_world(tid.x, tid.y, 0.0);\n float3 ray_dir = normalize(col_world - fog.cam_pos.xyz);\n float cos_theta = dot(ray_dir, normalize(fog.sun_dir.xyz));\n float phase = henyey_greenstein(cos_theta, fog.phase_g);\n\n // Ambient is isotropic (no phase modulation) so the medium still reads in\n // shaded regions.\n float3 sun_inscatter_unshadowed = fog.sun_color.xyz * phase * fog.color.rgb;\n float3 ambient_inscatter = fog.color.rgb * fog.ambient;\n\n float step_len = (froxel.z_far - froxel.z_near) / float(froxel.froxel_dims.z);\n\n float3 accumulated = float3(0.0);\n float transmittance = 1.0;\n\n for (uint z = 0u; z < froxel.froxel_dims.z; ++z)\n {\n // Jittered slab centre. The slab integral stays exact in the\n // constant-density limit because `tau` uses the full slab width; only\n // the sample point shifts.\n float z_jittered = float(z) + ign - 0.5;\n float3 pos = froxel_to_world(tid.x, tid.y, z_jittered);\n\n // Exponential height falloff, matching the inline ray-march path.\n float h = pos.y - fog.height_reference;\n float local_density = fog.density * exp(-max(h, -50.0) * fog.height_falloff);\n\n float slab_view_z = lerp(froxel.z_near, froxel.z_far,\n (z_jittered + 0.5) / float(froxel.froxel_dims.z));\n float shad = fog_shadow_factor(pos, slab_view_z);\n\n // Per-slab Beer-Lambert plus an analytic energy-conserving in-scatter.\n float tau = local_density * step_len;\n float slab_T = exp(-tau);\n float3 inscatter = sun_inscatter_unshadowed * shad + ambient_inscatter;\n accumulated += transmittance * (1.0 - slab_T) * inscatter;\n transmittance *= slab_T;\n\n // Each slice carries the camera->slice integral, so a sample at any\n // depth is the right value without a second accumulation pass.\n float4 stored = float4(accumulated, 1.0 - transmittance);\n fog_volume.Store(uint3(tid.x, tid.y, z), stored);\n\n // Once the medium is almost opaque the remaining slices keep the\n // saturated value; any sample past this point reads the same pair.\n if (transmittance < 0.005)\n {\n transmittance = 0.0;\n for (uint zz = z + 1u; zz < froxel.froxel_dims.z; ++zz)\n {\n fog_volume.Store(uint3(tid.x, tid.y, zz), stored);\n }\n break;\n }\n }\n}\n\n#else\n\n// The fragment reads no varying: `uv` has to come from SV_Position, because the\n// shared vertex\'s varying is the texture-space map (unflipped on Vulkan, which\n// rasterises through a negative-height viewport) while both uses here -- the\n// depth unprojection and the froxel volume, which the kernel fills in screen\n// tile order -- want the framebuffer-relative one.\n[shader(\"fragment\")]\nfloat4 fog_fragment(\n#ifdef DXIL_STAGE_PACKING\n // Declared, never read. D3D packs a stage signature in declaration order\n // and links the two stages by matching semantic *and* register, so a\n // fragment taking SV_Position alone reads it from register 0 while the\n // shared fullscreen vertex hands TEXCOORD0 out there and SV_Position from\n // register 1 -- the PSO fails to create with a `mismatched hardware\n // registers` linkage error. Taking the varying restores the packing; the\n // body still derives its own uv below, for the reason above. Metal and\n // Vulkan do not need it (both varyings carry an attribute or an explicit\n // location) and are byte-identical without it, so the gate keeps them so.\n [[vk::location(0)]] float2 vertex_uv : TEXCOORD0,\n#endif\n float4 sv_pos : SV_Position) : SV_Target\n{\n int2 pixel = int2(sv_pos.xy);\n if (pixel.x < 0 || pixel.y < 0 ||\n pixel.x >= int(fog.viewport.x) || pixel.y >= int(fog.viewport.y))\n {\n discard;\n }\n#if USE_MSAA\n float depth = scene_depth.Load(pixel, 0);\n#else\n float depth = scene_depth.Load(int3(pixel, 0));\n#endif\n\n float2 uv = sv_pos.xy / fog.viewport;\n float2 ndc_xy = float2(uv.x * 2.0 - 1.0, -(uv.y * 2.0 - 1.0));\n\n // Reconstruct view-space depth at the pixel. depth == 1.0 (skybox, or never\n // written) maps to the far edge of the volume, so the sky takes fog\n // integrated across the whole volume.\n float view_z;\n if (depth < 1.0)\n {\n float4 world = mul(fog.inv_vp, float4(ndc_xy, depth, 1.0));\n world /= world.w;\n view_z = -mul(froxel.view, float4(world.xyz, 1.0)).z;\n }\n else\n {\n view_z = froxel.z_far;\n }\n\n // Normalised volume W. Clamped so the skybox and anything past the volume\'s\n // far edge sample the fully-integrated last slice.\n float z01 = saturate((view_z - froxel.z_near) / max(froxel.z_far - froxel.z_near, 1e-4));\n\n // The volume already stores camera->slice integrated (scattered, 1 - T), so\n // the trilinear sample IS the output blend pair.\n return fog_volume.SampleLevel(float3(uv, z01), 0);\n}\n\n#endif\n";Expand description
fog.slang.