@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,1996 @@
|
|
|
1
|
+
// Material system — the runtime state behind Phase 1c.
|
|
2
|
+
//
|
|
3
|
+
// Owns the compiled `MaterialPipeline`s, the per-frame / per-view /
|
|
4
|
+
// per-material / per-draw uniform buffers, the bind groups wiring
|
|
5
|
+
// them to the ABI layouts, and the per-frame draw command list.
|
|
6
|
+
//
|
|
7
|
+
// The Renderer owns one `MaterialSystem` instance. Games interact via
|
|
8
|
+
// three methods: `compile_material`, `submit_draw`, and (internally)
|
|
9
|
+
// `dispatch` which runs inside the main HDR pass.
|
|
10
|
+
|
|
11
|
+
use wgpu::util::DeviceExt;
|
|
12
|
+
|
|
13
|
+
use super::material_pipeline::{
|
|
14
|
+
MaterialAbiLayouts, MaterialPipeline, MaterialCompileDesc, FragmentProfile,
|
|
15
|
+
Bucket, compile_material, MaterialCompileError,
|
|
16
|
+
};
|
|
17
|
+
use super::types::Vertex3D;
|
|
18
|
+
|
|
19
|
+
// =====================================================================
|
|
20
|
+
// Uniform structs — repr(C), bytemuck-Pod, mirror the WGSL in
|
|
21
|
+
// material_abi.wgsl. Kept local to this module so changes to ABI
|
|
22
|
+
// struct layouts happen in exactly one place per language.
|
|
23
|
+
// =====================================================================
|
|
24
|
+
|
|
25
|
+
#[repr(C)]
|
|
26
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
27
|
+
pub struct PerFrameUniforms {
|
|
28
|
+
pub time: f32,
|
|
29
|
+
pub delta_time: f32,
|
|
30
|
+
pub frame_index: u32,
|
|
31
|
+
pub _pad0: u32,
|
|
32
|
+
pub screen_resolution: [f32; 2],
|
|
33
|
+
pub render_resolution: [f32; 2],
|
|
34
|
+
pub taa_jitter: [f32; 2],
|
|
35
|
+
pub _pad1: [f32; 2],
|
|
36
|
+
/// Global wind: x=dir_x, y=dir_z, z=amplitude, w=frequency.
|
|
37
|
+
pub wind: [f32; 4],
|
|
38
|
+
/// Cloud deck: x = shadow strength, y = deck height (m), z = feature scale,
|
|
39
|
+
/// w = drift speed (m/s). Materials feed this to `cloud_shadow_at` from
|
|
40
|
+
/// common/clouds.wgsl — the same deck the sky pass draws.
|
|
41
|
+
pub cloud: [f32; 4],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[repr(C)]
|
|
45
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
46
|
+
pub struct PerViewDirLight {
|
|
47
|
+
pub direction: [f32; 4], // xyz + intensity
|
|
48
|
+
pub color: [f32; 4],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[repr(C)]
|
|
52
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
53
|
+
pub struct PerViewPointLight {
|
|
54
|
+
pub position: [f32; 4],
|
|
55
|
+
pub color: [f32; 4],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[repr(C)]
|
|
59
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
60
|
+
pub struct PerViewUniforms {
|
|
61
|
+
pub view: [[f32; 4]; 4],
|
|
62
|
+
pub proj: [[f32; 4]; 4],
|
|
63
|
+
pub view_proj: [[f32; 4]; 4],
|
|
64
|
+
pub prev_view_proj: [[f32; 4]; 4],
|
|
65
|
+
pub inv_proj: [[f32; 4]; 4],
|
|
66
|
+
pub camera_pos: [f32; 4],
|
|
67
|
+
pub camera_dir: [f32; 4],
|
|
68
|
+
pub ambient: [f32; 4],
|
|
69
|
+
pub fog: [f32; 4],
|
|
70
|
+
pub sun_dir: [f32; 4],
|
|
71
|
+
pub sun_color: [f32; 4],
|
|
72
|
+
pub dir_light_count: [f32; 4],
|
|
73
|
+
pub dir_lights: [PerViewDirLight; 8],
|
|
74
|
+
pub point_light_count: [f32; 4],
|
|
75
|
+
pub point_lights: [PerViewPointLight; 256],
|
|
76
|
+
pub shadow_splits: [f32; 4],
|
|
77
|
+
pub shadow_view: [[f32; 4]; 4],
|
|
78
|
+
pub shadow_cascades: [[[f32; 4]; 4]; 3],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[repr(C)]
|
|
82
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
83
|
+
pub struct MaterialFactorsUniforms {
|
|
84
|
+
pub metal_rough: [f32; 4],
|
|
85
|
+
pub emissive: [f32; 4],
|
|
86
|
+
pub base_color: [f32; 4],
|
|
87
|
+
/// EN-012 — shading-model selector + foliage transmission tint.
|
|
88
|
+
/// x = shading_model enum (0 = default lit, 1 = foliage,
|
|
89
|
+
/// 2 = subsurface — V2 stub),
|
|
90
|
+
/// yzw = transmission_color (rgb tint for back-lit foliage; ignored
|
|
91
|
+
/// when shading_model == 0).
|
|
92
|
+
pub shading_model: [f32; 4],
|
|
93
|
+
/// EN-012 — foliage shading parameters. Only consumed when
|
|
94
|
+
/// `shading_model.x == 1.0`.
|
|
95
|
+
/// x = transmission_amount (0..1),
|
|
96
|
+
/// y = wrap_factor (0..1),
|
|
97
|
+
/// zw = reserved.
|
|
98
|
+
pub foliage_params: [f32; 4],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl Default for MaterialFactorsUniforms {
|
|
102
|
+
fn default() -> Self {
|
|
103
|
+
Self {
|
|
104
|
+
metal_rough: [0.0, 1.0, 0.0, 0.0], // non-metal, rough, no MR tex, no cutoff
|
|
105
|
+
emissive: [0.0, 0.0, 0.0, 0.0],
|
|
106
|
+
base_color: [1.0, 1.0, 1.0, 1.0],
|
|
107
|
+
// EN-012 — default lit, white transmission tint. Materials
|
|
108
|
+
// that never call `setMaterialShadingModel` get standard PBR
|
|
109
|
+
// (shading_model.x == 0.0).
|
|
110
|
+
shading_model: [0.0, 1.0, 1.0, 1.0],
|
|
111
|
+
// EN-012 — moderate defaults so a freshly-flagged foliage
|
|
112
|
+
// material (shading_model = 1) looks reasonable before any
|
|
113
|
+
// tuning. Wrap=0.5 + transmission=0.5 gives soft back-face
|
|
114
|
+
// shading + a noticeable halo against the sun.
|
|
115
|
+
foliage_params: [0.5, 0.5, 0.0, 0.0],
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[repr(C)]
|
|
121
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
122
|
+
pub struct PerDrawUniforms {
|
|
123
|
+
pub mvp: [[f32; 4]; 4],
|
|
124
|
+
pub model: [[f32; 4]; 4],
|
|
125
|
+
pub prev_mvp: [[f32; 4]; 4],
|
|
126
|
+
pub model_tint: [f32; 4],
|
|
127
|
+
pub skin_info: [u32; 4],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// =====================================================================
|
|
131
|
+
// Handles + draw commands
|
|
132
|
+
// =====================================================================
|
|
133
|
+
|
|
134
|
+
pub type MaterialHandle = u32;
|
|
135
|
+
|
|
136
|
+
pub struct MaterialDrawCommand {
|
|
137
|
+
pub material: MaterialHandle,
|
|
138
|
+
pub mesh_handle: u64, // matches model_gpu_cache keys
|
|
139
|
+
pub mesh_idx: usize, // sub-mesh index within that cached model
|
|
140
|
+
pub draw_slot: usize, // which slot in per_draw_buffers to bind
|
|
141
|
+
/// Clip-space w of the object pivot at submit time (= view-space
|
|
142
|
+
/// depth for a standard projection). Drives the back-to-front sort
|
|
143
|
+
/// of the translucent bucket; opaque draws ignore it.
|
|
144
|
+
pub view_depth: f32,
|
|
145
|
+
/// EN-001 — when set, the engine binds vertex slot 1 to this
|
|
146
|
+
/// instance buffer and emits draw_indexed with `0..count` instances.
|
|
147
|
+
/// `None` means a single-instance draw (the legacy path).
|
|
148
|
+
pub instance: Option<InstanceDrawInfo>,
|
|
149
|
+
/// CPU-side copy of the model matrix (the GPU one lives in the
|
|
150
|
+
/// per-draw UBO slot). The shadow pass needs it to re-render these
|
|
151
|
+
/// draws into the sun cascades without reading buffers back.
|
|
152
|
+
pub model: [[f32; 4]; 4],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Reference to an instance buffer for an instanced draw command.
|
|
156
|
+
/// `buffer_handle` is 1-based into `MaterialSystem::instance_buffers`;
|
|
157
|
+
/// `count` is the number of GPU instances to emit. Created via
|
|
158
|
+
/// `MaterialSystem::create_instance_buffer`, consumed by
|
|
159
|
+
/// `submit_draw_instanced`.
|
|
160
|
+
#[derive(Copy, Clone, Debug)]
|
|
161
|
+
pub struct InstanceDrawInfo {
|
|
162
|
+
pub buffer_handle: u32,
|
|
163
|
+
pub count: u32,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// EN-001 — owned wgpu::Buffer + element count for an instance buffer.
|
|
167
|
+
/// Lives in the `MaterialSystem::instance_buffers` registry, indexed
|
|
168
|
+
/// by 1-based handle (0 = invalid).
|
|
169
|
+
pub struct InstanceBuffer {
|
|
170
|
+
pub buffer: wgpu::Buffer,
|
|
171
|
+
pub count: u32,
|
|
172
|
+
/// Spatial tiles over the instances (built at creation by
|
|
173
|
+
/// reordering instances into an XZ grid). Empty = untiled (small
|
|
174
|
+
/// buffers). Opaque/cutout instanced draws use these to
|
|
175
|
+
/// frustum-cull whole tiles per view — a 20k-instance grass field
|
|
176
|
+
/// stops rasterizing the half behind the camera. Translucent draws
|
|
177
|
+
/// ignore tiles (their submission order is author-controlled).
|
|
178
|
+
pub tiles: Vec<InstanceTile>,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// One spatial tile of an instance buffer: a contiguous instance range
|
|
182
|
+
/// plus the AABB of the member instances' POSITIONS and their max
|
|
183
|
+
/// scale. At cull time the position AABB is inflated by
|
|
184
|
+
/// `max_scale × (drawn mesh's local half-diagonal)` — conservative for
|
|
185
|
+
/// any rotation, exact enough at tile granularity.
|
|
186
|
+
#[derive(Copy, Clone)]
|
|
187
|
+
pub struct InstanceTile {
|
|
188
|
+
pub first: u32,
|
|
189
|
+
pub count: u32,
|
|
190
|
+
pub pmin: [f32; 3],
|
|
191
|
+
pub pmax: [f32; 3],
|
|
192
|
+
pub max_scale: f32,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// EN-014 — owned wgpu::Texture + view + layer-count for a texture
|
|
196
|
+
/// array. Lives in the `MaterialSystem::texture_arrays` registry,
|
|
197
|
+
/// indexed by 1-based handle (0 = invalid). The view is a D2Array
|
|
198
|
+
/// view over all `layer_count` layers (so `textureSample(arr, samp,
|
|
199
|
+
/// uv, layer_idx)` resolves layers 0..layer_count-1).
|
|
200
|
+
pub struct TextureArray {
|
|
201
|
+
pub texture: wgpu::Texture,
|
|
202
|
+
pub view: wgpu::TextureView,
|
|
203
|
+
pub layer_count: u32,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// EN-014 — V1 cap on layers per texture array. The ticket calls this
|
|
207
|
+
/// "enough for any landscape shader" (UE5's typical landscape uses
|
|
208
|
+
/// 4–8 layers; 16 leaves headroom for foliage/debris splats).
|
|
209
|
+
pub const MAX_TEXTURE_ARRAY_LAYERS: u32 = 16;
|
|
210
|
+
|
|
211
|
+
/// EN-014 V2 — texture-array format codes, as exposed to the TS API
|
|
212
|
+
/// via `TEX_ARRAY_FORMAT_*`. Anything unrecognised falls back to sRGB
|
|
213
|
+
/// since albedo is the most common splat layer.
|
|
214
|
+
pub const TEX_ARRAY_FORMAT_SRGB: u32 = 0;
|
|
215
|
+
pub const TEX_ARRAY_FORMAT_LINEAR: u32 = 1;
|
|
216
|
+
|
|
217
|
+
/// Map a TS-side format code to a wgpu format. sRGB suits albedo; linear
|
|
218
|
+
/// is mandatory for normal/MR data textures (sRGB decoding would corrupt
|
|
219
|
+
/// the encoded normals or rough/metal channels).
|
|
220
|
+
pub fn map_texture_array_format(code: u32) -> wgpu::TextureFormat {
|
|
221
|
+
match code {
|
|
222
|
+
TEX_ARRAY_FORMAT_SRGB => wgpu::TextureFormat::Rgba8UnormSrgb,
|
|
223
|
+
TEX_ARRAY_FORMAT_LINEAR => wgpu::TextureFormat::Rgba8Unorm,
|
|
224
|
+
_ => wgpu::TextureFormat::Rgba8UnormSrgb,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// =====================================================================
|
|
229
|
+
// The system
|
|
230
|
+
// =====================================================================
|
|
231
|
+
|
|
232
|
+
pub struct MaterialSystem {
|
|
233
|
+
pub layouts: MaterialAbiLayouts,
|
|
234
|
+
|
|
235
|
+
// Compiled pipelines, indexed by MaterialHandle (1-based; 0 = invalid).
|
|
236
|
+
pub pipelines: Vec<Option<MaterialPipeline>>,
|
|
237
|
+
|
|
238
|
+
// Per-material "render into planar reflection probes" flag (1-based
|
|
239
|
+
// like `pipelines`, default true). Authoring control for content
|
|
240
|
+
// that is sub-pixel or irrelevant at probe resolution — e.g. a 20k-
|
|
241
|
+
// instance grass field in a 512² water probe — mirroring the
|
|
242
|
+
// "visible in reflections" toggle every major engine exposes.
|
|
243
|
+
pub probe_visible: Vec<bool>,
|
|
244
|
+
|
|
245
|
+
// Per-frame UBO + bind group (rewritten at the start of every frame).
|
|
246
|
+
pub per_frame_buffer: wgpu::Buffer,
|
|
247
|
+
pub per_frame_bg: wgpu::BindGroup,
|
|
248
|
+
|
|
249
|
+
// Per-view UBO — one for now (single camera). Phase 2 may add more
|
|
250
|
+
// for split-screen / shadow cascades.
|
|
251
|
+
pub per_view_buffer: wgpu::Buffer,
|
|
252
|
+
pub per_view_bg: wgpu::BindGroup,
|
|
253
|
+
|
|
254
|
+
// Default per-material bind group: all white 1×1 textures, default
|
|
255
|
+
// factors, zero-initialised user-params. Materials that don't
|
|
256
|
+
// provide their own share this.
|
|
257
|
+
pub default_per_material_bg: wgpu::BindGroup,
|
|
258
|
+
/// Kept alive so the BG it backs doesn't dangle.
|
|
259
|
+
_default_material_factors_buffer: wgpu::Buffer,
|
|
260
|
+
_default_user_params_buffer: wgpu::Buffer,
|
|
261
|
+
_default_white_tex: wgpu::Texture,
|
|
262
|
+
_default_white_view: wgpu::TextureView,
|
|
263
|
+
_default_sampler: wgpu::Sampler,
|
|
264
|
+
/// EN-011 — 1×1 black texture bound at @group(2) @binding(12) for
|
|
265
|
+
/// materials that don't have a planar reflection probe linked.
|
|
266
|
+
/// Lets shaders unconditionally `textureSample(planar_reflection_tex,
|
|
267
|
+
/// …)` without branching on probe presence.
|
|
268
|
+
_default_black_tex: wgpu::Texture,
|
|
269
|
+
pub default_black_view: wgpu::TextureView,
|
|
270
|
+
/// EN-014 — 1×1×1 transparent-black texture-array stub bound to
|
|
271
|
+
/// bindings 14/15/16 (@group(2)) for materials that don't declare
|
|
272
|
+
/// their own array. Has to be a real D2Array view (not a 2D
|
|
273
|
+
/// texture cast) so the layout's `view_dimension: D2Array`
|
|
274
|
+
/// matches at bind time.
|
|
275
|
+
_default_array_tex: wgpu::Texture,
|
|
276
|
+
pub default_array_view: wgpu::TextureView,
|
|
277
|
+
|
|
278
|
+
/// Phase 5 — per-material `user_params` UBOs. Indexed by
|
|
279
|
+
/// `MaterialHandle - 1`; `None` means the material uses the default
|
|
280
|
+
/// (zero-initialised) bind group. Created lazily on first
|
|
281
|
+
/// `set_user_params` call.
|
|
282
|
+
pub material_params_buffers: Vec<Option<wgpu::Buffer>>,
|
|
283
|
+
pub material_per_material_bgs: Vec<Option<wgpu::BindGroup>>,
|
|
284
|
+
|
|
285
|
+
/// EN-011 — link from material → planar reflection probe handle
|
|
286
|
+
/// (1-based; `None` → default 1×1 black texture at binding 12).
|
|
287
|
+
/// Indexed by `MaterialHandle - 1`. Set via
|
|
288
|
+
/// `Renderer::set_material_reflection_probe`; the per-material
|
|
289
|
+
/// bind group is rebuilt at that call so subsequent draws see
|
|
290
|
+
/// the probe's texture even though the engine repaints the
|
|
291
|
+
/// texture each frame (the wgpu Texture identity stays stable).
|
|
292
|
+
pub material_reflection_probe: Vec<Option<u32>>,
|
|
293
|
+
|
|
294
|
+
// Per-draw UBO pool. Buffers grow 1-by-1 as draws pile up.
|
|
295
|
+
// Each entry is `(PerDraw UBO, bind group binding it + the global
|
|
296
|
+
// joint buffer at binding 1)`.
|
|
297
|
+
pub per_draw_buffers: Vec<wgpu::Buffer>,
|
|
298
|
+
pub per_draw_bgs: Vec<wgpu::BindGroup>,
|
|
299
|
+
|
|
300
|
+
// Phase 4b — group 4 (SceneInputs) bind group. Rebuilt per-frame
|
|
301
|
+
// when any Refractive material is submitted and a scene-colour
|
|
302
|
+
// snapshot is available. `None` means "no material needs this
|
|
303
|
+
// group this frame" — translucent dispatch skips group 4
|
|
304
|
+
// binding entirely.
|
|
305
|
+
pub scene_inputs_bg: Option<wgpu::BindGroup>,
|
|
306
|
+
/// Linear sampler for scene-colour sampling (group 4 binding 1).
|
|
307
|
+
_scene_color_sampler: wgpu::Sampler,
|
|
308
|
+
/// Non-filtering sampler for scene-depth sampling (binding 3).
|
|
309
|
+
_scene_depth_sampler: wgpu::Sampler,
|
|
310
|
+
/// 1×1 default texture for impulse / motion-vectors slots when
|
|
311
|
+
/// no Phase 7 impulse system is wired up yet.
|
|
312
|
+
_scene_stub_tex: wgpu::Texture,
|
|
313
|
+
_scene_stub_view: wgpu::TextureView,
|
|
314
|
+
/// 1×1 stub depth texture — bound to scene_depth_tex in Phase 4b
|
|
315
|
+
/// because the live depth buffer can't be simultaneously sampled
|
|
316
|
+
/// and used as a depth-stencil attachment. Phase 4c will add a
|
|
317
|
+
/// copy-to-sample depth snapshot for shoreline-fade materials.
|
|
318
|
+
_scene_stub_depth: wgpu::Texture,
|
|
319
|
+
_scene_stub_depth_view: wgpu::TextureView,
|
|
320
|
+
|
|
321
|
+
// Frame state — commands split by bucket so the graph can
|
|
322
|
+
// schedule them into the right pass. Phase 4a keeps them in
|
|
323
|
+
// parallel lists; Phase 4b dispatches the translucent lists in
|
|
324
|
+
// their own sub-pass.
|
|
325
|
+
pub commands: Vec<MaterialDrawCommand>, // Bucket::Opaque + Bucket::Cutout
|
|
326
|
+
pub translucent_commands: Vec<MaterialDrawCommand>, // Transparent + Refractive + Additive
|
|
327
|
+
next_draw_slot: usize,
|
|
328
|
+
|
|
329
|
+
/// EN-022 — per-slot model matrices from the PREVIOUS frame, keyed
|
|
330
|
+
/// by draw slot (= submission order, which is stable frame-to-frame
|
|
331
|
+
/// for immediate-mode games; the cached-model path relies on the
|
|
332
|
+
/// same convention). `submit_draw` composes prev_vp × prev_model
|
|
333
|
+
/// into PerDraw.prev_mvp so materials can output real motion
|
|
334
|
+
/// vectors — the ABI field existed since day one but was filled
|
|
335
|
+
/// with the CURRENT mvp, making world velocity identically zero
|
|
336
|
+
/// (round-2 audit F8).
|
|
337
|
+
prev_models: Vec<[[f32; 4]; 4]>,
|
|
338
|
+
cur_models: Vec<[[f32; 4]; 4]>,
|
|
339
|
+
/// Previous frame's view-projection (set in reset_draw_slot).
|
|
340
|
+
prev_vp: [[f32; 4]; 4],
|
|
341
|
+
|
|
342
|
+
/// EN-001 — instance buffers, indexed by InstanceBufferHandle
|
|
343
|
+
/// (1-based; 0 = invalid). Each entry owns a wgpu Buffer + element
|
|
344
|
+
/// count. Created via `create_instance_buffer`, consumed by
|
|
345
|
+
/// `submit_draw_instanced` commands. Slots remain `None` after
|
|
346
|
+
/// `destroy_instance_buffer` so existing handles never collide
|
|
347
|
+
/// with re-issued ones.
|
|
348
|
+
pub instance_buffers: Vec<Option<InstanceBuffer>>,
|
|
349
|
+
|
|
350
|
+
/// EN-014 — texture arrays, indexed by 1-based handle (0 = invalid).
|
|
351
|
+
/// Each entry owns a wgpu D2Array texture + view + layer-count.
|
|
352
|
+
/// Created via `create_texture_array`, linked to a material's
|
|
353
|
+
/// per-material BG via `set_material_texture_array`.
|
|
354
|
+
pub texture_arrays: Vec<Option<TextureArray>>,
|
|
355
|
+
|
|
356
|
+
/// EN-014 — per-material → array-handle link, one slot for each
|
|
357
|
+
/// of the 3 array bindings (0 = albedo, 1 = normal, 2 = MR).
|
|
358
|
+
/// `None` means "use the default 1×1×1 stub array". Indexed by
|
|
359
|
+
/// `MaterialHandle - 1`, parallel to `material_per_material_bgs`
|
|
360
|
+
/// / `material_reflection_probe`.
|
|
361
|
+
pub material_texture_arrays: Vec<[Option<u32>; 3]>,
|
|
362
|
+
|
|
363
|
+
/// EN-012 — per-material `MaterialFactorsUniforms` UBO. `None` means
|
|
364
|
+
/// the material uses the shared `_default_material_factors_buffer`
|
|
365
|
+
/// (uniform default) — lazily allocated on the first
|
|
366
|
+
/// `set_material_shading_model` / `set_material_foliage` call.
|
|
367
|
+
/// Indexed by `MaterialHandle - 1`. The CPU-side mirror in
|
|
368
|
+
/// `material_factors_data` lets us partial-update one field at a
|
|
369
|
+
/// time without losing the others.
|
|
370
|
+
pub material_factors_buffers: Vec<Option<wgpu::Buffer>>,
|
|
371
|
+
pub material_factors_data: Vec<MaterialFactorsUniforms>,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
impl MaterialSystem {
|
|
375
|
+
pub fn new(
|
|
376
|
+
device: &wgpu::Device,
|
|
377
|
+
queue: &wgpu::Queue,
|
|
378
|
+
// Joint-buffer plumbing is per-draw (used by ensure_draw_slot,
|
|
379
|
+
// not at construction time). Kept on the constructor's
|
|
380
|
+
// signature for symmetry with the per-draw path.
|
|
381
|
+
_joint_buffer: &wgpu::Buffer,
|
|
382
|
+
) -> Self {
|
|
383
|
+
let layouts = MaterialAbiLayouts::create(device);
|
|
384
|
+
|
|
385
|
+
// Per-frame UBO.
|
|
386
|
+
let per_frame_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
387
|
+
label: Some("material_per_frame"),
|
|
388
|
+
size: std::mem::size_of::<PerFrameUniforms>() as u64,
|
|
389
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
390
|
+
mapped_at_creation: false,
|
|
391
|
+
});
|
|
392
|
+
// EN-063 — on wasm32 the per_frame group also carries the seven
|
|
393
|
+
// folded SceneInputs bindings, whose stub resources are only
|
|
394
|
+
// created further down; the wasm32 bind group is built there.
|
|
395
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
396
|
+
let per_frame_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
397
|
+
label: Some("material_per_frame_bg"),
|
|
398
|
+
layout: &layouts.per_frame,
|
|
399
|
+
entries: &[wgpu::BindGroupEntry {
|
|
400
|
+
binding: 0, resource: per_frame_buffer.as_entire_binding(),
|
|
401
|
+
}],
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Default white 1×1 texture + sampler shared across optional PBR bindings.
|
|
405
|
+
let default_white_tex = device.create_texture_with_data(
|
|
406
|
+
queue,
|
|
407
|
+
&wgpu::TextureDescriptor {
|
|
408
|
+
label: Some("material_default_white"),
|
|
409
|
+
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
410
|
+
mip_level_count: 1,
|
|
411
|
+
sample_count: 1,
|
|
412
|
+
dimension: wgpu::TextureDimension::D2,
|
|
413
|
+
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
|
414
|
+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
|
415
|
+
view_formats: &[],
|
|
416
|
+
},
|
|
417
|
+
Default::default(),
|
|
418
|
+
&[255, 255, 255, 255],
|
|
419
|
+
);
|
|
420
|
+
let default_white_view = default_white_tex.create_view(&Default::default());
|
|
421
|
+
|
|
422
|
+
// EN-011 — Default 1×1 black HDR texture for the planar
|
|
423
|
+
// reflection slot (binding 12). Format is Rgba16Float to match
|
|
424
|
+
// the actual probe RT so the bind-group layout is identical
|
|
425
|
+
// whether a probe is bound or the default.
|
|
426
|
+
let black_pixel: [u16; 4] = [0, 0, 0, 0];
|
|
427
|
+
let default_black_tex = device.create_texture_with_data(
|
|
428
|
+
queue,
|
|
429
|
+
&wgpu::TextureDescriptor {
|
|
430
|
+
label: Some("material_default_black_reflection"),
|
|
431
|
+
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
432
|
+
mip_level_count: 1,
|
|
433
|
+
sample_count: 1,
|
|
434
|
+
dimension: wgpu::TextureDimension::D2,
|
|
435
|
+
format: super::formats::HDR_FORMAT,
|
|
436
|
+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
|
437
|
+
view_formats: &[],
|
|
438
|
+
},
|
|
439
|
+
Default::default(),
|
|
440
|
+
bytemuck::cast_slice(&black_pixel),
|
|
441
|
+
);
|
|
442
|
+
let default_black_view = default_black_tex.create_view(&Default::default());
|
|
443
|
+
|
|
444
|
+
// EN-014 — Default 1×1×1 transparent-black texture-array stub
|
|
445
|
+
// for bindings 14/15/16. Has to be a real D2Array (depth_or_
|
|
446
|
+
// _array_layers = 1, viewed as D2Array) so the bind-group
|
|
447
|
+
// layout's view_dimension matches at bind time. Format is
|
|
448
|
+
// Rgba8Unorm — same as a typical albedo source so a game
|
|
449
|
+
// dropping in albedo data without thinking gets the expected
|
|
450
|
+
// sRGB-vs-linear behaviour. Materials that need linear
|
|
451
|
+
// (normal/MR) typically encode that into the layer they upload.
|
|
452
|
+
let default_array_tex = device.create_texture_with_data(
|
|
453
|
+
queue,
|
|
454
|
+
&wgpu::TextureDescriptor {
|
|
455
|
+
label: Some("material_default_array_stub"),
|
|
456
|
+
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
457
|
+
mip_level_count: 1,
|
|
458
|
+
sample_count: 1,
|
|
459
|
+
dimension: wgpu::TextureDimension::D2,
|
|
460
|
+
format: wgpu::TextureFormat::Rgba8Unorm,
|
|
461
|
+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
|
462
|
+
view_formats: &[],
|
|
463
|
+
},
|
|
464
|
+
Default::default(),
|
|
465
|
+
&[0, 0, 0, 0],
|
|
466
|
+
);
|
|
467
|
+
let default_array_view = default_array_tex.create_view(&wgpu::TextureViewDescriptor {
|
|
468
|
+
label: Some("material_default_array_stub_view"),
|
|
469
|
+
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
|
470
|
+
..Default::default()
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
let default_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
474
|
+
label: Some("material_default_samp"),
|
|
475
|
+
address_mode_u: wgpu::AddressMode::Repeat,
|
|
476
|
+
address_mode_v: wgpu::AddressMode::Repeat,
|
|
477
|
+
mag_filter: wgpu::FilterMode::Linear,
|
|
478
|
+
min_filter: wgpu::FilterMode::Linear,
|
|
479
|
+
mipmap_filter: wgpu::MipmapFilterMode::Linear,
|
|
480
|
+
..Default::default()
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Per-view UBO — zero init; write the real data each frame in `begin_frame`.
|
|
484
|
+
let per_view_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
485
|
+
label: Some("material_per_view"),
|
|
486
|
+
size: std::mem::size_of::<PerViewUniforms>() as u64,
|
|
487
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
488
|
+
mapped_at_creation: false,
|
|
489
|
+
});
|
|
490
|
+
// We don't have the env / BRDF LUT / shadow textures plumbed through
|
|
491
|
+
// the material-system layout yet — Phase 2 wires them from the
|
|
492
|
+
// existing renderer state. For now the PerView bind group uses the
|
|
493
|
+
// default white texture + sampler for the env / BRDF / shadow slots
|
|
494
|
+
// as placeholders so draws validate.
|
|
495
|
+
let white_samp = &default_sampler;
|
|
496
|
+
let white_view = &default_white_view;
|
|
497
|
+
let cmp_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
498
|
+
label: Some("material_default_cmp_samp"),
|
|
499
|
+
compare: Some(wgpu::CompareFunction::LessEqual),
|
|
500
|
+
..Default::default()
|
|
501
|
+
});
|
|
502
|
+
// PerView layout wants a depth texture for shadow bindings 6-8.
|
|
503
|
+
// Use a 1×1 depth as a stub.
|
|
504
|
+
let stub_depth = device.create_texture(&wgpu::TextureDescriptor {
|
|
505
|
+
label: Some("material_stub_depth"),
|
|
506
|
+
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
507
|
+
mip_level_count: 1,
|
|
508
|
+
sample_count: 1,
|
|
509
|
+
dimension: wgpu::TextureDimension::D2,
|
|
510
|
+
format: wgpu::TextureFormat::Depth32Float,
|
|
511
|
+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
512
|
+
view_formats: &[],
|
|
513
|
+
});
|
|
514
|
+
let stub_depth_view = stub_depth.create_view(&Default::default());
|
|
515
|
+
let per_view_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
516
|
+
label: Some("material_per_view_bg"),
|
|
517
|
+
layout: &layouts.per_view,
|
|
518
|
+
entries: &[
|
|
519
|
+
wgpu::BindGroupEntry { binding: 0, resource: per_view_buffer.as_entire_binding() },
|
|
520
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(white_view) },
|
|
521
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(white_samp) },
|
|
522
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(white_view) },
|
|
523
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(white_view) },
|
|
524
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(white_samp) },
|
|
525
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&stub_depth_view) },
|
|
526
|
+
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&stub_depth_view) },
|
|
527
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&stub_depth_view) },
|
|
528
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&cmp_sampler) },
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
// The stub_depth texture and cmp_sampler outlive the bind group via
|
|
532
|
+
// wgpu internal Arc; we don't need to hold them in the struct.
|
|
533
|
+
std::mem::forget(stub_depth);
|
|
534
|
+
std::mem::forget(stub_depth_view);
|
|
535
|
+
std::mem::forget(cmp_sampler);
|
|
536
|
+
|
|
537
|
+
// Default MaterialFactors UBO.
|
|
538
|
+
let default_mf = MaterialFactorsUniforms::default();
|
|
539
|
+
let default_material_factors_buffer = device.create_buffer_init(
|
|
540
|
+
&wgpu::util::BufferInitDescriptor {
|
|
541
|
+
label: Some("material_default_factors"),
|
|
542
|
+
contents: bytemuck::bytes_of(&default_mf),
|
|
543
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
// Default user-params UBO — 256 bytes of zeros (ABI §1.4).
|
|
547
|
+
let default_user_params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
548
|
+
label: Some("material_default_user_params"),
|
|
549
|
+
size: 256,
|
|
550
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
551
|
+
mapped_at_creation: false,
|
|
552
|
+
});
|
|
553
|
+
// Need a SECOND sampler here because wgpu requires distinct sampler
|
|
554
|
+
// handles when binding the same logical sampler to multiple slots
|
|
555
|
+
// within one BG — actually it doesn't, the same handle works fine.
|
|
556
|
+
// We reuse default_sampler for every material-tex sampler slot.
|
|
557
|
+
let default_per_material_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
558
|
+
label: Some("material_default_per_material_bg"),
|
|
559
|
+
layout: &layouts.per_material,
|
|
560
|
+
entries: &[
|
|
561
|
+
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&default_white_view) },
|
|
562
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&default_sampler) },
|
|
563
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&default_white_view) },
|
|
564
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&default_sampler) },
|
|
565
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&default_white_view) },
|
|
566
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&default_sampler) },
|
|
567
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&default_white_view) },
|
|
568
|
+
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&default_sampler) },
|
|
569
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&default_white_view) },
|
|
570
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&default_sampler) },
|
|
571
|
+
wgpu::BindGroupEntry { binding: 10, resource: default_material_factors_buffer.as_entire_binding() },
|
|
572
|
+
wgpu::BindGroupEntry { binding: 11, resource: default_user_params_buffer.as_entire_binding() },
|
|
573
|
+
// EN-011 — default black 1×1 reflection texture +
|
|
574
|
+
// shared linear sampler. Replaced per-material when a
|
|
575
|
+
// game calls `set_material_reflection_probe`.
|
|
576
|
+
wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(&default_black_view) },
|
|
577
|
+
wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&default_sampler) },
|
|
578
|
+
// EN-014 — default 1×1×1 stub array bound to all 3
|
|
579
|
+
// texture-array slots, with the shared default
|
|
580
|
+
// sampler at binding 17. Replaced per-slot when a
|
|
581
|
+
// game calls `set_material_texture_array`.
|
|
582
|
+
wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(&default_array_view) },
|
|
583
|
+
wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(&default_array_view) },
|
|
584
|
+
wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(&default_array_view) },
|
|
585
|
+
wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&default_sampler) },
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Phase 4b — scene-inputs scratch resources. Samplers are
|
|
590
|
+
// stable across frames; the bind group itself is rebuilt
|
|
591
|
+
// when a scene-colour snapshot becomes available.
|
|
592
|
+
let scene_color_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
593
|
+
label: Some("scene_color_samp"),
|
|
594
|
+
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
|
595
|
+
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
|
596
|
+
mag_filter: wgpu::FilterMode::Linear,
|
|
597
|
+
min_filter: wgpu::FilterMode::Linear,
|
|
598
|
+
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
|
599
|
+
..Default::default()
|
|
600
|
+
});
|
|
601
|
+
let scene_depth_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
|
602
|
+
label: Some("scene_depth_samp"),
|
|
603
|
+
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
|
604
|
+
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
|
605
|
+
mag_filter: wgpu::FilterMode::Nearest,
|
|
606
|
+
min_filter: wgpu::FilterMode::Nearest,
|
|
607
|
+
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
|
608
|
+
..Default::default()
|
|
609
|
+
});
|
|
610
|
+
// 1×1 black texture as the stub impulse / motion-vector slot
|
|
611
|
+
// until Phase 7 wires real sources.
|
|
612
|
+
let scene_stub_tex = device.create_texture_with_data(
|
|
613
|
+
queue,
|
|
614
|
+
&wgpu::TextureDescriptor {
|
|
615
|
+
label: Some("scene_inputs_stub"),
|
|
616
|
+
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
617
|
+
mip_level_count: 1,
|
|
618
|
+
sample_count: 1,
|
|
619
|
+
dimension: wgpu::TextureDimension::D2,
|
|
620
|
+
format: wgpu::TextureFormat::Rgba8Unorm,
|
|
621
|
+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
|
622
|
+
view_formats: &[],
|
|
623
|
+
},
|
|
624
|
+
Default::default(),
|
|
625
|
+
&[0, 0, 0, 0],
|
|
626
|
+
);
|
|
627
|
+
let scene_stub_view = scene_stub_tex.create_view(&Default::default());
|
|
628
|
+
|
|
629
|
+
// Stub depth texture — Depth32Float 1×1, cleared to 1.0 (far).
|
|
630
|
+
let scene_stub_depth = device.create_texture(&wgpu::TextureDescriptor {
|
|
631
|
+
label: Some("scene_depth_stub"),
|
|
632
|
+
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
633
|
+
mip_level_count: 1,
|
|
634
|
+
sample_count: 1,
|
|
635
|
+
dimension: wgpu::TextureDimension::D2,
|
|
636
|
+
format: wgpu::TextureFormat::Depth32Float,
|
|
637
|
+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
638
|
+
view_formats: &[],
|
|
639
|
+
});
|
|
640
|
+
let scene_stub_depth_view = scene_stub_depth.create_view(&Default::default());
|
|
641
|
+
|
|
642
|
+
// EN-063 — wasm32 per_frame bind group: the UBO plus the seven
|
|
643
|
+
// folded SceneInputs slots, initially bound to the same stub
|
|
644
|
+
// resources `update_scene_inputs` falls back to. Rebuilt with
|
|
645
|
+
// the real snapshot views each frame by `update_scene_inputs`.
|
|
646
|
+
#[cfg(target_arch = "wasm32")]
|
|
647
|
+
let per_frame_bg = super::material_system_wasm::build_per_frame_bg_wasm(
|
|
648
|
+
device,
|
|
649
|
+
&layouts.per_frame,
|
|
650
|
+
&per_frame_buffer,
|
|
651
|
+
&scene_stub_view, // scene_color_tex stub
|
|
652
|
+
&scene_color_sampler,
|
|
653
|
+
&scene_stub_depth_view, // scene_depth_tex stub
|
|
654
|
+
&scene_depth_sampler,
|
|
655
|
+
&scene_stub_view, // impulse_tex stub
|
|
656
|
+
&scene_depth_sampler, // impulse_samp — NonFiltering, matches layout
|
|
657
|
+
&scene_stub_view, // motion_vectors stub
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
Self {
|
|
661
|
+
layouts,
|
|
662
|
+
pipelines: Vec::new(),
|
|
663
|
+
probe_visible: Vec::new(),
|
|
664
|
+
per_frame_buffer,
|
|
665
|
+
per_frame_bg,
|
|
666
|
+
per_view_buffer,
|
|
667
|
+
per_view_bg,
|
|
668
|
+
default_per_material_bg,
|
|
669
|
+
_default_material_factors_buffer: default_material_factors_buffer,
|
|
670
|
+
_default_user_params_buffer: default_user_params_buffer,
|
|
671
|
+
_default_white_tex: default_white_tex,
|
|
672
|
+
_default_white_view: default_white_view,
|
|
673
|
+
_default_sampler: default_sampler,
|
|
674
|
+
_default_black_tex: default_black_tex,
|
|
675
|
+
default_black_view,
|
|
676
|
+
_default_array_tex: default_array_tex,
|
|
677
|
+
default_array_view,
|
|
678
|
+
material_params_buffers: Vec::new(),
|
|
679
|
+
material_per_material_bgs: Vec::new(),
|
|
680
|
+
material_reflection_probe: Vec::new(),
|
|
681
|
+
per_draw_buffers: Vec::new(),
|
|
682
|
+
per_draw_bgs: Vec::new(),
|
|
683
|
+
scene_inputs_bg: None,
|
|
684
|
+
_scene_color_sampler: scene_color_sampler,
|
|
685
|
+
_scene_depth_sampler: scene_depth_sampler,
|
|
686
|
+
_scene_stub_tex: scene_stub_tex,
|
|
687
|
+
_scene_stub_view: scene_stub_view,
|
|
688
|
+
_scene_stub_depth: scene_stub_depth,
|
|
689
|
+
_scene_stub_depth_view: scene_stub_depth_view,
|
|
690
|
+
commands: Vec::new(),
|
|
691
|
+
translucent_commands: Vec::new(),
|
|
692
|
+
next_draw_slot: 0,
|
|
693
|
+
prev_models: Vec::new(),
|
|
694
|
+
cur_models: Vec::new(),
|
|
695
|
+
prev_vp: super::IDENTITY_MAT4,
|
|
696
|
+
instance_buffers: Vec::new(),
|
|
697
|
+
texture_arrays: Vec::new(),
|
|
698
|
+
material_texture_arrays: Vec::new(),
|
|
699
|
+
// EN-012 — per-material MaterialFactors UBOs are lazy.
|
|
700
|
+
// Until a material calls `set_shading_model` /
|
|
701
|
+
// `set_foliage`, it shares the default factors buffer.
|
|
702
|
+
material_factors_buffers: Vec::new(),
|
|
703
|
+
material_factors_data: Vec::new(),
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// --- Material registry --------------------------------------------
|
|
708
|
+
|
|
709
|
+
/// Compile a material and return its handle. Handles are 1-based;
|
|
710
|
+
/// 0 is reserved for "invalid material".
|
|
711
|
+
pub fn compile(
|
|
712
|
+
&mut self,
|
|
713
|
+
device: &wgpu::Device,
|
|
714
|
+
wgsl_source: &str,
|
|
715
|
+
profile: FragmentProfile,
|
|
716
|
+
bucket: Bucket,
|
|
717
|
+
reads_scene: bool,
|
|
718
|
+
wants_instancing: bool,
|
|
719
|
+
hdr_format: wgpu::TextureFormat,
|
|
720
|
+
material_format: wgpu::TextureFormat,
|
|
721
|
+
velocity_format: wgpu::TextureFormat,
|
|
722
|
+
albedo_format: wgpu::TextureFormat,
|
|
723
|
+
depth_format: wgpu::TextureFormat,
|
|
724
|
+
) -> Result<MaterialHandle, MaterialCompileError> {
|
|
725
|
+
// Inject the user's WGSL under a synthetic path so the
|
|
726
|
+
// preprocessor can resolve `#include "material_abi.wgsl"` etc.
|
|
727
|
+
let entry_path = "__user_material.wgsl";
|
|
728
|
+
let desc = MaterialCompileDesc {
|
|
729
|
+
label: "user_material",
|
|
730
|
+
entry_path,
|
|
731
|
+
extra_sources: &[(entry_path, wgsl_source)],
|
|
732
|
+
profile,
|
|
733
|
+
bucket,
|
|
734
|
+
reads_scene,
|
|
735
|
+
hdr_format,
|
|
736
|
+
material_format,
|
|
737
|
+
velocity_format,
|
|
738
|
+
albedo_format,
|
|
739
|
+
depth_format,
|
|
740
|
+
vertex_buffers: &[Vertex3D::desc()],
|
|
741
|
+
wants_instancing,
|
|
742
|
+
};
|
|
743
|
+
let pipeline = compile_material(device, &self.layouts, &desc)?;
|
|
744
|
+
self.pipelines.push(Some(pipeline));
|
|
745
|
+
self.probe_visible.push(true);
|
|
746
|
+
Ok(self.pipelines.len() as MaterialHandle)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/// Set whether a material's draws render into planar-reflection
|
|
750
|
+
/// probes (default true). See `probe_visible` field docs.
|
|
751
|
+
pub fn set_probe_visible(&mut self, material: MaterialHandle, visible: bool) {
|
|
752
|
+
if material >= 1 {
|
|
753
|
+
if let Some(slot) = self.probe_visible.get_mut(material as usize - 1) {
|
|
754
|
+
*slot = visible;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
pub fn material_probe_visible(&self, material: MaterialHandle) -> bool {
|
|
760
|
+
if material < 1 { return true; }
|
|
761
|
+
self.probe_visible.get(material as usize - 1).copied().unwrap_or(true)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// --- Frame lifecycle ----------------------------------------------
|
|
765
|
+
|
|
766
|
+
/// Write the per-frame + per-view UBOs. Safe to call any time
|
|
767
|
+
/// before `dispatch`; callers should call exactly once per frame
|
|
768
|
+
/// to keep `PerFrame.time` accurate. Does NOT clear the commands
|
|
769
|
+
/// list — that's `reset_frame`'s job (called at begin_frame, not
|
|
770
|
+
/// end_frame, so draws submitted during the frame survive).
|
|
771
|
+
pub fn update_frame_uniforms(
|
|
772
|
+
&mut self,
|
|
773
|
+
queue: &wgpu::Queue,
|
|
774
|
+
per_frame: &PerFrameUniforms,
|
|
775
|
+
per_view: &PerViewUniforms,
|
|
776
|
+
) {
|
|
777
|
+
queue.write_buffer(&self.per_frame_buffer, 0, bytemuck::bytes_of(per_frame));
|
|
778
|
+
queue.write_buffer(&self.per_view_buffer, 0, bytemuck::bytes_of(per_view));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/// Shadow-flicker fix — re-upload just PerView's trailing shadow
|
|
782
|
+
/// fields. `update_frame_uniforms` runs BEFORE the shadow pass fits
|
|
783
|
+
/// this frame's cascade VPs, so PerView otherwise carries the
|
|
784
|
+
/// PREVIOUS frame's matrices while the depth maps hold this frame's
|
|
785
|
+
/// content — a one-frame mismatch that flashes acne bands across
|
|
786
|
+
/// material-path receivers whenever the fit moves (the deferred path
|
|
787
|
+
/// reads the freshly-written lighting buffer and never had this).
|
|
788
|
+
/// Called from `record_shadow_pass` after the fit; queued buffer
|
|
789
|
+
/// writes all apply before any of this frame's draws execute, so
|
|
790
|
+
/// this patch wins over the earlier stale upload.
|
|
791
|
+
pub fn refresh_shadow_uniforms(
|
|
792
|
+
&self,
|
|
793
|
+
queue: &wgpu::Queue,
|
|
794
|
+
shadow_splits: [f32; 4],
|
|
795
|
+
shadow_view: [[f32; 4]; 4],
|
|
796
|
+
shadow_cascades: [[[f32; 4]; 4]; 3],
|
|
797
|
+
) {
|
|
798
|
+
#[repr(C)]
|
|
799
|
+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
|
800
|
+
struct ShadowTail {
|
|
801
|
+
shadow_splits: [f32; 4],
|
|
802
|
+
shadow_view: [[f32; 4]; 4],
|
|
803
|
+
shadow_cascades: [[[f32; 4]; 4]; 3],
|
|
804
|
+
}
|
|
805
|
+
let tail = ShadowTail { shadow_splits, shadow_view, shadow_cascades };
|
|
806
|
+
// The shadow fields are the last three in PerViewUniforms; Pod
|
|
807
|
+
// guarantees no padding, so the tail offset is exact.
|
|
808
|
+
let offset = (std::mem::size_of::<PerViewUniforms>()
|
|
809
|
+
- std::mem::size_of::<ShadowTail>()) as u64;
|
|
810
|
+
queue.write_buffer(&self.per_view_buffer, offset, bytemuck::bytes_of(&tail));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/// Reset the per-draw slot cursor. Commands lists are cleared by
|
|
814
|
+
/// the Renderer from its own `begin_frame` so the order of reset
|
|
815
|
+
/// vs. submit is deterministic.
|
|
816
|
+
///
|
|
817
|
+
/// EN-022 — also rotates the per-slot model history: this frame's
|
|
818
|
+
/// models become the previous frame's, and `prev_vp` is set to the
|
|
819
|
+
/// VP that was current when those models were submitted.
|
|
820
|
+
pub fn reset_draw_slot(&mut self, prev_vp: [[f32; 4]; 4]) {
|
|
821
|
+
self.next_draw_slot = 0;
|
|
822
|
+
std::mem::swap(&mut self.prev_models, &mut self.cur_models);
|
|
823
|
+
self.cur_models.clear();
|
|
824
|
+
self.prev_vp = prev_vp;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/// EN-022 fix — `begin_mode_3d` overrides the velocity reference
|
|
828
|
+
/// with the previous frame's UNJITTERED VP re-jittered with the
|
|
829
|
+
/// CURRENT frame's offsets, so prev_mvp cancels the TAA jitter
|
|
830
|
+
/// exactly. (reset_draw_slot runs at begin_frame, before the
|
|
831
|
+
/// current frame's jitter is known.)
|
|
832
|
+
pub fn set_velocity_reference_vp(&mut self, vp: [[f32; 4]; 4]) {
|
|
833
|
+
self.prev_vp = vp;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/// Phase 5 — set/replace `user_params` for a specific material. The
|
|
837
|
+
/// next dispatch of this handle binds a per-material BindGroup with
|
|
838
|
+
/// the given bytes uploaded to `@group(2) @binding(11)`. Materials
|
|
839
|
+
/// that never receive a `set_user_params` call keep using the
|
|
840
|
+
/// default zero-initialised UBO.
|
|
841
|
+
///
|
|
842
|
+
/// `params.len()` must be ≤ 256 bytes (ABI §1.4 cap). The buffer
|
|
843
|
+
/// is allocated lazily on first call per handle and reused on
|
|
844
|
+
/// subsequent updates. Pass an empty slice to revert to the default.
|
|
845
|
+
pub fn set_user_params(
|
|
846
|
+
&mut self,
|
|
847
|
+
device: &wgpu::Device,
|
|
848
|
+
queue: &wgpu::Queue,
|
|
849
|
+
handle: MaterialHandle,
|
|
850
|
+
params: &[u8],
|
|
851
|
+
) -> Result<(), &'static str> {
|
|
852
|
+
if handle == 0 { return Err("invalid material handle"); }
|
|
853
|
+
let idx = (handle - 1) as usize;
|
|
854
|
+
if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
|
|
855
|
+
return Err("material handle not registered");
|
|
856
|
+
}
|
|
857
|
+
if params.len() > 256 {
|
|
858
|
+
return Err("user_params exceeds 256-byte cap");
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Grow parallel vectors so handle index is valid.
|
|
862
|
+
while self.material_params_buffers.len() <= idx {
|
|
863
|
+
self.material_params_buffers.push(None);
|
|
864
|
+
self.material_per_material_bgs.push(None);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Reverting to default — drop the per-material entries.
|
|
868
|
+
if params.is_empty() {
|
|
869
|
+
self.material_params_buffers[idx] = None;
|
|
870
|
+
self.material_per_material_bgs[idx] = None;
|
|
871
|
+
return Ok(());
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Allocate the per-material UBO + BG on first set. Padded to 256 B
|
|
875
|
+
// so the ABI cap is reflected in the buffer size and write_buffer
|
|
876
|
+
// never partially fills. EN-011 bindings 12/13 default to the
|
|
877
|
+
// shared 1×1 black reflection texture; if a probe was previously
|
|
878
|
+
// linked via `set_reflection_probe`, the reflection BG will be
|
|
879
|
+
// rebuilt by that path the next time the link changes.
|
|
880
|
+
if self.material_params_buffers[idx].is_none() {
|
|
881
|
+
let buf = device.create_buffer(&wgpu::BufferDescriptor {
|
|
882
|
+
label: Some("material_user_params"),
|
|
883
|
+
size: 256,
|
|
884
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
885
|
+
mapped_at_creation: false,
|
|
886
|
+
});
|
|
887
|
+
// EN-012 — resolve the per-material factors UBO so a
|
|
888
|
+
// material that previously called `set_shading_model` /
|
|
889
|
+
// `set_foliage` keeps its custom factors at binding 10
|
|
890
|
+
// when user_params is later set on it.
|
|
891
|
+
let factors_buf: &wgpu::Buffer = self.material_factors_buffers
|
|
892
|
+
.get(idx)
|
|
893
|
+
.and_then(|b| b.as_ref())
|
|
894
|
+
.unwrap_or(&self._default_material_factors_buffer);
|
|
895
|
+
// EN-014 BUG FIX: this used to hardcode the 1×1 stub array on
|
|
896
|
+
// bindings 14/15/16, so setting user params AFTER linking a texture
|
|
897
|
+
// array silently unbound the array — the material kept sampling the
|
|
898
|
+
// stub and every fetch came back empty. Nothing errored; the pixels
|
|
899
|
+
// simply never appeared. Resolve the links like every other
|
|
900
|
+
// BG-build path does.
|
|
901
|
+
let [albedo_view, normal_view, mr_view] = self.resolve_array_views(idx);
|
|
902
|
+
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
903
|
+
label: Some("material_per_material_bg_user"),
|
|
904
|
+
layout: &self.layouts.per_material,
|
|
905
|
+
entries: &[
|
|
906
|
+
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
907
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
908
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
909
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
910
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
911
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
912
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
913
|
+
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
914
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
915
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
916
|
+
wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
|
|
917
|
+
wgpu::BindGroupEntry { binding: 11, resource: buf.as_entire_binding() },
|
|
918
|
+
wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(&self.default_black_view) },
|
|
919
|
+
wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
920
|
+
wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(albedo_view) },
|
|
921
|
+
wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(normal_view) },
|
|
922
|
+
wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(mr_view) },
|
|
923
|
+
wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
924
|
+
],
|
|
925
|
+
});
|
|
926
|
+
self.material_params_buffers[idx] = Some(buf);
|
|
927
|
+
self.material_per_material_bgs[idx] = Some(bg);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Pad short writes to 256 B so write_buffer doesn't read past `params`.
|
|
931
|
+
let mut padded = [0u8; 256];
|
|
932
|
+
padded[..params.len()].copy_from_slice(params);
|
|
933
|
+
let buf = self.material_params_buffers[idx].as_ref().unwrap();
|
|
934
|
+
queue.write_buffer(buf, 0, &padded);
|
|
935
|
+
Ok(())
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/// Per-material BG when set, otherwise the shared default.
|
|
939
|
+
fn per_material_bg_for(&self, handle: MaterialHandle) -> &wgpu::BindGroup {
|
|
940
|
+
let idx = (handle as usize).wrapping_sub(1);
|
|
941
|
+
self.material_per_material_bgs.get(idx)
|
|
942
|
+
.and_then(|b| b.as_ref())
|
|
943
|
+
.unwrap_or(&self.default_per_material_bg)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/// EN-011 — link a material to a planar reflection probe's RT
|
|
947
|
+
/// view. The per-material bind group is rebuilt so binding 12
|
|
948
|
+
/// resolves to the probe's `color_view` instead of the default
|
|
949
|
+
/// 1×1 black texture. Subsequent draws of `handle` see the
|
|
950
|
+
/// probe's contents.
|
|
951
|
+
///
|
|
952
|
+
/// Pass `probe_view = None` (or revert to the default by passing
|
|
953
|
+
/// `&self.default_black_view` from the caller side) to unlink.
|
|
954
|
+
/// The `probe_handle` is opaque to this method — it's stored
|
|
955
|
+
/// purely so `material_reflection_probe[idx]` reflects the link
|
|
956
|
+
/// for diagnostics + the renderer's per-frame dispatch.
|
|
957
|
+
///
|
|
958
|
+
/// The wgpu Texture identity is stable for the probe's lifetime,
|
|
959
|
+
/// so we DON'T need to rebuild this bind group every frame; the
|
|
960
|
+
/// engine repaints the probe's texture but the same view keeps
|
|
961
|
+
/// pointing at it.
|
|
962
|
+
///
|
|
963
|
+
/// Why we route through here vs direct field access: the BG
|
|
964
|
+
/// also needs to bind a user-params UBO (binding 11) — either
|
|
965
|
+
/// the per-material UBO when one was allocated by
|
|
966
|
+
/// `set_user_params`, or the shared default zero-UBO. This
|
|
967
|
+
/// helper picks the right one.
|
|
968
|
+
pub fn set_reflection_probe(
|
|
969
|
+
&mut self,
|
|
970
|
+
device: &wgpu::Device,
|
|
971
|
+
handle: MaterialHandle,
|
|
972
|
+
probe_handle: u32,
|
|
973
|
+
probe_view: &wgpu::TextureView,
|
|
974
|
+
) -> Result<(), &'static str> {
|
|
975
|
+
if handle == 0 { return Err("invalid material handle"); }
|
|
976
|
+
let idx = (handle - 1) as usize;
|
|
977
|
+
if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
|
|
978
|
+
return Err("material handle not registered");
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Grow parallel vectors so the index is in bounds. We grow
|
|
982
|
+
// BOTH params + reflection registries together — they share
|
|
983
|
+
// an index domain (MaterialHandle - 1).
|
|
984
|
+
while self.material_params_buffers.len() <= idx {
|
|
985
|
+
self.material_params_buffers.push(None);
|
|
986
|
+
self.material_per_material_bgs.push(None);
|
|
987
|
+
}
|
|
988
|
+
while self.material_reflection_probe.len() <= idx {
|
|
989
|
+
self.material_reflection_probe.push(None);
|
|
990
|
+
}
|
|
991
|
+
self.material_reflection_probe[idx] = Some(probe_handle);
|
|
992
|
+
|
|
993
|
+
// Resolve binding 11 — per-material UBO if one's been
|
|
994
|
+
// allocated, else the shared zero-init default.
|
|
995
|
+
let user_params_buf: &wgpu::Buffer = self.material_params_buffers
|
|
996
|
+
.get(idx)
|
|
997
|
+
.and_then(|b| b.as_ref())
|
|
998
|
+
.unwrap_or(&self._default_user_params_buffer);
|
|
999
|
+
|
|
1000
|
+
// EN-012 — resolve binding 10 to the per-material factors UBO
|
|
1001
|
+
// when one's been allocated by `set_shading_model` /
|
|
1002
|
+
// `set_foliage`. Otherwise the reflection-probe rebind would
|
|
1003
|
+
// silently revert a foliage-flagged material back to the
|
|
1004
|
+
// default factors at binding 10.
|
|
1005
|
+
let factors_buf: &wgpu::Buffer = self.material_factors_buffers
|
|
1006
|
+
.get(idx)
|
|
1007
|
+
.and_then(|b| b.as_ref())
|
|
1008
|
+
.unwrap_or(&self._default_material_factors_buffer);
|
|
1009
|
+
|
|
1010
|
+
// EN-014 — resolve binding 14/15/16 from any per-material
|
|
1011
|
+
// texture-array links so a probe-bound material doesn't lose
|
|
1012
|
+
// its splat layers when the BG gets rebuilt.
|
|
1013
|
+
let [albedo_view, normal_view, mr_view] = self.resolve_array_views(idx);
|
|
1014
|
+
|
|
1015
|
+
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
1016
|
+
label: Some("material_per_material_bg_reflection"),
|
|
1017
|
+
layout: &self.layouts.per_material,
|
|
1018
|
+
entries: &[
|
|
1019
|
+
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1020
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1021
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1022
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1023
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1024
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1025
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1026
|
+
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1027
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1028
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1029
|
+
wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
|
|
1030
|
+
wgpu::BindGroupEntry { binding: 11, resource: user_params_buf.as_entire_binding() },
|
|
1031
|
+
// EN-011 — probe's color view + a filtering sampler.
|
|
1032
|
+
wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(probe_view) },
|
|
1033
|
+
wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1034
|
+
// EN-014 — resolved texture arrays + shared sampler.
|
|
1035
|
+
wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(albedo_view) },
|
|
1036
|
+
wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(normal_view) },
|
|
1037
|
+
wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(mr_view) },
|
|
1038
|
+
wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1039
|
+
],
|
|
1040
|
+
});
|
|
1041
|
+
self.material_per_material_bgs[idx] = Some(bg);
|
|
1042
|
+
Ok(())
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/// EN-014 — resolve the 3 D2Array texture views for a material
|
|
1046
|
+
/// (albedo / normal / MR), falling back to the shared 1×1×1 stub
|
|
1047
|
+
/// when the slot has no link. Returns borrowed views that share
|
|
1048
|
+
/// the lifetime of `self`. Callers use these to populate bindings
|
|
1049
|
+
/// 14/15/16 when (re)building a per-material BG.
|
|
1050
|
+
fn resolve_array_views(&self, idx: usize) -> [&wgpu::TextureView; 3] {
|
|
1051
|
+
let mut out: [&wgpu::TextureView; 3] = [
|
|
1052
|
+
&self.default_array_view,
|
|
1053
|
+
&self.default_array_view,
|
|
1054
|
+
&self.default_array_view,
|
|
1055
|
+
];
|
|
1056
|
+
if let Some(slots) = self.material_texture_arrays.get(idx) {
|
|
1057
|
+
for (slot, link) in slots.iter().enumerate() {
|
|
1058
|
+
if let Some(handle) = link {
|
|
1059
|
+
let h = *handle as usize;
|
|
1060
|
+
if h > 0 {
|
|
1061
|
+
if let Some(Some(arr)) = self.texture_arrays.get(h - 1) {
|
|
1062
|
+
out[slot] = &arr.view;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
out
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/// EN-012 — resolve the per-material `MaterialFactors` UBO for
|
|
1072
|
+
/// `idx`, falling back to the shared default buffer when no
|
|
1073
|
+
/// per-material factors UBO has been allocated yet. Used by every
|
|
1074
|
+
/// per-material BG-build path so EN-011 / EN-014 rebinds don't
|
|
1075
|
+
/// silently revert a material's foliage / shading-model state to
|
|
1076
|
+
/// the default factors.
|
|
1077
|
+
fn resolve_factors_buffer(&self, idx: usize) -> &wgpu::Buffer {
|
|
1078
|
+
self.material_factors_buffers
|
|
1079
|
+
.get(idx)
|
|
1080
|
+
.and_then(|b| b.as_ref())
|
|
1081
|
+
.unwrap_or(&self._default_material_factors_buffer)
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/// EN-012 — ensure a per-material `MaterialFactorsUniforms` UBO
|
|
1085
|
+
/// exists for `idx`, allocating + initialising from the default
|
|
1086
|
+
/// values on first use. Returns mutable access to the CPU mirror
|
|
1087
|
+
/// so the caller can mutate one field then write back via
|
|
1088
|
+
/// `flush_material_factors`. Grows `material_factors_buffers` /
|
|
1089
|
+
/// `material_factors_data` so `idx` is in bounds.
|
|
1090
|
+
fn ensure_material_factors(
|
|
1091
|
+
&mut self, device: &wgpu::Device, idx: usize,
|
|
1092
|
+
) -> &mut MaterialFactorsUniforms {
|
|
1093
|
+
while self.material_factors_buffers.len() <= idx {
|
|
1094
|
+
self.material_factors_buffers.push(None);
|
|
1095
|
+
self.material_factors_data.push(MaterialFactorsUniforms::default());
|
|
1096
|
+
}
|
|
1097
|
+
if self.material_factors_buffers[idx].is_none() {
|
|
1098
|
+
// Allocate a fresh per-material factors UBO seeded with the
|
|
1099
|
+
// current CPU-side data (defaults on first call).
|
|
1100
|
+
let init = self.material_factors_data[idx];
|
|
1101
|
+
let buf = device.create_buffer_init(
|
|
1102
|
+
&wgpu::util::BufferInitDescriptor {
|
|
1103
|
+
label: Some("material_factors_per_material"),
|
|
1104
|
+
contents: bytemuck::bytes_of(&init),
|
|
1105
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
1106
|
+
},
|
|
1107
|
+
);
|
|
1108
|
+
self.material_factors_buffers[idx] = Some(buf);
|
|
1109
|
+
}
|
|
1110
|
+
&mut self.material_factors_data[idx]
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/// EN-012 — write the current CPU-side `MaterialFactorsUniforms`
|
|
1114
|
+
/// for `idx` to the per-material UBO. Caller must have previously
|
|
1115
|
+
/// called `ensure_material_factors` so the buffer exists.
|
|
1116
|
+
fn flush_material_factors(&self, queue: &wgpu::Queue, idx: usize) {
|
|
1117
|
+
if let Some(Some(buf)) = self.material_factors_buffers.get(idx) {
|
|
1118
|
+
let data = &self.material_factors_data[idx];
|
|
1119
|
+
queue.write_buffer(buf, 0, bytemuck::bytes_of(data));
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/// EN-012 — rebuild the per-material BG for `idx` after a
|
|
1124
|
+
/// MaterialFactors mutation. Resolves the per-material UBO,
|
|
1125
|
+
/// user_params UBO, reflection-probe view, and texture-array
|
|
1126
|
+
/// views from the current state so we don't clobber any of the
|
|
1127
|
+
/// EN-005/EN-011/EN-014 wiring. Caller is responsible for
|
|
1128
|
+
/// passing `probe_view` (the renderer wrapper looks the linked
|
|
1129
|
+
/// probe up via `material_reflection_probe_handle` and supplies
|
|
1130
|
+
/// either the probe's RT view or the default 1×1 black view).
|
|
1131
|
+
fn rebuild_per_material_bg(
|
|
1132
|
+
&mut self,
|
|
1133
|
+
device: &wgpu::Device,
|
|
1134
|
+
idx: usize,
|
|
1135
|
+
probe_view: &wgpu::TextureView,
|
|
1136
|
+
) {
|
|
1137
|
+
// Grow parallel BG vector so `idx` is in bounds.
|
|
1138
|
+
while self.material_per_material_bgs.len() <= idx {
|
|
1139
|
+
self.material_params_buffers.push(None);
|
|
1140
|
+
self.material_per_material_bgs.push(None);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
let factors_buf: &wgpu::Buffer = self.resolve_factors_buffer(idx);
|
|
1144
|
+
let user_params_buf: &wgpu::Buffer = self.material_params_buffers
|
|
1145
|
+
.get(idx)
|
|
1146
|
+
.and_then(|b| b.as_ref())
|
|
1147
|
+
.unwrap_or(&self._default_user_params_buffer);
|
|
1148
|
+
let [albedo_view, normal_view, mr_view] = self.resolve_array_views(idx);
|
|
1149
|
+
|
|
1150
|
+
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
1151
|
+
label: Some("material_per_material_bg_factors"),
|
|
1152
|
+
layout: &self.layouts.per_material,
|
|
1153
|
+
entries: &[
|
|
1154
|
+
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1155
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1156
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1157
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1158
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1159
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1160
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1161
|
+
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1162
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1163
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1164
|
+
wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
|
|
1165
|
+
wgpu::BindGroupEntry { binding: 11, resource: user_params_buf.as_entire_binding() },
|
|
1166
|
+
wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(probe_view) },
|
|
1167
|
+
wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1168
|
+
wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(albedo_view) },
|
|
1169
|
+
wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(normal_view) },
|
|
1170
|
+
wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(mr_view) },
|
|
1171
|
+
wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1172
|
+
],
|
|
1173
|
+
});
|
|
1174
|
+
self.material_per_material_bgs[idx] = Some(bg);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/// EN-012 — set the shading model for a material (`0` = default
|
|
1178
|
+
/// lit, `1` = foliage, `2` = subsurface — V2 stub). Lazily
|
|
1179
|
+
/// allocates a per-material `MaterialFactors` UBO on first call,
|
|
1180
|
+
/// then rebuilds the per-material BG so subsequent draws sample
|
|
1181
|
+
/// the new shading model. Caller passes `probe_view` so EN-011
|
|
1182
|
+
/// reflection links survive — see `Renderer::set_material_shading_model`.
|
|
1183
|
+
pub fn set_material_shading_model(
|
|
1184
|
+
&mut self,
|
|
1185
|
+
device: &wgpu::Device,
|
|
1186
|
+
queue: &wgpu::Queue,
|
|
1187
|
+
material: MaterialHandle,
|
|
1188
|
+
model: u32,
|
|
1189
|
+
probe_view: &wgpu::TextureView,
|
|
1190
|
+
) -> Result<(), &'static str> {
|
|
1191
|
+
if material == 0 { return Err("invalid material handle"); }
|
|
1192
|
+
let idx = (material - 1) as usize;
|
|
1193
|
+
if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
|
|
1194
|
+
return Err("material handle not registered");
|
|
1195
|
+
}
|
|
1196
|
+
{
|
|
1197
|
+
let factors = self.ensure_material_factors(device, idx);
|
|
1198
|
+
factors.shading_model[0] = model as f32;
|
|
1199
|
+
}
|
|
1200
|
+
self.flush_material_factors(queue, idx);
|
|
1201
|
+
self.rebuild_per_material_bg(device, idx, probe_view);
|
|
1202
|
+
Ok(())
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/// EN-012 — set the foliage shading parameters for a material.
|
|
1206
|
+
/// Only takes effect when `shading_model == 1` (foliage). Updates
|
|
1207
|
+
/// `MaterialFactors.shading_model.yzw` (transmission_color) and
|
|
1208
|
+
/// `foliage_params.xy` (transmission_amount, wrap_factor). Lazily
|
|
1209
|
+
/// allocates a per-material UBO on first call.
|
|
1210
|
+
pub fn set_material_foliage(
|
|
1211
|
+
&mut self,
|
|
1212
|
+
device: &wgpu::Device,
|
|
1213
|
+
queue: &wgpu::Queue,
|
|
1214
|
+
material: MaterialHandle,
|
|
1215
|
+
trans_color: [f32; 3],
|
|
1216
|
+
trans_amount: f32,
|
|
1217
|
+
wrap_factor: f32,
|
|
1218
|
+
probe_view: &wgpu::TextureView,
|
|
1219
|
+
) -> Result<(), &'static str> {
|
|
1220
|
+
if material == 0 { return Err("invalid material handle"); }
|
|
1221
|
+
let idx = (material - 1) as usize;
|
|
1222
|
+
if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
|
|
1223
|
+
return Err("material handle not registered");
|
|
1224
|
+
}
|
|
1225
|
+
{
|
|
1226
|
+
let factors = self.ensure_material_factors(device, idx);
|
|
1227
|
+
factors.shading_model[1] = trans_color[0];
|
|
1228
|
+
factors.shading_model[2] = trans_color[1];
|
|
1229
|
+
factors.shading_model[3] = trans_color[2];
|
|
1230
|
+
factors.foliage_params[0] = trans_amount;
|
|
1231
|
+
factors.foliage_params[1] = wrap_factor;
|
|
1232
|
+
}
|
|
1233
|
+
self.flush_material_factors(queue, idx);
|
|
1234
|
+
self.rebuild_per_material_bg(device, idx, probe_view);
|
|
1235
|
+
Ok(())
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/// EN-014 — create a 2D texture array from a slice of layer data.
|
|
1239
|
+
/// Each `(bytes, w, h)` describes one layer's RGBA8 source. All
|
|
1240
|
+
/// layers must share `w × h` (wgpu requires a uniform extent for
|
|
1241
|
+
/// D2Array). V1 panics on mismatch — V2 may resize. Layer count
|
|
1242
|
+
/// is capped at `MAX_TEXTURE_ARRAY_LAYERS`; extra layers are
|
|
1243
|
+
/// dropped. Returns a 1-based handle (0 on failure: empty layers
|
|
1244
|
+
/// or zero extent).
|
|
1245
|
+
///
|
|
1246
|
+
/// Defaults: `format = 0` (Rgba8UnormSrgb, suitable for albedo),
|
|
1247
|
+
/// `mip_levels = 1` (no mips). For data textures (normal / MR)
|
|
1248
|
+
/// or auto-mip generation, see `create_texture_array_ex`.
|
|
1249
|
+
pub fn create_texture_array(
|
|
1250
|
+
&mut self,
|
|
1251
|
+
device: &wgpu::Device,
|
|
1252
|
+
queue: &wgpu::Queue,
|
|
1253
|
+
layers: &[(&[u8], u32, u32)],
|
|
1254
|
+
) -> u32 {
|
|
1255
|
+
// V1 default: sRGB albedo, no mips. V2 callers use the _ex
|
|
1256
|
+
// variant directly.
|
|
1257
|
+
self.create_texture_array_ex(device, queue, layers, 0, 1)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/// EN-014 V2 — create a 2D texture array with explicit pixel format
|
|
1261
|
+
/// and mip-level control. Layer extent / count rules match V1.
|
|
1262
|
+
///
|
|
1263
|
+
/// `format`:
|
|
1264
|
+
/// 0 → `Rgba8UnormSrgb` (albedo / colour textures; default)
|
|
1265
|
+
/// 1 → `Rgba8Unorm` (normal / MR / data textures — linear)
|
|
1266
|
+
/// _ → falls back to `Rgba8UnormSrgb`
|
|
1267
|
+
///
|
|
1268
|
+
/// `mip_levels`:
|
|
1269
|
+
/// 1 → no mips (matches V1 behaviour)
|
|
1270
|
+
/// 0 → auto-generate `floor(log2(max(w,h))) + 1` mips, filled
|
|
1271
|
+
/// by point-downsample (`copy_texture_to_texture` halving
|
|
1272
|
+
/// the previous mip). Cheap, correct sized, but aliased
|
|
1273
|
+
/// — a render-pass box filter is the V2.5 follow-up.
|
|
1274
|
+
/// N > 1 → not yet supported (game-supplied per-mip bytes); V2
|
|
1275
|
+
/// treats this as auto-generate.
|
|
1276
|
+
pub fn create_texture_array_ex(
|
|
1277
|
+
&mut self,
|
|
1278
|
+
device: &wgpu::Device,
|
|
1279
|
+
queue: &wgpu::Queue,
|
|
1280
|
+
layers: &[(&[u8], u32, u32)],
|
|
1281
|
+
format: u32,
|
|
1282
|
+
mip_levels: u32,
|
|
1283
|
+
) -> u32 {
|
|
1284
|
+
let layer_count = (layers.len() as u32).min(MAX_TEXTURE_ARRAY_LAYERS);
|
|
1285
|
+
if layer_count == 0 { return 0; }
|
|
1286
|
+
let (_first_bytes, w, h) = layers[0];
|
|
1287
|
+
if w == 0 || h == 0 { return 0; }
|
|
1288
|
+
// Uniform extent check — V1 hard-fail surfaces obvious bugs at
|
|
1289
|
+
// creation rather than during silent GPU truncation later.
|
|
1290
|
+
for (i, (_, lw, lh)) in layers.iter().enumerate().take(layer_count as usize) {
|
|
1291
|
+
if *lw != w || *lh != h {
|
|
1292
|
+
eprintln!(
|
|
1293
|
+
"[texture_array] layer {} extent {}×{} does not match layer 0 ({}×{}); aborting create",
|
|
1294
|
+
i, lw, lh, w, h,
|
|
1295
|
+
);
|
|
1296
|
+
return 0;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
let wgpu_format = map_texture_array_format(format);
|
|
1300
|
+
// Resolve mip count. mip_levels = 1 → single mip. mip_levels = 0
|
|
1301
|
+
// → engine-generated max. Anything else (game-supplied per-mip)
|
|
1302
|
+
// is V2.5; for now treat N > 1 as auto so games opting into mips
|
|
1303
|
+
// don't silently regress to no-mips.
|
|
1304
|
+
let max_mips = (w.max(h) as f32).log2().floor() as u32 + 1;
|
|
1305
|
+
let auto_generate = mip_levels == 0 || mip_levels > 1;
|
|
1306
|
+
let mip_level_count = if mip_levels == 1 { 1 } else { max_mips.max(1) };
|
|
1307
|
+
// Auto-gen needs COPY_SRC on the texture so we can ping-pong each
|
|
1308
|
+
// mip into the next via copy_texture_to_texture.
|
|
1309
|
+
let mut usage = wgpu::TextureUsages::TEXTURE_BINDING
|
|
1310
|
+
| wgpu::TextureUsages::COPY_DST;
|
|
1311
|
+
if auto_generate && mip_level_count > 1 {
|
|
1312
|
+
usage |= wgpu::TextureUsages::COPY_SRC;
|
|
1313
|
+
}
|
|
1314
|
+
let bytes_per_layer = (w as usize) * (h as usize) * 4;
|
|
1315
|
+
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
|
1316
|
+
label: Some("material_texture_array"),
|
|
1317
|
+
size: wgpu::Extent3d {
|
|
1318
|
+
width: w,
|
|
1319
|
+
height: h,
|
|
1320
|
+
depth_or_array_layers: layer_count,
|
|
1321
|
+
},
|
|
1322
|
+
mip_level_count,
|
|
1323
|
+
sample_count: 1,
|
|
1324
|
+
dimension: wgpu::TextureDimension::D2,
|
|
1325
|
+
format: wgpu_format,
|
|
1326
|
+
usage,
|
|
1327
|
+
view_formats: &[],
|
|
1328
|
+
});
|
|
1329
|
+
for (i, (bytes, _, _)) in layers.iter().enumerate().take(layer_count as usize) {
|
|
1330
|
+
// Defensive — a short layer slice would panic in
|
|
1331
|
+
// write_texture; skip and emit a diagnostic instead.
|
|
1332
|
+
if bytes.len() < bytes_per_layer {
|
|
1333
|
+
eprintln!(
|
|
1334
|
+
"[texture_array] layer {} short: {} B < {} B (skipping)",
|
|
1335
|
+
i, bytes.len(), bytes_per_layer,
|
|
1336
|
+
);
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
queue.write_texture(
|
|
1340
|
+
wgpu::TexelCopyTextureInfo {
|
|
1341
|
+
texture: &texture,
|
|
1342
|
+
mip_level: 0,
|
|
1343
|
+
origin: wgpu::Origin3d { x: 0, y: 0, z: i as u32 },
|
|
1344
|
+
aspect: wgpu::TextureAspect::All,
|
|
1345
|
+
},
|
|
1346
|
+
&bytes[..bytes_per_layer],
|
|
1347
|
+
wgpu::TexelCopyBufferLayout {
|
|
1348
|
+
offset: 0,
|
|
1349
|
+
bytes_per_row: Some(w * 4),
|
|
1350
|
+
rows_per_image: Some(h),
|
|
1351
|
+
},
|
|
1352
|
+
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
// EN-014 V2 — auto-generate the mip chain via point-sample copies.
|
|
1356
|
+
// For each mip > 0, copy a half-size region of mip-1 into mip,
|
|
1357
|
+
// for every layer. wgpu's copy_texture_to_texture covers a single
|
|
1358
|
+
// mip level + array layer per call. This is point-filtered (not
|
|
1359
|
+
// box-filtered): correct sizes, sampleable at distance, but
|
|
1360
|
+
// aliased. V2.5 follow-up upgrades this to a render-pass box
|
|
1361
|
+
// filter (one fullscreen draw per (mip, layer)).
|
|
1362
|
+
if auto_generate && mip_level_count > 1 {
|
|
1363
|
+
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
1364
|
+
label: Some("material_texture_array_mipgen"),
|
|
1365
|
+
});
|
|
1366
|
+
for mip in 1..mip_level_count {
|
|
1367
|
+
let src_w = (w >> (mip - 1)).max(1);
|
|
1368
|
+
let src_h = (h >> (mip - 1)).max(1);
|
|
1369
|
+
let dst_w = (w >> mip).max(1);
|
|
1370
|
+
let dst_h = (h >> mip).max(1);
|
|
1371
|
+
// Copy region is dst-sized so we read the top-left
|
|
1372
|
+
// 2x2 reduction implicitly. Truly we'd want a filter,
|
|
1373
|
+
// but copy is the cheapest "mips exist" path.
|
|
1374
|
+
let copy_w = dst_w.min(src_w);
|
|
1375
|
+
let copy_h = dst_h.min(src_h);
|
|
1376
|
+
for layer in 0..layer_count {
|
|
1377
|
+
encoder.copy_texture_to_texture(
|
|
1378
|
+
wgpu::TexelCopyTextureInfo {
|
|
1379
|
+
texture: &texture,
|
|
1380
|
+
mip_level: mip - 1,
|
|
1381
|
+
origin: wgpu::Origin3d { x: 0, y: 0, z: layer },
|
|
1382
|
+
aspect: wgpu::TextureAspect::All,
|
|
1383
|
+
},
|
|
1384
|
+
wgpu::TexelCopyTextureInfo {
|
|
1385
|
+
texture: &texture,
|
|
1386
|
+
mip_level: mip,
|
|
1387
|
+
origin: wgpu::Origin3d { x: 0, y: 0, z: layer },
|
|
1388
|
+
aspect: wgpu::TextureAspect::All,
|
|
1389
|
+
},
|
|
1390
|
+
wgpu::Extent3d {
|
|
1391
|
+
width: copy_w,
|
|
1392
|
+
height: copy_h,
|
|
1393
|
+
depth_or_array_layers: 1,
|
|
1394
|
+
},
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
queue.submit(std::iter::once(encoder.finish()));
|
|
1399
|
+
}
|
|
1400
|
+
let view = texture.create_view(&wgpu::TextureViewDescriptor {
|
|
1401
|
+
label: Some("material_texture_array_view"),
|
|
1402
|
+
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
|
1403
|
+
..Default::default()
|
|
1404
|
+
});
|
|
1405
|
+
self.texture_arrays.push(Some(TextureArray {
|
|
1406
|
+
texture, view, layer_count,
|
|
1407
|
+
}));
|
|
1408
|
+
self.texture_arrays.len() as u32
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/// EN-014 — link a texture array to a material's per-material
|
|
1412
|
+
/// bind-group at one of three slots (0 = albedo, 1 = normal,
|
|
1413
|
+
/// 2 = MR). Pass `array = 0` to revert the slot to the default
|
|
1414
|
+
/// 1×1×1 stub. The per-material BG is rebuilt so subsequent draws
|
|
1415
|
+
/// see the new array view at @binding 14/15/16.
|
|
1416
|
+
///
|
|
1417
|
+
/// `probe_view` is the binding-12 source — the caller (typically
|
|
1418
|
+
/// `Renderer::set_material_texture_array`) resolves the
|
|
1419
|
+
/// material's currently-linked reflection probe and passes its
|
|
1420
|
+
/// `color_view` here, or `&self.default_black_view` to default.
|
|
1421
|
+
/// This indirection keeps EN-011 reflection links live across
|
|
1422
|
+
/// EN-014 rebinds without the MaterialSystem having to reach
|
|
1423
|
+
/// into the Renderer's probe registry. Out-of-range slots /
|
|
1424
|
+
/// unknown handles are no-ops with a diagnostic.
|
|
1425
|
+
pub fn set_material_texture_array(
|
|
1426
|
+
&mut self,
|
|
1427
|
+
device: &wgpu::Device,
|
|
1428
|
+
material: MaterialHandle,
|
|
1429
|
+
slot: u32,
|
|
1430
|
+
array: u32,
|
|
1431
|
+
probe_view: &wgpu::TextureView,
|
|
1432
|
+
) {
|
|
1433
|
+
if material == 0 { return; }
|
|
1434
|
+
let idx = (material - 1) as usize;
|
|
1435
|
+
if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
|
|
1436
|
+
eprintln!("[texture_array] unknown material handle {material}");
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if slot >= 3 {
|
|
1440
|
+
eprintln!("[texture_array] slot {slot} out of range (0..=2)");
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if array != 0 {
|
|
1444
|
+
let h = array as usize;
|
|
1445
|
+
if h == 0 || h > self.texture_arrays.len()
|
|
1446
|
+
|| self.texture_arrays[h - 1].is_none()
|
|
1447
|
+
{
|
|
1448
|
+
eprintln!("[texture_array] unknown array handle {array}");
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Grow the link table so `idx` is in bounds. Parallel to
|
|
1454
|
+
// the params + reflection registries but not strictly tied
|
|
1455
|
+
// to either — texture-array links are independent.
|
|
1456
|
+
while self.material_texture_arrays.len() <= idx {
|
|
1457
|
+
self.material_texture_arrays.push([None, None, None]);
|
|
1458
|
+
}
|
|
1459
|
+
let link = if array == 0 { None } else { Some(array) };
|
|
1460
|
+
self.material_texture_arrays[idx][slot as usize] = link;
|
|
1461
|
+
|
|
1462
|
+
// Rebuild the per-material BG. Resolve user_params from
|
|
1463
|
+
// existing state so we don't clobber EN-005 links.
|
|
1464
|
+
while self.material_params_buffers.len() <= idx {
|
|
1465
|
+
self.material_params_buffers.push(None);
|
|
1466
|
+
self.material_per_material_bgs.push(None);
|
|
1467
|
+
}
|
|
1468
|
+
let user_params_buf: &wgpu::Buffer = self.material_params_buffers
|
|
1469
|
+
.get(idx)
|
|
1470
|
+
.and_then(|b| b.as_ref())
|
|
1471
|
+
.unwrap_or(&self._default_user_params_buffer);
|
|
1472
|
+
// EN-012 — preserve any per-material MaterialFactors UBO
|
|
1473
|
+
// across an EN-014 array rebind.
|
|
1474
|
+
let factors_buf: &wgpu::Buffer = self.material_factors_buffers
|
|
1475
|
+
.get(idx)
|
|
1476
|
+
.and_then(|b| b.as_ref())
|
|
1477
|
+
.unwrap_or(&self._default_material_factors_buffer);
|
|
1478
|
+
|
|
1479
|
+
let [albedo_view, normal_view, mr_view] = self.resolve_array_views(idx);
|
|
1480
|
+
|
|
1481
|
+
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
1482
|
+
label: Some("material_per_material_bg_array"),
|
|
1483
|
+
layout: &self.layouts.per_material,
|
|
1484
|
+
entries: &[
|
|
1485
|
+
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1486
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1487
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1488
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1489
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1490
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1491
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1492
|
+
wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1493
|
+
wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
|
|
1494
|
+
wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1495
|
+
wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
|
|
1496
|
+
wgpu::BindGroupEntry { binding: 11, resource: user_params_buf.as_entire_binding() },
|
|
1497
|
+
wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(probe_view) },
|
|
1498
|
+
wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1499
|
+
wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(albedo_view) },
|
|
1500
|
+
wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(normal_view) },
|
|
1501
|
+
wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(mr_view) },
|
|
1502
|
+
wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
|
|
1503
|
+
],
|
|
1504
|
+
});
|
|
1505
|
+
self.material_per_material_bgs[idx] = Some(bg);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/// EN-014 — accessor for `material_reflection_probe[idx]`. The
|
|
1509
|
+
/// Renderer wrapper uses this to resolve the currently-linked
|
|
1510
|
+
/// probe handle when rebuilding the BG via
|
|
1511
|
+
/// `set_material_texture_array`. Returns `None` for unset / out-
|
|
1512
|
+
/// of-range / unlinked materials.
|
|
1513
|
+
pub fn material_reflection_probe_handle(&self, material: MaterialHandle) -> Option<u32> {
|
|
1514
|
+
if material == 0 { return None; }
|
|
1515
|
+
let idx = (material as usize).checked_sub(1)?;
|
|
1516
|
+
self.material_reflection_probe.get(idx).copied().flatten()
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/// Convenience — true if either bucket has queued work this frame.
|
|
1520
|
+
pub fn any_commands(&self) -> bool {
|
|
1521
|
+
!self.commands.is_empty() || !self.translucent_commands.is_empty()
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/// Submit a draw against a compiled material. Allocates (or reuses)
|
|
1525
|
+
/// a per-draw UBO slot, writes the MVP / model / tint / skin info,
|
|
1526
|
+
/// and queues the command for dispatch.
|
|
1527
|
+
pub fn submit_draw(
|
|
1528
|
+
&mut self,
|
|
1529
|
+
device: &wgpu::Device,
|
|
1530
|
+
queue: &wgpu::Queue,
|
|
1531
|
+
joint_buffer: &wgpu::Buffer,
|
|
1532
|
+
material: MaterialHandle,
|
|
1533
|
+
mesh_handle: u64,
|
|
1534
|
+
mesh_idx: usize,
|
|
1535
|
+
mvp: [[f32; 4]; 4],
|
|
1536
|
+
model: [[f32; 4]; 4],
|
|
1537
|
+
// EN-022: ignored — callers historically passed the CURRENT mvp
|
|
1538
|
+
// here, zeroing every motion vector. The real previous-frame
|
|
1539
|
+
// transform is reconstructed from the per-slot model history.
|
|
1540
|
+
_legacy_prev_mvp: [[f32; 4]; 4],
|
|
1541
|
+
tint: [f32; 4],
|
|
1542
|
+
skin_info: [u32; 4],
|
|
1543
|
+
) {
|
|
1544
|
+
let idx = material as usize;
|
|
1545
|
+
if material == 0 || idx > self.pipelines.len() { return; }
|
|
1546
|
+
let bucket = match self.pipelines[idx - 1].as_ref() {
|
|
1547
|
+
Some(p) => p.bucket,
|
|
1548
|
+
None => return,
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
let slot = self.next_draw_slot;
|
|
1552
|
+
self.next_draw_slot += 1;
|
|
1553
|
+
self.ensure_draw_slot(device, joint_buffer, slot);
|
|
1554
|
+
|
|
1555
|
+
// EN-022 — draw identity is the submission slot (stable order
|
|
1556
|
+
// frame-to-frame, same convention the cached-model path uses).
|
|
1557
|
+
// A fresh slot (first frame, or the frame after a draw-count
|
|
1558
|
+
// change) falls back to the current model = zero object motion.
|
|
1559
|
+
let prev_model = self.prev_models.get(slot).copied().unwrap_or(model);
|
|
1560
|
+
let prev_mvp = super::mat4_multiply(self.prev_vp, prev_model);
|
|
1561
|
+
if self.cur_models.len() <= slot {
|
|
1562
|
+
self.cur_models.resize(slot + 1, model);
|
|
1563
|
+
}
|
|
1564
|
+
self.cur_models[slot] = model;
|
|
1565
|
+
|
|
1566
|
+
let per_draw = PerDrawUniforms { mvp, model, prev_mvp, model_tint: tint, skin_info };
|
|
1567
|
+
queue.write_buffer(&self.per_draw_buffers[slot], 0, bytemuck::bytes_of(&per_draw));
|
|
1568
|
+
|
|
1569
|
+
let cmd = MaterialDrawCommand {
|
|
1570
|
+
material, mesh_handle, mesh_idx, draw_slot: slot,
|
|
1571
|
+
view_depth: mvp[3][3],
|
|
1572
|
+
instance: None,
|
|
1573
|
+
model,
|
|
1574
|
+
};
|
|
1575
|
+
if bucket.is_translucent() {
|
|
1576
|
+
self.translucent_commands.push(cmd);
|
|
1577
|
+
} else {
|
|
1578
|
+
self.commands.push(cmd);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/// EN-001 — submit an instanced draw. Identical to `submit_draw`
|
|
1583
|
+
/// except the engine binds vertex slot 1 to the registered
|
|
1584
|
+
/// instance buffer and emits `draw_indexed(.., 0..count)`. The
|
|
1585
|
+
/// pipeline must have been compiled with `wants_instancing=true`
|
|
1586
|
+
/// (use `compile_material_instanced` on the renderer).
|
|
1587
|
+
///
|
|
1588
|
+
/// `model` / `mvp` here are the instance-local→world fallback
|
|
1589
|
+
/// transform — the per-instance buffer's `instance_pos`/`rot_y`/
|
|
1590
|
+
/// `scale` typically dominate, so callers usually pass identity
|
|
1591
|
+
/// for `model` and the camera VP for `mvp`. `tint` is multiplied
|
|
1592
|
+
/// per-draw (in addition to the per-instance tint).
|
|
1593
|
+
pub fn submit_draw_instanced(
|
|
1594
|
+
&mut self,
|
|
1595
|
+
device: &wgpu::Device,
|
|
1596
|
+
queue: &wgpu::Queue,
|
|
1597
|
+
joint_buffer: &wgpu::Buffer,
|
|
1598
|
+
material: MaterialHandle,
|
|
1599
|
+
mesh_handle: u64,
|
|
1600
|
+
mesh_idx: usize,
|
|
1601
|
+
instance_buffer: u32,
|
|
1602
|
+
instance_count: u32,
|
|
1603
|
+
mvp: [[f32; 4]; 4],
|
|
1604
|
+
model: [[f32; 4]; 4],
|
|
1605
|
+
// EN-022: ignored — see submit_draw.
|
|
1606
|
+
_legacy_prev_mvp: [[f32; 4]; 4],
|
|
1607
|
+
tint: [f32; 4],
|
|
1608
|
+
skin_info: [u32; 4],
|
|
1609
|
+
) {
|
|
1610
|
+
let idx = material as usize;
|
|
1611
|
+
if material == 0 || idx > self.pipelines.len() { return; }
|
|
1612
|
+
let bucket = match self.pipelines[idx - 1].as_ref() {
|
|
1613
|
+
Some(p) => p.bucket,
|
|
1614
|
+
None => return,
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
let slot = self.next_draw_slot;
|
|
1618
|
+
self.next_draw_slot += 1;
|
|
1619
|
+
self.ensure_draw_slot(device, joint_buffer, slot);
|
|
1620
|
+
|
|
1621
|
+
// EN-022 — same slot-history reconstruction as submit_draw.
|
|
1622
|
+
// Instance transforms live in the (static) instance buffer, so
|
|
1623
|
+
// prev vs current only differ by the fallback model + camera;
|
|
1624
|
+
// per-instance wind sway derives its own previous position in
|
|
1625
|
+
// the vertex shader from frame.time - frame.delta_time.
|
|
1626
|
+
let prev_model = self.prev_models.get(slot).copied().unwrap_or(model);
|
|
1627
|
+
let prev_mvp = super::mat4_multiply(self.prev_vp, prev_model);
|
|
1628
|
+
if self.cur_models.len() <= slot {
|
|
1629
|
+
self.cur_models.resize(slot + 1, model);
|
|
1630
|
+
}
|
|
1631
|
+
self.cur_models[slot] = model;
|
|
1632
|
+
|
|
1633
|
+
let per_draw = PerDrawUniforms { mvp, model, prev_mvp, model_tint: tint, skin_info };
|
|
1634
|
+
queue.write_buffer(&self.per_draw_buffers[slot], 0, bytemuck::bytes_of(&per_draw));
|
|
1635
|
+
|
|
1636
|
+
let cmd = MaterialDrawCommand {
|
|
1637
|
+
material, mesh_handle, mesh_idx, draw_slot: slot,
|
|
1638
|
+
// Instanced draws sort as a group by their fallback-transform
|
|
1639
|
+
// pivot — per-instance ordering inside one buffer is the
|
|
1640
|
+
// standard engine limitation.
|
|
1641
|
+
view_depth: mvp[3][3],
|
|
1642
|
+
instance: Some(InstanceDrawInfo {
|
|
1643
|
+
buffer_handle: instance_buffer,
|
|
1644
|
+
count: instance_count,
|
|
1645
|
+
}),
|
|
1646
|
+
model,
|
|
1647
|
+
};
|
|
1648
|
+
if bucket.is_translucent() {
|
|
1649
|
+
self.translucent_commands.push(cmd);
|
|
1650
|
+
} else {
|
|
1651
|
+
self.commands.push(cmd);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
fn ensure_draw_slot(
|
|
1657
|
+
&mut self,
|
|
1658
|
+
device: &wgpu::Device,
|
|
1659
|
+
joint_buffer: &wgpu::Buffer,
|
|
1660
|
+
slot: usize,
|
|
1661
|
+
) {
|
|
1662
|
+
while self.per_draw_buffers.len() <= slot {
|
|
1663
|
+
let buf = device.create_buffer(&wgpu::BufferDescriptor {
|
|
1664
|
+
label: Some("material_per_draw"),
|
|
1665
|
+
size: std::mem::size_of::<PerDrawUniforms>() as u64,
|
|
1666
|
+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
1667
|
+
mapped_at_creation: false,
|
|
1668
|
+
});
|
|
1669
|
+
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
1670
|
+
label: Some("material_per_draw_bg"),
|
|
1671
|
+
layout: &self.layouts.per_draw,
|
|
1672
|
+
entries: &[
|
|
1673
|
+
wgpu::BindGroupEntry { binding: 0, resource: buf.as_entire_binding() },
|
|
1674
|
+
wgpu::BindGroupEntry { binding: 1, resource: joint_buffer.as_entire_binding() },
|
|
1675
|
+
],
|
|
1676
|
+
});
|
|
1677
|
+
self.per_draw_buffers.push(buf);
|
|
1678
|
+
self.per_draw_bgs.push(bg);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/// Dispatch all queued material draws. Caller owns the render pass;
|
|
1683
|
+
/// this method binds the pipelines + groups + meshes and issues
|
|
1684
|
+
/// indexed draws. `mesh_fetch` is a closure that returns
|
|
1685
|
+
/// `(vertex_buffer, index_buffer, index_count)` for a given
|
|
1686
|
+
/// (mesh_handle, mesh_idx) — lets the renderer hand over its
|
|
1687
|
+
/// `model_gpu_cache` without this module taking a dependency on it.
|
|
1688
|
+
pub fn dispatch<'pass, F>(
|
|
1689
|
+
&'pass self,
|
|
1690
|
+
pass: &mut wgpu::RenderPass<'pass>,
|
|
1691
|
+
planes: Option<&[[f32; 4]; 6]>,
|
|
1692
|
+
mesh_fetch: F,
|
|
1693
|
+
)
|
|
1694
|
+
where F: FnMut(u64, usize) -> Option<(&'pass wgpu::Buffer, &'pass wgpu::Buffer, u32, [f32; 3], [f32; 3])>
|
|
1695
|
+
{
|
|
1696
|
+
self.dispatch_with_view(pass, &self.per_view_bg, |_| true, false, planes, mesh_fetch);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/// EN-011 — like `dispatch`, but uses a caller-supplied PerView
|
|
1700
|
+
/// bind group (so the planar-reflection pass can render the same
|
|
1701
|
+
/// draws against a mirrored camera UBO) and skips any command
|
|
1702
|
+
/// whose material handle the `accept` predicate rejects (so the
|
|
1703
|
+
/// reflection pass can drop hardcoded-excluded materials —
|
|
1704
|
+
/// foliage, particles, grass — without touching the original
|
|
1705
|
+
/// command list).
|
|
1706
|
+
///
|
|
1707
|
+
/// `accept` is consulted once per command; the engine passes a
|
|
1708
|
+
/// closure that closes over a small `HashSet<MaterialHandle>` of
|
|
1709
|
+
/// excluded material handles.
|
|
1710
|
+
///
|
|
1711
|
+
/// EN-011 V2 — `use_reflection_pipeline` swaps in the material's
|
|
1712
|
+
/// sibling pipeline (cull_mode flipped) when one was compiled.
|
|
1713
|
+
/// Falls back to the main pipeline if no reflection variant
|
|
1714
|
+
/// exists (translucent / cutout materials, where the original
|
|
1715
|
+
/// pipeline already cull-mode = None and no flip is needed).
|
|
1716
|
+
pub fn dispatch_with_view<'pass, F, A>(
|
|
1717
|
+
&'pass self,
|
|
1718
|
+
pass: &mut wgpu::RenderPass<'pass>,
|
|
1719
|
+
per_view_bg: &'pass wgpu::BindGroup,
|
|
1720
|
+
mut accept: A,
|
|
1721
|
+
use_reflection_pipeline: bool,
|
|
1722
|
+
// View frustum for instance-tile culling (`mesh_fetch` supplies
|
|
1723
|
+
// the mesh's local AABB). None = no per-tile culling.
|
|
1724
|
+
planes: Option<&[[f32; 4]; 6]>,
|
|
1725
|
+
mut mesh_fetch: F,
|
|
1726
|
+
)
|
|
1727
|
+
where
|
|
1728
|
+
F: FnMut(u64, usize) -> Option<(&'pass wgpu::Buffer, &'pass wgpu::Buffer, u32, [f32; 3], [f32; 3])>,
|
|
1729
|
+
A: FnMut(MaterialHandle) -> bool,
|
|
1730
|
+
{
|
|
1731
|
+
if self.commands.is_empty() { return; }
|
|
1732
|
+
|
|
1733
|
+
let mut last_material: MaterialHandle = 0;
|
|
1734
|
+
for cmd in &self.commands {
|
|
1735
|
+
if !accept(cmd.material) { continue; }
|
|
1736
|
+
if cmd.material != last_material {
|
|
1737
|
+
let mat = match self.pipelines.get(cmd.material as usize - 1) {
|
|
1738
|
+
Some(Some(m)) => m,
|
|
1739
|
+
_ => continue,
|
|
1740
|
+
};
|
|
1741
|
+
// Probe passes (use_reflection_pipeline) present the
|
|
1742
|
+
// 4-attachment opaque G-buffer layout. Translucent-
|
|
1743
|
+
// profile pipelines are single-target — binding one
|
|
1744
|
+
// there is a wgpu validation panic — and reads_scene
|
|
1745
|
+
// materials expect a group-4 bind the probe pass never
|
|
1746
|
+
// provides. Skip both; `last_material` stays latched on
|
|
1747
|
+
// the previously bound material, which is correct
|
|
1748
|
+
// because no pipeline/bind state changes here.
|
|
1749
|
+
if use_reflection_pipeline
|
|
1750
|
+
&& (matches!(mat.profile, FragmentProfile::Translucent) || mat.reads_scene)
|
|
1751
|
+
{
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
let pipeline = if use_reflection_pipeline {
|
|
1755
|
+
mat.reflection_pipeline.as_ref().unwrap_or(&mat.pipeline)
|
|
1756
|
+
} else {
|
|
1757
|
+
&mat.pipeline
|
|
1758
|
+
};
|
|
1759
|
+
pass.set_pipeline(pipeline);
|
|
1760
|
+
pass.set_bind_group(0, &self.per_frame_bg, &[]);
|
|
1761
|
+
pass.set_bind_group(1, per_view_bg, &[]);
|
|
1762
|
+
pass.set_bind_group(2, self.per_material_bg_for(cmd.material), &[]);
|
|
1763
|
+
last_material = cmd.material;
|
|
1764
|
+
}
|
|
1765
|
+
if let Some((vb, ib, icount, lmin, lmax)) = mesh_fetch(cmd.mesh_handle, cmd.mesh_idx) {
|
|
1766
|
+
pass.set_bind_group(3, &self.per_draw_bgs[cmd.draw_slot], &[]);
|
|
1767
|
+
pass.set_vertex_buffer(0, vb.slice(..));
|
|
1768
|
+
pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
|
|
1769
|
+
let instance_range = self.bind_instance_buffer(pass, &cmd.instance);
|
|
1770
|
+
if instance_range.end <= instance_range.start { continue; }
|
|
1771
|
+
// Instance-tile culling: `commands` holds only the
|
|
1772
|
+
// opaque + cutout buckets (translucent draws dispatch
|
|
1773
|
+
// elsewhere), so emitting the visible tile ranges is
|
|
1774
|
+
// output-identical to the full-range draw. Tiles' pos
|
|
1775
|
+
// AABBs are inflated by max_scale × the mesh's local
|
|
1776
|
+
// half-diagonal (rotation-safe). Adjacent visible tiles
|
|
1777
|
+
// merge into one draw.
|
|
1778
|
+
let tiles = cmd.instance
|
|
1779
|
+
.as_ref()
|
|
1780
|
+
.filter(|_| planes.is_some() && lmin[0] <= lmax[0])
|
|
1781
|
+
.and_then(|inst| {
|
|
1782
|
+
self.instance_buffers
|
|
1783
|
+
.get(inst.buffer_handle as usize - 1)
|
|
1784
|
+
.and_then(|s| s.as_ref())
|
|
1785
|
+
.map(|s| &s.tiles)
|
|
1786
|
+
})
|
|
1787
|
+
.filter(|t| !t.is_empty());
|
|
1788
|
+
match (tiles, planes) {
|
|
1789
|
+
(Some(tiles), Some(planes)) => {
|
|
1790
|
+
let half_diag = 0.5 * ((lmax[0] - lmin[0]).powi(2)
|
|
1791
|
+
+ (lmax[1] - lmin[1]).powi(2)
|
|
1792
|
+
+ (lmax[2] - lmin[2]).powi(2)).sqrt();
|
|
1793
|
+
let mut run: Option<(u32, u32)> = None;
|
|
1794
|
+
for tile in tiles.iter() {
|
|
1795
|
+
let r = tile.max_scale * half_diag;
|
|
1796
|
+
let bmin = [tile.pmin[0] - r, tile.pmin[1] - r, tile.pmin[2] - r];
|
|
1797
|
+
let bmax = [tile.pmax[0] + r, tile.pmax[1] + r, tile.pmax[2] + r];
|
|
1798
|
+
if crate::scene::aabb_outside_frustum(planes, bmin, bmax) {
|
|
1799
|
+
if let Some((s, e)) = run.take() {
|
|
1800
|
+
pass.draw_indexed(0..icount, 0, s..e);
|
|
1801
|
+
}
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
run = match run {
|
|
1805
|
+
Some((s, e)) if e == tile.first => Some((s, tile.first + tile.count)),
|
|
1806
|
+
Some((s, e)) => {
|
|
1807
|
+
pass.draw_indexed(0..icount, 0, s..e);
|
|
1808
|
+
Some((tile.first, tile.first + tile.count))
|
|
1809
|
+
}
|
|
1810
|
+
None => Some((tile.first, tile.first + tile.count)),
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
if let Some((s, e)) = run {
|
|
1814
|
+
pass.draw_indexed(0..icount, 0, s..e);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
_ => {
|
|
1818
|
+
pass.draw_indexed(0..icount, 0, instance_range);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/// EN-001 — resolve an instanced draw command's vertex slot 1 binding
|
|
1826
|
+
/// and return the instance range. For non-instanced draws (`info` is
|
|
1827
|
+
/// None) this is a no-op and returns `0..1`. For instanced draws
|
|
1828
|
+
/// with a missing/destroyed buffer slot we return an empty range
|
|
1829
|
+
/// so the caller skips the draw rather than crashing on a stale
|
|
1830
|
+
/// handle.
|
|
1831
|
+
fn bind_instance_buffer<'pass>(
|
|
1832
|
+
&'pass self,
|
|
1833
|
+
pass: &mut wgpu::RenderPass<'pass>,
|
|
1834
|
+
info: &Option<InstanceDrawInfo>,
|
|
1835
|
+
) -> std::ops::Range<u32> {
|
|
1836
|
+
match info {
|
|
1837
|
+
None => 0..1,
|
|
1838
|
+
Some(inst) => {
|
|
1839
|
+
if inst.buffer_handle == 0 { return 0..1; }
|
|
1840
|
+
let slot_idx = inst.buffer_handle as usize - 1;
|
|
1841
|
+
match self.instance_buffers.get(slot_idx).and_then(|s| s.as_ref()) {
|
|
1842
|
+
Some(ib_slot) => {
|
|
1843
|
+
pass.set_vertex_buffer(1, ib_slot.buffer.slice(..));
|
|
1844
|
+
0..inst.count
|
|
1845
|
+
}
|
|
1846
|
+
None => 0..0,
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/// Phase 4b — rebuild the SceneInputs (group 4) bind group with
|
|
1853
|
+
/// the current frame's snapshot textures. Called by the Renderer
|
|
1854
|
+
/// once per frame when translucent draws exist and a SceneColor
|
|
1855
|
+
/// transient has been allocated. `scene_color_view` is the
|
|
1856
|
+
/// copy-to-sample snapshot from `hdr_rt`; `scene_depth_view` is
|
|
1857
|
+
/// the live depth buffer the opaque pass wrote. Other slots
|
|
1858
|
+
/// (impulse, motion vectors) bind to internal stub textures
|
|
1859
|
+
/// until Phase 7 wires them.
|
|
1860
|
+
pub fn update_scene_inputs(
|
|
1861
|
+
&mut self,
|
|
1862
|
+
device: &wgpu::Device,
|
|
1863
|
+
scene_color_view: &wgpu::TextureView,
|
|
1864
|
+
scene_depth_view: Option<&wgpu::TextureView>,
|
|
1865
|
+
impulse_view: Option<(&wgpu::TextureView, &wgpu::Sampler)>,
|
|
1866
|
+
) {
|
|
1867
|
+
let depth_view = scene_depth_view.unwrap_or(&self._scene_stub_depth_view);
|
|
1868
|
+
// Layout entry 5 is NonFiltering — fallback uses the depth
|
|
1869
|
+
// sampler (which is also NonFiltering) rather than the
|
|
1870
|
+
// filtering color sampler so the layout matches either way.
|
|
1871
|
+
let (imp_view, imp_samp): (&wgpu::TextureView, &wgpu::Sampler) = match impulse_view {
|
|
1872
|
+
Some((v, s)) => (v, s),
|
|
1873
|
+
None => (&self._scene_stub_view, &self._scene_depth_sampler),
|
|
1874
|
+
};
|
|
1875
|
+
// Phase 4c — group 4 binding 2 receives a COPY_DST snapshot of
|
|
1876
|
+
// the opaque depth buffer, rather than the live depth-stencil
|
|
1877
|
+
// attachment (wgpu rejects read+write aliasing in the same
|
|
1878
|
+
// pass). Callers that don't need depth pass the stub view
|
|
1879
|
+
// already held here; `Renderer::end_frame_with_scene` acquires
|
|
1880
|
+
// a transient depth texture when any translucent material
|
|
1881
|
+
// declares a depth read.
|
|
1882
|
+
let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
1883
|
+
label: Some("scene_inputs_bg"),
|
|
1884
|
+
layout: &self.layouts.scene_inputs,
|
|
1885
|
+
entries: &[
|
|
1886
|
+
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(scene_color_view) },
|
|
1887
|
+
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._scene_color_sampler) },
|
|
1888
|
+
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(depth_view) },
|
|
1889
|
+
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._scene_depth_sampler) },
|
|
1890
|
+
wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(imp_view) },
|
|
1891
|
+
wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(imp_samp) },
|
|
1892
|
+
wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._scene_stub_view) },
|
|
1893
|
+
],
|
|
1894
|
+
});
|
|
1895
|
+
self.scene_inputs_bg = Some(bg);
|
|
1896
|
+
|
|
1897
|
+
// EN-063 — on wasm32 the scene inputs are folded into the
|
|
1898
|
+
// per_frame group (group 0), so the real snapshot views have
|
|
1899
|
+
// to land there: rebuild per_frame_bg with the same seven
|
|
1900
|
+
// resources bound above, at WASM_SCENE_INPUTS_BASE..+6. The
|
|
1901
|
+
// opaque pass re-binds this group too, which is harmless —
|
|
1902
|
+
// opaque materials never statically use the scene-input slots.
|
|
1903
|
+
#[cfg(target_arch = "wasm32")]
|
|
1904
|
+
{
|
|
1905
|
+
self.per_frame_bg = super::material_system_wasm::build_per_frame_bg_wasm(
|
|
1906
|
+
device,
|
|
1907
|
+
&self.layouts.per_frame,
|
|
1908
|
+
&self.per_frame_buffer,
|
|
1909
|
+
scene_color_view,
|
|
1910
|
+
&self._scene_color_sampler,
|
|
1911
|
+
depth_view,
|
|
1912
|
+
&self._scene_depth_sampler,
|
|
1913
|
+
imp_view,
|
|
1914
|
+
imp_samp,
|
|
1915
|
+
&self._scene_stub_view, // motion_vectors stub
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/// Sort the translucent bucket back-to-front by view depth. Call
|
|
1921
|
+
/// once per frame after all submits, before dispatch_translucent —
|
|
1922
|
+
/// alpha blending only composites correctly when farther surfaces
|
|
1923
|
+
/// land first. The sort is stable: equal-depth draws keep submission
|
|
1924
|
+
/// order, so existing games that hand-ordered their transparency
|
|
1925
|
+
/// keep their result. (Sorting costs pipeline-switch batching; with
|
|
1926
|
+
/// translucent counts in the hundreds the correctness is worth it.)
|
|
1927
|
+
pub fn sort_translucent(&mut self) {
|
|
1928
|
+
self.translucent_commands
|
|
1929
|
+
.sort_by(|a, b| b.view_depth.total_cmp(&a.view_depth));
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/// Dispatch translucent-bucket draws (Transparent, Refractive,
|
|
1933
|
+
/// Additive). Caller owns a render pass set up with a single HDR
|
|
1934
|
+
/// attachment (LoadOp::Load) + depth read-only. Refractive
|
|
1935
|
+
/// materials additionally receive the SceneInputs bind group at
|
|
1936
|
+
/// group 4 — `update_scene_inputs` must have been called this
|
|
1937
|
+
/// frame for that to be non-None.
|
|
1938
|
+
pub fn dispatch_translucent<'pass, F>(
|
|
1939
|
+
&'pass self,
|
|
1940
|
+
pass: &mut wgpu::RenderPass<'pass>,
|
|
1941
|
+
mut mesh_fetch: F,
|
|
1942
|
+
)
|
|
1943
|
+
where F: FnMut(u64, usize) -> Option<(&'pass wgpu::Buffer, &'pass wgpu::Buffer, u32)>
|
|
1944
|
+
{
|
|
1945
|
+
if self.translucent_commands.is_empty() { return; }
|
|
1946
|
+
|
|
1947
|
+
let mut last_material: MaterialHandle = 0;
|
|
1948
|
+
let mut last_reads_scene: bool = false;
|
|
1949
|
+
for cmd in &self.translucent_commands {
|
|
1950
|
+
if cmd.material != last_material {
|
|
1951
|
+
let mat = match self.pipelines.get(cmd.material as usize - 1) {
|
|
1952
|
+
Some(Some(m)) => m,
|
|
1953
|
+
_ => continue,
|
|
1954
|
+
};
|
|
1955
|
+
pass.set_pipeline(&mat.pipeline);
|
|
1956
|
+
pass.set_bind_group(0, &self.per_frame_bg, &[]);
|
|
1957
|
+
pass.set_bind_group(1, &self.per_view_bg, &[]);
|
|
1958
|
+
pass.set_bind_group(2, self.per_material_bg_for(cmd.material), &[]);
|
|
1959
|
+
// EN-063 — on wasm32 there is no group 4: the scene
|
|
1960
|
+
// inputs are folded into per_frame (group 0), already
|
|
1961
|
+
// bound above with the frame's snapshot views.
|
|
1962
|
+
if mat.reads_scene && cfg!(not(target_arch = "wasm32")) {
|
|
1963
|
+
if let Some(bg) = self.scene_inputs_bg.as_ref() {
|
|
1964
|
+
pass.set_bind_group(4, bg, &[]);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
last_material = cmd.material;
|
|
1968
|
+
last_reads_scene = mat.reads_scene;
|
|
1969
|
+
}
|
|
1970
|
+
// Re-bind group 4 if the material switches its reads_scene
|
|
1971
|
+
// between subsequent draws — rarely happens with a
|
|
1972
|
+
// stable bucket but keeps the state machine honest.
|
|
1973
|
+
let _ = last_reads_scene;
|
|
1974
|
+
|
|
1975
|
+
if let Some((vb, ib, icount)) = mesh_fetch(cmd.mesh_handle, cmd.mesh_idx) {
|
|
1976
|
+
pass.set_bind_group(3, &self.per_draw_bgs[cmd.draw_slot], &[]);
|
|
1977
|
+
pass.set_vertex_buffer(0, vb.slice(..));
|
|
1978
|
+
pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
|
|
1979
|
+
let instance_range = self.bind_instance_buffer(pass, &cmd.instance);
|
|
1980
|
+
if instance_range.end > instance_range.start {
|
|
1981
|
+
pass.draw_indexed(0..icount, 0, instance_range);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// `build_per_frame_bg_wasm` (the wasm32-only per_frame bind-group builder)
|
|
1989
|
+
// lives in `material_system_wasm.rs` now — see the 2000-line policy note there.
|
|
1990
|
+
|
|
1991
|
+
// ---- Tests ----------------------------------------------------------
|
|
1992
|
+
// GPU-backed tests live in material_system_tests.rs (2000-line file
|
|
1993
|
+
// policy); the #[path] keeps them a private child module of this one.
|
|
1994
|
+
#[cfg(test)]
|
|
1995
|
+
#[path = "material_system_tests.rs"]
|
|
1996
|
+
mod tests_file;
|