@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,1258 @@
1
+ import { spawn, parallelMap } from 'perry/thread';
2
+ import { Color, Model, Vec3, Mat4, BoundingBox } from '../core/types';
3
+
4
+ // FFI declarations
5
+ declare function bloom_load_model(path: number): number;
6
+ declare function bloom_unload_model(handle: number): void;
7
+ declare function bloom_draw_model(handle: number, x: number, y: number, z: number, scale: number, r: number, g: number, b: number, a: number): void;
8
+ declare function bloom_draw_model_rotated(handle: number, x: number, y: number, z: number, scale: number, rotY: number, colorPackedArgb: number): void;
9
+ declare function bloom_draw_model_transform16(
10
+ handle: number,
11
+ m0: number, m1: number, m2: number, m3: number,
12
+ m4: number, m5: number, m6: number, m7: number,
13
+ m8: number, m9: number, m10: number, m11: number,
14
+ m12: number, m13: number, m14: number, m15: number,
15
+ colorPackedArgb: number,
16
+ ): void;
17
+ declare function bloom_set_model_foliage_wind(handle: number, amount: number): void;
18
+ declare function bloom_set_foliage_shadow_motion(on: number): void;
19
+ declare function bloom_unload_model(handle: number): void;
20
+ declare function bloom_draw_cube(x: number, y: number, z: number, w: number, h: number, d: number, r: number, g: number, b: number, a: number): void;
21
+ declare function bloom_draw_cube_wires(x: number, y: number, z: number, w: number, h: number, d: number, r: number, g: number, b: number, a: number): void;
22
+ declare function bloom_draw_sphere(x: number, y: number, z: number, radius: number, r: number, g: number, b: number, a: number): void;
23
+ declare function bloom_draw_sphere_wires(x: number, y: number, z: number, radius: number, r: number, g: number, b: number, a: number): void;
24
+ declare function bloom_draw_cylinder(x: number, y: number, z: number, rt: number, rb: number, h: number, r: number, g: number, b: number, a: number): void;
25
+ declare function bloom_draw_cylinder_ex(x: number, y: number, z: number, rt: number, rb: number, h: number, slices: number, r: number, g: number, b: number, a: number): void;
26
+ declare function bloom_draw_plane(x: number, y: number, z: number, w: number, d: number, r: number, g: number, b: number, a: number): void;
27
+ declare function bloom_draw_grid(slices: number, spacing: number): void;
28
+ declare function bloom_draw_ray(ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, r: number, g: number, b: number, a: number): void;
29
+ declare function bloom_gen_mesh_cube(w: number, h: number, d: number): number;
30
+ declare function bloom_gen_mesh_heightmap(imageHandle: number, sizeX: number, sizeY: number, sizeZ: number): number;
31
+ declare function bloom_load_shader(source: number): number;
32
+ declare function bloom_compile_material(source: number): number;
33
+ declare function bloom_compile_material_refractive(source: number): number;
34
+ declare function bloom_compile_material_transparent(source: number): number;
35
+ declare function bloom_compile_material_additive(source: number): number;
36
+ declare function bloom_compile_material_cutout(source: number): number;
37
+ declare function bloom_compile_material_instanced(source: number): number;
38
+ declare function bloom_create_instance_buffer(dataPtr: any, instanceCount: number): number;
39
+ declare function bloom_create_instance_buffer_scratch(instanceCount: number): number;
40
+ declare function bloom_submit_material_draw_instanced(material: number, meshHandle: number, meshIdx: number, instanceBuffer: number, instanceCount: number): void;
41
+ declare function bloom_destroy_instance_buffer(handle: number): void;
42
+ declare function bloom_create_planar_reflection(planeY: number, normalX: number, normalY: number, normalZ: number, resolution: number): number;
43
+ declare function bloom_set_material_reflection_probe(material: number, probe: number): void;
44
+ declare function bloom_set_material_texture_array(material: number, slot: number, array: number): void;
45
+ declare function bloom_set_material_shading_model(material: number, model: number): void;
46
+ declare function bloom_set_material_probe_visible(material: number, visible: number): void;
47
+ declare function bloom_set_material_foliage(material: number, transR: number, transG: number, transB: number, transAmount: number, wrapFactor: number): void;
48
+ declare function bloom_compile_material_from_file(path: number, bucketKind: number): number;
49
+ declare function bloom_set_material_params_scratch(handle: number, paramCount: number): void;
50
+ declare function bloom_draw_material(material: number, meshHandle: number, meshIdx: number, x: number, y: number, z: number, scale: number, r: number, g: number, b: number, a: number): void;
51
+ declare function bloom_load_model_animation(path: number): number;
52
+ declare function bloom_instantiate_animation(src: number): number;
53
+ declare function bloom_update_model_animation(handle: number, animIndex: number, time: number, scale: number, px: number, py: number, pz: number, rotY: number): void;
54
+ declare function bloom_create_mesh(vertexPtr: number, vertexCount: number, indexPtr: number, indexCount: number): number;
55
+ declare function bloom_mesh_scratch_reset(): void;
56
+ declare function bloom_mesh_scratch_push_f32(v: number): void;
57
+ declare function bloom_mesh_scratch_push_u32(v: number): void;
58
+ declare function bloom_create_mesh_scratch(vertexCount: number, indexCount: number): number;
59
+ declare function bloom_set_ambient_light(r: number, g: number, b: number, intensity: number): void;
60
+ declare function bloom_set_directional_light(dx: number, dy: number, dz: number, r: number, g: number, b: number, intensity: number): void;
61
+ declare function bloom_set_procedural_sky(enabled: number, rayleighDensity: number, mieDensity: number, groundAlbedo: number): void;
62
+ declare function bloom_set_sun_direction(dx: number, dy: number, dz: number, intensity: number): void;
63
+ declare function bloom_gen_mesh_spline_ribbon(pointsPtr: number, pointCount: number, widthsPtr: number, widthCount: number): number;
64
+ declare function bloom_gen_mesh_spline_ribbon_scratch(pointCount: number, widthCount: number): number;
65
+ declare function bloom_get_model_mesh_count(handle: number): number;
66
+ declare function bloom_get_model_material_count(handle: number): number;
67
+ declare function bloom_get_model_bounds_min_x(handle: number): number;
68
+ declare function bloom_get_model_bounds_min_y(handle: number): number;
69
+ declare function bloom_get_model_bounds_min_z(handle: number): number;
70
+ declare function bloom_get_model_bounds_max_x(handle: number): number;
71
+ declare function bloom_get_model_bounds_max_y(handle: number): number;
72
+ declare function bloom_get_model_bounds_max_z(handle: number): number;
73
+
74
+ function makeModel(handle: number): Model {
75
+ const mc = bloom_get_model_mesh_count(handle);
76
+ const matc = bloom_get_model_material_count(handle);
77
+ return { handle, meshCount: mc, materialCount: matc, transform: [1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,0.0,0.0,1.0] };
78
+ }
79
+
80
+ // OBJ parser (pure TypeScript)
81
+ function parseOBJ(text: string): { vertices: number[]; indices: number[] } | null {
82
+ const positions: number[][] = [];
83
+ const normals: number[][] = [];
84
+ const texcoords: number[][] = [];
85
+ const vertexMap = new Map<string, number>();
86
+ const vertices: number[] = [];
87
+ const indices: number[] = [];
88
+ let vertexCount = 0;
89
+
90
+ const lines = text.split('\n');
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const line = lines[i].trim();
93
+ if (line.length === 0 || line[0] === '#') continue;
94
+
95
+ const parts = line.split(/\s+/);
96
+ const cmd = parts[0];
97
+
98
+ if (cmd === 'v' && parts.length >= 4) {
99
+ positions.push([parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])]);
100
+ } else if (cmd === 'vn' && parts.length >= 4) {
101
+ normals.push([parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])]);
102
+ } else if (cmd === 'vt' && parts.length >= 3) {
103
+ texcoords.push([parseFloat(parts[1]), parseFloat(parts[2])]);
104
+ } else if (cmd === 'f') {
105
+ // Triangulate face (fan from first vertex)
106
+ const faceIndices: number[] = [];
107
+ for (let j = 1; j < parts.length; j++) {
108
+ const key = parts[j];
109
+ if (vertexMap.has(key)) {
110
+ faceIndices.push(vertexMap.get(key)!);
111
+ } else {
112
+ const segs = key.split('/');
113
+ const pi = parseInt(segs[0]) - 1;
114
+ const ti = segs.length > 1 && segs[1] !== '' ? parseInt(segs[1]) - 1 : -1;
115
+ const ni = segs.length > 2 ? parseInt(segs[2]) - 1 : -1;
116
+
117
+ const pos = pi >= 0 && pi < positions.length ? positions[pi] : [0, 0, 0];
118
+ const norm = ni >= 0 && ni < normals.length ? normals[ni] : [0, 1, 0];
119
+ const uv = ti >= 0 && ti < texcoords.length ? texcoords[ti] : [0, 0];
120
+
121
+ // Format: x,y,z, nx,ny,nz, r,g,b,a, u,v (12 floats per vertex)
122
+ vertices.push(pos[0], pos[1], pos[2]);
123
+ vertices.push(norm[0], norm[1], norm[2]);
124
+ vertices.push(1, 1, 1, 1); // white color
125
+ vertices.push(uv[0], uv[1]);
126
+
127
+ const idx = vertexCount;
128
+ vertexCount++;
129
+ vertexMap.set(key, idx);
130
+ faceIndices.push(idx);
131
+ }
132
+ }
133
+
134
+ // Fan triangulation
135
+ for (let j = 2; j < faceIndices.length; j++) {
136
+ indices.push(faceIndices[0], faceIndices[j - 1], faceIndices[j]);
137
+ }
138
+ }
139
+ }
140
+
141
+ if (vertexCount === 0) return null;
142
+ return { vertices, indices };
143
+ }
144
+
145
+ declare function bloom_read_file(path: number): number;
146
+
147
+ /**
148
+ * Free a loaded model's CPU + GPU resources. The FFI existed for years with
149
+ * no TS wrapper, so nothing could ever call it — long-lived tools (the world
150
+ * editor's project switching) leaked every model.
151
+ */
152
+ export function unloadModel(model: Model): void {
153
+ bloom_unload_model(model.handle);
154
+ }
155
+
156
+ export function loadModel(path: string): Model {
157
+ // Check for OBJ format
158
+ const pathLower = (path as string).toLowerCase();
159
+ if (pathLower.endsWith('.obj')) {
160
+ const text: string = bloom_read_file(path as any) as any;
161
+ if (text) {
162
+ const parsed = parseOBJ(text);
163
+ if (parsed) {
164
+ return uploadMeshScratch(
165
+ parsed.vertices, parsed.vertices.length / 12,
166
+ parsed.indices, parsed.indices.length,
167
+ );
168
+ }
169
+ }
170
+ return makeModel(0);
171
+ }
172
+
173
+ const handle = bloom_load_model(path as any);
174
+ return makeModel(handle);
175
+ }
176
+
177
+ export function drawModel(model: Model, position: Vec3, scale: number, tint: Color): void {
178
+ bloom_draw_model(model.handle, position.x, position.y, position.z, scale, tint.r, tint.g, tint.b, tint.a);
179
+ }
180
+
181
+ /// Draw a model with a Y-axis rotation (radians). RGBA is packed into
182
+ /// a single f64 (ARGB byte order) to keep the FFI to 7 args, dodging
183
+ /// the Perry-ARM64 9th-arg quirk.
184
+ /**
185
+ * Draw a model with a Y-axis rotation in DEGREES (engine-wide angle
186
+ * convention, matching Camera2D.rotation and raylib; was radians before
187
+ * v0.5). Tint components are 0-255.
188
+ */
189
+ export function drawModelRotated(
190
+ model: Model, position: Vec3, scale: number, rotY: number, tint: Color,
191
+ ): void {
192
+ // Color components are 0..255 ints (matching drawModel above).
193
+ const a = (tint.a & 0xff) << 24;
194
+ const r = (tint.r & 0xff) << 16;
195
+ const g = (tint.g & 0xff) << 8;
196
+ const b = tint.b & 0xff;
197
+ // Use unsigned-shift-zero to keep the value positive when stored as f64.
198
+ const packed = (a | r | g | b) >>> 0;
199
+ bloom_draw_model_rotated(model.handle, position.x, position.y, position.z, scale, rotY * Math.PI / 180, packed);
200
+ }
201
+
202
+ /**
203
+ * EN-039 — draw a model under a full column-major 4x4 transform, so an
204
+ * immediate-mode draw can pitch and roll. `drawModelRotated` above can only
205
+ * express a Y rotation: a held weapon cannot tilt with the aim, and neither can
206
+ * a thrown prop or a debris chunk.
207
+ *
208
+ * `m16` is column-major (same layout as `setSceneNodeTransform16` and as
209
+ * `mat4_*` in this engine — note the two conventions warning in the PT docs:
210
+ * `mat4_multiply` is RAW). It carries translation and scale too, so this
211
+ * REPLACES position/scale rather than combining with them.
212
+ *
213
+ * Skinned models are a no-op here on purpose: their joint matrices already bake
214
+ * world orientation, so applying a model matrix on top double-transforms them.
215
+ * Use `animUpdate` for those.
216
+ *
217
+ * Tint components are 0-255. RGBA packs into one f64 (ARGB byte order), as in
218
+ * `drawModelRotated`.
219
+ */
220
+ export function drawModelTransform(model: Model, m16: number[], tint: Color): void {
221
+ const a = (tint.a & 0xff) << 24;
222
+ const r = (tint.r & 0xff) << 16;
223
+ const g = (tint.g & 0xff) << 8;
224
+ const b = tint.b & 0xff;
225
+ const packed = (a | r | g | b) >>> 0;
226
+ bloom_draw_model_transform16(
227
+ model.handle,
228
+ m16[0], m16[1], m16[2], m16[3],
229
+ m16[4], m16[5], m16[6], m16[7],
230
+ m16[8], m16[9], m16[10], m16[11],
231
+ m16[12], m16[13], m16[14], m16[15],
232
+ packed,
233
+ );
234
+ }
235
+
236
+ /**
237
+ * Return the axis-aligned bounding box of a loaded model in its local
238
+ * coordinate space. Computed once at load time from mesh vertex positions.
239
+ *
240
+ * Used by editors to:
241
+ * - size move/rotate/scale gizmos to the selected entity
242
+ * - auto-frame the camera on the current selection
243
+ * - snap placed entities onto terrain by the lowest vertex
244
+ * - build precise ray-pick colliders
245
+ *
246
+ * Returns a zero-sized box at the origin if the model handle is invalid.
247
+ */
248
+ /**
249
+ * Q9: Generate a ribbon mesh along a Catmull-Rom spline.
250
+ * `points` is a flat array [x0,y0,z0, x1,y1,z1, ...] of control points.
251
+ * `widths` has one width per control point.
252
+ * Returns a Model whose mesh is a smooth triangle-strip ribbon.
253
+ */
254
+ /**
255
+ * Build a ribbon mesh that follows a Catmull-Rom spline through `points`
256
+ * (flat x,y,z triples), with a per-point half-width from `widths`.
257
+ *
258
+ * Goes through the mesh scratch buffers — positions first, then widths — for
259
+ * the same reason as `createMesh`: Perry 0.5.x will not pass a `number[]` into
260
+ * an `i64` pointer param, which made the pointer form unreachable from TS.
261
+ */
262
+ export function genMeshSplineRibbon(points: number[], widths: number[]): Model {
263
+ const pointCount = Math.floor(points.length / 3);
264
+ const widthCount = widths.length;
265
+ if (pointCount < 2 || widthCount === 0) return makeModel(0);
266
+
267
+ bloom_mesh_scratch_reset();
268
+ for (let i = 0; i < pointCount * 3; i++) bloom_mesh_scratch_push_f32(points[i]);
269
+ for (let i = 0; i < widthCount; i++) bloom_mesh_scratch_push_f32(widths[i]);
270
+
271
+ const handle = bloom_gen_mesh_spline_ribbon_scratch(pointCount, widthCount);
272
+ return makeModel(handle);
273
+ }
274
+
275
+ export function getModelBounds(model: Model): BoundingBox {
276
+ return {
277
+ min: {
278
+ x: bloom_get_model_bounds_min_x(model.handle),
279
+ y: bloom_get_model_bounds_min_y(model.handle),
280
+ z: bloom_get_model_bounds_min_z(model.handle),
281
+ },
282
+ max: {
283
+ x: bloom_get_model_bounds_max_x(model.handle),
284
+ y: bloom_get_model_bounds_max_y(model.handle),
285
+ z: bloom_get_model_bounds_max_z(model.handle),
286
+ },
287
+ };
288
+ }
289
+
290
+ export interface DrawCubeOpts {
291
+ rotationY?: number;
292
+ }
293
+
294
+ export function drawCube(position: Vec3, width: number, height: number, depth: number, color: Color, opts?: DrawCubeOpts): void {
295
+ // Note: rotationY is accepted for API compatibility but applied only when native support exists
296
+ bloom_draw_cube(position.x, position.y, position.z, width, height, depth, color.r, color.g, color.b, color.a);
297
+ }
298
+
299
+ export function drawCubeWires(position: Vec3, width: number, height: number, depth: number, color: Color): void {
300
+ bloom_draw_cube_wires(position.x, position.y, position.z, width, height, depth, color.r, color.g, color.b, color.a);
301
+ }
302
+
303
+ export function drawSphere(position: Vec3, radius: number, color: Color): void {
304
+ bloom_draw_sphere(position.x, position.y, position.z, radius, color.r, color.g, color.b, color.a);
305
+ }
306
+
307
+ export function drawSphereWires(position: Vec3, radius: number, color: Color): void {
308
+ bloom_draw_sphere_wires(position.x, position.y, position.z, radius, color.r, color.g, color.b, color.a);
309
+ }
310
+
311
+ export function drawCylinder(position: Vec3, radiusTop: number, radiusBottom: number, height: number, color: Color, slices?: number): void {
312
+ bloom_draw_cylinder(position.x, position.y, position.z, radiusTop, radiusBottom, height, color.r, color.g, color.b, color.a);
313
+ }
314
+
315
+ export function drawPlane(position: Vec3, width: number, depth: number, color: Color): void {
316
+ bloom_draw_plane(position.x, position.y, position.z, width, depth, color.r, color.g, color.b, color.a);
317
+ }
318
+
319
+ export function drawGrid(slices: number, spacing: number): void {
320
+ bloom_draw_grid(slices, spacing);
321
+ }
322
+
323
+ export function drawRay(origin: Vec3, direction: Vec3, color: Color): void {
324
+ bloom_draw_ray(origin.x, origin.y, origin.z, direction.x, direction.y, direction.z, color.r, color.g, color.b, color.a);
325
+ }
326
+
327
+ export function genMeshCube(width: number, height: number, depth: number): Model {
328
+ const handle = bloom_gen_mesh_cube(width, height, depth);
329
+ return makeModel(handle, 1, 1);
330
+ }
331
+
332
+ export function genMeshHeightmap(imageHandle: number, sizeX: number, sizeY: number, sizeZ: number): Model {
333
+ const handle = bloom_gen_mesh_heightmap(imageHandle, sizeX, sizeY, sizeZ);
334
+ return makeModel(handle, 1, 1);
335
+ }
336
+
337
+ export function loadShader(wgslSource: string): number {
338
+ return bloom_load_shader(wgslSource as any);
339
+ }
340
+
341
+ /// Phase 1c — compile a material against the shader ABI in
342
+ /// `native/shared/shaders/material_abi.wgsl`. Source may `#include`
343
+ /// the header and common helpers. Returns a handle (>0 on success, 0
344
+ /// on compile failure — errors log to stderr).
345
+ export function compileMaterial(wgslSource: string): number {
346
+ return bloom_compile_material(wgslSource as any);
347
+ }
348
+
349
+ /// Material profile — matches `FragmentProfile` in the engine's
350
+ /// material_pipeline. Opaque writes the full 4-MRT G-buffer;
351
+ /// Translucent writes a single HDR attachment with alpha blending.
352
+ export const PROFILE_OPAQUE = 0;
353
+ export const PROFILE_TRANSLUCENT = 1;
354
+
355
+ /// Phase 4b — full-control material compile. Pass PROFILE_* and
356
+ /// BUCKET_* constants. `readsScene` enables the group-4 SceneInputs
357
+ /// binding; required for refraction / shoreline / depth-fade effects.
358
+ /// Phase 4b — compile a refractive material (profile=Translucent,
359
+ /// bucket=Refractive, reads_scene=true). The material's shader gets
360
+ /// the SceneInputs bind group at group 4 and can sample the
361
+ /// pre-translucent scene colour via `scene_color_tex`.
362
+ export function compileRefractiveMaterial(wgslSource: string): number {
363
+ return bloom_compile_material_refractive(wgslSource as any);
364
+ }
365
+
366
+ /// Compile a transparent material (profile=Translucent, bucket=
367
+ /// Transparent, reads_scene=false). Alpha-blended, sorted back-to-
368
+ /// front. No scene-colour sampling.
369
+ export function compileTransparentMaterial(wgslSource: string): number {
370
+ return bloom_compile_material_transparent(wgslSource as any);
371
+ }
372
+
373
+ /// Compile an additive material (profile=Translucent, bucket=
374
+ /// Additive, reads_scene=false). Order-independent — use for
375
+ /// particle flares, weapon glows, spell effects.
376
+ export function compileAdditiveMaterial(wgslSource: string): number {
377
+ return bloom_compile_material_additive(wgslSource as any);
378
+ }
379
+
380
+ /// Compile a material into the Cutout bucket: writes the full G-buffer
381
+ /// (so it casts/receives sun shadow + SSAO), but the fragment shader
382
+ /// is expected to call `discard` against `material.metal_rough.w`
383
+ /// (`MaterialFactors.alpha_cutoff`) to drop transparent texels. Use
384
+ /// for foliage cards, chain-link fences, leaf silhouettes. Rendered
385
+ /// double-sided (cull_mode=None) so foliage is visible from both
386
+ /// faces.
387
+ export function compileMaterialCutout(wgslSource: string): number {
388
+ return bloom_compile_material_cutout(wgslSource as any);
389
+ }
390
+
391
+ /// EN-001 — compile a material that draws via the instanced pipeline.
392
+ /// Per-instance data is uploaded once via `createInstanceBuffer` and
393
+ /// drawn via `drawMeshWithMaterialInstanced`. The game shader's
394
+ /// VertexInput must declare these locations IN ADDITION to the
395
+ /// standard 0..6 (see `material_abi.wgsl` for the canonical layout):
396
+ ///
397
+ /// @location(7) instance_pos: vec3<f32>
398
+ /// @location(8) instance_rot_y: f32
399
+ /// @location(9) instance_scale: f32
400
+ /// @location(10) instance_tint: vec4<f32>
401
+ ///
402
+ /// The pipeline lives in the Opaque bucket (cuts opaque profile +
403
+ /// no scene-color reads). For the shooter's grass/foliage workload
404
+ /// this is the right default; future work can extend to other buckets.
405
+ export function compileMaterialInstanced(wgslSource: string): number {
406
+ return bloom_compile_material_instanced(wgslSource as any);
407
+ }
408
+
409
+ declare function bloom_compile_material_instanced_bucket(src: number, bucket: number, readsScene: number): number;
410
+
411
+ /// Material bucket — the wire values `bloom_compile_material_instanced_bucket`
412
+ /// decodes (see `Renderer::compile_material_instanced_bucket`). These are the
413
+ /// FFI's own numbering, NOT the discriminants of the Rust `Bucket` enum, and
414
+ /// they only cover the buckets the instanced compile can reach: Refractive has
415
+ /// no wire value here — use `compileRefractiveMaterial` for that.
416
+ ///
417
+ /// A second, conflicting BUCKET_* block used to sit further up this file with
418
+ /// Phase 4b's numbering (TRANSPARENT=1, REFRACTIVE=2, ADDITIVE=3, CUTOUT=4).
419
+ /// Two `export const`s of the same name made Perry emit duplicate LLVM
420
+ /// definitions, so clang rejected this whole module and every game importing
421
+ /// `bloom/models` failed to link against empty `_perry_init_*` stubs.
422
+ export const BUCKET_OPAQUE = 0;
423
+ export const BUCKET_CUTOUT = 1;
424
+ export const BUCKET_ADDITIVE = 2;
425
+ export const BUCKET_TRANSPARENT = 3;
426
+
427
+ /// EN-026/027 — instanced compile into a chosen bucket. `compileMaterialInstanced`
428
+ /// is opaque-only, which suits grass and suits nothing that blends: particles
429
+ /// want BUCKET_ADDITIVE, decals want BUCKET_CUTOUT (alpha-tested against the
430
+ /// atlas so they still write depth and receive shadow).
431
+ ///
432
+ /// Set `readsScene` if the shader samples `scene_color_tex` / `scene_depth_tex`
433
+ /// — soft particles need it, and WITHOUT it the scene bind group is simply
434
+ /// absent from the pipeline layout and the shader fails validation when the
435
+ /// pipeline is created (not when it is written), which is a confusing way to
436
+ /// find out.
437
+ export function compileMaterialInstancedBucket(
438
+ wgslSource: string, bucket: number, readsScene: boolean = false,
439
+ ): number {
440
+ return bloom_compile_material_instanced_bucket(wgslSource as any, bucket, readsScene ? 1 : 0);
441
+ }
442
+
443
+ /// EN-001 — upload a flat per-instance buffer to the GPU. `data` is
444
+ /// laid out as 9 floats per instance:
445
+ /// [pos.x, pos.y, pos.z, rot_y, scale, tint.r, tint.g, tint.b, tint.a]
446
+ /// × instanceCount. Returns a handle to use with
447
+ /// `drawMeshWithMaterialInstanced`. The buffer is persistent across
448
+ /// frames; call `destroyInstanceBuffer` when the data is no longer
449
+ /// needed.
450
+ ///
451
+ /// IMPORTANT: pass `instanceCount` derived from a literal-init array
452
+ /// length OR from a manually-tracked counter. Don't compute
453
+ /// `data.length / 9` if `data` was built via `.push()` — Perry's
454
+ /// `.length` reports the literal-init size, not the post-push count
455
+ /// (a Perry codegen bug — see `feedback_perry_array_push.md`).
456
+ export function createInstanceBuffer(data: number[], instanceCount: number): number {
457
+ // Perry 0.5.x rejects JS arrays passed into i64 pointer params, so the
458
+ // instance data goes through the all-f64 mesh scratch instead (same fix
459
+ // as createMesh). 9 floats per instance; instance buffers are built once
460
+ // at startup, so the per-float FFI calls are a one-time cost.
461
+ bloom_mesh_scratch_reset();
462
+ const n = instanceCount * 9;
463
+ for (let i = 0; i < n; i++) bloom_mesh_scratch_push_f32(data[i]);
464
+ return bloom_create_instance_buffer_scratch(instanceCount);
465
+ }
466
+
467
+ /// EN-001 — draw `mesh` (a single primitive at `meshIdx`)
468
+ /// `instanceCount` times using `material`'s instanced pipeline. The
469
+ /// instance buffer is bound at vertex slot 1 and the engine emits a
470
+ /// single draw_indexed call. Per-draw model/MVP are identity / current
471
+ /// camera VP — per-instance pos/rot_y/scale dominate from the buffer.
472
+ export function drawMeshWithMaterialInstanced(
473
+ material: number, mesh: Model, meshIdx: number,
474
+ instanceBuffer: number, instanceCount: number,
475
+ ): void {
476
+ bloom_submit_material_draw_instanced(
477
+ material, mesh.handle, meshIdx, instanceBuffer, instanceCount,
478
+ );
479
+ }
480
+
481
+ /// EN-001 — release the GPU memory backing an instance buffer
482
+ /// returned by `createInstanceBuffer`. Safe to call with handle 0
483
+ /// (no-op).
484
+ export function destroyInstanceBuffer(handle: number): void {
485
+ bloom_destroy_instance_buffer(handle);
486
+ }
487
+
488
+ /// EN-011 — create a planar reflection probe. Each frame, the engine
489
+ /// renders the world (minus excluded materials) from a mirror camera
490
+ /// across the given plane into an HDR RT, bound at `@group(2)
491
+ /// @binding(12)` on materials that opt in via
492
+ /// `setMaterialReflectionProbe`.
493
+ ///
494
+ /// Use for water surfaces where the static `env_tex` sky reflection
495
+ /// isn't enough — the planar reflection captures the actual scene
496
+ /// geometry (trees, bridges) reflected on the water plane, with
497
+ /// wave-normal wobble applied by the material's WGSL.
498
+ ///
499
+ /// Arguments:
500
+ /// - `planeY` — world-space Y offset of the reflective plane
501
+ /// - `normalX/Y/Z` — plane normal (typically (0,1,0) for water)
502
+ /// - `resolution` — square texture side in pixels; pass 0 to
503
+ /// default to half the swapchain width
504
+ ///
505
+ /// Returns a 1-based probe handle (0 on failure).
506
+ ///
507
+ /// V1 limits: one probe per plane, repainted every frame, hardcoded
508
+ /// exclude list (materials linked to a probe never appear in their
509
+ /// own reflection — the water plane doesn't show up in itself).
510
+ /// Future: cull-mode flip for correct front-face winding (V1 leaves
511
+ /// pipelines' compiled cull mode in place; the artifact is mostly
512
+ /// hidden for typical horizontal-water-from-above viewpoints).
513
+ ///
514
+ /// WGSL pattern in a water material:
515
+ /// ```wgsl
516
+ /// let screen_uv = clip_position.xy / vec2<f32>(frame.screen_resolution);
517
+ /// let perturb = wave_normal.xz * 0.05;
518
+ /// let refl = textureSample(planar_reflection_tex,
519
+ /// planar_reflection_samp,
520
+ /// screen_uv + perturb).rgb;
521
+ /// ```
522
+ export function createPlanarReflection(
523
+ planeY: number,
524
+ normalX: number, normalY: number, normalZ: number,
525
+ resolution: number,
526
+ ): number {
527
+ return bloom_create_planar_reflection(planeY, normalX, normalY, normalZ, resolution);
528
+ }
529
+
530
+ /// EN-011 — link `material` to a planar reflection probe (handle
531
+ /// returned by `createPlanarReflection`). The probe's RT is bound at
532
+ /// `@group(2) @binding(12)` on subsequent draws. Pass `probe = 0` to
533
+ /// unlink and revert to the default 1×1 black texture.
534
+ ///
535
+ /// Materials linked to any probe are automatically excluded from
536
+ /// every probe's render — the water surface doesn't reflect itself.
537
+ export function setMaterialReflectionProbe(material: number, probe: number): void {
538
+ bloom_set_material_reflection_probe(material, probe);
539
+ }
540
+
541
+ /// Whether this material's draws render into planar-reflection probes
542
+ /// (default true). Turn off for content that is sub-pixel at probe
543
+ /// resolution — e.g. an instanced grass field in a 512-px water probe —
544
+ /// where it costs full vertex + raster work and contributes nothing
545
+ /// resolvable to the reflection.
546
+ export function setMaterialProbeVisible(material: number, visible: boolean): void {
547
+ bloom_set_material_probe_visible(material, visible ? 1 : 0);
548
+ }
549
+
550
+ /// EN-014 — slot indices for `setMaterialTextureArray`.
551
+ export const TEXTURE_ARRAY_ALBEDO = 0;
552
+ export const TEXTURE_ARRAY_NORMAL = 1;
553
+ export const TEXTURE_ARRAY_MR = 2;
554
+
555
+ /// EN-014 — create a 2D texture array from a flat RGBA8 byte buffer.
556
+ /// All `layerCount` layers must share the same `width × height` (wgpu
557
+ /// requires uniform extent for D2Array). The buffer holds each layer
558
+ /// back-to-back: byte[0..(W·H·4)] = layer 0, byte[W·H·4..2·W·H·4] =
559
+ /// layer 1, and so on. Layer count is capped at 16 in V1.
560
+ ///
561
+ /// Returns a 1-based handle (0 on failure: zero count, zero extent,
562
+ /// or short buffer). Pair with `setMaterialTextureArray` to bind to
563
+ /// one of three slots (albedo / normal / MR) on a terrain material;
564
+ /// the WGSL fragment samples the array via:
565
+ /// ```wgsl
566
+ /// let albedo = textureSample(albedo_array, albedo_array_samp,
567
+ /// uv_world_xz, layer_idx);
568
+ /// ```
569
+ ///
570
+ /// IMPORTANT: pass `dataLen` and `layerCount` derived from manually-
571
+ /// tracked counters — NOT `bytes.length` if `bytes` was built via
572
+ /// `.push()` — Perry's `.length` reports the literal-init size, not
573
+ /// the post-push count. (See `feedback_perry_array_push.md`.)
574
+ export function createTextureArray(
575
+ bytes: number[], dataLen: number,
576
+ width: number, height: number, layerCount: number,
577
+ ): number {
578
+ // Routes through the all-f64 mesh scratch (like createTextureArrayFromTexels):
579
+ // the raw *const u8 FFI is uncallable from Perry (number[] into an i64 pointer
580
+ // param throws). `bytes` is RGBA8 back-to-back, so pack each 4 bytes into one
581
+ // u32 the scratch consumer expects. SRGB / 1 mip to match the V1 semantics.
582
+ bloom_mesh_scratch_reset();
583
+ const texelCount = dataLen / 4;
584
+ for (let i = 0; i < texelCount; i = i + 1) {
585
+ const b = i * 4;
586
+ bloom_mesh_scratch_push_u32(
587
+ bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24),
588
+ );
589
+ }
590
+ return bloom_create_texture_array_scratch(
591
+ width, height, layerCount, TEX_ARRAY_FORMAT_SRGB, 1,
592
+ );
593
+ }
594
+
595
+ /// EN-014 V2 — texture-array pixel format codes for `createTextureArrayEx`.
596
+ /// `TEX_ARRAY_FORMAT_SRGB` (0) → Rgba8UnormSrgb (albedo / colour textures)
597
+ /// `TEX_ARRAY_FORMAT_LINEAR` (1) → Rgba8Unorm (normal / MR / data textures —
598
+ /// mandatory for normal maps so the GPU doesn't sRGB-decode the encoded
599
+ /// data and silently corrupt the channels).
600
+ export const TEX_ARRAY_FORMAT_SRGB: number = 0;
601
+ export const TEX_ARRAY_FORMAT_LINEAR: number = 1;
602
+
603
+ /// EN-014 V2 — create a texture array with explicit format + mip control.
604
+ ///
605
+ /// `format`:
606
+ /// `TEX_ARRAY_FORMAT_SRGB` (0) → Rgba8UnormSrgb (albedo / colour)
607
+ /// `TEX_ARRAY_FORMAT_LINEAR` (1) → Rgba8Unorm (normal / MR / data)
608
+ ///
609
+ /// `mipLevels`:
610
+ /// `1` → no mips (matches V1 `createTextureArray`; data is just mip 0)
611
+ /// `0` → auto-generate `floor(log2(max(w,h))) + 1` levels, filled by
612
+ /// point-downsample copies. V2.5 follow-up will upgrade to a
613
+ /// render-pass box filter for higher-quality minification.
614
+ /// `N` (N > 1) → not yet supported in V2; treated as auto-generate.
615
+ ///
616
+ /// Backwards compatible: `createTextureArray` (no Ex) stays available
617
+ /// and is equivalent to `createTextureArrayEx(.., TEX_ARRAY_FORMAT_SRGB, 1)`.
618
+ export function createTextureArrayEx(
619
+ bytes: number[], dataLen: number,
620
+ width: number, height: number, layerCount: number,
621
+ format: number, mipLevels: number,
622
+ ): number {
623
+ // Same scratch reroute as createTextureArray, with explicit format + mips.
624
+ bloom_mesh_scratch_reset();
625
+ const texelCount = dataLen / 4;
626
+ for (let i = 0; i < texelCount; i = i + 1) {
627
+ const b = i * 4;
628
+ bloom_mesh_scratch_push_u32(
629
+ bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24),
630
+ );
631
+ }
632
+ return bloom_create_texture_array_scratch(width, height, layerCount, format, mipLevels);
633
+ }
634
+
635
+ declare function bloom_create_texture_array_scratch(
636
+ width: number, height: number, layerCount: number, format: number, mipLevels: number,
637
+ ): number;
638
+
639
+ /// EN-049 — build a texture array from data you computed, not from files.
640
+ ///
641
+ /// `createTextureArray`/`createTextureArrayEx` originally took a `*const u8`,
642
+ /// which the manifest must declare `i64` — and Perry cannot pass a `number[]`
643
+ /// to an i64 param ("Expected safe integer for native i64 parameter"). They now
644
+ /// reroute through this same scratch path internally (repacking their RGBA8
645
+ /// bytes into packed u32s), so they are callable again; this entry point is the
646
+ /// direct one when your data is already packed u32-per-texel — e.g. a terrain
647
+ /// splat map computed at load out of the world file, with no file to point at.
648
+ ///
649
+ /// So the payload goes through the mesh scratch buffer, exactly as
650
+ /// `updateSceneNodeGeometry` already does for vertices. `texels` is one PACKED
651
+ /// u32 per texel — `r | g<<8 | b<<16 | a<<24`, each channel 0..255 — so a 128²
652
+ /// map is 16,384 FFI calls, not 65,536. Layers are back-to-back, layer 0 first.
653
+ ///
654
+ /// Load-time only. It is linear in the texel count and crosses the FFI once per
655
+ /// texel; do not put it on a frame path.
656
+ export function createTextureArrayFromTexels(
657
+ texels: number[], texelCount: number,
658
+ width: number, height: number, layerCount: number,
659
+ format: number = TEX_ARRAY_FORMAT_LINEAR, mipLevels: number = 1,
660
+ ): number {
661
+ bloom_mesh_scratch_reset();
662
+ for (let i = 0; i < texelCount; i = i + 1) bloom_mesh_scratch_push_u32(texels[i]);
663
+ return bloom_create_texture_array_scratch(width, height, layerCount, format, mipLevels);
664
+ }
665
+
666
+ declare function bloom_create_texture_array_from_files(paths: number, format: number, mipLevels: number): number;
667
+
668
+ /// EN-014 V3 — build a texture array by naming the files, letting the engine
669
+ /// decode them.
670
+ ///
671
+ /// Prefer this to `createTextureArray`: that one asks the caller to marshal
672
+ /// every texel across the FFI (a 6-layer 256² array is 1.5 M numbers), which is
673
+ /// both slow and exactly the call shape Perry's array bridge handles worst.
674
+ /// Here the only thing crossing is a path list, parsed once at load — never on
675
+ /// a frame path (perry-quirks #5).
676
+ ///
677
+ /// All layers must share dimensions; the first file's size wins and mismatched
678
+ /// ones are skipped with a warning. Layer index = position in the list, so the
679
+ /// ORDER IS AN ABI — shaders index by it. Returns 0 if nothing decoded.
680
+ ///
681
+ /// Not available on web (no filesystem in wasm); use `createTextureArrayEx`
682
+ /// there.
683
+ export function createTextureArrayFromFiles(
684
+ paths: string[],
685
+ format: number = TEX_ARRAY_FORMAT_SRGB,
686
+ mipLevels: number = 1,
687
+ ): number {
688
+ // Join rather than pass an array: one string is one pointer, and the engine
689
+ // splits it. Building the joined string with an explicit loop keeps clear of
690
+ // Perry's `.push()`/`.length` foot-gun.
691
+ let joined = '';
692
+ for (let i = 0; i < paths.length; i = i + 1) {
693
+ if (i > 0) joined = joined + ',';
694
+ joined = joined + paths[i];
695
+ }
696
+ return bloom_create_texture_array_from_files(joined as any, format, mipLevels);
697
+ }
698
+
699
+ /// EN-014 — link a texture-array handle to a material at one of three
700
+ /// slots: `TEXTURE_ARRAY_ALBEDO` (binding 14), `TEXTURE_ARRAY_NORMAL`
701
+ /// (binding 15), `TEXTURE_ARRAY_MR` (binding 16). Pass `array = 0` to
702
+ /// revert the slot to the engine's 1×1×1 stub.
703
+ ///
704
+ /// Materials don't need to bind every slot — the stub is safe to
705
+ /// sample. A common pattern is to bind only `TEXTURE_ARRAY_ALBEDO`
706
+ /// for a non-PBR splat-mapped terrain.
707
+ export function setMaterialTextureArray(material: number, slot: number, array: number): void {
708
+ bloom_set_material_texture_array(material, slot, array);
709
+ }
710
+
711
+ /// EN-012 — shading-model selectors. Pass to `setMaterialShadingModel`.
712
+ export const SHADING_MODEL_DEFAULT_LIT = 0;
713
+ export const SHADING_MODEL_FOLIAGE = 1;
714
+ export const SHADING_MODEL_SUBSURFACE = 2; // V2 stub — currently behaves as default lit
715
+
716
+ /// EN-012 — switch a material's shading model. The game shader is
717
+ /// responsible for branching on `material.shading_model.x` and calling
718
+ /// either standard PBR or `shade_foliage` (wrap-lambert + transmission)
719
+ /// from `common/pbr.wgsl`. The engine just exposes the slot.
720
+ ///
721
+ /// V1 limitation: SSAO doesn't half-strength on backfaces — the
722
+ /// G-buffer doesn't carry an isFrontFace channel today. Documented as a
723
+ /// follow-up requirement on the EN-012 ticket.
724
+ export function setMaterialShadingModel(material: number, model: number): void {
725
+ bloom_set_material_shading_model(material, model);
726
+ }
727
+
728
+ /// EN-012 — set the foliage shading parameters for a material. Only
729
+ /// takes effect when `shading_model == SHADING_MODEL_FOLIAGE`.
730
+ ///
731
+ /// `transmissionR/G/B`: rgb tint for back-lit foliage (1,1,1 = neutral).
732
+ /// `transmissionAmount`: 0..1 — how much sun bleeds through the leaf.
733
+ /// `wrapFactor`: 0..1 — wrap-lambert intensity. 0 = standard lambert
734
+ /// (back face goes pure black), 1 = light wraps fully around to the
735
+ /// back face.
736
+ export function setMaterialFoliage(
737
+ material: number,
738
+ transmissionR: number, transmissionG: number, transmissionB: number,
739
+ transmissionAmount: number, wrapFactor: number,
740
+ ): void {
741
+ bloom_set_material_foliage(material, transmissionR, transmissionG, transmissionB, transmissionAmount, wrapFactor);
742
+ }
743
+
744
+ /**
745
+ * Phase 6 — file-backed material compile with hot reload. Reads the
746
+ * WGSL from disk, compiles it, and registers the path with the
747
+ * engine's hot-reload watcher. Editing the file while the game is
748
+ * running re-compiles the pipeline and replaces it in place — the
749
+ * material handle stays valid; existing draws automatically pick up
750
+ * the new shader on the next frame.
751
+ *
752
+ * Failures during reload (parse error, validation) keep the previous
753
+ * pipeline running; the error is logged but doesn't crash the game.
754
+ *
755
+ * `bucket` selects the same presets as the dedicated compile* APIs:
756
+ * 'opaque' | 'cutout' | 'transparent' | 'refractive' | 'additive'
757
+ */
758
+ export function compileMaterialFromFile(
759
+ path: string,
760
+ bucket: 'opaque' | 'cutout' | 'transparent' | 'refractive' | 'additive',
761
+ ): number {
762
+ const kind = bucket === 'opaque' ? 0
763
+ : bucket === 'transparent' ? 1
764
+ : bucket === 'refractive' ? 2
765
+ : bucket === 'additive' ? 3
766
+ : 4; // cutout
767
+ return bloom_compile_material_from_file(path as any, kind);
768
+ }
769
+
770
+ /**
771
+ * Phase 5 — material descriptor loader. Compiles a material from
772
+ * a typed descriptor in one call:
773
+ * - resolves `shader` via `compileMaterialFromFile` (gets hot
774
+ * reload)
775
+ * - applies `params` via `setMaterialParams` if provided
776
+ *
777
+ * Returns the material handle (0 on failure).
778
+ *
779
+ * Why a typed object instead of a JSON string: Perry's runtime
780
+ * `JSON.parse` mishandles array `.length`, so a JSON-string variant
781
+ * would force every game to roll its own parser. Games that DO
782
+ * want JSON-on-disk should preprocess at build time (see
783
+ * `shooter/tools/build-world.ts` for the pattern) and emit a TS
784
+ * module that calls `loadMaterial` with literal descriptors.
785
+ */
786
+ export interface MaterialDesc {
787
+ shader: string;
788
+ bucket: 'opaque' | 'cutout' | 'transparent' | 'refractive' | 'additive';
789
+ params?: number[];
790
+ }
791
+
792
+ export function loadMaterial(desc: MaterialDesc): number {
793
+ const handle = compileMaterialFromFile(desc.shader, desc.bucket);
794
+ if (handle > 0 && desc.params) {
795
+ // Params go through the all-f64 mesh scratch, same as setMaterialParams:
796
+ // the raw pointer variant (bloom_set_material_params) is not in the FFI
797
+ // manifest, so Perry silently no-ops it, and it also hits the
798
+ // number[]-into-i64-pointer bug. The _scratch path avoids both.
799
+ const p = desc.params;
800
+ if (p.length > 0) {
801
+ bloom_mesh_scratch_reset();
802
+ for (let i = 0; i < p.length; i++) bloom_mesh_scratch_push_f32(p[i]);
803
+ bloom_set_material_params_scratch(handle, p.length);
804
+ }
805
+ }
806
+ return handle;
807
+ }
808
+
809
+ /// Draw a mesh with a material. `mesh` must be a Model created via
810
+ /// `createMesh` or `loadModel`. Transform is a position + uniform
811
+ /// scale; tint is an RGBA color multiplied into PerDraw.model_tint.
812
+ /// `meshIdx` selects which primitive of a multi-mesh GLB to draw —
813
+ /// default 0 for single-mesh models. Loop 0..mesh.meshCount when
814
+ /// rendering a multi-primitive GLB through a custom material.
815
+ export function drawMeshWithMaterial(
816
+ material: number, mesh: Model,
817
+ position: Vec3, scale: number, tint: Color,
818
+ meshIdx: number = 0,
819
+ ): void {
820
+ bloom_draw_material(
821
+ material, mesh.handle, meshIdx,
822
+ position.x, position.y, position.z, scale,
823
+ tint.r, tint.g, tint.b, tint.a,
824
+ );
825
+ }
826
+
827
+ /// Draw every primitive of a multi-mesh GLB with the same material.
828
+ /// Convenience wrapper around `drawMeshWithMaterial` that loops
829
+ /// 0..mesh.meshCount internally.
830
+ export function drawModelWithMaterial(
831
+ material: number, mesh: Model,
832
+ position: Vec3, scale: number, tint: Color,
833
+ ): void {
834
+ for (let i = 0; i < mesh.meshCount; i = i + 1) {
835
+ bloom_draw_material(
836
+ material, mesh.handle, i,
837
+ position.x, position.y, position.z, scale,
838
+ tint.r, tint.g, tint.b, tint.a,
839
+ );
840
+ }
841
+ }
842
+
843
+ export function loadModelAnimation(path: string): number {
844
+ return bloom_load_model_animation(path as any);
845
+ }
846
+
847
+ /// EN-055 — a fresh animation INSTANCE (own mixer, own joint matrices) over
848
+ /// an already-loaded clip set. The parsed keyframe data is shared, so a crowd
849
+ /// of N characters costs one GLB parse + N cheap instances instead of N
850
+ /// parses. Returns 0 if `src` is not a live animation handle.
851
+ export function instantiateAnimation(src: number): number {
852
+ return bloom_instantiate_animation(src);
853
+ }
854
+
855
+ export function updateModelAnimation(handle: number, animIndex: number, time: number, scale: number, px: number, py: number, pz: number, rotY: number): void {
856
+ bloom_update_model_animation(handle, animIndex, time, scale, px, py, pz, rotY);
857
+ }
858
+
859
+ // ---- EN-028: animation mixer -----------------------------------------------
860
+ // The single-clip `updateModelAnimation` above stays for callers that drive
861
+ // their own clip clock. The mixer below owns the clock instead, which is what
862
+ // makes crossfades possible at all: a fade needs the *outgoing* clip to keep
863
+ // advancing, and a caller that only passes one time value cannot express that.
864
+ //
865
+ // Typical use, per model per frame:
866
+ // animPlay(h, moving ? CLIP_WALK : CLIP_IDLE, 0.15); // idempotent
867
+ // animSetLayer(h, attacking ? CLIP_ATTACK : -1, 1, spineJoint);
868
+ // animUpdate(h, dt, scale, x, y, z, yaw);
869
+
870
+ declare function bloom_anim_play(handle: number, clip: number, fade: number, speed: number, looping: number): void;
871
+ declare function bloom_anim_set_layer(handle: number, clip: number, weight: number, maskRoot: number, speed: number, looping: number): void;
872
+ declare function bloom_anim_set_root_motion(handle: number, on: number): void;
873
+ declare function bloom_anim_update(handle: number, dt: number, scale: number, px: number, py: number, pz: number, rotY: number): void;
874
+ declare function bloom_anim_finished(handle: number): number;
875
+ declare function bloom_anim_clip_duration(handle: number, clip: number): number;
876
+ declare function bloom_anim_root_delta(handle: number, axis: number): number;
877
+ declare function bloom_model_find_joint(handle: number, name: number): number;
878
+ declare function bloom_model_joint_world(handle: number, joint: number, comp: number): number;
879
+
880
+ /// Transition the base track to `clip` over `fade` seconds. Safe to call every
881
+ /// frame with the clip you *want* — re-requesting the clip already playing is
882
+ /// a no-op, so callers don't have to track edges.
883
+ export function animPlay(handle: number, clip: number, fade: number = 0.15, speed: number = 1.0, looping: boolean = true): void {
884
+ bloom_anim_play(handle, clip, fade, speed, looping ? 1 : 0);
885
+ }
886
+
887
+ /// Drive the subtree below `maskRoot` (a joint index — see `findJoint`) from a
888
+ /// second clip at `weight`. Pass clip = -1 to switch the layer off. This is how
889
+ /// a character attacks while still walking.
890
+ export function animSetLayer(handle: number, clip: number, weight: number, maskRoot: number, speed: number = 1.0, looping: boolean = false): void {
891
+ bloom_anim_set_layer(handle, clip, weight, maskRoot, speed, looping ? 1 : 0);
892
+ }
893
+
894
+ /// Opt in to authored root motion. Off by default: with it on, the pose stops
895
+ /// carrying the root translation and you must feed `animRootDelta` to your
896
+ /// character controller, or the model animates in place.
897
+ export function animSetRootMotion(handle: number, on: boolean): void {
898
+ bloom_anim_set_root_motion(handle, on ? 1 : 0);
899
+ }
900
+
901
+ /// Advance all clocks on this model and upload the blended pose. One call per
902
+ /// model per frame, in place of `updateModelAnimation`.
903
+ export function animUpdate(handle: number, dt: number, scale: number, px: number, py: number, pz: number, rotY: number): void {
904
+ bloom_anim_update(handle, dt, scale, px, py, pz, rotY);
905
+ }
906
+
907
+ /// True once a non-looping clip has run past its end — the death/attack
908
+ /// one-shot query.
909
+ export function animFinished(handle: number): boolean {
910
+ return bloom_anim_finished(handle) !== 0;
911
+ }
912
+
913
+ export function animClipDuration(handle: number, clip: number): number {
914
+ return bloom_anim_clip_duration(handle, clip);
915
+ }
916
+
917
+ /// Root-motion translation applied by the last `animUpdate`, in model space.
918
+ export function animRootDelta(handle: number, axis: number): number {
919
+ return bloom_anim_root_delta(handle, axis);
920
+ }
921
+
922
+ // ---- EN-033: bone sockets ---------------------------------------------------
923
+
924
+ /// Joint index by name (exact, else case-insensitive substring). Call once at
925
+ /// load and cache — it parses a string, which must never happen per-frame
926
+ /// (perry-quirks #5). Returns -1 if not found.
927
+ export function findJoint(handle: number, name: string): number {
928
+ return bloom_model_find_joint(handle, name as any);
929
+ }
930
+
931
+ /// One component of a joint's model-space 4x4 (column-major, 0..15).
932
+ /// Translation is 12/13/14. Model-space, not world: apply the same scale /
933
+ /// position / yaw you passed to `animUpdate` to place it in the world.
934
+ export function jointWorld(handle: number, joint: number, comp: number): number {
935
+ return bloom_model_joint_world(handle, joint, comp);
936
+ }
937
+
938
+ // Upload a mesh via the scratch buffer (array-free). Perry 0.5.1171 rejects
939
+ // passing a `number[]` to a native `i64` pointer param (strict safe-integer
940
+ // check), so we push each vertex float + index scalar through the all-f64
941
+ // scratch FFI and then build. One-time init cost; fine for static meshes.
942
+ function uploadMeshScratch(
943
+ vertices: number[], vertexCount: number,
944
+ indices: number[], indexCount: number,
945
+ ): Model {
946
+ bloom_mesh_scratch_reset();
947
+ const vfloats = vertexCount * 12;
948
+ for (let i = 0; i < vfloats; i++) bloom_mesh_scratch_push_f32(vertices[i]);
949
+ for (let i = 0; i < indexCount; i++) bloom_mesh_scratch_push_u32(indices[i]);
950
+ const handle = bloom_create_mesh_scratch(vertexCount, indexCount);
951
+ return makeModel(handle, 1, 1);
952
+ }
953
+
954
+ export function createMesh(vertices: number[], indices: number[]): Model {
955
+ // vertices: flat array of [x,y,z, nx,ny,nz, r,g,b,a, u,v] per vertex (12 floats each)
956
+ // NOTE: `vertices.length` / `indices.length` are correct only for arrays
957
+ // built via literals or `new Array(N)` + index assignment. Arrays built via
958
+ // `.push()` report the literal-init size (a Perry codegen bug) — use
959
+ // createMeshExplicit and pass the counts manually for those.
960
+ return uploadMeshScratch(vertices, vertices.length / 12, indices, indices.length);
961
+ }
962
+
963
+ /// Explicit-count variant of createMesh — pass `vertexCount` (number
964
+ /// of complete 12-float vertex records) and `indexCount` directly.
965
+ /// Use this when the underlying `number[]` arrays were built with
966
+ /// `.push()`, since Perry's `.length` doesn't reflect post-push size.
967
+ export function createMeshExplicit(
968
+ vertices: number[], vertexCount: number,
969
+ indices: number[], indexCount: number,
970
+ ): Model {
971
+ return uploadMeshScratch(vertices, vertexCount, indices, indexCount);
972
+ }
973
+
974
+ export function setAmbientLight(color: Color, intensity: number): void {
975
+ bloom_set_ambient_light(color.r, color.g, color.b, intensity);
976
+ }
977
+
978
+ export function setDirectionalLight(direction: Vec3, color: Color, intensity: number): void {
979
+ bloom_set_directional_light(direction.x, direction.y, direction.z, color.r, color.g, color.b, intensity);
980
+ }
981
+
982
+ // EN-005 — Hillaire 2020 procedural sky.
983
+ //
984
+ // `setProceduralSky(true)` swaps the static HDR-panorama background
985
+ // for a physics-based atmosphere driven by Rayleigh + Mie scattering.
986
+ // `setSunDirection` then steers the sun: the sky-view LUT re-bakes
987
+ // on the next frame and the sun disk + transmittance update together.
988
+ //
989
+ // `setProceduralSky(false)` returns to the panorama path; the
990
+ // existing `loadEnvironment` / `setEnvironmentIntensity` flow is
991
+ // untouched.
992
+
993
+ export interface ProceduralSkyOptions {
994
+ /** Rayleigh density multiplier. 1.0 = Earth standard. Higher = bluer
995
+ * thicker air; lower = thinner. */
996
+ rayleighDensity?: number;
997
+ /** Mie density multiplier. Drives haze + sun-glow size. 1.0 = Earth
998
+ * standard, raise for hazy/dusty scenes. */
999
+ mieDensity?: number;
1000
+ /** Ground albedo (0..1). Affects how much light bounces back up
1001
+ * into the lower atmosphere. 0.1 = soil/grass, 0.9 = snow. */
1002
+ groundAlbedo?: number;
1003
+ }
1004
+
1005
+ export function setProceduralSky(enabled: boolean, opts?: ProceduralSkyOptions): void {
1006
+ const rd = opts?.rayleighDensity ?? 1.0;
1007
+ const md = opts?.mieDensity ?? 1.0;
1008
+ const ga = opts?.groundAlbedo ?? 0.1;
1009
+ bloom_set_procedural_sky(enabled ? 1 : 0, rd, md, ga);
1010
+ }
1011
+
1012
+ export function setSunDirection(direction: Vec3, intensity: number = 1.0): void {
1013
+ bloom_set_sun_direction(direction.x, direction.y, direction.z, intensity);
1014
+ }
1015
+
1016
+ declare function bloom_set_joint_test(joint: number, angle: number): void;
1017
+ export function setJointTest(joint: number, angle: number): void {
1018
+ bloom_set_joint_test(joint, angle);
1019
+ }
1020
+
1021
+ // Async / threaded loading
1022
+
1023
+ declare function bloom_stage_model(path: number): number;
1024
+ declare function bloom_commit_model(handle: number): number;
1025
+
1026
+ export async function loadModelAsync(path: string): Promise<Model> {
1027
+ const pathLower = (path as string).toLowerCase();
1028
+ if (pathLower.endsWith('.obj')) {
1029
+ const parsed = await spawn(() => {
1030
+ const text: string = bloom_read_file(path as any) as any;
1031
+ return text ? parseOBJ(text) : null;
1032
+ });
1033
+ if (parsed) {
1034
+ const handle = bloom_create_mesh(
1035
+ parsed.vertices as any,
1036
+ parsed.vertices.length / 12,
1037
+ parsed.indices as any,
1038
+ parsed.indices.length,
1039
+ );
1040
+ return makeModel(handle, 1, 1);
1041
+ }
1042
+ return makeModel(0);
1043
+ }
1044
+ const stagingHandle = await spawn(() => bloom_stage_model(path as any));
1045
+ const handle = bloom_commit_model(stagingHandle);
1046
+ return makeModel(handle);
1047
+ }
1048
+
1049
+ export function stageModels(paths: string[]): number[] {
1050
+ return parallelMap(paths, (path: string) => bloom_stage_model(path as any));
1051
+ }
1052
+
1053
+ /// Sequential twin of stageModels for hosts where worker threads have no
1054
+ /// engine FFI (web: each Perry worker is its own WASM instance, but the
1055
+ /// engine lives on the main thread, so a staged load from a worker reaches
1056
+ /// nothing). Same tickets, same commitModel() contract — just decoded on
1057
+ /// the calling thread.
1058
+ export function stageModelsSync(paths: string[]): number[] {
1059
+ const out = new Array<number>(paths.length);
1060
+ for (let i = 0; i < paths.length; i++) {
1061
+ out[i] = bloom_stage_model(paths[i] as any);
1062
+ }
1063
+ return out;
1064
+ }
1065
+
1066
+ export function commitModel(stagingHandle: number): Model {
1067
+ const handle = bloom_commit_model(stagingHandle);
1068
+ return makeModel(handle);
1069
+ }
1070
+
1071
+ // ---------------------------------------------------------------------
1072
+ // EN-015 V1 — Octahedral imposter / billboard helpers.
1073
+ //
1074
+ // V1 ships the runtime piece only: the WGSL helper library
1075
+ // (`common/imposter.wgsl`) plus this TS-side LOD selector +
1076
+ // `drawImposterAtlas` wrapper. Bake tooling is a follow-up — V1 expects
1077
+ // games to bake atlases externally (Blender's ScreenSpace add-on,
1078
+ // Unity Tree Creator, etc.) until the engine bake tool ships.
1079
+ //
1080
+ // The atlas convention is fixed at 8×8 octahedral views (64 cells
1081
+ // total) packed into a single RGBA8 texture.
1082
+ //
1083
+ // Typical game-side imposter material WGSL (drop in your own .wgsl
1084
+ // file and pass to compileMaterial):
1085
+ //
1086
+ // #include "material_abi.wgsl"
1087
+ // #include "common/imposter.wgsl"
1088
+ //
1089
+ // struct VsOut {
1090
+ // @builtin(position) clip_pos: vec4<f32>,
1091
+ // @location(0) view_dir: vec3<f32>,
1092
+ // @location(1) uv: vec2<f32>,
1093
+ // };
1094
+ //
1095
+ // @vertex
1096
+ // fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {
1097
+ // // node.transform[3].xyz holds the per-instance world position;
1098
+ // // node.scale_x is the imposter scale.
1099
+ // let center = node.transform[3].xyz;
1100
+ // let bb = billboard_quad(
1101
+ // center, node.scale_x,
1102
+ // view.camera_pos.xyz, vec3<f32>(0.0, 1.0, 0.0),
1103
+ // vid,
1104
+ // );
1105
+ // var out: VsOut;
1106
+ // out.clip_pos = view.view_proj * vec4<f32>(bb.world_pos, 1.0);
1107
+ // out.view_dir = normalize(view.camera_pos.xyz - center);
1108
+ // out.uv = bb.uv;
1109
+ // return out;
1110
+ // }
1111
+ //
1112
+ // @fragment
1113
+ // fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
1114
+ // let atlas_uv = imposter_atlas_uv(in.view_dir, in.uv);
1115
+ // return textureSample(material_albedo, material_sampler, atlas_uv);
1116
+ // }
1117
+ //
1118
+ // Then at runtime:
1119
+ //
1120
+ // const useImposter = pickImposterLOD(
1121
+ // camera.x, camera.y, camera.z,
1122
+ // tree.x, tree.y, tree.z,
1123
+ // 50.0,
1124
+ // );
1125
+ // if (useImposter) {
1126
+ // drawImposterAtlas(impMaterial, impAtlasMesh, treePos, treeScale);
1127
+ // } else {
1128
+ // drawModel(treeModel, treePos, treeScale, WHITE);
1129
+ // }
1130
+ // ---------------------------------------------------------------------
1131
+
1132
+ /// EN-015 — pick LOD by distance. Returns true if the camera is far
1133
+ /// enough that the imposter should be drawn in place of the full mesh.
1134
+ ///
1135
+ /// Typical `switchDistance` is 40–60 m for trees. To mitigate pop at
1136
+ /// the LOD boundary, layer a small hysteresis band on top from the
1137
+ /// caller (e.g. switch to imposter at 50 m, switch back to mesh at
1138
+ /// 47 m); when TAA is enabled the residual flicker is mostly resolved
1139
+ /// across frames.
1140
+ export function pickImposterLOD(
1141
+ cameraX: number, cameraY: number, cameraZ: number,
1142
+ worldX: number, worldY: number, worldZ: number,
1143
+ switchDistance: number,
1144
+ ): boolean {
1145
+ const dx = cameraX - worldX;
1146
+ const dy = cameraY - worldY;
1147
+ const dz = cameraZ - worldZ;
1148
+ const distSq = dx * dx + dy * dy + dz * dz;
1149
+ const threshSq = switchDistance * switchDistance;
1150
+ return distSq > threshSq;
1151
+ }
1152
+
1153
+ /// EN-015 — draw a billboard quad sampling an imposter atlas.
1154
+ ///
1155
+ /// V1 is a thin wrapper around `drawMeshWithMaterial`: the game
1156
+ /// supplies its own quad mesh (any 4-vertex / 2-triangle plane the
1157
+ /// imposter material's vertex shader will reposition via
1158
+ /// `billboard_quad`). The atlas texture is bound through the standard
1159
+ /// material albedo slot before this call (via the engine's material
1160
+ /// param / texture binding APIs).
1161
+ ///
1162
+ /// Future revs may add a dedicated FFI that submits a hard-coded unit
1163
+ /// quad so games don't need to author a quad mesh; for V1 we keep the
1164
+ /// API surface tight and reuse the existing draw path.
1165
+ export function drawImposterAtlas(
1166
+ material: number, quadMesh: Model,
1167
+ position: Vec3, scale: number,
1168
+ ): void {
1169
+ bloom_draw_material(
1170
+ material, quadMesh.handle, 0,
1171
+ position.x, position.y, position.z, scale,
1172
+ 1.0, 1.0, 1.0, 1.0,
1173
+ );
1174
+ }
1175
+
1176
+ // ---- EN-025: ragdolls -------------------------------------------------------
1177
+ //
1178
+ // A ragdoll is not something you configure; it is something you TRIGGER, once,
1179
+ // at the moment of death, and then forget about. So the API is four calls:
1180
+ // build it, shove it, read the pose, put it away.
1181
+ //
1182
+ // The engine builds the bodies from the skeleton it already has — one capsule
1183
+ // per bone, one limited six-DOF joint per articulation — so there is no ragdoll
1184
+ // asset to author and it works for any skinned model the game loads.
1185
+
1186
+ declare function bloom_ragdoll_create(): number;
1187
+ declare function bloom_ragdoll_activate(rag: number, anim: number, world: number, scale: number, px: number, py: number, pz: number, rotY: number): number;
1188
+ declare function bloom_ragdoll_push(rag: number, dx: number, dy: number, dz: number, impulse: number): void;
1189
+ declare function bloom_ragdoll_update(rag: number, anim: number, dt: number): number;
1190
+ declare function bloom_ragdoll_release(rag: number): void;
1191
+
1192
+ /// Allocate a ragdoll slot. Pool these — one per corpse you intend to have on
1193
+ /// screen at once — and reuse them; a slot is cheap, but the bodies it holds
1194
+ /// are not.
1195
+ export function createRagdoll(): number {
1196
+ return bloom_ragdoll_create();
1197
+ }
1198
+
1199
+ /// Build the bodies from the model's CURRENT pose and let go.
1200
+ ///
1201
+ /// Pass the same `(scale, position, yaw)` you last handed `animUpdate`. That
1202
+ /// transform is the bridge between model space (where skinning lives) and world
1203
+ /// space (where physics lives), and it is FROZEN here — from now on the corpse's
1204
+ /// motion belongs to the bodies, not to the dead thing's old position.
1205
+ ///
1206
+ /// Returns false if the model has no skeleton, or no bones long enough to
1207
+ /// simulate.
1208
+ export function activateRagdoll(
1209
+ rag: number, anim: number, world: number,
1210
+ scale: number, px: number, py: number, pz: number, rotY: number,
1211
+ ): boolean {
1212
+ return bloom_ragdoll_activate(rag, anim, world, scale, px, py, pz, rotY) !== 0;
1213
+ }
1214
+
1215
+ /// The killing blow. Applied across all the bodies, so the corpse is thrown as
1216
+ /// a whole rather than having one limb yanked off.
1217
+ export function pushRagdoll(rag: number, dx: number, dy: number, dz: number, impulse: number): void {
1218
+ bloom_ragdoll_push(rag, dx, dy, dz, impulse);
1219
+ }
1220
+
1221
+ /// Pull the simulated pose back into the model's joint matrices and upload it.
1222
+ /// Call once per frame per active ragdoll, then `drawModel()` as usual — no
1223
+ /// `animUpdate`, because physics owns the pose now. Returns the ragdoll's age in
1224
+ /// seconds, which is what you settle and despawn on.
1225
+ export function updateRagdoll(rag: number, anim: number, dt: number): number {
1226
+ return bloom_ragdoll_update(rag, anim, dt);
1227
+ }
1228
+
1229
+ /// Destroy the bodies and constraints and free the slot for reuse. A pooled
1230
+ /// ragdoll that is never released leaks bodies into the physics world — a slow,
1231
+ /// invisible death.
1232
+ export function releaseRagdoll(rag: number): void {
1233
+ bloom_ragdoll_release(rag);
1234
+ }
1235
+
1236
+ /// Mark a model as foliage, so the wind actually bends it.
1237
+ ///
1238
+ /// `amount` scales the whole effect: ~1.0 for a tree, less for a stiff shrub,
1239
+ /// 0 to turn it off. Direction, strength and rate come from `setWind`.
1240
+ ///
1241
+ /// The wind is hierarchical (trunk bend, branch sway, leaf flutter) and the
1242
+ /// layer weights are derived from where each vertex sits relative to the model
1243
+ /// origin — nothing has to be authored into the mesh. Before this the engine
1244
+ /// swayed alpha-cut materials only, which meant leaf cards fluttered while every
1245
+ /// trunk in the scene stood perfectly rigid.
1246
+ export function setModelFoliageWind(model: Model, amount: number): void {
1247
+ bloom_set_model_foliage_wind(model.handle, amount);
1248
+ }
1249
+
1250
+ /// Let foliage sway in the SHADOW pass too, so a bending tree and its shadow bend
1251
+ /// together and the canopy dapple on the ground actually moves.
1252
+ ///
1253
+ /// Off by default, and not free: a caster that moves every frame cannot reuse the
1254
+ /// cached static shadow depth, so every plant re-renders into every cascade every
1255
+ /// frame. Measure before leaving it on.
1256
+ export function setFoliageShadowMotion(on: boolean): void {
1257
+ bloom_set_foliage_shadow_motion(on ? 1 : 0);
1258
+ }