├── .cargo └── config.toml ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE.APACHE ├── LICENSE.MIT ├── LICENSE.ZLIB ├── README.md ├── README.tpl ├── build.bash ├── deny.toml ├── examples ├── Cargo.toml ├── README.md ├── resources │ └── index.html └── src │ ├── animation │ ├── README.md │ ├── mod.rs │ ├── resources │ │ ├── LICENSE │ │ ├── cube_3.gltf │ │ ├── scene.bin │ │ ├── scene.gltf │ │ └── textures │ │ │ └── rabbit_baseColor.jpeg │ └── screenshot.png │ ├── cube │ ├── README.md │ ├── mod.rs │ └── screenshot.png │ ├── cube_no_framework │ ├── README.md │ └── mod.rs │ ├── egui │ ├── images │ │ └── rust-logo-128x128-blk.png │ ├── mod.rs │ └── screenshot.png │ ├── lib.rs │ ├── main.rs │ ├── scene_viewer │ ├── README.md │ ├── bistro.png │ ├── emerald-square.jpg │ ├── mod.rs │ ├── resources │ │ └── skybox │ │ │ ├── back.jpg │ │ │ ├── bottom.jpg │ │ │ ├── front.jpg │ │ │ ├── left.jpg │ │ │ ├── right.jpg │ │ │ └── top.jpg │ ├── scifi-base.jpg │ └── screenshot.png │ ├── skinning │ ├── README.md │ ├── RiggedSimple.glb │ ├── mod.rs │ └── screenshot.png │ ├── static_gltf │ ├── README.md │ ├── data.glb │ ├── mod.rs │ └── screenshot.png │ ├── tests.rs │ └── textured_quad │ ├── README.md │ ├── checker.png │ ├── mod.rs │ └── screenshot.png ├── release.toml ├── rend3-anim ├── Cargo.toml └── src │ └── lib.rs ├── rend3-egui ├── Cargo.toml └── src │ └── lib.rs ├── rend3-framework ├── Cargo.toml └── src │ ├── assets.rs │ ├── grab.rs │ ├── grab │ ├── native.rs │ └── wasm.rs │ └── lib.rs ├── rend3-gltf ├── Cargo.toml └── src │ └── lib.rs ├── rend3-routine ├── Cargo.toml ├── shaders │ └── src │ │ ├── blit.wgsl │ │ ├── depth.wgsl │ │ ├── material.wgsl │ │ ├── math │ │ ├── brdf.wgsl │ │ ├── color.wgsl │ │ ├── consts.wgsl │ │ ├── frustum.wgsl │ │ ├── matrix.wgsl │ │ └── sphere.wgsl │ │ ├── opaque.wgsl │ │ ├── shadow │ │ └── pcf.wgsl │ │ ├── skinning.wgsl │ │ ├── skybox.wgsl │ │ ├── structures.wgsl │ │ └── structures_object.wgsl └── src │ ├── base.rs │ ├── clear.rs │ ├── common │ ├── camera.rs │ ├── interfaces.rs │ ├── mod.rs │ └── samplers.rs │ ├── forward.rs │ ├── lib.rs │ ├── pbr │ ├── material.rs │ ├── mod.rs │ └── routine.rs │ ├── shaders.rs │ ├── skinning.rs │ ├── skybox.rs │ ├── tonemapping.rs │ └── uniforms.rs ├── rend3-test ├── Cargo.toml ├── src │ ├── helpers.rs │ ├── lib.rs │ ├── runner.rs │ └── threshold.rs └── tests │ ├── msaa.rs │ ├── object.rs │ ├── results │ ├── msaa │ │ ├── four.png │ │ ├── sample-coverage-1.png │ │ └── sample-coverage-4.png │ ├── object │ │ ├── duplicate-object-retain-left.png │ │ ├── duplicate-object-retain-right.png │ │ ├── multi-frame-add-0.png │ │ └── multi-frame-add-1.png │ ├── shadow │ │ ├── cube.png │ │ └── plane.png │ ├── simple │ │ ├── coordinate-space-NegX.png │ │ ├── coordinate-space-NegY.png │ │ ├── coordinate-space-NegZ.png │ │ ├── coordinate-space-X.png │ │ ├── coordinate-space-Y.png │ │ ├── coordinate-space-Z.png │ │ ├── empty.png │ │ ├── triangle-backface.png │ │ └── triangle.png │ └── transparency │ │ └── blending.png │ ├── root.rs │ ├── shadow.rs │ ├── simple.rs │ └── transparency.rs ├── rend3-types ├── Cargo.toml └── src │ ├── attribute.rs │ └── lib.rs └── rend3 ├── Cargo.toml ├── shaders ├── mipmap.wgsl ├── scatter_copy.wgsl ├── vertex_attributes.wgsl └── vertex_attributes_store.wgsl └── src ├── graph ├── encpass.rs ├── graph.rs ├── mod.rs ├── node.rs ├── store.rs ├── temp.rs └── texture_store.rs ├── instruction.rs ├── lib.rs ├── managers ├── camera.rs ├── directional.rs ├── directional │ ├── shadow_alloc.rs │ └── shadow_camera.rs ├── graph_storage.rs ├── handle_alloc.rs ├── material.rs ├── material │ └── texture_dedupe.rs ├── mesh.rs ├── object.rs ├── point.rs ├── skeleton.rs └── texture.rs ├── profile.rs ├── renderer ├── error.rs ├── eval.rs ├── mod.rs └── setup.rs ├── setup.rs ├── shader.rs ├── surface.rs └── util ├── bind_merge.rs ├── buffer.rs ├── error_scope.rs ├── freelist ├── buffer.rs └── vec.rs ├── frustum.rs ├── iter.rs ├── math.rs ├── mipmap.rs ├── output.rs ├── scatter_copy.rs ├── sync.rs ├── typedefs.rs └── upload.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | rend3-doc = [ 3 | "doc", 4 | "--no-deps", 5 | "--lib", 6 | "--workspace", 7 | "--exclude", 8 | "scene-viewer", 9 | "--exclude", 10 | "rend3-cube-example", 11 | ] 12 | 13 | [build] 14 | rustflags = [] 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.gltf binary 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: cwfitzgerald 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "08:00" 8 | timezone: America/New_York 9 | open-pull-requests-limit: 10 10 | - package-ecosystem: github-actions 11 | directory: / 12 | schedule: 13 | interval: daily 14 | time: "08:00" 15 | timezone: America/New_York -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Checklist 4 | 5 | - CI Checked: 6 | - [ ] `cargo fmt` has been ran 7 | - [ ] `cargo clippy` reports no issues 8 | - [ ] `cargo test` succeeds 9 | - [ ] `cargo rend3-doc` has no warnings 10 | - [ ] `cargo deny check` issues have been fixed or added to deny.toml 11 | - Manually Checked: 12 | - [ ] relevant examples/test cases run 13 | - [ ] changes added to changelog 14 | - [ ] Add credit to yourself for each change: `Added new functionality @githubname`. 15 | 16 | ## Related Issues 17 | 18 | ## Description 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["trunk", "CI/**"] 6 | pull_request: 7 | 8 | env: 9 | RUSTFLAGS: -D warnings 10 | RUSTDOCFLAGS: -D warnings 11 | 12 | jobs: 13 | build: 14 | timeout-minutes: 30 15 | 16 | strategy: 17 | matrix: 18 | include: 19 | # wasm 20 | - name: "wasm" 21 | os: "ubuntu-latest" 22 | target: "wasm32-unknown-unknown" 23 | 24 | # native 25 | - name: "linux" 26 | os: "ubuntu-latest" 27 | target: "x86_64-unknown-linux-gnu" 28 | - name: "mac" 29 | os: "macos-14" 30 | target: "aarch64-apple-darwin" 31 | - name: "windows" 32 | os: "windows-latest" 33 | target: "x86_64-pc-windows-msvc" 34 | fail-fast: false 35 | runs-on: ${{ matrix.os }} 36 | name: ${{ matrix.name }} 37 | 38 | steps: 39 | - uses: actions/checkout@v4 40 | 41 | - name: Set up Rust toolchain 42 | id: setup-rust 43 | run: | 44 | rustup toolchain install 1.76 -c clippy -t ${{ matrix.target }} 45 | 46 | - name: Install cargo-nextest and cargo-llvm-cov 47 | uses: taiki-e/install-action@v2 48 | with: 49 | tool: cargo-nextest 50 | 51 | - name: caching 52 | uses: Swatinem/rust-cache@v2 53 | if: matrix.os[1] != 'gpu' 54 | with: 55 | key: ${{ matrix.target }}-b # suffix for cache busting 56 | 57 | - name: build 58 | run: | 59 | cargo +1.76 build --target ${{ matrix.target }} --profile ci 60 | if: matrix.target != 'wasm32-unknown-unknown' 61 | 62 | - name: clippy (rend3-gltf featureless) 63 | run: | 64 | cargo +1.76 clippy --target ${{ matrix.target }} --profile ci -p rend3-gltf --no-default-features 65 | 66 | - name: clippy 67 | run: | 68 | cargo +1.76 clippy --target ${{ matrix.target }} --profile ci 69 | 70 | - name: doc 71 | run: | 72 | cargo +1.76 doc --target ${{ matrix.target }} --profile ci --no-deps 73 | 74 | - name: download test resources 75 | if: matrix.os != 'macos-14' && matrix.target != 'wasm32-unknown-unknown' 76 | run: | 77 | bash ./build.bash download-assets 78 | 79 | - name: test 80 | if: matrix.os != 'macos-14' && matrix.target != 'wasm32-unknown-unknown' 81 | run: | 82 | cargo +1.76 nextest run --target ${{ matrix.target }} --cargo-profile ci --no-fail-fast 83 | 84 | - uses: actions/upload-artifact@v4 85 | # always run 86 | if: ${{ !cancelled() }} 87 | with: 88 | name: comparison-images-${{ matrix.name }} 89 | path: | 90 | **/*-success.png 91 | **/*-failure.png 92 | **/*-diff.png 93 | 94 | cargo-fmt: 95 | runs-on: ubuntu-latest 96 | steps: 97 | - uses: actions/checkout@v4 98 | 99 | - name: Set up Rust toolchain 100 | id: setup-rust 101 | run: | 102 | rustup toolchain install 1.76 -c rustfmt 103 | 104 | - name: format 105 | run: | 106 | cargo +1.76 fmt --check 107 | 108 | cargo-deny: 109 | runs-on: ubuntu-latest 110 | steps: 111 | - uses: actions/checkout@v4 112 | - uses: EmbarkStudios/cargo-deny-action@v1 113 | with: 114 | log-level: warn 115 | command: check 116 | arguments: --all-features 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vs 3 | /target 4 | /tmp 5 | /tmp-alt 6 | 7 | capstone.zip 8 | cargo-timing* 9 | Cargo.lock 10 | 11 | /profile.json 12 | /examples/src/scene_viewer/resources/ 13 | !/examples/src/scene_viewer/resources/skybox 14 | 15 | *-diff.png 16 | *-success.png 17 | *-failure.png 18 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | use_small_heuristics = "Max" 3 | use_field_init_shorthand = true 4 | use_try_shorthand = true 5 | edition = "2021" 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me (at) cwfitz.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "examples/", 5 | "rend3", 6 | "rend3-anim", 7 | "rend3-egui", 8 | "rend3-framework", 9 | "rend3-gltf", 10 | "rend3-routine", 11 | "rend3-test", 12 | "rend3-types", 13 | ] 14 | 15 | [profile.ci] 16 | inherits = "dev" 17 | debug = false 18 | incremental = false 19 | 20 | [profile.ci.package."*"] 21 | opt-level = 0 22 | 23 | [profile.dev.package."*"] 24 | opt-level = 3 25 | 26 | [profile.release] 27 | debug = true 28 | lto = "thin" 29 | 30 | [patch.crates-io] 31 | # wgpu = { git = "https://github.com/gfx-rs/wgpu.git", rev = "fac4731288117d951d0944d96cf0b00fa006dd6c" } 32 | # wgpu-core = { git = "https://github.com/gfx-rs/wgpu.git", rev = "fac4731288117d951d0944d96cf0b00fa006dd6c" } 33 | # wgpu-hal = { git = "https://github.com/gfx-rs/wgpu.git", rev = "fac4731288117d951d0944d96cf0b00fa006dd6c" } 34 | # wgpu-types = { git = "https://github.com/gfx-rs/wgpu.git", rev = "fac4731288117d951d0944d96cf0b00fa006dd6c" } 35 | # wgpu = { git = "https://github.com/cwfitzgerald/wgpu.git", rev = "bda861f77e0ca0b97697850ad19d19a8b8f1cc9c" } 36 | # wgpu-core = { git = "https://github.com/cwfitzgerald/wgpu.git", rev = "bda861f77e0ca0b97697850ad19d19a8b8f1cc9c" } 37 | # wgpu-hal = { git = "https://github.com/cwfitzgerald/wgpu.git", rev = "bda861f77e0ca0b97697850ad19d19a8b8f1cc9c" } 38 | # wgpu-types = { git = "https://github.com/cwfitzgerald/wgpu.git", rev = "bda861f77e0ca0b97697850ad19d19a8b8f1cc9c" } 39 | # wgpu = { path = "../wgpu/wgpu" } 40 | # wgpu-core = { path = "../wgpu/wgpu-core" } 41 | # wgpu-hal = { path = "../wgpu/wgpu-hal" } 42 | # wgpu-types = { path = "../wgpu/wgpu-types" } 43 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Connor Fitzgerald 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.ZLIB: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | (C) 2020 Connor Fitzgerald 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rend3 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/BVE-Reborn/rend3/ci.yml) 4 | [![Crates.io](https://img.shields.io/crates/v/rend3)](https://crates.io/crates/rend3) 5 | [![Documentation](https://docs.rs/rend3/badge.svg)](https://docs.rs/rend3) 6 | ![License](https://img.shields.io/crates/l/rend3) 7 | [![Matrix](https://img.shields.io/static/v1?label=rend3%20dev&message=%23rend3&color=blueviolet&logo=matrix)](https://matrix.to/#/#rend3:matrix.org) 8 | [![Matrix](https://img.shields.io/static/v1?label=rend3%20users&message=%23rend3-users&color=blueviolet&logo=matrix)](https://matrix.to/#/#rend3-users:matrix.org) 9 | [![Discord](https://img.shields.io/discord/451037457475960852?color=7289DA&label=discord)](https://discord.gg/mjxXTVzaDg) 10 | 11 | 12 | Easy to use, customizable, efficient 3D renderer library built on wgpu. 13 | 14 | Library is under active development. While internals are might change in the 15 | future, the external api remains stable, with only minor changes occuring as 16 | features are added. 17 | 18 | ## Examples 19 | 20 | Take a look at the [examples] for getting started with the api. The examples 21 | will show how the core library and helper crates can be used. 22 | 23 | [examples]: https://github.com/BVE-Reborn/rend3/tree/trunk/examples 24 | 25 | #### Screenshots 26 | 27 | These screenshots are from the scene_viewer example. 28 | 29 | ![scifi-base](https://raw.githubusercontent.com/BVE-Reborn/rend3/trunk/examples/src/scene_viewer/scifi-base.jpg) 30 | ![example](https://raw.githubusercontent.com/BVE-Reborn/rend3/trunk/examples/src/scene_viewer/screenshot.jpg) 31 | ![bistro](https://raw.githubusercontent.com/BVE-Reborn/rend3/trunk/examples/src/scene_viewer/bistro.jpg) 32 | ![emerald-square](https://raw.githubusercontent.com/BVE-Reborn/rend3/trunk/examples/src/scene_viewer/emerald-square.jpg) 33 | 34 | ## Crates 35 | 36 | The `rend3` ecosystem is composed of a couple core crates which provide most 37 | of the functionality and exensibility to the library, extension crates, and 38 | integration crates 39 | 40 | #### Core 41 | 42 | - `rend3`: The core crate. Performs all handling of world data, provides the 43 | Renderer and RenderGraph and defines vocabulary types. 44 | - `rend3-routine`: Implementation of various "Render Routines" on top of the 45 | RenderGraph. Also provides for re-usable graphics work. Provides PBR 46 | rendering, Skyboxes, Shadow Rendering, and Tonemapping. 47 | 48 | #### Extensions 49 | 50 | There are extension crates that are not required, but provide pre-made bits 51 | of useful code that I would recommend using. 52 | 53 | - `rend3-framework`: Vastly simplifies correct handling of the window and 54 | surface across platforms. 55 | - `rend3-gltf`: Modular gltf file and scene loader. 56 | 57 | #### Integration 58 | 59 | Integration with other external libraries are also offered. Due to external 60 | dependencies, the versions of these may increase at a much higher rate than 61 | the rest of the ecosystem. 62 | 63 | - `rend3-anim`: Skeletal animation playback utilities. Currently tied to rend3-gltf. 64 | - `rend3-egui`: Integration with the [egui](https://github.com/emilk/egui) 65 | immediate mode gui. 66 | 67 | ## Purpose 68 | 69 | `rend3` tries to fulfill the following usecases: 70 | 1. Games and visualizations that need a customizable, and efficient renderer. 71 | 2. Projects that just want to put objects on screen, but want lighting and effects. 72 | 3. A small cog in a big machine: a renderer that doesn't interfere with the rest of the program. 73 | 74 | `rend3` is not: 75 | 1. A framework or engine. It does not include all the parts needed to make an 76 | advanced game or simulation nor care how you structure your program. 77 | If you want a very basic framework to deal with windowing and event loop management, 78 | `rend3-framework` can help you. This will always be optional and is just there to help 79 | with the limited set of cases it can help. 80 | 81 | ## Future Plans 82 | 83 | I have grand plans for this library. An overview can be found in the issue 84 | tracker under the [enhancement] label. 85 | 86 | [enhancement]: https://github.com/BVE-Reborn/rend3/labels/enhancement 87 | 88 | ## Matrix Chatroom 89 | 90 | We have a matrix chatroom that you can come and join if you want to chat 91 | about using rend3 or developing it: 92 | 93 | [![Matrix](https://img.shields.io/static/v1?label=rend3%20dev&message=%23rend3&color=blueviolet&logo=matrix)](https://matrix.to/#/#rend3:matrix.org) 94 | [![Matrix](https://img.shields.io/static/v1?label=rend3%20users&message=%23rend3-users&color=blueviolet&logo=matrix)](https://matrix.to/#/#rend3-users:matrix.org) 95 | 96 | If discord is more your style, our meta project has a channel which mirrors 97 | the matrix rooms: 98 | 99 | [![Discord](https://img.shields.io/discord/451037457475960852?color=7289DA&label=discord)](https://discord.gg/mjxXTVzaDg) 100 | 101 | ## Helping Out 102 | 103 | We welcome all contributions and ideas. If you want to participate or have 104 | ideas for this library, we'd love to hear them! 105 | 106 | License: MIT OR Apache-2.0 OR Zlib 107 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # {{crate}} 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/BVE-Reborn/rend3/CI) 4 | [![Crates.io](https://img.shields.io/crates/v/rend3)](https://crates.io/crates/rend3) 5 | [![Documentation](https://docs.rs/rend3/badge.svg)](https://docs.rs/rend3) 6 | ![License](https://img.shields.io/crates/l/rend3) 7 | [![Matrix](https://img.shields.io/static/v1?label=rend3%20dev&message=%23rend3&color=blueviolet&logo=matrix)](https://matrix.to/#/#rend3:matrix.org) 8 | [![Matrix](https://img.shields.io/static/v1?label=rend3%20users&message=%23rend3-users&color=blueviolet&logo=matrix)](https://matrix.to/#/#rend3-users:matrix.org) 9 | [![Discord](https://img.shields.io/discord/451037457475960852?color=7289DA&label=discord)](https://discord.gg/mjxXTVzaDg) 10 | 11 | 12 | {{readme}} 13 | 14 | License: {{license}} -------------------------------------------------------------------------------- /build.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | case $1 in 6 | web-bin) 7 | shift 8 | if [ $1 == "release" ]; then 9 | shift 10 | BUILD_FLAGS=--release 11 | WASM_BUILD_DIR=release 12 | else 13 | WASM_BUILD_DIR=debug 14 | fi 15 | cargo build --target wasm32-unknown-unknown $BUILD_FLAGS --bin rend3-examples 16 | mkdir -p target/generated/ 17 | rm -rf target/generated/* 18 | cp -r examples/src/$1/resources target/generated/ || true 19 | sed "s/{{example}}/$1/g" > target/generated/index.html < examples/resources/index.html 20 | wasm-bindgen --out-dir target/generated --target web target/wasm32-unknown-unknown/$WASM_BUILD_DIR/rend3-examples.wasm 21 | ;; 22 | serve) 23 | shift 24 | simple-http-server target/generated -c wasm,html,js -i --nocache 25 | ;; 26 | ci) 27 | cargo fmt 28 | cargo clippy 29 | cargo test 30 | cargo rend3-doc 31 | cargo clippy --target wasm32-unknown-unknown --workspace --exclude rend3-imgui --exclude rend3-imgui-example 32 | cargo deny --all-features check 33 | ;; 34 | download-assets) 35 | curl https://cdn.cwfitz.com/scenes/rend3-default-scene.tar -o ./examples/src/scene_viewer/resources/rend3-default-scene.tar 36 | tar xf ./examples/src/scene_viewer/resources/rend3-default-scene.tar -C ./examples/src/scene_viewer/resources 37 | curl https://cdn.cwfitz.com/scenes/bistro-full.zip -o ./examples/src/scene_viewer/resources/bistro-full.zip 38 | unzip ./examples/src/scene_viewer/resources/bistro-full.zip -d ./examples/src/scene_viewer/resources 39 | ;; 40 | update-readme) 41 | cd rend3 42 | cargo install cargo-readme 43 | cargo readme -t ../README.tpl -o ../README.md 44 | ;; 45 | help | *) 46 | set +x 47 | echo "rend3 build script" 48 | echo "" 49 | echo "Contains helpful sets of commands for rend3's development." 50 | echo "Building rend3 does not require any of these. Just use cargo as normal." 51 | echo "" 52 | echo "Subcommands:" 53 | echo "help This message." 54 | echo "update-readme Rebuilds the README.md file from the rend3 crate docs." 55 | echo "download-assets Downloads the assets used in the examples/tests." 56 | echo "web-bin [release] Builds BINARY as wasm, and runs wasm-bindgen on the result." 57 | echo "serve Serve a web server from target/generated using simple-http-server." 58 | esac 59 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | unlicensed = "deny" 3 | allow-osi-fsf-free = "either" 4 | allow = ["Apache-2.0", "MIT", "MPL-2.0", "LicenseRef-UFL-1.0", "Unlicense"] 5 | copyleft = "allow" 6 | 7 | [[licenses.clarify]] 8 | name = "encoding_rs" 9 | expression = "(Apache-2.0 OR MIT) AND BSD-3-Clause" 10 | license-files = [{ path = "COPYRIGHT", hash = 972598577 }] 11 | 12 | [bans] 13 | multiple-versions = "deny" 14 | wildcards = "allow" 15 | skip = [ 16 | # gltf / reqwest 17 | { name = "base64", version = "0.13.1" }, 18 | # ndk_glue 19 | { name = "env_logger", version = "0.10.2" }, 20 | ] 21 | skip-tree = [ 22 | # winit brings in lots of duplicate deps that we can't fix 23 | { name = "winit", version = "0.29.4" }, 24 | 25 | ] 26 | 27 | [advisories] 28 | vulnerability = "warn" 29 | unmaintained = "warn" 30 | ignore = [] 31 | 32 | [sources] 33 | unknown-registry = "deny" 34 | unknown-git = "allow" 35 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-examples-package" 3 | license = "MIT OR Apache-2.0 OR Zlib" 4 | version = "0.3.0" 5 | authors = ["The rend3 Developers"] 6 | edition = "2021" 7 | publish = false 8 | rust-version = "1.71" 9 | 10 | [lib] 11 | crate-type = ["lib", "cdylib"] 12 | 13 | [[bin]] 14 | name = "rend3-examples" 15 | path = "src/main.rs" 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | # error handling 21 | anyhow = "1" 22 | # The egui immediate mode gui library 23 | egui = "0.26" 24 | # Winit integration with egui (turn off the clipboard feature) 25 | egui-winit = { version = "0.26", default-features = false, features = ["links", "wayland"] } 26 | # logging 27 | env_logger = { version = "0.11", default-features = false, features = ["auto-color", "humantime"] } 28 | # Linear algebra library 29 | glam = "0.25" 30 | # gltf model loading 31 | gltf = "1.4" 32 | # Channel 33 | flume = "0.11" 34 | # Logging infrastructure 35 | log = "0.4" 36 | # Inline string 37 | indoc = "2" 38 | # Importing images 39 | image = { version = "0.24", default-features = false, features = [ 40 | "png", 41 | "jpeg", 42 | "tiff", 43 | ] } 44 | # profiling 45 | profiling = "1" 46 | # argument parsing 47 | pico-args = "0.5" 48 | # block on async functions 49 | pollster = "0.3" 50 | # Renderer core 51 | rend3 = { version = "^0.3.0", path = "../rend3" } 52 | # Play animations on imported gltf models 53 | rend3-anim = { version = "^0.3.0", path = "../rend3-anim" } 54 | # Egui integration with rend3 55 | rend3-egui = { version = "^0.3.0", path = "../rend3-egui" } 56 | # Programmable render list that dictates how the scene renders 57 | rend3-routine = { version = "^0.3.0", path = "../rend3-routine" } 58 | # Framework that deals with the event loop, setting up the renderer, and platform differences. 59 | rend3-framework = { version = "^0.3.0", path = "../rend3-framework" } 60 | # Import gltf models 61 | rend3-gltf = { version = "^0.3.0", path = "../rend3-gltf" } 62 | # Opening URL's 63 | webbrowser = "0.8.2" 64 | # Instant but portable to the web 65 | web-time = "1.1" 66 | # windowing 67 | winit = "0.29.4" 68 | # Integration with wgpu 69 | wgpu = "0.19.0" 70 | # Profiling with wgpu 71 | wgpu-profiler = "0.16.0" 72 | 73 | [target.'cfg(target_arch = "wasm32")'.dependencies] 74 | console_log = "1" 75 | console_error_panic_hook = "0.1" 76 | js-sys = "0.3" 77 | gloo-console = "0.3" 78 | web-sys = "0.3.67" 79 | wasm-bindgen = "0.2.83" 80 | wasm-bindgen-futures = "0.4" 81 | 82 | [target.'cfg(target_os = "android")'.dependencies] 83 | ndk-glue = { version = "0.7", features = ["logger"] } 84 | 85 | [dev-dependencies] 86 | rend3-test = { version = "^0.3.0", path = "../rend3-test" } 87 | tokio = "1" 88 | 89 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 90 | wasm-bindgen-test = { version = "0.3" } 91 | 92 | [package.metadata.android] 93 | build_targets = ["aarch64-linux-android"] 94 | 95 | [package.metadata.android.sdk] 96 | min_sdk_version = 28 97 | target_sdk_version = 28 98 | compile_sdk_version = 28 99 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # rend3 Examples 2 | 3 | The examples are separate crates, so to run an example run `cargo run `. 4 | 5 | If you want to demo running rend3, [`scene-viewer`](scene-viewer) is what you should use. It is a full fledged gltf viewer. 6 | 7 | If you want to look at how the basics of the code works, look at either the [`cube`](cube) or [`cube-no-framework`](cube-no-framework) example. 8 | 9 | | Name | Description | Image | 10 | |:-------------------:|:------------|-------| 11 | | scene-viewer | Advanced gltf model and scene viewer. Used to test all of rend3's complex features. | ![](src/scene_viewer/scifi-base.jpg) ![](src/scene_viewer/screenshot.jpg) ![](src/scene_viewer/bistro.jpg) ![](src/scene_viewer/emerald-square.jpg) | 12 | | cube | Basic example. Shows how to get started with the api, both with and without the use of rend3-framework. | ![](src/cube/screenshot.png) | 13 | | egui | Shows integration with egui and rend3-egui | ![](src/egui/screenshot.png) | 14 | | skinning | Basic bone deformation of a loaded gltf model | ![](src/skinning/screenshot.png) | 15 | | static-gltf | Similar to cube, but geometry is pulled from a simple gltf file | ![](src/static_gltf/screenshot.png) | 16 | | textured-quad | Basic 2D rendering with an orthographic camera | ![](src/textured_quad/screenshot.png) | 17 | -------------------------------------------------------------------------------- /examples/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/src/animation/README.md: -------------------------------------------------------------------------------- 1 | # skinning 2 | 3 | This example showcases how to import an animated gltf character and play an animation using `rend3-gltf` and `rend3-anim`. 4 | 5 | ![](screenshot.png) 6 | -------------------------------------------------------------------------------- /examples/src/animation/resources/LICENSE: -------------------------------------------------------------------------------- 1 | "Gangnam Style Dancing Rabbit Character" (https://skfb.ly/6V6Ty) by antonmoek is 2 | licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/). -------------------------------------------------------------------------------- /examples/src/animation/resources/scene.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/animation/resources/scene.bin -------------------------------------------------------------------------------- /examples/src/animation/resources/textures/rabbit_baseColor.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/animation/resources/textures/rabbit_baseColor.jpeg -------------------------------------------------------------------------------- /examples/src/animation/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/animation/screenshot.png -------------------------------------------------------------------------------- /examples/src/cube/README.md: -------------------------------------------------------------------------------- 1 | # cube 2 | 3 | Basic example, best way to get started with the api. 4 | 5 | ![](screenshot.png) -------------------------------------------------------------------------------- /examples/src/cube/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/cube/screenshot.png -------------------------------------------------------------------------------- /examples/src/cube_no_framework/README.md: -------------------------------------------------------------------------------- 1 | # cube no framework 2 | 3 | Basic example but one that doesn't use the framework, so it is easy to see how all the parts fit together when the framework isn't involved. 4 | 5 | ![](../cube/screenshot.png) -------------------------------------------------------------------------------- /examples/src/egui/images/rust-logo-128x128-blk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/egui/images/rust-logo-128x128-blk.png -------------------------------------------------------------------------------- /examples/src/egui/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/egui/screenshot.png -------------------------------------------------------------------------------- /examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::arc_with_non_send_sync)] 2 | 3 | #[cfg(target_arch = "wasm32")] 4 | use wasm_bindgen::prelude::*; 5 | 6 | mod animation; 7 | mod cube; 8 | mod cube_no_framework; 9 | mod egui; 10 | mod scene_viewer; 11 | mod skinning; 12 | mod static_gltf; 13 | mod textured_quad; 14 | 15 | #[cfg(target_arch = "wasm32")] 16 | use gloo_console::info as output; 17 | #[cfg(not(target_arch = "wasm32"))] 18 | use std::println as output; 19 | 20 | #[cfg(test)] 21 | mod tests; 22 | 23 | struct ExampleDesc { 24 | name: &'static str, 25 | run: fn(), 26 | } 27 | 28 | const EXAMPLES: &[ExampleDesc] = &[ 29 | ExampleDesc { name: "animation", run: animation::main }, 30 | ExampleDesc { name: "cube", run: cube::main }, 31 | ExampleDesc { name: "cube-no-framework", run: cube_no_framework::main }, 32 | ExampleDesc { name: "egui", run: egui::main }, 33 | ExampleDesc { name: "scene_viewer", run: scene_viewer::main }, 34 | ExampleDesc { name: "skinning", run: skinning::main }, 35 | ExampleDesc { name: "static_gltf", run: static_gltf::main }, 36 | ExampleDesc { name: "textured_quad", run: textured_quad::main }, 37 | ]; 38 | 39 | fn print_examples() { 40 | output!("Usage: cargo run \n"); 41 | output!("Available examples:"); 42 | for example in EXAMPLES { 43 | output!(" {}", example.name); 44 | } 45 | } 46 | 47 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen)] 48 | pub fn main_with_name(example_name: Option) { 49 | let Some(example_name) = example_name else { 50 | print_examples(); 51 | return; 52 | }; 53 | 54 | let Some(example) = EXAMPLES.iter().find(|example| example.name == example_name) else { 55 | output!("Unknown example: {}\n", example_name); 56 | print_examples(); 57 | return; 58 | }; 59 | 60 | (example.run)(); 61 | } 62 | 63 | #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on", logger(level = "debug")))] 64 | pub fn main() { 65 | main_with_name(std::env::args().nth(1)) 66 | } 67 | -------------------------------------------------------------------------------- /examples/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | rend3_examples_package::main(); 3 | } 4 | -------------------------------------------------------------------------------- /examples/src/scene_viewer/README.md: -------------------------------------------------------------------------------- 1 | # scene-viewer 2 | 3 | gltf (and glb) loader and viewer using the [rend3](https://crates.io/crates/rend3) rendering engine. 4 | 5 | ## Default Scene 6 | 7 | To download the default scene: 8 | 9 | ```bash 10 | # On windows, make sure to type curl.exe to get real curl, not the alias in powershell. 11 | # On *nix, just type `curl`. 12 | curl.exe https://cdn.cwfitz.com/scenes/rend3-default-scene.tar -o ./examples/scene-viewer/resources/rend3-default-scene.tar 13 | tar xf ./examples/scene-viewer/resources/rend3-default-scene.tar -C ./examples/scene-viewer/resources 14 | ``` 15 | 16 | The source of the default scene is available here: 17 | 18 | https://cdn.cwfitz.com/scenes/rend3-default-scene.blend 19 | 20 | ## Default Scene 21 | 22 | Default scene, exposed through glTF: 23 | 24 | ![](screenshot.jpg) 25 | 26 | ``` 27 | cargo run --bin scene-viewer --release -- --shadow-distance 40 --msaa 4 --fullscreen 28 | ``` 29 | 30 | ## Sci-fi Base 31 | 32 | Exported Unity Scene through glTF: 33 | 34 | ![](scifi-base.jpg) 35 | 36 | ``` 37 | cargo run --bin scene-viewer --release -- ..\gltf-export\sci-fi-base-ktx\sci-fi-base.gltf --shadow-distance 1000 --msaa 4 --gltf-disable-directional-lights --directional-light -1,-0.5,-1 --directional-light-intensity 20 --fullscreen 38 | ``` 39 | 40 | ## Bistro 41 | 42 | Bistro scene from [NVIDIA ORCA](https://developer.nvidia.com/orca) touched up by https://github.com/aclysma/rendering-demo-scenes 43 | 44 | ![](bistro.jpg) 45 | 46 | ``` 47 | cargo run --bin scene-viewer --profile release -- ..\rendering-demo-scenes\bistro\bistro.gltf --normal-y-down --msaa 4 --gltf-disable-directional-lights --directional-light 1,-5,-1 --directional-light-intensity 15 --fullscreen 48 | ``` 49 | 50 | ## Emerald Square 51 | 52 | Emerald-Square from [NVIDIA ORCA](https://developer.nvidia.com/orca) exported to GLTF with blender: 53 | 54 | ![](emerald-square.jpg) 55 | 56 | ``` 57 | cargo run --bin scene-viewer --release -- ..\gltf-export\emerald-square\untitled.gltf --shadow-distance 1000 --msaa 4 --gltf-disable-directional-lights --directional-light -1,-1,-1 58 | --directional-light-intensity 20 --fullscreen 59 | ``` 60 | -------------------------------------------------------------------------------- /examples/src/scene_viewer/bistro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/bistro.png -------------------------------------------------------------------------------- /examples/src/scene_viewer/emerald-square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/emerald-square.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/resources/skybox/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/resources/skybox/back.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/resources/skybox/bottom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/resources/skybox/bottom.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/resources/skybox/front.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/resources/skybox/front.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/resources/skybox/left.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/resources/skybox/left.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/resources/skybox/right.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/resources/skybox/right.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/resources/skybox/top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/resources/skybox/top.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/scifi-base.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/scifi-base.jpg -------------------------------------------------------------------------------- /examples/src/scene_viewer/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/scene_viewer/screenshot.png -------------------------------------------------------------------------------- /examples/src/skinning/README.md: -------------------------------------------------------------------------------- 1 | # skinning 2 | 3 | This example showcases how to import a mesh with an armature and position its bones. 4 | 5 | ![](screenshot.png) 6 | -------------------------------------------------------------------------------- /examples/src/skinning/RiggedSimple.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/skinning/RiggedSimple.glb -------------------------------------------------------------------------------- /examples/src/skinning/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/skinning/screenshot.png -------------------------------------------------------------------------------- /examples/src/static_gltf/README.md: -------------------------------------------------------------------------------- 1 | # static-gltf 2 | 3 | Quite similar to the cube example, but the geometry to render is pulled from `data.glb` using the gltf crate. 4 | 5 | Note that only a small small portion of the gltf spec is used here; you could pull out and render a lot more data with rend3. 6 | Materials in particular are largely ignored. 7 | 8 | If you want a full fledged gltf viewer, look at [scene-viewer](../scene-viewer). 9 | 10 | ![](screenshot.png) 11 | -------------------------------------------------------------------------------- /examples/src/static_gltf/data.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/static_gltf/data.glb -------------------------------------------------------------------------------- /examples/src/static_gltf/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/static_gltf/screenshot.png -------------------------------------------------------------------------------- /examples/src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, sync::Arc}; 2 | 3 | use anyhow::Context; 4 | use glam::UVec2; 5 | use rend3_framework::{App, DefaultRoutines, Mutex, RedrawContext, SetupContext}; 6 | use rend3_test::{compare_image_to_path, download_image}; 7 | 8 | pub struct TestConfiguration { 9 | pub app: A, 10 | pub reference_path: &'static str, 11 | pub size: UVec2, 12 | pub threshold_set: rend3_test::ThresholdSet, 13 | } 14 | 15 | #[allow(clippy::await_holding_lock)] // false positive 16 | pub async fn test_app, T: 'static>(mut config: TestConfiguration) -> anyhow::Result<()> { 17 | config.app.register_logger(); 18 | config.app.register_panic_hook(); 19 | 20 | let iad = 21 | rend3_test::no_gpu_return!(config.app.create_iad().await).context("InstanceAdapterDevice creation failed")?; 22 | 23 | let renderer = 24 | rend3::Renderer::new(iad.clone(), A::HANDEDNESS, Some(config.size.x as f32 / config.size.y as f32)).unwrap(); 25 | 26 | let mut spp = rend3::ShaderPreProcessor::new(); 27 | rend3_routine::builtin_shaders(&mut spp); 28 | 29 | let base_rendergraph = config.app.create_base_rendergraph(&renderer, &spp); 30 | let mut data_core = renderer.data_core.lock(); 31 | let routines = Arc::new(DefaultRoutines { 32 | pbr: Mutex::new(rend3_routine::pbr::PbrRoutine::new( 33 | &renderer, 34 | &mut data_core, 35 | &spp, 36 | &base_rendergraph.interfaces, 37 | )), 38 | skybox: Mutex::new(rend3_routine::skybox::SkyboxRoutine::new(&renderer, &spp, &base_rendergraph.interfaces)), 39 | tonemapping: Mutex::new(rend3_routine::tonemapping::TonemappingRoutine::new( 40 | &renderer, 41 | &spp, 42 | &base_rendergraph.interfaces, 43 | wgpu::TextureFormat::Rgba8UnormSrgb, 44 | )), 45 | }); 46 | drop(data_core); 47 | 48 | let surface_format = wgpu::TextureFormat::Rgba8UnormSrgb; 49 | config.app.setup(SetupContext { 50 | windowing: None, 51 | renderer: &renderer, 52 | routines: &routines, 53 | surface_format, 54 | resolution: config.size, 55 | scale_factor: 1.0, 56 | }); 57 | 58 | let texture = renderer.device.create_texture(&wgpu::TextureDescriptor { 59 | label: Some("Render Texture"), 60 | size: wgpu::Extent3d { width: config.size.x, height: config.size.y, depth_or_array_layers: 1 }, 61 | mip_level_count: 1, 62 | sample_count: 1, 63 | dimension: wgpu::TextureDimension::D2, 64 | format: surface_format, 65 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, 66 | view_formats: &[], 67 | }); 68 | 69 | config.app.handle_redraw(RedrawContext { 70 | window: None, 71 | renderer: &renderer, 72 | routines: &routines, 73 | base_rendergraph: &base_rendergraph, 74 | surface_texture: &texture, 75 | resolution: config.size, 76 | control_flow: &mut |_| unreachable!(), 77 | event_loop_window_target: None, 78 | delta_t_seconds: 0.0, 79 | }); 80 | 81 | let image = download_image(&renderer, texture, config.size).await.unwrap(); 82 | 83 | compare_image_to_path(&image, Path::new(config.reference_path), config.threshold_set).unwrap(); 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /examples/src/textured_quad/README.md: -------------------------------------------------------------------------------- 1 | # textured-quad 2 | 3 | Basic example of 2D textured quad, no light/shadow, and orthographic camera. 4 | 5 | ![](screenshot.png) -------------------------------------------------------------------------------- /examples/src/textured_quad/checker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/textured_quad/checker.png -------------------------------------------------------------------------------- /examples/src/textured_quad/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/examples/src/textured_quad/screenshot.png -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | consolidate-commits = true 2 | consolidate-pushes = true 3 | dev-version = false 4 | sign-commit = true 5 | sign-tag = true 6 | allow-branch = ["trunk", "v*"] 7 | tag = false 8 | pre-release-commit-message = "Version {{version}}" -------------------------------------------------------------------------------- /rend3-anim/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-anim" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "Skeletal animation playback utilities for rend3 rendering library." 7 | repository = "https://github.com/BVE-Reborn/rend3" 8 | license = "MIT OR Apache-2.0 OR Zlib" 9 | keywords = ["3d", "animation", "rend3", "renderer", "wgpu"] 10 | categories = ["game-development", "graphics", "rendering", "rendering::engine", "wasm"] 11 | rust-version = "1.71" 12 | 13 | [dependencies] 14 | itertools = "0.12" 15 | rend3 = { version = "^0.3.0", path = "../rend3" } 16 | rend3-routine = { version = "^0.3.0", path = "../rend3-routine" } 17 | rend3-gltf = { version = "^0.3.0", path = "../rend3-gltf" } 18 | -------------------------------------------------------------------------------- /rend3-egui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-egui" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "Egui Render Routine for the rend3 rendering library." 7 | readme = "../README.md" 8 | license = "MIT OR Apache-2.0 OR Zlib" 9 | repository = "https://github.com/BVE-Reborn/rend3" 10 | keywords = ["3d", "graphics", "rend3", "renderer", "egui"] 11 | categories = ["game-development", "graphics", "rendering", "rendering::engine", "wasm"] 12 | rust-version = "1.71" 13 | 14 | [dependencies] 15 | egui = "0.26" 16 | egui-wgpu = "0.26" 17 | glam = "0.25" 18 | rend3 = { version = "^0.3.0", path = "../rend3" } 19 | wgpu = "0.19.0" 20 | wgpu-types = "0.19.0" 21 | -------------------------------------------------------------------------------- /rend3-framework/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-framework" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "Simple framework for making applications with the rend3 rendering library." 7 | readme = "../README.md" 8 | repository = "https://github.com/BVE-Reborn/rend3" 9 | license = "MIT OR Apache-2.0 OR Zlib" 10 | keywords = ["3d", "graphics", "rend3", "renderer", "wgpu"] 11 | categories = ["game-development", "graphics", "rendering", "rendering::engine", "wasm"] 12 | rust-version = "1.71" 13 | 14 | [dependencies] 15 | anyhow = "1" 16 | cfg-if = "1" 17 | glam = "0.25" 18 | log = "0.4" 19 | parking_lot = "0.12" 20 | profiling = { version = "1", default-features = false } 21 | rend3 = { version = "0.3.0", path = "../rend3" } 22 | rend3-routine = { version = "0.3.0", path = "../rend3-routine" } 23 | thiserror = { version = "1" } 24 | web-time = "1.1" 25 | winit = { version = "0.29.4", features = ["rwh_05"] } 26 | wgpu = "0.19.0" 27 | 28 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 29 | # logging 30 | env_logger = { version = "0.11", default-features = false, features = ["auto-color", "humantime"] } 31 | pollster = "0.3" 32 | 33 | [target.'cfg(target_arch = "wasm32")'.dependencies] 34 | console_error_panic_hook = "0.1" 35 | console_log = "1" 36 | js-sys = "0.3" 37 | gloo-net = { version = "0.5", default-features = false, features = ["http"] } 38 | once_cell = "1.8" 39 | wasm-bindgen = "0.2.87" 40 | wasm-bindgen-futures = "0.4" 41 | web-sys = "0.3.67" 42 | 43 | [target.'cfg(target_os = "android")'.dependencies] 44 | ndk-glue = "0.7" 45 | -------------------------------------------------------------------------------- /rend3-framework/src/assets.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use rend3::util::typedefs::SsoString; 4 | use thiserror::Error; 5 | 6 | #[derive(Debug, Error)] 7 | pub enum AssetError { 8 | #[error("Could not read {path} from disk")] 9 | #[cfg(not(target_arch = "wasm32"))] 10 | FileError { 11 | path: SsoString, 12 | #[source] 13 | error: std::io::Error, 14 | }, 15 | #[cfg(target_arch = "wasm32")] 16 | #[error("Could not read {path} from network")] 17 | NetworkError { 18 | path: SsoString, 19 | #[source] 20 | error: gloo_net::Error, 21 | }, 22 | } 23 | 24 | pub enum AssetPath<'a> { 25 | Internal(&'a str), 26 | External(&'a str), 27 | } 28 | impl<'a> AssetPath<'a> { 29 | fn get_path(self, base: &str) -> Cow<'a, str> { 30 | match self { 31 | Self::Internal(p) => Cow::Owned(base.to_owned() + p), 32 | Self::External(p) => Cow::Borrowed(p), 33 | } 34 | } 35 | } 36 | 37 | pub struct AssetLoader { 38 | base: SsoString, 39 | } 40 | impl AssetLoader { 41 | pub fn new_local(_base_file: &str, _base_asset: &str, _base_url: &str) -> Self { 42 | cfg_if::cfg_if!( 43 | if #[cfg(target_arch = "wasm32")] { 44 | let base = _base_url; 45 | } else if #[cfg(target_os = "android")] { 46 | let base = _base_asset; 47 | } else { 48 | let base = _base_file; 49 | } 50 | ); 51 | 52 | Self { base: SsoString::from(base) } 53 | } 54 | 55 | pub fn get_asset_path<'a>(&self, path: AssetPath<'a>) -> Cow<'a, str> { 56 | path.get_path(&self.base) 57 | } 58 | 59 | #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] 60 | pub async fn get_asset(&self, path: AssetPath<'_>) -> Result, AssetError> { 61 | let full_path = path.get_path(&self.base); 62 | std::fs::read(&*full_path).map_err(|error| AssetError::FileError { path: SsoString::from(full_path), error }) 63 | } 64 | 65 | #[cfg(target_os = "android")] 66 | pub async fn get_asset(&self, path: AssetPath<'_>) -> Result, AssetError> { 67 | use std::ffi::CString; 68 | 69 | let manager = ndk_glue::native_activity().asset_manager(); 70 | 71 | let full_path = path.get_path(&self.base); 72 | manager 73 | .open(&CString::new(&*full_path).unwrap()) 74 | .ok_or_else(|| AssetError::FileError { 75 | path: SsoString::from(&*full_path), 76 | error: std::io::Error::new(std::io::ErrorKind::NotFound, "could not find file in asset manager"), 77 | }) 78 | .and_then(|mut file| { 79 | file.get_buffer() 80 | .map(|b| b.to_vec()) 81 | .map_err(|error| AssetError::FileError { path: SsoString::from(full_path), error }) 82 | }) 83 | } 84 | 85 | #[cfg(target_arch = "wasm32")] 86 | pub async fn get_asset(&self, path: AssetPath<'_>) -> Result, AssetError> { 87 | let full_path = path.get_path(&self.base); 88 | 89 | gloo_net::http::Request::get(&full_path) 90 | .build() 91 | .map_err(|error| AssetError::NetworkError { path: SsoString::from(&*full_path), error })? 92 | .send() 93 | .await 94 | .map_err(|error| AssetError::NetworkError { path: SsoString::from(&*full_path), error })? 95 | .binary() 96 | .await 97 | .map_err(|error| AssetError::NetworkError { path: SsoString::from(&*full_path), error }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /rend3-framework/src/grab.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_arch = "wasm32")] 2 | mod wasm; 3 | #[cfg(target_arch = "wasm32")] 4 | pub use wasm::*; 5 | 6 | #[cfg(not(target_arch = "wasm32"))] 7 | mod native; 8 | #[cfg(not(target_arch = "wasm32"))] 9 | pub use native::*; 10 | -------------------------------------------------------------------------------- /rend3-framework/src/grab/native.rs: -------------------------------------------------------------------------------- 1 | use winit::window::{CursorGrabMode, Window}; 2 | 3 | pub struct Grabber { 4 | grabbed: bool, 5 | } 6 | impl Grabber { 7 | pub fn new(_window: &Window) -> Self { 8 | Self { grabbed: false } 9 | } 10 | 11 | pub fn request_grab(&mut self, window: &Window) { 12 | let _ = window.set_cursor_grab(CursorGrabMode::Locked); 13 | let _ = window.set_cursor_grab(CursorGrabMode::Confined); 14 | window.set_cursor_visible(false); 15 | 16 | self.grabbed = true; 17 | } 18 | 19 | pub fn request_ungrab(&mut self, window: &Window) { 20 | let _ = window.set_cursor_grab(CursorGrabMode::None); 21 | window.set_cursor_visible(true); 22 | 23 | self.grabbed = false; 24 | } 25 | 26 | pub fn grabbed(&self) -> bool { 27 | self.grabbed 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rend3-framework/src/grab/wasm.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicBool, Ordering}, 3 | Arc, 4 | }; 5 | 6 | use js_sys::Function; 7 | use once_cell::race::OnceBox; 8 | use wasm_bindgen::{prelude::Closure, JsCast}; 9 | use winit::{platform::web::WindowExtWebSys, window::Window}; 10 | 11 | struct GrabberInner { 12 | grabbed: AtomicBool, 13 | callback: OnceBox, 14 | } 15 | 16 | pub struct Grabber { 17 | inner: Arc, 18 | } 19 | impl Grabber { 20 | pub fn new(window: &Window) -> Self { 21 | let inner = Arc::new(GrabberInner { grabbed: AtomicBool::new(false), callback: OnceBox::new() }); 22 | 23 | let inner_clone = Arc::clone(&inner); 24 | 25 | let canvas = window.canvas().unwrap(); 26 | let document = canvas.owner_document().unwrap(); 27 | 28 | let function: Box = Box::new(move || { 29 | if document.pointer_lock_element().as_ref() == Some(&*canvas) { 30 | log::info!("true"); 31 | inner_clone.grabbed.store(true, Ordering::Release); 32 | } else { 33 | log::info!("false"); 34 | document 35 | .remove_event_listener_with_callback("pointerlockchange", inner_clone.callback.get().unwrap()) 36 | .unwrap(); 37 | inner_clone.grabbed.store(false, Ordering::Release); 38 | } 39 | }); 40 | 41 | let closure = Closure::wrap(function); 42 | let closure_function = closure.into_js_value().dyn_into::().unwrap(); 43 | 44 | inner.callback.set(Box::new(closure_function)).unwrap(); 45 | 46 | Self { inner } 47 | } 48 | 49 | pub fn request_grab(&mut self, window: &Window) { 50 | let canvas = window.canvas().unwrap(); 51 | let document = canvas.owner_document().unwrap(); 52 | canvas.request_pointer_lock(); 53 | 54 | document.add_event_listener_with_callback("pointerlockchange", self.inner.callback.get().unwrap()).unwrap(); 55 | 56 | self.inner.grabbed.store(true, Ordering::Relaxed); 57 | } 58 | 59 | pub fn request_ungrab(&mut self, window: &Window) { 60 | let canvas = window.canvas().unwrap(); 61 | let document = canvas.owner_document().unwrap(); 62 | 63 | document.remove_event_listener_with_callback("pointerlockchange", self.inner.callback.get().unwrap()).unwrap(); 64 | 65 | document.exit_pointer_lock(); 66 | self.inner.grabbed.store(false, Ordering::Relaxed); 67 | } 68 | 69 | pub fn grabbed(&self) -> bool { 70 | self.inner.grabbed.load(Ordering::Acquire) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rend3-gltf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-gltf" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "gltf scene and model loader for the rend3 rendering library." 7 | readme = "../README.md" 8 | repository = "https://github.com/BVE-Reborn/rend3" 9 | license = "MIT OR Apache-2.0 OR Zlib" 10 | keywords = ["3d", "graphics", "rend3", "gltf", "wgpu"] 11 | categories = ["game-development", "graphics", "rendering", "rendering::engine", "wasm"] 12 | rust-version = "1.71" 13 | 14 | [features] 15 | default = ["ddsfile", "ktx2"] 16 | 17 | [dependencies] 18 | arrayvec = "0.7" 19 | base64 = "0.22" 20 | bytemuck = "1" 21 | ddsfile = { version = "0.5", optional = true } 22 | float-ord = "0.3.2" 23 | glam = "0.25" 24 | gltf = { version = "1.0", default-features = false, features = ["KHR_lights_punctual", "KHR_texture_transform", "KHR_materials_unlit", "extras", "names", "utils"] } 25 | image = { version = "0.24", default-features = false } 26 | ktx2 = { version = "0.3", optional = true } 27 | log = "0.4" 28 | profiling = {version = "1", default-features = false } 29 | rend3 = { version = "^0.3.0", path = "../rend3" } 30 | rend3-routine = { version = "^0.3.0", path = "../rend3-routine" } 31 | rustc-hash = "1" 32 | thiserror = "1" 33 | 34 | [dev-dependencies] 35 | pollster = "0.3" 36 | -------------------------------------------------------------------------------- /rend3-routine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-routine" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "Customizable Render Routines for the rend3 rendering library." 7 | readme = "../README.md" 8 | repository = "https://github.com/BVE-Reborn/rend3" 9 | license = "MIT OR Apache-2.0 OR Zlib" 10 | keywords = ["3d", "graphics", "rend3", "renderer", "wgpu"] 11 | categories = ["game-development", "graphics", "rendering", "rendering::engine", "wasm"] 12 | rust-version = "1.71" 13 | 14 | [dependencies] 15 | arrayvec = "0.7" 16 | bitflags = "2" 17 | bytemuck = "1" 18 | codespan-reporting = "0.11" 19 | encase = { version = "0.7", features = ["glam"] } 20 | flume = "0.11" 21 | glam = { version = "0.25.0", features = ["bytemuck"] } 22 | log = "0.4" 23 | naga = { version = "0.19.0", features = ["wgsl-in"] } 24 | ordered-float = "4" 25 | parking_lot = "0.12" 26 | profiling = {version = "1", default-features = false } 27 | rend3 = { version = "^0.3.0", path = "../rend3" } 28 | rust-embed = { version = "8", features = ["interpolate-folder-path"] } 29 | serde = { version = "1", features = ["derive"] } 30 | serde_json = "1" 31 | wgpu = "0.19.0" 32 | wgpu-profiler = "0.16.0" 33 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/blit.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/math/color.wgsl"}} 2 | 3 | struct VertexOutput { 4 | @builtin(position) position: vec4, 5 | @location(0) tex_coords: vec2, 6 | } 7 | 8 | @vertex 9 | fn vs_main(@builtin(vertex_index) id: u32) -> VertexOutput { 10 | var output: VertexOutput; 11 | output.position = vec4(f32(id / 2u) * 4.0 - 1.0, f32(id % 2u) * 4.0 - 1.0, 0.0, 1.0); 12 | output.tex_coords = vec2(f32(id / 2u) * 2.0, 1.0 - (f32(id % 2u) * 2.0)); 13 | return output; 14 | } 15 | 16 | @group(0) @binding(0) 17 | var primary_sampler: sampler; 18 | @group(1) @binding(0) 19 | var source: texture_2d; 20 | 21 | @fragment 22 | fn fs_main_scene(vout: VertexOutput) -> @location(0) vec4 { 23 | var sampled = textureSample(source, primary_sampler, vout.tex_coords); 24 | return sampled; 25 | } 26 | 27 | @fragment 28 | fn fs_main_monitor(vout: VertexOutput) -> @location(0) vec4 { 29 | var sampled = textureSample(source, primary_sampler, vout.tex_coords); 30 | return vec4(srgb_scene_to_display(sampled.rgb), sampled.a); 31 | } -------------------------------------------------------------------------------- /rend3-routine/shaders/src/depth.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/structures.wgsl"}} 2 | {{include "rend3-routine/structures_object.wgsl"}} 3 | {{include "rend3-routine/material.wgsl"}} 4 | 5 | @group(0) @binding(0) 6 | var primary_sampler: sampler; 7 | @group(0) @binding(3) 8 | var uniforms: UniformData; 9 | 10 | @group(1) @binding(0) 11 | var object_buffer: array; 12 | @group(1) @binding(1) 13 | var vertex_buffer: array; 14 | @group(1) @binding(2) 15 | var per_camera_uniform: PerCameraUniform; 16 | 17 | {{#if (eq profile "GpuDriven")}} 18 | @group(1) @binding(3) 19 | var materials: array; 20 | @group(2) @binding(0) 21 | var textures: binding_array>; 22 | {{/if}} 23 | 24 | {{#if (eq profile "CpuDriven")}} 25 | @group(1) @binding(3) 26 | var materials: array; 27 | @group(2) @binding(0) 28 | var albedo_tex: texture_2d; 29 | {{/if}} 30 | 31 | {{ 32 | vertex_fetch 33 | 34 | object_buffer 35 | 36 | position 37 | texture_coords_0 38 | color_0 39 | }} 40 | 41 | struct VertexOutput { 42 | @builtin(position) position: vec4, 43 | @location(0) coords0: vec2, 44 | @location(1) alpha: f32, 45 | @location(2) @interpolate(flat) material: u32, 46 | } 47 | 48 | @vertex 49 | fn vs_main(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) vertex_index: u32) -> VertexOutput { 50 | let indices = Indices(instance_index, vertex_index); 51 | 52 | let data = object_buffer[indices.object]; 53 | 54 | let vs_in = get_vertices(indices); 55 | 56 | let model_view_proj = per_camera_uniform.view_proj * object_buffer[indices.object].transform; 57 | 58 | let position_vec4 = vec4(vs_in.position, 1.0); 59 | 60 | var vs_out: VertexOutput; 61 | vs_out.material = data.material_index; 62 | vs_out.coords0 = vs_in.texture_coords_0; 63 | vs_out.alpha = vs_in.color_0.a; 64 | vs_out.position = model_view_proj * position_vec4; 65 | 66 | return vs_out; 67 | } 68 | 69 | {{#if (eq profile "GpuDriven")}} 70 | alias Material = GpuMaterialData; 71 | 72 | fn has_albedo_texture(material: ptr) -> bool { return (*material).albedo_tex != 0u; } 73 | 74 | fn albedo_texture(material: ptr, samp: sampler, coords: vec2, ddx: vec2, ddy: vec2) -> vec4 { return textureSampleGrad(textures[(*material).albedo_tex - 1u], samp, coords, ddx, ddy); } 75 | {{else}} 76 | alias Material = CpuMaterialData; 77 | 78 | fn has_albedo_texture(material: ptr) -> bool { return bool(((*material).texture_enable >> 0u) & 0x1u); } 79 | 80 | fn albedo_texture(material: ptr, samp: sampler, coords: vec2, ddx: vec2, ddy: vec2) -> vec4 { return textureSampleGrad(albedo_tex, samp, coords, ddx, ddy); } 81 | {{/if}} 82 | 83 | @fragment 84 | fn fs_main(vs_out: VertexOutput) { 85 | {{#if discard}} 86 | var material = materials[vs_out.material]; 87 | 88 | let coords = vs_out.coords0; 89 | let uvdx = dpdx(coords); 90 | let uvdy = dpdx(coords); 91 | 92 | var alpha = 1.0; 93 | if (extract_material_flag(material.flags, FLAGS_ALBEDO_ACTIVE)) { 94 | if (has_albedo_texture(&material)) { 95 | alpha = albedo_texture(&material, primary_sampler, coords, uvdx, uvdy).a; 96 | } 97 | if (extract_material_flag(material.flags, FLAGS_ALBEDO_BLEND)) { 98 | alpha *= vs_out.alpha; 99 | } 100 | } 101 | alpha *= material.albedo.a; 102 | 103 | if (alpha < material.alpha_cutout) { 104 | discard; 105 | } 106 | {{/if}} 107 | } 108 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/material.wgsl: -------------------------------------------------------------------------------- 1 | const FLAGS_ALBEDO_ACTIVE: u32 = 0x0001u; 2 | const FLAGS_ALBEDO_BLEND: u32 = 0x0002u; 3 | const FLAGS_ALBEDO_VERTEX_SRGB: u32 = 0x0004u; 4 | const FLAGS_BICOMPONENT_NORMAL: u32 = 0x0008u; 5 | const FLAGS_SWIZZLED_NORMAL: u32 = 0x0010u; 6 | const FLAGS_YDOWN_NORMAL: u32 = 0x0020u; 7 | const FLAGS_AOMR_COMBINED: u32 = 0x0040u; 8 | const FLAGS_AOMR_SWIZZLED_SPLIT: u32 = 0x0080u; 9 | const FLAGS_AOMR_SPLIT: u32 = 0x0100u; 10 | const FLAGS_AOMR_BW_SPLIT: u32 = 0x0200u; 11 | const FLAGS_CC_GLTF_COMBINED: u32 = 0x0400u; 12 | const FLAGS_CC_GLTF_SPLIT: u32 = 0x0800u; 13 | const FLAGS_CC_BW_SPLIT: u32 = 0x1000u; 14 | const FLAGS_UNLIT: u32 = 0x2000u; 15 | const FLAGS_NEAREST: u32 = 0x4000u; 16 | 17 | fn extract_material_flag(data: u32, flag: u32) -> bool { 18 | return bool(data & flag); 19 | } 20 | 21 | struct GpuMaterialData { 22 | albedo_tex: u32, 23 | normal_tex: u32, 24 | roughness_tex: u32, 25 | metallic_tex: u32, 26 | // -- 16 -- 27 | reflectance_tex: u32, 28 | clear_coat_tex: u32, 29 | clear_coat_roughness_tex: u32, 30 | emissive_tex: u32, 31 | // -- 16 -- 32 | anisotropy_tex: u32, 33 | ambient_occlusion_tex: u32, 34 | _padding0: u32, 35 | _padding1: u32, 36 | 37 | // -- 16 -- 38 | 39 | uv_transform0: mat3x3, 40 | // -- 16 -- 41 | uv_transform1: mat3x3, 42 | // -- 16 -- 43 | albedo: vec4, 44 | // -- 16 -- 45 | emissive: vec3, 46 | roughness: f32, 47 | // -- 16 -- 48 | metallic: f32, 49 | reflectance: f32, 50 | clear_coat: f32, 51 | clear_coat_roughness: f32, 52 | // -- 16 -- 53 | anisotropy: f32, 54 | ambient_occlusion: f32, 55 | alpha_cutout: f32, 56 | flags: u32, 57 | } 58 | 59 | struct CpuMaterialData { 60 | uv_transform0: mat3x3, 61 | // -- 16 -- 62 | uv_transform1: mat3x3, 63 | // -- 16 -- 64 | albedo: vec4, 65 | // -- 16 -- 66 | emissive: vec3, 67 | roughness: f32, 68 | // -- 16 -- 69 | metallic: f32, 70 | reflectance: f32, 71 | clear_coat: f32, 72 | clear_coat_roughness: f32, 73 | // -- 16 -- 74 | anisotropy: f32, 75 | ambient_occlusion: f32, 76 | alpha_cutout: f32, 77 | flags: u32, 78 | 79 | // -- 16 -- 80 | texture_enable: u32, 81 | }; 82 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/math/brdf.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/math/consts.wgsl"}} 2 | 3 | fn brdf_d_ggx(noh: f32, a: f32) -> f32 { 4 | let a2 = a * a; 5 | let f = (noh * a2 - noh) * noh + 1.0; 6 | return a2 / (PI * f * f); 7 | } 8 | 9 | fn brdf_f_schlick_vec3(u: f32, f0: vec3, f90: f32) -> vec3 { 10 | return f0 + (f90 - f0) * pow(1.0 - u, 5.0); 11 | } 12 | 13 | fn brdf_f_schlick_f32(u: f32, f0: f32, f90: f32) -> f32 { 14 | return f0 + (f90 - f0) * pow(1.0 - u, 5.0); 15 | } 16 | 17 | fn brdf_fd_burley(nov: f32, nol: f32, loh: f32, roughness: f32) -> f32 { 18 | let f90 = 0.5 + 2.0 * roughness * loh * loh; 19 | let light_scatter = brdf_f_schlick_f32(nol, 1.0, f90); 20 | let view_scatter = brdf_f_schlick_f32(nov, 1.0, f90); 21 | return light_scatter * view_scatter * (1.0 / PI); 22 | } 23 | 24 | fn brdf_fd_lambert() -> f32 { 25 | return 1.0 / PI; 26 | } 27 | 28 | fn brdf_v_smith_ggx_correlated(nov: f32, nol: f32, a: f32) -> f32 { 29 | let a2 = a * a; 30 | let ggxl = nov * sqrt((-nol * a2 + nol) * nol + a2); 31 | let ggxv = nol * sqrt((-nov * a2 + nov) * nov + a2); 32 | return 0.5 / (ggxl + ggxv); 33 | } 34 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/math/color.wgsl: -------------------------------------------------------------------------------- 1 | // The SRGB EOTF 2 | // aka "srgb_to_linear" but with a better name 3 | fn srgb_display_to_scene(electro: vec3) -> vec3 { 4 | let selector = electro > vec3(0.04045); 5 | let under = electro / 12.92; 6 | let over = pow((electro + 0.055) / 1.055, vec3(2.4)); 7 | let optical = select(under, over, selector); 8 | return optical; 9 | } 10 | 11 | // The SRGB OETF 12 | // aka "linear_to_srgb" but with a better name 13 | fn srgb_scene_to_display(opto: vec3) -> vec3 { 14 | let selector = opto > vec3(0.0031308); 15 | let under = opto * 12.92; 16 | let over = 1.055 * pow(opto, vec3(0.4166)) - 0.055; 17 | let electrical = select(under, over, selector); 18 | return electrical; 19 | } 20 | 21 | fn saturate(v: f32) -> f32 { 22 | return clamp(v, 0.0, 1.0); 23 | } 24 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/math/consts.wgsl: -------------------------------------------------------------------------------- 1 | const PI = 3.14159265359; 2 | const PI_2 = 1.570796327; 3 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/math/frustum.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/math/sphere.wgsl"}} 2 | 3 | struct Plane { 4 | inner: vec4, 5 | } 6 | 7 | fn plane_distance_to_point(plane: Plane, pint: vec3) -> f32 { 8 | return dot(plane.inner.xyz, pint) + plane.inner.w; 9 | } 10 | 11 | struct Frustum { 12 | left: Plane, 13 | right: Plane, 14 | top: Plane, 15 | bottom: Plane, 16 | near: Plane, 17 | } 18 | 19 | fn frustum_contains_sphere(frustum: Frustum, sphere: Sphere) -> bool { 20 | let neg_radius = -sphere.radius; 21 | 22 | if (!(plane_distance_to_point(frustum.left, sphere.location) >= neg_radius)) { 23 | return false; 24 | } 25 | if (!(plane_distance_to_point(frustum.right, sphere.location) >= neg_radius)) { 26 | return false; 27 | } 28 | if (!(plane_distance_to_point(frustum.top, sphere.location) >= neg_radius)) { 29 | return false; 30 | } 31 | if (!(plane_distance_to_point(frustum.bottom, sphere.location) >= neg_radius)) { 32 | return false; 33 | } 34 | if (!(plane_distance_to_point(frustum.near, sphere.location) >= neg_radius)) { 35 | return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/math/matrix.wgsl: -------------------------------------------------------------------------------- 1 | fn mat3_inv_scale_squared(transform: mat3x3) -> vec3 { 2 | return vec3( 3 | 1.0 / dot(transform[0].xyz, transform[0].xyz), 4 | 1.0 / dot(transform[1].xyz, transform[1].xyz), 5 | 1.0 / dot(transform[2].xyz, transform[2].xyz) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/math/sphere.wgsl: -------------------------------------------------------------------------------- 1 | struct Sphere { 2 | location: vec3, 3 | radius: f32, 4 | } 5 | 6 | fn sphere_transform_by_mat4(sphere: Sphere, transform: mat4x4) -> Sphere { 7 | let length0 = length(transform[0].xyz); 8 | let length1 = length(transform[1].xyz); 9 | let length2 = length(transform[2].xyz); 10 | let max_scale = max(max(length0, length1), length2); 11 | let center = (transform * vec4(sphere.location, 1.0)).xyz; 12 | let radius = sphere.radius * max_scale; 13 | 14 | return Sphere(center, radius); 15 | } 16 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/shadow/pcf.wgsl: -------------------------------------------------------------------------------- 1 | fn shadow_sample_pcf5(tex: texture_depth_2d, samp: sampler_comparison, coords: vec2, depth: f32) -> f32 { 2 | var result: f32 = 0.0; 3 | result = result + textureSampleCompareLevel(tex, samp, coords, depth); 4 | result = result + textureSampleCompareLevel(tex, samp, coords, depth, vec2( 0, 1)); 5 | result = result + textureSampleCompareLevel(tex, samp, coords, depth, vec2( 0, -1)); 6 | result = result + textureSampleCompareLevel(tex, samp, coords, depth, vec2( 1, 0)); 7 | result = result + textureSampleCompareLevel(tex, samp, coords, depth, vec2(-1, 0)); 8 | return result * 0.2; 9 | } 10 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/skinning.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/math/matrix.wgsl"}} 2 | 3 | struct SkinningInput { 4 | /// Byte offset into vertex buffer of position attribute of unskinned mesh. 5 | base_position_offset: u32, 6 | /// Byte offset into vertex buffer of normal attribute of unskinned mesh. 7 | base_normal_offset: u32, 8 | /// Byte offset into vertex buffer of tangent attribute of unskinned mesh. 9 | base_tangent_offset: u32, 10 | /// Byte offset into vertex buffer of joint indices of mesh. 11 | joint_indices_offset: u32, 12 | /// Byte offset into vertex buffer of joint weights of mesh. 13 | joint_weight_offset: u32, 14 | /// Byte offset into vertex buffer of position attribute of skinned mesh. 15 | updated_position_offset: u32, 16 | /// Byte offset into vertex buffer of normal attribute of skinned mesh. 17 | updated_normal_offset: u32, 18 | /// Byte offset into vertex buffer of tangent attribute of skinned mesh. 19 | updated_tangent_offset: u32, 20 | 21 | /// Index into the matrix buffer that joint_indices is relative to. 22 | joint_matrix_base_offset: u32, 23 | /// Count of vertices in this mesh. 24 | vertex_count: u32, 25 | } 26 | 27 | @group(0) @binding(0) 28 | var vertex_buffer: array; 29 | @group(0) @binding(1) 30 | var input: SkinningInput; 31 | @group(0) @binding(2) 32 | var joint_matrices: array>; 33 | 34 | {{include "rend3/vertex_attributes.wgsl"}} 35 | {{include "rend3/vertex_attributes_store.wgsl"}} 36 | 37 | @compute @workgroup_size(256) 38 | fn main(@builtin(global_invocation_id) global_id: vec3) { 39 | let idx = global_id.x; 40 | 41 | if (idx >= input.vertex_count) { 42 | return; 43 | } 44 | 45 | let joint_indices = extract_attribute_vec4_u16(input.joint_indices_offset, idx); 46 | let joint_weights = extract_attribute_vec4_f32(input.joint_weight_offset, idx); 47 | 48 | // Compute the skinned position 49 | var pos_acc = vec3(0.0); 50 | var norm_acc = vec3(0.0); 51 | var tang_acc = vec3(0.0); 52 | 53 | var pos = vec3(0.0); 54 | var normal = vec3(0.0); 55 | var tangent = vec3(0.0); 56 | if (input.base_position_offset != 0xFFFFFFFFu) { 57 | pos = extract_attribute_vec3_f32(input.base_position_offset, idx); 58 | } 59 | if (input.base_normal_offset != 0xFFFFFFFFu) { 60 | normal = extract_attribute_vec3_f32(input.base_normal_offset, idx); 61 | } 62 | if (input.base_tangent_offset != 0xFFFFFFFFu) { 63 | tangent = extract_attribute_vec3_f32(input.base_tangent_offset, idx); 64 | } 65 | 66 | for (var i = 0; i < 4; i++) { 67 | let weight = joint_weights[i]; 68 | 69 | if (weight > 0.0) { 70 | let joint_index = joint_indices[i]; 71 | let joint_matrix = joint_matrices[input.joint_matrix_base_offset + joint_index]; 72 | let joint_matrix3 = mat3x3(joint_matrix[0].xyz, joint_matrix[1].xyz, joint_matrix[2].xyz); 73 | pos_acc += (joint_matrix * vec4(pos, 1.0)).xyz * weight; 74 | 75 | let inv_scale_sq = mat3_inv_scale_squared(joint_matrix3); 76 | norm_acc += (joint_matrix3 * (inv_scale_sq * normal)) * weight; 77 | tang_acc += (joint_matrix3 * (inv_scale_sq * tangent)) * weight; 78 | } 79 | } 80 | 81 | norm_acc = normalize(norm_acc); 82 | tang_acc = normalize(tang_acc); 83 | 84 | // Write to output region of buffer 85 | if (input.updated_position_offset != 0xFFFFFFFFu) { 86 | store_attribute_vec3_f32(input.updated_position_offset, idx, pos_acc); 87 | } 88 | if (input.updated_normal_offset != 0xFFFFFFFFu) { 89 | store_attribute_vec3_f32(input.updated_normal_offset, idx, norm_acc); 90 | } 91 | if (input.updated_tangent_offset != 0xFFFFFFFFu) { 92 | store_attribute_vec3_f32(input.updated_tangent_offset, idx, tang_acc); 93 | } 94 | } -------------------------------------------------------------------------------- /rend3-routine/shaders/src/skybox.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/structures.wgsl"}} 2 | 3 | struct VertexOutput { 4 | @builtin(position) pos: vec4, 5 | @location(0) clip_position: vec2, 6 | } 7 | 8 | @vertex 9 | fn vs_main(@builtin(vertex_index) id: u32) -> VertexOutput { 10 | let clip_position = vec2(f32(id / 2u) * 4.0 - 1.0, f32(id % 2u) * 4.0 - 1.0); 11 | 12 | return VertexOutput(vec4(clip_position, 0.0, 1.0), clip_position); 13 | } 14 | 15 | @group(0) @binding(0) 16 | var primary_sampler: sampler; 17 | @group(0) @binding(3) 18 | var uniforms: UniformData; 19 | @group(1) @binding(0) 20 | var skybox: texture_cube; 21 | 22 | @fragment 23 | fn fs_main(output: VertexOutput) -> @location(0) vec4 { 24 | // We use the near plane as depth here, as if we used the far plane, it would all NaN out. Doesn't _really_ matter, 25 | // but 1.0 is a nice round number and results in a depth of 0.1 with my near plane. Good 'nuf. 26 | let clip = vec4(output.clip_position, 1.0, 1.0); 27 | let world_undiv = uniforms.inv_origin_view_proj * clip; 28 | let world = world_undiv.xyz / world_undiv.w; 29 | let world_dir = normalize(world); 30 | 31 | let background = textureSample(skybox, primary_sampler, world_dir).rgb; 32 | 33 | return vec4(background, 1.0); 34 | } 35 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/structures.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/math/sphere.wgsl"}} 2 | {{include "rend3-routine/math/frustum.wgsl"}} 3 | 4 | struct ObjectInputData { 5 | start_idx: u32, 6 | count: u32, 7 | vertex_offset: i32, 8 | material_idx: u32, 9 | transform: mat4x4, 10 | bounding_sphere: Sphere, 11 | } 12 | 13 | struct ObjectOutputData { 14 | model_view: mat4x4, 15 | model_view_proj: mat4x4, 16 | material_idx: u32, 17 | inv_scale_sq: vec3, 18 | } 19 | 20 | struct IndirectCall { 21 | vertex_count: atomic, 22 | instance_count: u32, 23 | base_index: u32, 24 | vertex_offset: i32, 25 | base_instance: u32, 26 | } 27 | 28 | struct UniformData { 29 | view: mat4x4, 30 | view_proj: mat4x4, 31 | origin_view_proj: mat4x4, 32 | inv_view: mat4x4, 33 | inv_view_proj: mat4x4, 34 | inv_origin_view_proj: mat4x4, 35 | frustum: Frustum, 36 | ambient: vec4, 37 | resolution: vec2, 38 | } 39 | 40 | struct PerCameraUniform { 41 | // TODO: use less space 42 | view: mat4x4, 43 | // TODO: use less space 44 | view_proj: mat4x4, 45 | frustum: Frustum, 46 | object_count: u32, 47 | } 48 | 49 | struct DirectionalLight { 50 | /// View/Projection of directional light. Shadow rendering uses viewports 51 | /// so this always outputs [-1, 1] no matter where in the atlast the shadow is. 52 | view_proj: mat4x4, 53 | /// Color/intensity of the light 54 | color: vec3, 55 | /// Direction of the light 56 | direction: vec3, 57 | /// 1 / resolution of whole shadow map 58 | inv_resolution: vec2, 59 | /// [0, 1] offset of the shadow map in the atlas. 60 | offset: vec2, 61 | /// [0, 1] size of the shadow map in the atlas. 62 | size: vec2, 63 | } 64 | 65 | struct DirectionalLightData { 66 | count: u32, 67 | data: array, 68 | } 69 | 70 | struct PointLight { 71 | /// The position of the light in world space. 72 | position: vec4, 73 | // Color/intensity of the light. 74 | color: vec3, 75 | /// The radius of the light. 76 | radius: f32, 77 | } 78 | 79 | struct PointLightData { 80 | count: u32, 81 | data: array, 82 | } 83 | 84 | struct PixelData { 85 | albedo: vec4, 86 | diffuse_color: vec3, 87 | roughness: f32, 88 | normal: vec3, 89 | metallic: f32, 90 | f0: vec3, 91 | perceptual_roughness: f32, 92 | emissive: vec3, 93 | reflectance: f32, 94 | clear_coat: f32, 95 | clear_coat_roughness: f32, 96 | clear_coat_perceptual_roughness: f32, 97 | anisotropy: f32, 98 | ambient_occlusion: f32, 99 | material_flags: u32, 100 | } 101 | -------------------------------------------------------------------------------- /rend3-routine/shaders/src/structures_object.wgsl: -------------------------------------------------------------------------------- 1 | {{include "rend3-routine/math/sphere.wgsl"}} 2 | 3 | struct Object { 4 | transform: mat4x4, 5 | bounding_sphere: Sphere, 6 | first_index: u32, 7 | index_count: u32, 8 | material_index: u32, 9 | vertex_attribute_start_offsets: array, 10 | } 11 | -------------------------------------------------------------------------------- /rend3-routine/src/clear.rs: -------------------------------------------------------------------------------- 1 | use rend3::graph::{NodeResourceUsage, RenderGraph, RenderPassDepthTarget, RenderPassTargets, RenderTargetHandle}; 2 | 3 | /// Due to limitations of how we auto-clear buffers, we need to explicitly clear the shadow depth buffer. 4 | pub fn add_depth_clear_to_graph(graph: &mut RenderGraph<'_>, depth: RenderTargetHandle, depth_clear: f32) { 5 | let mut builder = graph.add_node("Clear Depth"); 6 | 7 | let _rpass_handle = builder.add_renderpass( 8 | RenderPassTargets { 9 | targets: vec![], 10 | depth_stencil: Some(RenderPassDepthTarget { 11 | target: depth, 12 | depth_clear: Some(depth_clear), 13 | stencil_clear: None, 14 | }), 15 | }, 16 | NodeResourceUsage::Output, 17 | ); 18 | 19 | builder.build(|_| ()) 20 | } 21 | -------------------------------------------------------------------------------- /rend3-routine/src/common/camera.rs: -------------------------------------------------------------------------------- 1 | /// Specifier representing which camera we're referring to. 2 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 3 | pub enum CameraSpecifier { 4 | Viewport, 5 | Shadow(u32), 6 | } 7 | 8 | impl CameraSpecifier { 9 | /// Returns `true` if the camera specifier is [`Viewport`]. 10 | /// 11 | /// [`Viewport`]: CameraSpecifier::Viewport 12 | #[must_use] 13 | pub fn is_viewport(&self) -> bool { 14 | matches!(self, Self::Viewport) 15 | } 16 | 17 | /// Returns `true` if the camera specifier is [`Shadow`]. 18 | /// 19 | /// [`Shadow`]: CameraSpecifier::Shadow 20 | #[must_use] 21 | pub fn is_shadow(&self) -> bool { 22 | matches!(self, Self::Shadow(..)) 23 | } 24 | 25 | /// Returns a shader compatible index for the camera, using u32::MAX for the viewport camera. 26 | #[must_use] 27 | pub fn to_shader_index(&self) -> u32 { 28 | match *self { 29 | Self::Viewport => u32::MAX, 30 | Self::Shadow(index) => { 31 | assert_ne!(index, u32::MAX, "Shadow camera index cannot be 0xFFFF_FFFF"); 32 | index 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rend3-routine/src/common/interfaces.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, mem, num::NonZeroU64}; 2 | 3 | use encase::ShaderType; 4 | use glam::{Mat4, Vec3}; 5 | use rend3::{ 6 | managers::{DirectionalLightManager, PointLightManager}, 7 | types::Material, 8 | util::bind_merge::BindGroupLayoutBuilder, 9 | }; 10 | use wgpu::{ 11 | BindGroupLayout, BindingType, BufferBindingType, Device, ShaderStages, TextureSampleType, TextureViewDimension, 12 | }; 13 | 14 | use crate::{ 15 | common::samplers::Samplers, 16 | uniforms::{FrameUniforms, PerCameraUniform}, 17 | }; 18 | 19 | /// Interfaces which are used throughout the whole frame. 20 | /// 21 | /// Contains the samplers, per frame uniforms, and directional light 22 | /// information. 23 | pub struct WholeFrameInterfaces { 24 | /// Includes everything excluding the directional light information to 25 | /// prevent cycles when rendering to shadow maps. 26 | pub depth_uniform_bgl: BindGroupLayout, 27 | /// Includes everything. 28 | pub forward_uniform_bgl: BindGroupLayout, 29 | } 30 | 31 | impl WholeFrameInterfaces { 32 | pub fn new(device: &Device) -> Self { 33 | profiling::scope!("ShaderInterfaces::new"); 34 | 35 | let mut uniform_bglb = BindGroupLayoutBuilder::new(); 36 | 37 | Samplers::add_to_bgl(&mut uniform_bglb); 38 | 39 | uniform_bglb.append( 40 | ShaderStages::VERTEX_FRAGMENT, 41 | BindingType::Buffer { 42 | ty: BufferBindingType::Uniform, 43 | has_dynamic_offset: false, 44 | min_binding_size: NonZeroU64::new(mem::size_of::() as _), 45 | }, 46 | None, 47 | ); 48 | 49 | DirectionalLightManager::add_to_bgl(&mut uniform_bglb); 50 | PointLightManager::add_to_bgl(&mut uniform_bglb); 51 | 52 | let shadow_uniform_bgl = uniform_bglb.build(device, Some("shadow uniform bgl")); 53 | 54 | // Shadow texture 55 | uniform_bglb.append( 56 | ShaderStages::FRAGMENT, 57 | BindingType::Texture { 58 | sample_type: TextureSampleType::Depth, 59 | view_dimension: TextureViewDimension::D2, 60 | multisampled: false, 61 | }, 62 | None, 63 | ); 64 | 65 | let forward_uniform_bgl = uniform_bglb.build(device, Some("forward uniform bgl")); 66 | 67 | Self { depth_uniform_bgl: shadow_uniform_bgl, forward_uniform_bgl } 68 | } 69 | } 70 | 71 | /// The input structure that the culling shaders/functions output and drawing 72 | /// shaders read. 73 | #[repr(C, align(16))] 74 | #[derive(Debug, Copy, Clone)] 75 | pub struct PerObjectDataAbi { 76 | pub model_view: Mat4, 77 | pub model_view_proj: Mat4, 78 | // Only read when GpuDriven. Materials are directly bound when CpuDriven. 79 | pub material_idx: u32, 80 | pub pad0: [u8; 12], 81 | pub inv_squared_scale: Vec3, 82 | } 83 | 84 | unsafe impl bytemuck::Pod for PerObjectDataAbi {} 85 | unsafe impl bytemuck::Zeroable for PerObjectDataAbi {} 86 | 87 | /// Interface which has all per-material-archetype data: the object output 88 | /// buffer and the gpu material buffer. 89 | pub struct PerMaterialArchetypeInterface { 90 | pub bgl: BindGroupLayout, 91 | _phantom: PhantomData, 92 | } 93 | impl PerMaterialArchetypeInterface { 94 | pub fn new(device: &Device) -> Self { 95 | let bgl = BindGroupLayoutBuilder::new() 96 | // Object data buffer 97 | .append( 98 | ShaderStages::VERTEX_FRAGMENT, 99 | BindingType::Buffer { 100 | ty: BufferBindingType::Storage { read_only: true }, 101 | has_dynamic_offset: false, 102 | min_binding_size: None, 103 | }, 104 | None, 105 | ) 106 | // Vertex buffer 107 | .append( 108 | ShaderStages::VERTEX_FRAGMENT, 109 | BindingType::Buffer { 110 | ty: BufferBindingType::Storage { read_only: true }, 111 | has_dynamic_offset: false, 112 | min_binding_size: Some(NonZeroU64::new(4).unwrap()), 113 | }, 114 | None, 115 | ) 116 | // Per-Camera uniforms 117 | .append( 118 | ShaderStages::VERTEX_FRAGMENT, 119 | BindingType::Buffer { 120 | ty: BufferBindingType::Storage { read_only: true }, 121 | has_dynamic_offset: false, 122 | min_binding_size: Some(PerCameraUniform::min_size()), 123 | }, 124 | None, 125 | ) 126 | // Material data 127 | .append( 128 | ShaderStages::VERTEX_FRAGMENT, 129 | BindingType::Buffer { 130 | ty: BufferBindingType::Storage { read_only: true }, 131 | has_dynamic_offset: false, 132 | min_binding_size: None, 133 | }, 134 | None, 135 | ) 136 | .build(device, Some("per material bgl")); 137 | 138 | Self { bgl, _phantom: PhantomData } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /rend3-routine/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common utilities used throughout the crate. 2 | 3 | mod camera; 4 | mod interfaces; 5 | mod samplers; 6 | 7 | pub use camera::*; 8 | pub use interfaces::*; 9 | pub use samplers::*; 10 | -------------------------------------------------------------------------------- /rend3-routine/src/common/samplers.rs: -------------------------------------------------------------------------------- 1 | use rend3::util::bind_merge::{BindGroupBuilder, BindGroupLayoutBuilder}; 2 | use wgpu::{ 3 | AddressMode, BindingType, CompareFunction, Device, FilterMode, Sampler, SamplerBindingType, SamplerDescriptor, 4 | ShaderStages, 5 | }; 6 | 7 | /// Container holding a variety of samplers. 8 | pub struct Samplers { 9 | /// Aniso 16 sampler 10 | pub linear: Sampler, 11 | /// Nearest neighbor sampler 12 | pub nearest: Sampler, 13 | /// Bilinear greater-or-equal comparison sampler 14 | pub shadow: Sampler, 15 | } 16 | 17 | impl Samplers { 18 | /// Create a new set of samplers with this device. 19 | pub fn new(device: &Device) -> Self { 20 | profiling::scope!("Samplers::new"); 21 | 22 | let linear = create_sampler(device, FilterMode::Linear, None); 23 | let nearest = create_sampler(device, FilterMode::Nearest, None); 24 | let shadow = create_sampler(device, FilterMode::Linear, Some(CompareFunction::GreaterEqual)); 25 | 26 | Self { linear, nearest, shadow } 27 | } 28 | 29 | /// Add the samplers to the given bind group layout builder. 30 | pub fn add_to_bgl(bglb: &mut BindGroupLayoutBuilder) { 31 | bglb.append(ShaderStages::FRAGMENT, BindingType::Sampler(SamplerBindingType::Filtering), None) 32 | .append(ShaderStages::FRAGMENT, BindingType::Sampler(SamplerBindingType::NonFiltering), None) 33 | .append(ShaderStages::FRAGMENT, BindingType::Sampler(SamplerBindingType::Comparison), None); 34 | } 35 | 36 | /// Add the samplers to the given bind group builder. 37 | pub fn add_to_bg<'a>(&'a self, bgb: &mut BindGroupBuilder<'a>) { 38 | bgb.append_sampler(&self.linear).append_sampler(&self.nearest).append_sampler(&self.shadow); 39 | } 40 | } 41 | 42 | fn create_sampler(device: &Device, filter: FilterMode, compare: Option) -> Sampler { 43 | device.create_sampler(&SamplerDescriptor { 44 | label: Some("linear"), 45 | address_mode_u: AddressMode::Repeat, 46 | address_mode_v: AddressMode::Repeat, 47 | address_mode_w: AddressMode::Repeat, 48 | mag_filter: filter, 49 | min_filter: filter, 50 | mipmap_filter: filter, 51 | lod_min_clamp: 0.0, 52 | lod_max_clamp: 100.0, 53 | compare, 54 | anisotropy_clamp: 1, 55 | border_color: None, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /rend3-routine/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))] 2 | 3 | //! Render Routines for the rend3 3D renderer library. 4 | //! 5 | //! The routines in this crate provide powerful default routines as well as 6 | //! building blocks for writing your own custom render routines. 7 | //! 8 | //! # Getting Started 9 | //! 10 | //! The starting point when using this crate is 11 | //! [`BaseRenderGraph`](base::BaseRenderGraph), which provides a 12 | //! fully-put-together rendergraph including the PBR impl, skybox renderer, and 13 | //! tonemapper. 14 | //! 15 | //! As you reach for more customization, you can copy 16 | //! [`BaseRenderGraph::add_to_graph`](base::BaseRenderGraph::add_to_graph) into 17 | //! your own code and adding/modifying the routine to your hearts content. The 18 | //! abstraction is designed to be easily replaced and extended without needing 19 | //! too much user side boilerplate. 20 | 21 | pub mod base; 22 | pub mod clear; 23 | pub mod common; 24 | pub mod forward; 25 | pub mod pbr; 26 | mod shaders; 27 | pub mod skinning; 28 | pub mod skybox; 29 | pub mod tonemapping; 30 | pub mod uniforms; 31 | 32 | pub use shaders::builtin_shaders; 33 | -------------------------------------------------------------------------------- /rend3-routine/src/pbr/mod.rs: -------------------------------------------------------------------------------- 1 | //! Realism-focused PBR rendering routines and material. 2 | 3 | mod material; 4 | mod routine; 5 | 6 | pub use material::*; 7 | pub use routine::*; 8 | -------------------------------------------------------------------------------- /rend3-routine/src/pbr/routine.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, sync::Arc}; 2 | 3 | use rend3::{Renderer, RendererDataCore, RendererProfile, ShaderPreProcessor, ShaderVertexBufferConfig}; 4 | use serde::Serialize; 5 | use wgpu::{BlendState, ShaderModuleDescriptor, ShaderSource}; 6 | 7 | use crate::{ 8 | common::{PerMaterialArchetypeInterface, WholeFrameInterfaces}, 9 | forward::{ForwardRoutine, ForwardRoutineCreateArgs, RoutineType, ShaderModulePair}, 10 | pbr::{PbrMaterial, TransparencyType}, 11 | }; 12 | 13 | #[derive(Serialize)] 14 | struct BlendModeWrapper { 15 | profile: RendererProfile, 16 | discard: bool, 17 | } 18 | 19 | /// Render routine that renders the using PBR materials 20 | pub struct PbrRoutine { 21 | pub opaque_depth: ForwardRoutine, 22 | pub cutout_depth: ForwardRoutine, 23 | pub opaque_routine: ForwardRoutine, 24 | pub cutout_routine: ForwardRoutine, 25 | pub blend_routine: ForwardRoutine, 26 | pub per_material: PerMaterialArchetypeInterface, 27 | } 28 | 29 | impl PbrRoutine { 30 | pub fn new( 31 | renderer: &Arc, 32 | data_core: &mut RendererDataCore, 33 | spp: &ShaderPreProcessor, 34 | interfaces: &WholeFrameInterfaces, 35 | ) -> Self { 36 | profiling::scope!("PbrRenderRoutine::new"); 37 | 38 | // This ensures the BGLs for the material are created 39 | data_core.material_manager.ensure_archetype::(&renderer.device, renderer.profile); 40 | 41 | let per_material = PerMaterialArchetypeInterface::::new(&renderer.device); 42 | 43 | let pbr_depth_cutout = renderer.device.create_shader_module(ShaderModuleDescriptor { 44 | label: Some("pbr depth cutout sm"), 45 | source: ShaderSource::Wgsl(Cow::Owned( 46 | spp.render_shader( 47 | "rend3-routine/depth.wgsl", 48 | &BlendModeWrapper { profile: renderer.profile, discard: true }, 49 | Some(&ShaderVertexBufferConfig::from_material::()), 50 | ) 51 | .unwrap(), 52 | )), 53 | }); 54 | 55 | let pbr_depth = renderer.device.create_shader_module(ShaderModuleDescriptor { 56 | label: Some("pbr depth sm"), 57 | source: ShaderSource::Wgsl(Cow::Owned( 58 | spp.render_shader( 59 | "rend3-routine/depth.wgsl", 60 | &BlendModeWrapper { profile: renderer.profile, discard: false }, 61 | Some(&ShaderVertexBufferConfig::from_material::()), 62 | ) 63 | .unwrap(), 64 | )), 65 | }); 66 | 67 | let pbr_cutout = renderer.device.create_shader_module(ShaderModuleDescriptor { 68 | label: Some("pbr opaque cutout sm"), 69 | source: ShaderSource::Wgsl(Cow::Owned( 70 | spp.render_shader( 71 | "rend3-routine/opaque.wgsl", 72 | &BlendModeWrapper { profile: renderer.profile, discard: true }, 73 | Some(&ShaderVertexBufferConfig::from_material::()), 74 | ) 75 | .unwrap(), 76 | )), 77 | }); 78 | 79 | let pbr_forward = renderer.device.create_shader_module(ShaderModuleDescriptor { 80 | label: Some("pbr opaque sm"), 81 | source: ShaderSource::Wgsl(Cow::Owned( 82 | spp.render_shader( 83 | "rend3-routine/opaque.wgsl", 84 | &BlendModeWrapper { profile: renderer.profile, discard: false }, 85 | Some(&ShaderVertexBufferConfig::from_material::()), 86 | ) 87 | .unwrap(), 88 | )), 89 | }); 90 | 91 | let mut inner = |routine_type, module, transparency| { 92 | ForwardRoutine::new(ForwardRoutineCreateArgs { 93 | name: &format!("pbr {routine_type:?} {transparency:?}"), 94 | renderer, 95 | data_core, 96 | spp, 97 | interfaces, 98 | per_material: &per_material, 99 | material_key: transparency as u64, 100 | routine_type, 101 | shaders: ShaderModulePair { 102 | vs_entry: "vs_main", 103 | vs_module: module, 104 | fs_entry: "fs_main", 105 | fs_module: module, 106 | }, 107 | extra_bgls: &[], 108 | descriptor_callback: Some(&|desc, targets| { 109 | if transparency == TransparencyType::Blend { 110 | desc.depth_stencil.as_mut().unwrap().depth_write_enabled = false; 111 | targets[0].as_mut().unwrap().blend = Some(BlendState::ALPHA_BLENDING) 112 | } 113 | }), 114 | }) 115 | }; 116 | 117 | Self { 118 | opaque_depth: inner(RoutineType::Depth, &pbr_depth, TransparencyType::Opaque), 119 | cutout_depth: inner(RoutineType::Depth, &pbr_depth_cutout, TransparencyType::Cutout), 120 | opaque_routine: inner(RoutineType::Forward, &pbr_forward, TransparencyType::Opaque), 121 | cutout_routine: inner(RoutineType::Forward, &pbr_cutout, TransparencyType::Cutout), 122 | blend_routine: inner(RoutineType::Forward, &pbr_forward, TransparencyType::Blend), 123 | per_material, 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /rend3-routine/src/shaders.rs: -------------------------------------------------------------------------------- 1 | //! Holds the shader processing infrastructure for all shaders. 2 | use rend3::ShaderPreProcessor; 3 | use rust_embed::RustEmbed; 4 | 5 | #[derive(RustEmbed)] 6 | #[folder = "$CARGO_MANIFEST_DIR/shaders/src"] 7 | struct Rend3RoutineShaderSources; 8 | 9 | pub fn builtin_shaders(spp: &mut ShaderPreProcessor) { 10 | spp.add_shaders_embed::("rend3-routine"); 11 | } 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | use std::error::Error; 16 | 17 | use codespan_reporting::{ 18 | diagnostic::{Diagnostic, Label}, 19 | files::SimpleFile, 20 | term::{ 21 | self, 22 | termcolor::{ColorChoice, StandardStream}, 23 | }, 24 | }; 25 | use naga::WithSpan; 26 | use rend3::{RendererProfile, ShaderPreProcessor, ShaderVertexBufferConfig}; 27 | use serde_json::json; 28 | 29 | use crate::{pbr::PbrMaterial, shaders::Rend3RoutineShaderSources}; 30 | 31 | fn print_err(error: &dyn Error) { 32 | eprint!("{}", error); 33 | 34 | let mut e = error.source(); 35 | if e.is_some() { 36 | eprintln!(": "); 37 | } else { 38 | eprintln!(); 39 | } 40 | 41 | while let Some(source) = e { 42 | eprintln!("\t{}", source); 43 | e = source.source(); 44 | } 45 | } 46 | 47 | pub fn emit_annotated_error(ann_err: &WithSpan, filename: &str, source: &str) { 48 | let files = SimpleFile::new(filename, source); 49 | let config = codespan_reporting::term::Config::default(); 50 | let writer = StandardStream::stderr(ColorChoice::Auto); 51 | 52 | let diagnostic = Diagnostic::error().with_labels( 53 | ann_err 54 | .spans() 55 | .map(|(span, desc)| Label::primary((), span.to_range().unwrap()).with_message(desc.to_owned())) 56 | .collect(), 57 | ); 58 | 59 | term::emit(&mut writer.lock(), &config, &files, &diagnostic).expect("cannot write error"); 60 | } 61 | 62 | #[test] 63 | fn validate_inherent_shaders() { 64 | let mut pp = ShaderPreProcessor::new(); 65 | pp.add_shaders_embed::("rend3-routine"); 66 | 67 | for shader in pp.files() { 68 | if !shader.contains(".wgsl") { 69 | continue; 70 | } 71 | 72 | let source = pp.get(shader).unwrap(); 73 | let configs = if source.contains("#if") { 74 | vec![ 75 | json!({ 76 | "profile": Some(RendererProfile::GpuDriven), 77 | "position_attribute_offset": 0, 78 | "SAMPLES": 1, 79 | }), 80 | json!({ 81 | "profile": Some(RendererProfile::CpuDriven), 82 | "position_attribute_offset": 0, 83 | "SAMPLES": 1, 84 | }), 85 | ] 86 | } else { 87 | vec![json!({ 88 | "profile": Some(RendererProfile::CpuDriven), 89 | "position_attribute_offset": 0, 90 | "SAMPLES": 1, 91 | })] 92 | }; 93 | 94 | if source.contains("DO NOT VALIDATE") { 95 | continue; 96 | } 97 | 98 | for config in configs { 99 | println!("Testing shader {shader} with config {config:?}"); 100 | 101 | let output = 102 | pp.render_shader(shader, &config, Some(&ShaderVertexBufferConfig::from_material::())); 103 | 104 | assert!(output.is_ok(), "Expected preprocessing success, got {output:?}"); 105 | let output = output.unwrap_or_else(|e| panic!("Expected preprocessing success, got {e:?}")); 106 | 107 | let sm = match naga::front::wgsl::parse_str(&output) { 108 | Ok(m) => m, 109 | Err(e) => { 110 | e.emit_to_stderr_with_path(&output, shader); 111 | panic!(); 112 | } 113 | }; 114 | 115 | let mut validator = 116 | naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::all()); 117 | 118 | match validator.validate(&sm) { 119 | Ok(_) => {} 120 | Err(err) => { 121 | emit_annotated_error(&err, shader, &output); 122 | print_err(&err); 123 | panic!() 124 | } 125 | }; 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /rend3-routine/src/uniforms.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for building the per-camera uniform data used for cameras and 2 | //! shadows. 3 | 4 | use encase::{ShaderSize, ShaderType, UniformBuffer}; 5 | use glam::{Mat4, UVec2, Vec4}; 6 | use rend3::{ 7 | graph::{DataHandle, NodeResourceUsage, RenderGraph, RenderTargetHandle}, 8 | managers::CameraState, 9 | util::{bind_merge::BindGroupBuilder, frustum::Frustum}, 10 | }; 11 | use wgpu::{BindGroup, BufferUsages}; 12 | 13 | use crate::common::{Samplers, WholeFrameInterfaces}; 14 | 15 | #[derive(ShaderType)] 16 | pub struct PerCameraUniform { 17 | // TODO: use less space 18 | pub view: Mat4, 19 | // TODO: use less space 20 | pub view_proj: Mat4, 21 | pub frustum: Frustum, 22 | pub object_count: u32, 23 | } 24 | 25 | /// Set of uniforms that are useful for the whole frame. 26 | #[derive(Debug, Copy, Clone, ShaderType)] 27 | pub struct FrameUniforms { 28 | pub view: Mat4, 29 | pub view_proj: Mat4, 30 | pub origin_view_proj: Mat4, 31 | pub inv_view: Mat4, 32 | pub inv_view_proj: Mat4, 33 | pub inv_origin_view_proj: Mat4, 34 | pub frustum: Frustum, 35 | pub ambient: Vec4, 36 | pub resolution: UVec2, 37 | } 38 | impl FrameUniforms { 39 | /// Use the given camera to generate these uniforms. 40 | pub fn new(camera: &CameraState, info: &UniformInformation<'_>) -> Self { 41 | profiling::scope!("create uniforms"); 42 | 43 | let view = camera.view(); 44 | let view_proj = camera.view_proj(); 45 | let origin_view_proj = camera.origin_view_proj(); 46 | 47 | Self { 48 | view, 49 | view_proj, 50 | origin_view_proj, 51 | inv_view: view.inverse(), 52 | inv_view_proj: view_proj.inverse(), 53 | inv_origin_view_proj: origin_view_proj.inverse(), 54 | frustum: Frustum::from_matrix(camera.proj()), 55 | ambient: info.ambient, 56 | resolution: info.resolution, 57 | } 58 | } 59 | } 60 | 61 | /// Various information sources for the uniform data. 62 | pub struct UniformInformation<'node> { 63 | /// Struct containing the default set of samplers. 64 | pub samplers: &'node Samplers, 65 | /// Ambient light color. 66 | pub ambient: Vec4, 67 | /// Resolution of the viewport. 68 | pub resolution: UVec2, 69 | } 70 | 71 | pub struct UniformBindingHandles<'node> { 72 | /// Interfaces containing the bind group layouts for the uniform bind groups. 73 | pub interfaces: &'node WholeFrameInterfaces, 74 | /// The output bind group handle for the shadow uniform data. This does not 75 | /// include the shadow map texture, preventing a cycle. 76 | pub shadow_uniform_bg: DataHandle, 77 | /// The output bind group handle for the forward uniform data. This does 78 | /// include the shadow map texture. 79 | pub forward_uniform_bg: DataHandle, 80 | } 81 | 82 | /// Add the creation of these uniforms to the graph. 83 | pub fn add_to_graph<'node>( 84 | graph: &mut RenderGraph<'node>, 85 | shadow_target: RenderTargetHandle, 86 | binding_handles: UniformBindingHandles<'node>, 87 | info: UniformInformation<'node>, 88 | ) { 89 | let mut builder = graph.add_node("build uniform data"); 90 | let shadow_handle = builder.add_data(binding_handles.shadow_uniform_bg, NodeResourceUsage::Output); 91 | let forward_handle = builder.add_data(binding_handles.forward_uniform_bg, NodeResourceUsage::Output); 92 | 93 | // Get the shadow target and declare it a dependency of the forward_uniform_bg 94 | let shadow_target_handle = builder.add_render_target(shadow_target, NodeResourceUsage::Reference); 95 | builder.add_dependencies_to_render_targets(binding_handles.forward_uniform_bg, [shadow_target]); 96 | 97 | builder.build(move |ctx| { 98 | let shadow_target = ctx.graph_data.get_render_target(shadow_target_handle); 99 | 100 | let mut bgb = BindGroupBuilder::new(); 101 | 102 | info.samplers.add_to_bg(&mut bgb); 103 | 104 | let uniforms = FrameUniforms::new(&ctx.data_core.viewport_camera_state, &info); 105 | let uniform_buffer = ctx.renderer.device.create_buffer(&wgpu::BufferDescriptor { 106 | label: Some("Frame Uniforms"), 107 | size: FrameUniforms::SHADER_SIZE.get(), 108 | usage: BufferUsages::UNIFORM, 109 | mapped_at_creation: true, 110 | }); 111 | let mut mapping = uniform_buffer.slice(..).get_mapped_range_mut(); 112 | UniformBuffer::new(&mut *mapping).write(&uniforms).unwrap(); 113 | drop(mapping); 114 | uniform_buffer.unmap(); 115 | 116 | bgb.append_buffer(&uniform_buffer); 117 | 118 | ctx.data_core.directional_light_manager.add_to_bg(&mut bgb); 119 | ctx.data_core.point_light_manager.add_to_bg(&mut bgb); 120 | 121 | let shadow_uniform_bg = 122 | bgb.build(&ctx.renderer.device, Some("shadow uniform bg"), &binding_handles.interfaces.depth_uniform_bgl); 123 | 124 | bgb.append_texture_view(shadow_target); 125 | 126 | let forward_uniform_bg = bgb.build( 127 | &ctx.renderer.device, 128 | Some("forward uniform bg"), 129 | &binding_handles.interfaces.forward_uniform_bgl, 130 | ); 131 | 132 | ctx.graph_data.set_data(shadow_handle, Some(shadow_uniform_bg)); 133 | ctx.graph_data.set_data(forward_handle, Some(forward_uniform_bg)); 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /rend3-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-test" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "Testing utilities for rend3." 7 | license = "MIT OR Apache-2.0 OR Zlib" 8 | rust-version = "1.71" 9 | publish=false 10 | autotests = false 11 | 12 | [[test]] 13 | path = "tests/root.rs" 14 | name = "rend3-tests" 15 | 16 | [dependencies] 17 | anyhow = "1" 18 | env_logger = "0.11" 19 | flume = { version = "0.11", features = ["spin"] } 20 | glam = "0.25" 21 | image = { version = "0.24", default-features = false, features = ["png"] } 22 | rend3 = { path = "../rend3" } 23 | rend3-routine = { path = "../rend3-routine" } 24 | wgpu = "0.19.0" 25 | 26 | # These should be dev-deps but doing this allows us to have a single attribute on all tests 27 | # and not need to have conditional attributes on every single test. 28 | tokio = { version = "1", features = ["rt", "macros"] } 29 | wasm-bindgen-test = { version = "0.3" } 30 | 31 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 32 | nv-flip = "0.1" 33 | -------------------------------------------------------------------------------- /rend3-test/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use glam::{Mat4, Vec3, Vec4}; 4 | use rend3::types::{DirectionalLightHandle, MaterialHandle, MeshBuilder, ObjectHandle}; 5 | use rend3_routine::pbr::{PbrMaterial, Transparency}; 6 | use wgpu::Device; 7 | 8 | use crate::TestRunner; 9 | 10 | pub struct CaptureDropGuard { 11 | device: Arc, 12 | } 13 | impl CaptureDropGuard { 14 | pub fn start_capture(device: Arc) -> Self { 15 | device.start_capture(); 16 | Self { device } 17 | } 18 | } 19 | impl Drop for CaptureDropGuard { 20 | fn drop(&mut self) { 21 | self.device.stop_capture(); 22 | // Wait long enough for the Renderdoc UI to pick up the capture. 23 | std::thread::sleep(std::time::Duration::from_secs(1)); 24 | } 25 | } 26 | 27 | impl TestRunner { 28 | pub fn add_directional_light(&self, direction: Vec3) -> DirectionalLightHandle { 29 | self.renderer.add_directional_light(rend3::types::DirectionalLight { 30 | color: glam::Vec3::ONE, 31 | resolution: 256, 32 | distance: 5.0, 33 | intensity: 1.0, 34 | direction, 35 | }) 36 | } 37 | 38 | pub fn add_unlit_material(&self, color: Vec4) -> MaterialHandle { 39 | self.renderer.add_material(PbrMaterial { 40 | albedo: rend3_routine::pbr::AlbedoComponent::Value(color), 41 | unlit: true, 42 | ..Default::default() 43 | }) 44 | } 45 | 46 | pub fn add_transparent_material(&self, color: Vec4) -> MaterialHandle { 47 | self.renderer.add_material(PbrMaterial { 48 | albedo: rend3_routine::pbr::AlbedoComponent::Value(color), 49 | unlit: true, 50 | transparency: Transparency::Blend, 51 | ..Default::default() 52 | }) 53 | } 54 | 55 | pub fn add_lit_material(&self, color: Vec4) -> MaterialHandle { 56 | self.renderer.add_material(PbrMaterial { 57 | albedo: rend3_routine::pbr::AlbedoComponent::Value(color), 58 | unlit: false, 59 | ..Default::default() 60 | }) 61 | } 62 | 63 | /// Creates a plane object that is [-1, 1] 64 | pub fn plane(&self, material: MaterialHandle, transform: Mat4) -> ObjectHandle { 65 | let mesh = MeshBuilder::new( 66 | vec![ 67 | glam::Vec3::new(-1.0, -1.0, 0.0), 68 | glam::Vec3::new(-1.0, 1.0, 0.0), 69 | glam::Vec3::new(1.0, 1.0, 0.0), 70 | glam::Vec3::new(1.0, -1.0, 0.0), 71 | ], 72 | rend3::types::Handedness::Left, 73 | ) 74 | .with_indices(vec![0, 2, 1, 0, 3, 2]) 75 | .build() 76 | .unwrap(); 77 | 78 | self.add_object(rend3::types::Object { 79 | mesh_kind: rend3::types::ObjectMeshKind::Static(self.add_mesh(mesh).unwrap()), 80 | material, 81 | transform, 82 | }) 83 | } 84 | 85 | /// Creates a cube object that is [-1, 1] 86 | pub fn cube(&self, material: MaterialHandle, transform: Mat4) -> ObjectHandle { 87 | let vertex_positions = vec![ 88 | // far side (0.0, 0.0, 1.0) 89 | glam::Vec3::new(-1.0, -1.0, 1.0), 90 | glam::Vec3::new(1.0, -1.0, 1.0), 91 | glam::Vec3::new(1.0, 1.0, 1.0), 92 | glam::Vec3::new(-1.0, 1.0, 1.0), 93 | // near side (0.0, 0.0, -1.0) 94 | glam::Vec3::new(-1.0, 1.0, -1.0), 95 | glam::Vec3::new(1.0, 1.0, -1.0), 96 | glam::Vec3::new(1.0, -1.0, -1.0), 97 | glam::Vec3::new(-1.0, -1.0, -1.0), 98 | // right side (1.0, 0.0, 0.0) 99 | glam::Vec3::new(1.0, -1.0, -1.0), 100 | glam::Vec3::new(1.0, 1.0, -1.0), 101 | glam::Vec3::new(1.0, 1.0, 1.0), 102 | glam::Vec3::new(1.0, -1.0, 1.0), 103 | // left side (-1.0, 0.0, 0.0) 104 | glam::Vec3::new(-1.0, -1.0, 1.0), 105 | glam::Vec3::new(-1.0, 1.0, 1.0), 106 | glam::Vec3::new(-1.0, 1.0, -1.0), 107 | glam::Vec3::new(-1.0, -1.0, -1.0), 108 | // top (0.0, 1.0, 0.0) 109 | glam::Vec3::new(1.0, 1.0, -1.0), 110 | glam::Vec3::new(-1.0, 1.0, -1.0), 111 | glam::Vec3::new(-1.0, 1.0, 1.0), 112 | glam::Vec3::new(1.0, 1.0, 1.0), 113 | // bottom (0.0, -1.0, 0.0) 114 | glam::Vec3::new(1.0, -1.0, 1.0), 115 | glam::Vec3::new(-1.0, -1.0, 1.0), 116 | glam::Vec3::new(-1.0, -1.0, -1.0), 117 | glam::Vec3::new(1.0, -1.0, -1.0), 118 | ]; 119 | 120 | let index_data = vec![ 121 | 0, 1, 2, 2, 3, 0, // far 122 | 4, 5, 6, 6, 7, 4, // near 123 | 8, 9, 10, 10, 11, 8, // right 124 | 12, 13, 14, 14, 15, 12, // left 125 | 16, 17, 18, 18, 19, 16, // top 126 | 20, 21, 22, 22, 23, 20, // bottom 127 | ]; 128 | 129 | let mesh = MeshBuilder::new(vertex_positions, rend3::types::Handedness::Left) 130 | .with_indices(index_data) 131 | .build() 132 | .unwrap(); 133 | 134 | self.add_object(rend3::types::Object { 135 | mesh_kind: rend3::types::ObjectMeshKind::Static(self.add_mesh(mesh).unwrap()), 136 | material, 137 | transform, 138 | }) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /rend3-test/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod helpers; 2 | mod runner; 3 | mod threshold; 4 | 5 | pub use runner::{compare_image_to_path, download_image, FrameRenderSettings, TestRunner}; 6 | pub use threshold::{Threshold, ThresholdSet}; 7 | 8 | #[macro_export] 9 | macro_rules! no_gpu_return { 10 | ($value:expr) => { 11 | match $value { 12 | Ok(value) => Ok(value), 13 | Err(rend3::RendererInitializationError::MissingAdapter) => { 14 | eprintln!("No adapter found, skipping test"); 15 | return Ok(()); 16 | } 17 | Err(err) => Err(err), 18 | } 19 | }; 20 | } 21 | 22 | // These always need to go last in the file, or RA gets mightily confused. 23 | #[cfg(not(target_arch = "wasm32"))] 24 | pub use tokio::test as test_attr; 25 | #[cfg(target_arch = "wasm32")] 26 | pub use wasm_bindgen_test::wasm_bindgen_test as test_attr; 27 | -------------------------------------------------------------------------------- /rend3-test/src/threshold.rs: -------------------------------------------------------------------------------- 1 | pub struct ThresholdSet { 2 | #[cfg_attr(target_arch = "wasm32", allow(unused))] 3 | thresholds: Vec, 4 | } 5 | 6 | impl ThresholdSet { 7 | #[cfg(not(target_arch = "wasm32"))] 8 | pub fn check(self, pool: &mut nv_flip::FlipPool) -> bool { 9 | // If there are no checks, we want to fail the test. 10 | let mut all_passed = !self.thresholds.is_empty(); 11 | // We always iterate all of these, as the call to check prints 12 | for check in self.thresholds { 13 | all_passed &= check.check(pool); 14 | } 15 | all_passed 16 | } 17 | } 18 | 19 | #[derive(Debug, Copy, Clone, PartialEq)] 20 | pub enum Threshold { 21 | Mean(f32), 22 | Percentile { percentile: f32, threshold: f32 }, 23 | } 24 | 25 | impl Threshold { 26 | #[cfg(not(target_arch = "wasm32"))] 27 | fn check(&self, pool: &mut nv_flip::FlipPool) -> bool { 28 | match *self { 29 | Self::Mean(v) => { 30 | let mean = pool.mean(); 31 | let within = mean <= v; 32 | println!( 33 | " Expected Mean ({:.6}) to be under expected maximum ({}): {}", 34 | mean, 35 | v, 36 | if within { "PASS" } else { "FAIL" } 37 | ); 38 | within 39 | } 40 | Self::Percentile { percentile: p, threshold: v } => { 41 | let percentile = pool.get_percentile(p, true); 42 | let within = percentile <= v; 43 | println!( 44 | " Expected {}% ({:.6}) to be under expected maximum ({}): {}", 45 | p * 100.0, 46 | percentile, 47 | v, 48 | if within { "PASS" } else { "FAIL" } 49 | ); 50 | within 51 | } 52 | } 53 | } 54 | } 55 | 56 | impl From for ThresholdSet { 57 | fn from(threshold: Threshold) -> Self { 58 | Self { thresholds: vec![threshold] } 59 | } 60 | } 61 | 62 | impl From<&[Threshold]> for ThresholdSet { 63 | fn from(thresholds: &[Threshold]) -> Self { 64 | Self { thresholds: thresholds.into() } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rend3-test/tests/msaa.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use glam::{Mat4, Vec3, Vec4}; 3 | use rend3::types::{Camera, Handedness, MeshBuilder, Object, ObjectMeshKind, SampleCount}; 4 | use rend3_test::{no_gpu_return, test_attr, FrameRenderSettings, TestRunner, Threshold}; 5 | 6 | #[test_attr] 7 | pub async fn triangle() -> anyhow::Result<()> { 8 | let iad = no_gpu_return!(rend3::create_iad(None, None, None, None).await) 9 | .context("InstanceAdapterDevice creation failed")?; 10 | 11 | let Ok(runner) = TestRunner::builder().iad(iad.clone()).handedness(Handedness::Left).build().await else { 12 | return Ok(()); 13 | }; 14 | 15 | // Clockwise triangle 16 | let mesh = MeshBuilder::new( 17 | vec![Vec3::new(0.5, -0.5, 0.0), Vec3::new(-0.5, -0.5, 0.0), Vec3::new(0.0, 0.5, 0.0)], 18 | Handedness::Left, 19 | ) 20 | .build() 21 | .context("Failed to create mesh")?; 22 | 23 | let mesh_hdl = runner.add_mesh(mesh).unwrap(); 24 | let material_hdl = runner.add_unlit_material(Vec4::new(0.25, 0.5, 0.75, 1.0)); 25 | let object = 26 | Object { mesh_kind: ObjectMeshKind::Static(mesh_hdl), material: material_hdl, transform: Mat4::IDENTITY }; 27 | let _object_hdl = runner.add_object(object); 28 | 29 | runner.set_camera_data(Camera { 30 | projection: rend3::types::CameraProjection::Raw(Mat4::IDENTITY), 31 | view: Mat4::IDENTITY, 32 | }); 33 | 34 | runner 35 | .render_and_compare( 36 | FrameRenderSettings::new().samples(SampleCount::Four), 37 | "tests/results/msaa/four.png", 38 | Threshold::Mean(0.0), 39 | ) 40 | .await?; 41 | 42 | Ok(()) 43 | } 44 | 45 | #[test_attr] 46 | pub async fn sample_coverage() -> anyhow::Result<()> { 47 | let iad = no_gpu_return!(rend3::create_iad(None, None, None, None).await) 48 | .context("InstanceAdapterDevice creation failed")?; 49 | 50 | let Ok(runner) = TestRunner::builder().iad(iad.clone()).handedness(Handedness::Left).build().await else { 51 | return Ok(()); 52 | }; 53 | 54 | let material = runner.add_unlit_material(Vec4::ONE); 55 | 56 | // Make a plane whose (0, 0) is at the top left, and is 1 unit large. 57 | let base_matrix = Mat4::from_translation(Vec3::new(0.5, 0.5, 0.0)) * Mat4::from_scale(Vec3::new(0.5, 0.5, 1.0)); 58 | // 64 x 64 grid of planes 59 | let mut planes = Vec::with_capacity(64 * 64); 60 | for x in 0..64 { 61 | for y in 0..64 { 62 | planes.push(runner.plane( 63 | material.clone(), 64 | Mat4::from_translation(Vec3::new(x as f32, y as f32, 0.0)) 65 | * Mat4::from_scale(Vec3::new(1.0 - (x as f32 / 63.0), 1.0 - (y as f32 / 63.0), 1.0)) 66 | * base_matrix, 67 | )); 68 | } 69 | } 70 | 71 | runner.set_camera_data(Camera { 72 | projection: rend3::types::CameraProjection::Raw(Mat4::orthographic_lh(0.0, 64.0, 64.0, 0.0, 0.0, 1.0)), 73 | view: Mat4::IDENTITY, 74 | }); 75 | 76 | for samples in SampleCount::ARRAY { 77 | runner 78 | .render_and_compare( 79 | FrameRenderSettings::new().samples(samples), 80 | &format!("tests/results/msaa/sample-coverage-{}.png", samples as u8), 81 | Threshold::Mean(0.0), 82 | ) 83 | .await?; 84 | } 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /rend3-test/tests/object.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use glam::{Mat4, Quat, Vec3, Vec4}; 3 | use rend3::{ 4 | types::{Camera, Handedness, ObjectChange}, 5 | util::freelist::FreelistDerivedBuffer, 6 | }; 7 | use rend3_test::{no_gpu_return, test_attr, FrameRenderSettings, TestRunner, Threshold}; 8 | 9 | /// Ensure that duplicate_object doesn't retain the object for an extra frame. 10 | #[test_attr] 11 | pub async fn deuplicate_object_retain() -> anyhow::Result<()> { 12 | let iad = no_gpu_return!(rend3::create_iad(None, None, None, None).await) 13 | .context("InstanceAdapterDevice creation failed")?; 14 | 15 | let Ok(runner) = TestRunner::builder().iad(iad.clone()).handedness(Handedness::Left).build().await else { 16 | return Ok(()); 17 | }; 18 | 19 | runner.set_camera_data(Camera { 20 | projection: rend3::types::CameraProjection::Raw(Mat4::IDENTITY), 21 | view: Mat4::IDENTITY, 22 | }); 23 | 24 | let material = runner.add_unlit_material(Vec4::ONE); 25 | let object1 = runner.plane( 26 | material, 27 | Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(-0.5, 0.0, 0.0)), 28 | ); 29 | 30 | runner 31 | .render_and_compare( 32 | FrameRenderSettings::new(), 33 | "tests/results/object/duplicate-object-retain-left.png", 34 | Threshold::Mean(0.0), 35 | ) 36 | .await?; 37 | 38 | let _object2 = runner.duplicate_object( 39 | &object1, 40 | ObjectChange { 41 | transform: Some(Mat4::from_scale_rotation_translation( 42 | Vec3::new(-0.25, 0.25, 0.25), 43 | Quat::IDENTITY, 44 | Vec3::new(0.5, 0.0, 0.0), 45 | )), 46 | ..Default::default() 47 | }, 48 | ); 49 | drop(object1); 50 | 51 | runner 52 | .render_and_compare( 53 | FrameRenderSettings::new(), 54 | "tests/results/object/duplicate-object-retain-right.png", 55 | Threshold::Mean(0.0), 56 | ) 57 | .await?; 58 | 59 | Ok(()) 60 | } 61 | 62 | /// There was a bug in the culling implementation where the per-material buffer 63 | /// was never resized to fit the number of objects in the scene once it was initially 64 | /// created. This manifested as objects above the initial frame count would get all-zero 65 | /// transforms and be completely hidden. We reproduce those conditions here, and ensure 66 | /// that the bug is fixed. 67 | #[test_attr] 68 | pub async fn multi_frame_add() -> anyhow::Result<()> { 69 | let iad = no_gpu_return!(rend3::create_iad(None, None, None, None).await) 70 | .context("InstanceAdapterDevice creation failed")?; 71 | 72 | let Ok(runner) = TestRunner::builder().iad(iad.clone()).handedness(Handedness::Left).build().await else { 73 | return Ok(()); 74 | }; 75 | 76 | let material = runner.add_unlit_material(Vec4::ONE); 77 | 78 | // Make a plane whose (0, 0) is at the top left, and is 1 unit large. 79 | let base_matrix = Mat4::from_translation(Vec3::new(0.5, 0.5, 0.0)) * Mat4::from_scale(Vec3::new(0.5, 1.0, 1.0)); 80 | 81 | runner.set_camera_data(Camera { 82 | projection: rend3::types::CameraProjection::Raw(Mat4::orthographic_lh(0.0, 2.0, 16.0, 0.0, 0.0, 1.0)), 83 | view: Mat4::IDENTITY, 84 | }); 85 | 86 | // We use the starting size amount of objects for each column, ensuring that the buffer 87 | // will need to be resized on the second column. 88 | let count = FreelistDerivedBuffer::STARTING_SIZE; 89 | 90 | // 2 side by side columns made up of `count` rows 91 | let mut planes = Vec::with_capacity(2); 92 | for x in 0..2 { 93 | for y in 0..count { 94 | planes.push( 95 | runner 96 | .plane(material.clone(), Mat4::from_translation(Vec3::new(x as f32, y as f32, 0.0)) * base_matrix), 97 | ); 98 | } 99 | runner 100 | .render_and_compare( 101 | FrameRenderSettings::new(), 102 | &format!("tests/results/object/multi-frame-add-{}.png", x), 103 | Threshold::Mean(0.0), 104 | ) 105 | .await?; 106 | } 107 | 108 | Ok(()) 109 | } 110 | -------------------------------------------------------------------------------- /rend3-test/tests/results/msaa/four.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/msaa/four.png -------------------------------------------------------------------------------- /rend3-test/tests/results/msaa/sample-coverage-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/msaa/sample-coverage-1.png -------------------------------------------------------------------------------- /rend3-test/tests/results/msaa/sample-coverage-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/msaa/sample-coverage-4.png -------------------------------------------------------------------------------- /rend3-test/tests/results/object/duplicate-object-retain-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/object/duplicate-object-retain-left.png -------------------------------------------------------------------------------- /rend3-test/tests/results/object/duplicate-object-retain-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/object/duplicate-object-retain-right.png -------------------------------------------------------------------------------- /rend3-test/tests/results/object/multi-frame-add-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/object/multi-frame-add-0.png -------------------------------------------------------------------------------- /rend3-test/tests/results/object/multi-frame-add-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/object/multi-frame-add-1.png -------------------------------------------------------------------------------- /rend3-test/tests/results/shadow/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/shadow/cube.png -------------------------------------------------------------------------------- /rend3-test/tests/results/shadow/plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/shadow/plane.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/coordinate-space-NegX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/coordinate-space-NegX.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/coordinate-space-NegY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/coordinate-space-NegY.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/coordinate-space-NegZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/coordinate-space-NegZ.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/coordinate-space-X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/coordinate-space-X.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/coordinate-space-Y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/coordinate-space-Y.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/coordinate-space-Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/coordinate-space-Z.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/empty.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/triangle-backface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/triangle-backface.png -------------------------------------------------------------------------------- /rend3-test/tests/results/simple/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/simple/triangle.png -------------------------------------------------------------------------------- /rend3-test/tests/results/transparency/blending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BVE-Reborn/rend3/d088a841b0469d07d5a7ff3f4d784e97b4a194d5/rend3-test/tests/results/transparency/blending.png -------------------------------------------------------------------------------- /rend3-test/tests/root.rs: -------------------------------------------------------------------------------- 1 | mod msaa; 2 | mod object; 3 | mod shadow; 4 | mod simple; 5 | mod transparency; 6 | -------------------------------------------------------------------------------- /rend3-test/tests/shadow.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::FRAC_PI_2; 2 | 3 | use anyhow::Context; 4 | use glam::{Mat4, Quat, Vec3, Vec3A, Vec4}; 5 | use rend3::types::{Camera, Handedness}; 6 | use rend3_test::{no_gpu_return, test_attr, FrameRenderSettings, TestRunner, Threshold}; 7 | 8 | #[test_attr] 9 | pub async fn shadows() -> anyhow::Result<()> { 10 | let iad = no_gpu_return!(rend3::create_iad(None, None, None, None).await) 11 | .context("InstanceAdapterDevice creation failed")?; 12 | 13 | let Ok(runner) = TestRunner::builder().iad(iad.clone()).handedness(Handedness::Left).build().await else { 14 | return Ok(()); 15 | }; 16 | 17 | let _light = runner.add_directional_light(Vec3::new(-1.0, -1.0, 1.0)); 18 | 19 | let material1 = runner.add_lit_material(Vec4::new(0.25, 0.5, 0.75, 1.0)); 20 | 21 | let _plane = runner.plane(material1, Mat4::from_rotation_x(-FRAC_PI_2)); 22 | 23 | runner.set_camera_data(Camera { 24 | projection: rend3::types::CameraProjection::Orthographic { size: Vec3A::new(2.5, 2.5, 5.0) }, 25 | view: Mat4::look_at_lh(Vec3::new(0.0, 1.0, -1.0), Vec3::ZERO, Vec3::Y), 26 | }); 27 | 28 | let file_name = "tests/results/shadow/plane.png"; 29 | runner 30 | .render_and_compare( 31 | FrameRenderSettings::new().size(256)?, 32 | file_name, 33 | Threshold::Percentile { percentile: 0.5, threshold: 0.04 }, 34 | ) 35 | .await?; 36 | 37 | let material2 = runner.add_lit_material(Vec4::new(0.75, 0.5, 0.25, 1.0)); 38 | 39 | let _cube = runner.cube( 40 | material2, 41 | Mat4::from_scale_rotation_translation(Vec3::splat(0.25), Quat::IDENTITY, Vec3::new(0.25, 0.25, -0.25)), 42 | ); 43 | 44 | let file_name = "tests/results/shadow/cube.png"; 45 | runner 46 | .render_and_compare( 47 | FrameRenderSettings::new().size(256)?, 48 | file_name, 49 | Threshold::Percentile { percentile: 0.5, threshold: 0.04 }, 50 | ) 51 | .await?; 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /rend3-test/tests/transparency.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use glam::{Mat4, Quat, Vec3, Vec4}; 3 | use rend3::types::{Camera, Handedness}; 4 | use rend3_test::{no_gpu_return, test_attr, FrameRenderSettings, TestRunner, Threshold}; 5 | 6 | /// Ensure that transparency is ordered correctly 7 | // 8 | // Todo: This test never fails, even if I remove the sort. 9 | #[test_attr] 10 | pub async fn transparency() -> anyhow::Result<()> { 11 | let iad = no_gpu_return!(rend3::create_iad(None, None, None, None).await) 12 | .context("InstanceAdapterDevice creation failed")?; 13 | 14 | let Ok(runner) = TestRunner::builder().iad(iad.clone()).handedness(Handedness::Left).build().await else { 15 | return Ok(()); 16 | }; 17 | 18 | runner.set_camera_data(Camera { 19 | projection: rend3::types::CameraProjection::Raw(Mat4::IDENTITY), 20 | view: Mat4::IDENTITY, 21 | }); 22 | 23 | let material1 = runner.add_transparent_material(Vec4::new(1.0, 0.0, 0.0, 0.5)); 24 | let material2 = runner.add_transparent_material(Vec4::new(0.0, 1.0, 0.0, 0.5)); 25 | let _object_left_1 = runner.plane( 26 | material1.clone(), 27 | Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(-0.5, 0.0, -0.5)), 28 | ); 29 | 30 | let _object_left_2 = runner.plane( 31 | material2.clone(), 32 | Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(-0.5, 0.0, 0.5)), 33 | ); 34 | 35 | let _object_right_2 = runner.plane( 36 | material2, 37 | Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(0.5, 0.0, 0.5)), 38 | ); 39 | 40 | let _object_right_1 = runner.plane( 41 | material1, 42 | Mat4::from_scale_rotation_translation(Vec3::new(-0.25, 0.25, 0.25), Quat::IDENTITY, Vec3::new(0.5, 0.0, -0.5)), 43 | ); 44 | 45 | runner 46 | .render_and_compare(FrameRenderSettings::new(), "tests/results/transparency/blending.png", Threshold::Mean(0.0)) 47 | .await?; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /rend3-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3-types" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "Type definitions for the rend3 rendering library." 7 | readme = "../README.md" 8 | repository = "https://github.com/BVE-Reborn/rend3" 9 | license = "MIT OR Apache-2.0 OR Zlib" 10 | keywords = ["3d", "graphics", "rend3", "renderer", "wgpu"] 11 | categories = ["game-development", "graphics", "rendering", "rendering::engine", "wasm"] 12 | rust-version = "1.71" 13 | 14 | [dependencies] 15 | bitflags = "2" 16 | bytemuck = { version = "1", features = ["min_const_generics"] } 17 | cfg-if = "1" 18 | encase = { version = "0.7", features = ["glam"] } 19 | glam = { version = "0.25", features = ["bytemuck"] } 20 | list-any = "0.2" 21 | once_cell = "1" 22 | thiserror = "1" 23 | wgt = { package = "wgpu-types", version = "0.19" } 24 | -------------------------------------------------------------------------------- /rend3-types/src/attribute.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | marker::PhantomData, 3 | ops::Deref, 4 | sync::atomic::{AtomicUsize, Ordering}, 5 | }; 6 | 7 | use bytemuck::Pod; 8 | use once_cell::sync::OnceCell; 9 | 10 | #[derive(Debug, Copy, Clone)] 11 | pub struct VertexAttributeId { 12 | inner: usize, 13 | default_value: Option<&'static str>, 14 | name: &'static str, 15 | metadata: &'static VertexFormatMetadata, 16 | } 17 | 18 | impl PartialEq for VertexAttributeId { 19 | fn eq(&self, other: &Self) -> bool { 20 | self.inner == other.inner 21 | } 22 | } 23 | 24 | impl Eq for VertexAttributeId {} 25 | 26 | impl VertexAttributeId { 27 | pub fn name(&self) -> &'static str { 28 | self.name 29 | } 30 | 31 | pub fn metadata(&self) -> &'static VertexFormatMetadata { 32 | self.metadata 33 | } 34 | 35 | pub fn default_value(&self) -> Option<&'static str> { 36 | self.default_value 37 | } 38 | } 39 | 40 | static VERTEX_ATTRIBUTE_INDEX_ALLOCATOR: AtomicUsize = AtomicUsize::new(0); 41 | 42 | pub struct VertexAttribute 43 | where 44 | T: VertexFormat, 45 | { 46 | name: &'static str, 47 | default_value: Option<&'static str>, 48 | id: OnceCell, 49 | _phantom: PhantomData, 50 | } 51 | impl VertexAttribute 52 | where 53 | T: VertexFormat, 54 | { 55 | pub const fn new(name: &'static str, default_value: Option<&'static str>) -> Self { 56 | Self { name, default_value, id: OnceCell::new(), _phantom: PhantomData } 57 | } 58 | 59 | pub fn name(&self) -> &'static str { 60 | self.name 61 | } 62 | 63 | pub fn id(&self) -> &VertexAttributeId { 64 | self.id.get_or_init(|| VertexAttributeId { 65 | name: self.name, 66 | default_value: self.default_value, 67 | inner: VERTEX_ATTRIBUTE_INDEX_ALLOCATOR.fetch_add(1, Ordering::Relaxed), 68 | metadata: &T::METADATA, 69 | }) 70 | } 71 | } 72 | 73 | impl Deref for VertexAttribute 74 | where 75 | T: VertexFormat, 76 | { 77 | type Target = VertexAttributeId; 78 | 79 | fn deref(&self) -> &Self::Target { 80 | self.id() 81 | } 82 | } 83 | 84 | #[derive(Debug)] 85 | pub struct VertexFormatMetadata { 86 | pub size: u32, 87 | pub shader_extract_fn: &'static str, 88 | pub shader_type: &'static str, 89 | } 90 | 91 | pub trait VertexFormat: Pod + Send + Sync + 'static { 92 | const METADATA: VertexFormatMetadata; 93 | } 94 | 95 | // TODO: More formats 96 | 97 | impl VertexFormat for glam::Vec2 { 98 | const METADATA: VertexFormatMetadata = 99 | VertexFormatMetadata { size: 8, shader_extract_fn: "extract_attribute_vec2_f32", shader_type: "vec2" }; 100 | } 101 | 102 | impl VertexFormat for glam::Vec3 { 103 | const METADATA: VertexFormatMetadata = 104 | VertexFormatMetadata { size: 12, shader_extract_fn: "extract_attribute_vec3_f32", shader_type: "vec3" }; 105 | } 106 | 107 | impl VertexFormat for glam::Vec4 { 108 | const METADATA: VertexFormatMetadata = 109 | VertexFormatMetadata { size: 16, shader_extract_fn: "extract_attribute_vec4_f32", shader_type: "vec4" }; 110 | } 111 | 112 | impl VertexFormat for [u16; 4] { 113 | const METADATA: VertexFormatMetadata = 114 | VertexFormatMetadata { size: 8, shader_extract_fn: "extract_attribute_vec4_u16", shader_type: "vec4" }; 115 | } 116 | 117 | impl VertexFormat for [u8; 4] { 118 | const METADATA: VertexFormatMetadata = VertexFormatMetadata { 119 | size: 8, 120 | shader_extract_fn: "extract_attribute_vec4_u8_unorm", 121 | shader_type: "vec4", 122 | }; 123 | } 124 | 125 | pub static VERTEX_ATTRIBUTE_POSITION: VertexAttribute = VertexAttribute::new("position", None); 126 | pub static VERTEX_ATTRIBUTE_NORMAL: VertexAttribute = VertexAttribute::new("normal", None); 127 | pub static VERTEX_ATTRIBUTE_TANGENT: VertexAttribute = VertexAttribute::new("tangent", None); 128 | pub static VERTEX_ATTRIBUTE_TEXTURE_COORDINATES_0: VertexAttribute = 129 | VertexAttribute::new("texture_coords_0", None); 130 | pub static VERTEX_ATTRIBUTE_TEXTURE_COORDINATES_1: VertexAttribute = 131 | VertexAttribute::new("texture_coords_1", None); 132 | pub static VERTEX_ATTRIBUTE_COLOR_0: VertexAttribute<[u8; 4]> = VertexAttribute::new("color_0", Some("vec4(1.0)")); 133 | pub static VERTEX_ATTRIBUTE_COLOR_1: VertexAttribute<[u8; 4]> = VertexAttribute::new("color_1", Some("vec4(1.0)")); 134 | pub static VERTEX_ATTRIBUTE_JOINT_INDICES: VertexAttribute<[u16; 4]> = VertexAttribute::new("joint_indices", None); 135 | pub static VERTEX_ATTRIBUTE_JOINT_WEIGHTS: VertexAttribute = VertexAttribute::new("joint_weights", None); 136 | -------------------------------------------------------------------------------- /rend3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rend3" 3 | version = "0.3.0" 4 | authors = ["The rend3 Developers"] 5 | edition = "2021" 6 | description = "Easy to use, customizable, efficient 3D renderer library built on wgpu." 7 | readme = "../README.md" 8 | repository = "https://github.com/BVE-Reborn/rend3" 9 | license = "MIT OR Apache-2.0 OR Zlib" 10 | keywords = ["3d", "graphics", "rend3", "renderer", "wgpu"] 11 | categories = ["game-development", "graphics", "rendering", "rendering::engine", "wasm"] 12 | rust-version = "1.71" 13 | 14 | [package.metadata.release] 15 | tag = true 16 | tag-prefix = "" 17 | pre-release-hook = ["cargo", "readme", "-o", "../README.md", "-t", "../README.tpl"] 18 | [[package.metadata.release.pre-release-replacements]] 19 | file = "../CHANGELOG.md" 20 | search = "\\[Unreleased\\]\\(#unreleased\\)" 21 | replace = "[Unreleased](#unreleased)\n- [v{{version}}](#v{{version}})" 22 | [[package.metadata.release.pre-release-replacements]] 23 | file = "../CHANGELOG.md" 24 | search = "\\[v([0-9+])\\.([0-9+])\\.([0-9+])\\]\\(#v[0-9\\.]+\\)" 25 | replace = "[v$1.$2.$3](#v$1$2$3)" 26 | [[package.metadata.release.pre-release-replacements]] 27 | file = "../CHANGELOG.md" 28 | search = "## Unreleased" 29 | replace = "## Unreleased\n\n## v{{version}}\n\nReleased {{date}}" 30 | [[package.metadata.release.pre-release-replacements]] 31 | file = "../CHANGELOG.md" 32 | search = "\\[Unreleased\\]\\(https://github.com/BVE-Reborn/rend3/compare/v([a-z0-9.-]+)\\.\\.\\.HEAD\\)" 33 | replace = "[Unreleased](https://github.com/BVE-Reborn/rend3/compare/v{{version}}...HEAD)\n- [v{{version}}](https://github.com/BVE-Reborn/rend3/compare/v$1...v{{version}})" 34 | 35 | [dependencies] 36 | arrayvec = "0.7" 37 | bimap = "0.6" 38 | bitflags = "2" 39 | bumpalo = "3" 40 | bytemuck = "1" 41 | encase = { version = "0.7" } 42 | flume = "0.11" 43 | glam = { version = "0.25.0", features = ["bytemuck"] } 44 | handlebars = "5" 45 | indexmap = "2" 46 | list-any = "0.2" 47 | log = "0.4" 48 | noop-waker = "0.1" 49 | num-traits = "0.2" 50 | once_cell = "1" 51 | parking_lot = "0.12" 52 | profiling = { version = "1", default-features = false } 53 | range-alloc = "0.1.3" 54 | rend3-types = { version = "^0.3.0", path = "../rend3-types" } 55 | rust-embed = { version = "8", features = ["interpolate-folder-path"] } 56 | rustc-hash = "1" 57 | serde = { version = "1", features = ["derive"] } 58 | smallvec = "1" 59 | smartstring = "1.0" 60 | thiserror = "1" 61 | wgpu = "0.19.0" 62 | wgpu-profiler = "0.16.0" 63 | 64 | [dev-dependencies] 65 | pollster = "0.3" 66 | 67 | [target.'cfg(target_arch = "wasm32")'.dependencies] 68 | # Needed to embed shaders in the binary on wasm 69 | rust-embed = { version = "8", features = ["interpolate-folder-path", "debug-embed"] } 70 | -------------------------------------------------------------------------------- /rend3/shaders/mipmap.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexOutput { 2 | @builtin(position) position: vec4, 3 | @location(0) tex_coords: vec2, 4 | }; 5 | 6 | @vertex 7 | fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { 8 | var out: VertexOutput; 9 | let x = i32(vertex_index) / 2; 10 | let y = i32(vertex_index) & 1; 11 | let tc = vec2( 12 | f32(x) * 2.0, 13 | f32(y) * 2.0 14 | ); 15 | out.position = vec4( 16 | tc.x * 2.0 - 1.0, 17 | 1.0 - tc.y * 2.0, 18 | 0.0, 1.0 19 | ); 20 | out.tex_coords = tc; 21 | return out; 22 | } 23 | 24 | @group(0) @binding(0) 25 | var r_color: texture_2d; 26 | @group(1) @binding(0) 27 | var r_sampler: sampler; 28 | 29 | @fragment 30 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 31 | return textureSample(r_color, r_sampler, in.tex_coords); 32 | } 33 | -------------------------------------------------------------------------------- /rend3/shaders/scatter_copy.wgsl: -------------------------------------------------------------------------------- 1 | // Generic data scatter compute shader. 2 | // 3 | // Takes a sparse self-describing copy operation 4 | // and executes the copy into the given destination buffer. 5 | // 6 | // This allows us to do sparse cpu buffer -> gpu buffer copies at 7 | // the expense of 4 bytes per value. 8 | 9 | struct TransferSource { 10 | // Count of 4-byte words of data to copy. 11 | words_to_copy: u32, 12 | // Count of structures (_not_ words) to copy. 13 | count: u32, 14 | 15 | // Really an array of the following structures `(words_to_copy + 4) * 4` bytes apart. 16 | // 17 | // { 18 | // // Offset in the destination buffer in 4-byte words. 19 | // destination_word_offset: u32, 20 | // data_word_0: u32, 21 | // .., 22 | // data_word_N: u32, 23 | // } 24 | data: array, 25 | } 26 | 27 | @group(0) @binding(0) 28 | var transfer_source: TransferSource; 29 | @group(0) @binding(1) 30 | var transfer_destination: array; 31 | 32 | @compute @workgroup_size(64) 33 | fn cs_main(@builtin(global_invocation_id) gid: vec3) { 34 | // Each invocation copies a whole T, which can be any size, though it's presumed small. 35 | let index = gid.x; 36 | if (index >= transfer_source.count) { 37 | return; 38 | } 39 | 40 | let words_to_copy = transfer_source.words_to_copy; 41 | let stride = words_to_copy + 1u; 42 | 43 | let struct_word_offset = index * stride; 44 | 45 | let destination_word_offset = transfer_source.data[struct_word_offset]; 46 | let data_word_offset = struct_word_offset + 1u; 47 | 48 | for (var i = 0u; i < words_to_copy; i++) { 49 | transfer_destination[destination_word_offset + i] = transfer_source.data[data_word_offset + i]; 50 | } 51 | } -------------------------------------------------------------------------------- /rend3/shaders/vertex_attributes.wgsl: -------------------------------------------------------------------------------- 1 | // -- DO NOT VALIDATE -- 2 | 3 | struct Indices { 4 | object: u32, 5 | vertex: u32, 6 | } 7 | 8 | alias TriangleVertices = array; 9 | alias TriangleIndices = array; 10 | struct Triangle { 11 | vertices: TriangleVertices, 12 | indices: TriangleIndices, 13 | } 14 | 15 | fn extract_attribute_vec2_f32(byte_base_offset: u32, vertex_index: u32) -> vec2 { 16 | let first_element_idx = byte_base_offset / 4u + vertex_index * 2u; 17 | return vec2( 18 | bitcast(vertex_buffer[first_element_idx]), 19 | bitcast(vertex_buffer[first_element_idx + 1u]), 20 | ); 21 | } 22 | 23 | fn extract_attribute_vec3_f32(byte_base_offset: u32, vertex_index: u32) -> vec3 { 24 | let first_element_idx = byte_base_offset / 4u + vertex_index * 3u; 25 | return vec3( 26 | bitcast(vertex_buffer[first_element_idx]), 27 | bitcast(vertex_buffer[first_element_idx + 1u]), 28 | bitcast(vertex_buffer[first_element_idx + 2u]), 29 | ); 30 | } 31 | 32 | fn extract_attribute_vec4_f32(byte_base_offset: u32, vertex_index: u32) -> vec4 { 33 | let first_element_idx = byte_base_offset / 4u + vertex_index * 4u; 34 | return vec4( 35 | bitcast(vertex_buffer[first_element_idx]), 36 | bitcast(vertex_buffer[first_element_idx + 1u]), 37 | bitcast(vertex_buffer[first_element_idx + 2u]), 38 | bitcast(vertex_buffer[first_element_idx + 3u]), 39 | ); 40 | } 41 | 42 | fn extract_attribute_vec4_u16(byte_base_offset: u32, vertex_index: u32) -> vec4 { 43 | let first_element_idx = byte_base_offset / 4u + vertex_index * 2u; 44 | let value_0 = vertex_buffer[first_element_idx]; 45 | let value_1 = vertex_buffer[first_element_idx + 1u]; 46 | return vec4( 47 | value_0 & 0xFFFFu, 48 | (value_0 >> 16u) & 0xFFFFu, 49 | value_1 & 0xFFFFu, 50 | (value_1 >> 16u) & 0xFFFFu, 51 | ); 52 | } 53 | 54 | fn extract_attribute_vec4_u8_unorm(byte_base_offset: u32, vertex_index: u32) -> vec4 { 55 | let first_element_idx = byte_base_offset / 4u + vertex_index; 56 | return unpack4x8unorm(vertex_buffer[first_element_idx]); 57 | } 58 | -------------------------------------------------------------------------------- /rend3/shaders/vertex_attributes_store.wgsl: -------------------------------------------------------------------------------- 1 | // -- DO NOT VALIDATE -- 2 | 3 | fn store_attribute_vec3_f32(byte_base_offset: u32, vertex_index: u32, value: vec3) { 4 | let first_element_idx = byte_base_offset / 4u + vertex_index * 3u; 5 | 6 | vertex_buffer[first_element_idx] = bitcast(value.x); 7 | vertex_buffer[first_element_idx + 1u] = bitcast(value.y); 8 | vertex_buffer[first_element_idx + 2u] = bitcast(value.z); 9 | } 10 | -------------------------------------------------------------------------------- /rend3/src/graph/encpass.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use wgpu::{CommandEncoder, RenderPass}; 4 | use wgpu_profiler::ProfilerCommandRecorder; 5 | 6 | use crate::graph::DeclaredDependency; 7 | 8 | /// Handle to a declared renderpass output. 9 | pub struct RenderPassHandle; 10 | 11 | #[derive(Default)] 12 | pub(super) enum RenderGraphEncoderOrPassInner<'a, 'pass> { 13 | Encoder(&'a mut CommandEncoder), 14 | RenderPass(&'a mut RenderPass<'pass>), 15 | #[default] 16 | None, 17 | } 18 | 19 | impl<'a, 'pass> ProfilerCommandRecorder for RenderGraphEncoderOrPassInner<'a, 'pass> { 20 | fn is_pass(&self) -> bool { 21 | matches!(self, RenderGraphEncoderOrPassInner::RenderPass(_)) 22 | } 23 | 24 | fn write_timestamp(&mut self, query_set: &wgpu::QuerySet, query_index: u32) { 25 | match self { 26 | RenderGraphEncoderOrPassInner::Encoder(e) => e.write_timestamp(query_set, query_index), 27 | RenderGraphEncoderOrPassInner::RenderPass(rp) => rp.write_timestamp(query_set, query_index), 28 | RenderGraphEncoderOrPassInner::None => panic!("Already removed the contents of RenderGraphEncoderOrPass"), 29 | } 30 | } 31 | 32 | fn push_debug_group(&mut self, label: &str) { 33 | match self { 34 | RenderGraphEncoderOrPassInner::Encoder(e) => e.push_debug_group(label), 35 | RenderGraphEncoderOrPassInner::RenderPass(rp) => rp.push_debug_group(label), 36 | RenderGraphEncoderOrPassInner::None => panic!("Already removed the contents of RenderGraphEncoderOrPass"), 37 | } 38 | } 39 | 40 | fn pop_debug_group(&mut self) { 41 | match self { 42 | RenderGraphEncoderOrPassInner::Encoder(e) => e.pop_debug_group(), 43 | RenderGraphEncoderOrPassInner::RenderPass(rp) => rp.pop_debug_group(), 44 | RenderGraphEncoderOrPassInner::None => panic!("Already removed the contents of RenderGraphEncoderOrPass"), 45 | } 46 | } 47 | } 48 | 49 | /// Holds either a renderpass or an encoder. 50 | pub struct RenderGraphEncoderOrPass<'a, 'pass>(pub(super) RenderGraphEncoderOrPassInner<'a, 'pass>); 51 | 52 | impl<'a, 'pass> RenderGraphEncoderOrPass<'a, 'pass> { 53 | /// Takes the encoder out of this struct. 54 | /// 55 | /// # Panics 56 | /// 57 | /// - If this node requested a renderpass. 58 | /// - If a take_* function is called twice. 59 | pub fn take_encoder(&mut self) -> &'a mut CommandEncoder { 60 | match mem::take(&mut self.0) { 61 | RenderGraphEncoderOrPassInner::Encoder(e) => e, 62 | RenderGraphEncoderOrPassInner::RenderPass(_) => { 63 | panic!("called get_encoder when the rendergraph node asked for a renderpass"); 64 | } 65 | RenderGraphEncoderOrPassInner::None => panic!("Already removed the contents of RenderGraphEncoderOrPass"), 66 | } 67 | } 68 | 69 | /// Takes the renderpass out of this struct using the given handle. 70 | /// 71 | /// # Panics 72 | /// 73 | /// - If a take_* function is called twice. 74 | pub fn take_rpass(&mut self, _handle: DeclaredDependency) -> &'a mut RenderPass<'pass> { 75 | match mem::take(&mut self.0) { 76 | RenderGraphEncoderOrPassInner::Encoder(_) => { 77 | panic!("Internal rendergraph error: trying to get renderpass when one was not asked for") 78 | } 79 | RenderGraphEncoderOrPassInner::RenderPass(rpass) => rpass, 80 | RenderGraphEncoderOrPassInner::None => panic!("Already removed the contents of RenderGraphEncoderOrPass"), 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /rend3/src/graph/store.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, marker::PhantomData}; 2 | 3 | use wgpu::TextureView; 4 | 5 | use crate::{ 6 | graph::{ 7 | DataContents, DeclaredDependency, GraphSubResource, RenderTargetHandle, RpassTemporaryPool, TextureRegion, 8 | }, 9 | util::typedefs::FastHashMap, 10 | }; 11 | 12 | /// Handle to arbitrary graph-stored data. 13 | pub struct DataHandle { 14 | pub(super) idx: usize, 15 | pub(super) _phantom: PhantomData, 16 | } 17 | 18 | impl std::fmt::Debug for DataHandle { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | f.debug_struct("DataHandle").field("idx", &self.idx).finish() 21 | } 22 | } 23 | 24 | impl Copy for DataHandle {} 25 | 26 | impl Clone for DataHandle { 27 | fn clone(&self) -> Self { 28 | *self 29 | } 30 | } 31 | 32 | impl PartialEq for DataHandle { 33 | fn eq(&self, other: &Self) -> bool { 34 | self.idx == other.idx && self._phantom == other._phantom 35 | } 36 | } 37 | 38 | /// Provides read-only access to the renderer and access to graph resources. 39 | /// 40 | /// This is how you turn [DeclaredDependency] into actual wgpu resources. 41 | pub struct RenderGraphDataStore<'a> { 42 | pub(super) texture_mapping: &'a FastHashMap, 43 | pub(super) external_texture_mapping: &'a FastHashMap, 44 | pub(super) data: &'a [DataContents], // Any is RefCell> where T is the stored data 45 | } 46 | 47 | impl<'a> RenderGraphDataStore<'a> { 48 | /// Get a rendertarget from the handle to one. 49 | pub fn get_render_target(&self, dep: DeclaredDependency) -> &'a TextureView { 50 | match dep.handle.resource { 51 | GraphSubResource::Texture(name) => { 52 | self.texture_mapping.get(&name).expect("internal rendergraph error: failed to get named texture") 53 | } 54 | GraphSubResource::ImportedTexture(name) => self 55 | .external_texture_mapping 56 | .get(&name) 57 | .expect("internal rendergraph error: failed to get named texture"), 58 | r => { 59 | panic!("internal rendergraph error: tried to get a {:?} as a render target", r) 60 | } 61 | } 62 | } 63 | 64 | /// Set the custom data behind a data handle. 65 | /// 66 | /// # Panics 67 | /// 68 | /// If get_data was called in the same renderpass, calling this will panic. 69 | pub fn set_data(&self, dep: DeclaredDependency>, data: Option) { 70 | *self 71 | .data 72 | .get(dep.handle.idx) 73 | .expect("internal rendergraph error: failed to get buffer") 74 | .inner 75 | .downcast_ref::>>() 76 | .expect("internal rendergraph error: downcasting failed") 77 | .try_borrow_mut() 78 | .expect("tried to call set_data on a handle that has an outstanding borrow through get_data") = data 79 | } 80 | 81 | /// Gets the custom data behind a data handle. If it has not been set, or 82 | /// set to None, this will return None. 83 | pub fn get_data( 84 | &self, 85 | temps: &'a RpassTemporaryPool<'a>, 86 | dep: DeclaredDependency>, 87 | ) -> Option<&'a T> { 88 | let borrow = self 89 | .data 90 | .get(dep.handle.idx) 91 | .expect("internal rendergraph error: failed to get buffer") 92 | .inner 93 | .downcast_ref::>>() 94 | .expect("internal rendergraph error: downcasting failed") 95 | .try_borrow() 96 | .expect("internal rendergraph error: read-only borrow failed"); 97 | match *borrow { 98 | Some(_) => { 99 | let r = temps.add(borrow); 100 | Some(r.as_ref().unwrap()) 101 | } 102 | None => None, 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /rend3/src/graph/temp.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use bumpalo::Bump; 4 | 5 | /// Pool which extends the lifetime of objects to the length of the renderpass. 6 | /// 7 | /// Essentially just bumpalo + destructors. 8 | pub struct RpassTemporaryPool<'rpass> { 9 | bump: Bump, 10 | dtors: RefCell>>, 11 | } 12 | impl<'rpass> RpassTemporaryPool<'rpass> { 13 | pub(super) fn new() -> Self { 14 | Self { bump: Bump::new(), dtors: RefCell::new(Vec::new()) } 15 | } 16 | 17 | #[allow(clippy::mut_from_ref)] 18 | pub fn add(&'rpass self, v: T) -> &'rpass mut T { 19 | let r = self.bump.alloc(v); 20 | let ptr = r as *mut T; 21 | self.dtors.borrow_mut().push(Box::new(move || unsafe { std::ptr::drop_in_place(ptr) })); 22 | r 23 | } 24 | 25 | pub(crate) unsafe fn clear(&mut self) { 26 | for dtor in self.dtors.get_mut().drain(..) { 27 | dtor() 28 | } 29 | self.bump.reset(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rend3/src/graph/texture_store.rs: -------------------------------------------------------------------------------- 1 | //! Rendergraph 2 | 3 | use std::sync::Arc; 4 | 5 | use wgpu::{Device, Extent3d, Texture, TextureDescriptor, TextureDimension}; 6 | 7 | use crate::{ 8 | graph::RenderTargetCore, 9 | util::typedefs::{FastBuildHasher, FastHashMap}, 10 | }; 11 | 12 | struct StoredTexture { 13 | inner: Arc, 14 | used: bool, 15 | } 16 | 17 | pub(crate) struct GraphTextureStore { 18 | textures: FastHashMap>, 19 | } 20 | impl GraphTextureStore { 21 | pub fn new() -> Self { 22 | Self { textures: FastHashMap::with_capacity_and_hasher(32, FastBuildHasher::default()) } 23 | } 24 | 25 | pub fn get_texture(&mut self, device: &Device, desc: RenderTargetCore) -> Arc { 26 | let vec = self.textures.entry(desc).or_insert_with(|| Vec::with_capacity(16)); 27 | if let Some(tex) = vec.pop() { 28 | return Arc::clone(&tex.inner); 29 | } 30 | 31 | Arc::new(device.create_texture(&TextureDescriptor { 32 | label: None, 33 | size: Extent3d { width: desc.resolution.x, height: desc.resolution.y, depth_or_array_layers: 1 }, 34 | mip_level_count: desc.mip_count() as u32, 35 | sample_count: desc.samples as _, 36 | dimension: TextureDimension::D2, 37 | format: desc.format, 38 | usage: desc.usage, 39 | view_formats: &[], 40 | })) 41 | } 42 | 43 | pub fn return_texture(&mut self, desc: RenderTargetCore, tex: Arc) { 44 | let vec = self.textures.entry(desc).or_insert_with(|| Vec::with_capacity(16)); 45 | 46 | vec.push(StoredTexture { inner: tex, used: true }); 47 | } 48 | 49 | pub fn mark_unused(&mut self) { 50 | for vec in self.textures.values_mut() { 51 | for tex in vec { 52 | tex.used = false; 53 | } 54 | } 55 | } 56 | 57 | pub fn remove_unused(&mut self) { 58 | for vec in self.textures.values_mut() { 59 | vec.retain(|t| t.used); 60 | } 61 | 62 | self.textures.retain(|_, v| !v.is_empty()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rend3/src/managers/camera.rs: -------------------------------------------------------------------------------- 1 | use glam::{Mat4, Vec3}; 2 | use rend3_types::Handedness; 3 | 4 | use crate::{ 5 | types::{Camera, CameraProjection}, 6 | util::frustum::Frustum, 7 | }; 8 | 9 | /// Manages the camera's location and projection settings. 10 | #[derive(Debug, Clone)] 11 | pub struct CameraState { 12 | handedness: Handedness, 13 | orig_view: Mat4, 14 | proj: Mat4, 15 | inv_view: Mat4, 16 | world_frustum: Frustum, 17 | data: Camera, 18 | aspect_ratio: f32, 19 | } 20 | impl CameraState { 21 | /// Builds a new camera, using the given aspect ratio. If no aspect ratio is 22 | /// given it is assumed that no aspect ratio scaling should be done. 23 | pub fn new(data: Camera, handedness: Handedness, aspect_ratio: Option) -> Self { 24 | profiling::scope!("CameraState::new"); 25 | 26 | let aspect_ratio = aspect_ratio.unwrap_or(1.0); 27 | let proj = compute_projection_matrix(data, handedness, aspect_ratio); 28 | let orig_view = compute_origin_matrix(data); 29 | 30 | let frustum = Frustum::from_matrix(proj * data.view); 31 | 32 | Self { handedness, orig_view, proj, inv_view: data.view.inverse(), world_frustum: frustum, data, aspect_ratio } 33 | } 34 | 35 | /// Sets the camera data, rebuilding the using the given aspect ratio. If no 36 | /// aspect ratio is given it is assumed that no aspect ratio scaling 37 | /// should be done. 38 | pub fn set_data(&mut self, data: Camera) { 39 | self.set_aspect_data(data, self.aspect_ratio) 40 | } 41 | 42 | pub fn set_aspect_ratio(&mut self, aspect_ratio: Option) { 43 | self.set_aspect_data(self.data, aspect_ratio.unwrap_or(1.0)); 44 | } 45 | 46 | pub fn set_aspect_data(&mut self, data: Camera, aspect_ratio: f32) { 47 | self.proj = compute_projection_matrix(data, self.handedness, aspect_ratio); 48 | self.orig_view = compute_origin_matrix(data); 49 | self.inv_view = data.view.inverse(); 50 | self.world_frustum = Frustum::from_matrix(self.proj * data.view); 51 | self.data = data; 52 | self.aspect_ratio = aspect_ratio; 53 | } 54 | 55 | pub fn get_data(&self) -> Camera { 56 | self.data 57 | } 58 | 59 | pub fn handedness(&self) -> Handedness { 60 | self.handedness 61 | } 62 | 63 | pub fn view(&self) -> Mat4 { 64 | self.data.view 65 | } 66 | 67 | pub fn view_proj(&self) -> Mat4 { 68 | self.proj * self.data.view 69 | } 70 | 71 | pub fn origin_view_proj(&self) -> Mat4 { 72 | self.proj * self.orig_view 73 | } 74 | 75 | pub fn proj(&self) -> Mat4 { 76 | self.proj 77 | } 78 | 79 | pub fn world_frustum(&self) -> Frustum { 80 | self.world_frustum 81 | } 82 | 83 | pub fn location(&self) -> Vec3 { 84 | self.inv_view.w_axis.truncate() 85 | } 86 | } 87 | 88 | fn compute_projection_matrix(data: Camera, handedness: Handedness, aspect_ratio: f32) -> Mat4 { 89 | match data.projection { 90 | CameraProjection::Orthographic { size } => { 91 | let half = size * 0.5; 92 | if handedness == Handedness::Left { 93 | Mat4::orthographic_lh(-half.x, half.x, -half.y, half.y, half.z, -half.z) 94 | } else { 95 | Mat4::orthographic_rh(-half.x, half.x, -half.y, half.y, half.z, -half.z) 96 | } 97 | } 98 | CameraProjection::Perspective { vfov, near } => { 99 | if handedness == Handedness::Left { 100 | Mat4::perspective_infinite_reverse_lh(vfov.to_radians(), aspect_ratio, near) 101 | } else { 102 | Mat4::perspective_infinite_reverse_rh(vfov.to_radians(), aspect_ratio, near) 103 | } 104 | } 105 | CameraProjection::Raw(proj) => proj, 106 | } 107 | } 108 | 109 | fn compute_origin_matrix(data: Camera) -> Mat4 { 110 | let mut view = data.view; 111 | 112 | view.w_axis = glam::Vec4::W; 113 | view 114 | } 115 | -------------------------------------------------------------------------------- /rend3/src/managers/directional/shadow_camera.rs: -------------------------------------------------------------------------------- 1 | use glam::{Mat4, Vec3, Vec3A}; 2 | use rend3_types::{Camera, CameraProjection, Handedness}; 3 | 4 | use crate::managers::{CameraState, InternalDirectionalLight}; 5 | 6 | pub(super) fn shadow_camera(l: &InternalDirectionalLight, user_camera: &CameraState) -> CameraState { 7 | let camera_location = user_camera.location(); 8 | 9 | let shadow_texel_size = l.inner.distance / l.inner.resolution as f32; 10 | 11 | let look_at = match user_camera.handedness() { 12 | Handedness::Left => Mat4::look_at_lh, 13 | Handedness::Right => Mat4::look_at_rh, 14 | }; 15 | 16 | let origin_view = look_at(Vec3::ZERO, l.inner.direction, Vec3::Y); 17 | let camera_origin_view = origin_view.transform_point3(camera_location); 18 | 19 | let offset = camera_origin_view.truncate() % shadow_texel_size; 20 | let shadow_location = camera_origin_view - Vec3::from((offset, 0.0)); 21 | 22 | let inv_origin_view = origin_view.inverse(); 23 | let new_shadow_location = inv_origin_view.transform_point3(shadow_location); 24 | 25 | CameraState::new( 26 | Camera { 27 | projection: CameraProjection::Orthographic { size: Vec3A::splat(l.inner.distance) }, 28 | view: look_at(new_shadow_location, new_shadow_location + l.inner.direction, Vec3::Y), 29 | }, 30 | user_camera.handedness(), 31 | None, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /rend3/src/managers/graph_storage.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::Any, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use parking_lot::RwLock; 7 | use rend3_types::{GraphDataHandle, RawGraphDataHandleUntyped, WasmNotSend}; 8 | 9 | #[derive(Default)] 10 | pub struct GraphStorage { 11 | // Type under any is RwLock 12 | #[cfg(not(target_arch = "wasm32"))] 13 | data: Vec>>, 14 | #[cfg(target_arch = "wasm32")] 15 | data: Vec>>, 16 | } 17 | 18 | impl GraphStorage { 19 | pub fn new() -> Self { 20 | Self::default() 21 | } 22 | 23 | pub fn add(&mut self, handle: &RawGraphDataHandleUntyped, data: T) { 24 | if handle.idx >= self.data.len() { 25 | self.data.resize_with(handle.idx + 1, || None); 26 | } 27 | self.data[handle.idx] = Some(Box::new(RwLock::new(data))); 28 | } 29 | 30 | pub fn get(&self, handle: &GraphDataHandle) -> impl Deref + '_ { 31 | let rw_lock: &RwLock = self.data[handle.0.idx].as_ref().unwrap().downcast_ref().unwrap(); 32 | rw_lock.try_read().expect("Called get on the same handle that was already borrowed mutably within a renderpass") 33 | } 34 | 35 | pub fn get_mut(&self, handle: &GraphDataHandle) -> impl DerefMut + '_ { 36 | let rw_lock: &RwLock = self.data[handle.0.idx].as_ref().unwrap().downcast_ref().unwrap(); 37 | rw_lock.try_write().expect("Tried to call get_mut on the same handle twice within a renderpass") 38 | } 39 | 40 | pub fn remove(&mut self, handle: &RawGraphDataHandleUntyped) { 41 | self.data[handle.idx] = None; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rend3/src/managers/handle_alloc.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | marker::PhantomData, 3 | panic::Location, 4 | sync::{ 5 | atomic::{AtomicUsize, Ordering}, 6 | Arc, 7 | }, 8 | }; 9 | 10 | use parking_lot::Mutex; 11 | use rend3_types::{RawResourceHandle, ResourceHandle}; 12 | 13 | use crate::{instruction::DeletableRawResourceHandle, Renderer}; 14 | 15 | pub(crate) struct HandleAllocator 16 | where 17 | RawResourceHandle: DeletableRawResourceHandle, 18 | { 19 | max_allocated: AtomicUsize, 20 | freelist: Mutex>, 21 | _phantom: PhantomData, 22 | } 23 | 24 | impl HandleAllocator 25 | where 26 | RawResourceHandle: DeletableRawResourceHandle, 27 | { 28 | pub fn new() -> Self { 29 | Self { max_allocated: AtomicUsize::new(0), freelist: Mutex::new(Vec::new()), _phantom: PhantomData } 30 | } 31 | 32 | pub fn allocate(&self, renderer: &Arc) -> ResourceHandle { 33 | let maybe_idx = self.freelist.lock().pop(); 34 | let idx = maybe_idx.unwrap_or_else(|| self.max_allocated.fetch_add(1, Ordering::Relaxed)); 35 | 36 | let renderer = Arc::clone(renderer); 37 | let destroy_fn = move |handle: RawResourceHandle| { 38 | renderer.instructions.push(handle.into_delete_instruction_kind(), *Location::caller()) 39 | }; 40 | 41 | ResourceHandle::new(destroy_fn, idx) 42 | } 43 | 44 | pub fn deallocate(&self, handle: RawResourceHandle) { 45 | let idx = handle.idx; 46 | self.freelist.lock().push(idx); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rend3/src/managers/material/texture_dedupe.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Index; 2 | 3 | use arrayvec::ArrayVec; 4 | use bimap::BiMap; 5 | use rend3_types::RawTexture2DHandle; 6 | use wgpu::{ 7 | BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, 8 | BindingResource, Device, ShaderStages, TextureViewDimension, 9 | }; 10 | 11 | use crate::{ 12 | managers::TextureManager, 13 | util::freelist::{FreelistIndex, FreelistVec}, 14 | }; 15 | 16 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 17 | pub struct TextureBindGroupIndex(FreelistIndex); 18 | 19 | impl std::fmt::Debug for TextureBindGroupIndex { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | let mut tuple = f.debug_tuple("TextureBindGroupIndex"); 22 | if *self == Self::DUMMY { 23 | tuple.field(&"DUMMY"); 24 | } else { 25 | tuple.field(&self.0 .0); 26 | } 27 | tuple.finish() 28 | } 29 | } 30 | 31 | impl TextureBindGroupIndex { 32 | pub const DUMMY: Self = Self(FreelistIndex(usize::MAX)); 33 | } 34 | 35 | struct StoredBindGroup { 36 | refcount: usize, 37 | inner: BindGroup, 38 | } 39 | 40 | pub struct TextureDeduplicator { 41 | bgls: Vec, 42 | deduplication_map: BiMap>, TextureBindGroupIndex>, 43 | storage: FreelistVec, 44 | } 45 | impl TextureDeduplicator { 46 | pub fn new(device: &Device) -> Self { 47 | let entries: Vec<_> = (0..16) 48 | .map(|i| BindGroupLayoutEntry { 49 | binding: i, 50 | visibility: ShaderStages::VERTEX_FRAGMENT, 51 | ty: wgpu::BindingType::Texture { 52 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 53 | view_dimension: TextureViewDimension::D2, 54 | multisampled: false, 55 | }, 56 | count: None, 57 | }) 58 | .collect(); 59 | 60 | let bgls = (0..16_usize) 61 | .map(|max| { 62 | let max_name = max + 1; 63 | device.create_bind_group_layout(&BindGroupLayoutDescriptor { 64 | label: Some(&format!("rend3 texture BGL for {max_name} textures")), 65 | entries: &entries[0..max], 66 | }) 67 | }) 68 | .collect(); 69 | 70 | Self { bgls, deduplication_map: BiMap::default(), storage: FreelistVec::new() } 71 | } 72 | 73 | pub fn get_or_insert( 74 | &mut self, 75 | device: &Device, 76 | texture_manager_2d: &TextureManager, 77 | array: &[Option], 78 | ) -> TextureBindGroupIndex { 79 | if let Some(&index) = self.deduplication_map.get_by_left(array) { 80 | self.storage[index.0].refcount += 1; 81 | 82 | return index; 83 | } 84 | 85 | let entries: ArrayVec<_, 32> = array 86 | .iter() 87 | .enumerate() 88 | .map(|(idx, handle)| { 89 | let view = if let Some(handle) = *handle { 90 | texture_manager_2d.get_view(handle) 91 | } else { 92 | texture_manager_2d.get_null_view() 93 | }; 94 | 95 | BindGroupEntry { binding: idx as u32, resource: BindingResource::TextureView(view) } 96 | }) 97 | .collect(); 98 | 99 | let bg = device.create_bind_group(&BindGroupDescriptor { 100 | label: None, 101 | layout: &self.bgls[array.len()], 102 | entries: &entries, 103 | }); 104 | 105 | let index = self.storage.push(StoredBindGroup { refcount: 1, inner: bg }); 106 | let index = TextureBindGroupIndex(index); 107 | 108 | self.deduplication_map.insert(array.to_vec(), index); 109 | 110 | index 111 | } 112 | 113 | pub fn remove(&mut self, index: TextureBindGroupIndex) { 114 | let refcount = &mut self.storage[index.0].refcount; 115 | *refcount = refcount.checked_sub(1).unwrap(); 116 | 117 | if *refcount == 0 { 118 | self.storage.remove(index.0); 119 | self.deduplication_map.remove_by_right(&index); 120 | } 121 | } 122 | 123 | pub fn get_bgl(&self, count: usize) -> &BindGroupLayout { 124 | &self.bgls[count] 125 | } 126 | } 127 | 128 | impl Index for TextureDeduplicator { 129 | type Output = BindGroup; 130 | 131 | fn index(&self, index: TextureBindGroupIndex) -> &Self::Output { 132 | &self.storage[index.0].inner 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /rend3/src/managers/point.rs: -------------------------------------------------------------------------------- 1 | use encase::{ArrayLength, ShaderType}; 2 | use glam::{Vec3, Vec4}; 3 | use rend3_types::{PointLight, PointLightChange, RawPointLightHandle}; 4 | use wgpu::{BufferUsages, Device, ShaderStages}; 5 | 6 | use crate::{ 7 | util::{ 8 | bind_merge::{BindGroupBuilder, BindGroupLayoutBuilder}, 9 | buffer::WrappedPotBuffer, 10 | }, 11 | Renderer, 12 | }; 13 | 14 | #[derive(Debug, Clone, ShaderType)] 15 | struct ShaderPointLightBuffer { 16 | count: ArrayLength, 17 | #[size(runtime)] 18 | array: Vec, 19 | } 20 | 21 | #[derive(Debug, Copy, Clone, ShaderType)] 22 | struct ShaderPointLight { 23 | pub position: Vec4, 24 | pub color: Vec3, 25 | pub radius: f32, 26 | } 27 | 28 | /// Manages point lights and their associated shadow maps. 29 | pub struct PointLightManager { 30 | data: Vec>, 31 | data_buffer: WrappedPotBuffer, 32 | } 33 | 34 | impl PointLightManager { 35 | pub fn new(device: &Device) -> Self { 36 | Self { 37 | data: Vec::new(), 38 | data_buffer: WrappedPotBuffer::new(device, BufferUsages::STORAGE, "point light buffer"), 39 | } 40 | } 41 | 42 | pub fn add(&mut self, handle: RawPointLightHandle, light: PointLight) { 43 | if handle.idx >= self.data.len() { 44 | self.data.resize(handle.idx + 1, None); 45 | } 46 | 47 | self.data[handle.idx] = Some(light); 48 | } 49 | 50 | pub fn update(&mut self, handle: RawPointLightHandle, change: PointLightChange) { 51 | self.data[handle.idx].as_mut().unwrap().update_from_changes(change); 52 | } 53 | 54 | pub fn remove(&mut self, handle: RawPointLightHandle) { 55 | self.data[handle.idx].take().unwrap(); 56 | } 57 | 58 | pub fn evaluate(&mut self, renderer: &Renderer) { 59 | let buffer = ShaderPointLightBuffer { 60 | count: ArrayLength, 61 | array: self 62 | .data 63 | .iter() 64 | .flatten() 65 | .map(|light| ShaderPointLight { 66 | position: light.position.extend(1.0), 67 | color: light.color * light.intensity, 68 | radius: light.radius, 69 | }) 70 | .collect(), 71 | }; 72 | 73 | self.data_buffer.write_to_buffer(&renderer.device, &renderer.queue, &buffer); 74 | } 75 | 76 | pub fn add_to_bgl(bglb: &mut BindGroupLayoutBuilder) { 77 | bglb.append( 78 | ShaderStages::FRAGMENT, 79 | wgpu::BindingType::Buffer { 80 | ty: wgpu::BufferBindingType::Storage { read_only: true }, 81 | has_dynamic_offset: false, 82 | min_binding_size: Some(ShaderPointLightBuffer::min_size()), 83 | }, 84 | None, 85 | ); 86 | } 87 | 88 | pub fn add_to_bg<'a>(&'a self, bgb: &mut BindGroupBuilder<'a>) { 89 | bgb.append_buffer(&self.data_buffer); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /rend3/src/profile.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | /// Determines if the renderer is using cpu-driven rendering, or faster gpu-driven 4 | /// rendering. 5 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] 6 | pub enum RendererProfile { 7 | CpuDriven, 8 | GpuDriven, 9 | } 10 | 11 | impl RendererProfile { 12 | /// Turns a RendererMode into a [`ProfileData`] calling the appropriate 13 | /// initalization function. 14 | pub fn into_data(self, cpu: impl FnOnce() -> C, gpu: impl FnOnce() -> G) -> ProfileData { 15 | match self { 16 | Self::CpuDriven => ProfileData::Cpu(cpu()), 17 | Self::GpuDriven => ProfileData::Gpu(gpu()), 18 | } 19 | } 20 | 21 | /// Returns `true` if the renderer profile is [`CpuDriven`]. 22 | /// 23 | /// [`CpuDriven`]: RendererProfile::CpuDriven 24 | #[must_use] 25 | pub fn is_cpu_driven(&self) -> bool { 26 | matches!(self, Self::CpuDriven) 27 | } 28 | 29 | /// Returns `true` if the renderer profile is [`GpuDriven`]. 30 | /// 31 | /// [`GpuDriven`]: RendererProfile::GpuDriven 32 | #[must_use] 33 | pub fn is_gpu_driven(&self) -> bool { 34 | matches!(self, Self::GpuDriven) 35 | } 36 | } 37 | 38 | /// Stores two different types of data depending on the renderer mode. 39 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 40 | pub enum ProfileData { 41 | Cpu(C), 42 | Gpu(G), 43 | } 44 | #[allow(dead_code)] // Even if these are unused, don't warn 45 | impl ProfileData { 46 | pub fn profile(&self) -> RendererProfile { 47 | match self { 48 | Self::Cpu(_) => RendererProfile::CpuDriven, 49 | Self::Gpu(_) => RendererProfile::GpuDriven, 50 | } 51 | } 52 | 53 | pub fn into_cpu(self) -> C { 54 | match self { 55 | Self::Cpu(c) => c, 56 | Self::Gpu(_) => panic!("tried to extract cpu data in gpu mode"), 57 | } 58 | } 59 | 60 | pub fn as_cpu(&self) -> &C { 61 | match self { 62 | Self::Cpu(c) => c, 63 | Self::Gpu(_) => panic!("tried to extract cpu data in gpu mode"), 64 | } 65 | } 66 | 67 | pub fn as_cpu_mut(&mut self) -> &mut C { 68 | match self { 69 | Self::Cpu(c) => c, 70 | Self::Gpu(_) => panic!("tried to extract cpu data in gpu mode"), 71 | } 72 | } 73 | 74 | pub fn as_cpu_only_ref(&self) -> ProfileData<&C, ()> { 75 | match self { 76 | Self::Cpu(c) => ProfileData::Cpu(c), 77 | Self::Gpu(_) => ProfileData::Gpu(()), 78 | } 79 | } 80 | 81 | pub fn as_cpu_only_mut(&mut self) -> ProfileData<&mut C, ()> { 82 | match self { 83 | Self::Cpu(c) => ProfileData::Cpu(c), 84 | Self::Gpu(_) => ProfileData::Gpu(()), 85 | } 86 | } 87 | 88 | pub fn into_gpu(self) -> G { 89 | match self { 90 | Self::Gpu(g) => g, 91 | Self::Cpu(_) => panic!("tried to extract gpu data in cpu mode"), 92 | } 93 | } 94 | 95 | pub fn as_gpu(&self) -> &G { 96 | match self { 97 | Self::Gpu(g) => g, 98 | Self::Cpu(_) => panic!("tried to extract gpu data in cpu mode"), 99 | } 100 | } 101 | 102 | pub fn as_gpu_mut(&mut self) -> &mut G { 103 | match self { 104 | Self::Gpu(g) => g, 105 | Self::Cpu(_) => panic!("tried to extract gpu data in cpu mode"), 106 | } 107 | } 108 | 109 | pub fn as_gpu_only_ref(&self) -> ProfileData<(), &G> { 110 | match self { 111 | Self::Gpu(g) => ProfileData::Gpu(g), 112 | Self::Cpu(_) => ProfileData::Cpu(()), 113 | } 114 | } 115 | 116 | pub fn as_gpu_only_mut(&mut self) -> ProfileData<(), &mut G> { 117 | match self { 118 | Self::Gpu(g) => ProfileData::Gpu(g), 119 | Self::Cpu(_) => ProfileData::Cpu(()), 120 | } 121 | } 122 | 123 | pub fn as_ref(&self) -> ProfileData<&C, &G> { 124 | match self { 125 | Self::Cpu(c) => ProfileData::Cpu(c), 126 | Self::Gpu(c) => ProfileData::Gpu(c), 127 | } 128 | } 129 | 130 | pub fn as_ref_mut(&mut self) -> ProfileData<&mut C, &mut G> { 131 | match self { 132 | Self::Cpu(c) => ProfileData::Cpu(c), 133 | Self::Gpu(c) => ProfileData::Gpu(c), 134 | } 135 | } 136 | 137 | pub fn map_cpu(self, func: impl FnOnce(C) -> C2) -> ProfileData { 138 | match self { 139 | Self::Cpu(c) => ProfileData::Cpu(func(c)), 140 | Self::Gpu(g) => ProfileData::Gpu(g), 141 | } 142 | } 143 | 144 | pub fn map_gpu(self, func: impl FnOnce(G) -> G2) -> ProfileData { 145 | match self { 146 | Self::Cpu(c) => ProfileData::Cpu(c), 147 | Self::Gpu(g) => ProfileData::Gpu(func(g)), 148 | } 149 | } 150 | 151 | pub fn map(self, cpu_func: impl FnOnce(C) -> C2, gpu_func: impl FnOnce(G) -> G2) -> ProfileData { 152 | match self { 153 | Self::Cpu(c) => ProfileData::Cpu(cpu_func(c)), 154 | Self::Gpu(g) => ProfileData::Gpu(gpu_func(g)), 155 | } 156 | } 157 | } 158 | 159 | impl ProfileData { 160 | pub fn into_common(self) -> T { 161 | match self { 162 | Self::Cpu(c) => c, 163 | Self::Gpu(g) => g, 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /rend3/src/renderer/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | use wgpu::Features; 3 | use wgpu_profiler::CreationError; 4 | 5 | /// Enum mapping to each of a device's limit. 6 | #[derive(Debug)] 7 | pub enum LimitType { 8 | BindGroups, 9 | MaxBindingsPerBindGroup, 10 | DynamicUniformBuffersPerPipelineLayout, 11 | DynamicStorageBuffersPerPipelineLayout, 12 | SampledTexturesPerShaderStages, 13 | SamplersPerShaderStages, 14 | StorageBuffersPerShaderStages, 15 | StorageTexturesPerShaderStages, 16 | UniformBuffersPerShaderStages, 17 | UniformBufferBindingSize, 18 | UniformBufferBindingAlignment, 19 | StorageBufferBindingAlignment, 20 | PushConstantSize, 21 | MaxTextureDimension1d, 22 | MaxTextureDimension2d, 23 | MaxTextureDimension3d, 24 | MaxTextureArrayLayers, 25 | MaxStorageBufferBindingSize, 26 | MaxVertexBuffers, 27 | MaxVertexAttributes, 28 | MaxVertexBufferArrayStride, 29 | MaxInterStageShaderComponents, 30 | MaxComputeWorkgroupStorageSize, 31 | MaxComputeInvocationsPerWorkgroup, 32 | MaxComputeWorkgroupSizeX, 33 | MaxComputeWorkgroupSizeY, 34 | MaxComputeWorkgroupSizeZ, 35 | MaxComputeWorkgroupsPerDimension, 36 | MaxBufferSize, 37 | } 38 | 39 | /// Reason why the renderer failed to initialize. 40 | #[derive(Error, Debug)] 41 | pub enum RendererInitializationError { 42 | #[error("No supported adapter found")] 43 | MissingAdapter, 44 | #[error("The device limit of {:?} is {} but renderer requires at least {}", ty, device_limit, required_limit)] 45 | LowDeviceLimit { ty: LimitType, device_limit: u64, required_limit: u64 }, 46 | #[error("Device is missing required features: {:?}", features)] 47 | MissingDeviceFeatures { features: Features }, 48 | #[error("Requesting a device failed")] 49 | RequestDeviceFailed, 50 | #[error("Failed to create GpuProfiler")] 51 | GpuProfilerCreation(#[source] CreationError), 52 | } 53 | -------------------------------------------------------------------------------- /rend3/src/renderer/setup.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use parking_lot::Mutex; 4 | use rend3_types::{Camera, Handedness, TextureFormat}; 5 | use wgpu::TextureViewDimension; 6 | use wgpu_profiler::GpuProfilerSettings; 7 | 8 | use crate::{ 9 | graph::GraphTextureStore, 10 | instruction::InstructionStreamPair, 11 | managers::{ 12 | CameraState, DirectionalLightManager, GraphStorage, MaterialManager, MeshManager, ObjectManager, 13 | PointLightManager, SkeletonManager, TextureManager, 14 | }, 15 | renderer::{HandleAllocators, RendererDataCore}, 16 | util::{mipmap::MipmapGenerator, scatter_copy::ScatterCopy}, 17 | InstanceAdapterDevice, Renderer, RendererInitializationError, 18 | }; 19 | 20 | pub fn create_renderer( 21 | iad: InstanceAdapterDevice, 22 | handedness: Handedness, 23 | aspect_ratio: Option, 24 | ) -> Result, RendererInitializationError> { 25 | profiling::scope!("Renderer::new"); 26 | 27 | let features = iad.device.features(); 28 | let limits = iad.device.limits(); 29 | let downlevel = iad.adapter.get_downlevel_capabilities(); 30 | 31 | let camera_state = CameraState::new(Camera::default(), handedness, aspect_ratio); 32 | 33 | let d2_texture_manager = TextureManager::new( 34 | &iad.device, 35 | iad.profile, 36 | limits.max_sampled_textures_per_shader_stage, 37 | TextureViewDimension::D2, 38 | ); 39 | let d2c_texture_manager = TextureManager::new( 40 | &iad.device, 41 | iad.profile, 42 | limits.max_sampled_textures_per_shader_stage, 43 | TextureViewDimension::Cube, 44 | ); 45 | let mesh_manager = MeshManager::new(&iad.device); 46 | let material_manager = MaterialManager::new(&iad.device); 47 | let object_manager = ObjectManager::new(); 48 | let directional_light_manager = DirectionalLightManager::new(&iad.device); 49 | let point_light_manager = PointLightManager::new(&iad.device); 50 | let skeleton_manager = SkeletonManager::new(); 51 | let graph_storage = GraphStorage::new(); 52 | 53 | let mipmap_generator = MipmapGenerator::new( 54 | &iad.device, 55 | &[ 56 | TextureFormat::Rgba8Unorm, 57 | TextureFormat::Rgba8UnormSrgb, 58 | TextureFormat::Bgra8Unorm, 59 | TextureFormat::Bgra8UnormSrgb, 60 | TextureFormat::Rgba16Float, 61 | ], 62 | ); 63 | 64 | let profiler = Mutex::new( 65 | wgpu_profiler::GpuProfiler::new(GpuProfilerSettings { 66 | enable_timer_queries: features.contains(wgpu::Features::TIMESTAMP_QUERY) && !cfg!(target_arch = "wasm32"), 67 | enable_debug_groups: true, 68 | max_num_pending_frames: 4, 69 | }) 70 | .map_err(RendererInitializationError::GpuProfilerCreation)?, 71 | ); 72 | 73 | let scatter = ScatterCopy::new(&iad.device); 74 | 75 | Ok(Arc::new(Renderer { 76 | instructions: InstructionStreamPair::new(), 77 | 78 | profile: iad.profile, 79 | adapter_info: iad.info, 80 | queue: iad.queue, 81 | device: iad.device, 82 | 83 | features, 84 | limits, 85 | downlevel, 86 | handedness, 87 | 88 | resource_handle_allocators: HandleAllocators::default(), 89 | mesh_manager, 90 | data_core: Mutex::new(RendererDataCore { 91 | viewport_camera_state: camera_state, 92 | d2_texture_manager, 93 | d2c_texture_manager, 94 | material_manager, 95 | object_manager, 96 | directional_light_manager, 97 | point_light_manager, 98 | skeleton_manager, 99 | graph_storage, 100 | profiler, 101 | graph_texture_store: GraphTextureStore::new(), 102 | }), 103 | 104 | mipmap_generator, 105 | scatter, 106 | })) 107 | } 108 | -------------------------------------------------------------------------------- /rend3/src/surface.rs: -------------------------------------------------------------------------------- 1 | use glam::UVec2; 2 | use rend3_types::{TextureFormat, TextureUsages}; 3 | use wgpu::{CompositeAlphaMode, Device, SurfaceConfiguration}; 4 | 5 | use crate::types::{PresentMode, Surface}; 6 | 7 | /// Convinence function that re-configures the surface with the expected usages. 8 | pub fn configure_surface( 9 | surface: &Surface, 10 | device: &Device, 11 | format: TextureFormat, 12 | size: UVec2, 13 | present_mode: PresentMode, 14 | ) { 15 | surface.configure( 16 | device, 17 | &SurfaceConfiguration { 18 | usage: TextureUsages::RENDER_ATTACHMENT, 19 | format, 20 | width: size.x, 21 | height: size.y, 22 | present_mode, 23 | desired_maximum_frame_latency: 2, 24 | alpha_mode: CompositeAlphaMode::Auto, 25 | view_formats: vec![], 26 | }, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /rend3/src/util/bind_merge.rs: -------------------------------------------------------------------------------- 1 | //! Builders for BindGroup and BindGroupLayouts. 2 | //! 3 | //! Automates some boilerplate including index generation. 4 | use std::num::{NonZeroU32, NonZeroU64}; 5 | 6 | use wgpu::{ 7 | BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, 8 | BindingResource, BindingType, Buffer, BufferBinding, BufferBindingType, Device, Sampler, ShaderStages, TextureView, 9 | }; 10 | 11 | /// Builder for BindGroupLayouts. 12 | pub struct BindGroupLayoutBuilder { 13 | bgl_entries: Vec, 14 | } 15 | impl BindGroupLayoutBuilder { 16 | pub fn new() -> Self { 17 | Self { bgl_entries: Vec::with_capacity(16) } 18 | } 19 | 20 | pub fn append(&mut self, visibility: ShaderStages, ty: BindingType, count: Option) -> &mut Self { 21 | let binding = self.bgl_entries.len() as u32; 22 | self.bgl_entries.push(BindGroupLayoutEntry { binding, visibility, ty, count }); 23 | self 24 | } 25 | 26 | pub fn append_buffer( 27 | &mut self, 28 | visibility: ShaderStages, 29 | ty: BufferBindingType, 30 | has_dynamic_offset: bool, 31 | min_binding_size: u64, 32 | ) -> &mut Self { 33 | self.append( 34 | visibility, 35 | BindingType::Buffer { 36 | ty, 37 | has_dynamic_offset, 38 | min_binding_size: Some(NonZeroU64::new(min_binding_size).unwrap()), 39 | }, 40 | None, 41 | ) 42 | } 43 | 44 | pub fn build(&self, device: &Device, label: Option<&str>) -> BindGroupLayout { 45 | device.create_bind_group_layout(&BindGroupLayoutDescriptor { label, entries: &self.bgl_entries }) 46 | } 47 | } 48 | 49 | impl Default for BindGroupLayoutBuilder { 50 | fn default() -> Self { 51 | Self::new() 52 | } 53 | } 54 | 55 | /// Builder for BindGroups. 56 | pub struct BindGroupBuilder<'a> { 57 | bg_entries: Vec>, 58 | } 59 | impl<'a> BindGroupBuilder<'a> { 60 | pub fn new() -> Self { 61 | Self { bg_entries: Vec::with_capacity(16) } 62 | } 63 | 64 | pub fn append(&mut self, resource: BindingResource<'a>) -> &mut Self { 65 | let index = self.bg_entries.len(); 66 | self.bg_entries.push(BindGroupEntry { binding: index as u32, resource }); 67 | self 68 | } 69 | 70 | pub fn append_buffer(&mut self, buffer: &'a Buffer) -> &mut Self { 71 | self.append(buffer.as_entire_binding()); 72 | self 73 | } 74 | 75 | pub fn append_buffer_with_size(&mut self, buffer: &'a Buffer, size: u64) -> &mut Self { 76 | self.append(BindingResource::Buffer(BufferBinding { buffer, offset: 0, size: NonZeroU64::new(size) })); 77 | self 78 | } 79 | 80 | pub fn append_buffer_with_offset_and_size(&mut self, buffer: &'a Buffer, offset: u64, size: u64) -> &mut Self { 81 | self.append(BindingResource::Buffer(BufferBinding { buffer, offset, size: NonZeroU64::new(size) })); 82 | self 83 | } 84 | 85 | pub fn append_sampler(&mut self, sampler: &'a Sampler) -> &mut Self { 86 | self.append(BindingResource::Sampler(sampler)); 87 | self 88 | } 89 | 90 | pub fn append_texture_view(&mut self, texture_view: &'a TextureView) -> &mut Self { 91 | self.append(BindingResource::TextureView(texture_view)); 92 | self 93 | } 94 | 95 | pub fn append_texture_view_array(&mut self, texture_view_array: &'a [&'a TextureView]) -> &mut Self { 96 | self.append(BindingResource::TextureViewArray(texture_view_array)); 97 | self 98 | } 99 | 100 | pub fn build(&self, device: &Device, label: Option<&str>, bgl: &BindGroupLayout) -> BindGroup { 101 | device.create_bind_group(&BindGroupDescriptor { label, layout: bgl, entries: &self.bg_entries }) 102 | } 103 | } 104 | 105 | impl<'a> Default for BindGroupBuilder<'a> { 106 | fn default() -> Self { 107 | Self::new() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /rend3/src/util/buffer.rs: -------------------------------------------------------------------------------- 1 | //! Automatic management of Power-of-Two sized buffers. 2 | 3 | use std::{marker::PhantomData, ops::Deref, sync::Arc}; 4 | 5 | use encase::{private::WriteInto, ShaderType}; 6 | use wgpu::{Buffer, BufferAddress, BufferDescriptor, BufferUsages, Device, Queue}; 7 | 8 | use crate::util::typedefs::SsoString; 9 | 10 | /// Creates, fills, and automatically resizes a power-of-two sized buffer. 11 | #[derive(Debug)] 12 | pub struct WrappedPotBuffer { 13 | inner: Arc, 14 | size: BufferAddress, 15 | // This field is assumed to be a power of 2. 16 | minimum: BufferAddress, 17 | usage: BufferUsages, 18 | label: SsoString, 19 | _phantom: PhantomData, 20 | } 21 | 22 | impl WrappedPotBuffer 23 | where 24 | T: ShaderType + WriteInto, 25 | { 26 | pub fn new(device: &Device, usage: BufferUsages, label: &str) -> Self { 27 | profiling::scope!("WrappedPotBuffer::new"); 28 | 29 | let minimum = T::min_size().get().next_power_of_two().max(4); 30 | 31 | let usage = usage | BufferUsages::COPY_DST; 32 | 33 | Self { 34 | inner: Arc::new(device.create_buffer(&BufferDescriptor { 35 | label: Some(label), 36 | size: minimum, 37 | usage, 38 | mapped_at_creation: false, 39 | })), 40 | size: minimum, 41 | minimum, 42 | usage, 43 | label: SsoString::from(label), 44 | _phantom: PhantomData, 45 | } 46 | } 47 | 48 | fn ensure_size(&mut self, device: &Device, desired: BufferAddress) { 49 | let resize = resize_po2(self.size, desired, self.minimum); 50 | if let Some(size) = resize { 51 | self.size = size; 52 | self.inner = Arc::new(device.create_buffer(&BufferDescriptor { 53 | label: Some(&self.label), 54 | size, 55 | usage: self.usage, 56 | mapped_at_creation: false, 57 | })); 58 | } 59 | } 60 | 61 | pub fn write_to_buffer(&mut self, device: &Device, queue: &Queue, data: &T) { 62 | let size = data.size(); 63 | self.ensure_size(device, size.get()); 64 | 65 | let mut mapped = queue.write_buffer_with(&self.inner, 0, size).unwrap(); 66 | encase::StorageBuffer::new(&mut *mapped).write(data).unwrap(); 67 | drop(mapped); 68 | } 69 | } 70 | 71 | impl Deref for WrappedPotBuffer { 72 | type Target = Buffer; 73 | 74 | fn deref(&self) -> &Self::Target { 75 | &self.inner 76 | } 77 | } 78 | 79 | fn resize_po2(current: BufferAddress, desired: BufferAddress, minimum: BufferAddress) -> Option { 80 | assert!(current.is_power_of_two()); 81 | if current == minimum && desired <= minimum { 82 | return None; 83 | } 84 | let lower_bound = current / 4; 85 | if desired <= lower_bound || current < desired { 86 | Some((desired + 1).next_power_of_two()) 87 | } else { 88 | None 89 | } 90 | } 91 | 92 | #[cfg(test)] 93 | mod test { 94 | use super::resize_po2; 95 | 96 | #[test] 97 | fn automated_buffer_resize() { 98 | assert_eq!(resize_po2(64, 128, 0), Some(256)); 99 | assert_eq!(resize_po2(128, 128, 0), None); 100 | assert_eq!(resize_po2(256, 128, 0), None); 101 | 102 | assert_eq!(resize_po2(64, 64, 0), None); 103 | assert_eq!(resize_po2(128, 64, 0), None); 104 | assert_eq!(resize_po2(256, 65, 0), None); 105 | assert_eq!(resize_po2(256, 64, 0), Some(128)); 106 | assert_eq!(resize_po2(256, 63, 0), Some(64)); 107 | 108 | assert_eq!(resize_po2(16, 16, 0), None); 109 | assert_eq!(resize_po2(16, 8, 0), None); 110 | assert_eq!(resize_po2(16, 4, 0), Some(8)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /rend3/src/util/error_scope.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for working with wgpu error scopes. 2 | 3 | use std::{ 4 | future::Future, 5 | pin::Pin, 6 | task::{Context, Poll}, 7 | }; 8 | 9 | use wgpu::Device; 10 | 11 | /// Helper for working with allocation failure error scopes. 12 | /// 13 | /// Because WebGPU uses asynchronous allocation, we cannot handle allocation failures 14 | /// on WebGPU. This will always return success on WebGPU. 15 | #[must_use = "All error scopes must end in a call to `end`"] 16 | pub struct AllocationErrorScope<'a> { 17 | device: &'a Device, 18 | /// Used to communicate with the destructor if `end` was called on this or not. 19 | ended: bool, 20 | } 21 | 22 | impl<'a> AllocationErrorScope<'a> { 23 | /// Create a new AllocationErrorScope on this device. 24 | pub fn new(device: &'a Device) -> Self { 25 | device.push_error_scope(wgpu::ErrorFilter::OutOfMemory); 26 | Self { device, ended: false } 27 | } 28 | 29 | pub fn end(mut self) -> Result<(), wgpu::Error> { 30 | // End has been called, no need to error. 31 | self.ended = true; 32 | 33 | // The future we get from wgpu will always be immedately ready on webgl/native. We can't 34 | // reasonably handle failures on webgpu. As such we don't want to wait 35 | // for the future to complete, just manually poll it once. 36 | 37 | let mut future = self.device.pop_error_scope(); 38 | let pin = Pin::new(&mut future); 39 | match pin.poll(&mut Context::from_waker(&noop_waker::noop_waker())) { 40 | // We got an error, so return an error. 41 | Poll::Ready(Some(error)) => Err(error), 42 | // We got no error, so return success. 43 | Poll::Ready(None) => Ok(()), 44 | // We're on webgpu, pretend everything always works. 45 | Poll::Pending => Ok(()), 46 | } 47 | } 48 | } 49 | 50 | impl<'a> Drop for AllocationErrorScope<'a> { 51 | fn drop(&mut self) { 52 | if !self.ended { 53 | log::error!("AllocationErrorScope dropped without calling `end`"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rend3/src/util/freelist/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::{any::TypeId, ops::Deref}; 2 | 3 | use encase::{private::WriteInto, ShaderSize}; 4 | use wgpu::{Buffer, BufferDescriptor, BufferUsages, CommandEncoder, Device}; 5 | 6 | use crate::util::scatter_copy::{ScatterCopy, ScatterData}; 7 | 8 | pub struct FreelistDerivedBuffer { 9 | inner: Buffer, 10 | 11 | current_count: usize, 12 | reserved_count: usize, 13 | rounded_size: u64, 14 | stored_type: TypeId, 15 | 16 | stale: Vec, 17 | } 18 | impl FreelistDerivedBuffer { 19 | pub const STARTING_SIZE: usize = 16; 20 | pub const NEEDED_USAGES: BufferUsages = 21 | BufferUsages::STORAGE.union(BufferUsages::COPY_DST).union(BufferUsages::COPY_SRC); 22 | 23 | pub fn new(device: &Device) -> Self 24 | where 25 | T: ShaderSize + WriteInto + 'static, 26 | { 27 | let rounded_size = T::METADATA.alignment().round_up(T::SHADER_SIZE.get()); 28 | 29 | let buffer = device.create_buffer(&BufferDescriptor { 30 | label: Some("freelist buffer"), 31 | size: rounded_size * Self::STARTING_SIZE as u64, 32 | usage: Self::NEEDED_USAGES, 33 | mapped_at_creation: false, 34 | }); 35 | 36 | Self { 37 | inner: buffer, 38 | 39 | current_count: Self::STARTING_SIZE, 40 | reserved_count: Self::STARTING_SIZE, 41 | rounded_size, 42 | stored_type: TypeId::of::(), 43 | 44 | stale: Vec::new(), 45 | } 46 | } 47 | 48 | pub fn use_index(&mut self, index: usize) { 49 | if index > self.reserved_count { 50 | self.reserved_count = index.next_power_of_two(); 51 | } 52 | 53 | self.stale.push(index); 54 | } 55 | 56 | pub fn apply( 57 | &mut self, 58 | device: &Device, 59 | encoder: &mut CommandEncoder, 60 | scatter: &ScatterCopy, 61 | mut get_value: F, 62 | ) where 63 | T: ShaderSize + WriteInto + 'static, 64 | F: FnMut(usize) -> T, 65 | { 66 | assert_eq!(self.stored_type, TypeId::of::()); 67 | 68 | if self.current_count != self.reserved_count { 69 | let new_buffer = device.create_buffer(&BufferDescriptor { 70 | label: Some("freelist buffer"), 71 | size: self.rounded_size * self.reserved_count as u64, 72 | usage: Self::NEEDED_USAGES, 73 | mapped_at_creation: false, 74 | }); 75 | 76 | encoder.copy_buffer_to_buffer( 77 | &self.inner, 78 | 0, 79 | &new_buffer, 80 | 0, 81 | self.current_count as u64 * self.rounded_size, 82 | ); 83 | 84 | self.inner = new_buffer; 85 | self.current_count = self.reserved_count; 86 | } 87 | 88 | if self.stale.is_empty() { 89 | return; 90 | } 91 | 92 | let data = self.stale.drain(..).map(|idx| { 93 | let data = get_value(idx); 94 | ScatterData { word_offset: u32::try_from((idx as u64 * self.rounded_size) / 4).unwrap(), data } 95 | }); 96 | 97 | scatter.execute_copy(device, encoder, &self.inner, data); 98 | } 99 | } 100 | 101 | impl Deref for FreelistDerivedBuffer { 102 | type Target = wgpu::Buffer; 103 | 104 | fn deref(&self) -> &Self::Target { 105 | &self.inner 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /rend3/src/util/freelist/vec.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Index, IndexMut}; 2 | 3 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 4 | pub struct FreelistIndex(pub usize); 5 | 6 | pub struct FreelistVec { 7 | data: Vec>, 8 | freelist: Vec, 9 | } 10 | 11 | impl FreelistVec { 12 | pub fn new() -> Self { 13 | Self { data: Vec::new(), freelist: Vec::new() } 14 | } 15 | 16 | pub fn push(&mut self, value: T) -> FreelistIndex { 17 | if let Some(index) = self.freelist.pop() { 18 | debug_assert!(self.data[index].is_none()); 19 | self.data[index] = Some(value); 20 | FreelistIndex(index) 21 | } else { 22 | let index = self.data.len(); 23 | self.data.push(Some(value)); 24 | FreelistIndex(index) 25 | } 26 | } 27 | 28 | pub fn remove(&mut self, index: FreelistIndex) { 29 | debug_assert!(self.data[index.0].is_some()); 30 | self.data[index.0] = None; 31 | self.freelist.push(index.0); 32 | } 33 | } 34 | 35 | impl Default for FreelistVec { 36 | fn default() -> Self { 37 | Self::new() 38 | } 39 | } 40 | 41 | impl Index for FreelistVec { 42 | type Output = T; 43 | 44 | fn index(&self, index: FreelistIndex) -> &Self::Output { 45 | self.data[index.0].as_ref().unwrap() 46 | } 47 | } 48 | 49 | impl IndexMut for FreelistVec { 50 | fn index_mut(&mut self, index: FreelistIndex) -> &mut Self::Output { 51 | self.data[index.0].as_mut().unwrap() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rend3/src/util/frustum.rs: -------------------------------------------------------------------------------- 1 | //! Frustums and bounding spheres. 2 | //! 3 | //! This entire module only exists because of . 4 | 5 | use encase::ShaderType; 6 | use glam::{Mat4, Vec3, Vec3A, Vec4Swizzles}; 7 | 8 | /// Represents a point in space and a radius from that point. 9 | #[derive(Debug, Default, Clone, Copy, ShaderType)] 10 | pub struct BoundingSphere { 11 | pub center: Vec3, 12 | pub radius: f32, 13 | } 14 | impl BoundingSphere { 15 | pub fn from_mesh(mesh: &[Vec3]) -> Self { 16 | let center = find_mesh_center(mesh); 17 | let radius = find_mesh_bounding_sphere_radius(center, mesh); 18 | 19 | Self { center: Vec3::from(center), radius } 20 | } 21 | 22 | pub fn apply_transform(self, matrix: Mat4) -> Self { 23 | let max_scale = matrix 24 | .x_axis 25 | .xyz() 26 | .length_squared() 27 | .max(matrix.y_axis.xyz().length_squared().max(matrix.z_axis.xyz().length_squared())) 28 | .sqrt(); 29 | let center = matrix * self.center.extend(1.0); 30 | 31 | Self { center: center.truncate(), radius: max_scale * self.radius } 32 | } 33 | } 34 | 35 | fn find_mesh_center(mesh: &[Vec3]) -> Vec3A { 36 | let first = if let Some(first) = mesh.first() { 37 | *first 38 | } else { 39 | return Vec3A::ZERO; 40 | }; 41 | // Bounding box time baby! 42 | let mut max = Vec3A::from(first); 43 | let mut min = max; 44 | 45 | for pos in mesh.iter().skip(1) { 46 | let pos = Vec3A::from(*pos); 47 | max = max.max(pos); 48 | min = min.min(pos); 49 | } 50 | 51 | (max + min) / 2.0 52 | } 53 | 54 | fn find_mesh_bounding_sphere_radius(mesh_center: Vec3A, mesh: &[Vec3]) -> f32 { 55 | mesh.iter().fold(0.0, |distance, pos| distance.max((Vec3A::from(*pos) - mesh_center).length())) 56 | } 57 | 58 | /// Represents a plane as a vec4 (or vec3 + f32) 59 | #[derive(Debug, Copy, Clone, ShaderType)] 60 | pub struct Plane { 61 | pub abc: Vec3, 62 | pub d: f32, 63 | } 64 | 65 | impl Plane { 66 | pub fn new(a: f32, b: f32, c: f32, d: f32) -> Self { 67 | Self { abc: Vec3::new(a, b, c), d } 68 | } 69 | 70 | pub fn normalize(mut self) -> Self { 71 | let mag = self.abc.length(); 72 | 73 | self.abc /= mag; 74 | self.d /= mag; 75 | 76 | self 77 | } 78 | 79 | pub fn distance(self, point: Vec3) -> f32 { 80 | self.abc.dot(point) + self.d 81 | } 82 | } 83 | 84 | /// A frustum composed of 5 different planes. Has no far plane as it assumes 85 | /// infinite. 86 | #[derive(Debug, Copy, Clone, ShaderType)] 87 | pub struct Frustum { 88 | left: Plane, 89 | right: Plane, 90 | top: Plane, 91 | bottom: Plane, 92 | near: Plane, 93 | } 94 | 95 | impl Frustum { 96 | pub fn from_matrix(matrix: Mat4) -> Self { 97 | let mat_arr = matrix.to_cols_array_2d(); 98 | 99 | let left = Plane::new( 100 | mat_arr[0][3] + mat_arr[0][0], 101 | mat_arr[1][3] + mat_arr[1][0], 102 | mat_arr[2][3] + mat_arr[2][0], 103 | mat_arr[3][3] + mat_arr[3][0], 104 | ); 105 | 106 | let right = Plane::new( 107 | mat_arr[0][3] - mat_arr[0][0], 108 | mat_arr[1][3] - mat_arr[1][0], 109 | mat_arr[2][3] - mat_arr[2][0], 110 | mat_arr[3][3] - mat_arr[3][0], 111 | ); 112 | 113 | let top = Plane::new( 114 | mat_arr[0][3] - mat_arr[0][1], 115 | mat_arr[1][3] - mat_arr[1][1], 116 | mat_arr[2][3] - mat_arr[2][1], 117 | mat_arr[3][3] - mat_arr[3][1], 118 | ); 119 | 120 | let bottom = Plane::new( 121 | mat_arr[0][3] + mat_arr[0][1], 122 | mat_arr[1][3] + mat_arr[1][1], 123 | mat_arr[2][3] + mat_arr[2][1], 124 | mat_arr[3][3] + mat_arr[3][1], 125 | ); 126 | 127 | // no far plane as we have infinite depth 128 | 129 | // this is the far plane in the algorithm, but we're using inverse Z, so near 130 | // and far get flipped. 131 | let near = Plane::new( 132 | mat_arr[0][3] - mat_arr[0][2], 133 | mat_arr[1][3] - mat_arr[1][2], 134 | mat_arr[2][3] - mat_arr[2][2], 135 | mat_arr[3][3] - mat_arr[3][2], 136 | ); 137 | 138 | Self { 139 | left: left.normalize(), 140 | right: right.normalize(), 141 | top: top.normalize(), 142 | bottom: bottom.normalize(), 143 | near: near.normalize(), 144 | } 145 | } 146 | 147 | /// Determins if the sphere is at all inside the frustum. 148 | pub fn contains_sphere(&self, sphere: BoundingSphere) -> bool { 149 | let neg_radius = -sphere.radius; 150 | 151 | let array = [self.left, self.right, self.top, self.bottom, self.near]; 152 | 153 | for plane in &array { 154 | let inside = plane.distance(sphere.center) >= neg_radius; 155 | if !inside { 156 | return false; 157 | } 158 | } 159 | 160 | true 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /rend3/src/util/iter.rs: -------------------------------------------------------------------------------- 1 | /// Iterator adapter that implements ExactSizeIterator even 2 | /// when the inner iterator doesn't. Size must be accurate 3 | /// for the count of the iterator. 4 | #[derive(Clone)] 5 | pub struct ExactSizerIterator { 6 | inner: I, 7 | size: usize, 8 | } 9 | 10 | impl ExactSizerIterator 11 | where 12 | I: Iterator + Clone, 13 | { 14 | /// Creates a new iterator adapter. In debug validates 15 | /// that the iterator is the same length as it says it is. 16 | pub fn new(inner: I, size: usize) -> Self { 17 | debug_assert_eq!(inner.clone().count(), size); 18 | Self { inner, size } 19 | } 20 | } 21 | 22 | impl Iterator for ExactSizerIterator 23 | where 24 | I: Iterator, 25 | { 26 | type Item = I::Item; 27 | 28 | fn next(&mut self) -> Option { 29 | self.inner.next() 30 | } 31 | 32 | fn size_hint(&self) -> (usize, Option) { 33 | (self.size, Some(self.size)) 34 | } 35 | } 36 | 37 | impl ExactSizeIterator for ExactSizerIterator 38 | where 39 | I: Iterator, 40 | { 41 | fn len(&self) -> usize { 42 | let (lower, _) = self.size_hint(); 43 | lower 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rend3/src/util/math.rs: -------------------------------------------------------------------------------- 1 | //! Math utilites. 2 | 3 | use num_traits::PrimInt; 4 | 5 | pub trait IntegerExt: PrimInt { 6 | /// Rounds T away from zero to the nearest multiple of b. 7 | /// 8 | /// Panics if b is zero or negative. 9 | fn round_up(self, b: Self) -> Self { 10 | round_up(self, b) 11 | } 12 | 13 | /// Performs integer division between a and b rounding away from zero, instead of towards it. 14 | /// 15 | /// Panics if b is zero or negative. 16 | fn div_round_up(self, b: Self) -> Self { 17 | div_round_up(self, b) 18 | } 19 | } 20 | 21 | impl IntegerExt for T {} 22 | 23 | /// Rounds T away from zero to the nearest multiple of b. 24 | /// 25 | /// Panics if b is zero or negative. 26 | pub fn round_up(a: T, b: T) -> T { 27 | assert!(b > T::zero(), "divisor must be non-zero and positive"); 28 | // All the negative infrastructure will compile away if T is unsigned as this is unconditionally false 29 | let negative = a < T::zero(); 30 | 31 | let pos_a = if negative { T::zero() - a } else { a }; 32 | 33 | let rem = pos_a % b; 34 | if rem == T::zero() { 35 | return a; 36 | } 37 | 38 | let pos_res = pos_a + (b - rem); 39 | 40 | if negative { 41 | T::zero() - pos_res 42 | } else { 43 | pos_res 44 | } 45 | } 46 | 47 | /// Performs integer division between a and b rounding away from zero, instead of towards it. 48 | /// 49 | /// Panics if b is zero or negative. 50 | pub fn div_round_up(a: T, b: T) -> T { 51 | assert!(b > T::zero(), "divisor must be non-zero and positive"); 52 | // All the negative infrastructure will compile away if T is unsigned as this is unconditionally false 53 | let negative = a < T::zero(); 54 | 55 | let pos_a = if negative { T::zero() - a } else { a }; 56 | 57 | let pos_res = (pos_a + (b - T::one())) / b; 58 | 59 | if negative { 60 | T::zero() - pos_res 61 | } else { 62 | pos_res 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | #[test] 69 | fn round_up() { 70 | assert_eq!(super::round_up(2, 12), 12); 71 | assert_eq!(super::round_up(12, 12), 12); 72 | assert_eq!(super::round_up(0, 12), 0); 73 | 74 | // Negatives 75 | assert_eq!(super::round_up(-14, 12), -24); 76 | assert_eq!(super::round_up(-8, 12), -12); 77 | 78 | // Identity 79 | assert_eq!(super::round_up(2, 1), 2); 80 | } 81 | 82 | #[test] 83 | fn round_up_div() { 84 | assert_eq!(super::div_round_up(2, 12), 1); 85 | assert_eq!(super::div_round_up(12, 12), 1); 86 | assert_eq!(super::div_round_up(18, 12), 2); 87 | assert_eq!(super::div_round_up(0, 12), 0); 88 | 89 | // Negatives 90 | assert_eq!(super::div_round_up(-14, 12), -2); 91 | assert_eq!(super::div_round_up(-8, 12), -1); 92 | 93 | // Identity 94 | assert_eq!(super::div_round_up(2, 1), 2); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rend3/src/util/output.rs: -------------------------------------------------------------------------------- 1 | //! Output frame and surface acquisition. 2 | 3 | use std::sync::Arc; 4 | 5 | use wgpu::{SurfaceTexture, TextureView}; 6 | 7 | /// Anything that resembles a surface to render to. 8 | pub enum OutputFrame { 9 | // Pre-acquired surface. rend3 will present it. 10 | SurfaceAcquired { view: TextureView, surface_tex: SurfaceTexture }, 11 | // Arbitrary texture view. 12 | View(Arc), 13 | } 14 | 15 | impl OutputFrame { 16 | /// Turn the given surface into a texture view, if it has one. 17 | pub fn as_view(&self) -> Option<&TextureView> { 18 | match self { 19 | Self::SurfaceAcquired { view, .. } => Some(view), 20 | Self::View(inner) => Some(&**inner), 21 | } 22 | } 23 | 24 | /// Present the surface, if needed. 25 | pub fn present(self) { 26 | if let Self::SurfaceAcquired { surface_tex: surface, .. } = self { 27 | surface.present(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rend3/src/util/sync.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use parking_lot::{Condvar, Mutex}; 4 | 5 | // Syncronization primitive that allows waiting for all registered work to finish. 6 | pub struct WaitGroup { 7 | counter: Mutex, 8 | condvar: Condvar, 9 | } 10 | 11 | impl WaitGroup { 12 | pub fn new() -> Arc { 13 | Arc::new(Self { counter: Mutex::new(0), condvar: Condvar::new() }) 14 | } 15 | 16 | pub fn increment(self: &Arc) -> DecrementGuard { 17 | *self.counter.lock() += 1; 18 | 19 | DecrementGuard { wg: self.clone() } 20 | } 21 | 22 | fn decrement(&self) { 23 | let mut counter = self.counter.lock(); 24 | *counter -= 1; 25 | if *counter == 0 { 26 | self.condvar.notify_all(); 27 | } 28 | } 29 | 30 | pub fn wait(&self) { 31 | let mut counter = self.counter.lock(); 32 | while *counter != 0 { 33 | self.condvar.wait(&mut counter); 34 | } 35 | } 36 | } 37 | 38 | pub struct DecrementGuard { 39 | wg: Arc, 40 | } 41 | 42 | impl Drop for DecrementGuard { 43 | fn drop(&mut self) { 44 | self.wg.decrement(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rend3/src/util/typedefs.rs: -------------------------------------------------------------------------------- 1 | //! Typedefs for commonly used structures from other crates. 2 | 3 | /// A string which uses SmallString optimization for strings shorter than 23 4 | /// characters. 5 | pub type SsoString = smartstring::SmartString; 6 | /// Hash map designed for small keys. 7 | pub type FastHashMap = rustc_hash::FxHashMap; 8 | /// Hash set designed for small keys. 9 | pub type FastHashSet = rustc_hash::FxHashSet; 10 | /// Hasher designed for small keys. 11 | pub type FastHasher = rustc_hash::FxHasher; 12 | /// Build hasher designed for small keys. 13 | pub type FastBuildHasher = std::hash::BuildHasherDefault; 14 | /// Output of wgpu_profiler's code. 15 | pub type RendererStatistics = Vec; 16 | 17 | #[macro_export] 18 | /// Similar to the [`format`] macro, but creates a [`SsoString`]. 19 | macro_rules! format_sso { 20 | ($($arg:tt)*) => {{ 21 | use std::fmt::Write as _; 22 | let mut buffer = $crate::util::typedefs::SsoString::new(); 23 | write!(buffer, $($arg)*).expect("unexpected formatting error"); 24 | buffer 25 | }}; 26 | } 27 | -------------------------------------------------------------------------------- /rend3/src/util/upload.rs: -------------------------------------------------------------------------------- 1 | use wgpu::{Buffer, CommandEncoder, Device}; 2 | 3 | use crate::util::error_scope::AllocationErrorScope; 4 | 5 | struct Upload<'a> { 6 | staging_offset: u64, 7 | offset: u64, 8 | data: &'a [u8], 9 | } 10 | 11 | pub struct UploadChainer<'a> { 12 | staging_buffer: Option, 13 | uploads: Vec>, 14 | total_size: u64, 15 | } 16 | 17 | impl<'a> UploadChainer<'a> { 18 | pub fn new() -> Self { 19 | Self { staging_buffer: None, uploads: Vec::new(), total_size: 0 } 20 | } 21 | 22 | pub fn add(&mut self, offset: u64, data: &'a [u8]) { 23 | self.uploads.push(Upload { staging_offset: self.total_size, offset, data }); 24 | self.total_size += data.len() as u64; 25 | } 26 | 27 | pub fn create_staging_buffer(&mut self, device: &Device) -> Result<(), wgpu::Error> { 28 | let scope = AllocationErrorScope::new(device); 29 | self.staging_buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { 30 | label: Some("mesh staging buffer"), 31 | size: self.total_size, 32 | usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::MAP_WRITE, 33 | mapped_at_creation: true, 34 | })); 35 | scope.end()?; 36 | 37 | Ok(()) 38 | } 39 | 40 | pub fn encode_upload(&self, encoder: &mut CommandEncoder, buffer: &Buffer) { 41 | let staging_buffer = self.staging_buffer.as_ref().unwrap(); 42 | 43 | for upload in &self.uploads { 44 | encoder.copy_buffer_to_buffer( 45 | staging_buffer, 46 | upload.staging_offset, 47 | buffer, 48 | upload.offset, 49 | upload.data.len() as u64, 50 | ); 51 | } 52 | } 53 | 54 | pub fn stage(&mut self) { 55 | let staging_buffer = self.staging_buffer.as_ref().unwrap(); 56 | 57 | let mut mapping = staging_buffer.slice(..).get_mapped_range_mut(); 58 | for upload in &self.uploads { 59 | mapping[upload.staging_offset as usize..][..upload.data.len()].copy_from_slice(upload.data); 60 | } 61 | drop(mapping); 62 | 63 | staging_buffer.unmap(); 64 | } 65 | } 66 | 67 | impl<'a> Default for UploadChainer<'a> { 68 | fn default() -> Self { 69 | Self::new() 70 | } 71 | } 72 | --------------------------------------------------------------------------------