@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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,593 @@
1
+ //! EN-011 planar-reflection probe pass: mirrored PerView build,
2
+ //! per-probe bind-group cache, mirrored-frustum culling and the two
3
+ //! probe render passes (materials + cached models). Split from
4
+ //! renderer/mod.rs (2000-line file policy — same pattern as
5
+ //! shadow_pass.rs / model_draw.rs / scene_pass.rs).
6
+
7
+ use super::*;
8
+
9
+ impl Renderer {
10
+ /// EN-011 — render every registered probe's RT for this frame.
11
+ /// Called from `end_frame_with_scene` BEFORE the main material
12
+ /// pass so the probe textures are ready when materials sample
13
+ /// them.
14
+ ///
15
+ /// For each probe:
16
+ /// 1. Build the mirrored PerView (camera reflected across the
17
+ /// probe's plane; same projection as the main camera).
18
+ /// 2. Upload the mirrored PerView to the probe's UBO.
19
+ /// 3. Begin a render pass against the probe's RT + depth (clear
20
+ /// colour to fog, depth to 1.0).
21
+ /// 4. Walk material_system.commands and dispatch each non-
22
+ /// excluded draw with the mirrored per-view bind group.
23
+ ///
24
+ /// V1 cull list: excludes any material whose handle equals one
25
+ /// of the materials linked to a probe (so the water plane itself
26
+ /// doesn't reflect). Future revisions can expand this with a
27
+ /// hardcoded foliage / particle bucket filter via the bucket
28
+ /// metadata on each compiled pipeline.
29
+ pub fn dispatch_planar_reflections(
30
+ &mut self,
31
+ encoder: &mut wgpu::CommandEncoder,
32
+ scene: &crate::scene::SceneGraph,
33
+ profiler: &mut crate::profiler::Profiler,
34
+ ) {
35
+ if self.planar_probes.iter().all(|p| p.is_none()) { return; }
36
+ // Scene-graph nodes render into the probe too (they share the
37
+ // Vertex3D layout and the scene material bind-group layout), so a
38
+ // fully retained-mode game gets real water reflections as well.
39
+ let scene_draws = scene.reflect_draw_list();
40
+ if self.material_system.commands.is_empty()
41
+ && self.model_draw_commands.is_empty()
42
+ && scene_draws.is_empty() { return; }
43
+
44
+ // EN-011 — lazily build the single-target reflection pipeline + buffers
45
+ // used to render cached models (trees/house) into the probe with a
46
+ // mirrored VP. Owned layouts: g0 dynamic per-draw model uniform, g1
47
+ // sun/ambient; g2 reuses the scene material layout for base colour.
48
+ const REFLECT_STRIDE: u64 = 256;
49
+ const REFLECT_MAX_DRAWS: usize = 1024;
50
+ if self.reflect_scene_pipeline.is_none() {
51
+ let model_dyn_layout = self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
52
+ label: Some("reflect_model_dyn_layout"),
53
+ entries: &[wgpu::BindGroupLayoutEntry {
54
+ binding: 0, visibility: wgpu::ShaderStages::VERTEX,
55
+ ty: wgpu::BindingType::Buffer {
56
+ ty: wgpu::BufferBindingType::Uniform,
57
+ has_dynamic_offset: true,
58
+ min_binding_size: std::num::NonZeroU64::new(128),
59
+ },
60
+ count: None,
61
+ }],
62
+ });
63
+ let shadow_tex_entry = |binding: u32| wgpu::BindGroupLayoutEntry {
64
+ binding, visibility: wgpu::ShaderStages::FRAGMENT,
65
+ ty: wgpu::BindingType::Texture {
66
+ sample_type: wgpu::TextureSampleType::Depth,
67
+ view_dimension: wgpu::TextureViewDimension::D2,
68
+ multisampled: false,
69
+ },
70
+ count: None,
71
+ };
72
+ let light_layout = self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
73
+ label: Some("reflect_light_layout"),
74
+ entries: &[
75
+ wgpu::BindGroupLayoutEntry {
76
+ binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
77
+ ty: wgpu::BindingType::Buffer {
78
+ ty: wgpu::BufferBindingType::Uniform,
79
+ has_dynamic_offset: false, min_binding_size: None,
80
+ },
81
+ count: None,
82
+ },
83
+ // Shadow cascades + comparison sampler so the mirrored
84
+ // scene is sun-shadowed like the real one (the probe
85
+ // previously rendered everything fully lit, which made
86
+ // water reflections disagree with the scene above them).
87
+ shadow_tex_entry(1),
88
+ shadow_tex_entry(2),
89
+ shadow_tex_entry(3),
90
+ wgpu::BindGroupLayoutEntry {
91
+ binding: 4, visibility: wgpu::ShaderStages::FRAGMENT,
92
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
93
+ count: None,
94
+ },
95
+ ],
96
+ });
97
+ let shader = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
98
+ label: Some("reflect_scene_shader"),
99
+ source: wgpu::ShaderSource::Wgsl(REFLECT_SCENE_WGSL.into()),
100
+ });
101
+ let pl = self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
102
+ label: Some("reflect_scene_pl"),
103
+ bind_group_layouts: &[Some(&model_dyn_layout), Some(&light_layout), Some(&self.scene_material_layout)],
104
+ immediate_size: 0,
105
+ });
106
+ let pipeline = self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
107
+ label: Some("reflect_scene_pipeline"),
108
+ layout: Some(&pl),
109
+ vertex: wgpu::VertexState {
110
+ module: &shader, entry_point: Some("vs_reflect"),
111
+ buffers: &[Vertex3D::desc()], compilation_options: Default::default(),
112
+ },
113
+ fragment: Some(wgpu::FragmentState {
114
+ module: &shader, entry_point: Some("fs_reflect"),
115
+ targets: &[Some(wgpu::ColorTargetState {
116
+ format: HDR_FORMAT, blend: None, write_mask: wgpu::ColorWrites::ALL,
117
+ })],
118
+ compilation_options: Default::default(),
119
+ }),
120
+ primitive: wgpu::PrimitiveState {
121
+ topology: wgpu::PrimitiveTopology::TriangleList,
122
+ cull_mode: None, ..Default::default()
123
+ },
124
+ depth_stencil: Some(wgpu::DepthStencilState {
125
+ format: DEPTH_FORMAT, depth_write_enabled: Some(true),
126
+ depth_compare: Some(wgpu::CompareFunction::Less),
127
+ stencil: Default::default(), bias: Default::default(),
128
+ }),
129
+ multisample: Default::default(), multiview_mask: None, cache: None,
130
+ });
131
+ let model_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
132
+ label: Some("reflect_model_buf"),
133
+ size: REFLECT_STRIDE * REFLECT_MAX_DRAWS as u64,
134
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
135
+ mapped_at_creation: false,
136
+ });
137
+ let model_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
138
+ label: Some("reflect_model_bg"), layout: &model_dyn_layout,
139
+ entries: &[wgpu::BindGroupEntry {
140
+ binding: 0,
141
+ resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
142
+ buffer: &model_buf, offset: 0, size: std::num::NonZeroU64::new(128),
143
+ }),
144
+ }],
145
+ });
146
+ // sun_dir + sun_color + ambient + cam_pos + shadow_splits (5 vec4)
147
+ // + 3 cascade mat4s = 80 + 192 = 272 bytes.
148
+ let light_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
149
+ label: Some("reflect_light_buf"), size: 272,
150
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
151
+ mapped_at_creation: false,
152
+ });
153
+ let light_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
154
+ label: Some("reflect_light_bg"), layout: &light_layout,
155
+ entries: &[
156
+ wgpu::BindGroupEntry { binding: 0, resource: light_buf.as_entire_binding() },
157
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[0]) },
158
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[1]) },
159
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[2]) },
160
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.shadow_map.sampler) },
161
+ ],
162
+ });
163
+ self.reflect_scene_pipeline = Some(pipeline);
164
+ self.reflect_model_buf = Some(model_buf);
165
+ self.reflect_model_bg = Some(model_bg);
166
+ self.reflect_light_buf = Some(light_buf);
167
+ self.reflect_light_bg = Some(light_bg);
168
+ }
169
+ // Sun/ambient + shadow data for the reflection shading (same values
170
+ // as the main pass, so the mirrored scene is lit AND shadowed like
171
+ // the real one). shadow_splits.w carries the shadows-enabled flag —
172
+ // in THIS struct .w is free (no LOD-bias tenant).
173
+ {
174
+ let ld = self.lighting_uniforms.light_dir;
175
+ let lc = self.lighting_uniforms.light_color;
176
+ let amb = self.lighting_uniforms.ambient;
177
+ let cam = self.current_camera_pos;
178
+ let sp = self.lighting_uniforms.shadow_cascade_splits;
179
+ let shadows_flag = if self.shadow_map.enabled { 1.0 } else { 0.0 };
180
+ let mut light_data = [0.0f32; 68];
181
+ light_data[0..4].copy_from_slice(&[ld[0], ld[1], ld[2], ld[3]]);
182
+ light_data[4..8].copy_from_slice(&[lc[0], lc[1], lc[2], 0.0]);
183
+ light_data[8..12].copy_from_slice(&[amb[0], amb[1], amb[2], amb[3]]);
184
+ light_data[12..16].copy_from_slice(&[cam[0], cam[1], cam[2], 0.0]);
185
+ light_data[16..20].copy_from_slice(&[sp[0], sp[1], sp[2], shadows_flag]);
186
+ let vps = &self.lighting_uniforms.shadow_cascade_vps;
187
+ for c in 0..3 {
188
+ for col in 0..4 {
189
+ for row in 0..4 {
190
+ light_data[20 + c * 16 + col * 4 + row] = vps[c][col][row];
191
+ }
192
+ }
193
+ }
194
+ if let Some(buf) = &self.reflect_light_buf {
195
+ self.queue.write_buffer(buf, 0, bytemuck::cast_slice(&light_data));
196
+ }
197
+ }
198
+
199
+ // Build the V1 exclude set: every material linked to any
200
+ // probe. The water material itself shouldn't appear in its
201
+ // own reflection (it'd black-on-black self-occlude the
202
+ // surface).
203
+ let mut excluded: std::collections::HashSet<material_system::MaterialHandle> =
204
+ std::collections::HashSet::new();
205
+ for (i, probe_link) in self.material_system.material_reflection_probe.iter().enumerate() {
206
+ if probe_link.is_some() {
207
+ excluded.insert((i + 1) as material_system::MaterialHandle);
208
+ }
209
+ }
210
+
211
+ // Cache main-pass per-view inputs once outside the loop.
212
+ let main_view = self.current_view_matrix;
213
+ let proj = self.current_proj_matrix;
214
+ let cam_pos = self.current_camera_pos;
215
+
216
+ // Snapshot the existing PerView uniforms by reconstructing
217
+ // the same struct material_system_begin_frame writes — we
218
+ // need a fresh copy per probe to swap view/view_proj.
219
+ let base_per_view = material_system::PerViewUniforms {
220
+ view: main_view,
221
+ proj,
222
+ view_proj: self.current_vp_matrix,
223
+ // EN-022 fix: velocity reference (prev unjittered VP +
224
+ // current jitter), so material shaders computing
225
+ // `prev_view_proj * world` get true zero velocity on
226
+ // static geometry instead of TAA jitter-delta noise.
227
+ prev_view_proj: self.velocity_ref_vp,
228
+ inv_proj: self.current_inv_proj_matrix,
229
+ camera_pos: [
230
+ cam_pos[0], cam_pos[1], cam_pos[2],
231
+ self.lighting_uniforms.camera_pos[3],
232
+ ],
233
+ camera_dir: [0.0, 0.0, -1.0, 70.0_f32.to_radians()],
234
+ ambient: self.lighting_uniforms.ambient,
235
+ fog: [self.fog_color[0], self.fog_color[1], self.fog_color[2], self.fog_density],
236
+ sun_dir: self.lighting_uniforms.light_dir,
237
+ sun_color: self.lighting_uniforms.light_color,
238
+ dir_light_count: self.lighting_uniforms.dir_light_count,
239
+ dir_lights: std::array::from_fn(|i| material_system::PerViewDirLight {
240
+ direction: self.lighting_uniforms.dir_lights[i].direction,
241
+ color: self.lighting_uniforms.dir_lights[i].color,
242
+ }),
243
+ point_light_count: self.lighting_uniforms.point_light_count,
244
+ point_lights: std::array::from_fn(|i| material_system::PerViewPointLight {
245
+ position: self.lighting_uniforms.point_lights[i].position,
246
+ color: self.lighting_uniforms.point_lights[i].color,
247
+ }),
248
+ shadow_splits: self.lighting_uniforms.shadow_cascade_splits,
249
+ shadow_view: self.lighting_uniforms.shadow_view_matrix,
250
+ shadow_cascades: self.lighting_uniforms.shadow_cascade_vps,
251
+ };
252
+
253
+ // Iterate probes by index — we mutate `material_system`'s
254
+ // commands view while iterating, so collect the work first.
255
+ let probe_count = self.planar_probes.len();
256
+ for i in 0..probe_count {
257
+ let (plane_y, normal, color_view, depth_view,
258
+ aux_material_view, aux_velocity_view, aux_albedo_view) =
259
+ match &self.planar_probes[i] {
260
+ Some(p) => (p.plane_y, p.normal, p.color_view.clone(), p.depth_view.clone(),
261
+ p.aux_material_view.clone(), p.aux_velocity_view.clone(),
262
+ p.aux_albedo_view.clone()),
263
+ None => continue,
264
+ };
265
+ let view_buf = match self.planar_probe_view_buffers[i].as_ref() {
266
+ Some(b) => b, None => continue,
267
+ };
268
+
269
+ // Mirror the camera + recompute view_proj for the probe.
270
+ let mirror_view = planar_reflection::mirrored_view(main_view, plane_y, normal);
271
+ let mirror_cam = planar_reflection::mirrored_camera_pos(cam_pos, plane_y, normal);
272
+
273
+ // EN-011 V2 — oblique near-plane clip. Replace the
274
+ // projection's near plane with the water plane (in
275
+ // mirror-eye-space) so geometry below the plane is
276
+ // clipped at the rasterizer instead of polluting the
277
+ // reflection edge.
278
+ //
279
+ // World-space plane equation: `N · p + d_w = 0` with
280
+ // kept-side `N · p + d_w > 0` (above water). For a
281
+ // horizontal mirror at world y = plane_y with normal
282
+ // +Y, d_w = -plane_y so the kept side is y > plane_y.
283
+ // Transformed via mirror-view's inverse-transpose, the
284
+ // eye-space plane defines the same physical half-space
285
+ // (the side the kept geometry lives on after the
286
+ // reflection has rolled it through the view).
287
+ let d_w = -(normal[0] * 0.0 + normal[1] * plane_y + normal[2] * 0.0);
288
+ let plane_world = [normal[0], normal[1], normal[2], d_w];
289
+ let plane_eye = planar_reflection::world_plane_to_eye_space(mirror_view, plane_world);
290
+ let mirror_proj = planar_reflection::oblique_proj(proj, plane_eye);
291
+ let mirror_vp = mat4_multiply(mirror_proj, mirror_view);
292
+
293
+ let mut per_view = base_per_view;
294
+ per_view.view = mirror_view;
295
+ per_view.proj = mirror_proj;
296
+ per_view.view_proj = mirror_vp;
297
+ per_view.inv_proj = planar_reflection::inv_proj_for(mirror_proj);
298
+ per_view.camera_pos[0] = mirror_cam[0];
299
+ per_view.camera_pos[1] = mirror_cam[1];
300
+ per_view.camera_pos[2] = mirror_cam[2];
301
+ // prev_view_proj stays as the main camera's previous VP
302
+ // — TAA reprojection isn't meaningful for the reflection
303
+ // probe (we don't temporally accumulate it), so this is
304
+ // benign.
305
+ self.queue.write_buffer(view_buf, 0, bytemuck::bytes_of(&per_view));
306
+
307
+ // EN-011 V2 — rebuild the per-probe PerView bind group with
308
+ // the live env / BRDF / shadow views. V1 bound 1×1 stub
309
+ // textures here, which left mirrored draws lit by a flat
310
+ // grey IBL (no specular reflections, no sun shadow) — the
311
+ // reflection painting the lit scene differently from the
312
+ // main pass made the surface look "off". Rebuilding once
313
+ // per probe per frame is cheap (a single bind-group
314
+ // create); it also picks up any env hot-load that
315
+ // happens between frames without needing explicit dirty
316
+ // tracking on the probe side.
317
+ //
318
+ // The sky env (binding 1) and env_diffuse (binding 3)
319
+ // default to the renderer's 1×1 grey fallback when no HDR
320
+ // is loaded — same default the main pass uses, so the
321
+ // reflection's IBL stays consistent pre/post
322
+ // `load_env_from_hdr`. The sky_texture's view doesn't sit
323
+ // on a struct field (it's owned by `sky_bind_group`), so
324
+ // we build fresh views here each frame; that's a cheap
325
+ // Arc bump on the underlying wgpu Texture.
326
+ // Cached per-probe PerView bind group — inputs (env, BRDF,
327
+ // shadow views, the per-probe UBO) are all stable objects;
328
+ // env (re)loads and probe creation clear the cache slot.
329
+ if self.planar_probe_view_bgs[i].is_none() {
330
+ let sky_view_owned: Option<wgpu::TextureView> = self.sky_texture
331
+ .as_ref()
332
+ .map(|t| t.create_view(&Default::default()));
333
+ let env_view: &wgpu::TextureView = sky_view_owned
334
+ .as_ref()
335
+ .unwrap_or(&self.scene_env_default_view);
336
+ let diffuse_view_owned: Option<wgpu::TextureView> = self.env_diffuse_texture
337
+ .as_ref()
338
+ .map(|t| t.create_view(&Default::default()));
339
+ let env_diffuse_view: &wgpu::TextureView = diffuse_view_owned
340
+ .as_ref()
341
+ .unwrap_or(&self.scene_env_default_view);
342
+ self.planar_probe_view_bgs[i] = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
343
+ label: Some("planar_probe_per_view_bg_live"),
344
+ layout: &self.material_system.layouts.per_view,
345
+ entries: &[
346
+ wgpu::BindGroupEntry { binding: 0, resource: view_buf.as_entire_binding() },
347
+ // env (specular) tex + sampler
348
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(env_view) },
349
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&self.env_sampler) },
350
+ // env diffuse tex
351
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(env_diffuse_view) },
352
+ // BRDF LUT tex + sampler
353
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self.brdf_lut_view) },
354
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self.brdf_lut_sampler) },
355
+ // 3 shadow cascades — same depth views the main
356
+ // pass binds, so the reflection picks up sun
357
+ // shadows without re-rendering the cascades.
358
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[0]) },
359
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[1]) },
360
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[2]) },
361
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self.shadow_map.sampler) },
362
+ ],
363
+ }));
364
+ }
365
+ let probe_view_bg = self.planar_probe_view_bgs[i].as_ref().unwrap();
366
+
367
+ // Stage each surviving draw's [mirror_mvp, model] into ONE
368
+ // batched upload (this was ~450 individual 128 B
369
+ // `write_buffer` calls per probe per frame), and record the
370
+ // draw list. Draws are culled against the MIRRORED frustum —
371
+ // the oblique near plane doubles as the water-plane clip, so
372
+ // below-plane geometry culls here too. Scene-graph nodes
373
+ // append after the cached models in the same slot space —
374
+ // creation order decides who survives the cap, so games
375
+ // should create hero geometry before filler (grass).
376
+ let mirror_planes = crate::scene::extract_frustum_planes(&mirror_vp);
377
+ let mut reflect_draws: Vec<(u64, usize, u32)> = Vec::new();
378
+ let mut node_slots: Vec<(usize, u32)> = Vec::new();
379
+ if let Some(model_buf) = &self.reflect_model_buf {
380
+ let stride = REFLECT_STRIDE as usize;
381
+ let mut staged: Vec<u8> = Vec::with_capacity(stride * 128);
382
+ for cmd in self.model_draw_commands.iter() {
383
+ // REFLECT_SCENE_WGSL can't skin — a skinned draw would
384
+ // mirror its bind pose at the origin. Skinned models
385
+ // (enemies) were never reflected on the old immediate
386
+ // path either, so skipping preserves that.
387
+ if cmd.skinned { continue; }
388
+ let slot = reflect_draws.len();
389
+ if slot >= REFLECT_MAX_DRAWS { break; }
390
+ let Some(Some(meshes)) = self.model_gpu_cache.get(&cmd.cache_handle) else { continue };
391
+ if cmd.mesh_idx >= meshes.len() { continue; }
392
+ let mesh = &meshes[cmd.mesh_idx];
393
+ let (wmin, wmax) =
394
+ transform_aabb(&cmd.model, mesh.local_min, mesh.local_max);
395
+ if wmin[0] <= wmax[0]
396
+ && crate::scene::aabb_outside_frustum(&mirror_planes, wmin, wmax)
397
+ {
398
+ continue;
399
+ }
400
+ let mirror_mvp = mat4_multiply(mirror_vp, cmd.model);
401
+ let base = staged.len();
402
+ staged.resize(base + stride, 0);
403
+ staged[base..base + 64].copy_from_slice(bytemuck::bytes_of(&mirror_mvp));
404
+ staged[base + 64..base + 128].copy_from_slice(bytemuck::bytes_of(&cmd.model));
405
+ reflect_draws.push((cmd.cache_handle, cmd.mesh_idx, slot as u32));
406
+ }
407
+ for (i, (_vb, _ib, _ic, _bg, model, wmin, wmax)) in scene_draws.iter().enumerate() {
408
+ let slot = reflect_draws.len() + node_slots.len();
409
+ if slot >= REFLECT_MAX_DRAWS { break; }
410
+ if wmin[0] <= wmax[0]
411
+ && crate::scene::aabb_outside_frustum(&mirror_planes, *wmin, *wmax)
412
+ {
413
+ continue;
414
+ }
415
+ let mirror_mvp = mat4_multiply(mirror_vp, *model);
416
+ let base = staged.len();
417
+ staged.resize(base + stride, 0);
418
+ staged[base..base + 64].copy_from_slice(bytemuck::bytes_of(&mirror_mvp));
419
+ staged[base + 64..base + 128].copy_from_slice(bytemuck::bytes_of(model));
420
+ node_slots.push((i, slot as u32));
421
+ }
422
+ if !staged.is_empty() {
423
+ self.queue.write_buffer(model_buf, 0, &staged);
424
+ }
425
+ }
426
+
427
+ // Clear the probe to transparent black. Geometry fragments write
428
+ // alpha 1, so the water shader can blend the probe over its analytic
429
+ // sky by alpha (a=0 → no reflected geometry → show the sky dome).
430
+ let clear_color = wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
431
+
432
+ let view_bg = probe_view_bg;
433
+ let cache = &self.model_gpu_cache;
434
+ let mat_sys = &self.material_system;
435
+ let refl_pipeline = self.reflect_scene_pipeline.as_ref();
436
+ let refl_model_bg = self.reflect_model_bg.as_ref();
437
+ let refl_light_bg = self.reflect_light_bg.as_ref();
438
+ // Pass A — user materials. Opaque-profile material pipelines
439
+ // (and their `_reflection` siblings) target the full opaque
440
+ // G-buffer layout, so this pass presents the same four
441
+ // attachments; the three aux targets are probe-resolution
442
+ // dummies cleared here and discarded at store. Only the hdr
443
+ // attachment (and depth, which pass B tests against) is kept.
444
+ {
445
+ let aux_ops = wgpu::Operations {
446
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
447
+ store: wgpu::StoreOp::Discard,
448
+ };
449
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
450
+ label: Some("bloom_planar_reflection_materials"),
451
+ color_attachments: &[
452
+ Some(wgpu::RenderPassColorAttachment {
453
+ view: &color_view,
454
+ resolve_target: None,
455
+ depth_slice: None,
456
+ ops: wgpu::Operations {
457
+ load: wgpu::LoadOp::Clear(clear_color),
458
+ store: wgpu::StoreOp::Store,
459
+ },
460
+ }),
461
+ Some(wgpu::RenderPassColorAttachment {
462
+ view: &aux_material_view,
463
+ resolve_target: None,
464
+ depth_slice: None,
465
+ ops: aux_ops,
466
+ }),
467
+ Some(wgpu::RenderPassColorAttachment {
468
+ view: &aux_velocity_view,
469
+ resolve_target: None,
470
+ depth_slice: None,
471
+ ops: aux_ops,
472
+ }),
473
+ Some(wgpu::RenderPassColorAttachment {
474
+ view: &aux_albedo_view,
475
+ resolve_target: None,
476
+ depth_slice: None,
477
+ ops: aux_ops,
478
+ }),
479
+ ],
480
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
481
+ view: &depth_view,
482
+ depth_ops: Some(wgpu::Operations {
483
+ load: wgpu::LoadOp::Clear(1.0),
484
+ store: wgpu::StoreOp::Store,
485
+ }),
486
+ stencil_ops: None,
487
+ }),
488
+ timestamp_writes: profiler.pass_timestamp_writes("planar_probe_materials"),
489
+ occlusion_query_set: None,
490
+ multiview_mask: None,
491
+ });
492
+ mat_sys.dispatch_with_view(
493
+ &mut pass, view_bg,
494
+ // Excluded: probe-linked materials (the water plane
495
+ // itself) and anything flagged not-probe-visible
496
+ // (authoring control — e.g. sub-pixel instanced
497
+ // grass in a 512² probe).
498
+ |handle| !excluded.contains(&handle)
499
+ && mat_sys.material_probe_visible(handle),
500
+ // EN-011 V2 — swap to each material's sibling
501
+ // pipeline with cull_mode flipped Front→Back.
502
+ // Reflection mirrors world-space, which inverts
503
+ // triangle winding; without the flip, single-
504
+ // sided opaque geometry renders inside-out in
505
+ // the probe's RT. Cutout materials have
506
+ // `reflection_pipeline = None` and gracefully fall
507
+ // back to the main pipeline (no cull change needed
508
+ // since they're already double-sided); translucent-
509
+ // profile materials are skipped inside (their
510
+ // single-target pipelines can't render into this
511
+ // 4-target pass).
512
+ true,
513
+ // Instance tiles cull against the MIRRORED frustum.
514
+ Some(&mirror_planes),
515
+ |handle, idx| {
516
+ if let Some(Some(meshes)) = cache.get(&handle) {
517
+ if idx < meshes.len() {
518
+ let mesh = &meshes[idx];
519
+ return Some((
520
+ &mesh.vb, &mesh.ib, mesh.index_count,
521
+ mesh.local_min, mesh.local_max,
522
+ ));
523
+ }
524
+ }
525
+ None
526
+ },
527
+ );
528
+ }
529
+
530
+ // Pass B — cached models (trees/house/foliage) + scene-graph
531
+ // nodes with the single-target REFLECT_SCENE pipeline. Loads
532
+ // pass A's color + depth so the two batches depth-test
533
+ // against each other.
534
+ {
535
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
536
+ label: Some("bloom_planar_reflection_models"),
537
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
538
+ view: &color_view,
539
+ resolve_target: None,
540
+ depth_slice: None,
541
+ ops: wgpu::Operations {
542
+ load: wgpu::LoadOp::Load,
543
+ store: wgpu::StoreOp::Store,
544
+ },
545
+ })],
546
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
547
+ view: &depth_view,
548
+ depth_ops: Some(wgpu::Operations {
549
+ load: wgpu::LoadOp::Load,
550
+ store: wgpu::StoreOp::Store,
551
+ }),
552
+ stencil_ops: None,
553
+ }),
554
+ timestamp_writes: profiler.pass_timestamp_writes("planar_probe_models"),
555
+ occlusion_query_set: None,
556
+ multiview_mask: None,
557
+ });
558
+ if let (Some(rp), Some(rmbg), Some(rlbg)) =
559
+ (refl_pipeline, refl_model_bg, refl_light_bg)
560
+ {
561
+ if !reflect_draws.is_empty() || !node_slots.is_empty() {
562
+ pass.set_pipeline(rp);
563
+ pass.set_bind_group(1, rlbg, &[]);
564
+ for (handle, midx, slot) in &reflect_draws {
565
+ if let Some(Some(meshes)) = cache.get(handle) {
566
+ if *midx < meshes.len() {
567
+ let mesh = &meshes[*midx];
568
+ pass.set_bind_group(0, rmbg, &[*slot * REFLECT_STRIDE as u32]);
569
+ pass.set_bind_group(2, &mesh.material_bg, &[]);
570
+ pass.set_vertex_buffer(0, mesh.vb.slice(..));
571
+ pass.set_index_buffer(mesh.ib.slice(..), wgpu::IndexFormat::Uint32);
572
+ pass.draw_indexed(0..mesh.index_count, 0, 0..1);
573
+ }
574
+ }
575
+ }
576
+ // Scene-graph nodes: same pipeline — node geometry is
577
+ // Vertex3D and node material bind groups share the
578
+ // scene material layout the pipeline's g2 expects.
579
+ for (i, slot) in &node_slots {
580
+ let (vb, ib, index_count, mat_bg, _model, _wmin, _wmax) =
581
+ &scene_draws[*i];
582
+ pass.set_bind_group(0, rmbg, &[*slot * REFLECT_STRIDE as u32]);
583
+ pass.set_bind_group(2, *mat_bg, &[]);
584
+ pass.set_vertex_buffer(0, vb.slice(..));
585
+ pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
586
+ pass.draw_indexed(0..*index_count, 0, 0..1);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ }
593
+ }