@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,456 @@
|
|
|
1
|
+
//! Phase 7 — world-space impulse field.
|
|
2
|
+
//!
|
|
3
|
+
//! A small R32Float texture covering the playable world as a top-down
|
|
4
|
+
//! XZ projection. Games call `submit_splat(x, z, radius, strength)` on
|
|
5
|
+
//! events of interest (player enters water, explosion lands, footstep
|
|
6
|
+
//! on mud), and a per-frame compute pass decays the previous frame's
|
|
7
|
+
//! value and adds the new splats. Materials in the translucent pass
|
|
8
|
+
//! sample it from `@group(4) @binding(4)` for ripples, decals, snow,
|
|
9
|
+
//! wet spots — whatever decays-over-time, localised-in-world
|
|
10
|
+
//! phenomenon the material wants to express.
|
|
11
|
+
//!
|
|
12
|
+
//! Texture is ping-ponged to avoid a read/write alias on a single
|
|
13
|
+
//! storage resource; read side is a plain sampled `texture_2d<f32>`
|
|
14
|
+
//! so games don't need the read_write storage feature flag.
|
|
15
|
+
//!
|
|
16
|
+
//! World footprint is a hardcoded 128 m square centred on origin at
|
|
17
|
+
//! 256×256 texels = 0.5 m/texel for now. A later phase can replace
|
|
18
|
+
//! that with a per-game descriptor.
|
|
19
|
+
use crate::renderer::shader_include::ShaderSource;
|
|
20
|
+
use crate::renderer::shader_library::library;
|
|
21
|
+
|
|
22
|
+
/// Max splats accumulated in a single frame. Extra submissions are
|
|
23
|
+
/// dropped silently — games should batch their own impulses.
|
|
24
|
+
pub const MAX_SPLATS_PER_FRAME: usize = 16;
|
|
25
|
+
|
|
26
|
+
/// Pixel side of the impulse texture. Power-of-two, workgroup-divisible.
|
|
27
|
+
pub const IMPULSE_SIZE: u32 = 256;
|
|
28
|
+
|
|
29
|
+
/// Default world footprint — a centred square. Half-extent in metres.
|
|
30
|
+
pub const IMPULSE_WORLD_HALF_EXTENT: f32 = 64.0;
|
|
31
|
+
|
|
32
|
+
/// Per-frame decay multiplier. A splat with strength 1.0 fades to
|
|
33
|
+
/// ~2% after 120 frames @ 60 fps (two seconds) — matches the RFC's
|
|
34
|
+
/// acceptance criterion.
|
|
35
|
+
pub const IMPULSE_DECAY_PER_FRAME: f32 = 0.968;
|
|
36
|
+
|
|
37
|
+
#[repr(C)]
|
|
38
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
39
|
+
struct SplatData {
|
|
40
|
+
pos: [f32; 2], // world xz
|
|
41
|
+
radius: f32,
|
|
42
|
+
strength: f32,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[repr(C)]
|
|
46
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
47
|
+
struct InfoUniforms {
|
|
48
|
+
world_min: [f32; 2], // meters
|
|
49
|
+
world_size: [f32; 2], // meters
|
|
50
|
+
decay: f32,
|
|
51
|
+
_pad0: f32,
|
|
52
|
+
splat_count: u32,
|
|
53
|
+
_pad1: u32,
|
|
54
|
+
splats: [SplatData; MAX_SPLATS_PER_FRAME],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub struct ImpulseField {
|
|
58
|
+
pipeline: wgpu::ComputePipeline,
|
|
59
|
+
bg_layout: wgpu::BindGroupLayout,
|
|
60
|
+
info_buf: wgpu::Buffer,
|
|
61
|
+
/// Backing textures for view_a/view_b — kept alive so the views
|
|
62
|
+
/// don't dangle. Sampling and binding go through the views; these
|
|
63
|
+
/// fields are write-only handles.
|
|
64
|
+
_tex_a: wgpu::Texture,
|
|
65
|
+
_tex_b: wgpu::Texture,
|
|
66
|
+
view_a: wgpu::TextureView,
|
|
67
|
+
view_b: wgpu::TextureView,
|
|
68
|
+
sampler: wgpu::Sampler,
|
|
69
|
+
/// When true, view_a is the "front" that scene_inputs reads and
|
|
70
|
+
/// the next compute pass reads-from. After dispatch we swap.
|
|
71
|
+
front_is_a: bool,
|
|
72
|
+
splats: Vec<SplatData>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
impl ImpulseField {
|
|
76
|
+
pub fn new(device: &wgpu::Device) -> Self {
|
|
77
|
+
let bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
78
|
+
label: Some("impulse_field_bg_layout"),
|
|
79
|
+
entries: &[
|
|
80
|
+
// 0: src (previous-frame sampled texture)
|
|
81
|
+
wgpu::BindGroupLayoutEntry {
|
|
82
|
+
binding: 0, visibility: wgpu::ShaderStages::COMPUTE,
|
|
83
|
+
ty: wgpu::BindingType::Texture {
|
|
84
|
+
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
|
85
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
86
|
+
multisampled: false,
|
|
87
|
+
},
|
|
88
|
+
count: None,
|
|
89
|
+
},
|
|
90
|
+
// 1: dst (write-only storage)
|
|
91
|
+
wgpu::BindGroupLayoutEntry {
|
|
92
|
+
binding: 1, visibility: wgpu::ShaderStages::COMPUTE,
|
|
93
|
+
ty: wgpu::BindingType::StorageTexture {
|
|
94
|
+
access: wgpu::StorageTextureAccess::WriteOnly,
|
|
95
|
+
format: wgpu::TextureFormat::R32Float,
|
|
96
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
97
|
+
},
|
|
98
|
+
count: None,
|
|
99
|
+
},
|
|
100
|
+
// 2: info UBO (splats + bounds + decay)
|
|
101
|
+
wgpu::BindGroupLayoutEntry {
|
|
102
|
+
binding: 2, visibility: wgpu::ShaderStages::COMPUTE,
|
|
103
|
+
ty: wgpu::BindingType::Buffer {
|
|
104
|
+
ty: wgpu::BufferBindingType::Uniform,
|
|
105
|
+
has_dynamic_offset: false,
|
|
106
|
+
min_binding_size: None,
|
|
107
|
+
},
|
|
108
|
+
count: None,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
114
|
+
label: Some("impulse_field_pipeline_layout"),
|
|
115
|
+
bind_group_layouts: &[Some(&bg_layout)],
|
|
116
|
+
immediate_size: 0,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let shader_src = library().fetch("impulse_field.wgsl")
|
|
120
|
+
.expect("impulse_field.wgsl must be present in shader_library")
|
|
121
|
+
.to_string();
|
|
122
|
+
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
123
|
+
label: Some("impulse_field"),
|
|
124
|
+
source: wgpu::ShaderSource::Wgsl(shader_src.into()),
|
|
125
|
+
});
|
|
126
|
+
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
|
127
|
+
label: Some("impulse_field_pipeline"),
|
|
128
|
+
layout: Some(&pipeline_layout),
|
|
129
|
+
module: &module,
|
|
130
|
+
entry_point: Some("cs_main"),
|
|
131
|
+
compilation_options: Default::default(),
|
|
132
|
+
cache: None,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let info_buf = device.create_buffer(&wgpu::BufferDescriptor {
|
|
136
|
+
label: Some("impulse_field_info"),
|
|
137
|
+
size: std::mem::size_of::<InfoUniforms>() as u64,
|
|
138
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
139
|
+
mapped_at_creation: false,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let make_tex = |label: &str| {
|
|
143
|
+
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
|
144
|
+
label: Some(label),
|
|
145
|
+
size: wgpu::Extent3d {
|
|
146
|
+
width: IMPULSE_SIZE, height: IMPULSE_SIZE,
|
|
147
|
+
depth_or_array_layers: 1,
|
|
148
|
+
},
|
|
149
|
+
mip_level_count: 1, sample_count: 1,
|
|
150
|
+
dimension: wgpu::TextureDimension::D2,
|
|
151
|
+
format: wgpu::TextureFormat::R32Float,
|
|
152
|
+
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
|
153
|
+
| wgpu::TextureUsages::STORAGE_BINDING
|
|
154
|
+
| wgpu::TextureUsages::COPY_SRC,
|
|
155
|
+
view_formats: &[],
|
|
156
|
+
});
|
|
157
|
+
let view = tex.create_view(&Default::default());
|
|
158
|
+
(tex, view)
|
|
159
|
+
};
|
|
160
|
+
let (tex_a, view_a) = make_tex("impulse_field_a");
|
|
161
|
+
let (tex_b, view_b) = make_tex("impulse_field_b");
|
|
162
|
+
|
|
163
|
+
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
164
|
+
label: Some("impulse_field_sampler"),
|
|
165
|
+
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
|
166
|
+
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
|
167
|
+
mag_filter: wgpu::FilterMode::Nearest,
|
|
168
|
+
min_filter: wgpu::FilterMode::Nearest,
|
|
169
|
+
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
|
170
|
+
..Default::default()
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
Self {
|
|
174
|
+
pipeline, bg_layout, info_buf,
|
|
175
|
+
_tex_a: tex_a, _tex_b: tex_b, view_a, view_b, sampler,
|
|
176
|
+
front_is_a: true,
|
|
177
|
+
splats: Vec::with_capacity(MAX_SPLATS_PER_FRAME),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Gameplay API — queue a splat at world xz with given radius (m)
|
|
182
|
+
/// and peak strength. Silently drops overflow.
|
|
183
|
+
pub fn submit_splat(&mut self, world_x: f32, world_z: f32, radius: f32, strength: f32) {
|
|
184
|
+
if self.splats.len() >= MAX_SPLATS_PER_FRAME { return; }
|
|
185
|
+
self.splats.push(SplatData {
|
|
186
|
+
pos: [world_x, world_z], radius, strength,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Run the decay + splat compute pass. Caller encodes just before
|
|
191
|
+
/// the translucent pass so scene_inputs sees the latest field.
|
|
192
|
+
/// After this, `front_view()` returns the view containing the new
|
|
193
|
+
/// field and `update_scene_inputs` should bind it at group 4
|
|
194
|
+
/// binding 4.
|
|
195
|
+
pub fn update(&mut self, device: &wgpu::Device, queue: &wgpu::Queue,
|
|
196
|
+
encoder: &mut wgpu::CommandEncoder) {
|
|
197
|
+
// Ping-pong: read from the CURRENT front (last frame's output),
|
|
198
|
+
// write into the other side, then flip `front_is_a`.
|
|
199
|
+
let (src_view, dst_view) = if self.front_is_a {
|
|
200
|
+
(&self.view_a, &self.view_b)
|
|
201
|
+
} else {
|
|
202
|
+
(&self.view_b, &self.view_a)
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Build info UBO from the queued splats.
|
|
206
|
+
let mut info = InfoUniforms {
|
|
207
|
+
world_min: [-IMPULSE_WORLD_HALF_EXTENT, -IMPULSE_WORLD_HALF_EXTENT],
|
|
208
|
+
world_size: [IMPULSE_WORLD_HALF_EXTENT * 2.0, IMPULSE_WORLD_HALF_EXTENT * 2.0],
|
|
209
|
+
decay: IMPULSE_DECAY_PER_FRAME,
|
|
210
|
+
_pad0: 0.0,
|
|
211
|
+
splat_count: self.splats.len() as u32,
|
|
212
|
+
_pad1: 0,
|
|
213
|
+
splats: [SplatData { pos: [0.0; 2], radius: 0.0, strength: 0.0 };
|
|
214
|
+
MAX_SPLATS_PER_FRAME],
|
|
215
|
+
};
|
|
216
|
+
for (i, s) in self.splats.iter().enumerate() {
|
|
217
|
+
info.splats[i] = *s;
|
|
218
|
+
}
|
|
219
|
+
queue.write_buffer(&self.info_buf, 0, bytemuck::bytes_of(&info));
|
|
220
|
+
self.splats.clear();
|
|
221
|
+
|
|
222
|
+
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
223
|
+
label: Some("impulse_field_bg"),
|
|
224
|
+
layout: &self.bg_layout,
|
|
225
|
+
entries: &[
|
|
226
|
+
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(src_view) },
|
|
227
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(dst_view) },
|
|
228
|
+
wgpu::BindGroupEntry { binding: 2, resource: self.info_buf.as_entire_binding() },
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
|
234
|
+
label: Some("impulse_field_pass"),
|
|
235
|
+
timestamp_writes: None,
|
|
236
|
+
});
|
|
237
|
+
pass.set_pipeline(&self.pipeline);
|
|
238
|
+
pass.set_bind_group(0, &bg, &[]);
|
|
239
|
+
let groups = IMPULSE_SIZE / 8;
|
|
240
|
+
pass.dispatch_workgroups(groups, groups, 1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
self.front_is_a = !self.front_is_a;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// The view containing the latest impulse field — what materials
|
|
247
|
+
/// should sample. Available AFTER `update` ran this frame.
|
|
248
|
+
pub fn front_view(&self) -> &wgpu::TextureView {
|
|
249
|
+
if self.front_is_a { &self.view_a } else { &self.view_b }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
pub fn sampler(&self) -> &wgpu::Sampler { &self.sampler }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// =====================================================================
|
|
256
|
+
// Tests
|
|
257
|
+
// =====================================================================
|
|
258
|
+
|
|
259
|
+
#[cfg(test)]
|
|
260
|
+
mod tests {
|
|
261
|
+
use super::*;
|
|
262
|
+
|
|
263
|
+
/// Headless wgpu device for tests. Mirrors the helper in
|
|
264
|
+
/// `transient.rs` — falls back to a low-power adapter and returns
|
|
265
|
+
/// `None` when no adapter is available so CI runners without a GPU
|
|
266
|
+
/// skip the test instead of failing.
|
|
267
|
+
///
|
|
268
|
+
/// The impulse field uses a `R32Float` storage texture (writeOnly).
|
|
269
|
+
/// `downlevel_defaults` does **not** allow R32Float storage, so we
|
|
270
|
+
/// request the explicit `Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES`
|
|
271
|
+
/// upgrade. If the adapter doesn't expose it, return `None` — the
|
|
272
|
+
/// test then skips gracefully (matches the "skipped on adapters
|
|
273
|
+
/// without storage textures" doc-comment contract).
|
|
274
|
+
fn try_create_device() -> Option<(wgpu::Device, wgpu::Queue)> {
|
|
275
|
+
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
|
276
|
+
backends: wgpu::Backends::all(),
|
|
277
|
+
..wgpu::InstanceDescriptor::new_without_display_handle()
|
|
278
|
+
});
|
|
279
|
+
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
|
|
280
|
+
power_preference: wgpu::PowerPreference::LowPower,
|
|
281
|
+
compatible_surface: None,
|
|
282
|
+
force_fallback_adapter: true,
|
|
283
|
+
})).ok()?;
|
|
284
|
+
// Only proceed if the adapter advertises R32Float storage support.
|
|
285
|
+
let needed = wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES;
|
|
286
|
+
let supported = adapter.features();
|
|
287
|
+
if !supported.contains(needed) {
|
|
288
|
+
return None;
|
|
289
|
+
}
|
|
290
|
+
let (device, queue) = pollster::block_on(adapter.request_device(
|
|
291
|
+
&wgpu::DeviceDescriptor {
|
|
292
|
+
label: Some("impulse-test-device"),
|
|
293
|
+
required_features: needed,
|
|
294
|
+
required_limits: wgpu::Limits::downlevel_defaults(),
|
|
295
|
+
..Default::default()
|
|
296
|
+
},
|
|
297
|
+
)).ok()?;
|
|
298
|
+
Some((device, queue))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Read back the entire impulse front texture as `IMPULSE_SIZE *
|
|
302
|
+
/// IMPULSE_SIZE` floats. R32Float = 4 B/texel, padded to 256 B per
|
|
303
|
+
/// row by wgpu's copy-to-buffer rules.
|
|
304
|
+
fn read_front_texture(
|
|
305
|
+
device: &wgpu::Device,
|
|
306
|
+
queue: &wgpu::Queue,
|
|
307
|
+
field: &ImpulseField,
|
|
308
|
+
) -> Vec<f32> {
|
|
309
|
+
let bpr_unpadded = IMPULSE_SIZE * 4;
|
|
310
|
+
let bpr = (bpr_unpadded + 255) & !255;
|
|
311
|
+
let staging = device.create_buffer(&wgpu::BufferDescriptor {
|
|
312
|
+
label: Some("impulse_test_staging"),
|
|
313
|
+
size: (bpr * IMPULSE_SIZE) as u64,
|
|
314
|
+
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
|
315
|
+
mapped_at_creation: false,
|
|
316
|
+
});
|
|
317
|
+
// Pull the actual texture handle that backs `front_view`.
|
|
318
|
+
let front_tex = if field.front_is_a { &field._tex_a } else { &field._tex_b };
|
|
319
|
+
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
320
|
+
label: Some("impulse_test_readback"),
|
|
321
|
+
});
|
|
322
|
+
encoder.copy_texture_to_buffer(
|
|
323
|
+
wgpu::TexelCopyTextureInfo {
|
|
324
|
+
texture: front_tex,
|
|
325
|
+
mip_level: 0,
|
|
326
|
+
origin: wgpu::Origin3d::ZERO,
|
|
327
|
+
aspect: wgpu::TextureAspect::All,
|
|
328
|
+
},
|
|
329
|
+
wgpu::TexelCopyBufferInfo {
|
|
330
|
+
buffer: &staging,
|
|
331
|
+
layout: wgpu::TexelCopyBufferLayout {
|
|
332
|
+
offset: 0,
|
|
333
|
+
bytes_per_row: Some(bpr),
|
|
334
|
+
rows_per_image: Some(IMPULSE_SIZE),
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
wgpu::Extent3d { width: IMPULSE_SIZE, height: IMPULSE_SIZE, depth_or_array_layers: 1 },
|
|
338
|
+
);
|
|
339
|
+
queue.submit(std::iter::once(encoder.finish()));
|
|
340
|
+
let slice = staging.slice(..);
|
|
341
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
342
|
+
slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
|
|
343
|
+
let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
|
|
344
|
+
rx.recv().expect("map_async sender dropped").expect("map failed");
|
|
345
|
+
let data = slice.get_mapped_range();
|
|
346
|
+
let mut out: Vec<f32> = Vec::with_capacity((IMPULSE_SIZE * IMPULSE_SIZE) as usize);
|
|
347
|
+
for row in 0..IMPULSE_SIZE {
|
|
348
|
+
let row_start = (row * bpr) as usize;
|
|
349
|
+
let row_end = row_start + (bpr_unpadded as usize);
|
|
350
|
+
let slice = &data[row_start..row_end];
|
|
351
|
+
let floats: &[f32] = bytemuck::cast_slice(slice);
|
|
352
|
+
out.extend_from_slice(floats);
|
|
353
|
+
}
|
|
354
|
+
drop(data);
|
|
355
|
+
staging.unmap();
|
|
356
|
+
out
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Convert a world-XZ position into the impulse texel index that
|
|
360
|
+
/// owns it. Mirrors the WGSL `world_min + uv * world_size` math.
|
|
361
|
+
fn world_to_texel(x: f32, z: f32) -> (u32, u32) {
|
|
362
|
+
let w_min = -IMPULSE_WORLD_HALF_EXTENT;
|
|
363
|
+
let w_size = IMPULSE_WORLD_HALF_EXTENT * 2.0;
|
|
364
|
+
let u = ((x - w_min) / w_size).clamp(0.0, 1.0 - 1e-6);
|
|
365
|
+
let v = ((z - w_min) / w_size).clamp(0.0, 1.0 - 1e-6);
|
|
366
|
+
let tx = (u * IMPULSE_SIZE as f32) as u32;
|
|
367
|
+
let ty = (v * IMPULSE_SIZE as f32) as u32;
|
|
368
|
+
(tx, ty)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// EN-006 — verifies that submitting a splat + dispatching the
|
|
372
|
+
/// impulse-field compute shader writes a non-zero value at the
|
|
373
|
+
/// expected texel and that the value decays monotonically across
|
|
374
|
+
/// subsequent frames with no new splats.
|
|
375
|
+
/// Skipped on adapters without `TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES`
|
|
376
|
+
/// (R32Float storage textures).
|
|
377
|
+
#[test]
|
|
378
|
+
fn splat_appears_then_decays_across_frames() {
|
|
379
|
+
let Some((device, queue)) = try_create_device() else { return; };
|
|
380
|
+
let mut field = ImpulseField::new(&device);
|
|
381
|
+
|
|
382
|
+
// Submit a splat at world origin with a generous radius so it
|
|
383
|
+
// covers a couple of texels (the texture is 0.5 m / texel; a
|
|
384
|
+
// 2 m radius lights up a 4-texel disc).
|
|
385
|
+
field.submit_splat(0.0, 0.0, 2.0, 1.0);
|
|
386
|
+
|
|
387
|
+
// Dispatch frame 1.
|
|
388
|
+
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
389
|
+
label: Some("impulse_test_frame_1"),
|
|
390
|
+
});
|
|
391
|
+
field.update(&device, &queue, &mut encoder);
|
|
392
|
+
queue.submit(std::iter::once(encoder.finish()));
|
|
393
|
+
|
|
394
|
+
let pixels = read_front_texture(&device, &queue, &field);
|
|
395
|
+
let (tx, ty) = world_to_texel(0.0, 0.0);
|
|
396
|
+
let center_idx = (ty * IMPULSE_SIZE + tx) as usize;
|
|
397
|
+
let center_v1 = pixels[center_idx];
|
|
398
|
+
assert!(
|
|
399
|
+
center_v1 > 0.0,
|
|
400
|
+
"splat should write a positive value at the centre texel ({}, {}); got {}",
|
|
401
|
+
tx, ty, center_v1,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// A texel far outside the splat radius (16 m away ≫ 2 m radius)
|
|
405
|
+
// must remain zero — confirms the splat is localised.
|
|
406
|
+
let far_idx = {
|
|
407
|
+
let (fx, fy) = world_to_texel(16.0, 16.0);
|
|
408
|
+
(fy * IMPULSE_SIZE + fx) as usize
|
|
409
|
+
};
|
|
410
|
+
assert_eq!(pixels[far_idx], 0.0, "no splat far from origin");
|
|
411
|
+
|
|
412
|
+
// Dispatch a few more frames with no new splats. The value at
|
|
413
|
+
// the centre texel must decay strictly monotonically — the
|
|
414
|
+
// shader multiplies by IMPULSE_DECAY_PER_FRAME (~0.968) every
|
|
415
|
+
// frame.
|
|
416
|
+
let mut prev = center_v1;
|
|
417
|
+
for frame in 2..=4 {
|
|
418
|
+
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
419
|
+
label: Some("impulse_test_decay"),
|
|
420
|
+
});
|
|
421
|
+
field.update(&device, &queue, &mut encoder);
|
|
422
|
+
queue.submit(std::iter::once(encoder.finish()));
|
|
423
|
+
let pixels = read_front_texture(&device, &queue, &field);
|
|
424
|
+
let v = pixels[center_idx];
|
|
425
|
+
assert!(
|
|
426
|
+
v < prev,
|
|
427
|
+
"frame {} centre value {} should be < previous {}",
|
|
428
|
+
frame, v, prev,
|
|
429
|
+
);
|
|
430
|
+
assert!(v > 0.0, "frame {} centre value should still be positive (decay, not clear)", frame);
|
|
431
|
+
// Verify the multiplier is roughly the documented decay.
|
|
432
|
+
let ratio = v / prev;
|
|
433
|
+
assert!(
|
|
434
|
+
(ratio - IMPULSE_DECAY_PER_FRAME).abs() < 1e-3,
|
|
435
|
+
"frame {} ratio {} should match documented decay {}",
|
|
436
|
+
frame, ratio, IMPULSE_DECAY_PER_FRAME,
|
|
437
|
+
);
|
|
438
|
+
prev = v;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/// EN-006 — guard: `submit_splat` silently drops when the
|
|
443
|
+
/// per-frame queue is full. Pure CPU; runs everywhere.
|
|
444
|
+
#[test]
|
|
445
|
+
fn submit_splat_caps_at_max_per_frame() {
|
|
446
|
+
// Construct only the CPU-side state we need to assert on.
|
|
447
|
+
// Avoid `ImpulseField::new` here so this runs without a GPU.
|
|
448
|
+
let mut splats: Vec<SplatData> = Vec::with_capacity(MAX_SPLATS_PER_FRAME);
|
|
449
|
+
for _ in 0..(MAX_SPLATS_PER_FRAME + 5) {
|
|
450
|
+
if splats.len() < MAX_SPLATS_PER_FRAME {
|
|
451
|
+
splats.push(SplatData { pos: [0.0, 0.0], radius: 1.0, strength: 1.0 });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
assert_eq!(splats.len(), MAX_SPLATS_PER_FRAME);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//! Group-1 lighting bind group — layout and construction.
|
|
2
|
+
//!
|
|
3
|
+
//! The scene + immediate-mode 3D pipelines share one bind-group layout
|
|
4
|
+
//! for lighting data: the Lighting UBO, env/IBL textures, the shadow
|
|
5
|
+
//! cascade, and (on clustered devices) the froxel buffers at bindings
|
|
6
|
+
//! 10-12. The bind group is rebuilt whenever the env source changes
|
|
7
|
+
//! (HDR load, panorama, procedural sky); every rebuild goes through
|
|
8
|
+
//! [`Renderer::make_lighting_bind_group`] so the entry list exists in
|
|
9
|
+
//! exactly one place and cannot drift between call sites.
|
|
10
|
+
|
|
11
|
+
use super::{froxel, Renderer};
|
|
12
|
+
|
|
13
|
+
/// Create the group-1 layout. `clustered` appends the froxel bindings —
|
|
14
|
+
/// set when [`froxel::FroxelPass::supported`] holds for the device.
|
|
15
|
+
/// Pipelines whose shaders don't reference bindings 10-12 (pipeline_3d)
|
|
16
|
+
/// share the layout unaffected; extra entries are legal as long as the
|
|
17
|
+
/// bind group provides them.
|
|
18
|
+
pub(super) fn create_lighting_layout(
|
|
19
|
+
device: &wgpu::Device,
|
|
20
|
+
clustered: bool,
|
|
21
|
+
) -> wgpu::BindGroupLayout {
|
|
22
|
+
let tex_float = wgpu::BindingType::Texture {
|
|
23
|
+
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
|
24
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
25
|
+
multisampled: false,
|
|
26
|
+
};
|
|
27
|
+
let tex_depth = wgpu::BindingType::Texture {
|
|
28
|
+
sample_type: wgpu::TextureSampleType::Depth,
|
|
29
|
+
view_dimension: wgpu::TextureViewDimension::D2,
|
|
30
|
+
multisampled: false,
|
|
31
|
+
};
|
|
32
|
+
let frag = wgpu::ShaderStages::FRAGMENT;
|
|
33
|
+
let mut entries = vec![
|
|
34
|
+
// 0: Lighting UBO. VERTEX_FRAGMENT so the scene vertex shader can read
|
|
35
|
+
// `wind` (foliage sway); the fragment stage uses the full struct.
|
|
36
|
+
wgpu::BindGroupLayoutEntry {
|
|
37
|
+
binding: 0,
|
|
38
|
+
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
|
39
|
+
ty: wgpu::BindingType::Buffer {
|
|
40
|
+
ty: wgpu::BufferBindingType::Uniform,
|
|
41
|
+
has_dynamic_offset: false,
|
|
42
|
+
min_binding_size: None,
|
|
43
|
+
},
|
|
44
|
+
count: None,
|
|
45
|
+
},
|
|
46
|
+
// 1/2: env (IBL specular) texture + sampler
|
|
47
|
+
wgpu::BindGroupLayoutEntry { binding: 1, visibility: frag, ty: tex_float, count: None },
|
|
48
|
+
wgpu::BindGroupLayoutEntry {
|
|
49
|
+
binding: 2,
|
|
50
|
+
visibility: frag,
|
|
51
|
+
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
|
52
|
+
count: None,
|
|
53
|
+
},
|
|
54
|
+
// 3/4: BRDF LUT + sampler
|
|
55
|
+
wgpu::BindGroupLayoutEntry { binding: 3, visibility: frag, ty: tex_float, count: None },
|
|
56
|
+
wgpu::BindGroupLayoutEntry {
|
|
57
|
+
binding: 4,
|
|
58
|
+
visibility: frag,
|
|
59
|
+
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
|
60
|
+
count: None,
|
|
61
|
+
},
|
|
62
|
+
// 5-7: shadow cascades, 8: comparison sampler
|
|
63
|
+
wgpu::BindGroupLayoutEntry { binding: 5, visibility: frag, ty: tex_depth, count: None },
|
|
64
|
+
wgpu::BindGroupLayoutEntry { binding: 6, visibility: frag, ty: tex_depth, count: None },
|
|
65
|
+
wgpu::BindGroupLayoutEntry { binding: 7, visibility: frag, ty: tex_depth, count: None },
|
|
66
|
+
wgpu::BindGroupLayoutEntry {
|
|
67
|
+
binding: 8,
|
|
68
|
+
visibility: frag,
|
|
69
|
+
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
|
|
70
|
+
count: None,
|
|
71
|
+
},
|
|
72
|
+
// 9: env diffuse (IBL irradiance)
|
|
73
|
+
wgpu::BindGroupLayoutEntry { binding: 9, visibility: frag, ty: tex_float, count: None },
|
|
74
|
+
];
|
|
75
|
+
if clustered {
|
|
76
|
+
entries.extend(froxel::extra_lighting_layout_entries());
|
|
77
|
+
}
|
|
78
|
+
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
79
|
+
label: Some("lighting_layout"),
|
|
80
|
+
entries: &entries,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Everything a lighting bind group references besides the env views.
|
|
85
|
+
/// `Renderer::new` builds one from constructor locals (before `self`
|
|
86
|
+
/// exists); [`Renderer::make_lighting_bind_group`] from fields.
|
|
87
|
+
pub(super) struct LightingBindSources<'a> {
|
|
88
|
+
pub lighting_buffer: &'a wgpu::Buffer,
|
|
89
|
+
pub env_sampler: &'a wgpu::Sampler,
|
|
90
|
+
pub brdf_lut_view: &'a wgpu::TextureView,
|
|
91
|
+
pub brdf_lut_sampler: &'a wgpu::Sampler,
|
|
92
|
+
pub shadow_map: &'a crate::shadows::ShadowMap,
|
|
93
|
+
pub froxel: Option<&'a froxel::FroxelPass>,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// The single source of truth for the group-1 entry list — every
|
|
97
|
+
/// lighting bind group the renderer ever creates goes through here.
|
|
98
|
+
pub(super) fn create_lighting_bind_group(
|
|
99
|
+
device: &wgpu::Device,
|
|
100
|
+
layout: &wgpu::BindGroupLayout,
|
|
101
|
+
label: &str,
|
|
102
|
+
src: &LightingBindSources<'_>,
|
|
103
|
+
env_view: &wgpu::TextureView,
|
|
104
|
+
diffuse_view: &wgpu::TextureView,
|
|
105
|
+
) -> wgpu::BindGroup {
|
|
106
|
+
let mut entries = vec![
|
|
107
|
+
wgpu::BindGroupEntry { binding: 0, resource: src.lighting_buffer.as_entire_binding() },
|
|
108
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(env_view) },
|
|
109
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(src.env_sampler) },
|
|
110
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(src.brdf_lut_view) },
|
|
111
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(src.brdf_lut_sampler) },
|
|
112
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&src.shadow_map.depth_views[0]) },
|
|
113
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&src.shadow_map.depth_views[1]) },
|
|
114
|
+
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&src.shadow_map.depth_views[2]) },
|
|
115
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::Sampler(&src.shadow_map.sampler) },
|
|
116
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::TextureView(diffuse_view) },
|
|
117
|
+
];
|
|
118
|
+
if let Some(f) = src.froxel {
|
|
119
|
+
entries.extend(f.extra_lighting_bind_entries());
|
|
120
|
+
}
|
|
121
|
+
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
122
|
+
label: Some(label),
|
|
123
|
+
layout,
|
|
124
|
+
entries: &entries,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl Renderer {
|
|
129
|
+
/// Build a group-1 lighting bind group for the given env-specular /
|
|
130
|
+
/// env-diffuse views. Everything else (UBO, BRDF LUT, shadow
|
|
131
|
+
/// cascade, froxel buffers when clustered) comes from `self`.
|
|
132
|
+
pub(super) fn make_lighting_bind_group(
|
|
133
|
+
&self,
|
|
134
|
+
label: &str,
|
|
135
|
+
env_view: &wgpu::TextureView,
|
|
136
|
+
diffuse_view: &wgpu::TextureView,
|
|
137
|
+
) -> wgpu::BindGroup {
|
|
138
|
+
create_lighting_bind_group(
|
|
139
|
+
&self.device,
|
|
140
|
+
&self.lighting_layout,
|
|
141
|
+
label,
|
|
142
|
+
&LightingBindSources {
|
|
143
|
+
lighting_buffer: &self.lighting_buffer,
|
|
144
|
+
env_sampler: &self.env_sampler,
|
|
145
|
+
brdf_lut_view: &self.brdf_lut_view,
|
|
146
|
+
brdf_lut_sampler: &self.brdf_lut_sampler,
|
|
147
|
+
shadow_map: &self.shadow_map,
|
|
148
|
+
froxel: self.froxel.as_ref(),
|
|
149
|
+
},
|
|
150
|
+
env_view,
|
|
151
|
+
diffuse_view,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|