mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-09 00:09:55 +00:00
Studio Headless Mode (#36)
This PR adds the capaibility to run Stroyteller studio from the command line, rendering the result to images. This is the basic functionality, and there are somethings that I am saving for later when we make this a more fully fledged feature. Limitations: - Right now, you cannot do image output resolutions that aren't 2^n. This is a bug I plan on tackling later. If you have questions on how the command line works, use the --help argument. Let me know what you think! --------- Co-authored-by: Danny McGee <dannymcgee@gmail.com>
This commit is contained in:
@@ -39,3 +39,5 @@ testem.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
/target
|
||||
|
||||
/screen_shots
|
||||
|
||||
Generated
+355
-19
@@ -175,6 +175,54 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.79"
|
||||
@@ -267,7 +315,7 @@ dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand 2.0.1",
|
||||
"futures-lite",
|
||||
"futures-lite 1.13.0",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -280,7 +328,7 @@ dependencies = [
|
||||
"async-lock",
|
||||
"autocfg",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"futures-lite 1.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -463,7 +511,7 @@ dependencies = [
|
||||
"crossbeam-channel",
|
||||
"downcast-rs",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"futures-lite 1.13.0",
|
||||
"js-sys",
|
||||
"parking_lot",
|
||||
"ron",
|
||||
@@ -1169,7 +1217,7 @@ dependencies = [
|
||||
"codespan-reporting",
|
||||
"downcast-rs",
|
||||
"encase",
|
||||
"futures-lite",
|
||||
"futures-lite 1.13.0",
|
||||
"hexasphere",
|
||||
"image",
|
||||
"js-sys",
|
||||
@@ -1255,7 +1303,7 @@ dependencies = [
|
||||
"async-executor",
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"futures-lite",
|
||||
"futures-lite 1.13.0",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
@@ -1485,6 +1533,12 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
|
||||
|
||||
[[package]]
|
||||
name = "bitfield"
|
||||
version = "0.14.0"
|
||||
@@ -1567,7 +1621,7 @@ dependencies = [
|
||||
"async-task",
|
||||
"fastrand 2.0.1",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"futures-lite 1.13.0",
|
||||
"piper",
|
||||
"tracing",
|
||||
]
|
||||
@@ -1591,9 +1645,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.0"
|
||||
version = "1.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||
checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9"
|
||||
dependencies = [
|
||||
"bytemuck_derive",
|
||||
]
|
||||
@@ -1675,6 +1729,46 @@ dependencies = [
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "4.5.0"
|
||||
@@ -1702,6 +1796,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "com-rs"
|
||||
version = "0.2.1"
|
||||
@@ -1946,6 +2046,12 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "curl"
|
||||
version = "0.4.44"
|
||||
@@ -2196,6 +2302,22 @@ version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.72.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"flume",
|
||||
"half",
|
||||
"lebe",
|
||||
"miniz_oxide",
|
||||
"rayon-core",
|
||||
"smallvec",
|
||||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -2236,6 +2358,15 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -2300,16 +2431,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-intrusive"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"lock_api",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.28"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -2326,6 +2504,60 @@ dependencies = [
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
|
||||
dependencies = [
|
||||
"fastrand 2.0.1",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
@@ -2358,6 +2590,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs"
|
||||
version = "0.10.2"
|
||||
@@ -2541,6 +2783,16 @@ dependencies = [
|
||||
"svg_fmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -2627,16 +2879,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.7"
|
||||
version = "0.24.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
|
||||
checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-rational",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-traits",
|
||||
"png",
|
||||
"qoi",
|
||||
"tiff",
|
||||
]
|
||||
|
||||
@@ -2713,7 +2968,7 @@ dependencies = [
|
||||
"curl-sys",
|
||||
"encoding_rs",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"futures-lite 1.13.0",
|
||||
"http",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -2806,6 +3061,9 @@ name = "jpeg-decoder"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@@ -2848,6 +3106,12 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "lewton"
|
||||
version = "0.10.2"
|
||||
@@ -3502,9 +3766,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067"
|
||||
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
@@ -3604,6 +3868,12 @@ version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.1"
|
||||
@@ -3714,6 +3984,15 @@ version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f89dff0959d98c9758c88826cc002e2c3d0b9dfac4139711d1f30de442f1139b"
|
||||
|
||||
[[package]]
|
||||
name = "qoi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
@@ -3774,6 +4053,26 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rectangle-pack"
|
||||
version = "0.4.2"
|
||||
@@ -4185,6 +4484,15 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.2.0+1.5.4"
|
||||
@@ -4207,6 +4515,12 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "studio"
|
||||
version = "0.1.0"
|
||||
@@ -4220,7 +4534,13 @@ dependencies = [
|
||||
"bevy_rapier3d",
|
||||
"bevy_web_asset",
|
||||
"bitflags 2.4.0",
|
||||
"bytemuck",
|
||||
"clap",
|
||||
"console_error_panic_hook",
|
||||
"futures",
|
||||
"futures-intrusive",
|
||||
"futures-lite 2.2.0",
|
||||
"image",
|
||||
"js-sys",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
@@ -4230,6 +4550,7 @@ dependencies = [
|
||||
"studio-bvh",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"wgpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4571,6 +4892,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.1"
|
||||
@@ -5269,3 +5596,12 @@ dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-inflate"
|
||||
version = "0.2.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"build": {
|
||||
"executor": "@nxrs/cargo:build",
|
||||
"options": {
|
||||
"release": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
||||
@@ -12,6 +12,10 @@ path = "src/lib.rs"
|
||||
name = "studio-standalone"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "studio-headless"
|
||||
path = "src/main-headless.rs"
|
||||
|
||||
[dependencies]
|
||||
bevy-inspector-egui = "0.22.0"
|
||||
bevy_mod_raycast = "0.16.0"
|
||||
@@ -34,6 +38,13 @@ web-sys = { version = "0.3.65", optional = true, features = [
|
||||
"HtmlElement",
|
||||
"Window",
|
||||
] }
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
futures-intrusive = "0.5.0"
|
||||
wgpu = "0.17.1"
|
||||
futures-lite = "2.2.0"
|
||||
futures = "0.3.30"
|
||||
image = "0.24.8"
|
||||
bytemuck = "1.14.1"
|
||||
space_editor = {git = "https://github.com/rewin123/space_editor", rev = "ce83f86dc902787ab16213f3209276a784654df6"}
|
||||
anyhow.workspace = true
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"release": false
|
||||
},
|
||||
"configurations": {
|
||||
"headless" : {
|
||||
"release" : true,
|
||||
"bin": "studio-headless"
|
||||
},
|
||||
"production": {
|
||||
"release": true
|
||||
}
|
||||
@@ -34,6 +38,12 @@
|
||||
"bin": "studio-standalone"
|
||||
}
|
||||
},
|
||||
"headless": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"command": "cargo run -p studio --bin studio-headless -- {args}",
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nxrs/cargo:test",
|
||||
"options": {}
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct BvhPlugin;
|
||||
impl Plugin for BvhPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_asset::<Bvh>()
|
||||
.add_event::<BvhAnimCompleteEvent>()
|
||||
.register_asset_loader(BvhLoader)
|
||||
// FIXME: Need to add a state for this module (similar to scene
|
||||
// module's `SceneState`) to manage the order/frequency at
|
||||
@@ -106,6 +107,7 @@ fn compute_inverse_bindposes(
|
||||
pub fn animate_bvh(
|
||||
mut q_bvh_anims: Query<(&mut BvhAnim, &Children)>,
|
||||
mut q_bvh_joints: Query<JointQuery, Without<BvhAnim>>,
|
||||
mut bvh_event_writer: EventWriter<BvhAnimCompleteEvent>,
|
||||
r_bvh_assets: Res<Assets<Bvh>>,
|
||||
r_time: Res<Time>,
|
||||
) {
|
||||
@@ -122,6 +124,9 @@ pub fn animate_bvh(
|
||||
|
||||
if anim.timer.just_finished() {
|
||||
anim.frame = (anim.frame + 1) % bvh_len;
|
||||
if anim.frame == 0 {
|
||||
bvh_event_writer.send(BvhAnimCompleteEvent);
|
||||
}
|
||||
}
|
||||
|
||||
anim.timer.percent()
|
||||
@@ -204,6 +209,16 @@ fn lerp_frames<T: ReadOnlyWorldQuery>(
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Bvh Anim Events
|
||||
//==============================================================================
|
||||
|
||||
// This may seem weird, but this is for the Headless mode to
|
||||
// know when to stop recording.
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct BvhAnimCompleteEvent;
|
||||
|
||||
//==============================================================================
|
||||
// Bvh Anim Component
|
||||
//==============================================================================
|
||||
|
||||
+119
-2
@@ -1,11 +1,20 @@
|
||||
#![allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||
|
||||
use bevy::{app::PluginGroupBuilder, prelude::*};
|
||||
use std::time::Duration;
|
||||
|
||||
use bevy::{
|
||||
app::{AppExit, PluginGroupBuilder, ScheduleRunnerPlugin},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy_mod_picking::DefaultPickingPlugins;
|
||||
use bevy_web_asset::WebAssetPlugin;
|
||||
use bvh::{BvhAnimCompleteEvent, TestBvh};
|
||||
use screenshot::{ScreenCapture, ScreenShotPlugin};
|
||||
use space_editor::prelude::{EditorEvent, EditorPrefabPath};
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wgpu::Extent3d;
|
||||
|
||||
mod anim;
|
||||
mod bvh;
|
||||
@@ -20,6 +29,7 @@ pub mod math;
|
||||
mod mixamo;
|
||||
mod persistance;
|
||||
mod scene;
|
||||
pub mod screenshot;
|
||||
mod skybox;
|
||||
mod ui;
|
||||
pub mod viewport;
|
||||
@@ -53,6 +63,7 @@ pub(crate) struct MainCamera;
|
||||
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum StudioMode {
|
||||
Editor,
|
||||
Headless,
|
||||
Viewer,
|
||||
}
|
||||
|
||||
@@ -62,7 +73,7 @@ pub(crate) fn is_editor(r_studio_mode: Res<StudioMode>) -> bool {
|
||||
|
||||
/// The collection of plugins that should be loaded for both viewer-mode and
|
||||
/// editor-mode instances of the app.
|
||||
struct CommonPlugins;
|
||||
pub struct CommonPlugins;
|
||||
impl PluginGroup for CommonPlugins {
|
||||
fn build(self) -> PluginGroupBuilder {
|
||||
PluginGroupBuilder::start::<Self>()
|
||||
@@ -272,3 +283,109 @@ pub fn start_internal(
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
pub enum HeadlessMode {
|
||||
Bvh(String, String),
|
||||
Scene(String, String),
|
||||
}
|
||||
|
||||
pub fn start_headless(
|
||||
mode: HeadlessMode,
|
||||
fps: u32,
|
||||
outout_dir: &str,
|
||||
frames_to_capture: u32,
|
||||
image_res: Extent3d,
|
||||
) -> App {
|
||||
let mut app = App::new();
|
||||
|
||||
app.insert_resource(StudioMode::Headless);
|
||||
app.insert_resource(HeadlessSettings {
|
||||
frames_to_capture,
|
||||
output_dims: image_res,
|
||||
output_dir: outout_dir.to_string(),
|
||||
});
|
||||
|
||||
app.add_plugins((
|
||||
ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0 / fps as f64)),
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: None,
|
||||
exit_condition: bevy::window::ExitCondition::DontExit,
|
||||
close_when_requested: true,
|
||||
}),
|
||||
ScreenShotPlugin,
|
||||
CommonPlugins,
|
||||
));
|
||||
|
||||
match mode {
|
||||
HeadlessMode::Bvh(bvh_dir, skybox) => {
|
||||
app.add_plugins(BvhPlugin);
|
||||
app.add_systems(
|
||||
Startup,
|
||||
move |mut cmd: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut ew: EventWriter<LoadSceneEvent>| {
|
||||
cmd.insert_resource(SkyboxId(skybox.clone()));
|
||||
cmd.insert_resource(TestBvh(asset_server.load(bvh_dir.clone())));
|
||||
|
||||
ew.send(LoadSceneEvent("Mannequin.gltf".to_string()));
|
||||
},
|
||||
);
|
||||
|
||||
app.add_systems(PostUpdate, check_for_capture_complete);
|
||||
}
|
||||
HeadlessMode::Scene(scene, skybox) => {
|
||||
app.add_systems(
|
||||
Startup,
|
||||
move |mut cmd: Commands, mut ew: EventWriter<EditorEvent>| {
|
||||
cmd.insert_resource(SkyboxId(skybox.clone()));
|
||||
ew.send(EditorEvent::Load(EditorPrefabPath::File(scene.clone())))
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
pub fn is_headless(studio_state: Res<StudioMode>) -> bool {
|
||||
*studio_state == StudioMode::Headless
|
||||
}
|
||||
|
||||
pub fn is_not_headless(studio_state: Res<StudioMode>) -> bool {
|
||||
*studio_state != StudioMode::Headless
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct HeadlessSettings {
|
||||
pub output_dims: Extent3d,
|
||||
pub output_dir: String,
|
||||
pub frames_to_capture: u32,
|
||||
}
|
||||
|
||||
impl Default for HeadlessSettings {
|
||||
fn default() -> Self {
|
||||
HeadlessSettings {
|
||||
output_dims: Extent3d {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
output_dir: "./screen_shots/".to_string(),
|
||||
frames_to_capture: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_capture_complete(
|
||||
main_camera: Query<&ScreenCapture, With<MainCamera>>,
|
||||
mut close_app: EventWriter<AppExit>,
|
||||
bvh_event_reader: EventReader<BvhAnimCompleteEvent>,
|
||||
) {
|
||||
let Ok(screen_capture) = main_camera.get_single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if screen_capture.is_complete() || !bvh_event_reader.is_empty() {
|
||||
close_app.send(AppExit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
use bevy::render::render_resource::Extent3d;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use studio::{start_headless, HeadlessMode};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "StorytellerStudioHeadless")]
|
||||
#[command(author = "The Storyteller Team")]
|
||||
#[command(version = "1.0")]
|
||||
#[command(
|
||||
about = "Executable for Storyteller Studio Headless. Allows you to render frames to images."
|
||||
)]
|
||||
#[command(propagate_version = true)]
|
||||
struct StudioArgs {
|
||||
#[command(subcommand)]
|
||||
subcommand: Subcommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Subcommands {
|
||||
Scene {
|
||||
scene_dir: String,
|
||||
#[arg(long, short)]
|
||||
output_dir: Option<String>,
|
||||
#[arg(long)]
|
||||
skybox: Option<String>,
|
||||
#[arg(long = "res", value_parser = extent3d_parser)]
|
||||
output_resolution: Option<Extent3d>,
|
||||
#[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
|
||||
fps: Option<u32>,
|
||||
#[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
|
||||
frames: Option<u32>,
|
||||
},
|
||||
Bvh {
|
||||
bvh_dir: String,
|
||||
#[arg(long, short = 'o', short)]
|
||||
out: Option<String>,
|
||||
#[arg(long)]
|
||||
skybox: Option<String>,
|
||||
#[arg(long = "res", value_parser = extent3d_parser)]
|
||||
output_resolution: Option<Extent3d>,
|
||||
#[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
|
||||
fps: Option<u32>,
|
||||
#[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
|
||||
frames: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args, Clone)]
|
||||
struct CommonArgs {}
|
||||
|
||||
fn main() {
|
||||
let args = StudioArgs::parse();
|
||||
|
||||
let mut app = match args.subcommand {
|
||||
Subcommands::Scene {
|
||||
scene_dir,
|
||||
skybox,
|
||||
output_dir,
|
||||
output_resolution,
|
||||
fps,
|
||||
frames,
|
||||
} => {
|
||||
let output_dir = output_dir.unwrap_or("./screen_shots/".to_string());
|
||||
let output_res = output_resolution.unwrap_or(Extent3d {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
depth_or_array_layers: 1,
|
||||
});
|
||||
let fps = fps.unwrap_or(24);
|
||||
let frames = frames.unwrap_or(30);
|
||||
let skybox = skybox.unwrap_or("gum_trees_4k".to_string());
|
||||
start_headless(
|
||||
HeadlessMode::Scene(scene_dir, skybox),
|
||||
fps,
|
||||
&output_dir,
|
||||
frames,
|
||||
output_res,
|
||||
)
|
||||
}
|
||||
Subcommands::Bvh {
|
||||
bvh_dir,
|
||||
skybox,
|
||||
out: output_dir,
|
||||
output_resolution,
|
||||
fps,
|
||||
frames,
|
||||
} => {
|
||||
let output_dir = output_dir.unwrap_or("./screen_shots/".to_string());
|
||||
let output_res = output_resolution.unwrap_or(Extent3d {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
depth_or_array_layers: 1,
|
||||
});
|
||||
let fps = fps.unwrap_or(24);
|
||||
let frames = frames.unwrap_or(u32::MAX);
|
||||
let skybox = skybox.unwrap_or("gum_trees_4k".to_string());
|
||||
start_headless(
|
||||
HeadlessMode::Bvh(bvh_dir, skybox),
|
||||
fps,
|
||||
&output_dir,
|
||||
frames,
|
||||
output_res,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Parsers
|
||||
//==============================================================================
|
||||
|
||||
fn extent3d_parser(input: &str) -> Result<Extent3d, String> {
|
||||
let split = input.split('x').collect::<Vec<_>>();
|
||||
|
||||
if split.len() != 2 {
|
||||
return Err(
|
||||
"Invalid format for the Resolution argument, must follow the following pattern: <WIDTH>x<HEIGHT>"
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
let Ok(width) = split[0].parse::<u32>() else {
|
||||
return Err("Width must be a number when defining the output resolution.".to_string());
|
||||
};
|
||||
|
||||
let Ok(height) = split[1].parse::<u32>() else {
|
||||
return Err("Width must be a number when defining the output resolution.".to_string());
|
||||
};
|
||||
|
||||
Ok(Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
})
|
||||
}
|
||||
+21
-3
@@ -1,11 +1,28 @@
|
||||
use bevy::{app::Update, window::close_on_esc};
|
||||
use studio::{StudioMode, ViewportSize};
|
||||
use clap::Parser;
|
||||
use studio::{screenshot::ScreenShotPlugin, StudioMode, ViewportSize};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "StorytellerStudio")]
|
||||
#[command(author = "The Storyteller Team")]
|
||||
#[command(version = "1.0")]
|
||||
#[command(about = "Executable for Storyteller Studio.")]
|
||||
#[command(propagate_version = true)]
|
||||
struct StudioArgs {
|
||||
model: Option<String>,
|
||||
skybox: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = StudioArgs::parse();
|
||||
|
||||
let model_path = args.model.unwrap_or("Mannequin.gltf".to_string());
|
||||
let skybox_path = args.skybox.unwrap_or("gum_trees_4k".to_string());
|
||||
|
||||
// Edit the arguments here to change the default startup scene or skybox
|
||||
let mut app = studio::start_internal(
|
||||
"bookcase.gltf",
|
||||
"gum_trees_4k",
|
||||
model_path.as_str(),
|
||||
skybox_path.as_str(),
|
||||
"", // ignored for non-WASM
|
||||
"Storyteller Studio",
|
||||
ViewportSize::default(),
|
||||
@@ -14,6 +31,7 @@ fn main() {
|
||||
|
||||
// Add any additional systems, resources, etc. for local debugging
|
||||
app.add_systems(Update, close_on_esc);
|
||||
app.add_plugins(ScreenShotPlugin);
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
+61
-3
@@ -9,19 +9,22 @@ use bevy::{
|
||||
math::vec2,
|
||||
pbr::ShadowFilteringMethod,
|
||||
prelude::*,
|
||||
render::primitives::Aabb,
|
||||
render::{camera::RenderTarget, primitives::Aabb},
|
||||
scene::InstanceId,
|
||||
};
|
||||
|
||||
use space_editor::{prelude::PrefabMarker, space_prefab::component::SceneAutoChild};
|
||||
#[cfg(feature = "wasm")]
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wgpu::{TextureDescriptor, TextureUsages};
|
||||
|
||||
use crate::{
|
||||
camera_controller::{CameraController, CameraControllerBundle},
|
||||
debug::DebugPoint,
|
||||
is_headless, is_not_headless,
|
||||
math::{Combinable, Extrema},
|
||||
MainCamera,
|
||||
screenshot::ScreenCapture,
|
||||
HeadlessSettings, MainCamera,
|
||||
};
|
||||
|
||||
pub struct ScenePlugin;
|
||||
@@ -35,7 +38,13 @@ impl Plugin for ScenePlugin {
|
||||
.insert_resource(Msaa::Off)
|
||||
.add_state::<SceneState>()
|
||||
.add_event::<LoadSceneEvent>()
|
||||
.add_systems(Startup, initialize)
|
||||
.add_systems(
|
||||
Startup,
|
||||
(
|
||||
initialize.run_if(is_not_headless),
|
||||
initialiaze_headless.run_if(is_headless),
|
||||
),
|
||||
)
|
||||
.add_systems(Update, init_scene_loading.in_set(SceneSpawnerSystem))
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Active),
|
||||
@@ -210,6 +219,55 @@ fn initialize(mut cmd: Commands) {
|
||||
));
|
||||
}
|
||||
|
||||
fn initialiaze_headless(
|
||||
mut cmd: Commands,
|
||||
mut image_assets: ResMut<Assets<Image>>,
|
||||
headless_settings: Res<HeadlessSettings>,
|
||||
) {
|
||||
// Camera, graphics settings
|
||||
let focus = Vec3::ZERO;
|
||||
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
|
||||
|
||||
let mut test_image = Image {
|
||||
texture_descriptor: TextureDescriptor {
|
||||
label: Some("render_texture"),
|
||||
size: headless_settings.output_dims,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: TextureUsages::TEXTURE_BINDING
|
||||
| TextureUsages::COPY_SRC
|
||||
| TextureUsages::COPY_DST
|
||||
| TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
test_image.resize(headless_settings.output_dims);
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
camera: Camera {
|
||||
hdr: false,
|
||||
target: RenderTarget::Image(image_assets.add(test_image)),
|
||||
..Default::default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
CameraControllerBundle::new(focus, camera_xform.translation),
|
||||
ScreenCapture::number_of_frames(
|
||||
headless_settings.frames_to_capture,
|
||||
&headless_settings.output_dir,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
fn init_scene_loading(mut cmd: Commands, mut er: EventReader<LoadSceneEvent>) {
|
||||
if let Some(id) = er.read().last() {
|
||||
info!("Loading scene: {}", id.0);
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
use bevy::{
|
||||
core_pipeline::{core_3d, prepass::ViewPrepassTextures},
|
||||
prelude::*,
|
||||
render::{
|
||||
render_graph::{RenderGraphApp, ViewNode, ViewNodeRunner},
|
||||
render_resource::{
|
||||
Buffer, BufferDescriptor, BufferUsages, ImageCopyBuffer, ImageDataLayout, MapMode,
|
||||
},
|
||||
renderer::RenderDevice,
|
||||
view::ViewTarget,
|
||||
Extract, Render, RenderSet,
|
||||
},
|
||||
};
|
||||
use bytemuck::AnyBitPattern;
|
||||
use image::{
|
||||
error::UnsupportedErrorKind, EncodableLayout, ImageBuffer, ImageError, Pixel,
|
||||
PixelWithColorType, Rgba,
|
||||
};
|
||||
use wgpu::{Extent3d, MaintainBase};
|
||||
|
||||
use crate::scene::SceneState;
|
||||
|
||||
//==============================================================================
|
||||
// Screen Shot Plugin
|
||||
//==============================================================================
|
||||
|
||||
pub struct ScreenShotPlugin;
|
||||
|
||||
impl Plugin for ScreenShotPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
(test_system, update_screen_capture_on_extract_step)
|
||||
.run_if(in_state(SceneState::Active)),
|
||||
);
|
||||
|
||||
let Ok(render_world) = app.get_sub_app_mut(bevy::render::RenderApp) else {
|
||||
return;
|
||||
};
|
||||
|
||||
render_world
|
||||
.add_systems(ExtractSchedule, extract_screen_capture_components)
|
||||
.add_systems(
|
||||
Render,
|
||||
(
|
||||
prepare_screen_capture_buffers.in_set(RenderSet::PrepareResources),
|
||||
save_buffer_to_file
|
||||
.after(RenderSet::Render)
|
||||
.before(RenderSet::Cleanup),
|
||||
),
|
||||
)
|
||||
//Render Graph Additions
|
||||
.add_render_graph_node::<ViewNodeRunner<ScreenCaptureNode>>(
|
||||
core_3d::graph::NAME,
|
||||
ScreenCaptureNode::NAME,
|
||||
)
|
||||
.add_render_graph_edges(
|
||||
core_3d::graph::NAME,
|
||||
&[
|
||||
core_3d::graph::node::TONEMAPPING,
|
||||
ScreenCaptureNode::NAME,
|
||||
core_3d::graph::node::END_MAIN_PASS_POST_PROCESSING,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn test_system(
|
||||
mut commands: Commands,
|
||||
camera: Query<Entity, With<Camera>>,
|
||||
input: Res<Input<KeyCode>>,
|
||||
) {
|
||||
if input.just_pressed(KeyCode::F10) {
|
||||
for entity in camera.iter() {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(ScreenCapture::number_of_frames(16, "./scrnshot/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Sceen Shot Component
|
||||
//==============================================================================
|
||||
|
||||
#[derive(Component, Clone)]
|
||||
pub struct ScreenCapture {
|
||||
number_of_frames: u32,
|
||||
to_directory: String,
|
||||
capture_depth: bool,
|
||||
}
|
||||
|
||||
impl ScreenCapture {
|
||||
pub fn number_of_frames(frames: u32, output_dir: &str) -> Self {
|
||||
ScreenCapture {
|
||||
number_of_frames: frames,
|
||||
to_directory: output_dir.to_string(),
|
||||
capture_depth: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capture_depth(mut self) -> Self {
|
||||
self.capture_depth = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
println!("Number of Frames Left: {}", self.number_of_frames);
|
||||
self.number_of_frames == 1
|
||||
}
|
||||
}
|
||||
|
||||
fn update_screen_capture_on_extract_step(
|
||||
mut commands: Commands,
|
||||
mut screen_captures: Query<(Entity, &mut ScreenCapture)>,
|
||||
) {
|
||||
for (entity, mut screen_capture) in screen_captures.iter_mut() {
|
||||
screen_capture.number_of_frames -= 1;
|
||||
if screen_capture.number_of_frames == 0 {
|
||||
commands.entity(entity).remove::<ScreenCapture>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//This system extracts the ScreenCaptureComponent to the render world.
|
||||
fn extract_screen_capture_components(
|
||||
mut commands: Commands,
|
||||
screen_capure_comps: Extract<Query<(Entity, &ScreenCapture), With<Camera>>>,
|
||||
) {
|
||||
for (entity, screen_capture) in screen_capure_comps.iter() {
|
||||
commands.get_or_spawn(entity).insert(screen_capture.clone());
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// ScreenCaptureBuffers
|
||||
//==============================================================================
|
||||
|
||||
#[derive(Component)]
|
||||
struct ScreenCaptureBuffers {
|
||||
main_buffer: Buffer,
|
||||
main_buffer_extent: Extent3d,
|
||||
main_bytes_per_row: u32,
|
||||
main_padded_bytes_per_row: u32,
|
||||
}
|
||||
|
||||
//During this function, we are making the buffers for the screenshot to be saved in
|
||||
//This is where we are instatiating the ScreenCaptureBuffers component.
|
||||
fn prepare_screen_capture_buffers(
|
||||
mut commands: Commands,
|
||||
views: Query<(Entity, &ViewTarget, &ScreenCapture)>,
|
||||
device: Res<RenderDevice>,
|
||||
) {
|
||||
for (entity, view_target, screen_capture) in views.iter() {
|
||||
if screen_capture.number_of_frames > 0 {
|
||||
let texture = view_target.main_texture();
|
||||
let size = texture.size();
|
||||
let format = texture.format();
|
||||
let bytes_per_row =
|
||||
(size.width / format.block_dimensions().0) * format.block_size(None).unwrap();
|
||||
let padded_bytes_per_row =
|
||||
RenderDevice::align_copy_bytes_per_row(bytes_per_row as usize) as u32;
|
||||
|
||||
let main_buffer = device.create_buffer(&BufferDescriptor {
|
||||
label: None,
|
||||
size: (size.height * bytes_per_row) as u64,
|
||||
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
commands.entity(entity).insert(ScreenCaptureBuffers {
|
||||
main_buffer,
|
||||
main_buffer_extent: size,
|
||||
main_bytes_per_row: bytes_per_row,
|
||||
main_padded_bytes_per_row: padded_bytes_per_row,
|
||||
});
|
||||
|
||||
println!("Buffers Created!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Screen Shot View Node
|
||||
//==============================================================================
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScreenCaptureNode;
|
||||
|
||||
impl ScreenCaptureNode {
|
||||
pub const NAME: &'static str = "ScreenCaptureNode";
|
||||
}
|
||||
|
||||
impl ViewNode for ScreenCaptureNode {
|
||||
type ViewQuery = (
|
||||
Entity,
|
||||
&'static ViewTarget,
|
||||
&'static ScreenCapture,
|
||||
&'static ScreenCaptureBuffers,
|
||||
Option<&'static ViewPrepassTextures>,
|
||||
);
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_: &mut bevy::render::render_graph::RenderGraphContext,
|
||||
render_context: &mut bevy::render::renderer::RenderContext,
|
||||
view_query: bevy::ecs::query::QueryItem<Self::ViewQuery>,
|
||||
_: &World,
|
||||
) -> Result<(), bevy::render::render_graph::NodeRunError> {
|
||||
render_context.command_encoder().copy_texture_to_buffer(
|
||||
view_query.1.main_texture().as_image_copy(),
|
||||
ImageCopyBuffer {
|
||||
buffer: &view_query.3.main_buffer,
|
||||
layout: ImageDataLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(view_query.3.main_padded_bytes_per_row),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
view_query.1.main_texture().size(),
|
||||
);
|
||||
println!("Saved to buffer!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Saving the buffer to file
|
||||
//==============================================================================
|
||||
|
||||
fn save_buffer_to_file(
|
||||
buffers: Query<(&ScreenCapture, &ScreenCaptureBuffers)>,
|
||||
device: Res<RenderDevice>,
|
||||
) {
|
||||
for (screen_capture, buffers) in buffers.iter() {
|
||||
if buffers.main_bytes_per_row != buffers.main_padded_bytes_per_row {
|
||||
println!("These don't match!");
|
||||
}
|
||||
|
||||
let main_image_values = {
|
||||
let main_buffer_slice = buffers.main_buffer.slice(..);
|
||||
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
main_buffer_slice.map_async(MapMode::Read, move |result| {
|
||||
tx.send(result).unwrap();
|
||||
});
|
||||
device.poll(MaintainBase::Wait);
|
||||
futures_lite::future::block_on(rx).unwrap().unwrap();
|
||||
|
||||
main_buffer_slice.get_mapped_range().to_vec()
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&screen_capture.to_directory)
|
||||
.expect("Output path could not be created");
|
||||
|
||||
let path = format!(
|
||||
"{}img_{:0.5}.png",
|
||||
screen_capture.to_directory, screen_capture.number_of_frames
|
||||
);
|
||||
|
||||
save_buffer::<Rgba<u8>>(&main_image_values, &buffers.main_buffer_extent, &path);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_buffer<P: Pixel + PixelWithColorType>(
|
||||
image_bytes: &[P::Subpixel],
|
||||
source_size: &Extent3d,
|
||||
path: &str,
|
||||
) where
|
||||
P::Subpixel: AnyBitPattern,
|
||||
[P::Subpixel]: EncodableLayout,
|
||||
{
|
||||
match ImageBuffer::<P, _>::from_raw(source_size.width, source_size.height, image_bytes) {
|
||||
Some(buffer) => {
|
||||
if let Err(ImageError::Unsupported(err)) = buffer.save(path) {
|
||||
if let UnsupportedErrorKind::Format(hint) = err.kind() {
|
||||
println!("Image format {} is not supported", hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("Failed creating image buffer for '{}'", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user