mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-16 00:03:29 +00:00
Object picking (#8)
This PR implements basic object selection, with help from `bevy_mod_picking` and `bevy_mod_outline`. ### Work Done * Click to select objects in the world * Selected objects become visually highlighted * The selected entity is available globally on the `Res<Selection>` resource * Selected entities can also be queried via the `Selected` marker component * Pressing `F` will "focus" the camera on the selected entity * Pressing `Esc` will clear the active selection ### Known Issues * We're currently using `bevy_mod_picking`'s "Selection" feature, which isn't aware of our own input events. As a consequence, it will select the object under the cursor when releasing `LMB` after orbiting the camera, which is not what we want. This should be pretty easily fixable by dropping `bevy_mod_picking`'s selection feature and reading the object under the cursor in our own click listener that only runs if the LMB event is unhandled. * The skinned mesh is not currently selectable. This is likely fixable by adjusting the logic in `scene::mark_scene_objects`... somehow. The `Gltf` data structure is kind of a nightmare to work with, so this will take some investigation. ### Future Enhancements * For parity with other DCC apps, the "Focus" action should also adjust the camera zoom level to "frame" the selected object in the view. Punting on that for now because I'm not super confident about the math involved. * Group selection with Shift/Ctrl and/or marquee selection would be really nice to have, but probably not critical for now.
This commit is contained in:
Generated
+547
@@ -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"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"./bevy_mod_outline",
|
||||
"./studio"
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
] }
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
[](https://crates.io/crates/bevy_mod_outline)
|
||||
[](https://docs.rs/bevy_mod_outline)
|
||||
|
||||

|
||||
|
||||
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 <http://www.apache.org/licenses/LICENSE-2.0>) or the MIT
|
||||
licence (see LICENCE-MIT or <http://opensource.org/licenses/MIT>), 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.
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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<Entity>,
|
||||
}
|
||||
|
||||
/// 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<GlobalTransform>,
|
||||
Option<(&SetOutlineDepth, Changed<SetOutlineDepth>)>,
|
||||
Option<&Children>,
|
||||
),
|
||||
Without<InheritOutlineDepth>,
|
||||
>,
|
||||
mut computed_query: Query<&mut ComputedOutlineDepth, With<InheritOutlineDepth>>,
|
||||
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<InheritOutlineDepth>>,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<DrawFunctions<StencilOutline>>,
|
||||
stencil_pipeline: Res<OutlinePipeline>,
|
||||
msaa: Res<Msaa>,
|
||||
mut pipelines: ResMut<SpecializedMeshPipelines<OutlinePipeline>>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
render_meshes: Res<RenderAssets<Mesh>>,
|
||||
adapter_info: Res<RenderAdapterInfo>,
|
||||
material_meshes: Query<(
|
||||
Entity,
|
||||
&Handle<Mesh>,
|
||||
&OutlineStencilUniform,
|
||||
&OutlineStencilFlags,
|
||||
&OutlineRenderLayers,
|
||||
)>,
|
||||
mut views: Query<(
|
||||
&ExtractedView,
|
||||
&mut RenderPhase<StencilOutline>,
|
||||
Option<&RenderLayers>,
|
||||
)>,
|
||||
) {
|
||||
let draw_stencil = stencil_draw_functions
|
||||
.read()
|
||||
.get_id::<DrawStencil>()
|
||||
.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<DrawFunctions<OpaqueOutline>>,
|
||||
transparent_draw_functions: Res<DrawFunctions<TransparentOutline>>,
|
||||
outline_pipeline: Res<OutlinePipeline>,
|
||||
msaa: Res<Msaa>,
|
||||
mut pipelines: ResMut<SpecializedMeshPipelines<OutlinePipeline>>,
|
||||
pipeline_cache: Res<PipelineCache>,
|
||||
render_meshes: Res<RenderAssets<Mesh>>,
|
||||
adapter_info: Res<RenderAdapterInfo>,
|
||||
material_meshes: Query<(
|
||||
Entity,
|
||||
&Handle<Mesh>,
|
||||
&OutlineVolumeUniform,
|
||||
&OutlineVolumeFlags,
|
||||
&OutlineFragmentUniform,
|
||||
&OutlineRenderLayers,
|
||||
)>,
|
||||
mut views: Query<(
|
||||
&ExtractedView,
|
||||
&mut RenderPhase<OpaqueOutline>,
|
||||
&mut RenderPhase<TransparentOutline>,
|
||||
Option<&RenderLayers>,
|
||||
)>,
|
||||
) {
|
||||
let draw_opaque_outline = opaque_draw_functions
|
||||
.read()
|
||||
.get_id::<DrawOutline>()
|
||||
.unwrap();
|
||||
let draw_transparent_outline = transparent_draw_functions
|
||||
.read()
|
||||
.get_id::<DrawOutline>()
|
||||
.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
struct FragmentOutput {
|
||||
@location(0) colour: vec4<f32>,
|
||||
#ifdef OPENGL_WORKAROUND
|
||||
@builtin(frag_depth) frag_depth: f32,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct OutlineFragmentUniform {
|
||||
@align(16)
|
||||
colour: vec4<f32>,
|
||||
};
|
||||
|
||||
#ifdef VOLUME
|
||||
@group(3) @binding(1)
|
||||
var<uniform> 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;
|
||||
}
|
||||
@@ -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<usize>),
|
||||
}
|
||||
|
||||
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<Self::Item> {
|
||||
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<usize>) {
|
||||
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<Assets<Mesh>>,
|
||||
mut events: EventReader<'_, '_, AssetEvent<Mesh>>,
|
||||
mut squelch: Local<HashSet<Handle<Mesh>>>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<OutlineVolume>, With<OutlineStencil>)>;
|
||||
type Out = Self;
|
||||
|
||||
fn extract_component(
|
||||
(outline_mask, object_mask): (Option<&OutlineRenderLayers>, Option<&RenderLayers>),
|
||||
) -> Option<Self> {
|
||||
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::<OutlineRenderLayers>::default(),
|
||||
UniformComponentPlugin::<OutlineStencilUniform>::default(),
|
||||
UniformComponentPlugin::<OutlineVolumeUniform>::default(),
|
||||
UniformComponentPlugin::<OutlineFragmentUniform>::default(),
|
||||
UniformComponentPlugin::<OutlineViewUniform>::default(),
|
||||
))
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
compute_outline_depth.after(TransformSystem::TransformPropagate),
|
||||
)
|
||||
.sub_app_mut(RenderApp)
|
||||
.init_resource::<DrawFunctions<StencilOutline>>()
|
||||
.init_resource::<DrawFunctions<OpaqueOutline>>()
|
||||
.init_resource::<DrawFunctions<TransparentOutline>>()
|
||||
.init_resource::<SpecializedMeshPipelines<OutlinePipeline>>()
|
||||
.add_render_command::<StencilOutline, DrawStencil>()
|
||||
.add_render_command::<OpaqueOutline, DrawOutline>()
|
||||
.add_render_command::<TransparentOutline, DrawOutline>()
|
||||
.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::<StencilOutline>.in_set(RenderSet::PhaseSort),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
sort_phase_system::<OpaqueOutline>.in_set(RenderSet::PhaseSort),
|
||||
)
|
||||
.add_systems(
|
||||
Render,
|
||||
sort_phase_system::<TransparentOutline>.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::<RenderGraph>();
|
||||
|
||||
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::<OutlinePipeline>();
|
||||
}
|
||||
}
|
||||
@@ -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<FloatOrd>;
|
||||
|
||||
#[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<FloatOrd>;
|
||||
|
||||
#[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<ExtractedCamera>,
|
||||
Read<RenderPhase<StencilOutline>>,
|
||||
Read<RenderPhase<OpaqueOutline>>,
|
||||
Read<RenderPhase<TransparentOutline>>,
|
||||
Read<Camera3d>,
|
||||
Read<ViewTarget>,
|
||||
Read<ViewDepthTexture>,
|
||||
),
|
||||
With<ExtractedView>,
|
||||
>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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<f32>,
|
||||
#endif
|
||||
#ifndef OFFSET_ZERO
|
||||
@location(1) outline_normal: vec3<f32>,
|
||||
#endif
|
||||
#ifdef SKINNED
|
||||
@location(5) joint_indices: vec4<u32>,
|
||||
@location(6) joint_weights: vec4<f32>,
|
||||
#endif
|
||||
#ifdef MORPH_TARGETS
|
||||
@builtin(vertex_index) index: u32,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
#ifdef OPENGL_WORKAROUND
|
||||
@location(0) normalised_depth: f32,
|
||||
#endif
|
||||
};
|
||||
|
||||
struct OutlineViewUniform {
|
||||
@align(16)
|
||||
scale: vec2<f32>,
|
||||
};
|
||||
|
||||
struct OutlineVertexUniform {
|
||||
@align(16)
|
||||
origin: vec3<f32>,
|
||||
offset: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0)
|
||||
var<uniform> view: View;
|
||||
|
||||
@group(1) @binding(0)
|
||||
var<uniform> mesh: Mesh;
|
||||
|
||||
#import bevy_pbr::skinning
|
||||
#import bevy_pbr::morph
|
||||
|
||||
@group(2) @binding(0)
|
||||
var<uniform> view_uniform: OutlineViewUniform;
|
||||
|
||||
@group(3) @binding(0)
|
||||
var<uniform> 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<f32>) -> mat3x3<f32> {
|
||||
return mat3x3<f32>(
|
||||
m[0].xyz, m[1].xyz, m[2].xyz
|
||||
);
|
||||
}
|
||||
|
||||
fn model_origin_z(plane: vec3<f32>, view_proj: mat4x4<f32>) -> f32 {
|
||||
var proj_zw = mat4x2<f32>(
|
||||
view_proj[0].zw, view_proj[1].zw,
|
||||
view_proj[2].zw, view_proj[3].zw);
|
||||
var zw = proj_zw * vec4<f32>(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<f32>(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<f32>(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;
|
||||
}
|
||||
@@ -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<PipelineKey> 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::<MeshPipeline>().unwrap().clone();
|
||||
let render_device = world.get_resource::<RenderDevice>().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<RenderPipelineDescriptor, SpecializedMeshPipelineError> {
|
||||
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::<Shader>(),
|
||||
entry_point: "vertex".into(),
|
||||
shader_defs: vertex_defs,
|
||||
buffers,
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader: FRAGMENT_SHADER_HANDLE.typed::<Shader>(),
|
||||
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")),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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<Query<(Entity, &OutlineStencil, &ComputedOutlineDepth)>>,
|
||||
) {
|
||||
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<Query<(Entity, &OutlineVolume, &ComputedOutlineDepth)>>,
|
||||
) {
|
||||
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<RenderDevice>,
|
||||
outline_pipeline: Res<OutlinePipeline>,
|
||||
vertex: Res<ComponentUniforms<OutlineStencilUniform>>,
|
||||
) {
|
||||
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<RenderDevice>,
|
||||
outline_pipeline: Res<OutlinePipeline>,
|
||||
vertex: Res<ComponentUniforms<OutlineVolumeUniform>>,
|
||||
fragment: Res<ComponentUniforms<OutlineFragmentUniform>>,
|
||||
) {
|
||||
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<const I: usize>();
|
||||
|
||||
impl<const I: usize> RenderCommand<StencilOutline> for SetOutlineStencilBindGroup<I> {
|
||||
type ViewWorldQuery = ();
|
||||
type ItemWorldQuery = Read<DynamicUniformIndex<OutlineStencilUniform>>;
|
||||
type Param = SRes<OutlineStencilBindGroup>;
|
||||
fn render<'w>(
|
||||
_item: &StencilOutline,
|
||||
_view_data: (),
|
||||
entity_data: &DynamicUniformIndex<OutlineStencilUniform>,
|
||||
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<const I: usize>();
|
||||
|
||||
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetOutlineVolumeBindGroup<I> {
|
||||
type ViewWorldQuery = ();
|
||||
type ItemWorldQuery = (
|
||||
Read<DynamicUniformIndex<OutlineVolumeUniform>>,
|
||||
Read<DynamicUniformIndex<OutlineFragmentUniform>>,
|
||||
);
|
||||
type Param = SRes<OutlineVolumeBindGroup>;
|
||||
fn render<'w>(
|
||||
_item: &P,
|
||||
_view_data: (),
|
||||
entity_data: (
|
||||
&DynamicUniformIndex<OutlineVolumeUniform>,
|
||||
&DynamicUniformIndex<OutlineFragmentUniform>,
|
||||
),
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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<Query<(Entity, &Camera, Option<&RenderLayers>), With<Camera3d>>>,
|
||||
) {
|
||||
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::<StencilOutline>::default())
|
||||
.insert(RenderPhase::<OpaqueOutline>::default())
|
||||
.insert(RenderPhase::<TransparentOutline>::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<RenderDevice>,
|
||||
outline_pipeline: Res<OutlinePipeline>,
|
||||
view_uniforms: Res<ComponentUniforms<OutlineViewUniform>>,
|
||||
) {
|
||||
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<const I: usize>();
|
||||
|
||||
impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetOutlineViewBindGroup<I> {
|
||||
type ViewWorldQuery = Read<DynamicUniformIndex<OutlineViewUniform>>;
|
||||
type ItemWorldQuery = ();
|
||||
type Param = SRes<OutlineViewBindGroup>;
|
||||
fn render<'w>(
|
||||
_item: &P,
|
||||
view_data: &DynamicUniformIndex<OutlineViewUniform>,
|
||||
_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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ export class HelpElement extends LitElement {
|
||||
protected override render = () => html`
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="section-header" colspan="3">
|
||||
Navigation
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Controls</th>
|
||||
@@ -32,11 +37,7 @@ export class HelpElement extends LitElement {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Free fly</td>
|
||||
<td colspan="2">W, A, S, D</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-left: 20px">Up / Down</td>
|
||||
<td colspan="2">E, Q</td>
|
||||
<td colspan="2">W, A, S, D, E, Q</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Free look</td>
|
||||
@@ -44,6 +45,32 @@ export class HelpElement extends LitElement {
|
||||
<td>Shift</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="section-header" colspan="3">
|
||||
Interaction
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Controls</th>
|
||||
<th>Alternate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Select</td>
|
||||
<td colspan="2">LMB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Clear Selection</td>
|
||||
<td colspan="2">Esc</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Focus Selection</td>
|
||||
<td colspan="2">F</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
+17
-1
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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<InputAction>,
|
||||
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<Selected>>,
|
||||
) {
|
||||
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::<InterpVelocity>();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
+46
-5
@@ -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::<InputAction>();
|
||||
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<CameraMotion> 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<Selection> 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<Input<KeyCode>>, mut input: EventWriter<InputAction>) {
|
||||
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<MouseMotion>,
|
||||
mut mouse_wheel: EventReader<MouseWheel>,
|
||||
mouse_buttons: Res<Input<MouseButton>>,
|
||||
keyboard: Res<Input<KeyCode>>,
|
||||
mut input: EventWriter<InputAction>,
|
||||
@@ -148,7 +183,13 @@ fn translate_bevy_events(
|
||||
CameraMotion::Rotate(Vec2::ZERO).into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_fps_camera_events(
|
||||
mut mouse_wheel: EventReader<MouseWheel>,
|
||||
keyboard: Res<Input<KeyCode>>,
|
||||
mut input: EventWriter<InputAction>,
|
||||
) {
|
||||
// Camera Translation (FPS-like)
|
||||
use KeyCode::*;
|
||||
let wasd_keys = [W, A, S, D, E, Q];
|
||||
|
||||
@@ -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::<Selection>()),
|
||||
draw_selection_axes
|
||||
.run_if(resource_exists::<Selection>())
|
||||
.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<Pointer<Select>>,
|
||||
mut ev_input: EventReader<InputAction>,
|
||||
q_parents: Query<&Parent>,
|
||||
q_selectable: Query<&Selectable>,
|
||||
q_selected: Query<Entity, With<Selected>>,
|
||||
) {
|
||||
if let Some(event) = ev_select.iter().last() {
|
||||
// Clear any existing `Selected` components
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
}
|
||||
|
||||
// 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::<Selection>();
|
||||
}
|
||||
} else if ev_input.iter().any(|action| {
|
||||
matches!(
|
||||
action,
|
||||
InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Clear))
|
||||
)
|
||||
}) {
|
||||
cmd.remove_resource::<Selection>();
|
||||
|
||||
for ent in q_selected.iter() {
|
||||
cmd.entity(ent).remove::<Selected>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_selection_axes(
|
||||
mut gizmos: Gizmos,
|
||||
selection: Res<Selection>,
|
||||
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<Selected>>,
|
||||
q_meshes: Query<Entity, (With<Handle<Mesh>>, Without<OutlineVolume>)>,
|
||||
q_outlines: Query<Entity, With<OutlineVolume>>,
|
||||
) {
|
||||
// Remove existing outlines
|
||||
for ent in q_outlines.iter() {
|
||||
cmd.entity(ent).remove::<OutlineBundle>();
|
||||
}
|
||||
|
||||
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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-3
@@ -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<GizmoConfig>,
|
||||
q_window: Query<&Window, (Changed<Window>, With<PrimaryWindow>)>,
|
||||
) {
|
||||
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();
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ where
|
||||
f32: Lerp<T>,
|
||||
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<Time>, mut q_interp: Query<(&mut Velocity, &mut Int
|
||||
|
||||
for (mut velocity, mut interp) in q_interp.iter_mut() {
|
||||
let dt = delta_seconds / interp.accel_zero2max;
|
||||
**velocity = interp.tick(dt);
|
||||
**velocity = interp.tick(dt).current();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ mod traits;
|
||||
pub enum Easing {
|
||||
Linear,
|
||||
Sine,
|
||||
SineOut,
|
||||
#[allow(dead_code)]
|
||||
SineIn,
|
||||
}
|
||||
|
||||
impl Default for Easing {
|
||||
|
||||
@@ -73,6 +73,18 @@ pub trait CurveSin {
|
||||
/// }
|
||||
/// ```
|
||||
fn curve_sin(self) -> Self;
|
||||
|
||||
/// Works like `curve_sin`, except that only the lower half of the line is
|
||||
/// curved, with the upper half progressing linearly.
|
||||
///
|
||||
/// https://www.desmos.com/calculator/ngceukl6g5
|
||||
fn curve_sin_in(self) -> Self;
|
||||
|
||||
/// Works like `curve_sin`, except that only the upper half of the line is
|
||||
/// curved, with the lower half progressing linearly.
|
||||
///
|
||||
/// https://www.desmos.com/calculator/umvvxhkd46
|
||||
fn curve_sin_out(self) -> Self;
|
||||
}
|
||||
|
||||
impl<T, Operand> Lerp<Operand> for T
|
||||
@@ -141,6 +153,18 @@ impl CurveSin for f32 {
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
|
||||
fn curve_sin_in(self) -> Self {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
1. - ((self * PI) / 2.).cos()
|
||||
}
|
||||
|
||||
fn curve_sin_out(self) -> Self {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
((self * PI) / 2.).sin()
|
||||
}
|
||||
}
|
||||
|
||||
impl CurveSin for f64 {
|
||||
@@ -149,4 +173,16 @@ impl CurveSin for f64 {
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
|
||||
fn curve_sin_in(self) -> Self {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
1. - ((self * PI) / 2.).cos()
|
||||
}
|
||||
|
||||
fn curve_sin_out(self) -> Self {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
((self * PI) / 2.).sin()
|
||||
}
|
||||
}
|
||||
|
||||
+143
-24
@@ -1,18 +1,23 @@
|
||||
use bevy::{
|
||||
asset::LoadState,
|
||||
core_pipeline::{
|
||||
bloom::{BloomCompositeMode, BloomPrefilterSettings, BloomSettings},
|
||||
fxaa::Fxaa,
|
||||
tonemapping::Tonemapping,
|
||||
},
|
||||
gltf::{self, Gltf},
|
||||
math::vec3,
|
||||
pbr::NotShadowCaster,
|
||||
prelude::*,
|
||||
reflect::{TypePath, TypeUuid},
|
||||
render::render_resource::{AsBindGroup, ShaderRef},
|
||||
utils::HashSet,
|
||||
};
|
||||
use bevy_mod_picking::PickableBundle;
|
||||
|
||||
use crate::{
|
||||
camera_controller::{CameraController, CameraControllerBundle, CameraMotionSystem},
|
||||
interaction::Selectable,
|
||||
skybox::SkyboxId,
|
||||
};
|
||||
|
||||
@@ -21,8 +26,17 @@ pub struct ScenePlugin;
|
||||
impl Plugin for ScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(ClearColor(Color::hsl(0.0, 0.0, 0.25)))
|
||||
.insert_resource(Msaa::Off)
|
||||
.add_plugins(MaterialPlugin::<ProceduralGridMaterial>::default())
|
||||
.add_systems(Startup, spawn_scene)
|
||||
.add_systems(Startup, initialize)
|
||||
.add_systems(
|
||||
Update,
|
||||
mark_scene_objects.run_if(
|
||||
resource_exists::<NewGltf>()
|
||||
.and_then(gltf_loaded)
|
||||
.and_then(gltf_scene_spawned),
|
||||
),
|
||||
)
|
||||
.add_systems(PostUpdate, move_plane_with_camera.after(CameraMotionSystem));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +44,10 @@ impl Plugin for ScenePlugin {
|
||||
#[derive(Component)]
|
||||
struct WorldGridPlane;
|
||||
|
||||
fn spawn_scene(
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
struct NewGltf(pub Handle<Gltf>);
|
||||
|
||||
fn initialize(
|
||||
mut cmd: Commands,
|
||||
assets: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
@@ -40,15 +57,20 @@ fn spawn_scene(
|
||||
cmd.insert_resource(SkyboxId("test_scene".into()));
|
||||
|
||||
// glTF scene
|
||||
cmd.spawn(SceneBundle {
|
||||
scene: assets.load("gltf/sample-room.gltf#Scene0"),
|
||||
..default()
|
||||
});
|
||||
cmd.insert_resource(NewGltf(assets.load("gltf/sample-room.gltf")));
|
||||
cmd.spawn((
|
||||
Name::new("Scene"),
|
||||
SceneBundle {
|
||||
scene: assets.load("gltf/sample-room.gltf#Scene0"),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Camera, graphics settings
|
||||
let focus = vec3(0.35011, 0.5, -0.99576);
|
||||
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
@@ -73,30 +95,36 @@ fn spawn_scene(
|
||||
|
||||
// Main light
|
||||
// TODO: get Blender to export enough metadata to automate light placements
|
||||
cmd.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::hex("FFE5B4").unwrap(),
|
||||
intensity: 200.,
|
||||
radius: 0.1,
|
||||
shadows_enabled: true,
|
||||
cmd.spawn((
|
||||
Name::new("Ceiling Light"),
|
||||
PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::hex("FFE5B4").unwrap(),
|
||||
intensity: 200.,
|
||||
radius: 0.1,
|
||||
shadows_enabled: true,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.35263, 2.4348, 0.19601),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.35263, 2.4348, 0.19601),
|
||||
..default()
|
||||
});
|
||||
));
|
||||
|
||||
// Monitor glow
|
||||
cmd.spawn(PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::WHITE,
|
||||
intensity: 6.,
|
||||
radius: 0.25,
|
||||
shadows_enabled: false,
|
||||
cmd.spawn((
|
||||
Name::new("Monitor Glow"),
|
||||
PointLightBundle {
|
||||
point_light: PointLight {
|
||||
color: Color::WHITE,
|
||||
intensity: 6.,
|
||||
radius: 0.25,
|
||||
shadows_enabled: false,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.42615, 1.0706, -2.0705),
|
||||
..default()
|
||||
},
|
||||
transform: Transform::from_xyz(0.42615, 1.0706, -2.0705),
|
||||
..default()
|
||||
});
|
||||
));
|
||||
|
||||
// Plane
|
||||
let plane = meshes.add(Mesh::from(shape::Plane {
|
||||
@@ -106,6 +134,7 @@ fn spawn_scene(
|
||||
let proc_grid = mat_proc_grid.add(ProceduralGridMaterial {});
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("World Grid"),
|
||||
SpatialBundle::from_transform(Transform::IDENTITY),
|
||||
WorldGridPlane,
|
||||
))
|
||||
@@ -131,6 +160,96 @@ fn spawn_scene(
|
||||
});
|
||||
}
|
||||
|
||||
fn gltf_loaded(assets: Res<AssetServer>, gltf: Res<NewGltf>) -> bool {
|
||||
assets.get_load_state(&**gltf) == LoadState::Loaded
|
||||
}
|
||||
|
||||
fn gltf_scene_spawned(
|
||||
r_assets: Res<AssetServer>,
|
||||
r_gltf: Res<NewGltf>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
q_scenes: Query<&Handle<Scene>, (With<Name>, With<Children>)>,
|
||||
) -> bool {
|
||||
let gltf = ra_gltfs.get(&r_gltf).unwrap();
|
||||
let Some(gltf_scene_path) = gltf
|
||||
.default_scene
|
||||
.as_ref()
|
||||
.and_then(|handle| r_assets.get_handle_path(handle))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
q_scenes.iter().any(|handle| {
|
||||
let Some(path) = r_assets.get_handle_path(handle) else {
|
||||
return false;
|
||||
};
|
||||
path == gltf_scene_path
|
||||
})
|
||||
}
|
||||
|
||||
fn mark_scene_objects(
|
||||
mut cmd: Commands,
|
||||
r_gltf: Res<NewGltf>,
|
||||
r_assets: Res<AssetServer>,
|
||||
ra_gltfs: Res<Assets<Gltf>>,
|
||||
ra_gltf_nodes: Res<Assets<gltf::GltfNode>>,
|
||||
ra_gltf_meshes: Res<Assets<gltf::GltfMesh>>,
|
||||
q_parents: Query<(Entity, &Name, &Children), Without<Selectable>>,
|
||||
q_primitives: Query<&Handle<Mesh>>,
|
||||
) {
|
||||
let gltf = ra_gltfs.get(&r_gltf).unwrap();
|
||||
|
||||
for (ent_parent, name, children) in q_parents.iter() {
|
||||
// See if this name matches any of the meshes or nodes in our glTF scene
|
||||
let Some(gltf_mesh) = gltf
|
||||
.named_meshes
|
||||
.get(&**name)
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
gltf.named_nodes.get(&**name).and_then(|node_handle| {
|
||||
ra_gltf_nodes
|
||||
.get(node_handle)
|
||||
.and_then(|node| node.mesh.clone())
|
||||
})
|
||||
})
|
||||
.and_then(|handle| ra_gltf_meshes.get(&handle))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Collect all of the mesh handles in the glTF mesh
|
||||
let prims_from_gltf = gltf_mesh
|
||||
.primitives
|
||||
.iter()
|
||||
.filter_map(|prim| r_assets.get_handle_path(&prim.mesh))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// Collect all of the mesh handles in this entity's children
|
||||
let prims_from_ecs = children
|
||||
.iter()
|
||||
.filter_map(|ent_child| {
|
||||
q_primitives
|
||||
.get(*ent_child)
|
||||
.ok()
|
||||
.and_then(|handle| r_assets.get_handle_path(handle))
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// If the sets match, this is the entity that represents the glTF mesh
|
||||
if prims_from_gltf == prims_from_ecs {
|
||||
cmd.entity(ent_parent).insert(Selectable);
|
||||
|
||||
for &ent_child in children.iter() {
|
||||
if q_primitives.contains(ent_child) {
|
||||
cmd.entity(ent_child).insert(PickableBundle::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd.remove_resource::<NewGltf>();
|
||||
}
|
||||
|
||||
fn move_plane_with_camera(
|
||||
mut plane: Query<(&WorldGridPlane, &mut Transform)>,
|
||||
camera: Query<&CameraController, Changed<CameraController>>,
|
||||
|
||||
@@ -19,6 +19,7 @@ struct FramerateCounter;
|
||||
fn layout(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
FramerateCounter,
|
||||
Name::new("Framerate Counter"),
|
||||
TextBundle::from_section(
|
||||
"0 fps",
|
||||
TextStyle {
|
||||
|
||||
Reference in New Issue
Block a user