diff --git a/Cargo.lock b/Cargo.lock index 35b92c9..9c3df2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,7 @@ dependencies = [ "serde-wasm-bindgen", "tracing", "wasm-bindgen", + "wgpu-types", ] [[package]] @@ -171,6 +172,22 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" +dependencies = [ + "anstyle", + "unicode-width 0.2.1", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + [[package]] name = "anyhow" version = "1.0.98" @@ -241,6 +258,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -384,6 +410,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bevy" version = "0.16.1" @@ -495,7 +527,7 @@ dependencies = [ "bitflags 2.9.1", "blake3", "crossbeam-channel", - "derive_more", + "derive_more 1.0.0", "disqualified", "downcast-rs 2.0.1", "either", @@ -535,7 +567,7 @@ dependencies = [ "bevy_math", "bevy_reflect", "bytemuck", - "derive_more", + "derive_more 1.0.0", "encase", "serde", "thiserror 2.0.12", @@ -616,7 +648,7 @@ dependencies = [ "bitflags 2.9.1", "bumpalo", "concurrent-queue", - "derive_more", + "derive_more 1.0.0", "disqualified", "fixedbitset", "indexmap", @@ -760,7 +792,7 @@ dependencies = [ "bevy_platform", "bevy_reflect", "bevy_utils", - "derive_more", + "derive_more 1.0.0", "log", "smol_str", "thiserror 2.0.12", @@ -859,7 +891,7 @@ checksum = "68553e0090fe9c3ba066c65629f636bd58e4ebd9444fdba097b91af6cd3e243f" dependencies = [ "approx", "bevy_reflect", - "derive_more", + "derive_more 1.0.0", "glam", "itertools 0.14.0", "libm", @@ -928,7 +960,7 @@ dependencies = [ "bevy_window", "bitflags 2.9.1", "bytemuck", - "derive_more", + "derive_more 1.0.0", "fixedbitset", "nonmax", "offset-allocator", @@ -997,7 +1029,7 @@ dependencies = [ "bevy_ptr", "bevy_reflect_derive", "bevy_utils", - "derive_more", + "derive_more 1.0.0", "disqualified", "downcast-rs 2.0.1", "erased-serde", @@ -1053,7 +1085,7 @@ dependencies = [ "bitflags 2.9.1", "bytemuck", "codespan-reporting", - "derive_more", + "derive_more 1.0.0", "downcast-rs 2.0.1", "encase", "fixedbitset", @@ -1073,6 +1105,7 @@ dependencies = [ "variadics_please", "wasm-bindgen", "web-sys", + "wesl", "wgpu", ] @@ -1103,7 +1136,7 @@ dependencies = [ "bevy_render", "bevy_transform", "bevy_utils", - "derive_more", + "derive_more 1.0.0", "serde", "thiserror 2.0.12", "uuid", @@ -1160,7 +1193,7 @@ dependencies = [ "bevy_utils", "bitflags 2.9.1", "bytemuck", - "derive_more", + "derive_more 1.0.0", "fixedbitset", "nonmax", "radsort", @@ -1209,7 +1242,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "crossbeam-queue", - "derive_more", + "derive_more 1.0.0", "futures-channel", "futures-lite", "heapless", @@ -1275,7 +1308,7 @@ dependencies = [ "bevy_reflect", "bevy_tasks", "bevy_utils", - "derive_more", + "derive_more 1.0.0", "serde", "thiserror 2.0.12", ] @@ -1316,7 +1349,7 @@ dependencies = [ "bevy_utils", "bevy_window", "bytemuck", - "derive_more", + "derive_more 1.0.0", "nonmax", "smallvec", "taffy", @@ -1663,7 +1696,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ "termcolor", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1755,6 +1788,15 @@ dependencies = [ "const_soft_float", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1929,7 +1971,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -1944,6 +1995,19 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "digest" version = "0.10.7" @@ -2129,6 +2193,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encase" version = "0.10.0" @@ -2614,6 +2687,7 @@ checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", + "num-traits", ] [[package]] @@ -2676,6 +2750,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "1.3.1" @@ -2844,6 +2927,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -2861,6 +2953,37 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set 0.8.0", + "ena", + "itertools 0.14.0", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.8.5", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata 0.4.9", + "rustversion", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2873,6 +2996,79 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[package]] +name = "lexical" +version = "7.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ed980ff02623721dc334b9105150b66d0e1f246a92ab5a2eca0335d54c48f6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b765c31809609075565a70b4b71402281283aeda7ecaf4818ac14a7b2ade8958" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afc668a27f460fb45a81a757b6bf2f43c2d7e30cb5a2dcd3abf294c78d62bd" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629ddff1a914a836fb245616a7888b62903aae58fa771e1d83943035efa0f978" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.172" @@ -2950,6 +3146,40 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "logos" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab6f536c1af4c7cc81edf73da1f8029896e7e1e16a219ef09b184e76a296f3db" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189bbfd0b61330abea797e5e9276408f2edbe4f822d7ad08685d67419aafb34e" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax 0.8.5", + "rustc_version", + "syn", +] + +[[package]] +name = "logos-derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfe8e1a19049ddbfccbd14ac834b215e11b85b90bab0c2dba7c7b92fb5d5cba" +dependencies = [ + "logos-codegen", +] + [[package]] name = "longest-increasing-subsequence" version = "0.1.0" @@ -3627,6 +3857,25 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -3737,6 +3986,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "presser" version = "0.3.1" @@ -4112,6 +4367,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4207,6 +4471,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "send_wrapper" version = "0.6.0" @@ -4276,6 +4546,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -4306,6 +4586,12 @@ dependencies = [ "quote", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "skrifa" version = "0.31.3" @@ -4417,6 +4703,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strum" version = "0.26.3" @@ -4533,6 +4831,16 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "term" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a984c8d058c627faaf5e8e2ed493fa3c51771889196de1016cf9c1c6e90d750" +dependencies = [ + "home", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -4865,6 +5173,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -5182,6 +5496,35 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "wesl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a997b82782eb6019bb7c4de169a4e1eb0b96b59495f64fabc724587ff58378" +dependencies = [ + "annotate-snippets", + "bytes", + "derive_more 2.0.1", + "half", + "itertools 0.14.0", + "num-traits", + "thiserror 2.0.12", + "wesl-macros", + "wgsl-parse", +] + +[[package]] +name = "wesl-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26e545df8611385f6b9fa32d0d840e5bd3e86b4a968ffe31adfcc4743d5811a" +dependencies = [ + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "wgpu" version = "24.0.5" @@ -5292,6 +5635,22 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wgsl-parse" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d2e96b673c61dd68961f7ed0bdec3f1486ba0d6dbd63b52b7429bc82e790b4" +dependencies = [ + "annotate-snippets", + "derive_more 2.0.1", + "itertools 0.14.0", + "lalrpop", + "lalrpop-util", + "lexical", + "logos", + "thiserror 2.0.12", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/abiogenesis/Cargo.toml b/abiogenesis/Cargo.toml index e384867..db6c110 100644 --- a/abiogenesis/Cargo.toml +++ b/abiogenesis/Cargo.toml @@ -20,6 +20,7 @@ bevy = { version = "0.16", default-features = false, features = [ "png", "std", "webgl2", + "shader_format_wesl" ] } rand = "0.8" log = { version = "0.4", features = [ @@ -38,6 +39,7 @@ bevy_tweening = { version = "0.13.0", default-features = false, features = [ itertools = "0.14.0" rand_distr = "0.4.3" serde = { version = "1.0.219", features = ["derive"] } +wgpu-types = "24.0.0" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2.100" } diff --git a/abiogenesis/assets/shaders/compute_shader.wgsl b/abiogenesis/assets/shaders/compute_shader.wgsl new file mode 100644 index 0000000..4465950 --- /dev/null +++ b/abiogenesis/assets/shaders/compute_shader.wgsl @@ -0,0 +1,131 @@ +#import utils::colours::NUM_COLOURS +#import utils::math::{ + lcg, random_float, remap, + toroidal_displacement, toroidal_wrap, + Rect, clamp_length, safe_normalize +} +#import bevy_render::view::View +#import bevy_render::globals::Globals + +@group(0) @binding(0) +var view: View; + +@group(0) @binding(1) +var globals: Globals; + +@group(0) @binding(2) +var particle_colours: array; + +@group(0) @binding(3) +var in_position: array>; + +@group(0) @binding(4) +var in_velocity: array>; + +@group(0) @binding(5) +var out_position: array>; + +@group(0) @binding(6) +var out_velocity: array>; + +override NUM_PARTICLES = 400u * 64u; + +override x_dim = 4.0 * 1920.0; +override y_dim = 4.0 * 1080.0; + + +@compute @workgroup_size(64) +fn init(@builtin(global_invocation_id) id: vec3) { + let index = id.x; + + particle_colours[index] = lcg(index) % 3; + + out_position[index] = vec2( + mix(-x_dim / 2.0, x_dim / 2.0, random_float(index + 1283718)), + mix(-y_dim / 2.0, y_dim / 2.0, random_float(index + 3879349)), + ); + + out_velocity[index] = vec2(0.0, 0.0); +} + +override friction = 2.0; +const model = array( + 0.3, 0.4, 0.5, + 0.7, -0.4, 0.3, + -0.5, 0.5, 0.0 +); + +fn get_model_value(a: u32, b: u32) -> f32 { + return model[a * 3 + b]; +} + +const BOUNDS: Rect = Rect( + vec2(-4.0 * 1920.0 / 2.0, -4.0 * 1080.0 / 2.0), + vec2(4.0 * 1920.0 / 2.0, 4.0 * 1080.0 / 2.0) +); + +@compute @workgroup_size(64) +fn update(@builtin(global_invocation_id) id: vec3) { + let index = id.x; + + var position = in_position[index]; + + let lower_index = u32(((globals.time - globals.delta_time) % 60.0 / 60.0) * f32(NUM_PARTICLES)); + let upper_index = u32((globals.time % 60.0 / 60.0) * f32(NUM_PARTICLES)); + + if lower_index <= index && index < upper_index { + particle_colours[index] = lcg(index + u32(globals.time)) % 3; + position = vec2( + mix(-x_dim / 2.0, x_dim / 2.0, random_float(index + 1283718)), + mix(-y_dim / 2.0, y_dim / 2.0, random_float(index + 3879349)), + ); + } + + let friction = exp(-friction * globals.delta_time); + + var force = vec2(0.0, 0.0); + + for (var other = 0u; other < NUM_PARTICLES; other++) { + if other == index { continue; } + + let displacement = toroidal_displacement(BOUNDS, position, in_position[other]); + // let displacement = in_position[other] - position; + + let magnitude = influence(get_model_value(particle_colours[index], particle_colours[other]), length(displacement)); + + force += 100.0 * magnitude * safe_normalize(displacement); + } + + + let velocity = in_velocity[index] + clamp_length(force, 0.0, 1000.0) * globals.delta_time; + out_velocity[index] = clamp_length(velocity * friction, 0.0, 400.0); + out_position[index] = toroidal_wrap(BOUNDS, position + velocity * globals.delta_time); +} + +override replusion_radius = 25.0; +override peak_attraction_radius = 50.0; +override attraction_radius = 75.0; + +fn influence(factor: f32, distance: f32) -> f32 { + if distance <= replusion_radius { + return remap(distance, 0.0, replusion_radius, -1.0, 0.0); + } else if distance <= peak_attraction_radius { + return remap( + distance, + replusion_radius, + peak_attraction_radius, + 0.0, + factor, + ); + } else if distance <= attraction_radius { + return remap( + distance, + 66.6, + attraction_radius, + factor, + 0.0, + ); + } else { + return 0.0; + } +} diff --git a/abiogenesis/assets/shaders/particle_render.wgsl b/abiogenesis/assets/shaders/particle_render.wgsl new file mode 100644 index 0000000..b1ca76a --- /dev/null +++ b/abiogenesis/assets/shaders/particle_render.wgsl @@ -0,0 +1,84 @@ +#import bevy_render::view::View +#import utils::colours::COLOURS +#import utils::math::{Rect, toroidal_displacement} + +struct VertexInput { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) fragUV: vec2, + @location(1) color: vec3, +} + +@group(0) @binding(0) var view: View; +@group(0) @binding(1) var particle_positions: array>; +@group(0) @binding(2) var particle_colours: array; + +override size = 1.0; +override sharpness = 1.0; + +const BOUNDS: Rect = Rect( + vec2(-4.0 * 1920.0 / 2.0, -4.0 * 1080.0 / 2.0), + vec2(4.0 * 1920.0 / 2.0, 4.0 * 1080.0 / 2.0) +); + +fn wrap_particle_position(particle_pos: vec2, camera_pos: vec2) -> vec2 { + let width = BOUNDS.max.x - BOUNDS.min.x; + let height = BOUNDS.max.y - BOUNDS.min.y; + + var wrapped_pos = particle_pos; + + // Calculate the displacement from camera to particle + let displacement = toroidal_displacement(BOUNDS, camera_pos, particle_pos); + let direct_displacement = particle_pos - camera_pos; + + // If the shortest path crosses a boundary, render at the wrapped position + if abs(displacement.x) < abs(direct_displacement.x) { + if direct_displacement.x > 0.0 && displacement.x < 0.0 { + wrapped_pos.x -= width; + } else if direct_displacement.x < 0.0 && displacement.x > 0.0 { + wrapped_pos.x += width; + } + } + + if abs(displacement.y) < abs(direct_displacement.y) { + if direct_displacement.y > 0.0 && displacement.y < 0.0 { + wrapped_pos.y -= height; + } else if direct_displacement.y < 0.0 && displacement.y > 0.0 { + wrapped_pos.y += height; + } + } + + return wrapped_pos; +} + +@vertex +fn vertex(input: VertexInput) -> VertexOutput { + var out: VertexOutput; + + let raw_particle_pos = particle_positions[input.instance_index]; + + // Extract camera position from the existing View uniform + let camera_pos = view.world_position.xy; + + // Wrap the particle position for seamless toroidal rendering + let wrapped_particle_pos = wrap_particle_position(raw_particle_pos, camera_pos); + + let world_position = vec4(input.position.xy * size + wrapped_particle_pos, 0.0, 1.0); + out.clip_position = view.clip_from_world * world_position; + + out.color = COLOURS[particle_colours[input.instance_index]]; + out.fragUV = input.position.xy; + + return out; +} + +@fragment +fn fragment(input: VertexOutput) -> @location(0) vec4 { + let alpha = clamp(smoothstep(1.0, 0.0, sharpness * (dot(input.fragUV, input.fragUV) * 2.0 - 1.0)), 0.0, 1.0); + + return vec4(input.color, alpha); +} diff --git a/abiogenesis/assets/shaders/utils/colours.wgsl b/abiogenesis/assets/shaders/utils/colours.wgsl new file mode 100644 index 0000000..97fd516 --- /dev/null +++ b/abiogenesis/assets/shaders/utils/colours.wgsl @@ -0,0 +1,21 @@ +#define_import_path utils::colours + +const RED: vec3 = vec3(172.0 / 255.0, 40.0 / 255.0, 71.0 / 255.0); +const GREEN: vec3 = vec3(90.0 / 255.0, 181.0 / 255.0, 82.0 / 255.0); +const BLUE: vec3 = vec3(51.0 / 255.0, 136.0 / 255.0, 222.0 / 255.0); +const ORANGE: vec3 = vec3(255.0 / 255.0, 155.0 / 255.0, 37.0 / 255.0); +const PINK: vec3 = vec3(233.0 / 255.0, 75.0 / 255.0, 234.0 / 255.0); +const AQUA: vec3 = vec3(57.0 / 255.0, 247.0 / 255.0, 241.0 / 255.0); + +const COLOURS: array, NUM_COLOURS> = array, NUM_COLOURS>(RED, GREEN, BLUE, ORANGE, PINK, AQUA); + +const NUM_COLOURS: u32 = 6; + +const RED_ID: u32 = 0; +const GREEN_ID: u32 = 1; +const BLUE_ID: u32 = 2; +const ORANGE_ID: u32 = 3; +const PINK_ID: u32 = 4; +const AQUA_ID: u32 = 5; + +const EMPTY_ID: u32 = 0 << 1; diff --git a/abiogenesis/assets/shaders/utils/math.wgsl b/abiogenesis/assets/shaders/utils/math.wgsl new file mode 100644 index 0000000..642dbe4 --- /dev/null +++ b/abiogenesis/assets/shaders/utils/math.wgsl @@ -0,0 +1,94 @@ +#define_import_path utils::math + +fn lcg(seed: u32) -> u32 { + var result = seed; + + result ^= 2747636419u; + result *= 2654435769u; + result ^= result >> 16; + result *= 2654435769u; + result ^= result >> 16; + result *= 2654435769u; + + return result; +} + +fn random_float(seed: u32) -> f32 { + let rand = lcg(seed); + return f32(rand) / f32(0xffffffffu); +} + +fn clamp_length(value: vec2, min: f32, max: f32) -> vec2 { + let len = length(value); + + if len == 0.0 { + return vec2f(0.0); + } else { + return value * clamp(len, min, max) / len; + } +} + +fn lerp(a: f32, b: f32, t: f32) -> f32 { + return a + (b - a) * t; +} + +fn inverse_lerp(a: f32, b: f32, value: f32) -> f32 { + return (value - a) / (b - a); +} + +fn remap(value: f32, a: f32, b: f32, c: f32, d: f32) -> f32 { + return lerp(c, d, inverse_lerp(a, b, value)); +} + +struct Rect { + min: vec2f, + max: vec2f, +} + +fn toroidal_displacement(bounds: Rect, a: vec2f, b: vec2f) -> vec2f { + let width = bounds.max.x - bounds.min.x; + let height = bounds.max.y - bounds.min.y; + + var dx = b.x - a.x; + var dy = b.y - a.y; + + // Adjust x displacement for wrapping + if dx > width / 2.0 { + dx -= width; + } else if dx < -width / 2.0 { + dx += width; + } + + // Adjust y displacement for wrapping + if dy > height / 2.0 { + dy -= height; + } else if dy < -height / 2.0 { + dy += height; + } + + return vec2f(dx, dy); +} + +fn toroidal_wrap(bounds: Rect, pos: vec2f) -> vec2f { + let width = bounds.max.x - bounds.min.x; + let height = bounds.max.y - bounds.min.y; + + // Normalize position relative to bounds origin + let relative_x = pos.x - bounds.min.x; + let relative_y = pos.y - bounds.min.y; + + // Use modulo operation to wrap coordinates + let wrapped_x = relative_x - floor(relative_x / width) * width; + let wrapped_y = relative_y - floor(relative_y / height) * height; + + // Convert back to absolute coordinates + return vec2f( + wrapped_x + bounds.min.x, + wrapped_y + bounds.min.y + ); +} + +fn safe_normalize(v: vec2) -> vec2 { + let len = length(v); + return select(v / len, vec2f(0.0), len == 0.0); +} diff --git a/abiogenesis/src/bundle_fn.rs b/abiogenesis/src/bundle_fn.rs index b8224e5..6a8fc69 100644 --- a/abiogenesis/src/bundle_fn.rs +++ b/abiogenesis/src/bundle_fn.rs @@ -5,9 +5,9 @@ use bevy::prelude::*; pub trait Thunk: FnOnce(&mut EntityWorldMut) + Send + Sync + 'static {} impl Thunk for F {} -pub struct BundleFn(pub F); +pub struct BundleFn(pub T); -unsafe impl Bundle for BundleFn { +unsafe impl Bundle for BundleFn { fn component_ids(_: &mut ComponentsRegistrator, _: &mut impl FnMut(ComponentId)) {} fn get_component_ids(_: &Components, _: &mut impl FnMut(Option)) {} @@ -15,8 +15,8 @@ unsafe impl Bundle for BundleFn { fn register_required_components(_: &mut ComponentsRegistrator, _: &mut RequiredComponents) {} } -impl DynamicBundle for BundleFn { - type Effect = BundleFn; +impl DynamicBundle for BundleFn { + type Effect = BundleFn; fn get_components( self, @@ -26,7 +26,7 @@ impl DynamicBundle for BundleFn { } } -impl BundleEffect for BundleFn { +impl BundleEffect for BundleFn { fn apply(self, entity: &mut EntityWorldMut) { (self.0)(entity); } diff --git a/abiogenesis/src/compute_shader.rs b/abiogenesis/src/compute_shader.rs new file mode 100644 index 0000000..e341bf5 --- /dev/null +++ b/abiogenesis/src/compute_shader.rs @@ -0,0 +1,330 @@ +use std::{borrow::Cow, num::NonZeroU64}; + +use bevy::{ + asset::RenderAssetUsages, + core_pipeline::core_2d::graph::Core2d, + ecs::query::QueryItem, + prelude::*, + render::{ + Render, RenderApp, RenderSet, + extract_resource::{ExtractResource, ExtractResourcePlugin}, + globals::{GlobalsBuffer, GlobalsUniform}, + gpu_readback::{Readback, ReadbackComplete}, + render_asset::RenderAssets, + render_graph::{ + NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel, ViewNode, ViewNodeRunner, + }, + render_resource::{ + BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BufferUsages, + CachedComputePipelineId, CachedPipelineState, ComputePassDescriptor, + ComputePipelineDescriptor, PipelineCache, PipelineCacheError, ShaderStages, + ValidateShader, + binding_types::{storage_buffer_read_only_sized, storage_buffer_sized, uniform_buffer}, + }, + renderer::{RenderContext, RenderDevice}, + storage::{GpuShaderStorageBuffer, ShaderStorageBuffer}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, + }, +}; + +pub const NUM_PARTICLES: usize = 400 * 64; +pub const BUFFER_SIZE: Option = + NonZeroU64::new(NUM_PARTICLES as u64 * 2 * std::mem::size_of::() as u64); +pub const WORKGROUP_SIZE: usize = 64; +pub const SHADER_ASSET_PATH: &str = "shaders/compute_shader.wgsl"; + +pub struct ComputeShaderPlugin; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +pub struct ParticleLifeLabel; + +impl Plugin for ComputeShaderPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, setup_buffers); + let asset_server = app.world_mut().resource_mut::(); + + // Leak the handle for shared shader libs to ensure they are not dropped prematurely + let load_shader = |path: &str| Box::leak(Box::new(asset_server.load::(path))); + + load_shader("shaders/utils/colours.wgsl"); + load_shader("shaders/utils/math.wgsl"); + + app.insert_resource(ParticleLifeState::default()); + app.add_plugins(ExtractResourcePlugin::::default()); + app.add_plugins(ExtractResourcePlugin::::default()); + + let render_app = app.sub_app_mut(RenderApp); + render_app.add_systems( + Render, + prepare_bind_group + .in_set(RenderSet::PrepareBindGroups) + .run_if(not(resource_exists::)), + ); + + render_app + .add_render_graph_node::>(Core2d, ParticleLifeLabel); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + render_app.init_resource::(); + } +} + +#[derive(Resource, Clone, ExtractResource)] +pub struct ParticleLifeBuffers { + pub positions_a: Handle, + pub velocity_a: Handle, + pub colours: Handle, + pub positions_b: Handle, + pub velocity_b: Handle, +} + +#[derive(Resource)] +struct GameOfLifeImageBindGroups([BindGroup; 2]); + +fn setup_buffers(mut commands: Commands, mut buffers: ResMut>) { + let size = BUFFER_SIZE.unwrap().get() as usize; + + let mut positions_a = ShaderStorageBuffer::with_size(size, RenderAssetUsages::RENDER_WORLD); + positions_a.buffer_description.usage |= BufferUsages::COPY_SRC; + let positions_a = buffers.add(positions_a); + + let velocity_a = buffers.add(ShaderStorageBuffer::with_size( + size, + RenderAssetUsages::RENDER_WORLD, + )); + + let colours = buffers.add(ShaderStorageBuffer::with_size( + size, + RenderAssetUsages::RENDER_WORLD, + )); + + let mut positions_b = ShaderStorageBuffer::with_size(size, RenderAssetUsages::RENDER_WORLD); + positions_b.buffer_description.usage |= BufferUsages::COPY_SRC; + let positions_b = buffers.add(positions_b); + + let velocity_b = buffers.add(ShaderStorageBuffer::with_size( + size, + RenderAssetUsages::RENDER_WORLD, + )); + + commands.insert_resource(ParticleLifeBuffers { + positions_a: positions_a.clone(), + velocity_a: velocity_a, + colours: colours, + positions_b: positions_b.clone(), + velocity_b: velocity_b, + }); + + // commands + // .spawn(Readback::buffer(positions_a)) + // .observe(|trigger: Trigger| { + // tracing::info!(data = ?trigger.event().to_shader_type::>()[100], "Buffer A Readback complete"); + // }); + + // commands + // .spawn(Readback::buffer(positions_b)) + // .observe(|trigger: Trigger| { + // tracing::info!(data = ?trigger.event().to_shader_type::>(), "Buffer B Readback complete"); + // }); +} + +fn prepare_bind_group( + mut commands: Commands, + pipeline: Res, + gpu_buffers: Res>, + particle_life_buffers: Res, + view_uniforms: Res, + globals_buffer: Res, + render_device: Res, +) { + let positions_a = gpu_buffers.get(&particle_life_buffers.positions_a).unwrap(); + let velocity_a = gpu_buffers.get(&particle_life_buffers.velocity_a).unwrap(); + let colours = gpu_buffers.get(&particle_life_buffers.colours).unwrap(); + let positions_b = gpu_buffers.get(&particle_life_buffers.positions_b).unwrap(); + let velocity_b = gpu_buffers.get(&particle_life_buffers.velocity_b).unwrap(); + + let bind_group_0 = render_device.create_bind_group( + None, + &pipeline.bind_group_layout, + &BindGroupEntries::sequential(( + view_uniforms.uniforms.binding().unwrap(), + globals_buffer.buffer.binding().unwrap(), + colours.buffer.as_entire_buffer_binding(), + positions_a.buffer.as_entire_buffer_binding(), + velocity_a.buffer.as_entire_buffer_binding(), + positions_b.buffer.as_entire_buffer_binding(), + velocity_b.buffer.as_entire_buffer_binding(), + )), + ); + + let bind_group_1 = render_device.create_bind_group( + None, + &pipeline.bind_group_layout, + &BindGroupEntries::sequential(( + view_uniforms.uniforms.binding().unwrap(), + globals_buffer.buffer.binding().unwrap(), + colours.buffer.as_entire_buffer_binding(), + positions_b.buffer.as_entire_buffer_binding(), + velocity_b.buffer.as_entire_buffer_binding(), + positions_a.buffer.as_entire_buffer_binding(), + velocity_a.buffer.as_entire_buffer_binding(), + )), + ); + + commands.insert_resource(GameOfLifeImageBindGroups([bind_group_0, bind_group_1])); +} + +#[derive(Resource)] +struct ParticleLifePipeline { + bind_group_layout: BindGroupLayout, + init_pipeline: CachedComputePipelineId, + update_pipeline: CachedComputePipelineId, +} + +impl FromWorld for ParticleLifePipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let buffer_bind_group_layout = render_device.create_bind_group_layout( + "ParticleLifeBuffers", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + uniform_buffer::(true), + uniform_buffer::(false), + storage_buffer_sized(false, BUFFER_SIZE), + storage_buffer_read_only_sized(false, BUFFER_SIZE), + storage_buffer_read_only_sized(false, BUFFER_SIZE), + storage_buffer_sized(false, BUFFER_SIZE), + storage_buffer_sized(false, BUFFER_SIZE), + ), + ), + ); + let shader = world.load_asset(SHADER_ASSET_PATH); + + tracing::info!(id = ?shader.id(), path = ?shader.path()); + + let pipeline_cache = world.resource::(); + let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: None, + layout: vec![buffer_bind_group_layout.clone()], + push_constant_ranges: Vec::new(), + shader: shader.clone(), + shader_defs: vec![], + entry_point: Cow::from("init"), + zero_initialize_workgroup_memory: false, + }); + let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: None, + layout: vec![buffer_bind_group_layout.clone()], + push_constant_ranges: Vec::new(), + shader, + shader_defs: vec![], + entry_point: Cow::from("update"), + zero_initialize_workgroup_memory: false, + }); + + ParticleLifePipeline { + bind_group_layout: buffer_bind_group_layout, + init_pipeline, + update_pipeline, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Resource, ExtractResource, Default)] +pub enum ParticleLifeState { + #[default] + Loading, + Init, + Update(usize), +} + +#[derive(Default)] +struct ParticleLifeNode; + +impl ViewNode for ParticleLifeNode { + type ViewQuery = (&'static ViewUniformOffset, &'static ViewTarget); + + fn update(&mut self, world: &mut World) { + // SAFETY: ParticleLifeState is unaliased in this function and so this is safe + // Rust's borrow checker isn't smart enough to know this doesn't conflict with the immutable borrows below + let pipeline_state = + unsafe { &mut *(world.resource_mut::().into_inner() as *mut _) }; + + let pipeline = world.resource::(); + let pipeline_cache = world.resource::(); + + // if the corresponding pipeline has loaded, transition to the next stage + match pipeline_state { + ParticleLifeState::Loading => { + match pipeline_cache.get_compute_pipeline_state(pipeline.init_pipeline) { + CachedPipelineState::Ok(_) => { + *pipeline_state = ParticleLifeState::Init; + } + // If the shader hasn't loaded yet, just wait. + CachedPipelineState::Err(PipelineCacheError::ShaderNotLoaded(_)) => {} + CachedPipelineState::Err(err) => { + panic!("Initializing assets/{SHADER_ASSET_PATH}:\n{err}") + } + _ => {} + } + } + ParticleLifeState::Init => { + if let CachedPipelineState::Ok(_) = + pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline) + { + *pipeline_state = ParticleLifeState::Update(1); + } + } + ParticleLifeState::Update(0) => { + *pipeline_state = ParticleLifeState::Update(1); + } + ParticleLifeState::Update(1) => { + *pipeline_state = ParticleLifeState::Update(0); + } + ParticleLifeState::Update(_) => unreachable!(), + } + } + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (view_uniforms_offset, view_target): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let bind_groups = &world.resource::().0; + let pipeline_cache = world.resource::(); + let pipeline = world.resource::(); + let pipeline_state = world.resource::(); + + let mut pass = render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor::default()); + + // select the pipeline based on the current state + match pipeline_state { + ParticleLifeState::Loading => {} + ParticleLifeState::Init => { + let init_pipeline = pipeline_cache + .get_compute_pipeline(pipeline.init_pipeline) + .unwrap(); + pass.set_bind_group(0, &bind_groups[0], &[view_uniforms_offset.offset]); + pass.set_pipeline(init_pipeline); + pass.dispatch_workgroups((NUM_PARTICLES / WORKGROUP_SIZE) as u32, 1, 1); + } + &ParticleLifeState::Update(index) => { + let update_pipeline = pipeline_cache + .get_compute_pipeline(pipeline.update_pipeline) + .unwrap(); + pass.set_bind_group(0, &bind_groups[index], &[view_uniforms_offset.offset]); + pass.set_pipeline(update_pipeline); + pass.dispatch_workgroups((NUM_PARTICLES / WORKGROUP_SIZE) as u32, 1, 1); + } + } + + Ok(()) + } +} diff --git a/abiogenesis/src/controls.rs b/abiogenesis/src/controls.rs index 311c19f..bd71ba4 100644 --- a/abiogenesis/src/controls.rs +++ b/abiogenesis/src/controls.rs @@ -1,3 +1,4 @@ +use crate::math::TorodialMath; use std::{cmp::Ordering, time::Duration}; use bevy::prelude::*; @@ -149,8 +150,9 @@ pub fn select_follow_particle( #[cfg_attr(feature = "hot_reload", bevy_simple_subsecond_system::hot)] pub fn drag_screen( trigger: Trigger>, - mut particles: Query<&mut Transform, With>, + mut camera_transform: Single<&mut Transform, With>, projection: Single<&Projection>, + bounds: SimulationSize, mut commands: Commands, ) { if !matches!(trigger.button, PointerButton::Secondary) { @@ -167,9 +169,11 @@ pub fn drag_screen( delta.y *= -1.0; delta *= project.scale; - for mut particle in &mut particles { - particle.translation += delta.extend(0.0); - } + // Move the camera instead of all particles + camera_transform.translation = bounds + .as_rect() + .toroidal_wrap(camera_transform.translation.truncate() - delta) + .extend(0.0); } #[cfg_attr(feature = "hot_reload", bevy_simple_subsecond_system::hot)] diff --git a/abiogenesis/src/main.rs b/abiogenesis/src/main.rs index eaf951f..20bcbb3 100644 --- a/abiogenesis/src/main.rs +++ b/abiogenesis/src/main.rs @@ -6,25 +6,25 @@ use bevy::{ asset::{AssetMetaCheck, load_internal_binary_asset}, prelude::*, - window::WindowResolution, }; use bevy_tweening::{AnimationSystem, TweeningPlugin}; use particles::ParticlePlugin; use ui::UIPlugin; use crate::{ - browser_state::BrowserStatePlugin, camera::CameraPlugin, controls::ControlsPlugin, - scenes::ScenePlugin, systems::AppSystems, + browser_state::BrowserStatePlugin, camera::CameraPlugin, compute_shader::ComputeShaderPlugin, + controls::ControlsPlugin, render_particles::ParticleRenderPlugin, systems::AppSystems, }; mod browser_state; mod bundle_fn; mod camera; +mod compute_shader; mod controls; mod math; mod observe; mod particles; -mod scenes; +mod render_particles; mod spatial_hash; mod systems; mod ui; @@ -71,8 +71,8 @@ fn bevy_systems(app: &mut App) { #[cfg(all(debug_assertions, not(feature = "hot_reload")))] app.edit_schedule(Update, |schedule| { - schedule.set_build_settings(ScheduleBuildSettings { - ambiguity_detection: LogLevel::Warn, + schedule.set_build_settings(bevy::ecs::schedule::ScheduleBuildSettings { + ambiguity_detection: bevy::ecs::schedule::LogLevel::Warn, ..default() }); }); @@ -98,10 +98,11 @@ fn app_systems(app: &mut App) { app.add_plugins(( ParticlePlugin, UIPlugin, - ScenePlugin, - CameraPlugin, + // CameraPlugin, ControlsPlugin, - BrowserStatePlugin, + // BrowserStatePlugin, + ComputeShaderPlugin, + ParticleRenderPlugin, )); app.configure_sets( diff --git a/abiogenesis/src/particles.rs b/abiogenesis/src/particles.rs index d04b4fa..46f5e6e 100644 --- a/abiogenesis/src/particles.rs +++ b/abiogenesis/src/particles.rs @@ -21,11 +21,11 @@ impl Plugin for ParticlePlugin { app.add_plugins(( particle::ParticlePlugin, ModelPlugin, - DecayPlugin, + // DecayPlugin, SimulationPlugin, - SimulationSizePlugin, - SpatialIndexPlugin, - SpawnerPlugin, + // SimulationSizePlugin, + // SpatialIndexPlugin, + // SpawnerPlugin, )); } } diff --git a/abiogenesis/src/particles/decay.rs b/abiogenesis/src/particles/decay.rs index 8d88eda..48b3555 100644 --- a/abiogenesis/src/particles/decay.rs +++ b/abiogenesis/src/particles/decay.rs @@ -7,7 +7,7 @@ use crate::{ particle::{ParticleIndex, Velocity}, simulation::SimulationParams, size::SimulationSize, - spawner::{OldestParticle, ParticleAssets}, + spawner::OldestParticle, }, }; @@ -29,7 +29,6 @@ fn particle_decay( follow_particle: Option>, params: Res, mut commands: Commands, - particle_assets: Res, ) -> Result<()> { let Vec2 { x: width, diff --git a/abiogenesis/src/particles/model.rs b/abiogenesis/src/particles/model.rs index 0e1ddbe..1edfd24 100644 --- a/abiogenesis/src/particles/model.rs +++ b/abiogenesis/src/particles/model.rs @@ -1,6 +1,5 @@ use std::sync::LazyLock; -use bevy::math::Rect; use bevy::prelude::*; use rand::Rng; use rand_distr::{Distribution, Normal}; @@ -13,7 +12,7 @@ use crate::particles::{ ATTRACTION_RADIUS_RANGE, FORCE_STRENGTH_RANGE, FRICTION_RANGE, PEAK_ATTRACTION_RADIUS_RANGE, REPULSION_RADIUS_RANGE, SimulationParams, }, - spawner::{SpawnShape, SpawnerConfig}, + spawner::SpawnerConfig, }; pub const NUM_PRESETS: usize = 9; diff --git a/abiogenesis/src/particles/simulation.rs b/abiogenesis/src/particles/simulation.rs index dd8e9f6..f14ac74 100644 --- a/abiogenesis/src/particles/simulation.rs +++ b/abiogenesis/src/particles/simulation.rs @@ -19,8 +19,8 @@ pub struct SimulationPlugin; impl Plugin for SimulationPlugin { fn build(&self, app: &mut App) { app.register_type::() - .insert_resource(PRESETS[0].3) - .add_systems(Update, compute_forces.in_set(AppSystems::Update)); + .insert_resource(PRESETS[0].3); + // .add_systems(Update, compute_forces.in_set(AppSystems::Update)); } } diff --git a/abiogenesis/src/particles/size.rs b/abiogenesis/src/particles/size.rs index 707ade8..51be74b 100644 --- a/abiogenesis/src/particles/size.rs +++ b/abiogenesis/src/particles/size.rs @@ -15,11 +15,19 @@ pub struct SimulationSize<'w> { // projection: Single<'w, &'static Projection>, } +const HEIGHT: f32 = 4.0 * 1080.0; +const WIDTH: f32 = 4.0 * 1920.0; + impl SimulationSize<'_> { pub fn dimensions(&self) -> Vec2 { - let (width, height) = (self.window.width(), self.window.height()); + // let (width, height) = (self.window.width(), self.window.height()); + + // Vec2::new(width.max(1920.0), height.max(1080.0)) + return Vec2::new(WIDTH, HEIGHT); + } - Vec2::new(width.max(1920.0), height.max(1080.0)) + pub fn as_rect(&self) -> Rect { + Rect::from_center_size(Vec2::ZERO, self.dimensions()) } pub fn scale_bounds(&self) -> (f32, f32) { @@ -37,7 +45,7 @@ fn resize_simulation(mut spatial_index: ResMut, simulation_size: S // Allows zooming out until you hit the screen edges fn scale_bounds(screen_width: f32, screen_height: f32) -> (f32, f32) { let (simulation_width, simulation_height) = - (screen_width.max(1920.0), screen_height.max(1080.0)); + (screen_width.max(WIDTH), screen_height.max(HEIGHT)); ( 0.1, diff --git a/abiogenesis/src/particles/spawner.rs b/abiogenesis/src/particles/spawner.rs index 467f6e6..e59d95c 100644 --- a/abiogenesis/src/particles/spawner.rs +++ b/abiogenesis/src/particles/spawner.rs @@ -1,5 +1,4 @@ use bevy::prelude::*; -use rand_distr::uniform; use crate::particles::{ colour::*, @@ -82,10 +81,12 @@ pub enum SpawnerConfig { None, #[default] Uniform, + #[allow(dead_code)] Custom(Vec<(ParticleColour, SpawnShape)>), } #[derive(Debug, Clone, PartialEq)] +#[allow(dead_code)] pub enum SpawnShape { Rect(Rect), Circle { diff --git a/abiogenesis/src/render_particles.rs b/abiogenesis/src/render_particles.rs new file mode 100644 index 0000000..14f4784 --- /dev/null +++ b/abiogenesis/src/render_particles.rs @@ -0,0 +1,332 @@ +use bevy::{ + core_pipeline::core_2d::graph::{Core2d, Node2d}, + prelude::*, + render::{ + Render, RenderApp, RenderSet, + camera::ExtractedCamera, + render_asset::RenderAssets, + render_graph::{RenderGraphApp, RenderLabel, ViewNode, ViewNodeRunner}, + render_resource::{ + BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendState, + Buffer, BufferInitDescriptor, BufferUsages, CachedRenderPipelineId, ColorTargetState, + ColorWrites, FragmentState, LoadOp, MultisampleState, Operations, PipelineCache, + PrimitiveState, RenderPassDescriptor, RenderPipelineDescriptor, ShaderStages, StoreOp, + VertexAttribute, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, + binding_types::{storage_buffer_read_only_sized, uniform_buffer}, + }, + renderer::{RenderContext, RenderDevice}, + storage::GpuShaderStorageBuffer, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, + }, +}; + +use crate::compute_shader::{ + BUFFER_SIZE, NUM_PARTICLES, ParticleLifeBuffers, ParticleLifeLabel, ParticleLifeState, +}; + +const PARTICLE_RENDER_SHADER_PATH: &str = "shaders/particle_render.wgsl"; + +pub struct ParticleRenderPlugin; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +pub struct ParticleRenderLabel; + +impl Plugin for ParticleRenderPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, update_fps_counter); + + app.add_systems(Startup, setup_camera); + + let render_app = app.sub_app_mut(RenderApp); + + render_app.add_systems( + Render, + ( + setup_quad_buffer + .in_set(RenderSet::PrepareResources) + .run_if(not(resource_exists::)), + prepare_particle_render_bind_group + .in_set(RenderSet::PrepareBindGroups) + .run_if(not(resource_exists::)), + ), + ); + + // Use the modern ViewNodeRunner approach + render_app.add_render_graph_node::>( + Core2d, + ParticleRenderLabel, + ); + + // Add it to the 2D render graph after the main pass + render_app.add_render_graph_edges( + Core2d, + ( + Node2d::EndMainPass, + ParticleLifeLabel, + ParticleRenderLabel, + Node2d::Bloom, + ), + ); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + render_app.init_resource::(); + } +} + +#[derive(Debug, Clone, Copy, Component)] +struct FPSCounter; + +fn setup_camera(mut commands: Commands, camera_query: Query>) { + camera_query.iter().for_each(|entity| { + commands.entity(entity).despawn(); + }); + + // Add a 2D camera to render to + commands.spawn(( + Camera2d, + Camera { + // clear_color: ClearColorConfig::Custom(Color::srgb(0.0, 1.0, 0.0)), + ..default() + }, + Projection::Orthographic(OrthographicProjection { + ..OrthographicProjection::default_2d() + }), + Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), + )); + + commands.spawn(( + Node { + margin: UiRect::left(Val::Auto), + padding: UiRect::all(Val::Px(16.0)), + ..default() + }, + children![(FPSCounter, Text::from("0.0"),)], + )); +} + +fn update_fps_counter( + mut text: Single<&mut Text, With>, + time: Res