diff --git a/Cargo.lock b/Cargo.lock index 807237b..c99e78b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,25 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arboard" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" +dependencies = [ + "clipboard-win", + "core-graphics", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot", + "thiserror", + "winapi", + "x11rb", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -286,6 +305,45 @@ dependencies = [ "bevy_internal", ] +[[package]] +name = "bevy-inspector-egui" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452f1258251b85c06c5bc9df9ea994d8f3b8c0696f346d43878eec52fce8a09c" +dependencies = [ + "bevy-inspector-egui-derive", + "bevy_app", + "bevy_asset", + "bevy_core", + "bevy_core_pipeline", + "bevy_ecs", + "bevy_egui", + "bevy_hierarchy", + "bevy_log", + "bevy_math", + "bevy_pbr", + "bevy_reflect", + "bevy_render", + "bevy_utils", + "bevy_window", + "egui", + "image", + "once_cell", + "pretty-type-name", + "smallvec", +] + +[[package]] +name = "bevy-inspector-egui-derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a60a8d711fa10879a4e7d10127077505ba9890cead8d1f5ecedc64d14b22b6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "bevy_a11y" version = "0.11.3" @@ -477,6 +535,19 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "bevy_egui" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1c1f6ad293c60fd8559c4502cda5e832e92b0e0f3d994929b33f24d4352d70" +dependencies = [ + "arboard", + "bevy", + "egui", + "thread_local", + "webbrowser", +] + [[package]] name = "bevy_encase_derive" version = "0.11.3" @@ -487,6 +558,39 @@ dependencies = [ "encase_derive_impl", ] +[[package]] +name = "bevy_eventlistener" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b089d6bee5e75b1bc2793752ce94c461d4ce581281c15ca44cbd84bb7794073" +dependencies = [ + "bevy_eventlistener_core", + "bevy_eventlistener_derive", +] + +[[package]] +name = "bevy_eventlistener_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c94b227693f271fd7dad522214f1a371c7856d88cedb6473d4c945e8169103" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_hierarchy", + "bevy_utils", +] + +[[package]] +name = "bevy_eventlistener_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b88fc692e14d483c9aedd74958d1a39f5adbc6f94d97d87a2b330e27fa6a616" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "bevy_gilrs" version = "0.11.3" @@ -669,6 +773,53 @@ dependencies = [ "glam", ] +[[package]] +name = "bevy_mod_outline" +version = "0.5.0" +dependencies = [ + "bevy", + "bitfield", + "interpolation", + "thiserror", + "wgpu-types", +] + +[[package]] +name = "bevy_mod_picking" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebb845900d5da6314f261cb982079266840820032fa29679341a5a901e2c5b9" +dependencies = [ + "bevy_app", + "bevy_core", + "bevy_ecs", + "bevy_eventlistener", + "bevy_math", + "bevy_picking_core", + "bevy_picking_highlight", + "bevy_picking_input", + "bevy_picking_raycast", + "bevy_picking_selection", + "bevy_picking_sprite", + "bevy_picking_ui", + "bevy_reflect", + "bevy_render", + "bevy_text", + "bevy_ui", + "bevy_utils", + "bevy_window", +] + +[[package]] +name = "bevy_mod_raycast" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833e94bd71d2ef27445f3e146b07da4a7922fdb7b464f0f1221521f85a5725a" +dependencies = [ + "bevy", + "crossbeam-channel", +] + [[package]] name = "bevy_pbr" version = "0.11.3" @@ -692,6 +843,122 @@ dependencies = [ "radsort", ] +[[package]] +name = "bevy_picking_core" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8194c5c1baf84c4a377223ece3cb7514fc8b9736c0c758efbc188898d19b27" +dependencies = [ + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_eventlistener", + "bevy_math", + "bevy_reflect", + "bevy_render", + "bevy_utils", + "bevy_window", +] + +[[package]] +name = "bevy_picking_highlight" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "603a4e422fa60c5d01e4c32b0293fd3cab06b437a700f0b83f314c010cc20461" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_ecs", + "bevy_picking_core", + "bevy_picking_selection", + "bevy_reflect", + "bevy_render", + "bevy_sprite", +] + +[[package]] +name = "bevy_picking_input" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71676712915c8ade223e58bccce29556977f497381497149de435358389d4b5e" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_hierarchy", + "bevy_input", + "bevy_math", + "bevy_picking_core", + "bevy_picking_selection", + "bevy_reflect", + "bevy_render", + "bevy_utils", + "bevy_window", +] + +[[package]] +name = "bevy_picking_raycast" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05af12237ff5056532bd2e550774a4e89fe976523b2681e5ae7a73e5da6147f0" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_mod_raycast", + "bevy_picking_core", + "bevy_reflect", + "bevy_render", + "bevy_transform", + "bevy_window", +] + +[[package]] +name = "bevy_picking_selection" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5147eecdc5cd7212a1790aaf8b415a134ed422daf64e2633719454a7c7a86deb" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_eventlistener", + "bevy_input", + "bevy_picking_core", + "bevy_reflect", + "bevy_utils", +] + +[[package]] +name = "bevy_picking_sprite" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2787e7c0fc71b78bce974f7c90bc2530d7ad0a7673c0532fe2133c87aed44f4" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_ecs", + "bevy_math", + "bevy_picking_core", + "bevy_render", + "bevy_sprite", + "bevy_transform", + "bevy_window", +] + +[[package]] +name = "bevy_picking_ui" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093df7c568d357edde0b8c114cae35ca985ac5666a2ddea23977883108829393" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_picking_core", + "bevy_render", + "bevy_transform", + "bevy_ui", + "bevy_window", +] + [[package]] name = "bevy_ptr" version = "0.11.3" @@ -1039,6 +1306,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + [[package]] name = "bitflags" version = "1.3.2" @@ -1165,6 +1438,17 @@ dependencies = [ "libloading 0.7.4", ] +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1391,6 +1675,35 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "ecolor" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf4e52dbbb615cfd30cf5a5265335c217b5fd8d669593cea74a517d9c605af" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "egui" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd69fed5fcf4fbb8225b24e80ea6193b61e17a625db105ef0c4d71dde6eb8b7" +dependencies = [ + "ahash", + "epaint", + "nohash-hasher", +] + +[[package]] +name = "emath" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef2b29de53074e575c18b694167ccbe6e5191f7b25fe65175a0d905a32eeec0" +dependencies = [ + "bytemuck", +] + [[package]] name = "encase" version = "0.6.1" @@ -1423,6 +1736,21 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "epaint" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58067b840d009143934d91d8dcb8ded054d8301d7c11a517ace0a99bb1e1595e" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "nohash-hasher", + "parking_lot", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1438,6 +1766,16 @@ dependencies = [ "serde", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "euclid" version = "0.22.9" @@ -1526,6 +1864,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1562,6 +1909,16 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -1804,6 +2161,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "image" version = "0.24.7" @@ -1816,6 +2192,7 @@ dependencies = [ "num-rational", "num-traits", "png", + "tiff", ] [[package]] @@ -1876,6 +2253,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "interpolation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02" + [[package]] name = "io-kit-sys" version = "0.3.0" @@ -1920,6 +2303,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -1935,6 +2334,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + [[package]] name = "js-sys" version = "0.3.64" @@ -2092,6 +2497,15 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "metal" version = "0.24.0" @@ -2213,6 +2627,7 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", + "memoffset", ] [[package]] @@ -2226,6 +2641,12 @@ dependencies = [ "libc", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "7.1.3" @@ -2367,6 +2788,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc-sys" version = "0.2.0-beta.2" @@ -2402,6 +2834,15 @@ dependencies = [ "cc", ] +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "object" version = "0.32.1" @@ -2564,6 +3005,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "pretty-type-name" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f73cdaf19b52e6143685c3606206e114a4dfa969d6b14ec3894c88eb38bd4b" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2851,11 +3298,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "studio" version = "0.1.0" dependencies = [ "bevy", + "bevy-inspector-egui", + "bevy_mod_outline", + "bevy_mod_picking", "console_error_panic_hook", "parking_lot", "wasm-bindgen", @@ -2974,6 +3430,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -3095,12 +3562,27 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.1.11" @@ -3113,6 +3595,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "uuid" version = "1.4.1" @@ -3250,6 +3743,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webbrowser" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b2391658b02c27719fc5a0a73d6e696285138e8b12fba9d4baa70451023c71" +dependencies = [ + "core-foundation", + "home", + "jni 0.21.1", + "log", + "ndk-context", + "objc", + "raw-window-handle", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "wgpu" version = "0.16.3" @@ -3381,6 +3897,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -3639,6 +4164,28 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname", + "nix 0.24.3", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.3", +] + [[package]] name = "xi-unicode" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index f6fa3ac..70f9ace 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "./bevy_mod_outline", "./studio" ] diff --git a/bevy_mod_outline/CHANGELOG.md b/bevy_mod_outline/CHANGELOG.md new file mode 100644 index 0000000..30de657 --- /dev/null +++ b/bevy_mod_outline/CHANGELOG.md @@ -0,0 +1,137 @@ +# Changelog + +## bevy_mod_outline 0.5.0 (2023-08-14) + +### Changed +- Updated Bevy dependency to 0.11. (@ramirezmike and @zainthemaynnn) +- Removed panic if specialising mesh pipeline fails. (@arjo129) + +## bevy_mod_outline 0.4.2 (2023-05-30) + +### Fixed +- Fixed failures to propagate ComputedOutlineDepth when needed. +- Fixed Z-fighting between overlay and stencil with OpenGL wgpu back-end. + +## bevy_mod_outline 0.4.1 (2023-04-11) + +### Fixed +- Fixed panic if mesh has unused vertex indices. +- Fixed panic if the DepthMode hasn't propagated before rendering. + +### Changed +- Changed normal weighting to use vertex rather than face normals if available. + +## bevy_mod_outline 0.4.0 (2023-03-22) + +### Added +- Added enabled flag to OutlineStencil. +- Added hollow example. + +### Fixed +- Fixed outline depth propagating when inheritance not enabled. + +### Changed +- Updated Bevy dependency to 0.10. +- Changed outline normal generator to use face normals. + +## bevy_mod_outline 0.3.5 (2023-03-08) + +### Fixed +- Fixed regression breaking SetOutlineDepth::Real. + +## bevy_mod_outline 0.3.4 (2023-03-08) + +### Fixed +- Fixed texture format error when HDR is enabled. +- Fixed bad clipping of outlines behind the camera. + +## bevy_mod_outline 0.3.3 (2023-02-21) + +### Fixed +- Fixed SetOutlineDepth being ignored in some cases (@mxgrey). +- Fixed defaulting to SetOutlineDepth::Real in some cases. +- Fixed missing examples from README. + +## bevy_mod_outline 0.3.2 (2023-01-15) + +### Added +- Added support for (Outline)RenderLayers components (@mxgrey). +- Added render_layers example. + +## bevy_mod_outline 0.3.1 (2023-01-05) + +### Added +- Added Lerp impls for OutlineVolume and OutlineStencil. +- Added Real mode to SetOutlineDepth enum. + +## bevy_mod_outline 0.3.0 (2022-11-22) + +### Added +- Added ComputedOutlineDepth, SetOutlineDepth, and InheritOutlineDepth. +- Added offset field to OutlineStencil. +- Added pieces example. + +### Removed +- Removed align16 feature. + +### Fixed +- Fixed errant debug println in AutoGenerateOutlineNormalsPlugin. + +### Changed +- Updated Bevy dependency to 0.9. +- Renamed Outline component to OutlineVolume. + +## bevy_mod_outline 0.2.5 (2023-01-14) + +### Added +- Added support for (Outline)RenderLayers components (@mxgrey) (back-port from 0.3.2). +- Added render_layers example (back-port from 0.3.2). + +### Fixed +- Fixed errant debug println in AutoGenerateOutlineNormalsPlugin (back-port from 0.3.0). + +## bevy_mod_outline 0.2.4 (2022-10-12) + +### Fixed +- Fixed adding outlines to jointed (skinned) meshes. + +## bevy_mod_outline 0.2.3 (2022-08-28) + +### Added +- Added AutoGenerateOutlineNormalsPlugin. + +## bevy_mod_outline 0.2.2 (2022-08-23) + +### Added +- Added a feature flag to control the dependency on bevy_ui (@Shatur). +- Added a feature flag to control uniform struct alignment. + +### Fixed +- Fixed compilation on 32-bit platforms. +- Fixed insufficient alignment causing errors with WebGL. + +### Changed +- Removed dependency on bevy's monolithic render feature flag (@Shatur). + +## bevy_mod_outline 0.2.1 (2022-08-10) + +### Added +- Added a constant to expose the outline pass node name. + +### Fixed +- Fixed outlines causing UI to disappear when MSAA is enabled. +- Fixed bad derive allowing OutlineBundle to be inserted as a component. + +## bevy_mod_outline 0.2.0 (2022-08-09) + +### Added +- Added support for outline normals. + +### Changed +- Updated Bevy dependency to 0.8. +- Changed from rendering in main pass to separate pass. +- Changed from using asset handles to plain components. + +## bevy_mod_outline 0.1.0 (2022-06-14) + +- Initial release diff --git a/bevy_mod_outline/Cargo.toml b/bevy_mod_outline/Cargo.toml new file mode 100644 index 0000000..feb0449 --- /dev/null +++ b/bevy_mod_outline/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "bevy_mod_outline" +version = "0.5.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "A mesh outlining plugin for Bevy." +readme = "README.md" +homepage = "https://github.com/komadori/bevy_mod_outline/" +repository = "https://github.com/komadori/bevy_mod_outline/" +keywords = ["gamedev", "bevy", "outline"] +categories = ["game-engines", "rendering"] + +[dependencies] +bevy = { version = "0.11", default-features = false, features = [ + "bevy_asset", + "bevy_render", + "bevy_pbr", + "ktx2", + "tonemapping_luts", + "zstd", + "bevy_core_pipeline", +] } +bitfield = "0.14" +interpolation = "0.2" +thiserror = "1.0" +wgpu-types = "0.16.1" + +[dev-dependencies] +bevy = { version = "0.11", default-features = false, features = [ + "animation", + "bevy_gltf", + "bevy_pbr", + "bevy_scene", + "bevy_winit", + "png", + "x11", +] } diff --git a/bevy_mod_outline/LICENCE-APACHE b/bevy_mod_outline/LICENCE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/bevy_mod_outline/LICENCE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bevy_mod_outline/LICENCE-MIT b/bevy_mod_outline/LICENCE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/bevy_mod_outline/LICENCE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/bevy_mod_outline/README.md b/bevy_mod_outline/README.md new file mode 100644 index 0000000..3fc91c5 --- /dev/null +++ b/bevy_mod_outline/README.md @@ -0,0 +1,96 @@ +> **NOTE**: This is a temporary fork of [bevy_mod_outline](https://github.com/komandori/bevy_mod_outline) +> to fix [this issue](https://github.com/komadori/bevy_mod_outline/issues/19). +> +> This can be removed and replaced with the crates.io import once the issue is +> fixed upstream. + +# bevy_mod_outline + +[![crates.io](https://img.shields.io/crates/v/bevy_mod_outline.svg)](https://crates.io/crates/bevy_mod_outline) +[![docs](https://docs.rs/bevy_mod_outline/badge.svg)](https://docs.rs/bevy_mod_outline) + +![Screenshot of bevy_mod_outline's shapes example](https://github.com/bevyengine/bevy-assets/blob/main/Assets/3D/bevy_mod_outline.png?raw=true) + +This crate provides a Bevy plugin for drawing outlines around meshes using the +vertex extrusion method. + +## Dependency + +```toml +[dependencies] +bevy_mod_outline = "0.5" +``` + +## Examples + +A rotating cube and torus with opaque and transparent outlines. + +```shell +cargo run --example shapes +``` + +Multiple intersecting meshes sharing an outline plane. The outline stencil is offset to create +a gap between the object and its outline. + +```shell +cargo run --example pieces +``` + +Many instances of the same mesh, with two different outline configurations, flying towards the +camera. + +```shell +cargo run --example flying_objects +``` + +An outlined torus viewed through four cameras with different combinations of render layers +enabled. + +```shell +cargo run --example render_layers +``` + +An animated jointed glTF model with an outline. + +```shell +cargo run --example animated_fox +``` + +A glTF model with pre-baked outline normals. + +```shell +cargo run --example hollow +``` + +An animated morphing glTF model with an outline. + +```shell +cargo run --example morph_targets +``` + +## Versions + +| This Version | Bevy version | +|--------------|--------------| +| 0.5.x | 0.11.x | +| 0.4.x | 0.10.x | +| 0.3.x | 0.9.x | +| 0.2.x | 0.8.x | +| 0.1.x | 0.7.x | + +## Features + +- `bevy_ui` _(default)_ - Adds a render graph edge to prevent clashing with the +UI. This adds a dependency on the `bevy_ui` crate and can be disabled if it is +not used. + +## Licence + +This crate is licensed under the Apache License, Version 2.0 (see +LICENCE-APACHE or ) or the MIT +licence (see LICENCE-MIT or ), at your +option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. diff --git a/bevy_mod_outline/project.json b/bevy_mod_outline/project.json new file mode 100644 index 0000000..dbb92db --- /dev/null +++ b/bevy_mod_outline/project.json @@ -0,0 +1,30 @@ +{ + "name": "bevy_mod_outline", + "$schema": "../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "./bevy_mod_outline/src", + "targets": { + "build": { + "executor": "@nxrs/cargo:build", + "options": {}, + "configurations": { + "production": { + "release": true + } + } + }, + "test": { + "executor": "@nxrs/cargo:test", + "options": {} + }, + "lint": { + "executor": "@nxrs/cargo:clippy", + "options": { + "fix": false, + "failOnWarnings": true, + "noDeps": true + } + } + }, + "tags": [] +} diff --git a/bevy_mod_outline/src/computed.rs b/bevy_mod_outline/src/computed.rs new file mode 100644 index 0000000..abce7d6 --- /dev/null +++ b/bevy_mod_outline/src/computed.rs @@ -0,0 +1,108 @@ +use bevy::prelude::*; + +use crate::uniforms::DepthMode; + +/// A component for storing the computed depth at which the outline lies. +#[derive(Clone, Component, Default)] +pub struct ComputedOutlineDepth { + pub(crate) world_origin: Vec3, + pub(crate) depth_mode: DepthMode, + pub(crate) inherited: Option, +} + +/// A component which specifies how the outline depth should be computed. +#[derive(Clone, Component)] +#[non_exhaustive] +pub enum SetOutlineDepth { + /// A flat plane facing the camera and intersecting the specified point in model-space. + Flat { model_origin: Vec3 }, + /// Real model-space. + Real, +} + +/// A component which specifies that this outline lies at the same depth as its parent. +#[derive(Clone, Component, Default)] +pub struct InheritOutlineDepth; + +#[allow(clippy::type_complexity)] +pub(crate) fn compute_outline_depth( + mut root_query: Query< + ( + Entity, + &mut ComputedOutlineDepth, + &GlobalTransform, + Changed, + Option<(&SetOutlineDepth, Changed)>, + Option<&Children>, + ), + Without, + >, + mut computed_query: Query<&mut ComputedOutlineDepth, With>, + child_query: Query<&Children>, +) { + for (entity, mut computed, transform, changed_transform, set_depth, children) in + root_query.iter_mut() + { + let changed = !computed.depth_mode.is_valid() + || computed.inherited.is_some() + || changed_transform + || set_depth.filter(|(_, c)| *c).is_some(); + if changed { + let (origin, depth_mode) = if let Some((sd, _)) = set_depth { + match sd { + SetOutlineDepth::Flat { + model_origin: origin, + } => (*origin, DepthMode::Flat), + SetOutlineDepth::Real => (Vec3::NAN, DepthMode::Real), + } + } else { + (Vec3::ZERO, DepthMode::Flat) + }; + let matrix = transform.compute_matrix(); + computed.world_origin = matrix.project_point3(origin); + computed.depth_mode = depth_mode; + computed.inherited = None; + } + if let Some(cs) = children { + for child in cs.iter() { + propagate_outline_depth( + &computed, + changed, + entity, + *child, + &mut computed_query, + &child_query, + ); + } + } + } +} + +fn propagate_outline_depth( + root_computed: &ComputedOutlineDepth, + mut changed: bool, + parent: Entity, + entity: Entity, + computed_query: &mut Query<&mut ComputedOutlineDepth, With>, + child_query: &Query<&Children>, +) { + if let Ok(mut computed) = computed_query.get_mut(entity) { + changed |= !computed.depth_mode.is_valid() | (computed.inherited != Some(parent)); + if changed { + *computed = root_computed.clone(); + computed.inherited = Some(parent); + } + if let Ok(cs) = child_query.get(entity) { + for child in cs.iter() { + propagate_outline_depth( + root_computed, + changed, + entity, + *child, + computed_query, + child_query, + ); + } + } + } +} diff --git a/bevy_mod_outline/src/draw.rs b/bevy_mod_outline/src/draw.rs new file mode 100644 index 0000000..902c443 --- /dev/null +++ b/bevy_mod_outline/src/draw.rs @@ -0,0 +1,194 @@ +use bevy::pbr::{DrawMesh, SetMeshBindGroup, SetMeshViewBindGroup}; +use bevy::prelude::*; +use bevy::render::render_asset::RenderAssets; +use bevy::render::render_phase::{DrawFunctions, RenderPhase, SetItemPipeline}; +use bevy::render::render_resource::{PipelineCache, SpecializedMeshPipelines}; +use bevy::render::renderer::RenderAdapterInfo; +use bevy::render::view::{ExtractedView, RenderLayers}; +use wgpu_types::Backend; + +use crate::node::{OpaqueOutline, StencilOutline, TransparentOutline}; +use crate::pipeline::{OutlinePipeline, PassType, PipelineKey}; +use crate::uniforms::{ + DepthMode, OutlineFragmentUniform, OutlineStencilFlags, OutlineStencilUniform, + OutlineVolumeFlags, OutlineVolumeUniform, SetOutlineStencilBindGroup, + SetOutlineVolumeBindGroup, +}; +use crate::view_uniforms::SetOutlineViewBindGroup; +use crate::OutlineRenderLayers; + +pub(crate) type DrawStencil = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshBindGroup<1>, + SetOutlineViewBindGroup<2>, + SetOutlineStencilBindGroup<3>, + DrawMesh, +); + +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +pub(crate) fn queue_outline_stencil_mesh( + stencil_draw_functions: Res>, + stencil_pipeline: Res, + msaa: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + render_meshes: Res>, + adapter_info: Res, + material_meshes: Query<( + Entity, + &Handle, + &OutlineStencilUniform, + &OutlineStencilFlags, + &OutlineRenderLayers, + )>, + mut views: Query<( + &ExtractedView, + &mut RenderPhase, + Option<&RenderLayers>, + )>, +) { + let draw_stencil = stencil_draw_functions + .read() + .get_id::() + .unwrap(); + + let base_key = PipelineKey::new() + .with_msaa(*msaa) + .with_pass_type(PassType::Stencil) + .with_opengl_workaround(adapter_info.0.backend == Backend::Gl); + + for (view, mut stencil_phase, view_mask) in views.iter_mut() { + let rangefinder = view.rangefinder3d(); + let view_mask = view_mask.copied().unwrap_or_default(); + for (entity, mesh_handle, stencil_uniform, stencil_flags, outline_mask) in + material_meshes.iter() + { + if !view_mask.intersects(outline_mask) { + continue; // Layer not enabled + } + if stencil_flags.depth_mode == DepthMode::Invalid { + continue; // DepthMode not propagated + } + let Some(mesh) = render_meshes.get(mesh_handle) else { + continue; // No mesh + }; + let key = base_key + .with_primitive_topology(mesh.primitive_topology) + .with_depth_mode(stencil_flags.depth_mode) + .with_offset_zero(stencil_uniform.offset == 0.0) + .with_morph_targets(mesh.morph_targets.is_some()); + let Ok(pipeline) = + pipelines.specialize(&pipeline_cache, &stencil_pipeline, key, &mesh.layout) + else { + continue; // No pipeline + }; + let distance = rangefinder.distance(&Mat4::from_translation(stencil_uniform.origin)); + stencil_phase.add(StencilOutline { + entity, + pipeline, + draw_function: draw_stencil, + distance, + }); + } + } +} + +pub(crate) type DrawOutline = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshBindGroup<1>, + SetOutlineViewBindGroup<2>, + SetOutlineVolumeBindGroup<3>, + DrawMesh, +); + +#[allow(clippy::too_many_arguments, clippy::type_complexity)] +pub(crate) fn queue_outline_volume_mesh( + opaque_draw_functions: Res>, + transparent_draw_functions: Res>, + outline_pipeline: Res, + msaa: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + render_meshes: Res>, + adapter_info: Res, + material_meshes: Query<( + Entity, + &Handle, + &OutlineVolumeUniform, + &OutlineVolumeFlags, + &OutlineFragmentUniform, + &OutlineRenderLayers, + )>, + mut views: Query<( + &ExtractedView, + &mut RenderPhase, + &mut RenderPhase, + Option<&RenderLayers>, + )>, +) { + let draw_opaque_outline = opaque_draw_functions + .read() + .get_id::() + .unwrap(); + let draw_transparent_outline = transparent_draw_functions + .read() + .get_id::() + .unwrap(); + + let base_key = PipelineKey::new() + .with_msaa(*msaa) + .with_opengl_workaround(adapter_info.0.backend == Backend::Gl); + + for (view, mut opaque_phase, mut transparent_phase, view_mask) in views.iter_mut() { + let view_mask = view_mask.copied().unwrap_or_default(); + let rangefinder = view.rangefinder3d(); + for (entity, mesh_handle, volume_uniform, volume_flags, fragment_uniform, outline_mask) in + material_meshes.iter() + { + if !view_mask.intersects(outline_mask) { + continue; // Layer not enabled + } + if volume_flags.depth_mode == DepthMode::Invalid { + continue; // DepthMode not propagated + } + let Some(mesh) = render_meshes.get(mesh_handle) else { + continue; // No mesh + }; + let transparent = fragment_uniform.colour[3] < 1.0; + let key = base_key + .with_primitive_topology(mesh.primitive_topology) + .with_pass_type(if transparent { + PassType::Transparent + } else { + PassType::Opaque + }) + .with_depth_mode(volume_flags.depth_mode) + .with_offset_zero(volume_uniform.offset == 0.0) + .with_hdr_format(view.hdr) + .with_morph_targets(mesh.morph_targets.is_some()); + let Ok(pipeline) = + pipelines.specialize(&pipeline_cache, &outline_pipeline, key, &mesh.layout) + else { + continue; // No pipeline + }; + let distance = rangefinder.distance(&Mat4::from_translation(volume_uniform.origin)); + if transparent { + transparent_phase.add(TransparentOutline { + entity, + pipeline, + draw_function: draw_transparent_outline, + distance, + }); + } else { + opaque_phase.add(OpaqueOutline { + entity, + pipeline, + draw_function: draw_opaque_outline, + distance, + }); + } + } + } +} diff --git a/bevy_mod_outline/src/fragment.wgsl b/bevy_mod_outline/src/fragment.wgsl new file mode 100644 index 0000000..4607f75 --- /dev/null +++ b/bevy_mod_outline/src/fragment.wgsl @@ -0,0 +1,32 @@ +struct FragmentOutput { + @location(0) colour: vec4, +#ifdef OPENGL_WORKAROUND + @builtin(frag_depth) frag_depth: f32, +#endif +}; + +struct OutlineFragmentUniform { + @align(16) + colour: vec4, +}; + +#ifdef VOLUME +@group(3) @binding(1) +var fstage: OutlineFragmentUniform; +#endif + +@fragment +#ifdef OPENGL_WORKAROUND +fn fragment(@location(0) normalised_depth: f32) -> FragmentOutput { +#else +fn fragment() -> FragmentOutput { +#endif + var out: FragmentOutput; +#ifdef VOLUME + out.colour = fstage.colour; +#endif +#ifdef OPENGL_WORKAROUND + out.frag_depth = normalised_depth; +#endif + return out; +} \ No newline at end of file diff --git a/bevy_mod_outline/src/generate.rs b/bevy_mod_outline/src/generate.rs new file mode 100644 index 0000000..862ebf7 --- /dev/null +++ b/bevy_mod_outline/src/generate.rs @@ -0,0 +1,173 @@ +use bevy::{ + prelude::*, + render::{ + mesh::{Indices, VertexAttributeValues}, + render_resource::{PrimitiveTopology, VertexFormat}, + }, + utils::{FloatOrd, HashMap, HashSet}, +}; + +use crate::ATTRIBUTE_OUTLINE_NORMAL; + +enum IndexIterator<'a> { + ExplicitU16(std::slice::Iter<'a, u16>), + ExplicitU32(std::slice::Iter<'a, u32>), + Implicit(std::ops::Range), +} + +impl<'a> From<&'a Mesh> for IndexIterator<'a> { + fn from(value: &'a Mesh) -> Self { + match value.indices() { + Some(Indices::U16(vec)) => IndexIterator::ExplicitU16(vec.iter()), + Some(Indices::U32(vec)) => IndexIterator::ExplicitU32(vec.iter()), + None => IndexIterator::Implicit(0..value.count_vertices()), + } + } +} + +impl Iterator for IndexIterator<'_> { + type Item = usize; + + fn next(&mut self) -> Option { + match self { + IndexIterator::ExplicitU16(iter) => iter.next().map(|val| *val as usize), + IndexIterator::ExplicitU32(iter) => iter.next().map(|val| *val as usize), + IndexIterator::Implicit(iter) => iter.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + match self { + IndexIterator::ExplicitU16(iter) => iter.size_hint(), + IndexIterator::ExplicitU32(iter) => iter.size_hint(), + IndexIterator::Implicit(iter) => iter.size_hint(), + } + } +} + +impl ExactSizeIterator for IndexIterator<'_> {} + +/// Failed to generate outline normals for the mesh. +#[derive(thiserror::Error, Debug)] +pub enum GenerateOutlineNormalsError { + #[error("unsupported primitive topology '{0:?}'")] + UnsupportedPrimitiveTopology(PrimitiveTopology), + #[error("missing vertex attributes '{0}'")] + MissingVertexAttribute(&'static str), + #[error("the '{0}' vertex attribute should have {1:?} format, but had {2:?} format")] + InvalidVertexAttributeFormat(&'static str, VertexFormat, VertexFormat), +} + +/// Extension methods for [`Mesh`]. +pub trait OutlineMeshExt { + /// Generates outline normals for the mesh. + /// + /// Vertex extrusion only works for meshes with smooth surface normals. Hard edges cause + /// visual artefacts. This function generates faux-smooth normals for outlining purposes + /// by grouping vertices by their position and averaging the normals at each point. These + /// outline normals are then inserted as a separate vertex attribute so that the regular + /// normals remain untouched. However, insofar as the outline normals are not + /// perpendicular to the surface of the mesh, this technique may result in non-uniform + /// outline thickness. + /// + /// This function only supports meshes with TriangleList topology. + fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError>; +} + +impl OutlineMeshExt for Mesh { + fn generate_outline_normals(&mut self) -> Result<(), GenerateOutlineNormalsError> { + if self.primitive_topology() != PrimitiveTopology::TriangleList { + return Err(GenerateOutlineNormalsError::UnsupportedPrimitiveTopology( + self.primitive_topology(), + )); + } + let positions = match self.attribute(Mesh::ATTRIBUTE_POSITION).ok_or( + GenerateOutlineNormalsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name), + )? { + VertexAttributeValues::Float32x3(p) => Ok(p), + v => Err(GenerateOutlineNormalsError::InvalidVertexAttributeFormat( + Mesh::ATTRIBUTE_POSITION.name, + VertexFormat::Float32x3, + v.into(), + )), + }?; + let normals = match self.attribute(Mesh::ATTRIBUTE_NORMAL) { + Some(VertexAttributeValues::Float32x3(p)) => Some(p), + _ => None, + }; + let mut map = HashMap::<[FloatOrd; 3], Vec3>::with_capacity(positions.len()); + let mut it = IndexIterator::from(&*self); + while let (Some(i0), Some(i1), Some(i2)) = (it.next(), it.next(), it.next()) { + for (j0, j1, j2) in [(i0, i1, i2), (i1, i2, i0), (i2, i0, i1)] { + let p0 = Vec3::from(positions[j0]); + let p1 = Vec3::from(positions[j1]); + let p2 = Vec3::from(positions[j2]); + let angle = (p1 - p0).angle_between(p2 - p0); + let n = map + .entry([FloatOrd(p0.x), FloatOrd(p0.y), FloatOrd(p0.z)]) + .or_default(); + *n += angle + * if let Some(ns) = normals { + // Use vertex normal + Vec3::from(ns[j0]) + } else { + // Calculate face normal + (p1 - p0).cross(p2 - p0).normalize_or_zero() + }; + } + } + let mut outlines = Vec::with_capacity(positions.len()); + for p in positions.iter() { + let key = [FloatOrd(p[0]), FloatOrd(p[1]), FloatOrd(p[2])]; + outlines.push( + map.get(&key) + .copied() + .unwrap_or(Vec3::ZERO) + .normalize_or_zero() + .to_array(), + ); + } + self.insert_attribute( + ATTRIBUTE_OUTLINE_NORMAL, + VertexAttributeValues::Float32x3(outlines), + ); + Ok(()) + } +} + +fn auto_generate_outline_normals( + mut meshes: ResMut>, + mut events: EventReader<'_, '_, AssetEvent>, + mut squelch: Local>>, +) { + for event in events.iter() { + match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + if squelch.contains(handle) { + // Suppress modification events created by this system + squelch.remove(handle); + } else if let Some(mesh) = meshes.get_mut(handle) { + let _ = mesh.generate_outline_normals(); + squelch.insert(handle.clone_weak()); + } + } + AssetEvent::Removed { handle } => { + squelch.remove(handle); + } + } + } +} + +/// Automatically runs [`generate_outline_normals`](OutlineMeshExt::generate_outline_normals) +/// on every mesh. +/// +/// This is provided as a convenience for simple projects. It runs the outline normal +/// generator every time a mesh asset is created or modified without consideration for +/// whether this is necessary or appropriate. +pub struct AutoGenerateOutlineNormalsPlugin; + +impl Plugin for AutoGenerateOutlineNormalsPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, auto_generate_outline_normals); + } +} diff --git a/bevy_mod_outline/src/lib.rs b/bevy_mod_outline/src/lib.rs new file mode 100644 index 0000000..8f14bf5 --- /dev/null +++ b/bevy_mod_outline/src/lib.rs @@ -0,0 +1,274 @@ +//! This crate provides a Bevy plugin, [`OutlinePlugin`], and associated components for +//! rendering outlines around meshes using the vertex extrusion method. +//! +//! Outlines are rendered in a seperate pass following the main 3D pass. The effect of this +//! pass is to present the outlines in depth sorted order according to the model translation +//! of each mesh. This ensures that outlines are not clipped by other geometry. +//! +//! The [`OutlineVolume`] component will, by itself, cover the original object entirely with +//! the outline colour. The [`OutlineStencil`] component must also be added to prevent the body +//! of an object from being filled it. This must be added to any entity which needs to appear on +//! top of an outline. +//! +//! The [`OutlineBundle`] and [`OutlineStencilBundle`] bundles can be used to add the right +//! components, including the required [`ComputedOutlineDepth`] component. Optionally, the +//! [`SetOutlineDepth`] and [`InheritOutlineDepth`] components may also be added to control the +//! depth ordering of outlines. +//! +//! Vertex extrusion works best with meshes that have smooth surfaces. To avoid visual +//! artefacts when outlining meshes with hard edges, see the +//! [`OutlineMeshExt::generate_outline_normals`] function and the +//! [`AutoGenerateOutlineNormalsPlugin`]. + +use bevy::asset::load_internal_asset; +use bevy::prelude::*; +use bevy::render::extract_component::{ + ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, +}; +use bevy::render::mesh::MeshVertexAttribute; +use bevy::render::render_graph::RenderGraph; +use bevy::render::render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions}; +use bevy::render::render_resource::{SpecializedMeshPipelines, VertexFormat}; +use bevy::render::view::RenderLayers; +use bevy::render::{Render, RenderApp, RenderSet}; +use bevy::transform::TransformSystem; +use interpolation::Lerp; + +use crate::draw::{ + queue_outline_stencil_mesh, queue_outline_volume_mesh, DrawOutline, DrawStencil, +}; +use crate::node::{OpaqueOutline, OutlineNode, StencilOutline, TransparentOutline}; +use crate::pipeline::{OutlinePipeline, FRAGMENT_SHADER_HANDLE, OUTLINE_SHADER_HANDLE}; +use crate::uniforms::{ + extract_outline_stencil_uniforms, extract_outline_volume_uniforms, + queue_outline_stencil_bind_group, queue_outline_volume_bind_group, OutlineFragmentUniform, + OutlineStencilUniform, OutlineVolumeUniform, +}; +use crate::view_uniforms::{ + extract_outline_view_uniforms, queue_outline_view_bind_group, OutlineViewUniform, +}; + +mod computed; +mod draw; +mod generate; +mod node; +mod pipeline; +mod uniforms; +mod view_uniforms; + +pub use computed::*; +pub use generate::*; + +// See https://alexanderameye.github.io/notes/rendering-outlines/ + +/// The direction to extrude the vertex when rendering the outline. +pub const ATTRIBUTE_OUTLINE_NORMAL: MeshVertexAttribute = + MeshVertexAttribute::new("Outline_Normal", 1585570526, VertexFormat::Float32x3); + +/// Name of the render graph node which draws the outlines. +/// +/// This node runs after the main 3D passes and before the UI pass. The name can be used to +/// add additional constraints on node execution order with respect to other passes. +pub const OUTLINE_PASS_NODE_NAME: &str = "bevy_mod_outline_node"; + +/// A component for stenciling meshes during outline rendering. +#[derive(Clone, Component)] +pub struct OutlineStencil { + /// Enable rendering of the stencil + pub enabled: bool, + /// Offset of the stencil in logical pixels + pub offset: f32, +} + +impl Default for OutlineStencil { + fn default() -> Self { + OutlineStencil { + enabled: true, + offset: 0.0, + } + } +} + +fn lerp_bool(this: bool, other: bool, scalar: f32) -> bool { + if scalar <= 0.0 { + this + } else if scalar >= 1.0 { + other + } else { + this | other + } +} + +impl Lerp for OutlineStencil { + type Scalar = f32; + + fn lerp(&self, other: &Self, scalar: &Self::Scalar) -> Self { + OutlineStencil { + enabled: lerp_bool(self.enabled, other.enabled, *scalar), + offset: self.offset.lerp(&other.offset, scalar), + } + } +} + +/// A component for rendering outlines around meshes. +#[derive(Clone, Component, Default)] +pub struct OutlineVolume { + /// Enable rendering of the outline + pub visible: bool, + /// Width of the outline in logical pixels + pub width: f32, + /// Colour of the outline + pub colour: Color, +} + +impl Lerp for OutlineVolume { + type Scalar = f32; + + fn lerp(&self, other: &Self, scalar: &Self::Scalar) -> Self { + OutlineVolume { + visible: lerp_bool(self.visible, other.visible, *scalar), + width: self.width.lerp(&other.width, scalar), + colour: { + let [r, g, b, a] = self + .colour + .as_linear_rgba_f32() + .lerp(&other.colour.as_linear_rgba_f32(), scalar); + Color::rgba_linear(r, g, b, a) + }, + } + } +} + +/// A component for specifying what layer(s) the outline should be rendered for. +#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, Default)] +pub struct OutlineRenderLayers(pub RenderLayers); + +impl ExtractComponent for OutlineRenderLayers { + type Query = ( + Option<&'static OutlineRenderLayers>, + Option<&'static RenderLayers>, + ); + type Filter = Or<(With, With)>; + type Out = Self; + + fn extract_component( + (outline_mask, object_mask): (Option<&OutlineRenderLayers>, Option<&RenderLayers>), + ) -> Option { + Some( + outline_mask + .copied() + .or_else(|| object_mask.copied().map(OutlineRenderLayers)) + .unwrap_or_default(), + ) + } +} + +/// A bundle for rendering stenciled outlines around meshes. +#[derive(Bundle, Clone, Default)] +pub struct OutlineBundle { + pub outline: OutlineVolume, + pub stencil: OutlineStencil, + pub plane: ComputedOutlineDepth, +} + +/// A bundle for stenciling meshes in the outlining pass. +#[derive(Bundle, Clone, Default)] +pub struct OutlineStencilBundle { + pub stencil: OutlineStencil, + pub plane: ComputedOutlineDepth, +} + +/// Adds support for rendering outlines. +pub struct OutlinePlugin; + +impl Plugin for OutlinePlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + OUTLINE_SHADER_HANDLE, + "outline.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + FRAGMENT_SHADER_HANDLE, + "fragment.wgsl", + Shader::from_wgsl + ); + + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )) + .add_systems( + PostUpdate, + compute_outline_depth.after(TransformSystem::TransformPropagate), + ) + .sub_app_mut(RenderApp) + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_systems(ExtractSchedule, extract_outline_view_uniforms) + .add_systems(ExtractSchedule, extract_outline_stencil_uniforms) + .add_systems(ExtractSchedule, extract_outline_volume_uniforms) + .add_systems( + Render, + sort_phase_system::.in_set(RenderSet::PhaseSort), + ) + .add_systems( + Render, + sort_phase_system::.in_set(RenderSet::PhaseSort), + ) + .add_systems( + Render, + sort_phase_system::.in_set(RenderSet::PhaseSort), + ) + .add_systems( + Render, + queue_outline_view_bind_group.in_set(RenderSet::Queue), + ) + .add_systems( + Render, + queue_outline_stencil_bind_group.in_set(RenderSet::Queue), + ) + .add_systems( + Render, + queue_outline_volume_bind_group.in_set(RenderSet::Queue), + ) + .add_systems(Render, queue_outline_stencil_mesh.in_set(RenderSet::Queue)) + .add_systems(Render, queue_outline_volume_mesh.in_set(RenderSet::Queue)); + + let world = &mut app.sub_app_mut(RenderApp).world; + let node = OutlineNode::new(world); + + let mut graph = world.resource_mut::(); + + let draw_3d_graph = graph + .get_sub_graph_mut(bevy::core_pipeline::core_3d::graph::NAME) + .unwrap(); + draw_3d_graph.add_node(OUTLINE_PASS_NODE_NAME, node); + + // Run after main 3D pass, but before UI psss + draw_3d_graph.add_node_edge( + bevy::core_pipeline::core_3d::graph::node::END_MAIN_PASS, + OUTLINE_PASS_NODE_NAME, + ); + #[cfg(feature = "bevy_ui")] + draw_3d_graph.add_node_edge( + OUTLINE_PASS_NODE_NAME, + bevy::ui::draw_ui_graph::node::UI_PASS, + ); + } + + fn finish(&self, app: &mut App) { + app.sub_app_mut(RenderApp) + .init_resource::(); + } +} diff --git a/bevy_mod_outline/src/node.rs b/bevy_mod_outline/src/node.rs new file mode 100644 index 0000000..b39624d --- /dev/null +++ b/bevy_mod_outline/src/node.rs @@ -0,0 +1,227 @@ +use std::cmp::Reverse; + +use bevy::ecs::system::lifetimeless::Read; +use bevy::prelude::*; +use bevy::render::camera::ExtractedCamera; +use bevy::render::render_graph::NodeRunError; +use bevy::render::render_phase::{ + CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem, RenderPhase, +}; +use bevy::render::render_resource::{ + CachedRenderPipelineId, LoadOp, Operations, RenderPassDepthStencilAttachment, + RenderPassDescriptor, +}; +use bevy::render::view::{ExtractedView, ViewDepthTexture, ViewTarget}; +use bevy::render::{ + render_graph::{Node, RenderGraphContext}, + renderer::RenderContext, +}; +use bevy::utils::FloatOrd; + +pub(crate) struct StencilOutline { + pub distance: f32, + pub pipeline: CachedRenderPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for StencilOutline { + type SortKey = Reverse; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + fn sort_key(&self) -> Self::SortKey { + Reverse(FloatOrd(self.distance)) + } + + fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId { + self.draw_function + } +} + +impl CachedRenderPipelinePhaseItem for StencilOutline { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +pub(crate) struct OpaqueOutline { + pub distance: f32, + pub pipeline: CachedRenderPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for OpaqueOutline { + type SortKey = Reverse; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + fn sort_key(&self) -> Self::SortKey { + Reverse(FloatOrd(self.distance)) + } + + fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId { + self.draw_function + } +} + +impl CachedRenderPipelinePhaseItem for OpaqueOutline { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +pub(crate) struct TransparentOutline { + pub distance: f32, + pub pipeline: CachedRenderPipelineId, + pub entity: Entity, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for TransparentOutline { + type SortKey = FloatOrd; + + #[inline] + fn entity(&self) -> Entity { + self.entity + } + + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + fn draw_function(&self) -> bevy::render::render_phase::DrawFunctionId { + self.draw_function + } +} + +impl CachedRenderPipelinePhaseItem for TransparentOutline { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +#[allow(clippy::type_complexity)] +pub(crate) struct OutlineNode { + query: QueryState< + ( + Read, + Read>, + Read>, + Read>, + Read, + Read, + Read, + ), + With, + >, +} + +impl OutlineNode { + pub(crate) fn new(world: &mut World) -> Self { + Self { + query: world.query_filtered(), + } + } +} + +impl Node for OutlineNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + let (camera, stencil_phase, opaque_phase, transparent_phase, camera_3d, target, depth) = + match self.query.get_manual(world, view_entity) { + Ok(query) => query, + Err(_) => { + return Ok(()); + } // No window + }; + + // If drawing anything, run stencil pass to clear the depth buffer + if !opaque_phase.items.is_empty() || !transparent_phase.items.is_empty() { + let pass_descriptor = RenderPassDescriptor { + label: Some("outline_stencil_pass"), + color_attachments: &[], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(Operations { + load: camera_3d.depth_load_op.clone().into(), + store: true, + }), + stencil_ops: None, + }), + }; + let mut tracked_pass = render_context.begin_tracked_render_pass(pass_descriptor); + if let Some(viewport) = camera.viewport.as_ref() { + tracked_pass.set_camera_viewport(viewport); + } + stencil_phase.render(&mut tracked_pass, world, view_entity); + } + + if !opaque_phase.items.is_empty() { + let pass_descriptor = RenderPassDescriptor { + label: Some("outline_opaque_pass"), + color_attachments: &[Some(target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }; + let mut tracked_pass = render_context.begin_tracked_render_pass(pass_descriptor); + if let Some(viewport) = camera.viewport.as_ref() { + tracked_pass.set_camera_viewport(viewport); + } + opaque_phase.render(&mut tracked_pass, world, view_entity); + } + + if !transparent_phase.items.is_empty() { + let pass_descriptor = RenderPassDescriptor { + label: Some("outline_transparent_pass"), + color_attachments: &[Some(target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: true, + }), + stencil_ops: None, + }), + }; + let mut tracked_pass = render_context.begin_tracked_render_pass(pass_descriptor); + if let Some(viewport) = camera.viewport.as_ref() { + tracked_pass.set_camera_viewport(viewport); + } + transparent_phase.render(&mut tracked_pass, world, view_entity); + } + + Ok(()) + } +} diff --git a/bevy_mod_outline/src/outline.wgsl b/bevy_mod_outline/src/outline.wgsl new file mode 100644 index 0000000..cd1587d --- /dev/null +++ b/bevy_mod_outline/src/outline.wgsl @@ -0,0 +1,114 @@ +#import bevy_render::view View +#import bevy_pbr::mesh_types Mesh +#import bevy_pbr::mesh_types SkinnedMesh + +struct Vertex { +#ifdef VERTEX_POSITIONS + @location(0) position: vec3, +#endif +#ifndef OFFSET_ZERO + @location(1) outline_normal: vec3, +#endif +#ifdef SKINNED + @location(5) joint_indices: vec4, + @location(6) joint_weights: vec4, +#endif +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif +}; + +struct VertexOutput { + @builtin(position) position: vec4, +#ifdef OPENGL_WORKAROUND + @location(0) normalised_depth: f32, +#endif +}; + +struct OutlineViewUniform { + @align(16) + scale: vec2, +}; + +struct OutlineVertexUniform { + @align(16) + origin: vec3, + offset: f32, +}; + +@group(0) @binding(0) +var view: View; + +@group(1) @binding(0) +var mesh: Mesh; + +#import bevy_pbr::skinning +#import bevy_pbr::morph + +@group(2) @binding(0) +var view_uniform: OutlineViewUniform; + +@group(3) @binding(0) +var vstage: OutlineVertexUniform; + +#ifdef MORPH_TARGETS +fn morph_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let weight_count = bevy_pbr::morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = bevy_pbr::morph::weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * bevy_pbr::morph::morph(vertex.index, bevy_pbr::morph::position_offset, i); + } + return vertex; +} +#endif + +fn mat4to3(m: mat4x4) -> mat3x3 { + return mat3x3( + m[0].xyz, m[1].xyz, m[2].xyz + ); +} + +fn model_origin_z(plane: vec3, view_proj: mat4x4) -> f32 { + var proj_zw = mat4x2( + view_proj[0].zw, view_proj[1].zw, + view_proj[2].zw, view_proj[3].zw); + var zw = proj_zw * vec4(plane, 1.0); + return zw.x / zw.y; +} + +@vertex +fn vertex(vertex_no_morph: Vertex) -> VertexOutput { +#ifdef MORPH_TARGETS + var vertex = morph_vertex(vertex_no_morph); +#else + var vertex = vertex_no_morph; +#endif +#ifdef SKINNED + let model = bevy_pbr::skinning::skin_model(vertex.joint_indices, vertex.joint_weights); +#else + let model = mesh.model; +#endif + let clip_pos = view.view_proj * (model * vec4(vertex.position, 1.0)); +#ifdef FLAT_DEPTH + let out_z = model_origin_z(vstage.origin, view.view_proj) * clip_pos.w; +#else + let out_z = clip_pos.z; +#endif +#ifdef OFFSET_ZERO + let out_xy = clip_pos.xy; +#else + let clip_norm = mat4to3(view.view_proj) * (mat4to3(model) * vertex.outline_normal); + let ndc_delta = vstage.offset * normalize(clip_norm.xy) * view_uniform.scale * clip_pos.w; + let out_xy = clip_pos.xy + ndc_delta; +#endif + var out: VertexOutput; + out.position = vec4(out_xy, out_z, clip_pos.w); +#ifdef OPENGL_WORKAROUND + out.normalised_depth = 10.0 + 0.5 * (out_z / clip_pos.w); +#endif + return out; +} diff --git a/bevy_mod_outline/src/pipeline.rs b/bevy_mod_outline/src/pipeline.rs new file mode 100644 index 0000000..10e8f69 --- /dev/null +++ b/bevy_mod_outline/src/pipeline.rs @@ -0,0 +1,369 @@ +use std::borrow::Cow; + +use bevy::pbr::{setup_morph_and_skinning_defs, MeshPipelineKey}; +use bevy::prelude::*; +use bevy::reflect::TypeUuid; +use bevy::render::render_resource::{ + BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BlendState, + BufferBindingType, BufferSize, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, + DepthStencilState, Face, FragmentState, FrontFace, MultisampleState, PolygonMode, + PrimitiveState, PrimitiveTopology, ShaderDefVal, ShaderSize, ShaderStages, StencilState, + TextureFormat, VertexState, +}; +use bevy::render::renderer::RenderDevice; +use bevy::render::texture::BevyDefault; +use bevy::render::view::ViewTarget; +use bevy::{ + pbr::MeshPipeline, + render::{ + mesh::MeshVertexBufferLayout, + render_resource::{ + RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError, + }, + }, +}; +use bitfield::{bitfield_bitrange, bitfield_fields}; + +use crate::uniforms::{ + DepthMode, OutlineFragmentUniform, OutlineStencilUniform, OutlineVolumeUniform, +}; +use crate::view_uniforms::OutlineViewUniform; +use crate::ATTRIBUTE_OUTLINE_NORMAL; + +pub(crate) const OUTLINE_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2101625026478770097); + +pub(crate) const FRAGMENT_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 12033806834125368121); + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum PassType { + Stencil = 1, + Opaque = 2, + Transparent = 3, +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct PipelineKey(u32); +bitfield_bitrange! {struct PipelineKey(u32)} + +impl PipelineKey { + bitfield_fields! { + u32; + msaa_samples_minus_one, set_msaa_samples_minus_one: 5, 0; + primitive_topology_int, set_primitive_topology_int: 8, 6; + pass_type_int, set_pass_type_int: 10, 9; + depth_mode_int, set_depth_mode_int: 12, 11; + pub offset_zero, set_offset_zero: 13; + pub hdr_format, set_hdr_format: 14; + pub opengl_workaround, set_opengl_workaround: 15; + pub morph_targets, set_morph_targets: 16; + } + + pub(crate) fn new() -> Self { + PipelineKey(0) + } + + pub(crate) fn with_msaa(mut self, msaa: Msaa) -> Self { + self.set_msaa_samples_minus_one(msaa as u32 - 1); + self + } + + pub(crate) fn msaa(&self) -> Msaa { + match self.msaa_samples_minus_one() + 1 { + x if x == Msaa::Off as u32 => Msaa::Off, + x if x == Msaa::Sample2 as u32 => Msaa::Sample2, + x if x == Msaa::Sample4 as u32 => Msaa::Sample4, + x if x == Msaa::Sample8 as u32 => Msaa::Sample8, + x => panic!("Invalid value for Msaa: {}", x), + } + } + + pub(crate) fn with_primitive_topology(mut self, primitive_topology: PrimitiveTopology) -> Self { + self.set_primitive_topology_int(primitive_topology as u32); + self + } + + pub(crate) fn primitive_topology(&self) -> PrimitiveTopology { + match self.primitive_topology_int() { + x if x == PrimitiveTopology::PointList as u32 => PrimitiveTopology::PointList, + x if x == PrimitiveTopology::LineList as u32 => PrimitiveTopology::LineList, + x if x == PrimitiveTopology::LineStrip as u32 => PrimitiveTopology::LineStrip, + x if x == PrimitiveTopology::TriangleList as u32 => PrimitiveTopology::TriangleList, + x if x == PrimitiveTopology::TriangleStrip as u32 => PrimitiveTopology::TriangleStrip, + x => panic!("Invalid value for PrimitiveTopology: {}", x), + } + } + + pub(crate) fn with_pass_type(mut self, pass_type: PassType) -> Self { + self.set_pass_type_int(pass_type as u32); + self + } + + pub(crate) fn pass_type(&self) -> PassType { + match self.pass_type_int() { + x if x == PassType::Stencil as u32 => PassType::Stencil, + x if x == PassType::Opaque as u32 => PassType::Opaque, + x if x == PassType::Transparent as u32 => PassType::Transparent, + x => panic!("Invalid value for PassType: {}", x), + } + } + + pub(crate) fn with_depth_mode(mut self, depth_mode: DepthMode) -> Self { + self.set_depth_mode_int(depth_mode as u32); + self + } + + pub(crate) fn depth_mode(&self) -> DepthMode { + match self.depth_mode_int() { + x if x == DepthMode::Flat as u32 => DepthMode::Flat, + x if x == DepthMode::Real as u32 => DepthMode::Real, + x => panic!("Invalid value for DepthMode: {}", x), + } + } + + pub(crate) fn with_offset_zero(mut self, offset_zero: bool) -> Self { + self.set_offset_zero(offset_zero); + self + } + + pub(crate) fn with_hdr_format(mut self, hdr_format: bool) -> Self { + self.set_hdr_format(hdr_format); + self + } + + pub(crate) fn with_opengl_workaround(mut self, opengl_workaround: bool) -> Self { + self.set_opengl_workaround(opengl_workaround); + self + } + + pub(crate) fn with_morph_targets(mut self, morph_targets: bool) -> Self { + self.set_morph_targets(morph_targets); + self + } +} + +impl From for MeshPipelineKey { + fn from(key: PipelineKey) -> Self { + if key.morph_targets() { + MeshPipelineKey::empty() | MeshPipelineKey::MORPH_TARGETS + } else { + MeshPipelineKey::empty() + } + } +} + +#[derive(Resource)] +pub(crate) struct OutlinePipeline { + mesh_pipeline: MeshPipeline, + pub outline_view_bind_group_layout: BindGroupLayout, + pub outline_stencil_bind_group_layout: BindGroupLayout, + pub outline_volume_bind_group_layout: BindGroupLayout, +} + +impl FromWorld for OutlinePipeline { + fn from_world(world: &mut World) -> Self { + let world = world.cell(); + let mesh_pipeline = world.get_resource::().unwrap().clone(); + let render_device = world.get_resource::().unwrap(); + let outline_view_bind_group_layout = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("outline_view_bind_group_layout"), + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(OutlineViewUniform::SHADER_SIZE.get()), + }, + count: None, + }], + }); + let outline_volume_bind_group_layout = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("outline_volume_bind_group_layout"), + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new( + OutlineVolumeUniform::SHADER_SIZE.get(), + ), + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new( + OutlineFragmentUniform::SHADER_SIZE.get(), + ), + }, + count: None, + }, + ], + }); + let outline_stencil_bind_group_layout = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("outline_stencil_bind_group_layout"), + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(OutlineStencilUniform::SHADER_SIZE.get()), + }, + count: None, + }], + }); + OutlinePipeline { + mesh_pipeline, + outline_view_bind_group_layout, + outline_stencil_bind_group_layout, + outline_volume_bind_group_layout, + } + } +} + +impl SpecializedMeshPipeline for OutlinePipeline { + type Key = PipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayout, + ) -> Result { + let mut targets = vec![]; + let mut vertex_defs = vec!["MESH_BINDGROUP_1".into()]; + let mut fragment_defs = vec![]; + let mut buffer_attrs = Vec::new(); + + if layout.contains(Mesh::ATTRIBUTE_POSITION) { + vertex_defs.push("VERTEX_POSITIONS".into()); + buffer_attrs.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + + let mut bind_layouts = vec![if key.msaa() == Msaa::Off { + self.mesh_pipeline.view_layout.clone() + } else { + self.mesh_pipeline.view_layout_multisampled.clone() + }]; + + bind_layouts.push(setup_morph_and_skinning_defs( + &self.mesh_pipeline.mesh_layouts, + layout, + 5, + &key.into(), + &mut vertex_defs, + &mut buffer_attrs, + )); + + bind_layouts.push(self.outline_view_bind_group_layout.clone()); + let cull_mode; + if key.depth_mode() == DepthMode::Flat { + vertex_defs.push(ShaderDefVal::from("FLAT_DEPTH")); + cull_mode = Some(Face::Back); + } else if key.pass_type() == PassType::Stencil { + cull_mode = Some(Face::Back); + } else { + cull_mode = Some(Face::Front); + } + if key.offset_zero() { + vertex_defs.push(ShaderDefVal::from("OFFSET_ZERO")); + } else { + buffer_attrs.push( + if layout.contains(ATTRIBUTE_OUTLINE_NORMAL) { + ATTRIBUTE_OUTLINE_NORMAL + } else { + Mesh::ATTRIBUTE_NORMAL + } + .at_shader_location(1), + ); + } + match key.pass_type() { + PassType::Stencil => { + bind_layouts.push(self.outline_stencil_bind_group_layout.clone()); + } + PassType::Opaque | PassType::Transparent => { + fragment_defs.push(ShaderDefVal::from("VOLUME")); + targets.push(Some(ColorTargetState { + format: if key.hdr_format() { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: Some(if key.pass_type() == PassType::Transparent { + BlendState::ALPHA_BLENDING + } else { + BlendState::REPLACE + }), + write_mask: ColorWrites::ALL, + })); + + bind_layouts.push(self.outline_volume_bind_group_layout.clone()); + } + } + if key.opengl_workaround() { + let val = ShaderDefVal::from("OPENGL_WORKAROUND"); + vertex_defs.push(val.clone()); + fragment_defs.push(val); + } + let buffers = vec![layout.get_layout(&buffer_attrs)?]; + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: OUTLINE_SHADER_HANDLE.typed::(), + entry_point: "vertex".into(), + shader_defs: vertex_defs, + buffers, + }, + fragment: Some(FragmentState { + shader: FRAGMENT_SHADER_HANDLE.typed::(), + shader_defs: fragment_defs, + entry_point: "fragment".into(), + targets, + }), + layout: bind_layouts, + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: key.primitive_topology(), + strip_index_format: None, + }, + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: CompareFunction::Greater, + stencil: StencilState::default(), + bias: if key.depth_mode() == DepthMode::Flat && key.pass_type() == PassType::Stencil + { + DepthBiasState { + // Values determined empirically + constant: 3, + slope_scale: 1.0, + ..default() + } + } else { + default() + }, + }), + multisample: MultisampleState { + count: key.msaa().samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + push_constant_ranges: default(), + label: Some(Cow::Borrowed("outline_pipeline")), + }) + } +} diff --git a/bevy_mod_outline/src/uniforms.rs b/bevy_mod_outline/src/uniforms.rs new file mode 100644 index 0000000..3fc5e03 --- /dev/null +++ b/bevy_mod_outline/src/uniforms.rs @@ -0,0 +1,212 @@ +use bevy::{ + ecs::system::{ + lifetimeless::{Read, SRes}, + SystemParamItem, + }, + prelude::*, + render::{ + extract_component::{ComponentUniforms, DynamicUniformIndex}, + render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, + render_resource::{BindGroup, BindGroupDescriptor, BindGroupEntry, ShaderType}, + renderer::RenderDevice, + Extract, + }, +}; + +use crate::{ + node::StencilOutline, pipeline::OutlinePipeline, ComputedOutlineDepth, OutlineStencil, + OutlineVolume, +}; + +#[derive(Clone, Component, ShaderType)] +pub(crate) struct OutlineStencilUniform { + #[align(16)] + pub origin: Vec3, + pub offset: f32, +} + +#[derive(Clone, Component, ShaderType)] +pub(crate) struct OutlineVolumeUniform { + #[align(16)] + pub origin: Vec3, + pub offset: f32, +} + +#[derive(Clone, Component, ShaderType)] +pub(crate) struct OutlineFragmentUniform { + #[align(16)] + pub colour: Vec4, +} + +#[derive(Clone, Copy, Default, PartialEq)] +pub(crate) enum DepthMode { + #[default] + Invalid = 0, + Flat = 1, + Real = 2, +} + +impl DepthMode { + pub fn is_valid(&self) -> bool { + *self != DepthMode::Invalid + } +} + +#[derive(Component)] +pub(crate) struct OutlineStencilFlags { + pub depth_mode: DepthMode, +} + +#[derive(Component)] +pub(crate) struct OutlineVolumeFlags { + pub depth_mode: DepthMode, +} + +#[derive(Resource)] +pub(crate) struct OutlineStencilBindGroup { + pub bind_group: BindGroup, +} + +#[derive(Resource)] +pub(crate) struct OutlineVolumeBindGroup { + pub bind_group: BindGroup, +} + +pub(crate) fn extract_outline_stencil_uniforms( + mut commands: Commands, + query: Extract>, +) { + for (entity, stencil, computed) in query.iter() { + if !stencil.enabled { + continue; + } + commands + .get_or_spawn(entity) + .insert(OutlineStencilUniform { + origin: computed.world_origin, + offset: stencil.offset, + }) + .insert(OutlineStencilFlags { + depth_mode: computed.depth_mode, + }); + } +} + +pub(crate) fn extract_outline_volume_uniforms( + mut commands: Commands, + query: Extract>, +) { + for (entity, outline, computed) in query.iter() { + if !outline.visible || outline.colour.a() == 0.0 { + continue; + } + commands + .get_or_spawn(entity) + .insert(OutlineVolumeUniform { + origin: computed.world_origin, + offset: outline.width, + }) + .insert(OutlineFragmentUniform { + colour: outline.colour.as_linear_rgba_f32().into(), + }) + .insert(OutlineVolumeFlags { + depth_mode: computed.depth_mode, + }); + } +} + +pub(crate) fn queue_outline_stencil_bind_group( + mut commands: Commands, + render_device: Res, + outline_pipeline: Res, + vertex: Res>, +) { + if let Some(vertex_binding) = vertex.binding() { + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: vertex_binding.clone(), + }], + label: Some("outline_stencil_bind_group"), + layout: &outline_pipeline.outline_stencil_bind_group_layout, + }); + commands.insert_resource(OutlineStencilBindGroup { bind_group }); + } +} + +pub(crate) fn queue_outline_volume_bind_group( + mut commands: Commands, + render_device: Res, + outline_pipeline: Res, + vertex: Res>, + fragment: Res>, +) { + if let (Some(vertex_binding), Some(fragment_binding)) = (vertex.binding(), fragment.binding()) { + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: vertex_binding.clone(), + }, + BindGroupEntry { + binding: 1, + resource: fragment_binding.clone(), + }, + ], + label: Some("outline_volume_bind_group"), + layout: &outline_pipeline.outline_volume_bind_group_layout, + }); + commands.insert_resource(OutlineVolumeBindGroup { bind_group }); + } +} + +pub(crate) struct SetOutlineStencilBindGroup(); + +impl RenderCommand for SetOutlineStencilBindGroup { + type ViewWorldQuery = (); + type ItemWorldQuery = Read>; + type Param = SRes; + fn render<'w>( + _item: &StencilOutline, + _view_data: (), + entity_data: &DynamicUniformIndex, + bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group( + I, + &bind_group.into_inner().bind_group, + &[entity_data.index()], + ); + RenderCommandResult::Success + } +} + +pub(crate) struct SetOutlineVolumeBindGroup(); + +impl RenderCommand

for SetOutlineVolumeBindGroup { + type ViewWorldQuery = (); + type ItemWorldQuery = ( + Read>, + Read>, + ); + type Param = SRes; + fn render<'w>( + _item: &P, + _view_data: (), + entity_data: ( + &DynamicUniformIndex, + &DynamicUniformIndex, + ), + bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let (vertex, fragment) = entity_data; + pass.set_bind_group( + I, + &bind_group.into_inner().bind_group, + &[vertex.index(), fragment.index()], + ); + RenderCommandResult::Success + } +} diff --git a/bevy_mod_outline/src/view_uniforms.rs b/bevy_mod_outline/src/view_uniforms.rs new file mode 100644 index 0000000..cbd4968 --- /dev/null +++ b/bevy_mod_outline/src/view_uniforms.rs @@ -0,0 +1,87 @@ +use bevy::ecs::system::lifetimeless::{Read, SRes}; +use bevy::ecs::system::SystemParamItem; +use bevy::prelude::*; +use bevy::render::extract_component::{ComponentUniforms, DynamicUniformIndex}; +use bevy::render::render_phase::{ + PhaseItem, RenderCommand, RenderCommandResult, RenderPhase, TrackedRenderPass, +}; +use bevy::render::render_resource::ShaderType; +use bevy::render::render_resource::{BindGroup, BindGroupDescriptor, BindGroupEntry}; +use bevy::render::renderer::RenderDevice; +use bevy::render::view::RenderLayers; +use bevy::render::Extract; + +use crate::node::{OpaqueOutline, StencilOutline, TransparentOutline}; +use crate::pipeline::OutlinePipeline; + +#[derive(Clone, Component, ShaderType)] +pub(crate) struct OutlineViewUniform { + #[align(16)] + scale: Vec2, +} + +#[derive(Resource)] +pub(crate) struct OutlineViewBindGroup { + bind_group: BindGroup, +} + +#[allow(clippy::type_complexity)] +pub(crate) fn extract_outline_view_uniforms( + mut commands: Commands, + query: Extract), With>>, +) { + for (entity, camera, view_mask) in query.iter() { + if !camera.is_active { + continue; + } + if let Some(size) = camera.logical_viewport_size() { + let mut entity_commands = commands.get_or_spawn(entity); + entity_commands + .insert(OutlineViewUniform { scale: 2.0 / size }) + .insert(RenderPhase::::default()) + .insert(RenderPhase::::default()) + .insert(RenderPhase::::default()); + + if let Some(view_mask) = view_mask { + entity_commands.insert(*view_mask); + } + } + } +} + +pub(crate) fn queue_outline_view_bind_group( + mut commands: Commands, + render_device: Res, + outline_pipeline: Res, + view_uniforms: Res>, +) { + if let Some(view_binding) = view_uniforms.binding() { + let bind_group = render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: view_binding.clone(), + }], + label: Some("outline_view_bind_group"), + layout: &outline_pipeline.outline_view_bind_group_layout, + }); + commands.insert_resource(OutlineViewBindGroup { bind_group }); + } +} + +pub(crate) struct SetOutlineViewBindGroup(); + +impl RenderCommand

for SetOutlineViewBindGroup { + type ViewWorldQuery = Read>; + type ItemWorldQuery = (); + type Param = SRes; + fn render<'w>( + _item: &P, + view_data: &DynamicUniformIndex, + _entity_data: (), + bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &bind_group.into_inner().bind_group, &[view_data.index()]); + RenderCommandResult::Success + } +} diff --git a/studio-frontend/src/app/help.element.scss b/studio-frontend/src/app/help.element.scss index d5e1d79..e7b7159 100644 --- a/studio-frontend/src/app/help.element.scss +++ b/studio-frontend/src/app/help.element.scss @@ -16,9 +16,16 @@ th { text-align: start; - color: #FFF; - font-weight: 700; + color: #FFFC; + font-weight: 500; line-height: 2; + + &.section-header { + padding: 0.5em 0 0; + color: #FFFF; + font-weight: 700; + line-height: 1.6333333; + } } td { @@ -30,3 +37,7 @@ td { padding-right: 30px; } } + +th:not(.section-header), td { + padding-left: 20px; +} diff --git a/studio-frontend/src/app/help.element.ts b/studio-frontend/src/app/help.element.ts index 343c86f..d7c207b 100644 --- a/studio-frontend/src/app/help.element.ts +++ b/studio-frontend/src/app/help.element.ts @@ -10,6 +10,11 @@ export class HelpElement extends LitElement { protected override render = () => html` + + + @@ -32,11 +37,7 @@ export class HelpElement extends LitElement { - - - - - + @@ -44,6 +45,32 @@ export class HelpElement extends LitElement { + + + + + + + + + + + + + + + + + + + + + + + +
+ Navigation +
Action Controls
Free flyW, A, S, D
Up / DownE, QW, A, S, D, E, Q
Free lookShift
+ Interaction +
ActionControlsAlternate
SelectLMB
Clear SelectionEsc
Focus SelectionF
`; } diff --git a/studio/Cargo.toml b/studio/Cargo.toml index be6451a..edcad28 100644 --- a/studio/Cargo.toml +++ b/studio/Cargo.toml @@ -13,12 +13,28 @@ name = "studio-standalone" path = "src/main.rs" [dependencies] -wasm-bindgen = { version = "0.2.84" } +bevy-inspector-egui = { version = "0.20.0" } +# bevy_mod_outline = { version = "0.5.0" } console_error_panic_hook = { version = "0.1.7", optional = true } parking_lot = "0.12.1" +wasm-bindgen = { version = "0.2.84" } [features] wasm = ["dep:console_error_panic_hook"] [dependencies.bevy] workspace = true + +[dependencies.bevy_mod_outline] +path = "../bevy_mod_outline" + +[dependencies.bevy_mod_picking] +version = "0.16.0" +default-features = false +features = [ + "backend_raycast", + "backend_bevy_ui", + "backend_sprite", + "debug", + "selection", +] diff --git a/studio/src/camera_controller.rs b/studio/src/camera_controller.rs index 0e7c15b..99e1b5d 100644 --- a/studio/src/camera_controller.rs +++ b/studio/src/camera_controller.rs @@ -6,8 +6,12 @@ use bevy::{ }; use crate::{ - input::{Action, CameraMotion, InputAction, Lifecycle}, - math::{Easing, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq, Velocity}, + input::{Action, CameraMotion, InputAction, Lifecycle, Selection}, + interaction::Selected, + math::{ + Easing, InterpPosition, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq, + Velocity, + }, }; pub struct CameraControllerPlugin; @@ -21,8 +25,8 @@ impl Plugin for CameraControllerPlugin { Update, ( camera_control_kinematic, - process_physical_inputs.before(InterpolationSystem), - camera_control_physical.after(InterpolationSystem), + (process_physical_inputs, process_focus_inputs).before(InterpolationSystem), + (camera_control_physical, interp_camera_focus).after(InterpolationSystem), ) .in_set(CameraMotionSystem), ); @@ -87,7 +91,6 @@ fn camera_control_kinematic( InputAction(_, Action::Camera(CameraMotion::Translate(_))) ) }) { - #[allow(irrefutable_let_patterns)] // Will eventually be more variants if let InputAction(lifecycle, Action::Camera(motion)) = action { use CameraMotion::*; @@ -151,6 +154,64 @@ fn process_physical_inputs( } } +fn process_focus_inputs( + mut cmd: Commands, + mut input: EventReader, + mut q_camera: Query<(Entity, &CameraController, &mut Velocity)>, + q_interp_vel: Query<&InterpVelocity>, + mut q_interp_pos: Query<&mut InterpPosition>, + q_selected_xform: Query<&GlobalTransform, With>, +) { + if input.iter().any(|action| { + matches!( + action, + InputAction(Lifecycle::Stop, Action::Selection(Selection::Focus)) + ) + }) { + let Ok(target) = q_selected_xform + .get_single() + .map(|xform| xform.translation()) + else { + return; + }; + + for (ent, controller, mut velocity) in q_camera.iter_mut() { + // Zero out any existing camera velocity or interpolation + **velocity = Vec3::ZERO; + + if q_interp_vel.get(ent).is_ok() { + cmd.entity(ent).remove::(); + } + + let initial = controller.focus; + let easing = Easing::SineOut; + let duration = 0.25; + + if let Ok(mut interp) = q_interp_pos.get_mut(ent) { + if interp.target.nearly_eq_within(target, 1e-3) { + continue; + } + + interp.initial = initial; + interp.target = target; + interp.easing = easing; + interp.t = 0.; + interp.duration = duration; + } else { + cmd.entity(ent).insert(InterpPosition { + inner: Interpolation { + initial, + target, + easing, + ..default() + }, + duration, + }); + } + } + } +} + fn camera_control_physical( mut q_camera: Query<(&mut CameraController, &mut Transform, &Velocity)>, ) { @@ -161,6 +222,18 @@ fn camera_control_physical( } } +fn interp_camera_focus( + mut q_camera: Query<(&mut CameraController, &mut Transform, &InterpPosition)>, +) { + for (mut controller, mut xform, interp) in q_camera.iter_mut() { + let target_focus = interp.current(); + let delta_pos = target_focus - controller.focus; + + controller.focus = target_focus; + xform.translation += delta_pos; + } +} + #[inline] fn capture_pointer(window: &mut Window) { window.cursor.visible = false; diff --git a/studio/src/input.rs b/studio/src/input.rs index b8bdb73..bd404af 100644 --- a/studio/src/input.rs +++ b/studio/src/input.rs @@ -4,14 +4,22 @@ use bevy::{ utils::HashSet, }; -pub struct StudioInputPlugin; +#[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] +pub struct StudioInputSystem; +pub struct StudioInputPlugin; impl Plugin for StudioInputPlugin { fn build(&self, app: &mut App) { app.add_event::(); app.add_systems( PreUpdate, - translate_bevy_events.after(bevy::input::InputSystem), + ( + translate_selection_events, + translate_dcc_camera_events, + translate_fps_camera_events, + ) + .in_set(StudioInputSystem) + .after(bevy::input::InputSystem), ); } } @@ -29,6 +37,7 @@ pub enum Lifecycle { #[derive(Clone, Copy)] pub enum Action { Camera(CameraMotion), + Selection(Selection), } #[derive(Clone, Copy)] @@ -39,17 +48,43 @@ pub enum CameraMotion { Orbit(Vec2), Rotate(Vec2), } - impl From for Action { fn from(value: CameraMotion) -> Self { Action::Camera(value) } } +#[derive(Clone, Copy)] +pub enum Selection { + /// Clear the current selection + Clear, + /// Focus the camera on the currently selected entity + Focus, +} +impl From for Action { + fn from(value: Selection) -> Self { + Action::Selection(value) + } +} + // TODO: Make the bindings of raw device inputs to InputActions configurable -fn translate_bevy_events( + +fn translate_selection_events(keyboard: Res>, mut input: EventWriter) { + if keyboard.just_pressed(KeyCode::Escape) { + input.send(InputAction(Lifecycle::Start, Selection::Clear.into())); + } else if keyboard.just_released(KeyCode::Escape) { + input.send(InputAction(Lifecycle::Stop, Selection::Clear.into())); + } + + if keyboard.just_pressed(KeyCode::F) { + input.send(InputAction(Lifecycle::Start, Selection::Focus.into())); + } else if keyboard.just_released(KeyCode::F) { + input.send(InputAction(Lifecycle::Stop, Selection::Focus.into())); + } +} + +fn translate_dcc_camera_events( mouse_motion: EventReader, - mut mouse_wheel: EventReader, mouse_buttons: Res>, keyboard: Res>, mut input: EventWriter, @@ -148,7 +183,13 @@ fn translate_bevy_events( CameraMotion::Rotate(Vec2::ZERO).into(), )); } +} +fn translate_fps_camera_events( + mut mouse_wheel: EventReader, + keyboard: Res>, + mut input: EventWriter, +) { // Camera Translation (FPS-like) use KeyCode::*; let wasd_keys = [W, A, S, D, E, Q]; diff --git a/studio/src/interaction.rs b/studio/src/interaction.rs new file mode 100644 index 0000000..73ad115 --- /dev/null +++ b/studio/src/interaction.rs @@ -0,0 +1,136 @@ +use bevy::prelude::*; +use bevy_mod_outline::{ + AutoGenerateOutlineNormalsPlugin, OutlineBundle, OutlinePlugin, OutlineVolume, +}; +use bevy_mod_picking::{prelude::Pointer, selection::Select}; + +use crate::{ + camera_controller::CameraMotionSystem, + input::{self, Action, InputAction, Lifecycle}, +}; + +#[derive(Component, Clone, Copy)] +pub struct Selectable; + +#[derive(Component, Clone, Copy)] +pub struct Selected; + +#[derive(Resource, Deref, DerefMut)] +pub struct Selection(Entity); + +pub struct InteractionPlugin; +impl Plugin for InteractionPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((OutlinePlugin, AutoGenerateOutlineNormalsPlugin)); + app.add_systems(Update, update_selection); + app.add_systems( + PostUpdate, + ( + draw_selection_outline.run_if(resource_changed_or_removed::()), + draw_selection_axes + .run_if(resource_exists::()) + .after(CameraMotionSystem), + ), + ); + } +} + +fn update_selection( + mut cmd: Commands, + // FIXME: These events aren't aware of our input actions, so we receive a + // selection event when LMB is released after orbiting the camera. + // Should drop bevy_mod_picking's Selection plugin/feature and just + // use our own conditional click listener. + mut ev_select: EventReader>, + mut ev_input: EventReader, + q_parents: Query<&Parent>, + q_selectable: Query<&Selectable>, + q_selected: Query>, +) { + if let Some(event) = ev_select.iter().last() { + // Clear any existing `Selected` components + for ent in q_selected.iter() { + cmd.entity(ent).remove::(); + } + + // Find the nearest Selectable parent of the target entity + let mut ent = event.target; + let ent_selected = loop { + if q_selectable.get(ent).is_ok() { + break Some(ent); + } + + if let Ok(parent) = q_parents.get(ent) { + ent = parent.get(); + } else { + break None; + } + }; + + if let Some(ent) = ent_selected { + cmd.insert_resource(Selection(ent)); + cmd.entity(ent).insert(Selected); + } else { + cmd.remove_resource::(); + } + } else if ev_input.iter().any(|action| { + matches!( + action, + InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Clear)) + ) + }) { + cmd.remove_resource::(); + + for ent in q_selected.iter() { + cmd.entity(ent).remove::(); + } + } +} + +fn draw_selection_axes( + mut gizmos: Gizmos, + selection: Res, + q_transform: Query<&GlobalTransform>, +) { + let Ok(xform) = q_transform.get(**selection) else { + return; + }; + + let origin = xform.translation(); + + let mut draw_axis = |direction: Vec3, color: Color| { + let end = origin + direction * 0.25; + gizmos.line(origin, end, color); + }; + + draw_axis(xform.right(), Color::RED); + draw_axis(xform.up(), Color::GREEN); + draw_axis(xform.back(), Color::BLUE); +} + +fn draw_selection_outline( + mut cmd: Commands, + q_selected: Query<&Children, With>, + q_meshes: Query>, Without)>, + q_outlines: Query>, +) { + // Remove existing outlines + for ent in q_outlines.iter() { + cmd.entity(ent).remove::(); + } + + for children in q_selected.iter() { + for &child in children.iter() { + if q_meshes.contains(child) { + cmd.entity(child).insert(OutlineBundle { + outline: OutlineVolume { + visible: true, + width: 4., + colour: Color::YELLOW, + }, + ..default() + }); + } + } + } +} diff --git a/studio/src/lib.rs b/studio/src/lib.rs index b22ce9b..9d8f3cd 100644 --- a/studio/src/lib.rs +++ b/studio/src/lib.rs @@ -1,8 +1,11 @@ -#![allow(clippy::type_complexity)] +#![allow(clippy::type_complexity, clippy::too_many_arguments)] -use bevy::prelude::*; +use bevy::{prelude::*, window::PrimaryWindow}; +use bevy_inspector_egui::quick::WorldInspectorPlugin; +use bevy_mod_picking::DefaultPickingPlugins; use camera_controller::{CameraController, CameraControllerPlugin, CameraMotionSystem}; use input::StudioInputPlugin; +use interaction::InteractionPlugin; use math::InterpolationPlugin; use scene::ScenePlugin; use skybox::SkyboxPlugin; @@ -12,6 +15,7 @@ use wasm_bindgen::prelude::*; mod camera_controller; mod input; +mod interaction; mod math; mod scene; mod skybox; @@ -37,6 +41,9 @@ pub fn init(viewport_size: ViewportSize) { }), ..default() }), + DefaultPickingPlugins, + WorldInspectorPlugin::new(), + InteractionPlugin, StudioInputPlugin, InterpolationPlugin, CameraControllerPlugin, @@ -44,10 +51,32 @@ pub fn init(viewport_size: ViewportSize) { SkyboxPlugin, UiLayerPlugin, )) - .add_systems(Update, draw_world_axes.after(CameraMotionSystem)) + .insert_resource(GizmoConfig { + enabled: true, + depth_bias: -0.1, + line_perspective: false, + line_width: 2., + ..default() + }) + .add_systems( + Update, + ( + update_gizmo_line_thickness, + draw_world_axes.after(CameraMotionSystem), + ), + ) .run(); } +fn update_gizmo_line_thickness( + mut config: ResMut, + q_window: Query<&Window, (Changed, With)>, +) { + if let Ok(scale_factor) = q_window.get_single().map(|win| win.scale_factor() as f32) { + config.line_width = 2. * scale_factor; + } +} + fn draw_world_axes(mut gizmos: Gizmos, q: Query<&CameraController>) { let &CameraController { focus, radius, .. } = q.single(); diff --git a/studio/src/math/interp/ecs.rs b/studio/src/math/interp/ecs.rs index bdf9b60..3dfdeb8 100644 --- a/studio/src/math/interp/ecs.rs +++ b/studio/src/math/interp/ecs.rs @@ -38,9 +38,9 @@ where f32: Lerp, T: Copy, { - fn tick(&mut self, dt: f32) -> T { + fn tick(&mut self, dt: f32) -> &mut Self { self.t = (self.t + dt).clamp(0., 1.); - self.current() + self } pub fn current(&self) -> T { @@ -52,6 +52,8 @@ where let t = match self.easing { Easing::Linear => self.t, Easing::Sine => self.t.curve_sin(), + Easing::SineIn => self.t.curve_sin_in(), + Easing::SineOut => self.t.curve_sin_out(), }; t.lerp(self.initial, self.target) @@ -97,7 +99,7 @@ fn interp_velocity(time: Res