diff --git a/Cargo.toml b/Cargo.toml index 4bbe3869..fa22b5d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,34 +40,44 @@ readme = "docs/README.md" [features] [dependencies] -blade-asset = { version = "0.2.0", path = "blade-asset" } -blade-egui = { version = "0.2.0", path = "blade-egui" } -blade-macros = { version = "0.2.1", path = "blade-macros" } -blade-graphics = { version = "0.3.0", path = "blade-graphics" } -blade-render = { version = "0.2.0", path = "blade-render" } +blade-asset = { version = "0.2", path = "blade-asset" } +blade-egui = { version = "0.2", path = "blade-egui" } +blade-graphics = { version = "0.3", path = "blade-graphics" } +blade-render = { version = "0.2", path = "blade-render" } +choir = { workspace = true } +colorsys = "0.6" +egui = { workspace = true } +nalgebra = { version = "0.32", features = ["mint"] } +log = { workspace = true } +mint = { workspace = true, features = ["serde"] } +num_cpus = "1" +profiling = { workspace = true } +rapier3d = { version = "0.17", features = ["debug-render"] } +serde = { version = "1", features = ["serde_derive"] } +slab = "0.4" +strum = { workspace = true } +winit = "0.28" [dev-dependencies] +blade-macros = { version = "0.2", path = "blade-macros" } bytemuck = { workspace = true } choir = { workspace = true } egui = { workspace = true } -del-msh = "0.1" +egui-gizmo = "0.12" +egui_plot = "0.23" +egui-winit = "0.23" +env_logger = "0.10" +# https://github.com/nobuyuki83/del-msh/issues/1 +del-msh = "=0.1.17" glam = { workspace = true } log = { workspace = true } mint = { workspace = true, features = ["serde"] } naga = { workspace = true } nanorand = { version = "0.7", default-features = false, features = ["wyrand"] } -num_cpus = "1" profiling = { workspace = true } ron = "0.8" serde = { version = "1", features = ["serde_derive"] } strum = { workspace = true } -winit = "0.28" - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -egui-winit = "0.23" -egui_plot = "0.23" -egui-gizmo = "0.12" -env_logger = "0.10" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_error_panic_hook = "0.1.7" diff --git a/blade-asset/README.md b/blade-asset/README.md index c275f52b..c5c29479 100644 --- a/blade-asset/README.md +++ b/blade-asset/README.md @@ -1,5 +1,8 @@ # Blade Asset Pipeline +[![Docs](https://docs.rs/blade-asset/badge.svg)](https://docs.rs/blade-asset) +[![Crates.io](https://img.shields.io/crates/v/blade-asset.svg?maxAge=2592000)](https://crates.io/crates/blade-asset) + This is an abstract library for defininig the pipeline of asset processing. It's tightly integrated with [Choir](https://github.com/kvark/choir) but otherwise doesn't know about GPUs and such. diff --git a/blade-egui/README.md b/blade-egui/README.md new file mode 100644 index 00000000..d6c3e8ec --- /dev/null +++ b/blade-egui/README.md @@ -0,0 +1,20 @@ +# Blade EGUI + +[![Docs](https://docs.rs/blade-egui/badge.svg)](https://docs.rs/blade-egui) +[![Crates.io](https://img.shields.io/crates/v/blade-egui.svg?maxAge=2592000)](https://crates.io/crates/blade-egui) + +[EGUI](https://www.egui.rs/) support for [Blade-graphics](https://crates.io/crates/blade-graphics). + +![scene editor](etc/scene-editor.jpg) + +## Instructions + +Just the usual :crab: workflow. E.g. to run the bunny-mark benchmark run: +```bash +cargo run --release --example bunnymark +``` + +## Platforms + +The full-stack Blade Engine can only run on Vulkan with hardware Ray Tracing support. +However, on secondary platforms, such as Metal and GLES/WebGL2, one can still use Blde-Graphics and Blade-Egui. diff --git a/docs/scene-editor.jpg b/blade-egui/etc/scene-editor.jpg similarity index 100% rename from docs/scene-editor.jpg rename to blade-egui/etc/scene-editor.jpg diff --git a/blade-graphics/README.md b/blade-graphics/README.md new file mode 100644 index 00000000..b2d0ba1b --- /dev/null +++ b/blade-graphics/README.md @@ -0,0 +1,52 @@ +# Blade Graphics + +[![Docs](https://docs.rs/blade-graphics/badge.svg)](https://docs.rs/blade-graphics) +[![Crates.io](https://img.shields.io/crates/v/blade-graphics.svg?maxAge=2592000)](https://crates.io/crates/blade-graphics) + +Blade-graphics is a lean and mean [GPU abstraction](https://youtu.be/63dnzjw4azI?t=623) aimed at ergonomics and fun. See [motivation](etc/motivation.md), [FAQ](etc/FAQ.md), and [performance](etc/performance.md) for details. + +## Examples + +![ray-query example](etc/ray-query.gif) +![particles example](etc/particles.png) + +## Platforms + +The backend is selected automatically based on the host platform: +- *Vulkan* on desktop Linux, Windows, and Android +- *Metal* on desktop macOS, and iOS +- *OpenGL ES3* on the Web + +| Feature | Vulkan | Metal | GLES | +| ------- | ------ | ----- | ---- | +| compute | :white_check_mark: | :white_check_mark: | | +| ray tracing | :white_check_mark: | | | + +### OpenGL ES + +GLES is also supported at a basic level. It's enabled for `wasm32-unknown-unknown` target, and can also be force-enabled on native: +```bash +RUSTFLAGS="--cfg gles" CARGO_TARGET_DIR=./target-gl cargo test +``` + +This path can be activated on all platforms via Angle library. +For example, on macOS it's sufficient to place `libEGL.dylib` and `libGLESv2.dylib` in the working directory. + +### WebGL2 + +Following command will start a web server offering the `bunnymark` example: +```bash +cargo run-wasm --example bunnymark +``` + +### Vulkan Portability + +First, ensure to load the environment from the Vulkan SDK: +```bash +cd /opt/VulkanSDK && source setup-env.sh +``` + +Vulkan backend can be forced on using "vulkan" config flag. Example invocation that produces a vulkan (portability) build into another target folder: +```bash +RUSTFLAGS="--cfg vulkan" CARGO_TARGET_DIR=./target-vk cargo test +``` diff --git a/docs/FAQ.md b/blade-graphics/etc/FAQ.md similarity index 99% rename from docs/FAQ.md rename to blade-graphics/etc/FAQ.md index 1a872162..8e9ea437 100644 --- a/docs/FAQ.md +++ b/blade-graphics/etc/FAQ.md @@ -1,22 +1,22 @@ -# Frequency Asked Questions - -## When should I *not* use Blade? - -- When you *target the Web*. Blade currently has no Web backends supported. Targeting WebGPU is desired, but will not be as performant as native. -- Similarly, when you target the *low-end GPUs* or old drivers. Blade has no OpenGL/D3D11 support, and it requires fresh drivers on Vulkan. -- When you render with 10K or *more draw calls*. State switching has overhead with Blade, and it is lower in GPU abstractions/libraries that have barriers and explicit bind groups. -- When you need something *off the shelf*. Blade is experimental and young, it assumes you'll be customizing it. - -## Why investing into this when there is `wgpu`? - -`wgpu` is becoming a standard solution for GPU access in Rust and beyond. It's wonderful, and by any means just use it if you have any doubts. It's a strong local maxima in a chosen space of low-level portability. It may very well be the global maxima as well, but we don't know this until we explore the *other* local maximas. Blade is an attempt to strike where `wgpu` can't reach, it makes a lot of the opposite design solutions. Try it and see. - -## Isn't this going to be slow? - -Blade creating a descriptor set (in Vulkan) for each draw call. It doesn't care about pipeline compatibility to preserve the bindings. How is this fast? - -Short answer is - yes, it's unlikely going to be faster than wgpu-hal. Long answer is - slow doesn't matter here. - -Take a look at Vulkan [performance](performance.md) numbers. wgpu-hal can get 60K bunnies on a slow machine, which is pretty much the maximum. Both wgpu and blade can reach about 20K. Honestly, if you are relying on 20K unique draw calls being fast, you are in a strange place. Generally, developers should switch to instancing or other batching methods whenever the object count grows above 100, not to mention a 1000. - -Similar reasoning goes to pipeline switches. If you are relying on many pipeline switches done efficiently, then it's good to reconsider your shaders, perhaps turning into the megashader alley a bit. In D3D12, a pipeline change requires all resources to be rebound anyway (and this is what wgpu-hal/dx12 does regardless of the pipeline compatibility), so this is fine in Blade. +# Frequency Asked Questions + +## When should I *not* use Blade? + +- When you *target the Web*. Blade currently has no Web backends supported. Targeting WebGPU is desired, but will not be as performant as native. +- Similarly, when you target the *low-end GPUs* or old drivers. Blade has no OpenGL/D3D11 support, and it requires fresh drivers on Vulkan. +- When you render with 10K or *more draw calls*. State switching has overhead with Blade, and it is lower in GPU abstractions/libraries that have barriers and explicit bind groups. +- When you need something *off the shelf*. Blade is experimental and young, it assumes you'll be customizing it. + +## Why investing into this when there is `wgpu`? + +`wgpu` is becoming a standard solution for GPU access in Rust and beyond. It's wonderful, and by any means just use it if you have any doubts. It's a strong local maxima in a chosen space of low-level portability. It may very well be the global maxima as well, but we don't know this until we explore the *other* local maximas. Blade is an attempt to strike where `wgpu` can't reach, it makes a lot of the opposite design solutions. Try it and see. + +## Isn't this going to be slow? + +Blade creating a descriptor set (in Vulkan) for each draw call. It doesn't care about pipeline compatibility to preserve the bindings. How is this fast? + +Short answer is - yes, it's unlikely going to be faster than wgpu-hal. Long answer is - slow doesn't matter here. + +Take a look at Vulkan [performance](performance.md) numbers. wgpu-hal can get 60K bunnies on a slow machine, which is pretty much the maximum. Both wgpu and blade can reach about 20K. Honestly, if you are relying on 20K unique draw calls being fast, you are in a strange place. Generally, developers should switch to instancing or other batching methods whenever the object count grows above 100, not to mention a 1000. + +Similar reasoning goes to pipeline switches. If you are relying on many pipeline switches done efficiently, then it's good to reconsider your shaders, perhaps turning into the megashader alley a bit. In D3D12, a pipeline change requires all resources to be rebound anyway (and this is what wgpu-hal/dx12 does regardless of the pipeline compatibility), so this is fine in Blade. diff --git a/docs/motivation.md b/blade-graphics/etc/motivation.md similarity index 98% rename from docs/motivation.md rename to blade-graphics/etc/motivation.md index 65eae8ce..55c44f4e 100644 --- a/docs/motivation.md +++ b/blade-graphics/etc/motivation.md @@ -1,70 +1,70 @@ -# Motivation - -## Goal - -Have a layer for graphics programming for those who know what they are doing, and who wants to get the stuff working fast. It's highly opinionated and ergonomic, but also designed specifically for mid to high range hardware and modern APIs. Today, the alternatives are either too high level (engines), too verbose (APIs directly), or just overly general. - -Opinionated means the programming model is very limited. But if something is written against this model, we want to guarantee that it's going to run very efficient, more efficient than any of the more general alternatives would do. - -This is basically a near-perfect graphics layer for myself, which I'd be happy to use on my projects. I hope it can be useful to others, too. - -## Alternatives - -*wgpu* provides the most thorough graphics abstraction in Rust ecosystem. The main API is portable over pretty much all the (open) platforms, including the Web. However, it is very restricted (by being a least common denominator of the platforms), fairly verbose (possible to write against it directly, but not quite convenient), and has overhead (for safety and portability). - -*wgpu-hal* provides an unsafe portable layer, which has virtually no overhead. The point about verbosity still applies. It's possible to write a more ergonomic layer on top of wgpu-hal, but one can't cut the corners embedded in wgpu-hal's design. For example, wgpu-hal expects resource states to be tracked by the user and changed (on a command encoder) explicitly. - -*rafx* attempts to offer a good vertically integrated engine with multiple backends. *rafx* itself is too high level, while *rafx-api* is too low level and verbose. - -*sierra* abstracts over Vulkan. It has great ergonomic features (some expressed via procedural macros). Essentially it has the same problem (for the purpose of fitting our goal) - choice is between low level overly generic API and a high-level one (*arcana*). - -Finally, we don't consider GL-based abstractions, such as *luminance*, since the API is largely outdated. - -# Design - -The API is supposed to be minimal, targeting the capabilities of mid to high range machines on popular platforms. It's also totally unsafe, assuming the developer knows what they are doing. We realy on native API validation to assist developers. - -## Compromises - -*Object lifetime* is explicit, no automatic tracking is done. This is similar to most of the alternatives. - -*Object memory* is automatically allocated based on a few profiles. - -Basic *resources*, such buffers and textures, are small `Copy` structs. - -*Resource states* do not exist. The API is built on an assumption that the driver knows better how to track resource states, and so our API doesn't need to care about this. The only command exposed is a catch-all barrier. - -*Bindings* are pushed directly to command encoders. This is similar to Metal Argument Buffers. There are no descriptor sets or pools. You take a structure and push it to the state. This structure includes any uniform data directly. Changing a pipeline invalidates all bindings, just like in DX12. - -In addition, several features may be added late or not added at all for the sake of keeping everything simple: - - - vertex buffers (use storage buffers instead) - - multisampling (too expensive) - -## Backends - -At first, the API should run on Vulkan and Metal. There is no DX12 support planned. - -On Metal side we want to take advantage of the argument buffers if available. - -On Vulkan we'll require certain features to make the translation simple: - - - [VK_KHR_push_descriptor](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_push_descriptor.html) - - [VK_KHR_descriptor_update_template](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_descriptor_update_template.html) - - [VK_EXT_inline_uniform_block](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_inline_uniform_block.html) - - [VK_KHR_dynamic_rendering](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_dynamic_rendering.html) - -## Assumptions - -Blade is based on different set of assumptions from wgpu-hal: -- *safety*: wgpu places safety first and foremost. Self-sufficient, guarantees no UB. Blade is on the opposite - considers safety to be secondary. Expects users to rely on native API's validation and tooling. -- *API reach*: wgpu attempts to be everywhere, having backends for all the APIs it can reach. Blade targets only the essential backends: Vulkan and Metal. -- *abstraction*: wgpu is completely opaque, with only a few unsafe APIs for interacting with external objects. Blade needs to be transparent, since it assumes modifcation by the user, and doens't provide safety. -- *errors*: wgpu considers all external errors recoverable. Blade doesn't expect any recovery after the initialization is done. -- *object copy*: wgpu-hal hides API objects so that they can only be `Clone`, and some of the backends use `Arc` and other heap-allocated backing for them. Blade keeps the API for resources to be are light as possible and allows them to be copied freely. -- *bind group creation cost*: wgpu considers it expensive, needs to be prepared ahead of time. Blade considers it cheap enough to always create on the fly. -| bind group invalidation | should be avoided by following pipeline compatibility rules | everything is re-bound on pipeline change | -- *barriers*: wgpu attempts to always use the optimal image layouts and can set reduced access flags on resources based on use. Placing the barriers optimally is a non-trivial task to solve, no universal solutions. Blade not only ignores this fight by making the user place the barrier, these barriers are only global, and there are no image layout changes - everything is GENERAL. -- *usage*: wgpu expects to be used as a Rust library. Blade expects to be vendored in and modified according to the needs of a user. Hopefully, some of the changes would appear upstream as PRs. - -In other words, this is a bit **experiment**. It may fail horribly, or it may open up new ideas and perspectives. +# Motivation + +## Goal + +Have a layer for graphics programming for those who know what they are doing, and who wants to get the stuff working fast. It's highly opinionated and ergonomic, but also designed specifically for mid to high range hardware and modern APIs. Today, the alternatives are either too high level (engines), too verbose (APIs directly), or just overly general. + +Opinionated means the programming model is very limited. But if something is written against this model, we want to guarantee that it's going to run very efficient, more efficient than any of the more general alternatives would do. + +This is basically a near-perfect graphics layer for myself, which I'd be happy to use on my projects. I hope it can be useful to others, too. + +## Alternatives + +*wgpu* provides the most thorough graphics abstraction in Rust ecosystem. The main API is portable over pretty much all the (open) platforms, including the Web. However, it is very restricted (by being a least common denominator of the platforms), fairly verbose (possible to write against it directly, but not quite convenient), and has overhead (for safety and portability). + +*wgpu-hal* provides an unsafe portable layer, which has virtually no overhead. The point about verbosity still applies. It's possible to write a more ergonomic layer on top of wgpu-hal, but one can't cut the corners embedded in wgpu-hal's design. For example, wgpu-hal expects resource states to be tracked by the user and changed (on a command encoder) explicitly. + +*rafx* attempts to offer a good vertically integrated engine with multiple backends. *rafx* itself is too high level, while *rafx-api* is too low level and verbose. + +*sierra* abstracts over Vulkan. It has great ergonomic features (some expressed via procedural macros). Essentially it has the same problem (for the purpose of fitting our goal) - choice is between low level overly generic API and a high-level one (*arcana*). + +Finally, we don't consider GL-based abstractions, such as *luminance*, since the API is largely outdated. + +# Design + +The API is supposed to be minimal, targeting the capabilities of mid to high range machines on popular platforms. It's also totally unsafe, assuming the developer knows what they are doing. We realy on native API validation to assist developers. + +## Compromises + +*Object lifetime* is explicit, no automatic tracking is done. This is similar to most of the alternatives. + +*Object memory* is automatically allocated based on a few profiles. + +Basic *resources*, such buffers and textures, are small `Copy` structs. + +*Resource states* do not exist. The API is built on an assumption that the driver knows better how to track resource states, and so our API doesn't need to care about this. The only command exposed is a catch-all barrier. + +*Bindings* are pushed directly to command encoders. This is similar to Metal Argument Buffers. There are no descriptor sets or pools. You take a structure and push it to the state. This structure includes any uniform data directly. Changing a pipeline invalidates all bindings, just like in DX12. + +In addition, several features may be added late or not added at all for the sake of keeping everything simple: + + - vertex buffers (use storage buffers instead) + - multisampling (too expensive) + +## Backends + +At first, the API should run on Vulkan and Metal. There is no DX12 support planned. + +On Metal side we want to take advantage of the argument buffers if available. + +On Vulkan we'll require certain features to make the translation simple: + + - [VK_KHR_push_descriptor](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_push_descriptor.html) + - [VK_KHR_descriptor_update_template](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_descriptor_update_template.html) + - [VK_EXT_inline_uniform_block](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_inline_uniform_block.html) + - [VK_KHR_dynamic_rendering](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_KHR_dynamic_rendering.html) + +## Assumptions + +Blade is based on different set of assumptions from wgpu-hal: +- *safety*: wgpu places safety first and foremost. Self-sufficient, guarantees no UB. Blade is on the opposite - considers safety to be secondary. Expects users to rely on native API's validation and tooling. +- *API reach*: wgpu attempts to be everywhere, having backends for all the APIs it can reach. Blade targets only the essential backends: Vulkan and Metal. +- *abstraction*: wgpu is completely opaque, with only a few unsafe APIs for interacting with external objects. Blade needs to be transparent, since it assumes modifcation by the user, and doens't provide safety. +- *errors*: wgpu considers all external errors recoverable. Blade doesn't expect any recovery after the initialization is done. +- *object copy*: wgpu-hal hides API objects so that they can only be `Clone`, and some of the backends use `Arc` and other heap-allocated backing for them. Blade keeps the API for resources to be are light as possible and allows them to be copied freely. +- *bind group creation cost*: wgpu considers it expensive, needs to be prepared ahead of time. Blade considers it cheap enough to always create on the fly. +| bind group invalidation | should be avoided by following pipeline compatibility rules | everything is re-bound on pipeline change | +- *barriers*: wgpu attempts to always use the optimal image layouts and can set reduced access flags on resources based on use. Placing the barriers optimally is a non-trivial task to solve, no universal solutions. Blade not only ignores this fight by making the user place the barrier, these barriers are only global, and there are no image layout changes - everything is GENERAL. +- *usage*: wgpu expects to be used as a Rust library. Blade expects to be vendored in and modified according to the needs of a user. Hopefully, some of the changes would appear upstream as PRs. + +In other words, this is a bit **experiment**. It may fail horribly, or it may open up new ideas and perspectives. diff --git a/docs/particles.png b/blade-graphics/etc/particles.png similarity index 100% rename from docs/particles.png rename to blade-graphics/etc/particles.png diff --git a/docs/performance.md b/blade-graphics/etc/performance.md similarity index 96% rename from docs/performance.md rename to blade-graphics/etc/performance.md index ec18f919..75c0f459 100644 --- a/docs/performance.md +++ b/blade-graphics/etc/performance.md @@ -1,43 +1,43 @@ -# Performance - -Blade doesn't expect to be faster than wgpu-hal, but it's important to understand how much the difference is. Testing is done on "bunnymark" example, which is ported from wgpu. Since every draw call is dynamic in Blade, this benchmark is the worst case of the usage. - -## MacBook Pro 2016 - -GPU: Intel Iris Graphics 550 - -Metal: - - Blade starts to slow down after about 23K bunnies - - wgpu-hal starts at 60K bunnies - - wgpu starts at 15K bunnies - -Vulkan Portability: - - Blade starts to slow down at around 18K bunnies - -## Thinkpad T495s - -GPU: Ryzen 3500U - -Windows/Vulkan: - - Blade starts at around 18K bunnies - - wgpu-hal starts at 60K bunnies - - wgpu starts at 20K bunnies - -## Thinkpad Z13 gen1 - -GPU: Ryzen 6850U - -Windows/Vulkan: - - Blade starts at around 50K bunnies - - wgpu-hal starts at 50K bunnies (also GPU-limited) - - wgpu starts at around 15K bunnies - -## Conclusions - -Amazingly, Blade performance in the worst case scenario is on par with wgpu (but still far from wgpu-hal). This is the best outcome we could hope for. - -As expected, Vulkan path on macOS via MoltenVK is slower than the native Metal backend. - -Ergonomically, our example is 335 LOC versus 830 LOC of wgpu-hal and 370-750 LOC in wgpu (depending on how we count the example framework). - -It's also closer to the hardware (than even wgpu-hal) and easier to debug. +# Performance + +Blade doesn't expect to be faster than wgpu-hal, but it's important to understand how much the difference is. Testing is done on "bunnymark" example, which is ported from wgpu. Since every draw call is dynamic in Blade, this benchmark is the worst case of the usage. + +## MacBook Pro 2016 + +GPU: Intel Iris Graphics 550 + +Metal: + - Blade starts to slow down after about 23K bunnies + - wgpu-hal starts at 60K bunnies + - wgpu starts at 15K bunnies + +Vulkan Portability: + - Blade starts to slow down at around 18K bunnies + +## Thinkpad T495s + +GPU: Ryzen 3500U + +Windows/Vulkan: + - Blade starts at around 18K bunnies + - wgpu-hal starts at 60K bunnies + - wgpu starts at 20K bunnies + +## Thinkpad Z13 gen1 + +GPU: Ryzen 6850U + +Windows/Vulkan: + - Blade starts at around 50K bunnies + - wgpu-hal starts at 50K bunnies (also GPU-limited) + - wgpu starts at around 15K bunnies + +## Conclusions + +Amazingly, Blade performance in the worst case scenario is on par with wgpu (but still far from wgpu-hal). This is the best outcome we could hope for. + +As expected, Vulkan path on macOS via MoltenVK is slower than the native Metal backend. + +Ergonomically, our example is 335 LOC versus 830 LOC of wgpu-hal and 370-750 LOC in wgpu (depending on how we count the example framework). + +It's also closer to the hardware (than even wgpu-hal) and easier to debug. diff --git a/docs/ray-query.gif b/blade-graphics/etc/ray-query.gif similarity index 100% rename from docs/ray-query.gif rename to blade-graphics/etc/ray-query.gif diff --git a/blade-render/README.md b/blade-render/README.md new file mode 100644 index 00000000..6dc79ba5 --- /dev/null +++ b/blade-render/README.md @@ -0,0 +1,13 @@ +# Blade Render + +[![Docs](https://docs.rs/blade-render/badge.svg)](https://docs.rs/blade-render) +[![Crates.io](https://img.shields.io/crates/v/blade-render.svg?maxAge=2592000)](https://crates.io/crates/blade-render) + +Ray-traced renderer based on [blade-graphics](https://crates.io/crates/blade-graphics) and [blade-asset](https://crates.io/crates/blade-asset). + +![sponza scene](etc/sponza.jpg) + +## Platforms + +Only Vulkan with hardware Ray Tracing is currently supported. +In the future, we should be able to run on Metal as well. diff --git a/docs/sponza.jpg b/blade-render/etc/sponza.jpg similarity index 100% rename from docs/sponza.jpg rename to blade-render/etc/sponza.jpg diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 981adc83..8864c27b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,7 @@ Changelog for Blade -## (TBD) +## blade-0.2 (TBD) +- high-level engine - support object motion ## blade-graphics-0.3, blade-render-0.2 (17 Nov 2023) diff --git a/docs/README.md b/docs/README.md index e095a314..530f37a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,66 +1,32 @@ -# Blade - -[![Matrix](https://img.shields.io/static/v1?label=dev&message=%23blade&color=blueviolet&logo=matrix)](https://matrix.to/#/#blade-dev:matrix.org) -[![Build Status](https://github.com/kvark/blade/workflows/check/badge.svg)](https://github.com/kvark/blade/actions) -[![Docs](https://docs.rs/blade/badge.svg)](https://docs.rs/blade) -[![Crates.io](https://img.shields.io/crates/v/blade-graphics.svg?maxAge=2592000)](https://crates.io/crates/blade-graphics) -[![Crates.io](https://img.shields.io/crates/v/blade-render.svg?maxAge=2592000)](https://crates.io/crates/blade-render) - -![](logo.png) - -Blade is an innovative rendering solution for Rust. It starts with an ergonomic [low-level GPU abstraction](https://youtu.be/63dnzjw4azI?t=623), which is actually fun to play with. It then grows into a high-level rendering library that utilizes hardware ray-tracing. Finally, a [task-parallel asset pipeline](https://youtu.be/1DiA3OYqvqU) together with [egui](https://www.egui.rs/) support turn it into a minimal rendering engine. - -See [motivation](motivation.md), [FAQ](FAQ.md), and [performance](performance.md) for more info about the low-level API. - -![sponza scene](sponza.jpg) -![ray-query example](ray-query.gif) -![scene editor](scene-editor.jpg) -![particles example](particles.png) - -## Instructions - -Just the usual :crab: workflow. E.g. to run the bunny-mark benchmark run: -```bash -cargo run --release --example bunnymark -``` - -## Platforms - -The backend is selected automatically based on the host platform: -- *Vulkan* on desktop Linux, Windows, and Android -- *Metal* on desktop macOS, and iOS -- *OpenGL ES3* on the Web - -| Feature | Vulkan | Metal | GLES | -| ------- | ------ | ----- | ---- | -| compute | :white_check_mark: | :white_check_mark: | | -| ray tracing | :white_check_mark: | | | - -### OpenGL ES - -GLES is also supported at a basic level. It's enabled for `wasm32-unknown-unknown` target, and can also be force-enabled on native: -```bash -RUSTFLAGS="--cfg gles" CARGO_TARGET_DIR=./target-gl cargo test -``` - -This path can be activated on all platforms via Angle library. -For example, on macOS it's sufficient to place `libEGL.dylib` and `libGLESv2.dylib` in the working directory. - -### WebGL2 - -Following command will start a web server offering the `bunnymark` example: -```bash -cargo run-wasm --example bunnymark -``` - -### Vulkan Portability - -First, ensure to load the environment from the Vulkan SDK: -```bash -cd /opt/VulkanSDK && source setup-env.sh -``` - -Vulkan backend can be forced on using "vulkan" config flag. Example invocation that produces a vulkan (portability) build into another target folder: -```bash -RUSTFLAGS="--cfg vulkan" CARGO_TARGET_DIR=./target-vk cargo test -``` +# Blade + +[![Matrix](https://img.shields.io/static/v1?label=dev&message=%23blade&color=blueviolet&logo=matrix)](https://matrix.to/#/#blade-dev:matrix.org) +[![Build Status](https://github.com/kvark/blade/workflows/check/badge.svg)](https://github.com/kvark/blade/actions) +[![Docs](https://docs.rs/blade/badge.svg)](https://docs.rs/blade) +[![Crates.io](https://img.shields.io/crates/v/blade.svg?label=blade)](https://crates.io/crates/blade) +[![Crates.io](https://img.shields.io/crates/v/blade-graphics.svg?label=blade-graphics)](https://crates.io/crates/blade-graphics) +[![Crates.io](https://img.shields.io/crates/v/blade-render.svg?label=blade-render)](https://crates.io/crates/blade-render) + +![](logo.png) + +Blade is an innovative rendering solution for Rust. It starts with a lean [low-level GPU abstraction](https://youtu.be/63dnzjw4azI?t=623) focused at ergonomics and fun. It then grows into a high-level rendering library that utilizes hardware ray-tracing. Finally, a [task-parallel asset pipeline](https://youtu.be/1DiA3OYqvqU) together with [egui](https://www.egui.rs/) support turn it into a minimal rendering engine. + +![architecture](architecture.png) + +## Examples + +![scene editor](../blade-egui/etc/scene-editor.jpg) +![particles example](../blade-graphics/etc/particles.png) +![sponza scene](../blade-render/etc/sponza.jpg) + +## Instructions + +Just the usual :crab: workflow. E.g. to run the bunny-mark benchmark run: +```bash +cargo run --release --example bunnymark +``` + +## Platforms + +The full-stack Blade Engine can only run on Vulkan with hardware Ray Tracing support. +However, on secondary platforms, such as Metal and GLES/WebGL2, one can still use Blde-Graphics and Blade-Egui. diff --git a/docs/architecture2.png b/docs/architecture2.png new file mode 100644 index 00000000..dfd6a8df Binary files /dev/null and b/docs/architecture2.png differ diff --git a/examples/bunnymark/main.rs b/examples/bunnymark/main.rs index f8cf1835..2d762d10 100644 --- a/examples/bunnymark/main.rs +++ b/examples/bunnymark/main.rs @@ -1,6 +1,6 @@ #![allow(irrefutable_let_patterns)] -use blade::graphics as gpu; +use blade_graphics as gpu; use bytemuck::{Pod, Zeroable}; use std::ptr; @@ -16,7 +16,7 @@ struct Globals { pad: [f32; 2], } -#[derive(blade::macros::ShaderData)] +#[derive(blade_macros::ShaderData)] struct Params { globals: Globals, sprite_texture: gpu::TextureView, @@ -32,7 +32,7 @@ struct Locals { pad: u32, } -#[derive(blade::macros::ShaderData)] +#[derive(blade_macros::ShaderData)] struct Sprite { locals: Locals, } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..a2a85254 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,57 @@ +fn default_vec() -> mint::Vector3 { + [0.0; 3].into() +} +fn default_scale() -> f32 { + 1.0 +} + +#[derive(serde::Deserialize)] +pub struct Visual { + pub model: String, + #[serde(default = "default_vec")] + pub pos: mint::Vector3, + #[serde(default = "default_vec")] + pub rot: mint::Vector3, + #[serde(default = "default_scale")] + pub scale: f32, +} +impl Default for Visual { + fn default() -> Self { + Self { + model: String::new(), + pos: default_vec(), + rot: default_vec(), + scale: default_scale(), + } + } +} + +#[derive(serde::Deserialize)] +pub enum Shape { + Ball { radius: f32 }, + Cylinder { half_height: f32, radius: f32 }, + Cuboid { half: mint::Vector3 }, + ConvexHull { points: Vec> }, +} + +#[derive(serde::Deserialize)] +pub struct Collider { + pub mass: f32, + pub shape: Shape, + #[serde(default = "default_vec")] + pub pos: mint::Vector3, + #[serde(default = "default_vec")] + pub rot: mint::Vector3, +} + +#[derive(serde::Deserialize)] +pub struct Object { + pub name: String, + pub visuals: Vec, + pub colliders: Vec, +} + +#[derive(serde::Deserialize)] +pub struct Engine { + pub shader_path: String, +} diff --git a/src/lib.rs b/src/lib.rs index b8b21b4b..6fd373c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,665 @@ -pub use blade_asset as asset; -pub use blade_graphics as graphics; -pub use blade_macros as macros; -pub use blade_render as render; +#![cfg(not(target_arch = "wasm32"))] +#![allow( + irrefutable_let_patterns, + clippy::new_without_default, + // Conflicts with `pattern_type_mismatch` + clippy::needless_borrowed_reference, +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_extern_crates, + unused_qualifications, + // We don't match on a reference, unless required. + clippy::pattern_type_mismatch, +)] + +use blade_graphics as gpu; +use std::{path::Path, sync::Arc}; + +pub mod config; + +//TODO: hide Rapier3D as a private dependency +pub use rapier3d::dynamics::ImpulseJointHandle as JointHandle; +pub use rapier3d::dynamics::RigidBodyType as BodyType; + +const MAX_DEPTH: f32 = 1e9; + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)] +pub struct ObjectHandle(usize); + +fn make_quaternion(degrees: mint::Vector3) -> nalgebra::geometry::UnitQuaternion { + nalgebra::geometry::UnitQuaternion::from_euler_angles( + degrees.x.to_radians(), + degrees.y.to_radians(), + degrees.z.to_radians(), + ) +} + +trait UiValue { + fn value(&mut self, v: String); +} +impl UiValue for egui::Ui { + fn value(&mut self, v: String) { + self.label( + egui::RichText::new(v) + .italics() + .background_color(egui::Color32::BLACK), + ); + } +} + +#[derive(Default)] +struct DebugPhysicsRender { + lines: Vec, +} +impl rapier3d::pipeline::DebugRenderBackend for DebugPhysicsRender { + fn draw_line( + &mut self, + _object: rapier3d::pipeline::DebugRenderObject, + a: nalgebra::Point3, + b: nalgebra::Point3, + color: [f32; 4], + ) { + // Looks like everybody encodes HSL(A) differently... + let hsl = colorsys::Hsl::new( + color[0] as f64, + color[1] as f64 * 100.0, + color[2] as f64 * 100.0, + None, + ); + let rgb = colorsys::Rgb::from(&hsl); + let color = [ + rgb.red(), + rgb.green(), + rgb.blue(), + color[3].clamp(0.0, 1.0) as f64 * 255.0, + ] + .iter() + .rev() + .fold(0u32, |u, &c| (u << 8) | c as u32); + self.lines.push(blade_render::DebugLine { + a: blade_render::DebugPoint { + pos: a.into(), + color, + }, + b: blade_render::DebugPoint { + pos: b.into(), + color, + }, + }); + } +} + +#[derive(Default)] +struct Physics { + rigid_bodies: rapier3d::dynamics::RigidBodySet, + integration_params: rapier3d::dynamics::IntegrationParameters, + island_manager: rapier3d::dynamics::IslandManager, + impulse_joints: rapier3d::dynamics::ImpulseJointSet, + multibody_joints: rapier3d::dynamics::MultibodyJointSet, + solver: rapier3d::dynamics::CCDSolver, + colliders: rapier3d::geometry::ColliderSet, + broad_phase: rapier3d::geometry::BroadPhase, + narrow_phase: rapier3d::geometry::NarrowPhase, + gravity: rapier3d::math::Vector, + pipeline: rapier3d::pipeline::PhysicsPipeline, + debug_pipeline: rapier3d::pipeline::DebugRenderPipeline, +} + +impl Physics { + fn step(&mut self, dt: f32) { + self.integration_params.dt = dt; + let physics_hooks = (); + let event_handler = (); + self.pipeline.step( + &self.gravity, + &self.integration_params, + &mut self.island_manager, + &mut self.broad_phase, + &mut self.narrow_phase, + &mut self.rigid_bodies, + &mut self.colliders, + &mut self.impulse_joints, + &mut self.multibody_joints, + &mut self.solver, + None, // query pipeline + &physics_hooks, + &event_handler, + ); + } + fn render_debug(&mut self) -> Vec { + let mut backend = DebugPhysicsRender::default(); + self.debug_pipeline.render( + &mut backend, + &self.rigid_bodies, + &self.colliders, + &self.impulse_joints, + &self.multibody_joints, + &self.narrow_phase, + ); + backend.lines + } +} + +struct Visual { + model: blade_asset::Handle, + similarity: nalgebra::geometry::Similarity3, +} + +struct Object { + name: String, + rigid_body: rapier3d::dynamics::RigidBodyHandle, + prev_isometry: nalgebra::Isometry3, + _colliders: Vec, + visuals: Vec, +} + +pub struct Camera { + pub isometry: nalgebra::Isometry3, + pub fov_y: f32, +} + +/// Blade Engine encapsulates all the context for applications, +/// such as the GPU context, Ray-tracing context, EGUI integration, +/// asset hub, physics context, task processing, and more. +pub struct Engine { + pacer: blade_render::util::FramePacer, + renderer: blade_render::Renderer, + physics: Physics, + load_tasks: Vec, + gui_painter: blade_egui::GuiPainter, + asset_hub: blade_render::AssetHub, + gpu_context: Arc, + environment_map: Option>, + objects: slab::Slab, + selected_object_index: Option, + render_objects: Vec, + debug: blade_render::DebugConfig, + need_accumulation_reset: bool, + is_debug_drawing: bool, + ray_config: blade_render::RayConfig, + denoiser_enabled: bool, + denoiser_config: blade_render::DenoiserConfig, + post_proc_config: blade_render::PostProcConfig, + track_hot_reloads: bool, + workers: Vec, + choir: Arc, +} + +impl Engine { + fn make_surface_config(physical_size: winit::dpi::PhysicalSize) -> gpu::SurfaceConfig { + gpu::SurfaceConfig { + size: gpu::Extent { + width: physical_size.width, + height: physical_size.height, + depth: 1, + }, + usage: gpu::TextureUsage::TARGET, + frame_count: 3, + } + } + + /// Create a new context based on a given window. + #[profiling::function] + pub fn new(window: &winit::window::Window, config: &config::Engine) -> Self { + log::info!("Initializing the engine"); + + let gpu_context = Arc::new(unsafe { + gpu::Context::init_windowed( + window, + gpu::ContextDesc { + validation: cfg!(debug_assertions), + capture: false, + }, + ) + .unwrap() + }); + + let surface_config = Self::make_surface_config(window.inner_size()); + let screen_size = surface_config.size; + let surface_format = gpu_context.resize(surface_config); + + let num_workers = num_cpus::get_physical().max((num_cpus::get() * 3 + 2) / 4); + log::info!("Initializing Choir with {} workers", num_workers); + let choir = choir::Choir::new(); + let workers = (0..num_workers) + .map(|i| choir.add_worker(&format!("Worker-{}", i))) + .collect(); + + let asset_hub = blade_render::AssetHub::new(Path::new("asset-cache"), &choir, &gpu_context); + let (shaders, shader_task) = + blade_render::Shaders::load(config.shader_path.as_ref(), &asset_hub); + + log::info!("Spinning up the renderer"); + shader_task.join(); + let mut pacer = blade_render::util::FramePacer::new(&gpu_context); + let (command_encoder, _) = pacer.begin_frame(); + + let render_config = blade_render::RenderConfig { + screen_size, + surface_format, + max_debug_lines: 1000, + }; + let renderer = blade_render::Renderer::new( + command_encoder, + &gpu_context, + shaders, + &asset_hub.shaders, + &render_config, + ); + + pacer.end_frame(&gpu_context); + + let gui_painter = blade_egui::GuiPainter::new(surface_format, &gpu_context); + let mut physics = Physics::default(); + physics.debug_pipeline.mode = rapier3d::pipeline::DebugRenderMode::empty(); + + Self { + pacer, + renderer, + physics, + load_tasks: Vec::new(), + gui_painter, + asset_hub, + gpu_context, + environment_map: None, + objects: slab::Slab::new(), + selected_object_index: None, + render_objects: Vec::new(), + debug: blade_render::DebugConfig::default(), + need_accumulation_reset: true, + is_debug_drawing: false, + ray_config: blade_render::RayConfig { + num_environment_samples: 1, + environment_importance_sampling: false, + temporal_history: 10, + spatial_taps: 1, + spatial_tap_history: 5, + spatial_radius: 10, + }, + denoiser_enabled: true, + denoiser_config: blade_render::DenoiserConfig { + num_passes: 4, + temporal_weight: 0.1, + }, + post_proc_config: blade_render::PostProcConfig { + average_luminocity: 1.0, + exposure_key_value: 1.0 / 9.6, + white_level: 1.0, + }, + track_hot_reloads: false, + workers, + choir, + } + } + + pub fn destroy(&mut self) { + self.workers.clear(); + self.pacer.destroy(&self.gpu_context); + self.gui_painter.destroy(&self.gpu_context); + self.renderer.destroy(&self.gpu_context); + self.asset_hub.destroy(); + } + + #[profiling::function] + pub fn update(&mut self, dt: f32) { + self.choir.check_panic(); + self.physics.step(dt); + } + + #[profiling::function] + pub fn render( + &mut self, + camera: &Camera, + gui_primitives: &[egui::ClippedPrimitive], + gui_textures: &egui::TexturesDelta, + physical_size: winit::dpi::PhysicalSize, + scale_factor: f32, + ) { + if self.track_hot_reloads { + self.need_accumulation_reset |= self.renderer.hot_reload( + &self.asset_hub, + &self.gpu_context, + self.pacer.last_sync_point().unwrap(), + ); + } + + // Note: the resize is split in 2 parts because `wait_for_previous_frame` + // wants to borrow `self` mutably, and `command_encoder` blocks that. + let surface_config = Self::make_surface_config(physical_size); + let new_render_size = surface_config.size; + if new_render_size != self.renderer.get_screen_size() { + log::info!("Resizing to {}", new_render_size); + self.pacer.wait_for_previous_frame(&self.gpu_context); + self.gpu_context.resize(surface_config); + } + + let (command_encoder, temp) = self.pacer.begin_frame(); + if new_render_size != self.renderer.get_screen_size() { + self.renderer + .resize_screen(new_render_size, command_encoder, &self.gpu_context); + self.need_accumulation_reset = true; + } + + self.gui_painter + .update_textures(command_encoder, gui_textures, &self.gpu_context); + + self.asset_hub.flush(command_encoder, &mut temp.buffers); + + self.load_tasks.retain(|task| !task.is_done()); + + // We should be able to update TLAS and render content + // even while it's still being loaded. + if self.load_tasks.is_empty() { + self.render_objects.clear(); + for (_, object) in self.objects.iter_mut() { + let isometry = self + .physics + .rigid_bodies + .get(object.rigid_body) + .unwrap() + .position(); + for visual in object.visuals.iter() { + let mc = (isometry * visual.similarity).to_homogeneous().transpose(); + let mp = (object.prev_isometry * visual.similarity) + .to_homogeneous() + .transpose(); + self.render_objects.push(blade_render::Object { + transform: gpu::Transform { + x: mc.column(0).into(), + y: mc.column(1).into(), + z: mc.column(2).into(), + }, + prev_transform: gpu::Transform { + x: mp.column(0).into(), + y: mp.column(1).into(), + z: mp.column(2).into(), + }, + model: visual.model, + }); + } + object.prev_isometry = *isometry; + } + + // Rebuilding every frame + self.renderer.build_scene( + command_encoder, + &self.render_objects, + self.environment_map, + &self.asset_hub, + &self.gpu_context, + temp, + ); + + self.renderer.prepare( + command_encoder, + &blade_render::Camera { + pos: camera.isometry.translation.vector.into(), + rot: camera.isometry.rotation.into(), + fov_y: camera.fov_y, + depth: MAX_DEPTH, + }, + self.is_debug_drawing, + self.debug.mouse_pos.is_some(), + self.need_accumulation_reset, + ); + self.need_accumulation_reset = false; + + if !self.render_objects.is_empty() { + self.renderer + .ray_trace(command_encoder, self.debug, self.ray_config); + if self.denoiser_enabled { + self.renderer.denoise(command_encoder, self.denoiser_config); + } + } + } + + let debug_lines = self.physics.render_debug(); + + let frame = self.gpu_context.acquire_frame(); + command_encoder.init_texture(frame.texture()); + + if let mut pass = command_encoder.render(gpu::RenderTargetSet { + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::Store, + }], + depth_stencil: None, + }) { + let screen_desc = blade_egui::ScreenDescriptor { + physical_size: (physical_size.width, physical_size.height), + scale_factor, + }; + if self.load_tasks.is_empty() { + self.renderer.post_proc( + &mut pass, + self.debug, + self.post_proc_config, + &debug_lines, + &[], + ); + } + self.gui_painter + .paint(&mut pass, gui_primitives, &screen_desc, &self.gpu_context); + } + + command_encoder.present(frame); + let sync_point = self.pacer.end_frame(&self.gpu_context); + self.gui_painter.after_submit(sync_point); + + profiling::finish_frame!(); + } + + #[profiling::function] + pub fn populate_hud(&mut self, ui: &mut egui::Ui) { + use strum::IntoEnumIterator as _; + egui::CollapsingHeader::new("Rendering") + .default_open(true) + .show(ui, |ui| { + ui.checkbox(&mut self.denoiser_enabled, "Enable Denoiser"); + egui::ComboBox::from_label("Debug mode") + .selected_text(format!("{:?}", self.debug.view_mode)) + .show_ui(ui, |ui| { + for value in blade_render::DebugMode::iter() { + ui.selectable_value( + &mut self.debug.view_mode, + value, + format!("{value:?}"), + ); + } + }); + }); + egui::CollapsingHeader::new("Visualize") + .default_open(true) + .show(ui, |ui| { + let all_bits = rapier3d::pipeline::DebugRenderMode::all().bits(); + for bit_pos in 0..=all_bits.ilog2() { + let flag = match rapier3d::pipeline::DebugRenderMode::from_bits(1 << bit_pos) { + Some(flag) => flag, + None => continue, + }; + let mut enabled = self.physics.debug_pipeline.mode.contains(flag); + ui.checkbox(&mut enabled, format!("{flag:?}")); + self.physics.debug_pipeline.mode.set(flag, enabled); + } + }); + egui::CollapsingHeader::new("Objects") + .default_open(true) + .show(ui, |ui| { + for (handle, object) in self.objects.iter() { + ui.selectable_value( + &mut self.selected_object_index, + Some(ObjectHandle(handle)), + &object.name, + ); + } + }); + if let Some(handle) = self.selected_object_index { + let object = &self.objects[handle.0]; + let rigid_body = &self.physics.rigid_bodies[object.rigid_body]; + let t = rigid_body.translation(); + ui.horizontal(|ui| { + ui.label(format!("Position:")); + ui.value(format!("{:.1}, {:.1}, {:.1}", t.x, t.y, t.z)); + }); + ui.horizontal(|ui| { + ui.label(format!("Linear damping:")); + ui.value(format!("{:.1}", rigid_body.linear_damping())); + }); + ui.horizontal(|ui| { + ui.label(format!("Linvel:")); + let v = rigid_body.linvel(); + ui.value(format!("{:.1}, {:.1}, {:.1}", v.x, v.y, v.z)); + }); + ui.horizontal(|ui| { + ui.label(format!("Angular damping:")); + ui.value(format!("{:.1}", rigid_body.angular_damping())); + }); + ui.horizontal(|ui| { + ui.label(format!("Angvel:")); + let v = rigid_body.angvel(); + ui.value(format!("{:.1}, {:.1}, {:.1}", v.x, v.y, v.z)); + }); + ui.horizontal(|ui| { + ui.label(format!("Kinematic energy:")); + ui.value(format!("{:.1}", rigid_body.kinetic_energy())); + }); + } + } + + pub fn screen_aspect(&self) -> f32 { + let size = self.renderer.get_screen_size(); + size.width as f32 / size.height.max(1) as f32 + } + + pub fn add_object( + &mut self, + config: &config::Object, + isometry: nalgebra::Isometry3, + body_type: BodyType, + ) -> ObjectHandle { + let mut visuals = Vec::new(); + for visual in config.visuals.iter() { + let (model, task) = self.asset_hub.models.load( + format!("data/{}", visual.model), + blade_render::model::Meta { + generate_tangents: true, + }, + ); + visuals.push(Visual { + model, + similarity: nalgebra::geometry::Similarity3::from_parts( + nalgebra::Vector3::from(visual.pos).into(), + make_quaternion(visual.rot), + visual.scale, + ), + }); + self.load_tasks.push(task.clone()); + } + + let rigid_body = rapier3d::dynamics::RigidBodyBuilder::new(body_type) + .position(isometry) + .build(); + let rb_handle = self.physics.rigid_bodies.insert(rigid_body); + + let mut colliders = Vec::new(); + for cc in config.colliders.iter() { + let isometry = nalgebra::geometry::Isometry3::from_parts( + nalgebra::Vector3::from(cc.pos).into(), + make_quaternion(cc.rot), + ); + let builder = match cc.shape { + config::Shape::Ball { radius } => rapier3d::geometry::ColliderBuilder::ball(radius), + config::Shape::Cylinder { + half_height, + radius, + } => rapier3d::geometry::ColliderBuilder::cylinder(half_height, radius), + config::Shape::Cuboid { half } => { + rapier3d::geometry::ColliderBuilder::cuboid(half.x, half.y, half.z) + } + config::Shape::ConvexHull { ref points } => { + let pv = points + .iter() + .map(|p| nalgebra::Vector3::from(*p).into()) + .collect::>(); + rapier3d::geometry::ColliderBuilder::convex_hull(&pv) + .expect("Unable to build convex full") + } + }; + let collider = builder.mass(cc.mass).position(isometry).build(); + let c_handle = self.physics.colliders.insert_with_parent( + collider, + rb_handle, + &mut self.physics.rigid_bodies, + ); + colliders.push(c_handle); + } + + let raw_handle = self.objects.insert(Object { + name: config.name.clone(), + rigid_body: rb_handle, + prev_isometry: nalgebra::Isometry3::default(), + _colliders: colliders, + visuals, + }); + ObjectHandle(raw_handle) + } + + pub fn add_joint( + &mut self, + a: ObjectHandle, + b: ObjectHandle, + data: impl Into, + ) -> JointHandle { + self.physics.impulse_joints.insert( + self.objects[a.0].rigid_body, + self.objects[b.0].rigid_body, + data, + true, + ) + } + + pub fn get_joint_mut(&mut self, handle: JointHandle) -> &mut rapier3d::dynamics::ImpulseJoint { + self.physics.impulse_joints.get_mut(handle).unwrap() + } + + pub fn get_object_isometry(&self, handle: ObjectHandle) -> &nalgebra::Isometry3 { + let object = &self.objects[handle.0]; + let body = &self.physics.rigid_bodies[object.rigid_body]; + body.position() + } + + pub fn apply_impulse(&mut self, handle: ObjectHandle, impulse: nalgebra::Vector3) { + let object = &self.objects[handle.0]; + let body = &mut self.physics.rigid_bodies[object.rigid_body]; + body.apply_impulse(impulse, false) + } + + pub fn set_environment_map(&mut self, path: &str) { + if path.is_empty() { + self.environment_map = None; + } else { + let full = format!("data/{}", path); + let (handle, task) = self.asset_hub.textures.load( + full, + blade_render::texture::Meta { + format: gpu::TextureFormat::Rgba32Float, + generate_mips: false, + y_flip: false, + }, + ); + self.environment_map = Some(handle); + self.load_tasks.push(task.clone()); + } + } + + pub fn set_gravity(&mut self, force: f32) { + self.physics.gravity.y = -force; + } + + pub fn set_average_luminosity(&mut self, avg_lum: f32) { + self.post_proc_config.average_luminocity = avg_lum; + } +}