@bornengine/engine 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,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
|
+
}
|