@bornengine/engine 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
//! Environment: GGX prefilter, sky pass, aerial perspective.
|
|
2
|
+
//! Split from renderer/shaders.rs.
|
|
3
|
+
|
|
4
|
+
pub(in crate::renderer) const PREFILTER_SHADER_WGSL: &str = "
|
|
5
|
+
struct PrefilterUniforms {
|
|
6
|
+
/// x = roughness (∈ [0, 1]), y = sample count, zw = mip resolution
|
|
7
|
+
/// in pixels (used for fragCoord → UV conversion).
|
|
8
|
+
params: vec4<f32>,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
@group(0) @binding(0) var<uniform> u: PrefilterUniforms;
|
|
12
|
+
@group(0) @binding(1) var src_tex: texture_2d<f32>;
|
|
13
|
+
@group(0) @binding(2) var src_samp: sampler;
|
|
14
|
+
|
|
15
|
+
const PI: f32 = 3.14159265;
|
|
16
|
+
|
|
17
|
+
fn radical_inverse_vdc(in_bits: u32) -> f32 {
|
|
18
|
+
var bits = in_bits;
|
|
19
|
+
bits = (bits << 16u) | (bits >> 16u);
|
|
20
|
+
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
|
|
21
|
+
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
|
|
22
|
+
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
|
|
23
|
+
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
|
|
24
|
+
return f32(bits) * 2.3283064e-10;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn hammersley(i: u32, n: u32) -> vec2<f32> {
|
|
28
|
+
return vec2<f32>(f32(i) / f32(n), radical_inverse_vdc(i));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn importance_sample_ggx(xi: vec2<f32>, n: vec3<f32>, roughness: f32) -> vec3<f32> {
|
|
32
|
+
let a = roughness * roughness;
|
|
33
|
+
let phi = 2.0 * PI * xi.x;
|
|
34
|
+
let cos_theta = sqrt((1.0 - xi.y) / (1.0 + (a*a - 1.0) * xi.y));
|
|
35
|
+
let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0));
|
|
36
|
+
let h_local = vec3<f32>(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta);
|
|
37
|
+
|
|
38
|
+
let up = select(vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(0.0, 0.0, 1.0), abs(n.z) < 0.999);
|
|
39
|
+
let t = normalize(cross(up, n));
|
|
40
|
+
let b = cross(n, t);
|
|
41
|
+
return normalize(t * h_local.x + b * h_local.y + n * h_local.z);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn dir_to_uv(dir: vec3<f32>) -> vec2<f32> {
|
|
45
|
+
let d = normalize(dir);
|
|
46
|
+
let theta = acos(clamp(d.y, -1.0, 1.0));
|
|
47
|
+
let phi = atan2(d.z, d.x);
|
|
48
|
+
let raw_u = phi / (2.0 * PI);
|
|
49
|
+
return vec2<f32>(raw_u - floor(raw_u), theta / PI);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn uv_to_dir(uv: vec2<f32>) -> vec3<f32> {
|
|
53
|
+
let phi = uv.x * 2.0 * PI;
|
|
54
|
+
let theta = uv.y * PI;
|
|
55
|
+
let sin_theta = sin(theta);
|
|
56
|
+
return vec3<f32>(sin_theta * cos(phi), cos(theta), sin_theta * sin(phi));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@vertex
|
|
60
|
+
fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {
|
|
61
|
+
let x = f32((vid & 1u) * 4u) - 1.0;
|
|
62
|
+
let y = f32((vid >> 1u) * 4u) - 1.0;
|
|
63
|
+
return vec4<f32>(x, y, 0.0, 1.0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@fragment
|
|
67
|
+
fn fs_main(@builtin(position) frag_pos: vec4<f32>) -> @location(0) vec4<f32> {
|
|
68
|
+
let mip_w = u.params.z;
|
|
69
|
+
let mip_h = u.params.w;
|
|
70
|
+
let uv = vec2<f32>(frag_pos.x / mip_w, frag_pos.y / mip_h);
|
|
71
|
+
let n = uv_to_dir(uv);
|
|
72
|
+
let v = n; // Karis simplification
|
|
73
|
+
|
|
74
|
+
let n_samples = u32(u.params.y);
|
|
75
|
+
let roughness = u.params.x;
|
|
76
|
+
var color = vec3<f32>(0.0);
|
|
77
|
+
var weight = 0.0;
|
|
78
|
+
|
|
79
|
+
for (var i = 0u; i < n_samples; i++) {
|
|
80
|
+
let xi = hammersley(i, n_samples);
|
|
81
|
+
let h = importance_sample_ggx(xi, n, roughness);
|
|
82
|
+
let l = normalize(2.0 * dot(v, h) * h - v);
|
|
83
|
+
let n_dot_l = max(dot(n, l), 0.0);
|
|
84
|
+
if (n_dot_l > 0.0) {
|
|
85
|
+
color += textureSampleLevel(src_tex, src_samp, dir_to_uv(l), 0.0).rgb * n_dot_l;
|
|
86
|
+
weight += n_dot_l;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return vec4<f32>(color / max(weight, 1e-4), 1.0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Diffuse irradiance convolution (cosine-weighted). Used to populate
|
|
93
|
+
// the env mip chain's smallest mip — the scene shader samples that
|
|
94
|
+
// mip for IBL diffuse. Cosine-weighted importance sampling means the
|
|
95
|
+
// per-sample weight (cos θ / π) cancels the PDF, so we can just
|
|
96
|
+
// average the env samples directly. Much closer to a proper diffuse
|
|
97
|
+
// irradiance map than 'GGX with roughness = 1' would be.
|
|
98
|
+
@fragment
|
|
99
|
+
fn fs_diffuse(@builtin(position) frag_pos: vec4<f32>) -> @location(0) vec4<f32> {
|
|
100
|
+
let mip_w = u.params.z;
|
|
101
|
+
let mip_h = u.params.w;
|
|
102
|
+
let uv = vec2<f32>(frag_pos.x / mip_w, frag_pos.y / mip_h);
|
|
103
|
+
let n = uv_to_dir(uv);
|
|
104
|
+
|
|
105
|
+
let up = select(vec3<f32>(1.0, 0.0, 0.0), vec3<f32>(0.0, 0.0, 1.0), abs(n.z) < 0.999);
|
|
106
|
+
let t = normalize(cross(up, n));
|
|
107
|
+
let b = cross(n, t);
|
|
108
|
+
|
|
109
|
+
let n_samples = u32(u.params.y);
|
|
110
|
+
var color = vec3<f32>(0.0);
|
|
111
|
+
|
|
112
|
+
for (var i = 0u; i < n_samples; i++) {
|
|
113
|
+
let xi = hammersley(i, n_samples);
|
|
114
|
+
// Cosine-weighted hemisphere sample (Malley's method).
|
|
115
|
+
let phi = 2.0 * PI * xi.x;
|
|
116
|
+
let cos_theta = sqrt(1.0 - xi.y);
|
|
117
|
+
let sin_theta = sqrt(xi.y);
|
|
118
|
+
let l_local = vec3<f32>(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta);
|
|
119
|
+
let l = t * l_local.x + b * l_local.y + n * l_local.z;
|
|
120
|
+
var sample = textureSampleLevel(src_tex, src_samp, dir_to_uv(l), 0.0).rgb;
|
|
121
|
+
// Firefly clamp per env sample. Raw outdoor.hdr has the sun
|
|
122
|
+
// disc at luma 1000+; at N samples per output texel the
|
|
123
|
+
// 1-in-N chance of a sample hitting the sun leaves a bright
|
|
124
|
+
// patch in the 'diffuse' irradiance map at every normal
|
|
125
|
+
// direction that aims near it. Capping each sample at luma
|
|
126
|
+
// 50 bounds the estimator's variance so the convolved output
|
|
127
|
+
// is actually smooth — which is what fs_main_scene expects
|
|
128
|
+
// when it multiplies by base_color for the IBL diffuse term.
|
|
129
|
+
// The Sponza floor's per-pixel normal-map variation was
|
|
130
|
+
// picking different 'bright spots' in the unsmoothed
|
|
131
|
+
// irradiance map, producing exactly the white speckle on
|
|
132
|
+
// the sunlit stone.
|
|
133
|
+
let sample_luma = dot(sample, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
134
|
+
let cap = 50.0;
|
|
135
|
+
if (sample_luma > cap) {
|
|
136
|
+
sample = sample * (cap / sample_luma);
|
|
137
|
+
}
|
|
138
|
+
color += sample;
|
|
139
|
+
}
|
|
140
|
+
return vec4<f32>(color / f32(n_samples), 1.0);
|
|
141
|
+
}
|
|
142
|
+
";
|
|
143
|
+
|
|
144
|
+
pub(in crate::renderer) const SKY_SHADER_WGSL: &str = "
|
|
145
|
+
struct SkyUniforms {
|
|
146
|
+
// Camera world basis (right, up, forward) and screen scale.
|
|
147
|
+
// Reconstructing view direction from these is more numerically
|
|
148
|
+
// robust than inverting the VP matrix and divides — and avoids
|
|
149
|
+
// edge cases like degenerate w divisions at the far plane.
|
|
150
|
+
right: vec4<f32>, // xyz = right*tan(fovy/2)*aspect; w unused
|
|
151
|
+
up: vec4<f32>, // xyz = up*tan(fovy/2); w unused
|
|
152
|
+
forward: vec4<f32>, // xyz = forward (unit); w unused
|
|
153
|
+
intensity: vec4<f32>, // x = multiplier; yzw padding
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
@group(0) @binding(0) var<uniform> u: SkyUniforms;
|
|
157
|
+
@group(0) @binding(1) var env_tex: texture_2d<f32>;
|
|
158
|
+
@group(0) @binding(2) var env_samp: sampler;
|
|
159
|
+
|
|
160
|
+
const PI: f32 = 3.14159265;
|
|
161
|
+
|
|
162
|
+
struct VsOut {
|
|
163
|
+
@builtin(position) clip_pos: vec4<f32>,
|
|
164
|
+
@location(0) ndc: vec2<f32>,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
@vertex
|
|
168
|
+
fn sky_vs(@builtin(vertex_index) vid: u32) -> VsOut {
|
|
169
|
+
// Single oversized triangle covering [-1,1]^2:
|
|
170
|
+
// vid 0 → (-1, -1)
|
|
171
|
+
// vid 1 → ( 3, -1)
|
|
172
|
+
// vid 2 → (-1, 3)
|
|
173
|
+
let x = f32((vid & 1u) * 4u) - 1.0;
|
|
174
|
+
let y = f32((vid >> 1u) * 4u) - 1.0;
|
|
175
|
+
var out: VsOut;
|
|
176
|
+
out.clip_pos = vec4<f32>(x, y, 1.0, 1.0);
|
|
177
|
+
out.ndc = vec2<f32>(x, y);
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn aces_tone(c: vec3<f32>) -> vec3<f32> {
|
|
182
|
+
let a = 2.51;
|
|
183
|
+
let b = 0.03;
|
|
184
|
+
let cc = 2.43;
|
|
185
|
+
let d = 0.59;
|
|
186
|
+
let e = 0.14;
|
|
187
|
+
return clamp((c * (c * a + b)) / (c * (c * cc + d) + e), vec3<f32>(0.0), vec3<f32>(1.0));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn linear_to_srgb_v(c: vec3<f32>) -> vec3<f32> {
|
|
191
|
+
let cutoff = vec3<f32>(0.0031308);
|
|
192
|
+
let lo = c * 12.92;
|
|
193
|
+
let hi = 1.055 * pow(c, vec3<f32>(1.0 / 2.4)) - 0.055;
|
|
194
|
+
return select(hi, lo, c <= cutoff);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
struct SkyOut {
|
|
198
|
+
@location(0) color: vec4<f32>,
|
|
199
|
+
@location(1) material: vec2<f32>,
|
|
200
|
+
@location(2) velocity: vec2<f32>,
|
|
201
|
+
@location(3) albedo: vec4<f32>,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
@fragment
|
|
205
|
+
fn sky_fs(in: VsOut) -> SkyOut {
|
|
206
|
+
// View direction = forward + ndc.x * (right*tan*aspect)
|
|
207
|
+
// + ndc.y * (up*tan)
|
|
208
|
+
// The right/up vectors already have the scale baked in.
|
|
209
|
+
let dir = normalize(u.forward.xyz + in.ndc.x * u.right.xyz + in.ndc.y * u.up.xyz);
|
|
210
|
+
|
|
211
|
+
// Equirectangular UV — must match bloom-reference exactly:
|
|
212
|
+
// u = (phi / 2π) wrapped to [0, 1) → +X direction at u=0
|
|
213
|
+
// v = theta / π → +Y at v=0
|
|
214
|
+
// Earlier `phi/2π + 0.5` gave a 180° rotation that put cloud
|
|
215
|
+
// patterns on the wrong side of the helmet vs the reference.
|
|
216
|
+
//
|
|
217
|
+
// Below-horizon clamp: most outdoor HDRIs (including
|
|
218
|
+
// assets/outdoor.hdr) only contain sky in the upper
|
|
219
|
+
// hemisphere — the lower half mirrors the upper because the
|
|
220
|
+
// photographer captured from a flat/reflective floor. Without
|
|
221
|
+
// a clamp, the sky-dome render shows the mirror when the
|
|
222
|
+
// camera looks below the horizon (e.g. standing on the
|
|
223
|
+
// Sponza roof, any pitch-down shot). Clamp v at 0.5 so
|
|
224
|
+
// below-horizon directions re-sample the horizon ring,
|
|
225
|
+
// smoothly fading to a 'hazy ground' appearance.
|
|
226
|
+
let theta = acos(clamp(dir.y, -1.0, 1.0));
|
|
227
|
+
let phi = atan2(dir.z, dir.x);
|
|
228
|
+
let raw_u = phi / (2.0 * PI);
|
|
229
|
+
let u_coord = raw_u - floor(raw_u); // fract(); WGSL has no rem_euclid
|
|
230
|
+
let v_unclamped = theta / PI;
|
|
231
|
+
// Slightly below v=0.5 to sample the horizon ring and not
|
|
232
|
+
// clip to a single texel row; texture filter smooths over
|
|
233
|
+
// the last 2 % of vertical space.
|
|
234
|
+
let v_coord = min(v_unclamped, 0.5);
|
|
235
|
+
|
|
236
|
+
// Clamp u to half-texel inside [0,1] so the bilinear filter
|
|
237
|
+
// never reaches across the ±180° seam (u wraps 0↔1).
|
|
238
|
+
let tex_w = f32(textureDimensions(env_tex, 0).x);
|
|
239
|
+
let half_texel = 0.5 / tex_w;
|
|
240
|
+
let safe_u = clamp(u_coord, half_texel, 1.0 - half_texel);
|
|
241
|
+
|
|
242
|
+
let radiance = textureSample(env_tex, env_samp, vec2<f32>(safe_u, v_coord)).rgb * u.intensity.x;
|
|
243
|
+
// Output linear HDR radiance — the composite pass downstream does
|
|
244
|
+
// the ACES tonemap + sRGB encode in one place. Sky writes to the
|
|
245
|
+
// material G-buffer too: 0 metallic, 1 roughness — sky never
|
|
246
|
+
// reflects, never gets reflected from (well, it gets sampled by
|
|
247
|
+
// SSR via the HDR RT, but that's expected behavior).
|
|
248
|
+
// Sky is at infinity — zero velocity (stationary background).
|
|
249
|
+
// Sky albedo is zero — sky is the indirect-light source itself,
|
|
250
|
+
// so SSGI rays landing on sky pixels must not multiply by anything
|
|
251
|
+
// (otherwise the bounce would be tinted by background radiance,
|
|
252
|
+
// which is wrong for a directional distant light).
|
|
253
|
+
return SkyOut(vec4<f32>(radiance, 1.0), vec2<f32>(0.0, 1.0), vec2<f32>(0.0, 0.0), vec4<f32>(0.0));
|
|
254
|
+
}
|
|
255
|
+
";
|
|
256
|
+
|
|
257
|
+
// EN-005 V2 — aerial-perspective 3D LUT bake.
|
|
258
|
+
//
|
|
259
|
+
// Each voxel (NDC.x, NDC.y, depth-slice) holds the integrated in-
|
|
260
|
+
// scattered radiance and accumulated transmittance from the camera
|
|
261
|
+
// to a sample point along the view ray. scene_compose samples this
|
|
262
|
+
// in place of its 16-step volumetric fog march when procedural sky
|
|
263
|
+
// is on, giving per-pixel angular variation (sunset side warmer than
|
|
264
|
+
// the opposite horizon) at a fraction of the per-frame cost.
|
|
265
|
+
//
|
|
266
|
+
// Engine units assumed metres; atmosphere math runs in kilometres.
|
|
267
|
+
// Camera height above sea level is treated as 0.5 km regardless of
|
|
268
|
+
// actual y — same approximation the sky-view shader uses, since the
|
|
269
|
+
// atmosphere is far thicker than typical scene height variation.
|
|
270
|
+
|
|
271
|
+
pub(in crate::renderer) const AERIAL_PERSPECTIVE_SHADER_WGSL: &str = "
|
|
272
|
+
struct AerialParams {
|
|
273
|
+
// xyz = camera world position (metres), w = max distance (km)
|
|
274
|
+
cam_pos: vec4<f32>,
|
|
275
|
+
inv_vp: mat4x4<f32>,
|
|
276
|
+
// xyz = sun direction (unit), w = sun intensity scale
|
|
277
|
+
sun: vec4<f32>,
|
|
278
|
+
// x = rayleigh density mult, y = mie density mult, zw unused
|
|
279
|
+
knobs: vec4<f32>,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
@group(0) @binding(0) var<uniform> u: AerialParams;
|
|
283
|
+
@group(0) @binding(1) var transmittance_lut: texture_2d<f32>;
|
|
284
|
+
@group(0) @binding(2) var multi_scattering_lut: texture_2d<f32>;
|
|
285
|
+
@group(0) @binding(3) var lut_samp: sampler;
|
|
286
|
+
@group(0) @binding(4) var aerial_out: texture_storage_3d<rgba16float, write>;
|
|
287
|
+
|
|
288
|
+
const PI: f32 = 3.14159265;
|
|
289
|
+
const GROUND_R: f32 = 6360.0;
|
|
290
|
+
const ATMOS_TOP: f32 = 6460.0;
|
|
291
|
+
const RAY_H: f32 = 8.0;
|
|
292
|
+
const MIE_H: f32 = 1.2;
|
|
293
|
+
const RAY_SCAT: vec3<f32> = vec3<f32>(5.802e-3, 13.558e-3, 33.100e-3);
|
|
294
|
+
const MIE_SCAT: f32 = 3.996e-3;
|
|
295
|
+
const MIE_EXT: f32 = 4.440e-3;
|
|
296
|
+
const O3_ABS: vec3<f32> = vec3<f32>(0.650e-3, 1.881e-3, 0.085e-3);
|
|
297
|
+
const O3_PEAK: f32 = 25.0;
|
|
298
|
+
const O3_HALF: f32 = 15.0;
|
|
299
|
+
|
|
300
|
+
const MARCH_STEPS: u32 = 16u;
|
|
301
|
+
|
|
302
|
+
fn ray_density(alt: f32) -> f32 { return exp(-alt / RAY_H); }
|
|
303
|
+
fn mie_density(alt: f32) -> f32 { return exp(-alt / MIE_H); }
|
|
304
|
+
fn o3_density(alt: f32) -> f32 { return max(0.0, 1.0 - abs(alt - O3_PEAK) / O3_HALF); }
|
|
305
|
+
|
|
306
|
+
fn extinction(alt: f32) -> vec3<f32> {
|
|
307
|
+
let rd = ray_density(alt) * u.knobs.x;
|
|
308
|
+
let md = mie_density(alt) * u.knobs.y;
|
|
309
|
+
let od = o3_density(alt);
|
|
310
|
+
return RAY_SCAT * rd + vec3<f32>(MIE_EXT) * md + O3_ABS * od;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fn sample_transmittance(r: f32, mu: f32) -> vec3<f32> {
|
|
314
|
+
let v = clamp((r - GROUND_R) / (ATMOS_TOP - GROUND_R), 0.0, 1.0);
|
|
315
|
+
let uu = clamp((mu + 1.0) * 0.5, 0.0, 1.0);
|
|
316
|
+
return textureSampleLevel(transmittance_lut, lut_samp, vec2<f32>(uu, v), 0.0).rgb;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn sample_multi_scattering(r: f32, mu_s: f32) -> vec3<f32> {
|
|
320
|
+
let v = clamp((r - GROUND_R) / (ATMOS_TOP - GROUND_R), 0.0, 1.0);
|
|
321
|
+
let uu = clamp((mu_s + 1.0) * 0.5, 0.0, 1.0);
|
|
322
|
+
return textureSampleLevel(multi_scattering_lut, lut_samp, vec2<f32>(uu, v), 0.0).rgb;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
fn rayleigh_phase(cos_t: f32) -> f32 {
|
|
326
|
+
return (3.0 / (16.0 * PI)) * (1.0 + cos_t * cos_t);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn mie_phase(cos_t: f32) -> f32 {
|
|
330
|
+
let g = 0.8;
|
|
331
|
+
let g2 = g * g;
|
|
332
|
+
let num = 3.0 * (1.0 - g2) * (1.0 + cos_t * cos_t);
|
|
333
|
+
let den = 8.0 * PI * (2.0 + g2) * pow(max(1.0 + g2 - 2.0 * g * cos_t, 1e-6), 1.5);
|
|
334
|
+
return num / den;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@compute @workgroup_size(8, 8, 1)
|
|
338
|
+
fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
339
|
+
let dims = textureDimensions(aerial_out);
|
|
340
|
+
if (gid.x >= dims.x || gid.y >= dims.y || gid.z >= dims.z) { return; }
|
|
341
|
+
|
|
342
|
+
// Voxel → NDC. Note: WebGPU NDC has Y up; our render target had
|
|
343
|
+
// Y down in pixel space but inv_vp produces world space directly,
|
|
344
|
+
// so we use NDC y = 1 - 2*v to match (top of screen = +Y in NDC).
|
|
345
|
+
let uv = (vec2<f32>(f32(gid.x), f32(gid.y)) + vec2<f32>(0.5))
|
|
346
|
+
/ vec2<f32>(f32(dims.x), f32(dims.y));
|
|
347
|
+
let ndc_xy = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
|
348
|
+
|
|
349
|
+
// Reconstruct a far-plane world point through (ndc_xy, z=1). The
|
|
350
|
+
// ray from cam_pos through that point is the per-voxel view ray.
|
|
351
|
+
let far_h = u.inv_vp * vec4<f32>(ndc_xy, 1.0, 1.0);
|
|
352
|
+
let far_world = far_h.xyz / far_h.w;
|
|
353
|
+
let view_dir = normalize(far_world - u.cam_pos.xyz);
|
|
354
|
+
|
|
355
|
+
// Depth slicing — linear from camera out to max_dist_km. The
|
|
356
|
+
// (gid.z + 0.5) / dims.z maps 0..max evenly; near voxels resolve
|
|
357
|
+
// close haze, far voxels capture distant horizon scatter.
|
|
358
|
+
let max_dist_km = u.cam_pos.w;
|
|
359
|
+
let target_dist_km = (f32(gid.z) + 0.5) / f32(dims.z) * max_dist_km;
|
|
360
|
+
|
|
361
|
+
// March from camera to (camera + view_dir * target_dist_km).
|
|
362
|
+
// Atmosphere math is in km; the engine world-space view ray is
|
|
363
|
+
// unit-length so we scale step-distance directly.
|
|
364
|
+
let n_steps = MARCH_STEPS;
|
|
365
|
+
let step_km = target_dist_km / f32(n_steps);
|
|
366
|
+
let sun = normalize(u.sun.xyz);
|
|
367
|
+
|
|
368
|
+
// Sea-level approximation for camera radius — sky-view shader
|
|
369
|
+
// uses the same trick. Atmosphere is so much thicker than scene
|
|
370
|
+
// height variation that the per-altitude correction is < 1 LSB.
|
|
371
|
+
let r0 = GROUND_R + 0.5;
|
|
372
|
+
let mu = view_dir.y;
|
|
373
|
+
let mu_s_cam = sun.y;
|
|
374
|
+
let nu = dot(view_dir, sun);
|
|
375
|
+
let phase_r = rayleigh_phase(nu);
|
|
376
|
+
let phase_m = mie_phase(nu);
|
|
377
|
+
|
|
378
|
+
var transmittance = vec3<f32>(1.0);
|
|
379
|
+
var in_scatter = vec3<f32>(0.0);
|
|
380
|
+
for (var i = 0u; i < n_steps; i = i + 1u) {
|
|
381
|
+
let d = (f32(i) + 0.5) * step_km;
|
|
382
|
+
let r_d = sqrt(r0 * r0 + d * d + 2.0 * r0 * mu * d);
|
|
383
|
+
let alt = max(0.0, r_d - GROUND_R);
|
|
384
|
+
|
|
385
|
+
let mu_s_p = clamp((r0 * mu_s_cam + d * dot(view_dir, sun)) / r_d, -1.0, 1.0);
|
|
386
|
+
|
|
387
|
+
let rd = ray_density(alt) * u.knobs.x;
|
|
388
|
+
let md = mie_density(alt) * u.knobs.y;
|
|
389
|
+
let scat_r = RAY_SCAT * rd;
|
|
390
|
+
let scat_m = vec3<f32>(MIE_SCAT) * md;
|
|
391
|
+
|
|
392
|
+
let ext = extinction(alt);
|
|
393
|
+
let step_t = exp(-ext * step_km);
|
|
394
|
+
|
|
395
|
+
let t_sun = sample_transmittance(r_d, mu_s_p);
|
|
396
|
+
let single = (scat_r * phase_r + scat_m * phase_m) * t_sun;
|
|
397
|
+
let psi_ms = sample_multi_scattering(r_d, mu_s_p);
|
|
398
|
+
let multi = (scat_r + scat_m) * psi_ms * (1.0 / (4.0 * PI));
|
|
399
|
+
|
|
400
|
+
in_scatter = in_scatter + transmittance * (single + multi) * step_km;
|
|
401
|
+
transmittance = transmittance * step_t;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
in_scatter = in_scatter * u.sun.w;
|
|
405
|
+
|
|
406
|
+
// Mean transmittance — scene_compose applies it as a single
|
|
407
|
+
// scalar attenuation. Per-channel transmittance would be tighter
|
|
408
|
+
// physics but pushes us off the existing fog composite shape.
|
|
409
|
+
let mean_t = (transmittance.x + transmittance.y + transmittance.z) / 3.0;
|
|
410
|
+
|
|
411
|
+
textureStore(
|
|
412
|
+
aerial_out,
|
|
413
|
+
vec3<i32>(i32(gid.x), i32(gid.y), i32(gid.z)),
|
|
414
|
+
vec4<f32>(in_scatter, mean_t),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
";
|
|
418
|
+
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// EN-005 Phase 2 — Procedural sky (Hillaire 2020)
|
|
421
|
+
// ============================================================================
|
|
422
|
+
//
|
|
423
|
+
// Two shaders working off the LUTs baked in atmosphere_lut.rs:
|
|
424
|
+
//
|
|
425
|
+
// 1. SKY_VIEW_LUT_SHADER_WGSL — compute pass, dispatched on sun-move.
|
|
426
|
+
// Ray-marches every (azimuth, elevation) texel of a 2D radiance
|
|
427
|
+
// cache, accumulating Rayleigh + Mie + ozone single-scattering
|
|
428
|
+
// and reading the multi-scattering LUT for the bounce term.
|
|
429
|
+
//
|
|
430
|
+
// 2. PROCEDURAL_SKY_SHADER_WGSL — fullscreen render pass per frame.
|
|
431
|
+
// Reconstructs view direction per pixel, samples the sky-view
|
|
432
|
+
// LUT for sky radiance, draws the sun disk attenuated by
|
|
433
|
+
// transmittance. Slots into the existing HDR pass in place of
|
|
434
|
+
// `sky_pipeline` when procedural sky is enabled.
|
|
435
|
+
//
|
|
436
|
+
// Shared atmosphere parameters are inlined as `const`s in each shader
|
|
437
|
+
// rather than uniformed — they're physical constants, not user knobs.
|
|
438
|
+
// The runtime knobs (sun direction, density multipliers) live in the
|
|
439
|
+
// uniform blocks.
|
|
440
|
+
|
|
441
|
+
pub(in crate::renderer) const SKY_VIEW_LUT_SHADER_WGSL: &str = "
|
|
442
|
+
struct SkyViewParams {
|
|
443
|
+
// xyz = sun direction (world space, unit), w = sun intensity scale
|
|
444
|
+
sun: vec4<f32>,
|
|
445
|
+
// x = rayleigh density multiplier
|
|
446
|
+
// y = mie density multiplier
|
|
447
|
+
// z = ground albedo (grey)
|
|
448
|
+
// w = unused
|
|
449
|
+
knobs: vec4<f32>,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
@group(0) @binding(0) var<uniform> u: SkyViewParams;
|
|
453
|
+
@group(0) @binding(1) var transmittance_lut: texture_2d<f32>;
|
|
454
|
+
@group(0) @binding(2) var multi_scattering_lut: texture_2d<f32>;
|
|
455
|
+
@group(0) @binding(3) var lut_samp: sampler;
|
|
456
|
+
@group(0) @binding(4) var sky_view_out: texture_storage_2d<rgba16float, write>;
|
|
457
|
+
|
|
458
|
+
// --- Atmosphere constants (must match atmosphere_lut.rs) ---
|
|
459
|
+
const PI: f32 = 3.14159265;
|
|
460
|
+
const GROUND_R: f32 = 6360.0;
|
|
461
|
+
const ATMOS_TOP: f32 = 6460.0;
|
|
462
|
+
const RAY_H: f32 = 8.0;
|
|
463
|
+
const MIE_H: f32 = 1.2;
|
|
464
|
+
const RAY_SCAT: vec3<f32> = vec3<f32>(5.802e-3, 13.558e-3, 33.100e-3);
|
|
465
|
+
const MIE_SCAT: f32 = 3.996e-3;
|
|
466
|
+
const MIE_EXT: f32 = 4.440e-3;
|
|
467
|
+
const O3_ABS: vec3<f32> = vec3<f32>(0.650e-3, 1.881e-3, 0.085e-3);
|
|
468
|
+
const O3_PEAK: f32 = 25.0;
|
|
469
|
+
const O3_HALF: f32 = 15.0;
|
|
470
|
+
|
|
471
|
+
const VIEW_STEPS: u32 = 32u;
|
|
472
|
+
|
|
473
|
+
fn ray_density(alt: f32) -> f32 { return exp(-alt / RAY_H); }
|
|
474
|
+
fn mie_density(alt: f32) -> f32 { return exp(-alt / MIE_H); }
|
|
475
|
+
fn o3_density(alt: f32) -> f32 { return max(0.0, 1.0 - abs(alt - O3_PEAK) / O3_HALF); }
|
|
476
|
+
|
|
477
|
+
fn extinction(alt: f32) -> vec3<f32> {
|
|
478
|
+
let rd = ray_density(alt) * u.knobs.x;
|
|
479
|
+
let md = mie_density(alt) * u.knobs.y;
|
|
480
|
+
let od = o3_density(alt);
|
|
481
|
+
return RAY_SCAT * rd + vec3<f32>(MIE_EXT) * md + O3_ABS * od;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
fn ray_atmosphere_intersect(r: f32, mu: f32, sphere_r: f32) -> f32 {
|
|
485
|
+
// Returns the first positive intersection distance, or -1 if none.
|
|
486
|
+
let disc = r * r * (mu * mu - 1.0) + sphere_r * sphere_r;
|
|
487
|
+
if (disc < 0.0) { return -1.0; }
|
|
488
|
+
let sd = sqrt(disc);
|
|
489
|
+
let t1 = -r * mu - sd;
|
|
490
|
+
let t2 = -r * mu + sd;
|
|
491
|
+
if (t1 > 0.0) { return t1; }
|
|
492
|
+
if (t2 > 0.0) { return t2; }
|
|
493
|
+
return -1.0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
fn dist_to_boundary(r: f32, mu: f32) -> f32 {
|
|
497
|
+
let to_top = ray_atmosphere_intersect(r, mu, ATMOS_TOP);
|
|
498
|
+
let to_ground = ray_atmosphere_intersect(r, mu, GROUND_R);
|
|
499
|
+
if (to_ground > 0.0 && (to_top < 0.0 || to_ground < to_top)) {
|
|
500
|
+
return to_ground;
|
|
501
|
+
}
|
|
502
|
+
return max(to_top, 0.0);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// LUT mappings (must match atmosphere_lut.rs).
|
|
506
|
+
fn sample_transmittance(r: f32, mu: f32) -> vec3<f32> {
|
|
507
|
+
let v = clamp((r - GROUND_R) / (ATMOS_TOP - GROUND_R), 0.0, 1.0);
|
|
508
|
+
let uu = clamp((mu + 1.0) * 0.5, 0.0, 1.0);
|
|
509
|
+
return textureSampleLevel(transmittance_lut, lut_samp, vec2<f32>(uu, v), 0.0).rgb;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fn sample_multi_scattering(r: f32, mu_s: f32) -> vec3<f32> {
|
|
513
|
+
let v = clamp((r - GROUND_R) / (ATMOS_TOP - GROUND_R), 0.0, 1.0);
|
|
514
|
+
let uu = clamp((mu_s + 1.0) * 0.5, 0.0, 1.0);
|
|
515
|
+
return textureSampleLevel(multi_scattering_lut, lut_samp, vec2<f32>(uu, v), 0.0).rgb;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
fn rayleigh_phase(cos_t: f32) -> f32 {
|
|
519
|
+
return (3.0 / (16.0 * PI)) * (1.0 + cos_t * cos_t);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fn mie_phase(cos_t: f32) -> f32 {
|
|
523
|
+
// Cornette-Shanks, g=0.8 — standard for atmospheric Mie.
|
|
524
|
+
let g = 0.8;
|
|
525
|
+
let g2 = g * g;
|
|
526
|
+
let num = 3.0 * (1.0 - g2) * (1.0 + cos_t * cos_t);
|
|
527
|
+
let den = 8.0 * PI * (2.0 + g2) * pow(max(1.0 + g2 - 2.0 * g * cos_t, 1e-6), 1.5);
|
|
528
|
+
return num / den;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Decode (workgroup-local) coords into a world-space view direction.
|
|
532
|
+
// V1 uses linear (azimuth, elevation) — simple, slightly horizon-poor;
|
|
533
|
+
// future revision can switch to Hillaire's sin² mapping for sharper
|
|
534
|
+
// horizon detail.
|
|
535
|
+
fn sky_view_uv_to_dir(uv: vec2<f32>) -> vec3<f32> {
|
|
536
|
+
let azimuth = uv.x * 2.0 * PI;
|
|
537
|
+
let elevation = (uv.y - 0.5) * PI; // [-π/2, π/2]
|
|
538
|
+
let cos_e = cos(elevation);
|
|
539
|
+
return vec3<f32>(cos_e * cos(azimuth), sin(elevation), cos_e * sin(azimuth));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@compute @workgroup_size(8, 8, 1)
|
|
543
|
+
fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
544
|
+
let dims = textureDimensions(sky_view_out);
|
|
545
|
+
if (gid.x >= dims.x || gid.y >= dims.y) { return; }
|
|
546
|
+
|
|
547
|
+
let uv = (vec2<f32>(f32(gid.x), f32(gid.y)) + vec2<f32>(0.5)) / vec2<f32>(f32(dims.x), f32(dims.y));
|
|
548
|
+
let view_dir = sky_view_uv_to_dir(uv);
|
|
549
|
+
|
|
550
|
+
// V1: camera at sea level, atmosphere centred at origin. The y axis
|
|
551
|
+
// is up; r = GROUND_R + altitude. Phase 3 lifts this once the host
|
|
552
|
+
// can pass per-frame camera height.
|
|
553
|
+
let r0 = GROUND_R + 0.5; // 0.5 km above ground = typical eye height + atmospheric haze
|
|
554
|
+
let mu = view_dir.y; // cos(view-zenith)
|
|
555
|
+
let sun = normalize(u.sun.xyz);
|
|
556
|
+
let mu_s = sun.y;
|
|
557
|
+
let nu = dot(view_dir, sun); // cos(view-sun angle)
|
|
558
|
+
|
|
559
|
+
let total_dist = dist_to_boundary(r0, mu);
|
|
560
|
+
if (total_dist <= 0.0) {
|
|
561
|
+
textureStore(sky_view_out, vec2<i32>(i32(gid.x), i32(gid.y)), vec4<f32>(0.0, 0.0, 0.0, 1.0));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let dx = total_dist / f32(VIEW_STEPS);
|
|
566
|
+
var radiance = vec3<f32>(0.0);
|
|
567
|
+
var transmittance = vec3<f32>(1.0);
|
|
568
|
+
|
|
569
|
+
let phase_r = rayleigh_phase(nu);
|
|
570
|
+
let phase_m = mie_phase(nu);
|
|
571
|
+
|
|
572
|
+
for (var i: u32 = 0u; i < VIEW_STEPS; i = i + 1u) {
|
|
573
|
+
let d = (f32(i) + 0.5) * dx;
|
|
574
|
+
// Radius at sample point.
|
|
575
|
+
let r_d = sqrt(r0 * r0 + d * d + 2.0 * r0 * mu * d);
|
|
576
|
+
let alt = max(0.0, r_d - GROUND_R);
|
|
577
|
+
|
|
578
|
+
// Sun zenith cosine at the sample point (parallel-sun
|
|
579
|
+
// approximation — sun direction stays fixed in world space).
|
|
580
|
+
let mu_s_p = clamp((r0 * mu_s + d * dot(view_dir, sun)) / r_d, -1.0, 1.0);
|
|
581
|
+
|
|
582
|
+
let rd = ray_density(alt) * u.knobs.x;
|
|
583
|
+
let md = mie_density(alt) * u.knobs.y;
|
|
584
|
+
let scat_r = RAY_SCAT * rd;
|
|
585
|
+
let scat_m = vec3<f32>(MIE_SCAT) * md;
|
|
586
|
+
|
|
587
|
+
let ext = extinction(alt);
|
|
588
|
+
let step_t = exp(-ext * dx);
|
|
589
|
+
|
|
590
|
+
// Sun radiance at sample after travelling to top of atmosphere.
|
|
591
|
+
let t_sun = sample_transmittance(r_d, mu_s_p);
|
|
592
|
+
|
|
593
|
+
// Single-scattering — Rayleigh + Mie with their phase functions.
|
|
594
|
+
let single = (scat_r * phase_r + scat_m * phase_m) * t_sun;
|
|
595
|
+
|
|
596
|
+
// Multi-scattering — isotropic (1/4π) scaled by the LUT-baked
|
|
597
|
+
// bounce coefficient. Hillaire's energy-conserving form.
|
|
598
|
+
let psi_ms = sample_multi_scattering(r_d, mu_s_p);
|
|
599
|
+
let multi = (scat_r + scat_m) * psi_ms * (1.0 / (4.0 * PI));
|
|
600
|
+
|
|
601
|
+
let in_scatter = (single + multi) * dx;
|
|
602
|
+
// Analytic in-scattering integral: contribution × accumulated transmittance.
|
|
603
|
+
radiance = radiance + transmittance * in_scatter;
|
|
604
|
+
transmittance = transmittance * step_t;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
radiance = radiance * u.sun.w;
|
|
608
|
+
|
|
609
|
+
textureStore(
|
|
610
|
+
sky_view_out,
|
|
611
|
+
vec2<i32>(i32(gid.x), i32(gid.y)),
|
|
612
|
+
vec4<f32>(radiance, 1.0),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
";
|
|
616
|
+
|
|
617
|
+
// EN-005 Phase 3 — sky-view LUT → equirect HDR re-projection. Drives
|
|
618
|
+
// the existing GGX prefilter chain (which is the IBL specular source
|
|
619
|
+
// for PBR materials), so material reflections track the procedural
|
|
620
|
+
// sky as the sun moves.
|
|
621
|
+
//
|
|
622
|
+
// Sky-view LUT v-axis goes nadir→zenith (0 → 1); prefilter equirect
|
|
623
|
+
// v-axis goes zenith→nadir (0 → 1). The only correction needed is
|
|
624
|
+
// `sky_view_v = 1 - equirect_v`. Destination dimensions arrive in a
|
|
625
|
+
// small uniform so the shader can map gl_FragCoord to UV (the
|
|
626
|
+
// existing prefilter shader uses the same pattern).
|
|
627
|
+
|
|
628
|
+
pub(in crate::renderer) const EQUIRECT_FROM_SKY_VIEW_SHADER_WGSL: &str = "
|
|
629
|
+
struct BakeParams {
|
|
630
|
+
// x = dest width, y = dest height, zw unused
|
|
631
|
+
dims: vec4<f32>,
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
@group(0) @binding(0) var<uniform> u: BakeParams;
|
|
635
|
+
@group(0) @binding(1) var sky_view_lut: texture_2d<f32>;
|
|
636
|
+
@group(0) @binding(2) var lut_samp: sampler;
|
|
637
|
+
|
|
638
|
+
@vertex
|
|
639
|
+
fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {
|
|
640
|
+
let x = f32((vid & 1u) * 4u) - 1.0;
|
|
641
|
+
let y = f32((vid >> 1u) * 4u) - 1.0;
|
|
642
|
+
return vec4<f32>(x, y, 0.0, 1.0);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
@fragment
|
|
646
|
+
fn fs_main(@builtin(position) frag_pos: vec4<f32>) -> @location(0) vec4<f32> {
|
|
647
|
+
// Equirect destination UV (matches `prefilter`'s `dir_to_uv`
|
|
648
|
+
// convention: u = phi/(2π), v = theta/π where theta is from zenith).
|
|
649
|
+
let uv = vec2<f32>(frag_pos.x / u.dims.x, frag_pos.y / u.dims.y);
|
|
650
|
+
|
|
651
|
+
// Convert to sky-view LUT UV — only the v axis differs:
|
|
652
|
+
// sky-view: v ∈ [0, 1] ↔ elevation ∈ [-π/2, π/2] (0 = nadir, 1 = zenith)
|
|
653
|
+
// equirect: v ∈ [0, 1] ↔ theta ∈ [0, π] (0 = zenith, 1 = nadir)
|
|
654
|
+
// → sky_view_v = 1 - equirect_v, u stays the same.
|
|
655
|
+
let lut_uv = vec2<f32>(uv.x, 1.0 - uv.y);
|
|
656
|
+
return textureSampleLevel(sky_view_lut, lut_samp, lut_uv, 0.0);
|
|
657
|
+
}
|
|
658
|
+
";
|
|
659
|
+
|
|
660
|
+
// The cloud deck is prepended verbatim rather than `#include`d: this shader is a
|
|
661
|
+
// raw source const compiled straight to wgpu, and does not run through the
|
|
662
|
+
// material preprocessor. Same file either way, so the sky and the ground cannot
|
|
663
|
+
// drift apart — which is exactly how they got out of sync in the first place.
|
|
664
|
+
pub(in crate::renderer) const PROCEDURAL_SKY_SHADER_WGSL: &str = concat!(
|
|
665
|
+
include_str!("../../../shaders/common/clouds.wgsl"),
|
|
666
|
+
r#"
|
|
667
|
+
struct SkyUniforms {
|
|
668
|
+
// xyz = camera basis (pre-scaled by tan(fovy/2) and aspect).
|
|
669
|
+
// w = camera world position, one component per row — the cloud deck is
|
|
670
|
+
// anchored in WORLD space, so the sky pass needs the camera's
|
|
671
|
+
// position and not merely its orientation. Packed into the spare
|
|
672
|
+
// .w lanes to avoid growing the bind group.
|
|
673
|
+
right: vec4<f32>,
|
|
674
|
+
up: vec4<f32>,
|
|
675
|
+
forward: vec4<f32>,
|
|
676
|
+
intensity: vec4<f32>, // x = scene-wide intensity multiplier, y = time (s)
|
|
677
|
+
cloud: vec4<f32>, // x = shadow strength, y = deck height, z = scale, w = drift speed
|
|
678
|
+
wind: vec4<f32>, // xy = wind direction in the XZ plane
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
struct SunUniforms {
|
|
682
|
+
sun: vec4<f32>, // xyz = sun direction, w = sun intensity
|
|
683
|
+
params: vec4<f32>, // x = sun angular radius (rad), y = limb darkening, zw unused
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
@group(0) @binding(0) var<uniform> u: SkyUniforms;
|
|
687
|
+
@group(0) @binding(1) var sky_view_lut: texture_2d<f32>;
|
|
688
|
+
@group(0) @binding(2) var lut_samp: sampler;
|
|
689
|
+
@group(0) @binding(3) var<uniform> sun_u: SunUniforms;
|
|
690
|
+
@group(0) @binding(4) var transmittance_lut: texture_2d<f32>;
|
|
691
|
+
|
|
692
|
+
const PI: f32 = 3.14159265;
|
|
693
|
+
const GROUND_R: f32 = 6360.0;
|
|
694
|
+
const ATMOS_TOP: f32 = 6460.0;
|
|
695
|
+
|
|
696
|
+
struct VsOut {
|
|
697
|
+
@builtin(position) clip_pos: vec4<f32>,
|
|
698
|
+
@location(0) ndc: vec2<f32>,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
@vertex
|
|
702
|
+
fn sky_vs(@builtin(vertex_index) vid: u32) -> VsOut {
|
|
703
|
+
let x = f32((vid & 1u) * 4u) - 1.0;
|
|
704
|
+
let y = f32((vid >> 1u) * 4u) - 1.0;
|
|
705
|
+
var out: VsOut;
|
|
706
|
+
out.clip_pos = vec4<f32>(x, y, 1.0, 1.0);
|
|
707
|
+
out.ndc = vec2<f32>(x, y);
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
fn dir_to_sky_uv(dir: vec3<f32>) -> vec2<f32> {
|
|
712
|
+
// Inverse of sky_view_uv_to_dir in the compute shader. Matches
|
|
713
|
+
// the linear (azimuth, elevation) parameterization.
|
|
714
|
+
let elevation = asin(clamp(dir.y, -1.0, 1.0));
|
|
715
|
+
let azimuth = atan2(dir.z, dir.x);
|
|
716
|
+
var u_norm = azimuth / (2.0 * PI);
|
|
717
|
+
if (u_norm < 0.0) { u_norm = u_norm + 1.0; }
|
|
718
|
+
let v_norm = elevation / PI + 0.5;
|
|
719
|
+
return vec2<f32>(u_norm, v_norm);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// The cloud field itself (cloud_fbm / cloud_cover_view / cloud_shadow_at) is
|
|
723
|
+
// prepended from common/clouds.wgsl — the same file the world materials include
|
|
724
|
+
// for their shadow term.
|
|
725
|
+
|
|
726
|
+
fn sample_transmittance(r: f32, mu: f32) -> vec3<f32> {
|
|
727
|
+
let v = clamp((r - GROUND_R) / (ATMOS_TOP - GROUND_R), 0.0, 1.0);
|
|
728
|
+
let uu = clamp((mu + 1.0) * 0.5, 0.0, 1.0);
|
|
729
|
+
return textureSampleLevel(transmittance_lut, lut_samp, vec2<f32>(uu, v), 0.0).rgb;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
struct SkyOut {
|
|
733
|
+
@location(0) color: vec4<f32>,
|
|
734
|
+
@location(1) material: vec2<f32>,
|
|
735
|
+
@location(2) velocity: vec2<f32>,
|
|
736
|
+
@location(3) albedo: vec4<f32>,
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
@fragment
|
|
740
|
+
fn sky_fs(in: VsOut) -> SkyOut {
|
|
741
|
+
let dir = normalize(u.forward.xyz + in.ndc.x * u.right.xyz + in.ndc.y * u.up.xyz);
|
|
742
|
+
let uv = dir_to_sky_uv(dir);
|
|
743
|
+
var radiance = textureSampleLevel(sky_view_lut, lut_samp, uv, 0.0).rgb;
|
|
744
|
+
|
|
745
|
+
// Sun disk: if view direction is within sun_angular_radius of sun
|
|
746
|
+
// direction, add the sun's radiance attenuated by atmospheric
|
|
747
|
+
// transmittance toward the sun. Below the horizon (sun.y < 0) we
|
|
748
|
+
// skip — sun has set, no disk.
|
|
749
|
+
let sun_dir = normalize(sun_u.sun.xyz);
|
|
750
|
+
let cos_to_sun = dot(dir, sun_dir);
|
|
751
|
+
let cos_radius = cos(sun_u.params.x);
|
|
752
|
+
if (cos_to_sun > cos_radius && sun_dir.y > -0.05) {
|
|
753
|
+
let r0 = GROUND_R + 0.5;
|
|
754
|
+
let t_sun = sample_transmittance(r0, sun_dir.y);
|
|
755
|
+
// Limb darkening: fade toward the disk edge by params.y.
|
|
756
|
+
let edge = (cos_to_sun - cos_radius) / (1.0 - cos_radius);
|
|
757
|
+
let limb = mix(1.0 - sun_u.params.y, 1.0, sqrt(max(edge, 0.0)));
|
|
758
|
+
// Sun radiance ~ 20 (linear) at unit intensity; t_sun colours it.
|
|
759
|
+
let sun_radiance = vec3<f32>(20.0) * sun_u.sun.w * limb * t_sun;
|
|
760
|
+
radiance = radiance + sun_radiance;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
radiance = radiance * u.intensity.x;
|
|
764
|
+
|
|
765
|
+
// Procedural cloud layer, composited over the scaled sky radiance. Cloud
|
|
766
|
+
// colour is absolute HDR (puffy white in sun, cool grey in shadow) so the
|
|
767
|
+
// clouds read brighter than the sky behind them regardless of env intensity.
|
|
768
|
+
//
|
|
769
|
+
// The deck is sampled in WORLD space from the camera's actual position, so
|
|
770
|
+
// the cloud overhead is the one whose shadow the ground is sampling. It also
|
|
771
|
+
// means the clouds now hold still as the player walks under them, instead of
|
|
772
|
+
// sliding along pinned to the camera.
|
|
773
|
+
let cam = vec3<f32>(u.right.w, u.up.w, u.forward.w);
|
|
774
|
+
let cc = cloud_cover_view(cam, dir, sun_dir, u.wind.xy, u.intensity.y, u.cloud);
|
|
775
|
+
if (cc.x > 0.0) {
|
|
776
|
+
let lit = cc.y * cc.y;
|
|
777
|
+
let cloud_col = mix(vec3<f32>(0.62, 0.66, 0.76), vec3<f32>(2.6, 2.5, 2.35), lit);
|
|
778
|
+
radiance = mix(radiance, cloud_col, cc.x);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// EN-005 Phase 4 — sub-LSB dither to break up the banding that
|
|
782
|
+
// Rgba16Float storage produces in low-frequency regions like the
|
|
783
|
+
// zenith (where sky color changes < 1 lsb across many pixels).
|
|
784
|
+
// Pseudo-blue-noise via the integer bit-mash of fragment coords;
|
|
785
|
+
// amplitude scaled to ~1/2 lsb of the smallest channel so it
|
|
786
|
+
// disappears once quantised.
|
|
787
|
+
let dither_seed = vec2<u32>(u32(in.clip_pos.x), u32(in.clip_pos.y));
|
|
788
|
+
let h = (dither_seed.x * 1597u + dither_seed.y * 31337u) ^ (dither_seed.x * 71u + dither_seed.y * 113u);
|
|
789
|
+
let dither = (f32(h & 0xFFu) / 255.0 - 0.5) * (1.0 / 1024.0);
|
|
790
|
+
radiance = radiance + vec3<f32>(dither);
|
|
791
|
+
|
|
792
|
+
var out: SkyOut;
|
|
793
|
+
out.color = vec4<f32>(radiance, 1.0);
|
|
794
|
+
out.material = vec2<f32>(0.0, 0.0);
|
|
795
|
+
out.velocity = vec2<f32>(0.0, 0.0);
|
|
796
|
+
out.albedo = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
"#);
|
|
800
|
+
|
|
801
|
+
/// EN-011 — single-target reflection shader for rendering cached models
|
|
802
|
+
/// (trees, house, etc.) into a planar-reflection probe with a mirrored
|
|
803
|
+
/// view-projection. Deliberately lightweight (base colour × sun N·L + ambient,
|
|
804
|
+
/// alpha-cutout discard) — a water reflection doesn't need the full deferred
|
|
805
|
+
/// PBR/SSAO stack, and a single HDR colour target (vs the main 4-target MRT)
|
|
806
|
+
/// lets it draw straight into the probe RT. Group layouts are owned by the
|
|
807
|
+
/// renderer: g0 = dynamic per-draw model uniform, g1 = sun/ambient, g2 = the
|
|
808
|
+
/// scene material bind group (we only sample base colour + alpha).
|
|
809
|
+
pub(in crate::renderer) const REFLECT_SCENE_WGSL: &str = "
|
|
810
|
+
struct ReflectModelU { mvp: mat4x4<f32>, model: mat4x4<f32> };
|
|
811
|
+
@group(0) @binding(0) var<uniform> u: ReflectModelU;
|
|
812
|
+
|
|
813
|
+
struct ReflectLight {
|
|
814
|
+
sun_dir: vec4<f32>, // xyz dir (TO-sun), w = intensity
|
|
815
|
+
sun_color: vec4<f32>, // rgb, w unused
|
|
816
|
+
ambient: vec4<f32>, // rgb, w = intensity
|
|
817
|
+
cam_pos: vec4<f32>, // xyz = MAIN camera pos (cascade selection), w unused
|
|
818
|
+
shadow_splits: vec4<f32>, // xyz = cascade split distances, w = shadows-enabled
|
|
819
|
+
shadow_cascades: array<mat4x4<f32>, 3>,
|
|
820
|
+
};
|
|
821
|
+
@group(1) @binding(0) var<uniform> light: ReflectLight;
|
|
822
|
+
@group(1) @binding(1) var refl_shadow_0: texture_depth_2d;
|
|
823
|
+
@group(1) @binding(2) var refl_shadow_1: texture_depth_2d;
|
|
824
|
+
@group(1) @binding(3) var refl_shadow_2: texture_depth_2d;
|
|
825
|
+
@group(1) @binding(4) var refl_shadow_samp: sampler_comparison;
|
|
826
|
+
|
|
827
|
+
@group(2) @binding(0) var base_tex: texture_2d<f32>;
|
|
828
|
+
@group(2) @binding(1) var base_samp: sampler;
|
|
829
|
+
|
|
830
|
+
struct VsIn {
|
|
831
|
+
@location(0) position: vec3<f32>,
|
|
832
|
+
@location(1) normal: vec3<f32>,
|
|
833
|
+
@location(2) color: vec4<f32>,
|
|
834
|
+
@location(3) uv: vec2<f32>,
|
|
835
|
+
};
|
|
836
|
+
struct VsOut {
|
|
837
|
+
@builtin(position) pos: vec4<f32>,
|
|
838
|
+
@location(0) n: vec3<f32>,
|
|
839
|
+
@location(1) uv: vec2<f32>,
|
|
840
|
+
@location(2) col: vec4<f32>,
|
|
841
|
+
@location(3) wpos: vec3<f32>,
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
@vertex
|
|
845
|
+
fn vs_reflect(in: VsIn) -> VsOut {
|
|
846
|
+
var o: VsOut;
|
|
847
|
+
o.pos = u.mvp * vec4<f32>(in.position, 1.0);
|
|
848
|
+
o.n = normalize((u.model * vec4<f32>(in.normal, 0.0)).xyz);
|
|
849
|
+
o.uv = in.uv;
|
|
850
|
+
o.col = in.color;
|
|
851
|
+
o.wpos = (u.model * vec4<f32>(in.position, 1.0)).xyz;
|
|
852
|
+
return o;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
fn srgb_lin(c: vec3<f32>) -> vec3<f32> {
|
|
856
|
+
let lo = c / 12.92;
|
|
857
|
+
let hi = pow(max((c + vec3<f32>(0.055)) / 1.055, vec3<f32>(0.0)), vec3<f32>(2.4));
|
|
858
|
+
return select(hi, lo, c <= vec3<f32>(0.04045));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Single-tap cascade shadow for the reflection pass. Cascade selection uses
|
|
862
|
+
// distance from the MAIN camera (the cascades were fit around it; reflected
|
|
863
|
+
// geometry is the same world geometry). Same normal-offset receiver bias as
|
|
864
|
+
// the main pass — texel-proportional push along the surface normal — so the
|
|
865
|
+
// mirrored scene doesn't acne. One tap (no PCF) is enough: the probe is
|
|
866
|
+
// consumed through the water's perturbed half-res lookup.
|
|
867
|
+
fn refl_sun_shadow(wp: vec3<f32>, n: vec3<f32>) -> f32 {
|
|
868
|
+
if (light.shadow_splits.w < 0.5) { return 1.0; }
|
|
869
|
+
let dist = length(wp - light.cam_pos.xyz);
|
|
870
|
+
var c = 2;
|
|
871
|
+
if (dist <= light.shadow_splits.x) { c = 0; }
|
|
872
|
+
else if (dist <= light.shadow_splits.y) { c = 1; }
|
|
873
|
+
var fit = light.shadow_splits.z;
|
|
874
|
+
if (c == 0) { fit = light.shadow_splits.x; }
|
|
875
|
+
else if (c == 1) { fit = light.shadow_splits.y; }
|
|
876
|
+
let dim = f32(textureDimensions(refl_shadow_0).x);
|
|
877
|
+
let p = wp + n * (2.0 * fit / dim) * 1.5;
|
|
878
|
+
let clip = light.shadow_cascades[c] * vec4<f32>(p, 1.0);
|
|
879
|
+
let ndc = clip.xyz / clip.w;
|
|
880
|
+
if (ndc.x < -1.0 || ndc.x > 1.0 || ndc.y < -1.0 || ndc.y > 1.0 ||
|
|
881
|
+
ndc.z < 0.0 || ndc.z > 1.0) {
|
|
882
|
+
return 1.0;
|
|
883
|
+
}
|
|
884
|
+
let uv = vec2<f32>(ndc.x * 0.5 + 0.5, 1.0 - (ndc.y * 0.5 + 0.5));
|
|
885
|
+
let ref_d = ndc.z - 0.001;
|
|
886
|
+
if (c == 0) { return textureSampleCompareLevel(refl_shadow_0, refl_shadow_samp, uv, ref_d); }
|
|
887
|
+
if (c == 1) { return textureSampleCompareLevel(refl_shadow_1, refl_shadow_samp, uv, ref_d); }
|
|
888
|
+
return textureSampleCompareLevel(refl_shadow_2, refl_shadow_samp, uv, ref_d);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
@fragment
|
|
892
|
+
fn fs_reflect(in: VsOut) -> @location(0) vec4<f32> {
|
|
893
|
+
let tex = textureSample(base_tex, base_samp, in.uv);
|
|
894
|
+
if (tex.a < 0.5) { discard; } // alpha-cutout foliage reflects its shape
|
|
895
|
+
let base = srgb_lin(tex.rgb) * in.col.rgb;
|
|
896
|
+
let n = normalize(in.n);
|
|
897
|
+
// sun_dir is direction-TO-sun (engine convention), so N.L uses +sun_dir.
|
|
898
|
+
let ndl = max(dot(n, normalize(light.sun_dir.xyz)), 0.0);
|
|
899
|
+
let shadow = refl_sun_shadow(in.wpos, n);
|
|
900
|
+
// Hemisphere tint on the ambient: up-facing surfaces see more sky.
|
|
901
|
+
let hemi = 0.75 + 0.25 * clamp(n.y, -1.0, 1.0);
|
|
902
|
+
let lit = base * (light.ambient.rgb * light.ambient.w * hemi
|
|
903
|
+
+ light.sun_color.rgb * light.sun_dir.w * ndl * shadow);
|
|
904
|
+
return vec4<f32>(lit, 1.0); // alpha 1 = 'real reflection here' for the water blend
|
|
905
|
+
}
|
|
906
|
+
";
|
|
907
|
+
|