From e2186214101123febbf56c7722756b3bb751d809 Mon Sep 17 00:00:00 2001 From: Ilja Kartashov Date: Thu, 2 Jan 2025 10:58:06 +0100 Subject: [PATCH] Add simple terrain implementation --- Cargo.lock | 298 ++++-- Cargo.toml | 44 +- README.md | 2 + configs/terrain.toml | 14 + features/vulkan/Cargo.toml | 8 - features/vulkan/src/lib.rs | 14 - src/features.rs | 2 + src/features/terrain.rs | 311 ++++++ src/features/terrain/colormap.rs | 38 + src/features/terrain/generators.rs | 406 ++++++++ src/features/terrain/heightmap.rs | 254 +++++ src/features/terrain/renderer.rs | 882 ++++++++++++++++++ src/features/terrain/shaders/terrain.frag | 71 ++ src/features/terrain/shaders/terrain.frag.spv | Bin 0 -> 5028 bytes src/features/terrain/shaders/terrain.vert | 47 + src/features/terrain/shaders/terrain.vert.spv | Bin 0 -> 2452 bytes src/graphics.rs | 7 +- src/graphics/frame.rs | 307 +++++- src/graphics/vulkan.rs | 470 +++------- src/lib.rs | 6 +- src/loaders/gltf_loader.rs | 32 +- src/loaders/image_loader.rs | 14 +- src/models/animations.rs | 2 +- src/models/colors.rs | 22 + src/models/renderer.rs | 703 ++++---------- src/models/shaders/only_mesh.vert | 17 +- src/models/shaders/only_mesh.vert.spv | Bin 4000 -> 4344 bytes src/models/vertices.rs | 8 + src/tasks/scheduler.rs | 12 - src/tasks/worker.rs | 2 +- src/window.rs | 19 +- src/world.rs | 72 +- src/world/camera.rs | 10 +- src/world/storage.rs | 7 + terrain.png | Bin 0 -> 168780 bytes utils/terrain/main.rs | 200 ++++ 36 files changed, 3299 insertions(+), 1002 deletions(-) create mode 100644 configs/terrain.toml delete mode 100644 features/vulkan/Cargo.toml delete mode 100644 features/vulkan/src/lib.rs create mode 100644 src/features.rs create mode 100644 src/features/terrain.rs create mode 100644 src/features/terrain/colormap.rs create mode 100644 src/features/terrain/generators.rs create mode 100644 src/features/terrain/heightmap.rs create mode 100644 src/features/terrain/renderer.rs create mode 100644 src/features/terrain/shaders/terrain.frag create mode 100644 src/features/terrain/shaders/terrain.frag.spv create mode 100644 src/features/terrain/shaders/terrain.vert create mode 100644 src/features/terrain/shaders/terrain.vert.spv create mode 100644 terrain.png create mode 100644 utils/terrain/main.rs diff --git a/Cargo.lock b/Cargo.lock index 9452c797..674fd1b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.82" @@ -96,7 +105,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -146,6 +155,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -259,7 +279,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -368,6 +388,21 @@ dependencies = [ "rand 0.4.6", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "cocoa" version = "0.25.0" @@ -552,9 +587,14 @@ dependencies = [ "gltf", "image", "log", + "noise", + "num", "once_cell", "rand 0.8.5", "raw-window-handle", + "serde", + "structopt", + "toml 0.8.19", "uuid", "winit", ] @@ -658,7 +698,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -833,7 +873,7 @@ dependencies = [ "inflections", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -860,9 +900,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "heck" @@ -870,6 +919,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -898,7 +956,7 @@ dependencies = [ "exr", "gif", "image-webp", - "num-traits 0.2.18", + "num-traits 0.2.19", "png", "qoi", "ravif", @@ -927,9 +985,9 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" [[package]] name = "indexmap" -version = "2.1.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -949,7 +1007,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -1203,6 +1261,17 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "noise" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da45c8333f2e152fc665d78a380be060eb84fad8ca4c9f7ac8ca29216cff0cc" +dependencies = [ + "num-traits 0.2.19", + "rand 0.8.5", + "rand_xorshift", +] + [[package]] name = "nom" version = "7.1.3" @@ -1219,15 +1288,37 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits 0.2.19", +] + [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", - "num-traits 0.2.18", + "num-traits 0.2.19", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits 0.2.19", ] [[package]] @@ -1243,24 +1334,33 @@ dependencies = [ [[package]] name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits 0.2.19", +] + +[[package]] +name = "num-iter" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", - "num-traits 0.2.18", + "num-integer", + "num-traits 0.2.19", ] [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", - "num-traits 0.2.18", + "num-traits 0.2.19", ] [[package]] @@ -1269,14 +1369,14 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" dependencies = [ - "num-traits 0.2.18", + "num-traits 0.2.19", ] [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1299,7 +1399,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -1567,7 +1667,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -1609,7 +1709,7 @@ checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.3.9", "pin-project-lite", "rustix", "tracing", @@ -1632,11 +1732,35 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.101", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1667,9 +1791,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1732,6 +1856,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.3", +] + [[package]] name = "rav1e" version = "0.6.6" @@ -1754,7 +1887,7 @@ dependencies = [ "new_debug_unreachable", "noop_proc_macro", "num-derive", - "num-traits 0.2.18", + "num-traits 0.2.19", "once_cell", "paste", "rand 0.8.5", @@ -1943,22 +2076,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.192" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -1974,9 +2107,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -2060,6 +2193,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck 0.3.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.101", +] + [[package]] name = "syn" version = "1.0.101" @@ -2073,9 +2236,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2089,9 +2252,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", - "heck", + "heck 0.5.0", "pkg-config", - "toml 0.8.12", + "toml 0.8.19", "version-compare", ] @@ -2101,6 +2264,15 @@ version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -2118,7 +2290,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] @@ -2183,21 +2355,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.12", + "toml_edit 0.22.22", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -2215,15 +2387,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.7", + "winnow 0.6.20", ] [[package]] @@ -2275,6 +2447,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "url" version = "2.4.1" @@ -2308,10 +2486,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" dependencies = [ "aligned-vec", - "num-traits 0.2.18", + "num-traits 0.2.19", "wasm-bindgen", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version-compare" version = "0.2.0" @@ -2361,7 +2545,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -2395,7 +2579,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2841,9 +3025,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.7" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -2925,7 +3109,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.90", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9ee4156a..b7fac3e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,9 @@ name = "dotrix" version = "0.6.0" authors = [ - "Elias Kartašov ", - "Štěpán Wünsch ", - "Nikita Zemtsov ", + "Elias Kartašov ", + "Štěpán Wünsch ", + "Nikita Zemtsov ", ] edition = "2021" description = "3D Engine" @@ -18,27 +18,45 @@ path = "src/lib.rs" name = "demo" path = "demo/main.rs" +[[bin]] +name = "dotrix-terrain" +path = "utils/terrain/main.rs" +required-features = ["terrain"] + [features] -default = [] +default = ["terrain"] +terrain = [ + "dep:noise", + "dep:rand", + "dep:num", + "dep:structopt", + "dep:toml", + "dep:serde", +] [dependencies] -rand = "0.8" bytemuck = { version = "1.4", features = ["derive"] } image = "0.25" uuid = { version = "1.1", features = ["v4"] } log = "0.4.20" once_cell = "1.18.0" -futures = {version = "0.3", default-features = false, features = ["std", "executor"]} -raw-window-handle = {version = "0.6.1"} -winit = {version = "0.30.5", features = ["serde", "rwh_06"]} +futures = { version = "0.3", default-features = false, features = [ + "std", + "executor", +] } +raw-window-handle = { version = "0.6.1" } +winit = { version = "0.30.5", features = ["serde", "rwh_06"] } bitflags = "2.4.1" ash = "0.38.0" ash-window = "0.13.0" gltf = "1.4.1" base64 = "0.22.0" glam = { version = "0.27.0", features = ["bytemuck"] } -genmesh = "0.6.2" - -[dev-dependencies] -#noise = { version = "0.8" } -#bytemuck = { version = "1.4", features = ["derive"] } +genmesh = "0.6.2" # TODO: make optional +# optional +noise = { version = "0.9.0", optional = true } +num = { version = "0.4", optional = true } +rand = { version = "0.8.4", features = ["small_rng"], optional = true } +structopt = { version = "0.3", optional = true } +toml = { version = "0.8", optional = true } +serde = { version = "1.0", optional = true } diff --git a/README.md b/README.md index c8196195..6d184a88 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ glslc -fshader-stage=vertex src/models/shaders/only_mesh.vert -o src/models/shad glslc -fshader-stage=fragment src/models/shaders/only_mesh.frag -o src/models/shaders/only_mesh.frag.spv glslc -fshader-stage=vertex src/models/shaders/skin_mesh.vert -o src/models/shaders/skin_mesh.vert.spv glslc -fshader-stage=fragment src/models/shaders/skin_mesh.frag -o src/models/shaders/skin_mesh.frag.spv +glslc -fshader-stage=vertex src/features/terrain/shaders/terrain.vert -o src/features/terrain/shaders/terrain.vert.spv +glslc -fshader-stage=fragment src/features/terrain/shaders/terrain.frag -o src/features/terrain/shaders/terrain.frag.spv ``` ## Sponsors diff --git a/configs/terrain.toml b/configs/terrain.toml new file mode 100644 index 00000000..4af5a393 --- /dev/null +++ b/configs/terrain.toml @@ -0,0 +1,14 @@ +name = "terrain" +size = 1024 + +[noise] +octaves = 4 +persistence = 0.5 # 0..1 +lacunarity = 2.0 # > 1 +scale = 250.0 +offset = [0.0, 0.0] +seed = 0 + +[falloff] +power = 2.6 +factor = 2.4 diff --git a/features/vulkan/Cargo.toml b/features/vulkan/Cargo.toml deleted file mode 100644 index cc74bf0a..00000000 --- a/features/vulkan/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "vulkan" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] diff --git a/features/vulkan/src/lib.rs b/features/vulkan/src/lib.rs deleted file mode 100644 index 7d12d9af..00000000 --- a/features/vulkan/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/src/features.rs b/src/features.rs new file mode 100644 index 00000000..0a6c1c5b --- /dev/null +++ b/src/features.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "terrain")] +pub mod terrain; diff --git a/src/features/terrain.rs b/src/features/terrain.rs new file mode 100644 index 00000000..501341a1 --- /dev/null +++ b/src/features/terrain.rs @@ -0,0 +1,311 @@ +mod colormap; +mod generators; +mod heightmap; +mod renderer; + +use std::collections::HashMap; + +use crate::{Any, Assets, Camera, Entity, Id, Mesh, Mut, Ref, Task, VertexAttribute, World}; +pub use colormap::ColorMap; +pub use generators::{Generator, LowPolyTerrain, SimpleTerrain, TileSetup}; +pub use heightmap::{FalloffConfig, HeightMap, NoiseConfig}; +pub use renderer::{LodSetup, RenderTerrain, RenderTerrainSetup}; + +pub const TILE_MIN_SIZE: u32 = 2; +pub const TILE_MAX_SIZE: u32 = 254; +pub const DEFAULT_TILE_SIZE: u32 = 120; +pub const DEFAULT_TILES_IN_VIEW_RANGE: u32 = 4; +pub const DEFAULT_MAX_LODS: u32 = 3; +pub const DEFAULT_HEIGHT_AMPLIFIER: f32 = 100.0; + +pub struct Moisture { + pub value: f32, +} + +impl VertexAttribute for Moisture { + type Raw = f32; + fn name() -> &'static str { + "Moisture" + } + fn pack(&self) -> Self::Raw { + self.value + } + fn format() -> crate::Format { + crate::Format::Float32 + } +} + +/// Terrain Level of Details +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] +pub struct LoD { + value: u32, +} + +impl LoD { + pub fn new(value: u32) -> Self { + LoD { value } + } + pub fn factor(&self) -> u32 { + if self.value != 0 { + self.value * 2 + } else { + 1 + } + } + pub fn value(&self) -> u32 { + self.value + } +} + +/// Terrain component +pub struct Terrain { + /// Terrain mesh + pub mesh: Mesh, + /// Terrain X world position + pub x: f32, + /// Terrain Z world position + pub z: f32, + /// Terrain Level Of Details (0..) + pub lod: LoD, +} + +pub struct SpawnTerrain { + /// number of sections (each section represents a LOD) + tiles_in_view_range: u32, + /// maximal number of LODs + max_lods: u32, + /// spawned entities index + index: HashMap>, + /// Height Map asset name + heightmap: String, + /// Moisture Map asset name + moisturemap: String, + /// Color Map asset name + colormap: String, + /// generator + generator: Box, +} + +impl Default for SpawnTerrain { + fn default() -> Self { + let total_visible_tiles = 4 * DEFAULT_TILES_IN_VIEW_RANGE * DEFAULT_TILES_IN_VIEW_RANGE; + Self { + tiles_in_view_range: DEFAULT_TILES_IN_VIEW_RANGE, + max_lods: DEFAULT_MAX_LODS, + index: HashMap::with_capacity(total_visible_tiles as usize), + heightmap: String::from("terrain::heightmap"), + moisturemap: String::from("terrain::moisturemap"), + colormap: String::from("terrain::colormap"), + generator: Box::new(SimpleTerrain::new( + DEFAULT_TILE_SIZE, + DEFAULT_HEIGHT_AMPLIFIER, + )), + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Tile { + xi: i32, + zi: i32, + lod: LoD, +} + +impl SpawnTerrain { + pub fn with_tiles_in_view_range(mut self, tiles_in_view_range: u32) -> Self { + self.tiles_in_view_range = tiles_in_view_range; + self + } + + pub fn with_max_lods(mut self, max_lods: u32) -> Self { + self.max_lods = max_lods; + self + } + + pub fn with_heightmap(mut self, heightmap: impl Into) -> Self { + self.heightmap = heightmap.into(); + self + } + + pub fn with_moisturemap(mut self, moisturemap: impl Into) -> Self { + self.moisturemap = moisturemap.into(); + self + } + + pub fn with_colormap(mut self, colormap: impl Into) -> Self { + self.colormap = colormap.into(); + self + } + + pub fn with_generator(mut self, generator: Box) -> Self { + self.generator = generator; + self + } + + fn get_tile_lod(&self, xi: i32, zi: i32) -> LoD { + for i in 0..self.max_lods { + if self.tiles_in_view_range < i { + break; + } + + let boundary = (self.tiles_in_view_range - i) as i32; + if xi == -boundary || xi == boundary - 1 || zi == -boundary || zi == boundary - 1 { + return LoD::new(self.max_lods - i - 1); + } + } + LoD::default() + } +} + +#[derive(Default, Debug)] +pub struct SpawnTerrainOutput { + pub tiles_to_exile: Vec>, // must have + pub tiles_to_spawn: Vec>, // not very usefull + pub scene: Vec>, // could be usefull +} + +impl Task for SpawnTerrain { + type Output = SpawnTerrainOutput; + type Context = (Any, Mut, Ref); + + fn run(&mut self, (camera, mut world, assets): Self::Context) -> Self::Output { + // log::debug!("SpawnTerrain::run()"); + let mut index = HashMap::with_capacity(self.index.capacity()); + let mut tiles_to_spawn: Vec> = Vec::with_capacity(4); + let pos_x = camera.target.x; + let pos_z = camera.target.z; + //log::debug!("SpawnTerrain::run(@{};{})", pos_x, pos_z); + let tile_size = self.generator.tile_size(); + + let terrain_xi = (pos_x / (tile_size as f32)).floor() as i32; + let terrain_zi = (pos_z / (tile_size as f32)).floor() as i32; + + let tiles_in_view_range = self.tiles_in_view_range as i32; + + let heightmap = match assets + .find::(&self.heightmap) + .and_then(|id| assets.get(id)) + { + Some(heightmap) => heightmap, + None => { + log::warn!("Terrain `{}` asset is not ready", self.heightmap); + return SpawnTerrainOutput::default(); + } + }; + + let moisturemap = match assets + .find::(&self.moisturemap) + .and_then(|id| assets.get(id)) + { + Some(moisturemap) => moisturemap, + None => { + log::warn!("Terrain `{}` asset is not ready", self.moisturemap); + return SpawnTerrainOutput::default(); + } + }; + + let colormap = match assets + .find::(&self.colormap) + .and_then(|id| assets.get(id)) + { + Some(colormap) => colormap, + None => { + log::warn!("Terrain `{}` asset is not ready", self.colormap); + return SpawnTerrainOutput::default(); + } + }; + + for offset_z in -tiles_in_view_range..tiles_in_view_range { + for offset_x in -tiles_in_view_range..tiles_in_view_range { + let zi = terrain_zi + offset_z; + let xi = terrain_xi + offset_x; + let lod = self.get_tile_lod(offset_x, offset_z); + // log::debug!("tile LOD: {};{} -> {}", xi, zi, lod.value()); + let tile = Tile { xi, zi, lod }; + + if let Some(entity_id) = self.index.remove(&tile) { + index.insert(tile, entity_id); + } else { + let map_offset = heightmap.size() as i32 / 2; + let setup = TileSetup { + lod, + position_x: xi * tile_size as i32, + position_z: zi * tile_size as i32, + map_offset_x: -map_offset, + map_offset_z: -map_offset, + heightmap, + moisturemap, + colormap, + }; + + let terrain = self.generator.generate(&setup); + + let entity_id = world + .spawn(Some(Entity::empty().with(terrain))) + .next() + .expect("Terrain entity id was not returned after spawning"); + index.insert(tile, entity_id); + // log::debug!("spawn -> ({};{} -> {})", xi, zi, lod.value()); + tiles_to_spawn.push(entity_id); + } + } + } + + // for (tile, entity) in index.iter() { + // log::debug!("terrain setup: [{:?}]: {:?}", entity, tile); + // } + + let mut tiles_to_exile = Vec::with_capacity(self.index.len()); + // Clear terrain out of view range + for entity_id in self.index.values() { + /* TODO: exile is broken on ECS level + if let Some(entity) = world.exile(entity_id) { + if let Some(terrain) = entity.get::() { + log::debug!("exile -> ({})", terrain.lod.value()); + } else { + log::error!("exile -> entity is not a terrain: {:?}", entity_id); + } + } else { + log::error!("exile -> failed {:?}", entity_id); + } + */ + tiles_to_exile.push(*entity_id); + } + let scene = index.values().copied().collect::>(); + self.index = index; + + // log::debug!("tiles: to_exile - {}", tiles_to_exile.len()); + // log::debug!("tiles: to_spawn - {}", tiles_to_spawn.len()); + // log::debug!("tiles: total - {}", self.index.len()); + + SpawnTerrainOutput { + tiles_to_exile, + tiles_to_spawn, + scene, + } + } +} + +#[cfg(test)] +mod tests { + + use super::SpawnTerrain; + + #[test] + fn detect_tile_lod_by_axis() { + let spawn_terrain = SpawnTerrain::default(); + let tiles_in_view_range = spawn_terrain.tiles_in_view_range as i32; + + assert_eq!(spawn_terrain.get_tile_lod(0, 0).value(), 0); + assert_eq!( + spawn_terrain.get_tile_lod(-tiles_in_view_range, 2).value(), + 2 + ); + assert_eq!( + spawn_terrain + .get_tile_lod(-tiles_in_view_range + 1, 1) + .value(), + 1 + ); + } +} diff --git a/src/features/terrain/colormap.rs b/src/features/terrain/colormap.rs new file mode 100644 index 00000000..b1b28042 --- /dev/null +++ b/src/features/terrain/colormap.rs @@ -0,0 +1,38 @@ +use crate::Asset; + +pub struct ColorMap { + name: String, + colors: Vec<[u8; 3]>, + moisture_blend_factor: f32, +} + +impl ColorMap { + pub fn new(name: impl Into, colors: Vec<[u8; 3]>, moisture_blend_factor: f32) -> Self { + Self { + name: name.into(), + colors, + moisture_blend_factor, + } + } + + pub fn color(&self, height: f32, moisture: f32) -> [u8; 3] { + let color_count = self.colors.len() as f32; + let color_step = 1.0 / color_count; + let moisture_blend = + moisture * self.moisture_blend_factor - (self.moisture_blend_factor / 2.0); + let color_height = (height + moisture_blend).clamp(0.0, 1.0); + let color_index = (color_height / color_step).floor() as usize; + + self.colors[if color_index < self.colors.len() { + color_index + } else { + self.colors.len() - 1 + }] + } +} + +impl Asset for ColorMap { + fn name(&self) -> &str { + &self.name + } +} diff --git a/src/features/terrain/generators.rs b/src/features/terrain/generators.rs new file mode 100644 index 00000000..fcd8c3d2 --- /dev/null +++ b/src/features/terrain/generators.rs @@ -0,0 +1,406 @@ +use super::{ColorMap, HeightMap, LoD, Moisture, Terrain}; +use crate::math::Vec3; +use crate::{Color, Mesh, VertexNormal, VertexPosition}; + +pub struct TileSetup<'a> { + pub lod: LoD, + pub map_offset_x: i32, + pub map_offset_z: i32, + pub position_x: i32, + pub position_z: i32, + pub heightmap: &'a HeightMap, + pub moisturemap: &'a HeightMap, + pub colormap: &'a ColorMap, +} + +pub trait Generator: Send + Sync { + fn tile_size(&self) -> u32; + fn amplify_height(&self, heightmap_value: f32) -> f32; + fn vertices_count(&self, lod: LoD) -> u32; + fn indices_count(&self, lod: LoD) -> u32; + fn indices(&self, lod: LoD) -> Vec; + fn generate(&self, setup: &TileSetup) -> Terrain; +} + +pub struct SimpleTerrain { + pub tile_size: u32, + pub height_factor: f32, +} + +impl SimpleTerrain { + pub fn new(tile_size: u32, height_factor: f32) -> Self { + Self { + tile_size, + height_factor, + } + } +} + +impl Generator for SimpleTerrain { + fn tile_size(&self) -> u32 { + self.tile_size + } + fn amplify_height(&self, heightmap_value: f32) -> f32 { + self.height_factor * heightmap_value.powi(4) + } + fn vertices_count(&self, lod: LoD) -> u32 { + let tile_size = self.tile_size / lod.factor(); + let vertices_per_side = tile_size + 1; + vertices_per_side * vertices_per_side + } + fn indices_count(&self, lod: LoD) -> u32 { + let tile_size = self.tile_size / lod.factor(); + tile_size * tile_size * 2 * 3 + } + fn indices(&self, lod: LoD) -> Vec { + let indices_count = self.indices_count(lod); + let tile_size = self.tile_size / lod.factor(); + + let mut indices = Vec::with_capacity(indices_count as usize); + let vertices_per_side = tile_size + 1; + for xi in 0..tile_size { + let offset = xi * vertices_per_side; + for zi in 0..tile_size { + /* + A *---* B + | \ | + D *---* C + */ + let index_a = offset + zi; + let index_b = index_a + 1; + let index_c = index_b + vertices_per_side; + let index_d = index_a + vertices_per_side; + + indices.extend([ + index_a, index_c, index_b, // face ACB + index_c, index_a, index_d, // face CAD + ]); + } + } + + // log::debug!(" terrain[{}]: indices=\n{:?}", tile_size, indices); + indices + } + fn generate(&self, setup: &TileSetup) -> Terrain { + let lod_factor = setup.lod.factor(); + let sqares_per_side = self.tile_size / lod_factor; + let vertices_per_side = sqares_per_side + 1; + let vertices_count = vertices_per_side * vertices_per_side; + let mut vertices = Vec::with_capacity(vertices_count as usize); + let mut colors = Vec::with_capacity(vertices_count as usize); + let mut normals = vec![[0.0, 0.0, 0.0]; vertices_count as usize]; + let mut moistures = vec![0.0; vertices_count as usize]; + + // log::debug!( + // "Generate terrain: lod={}, vertices_count={}", + // self.lod.value(), + // vertices_count + // ); + + let map_offset = (setup.heightmap.size() as i32) / 2; + + for zi in 0..vertices_per_side { + for xi in 0..vertices_per_side { + let world_x = setup.position_x + (xi * lod_factor) as i32; + let world_z = setup.position_z + (zi * lod_factor) as i32; + let map_x = (map_offset + world_x).clamp(0, setup.heightmap.size() as i32) as u32; + let map_z = (map_offset + world_z).clamp(0, setup.heightmap.size() as i32) as u32; + let height = setup.heightmap.value(map_x, map_z); + let world_y = self.amplify_height(height); + + /* + log::debug!( + "world:({};{}), map:({}:{}; size:{}, offset: {}), height:({}->{})", + world_x, + world_z, + map_x, + map_z, + heightmap.size(), + map_offset, + height, + world_y + );*/ + + vertices.push([world_x as f32, world_y, world_z as f32]); + + let moisture = setup.moisturemap.value(map_x, map_z); + moistures.push(moisture); + + let color: Color = + (&Color::::from(setup.colormap.color(height, moisture))).into(); + /* + let color = (color[0] as u32) << 24 + | (color[1] as u32) << 16 + | (color[2] as u32) << 8 + | 255; + */ + // let color = match self.lod.value() { + // 0 => Color::rgb(1.0, 0.0, 0.0), + // 1 => Color::rgb(0.0, 1.0, 0.0), + // 2 => Color::rgb(0.0, 0.0, 1.0), + // _ => Color::rgb(1.0, 1.0, 1.0), + // }; + colors.push((&color).into()); + } + } + + for zi in 0..sqares_per_side { + let offset = zi * vertices_per_side; + for xi in 0..sqares_per_side { + /* + A *---* B + | \ | + D *---* C + */ + let index_a = (offset + xi) as usize; + let index_b = index_a + 1; + let index_c = index_b + vertices_per_side as usize; + let index_d = index_a + vertices_per_side as usize; + + let vertex_a = Vec3::from(vertices[index_a]); + let vertex_b = Vec3::from(vertices[index_b]); + let vertex_c = Vec3::from(vertices[index_c]); + let vertex_d = Vec3::from(vertices[index_d]); + + // face ACB + normals[index_b] = (vertex_b - vertex_a) + .cross(vertex_c - vertex_a) + .normalize() + .into(); + // face CAD + normals[index_d] = (vertex_d - vertex_c) + .cross(vertex_a - vertex_c) + .normalize() + .into(); + } + } + + // log::debug!(" terrain: lod={}", self.lod.value(),); + // log::debug!(" terrain: vertices=\n{:?}", vertices); + // log::debug!(" terrain: colors=\n{:?}", colors,); + + let mut mesh = Mesh::new("terrain"); + mesh.set_vertices::(vertices); + mesh.set_vertices::(normals); + mesh.set_vertices::>(colors); + mesh.set_vertices::(moistures); + + Terrain { + mesh, + lod: setup.lod, + x: setup.position_x as f32, + z: setup.position_z as f32, + } + } +} + +pub struct LowPolyTerrain { + pub tile_size: u32, + pub height_factor: f32, +} + +impl LowPolyTerrain { + pub fn new(tile_size: u32, height_factor: f32) -> Self { + Self { + tile_size, + height_factor, + } + } + + fn calculate_duplicated_vertex_index(index: u32, vertices_per_side: u32, row: u32) -> u32 { + vertices_per_side * vertices_per_side + (index - vertices_per_side - (row - 1) * 2 - 1) + } + fn generate_sqaure_indices(square_x: u32, square_z: u32, vertices_per_side: u32) -> [u32; 6] { + let offset = square_z * vertices_per_side; + let top_left = offset + square_x; + let top_right = top_left + 1; + let bottom_left = top_left + vertices_per_side; + let bottom_right = top_right + vertices_per_side; + + let last_square = vertices_per_side - 2; + + if square_x % 2 == 0 { + // duplicate vertices + let real_top_left = if square_x == 0 || square_z == 0 { + top_left + } else { + Self::calculate_duplicated_vertex_index(top_left, vertices_per_side, square_z) + }; + let real_top_right = if square_x == last_square || square_z == 0 { + top_right + } else { + Self::calculate_duplicated_vertex_index(top_right, vertices_per_side, square_z) + }; + if square_z % 2 == 0 { + [ + real_top_left, + bottom_left, + bottom_right, + real_top_right, + top_left, + bottom_right, + ] + } else { + [ + real_top_left, + bottom_left, + top_right, + real_top_right, + bottom_left, + bottom_right, + ] + } + } else if square_z % 2 == 0 { + [ + bottom_left, + top_right, + top_left, + bottom_right, + top_right, + bottom_left, + ] + } else { + [ + bottom_left, + bottom_right, + top_left, + bottom_right, + top_right, + top_left, + ] + } + } +} + +impl Generator for LowPolyTerrain { + fn tile_size(&self) -> u32 { + self.tile_size + } + fn amplify_height(&self, heightmap_value: f32) -> f32 { + self.height_factor * heightmap_value.powi(4) + } + fn vertices_count(&self, lod: LoD) -> u32 { + let tile_size = self.tile_size / lod.factor(); + let vertices_per_side = tile_size + 1; + let duplicated_per_side = tile_size - 1; + vertices_per_side * vertices_per_side + duplicated_per_side * duplicated_per_side + } + fn indices_count(&self, lod: LoD) -> u32 { + let tile_size = self.tile_size / lod.factor(); + tile_size * tile_size * 2 * 3 + } + fn indices(&self, lod: LoD) -> Vec { + let indices_count = self.indices_count(lod); + let tile_size = self.tile_size / lod.factor(); + let squares_per_side = tile_size; + let vertices_per_side = tile_size + 1; + let mut indices = Vec::with_capacity(indices_count as usize); + + for square_z in 0..squares_per_side { + for square_x in 0..squares_per_side { + let square_indices = + Self::generate_sqaure_indices(square_x, square_z, vertices_per_side); + indices.extend(square_indices.into_iter()); + } + } + // log::debug!(" terrain[{}]: indices=\n{:?}", tile_size, indices); + indices + } + fn generate(&self, setup: &TileSetup) -> Terrain { + let lod_factor = setup.lod.factor(); + let squares_per_side = self.tile_size / lod_factor; + let vertices_per_side = squares_per_side + 1; + let duplicates_per_side = squares_per_side - 1; + let unique_vertices_count = vertices_per_side * vertices_per_side; + let vertices_count = unique_vertices_count + duplicates_per_side * duplicates_per_side; + let mut vertices = vec![[0.0, 0.0, 0.0]; vertices_count as usize]; + let mut colors = vec![[0.0, 0.0, 0.0, 0.0]; vertices_count as usize]; + let mut normals = vec![[0.0, 0.0, 0.0]; vertices_count as usize]; + let mut moistures = vec![0.0; vertices_count as usize]; + + // log::debug!( + // "Generate terrain: lod={}, vertices_count={}", + // self.lod.value(), + // vertices_count + // ); + + let map_offset = (setup.heightmap.size() as i32) / 2; + let mut vertex_cursor = 0; + let mut duplicates_cursor = unique_vertices_count as usize; + + for zi in 0..vertices_per_side { + for xi in 0..vertices_per_side { + let world_x = setup.position_x + (xi * lod_factor) as i32; + let world_z = setup.position_z + (zi * lod_factor) as i32; + let map_x = (map_offset + world_x).clamp(0, setup.heightmap.size() as i32) as u32; + let map_z = (map_offset + world_z).clamp(0, setup.heightmap.size() as i32) as u32; + let height = setup.heightmap.value(map_x, map_z); + let world_y = self.amplify_height(height); + + /* + log::debug!( + "world:({};{}), map:({}:{}; size:{}, offset: {}), height:({}->{})", + world_x, + world_z, + map_x, + map_z, + heightmap.size(), + map_offset, + height, + world_y + );*/ + + vertices[vertex_cursor] = [world_x as f32, world_y, world_z as f32]; + let moisture = setup.moisturemap.value(map_x, map_z); + let color: Color = + (&Color::::from(setup.colormap.color(height, moisture))).into(); + // let color: Color = Color::grey(); + colors[vertex_cursor] = (&color).into(); + moistures[vertex_cursor] = moisture; + if xi != 0 && xi != squares_per_side && zi != 0 && zi != squares_per_side { + // duplicate vertex + vertices[duplicates_cursor] = [world_x as f32, world_y, world_z as f32]; + colors[duplicates_cursor] = (&color).into(); + duplicates_cursor += 1; + } + vertex_cursor += 1; + } + } + + for square_z in 0..squares_per_side { + for square_x in 0..squares_per_side { + let indices = Self::generate_sqaure_indices(square_x, square_z, vertices_per_side); + + // log::debug!("square: ({}, {}) -> {:?}", square_x, square_z, indices)); + + for triangle in 0..2 { + let base_index = triangle * 3; + let index0 = indices[base_index] as usize; + let index1 = indices[base_index + 1] as usize; + let index2 = indices[base_index + 2] as usize; + normals[index0] = (Vec3::from(vertices[index1]) - Vec3::from(vertices[index0])) + .cross(Vec3::from(vertices[index2]) - Vec3::from(vertices[index0])) + .normalize() + .into(); + } + } + } + + // log::debug!(" terrain: lod={}", self.lod.value(),); + // log::debug!(" terrain: vertices=\n{:?}", vertices); + // log::debug!(" terrain: colors=\n{:?}", colors,); + + let mut mesh = Mesh::new("terrain"); + mesh.set_vertices::(vertices); + mesh.set_vertices::(normals); + mesh.set_vertices::>(colors); + mesh.set_vertices::(moistures); + + Terrain { + mesh, + lod: setup.lod, + x: setup.position_x as f32, + z: setup.position_z as f32, + } + } +} diff --git a/src/features/terrain/heightmap.rs b/src/features/terrain/heightmap.rs new file mode 100644 index 00000000..8903f996 --- /dev/null +++ b/src/features/terrain/heightmap.rs @@ -0,0 +1,254 @@ +use noise::{NoiseFn, Perlin}; +use serde::{Deserialize, Serialize}; + +use rand::rngs::SmallRng; +use rand::{RngCore, SeedableRng}; + +use crate::Asset; + +pub struct HeightMap { + name: String, + values: Vec, + size: u32, +} + +impl HeightMap { + pub fn new(name: impl Into, size: u32) -> Self { + Self { + name: name.into(), + values: vec![0.0; (size * size) as usize], + size, + } + } + + pub fn new_from_noise(name: impl Into, size: u32, noise_config: &NoiseConfig) -> Self { + let mut noise_values = vec![0.0; (size * size) as usize]; + let noise = Perlin::new(noise_config.seed); + + let mut pseudo_rng = SmallRng::seed_from_u64(noise_config.seed as u64); + + let octaves_offsets = (0..noise_config.octaves) + .map(|_| { + [ + Self::randomize_offset(noise_config.offset[0], &mut pseudo_rng), + Self::randomize_offset(noise_config.offset[1], &mut pseudo_rng), + ] + }) + .collect::>(); + + let mut min_noise_value: f32 = f32::MAX; + let mut max_noise_value: f32 = f32::MIN; + + let half_size = (size / 2) as f32; // to apply scale to the center + for z in 0..size { + let offset = z * size; + for x in 0..size { + let index = (offset + x) as usize; + let mut noise_value = 0.0; + + let mut amplitude = 1.0; + let mut frequency = 1.0; + + for octave_offset in octaves_offsets.iter() { + let xf = + (x as f32 - half_size) / noise_config.scale * frequency + octave_offset[0]; + let zf = + (z as f32 - half_size) / noise_config.scale * frequency + octave_offset[1]; + + let noise_octave_value = 2.0 * (noise.get([xf as f64, zf as f64]) as f32) - 1.0; + noise_value += noise_octave_value * amplitude; + + amplitude *= noise_config.persistence; + frequency *= noise_config.lacunarity; + } + + if noise_value < min_noise_value { + min_noise_value = noise_value; + } + + if noise_value > max_noise_value { + max_noise_value = noise_value; + } + + noise_values[index] = noise_value; + } + } + + // normalize values + let delta = max_noise_value - min_noise_value; + let values = noise_values + .into_iter() + .map(|value| (((value - min_noise_value) / delta).clamp(0.0, 1.0))) + .collect::>(); + + Self { + name: name.into(), + values, + size, + } + } + + /// Returns falloff map + pub fn new_from_falloff( + name: impl Into, + size: u32, + falloff_config: &FalloffConfig, + ) -> Self { + let mut values = vec![0.0; (size * size) as usize]; + for z in 0..size { + let offset = z * size; + for x in 0..size { + let index = (offset + x) as usize; + let value_x = (x as f32 / size as f32 * 2.0 - 1.0).abs(); + let value_z = (z as f32 / size as f32 * 2.0 - 1.0).abs(); + let value = if value_x > value_z { value_x } else { value_z }; + // soften the curve + let power_of_value = value.powf(falloff_config.power); + let value = power_of_value + / (power_of_value + + (falloff_config.factor - falloff_config.factor * value) + .powf(falloff_config.power)); + + values[index] = value.clamp(0.0, 1.0); + } + } + Self { + name: name.into(), + size, + values, + } + } + + pub fn new_from_bytes(name: impl Into, size: u32, bytes: &[u8]) -> Self { + let pixels_count = (size * size) as usize; + let bytes_per_pixel = bytes.len() / pixels_count; + let mut values = vec![0.0; pixels_count]; + + let mut max_value: u32 = 0; + for _ in 0..bytes_per_pixel { + max_value = (max_value << 8) | 0xFF; + } + + //log::debug!("max_value: {}", max_value); + + for i in 0..pixels_count { + let mut value_uint: u32 = 0; + for byte in (0..bytes_per_pixel).rev() { + // value_uint = + // value_uint | ((bytes[i * bytes_per_pixel + byte] as u32) << byte_shift); + value_uint = (value_uint << 8) | (bytes[i * bytes_per_pixel + byte] as u32); + } + // log::debug!("value_uint: 0x{:.04x}", value_uint); + values[i] = (value_uint as f64 / max_value as f64) as f32; + } + // panic!("brea"); + Self { + name: name.into(), + values, + size, + } + } + + pub fn size(&self) -> u32 { + self.size + } + + pub fn value(&self, x: u32, z: u32) -> f32 { + let i = (z * self.size + x) as usize; + self.values[i] + } + + pub fn subtract(&mut self, map: &Self) { + if self.size != map.size { + panic!( + "Could not subtract maps of different sizes: {} != {}", + self.size, map.size, + ); + } + + for z in 0..self.size { + let offset = z * self.size; + for x in 0..self.size { + let i = (offset + x) as usize; + self.values[i] = (self.values[i] - map.values[i]).clamp(0.0, 1.0); + } + } + } + + fn randomize_offset(value: f32, pseudo_rng: &mut SmallRng) -> f32 { + value + (pseudo_rng.next_u32() & 0xFFFF) as f32 - 32768.0 + } + + pub fn write_to_file( + &self, + path: &std::path::Path, + format: image::ImageFormat, + ) -> Result<(), image::ImageError> { + let size = self.size; + let buffer = (0..size) + .flat_map(|z| (0..size).map(move |x| ((self.value(x, z) * 65535.0).round() as u16))) + .collect::>(); + + let image_buffer: image::ImageBuffer, Vec> = + image::ImageBuffer::from_vec(size, size, buffer) + .expect("Could not generate heightmap pixels buffers"); + + image_buffer + //image::GrayImage::from_raw(size, size, pixels) + .save_with_format(path, format) + } +} + +impl Asset for HeightMap { + fn name(&self) -> &str { + &self.name + } +} + +/// Noise configuration +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +pub struct NoiseConfig { + /// Number of octaves + pub octaves: u32, + /// Noise persistence + pub persistence: f32, + /// Noise Lacunarity + pub lacunarity: f32, + /// Noise scale + pub scale: f32, + /// Offset of the noise sampling + pub offset: [f32; 2], + /// Noise seed + pub seed: u32, +} + +impl Default for NoiseConfig { + fn default() -> Self { + Self { + octaves: 4, + persistence: 0.5, + lacunarity: 2.0, + scale: 250.0, + offset: [0.0, 0.0], + seed: 0, + } + } +} + +/// Falloff settings +#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +pub struct FalloffConfig { + /// How rough should fall off + pub power: f32, + /// How far should fall off + pub factor: f32, +} + +impl Default for FalloffConfig { + fn default() -> Self { + Self { + power: 2.6, + factor: 2.4, + } + } +} diff --git a/src/features/terrain/renderer.rs b/src/features/terrain/renderer.rs new file mode 100644 index 00000000..514da303 --- /dev/null +++ b/src/features/terrain/renderer.rs @@ -0,0 +1,882 @@ +use std::collections::HashMap; +use std::io::Cursor; + +use ash::vk::Handle; + +use crate::graphics::vk; +use crate::graphics::{Buffer, CommandRecorder, Display, Extent2D, Gpu, RenderSubmit}; +use crate::models::{Transform3D, VertexBufferLayout, VertexNormal, VertexPosition}; +use crate::{Any, Camera, Entity, Frame, Id, Ref, Task, World}; + +use super::{ + LoD, Moisture, SpawnTerrainOutput, Terrain, DEFAULT_TILES_IN_VIEW_RANGE, DEFAULT_TILE_SIZE, +}; + +pub type MeshLayout = (VertexPosition, VertexNormal, Moisture); + +/// Terrain rendering task +pub struct RenderTerrain { + /// Wait for render submits from following + wait_for: Vec, + /// GPU instance + gpu: Gpu, + /// Version of surface to track changes and update framebuffers and fender pass + surface_version: u64, + /// globals buffer + globals_buffer: Buffer, + /// transform buffer + instance_buffer: Buffer, + /// indirect buffer + indirect_buffer: Buffer, + /// Indices buffer + index_buffer: Buffer, + /// Vertex buffer + vertex_buffer: Buffer, + /// Used bytes in vertex buffer + vertex_buffer_usage: u64, + /// descriptor sets + descriptor_sets: Vec, + /// descriptor pool + _descriptor_pool: vk::DescriptorPool, + /// descriptor set layouts + _desc_set_layouts: [vk::DescriptorSetLayout; 1], + /// Pipeline layout to render models + pipeline_layout_render: vk::PipelineLayout, + /// Graphics pipeline to render models + pipeline_render: vk::Pipeline, + /// Vertex shader module + shader_vertex: vk::ShaderModule, + /// Fragment shader module + shader_fragment: vk::ShaderModule, + /// Terrain tiles index + tiles_index: HashMap, Slot>, + /// Lod info index + lods_index: HashMap, + /// Instance buffer data + instance_buffer_data: Vec, + /// Indirect buffer data + indirect_buffer_data: Vec, +} + +#[derive(Clone, Copy, Debug, Default)] +struct Slot { + /// Terrain tile offset in the buffer + offset: u64, + /// Size + size: u64, +} + +#[derive(Clone, Debug, Default)] +pub struct LodSetup { + pub lod: LoD, + pub vertices_count: u32, + pub indices: Vec, +} + +#[derive(Clone, Copy, Debug, Default)] +struct LodInfo { + /// Number of vertices + vertices_count: u32, + /// Number of indices + indices_count: u32, + /// Base index of the LoD in buffer + first_index: u32, + /// Number of tiles + tiles_count: u32, +} + +impl Drop for RenderTerrain { + fn drop(&mut self) { + unsafe { + self.gpu.device_wait_idle().unwrap(); + + // pipelines + if self.pipeline_render != vk::Pipeline::null() { + self.gpu.destroy_pipeline(self.pipeline_render); + } + + // pipelines layouts + self.gpu + .destroy_pipeline_layout(self.pipeline_layout_render); + + // shaders + self.gpu.destroy_shader_module(self.shader_vertex); + self.gpu.destroy_shader_module(self.shader_fragment); + + // buffers + self.globals_buffer.free_memory_and_destroy(&self.gpu); + self.index_buffer.free_memory_and_destroy(&self.gpu); + self.vertex_buffer.free_memory_and_destroy(&self.gpu); + self.indirect_buffer.free_memory_and_destroy(&self.gpu); + self.instance_buffer.free_memory_and_destroy(&self.gpu); + + // descriptors + for &descriptor_set_layout in self._desc_set_layouts.iter() { + self.gpu + .destroy_descriptor_set_layout(descriptor_set_layout); + } + self.gpu.destroy_descriptor_pool(self._descriptor_pool); + }; + } +} + +impl Task for RenderTerrain { + type Output = RenderSubmit; + type Context = ( + Any, + Any, + Any, + Ref, + Ref, + ); + + fn run( + &mut self, + (spawn_terrain_output, camera, frame, display, world): Self::Context, + ) -> Self::Output { + let globals_uniform = [self.globals_uniform(&camera)]; + unsafe { + self.globals_buffer + .map_and_write_to_device_memory(&self.gpu, 0, &globals_uniform); + } + // verify pipeline + if let Some(surface_version) = display.surface_changed(self.surface_version) { + unsafe { + log::debug!("resize: Surface changed"); + self.gpu.device_wait_idle().unwrap(); + + // rebuild pipelines + if self.pipeline_render.is_null() { + log::debug!("resize: create_graphics_pipelines"); + self.pipeline_render = self.create_graphics_pipelines( + display.render_pass(), + display.surface_resolution(), + )[0]; + } + }; + self.surface_version = surface_version; + } + + // clear for new cycle + self.instance_buffer_data.clear(); + self.indirect_buffer_data.clear(); + + // TODO: to calculate UVs for terrain color samplier we will need to have global offset for + // X and Z to subtract from tile coordinates + + // 1. Mark slots occupied by exiled terrain as free + let mut free_slots = spawn_terrain_output + .tiles_to_exile + .iter() + .map(|i| { + self.tiles_index + .remove(i) + .expect("Terrain index is corrupted") + }) + .collect::>(); + + // 2. Iterate over terrain in the world and store in buffer missing meshes + let vertex_size = MeshLayout::vertex_size(); + + let mut lods_counters = HashMap::::new(); + + for (id, terrain) in world.query::<(&Id, &Terrain)>() { + if !spawn_terrain_output.scene.contains(id) { + log::error!("Garbage terrain from ECS: {:?}", id); + continue; + } + let lod_info = match self.lods_index.get(&terrain.lod) { + Some(lod_info) => lod_info, + None => panic!("No LoD info for {:?}", terrain.lod), + }; + let instance = InstanceUniform { + transform: Transform3D::default().matrix().to_cols_array_2d(), + }; + self.instance_buffer_data.push(instance); + + if !self.tiles_index.contains_key(id) { + let mesh_buffer_size = (vertex_size as u64) * (lod_info.vertices_count as u64); + let slot = self.find_free_slot(&mut free_slots, mesh_buffer_size); + let reuse_slot = slot.is_some(); + let slot = slot.unwrap_or(Slot { + offset: self.vertex_buffer_usage, + size: mesh_buffer_size, + }); + + if !reuse_slot && self.vertex_buffer_usage >= self.vertex_buffer.size { + log::error!( + "tiles: Vertex buffer will overflow by {} bytes of mesh (lod={})!", + mesh_buffer_size, + terrain.lod.value() + ); + } + self.tiles_index.insert(*id, slot); + // write to vertex buffer + let mesh_data = terrain + .mesh + .buffer::() + .expect("Terrain mesh must have required attributes"); + + let counter = lods_counters.entry(terrain.lod).or_insert(0); + + *counter += 1; + let vertex_buffer_usage = unsafe { + self.vertex_buffer.map_and_write_to_device_memory( + &self.gpu, + slot.offset, + mesh_data.as_slice(), + ) + }; + + if !reuse_slot { + self.vertex_buffer_usage += vertex_buffer_usage; + } + } + + let slot = match self.tiles_index.get(id) { + Some(slot) => slot, + None => panic!("Terrain slot information was not found for {id:?}"), + }; + + let instance_number = self.indirect_buffer_data.len() as u32; + self.indirect_buffer_data + .push(vk::DrawIndexedIndirectCommand { + instance_count: 1, + first_instance: instance_number, + first_index: lod_info.first_index, + index_count: lod_info.indices_count, + vertex_offset: (slot.offset / (vertex_size as u64)) as i32, + }); + } + + unsafe { + self.instance_buffer.map_and_write_to_device_memory( + &self.gpu, + 0, + self.instance_buffer_data.as_slice(), + ) + }; + + unsafe { + self.indirect_buffer.map_and_write_to_device_memory( + &self.gpu, + 0, + self.indirect_buffer_data.as_slice(), + ); + }; + // 3. Prepare and execute render pass + + let command_recorder = Recorder { + resolution: frame.resolution, + draw_count: self.indirect_buffer_data.len() as u32, + vertex_buffer: self.vertex_buffer.handle, + index_buffer: self.index_buffer.handle, + indirect_buffer: self.indirect_buffer.handle, + descriptor_sets: self.descriptor_sets.clone(), + pipeline_layout: self.pipeline_layout_render, + pipeline: self.pipeline_render, + }; + + RenderSubmit::new::(Box::new(command_recorder), &self.wait_for) + + /* + unsafe { + self.record_draw_commands(&frame, self.indirect_buffer_data.len() as u32); + } + + CommandBufferSubmitInfo { + wait_semaphores: self.wait_semaphores.clone(), + wait_dst_stage_mask: self + .wait_semaphores + .iter() + .map(|_| vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT) + .collect::>(), + command_buffers: vec![self.command_buffer_draw], + signal_semaphores: vec![self.signal_semaphore], + } + */ + } +} + +impl RenderTerrain { + pub fn setup() -> RenderTerrainSetup { + RenderTerrainSetup::default() + } + + pub fn new(display: &mut Display, setup: RenderTerrainSetup) -> Self { + let terrain_tiles_capacity = 4 * setup.tiles_in_view_range * setup.tiles_in_view_range; + + let gpu = display.gpu(); + + let globals_buffer = unsafe { + Self::create_globals_uniform_buffer(&gpu) + .expect("Could not allocate globals uniform buffer") + }; + + let instance_buffer = unsafe { + Self::create_storage_buffer( + &gpu, + (terrain_tiles_capacity as u64) * (std::mem::size_of::() as u64), + ) + .expect("Could not allocate instance storage buffer") + }; + + let indirect_buffer = unsafe { + Self::create_indirect_buffer( + &gpu, + (terrain_tiles_capacity as u64) + * (std::mem::size_of::() as u64), + ) + .expect("Could not allocate indirect buffer") + }; + + let mut index_buffer_data = Vec::new(); + let lods_index = Self::generate_lods_index( + setup.tiles_in_view_range, + setup.lods, + &mut index_buffer_data, + ); + + let vertex_buffer_size = lods_index + .values() + .map(|lod_info| { + (lod_info.vertices_count as u64) + * (lod_info.tiles_count as u64) + * (MeshLayout::vertex_size() as u64) + }) + .sum(); + + let index_buffer = unsafe { + Self::create_index_buffer( + &gpu, + (index_buffer_data.len() * std::mem::size_of::()) as u64, + ) + .expect("Could not allocate index buffer") + }; + // write indices to index buffer + unsafe { + index_buffer.map_and_write_to_device_memory(&gpu, 0, index_buffer_data.as_slice()); + }; + + let vertex_buffer = unsafe { + Self::create_vertex_buffer(&gpu, vertex_buffer_size) + .expect("Could not allocate vertex buffer (non-rigged)") + }; + + // bindings layout + let descriptor_sizes = [ + // Globals + vk::DescriptorPoolSize { + ty: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: 1, + }, + // Instances + vk::DescriptorPoolSize { + ty: vk::DescriptorType::STORAGE_BUFFER, + descriptor_count: 1, + }, + ]; + let descriptor_pool_info = vk::DescriptorPoolCreateInfo::default() + .pool_sizes(&descriptor_sizes) + .max_sets(1); + let descriptor_pool = unsafe { gpu.create_descriptor_pool(&descriptor_pool_info).unwrap() }; + + let desc_layout_bindings = [ + // Globals + vk::DescriptorSetLayoutBinding { + binding: 0, + descriptor_type: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: 1, + stage_flags: vk::ShaderStageFlags::VERTEX, + ..Default::default() + }, + // Instances + vk::DescriptorSetLayoutBinding { + binding: 1, + descriptor_type: vk::DescriptorType::STORAGE_BUFFER, + descriptor_count: 1, + stage_flags: vk::ShaderStageFlags::VERTEX | vk::ShaderStageFlags::FRAGMENT, + ..Default::default() + }, + ]; + let descriptor_info = + vk::DescriptorSetLayoutCreateInfo::default().bindings(&desc_layout_bindings); + + let desc_set_layouts = + unsafe { [gpu.create_descriptor_set_layout(&descriptor_info).unwrap()] }; + + let desc_alloc_info = vk::DescriptorSetAllocateInfo::default() + .descriptor_pool(descriptor_pool) + .set_layouts(&desc_set_layouts); + let descriptor_sets = unsafe { gpu.allocate_descriptor_sets(&desc_alloc_info).unwrap() }; + + let globals_uniform_buffer_descriptor = vk::DescriptorBufferInfo { + buffer: globals_buffer.handle, + offset: 0, + range: globals_buffer.size, + }; + + let instance_storage_buffer_descriptor = vk::DescriptorBufferInfo { + buffer: instance_buffer.handle, + offset: 0, + range: instance_buffer.size, + }; + + let write_desc_sets = [ + vk::WriteDescriptorSet { + dst_binding: 0, + dst_set: descriptor_sets[0], + descriptor_count: 1, + descriptor_type: vk::DescriptorType::UNIFORM_BUFFER, + p_buffer_info: &globals_uniform_buffer_descriptor, + ..Default::default() + }, + vk::WriteDescriptorSet { + dst_binding: 1, + dst_set: descriptor_sets[0], + descriptor_count: 1, + descriptor_type: vk::DescriptorType::STORAGE_BUFFER, + p_buffer_info: &instance_storage_buffer_descriptor, + ..Default::default() + }, + ]; + + unsafe { + gpu.update_descriptor_sets(&write_desc_sets, &[]); + }; + + // pipeline layout + let pipeline_layout_create_info = + vk::PipelineLayoutCreateInfo::default().set_layouts(&desc_set_layouts); + let pipeline_layout_render = unsafe { + gpu.create_pipeline_layout(&pipeline_layout_create_info) + .expect("Failed to create non-rigged pipeline layout") + }; + + let shader_vertex = unsafe { + Self::load_shader_module(&gpu, include_bytes!("shaders/terrain.vert.spv")) + .expect("Failed to load terrain vertex shader module") + }; + let shader_fragment = unsafe { + Self::load_shader_module(&gpu, include_bytes!("shaders/terrain.frag.spv")) + .expect("Failed to load terrain fragment shader module") + }; + + Self { + wait_for: vec![], + gpu, + surface_version: 0, + globals_buffer, + instance_buffer, + indirect_buffer, + index_buffer, + vertex_buffer, + vertex_buffer_usage: 0, + + descriptor_sets, + _descriptor_pool: descriptor_pool, + _desc_set_layouts: desc_set_layouts, + pipeline_layout_render, + pipeline_render: vk::Pipeline::null(), + shader_vertex, + shader_fragment, + + tiles_index: HashMap::with_capacity(16), + lods_index, + + instance_buffer_data: Vec::with_capacity(terrain_tiles_capacity as usize), + indirect_buffer_data: Vec::with_capacity(terrain_tiles_capacity as usize), + } + } + + fn find_free_slot(&self, slots: &mut Vec, size: u64) -> Option { + if let Some(slot_index) = + slots + .iter() + .enumerate() + .find_map(|(index, slot)| if slot.size == size { Some(index) } else { None }) + { + let slot = slots.remove(slot_index); + return Some(slot); + } + None + } + + fn generate_lods_index( + tiles_in_view_range: u32, + lods: Vec, + index_buffer_data: &mut Vec, + ) -> HashMap { + let mut result = HashMap::with_capacity(lods.len()); + let mut index_buffer_offset = 0; + let mut tiles_per_side_in_lod = 2 * tiles_in_view_range; + for lod_setup in lods.into_iter() { + let max_tiles_of_lod = tiles_per_side_in_lod * tiles_per_side_in_lod; + let tiles_count = if lod_setup.lod.value() != 0 { + let tiles_per_side_in_higher_lod = tiles_per_side_in_lod - 2; + tiles_per_side_in_lod -= 2; + max_tiles_of_lod - tiles_per_side_in_higher_lod * tiles_per_side_in_higher_lod + } else { + max_tiles_of_lod + }; + + let lod_info = LodInfo { + vertices_count: lod_setup.vertices_count, + indices_count: lod_setup.indices.len() as u32, + first_index: (index_buffer_offset / (std::mem::size_of::() as u64)) as u32, + tiles_count, + }; + + index_buffer_data.extend(lod_setup.indices.into_iter()); + index_buffer_offset = + (index_buffer_data.len() as u64) * (std::mem::size_of::() as u64); + result.insert(lod_setup.lod, lod_info); + } + + result + } + + /// Returns Buffer, binded memory and allocated size + unsafe fn create_globals_uniform_buffer(gpu: &Gpu) -> Result { + let buffer_create_info = vk::BufferCreateInfo { + size: std::mem::size_of::() as u64, + usage: vk::BufferUsageFlags::UNIFORM_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }; + + Buffer::create_and_allocate(gpu, &buffer_create_info) + } + + unsafe fn create_vertex_buffer(gpu: &Gpu, size: u64) -> Result { + let buffer_create_info = vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::VERTEX_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }; + + Buffer::create_and_allocate(gpu, &buffer_create_info) + } + + unsafe fn create_index_buffer(gpu: &Gpu, size: u64) -> Result { + let buffer_create_info = vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::INDEX_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }; + + Buffer::create_and_allocate(gpu, &buffer_create_info) + } + + unsafe fn create_storage_buffer(gpu: &Gpu, size: u64) -> Result { + let buffer_create_info = vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::STORAGE_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }; + + Buffer::create_and_allocate(gpu, &buffer_create_info) + } + + unsafe fn create_indirect_buffer(gpu: &Gpu, size: u64) -> Result { + let buffer_create_info = vk::BufferCreateInfo { + size, + usage: vk::BufferUsageFlags::INDIRECT_BUFFER, + sharing_mode: vk::SharingMode::EXCLUSIVE, + ..Default::default() + }; + + Buffer::create_and_allocate(gpu, &buffer_create_info) + } + + unsafe fn load_shader_module(gpu: &Gpu, bytes: &[u8]) -> Result { + // let bytes = include_bytes!("shaders/non-rigged.frag.spv"); + let mut cursor = Cursor::new(bytes); + let shader_code = ash::util::read_spv(&mut cursor).expect("Failed to read shader SPV code"); + let shader_module_create_info = vk::ShaderModuleCreateInfo::default().code(&shader_code); + + gpu.create_shader_module(&shader_module_create_info) + } + + unsafe fn create_graphics_pipelines( + &self, + render_pass: vk::RenderPass, + surface_resolution: Extent2D, + ) -> Vec { + let shader_entry_point = c"main"; + + let shader_stages = [ + vk::PipelineShaderStageCreateInfo { + module: self.shader_vertex, + p_name: shader_entry_point.as_ptr(), + stage: vk::ShaderStageFlags::VERTEX, + ..Default::default() + }, + vk::PipelineShaderStageCreateInfo { + s_type: vk::StructureType::PIPELINE_SHADER_STAGE_CREATE_INFO, + module: self.shader_fragment, + p_name: shader_entry_point.as_ptr(), + stage: vk::ShaderStageFlags::FRAGMENT, + ..Default::default() + }, + ]; + + let vertex_input_binding_descriptions = [vk::VertexInputBindingDescription { + binding: 0, + stride: std::mem::size_of::() as u32, + input_rate: vk::VertexInputRate::VERTEX, + }]; + let vertex_input_attribute_descriptions = [ + // position + vk::VertexInputAttributeDescription { + location: 0, + binding: 0, + format: vk::Format::R32G32B32_SFLOAT, + offset: 0, + }, + // normal + vk::VertexInputAttributeDescription { + location: 1, + binding: 0, + format: vk::Format::R32G32B32_SFLOAT, + offset: std::mem::size_of::() as u32, + }, + // moisture + vk::VertexInputAttributeDescription { + location: 2, + binding: 0, + // format: vk::Format::R32G32B32A32_SFLOAT, + format: vk::Format::R32_SFLOAT, + offset: std::mem::size_of::() as u32 + + std::mem::size_of::() as u32, + }, + ]; + + let vertex_input_state_info = vk::PipelineVertexInputStateCreateInfo::default() + .vertex_attribute_descriptions(&vertex_input_attribute_descriptions) + .vertex_binding_descriptions(&vertex_input_binding_descriptions); + let vertex_input_assembly_state_info = vk::PipelineInputAssemblyStateCreateInfo { + topology: vk::PrimitiveTopology::TRIANGLE_LIST, + ..Default::default() + }; + + let viewports = [vk::Viewport { + x: 0.0, + y: 0.0, + width: surface_resolution.width as f32, + height: surface_resolution.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }]; + let scissors = [(vk::Extent2D { + width: surface_resolution.width, + height: surface_resolution.height, + }) + .into()]; + let viewport_state_info = vk::PipelineViewportStateCreateInfo::default() + .scissors(&scissors) + .viewports(&viewports); + + let rasterization_info = vk::PipelineRasterizationStateCreateInfo { + front_face: vk::FrontFace::COUNTER_CLOCKWISE, + line_width: 1.0, + // polygon_mode: vk::PolygonMode::LINE, + polygon_mode: vk::PolygonMode::FILL, + cull_mode: vk::CullModeFlags::FRONT, + ..Default::default() + }; + let multisample_state_info = vk::PipelineMultisampleStateCreateInfo { + rasterization_samples: vk::SampleCountFlags::TYPE_1, + ..Default::default() + }; + let noop_stencil_state = vk::StencilOpState { + // fail_op: vk::StencilOp::KEEP, + // pass_op: vk::StencilOp::KEEP, + // depth_fail_op: vk::StencilOp::KEEP, + // compare_op: vk::CompareOp::ALWAYS, + ..Default::default() + }; + let depth_state_info = vk::PipelineDepthStencilStateCreateInfo { + depth_test_enable: 1, + depth_write_enable: 1, + depth_compare_op: vk::CompareOp::LESS_OR_EQUAL, + front: noop_stencil_state, + back: noop_stencil_state, + max_depth_bounds: 1.0, + ..Default::default() + }; + let color_blend_attachment_states = [vk::PipelineColorBlendAttachmentState { + blend_enable: 0, + src_color_blend_factor: vk::BlendFactor::SRC_COLOR, + dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_DST_COLOR, + color_blend_op: vk::BlendOp::ADD, + src_alpha_blend_factor: vk::BlendFactor::ZERO, + dst_alpha_blend_factor: vk::BlendFactor::ZERO, + alpha_blend_op: vk::BlendOp::ADD, + color_write_mask: vk::ColorComponentFlags::RGBA, + }]; + let color_blend_state = vk::PipelineColorBlendStateCreateInfo::default() + .logic_op(vk::LogicOp::CLEAR) + .attachments(&color_blend_attachment_states); + + let dynamic_state = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; + let dynamic_state_info = + vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_state); + + let graphic_pipeline_info = vk::GraphicsPipelineCreateInfo::default() + .stages(&shader_stages) + .vertex_input_state(&vertex_input_state_info) + .input_assembly_state(&vertex_input_assembly_state_info) + .viewport_state(&viewport_state_info) + .rasterization_state(&rasterization_info) + .multisample_state(&multisample_state_info) + .depth_stencil_state(&depth_state_info) + .color_blend_state(&color_blend_state) + .dynamic_state(&dynamic_state_info) + .layout(self.pipeline_layout_render) + .render_pass(render_pass); + + self.gpu + .create_graphics_pipelines(vk::PipelineCache::null(), &[graphic_pipeline_info]) + .expect("Failed to create graphics pipelines") + } + + pub fn globals_uniform(&self, camera: &Camera) -> GlobalsUniform { + // let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 800.0 / 600.0, 1.0, 10.0); + // let view = Mat4::look_at_rh(Vec3::new(1.5f32, -5.0, 3.0), Vec3::ZERO, Vec3::Z); + GlobalsUniform { + proj: camera.proj.to_cols_array_2d(), + view: camera.view.to_cols_array_2d(), + } + } +} + +pub struct RenderTerrainSetup { + wait_semaphores: Vec, + surface_format: vk::Format, + tile_size: u32, + tiles_in_view_range: u32, + lods: Vec, +} + +impl Default for RenderTerrainSetup { + fn default() -> Self { + Self { + wait_semaphores: Vec::new(), + surface_format: vk::Format::default(), + tile_size: DEFAULT_TILE_SIZE, + tiles_in_view_range: DEFAULT_TILES_IN_VIEW_RANGE, + lods: vec![], + } + } +} + +impl RenderTerrainSetup { + pub fn tile_size(mut self, value: u32) -> Self { + self.tile_size = value; + self + } + + pub fn tiles_in_view_range(mut self, value: u32) -> Self { + self.tiles_in_view_range = value; + self + } + + pub fn wait_semaphores(mut self, semaphores: impl IntoIterator) -> Self { + self.wait_semaphores.extend(semaphores); + self + } + + pub fn surface_format(mut self, surface_format: vk::Format) -> Self { + self.surface_format = surface_format; + self + } + + pub fn lods(mut self, lods: Vec) -> Self { + self.lods = lods; + self + } + + pub fn create(self, display: &mut Display) -> RenderTerrain { + RenderTerrain::new(display, self) + } +} + +#[repr(C)] +#[derive(Clone, Debug, Copy, Default)] +pub struct GlobalsUniform { + /// Projection matrix + pub proj: [[f32; 4]; 4], + /// View matrix + pub view: [[f32; 4]; 4], +} + +#[repr(C)] +#[derive(Clone, Debug, Copy, Default)] +pub struct InstanceUniform { + /// Terrain instance local transform matrix + pub transform: [[f32; 4]; 4], +} + +pub struct Recorder { + resolution: Extent2D, + draw_count: u32, + vertex_buffer: vk::Buffer, + index_buffer: vk::Buffer, + indirect_buffer: vk::Buffer, + pipeline_layout: vk::PipelineLayout, + pipeline: vk::Pipeline, + descriptor_sets: Vec, +} + +impl CommandRecorder for Recorder { + unsafe fn record(&self, gpu: &Gpu, command_buffer: vk::CommandBuffer) { + let viewports = [vk::Viewport { + x: 0.0, + y: 0.0, + width: self.resolution.width as f32, + height: self.resolution.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }]; + let scissors = [(vk::Extent2D { + width: self.resolution.width, + height: self.resolution.height, + }) + .into()]; + + gpu.cmd_bind_descriptor_sets( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline_layout, + 0, + &self.descriptor_sets[..], + &[], + ); + + // ONLY MESH + if self.draw_count != 0 { + gpu.cmd_bind_pipeline( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline, + ); + gpu.cmd_set_viewport(command_buffer, 0, &viewports); + gpu.cmd_set_scissor(command_buffer, 0, &scissors); + gpu.cmd_bind_vertex_buffers(command_buffer, 0, &[self.vertex_buffer], &[0]); + + gpu.cmd_bind_index_buffer(command_buffer, self.index_buffer, 0, vk::IndexType::UINT32); + + gpu.cmd_draw_indexed_indirect( + command_buffer, + self.indirect_buffer, + 0, + self.draw_count, + std::mem::size_of::() as u32, + ); + } + } +} diff --git a/src/features/terrain/shaders/terrain.frag b/src/features/terrain/shaders/terrain.frag new file mode 100644 index 00000000..44f599a7 --- /dev/null +++ b/src/features/terrain/shaders/terrain.frag @@ -0,0 +1,71 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout(location = 0) in vec3 world_position; +layout(location = 1) flat in vec3 world_normal; +layout(location = 2) in float vertex_moisture; +layout(location = 0) out vec4 o_frag_color; + +float inverse_lerp(float value, float min_value, float max_value) { + float result = (value - min_value) / (max_value - min_value); + return clamp(result, 0.0, 1.0); +} + +void main() { + vec3 ambient_light = vec3(0.5, 0.5, 0.5); + vec3 light_color = vec3(1.0, 1.0, 1.0); + vec3 light_position = vec3(0.0, 10000.0, 0.0); + + vec3 normal = normalize(world_normal); + vec3 light_direction = normalize(light_position - world_position); + //vec3 light_direction = (normalize(light_position)); + + vec3 diffuse = max(dot(normal, light_direction), 0.0) * light_color; + float min_height = 0.0; + float max_height = 100.0; + const int max_layers = 5; + vec3 colors[max_layers] = vec3[]( + vec3(0.059, 0.533, 0.737), // water + vec3(0.729, 0.714, 0.667), // sand + vec3(0.11, 0.286, 0.118), // grass + vec3(0.275, 0.263, 0.224), // rock + vec3(0.902, 0.937, 0.996) // snow + ); + float limits[max_layers] = float[]( + 0.0, + 0.01, + 0.05, + 0.4, + 0.8 + ); + float blends[max_layers] = float[]( + 0.0, + 0.0, + 0.4, + 0.2, + 0.1 + ); + float moisture_intensity[max_layers] = float[]( + 0.0, + 0.0, + 0.00, + 0.00, + 0.008 + ); + vec3 color = colors[0]; + float height = inverse_lerp(world_position.y, min_height, max_height); + for (int i = 0; i < max_layers; i++) { + if (height < limits[i]) { + break; + } + color = colors[i]; + } + for (int i = 0; i < max_layers; i++) { + float moisture_blend = vertex_moisture * moisture_intensity[i]; + float intensity = inverse_lerp(height - moisture_blend - limits[i], -blends[i] / 2.0 - 0.000001, blends[i] / 2.0); + color = color * (1.0 - intensity) + colors[i] * intensity; + } + o_frag_color = vec4((ambient_light + diffuse), 1.0) * vec4(color, 1.0); + // o_frag_color = vec4(color, 1.0); +} diff --git a/src/features/terrain/shaders/terrain.frag.spv b/src/features/terrain/shaders/terrain.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..fed47bfe15954fcead1a253ed065a1976729c483 GIT binary patch literal 5028 zcmZ9OS(H>)6^3uq0}XWMfw&gLCYTrlB1EQyp#dfB&`Ku7Ay^dMx0?#pRZUej0nI>y ziP40pF^N&pI48syAx4cuBI1PFsBb>_;PTNoeNp3zF2Aqp?#iX^VzdAM-}~&d&v0*b z_pHUUlcXz|mz#O-CE` zGjj-)Dieiru`)4ToJb2RuU9?_ z!akHLg}gRTt++p@^{|IYQcLU8<%Z+>l1t&m$z7$i(xC5&-N?;)i||ce7_FA8wPx<( zwl-C*ml~xiyKXO6pjWE3$zr)x({s43S}TtgLT%|w+T*j$p@2PjX07})>#jf_D~*p& z*VATCYvHbOciL*ybzh^#8ei3Vk|DHa*PNsMZHJdjlO@kpz5~9ioL0t~e0EZ_MzvIL zOxMyvsnSR*tnNT7cPwh*9b3ZZ=eiZ{6`IWA)egS*4Bnkg!>!TFeLUR8eaWNXb`QdT(;Xv9Gp@(vuuR%lC4;gFkl$?`ysz)xvnKIFTJP@nQP%{JbZ_ z)D(M1eCOnzmE1?}yC(PDlKb98`~p<Q%ok^fpS&(Kmb)dw-Pu+T3%j;9Hr`&x4e)h;skZrz@ISwNXM0^Y!5w3s zv8CKn_xondy>s36?0n{WPVd2cA3ZzI>AiT*Nj>^_A9(Jc7OxNfe)#E`k@h})pvh<2 z>-%6UPG9}aI=8^Rch+&N`Q}8u4}q!I9$#qk8TPp~eka`Vnq$p3C&uprSI_TWANiy3 z$PaIh_cp~3VqKWy<>Q!-+^{cVc+ zdVk;Y+~2o6-<#u}p}%htmmkh?b${m~u0PVj{SC{KeDM z=c^udt_C|-@Hd0ad4#%s_?4JB>I1A)Z53u+*I?XtFUGG1yY`2&zSe-%9OL}nCpEEm z$+2SZk-s})@6gQ|-j2T|!`_{p87|^SvCYimy_vuqH;zRwyEEOI-YdEJzN3?v`?m)( z-*r_mb@RPv@`z1=?bn!hP9Cuu*#3=q|607YS9SORWmlhz+&Ik^80RI#t!4-OdbMTTkn~C3jb-$ z$FUFNjq79X&w#BFXYvSGEzU&#Nz7V~6+6~@_*v|+jNA9)n6=_eJ_q*xtH+qngVpb4 zt=9MgW*v3c^Ju0;{ukkK?&g09Gso|V{0Yn)^;pxF!TCPE0{3pHN8PW2)uYbWz}8Za zI$sCp>wE*g15=MW-vq1QmF>xsV72?R{*Hpxp2A|TZ-LdH#$v8#!1=kp4R9^cU)fE{Nn*7gE8*0vD*L(KO@J?8pRrpH`AhC5f({R!9{^}E?y`+E^{K4bQ8 zuWH_P@4fd`AAP(8wvTwvehOBL_sp1@iY4$lL7HfQ1e`_J67 zF@2o>Ibg>JKNoC`xO?Y;%~6k>^TE~*egQZ?=0dnR>hTU;1h$qj*JmGUQR8B;+5jta Wov*>%&n1|#`I!6lKh|T+J^4TTP9(Vi literal 0 HcmV?d00001 diff --git a/src/features/terrain/shaders/terrain.vert b/src/features/terrain/shaders/terrain.vert new file mode 100644 index 00000000..04581bc2 --- /dev/null +++ b/src/features/terrain/shaders/terrain.vert @@ -0,0 +1,47 @@ +#version 450 +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable + +layout(location = 0) in vec3 pos; +layout(location = 1) in vec3 normal; +// layout(location = 2) in vec4 color; +layout(location = 2) in float moisture; + +layout(binding = 0) uniform DtxGlobals { + mat4 proj; + mat4 view; +} dtx_globals; + +struct DtxInstance { + mat4 transform; +}; + +layout(std430, binding = 1) buffer DtxInstanceLayout +{ + DtxInstance dtx_instance[]; +}; + +layout(location = 0) out vec3 o_world_position; +layout(location = 1) flat out vec3 o_world_normal; +// layout(location = 2) out vec4 o_color; +layout(location = 2) out float o_moisture; +void main() { + mat4 model_transform = dtx_instance[gl_InstanceIndex].transform; + + mat4 proj_view = dtx_globals.proj * dtx_globals.view; + o_world_position = pos; //vec3(model_transform * vec4(pos, 1.0)); + // o_world_normal = vec3(model_transform * vec4(normalize(normal), 1.0)); + o_world_normal = normalize(normal); + o_moisture = moisture; + + //vec4( + // float((color >> 24) & 0xFF) / 255.0, + // float((color >> 16) & 0xFF) / 255.0, + // float((color >> 8) & 0xFF) / 255.0, + // float(color & 0xFF) / 255.0 + // ); + + // o_color = vec4(0.0, 1.0, 0.0, 1.0); + + gl_Position = proj_view * vec4(o_world_position, 1.0); +} diff --git a/src/features/terrain/shaders/terrain.vert.spv b/src/features/terrain/shaders/terrain.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..50e802428d8b945204d861e2eef2cd778f25fcac GIT binary patch literal 2452 zcmY+F*>2NN5Qa||hkf6-l2Z0UDNy#MEsFwDBsPiLBa@h5aP7!;pm4=2z$5Wg+#vCP zwugojP0q|et7j(8)a2YI=O)}Xx5KTv`s{I&LY%Nxb$7aNx(h|wUs$@ah{aAfRT*Nl zOMaYb(OMX%@cTrkMQ26lMHfVuMOQ>OMN|4r>i(qw({79Fbp7hC>AZN+=|1(n;m{Z5*Cg^uoJPJM z=ZX}6j)=8+%q~uQ$*4bW5XfTuSpxN7t7=`#`cdM`JWPxCS-$21eYOdIQm#Kwi!w}m zkt|ykCfIFQI$3V2x-@3n4ZmiiQg*FIhflv;_pt?&9H>vbQ0NeYz77&^`JSi!XuZ}2 z?y#6)o_+M6HA?;#ATdS^QKN_Eli%RYUXB_s##$|{nOPRY{)^i6PaJj;-Vbonhkl`kqrc_<(7o$ zxnD;4+bA#VXQtQS9CTI={RHQEDm+fhH}N+`uX~M--IMNd5)Yp!cQd=%kIhCmN|Jv% zo{zodJkxV9cTOOBrf-wyD)fI$KQ6ah)REtI-7V3I2*3K5!Z6 z#q(^)>;t*yMbu@pwYp5U_~bOq>M}oKj{70ww<0n>F!Q5UV$n-#VPD2;MV=LL(6c); zl~l{@%Z5Bv?aa=`mUjj^^5M(-V_fE4f^50j8I!5i_QK|381cZk4I>5^JvR&f2 zIrn`T`(<(16YHk-mVUA)9Pr;l_)QXvz2ScdTa$U`BR`Oy+3puH*Hxj+^q`16VHePG zW6N9*iO@e(CgLKmDjR;sMa*~H{zQYD&B?}wxm(P(c<`T!LylPyer*wXs22=-W_?a{ zR)mgyKsMb)@yvL;`i, +} + +impl SubmitFrame { + pub fn new(display: &Display) -> Self { + let gpu = display.gpu(); + let pool_create_info = vk::CommandPoolCreateInfo::default() + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER) + .queue_family_index(gpu.queue_family_index()); + let command_pool = unsafe { + gpu.create_command_pool(&pool_create_info) + .expect("Failed to create a command pool") + }; + + let fence_create_info = + vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); + + let setup_fence = unsafe { gpu.create_fence(&fence_create_info) }; + + let command_buffer_allocate_info = vk::CommandBufferAllocateInfo::default() + .command_buffer_count(2) + .command_pool(command_pool) + .level(vk::CommandBufferLevel::PRIMARY); + + let (command_buffer_render, command_buffer_setup) = unsafe { + gpu.allocate_command_buffers(&command_buffer_allocate_info) + .into() + }; + + Self { + gpu, + command_pool, + command_buffer_render, + command_buffer_setup, + setup_fence, + framebuffers: vec![], + surface_version: 0, + } + } + + unsafe fn create_framebuffers(&mut self, display: &Display) { + let resolution = display.surface_resolution(); + self.framebuffers = display + .swapchain_image_views() + .map(|&present_image_view| { + let framebuffer_attachments = [present_image_view, display.depth_image_view()]; + let frame_buffer_create_info = vk::FramebufferCreateInfo::default() + .render_pass(display.render_pass()) + .attachments(&framebuffer_attachments) + .width(resolution.width) + .height(resolution.height) + .layers(1); + + self.gpu + .create_framebuffer(&frame_buffer_create_info) + .expect("Could not create a framebuffer") + }) + .collect::>() + } + + unsafe fn destroy_framebuffers(&mut self) { + for framebuffer in self.framebuffers.drain(..) { + self.gpu.destroy_framebuffer(framebuffer); + } + } + + unsafe fn setup_depth_image(&self, display: &Display) { + let depth_image = display.depth_image(); + + // begin: prepare + + self.gpu + .wait_for_fences(&[self.setup_fence], true, u64::MAX) + .expect("Wait for fence failed."); + + self.gpu + .reset_fences(&[self.setup_fence]) + .expect("Reset fences failed."); + + self.gpu + .reset_command_buffer( + self.command_buffer_setup, + vk::CommandBufferResetFlags::RELEASE_RESOURCES, + ) + .expect("Reset command buffer failed."); + + let command_buffer_begin_info = vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + + self.gpu + .begin_command_buffer(self.command_buffer_setup, &command_buffer_begin_info) + .expect("Begin commandbuffer"); + + // end: prepare + + let layout_transition_barriers = vk::ImageMemoryBarrier::default() + .image(depth_image) + .dst_access_mask( + vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_READ + | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE, + ) + .new_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL) + .old_layout(vk::ImageLayout::UNDEFINED) + .subresource_range( + vk::ImageSubresourceRange::default() + .aspect_mask(vk::ImageAspectFlags::DEPTH) + .layer_count(1) + .level_count(1), + ); + + self.gpu.cmd_pipeline_barrier( + self.command_buffer_setup, + vk::PipelineStageFlags::BOTTOM_OF_PIPE, + vk::PipelineStageFlags::LATE_FRAGMENT_TESTS, + vk::DependencyFlags::empty(), + &[], + &[], + &[layout_transition_barriers], + ); + + // submit + self.gpu + .end_command_buffer(self.command_buffer_setup) + .expect("End commandbuffer"); + + let command_buffers = [self.command_buffer_setup]; + let wait_mask = []; + let wait_semaphores = []; + let signal_semaphores = []; + + let submits = [vk::SubmitInfo::default() + .wait_semaphores(&wait_semaphores) + .wait_dst_stage_mask(&wait_mask) + .command_buffers(&command_buffers) + .signal_semaphores(&signal_semaphores)]; + + self.gpu + .submit_queue(&submits, self.setup_fence) + .expect("queue submit failed.") + } +} + +impl Drop for SubmitFrame { + fn drop(&mut self) { + unsafe { + self.gpu.device_wait_idle().unwrap(); + // framebuffers + self.destroy_framebuffers(); + // command buffers + self.gpu.destroy_command_pool(self.command_pool); + // destory fence + self.gpu.destroy_fence(self.setup_fence) + }; + } +} impl Task for SubmitFrame { - type Context = (Ref, Any, All); + type Context = (Ref, Any, Take>); type Output = FramePresenter; fn output_channel(&self) -> OutputChannel { OutputChannel::Scheduler } - fn run(&mut self, (display, frame, _): Self::Context) -> Self::Output { + fn run(&mut self, (display, frame, submits): Self::Context) -> Self::Output { log::info!("get presenter"); + + if let Some(surface_version) = display.surface_changed(self.surface_version) { + unsafe { + log::debug!("resize: Surface changed"); + // self.gpu.device_wait_idle().unwrap(); + + // rebuild framebuffers + log::debug!("resize: destroy_framebuffers"); + self.destroy_framebuffers(); + + log::debug!("resize: create_framebuffers"); + self.create_framebuffers(&display); + + log::debug!("resize: setup_depth_image"); + self.setup_depth_image(&display); + } + self.surface_version = surface_version; + } + + let mut submits = submits.take(); + + submits.sort_by(|a, b| { + let a_deps = a.wait_for(); + let b_deps = b.wait_for(); + let a_depends_on_b = a_deps.iter().any(|i| *i == b.id()); + let b_depends_on_a = b_deps.iter().any(|i| *i == a.id()); + + if a_depends_on_b && b_depends_on_a { + panic!("Circular rendering dependencies"); + } + + if a_depends_on_b && !b_depends_on_a { + return std::cmp::Ordering::Greater; + } + + if !a_depends_on_b && b_depends_on_a { + return std::cmp::Ordering::Less; + } + + a_deps.len().cmp(&b_deps.len()) + }); + /* + .iter() + .map(|i| { + vk::SubmitInfo::default() + .wait_semaphores(i.wait_semaphores.as_slice()) + .wait_dst_stage_mask(i.wait_dst_stage_mask.as_slice()) + .command_buffers(i.command_buffers.as_slice()) + .signal_semaphores(i.signal_semaphores.as_slice()) + }) + .collect::>(); + */ + + let draw_fence = display.draw_fence(); + + let command_buffer_begin_info = vk::CommandBufferBeginInfo::default() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + + let clear_values = [ + vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.0, 0.0, 0.1, 0.0], + }, + }, + vk::ClearValue { + depth_stencil: vk::ClearDepthStencilValue { + depth: 1.0, + stencil: 0, + }, + }, + ]; + + let render_pass = unsafe { display.render_pass() }; + let render_pass_begin_info = vk::RenderPassBeginInfo::default() + .render_pass(render_pass) + .framebuffer(self.framebuffers[frame.swapchain_index as usize]) + .render_area(vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: vk::Extent2D { + width: frame.resolution.width, + height: frame.resolution.height, + }, + }) + .clear_values(&clear_values); + + unsafe { + self.gpu + .reset_command_buffer( + self.command_buffer_render, + vk::CommandBufferResetFlags::RELEASE_RESOURCES, + ) + .expect("Failed to reset Vulkan command buffer"); + + self.gpu + .begin_command_buffer(self.command_buffer_render, &command_buffer_begin_info) + .expect("Failed to begin draw command buffer"); + + self.gpu.cmd_begin_render_pass( + self.command_buffer_render, + &render_pass_begin_info, + vk::SubpassContents::INLINE, + ); + + for submit in submits.into_iter() { + submit.record_command_buffer(&self.gpu, self.command_buffer_render); + } + + self.gpu.cmd_end_render_pass(self.command_buffer_render); + + self.gpu + .end_command_buffer(self.command_buffer_render) + .expect("End commandbuffer"); + + let present_complete_semaphore = display.present_complete_semaphore(); + let render_complete_semaphore = display.render_complete_semaphore(); + let wait_semaphores = [present_complete_semaphore]; + let wait_mask = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]; + let command_buffers = [self.command_buffer_render]; + let signal_semaphores = [render_complete_semaphore]; + + let submit_info = [vk::SubmitInfo::default() + .wait_semaphores(&wait_semaphores) + .wait_dst_stage_mask(&wait_mask) + .command_buffers(&command_buffers) + .signal_semaphores(&signal_semaphores)]; + + self.gpu + .submit_queue(&submit_info, draw_fence) + .expect("Failed to submit draw buffer to queue"); + } display.presenter(frame.swapchain_index) } } - -/// Render Pass Output -pub struct RenderPass {} diff --git a/src/graphics/vulkan.rs b/src/graphics/vulkan.rs index 8df31b91..7aba1179 100644 --- a/src/graphics/vulkan.rs +++ b/src/graphics/vulkan.rs @@ -3,7 +3,6 @@ use std::ffi::{c_char, CStr, CString}; use std::sync::Arc; pub use ash::vk; -use ash::vk::DescriptorPool; use crate::log; use crate::window; @@ -787,7 +786,7 @@ impl Gpu { pub unsafe fn create_descriptor_pool( &self, descriptor_pool_info: &vk::DescriptorPoolCreateInfo, - ) -> Result { + ) -> Result { self.device .vk_device .create_descriptor_pool(descriptor_pool_info, None) @@ -1151,6 +1150,7 @@ impl From for (vk::CommandBuffer, vk::CommandBuffer, vk::Comm /// Display abstraction layer pub struct Display { gpu: Gpu, + draw_fence: vk::Fence, swapchain: Arc, surface: Surface, window: window::Instance, @@ -1159,6 +1159,7 @@ pub struct Display { depth_image: vk::Image, depth_image_view: vk::ImageView, depth_image_memory: vk::DeviceMemory, + render_pass: vk::RenderPass, } impl Drop for Display { @@ -1174,10 +1175,16 @@ impl Drop for Display { self.depth_image_memory, ); self.gpu.destroy_semaphore(self.present_complete_semaphore); + self.gpu.destroy_semaphore(self.render_complete_semaphore); // NOTE: render_complete_semaphore is not owned + self.gpu.destroy_fence(self.draw_fence); + Swapchain::destroy(&self.swapchain, &self.gpu); + // render pass + self.gpu.destroy_render_pass(self.render_pass); + self.surface .loader .destroy_surface(self.surface.vk_surface, None); @@ -1229,6 +1236,7 @@ impl Display { let features = vk::PhysicalDeviceFeatures { shader_clip_distance: 1, vertex_pipeline_stores_and_atomics: 1, + multi_draw_indirect: 1, ..Default::default() }; let priorities = [1.0]; @@ -1278,19 +1286,81 @@ impl Display { .expect("Failed to create completion semaphore") }; + let render_complete_sempahore_create_info = vk::SemaphoreCreateInfo::default(); + let render_complete_semaphore = unsafe { + gpu.create_semaphore(&render_complete_sempahore_create_info) + .expect("Failed to create completion semaphore") + }; + let (depth_image, depth_image_view, depth_image_memory) = unsafe { Self::create_depth_image(&gpu, surface.vk_surface_resolution) }; + let fence_create_info = + vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); + let draw_fence = unsafe { gpu.create_fence(&fence_create_info) }; + + let renderpass_attachments = [ + vk::AttachmentDescription { + format: surface.vk_surface_format.format, + samples: vk::SampleCountFlags::TYPE_1, + load_op: vk::AttachmentLoadOp::CLEAR, + store_op: vk::AttachmentStoreOp::STORE, + final_layout: vk::ImageLayout::PRESENT_SRC_KHR, + ..Default::default() + }, + vk::AttachmentDescription { + format: vk::Format::D16_UNORM, + samples: vk::SampleCountFlags::TYPE_1, + load_op: vk::AttachmentLoadOp::CLEAR, + initial_layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + final_layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + ..Default::default() + }, + ]; + let color_attachment_refs = [vk::AttachmentReference { + attachment: 0, + layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + }]; + let depth_attachment_ref = vk::AttachmentReference { + attachment: 1, + layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + }; + let dependencies = [vk::SubpassDependency { + src_subpass: vk::SUBPASS_EXTERNAL, + src_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + dst_access_mask: vk::AccessFlags::COLOR_ATTACHMENT_READ + | vk::AccessFlags::COLOR_ATTACHMENT_WRITE, + dst_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, + ..Default::default() + }]; + + let subpass = vk::SubpassDescription::default() + .color_attachments(&color_attachment_refs) + .depth_stencil_attachment(&depth_attachment_ref) + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS); + + let renderpass_create_info = vk::RenderPassCreateInfo::default() + .attachments(&renderpass_attachments) + .subpasses(std::slice::from_ref(&subpass)) + .dependencies(&dependencies); + + let render_pass = unsafe { + gpu.create_render_pass(&renderpass_create_info) + .expect("Failed to create a render pass") + }; + Self { gpu, surface, window, swapchain: Arc::new(swapchain), present_complete_semaphore, - render_complete_semaphore: vk::Semaphore::null(), + render_complete_semaphore, + draw_fence, depth_image, depth_image_view, depth_image_memory, + render_pass, } } @@ -1398,11 +1468,32 @@ impl Display { self.depth_image } + /// # Safety + /// + /// This function requires valid Vulkan entities + pub unsafe fn render_pass(&self) -> vk::RenderPass { + self.render_pass + } + pub fn gpu(&self) -> Gpu { self.gpu.clone() } + pub fn draw_fence(&self) -> vk::Fence { + self.draw_fence + } + pub fn next_frame(&self) -> u32 { + unsafe { + self.gpu + .wait_for_fences(&[self.draw_fence], true, u64::MAX) + .expect("Failed to wait for the draw fence"); + + self.gpu + .reset_fences(&[self.draw_fence]) + .expect("Failed to reset draw fences"); + }; + let (present_index, is_suboptimal) = unsafe { log::debug!("Begin acquire image"); self.swapchain @@ -1493,15 +1584,11 @@ impl Display { FramePresenter { swapchain: Arc::clone(&self.swapchain), device: Arc::clone(&self.gpu.device), - render_complete_semaphore: self.render_complete_semaphore, swapchain_index, + wait_semaphore: self.render_complete_semaphore, } } - pub fn set_render_complete_semaphore(&mut self, semaphore: vk::Semaphore) { - self.render_complete_semaphore = semaphore; - } - /// # Safety /// /// This function requires valid Vulkan entities @@ -1509,6 +1596,13 @@ impl Display { self.present_complete_semaphore } + /// # Safety + /// + /// This function requires valid Vulkan entities + pub unsafe fn render_complete_semaphore(&self) -> vk::Semaphore { + self.render_complete_semaphore + } + /// # Safety /// /// This function requires valid Vulkan entities @@ -1633,187 +1727,80 @@ impl Display { }) .expect("Could not find a device that fulfill requirements") } +} - /* - pub fn present(&self) { - let present_index = self.next_frame(); - log::info!("present_index {}", present_index); - let clear_values = vec![ - vk::ClearValue { - color: vk::ClearColorValue { - float32: [0.1, 1.0, 1.0, 0.0], - }, - }, - //vk::ClearValue { - // depth_stencil: vk::ClearDepthStencilValue { - // depth: 1.0, - // stencil: 0, - // }, - //}, - ]; +impl<'a> From<&'a Display> for &'a Device { + fn from(display: &Display) -> &Device { + &display.gpu.device + } +} - // these array must live long enough or vulkan will sigsegv - let wait_semaphores = [self.vk_present_complete_semaphore]; - let signal_semaphores = [self.vk_render_complete_semaphore]; - let command_buffers = [self.vk_draw_command_buffer]; - - log::info!("render_pass_begin_info"); - let render_pass_begin_info = vk::RenderPassBeginInfo::builder() - .render_pass(self.renderpass) - .framebuffer(self.framebuffers[present_index as usize]) - .render_area(self.surface.vk_resolution.into()) - .clear_values(&clear_values) - .build(); - - log::info!("begin submit"); - // begin submit - unsafe { - self.vk_device - .wait_for_fences(&[self.vk_draw_commands_reuse_fence], true, u64::MAX) - .expect("Wait for fence failed."); - - log::info!("wait_for_fences"); - self.vk_device - .reset_fences(&[self.vk_draw_commands_reuse_fence]) - .expect("Reset fences failed."); - - log::info!("reset_fences"); - self.vk_device - .reset_command_buffer( - self.vk_draw_command_buffer, - vk::CommandBufferResetFlags::RELEASE_RESOURCES, - ) - .expect("Reset command buffer failed."); - - log::info!("reset_command_buffer"); - let command_buffer_begin_info = vk::CommandBufferBeginInfo::builder() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT) - .build(); - - log::info!("command_buffer_begin_info"); - self.vk_device - .begin_command_buffer(self.vk_draw_command_buffer, &command_buffer_begin_info) - .expect("Begin commandbuffer"); - log::info!("begin_command_buffer"); - } - // ------------ - unsafe { - self.vk_device.cmd_begin_render_pass( - self.vk_draw_command_buffer, - &render_pass_begin_info, - vk::SubpassContents::INLINE, - ); - log::info!("cmd_begin_render_pass"); - self.vk_device - .cmd_end_render_pass(self.vk_draw_command_buffer); - } - log::info!("cmd_end_render_pass"); - // ------------ - unsafe { - self.vk_device - .end_command_buffer(self.vk_draw_command_buffer) - .expect("End commandbuffer"); - log::info!("end_command_buffer"); - - let submit_info = vk::SubmitInfo::builder() - .wait_semaphores(&wait_semaphores) - .wait_dst_stage_mask(&[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]) - .command_buffers(&command_buffers) - .signal_semaphores(&signal_semaphores) - .build(); - log::info!("submit_info {:?}", self.vk_queue); - - self.vk_device - .queue_submit( - self.vk_queue, - &[submit_info], - self.vk_draw_commands_reuse_fence, - ) - .expect("queue submit failed."); - log::info!("queue_submit"); - } - // end submit +pub trait CommandRecorder: Send + Sync { + /// # Safety + /// + /// Requires valid Vulkan entities + unsafe fn record(&self, gpu: &Gpu, command_buffer: vk::CommandBuffer); +} - let wait_semaphores = [self.vk_render_complete_semaphore]; - let swapchains = [self.swapchain.vk_swapchain]; - let image_indices = [present_index]; - let present_info = vk::PresentInfoKHR::builder() - .wait_semaphores(&[self.vk_render_complete_semaphore]) - .swapchains(&swapchains) - .image_indices(&image_indices) - .build(); - log::info!("present_info"); +pub struct RenderSubmit { + /// Id of submitting task + id: std::any::TypeId, + /// dependencied of other tasks + dependencies: Vec, + /// command recorder + command_recorder: Box, +} - unsafe { - self.swapchain - .loader - .queue_present(self.vk_queue, &present_info) - .unwrap(); +impl RenderSubmit { + pub fn new( + command_recorder: Box, + dependencies: &[std::any::TypeId], + ) -> Self { + Self { + id: std::any::TypeId::of::(), + dependencies: dependencies.into(), + command_recorder, } - log::info!("queue_present"); } - */ - - /* - unsafe fn create_framebuffers( - device: &ash::Device, - surface: &Surface, - swapchain: &Swapchain, - renderpass: &vk::RenderPass, - ) -> Vec { - swapchain - .vk_present_image_views - .iter() - .map(|&present_image_view| { - let framebuffer_attachments = [present_image_view /*, base.depth_image_view*/]; - let frame_buffer_create_info = vk::FramebufferCreateInfo::builder() - .render_pass(*renderpass) - .attachments(&framebuffer_attachments) - .width(surface.vk_resolution.width) - .height(surface.vk_resolution.height) - .layers(1) - .build(); - device - .create_framebuffer(&frame_buffer_create_info, None) - .expect("Could not create a framebuffer") - }) - .collect::>() + pub fn id(&self) -> std::any::TypeId { + self.id } - unsafe fn destroy_framebuffers(&mut self) { - for framebuffer in self.framebuffers.iter() { - self.vk_device.destroy_framebuffer(*framebuffer, None); - } - self.framebuffers.clear(); + pub fn wait_for(&self) -> &[std::any::TypeId] { + self.dependencies.as_slice() } - */ -} -impl<'a> From<&'a Display> for &'a Device { - fn from(display: &Display) -> &Device { - &display.gpu.device + /// # Safety + /// + /// Requires valid Vulkan entities + pub unsafe fn record_command_buffer(&self, gpu: &Gpu, command_buffer: vk::CommandBuffer) { + self.command_recorder.record(gpu, command_buffer); } } pub struct FramePresenter { swapchain: Arc, device: Arc, - render_complete_semaphore: vk::Semaphore, + wait_semaphore: vk::Semaphore, swapchain_index: u32, } impl FramePresenter { pub fn present(self) { - let wait_semaphores = [self.render_complete_semaphore]; let swapchains = [self.swapchain.as_ref().vk_swapchain]; let image_indices = [self.swapchain_index]; + let wait_semaphores = [self.wait_semaphore]; let present_info = vk::PresentInfoKHR::default() .wait_semaphores(&wait_semaphores) .swapchains(&swapchains) .image_indices(&image_indices); - log::debug!("Begin present: {}", self.swapchain_index); + log::debug!( + "Begin present: {} ({} wait semaphores)", + self.swapchain_index, + wait_semaphores.len() + ); unsafe { let r = self @@ -1826,90 +1813,6 @@ impl FramePresenter { } } -/* -pub struct CommandRecorder<'a> { - gpu: &'a Gpu, - command_buffer: vk::CommandBuffer, - one_time_submit: bool, -} - -impl<'a> Drop for CommandRecorder<'a> { - fn drop(&mut self) { - if self.one_time_submit { - unsafe { - self.gpu.end_command_buffer(self.command_buffer); - } - } - } -} - -impl<'a> CommandRecorder<'a> { - pub fn setup() -> CommandRecorderSetup { - CommandRecorderSetup { - one_time_submit: true, - ..Default::default() - } - } - - -} - -#[derive(Default)] -pub struct CommandRecorderSetup { - pub command_buffer: vk::CommandBuffer, - pub reuse_fence: Option, - pub one_time_submit: bool, -} - -impl CommandRecorderSetup { - #[inline(always)] - pub fn command_buffer(mut self, command_buffer: vk::CommandBuffer) -> Self { - self.command_buffer = command_buffer; - self - } - #[inline(always)] - pub fn reuse_fence(mut self, reuse_fence: Option) -> Self { - self.reuse_fence = reuse_fence; - self - } - #[inline(always)] - pub fn one_time_submit(mut self, one_time_submit: bool) -> Self { - self.one_time_submit = one_time_submit; - self - } - - pub fn create<'a>(self, gpu: &'a Gpu) -> CommandRecorder<'a> { - unsafe { - if let Some(reuse_fence) = self.reuse_fence { - gpu.wait_for_fences(&[reuse_fence], true, u64::MAX) - .expect("Failed to wait for Vulkan fences"); - gpu.reset_fences(&[reuse_fence]) - .expect("Failed to reset Vulkan fences"); - } - - if self.one_time_submit { - let command_buffer_begin_info = vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); - - gpu.reset_command_buffer( - self.command_buffer, - vk::CommandBufferResetFlags::RELEASE_RESOURCES, - ) - .expect("Failed to reset Vulkan command buffer"); - - gpu.begin_command_buffer(self.command_buffer, &command_buffer_begin_info); - } - } - - CommandRecorder { - gpu, - command_buffer: self.command_buffer, - one_time_submit: self.one_time_submit, - } - } -} -*/ - pub struct Buffer { pub handle: vk::Buffer, pub device_memory: vk::DeviceMemory, @@ -1962,7 +1865,7 @@ impl Buffer { data: &[T], ) -> u64 { let align = std::mem::align_of::() as u64; - let size = (data.len() * std::mem::size_of::()) as u64; + let size = std::mem::size_of_val(data) as u64; log::debug!("map buffer: align({:?}), size({})", align, size); @@ -1991,83 +1894,6 @@ impl Buffer { } } -/* - -pub struct PipelineLayout { - vk_pipeline_layout: vk::PipelineLayout, - device: Arc, -} - -#[derive(Default)] -pub struct PipelineLayoutSetup<'a> { - vk_layout_create_info: vk::PipelineLayoutCreateInfo<'a>, -} - -impl PipelineLayout { - pub fn setup<'a>() -> PipelineLayoutSetup<'a> { - PipelineLayoutSetup::default() - } -} - -impl Drop for PipelineLayout { - fn drop(&mut self) { - unsafe { - self.device - .vk_device - .destroy_pipeline_layout(self.vk_pipeline_layout, None); - }; - } -} - -pub struct GraphicsPipeline { - vk_pipeline: vk::Pipeline, - device: Arc, -} - -impl GraphicsPipeline { - pub fn setup<'a>() -> GraphicsPipelineSetup<'a> { - GraphicsPipelineSetup::default() - } -} - -#[derive(Default)] -pub struct GraphicsPipelineSetup<'a> { - vk_graphics_pipline_create_info: vk::GraphicsPipelineCreateInfo<'a>, -} - -impl<'a> GraphicsPipelineSetup<'a> { - pub fn create(self, gpu: &Gpu) -> GraphicsPipeline { - gpu.create_graphics_pipeline(self.vk_graphics_pipline_create_info) - } -} - -impl Drop for GraphicsPipeline { - fn drop(&mut self) { - unsafe { - self.device - .vk_device - .destroy_pipeline(self.vk_pipeline, None); - }; - } -} - -pub struct ShaderModule { - device: Arc, - vk_shader_module: vk::ShaderModule, -} - -impl Drop for ShaderModule { - fn drop(&mut self) { - unsafe { - self.device - .vk_device - .destroy_shader_module(self.vk_shader_module, None); - }; - } -} - - */ - /// # Safety /// /// This function requires valid Vulkan entities diff --git a/src/lib.rs b/src/lib.rs index 910c6659..f9cccf27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,9 @@ pub use crate::log::Log; /// Math module pub mod math; +mod features; +pub use features::*; + /// Models abstractions pub mod models; pub use models::{ @@ -83,9 +86,6 @@ pub use window::{Input, ReadInput, Window}; // #[cfg(feature = "pbr")] // pub use dotrix_pbr as pbr; -// #[cfg(feature = "ui")] -// pub use dotrix_ui as ui; - /// Dotrix Settings pub trait Application { /// Startup diff --git a/src/loaders/gltf_loader.rs b/src/loaders/gltf_loader.rs index 1d5fb433..587c59e3 100644 --- a/src/loaders/gltf_loader.rs +++ b/src/loaders/gltf_loader.rs @@ -45,7 +45,10 @@ pub struct GltfLoader; impl ResourceLoader for GltfLoader { fn read(&self, path: &Path, targets: &HashSet) -> ResourceBundle { - let mut file = File::open(path).expect("Could not open GLTF resource file"); + let mut file = match File::open(path) { + Ok(file) => file, + Err(err) => panic!("Could not open GLTF resource file ({path:?}): {err:?}",), + }; let metadata = std::fs::metadata(path).expect("Could not read GLTF file metadata"); let mut data = vec![0; metadata.len() as usize]; file.read_exact(&mut data) @@ -75,7 +78,7 @@ impl ResourceLoader for GltfLoader { } } } - Err(e) => log::error!("Could not read GLTF file `{:?}`: {:?}", path, e), + Err(err) => log::error!("Could not read GLTF file `{path:?}`: {err:?}"), }; let mut bundle = targets @@ -121,16 +124,16 @@ impl GltfLoader { if let Some(stripped) = uri.strip_prefix(URI_BASE64) { match base64_decode(stripped) { Ok(buffer) => buffers.push(buffer), - Err(e) => { - log::error!("Could not decode Base64 buffer: {:?}", e); + Err(err) => { + log::error!("Could not decode Base64 buffer: {err:?}"); return None; } }; } else { match std::fs::read(path.parent().unwrap().join(uri)) { Ok(buffer) => buffers.push(buffer), - Err(e) => { - log::error!("Could not read GLTF buffer from file: {:?}", e); + Err(err) => { + log::error!("Could not read GLTF buffer from file: {err:?}"); return None; } }; @@ -271,7 +274,7 @@ impl GltfLoader { let mode = primitive.mode(); if mode != gltf::mesh::Mode::Triangles { - log::error!("Unsupported topology: {:?}", mode); + log::error!("Unsupported topology: {mode:?}"); return; }; @@ -391,8 +394,8 @@ impl GltfLoader { match base64_decode(&uri[URI_IMAGE_PNG.len()..]) { Ok(data) => (data, ImageFormat::Png), - Err(e) => { - log::error!("Could not decode texture data: {:?}", e); + Err(err) => { + log::error!("Could not decode texture data: {err:?}"); return Id::default(); } } @@ -400,7 +403,7 @@ impl GltfLoader { gltf::image::Source::View { view, mime_type } => { if mime_type != "image/png" { - log::warn!("Unsupported mime: {}", mime_type); + log::warn!("Unsupported mime: {mime_type}"); return Id::default(); } @@ -433,9 +436,9 @@ impl GltfLoader { let asset_name = animation .name() .map(|animation_name| [name, animation_name].join("::")) - .unwrap_or_else(|| format!("{}::animation[{}]", name, animation.index())); + .unwrap_or_else(|| format!("{name}::animation[{}]", animation.index())); - log::info!("importing animation as `{}`", asset_name); + log::info!("importing animation as `{asset_name}`"); let mut asset = Animation::new(asset_name); @@ -478,9 +481,8 @@ impl GltfLoader { }; } else { log::warn!( - "Animation {} refers target joint ({}), that does not exist", - asset.name(), - index + "Animation {} refers target joint ({index}), that does not exist", + asset.name() ); } } diff --git a/src/loaders/image_loader.rs b/src/loaders/image_loader.rs index 36c8cd59..8d24c431 100644 --- a/src/loaders/image_loader.rs +++ b/src/loaders/image_loader.rs @@ -23,7 +23,10 @@ impl ResourceLoader for ImageLoader { .and_then(|n| n.to_str()) .expect("Could not get file name from its path"); - let mut file = File::open(path).expect("Could not open Image resource file"); + let mut file = match File::open(path) { + Ok(file) => file, + Err(err) => panic!("Could not open image resource file ({path:?}): {err:?}"), + }; let metadata = std::fs::metadata(path).expect("Could not read Image file metadata"); let mut data = vec![0; metadata.len() as usize]; file.read_exact(&mut data) @@ -60,10 +63,11 @@ impl ImageLoader { ) -> Option { match image::load_from_memory_with_format(data, format) { Ok(img) => { - let img = img.into_rgba8(); - let (width, height) = img.dimensions(); - let resolution = Extent2D { width, height }; - Some(Image::new(name.into(), resolution, img.into_vec())) + let resolution = Extent2D { + width: img.width(), + height: img.height(), + }; + Some(Image::new(name.into(), resolution, img.into_bytes())) } Err(e) => { log::error!("Could not read image from buffer: {:?}", e); diff --git a/src/models/animations.rs b/src/models/animations.rs index 59b55965..fbf7d5ab 100644 --- a/src/models/animations.rs +++ b/src/models/animations.rs @@ -339,7 +339,7 @@ impl AnimationPlayer { } AnimationState::Loop(current) => { let mut new_duration = current + delta.mul_f32(self.speed); - if !(new_duration < duration) { + if new_duration >= duration { new_duration = Duration::from_secs_f32( new_duration.as_secs_f32() % duration.as_secs_f32(), ); diff --git a/src/models/colors.rs b/src/models/colors.rs index 5fd11019..609f8422 100644 --- a/src/models/colors.rs +++ b/src/models/colors.rs @@ -141,6 +141,17 @@ impl From<&Color> for [f32; 3] { } } +impl From<&Color> for Color { + fn from(color: &Color) -> Self { + Color { + r: (color.r as f32) / 255.0, + g: (color.g as f32) / 255.0, + b: (color.b as f32) / 255.0, + a: (color.a as f32) / 255.0, + } + } +} + impl VertexAttribute for Color { type Raw = u32; fn name() -> &'static str { @@ -170,3 +181,14 @@ impl From for Color { } } } + +impl From<[u8; 3]> for Color { + fn from(color: [u8; 3]) -> Self { + Self { + r: color[0], + g: color[1], + b: color[2], + a: 0xFF, + } + } +} diff --git a/src/models/renderer.rs b/src/models/renderer.rs index 19b09e61..022883e8 100644 --- a/src/models/renderer.rs +++ b/src/models/renderer.rs @@ -1,9 +1,8 @@ use std::collections::HashMap; -use std::ffi::CStr; use std::io::Cursor; -use crate::graphics::vk; -use crate::graphics::{Buffer, RenderPass}; +use crate::graphics::{vk, CommandRecorder}; +use crate::graphics::{Buffer, RenderSubmit}; use crate::loaders::Assets; use crate::models::materials::MAX_MATERIAL_IMAGES; use crate::utils::Id; @@ -19,8 +18,8 @@ use super::{ #[derive(Clone, Copy)] pub struct LayoutInBuffer { - /// Offset inside of the buffer in bytes - pub offset: u64, + // /// offset in bytes + // pub offset: u32, /// Offset of the first item (vertex or index) pub base: u32, /// Number of items (vertices or indices) @@ -46,24 +45,12 @@ pub struct DrawCount { pub struct RenderModels { /// GPU instance gpu: Gpu, - /// Wait for these semaphores before executing command buffers - wait_semaphores: Vec, - /// Signal these semaphores after rendering is done - signal_semaphore: vk::Semaphore, /// Command Pool command_pool: vk::CommandPool, /// Setup command buffer command_buffer_setup: vk::CommandBuffer, /// Setup command buffer reuse fence command_buffer_setup_reuse_fence: vk::Fence, - /// Draw command buffer - command_buffer_draw: vk::CommandBuffer, - /// Draw command buffer reuse fence - command_buffer_draw_reuse_fence: vk::Fence, - /// Framebuffers - framebuffers: Vec, - /// Render pass - render_pass: vk::RenderPass, /// Version of surface to track changes and update framebuffers and fender pass surface_version: u64, /// Index of instances by mesh (just mesh, indexed) @@ -115,8 +102,6 @@ pub struct RenderModels { material_layer_size: Extent2D, /// Material layer index in the material_image material_layer_index: HashMap, usize>, - /// Usage of layers in material image buffer - material_layer_usage: u32, /// Mesh layouts of non-rigged models mesh_registry: HashMap, MeshLayout>, /// descriptor sets @@ -201,22 +186,12 @@ impl Drop for RenderModels { } self.gpu.destroy_descriptor_pool(self.descriptor_pool); - // framebuffers - self.destroy_framebuffers(); - - // render pass - self.gpu.destroy_render_pass(self.render_pass); - // command buffers self.gpu.destroy_command_pool(self.command_pool); // fences self.gpu .destroy_fence(self.command_buffer_setup_reuse_fence); - self.gpu.destroy_fence(self.command_buffer_draw_reuse_fence); - - // semaphores - self.gpu.destroy_semaphore(self.signal_semaphore); } } } @@ -229,31 +204,24 @@ impl Task for RenderModels { Ref, Ref, ); - type Output = RenderPass; + type Output = RenderSubmit; fn run(&mut self, (frame, camera, assets, display, world): Self::Context) -> Self::Output { - log::debug!("pbr: begin"); - if let Some(surface_version) = display.surface_changed(self.surface_version) { unsafe { log::debug!("resize: Surface changed"); self.gpu.device_wait_idle().unwrap(); - // rebuild framebuffers - - log::debug!("resize: destroy_framebuffers"); - self.destroy_framebuffers(); - - log::debug!("resize: create_framebuffers"); - self.create_framebuffers(&display, self.render_pass); - // rebuild pipelines if self.pipeline_render_only_mesh == vk::Pipeline::null() { log::debug!("resize: destroy_graphics_pipelines"); + // NOTE: WHAT ARE WE DESTROYING HERE??? self.destroy_graphics_pipelines(); log::debug!("resize: create_graphics_pipelines"); - let graphic_pipelines = - self.create_graphics_pipelines(display.surface_resolution()); + let graphic_pipelines = self.create_graphics_pipelines( + display.render_pass(), + display.surface_resolution(), + ); self.pipeline_render_only_mesh = graphic_pipelines[0]; self.pipeline_render_skin_mesh = graphic_pipelines[1]; @@ -261,8 +229,6 @@ impl Task for RenderModels { log::debug!("resize: setup_depth_image"); self.setup_depth_image(&display); } - - log::debug!("resize: complete -> {}", surface_version); }; self.surface_version = surface_version; } @@ -271,13 +237,20 @@ impl Task for RenderModels { log::debug!("draw count: {:?}", draw_count); - unsafe { - self.execute_render_pass(&frame, draw_count); - self.submit_draw_commands(); - } + let command_recorder = Recorder { + resolution: frame.resolution, + draw_count, + pipeline_layout: self.pipeline_layout_render, + descriptor_sets: self.descriptor_sets.clone(), + pipeline_render_only_mesh: self.pipeline_render_only_mesh, + pipeline_render_skin_mesh: self.pipeline_render_skin_mesh, + indirect_buffer: self.indirect_buffer.handle, + index_buffer: self.index_buffer.handle, + vertex_buffer_only_mesh: self.vertex_buffer_only_mesh.handle, + vertex_buffer_skin_mesh: self.vertex_buffer_skin_mesh.handle, + }; - log::debug!("pbr: submit_command_buffer"); - RenderPass {} + RenderSubmit::new::(Box::new(command_recorder), &[]) } } @@ -297,11 +270,11 @@ impl RenderModels { }; let command_buffer_allocate_info = vk::CommandBufferAllocateInfo::default() - .command_buffer_count(2) + .command_buffer_count(1) .command_pool(command_pool) .level(vk::CommandBufferLevel::PRIMARY); - let (command_buffer_setup, command_buffer_draw) = unsafe { + let command_buffer_setup = unsafe { gpu.allocate_command_buffers(&command_buffer_allocate_info) .into() }; @@ -310,70 +283,6 @@ impl RenderModels { vk::FenceCreateInfo::default().flags(vk::FenceCreateFlags::SIGNALED); let command_buffer_setup_reuse_fence = unsafe { gpu.create_fence(&fence_create_info) }; - let command_buffer_draw_reuse_fence = unsafe { gpu.create_fence(&fence_create_info) }; - - let signal_semaphore_create_info = vk::SemaphoreCreateInfo::default(); - let signal_semaphore = unsafe { - gpu.create_semaphore(&signal_semaphore_create_info) - .expect("Failed to create a signal semaphore") - }; - let mut wait_semaphores = setup.wait_semaphores; - unsafe { - wait_semaphores.push(display.present_complete_semaphore()); - }; - - // TODO: this works only until we have one - display.set_render_complete_semaphore(signal_semaphore); - - let renderpass_attachments = [ - vk::AttachmentDescription { - format: setup.surface_format, - samples: vk::SampleCountFlags::TYPE_1, - load_op: vk::AttachmentLoadOp::CLEAR, - store_op: vk::AttachmentStoreOp::STORE, - final_layout: vk::ImageLayout::PRESENT_SRC_KHR, - ..Default::default() - }, - vk::AttachmentDescription { - format: vk::Format::D16_UNORM, - samples: vk::SampleCountFlags::TYPE_1, - load_op: vk::AttachmentLoadOp::CLEAR, - initial_layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, - final_layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, - ..Default::default() - }, - ]; - let color_attachment_refs = [vk::AttachmentReference { - attachment: 0, - layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL, - }]; - let depth_attachment_ref = vk::AttachmentReference { - attachment: 1, - layout: vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, - }; - let dependencies = [vk::SubpassDependency { - src_subpass: vk::SUBPASS_EXTERNAL, - src_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, - dst_access_mask: vk::AccessFlags::COLOR_ATTACHMENT_READ - | vk::AccessFlags::COLOR_ATTACHMENT_WRITE, - dst_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, - ..Default::default() - }]; - - let subpass = vk::SubpassDescription::default() - .color_attachments(&color_attachment_refs) - .depth_stencil_attachment(&depth_attachment_ref) - .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS); - - let renderpass_create_info = vk::RenderPassCreateInfo::default() - .attachments(&renderpass_attachments) - .subpasses(std::slice::from_ref(&subpass)) - .dependencies(&dependencies); - - let render_pass = unsafe { - gpu.create_render_pass(&renderpass_create_info) - .expect("Failed to create a render pass") - }; let globals_buffer = unsafe { Self::create_globals_uniform_buffer(&gpu) @@ -694,14 +603,8 @@ impl RenderModels { Self { gpu, command_pool, - wait_semaphores, - signal_semaphore, command_buffer_setup, - command_buffer_draw, command_buffer_setup_reuse_fence, - command_buffer_draw_reuse_fence, - render_pass, - framebuffers: Vec::new(), surface_version: 0, index_buffer, index_buffer_usage: 0, @@ -727,7 +630,6 @@ impl RenderModels { material_image_memory, material_staging_buffer, material_layer_count, - material_layer_usage: 0, material_layer_index: HashMap::new(), material_layer_size: setup.material_image_size, mesh_registry: HashMap::new(), @@ -753,13 +655,6 @@ impl RenderModels { } } - /// # Safety - /// - /// Leaks signal semaphore that should never be destroyed - pub unsafe fn signal_semaphore(&self) -> vk::Semaphore { - self.signal_semaphore - } - fn update_buffers(&mut self, camera: &Camera, assets: &Assets, world: &World) -> DrawCount { self.instances_skin_mesh_indexed.clear(); self.instances_only_mesh_indexed.clear(); @@ -775,14 +670,13 @@ impl RenderModels { .map_and_write_to_device_memory(&self.gpu, 0, &globals_uniform); } - for (entity_id, mesh_id, material_id, _armature_id, transform) in world.query::<( + for (_entity_id, mesh_id, material_id, _armature_id, transform) in world.query::<( &Id, &Id, &Id, &Id, &Transform, )>() { - log::debug!("Update buffers: {:?}", entity_id); let material_index = self.register_material(*material_id, assets); if material_index.is_none() { continue; @@ -806,7 +700,7 @@ impl RenderModels { transform: transform.model.matrix().to_cols_array_2d(), }); let mut joint_index = 0; // NOTE: real joint can never have a 0 index - if mesh_layout.has_skin && transform.armature.len() != 0 { + if mesh_layout.has_skin && !transform.armature.is_empty() { joint_index = self.transform_buffer_data.len() as u32; self.transform_buffer_data .extend(transform.armature.iter().map(|i| TransformUniform { @@ -823,31 +717,23 @@ impl RenderModels { ..Default::default() }); - log::debug!("\n\n material index: {} ({:?})\n transform_index: {}\n joint_index: {}\n\n", material_index, material_id, transform_index, joint_index); - if mesh_layout.has_skin {} } } unsafe { - let material_bytes = self.material_buffer.map_and_write_to_device_memory( + self.material_buffer.map_and_write_to_device_memory( &self.gpu, 0, self.material_buffer_data.as_slice(), ); - log::debug!( - "writing materials buffer ({:?}): {:?}", - material_bytes, - self.material_buffer_data.as_slice() - ); }; unsafe { - let transform_bytes = self.transform_buffer.map_and_write_to_device_memory( + self.transform_buffer.map_and_write_to_device_memory( &self.gpu, 0, self.transform_buffer_data.as_slice(), ); - log::debug!("writing materials buffer ({:?})", transform_bytes); }; let mut instances_total = 0; @@ -883,17 +769,6 @@ impl RenderModels { }) .collect::>(); - log::debug!( - "only mesh: instance buffer data (offest: {}): {:?}", - instance_buffer_offset, - instance_buffer_data - ); - log::debug!( - "only mesh: indirect buffer data (offest: {}): {:?}", - indirect_buffer_offset, - indirect_buffer_data - ); - instances_total += instances_count; unsafe { @@ -943,17 +818,6 @@ impl RenderModels { }) .collect::>(); - log::debug!( - "only mesh(indexed): instance buffer data (offest: {}): {:?}", - instance_buffer_offset, - instance_buffer_data - ); - log::debug!( - "only mesh(indexed): indirect buffer data (offest: {}): {:?}", - indirect_buffer_offset, - indirect_buffer_data - ); - instances_total += instances_count; unsafe { @@ -977,10 +841,7 @@ impl RenderModels { let skin_mesh_draws_count = if !self.instances_skin_mesh.is_empty() { indirect_buffer_offset += indirect_buffer_offset % std::mem::size_of::() as u64; - log::debug!( - "skin mesh: indirect buffer data (offest: {})", - indirect_buffer_offset, - ); + let instances_count = self .instances_skin_mesh .values() @@ -1029,10 +890,7 @@ impl RenderModels { let skin_mesh_indexed_draws_count = if !self.instances_skin_mesh_indexed.is_empty() { indirect_buffer_offset += indirect_buffer_offset % std::mem::size_of::() as u64; - log::debug!( - "skin mesh(indexed): indirect buffer data (offest: {})", - indirect_buffer_offset, - ); + let instances_count = self .instances_skin_mesh_indexed .values() @@ -1063,12 +921,12 @@ impl RenderModels { // instances_total += instances_count; unsafe { - indirect_buffer_offset += self.indirect_buffer.map_and_write_to_device_memory( + self.indirect_buffer.map_and_write_to_device_memory( &self.gpu, indirect_buffer_offset, indirect_buffer_data.as_slice(), ); - instance_buffer_offset += self.instance_buffer.map_and_write_to_device_memory( + self.instance_buffer.map_and_write_to_device_memory( &self.gpu, instance_buffer_offset, instance_buffer_data.as_slice(), @@ -1124,12 +982,12 @@ impl RenderModels { let mesh_layout = MeshLayout { vertices: LayoutInBuffer { - offset: vertex_offset, + // offset: vertex_offset, base: (vertex_offset / vertex_size) as u32, count: mesh.count_vertices() as u32, }, indices: index_data.map(|data| LayoutInBuffer { - offset: self.index_buffer_usage, + // offset: self.index_buffer_usage, base: (self.index_buffer_usage / (std::mem::size_of::() as u64)) as u32, count: data.len() as u32, @@ -1137,22 +995,10 @@ impl RenderModels { has_skin, }; - log::debug!( - "VB offset: {}, IB offset: {:?}, has skin: {:?}", - mesh_layout.vertices.offset, - mesh_layout.indices.as_ref().map(|i| i.offset), - has_skin - ); - self.mesh_registry.insert(mesh_id, mesh_layout); unsafe { if has_skin { - log::debug!( - "write {:?} bytes to VB(skin) at offset {:?}", - vertex_data.len(), - vertex_offset - ); self.vertex_buffer_skin_mesh_usage += self.vertex_buffer_skin_mesh.map_and_write_to_device_memory( &self.gpu, @@ -1160,11 +1006,6 @@ impl RenderModels { vertex_data.as_slice(), ); } else { - log::debug!( - "write {:?} bytes to VB(no skin) at offset {:?}", - vertex_data.len(), - vertex_offset - ); self.vertex_buffer_only_mesh_usage += self.vertex_buffer_only_mesh.map_and_write_to_device_memory( &self.gpu, @@ -1174,13 +1015,7 @@ impl RenderModels { } }; - // log::debug!("Indices @{}: {:?}", self.index_buffer_usage, index_data); if let Some(data) = index_data.as_ref() { - log::debug!( - "write {:?} bytes to IB at offset {:?}", - data.len() * std::mem::size_of::(), - self.index_buffer_usage - ); unsafe { self.index_buffer_usage += self.index_buffer.map_and_write_to_device_memory( @@ -1189,7 +1024,6 @@ impl RenderModels { data, ); } - log::debug!("self.index_buffer_usage -> {}", self.index_buffer_usage); } return Some(mesh_layout); @@ -1207,7 +1041,9 @@ impl RenderModels { .get(material.albedo_map) .map(|image| { let base_array_layer = self.material_layer_index.len(); - if !self.material_layer_index.contains_key(&material.albedo_map) { + if let std::collections::hash_map::Entry::Vacant(e) = + self.material_layer_index.entry(material.albedo_map) + { // write to buffer // TODO: verify material extent unsafe { @@ -1221,8 +1057,7 @@ impl RenderModels { image.data(), ); }; - self.material_layer_index - .insert(material.albedo_map, base_array_layer); + e.insert(base_array_layer); staging_layer_count += 1; } let albedo_map_index = self @@ -1239,13 +1074,8 @@ impl RenderModels { self.flush_material_staging_buffer(staging_layer_count, base_array_layer as u32); }; let mut material_uniform: MaterialUniform = material.into(); - material_uniform.maps_1 = [ - albedo_map_index, - std::u32::MAX, - std::u32::MAX, - std::u32::MAX, - ]; - material_uniform.maps_2 = [std::u32::MAX; 4]; + material_uniform.maps_1 = [albedo_map_index, u32::MAX, u32::MAX, u32::MAX]; + material_uniform.maps_2 = [u32::MAX; 4]; material_uniform } else { return None; @@ -1255,9 +1085,8 @@ impl RenderModels { self.material_buffer_index .get(&material_id) .cloned() - .map(|material_index| { + .inspect(|&material_index| { self.material_buffer_data[material_index as usize] = material_uniform; - material_index }) .or_else(|| { let material_index = self.material_buffer_index.len() as u32; @@ -1276,7 +1105,6 @@ impl RenderModels { if staging_layer_count == 0 { return; } - log::debug!("copy material to vk:Image ({} layers)", staging_layer_count); // begin self.gpu .wait_for_fences(&[self.command_buffer_setup_reuse_fence], true, u64::MAX) @@ -1300,24 +1128,6 @@ impl RenderModels { .begin_command_buffer(self.command_buffer_setup, &command_buffer_begin_info) .expect("Begin commandbuffer"); - // Command buffer - // let buffer_copy_regions = (0..staging_layer_count) - // .into_iter() - // .map(|_| { - // vk::BufferImageCopy::default() - // .image_subresource( - // vk::ImageSubresourceLayers::default() - // .aspect_mask(vk::ImageAspectFlags::COLOR) - // .base_array_layer(base_array_layer) - // .layer_count(1), - // ) - // .image_extent(vk::Extent3D { - // width: self.material_layer_size.width, - // height: self.material_layer_size.height, - // depth: 1, - // }) - // }) - // .collect::>(); let buffer_copy_regions = [vk::BufferImageCopy::default() .image_subresource( vk::ImageSubresourceLayers::default() @@ -1397,16 +1207,14 @@ impl RenderModels { .command_buffers(&command_buffers) .signal_semaphores(&signal_semaphores); - log::debug!("submit image copy"); self.gpu .submit_queue(&[submit_info], self.command_buffer_setup_reuse_fence) .expect("queue submit failed."); // wait let fences = [self.command_buffer_setup_reuse_fence]; self.gpu - .wait_for_fences(&fences, true, std::u64::MAX) + .wait_for_fences(&fences, true, u64::MAX) .expect("Failed to wait until end of textures bufering"); - log::debug!("wait_for_fences: done"); } /// Returns Buffer, binded memory and allocated size @@ -1485,34 +1293,12 @@ impl RenderModels { gpu.create_shader_module(&shader_module_create_info) } - unsafe fn create_framebuffers(&mut self, display: &Display, render_pass: vk::RenderPass) { - let resolution = display.surface_resolution(); - self.framebuffers = display - .swapchain_image_views() - .map(|&present_image_view| { - let framebuffer_attachments = [present_image_view, display.depth_image_view()]; - let frame_buffer_create_info = vk::FramebufferCreateInfo::default() - .render_pass(render_pass) - .attachments(&framebuffer_attachments) - .width(resolution.width) - .height(resolution.height) - .layers(1); - - self.gpu - .create_framebuffer(&frame_buffer_create_info) - .expect("Could not create a framebuffer") - }) - .collect::>() - } - - unsafe fn destroy_framebuffers(&mut self) { - for framebuffer in self.framebuffers.drain(..) { - self.gpu.destroy_framebuffer(framebuffer); - } - } - - unsafe fn create_graphics_pipelines(&self, surface_resolution: Extent2D) -> Vec { - let shader_entry_point = unsafe { CStr::from_bytes_with_nul_unchecked(b"main\0") }; + unsafe fn create_graphics_pipelines( + &self, + render_pass: vk::RenderPass, + surface_resolution: Extent2D, + ) -> Vec { + let shader_entry_point = c"main"; // ONLY MESH let only_mesh_shader_stages = [ vk::PipelineShaderStageCreateInfo { @@ -1662,12 +1448,23 @@ impl RenderModels { let rasterization_info = vk::PipelineRasterizationStateCreateInfo { front_face: vk::FrontFace::COUNTER_CLOCKWISE, + depth_clamp_enable: vk::FALSE, + rasterizer_discard_enable: vk::FALSE, + depth_bias_enable: vk::FALSE, + depth_bias_constant_factor: 0.0, + depth_bias_clamp: 0.0, + depth_bias_slope_factor: 0.0, line_width: 1.0, polygon_mode: vk::PolygonMode::FILL, ..Default::default() }; let multisample_state_info = vk::PipelineMultisampleStateCreateInfo { rasterization_samples: vk::SampleCountFlags::TYPE_1, + sample_shading_enable: vk::FALSE, + //multisampling defaulted to no multisampling (1 sample per pixel) + min_sample_shading: 1.0, + alpha_to_coverage_enable: vk::FALSE, + alpha_to_one_enable: vk::FALSE, ..Default::default() }; let noop_stencil_state = vk::StencilOpState { @@ -1715,7 +1512,7 @@ impl RenderModels { .color_blend_state(&color_blend_state) .dynamic_state(&dynamic_state_info) .layout(self.pipeline_layout_render) - .render_pass(self.render_pass); + .render_pass(render_pass); let skin_mesh_graphic_pipeline_info = vk::GraphicsPipelineCreateInfo::default() .stages(&skin_mesh_shader_stages) @@ -1728,7 +1525,7 @@ impl RenderModels { .color_blend_state(&color_blend_state) .dynamic_state(&dynamic_state_info) .layout(self.pipeline_layout_render) - .render_pass(self.render_pass); + .render_pass(render_pass); self.gpu .create_graphics_pipelines( @@ -1746,223 +1543,6 @@ impl RenderModels { self.gpu.destroy_pipeline(self.pipeline_render_skin_mesh); } - unsafe fn execute_render_pass(&self, frame: &Frame, draw_count: DrawCount) { - self.gpu - .wait_for_fences(&[self.command_buffer_draw_reuse_fence], true, u64::MAX) - .expect("Failed to wait for draw buffer fences"); - - self.gpu - .reset_fences(&[self.command_buffer_draw_reuse_fence]) - .expect("Failed to reset Vulkan fences"); - - let command_buffer_begin_info = vk::CommandBufferBeginInfo::default() - .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); - - self.gpu - .reset_command_buffer( - self.command_buffer_draw, - vk::CommandBufferResetFlags::RELEASE_RESOURCES, - ) - .expect("Failed to reset Vulkan command buffer"); - - self.gpu - .begin_command_buffer(self.command_buffer_draw, &command_buffer_begin_info) - .expect("Failed to begin draw command buffer"); - - let clear_values = [ - vk::ClearValue { - color: vk::ClearColorValue { - float32: [0.0, 0.0, 0.1, 0.0], - }, - }, - vk::ClearValue { - depth_stencil: vk::ClearDepthStencilValue { - depth: 1.0, - stencil: 0, - }, - }, - ]; - - let viewports = [vk::Viewport { - x: 0.0, - y: 0.0, - width: frame.resolution.width as f32, - height: frame.resolution.height as f32, - min_depth: 0.0, - max_depth: 1.0, - }]; - let scissors = [(vk::Extent2D { - width: frame.resolution.width, - height: frame.resolution.height, - }) - .into()]; - - let render_pass_begin_info = vk::RenderPassBeginInfo::default() - .render_pass(self.render_pass) - .framebuffer(self.framebuffers[frame.swapchain_index as usize]) - .render_area(vk::Rect2D { - offset: vk::Offset2D { x: 0, y: 0 }, - extent: vk::Extent2D { - width: frame.resolution.width, - height: frame.resolution.height, - }, - }) - .clear_values(&clear_values); - - self.gpu.cmd_begin_render_pass( - self.command_buffer_draw, - &render_pass_begin_info, - vk::SubpassContents::INLINE, - ); - - self.gpu.cmd_bind_descriptor_sets( - self.command_buffer_draw, - vk::PipelineBindPoint::GRAPHICS, - self.pipeline_layout_render, - 0, - &self.descriptor_sets[..], - &[], - ); - - let mut offset: u64 = 0; - - // ONLY MESH - if draw_count.only_mesh != 0 || draw_count.only_mesh_indexed != 0 { - self.gpu.cmd_bind_pipeline( - self.command_buffer_draw, - vk::PipelineBindPoint::GRAPHICS, - self.pipeline_render_only_mesh, - ); - self.gpu - .cmd_set_viewport(self.command_buffer_draw, 0, &viewports); - self.gpu - .cmd_set_scissor(self.command_buffer_draw, 0, &scissors); - self.gpu.cmd_bind_vertex_buffers( - self.command_buffer_draw, - 0, - &[self.vertex_buffer_only_mesh.handle], - &[0], - ); - - if draw_count.only_mesh != 0 { - log::debug!( - "cmd_draw_indirect(offset: {}, draw_count: {}, stride: {})", - offset, - draw_count.only_mesh, - std::mem::size_of::() as u32 - ); - self.gpu.cmd_draw_indirect( - self.command_buffer_draw, - self.indirect_buffer.handle, - offset, - draw_count.only_mesh, - std::mem::size_of::() as u32, - ); - offset += draw_count.only_mesh as u64 - * std::mem::size_of::() as u64; - } - - if draw_count.only_mesh_indexed != 0 { - // padding - offset += offset % std::mem::size_of::() as u64; - log::debug!( - "cmd_draw_indexed_indirect(offset: {}, draw_count: {}, stride: {})", - offset, - draw_count.only_mesh_indexed, - std::mem::size_of::() as u32 - ); - self.gpu.cmd_bind_index_buffer( - self.command_buffer_draw, - self.index_buffer.handle, - 0, - vk::IndexType::UINT32, - ); - - self.gpu.cmd_draw_indexed_indirect( - self.command_buffer_draw, - self.indirect_buffer.handle, - offset, - draw_count.only_mesh_indexed, - std::mem::size_of::() as u32, - ); - - offset += draw_count.only_mesh_indexed as u64 - * std::mem::size_of::() as u64; - } - } - - // MESH WITH SKIN - if draw_count.skin_mesh != 0 || draw_count.skin_mesh_indexed != 0 { - self.gpu.cmd_bind_pipeline( - self.command_buffer_draw, - vk::PipelineBindPoint::GRAPHICS, - self.pipeline_render_skin_mesh, - ); - self.gpu - .cmd_set_viewport(self.command_buffer_draw, 0, &viewports); - self.gpu - .cmd_set_scissor(self.command_buffer_draw, 0, &scissors); - self.gpu.cmd_bind_vertex_buffers( - self.command_buffer_draw, - 0, - &[self.vertex_buffer_skin_mesh.handle], - &[0], - ); - - if draw_count.skin_mesh != 0 { - // padding - offset += offset % std::mem::size_of::() as u64; - log::debug!( - "skin - cmd_draw_indirect(offset: {}, draw_count: {}, stride: {})", - offset, - draw_count.skin_mesh, - std::mem::size_of::() as u32 - ); - self.gpu.cmd_draw_indirect( - self.command_buffer_draw, - self.indirect_buffer.handle, - offset, - draw_count.skin_mesh, - std::mem::size_of::() as u32, - ); - offset += draw_count.skin_mesh as u64 - * std::mem::size_of::() as u64; - } - - if draw_count.skin_mesh_indexed != 0 { - offset += offset % std::mem::size_of::() as u64; - log::debug!( - "skin - cmd_draw_indexed_indirect(offset: {}, draw_count: {}, stride: {})", - offset, - draw_count.skin_mesh_indexed, - std::mem::size_of::() as u32 - ); - self.gpu.cmd_bind_index_buffer( - self.command_buffer_draw, - self.index_buffer.handle, - 0, - vk::IndexType::UINT32, - ); - - self.gpu.cmd_draw_indexed_indirect( - self.command_buffer_draw, - self.indirect_buffer.handle, - offset, - draw_count.skin_mesh_indexed, - std::mem::size_of::() as u32, - ); - } - } - - self.gpu.cmd_end_render_pass(self.command_buffer_draw); - - self.gpu - .end_command_buffer(self.command_buffer_draw) - .expect("End commandbuffer"); - - // panic!("--------------------------- BREAKPOINT ---------------------------"); - } - unsafe fn setup_depth_image(&self, display: &Display) { let depth_image = display.depth_image(); @@ -2037,33 +1617,6 @@ impl RenderModels { .submit_queue(&submits, self.command_buffer_setup_reuse_fence) .expect("queue submit failed.") } - - unsafe fn submit_draw_commands(&self) { - // panic!("----------------------- BREAKPOINT -----------------------"); - let (wait_semaphores, wait_dst_stage_mask): (Vec<_>, Vec<_>) = self - .wait_semaphores - .iter() - .map(|s| (s, vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT)) - .unzip(); - let signal_semaphores = [self.signal_semaphore]; - let command_buffers = [self.command_buffer_draw]; - let submits = [vk::SubmitInfo::default() - .wait_semaphores(wait_semaphores.as_slice()) - .wait_dst_stage_mask(wait_dst_stage_mask.as_slice()) - .command_buffers(&command_buffers) - .signal_semaphores(&signal_semaphores)]; - - log::debug!( - "buffers: {}, wait: {}, signal: {}, deps: {}", - command_buffers.len(), - wait_semaphores.len(), - signal_semaphores.len(), - wait_dst_stage_mask.len(), - ); - self.gpu - .submit_queue(&submits, self.command_buffer_draw_reuse_fence) - .expect("Failed to submit draw buffer to queue"); - } } pub struct RenderModelsSetup { @@ -2146,3 +1699,135 @@ pub struct TransformUniform { /// Model/Joint transform matrix pub transform: [[f32; 4]; 4], } + +pub struct Recorder { + resolution: Extent2D, + draw_count: DrawCount, + pipeline_layout: vk::PipelineLayout, + descriptor_sets: Vec, + pipeline_render_only_mesh: vk::Pipeline, + pipeline_render_skin_mesh: vk::Pipeline, + indirect_buffer: vk::Buffer, + index_buffer: vk::Buffer, + vertex_buffer_only_mesh: vk::Buffer, + vertex_buffer_skin_mesh: vk::Buffer, +} + +impl CommandRecorder for Recorder { + unsafe fn record(&self, gpu: &Gpu, command_buffer: vk::CommandBuffer) { + let viewports = [vk::Viewport { + x: 0.0, + y: 0.0, + width: self.resolution.width as f32, + height: self.resolution.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }]; + let scissors = [(vk::Extent2D { + width: self.resolution.width, + height: self.resolution.height, + }) + .into()]; + + gpu.cmd_bind_descriptor_sets( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline_layout, + 0, + &self.descriptor_sets[..], + &[], + ); + + let mut offset: u64 = 0; + + gpu.cmd_set_viewport(command_buffer, 0, &viewports); + gpu.cmd_set_scissor(command_buffer, 0, &scissors); + // ONLY MESH + if self.draw_count.only_mesh != 0 || self.draw_count.only_mesh_indexed != 0 { + gpu.cmd_bind_pipeline( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline_render_only_mesh, + ); + gpu.cmd_bind_vertex_buffers(command_buffer, 0, &[self.vertex_buffer_only_mesh], &[0]); + + if self.draw_count.only_mesh != 0 { + gpu.cmd_draw_indirect( + command_buffer, + self.indirect_buffer, + offset, + self.draw_count.only_mesh, + std::mem::size_of::() as u32, + ); + offset += self.draw_count.only_mesh as u64 + * std::mem::size_of::() as u64; + } + + if self.draw_count.only_mesh_indexed != 0 { + // padding + offset += offset % std::mem::size_of::() as u64; + gpu.cmd_bind_index_buffer( + command_buffer, + self.index_buffer, + 0, + vk::IndexType::UINT32, + ); + + gpu.cmd_draw_indexed_indirect( + command_buffer, + self.indirect_buffer, + offset, + self.draw_count.only_mesh_indexed, + std::mem::size_of::() as u32, + ); + + offset += self.draw_count.only_mesh_indexed as u64 + * std::mem::size_of::() as u64; + } + } + + // MESH WITH SKIN + if self.draw_count.skin_mesh != 0 || self.draw_count.skin_mesh_indexed != 0 { + gpu.cmd_bind_pipeline( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline_render_skin_mesh, + ); + gpu.cmd_bind_vertex_buffers(command_buffer, 0, &[self.vertex_buffer_skin_mesh], &[0]); + + if self.draw_count.skin_mesh != 0 { + // padding + offset += offset % std::mem::size_of::() as u64; + gpu.cmd_draw_indirect( + command_buffer, + self.indirect_buffer, + offset, + self.draw_count.skin_mesh, + std::mem::size_of::() as u32, + ); + offset += self.draw_count.skin_mesh as u64 + * std::mem::size_of::() as u64; + } + + if self.draw_count.skin_mesh_indexed != 0 { + offset += offset % std::mem::size_of::() as u64; + gpu.cmd_bind_index_buffer( + command_buffer, + self.index_buffer, + 0, + vk::IndexType::UINT32, + ); + + gpu.cmd_draw_indexed_indirect( + command_buffer, + self.indirect_buffer, + offset, + self.draw_count.skin_mesh_indexed, + std::mem::size_of::() as u32, + ); + } + } + + // panic!("--------------------------- BREAKPOINT ---------------------------"); + } +} diff --git a/src/models/shaders/only_mesh.vert b/src/models/shaders/only_mesh.vert index c32baf30..9efc33b9 100644 --- a/src/models/shaders/only_mesh.vert +++ b/src/models/shaders/only_mesh.vert @@ -2,11 +2,11 @@ #extension GL_ARB_separate_shader_objects : enable #extension GL_ARB_shading_language_420pack : enable -layout (location = 0) in vec3 pos; -layout (location = 1) in vec3 normal; -layout (location = 2) in vec2 texture; +layout(location = 0) in vec3 pos; +layout(location = 1) in vec3 normal; +layout(location = 2) in vec2 texture; -layout (binding = 0) uniform DtxGlobals { +layout(binding = 0) uniform DtxGlobals { mat4 proj; mat4 view; } dtx_globals; @@ -40,19 +40,22 @@ layout(std430, binding = 3) buffer DtxTransformsLayout mat4 dtx_transform[]; }; -layout (location = 0) out vec3 o_world_position; -layout (location = 1) out vec3 o_world_normal; -layout (location = 2) out vec4 o_color; +layout(location = 0) out vec3 o_world_position; +layout(location = 1) out vec3 o_world_normal; +layout(location = 2) out vec4 o_color; +layout(location = 3) out vec3 o_texture; void main() { uint transform_index = dtx_instance[gl_InstanceIndex].transform_index; uint material_index = dtx_instance[gl_InstanceIndex].material_index; mat4 model_transform = dtx_transform[transform_index]; vec4 material_color = dtx_material[material_index].color; + float albedo_layer = float(dtx_material[material_index].maps_1.x); mat4 proj_view = dtx_globals.proj * dtx_globals.view; o_world_position = vec3(model_transform * vec4(pos, 1.0)); o_world_normal = vec3(model_transform * vec4(normal, 1.0)); o_color = vec4(material_color); + o_texture = vec3(texture, albedo_layer); gl_Position = proj_view * vec4(o_world_position, 1.0); } diff --git a/src/models/shaders/only_mesh.vert.spv b/src/models/shaders/only_mesh.vert.spv index f0c1f645bcf4a9072d7d3b7480c0cfe7a9e7a73d..0d505f2e8eb6590c4aa6d072973236f35fd2a31d 100644 GIT binary patch literal 4344 zcmZ9OX?qk!5XUFSE|-Xi94dqj5f!5$#BeBA!r=l235R$)Om>E4!enOMom}V(zk+@w zKb3EM9{<0YuBD;#RJ!V4XIFRa?%3$mjvyEb_5^!_KZ1Jg4@QLq!kX3IUV6ARGbrPk z`LnZFycvvDhS=L%g*+-xuEJeSRG;P8E7Cl)l(!6v|hSTw|7sZ@h ztTOM={k)QUDcUa9OJUT2jun?%isXuu+)&^5R+>5nch${8tN(U&u*$9(9M=B&8R*4v z@FGsKaCnNUo=M^A#UpFez*R&&=vdVA(yOj#s#YmRGdLxFn6p!4MIW0+FfClM@4A-C zvc_KRy?(J8ZluYk!!B!AzIIluM48?=b%C+4WjBm7IagUVf~%6L zrR$t|56oN#;ZB?WoEVr>+X`1`7OdS z*(%rj8l%dGc{YUDze(16T2A_plfD!Zh1NJcIxCHLfjwuda6S8n=?g;3qtw(D@k2(yq9+)3z@crye3svme56t|ijacNSHs;K)4Vl_l=$W~RN~&eP^L&}_ zKV))X&v&6OGh@h>j~PR@-Y{RL=bXHI)}DH+?!aaUj67yXOfX^@1`o!&=dAc*cR>Hy zCxu}He^6H3V@t$pNv2N__=Ns5U+_L1Qv~J&-Gq4ZA>+>+SzQ;M%#2t*eoeGQBcf^P zk@tyM`12Ogf#Z+N4Egp4oQ(ZD;)zXthg8e^+FPQ-(jC=~OfQb;|1s@g$oSHC&I-<% zT8`&Tlj4!FJE5H%VAO;>5=a^jCzQ*BnpB*ubMw|MSNuJnJZ!tucI9u=S7bk zJIwr_I+=RA;>ltCdM2J&;MgOl4zvCgPChDqPdxr$+#fSVZZLEZb@5|>y3i9BneQsF zbDC}P^9ER8{|aen$Cmrzdx_m-exI4lTRrZ{ys=ZB%$@Vw#J9iXiAPTy&Vy{a55yCDQ5jEa z|4=j~!XF)WyvvV7=!iiNkS)fnc;a%VGur1wr$xj-XSHx&pNr5Fhu$GuoO9yqI6JG} zd?DH5pgS*OX3YLe@!0fKBk$m%2#na=1M;G7mvb$Mm>;W!GcAf1s=cD{sj zf9%mSuYV~ny89w>V`DKMI9%Hhd+0Df*CU73xtdko-wMMYd-R;OtsUIX`ki?HtUpNR zGs5!!C?1Su*iYiY{QN&V%s=Zd!tlo)y?@qUC6gN)iyMjuXBqZHJeZ#=a+rVC6=C>4 z5uxX-kF|qyR{EQWc(3SUC!_0%UlsA$h0Gm47xDhAj!Zl@EW_5sgHhYEW{=t$k_d-Q(X}K4_}dniU0rr literal 4000 zcmZ9OX?GM=5QaO;Ojr~|grGu5hP2}R_Mb+rxEE%P-ME?e58FtUblPDWb=vz;I}Ycs&UK^aGsQVUoZ9AQZR1YZ z>~_O`{xXfjH0i`)E9ogx@;oNiG0!gPG}HZ-Z%~ni-%CX^IHp?jUexJ7$$EQX(rLv9 zK}G+L3t!C-);s+?>NI0njun_-%_5I`Nt8PCfsUCSja8MFSj=QMOFFr8GMs6L-Ka(9 zVwHJJ-{w&6jp${zp9`Y~bevXxph&Jb$ra7L{90uUzEe95t^W0*vBItzjO#wy1GBgc zp0(mM9CT6CGbLQJcxYqlyNaj>9fx{ed6o4{mny}m1|N$bXP*)w&LESxR>jp>+lZ2Ip{1c z-?lD{x(i%ScRTq*TZT`mWA@VBPLu9x;_XRYINt!>ewq$+^wAK`pr^J&V znVrI(H;X)}oA^tjHObg=DkAckZ25dlznF`^?~7sJv-sohfICsVg~m9Z3ZS=JnAsaW?+8w;QLum zOGWI`56t|ijacNSHg?Ld4Vl_F=-ID{LMr8zBQk%_(31mu{=V3CmlT1$KsO?04lrs$o^}}e z_Z?3h>gFN+@%nI3t?VSFQEt%`!+&qedU zCkcOK?AZ%qkngc>bkvU?dB z`ugTa)4ux%npHF*A8OGH{=lGUm^Ui!ItmM-w|$;$31yM{AEw(jqv{&vq#VW z50DoWkN5wkh_}Bily`nk#4fQf=-2~n%ePcisIPY}Cxghf6ye-+{pd+5ec}F~Y z;!r=b>E07h>}6%-&3_=87U7Q$JKo)gB6P%{XJm^pC!VEciI1T!5_~5&|!YBKMKPi zd-Q&ey_g@!ykL}^vdraaC&9FzlnHL=ysfJb7)HD z9oRQ-iDy<;M=TzUW7re%V18|Fhxv1ODhz+@(fe~qB$FE(-tc4HyP}4OcTTOy*!`zW HH$?vfCGce- diff --git a/src/models/vertices.rs b/src/models/vertices.rs index ca517df6..c65a31fa 100644 --- a/src/models/vertices.rs +++ b/src/models/vertices.rs @@ -13,6 +13,14 @@ pub struct VertexPosition { pub value: Vec3, } +impl VertexPosition { + pub fn new(x: f32, y: f32, z: f32) -> Self { + Self { + value: Vec3::new(x, y, z), + } + } +} + impl VertexAttribute for VertexPosition { type Raw = [f32; 3]; fn name() -> &'static str { diff --git a/src/tasks/scheduler.rs b/src/tasks/scheduler.rs index c0ad5891..fb62cc29 100644 --- a/src/tasks/scheduler.rs +++ b/src/tasks/scheduler.rs @@ -158,22 +158,10 @@ pub fn spawn( // log::debug!("current state: {:?}", current_state); if current_state != default_state { if let Some(tasks) = pool.select_for_state(&default_state) { - for task in tasks.iter() { - // log::debug!( - // "tasks for default state: {:?}", - // pool.get(*task).and_then(|t| t.name()) - // ); - } queue.extend_from_slice(tasks); } } if let Some(tasks) = pool.select_for_state(¤t_state) { - for task in tasks.iter() { - // log::debug!( - // "tasks for current state: {:?}", - // pool.get(*task).and_then(|t| t.name()) - // ); - } queue.extend_from_slice(tasks); } diff --git a/src/tasks/worker.rs b/src/tasks/worker.rs index 52f05c67..820bcadf 100644 --- a/src/tasks/worker.rs +++ b/src/tasks/worker.rs @@ -28,7 +28,7 @@ pub fn spawn( break; } // TODO: implement Debug trait for message - _ => println!("{}: message ignored", name), + _ => println!("{name}: message ignored"), }; } }) diff --git a/src/window.rs b/src/window.rs index ce0e99d0..2c7acddc 100644 --- a/src/window.rs +++ b/src/window.rs @@ -167,7 +167,7 @@ impl winit::application::ApplicationHandler for EventLoop { .fps_request(app.fps_request()); scheduler.add_task(create_frame_task); - let submit_frame_task = graphics::SubmitFrame::default(); + let submit_frame_task = graphics::SubmitFrame::new(&display); scheduler.add_task(submit_frame_task); scheduler.add_task(input::ReadInput::default()); @@ -189,16 +189,13 @@ impl winit::application::ApplicationHandler for EventLoop { _device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - match event { - winit::event::DeviceEvent::MouseMotion { delta } => { - let (horizontal, vertical) = delta; - let input_event = event::Event::MouseMove { - horizontal, - vertical, - }; - self.task_manager.provide(input_event); - } - _ => {} + if let winit::event::DeviceEvent::MouseMotion { delta } = event { + let (horizontal, vertical) = delta; + let input_event = event::Event::MouseMove { + horizontal, + vertical, + }; + self.task_manager.provide(input_event); } } diff --git a/src/world.rs b/src/world.rs index 023e21a5..a742df41 100644 --- a/src/world.rs +++ b/src/world.rs @@ -40,6 +40,15 @@ impl World { } } + fn gen_entity_id(&self) -> Id { + loop { + let id = Id::new(); + if !self.index.contains_key(&id) { + return id; + } + } + } + /// Spawn single or multiple entities in the world /// /// Returns number of spawned entities @@ -78,7 +87,7 @@ impl World { /// Returns iterator over entities defined by Query pattern pub fn query<'w, Q>( &'w self, - ) -> impl Iterator::Iter as Iterator>::Item> + 'w + ) -> impl Iterator>::Iter as Iterator>::Item> + 'w where Q: Query<'w>, { @@ -123,7 +132,7 @@ impl World { /// Exiles an entity from the world pub fn exile(&mut self, id: &Id) -> Option { self.index - .get(id) + .remove(id) .map(|index| self.content[index.container].remove(index.address)) } @@ -346,7 +355,7 @@ where type Item = Id; fn next(&mut self) -> Option { self.entries.next().map(|entry| { - let id = Id::::new(); + let id = self.world.gen_entity_id(); let volatile = T::volatile(); let entity = entry.entity().with(id); self.container_index = self @@ -395,18 +404,30 @@ unsafe impl Sync for World {} #[cfg(test)] mod tests { + use crate::{Entity, Id}; + use super::World; #[derive(Debug, Eq, PartialEq, Copy, Clone)] struct Armor(u32); #[derive(Debug, Eq, PartialEq, Copy, Clone)] struct HealthComponent(u32); + #[derive(Debug, Eq, PartialEq, Copy, Clone)] struct SpeedComponent(u32); + #[derive(Debug, Eq, PartialEq, Copy, Clone)] struct DamageComponent(u32); + #[derive(Debug, Eq, PartialEq, Copy, Clone)] struct WeightComponent(u32); fn spawn() -> World { let mut world = World::new(); + + world + .spawn(Some((HealthComponent(80), SpeedComponent(10)))) + .count(); + world + .spawn(Some((SpeedComponent(50), DamageComponent(45)))) + .count(); world .spawn(Some(( Armor(100), @@ -414,12 +435,6 @@ mod tests { DamageComponent(300), ))) .count(); - world - .spawn(Some((HealthComponent(80), SpeedComponent(10)))) - .count(); - world - .spawn(Some((SpeedComponent(50), DamageComponent(45)))) - .count(); world.spawn(Some((DamageComponent(600), Armor(10)))).count(); let bulk = (0..9).map(|_| (SpeedComponent(35), WeightComponent(5000))); @@ -428,6 +443,45 @@ mod tests { world } + #[test] + fn can_spawn_and_exile() { + let mut world = spawn(); + let id_to_exile = { + let mut iter = + world.query::<(&Id, &Armor, &HealthComponent, &DamageComponent)>(); + + let item = iter.next(); + assert!(item.is_some()); + + let mut id_to_exile: Id = Id::null(); + + if let Some((id, _armor, _health, _damage)) = item { + id_to_exile = *id; + } + id_to_exile + }; + assert_ne!(id_to_exile, Id::null()); + + let exiled_entity = world.exile(&id_to_exile); + + assert!(exiled_entity.is_some()); + + let exiled_entity = exiled_entity.unwrap(); + let id = exiled_entity.get::>(); + let armor = exiled_entity.get::(); + let health = exiled_entity.get::(); + let damage = exiled_entity.get::(); + assert_eq!(id.cloned(), Some(id_to_exile)); + assert_eq!(armor.cloned(), Some(Armor(100))); + assert_eq!(health.cloned(), Some(HealthComponent(100))); + assert_eq!(damage.cloned(), Some(DamageComponent(300))); + + let mut iter = world.query::<(&Id, &Armor, &HealthComponent, &DamageComponent)>(); + + let item = iter.next(); + assert!(item.is_none()); + } + #[test] fn can_spawn_and_query() { let world = spawn(); diff --git a/src/world/camera.rs b/src/world/camera.rs index 5f867b4b..00ecc017 100644 --- a/src/world/camera.rs +++ b/src/world/camera.rs @@ -10,12 +10,14 @@ pub struct Camera { pub proj: Mat4, /// View matrix pub view: Mat4, + /// Target point + pub target: Vec3, } impl Camera { /// Constructs new instance of Camera - pub fn new(proj: Mat4, view: Mat4) -> Self { - Self { proj, view } + pub fn new(proj: Mat4, view: Mat4, target: Vec3) -> Self { + Self { proj, view, target } } /// Returns view matrix constructor @@ -56,8 +58,8 @@ impl Lens { impl Default for Lens { fn default() -> Self { Self { - fov: 1.1, // std::f32::consts::FRAC_PI_4 - plane: 0.0625..524288.06, + fov: 2.1, // std::f32::consts::FRAC_PI_4 + plane: 0.0625..5248.06, } } } diff --git a/src/world/storage.rs b/src/world/storage.rs index 073399e7..5a69b415 100644 --- a/src/world/storage.rs +++ b/src/world/storage.rs @@ -30,6 +30,12 @@ impl Entity { self.map.insert(component_type_id, component); } + pub fn get(&self) -> Option<&T> { + self.map + .get(&TypeId::of::()) + .and_then(|v| v.downcast_ref()) + } + pub fn archetype(&self) -> Archetype { Archetype { inner: self.map.keys(), @@ -175,6 +181,7 @@ impl Container { .map(|value| unsafe { (*(value.get())).downcast_ref::().unwrap() }) } + #[allow(clippy::mut_from_ref)] pub unsafe fn get_mut(&self, entity_index: usize) -> Option<&mut C> { self.data .get(&TypeId::of::()) diff --git a/terrain.png b/terrain.png new file mode 100644 index 0000000000000000000000000000000000000000..c4233541d84f3ab6ae77f7dc8f869df00b090176 GIT binary patch literal 168780 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

YLw=aeTN68YS<4&i-?W-{zPr;U5?jf{ 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�Xbs2a@M#!km);)8ab! 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 C(t(@+ literal 0 HcmV?d00001 diff --git a/utils/terrain/main.rs b/utils/terrain/main.rs new file mode 100644 index 00000000..2b77b237 --- /dev/null +++ b/utils/terrain/main.rs @@ -0,0 +1,200 @@ +use dotrix::terrain::{ColorMap, FalloffConfig, HeightMap, NoiseConfig}; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + io::{self, Write}, +}; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +pub enum Command { + Generate, +} + +#[derive(Debug, StructOpt)] +#[structopt(name = "dotrix-terrain")] +pub struct Inputs { + /// Path to .toml configuration file + #[structopt(default_value = "configs/terrain.toml", short, long)] + pub config: String, + + #[structopt(subcommand)] + pub command: Command, +} + +#[derive(Deserialize, Serialize)] +pub struct Config { + name: String, + size: u32, + heightmap: HeightmapSetup, + falloff: Option, + moisture: Option, + colormap: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct HeightmapSetup { + file: String, + falloff: bool, + noise: NoiseConfig, +} + +#[derive(Deserialize, Serialize)] +pub struct FalloffSetup { + file: String, + config: Option, +} + +#[derive(Deserialize, Serialize)] +pub struct MoistureSetup { + file: String, + noise: NoiseConfig, +} + +#[derive(Deserialize, Serialize)] +pub struct ColormapSetup { + file: String, + moisture: bool, + colors: Vec, +} + +fn main() -> Result<(), Box> { + let inputs = Inputs::from_args(); + let config: Config = toml::from_str( + std::fs::read_to_string(std::path::Path::new(inputs.config.as_str()))?.as_str(), + )?; + + // generate + if let Some((file, falloff)) = config.falloff.as_ref().and_then(|setup| { + setup + .config + .as_ref() + .map(|falloff| (setup.file.as_ref(), falloff)) + }) { + generate_falloff(file, config.size, falloff)?; + } + + generate_heightmap(&config)?; + + generate_moisture(&config)?; + + generate_colormap(&config)?; + + Ok(()) +} + +fn generate_falloff(file: &str, size: u32, falloff: &FalloffConfig) -> Result<(), Box> { + print!("Generate falloff map... "); + io::stdout().flush().ok(); + let heightmap = HeightMap::new_from_falloff("heightmap", size, falloff); + let pixels = (0..size) + .flat_map(|z| (0..size).map(move |x| (x, z))) + .map(|(x, z)| (heightmap.value(x, z) * 255.0) as u8) + .collect::>(); + + image::GrayImage::from_raw(size, size, pixels) + .expect("Could not generate heightmap pixels buffers") + .save_with_format(&std::path::Path::new(file), image::ImageFormat::Png)?; + println!("OK"); + Ok(()) +} + +fn generate_heightmap(config: &Config) -> Result<(), Box> { + print!("Generate heightmap... "); + io::stdout().flush().ok(); + let mut heightmap = + HeightMap::new_from_noise("heightmap", config.size, &config.heightmap.noise); + if config.heightmap.falloff { + if let Some(falloff_file) = config.falloff.as_ref().map(|setup| setup.file.as_str()) { + let image = image::io::Reader::open(falloff_file)?.decode()?; + let size = image.width(); + let bytes = image.into_bytes(); + let falloff_heightmap = HeightMap::new_from_bytes("falloff", size, &bytes); + heightmap.subtract(&falloff_heightmap); + } + } + heightmap.write_to_file( + std::path::Path::new(config.heightmap.file.as_str()), + image::ImageFormat::Png, + )?; + println!("OK"); + Ok(()) +} + +fn generate_moisture(config: &Config) -> Result<(), Box> { + if let Some(moisture) = config.moisture.as_ref() { + print!("Generate moisture map... "); + io::stdout().flush().ok(); + let heightmap = HeightMap::new_from_noise("moisturemap", config.size, &moisture.noise); + heightmap.write_to_file( + std::path::Path::new(moisture.file.as_str()), + image::ImageFormat::Png, + )?; + println!("OK"); + } + Ok(()) +} + +fn generate_colormap(config: &Config) -> Result<(), Box> { + if let Some(colormap_config) = config.colormap.as_ref() { + print!("Generate colormap... "); + io::stdout().flush().ok(); + let moisture = if colormap_config.moisture { + config.moisture.as_ref().map(|moisture| { + let image = image::io::Reader::open(moisture.file.as_str()) + .expect("Could not open moisture map") + .decode() + .expect("Could not decode moisture map"); + let size = image.width(); + let bytes = image.into_bytes(); + HeightMap::new_from_bytes("moisturemap", size, &bytes) + }) + } else { + None + }; + let image = image::io::Reader::open(config.heightmap.file.as_str()) + .expect("Could not open heightmap file") + .decode() + .expect("Could not decode heightmap map"); + let size = image.width(); + let bytes = image.into_bytes(); + let heightmap = HeightMap::new_from_bytes("heightmap", size, &bytes); + + let colors = colormap_config + .colors + .iter() + .map(|hex| { + [ + u8::from_str_radix(&hex[0..2], 16) + .expect("Red channel in color must be a valid HEX number"), + u8::from_str_radix(&hex[2..4], 16) + .expect("Green channel in color must be a valid HEX number"), + u8::from_str_radix(&hex[4..6], 16) + .expect("Blue channel in color must be a valid HEX number"), + ] + }) + .collect::>(); + + let colormap = ColorMap::new("colormap", colors, 0.2); + + let pixels = (0..size) + .flat_map(|x| (0..size).map(move |z| (x, z))) + .flat_map(|(x, z)| { + let moisture = moisture + .as_ref() + .map(|moisture| moisture.value(x, z)) + .unwrap_or(0.0); + colormap.color(heightmap.value(x, z), moisture).into_iter() + }) + .collect::>(); + + image::RgbImage::from_raw(size, size, pixels) + .expect("Could not generate colormap pixels buffers") + .save_with_format( + &std::path::Path::new(colormap_config.file.as_str()), + image::ImageFormat::Png, + )?; + println!("OK"); + } + Ok(()) +}