@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,298 @@
1
+ //! Scene graph picking — raycast from screen coordinates against scene nodes.
2
+ //!
3
+ //! Converts 2D screen position to a 3D world ray via inverse VP matrix,
4
+ //! then tests against all visible scene node triangles using Moller-Trumbore.
5
+
6
+ use crate::scene::SceneGraph;
7
+
8
+ /// Result of a scene pick operation.
9
+ #[derive(Clone, Debug)]
10
+ pub struct PickResult {
11
+ pub hit: bool,
12
+ pub handle: f64, // scene node handle that was hit
13
+ pub distance: f32,
14
+ pub point: [f32; 3], // world-space hit point
15
+ pub normal: [f32; 3], // face normal at hit point
16
+ }
17
+
18
+ impl PickResult {
19
+ pub fn miss() -> Self {
20
+ Self {
21
+ hit: false,
22
+ handle: 0.0,
23
+ distance: 0.0,
24
+ point: [0.0; 3],
25
+ normal: [0.0; 3],
26
+ }
27
+ }
28
+ }
29
+
30
+ /// Unproject screen coordinates to a world-space ray.
31
+ /// screen_x, screen_y: pixel coordinates (0,0 = top-left)
32
+ /// width, height: viewport dimensions
33
+ /// inv_vp: inverse view-projection matrix
34
+ /// camera_pos: camera world position
35
+ pub fn screen_to_ray(
36
+ screen_x: f32, screen_y: f32,
37
+ width: f32, height: f32,
38
+ inv_vp: &[[f32; 4]; 4],
39
+ _camera_pos: &[f32; 3],
40
+ ) -> ([f32; 3], [f32; 3]) {
41
+ // Convert screen coords to NDC (-1 to 1)
42
+ let ndc_x = (screen_x / width) * 2.0 - 1.0;
43
+ let ndc_y = 1.0 - (screen_y / height) * 2.0; // flip Y
44
+
45
+ // Unproject near point (z = -1 in NDC)
46
+ let near_ndc = [ndc_x, ndc_y, -1.0, 1.0];
47
+ let near_world = mat4_mul_vec4(inv_vp, &near_ndc);
48
+
49
+ // Unproject far point (z = 1 in NDC)
50
+ let far_ndc = [ndc_x, ndc_y, 1.0, 1.0];
51
+ let far_world = mat4_mul_vec4(inv_vp, &far_ndc);
52
+
53
+ // Perspective divide
54
+ let near = [
55
+ near_world[0] / near_world[3],
56
+ near_world[1] / near_world[3],
57
+ near_world[2] / near_world[3],
58
+ ];
59
+ let far = [
60
+ far_world[0] / far_world[3],
61
+ far_world[1] / far_world[3],
62
+ far_world[2] / far_world[3],
63
+ ];
64
+
65
+ // Ray direction
66
+ let dx = far[0] - near[0];
67
+ let dy = far[1] - near[1];
68
+ let dz = far[2] - near[2];
69
+ let len = (dx * dx + dy * dy + dz * dz).sqrt();
70
+ let dir = if len > 1e-8 {
71
+ [dx / len, dy / len, dz / len]
72
+ } else {
73
+ [0.0, 0.0, -1.0]
74
+ };
75
+
76
+ (near, dir)
77
+ }
78
+
79
+ /// Raycast against all visible scene nodes. Returns the closest hit.
80
+ pub fn raycast_scene(
81
+ scene: &SceneGraph,
82
+ origin: &[f32; 3],
83
+ direction: &[f32; 3],
84
+ ) -> PickResult {
85
+ let mut best = PickResult::miss();
86
+ let mut best_dist = f32::MAX;
87
+
88
+ for (handle, node) in scene.nodes.iter() {
89
+ if !node.visible || node.indices.is_empty() {
90
+ continue;
91
+ }
92
+
93
+ // Transform ray into node's local space via inverse transform
94
+ let inv_transform = mat4_inverse_local(&node.transform);
95
+ let local_origin = mat4_transform_point(&inv_transform, origin);
96
+ let local_dir = mat4_transform_dir(&inv_transform, direction);
97
+
98
+ // Test against all triangles
99
+ for tri in node.indices.chunks(3) {
100
+ if tri.len() < 3 { continue; }
101
+ let v0 = &node.vertices[tri[0] as usize];
102
+ let v1 = &node.vertices[tri[1] as usize];
103
+ let v2 = &node.vertices[tri[2] as usize];
104
+
105
+ if let Some((t, u, v)) = ray_triangle_intersection(
106
+ &local_origin, &local_dir,
107
+ &v0.position, &v1.position, &v2.position,
108
+ ) {
109
+ if t > 0.0 && t < best_dist {
110
+ best_dist = t;
111
+
112
+ // Compute hit point in world space
113
+ let hit_local = [
114
+ local_origin[0] + local_dir[0] * t,
115
+ local_origin[1] + local_dir[1] * t,
116
+ local_origin[2] + local_dir[2] * t,
117
+ ];
118
+ let hit_world = mat4_transform_point(&node.transform, &hit_local);
119
+
120
+ // Interpolate normal
121
+ let w = 1.0 - u - v;
122
+ let normal = [
123
+ v0.normal[0] * w + v1.normal[0] * u + v2.normal[0] * v,
124
+ v0.normal[1] * w + v1.normal[1] * u + v2.normal[1] * v,
125
+ v0.normal[2] * w + v1.normal[2] * u + v2.normal[2] * v,
126
+ ];
127
+ let nl = (normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]).sqrt();
128
+ let normal = if nl > 1e-6 {
129
+ [normal[0]/nl, normal[1]/nl, normal[2]/nl]
130
+ } else {
131
+ [0.0, 1.0, 0.0]
132
+ };
133
+
134
+ best = PickResult {
135
+ hit: true,
136
+ handle,
137
+ distance: t,
138
+ point: hit_world,
139
+ normal,
140
+ };
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ best
147
+ }
148
+
149
+ /// Q6: Raycast against all visible scene nodes. Returns ALL hits sorted by distance.
150
+ /// Used by editors for alt-click cycling through occluded objects.
151
+ pub fn raycast_scene_all(
152
+ scene: &SceneGraph,
153
+ origin: &[f32; 3],
154
+ direction: &[f32; 3],
155
+ max_results: usize,
156
+ ) -> Vec<PickResult> {
157
+ let mut results: Vec<PickResult> = Vec::new();
158
+
159
+ for (handle, node) in scene.nodes.iter() {
160
+ if !node.visible || node.indices.is_empty() {
161
+ continue;
162
+ }
163
+
164
+ let inv_transform = mat4_inverse_local(&node.transform);
165
+ let local_origin = mat4_transform_point(&inv_transform, origin);
166
+ let local_dir = mat4_transform_dir(&inv_transform, direction);
167
+
168
+ let mut node_best_dist = f32::MAX;
169
+ let mut node_best: Option<PickResult> = None;
170
+
171
+ for tri in node.indices.chunks(3) {
172
+ if tri.len() < 3 { continue; }
173
+ let v0 = &node.vertices[tri[0] as usize];
174
+ let v1 = &node.vertices[tri[1] as usize];
175
+ let v2 = &node.vertices[tri[2] as usize];
176
+
177
+ if let Some((t, u, v)) = ray_triangle_intersection(
178
+ &local_origin, &local_dir,
179
+ &v0.position, &v1.position, &v2.position,
180
+ ) {
181
+ if t > 0.0 && t < node_best_dist {
182
+ node_best_dist = t;
183
+ let hit_local = [
184
+ local_origin[0] + local_dir[0] * t,
185
+ local_origin[1] + local_dir[1] * t,
186
+ local_origin[2] + local_dir[2] * t,
187
+ ];
188
+ let hit_world = mat4_transform_point(&node.transform, &hit_local);
189
+ let w = 1.0 - u - v;
190
+ let normal = [
191
+ v0.normal[0] * w + v1.normal[0] * u + v2.normal[0] * v,
192
+ v0.normal[1] * w + v1.normal[1] * u + v2.normal[1] * v,
193
+ v0.normal[2] * w + v1.normal[2] * u + v2.normal[2] * v,
194
+ ];
195
+ let nl = (normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]).sqrt();
196
+ let normal = if nl > 1e-6 {
197
+ [normal[0]/nl, normal[1]/nl, normal[2]/nl]
198
+ } else {
199
+ [0.0, 1.0, 0.0]
200
+ };
201
+ node_best = Some(PickResult {
202
+ hit: true,
203
+ handle,
204
+ distance: t,
205
+ point: hit_world,
206
+ normal,
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+ if let Some(result) = node_best {
213
+ results.push(result);
214
+ }
215
+ }
216
+
217
+ // Sort by distance (closest first).
218
+ results.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
219
+ results.truncate(max_results);
220
+ results
221
+ }
222
+
223
+ // ============================================================
224
+ // Moller-Trumbore ray-triangle intersection
225
+ // ============================================================
226
+
227
+ fn ray_triangle_intersection(
228
+ origin: &[f32; 3], dir: &[f32; 3],
229
+ v0: &[f32; 3], v1: &[f32; 3], v2: &[f32; 3],
230
+ ) -> Option<(f32, f32, f32)> {
231
+ const EPSILON: f32 = 1e-7;
232
+
233
+ let e1 = [v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2]];
234
+ let e2 = [v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2]];
235
+
236
+ let h = cross(dir, &e2);
237
+ let a = dot(&e1, &h);
238
+ if a.abs() < EPSILON { return None; }
239
+
240
+ let f = 1.0 / a;
241
+ let s = [origin[0]-v0[0], origin[1]-v0[1], origin[2]-v0[2]];
242
+ let u = f * dot(&s, &h);
243
+ if u < 0.0 || u > 1.0 { return None; }
244
+
245
+ let q = cross(&s, &e1);
246
+ let v = f * dot(dir, &q);
247
+ if v < 0.0 || u + v > 1.0 { return None; }
248
+
249
+ let t = f * dot(&e2, &q);
250
+ if t > EPSILON {
251
+ Some((t, u, v))
252
+ } else {
253
+ None
254
+ }
255
+ }
256
+
257
+ fn cross(a: &[f32; 3], b: &[f32; 3]) -> [f32; 3] {
258
+ [
259
+ a[1]*b[2] - a[2]*b[1],
260
+ a[2]*b[0] - a[0]*b[2],
261
+ a[0]*b[1] - a[1]*b[0],
262
+ ]
263
+ }
264
+
265
+ fn dot(a: &[f32; 3], b: &[f32; 3]) -> f32 {
266
+ a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
267
+ }
268
+
269
+ fn mat4_mul_vec4(m: &[[f32; 4]; 4], v: &[f32; 4]) -> [f32; 4] {
270
+ [
271
+ m[0][0]*v[0] + m[1][0]*v[1] + m[2][0]*v[2] + m[3][0]*v[3],
272
+ m[0][1]*v[0] + m[1][1]*v[1] + m[2][1]*v[2] + m[3][1]*v[3],
273
+ m[0][2]*v[0] + m[1][2]*v[1] + m[2][2]*v[2] + m[3][2]*v[3],
274
+ m[0][3]*v[0] + m[1][3]*v[1] + m[2][3]*v[2] + m[3][3]*v[3],
275
+ ]
276
+ }
277
+
278
+ fn mat4_transform_point(m: &[[f32; 4]; 4], p: &[f32; 3]) -> [f32; 3] {
279
+ let v = mat4_mul_vec4(m, &[p[0], p[1], p[2], 1.0]);
280
+ if v[3].abs() > 1e-8 {
281
+ [v[0]/v[3], v[1]/v[3], v[2]/v[3]]
282
+ } else {
283
+ [v[0], v[1], v[2]]
284
+ }
285
+ }
286
+
287
+ fn mat4_transform_dir(m: &[[f32; 4]; 4], d: &[f32; 3]) -> [f32; 3] {
288
+ // Transform direction (no translation, no perspective divide)
289
+ [
290
+ m[0][0]*d[0] + m[1][0]*d[1] + m[2][0]*d[2],
291
+ m[0][1]*d[0] + m[1][1]*d[1] + m[2][1]*d[2],
292
+ m[0][2]*d[0] + m[1][2]*d[1] + m[2][2]*d[2],
293
+ ]
294
+ }
295
+
296
+ fn mat4_inverse_local(m: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
297
+ crate::renderer::mat4_invert(*m)
298
+ }
@@ -0,0 +1,345 @@
1
+ //! Post-processing effects for Bloom Engine.
2
+ //!
3
+ //! Implements a render-to-texture pipeline with:
4
+ //! - Object outlines (selected/hovered objects)
5
+ //! - Screen-space ambient occlusion (SSAO)
6
+ //! - Final composite pass
7
+ //!
8
+ //! The pipeline renders the scene to offscreen textures (color + depth + object ID),
9
+ //! then applies fullscreen post-processing passes before presenting to screen.
10
+
11
+ use wgpu::util::DeviceExt;
12
+
13
+ /// Fullscreen quad vertex shader (shared by all post-fx passes).
14
+ const FULLSCREEN_VERT: &str = "
15
+ struct VertexOutput {
16
+ @builtin(position) position: vec4<f32>,
17
+ @location(0) uv: vec2<f32>,
18
+ };
19
+
20
+ @vertex
21
+ fn vs_fullscreen(@builtin(vertex_index) idx: u32) -> VertexOutput {
22
+ var out: VertexOutput;
23
+ // Triangle strip covering screen: 3 vertices, no vertex buffer needed
24
+ let x = f32(i32(idx & 1u)) * 4.0 - 1.0;
25
+ let y = f32(i32(idx >> 1u)) * 4.0 - 1.0;
26
+ out.position = vec4<f32>(x, y, 0.0, 1.0);
27
+ out.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
28
+ return out;
29
+ }
30
+ ";
31
+
32
+ /// Outline detection shader — Sobel edge detection on object ID buffer.
33
+ const OUTLINE_FRAG: &str = "
34
+ struct OutlineParams {
35
+ color_selected: vec4<f32>, // rgba
36
+ color_hovered: vec4<f32>, // rgba
37
+ thickness: vec4<f32>, // [thickness, glow, pulse_time, 0]
38
+ screen_size: vec4<f32>, // [width, height, 0, 0]
39
+ };
40
+
41
+ @group(0) @binding(0) var scene_color: texture_2d<f32>;
42
+ @group(0) @binding(1) var object_id_tex: texture_2d<f32>;
43
+ @group(0) @binding(2) var tex_sampler: sampler;
44
+ @group(0) @binding(3) var<uniform> params: OutlineParams;
45
+
46
+ // VertexOutput is defined in FULLSCREEN_VERT — this string is concatenated
47
+ // after it, so redefining here would be a WGSL parser error.
48
+
49
+ @fragment
50
+ fn fs_outline(in: VertexOutput) -> @location(0) vec4<f32> {
51
+ let color = textureSample(scene_color, tex_sampler, in.uv);
52
+
53
+ // Object IDs must not be filtered — interpolating IDs produces nonsense
54
+ // values at edges. Use textureLoad with integer coords; this also avoids
55
+ // requiring the FLOAT32_FILTERABLE wgpu feature (not advertised on Apple GPUs).
56
+ let dim = vec2<i32>(textureDimensions(object_id_tex));
57
+ let max_xy = dim - vec2<i32>(1, 1);
58
+ let center_pix = clamp(vec2<i32>(in.uv * vec2<f32>(dim)), vec2<i32>(0, 0), max_xy);
59
+ let center_id = textureLoad(object_id_tex, center_pix, 0).r;
60
+
61
+ let t = max(1, i32(round(params.thickness.x)));
62
+
63
+ // Sample neighbors for edge detection
64
+ var edge = 0.0;
65
+ for (var dy = -1; dy <= 1; dy++) {
66
+ for (var dx = -1; dx <= 1; dx++) {
67
+ if (dx == 0 && dy == 0) { continue; }
68
+ let neighbor_pix = clamp(center_pix + vec2<i32>(dx, dy) * t, vec2<i32>(0, 0), max_xy);
69
+ let neighbor_id = textureLoad(object_id_tex, neighbor_pix, 0).r;
70
+ if (abs(neighbor_id - center_id) > 0.001) {
71
+ edge += 1.0;
72
+ }
73
+ }
74
+ }
75
+
76
+ // Normalize edge strength
77
+ edge = min(edge / 3.0, 1.0);
78
+
79
+ // Apply outline color if this pixel is on an edge
80
+ if (edge > 0.01) {
81
+ // Determine if selected or hovered based on the ID value
82
+ let outline_color = params.color_selected;
83
+ // Pulse effect
84
+ let pulse = 0.7 + 0.3 * sin(params.thickness.z * 3.0);
85
+ let glow = params.thickness.y * pulse;
86
+ return mix(color, outline_color, edge * (0.8 + glow * 0.2));
87
+ }
88
+
89
+ return color;
90
+ }
91
+ ";
92
+
93
+ // ============================================================
94
+ // Outline parameters
95
+ // ============================================================
96
+
97
+ #[repr(C)]
98
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
99
+ pub struct OutlineParams {
100
+ pub color_selected: [f32; 4],
101
+ pub color_hovered: [f32; 4],
102
+ pub thickness: [f32; 4], // [thickness, glow, pulse_time, 0]
103
+ pub screen_size: [f32; 4],
104
+ }
105
+
106
+ impl Default for OutlineParams {
107
+ fn default() -> Self {
108
+ Self {
109
+ color_selected: [0.2, 0.5, 1.0, 1.0], // blue
110
+ color_hovered: [0.8, 0.8, 0.2, 1.0], // yellow
111
+ thickness: [2.0, 0.3, 0.0, 0.0],
112
+ screen_size: [1280.0, 720.0, 0.0, 0.0],
113
+ }
114
+ }
115
+ }
116
+
117
+ // ============================================================
118
+ // Post-FX Pipeline
119
+ // ============================================================
120
+
121
+ pub struct PostFxPipeline {
122
+ // Offscreen render targets
123
+ pub color_texture: wgpu::Texture,
124
+ pub color_view: wgpu::TextureView,
125
+ pub object_id_texture: wgpu::Texture,
126
+ pub object_id_view: wgpu::TextureView,
127
+ pub depth_texture: wgpu::Texture,
128
+ pub depth_view: wgpu::TextureView,
129
+
130
+ // Outline pass
131
+ pub outline_pipeline: wgpu::RenderPipeline,
132
+ pub outline_bind_group: wgpu::BindGroup,
133
+ pub outline_params_buffer: wgpu::Buffer,
134
+ pub outline_params: OutlineParams,
135
+
136
+ // Common
137
+ pub sampler: wgpu::Sampler,
138
+ pub enabled: bool,
139
+ pub width: u32,
140
+ pub height: u32,
141
+
142
+ // Selected node handles for outline rendering
143
+ pub selected_handles: Vec<f64>,
144
+ pub hovered_handle: f64,
145
+ pub time: f32,
146
+ }
147
+
148
+ impl PostFxPipeline {
149
+ pub fn new(device: &wgpu::Device, width: u32, height: u32, surface_format: wgpu::TextureFormat) -> Self {
150
+ let (color_texture, color_view) = create_color_target(device, width, height, surface_format);
151
+ let (object_id_texture, object_id_view) = create_color_target(device, width, height, wgpu::TextureFormat::R32Float);
152
+ let (depth_texture, depth_view) = create_depth_target(device, width, height);
153
+
154
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
155
+ label: Some("postfx_sampler"),
156
+ mag_filter: wgpu::FilterMode::Linear,
157
+ min_filter: wgpu::FilterMode::Linear,
158
+ ..Default::default()
159
+ });
160
+
161
+ let outline_params = OutlineParams {
162
+ screen_size: [width as f32, height as f32, 0.0, 0.0],
163
+ ..Default::default()
164
+ };
165
+ let outline_params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
166
+ label: Some("outline_params"),
167
+ contents: bytemuck::bytes_of(&outline_params),
168
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
169
+ });
170
+
171
+ // Outline bind group layout
172
+ let outline_bg_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
173
+ label: Some("outline_bg_layout"),
174
+ entries: &[
175
+ bgl_texture(0, wgpu::TextureSampleType::Float { filterable: true }),
176
+ // R32Float — non-filterable on adapters without FLOAT32_FILTERABLE
177
+ // (e.g. Apple GPUs). Sampled via textureLoad in OUTLINE_FRAG.
178
+ bgl_texture(1, wgpu::TextureSampleType::Float { filterable: false }),
179
+ bgl_sampler(2),
180
+ bgl_uniform(3),
181
+ ],
182
+ });
183
+
184
+ let outline_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
185
+ label: Some("outline_bg"),
186
+ layout: &outline_bg_layout,
187
+ entries: &[
188
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&color_view) },
189
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&object_id_view) },
190
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&sampler) },
191
+ wgpu::BindGroupEntry { binding: 3, resource: outline_params_buffer.as_entire_binding() },
192
+ ],
193
+ });
194
+
195
+ // Outline pipeline
196
+ let outline_shader_src = format!("{}\n{}", FULLSCREEN_VERT, OUTLINE_FRAG);
197
+ let outline_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
198
+ label: Some("outline_shader"),
199
+ source: wgpu::ShaderSource::Wgsl(outline_shader_src.into()),
200
+ });
201
+
202
+ let outline_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
203
+ label: Some("outline_pipeline_layout"),
204
+ bind_group_layouts: &[Some(&outline_bg_layout)],
205
+ immediate_size: 0,
206
+ });
207
+
208
+ let outline_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
209
+ label: Some("outline_pipeline"),
210
+ layout: Some(&outline_pipeline_layout),
211
+ vertex: wgpu::VertexState {
212
+ module: &outline_shader,
213
+ entry_point: Some("vs_fullscreen"),
214
+ buffers: &[],
215
+ compilation_options: Default::default(),
216
+ },
217
+ fragment: Some(wgpu::FragmentState {
218
+ module: &outline_shader,
219
+ entry_point: Some("fs_outline"),
220
+ targets: &[Some(wgpu::ColorTargetState {
221
+ format: surface_format,
222
+ blend: None,
223
+ write_mask: wgpu::ColorWrites::ALL,
224
+ })],
225
+ compilation_options: Default::default(),
226
+ }),
227
+ primitive: wgpu::PrimitiveState {
228
+ topology: wgpu::PrimitiveTopology::TriangleStrip,
229
+ ..Default::default()
230
+ },
231
+ depth_stencil: None,
232
+ multisample: Default::default(),
233
+ multiview_mask: None,
234
+ cache: None,
235
+ });
236
+
237
+ Self {
238
+ color_texture,
239
+ color_view,
240
+ object_id_texture,
241
+ object_id_view,
242
+ depth_texture,
243
+ depth_view,
244
+ outline_pipeline,
245
+ outline_bind_group,
246
+ outline_params_buffer,
247
+ outline_params,
248
+ sampler,
249
+ enabled: false,
250
+ width,
251
+ height,
252
+ selected_handles: Vec::new(),
253
+ hovered_handle: 0.0,
254
+ time: 0.0,
255
+ }
256
+ }
257
+
258
+ pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32, surface_format: wgpu::TextureFormat) {
259
+ if width == self.width && height == self.height { return; }
260
+ *self = Self::new(device, width, height, surface_format);
261
+ }
262
+
263
+ pub fn set_selected(&mut self, handles: Vec<f64>) {
264
+ self.selected_handles = handles;
265
+ }
266
+
267
+ pub fn set_hovered(&mut self, handle: f64) {
268
+ self.hovered_handle = handle;
269
+ }
270
+
271
+ pub fn update(&mut self, queue: &wgpu::Queue, dt: f32) {
272
+ self.time += dt;
273
+ self.outline_params.thickness[2] = self.time;
274
+ queue.write_buffer(&self.outline_params_buffer, 0, bytemuck::bytes_of(&self.outline_params));
275
+ }
276
+ }
277
+
278
+ // ============================================================
279
+ // Helpers
280
+ // ============================================================
281
+
282
+ fn create_color_target(device: &wgpu::Device, width: u32, height: u32, format: wgpu::TextureFormat) -> (wgpu::Texture, wgpu::TextureView) {
283
+ let tex = device.create_texture(&wgpu::TextureDescriptor {
284
+ label: Some("postfx_color"),
285
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
286
+ mip_level_count: 1,
287
+ sample_count: 1,
288
+ dimension: wgpu::TextureDimension::D2,
289
+ format,
290
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
291
+ view_formats: &[],
292
+ });
293
+ let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
294
+ (tex, view)
295
+ }
296
+
297
+ fn create_depth_target(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::Texture, wgpu::TextureView) {
298
+ let tex = device.create_texture(&wgpu::TextureDescriptor {
299
+ label: Some("postfx_depth"),
300
+ size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
301
+ mip_level_count: 1,
302
+ sample_count: 1,
303
+ dimension: wgpu::TextureDimension::D2,
304
+ format: wgpu::TextureFormat::Depth32Float,
305
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
306
+ view_formats: &[],
307
+ });
308
+ let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
309
+ (tex, view)
310
+ }
311
+
312
+ fn bgl_texture(binding: u32, sample_type: wgpu::TextureSampleType) -> wgpu::BindGroupLayoutEntry {
313
+ wgpu::BindGroupLayoutEntry {
314
+ binding,
315
+ visibility: wgpu::ShaderStages::FRAGMENT,
316
+ ty: wgpu::BindingType::Texture {
317
+ sample_type,
318
+ view_dimension: wgpu::TextureViewDimension::D2,
319
+ multisampled: false,
320
+ },
321
+ count: None,
322
+ }
323
+ }
324
+
325
+ fn bgl_sampler(binding: u32) -> wgpu::BindGroupLayoutEntry {
326
+ wgpu::BindGroupLayoutEntry {
327
+ binding,
328
+ visibility: wgpu::ShaderStages::FRAGMENT,
329
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
330
+ count: None,
331
+ }
332
+ }
333
+
334
+ fn bgl_uniform(binding: u32) -> wgpu::BindGroupLayoutEntry {
335
+ wgpu::BindGroupLayoutEntry {
336
+ binding,
337
+ visibility: wgpu::ShaderStages::FRAGMENT,
338
+ ty: wgpu::BindingType::Buffer {
339
+ ty: wgpu::BufferBindingType::Uniform,
340
+ has_dynamic_offset: false,
341
+ min_binding_size: None,
342
+ },
343
+ count: None,
344
+ }
345
+ }