├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codecov.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── client.gif ├── index.html ├── mesh_to_sdf ├── ARCHITECTURE.md ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── assets │ ├── FlightHelmet.glb │ ├── README.md │ ├── annoted_cube.glb │ ├── ferris3d.glb │ ├── knight.glb │ └── suzanne.glb ├── benches │ ├── generate_grid_sdf.rs │ └── generate_sdf.rs ├── examples │ └── demo.rs ├── proptest-regressions │ └── geo.txt ├── src │ ├── bvh_ext.rs │ ├── generate │ │ ├── generic │ │ │ ├── bvh.rs │ │ │ ├── default.rs │ │ │ ├── mod.rs │ │ │ ├── rtree.rs │ │ │ └── rtree_bvh.rs │ │ ├── grid.rs │ │ └── mod.rs │ ├── geo.rs │ ├── grid.rs │ ├── lib.rs │ ├── point.rs │ ├── point │ │ ├── impl_array.rs │ │ ├── impl_cgmath.rs │ │ ├── impl_glam.rs │ │ ├── impl_mint.rs │ │ └── impl_nalgebra.rs │ └── serde.rs └── tests │ ├── generate_python_baseline.py │ ├── sdf_generic_v1.bin │ └── sdf_grid_v1.bin ├── mesh_to_sdf_client ├── ARCHITECTURE.md ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── shaders │ ├── draw_cubemap.wgsl │ ├── draw_model.wgsl │ ├── draw_raymarching.wgsl │ ├── draw_sdf.wgsl │ ├── draw_shadowmap.wgsl │ ├── draw_voxels.wgsl │ └── utility │ │ └── mipmap_generation.wgsl ├── src │ ├── camera.rs │ ├── camera_control.rs │ ├── cubemap.rs │ ├── frame_rate.rs │ ├── gltf │ │ ├── mod.rs │ │ ├── scene │ │ │ ├── camera.rs │ │ │ ├── light.rs │ │ │ ├── mod.rs │ │ │ └── model │ │ │ │ ├── material │ │ │ │ ├── emissive.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── normal.rs │ │ │ │ ├── occlusion.rs │ │ │ │ └── pbr.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── mode.rs │ │ │ │ └── vertex.rs │ │ └── utils │ │ │ ├── gltf_data.rs │ │ │ └── mod.rs │ ├── main.rs │ ├── passes │ │ ├── cubemap_generation_pass.rs │ │ ├── mip_generation_pass.rs │ │ ├── mod.rs │ │ ├── model_render_pass.rs │ │ ├── raymarch_pass.rs │ │ ├── sdf_render_pass.rs │ │ ├── shadow_pass.rs │ │ └── voxel_render_pass.rs │ ├── pbr │ │ ├── mesh.rs │ │ ├── mesh │ │ │ └── primitives.rs │ │ ├── mod.rs │ │ ├── model.rs │ │ ├── model_instance.rs │ │ └── shadow_map.rs │ ├── reload_flags.rs │ ├── runner.rs │ ├── sdf.rs │ ├── sdf_program.rs │ ├── sdf_program │ │ ├── command_stack.rs │ │ └── ui.rs │ ├── texture.rs │ └── utility │ │ ├── mip_generation.rs │ │ ├── mod.rs │ │ └── shader_builder.rs └── tests │ ├── box_sparse.glb │ ├── complete.glb │ ├── cube.glb │ ├── cube.png │ ├── cube_classic.bin │ ├── cube_classic.gltf │ ├── dragon.glb │ ├── head.glb │ └── suzanne.glb └── update_web.bat /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Required to avoid a linker error on Windows when building the dylib. 2 | [target.x86_64-pc-windows-msvc] 3 | # linker = "rust-lld.exe" 4 | # rustflags = ["-Zshare-generics=n"] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | labels: 17 | - "dependencies" 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI # Continuous Integration 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | name: Test Suite 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Update ubuntu 12 | run: sudo apt-get update -y 13 | - name: Install libgtk for rfd 14 | run: sudo apt-get install -y libgtk-3-dev 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | - name: Install Rust toolchain 18 | uses: dtolnay/rust-toolchain@stable 19 | - uses: Swatinem/rust-cache@v2 20 | - name: Run tests 21 | run: cargo test --all-features --workspace 22 | 23 | rustfmt: 24 | name: Rustfmt 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Update ubuntu 28 | run: sudo apt-get update -y 29 | - name: Install libgtk for rfd 30 | run: sudo apt-get install -y libgtk-3-dev 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | - name: Install Rust toolchain 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: rustfmt 37 | - uses: Swatinem/rust-cache@v2 38 | - name: Check formatting 39 | run: cargo fmt --all --check 40 | 41 | clippy: 42 | name: Clippy 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Update ubuntu 46 | run: sudo apt-get update -y 47 | - name: Install libgtk for rfd 48 | run: sudo apt-get install -y libgtk-3-dev 49 | - name: Checkout repository 50 | uses: actions/checkout@v4 51 | - name: Install Rust toolchain 52 | uses: dtolnay/rust-toolchain@stable 53 | with: 54 | components: clippy 55 | - uses: Swatinem/rust-cache@v2 56 | - name: Clippy check 57 | run: cargo clippy --all-features --workspace 58 | 59 | docs: 60 | name: Docs 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Update ubuntu 64 | run: sudo apt-get update -y 65 | - name: Install libgtk for rfd 66 | run: sudo apt-get install -y libgtk-3-dev 67 | - name: Checkout repository 68 | uses: actions/checkout@v4 69 | - name: Install Rust toolchain 70 | uses: dtolnay/rust-toolchain@stable 71 | - uses: Swatinem/rust-cache@v2 72 | - name: Check documentation 73 | env: 74 | RUSTDOCFLAGS: -D warnings 75 | run: cargo doc --no-deps --document-private-items --all-features --workspace 76 | 77 | hack: 78 | name: Check 79 | runs-on: ubuntu-latest 80 | steps: 81 | - name: Update ubuntu 82 | run: sudo apt-get update -y 83 | - name: Install libgtk for rfd 84 | run: sudo apt-get install -y libgtk-3-dev 85 | - uses: actions/checkout@v4 86 | with: 87 | submodules: true 88 | - name: Install stable 89 | uses: dtolnay/rust-toolchain@stable 90 | - name: cargo install cargo-hack 91 | uses: taiki-e/install-action@cargo-hack 92 | - uses: Swatinem/rust-cache@v2 93 | - name: Check for all features 94 | run: cargo hack --feature-powerset check 95 | 96 | # wasm: 97 | # name: Wasm 98 | # runs-on: ubuntu-latest 99 | # steps: 100 | # - name: Update ubuntu 101 | # run: sudo apt-get update -y 102 | # - name: Install libgtk for rfd 103 | # run: sudo apt-get install -y libgtk-3-dev 104 | # - uses: actions/checkout@v4 105 | # with: 106 | # submodules: true 107 | # - name: Install stable 108 | # uses: dtolnay/rust-toolchain@stable 109 | # with: 110 | # target: wasm32-unknown-unknown 111 | # - uses: Swatinem/rust-cache@v2 112 | # - name: Check for target wasm webgpu 113 | # env: 114 | # RUSTFLAGS: --cfg=web_sys_unstable_apis 115 | # run: cargo check --target wasm32-unknown-unknown 116 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | - '!dependabot/**' 8 | pull_request_target: 9 | 10 | jobs: 11 | coverage: 12 | runs-on: ubuntu-latest 13 | env: 14 | CARGO_TERM_COLOR: always 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install Rust 18 | run: rustup update stable 19 | - name: Install cargo-llvm-cov 20 | uses: taiki-e/install-action@cargo-llvm-cov 21 | - name: Generate code coverage 22 | run: cargo llvm-cov --all-features --package mesh_to_sdf --lcov --output-path lcov.info 23 | - name: Upload coverage to Codecov 24 | uses: codecov/codecov-action@v4 25 | with: 26 | files: lcov.info 27 | fail_ci_if_error: true 28 | token: ${{ secrets.CODECOV_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | release: 7 | name: release ${{ matrix.target }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - target: x86_64-pc-windows-gnu 14 | archive: zip 15 | - target: x86_64-unknown-linux-musl 16 | archive: tar.gz tar.xz tar.zst 17 | - target: x86_64-apple-darwin 18 | archive: zip 19 | steps: 20 | - uses: actions/checkout@master 21 | - name: Compile and release 22 | uses: rust-build/rust-build.action@v1.4.5 23 | env: 24 | SRC_DIR: "mesh_to_sdf_client" 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | TOOLCHAIN_VERSION: 1.74 # 1.75 does not work with x86_64-apple-darwin yet. 27 | ARGS: --locked --release 28 | with: 29 | RUSTTARGET: ${{ matrix.target }} 30 | ARCHIVE_TYPES: ${{ matrix.archive }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | mutants.out 3 | mutants.out.old 4 | .vscode/launch.json 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["mesh_to_sdf", "mesh_to_sdf_client"] 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | license = "MIT OR Apache-2.0" 8 | version = "0.2.1" 9 | 10 | 11 | [profile.release-with-debug] 12 | inherits = "release" 13 | debug = true 14 | 15 | [workspace.lints.rust] 16 | dead_code = "allow" 17 | missing_docs = "warn" 18 | unexpected_cfgs = "warn" 19 | 20 | [workspace.lints.clippy] 21 | all = { level = "warn", priority = -1 } 22 | pedantic = { level = "warn", priority = -1 } 23 | nursery = { level = "warn", priority = -1 } 24 | complexity = {level = "warn", priority = -1 } 25 | style = { level = "warn", priority = -1 } 26 | 27 | significant_drop_tightening = "allow" 28 | module_name_repetitions = "allow" 29 | cast_sign_loss = "allow" 30 | cast_precision_loss = "allow" 31 | cast_possible_truncation = "allow" 32 | missing_errors_doc = "allow" 33 | missing_panics_doc = "allow" 34 | 35 | # From restriction 36 | same_name_method = "allow" # because of rust embed, see https://github.com/pyrossh/rust-embed/issues/204 37 | std_instead_of_core = "warn" 38 | clone_on_ref_ptr = "warn" 39 | renamed_function_params = "warn" 40 | #unseparated_literal_suffix = "warn" 41 | redundant_type_annotations = "warn" 42 | partial_pub_fields = "warn" 43 | let_underscore_untyped = "warn" 44 | let_underscore_must_use = "warn" 45 | ref_patterns = "warn" 46 | undocumented_unsafe_blocks = "warn" 47 | wildcard_enum_match_arm = "warn" 48 | suboptimal_flops = "allow" 49 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /client.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/client.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /mesh_to_sdf/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | ## Mesh to SDF 2 | 3 | `generate_sdf` bruteforces the closest point on the mesh for each query point by checking the distance to each triangle, using `rayon` to parallelize the computation. Further work would be to use a spatial data structure (e.g. bvh) to speed up the computation, and letting the user choose the algorithm (since a bvh requires memory that grows linearly with the number of triangles). 4 | 5 | `generate_grid_sdf` uses a binary heap to keep track of the closest triangle for each cell. At initialization, the heap is filled with the closest triangle for each cell. Then, the heap is iteratively updated by checking the distance to the closest triangle for each cell, until the heap is empty. 6 | 7 | *Determining sign*: currently the only method is to check the normals of the triangles. A robust method is needed, via raycasting for example. Raycasting can be optimized for the grid by aligning the rays with the grid axes. 8 | 9 | Point - triangles methods can be optimized further. -------------------------------------------------------------------------------- /mesh_to_sdf/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0] - 2024-09-17 4 | 5 | ### Added 6 | 7 | - New `AccelerationMethod`s with `Rtree` and `RtreeBvh`. `Rtree` uses a r-tree for distances and the normal sign method, while `RtreeBvh` uses a r-tree for distances and a bvh for raycast. 8 | Both are about 4x faster than the previous `Bvh` for 10k and more queries. 9 | 10 | ### Changed 11 | 12 | - `generate_grid_sdf` is now fully parallelized. It's between 10x and 20x faster on a high end cpu, depending on the number of triangles and the grid resolution. 13 | - `AccelerationMethod` now includes the `SignMethod` when it makes sense. `generate_sdf` only takes the `AccelerationMethod` parameter, and the `SignMethod` is inferred from it. 14 | This is because not all acceleration methods support all sign methods. For example, `Rtree` only supports the normal sign method, while `RtreeBvh` supports raycasting only. 15 | - `Point` trait now requires `Debug` and `PartialEq` to be implemented. 16 | - `RtreeBvh` is the new default `AccelerationMethod`. 17 | 18 | 19 | ## [0.3.0] - 2024-09-06 20 | 21 | ### Added 22 | 23 | - `generate_sdf` takes an additional `AccelerationMethod` parameter, that can be either `AccelerationMethod::Bvh` (default) or `AccelerationMethod::None`. 24 | `Bvh` is recommended unless you're really tight on memory or your query is very small (couple hundreds of queries and triangles). 25 | For example, a query of 15k points with 100k triangles is 10 times faster with a bvh, and twice faster for 500 queries and 10k triangles. 26 | `Bvh` is also optimised for the `SignMethod::Raycast` sign method. https://github.com/Azkellas/mesh_to_sdf/pull/75 27 | - Both generic and grid based sdfs can now be (de-)serialized. Use `SerializeGrid` and `DeserializeGrid`, or the helpers `save_to_file` and `read_from_file`. 28 | 29 | ### Fixed 30 | 31 | - Fix https://github.com/Azkellas/mesh_to_sdf/issues/25 where `generate_grid_sdf` might panic if the grid does not contain the mesh. 32 | - Make grid based generation ~2x faster by improving heap generation. 33 | 34 | ### Removed 35 | 36 | - nalgebra is no longer optional as it is required by bvh. The feature was removed as it will always be available. 37 | 38 | 39 | ## [0.2.1] - 2024-02-18 40 | 41 | ### Changed 42 | 43 | - `generate_grid_sdf` with `SignMethod::Raycast` now tests in the three directions (+x, +y, +z). This does not slow the computation down, but it makes the result more robust. This does not affect `generate_sdf`. 44 | - `Grid::from_bounding_box` takes a `[usize; 3]` instead of `&[usize; 3]` for `cell_count` 45 | 46 | 47 | ## [0.2.0] - 2024-02-16 48 | 49 | ### Added 50 | 51 | - `SignMethod` enum to represent the sign method used to calculate the signed distance. 52 | - `generate_sdf` and `generate_grid_sdf` now take a `SignMethod` parameter. 53 | - `SignMethod::Raycast` to use raycasting to determine the sign of the distance. 54 | 55 | ## [0.1.0] - 2024-02-05 56 | 57 | First release of `mesh_to_sdf`. 58 | 59 | ### Added 60 | 61 | - `generate_sdf` function to get the signed distance from query points to a mesh. 62 | - `generate_grid_sdf` function to get the signed distance from a grid to a mesh. 63 | - `Point` trait to allow the use of any type as vertices. 64 | - `Topology` enum to represent the topology of the mesh. 65 | - `Grid` struct to represent a grid. 66 | - `[f32; 3]` implementation for `Point` trait. 67 | - `cgmath` implementation for `Point` trait. 68 | - `glam` implementation for `Point` trait. 69 | - `mint` implementation for `Point` trait. 70 | - `nalgebra` implementation for `Point` trait. 71 | -------------------------------------------------------------------------------- /mesh_to_sdf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mesh_to_sdf" 3 | version = "0.4.0" 4 | description = "Mesh to signed distance field (SDF) converter" 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | 8 | authors = ["Etienne Desbois "] 9 | homepage = "https://github.com/Azkellas/mesh_to_sdf" 10 | repository = "https://github.com/Azkellas/mesh_to_sdf" 11 | keywords = ["mesh", "sdf", "distance_field", "raymarching", "voxels"] 12 | categories = [ 13 | "algorithms", 14 | "rendering::data-formats", 15 | "game-development", 16 | "graphics", 17 | "data-structures", 18 | ] 19 | 20 | exclude = ["assets/*", "ARCHITECTURE.md"] 21 | 22 | [dependencies] 23 | float-cmp = "0.9.0" 24 | itertools = "0.13.0" 25 | log = "0.4.22" 26 | ordered-float = "4.2.2" 27 | rayon = "1.10.0" 28 | bvh = "0.10.0" 29 | 30 | glam = { version = "0.29.0", optional = true } 31 | mint = { version = "0.5.9", optional = true } 32 | cgmath = { version = "0.18.0", optional = true } 33 | # required for bvh. 34 | nalgebra = "0.33.0" 35 | 36 | serde = { version = "1.0.210", features = ["derive"], optional = true } 37 | rmp-serde = { version = "1.3.0", optional = true } 38 | rstar = "0.12.0" 39 | parking_lot = "0.12.3" 40 | web-time = "1.1.0" 41 | 42 | [features] 43 | cgmath = ["dep:cgmath"] 44 | glam = ["dep:glam"] 45 | mint = ["dep:mint"] 46 | serde = [ 47 | "dep:serde", 48 | "dep:rmp-serde", 49 | "glam?/serde", 50 | "mint?/serde", 51 | "nalgebra/serde-serialize", 52 | "cgmath?/serde", 53 | ] 54 | 55 | [dev-dependencies] 56 | criterion = { version = "0.5.1", features = ["html_reports"] } 57 | easy-gltf = "1.1.2" 58 | env_logger = "0.11.5" 59 | proptest = "1.5.0" 60 | tempfile = "3.12.0" 61 | 62 | [[bench]] 63 | name = "generate_sdf" 64 | harness = false 65 | 66 | [[bench]] 67 | name = "generate_grid_sdf" 68 | harness = false 69 | 70 | [lints] 71 | workspace = true 72 | 73 | [package.metadata.docs.rs] 74 | all-features = true 75 | -------------------------------------------------------------------------------- /mesh_to_sdf/README.md: -------------------------------------------------------------------------------- 1 | # mesh_to_sdf 2 | 3 | This crate provides two entry points: 4 | 5 | - [`generate_sdf`]: computes the signed distance field for the mesh defined by `vertices` and `indices` at the points `query_points`. 6 | - [`generate_grid_sdf`]: computes the signed distance field for the mesh defined by `vertices` and `indices` on a [Grid]. 7 | 8 | ```rust 9 | use mesh_to_sdf::{generate_sdf, generate_grid_sdf, SignMethod, AccelerationMethod, Topology, Grid}; 10 | // vertices are [f32; 3], but can be cgmath::Vector3, glam::Vec3, etc. 11 | let vertices: Vec<[f32; 3]> = vec![[0.5, 1.5, 0.5], [1., 2., 3.], [1., 3., 7.]]; 12 | let indices: Vec = vec![0, 1, 2]; 13 | 14 | // query points must be of the same type as vertices 15 | let query_points: Vec<[f32; 3]> = vec![[0.5, 0.5, 0.5]]; 16 | 17 | // Query points are expected to be in the same space as the mesh. 18 | let sdf: Vec = generate_sdf( 19 | &vertices, 20 | Topology::TriangleList(Some(&indices)), // TriangleList as opposed to TriangleStrip 21 | &query_points, 22 | AccelerationMethod::RtreeBvh, // Use an r-tree and a bvh to accelerate queries. 23 | ); 24 | 25 | for point in query_points.iter().zip(sdf.iter()) { 26 | // distance is positive outside the mesh and negative inside. 27 | println!("Distance to {:?}: {}", point.0, point.1); 28 | } 29 | 30 | // if you can, use generate_grid_sdf instead of generate_sdf as it's optimized and much faster. 31 | let bounding_box_min = [0., 0., 0.]; 32 | let bounding_box_max = [10., 10., 10.]; 33 | let cell_count = [10, 10, 10]; 34 | 35 | let grid = Grid::from_bounding_box(&bounding_box_min, &bounding_box_max, cell_count); 36 | 37 | let sdf: Vec = generate_grid_sdf( 38 | &vertices, 39 | Topology::TriangleList(Some(&indices)), 40 | &grid, 41 | SignMethod::Raycast, // How the sign is computed. 42 | // Raycast is robust but requires the mesh to be watertight. 43 | // and is more expensive. 44 | // Normal might leak negative distances outside the mesh 45 | ); // but works for all meshes, even surfaces. 46 | 47 | for x in 0..cell_count[0] { 48 | for y in 0..cell_count[1] { 49 | for z in 0..cell_count[2] { 50 | let index = grid.get_cell_idx(&[x, y, z]); 51 | log::info!("Distance to cell [{}, {}, {}]: {}", x, y, z, sdf[index as usize]); 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | --- 58 | 59 | ##### Mesh Topology 60 | 61 | Indices can be of any type that implements `Into`, e.g. `u16` and `u32`. Topology can be list or strip. 62 | If the indices are not provided, they are supposed to be `0..vertices.len()`. 63 | 64 | For vertices, this library aims to be as generic as possible by providing a trait `Point` that can be implemented for any type. 65 | Implementations for most common math libraries are gated behind feature flags. By default, `[f32; 3]` and `nalgebra::[Point3, Vector3]` are provided. 66 | If you do not find your favorite library, feel free to implement the trait for it and submit a PR or open an issue. 67 | 68 | --- 69 | 70 | ##### Computing sign 71 | 72 | This crate provides two methods to compute the sign of the distance: 73 | - [`SignMethod::Raycast`] (default): a robust method to compute the sign of the distance. It counts the number of intersections between a ray starting from the query point and the triangles of the mesh. 74 | It only works for watertight meshes, but guarantees the sign is correct. 75 | - [`SignMethod::Normal`]: uses the normals of the triangles to estimate the sign by doing a dot product with the direction of the query point. 76 | It works for non-watertight meshes but might leak negative distances outside the mesh. 77 | 78 | Using `Raycast` is slower than `Normal` but gives better results. Performances depends on the triangle count and method used. 79 | On big dataset, `Raycast` is 5-10% slower for grid generation and rtree based methods. On smaller dataset, the difference can be worse 80 | depending on whether the query is triangle intensive or query point intensive. 81 | For bvh the difference is negligible between the two methods. 82 | 83 | --- 84 | 85 | ##### Acceleration structures 86 | 87 | For generic queries, you can use acceleration structures to speed up the computation. 88 | - [`AccelerationMethod::None`]: no acceleration structure. This is the slowest method but requires no extra memory. Scales really poorly. 89 | - [`AccelerationMethod::Bvh`]: Bounding Volume Hierarchy. Accepts a `SignMethod`. 90 | - [`AccelerationMethod::Rtree`]: R-tree. Uses `SignMethod::Normal`. The fastest method assuming you have more than a couple thousands of queries. 91 | - [`AccelerationMethod::RtreeBvh`] (default): Uses R-tree for nearest neighbor search and Bvh for `SignMethod::Raycast`. 5-10% slower than `Rtree` on big datasets. 92 | 93 | If your mesh is watertight and you have more than a thousand queries/triangles, you should use `AccelerationMethod::RtreeBvh` for best performances. 94 | If it's not watertight, you can use `AccelerationMethod::Rtree` instead. 95 | 96 | `Rtree` methods are 3-4x faster than `Bvh` methods for big enough data. On small meshes, the difference is negligible. 97 | `AccelerationMethod::None` scales really poorly and should be avoided unless for small datasets or if you're really tight on memory. 98 | 99 | --- 100 | 101 | ##### Using your favorite library 102 | 103 | To use your favorite math library with `mesh_to_sdf`, you need to add it to `mesh_to_sdf` dependency. For example, to use `glam`: 104 | ```toml 105 | [dependencies] 106 | mesh_to_sdf = { version = "0.2.1", features = ["glam"] } 107 | ``` 108 | 109 | Currently, the following libraries are supported: 110 | - [cgmath] ([`cgmath::Vector3`]) 111 | - [glam] ([`glam::Vec3`]) 112 | - [mint] ([`mint::Vector3`] and [`mint::Point3`]) 113 | - [nalgebra] ([`nalgebra::Vector3`] and [`nalgebra::Point3`]) 114 | - `[f32; 3]` 115 | 116 | [nalgebra] is always available as it's used internally in the bvh tree. 117 | 118 | --- 119 | 120 | ##### Serialization 121 | 122 | If you want to serialize and deserialize signed distance fields, you need to enable the `serde` feature. 123 | This features also provides helpers to save and load signed distance fields to and from files via `save_to_file` and `read_from_file`. 124 | 125 | License: MIT OR Apache-2.0 126 | -------------------------------------------------------------------------------- /mesh_to_sdf/assets/FlightHelmet.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf/assets/FlightHelmet.glb -------------------------------------------------------------------------------- /mesh_to_sdf/assets/README.md: -------------------------------------------------------------------------------- 1 | Credits to Blender for suzanne.glb 2 | knight.glb and flighthelmet.glb come from [gltf samples](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/ABeautifulGame) 3 | ferris.glb comes from https://github.com/RayMarch/ferris3d 4 | suzanne.glb comes from blender -------------------------------------------------------------------------------- /mesh_to_sdf/assets/annoted_cube.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf/assets/annoted_cube.glb -------------------------------------------------------------------------------- /mesh_to_sdf/assets/ferris3d.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf/assets/ferris3d.glb -------------------------------------------------------------------------------- /mesh_to_sdf/assets/knight.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf/assets/knight.glb -------------------------------------------------------------------------------- /mesh_to_sdf/assets/suzanne.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf/assets/suzanne.glb -------------------------------------------------------------------------------- /mesh_to_sdf/benches/generate_grid_sdf.rs: -------------------------------------------------------------------------------- 1 | //! Benchmark for the `generate_grid_sdf` function 2 | use itertools::{Itertools, MinMaxResult}; 3 | 4 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 5 | fn criterion_benchmark(c: &mut Criterion) { 6 | // env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 7 | 8 | let path = "assets/knight.glb"; 9 | let gltf = easy_gltf::load(path).unwrap(); 10 | 11 | let model = &gltf.first().unwrap().models[0]; 12 | let vertices = model.vertices(); 13 | let indices = model.indices().unwrap(); 14 | 15 | let xbounds = vertices.iter().map(|v| v.position.x).minmax(); 16 | let ybounds = vertices.iter().map(|v| v.position.y).minmax(); 17 | let zbounds = vertices.iter().map(|v| v.position.z).minmax(); 18 | 19 | let MinMaxResult::MinMax(xmin, xmax) = xbounds else { 20 | panic!("No vertices"); 21 | }; 22 | let MinMaxResult::MinMax(ymin, ymax) = ybounds else { 23 | panic!("No vertices"); 24 | }; 25 | let MinMaxResult::MinMax(zmin, zmax) = zbounds else { 26 | panic!("No vertices"); 27 | }; 28 | 29 | let vertices = vertices 30 | .iter() 31 | .map(|v| [v.position.x, v.position.y, v.position.z]) 32 | .collect_vec(); 33 | 34 | let cell_count = [16, 16, 16]; 35 | let grid = 36 | mesh_to_sdf::Grid::from_bounding_box(&[xmin, ymin, zmin], &[xmax, ymax, zmax], cell_count); 37 | 38 | println!("vertices: {:?}", vertices.len()); 39 | println!("triangles: {:?}", indices.len() / 3); 40 | println!("grid size: {cell_count:?}"); 41 | 42 | c.bench_function("generate_grid_sdf_normal", |b| { 43 | b.iter(|| { 44 | mesh_to_sdf::generate_grid_sdf( 45 | black_box(&vertices), 46 | black_box(mesh_to_sdf::Topology::TriangleList(Some(indices))), 47 | black_box(&grid), 48 | black_box(mesh_to_sdf::SignMethod::Normal), 49 | ); 50 | }); 51 | }); 52 | 53 | c.bench_function("generate_grid_sdf_raycast", |b| { 54 | b.iter(|| { 55 | mesh_to_sdf::generate_grid_sdf( 56 | black_box(&vertices), 57 | black_box(mesh_to_sdf::Topology::TriangleList(Some(indices))), 58 | black_box(&grid), 59 | black_box(mesh_to_sdf::SignMethod::Raycast), 60 | ); 61 | }); 62 | }); 63 | } 64 | 65 | fn criterion_benchmark_big(c: &mut Criterion) { 66 | // env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); 67 | 68 | let path = "assets/knight.glb"; 69 | let gltf = easy_gltf::load(path).unwrap(); 70 | 71 | let model = &gltf.first().unwrap().models[0]; 72 | let vertices = model.vertices(); 73 | let indices = model.indices().unwrap(); 74 | 75 | let xbounds = vertices.iter().map(|v| v.position.x).minmax(); 76 | let ybounds = vertices.iter().map(|v| v.position.y).minmax(); 77 | let zbounds = vertices.iter().map(|v| v.position.z).minmax(); 78 | 79 | let MinMaxResult::MinMax(xmin, xmax) = xbounds else { 80 | panic!("No vertices"); 81 | }; 82 | let MinMaxResult::MinMax(ymin, ymax) = ybounds else { 83 | panic!("No vertices"); 84 | }; 85 | let MinMaxResult::MinMax(zmin, zmax) = zbounds else { 86 | panic!("No vertices"); 87 | }; 88 | 89 | let vertices = vertices 90 | .iter() 91 | .map(|v| [v.position.x, v.position.y, v.position.z]) 92 | .collect_vec(); 93 | 94 | let cell_count = [100, 100, 100]; 95 | let grid = 96 | mesh_to_sdf::Grid::from_bounding_box(&[xmin, ymin, zmin], &[xmax, ymax, zmax], cell_count); 97 | 98 | println!("vertices: {:?}", vertices.len()); 99 | println!("triangles: {:?}", indices.len() / 3); 100 | println!("grid size: {cell_count:?}"); 101 | 102 | c.bench_function("generate_grid_sdf_normal_big", |b| { 103 | b.iter(|| { 104 | mesh_to_sdf::generate_grid_sdf( 105 | black_box(&vertices), 106 | black_box(mesh_to_sdf::Topology::TriangleList(Some(indices))), 107 | black_box(&grid), 108 | black_box(mesh_to_sdf::SignMethod::Normal), 109 | ); 110 | }); 111 | }); 112 | 113 | c.bench_function("generate_grid_sdf_raycast_big", |b| { 114 | b.iter(|| { 115 | mesh_to_sdf::generate_grid_sdf( 116 | black_box(&vertices), 117 | black_box(mesh_to_sdf::Topology::TriangleList(Some(indices))), 118 | black_box(&grid), 119 | black_box(mesh_to_sdf::SignMethod::Raycast), 120 | ); 121 | }); 122 | }); 123 | } 124 | 125 | criterion_group!(benches, criterion_benchmark); 126 | 127 | criterion_group! { 128 | name = benches_big; 129 | config = Criterion::default().sample_size(10); 130 | targets = criterion_benchmark_big 131 | } 132 | 133 | criterion_main!(benches, benches_big); 134 | -------------------------------------------------------------------------------- /mesh_to_sdf/examples/demo.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates how to use `mesh_to_sdf` to generate signed distance fields from a glTF model. 2 | use itertools::Itertools; 3 | use mesh_to_sdf::{AccelerationMethod, SignMethod, Topology}; 4 | 5 | fn main() { 6 | let path = "assets/suzanne.glb"; 7 | let gltf = easy_gltf::load(path).unwrap(); 8 | 9 | let model = &gltf.first().unwrap().models[0]; 10 | let vertices = model.vertices(); 11 | let indices = model.indices().unwrap(); 12 | 13 | // easy_gltf use cgmath::Vector3 for vertices, which is compatible with mesh_to_sdf 14 | // you need to specify the cgmath feature in your Cargo.toml for mesh_to_sdf: 15 | // [dependencies] 16 | // mesh_to_sdf = { version = "0.2.1", features = ["cgmath"] } 17 | let vertices = vertices.iter().map(|v| v.position).collect_vec(); 18 | 19 | // query points must be of the same type as vertices 20 | // and in the vertices space 21 | // if you want to sample from world space, apply the model transform to the query points 22 | let query_points = [cgmath::Vector3::new(0.0, 0.0, 0.0)]; 23 | 24 | println!("query_points: {:?}", query_points.len()); 25 | println!("vertices: {:?}", vertices.len()); 26 | println!("triangles: {:?}", indices.len() / 3); 27 | 28 | // Get signed distance at query points 29 | let sdf = mesh_to_sdf::generate_sdf( 30 | &vertices, 31 | Topology::TriangleList(Some(indices)), 32 | &query_points, 33 | AccelerationMethod::Bvh(SignMethod::Raycast), 34 | ); 35 | 36 | for point in query_points.iter().zip(sdf.iter()) { 37 | println!("Distance to {:?}: {}", point.0, point.1); 38 | } 39 | 40 | // Get grid sdf 41 | // if you can, use generate_grid_sdf instead of generate_sdf. 42 | let bounding_box_min = cgmath::Vector3::new(0., 0., 0.); 43 | let bounding_box_max = cgmath::Vector3::new(2., 2., 2.); 44 | let cell_count = [3_usize, 3, 3]; 45 | 46 | let grid = 47 | mesh_to_sdf::Grid::from_bounding_box(&bounding_box_min, &bounding_box_max, cell_count); 48 | 49 | let sdf: Vec = mesh_to_sdf::generate_grid_sdf( 50 | &vertices, 51 | mesh_to_sdf::Topology::TriangleList(Some(indices)), 52 | &grid, 53 | mesh_to_sdf::SignMethod::Raycast, 54 | ); 55 | 56 | for x in 0..cell_count[0] { 57 | for y in 0..cell_count[1] { 58 | for z in 0..cell_count[2] { 59 | let index = grid.get_cell_idx(&[x, y, z]); 60 | println!("Distance to cell [{}, {}, {}]: {}", x, y, z, sdf[index]); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /mesh_to_sdf/proptest-regressions/geo.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 02a46fee8626317f750111468e954310caef98d16bc3da8fd57264608eb936c9 # shrinks to p = [0.0, -8.055119, 1.1846914], a = [0.0, 0.0, 0.0], b = [0.0, 0.0, 8.367966], c = [-7.806354, 9.330519, 0.0] 8 | cc 475313fb1ce575cb137783d150779b0fbf2aa09368c145c5ce06ed8804212f0e # shrinks to p = [0.0, -5.8359632, 4.405388], a = [0.0, 0.9572999, 9.758267], b = [6.9999175, -4.739112, 7.5462694], c = [0.0, -9.673183, 0.52112055] 9 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/bvh_ext.rs: -------------------------------------------------------------------------------- 1 | use bvh::aabb::{Aabb, Bounded}; 2 | use bvh::bvh::{Bvh, BvhNode}; 3 | 4 | use crate::Point; 5 | pub trait AabbExt { 6 | /// Returns the minimum and maximum distances to the box. 7 | /// The minimum distance is the distance to the closest point on the box, 8 | /// 0 if the point is inside the box. 9 | /// The maximum distance is the distance to the furthest point in the box. 10 | fn get_min_max_distances(&self, point: &V) -> (f32, f32); 11 | } 12 | 13 | impl AabbExt for Aabb { 14 | /// Returns the minimum and maximum distances to the box. 15 | /// The minimum distance is the distance to the closest point on the box, 16 | /// 0 if the point is inside the box. 17 | /// The maximum distance is the distance to the furthest point in the box. 18 | fn get_min_max_distances(&self, point: &V) -> (f32, f32) { 19 | // Convert point to nalgebra point 20 | let n_point: nalgebra::OPoint> = 21 | nalgebra::Point3::new(point.x(), point.y(), point.z()); 22 | 23 | let half_size = self.size() * 0.5; 24 | let center = self.center(); 25 | 26 | let delta = n_point - center; 27 | 28 | // See 29 | let q = delta.abs() - half_size; 30 | let min_dist = q.map(|x| x.max(0.0)).norm(); 31 | 32 | // The signum helps to determine the furthest point 33 | let signum = delta 34 | // Invert the signum to get the furthest vertex 35 | .map(|x| -x.signum()) 36 | // Make sure we're always on a vertex and not on a face if the point is aligned with the box 37 | .map(|x| if x == 0.0 { 1.0 } else { x }); 38 | 39 | let furthest = center + signum.component_mul(&half_size); 40 | let max_dist = (n_point - furthest).norm(); 41 | 42 | (min_dist, max_dist) 43 | } 44 | } 45 | 46 | pub trait BvhDistance> { 47 | /// Traverses the [`Bvh`]. 48 | /// Returns a subset of `shapes` which are candidates for being the closest to `point`. 49 | /// 50 | fn nearest_candidates(&self, origin: &V, shapes: &[Shape]) -> Vec 51 | where 52 | Self: core::marker::Sized; 53 | } 54 | 55 | impl> BvhDistance for Bvh { 56 | /// Traverses the [`Bvh`]. 57 | /// Returns a subset of `shapes` which are candidates for being the closest to `point`. 58 | /// 59 | fn nearest_candidates(&self, origin: &V, shapes: &[Shape]) -> Vec { 60 | let mut indices = Vec::new(); 61 | let mut best_min_distance = f32::MAX; 62 | let mut best_max_distance = f32::MAX; 63 | BvhNode::nearest_candidates_recursive( 64 | &self.nodes, 65 | 0, 66 | origin, 67 | shapes, 68 | &mut indices, 69 | &mut best_min_distance, 70 | &mut best_max_distance, 71 | ); 72 | 73 | indices 74 | .into_iter() 75 | .filter(|(_, node_min)| *node_min <= best_max_distance) 76 | .map(|(i, _)| i) 77 | .collect() 78 | } 79 | } 80 | 81 | pub trait BvhTraverseDistance> { 82 | /// Traverses the [`Bvh`] recursively and returns all shapes whose [`Aabb`] countains 83 | /// a candidate shape for being the nearest to the given point. 84 | /// 85 | fn nearest_candidates_recursive( 86 | nodes: &[Self], 87 | node_index: usize, 88 | origin: &V, 89 | shapes: &[Shape], 90 | indices: &mut Vec<(usize, f32)>, 91 | best_min_distance: &mut f32, 92 | best_max_distance: &mut f32, 93 | ) where 94 | Self: core::marker::Sized; 95 | } 96 | 97 | impl> BvhTraverseDistance for BvhNode { 98 | /// Traverses the [`Bvh`] recursively and returns all shapes whose [`Aabb`] countains 99 | /// a candidate shape for being the nearest to the given point. 100 | /// 101 | #[expect(clippy::similar_names)] 102 | fn nearest_candidates_recursive( 103 | nodes: &[Self], 104 | node_index: usize, 105 | origin: &V, 106 | shapes: &[Shape], 107 | indices: &mut Vec<(usize, f32)>, 108 | best_min_distance: &mut f32, 109 | best_max_distance: &mut f32, 110 | ) { 111 | match &nodes[node_index] { 112 | Self::Node { 113 | child_l_aabb, 114 | child_l_index, 115 | child_r_aabb, 116 | child_r_index, 117 | .. 118 | } => { 119 | // Compute min/max dists for both children 120 | let [child_l_dists, child_r_dists] = 121 | [child_l_aabb, child_r_aabb].map(|aabb| aabb.get_min_max_distances(origin)); 122 | 123 | // Update best max distance before traversing children to avoid unnecessary traversals 124 | // where right node prunes left node. 125 | *best_max_distance = best_max_distance.min(child_l_dists.1.min(child_r_dists.1)); 126 | 127 | // Traverse children 128 | for ((dist_min, dist_max), index) in [ 129 | (child_l_dists, child_l_index), 130 | (child_r_dists, child_r_index), 131 | ] { 132 | // Node is better by a margin. 133 | if dist_max <= *best_min_distance { 134 | indices.clear(); 135 | } 136 | 137 | // Node might contain a candidate 138 | if dist_min <= *best_max_distance { 139 | Self::nearest_candidates_recursive( 140 | nodes, 141 | *index, 142 | origin, 143 | shapes, 144 | indices, 145 | best_min_distance, 146 | best_max_distance, 147 | ); 148 | } 149 | } 150 | } 151 | Self::Leaf { shape_index, .. } => { 152 | let aabb = shapes[*shape_index].aabb(); 153 | let (min_dist, max_dist) = aabb.get_min_max_distances(origin); 154 | 155 | if !indices.is_empty() && max_dist < *best_min_distance { 156 | // Node is better by a margin 157 | indices.clear(); 158 | } 159 | 160 | // Also update min_dist here since we have a credible (small) bounding box. 161 | *best_min_distance = best_min_distance.min(min_dist); 162 | *best_max_distance = best_max_distance.min(max_dist); 163 | 164 | // we reached a leaf, we add it to the list of indices since it is a potential candidate 165 | indices.push((*shape_index, min_dist)); 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/generate/generic/default.rs: -------------------------------------------------------------------------------- 1 | //! Module containing the `generate_sdf_default` function. 2 | use rayon::prelude::*; 3 | 4 | use crate::{compare_distances, geo, Point, SignMethod, Topology}; 5 | 6 | /// Generate a signed distance field from a mesh. 7 | /// Query points are expected to be in the same space as the mesh. 8 | /// 9 | /// Returns a vector of signed distances. 10 | /// Queries outside the mesh will have a positive distance, and queries inside the mesh will have a negative distance. 11 | pub fn generate_sdf_default( 12 | vertices: &[V], 13 | indices: Topology, 14 | query_points: &[V], 15 | sign_method: SignMethod, 16 | ) -> Vec 17 | where 18 | V: Point, 19 | I: Copy + Into + Sync + Send, 20 | { 21 | // For each query point, we compute the distance to each triangle. 22 | // sign is estimated by comparing the normal to the direction. 23 | // when two triangles give the same distance (wrt floating point errors), 24 | // we keep the one with positive distance since to be inside means to be inside all triangles. 25 | // whereas to be outside means to be outside at least one triangle. 26 | // see `compare_distances` for more details. 27 | query_points 28 | .par_iter() 29 | .map(|query| { 30 | Topology::get_triangles(vertices, indices) 31 | .map(|(i, j, k)| (&vertices[i], &vertices[j], &vertices[k])) 32 | .map(|(a, b, c)| match sign_method { 33 | // Raycast: returns (distance, ray_intersection) 34 | SignMethod::Raycast => ( 35 | geo::point_triangle_distance(query, a, b, c), 36 | geo::ray_triangle_intersection_aligned(query, [a, b, c], geo::GridAlign::X) 37 | .is_some(), 38 | ), 39 | // Normal: returns (signed_distance, false) 40 | SignMethod::Normal => { 41 | (geo::point_triangle_signed_distance(query, a, b, c), false) 42 | } 43 | }) 44 | .fold( 45 | (f32::MAX, 0), 46 | |(min_distance, intersection_count), (distance, ray_intersection)| { 47 | match sign_method { 48 | SignMethod::Raycast => ( 49 | min_distance.min(distance), 50 | intersection_count + u32::from(ray_intersection), 51 | ), 52 | SignMethod::Normal => ( 53 | match compare_distances(distance, min_distance) { 54 | core::cmp::Ordering::Less => distance, 55 | core::cmp::Ordering::Equal | core::cmp::Ordering::Greater => { 56 | min_distance 57 | } 58 | }, 59 | intersection_count, 60 | ), 61 | } 62 | }, 63 | ) 64 | }) 65 | .map(|(distance, intersection_count)| { 66 | if intersection_count % 2 == 0 { 67 | distance 68 | } else { 69 | // can only be odd if in raycast mode 70 | -distance 71 | } 72 | }) 73 | .collect() 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | 80 | use itertools::Itertools; 81 | 82 | #[test] 83 | fn test_generate() { 84 | let model = &easy_gltf::load("assets/suzanne.glb").unwrap()[0].models[0]; 85 | let vertices = model.vertices().iter().map(|v| v.position).collect_vec(); 86 | let indices = model.indices().unwrap(); 87 | let query_points = [ 88 | cgmath::Vector3::new(0.0, 0.0, 0.0), 89 | cgmath::Vector3::new(1.0, 1.0, 1.0), 90 | cgmath::Vector3::new(0.1, 0.2, 0.2), 91 | ]; 92 | let sdf = generate_sdf_default( 93 | &vertices, 94 | crate::Topology::TriangleList(Some(indices)), 95 | &query_points, 96 | SignMethod::Normal, 97 | ); 98 | 99 | // pysdf [0.45216727 -0.6997909 0.45411023] # negative is outside in pysdf 100 | // mesh_to_sdf [-0.40961263 0.6929414 -0.46345082] # negative is inside in mesh_to_sdf 101 | let baseline = [-0.42, 0.69, -0.46]; 102 | 103 | // make sure the results are close enough. 104 | // the results are not exactly the same because the algorithm is not the same and baselines might not be exact. 105 | // this is mostly to make sure the results are not completely off. 106 | for (sdf, baseline) in sdf.iter().zip(baseline.iter()) { 107 | assert!((sdf - baseline).abs() < 0.1); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/generate/generic/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module containing the different `generate_sdf` functions, one for each acceleration structure. 2 | 3 | pub mod bvh; 4 | pub mod default; 5 | pub mod rtree; 6 | pub mod rtree_bvh; 7 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/generate/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module for generating SDFs from meshes. 2 | 3 | pub mod generic; 4 | pub mod grid; 5 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/point.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | 4 | mod impl_array; 5 | 6 | #[cfg(feature = "cgmath")] 7 | mod impl_cgmath; 8 | #[cfg(feature = "glam")] 9 | mod impl_glam; 10 | #[cfg(feature = "mint")] 11 | mod impl_mint; 12 | //#[cfg(feature = "nalgebra")] 13 | mod impl_nalgebra; 14 | 15 | /// Point is the trait that represents a point in 3D space. 16 | /// It is an abstraction over the type of point used in the client math library. 17 | /// 18 | /// While everything could be done with new/x/y/z only, 19 | /// the other methods are provided for optimization purposes, 20 | /// relying on the client math library to provide optimized implementations when possible. 21 | pub trait Point: Sized + Copy + Sync + Send + core::fmt::Debug + PartialEq { 22 | /// If the `serde` feature is enabled, the Point should be serializable and deserializable. 23 | /// You should set Serde to Self: 24 | /// ```ignore 25 | /// use mesh_to_sdf::Point; 26 | /// #[derive(Serialize, Deserialize)] 27 | /// struct MyPoint { 28 | /// x: f32, 29 | /// y: f32, 30 | /// z: f32, 31 | /// } 32 | /// 33 | /// impl Point for MyPoint { 34 | /// #[cfg(feature = "serde")] 35 | /// type Serde = Self; 36 | /// 37 | /// // ... 38 | /// } 39 | /// ``` 40 | #[cfg(feature = "serde")] 41 | type Serde: Serialize + DeserializeOwned; 42 | 43 | /// Create a new point. 44 | #[must_use] 45 | fn new(x: f32, y: f32, z: f32) -> Self; 46 | 47 | /// Get the x coordinate. 48 | #[must_use] 49 | fn x(&self) -> f32; 50 | /// Get the y coordinate. 51 | #[must_use] 52 | fn y(&self) -> f32; 53 | /// Get the z coordinate. 54 | #[must_use] 55 | fn z(&self) -> f32; 56 | 57 | /// Get the x coordinate mutably. 58 | fn x_mut(&mut self) -> &mut f32; 59 | /// Get the y coordinate. 60 | fn y_mut(&mut self) -> &mut f32; 61 | /// Get the z coordinate. 62 | fn z_mut(&mut self) -> &mut f32; 63 | 64 | /// Get the coordinate at index `i`. 65 | #[must_use] 66 | fn get(&self, i: usize) -> f32 { 67 | match i { 68 | 0 => self.x(), 69 | 1 => self.y(), 70 | 2 => self.z(), 71 | _ => panic!("Index out of bounds"), 72 | } 73 | } 74 | 75 | // Past this point, all methods are optional. 76 | // You are encouraged to implement them if your math library provides equivalent methods for optimization purposes, 77 | // but a default implementation is provided that uses new/x/y/z as a fallback. 78 | 79 | /// Add two points. 80 | #[must_use] 81 | fn add(&self, other: &Self) -> Self { 82 | Self::new( 83 | self.x() + other.x(), 84 | self.y() + other.y(), 85 | self.z() + other.z(), 86 | ) 87 | } 88 | /// Subtract two points. 89 | #[must_use] 90 | fn sub(&self, other: &Self) -> Self { 91 | Self::new( 92 | self.x() - other.x(), 93 | self.y() - other.y(), 94 | self.z() - other.z(), 95 | ) 96 | } 97 | /// Dot product of two points. 98 | #[must_use] 99 | fn dot(&self, other: &Self) -> f32 { 100 | self.x() * other.x() + self.y() * other.y() + self.z() * other.z() 101 | } 102 | /// Cross product of two points. 103 | #[must_use] 104 | fn cross(&self, other: &Self) -> Self { 105 | Self::new( 106 | self.y() * other.z() - self.z() * other.y(), 107 | self.z() * other.x() - self.x() * other.z(), 108 | self.x() * other.y() - self.y() * other.x(), 109 | ) 110 | } 111 | /// Length of the point. 112 | #[must_use] 113 | fn length(&self) -> f32 { 114 | self.dot(self).sqrt() 115 | } 116 | /// Distance between two points. 117 | #[must_use] 118 | fn dist(&self, other: &Self) -> f32 { 119 | self.sub(other).length() 120 | } 121 | /// Distance squared between two points. 122 | #[must_use] 123 | fn dist2(&self, other: &Self) -> f32 { 124 | let diff = self.sub(other); 125 | diff.dot(&diff) 126 | } 127 | 128 | /// Multiply a point by a scalar. 129 | #[must_use] 130 | fn fmul(&self, other: f32) -> Self { 131 | Self::new(self.x() * other, self.y() * other, self.z() * other) 132 | } 133 | /// Divide two points by components. 134 | #[must_use] 135 | fn comp_div(&self, other: &Self) -> Self { 136 | Self::new( 137 | self.x() / other.x(), 138 | self.y() / other.y(), 139 | self.z() / other.z(), 140 | ) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/point/impl_array.rs: -------------------------------------------------------------------------------- 1 | use super::Point; 2 | 3 | impl Point for [f32; 3] { 4 | #[cfg(feature = "serde")] 5 | type Serde = Self; 6 | 7 | fn new(x: f32, y: f32, z: f32) -> Self { 8 | [x, y, z] 9 | } 10 | 11 | fn x(&self) -> f32 { 12 | self[0] 13 | } 14 | 15 | fn y(&self) -> f32 { 16 | self[1] 17 | } 18 | 19 | fn z(&self) -> f32 { 20 | self[2] 21 | } 22 | 23 | fn x_mut(&mut self) -> &mut f32 { 24 | &mut self[0] 25 | } 26 | 27 | fn y_mut(&mut self) -> &mut f32 { 28 | &mut self[1] 29 | } 30 | 31 | fn z_mut(&mut self) -> &mut f32 { 32 | &mut self[2] 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | 40 | #[test] 41 | fn test_point_aray() { 42 | let p1 = [1.0, 2.0, 3.0]; 43 | let p2 = [4.0, 5.0, 6.0]; 44 | 45 | let p3: [f32; 3] = Point::new(p1.x(), p1.y(), p1.z()); 46 | assert_eq!(p3.x(), 1.0); 47 | assert_eq!(p3.y(), 2.0); 48 | assert_eq!(p3.z(), 3.0); 49 | 50 | assert_eq!(p1.add(&p2), [5.0, 7.0, 9.0]); 51 | assert_eq!(p1.sub(&p2), [-3.0, -3.0, -3.0]); 52 | assert_eq!(p1.dot(&p2), 32.0); 53 | assert_eq!(p1.length(), 3.7416575); 54 | assert_eq!(p1.dist(&p2), 5.196152); 55 | assert_eq!(p1.fmul(2.0), [2.0, 4.0, 6.0]); 56 | assert_eq!(p1.comp_div(&p2), [0.25, 0.4, 0.5]); 57 | 58 | let mut p = p1; 59 | *p.x_mut() = 10.0; 60 | *p.y_mut() = 20.0; 61 | *p.z_mut() = 30.0; 62 | assert_eq!(p, [10.0, 20.0, 30.0]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/point/impl_cgmath.rs: -------------------------------------------------------------------------------- 1 | use cgmath::{ElementWise, InnerSpace, MetricSpace}; 2 | 3 | use super::Point; 4 | 5 | impl Point for cgmath::Vector3 { 6 | #[cfg(feature = "serde")] 7 | type Serde = Self; 8 | 9 | /// Create a new point. 10 | fn new(x: f32, y: f32, z: f32) -> Self { 11 | cgmath::Vector3::new(x, y, z) 12 | } 13 | 14 | /// Get the x coordinate. 15 | fn x(&self) -> f32 { 16 | self.x 17 | } 18 | 19 | /// Get the y coordinate. 20 | fn y(&self) -> f32 { 21 | self.y 22 | } 23 | 24 | /// Get the z coordinate. 25 | fn z(&self) -> f32 { 26 | self.z 27 | } 28 | 29 | fn x_mut(&mut self) -> &mut f32 { 30 | &mut self.x 31 | } 32 | 33 | fn y_mut(&mut self) -> &mut f32 { 34 | &mut self.y 35 | } 36 | 37 | fn z_mut(&mut self) -> &mut f32 { 38 | &mut self.z 39 | } 40 | 41 | /// Add two points. 42 | fn add(&self, other: &Self) -> Self { 43 | *self + *other 44 | } 45 | /// Subtract two points. 46 | fn sub(&self, other: &Self) -> Self { 47 | *self - *other 48 | } 49 | /// Dot product of two points. 50 | fn dot(&self, other: &Self) -> f32 { 51 | cgmath::InnerSpace::dot(*self, *other) 52 | } 53 | /// Cross product of two points. 54 | fn cross(&self, other: &Self) -> Self { 55 | cgmath::Vector3::cross(*self, *other) 56 | } 57 | /// Length of the point. 58 | fn length(&self) -> f32 { 59 | self.magnitude() 60 | } 61 | /// Distance between two points. 62 | fn dist(&self, other: &Self) -> f32 { 63 | self.distance(*other) 64 | } 65 | /// Multiply a point by a scalar. 66 | fn fmul(&self, other: f32) -> Self { 67 | *self * other 68 | } 69 | /// Divide two points by components. 70 | fn comp_div(&self, other: &Self) -> Self { 71 | self.div_element_wise(*other) 72 | } 73 | } 74 | 75 | #[cfg(test)] 76 | mod tests { 77 | use super::*; 78 | use proptest::prelude::*; 79 | 80 | proptest! { 81 | #[test] 82 | fn test_point_cgmath_vector3( 83 | p1 in prop::array::uniform3(-100.0f32..100.0), 84 | p2 in prop::array::uniform3(-100.0f32..100.0), 85 | 86 | ) { 87 | let cmp = |a: cgmath::Vector3, b: [f32;3]| { 88 | if a.x.is_nan() && b[0].is_nan() { 89 | return true; 90 | } 91 | if a.y.is_nan() && b[1].is_nan() { 92 | return true; 93 | } 94 | if a.z.is_nan() && b[2].is_nan() { 95 | return true; 96 | } 97 | float_cmp::approx_eq!(f32, a.x, b[0]) && float_cmp::approx_eq!(f32, a.y, b[1]) && float_cmp::approx_eq!(f32, a.z, b[2]) 98 | }; 99 | 100 | let ap1 = p1; 101 | let ap2 = p2; 102 | 103 | let p1 = cgmath::Vector3::new(p1[0], p1[1], p1[2]); 104 | let p2 = cgmath::Vector3::new(p2[0], p2[1], p2[2]); 105 | 106 | let p3: cgmath::Vector3 = Point::new(p1.x(), p1.y(), p1.z()); 107 | assert_eq!(p3.x(), ap1[0]); 108 | assert_eq!(p3.y(), ap1[1]); 109 | assert_eq!(p3.z(), ap1[2]); 110 | 111 | assert!(cmp(Point::add(&p1, &p2), ap1.add(&ap2))); 112 | assert!(cmp(Point::sub(&p1, &p2), ap1.sub(&ap2))); 113 | assert!(Point::dot(&p1, &p2) == ap1.dot(&ap2)); 114 | assert!(cmp(Point::cross(&p1, &p2), ap1.cross(&ap2))); 115 | assert!(Point::length(&p1) == ap1.length()); 116 | assert!(Point::dist(&p1, &p2) == ap1.dist(&ap2)); 117 | assert!(cmp(Point::fmul(&p1, 2.0), ap1.fmul(2.0))); 118 | if ap2[0] != 0.0 && ap2[1] != 0.0 && ap2[2] != 0.0 { 119 | assert!(cmp(Point::comp_div(&p1, &p2), ap1.comp_div(&ap2))); 120 | } 121 | 122 | let mut p = p1; 123 | *p.x_mut() = p1.x() + 10.0; 124 | *p.y_mut() = p2.y() + 20.0; 125 | *p.z_mut() = p3.z() + 30.0; 126 | assert_eq!(p, Point::new(p1.x() + 10.0, p2.y() + 20.0, p3.z() + 30.0)); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/point/impl_glam.rs: -------------------------------------------------------------------------------- 1 | use super::Point; 2 | 3 | impl Point for glam::Vec3 { 4 | #[cfg(feature = "serde")] 5 | type Serde = Self; 6 | 7 | /// Create a new point. 8 | fn new(x: f32, y: f32, z: f32) -> Self { 9 | Self::new(x, y, z) 10 | } 11 | 12 | /// Get the x coordinate. 13 | fn x(&self) -> f32 { 14 | self.x 15 | } 16 | 17 | /// Get the y coordinate. 18 | fn y(&self) -> f32 { 19 | self.y 20 | } 21 | 22 | /// Get the z coordinate. 23 | fn z(&self) -> f32 { 24 | self.z 25 | } 26 | 27 | fn x_mut(&mut self) -> &mut f32 { 28 | &mut self.x 29 | } 30 | 31 | fn y_mut(&mut self) -> &mut f32 { 32 | &mut self.y 33 | } 34 | 35 | fn z_mut(&mut self) -> &mut f32 { 36 | &mut self.z 37 | } 38 | 39 | /// Add two points. 40 | fn add(&self, other: &Self) -> Self { 41 | *self + *other 42 | } 43 | /// Subtract two points. 44 | fn sub(&self, other: &Self) -> Self { 45 | *self - *other 46 | } 47 | /// Dot product of two points. 48 | fn dot(&self, other: &Self) -> f32 { 49 | Self::dot(*self, *other) 50 | } 51 | /// Cross product of two points. 52 | fn cross(&self, other: &Self) -> Self { 53 | Self::cross(*self, *other) 54 | } 55 | /// Length of the point. 56 | fn length(&self) -> f32 { 57 | Self::length(*self) 58 | } 59 | /// Distance between two points. 60 | fn dist(&self, other: &Self) -> f32 { 61 | Self::distance(*self, *other) 62 | } 63 | /// Multiply a point by a scalar. 64 | fn fmul(&self, other: f32) -> Self { 65 | *self * other 66 | } 67 | /// Divide two points by components. 68 | fn comp_div(&self, other: &Self) -> Self { 69 | *self / *other 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | use proptest::prelude::*; 77 | 78 | proptest! { 79 | #[test] 80 | fn test_point_glam_vec3( 81 | p1 in prop::array::uniform3(-100.0f32..100.0), 82 | p2 in prop::array::uniform3(-100.0f32..100.0), 83 | 84 | ) { 85 | let cmp = |a: glam::Vec3, b: [f32;3]| { 86 | if a.x.is_nan() && b[0].is_nan() { 87 | return true; 88 | } 89 | if a.y.is_nan() && b[1].is_nan() { 90 | return true; 91 | } 92 | if a.z.is_nan() && b[2].is_nan() { 93 | return true; 94 | } 95 | float_cmp::approx_eq!(f32, a.x, b[0]) && float_cmp::approx_eq!(f32, a.y, b[1]) && float_cmp::approx_eq!(f32, a.z, b[2]) 96 | }; 97 | 98 | let ap1 = p1; 99 | let ap2 = p2; 100 | 101 | let p1 = glam::Vec3::new(p1[0], p1[1], p1[2]); 102 | let p2 = glam::Vec3::new(p2[0], p2[1], p2[2]); 103 | 104 | let p3: glam::Vec3 = Point::new(p1.x(), p1.y(), p1.z()); 105 | assert_eq!(p3.x(), ap1[0]); 106 | assert_eq!(p3.y(), ap1[1]); 107 | assert_eq!(p3.z(), ap1[2]); 108 | 109 | assert!(cmp(Point::add(&p1, &p2), ap1.add(&ap2))); 110 | assert!(cmp(Point::sub(&p1, &p2), ap1.sub(&ap2))); 111 | assert!(Point::dot(&p1, &p2) == ap1.dot(&ap2)); 112 | assert!(cmp(Point::cross(&p1, &p2), ap1.cross(&ap2))); 113 | assert!(Point::length(&p1) == ap1.length()); 114 | assert!(Point::dist(&p1, &p2) == ap1.dist(&ap2)); 115 | assert!(cmp(Point::fmul(&p1, 2.0), ap1.fmul(2.0))); 116 | if ap2[0] != 0.0 && ap2[1] != 0.0 && ap2[2] != 0.0 { 117 | assert!(cmp(Point::comp_div(&p1, &p2), ap1.comp_div(&ap2))); 118 | } 119 | 120 | let mut p = p1; 121 | *p.x_mut() = p1.x() + 10.0; 122 | *p.y_mut() = p2.y() + 20.0; 123 | *p.z_mut() = p3.z() + 30.0; 124 | assert_eq!(p, Point::new(p1.x() + 10.0, p2.y() + 20.0, p3.z() + 30.0)); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/point/impl_mint.rs: -------------------------------------------------------------------------------- 1 | use super::Point; 2 | 3 | impl Point for mint::Point3 { 4 | #[cfg(feature = "serde")] 5 | type Serde = Self; 6 | 7 | /// Create a new point. 8 | fn new(x: f32, y: f32, z: f32) -> Self { 9 | mint::Point3 { x, y, z } 10 | } 11 | 12 | /// Get the x coordinate. 13 | fn x(&self) -> f32 { 14 | self.x 15 | } 16 | 17 | /// Get the y coordinate. 18 | fn y(&self) -> f32 { 19 | self.y 20 | } 21 | 22 | /// Get the z coordinate. 23 | fn z(&self) -> f32 { 24 | self.z 25 | } 26 | 27 | fn x_mut(&mut self) -> &mut f32 { 28 | &mut self.x 29 | } 30 | 31 | fn y_mut(&mut self) -> &mut f32 { 32 | &mut self.y 33 | } 34 | 35 | fn z_mut(&mut self) -> &mut f32 { 36 | &mut self.z 37 | } 38 | } 39 | 40 | impl Point for mint::Vector3 { 41 | #[cfg(feature = "serde")] 42 | type Serde = Self; 43 | 44 | /// Create a new point. 45 | fn new(x: f32, y: f32, z: f32) -> Self { 46 | mint::Vector3 { x, y, z } 47 | } 48 | 49 | /// Get the x coordinate. 50 | fn x(&self) -> f32 { 51 | self.x 52 | } 53 | 54 | /// Get the y coordinate. 55 | fn y(&self) -> f32 { 56 | self.y 57 | } 58 | 59 | /// Get the z coordinate. 60 | fn z(&self) -> f32 { 61 | self.z 62 | } 63 | 64 | fn x_mut(&mut self) -> &mut f32 { 65 | &mut self.x 66 | } 67 | 68 | fn y_mut(&mut self) -> &mut f32 { 69 | &mut self.y 70 | } 71 | 72 | fn z_mut(&mut self) -> &mut f32 { 73 | &mut self.z 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::*; 80 | use proptest::prelude::*; 81 | 82 | proptest! { 83 | #[test] 84 | fn test_point_mint_point3( 85 | p1 in prop::array::uniform3(-100.0f32..100.0), 86 | p2 in prop::array::uniform3(-100.0f32..100.0), 87 | 88 | ) { 89 | let cmp = |a: mint::Point3, b: [f32;3]| { 90 | if a.x.is_nan() && b[0].is_nan() { 91 | return true; 92 | } 93 | if a.y.is_nan() && b[1].is_nan() { 94 | return true; 95 | } 96 | if a.z.is_nan() && b[2].is_nan() { 97 | return true; 98 | } 99 | float_cmp::approx_eq!(f32, a.x, b[0]) && float_cmp::approx_eq!(f32, a.y, b[1]) && float_cmp::approx_eq!(f32, a.z, b[2]) 100 | }; 101 | 102 | let ap1 = p1; 103 | let ap2 = p2; 104 | 105 | let p1 = mint::Point3::new(p1[0], p1[1], p1[2]); 106 | let p2 = mint::Point3::new(p2[0], p2[1], p2[2]); 107 | 108 | let p3: mint::Point3 = Point::new(p1.x(), p1.y(), p1.z()); 109 | assert_eq!(p3.x(), ap1[0]); 110 | assert_eq!(p3.y(), ap1[1]); 111 | assert_eq!(p3.z(), ap1[2]); 112 | 113 | assert!(cmp(Point::add(&p1, &p2), ap1.add(&ap2))); 114 | assert!(cmp(Point::sub(&p1, &p2), ap1.sub(&ap2))); 115 | assert!(Point::dot(&p1, &p2) == ap1.dot(&ap2)); 116 | assert!(cmp(Point::cross(&p1, &p2), ap1.cross(&ap2))); 117 | assert!(Point::length(&p1) == ap1.length()); 118 | assert!(Point::dist(&p1, &p2) == ap1.dist(&ap2)); 119 | assert!(cmp(Point::fmul(&p1, 2.0), ap1.fmul(2.0))); 120 | if ap2[0] != 0.0 && ap2[1] != 0.0 && ap2[2] != 0.0 { 121 | assert!(cmp(Point::comp_div(&p1, &p2), ap1.comp_div(&ap2))); 122 | } 123 | 124 | let mut p = p1; 125 | *p.x_mut() = p1.x() + 10.0; 126 | *p.y_mut() = p2.y() + 20.0; 127 | *p.z_mut() = p3.z() + 30.0; 128 | assert_eq!(p, Point::new(p1.x() + 10.0, p2.y() + 20.0, p3.z() + 30.0)); 129 | } 130 | } 131 | 132 | proptest! { 133 | #[test] 134 | fn test_point_mint_vector3( 135 | p1 in prop::array::uniform3(-100.0f32..100.0), 136 | p2 in prop::array::uniform3(-100.0f32..100.0), 137 | 138 | ) { 139 | let cmp = |a: mint::Vector3, b: [f32;3]| { 140 | if a.x.is_nan() && b[0].is_nan() { 141 | return true; 142 | } 143 | if a.y.is_nan() && b[1].is_nan() { 144 | return true; 145 | } 146 | if a.z.is_nan() && b[2].is_nan() { 147 | return true; 148 | } 149 | float_cmp::approx_eq!(f32, a.x, b[0]) && float_cmp::approx_eq!(f32, a.y, b[1]) && float_cmp::approx_eq!(f32, a.z, b[2]) 150 | }; 151 | 152 | let ap1 = p1; 153 | let ap2 = p2; 154 | 155 | let p1 = mint::Vector3::new(p1[0], p1[1], p1[2]); 156 | let p2 = mint::Vector3::new(p2[0], p2[1], p2[2]); 157 | 158 | let p3: mint::Vector3 = Point::new(p1.x(), p1.y(), p1.z()); 159 | assert_eq!(p3.x(), ap1[0]); 160 | assert_eq!(p3.y(), ap1[1]); 161 | assert_eq!(p3.z(), ap1[2]); 162 | 163 | assert!(cmp(Point::add(&p1, &p2), ap1.add(&ap2))); 164 | assert!(cmp(Point::sub(&p1, &p2), ap1.sub(&ap2))); 165 | assert!(Point::dot(&p1, &p2) == ap1.dot(&ap2)); 166 | assert!(cmp(Point::cross(&p1, &p2), ap1.cross(&ap2))); 167 | assert!(Point::length(&p1) == ap1.length()); 168 | assert!(Point::dist(&p1, &p2) == ap1.dist(&ap2)); 169 | assert!(cmp(Point::fmul(&p1, 2.0), ap1.fmul(2.0))); 170 | if ap2[0] != 0.0 && ap2[1] != 0.0 && ap2[2] != 0.0 { 171 | assert!(cmp(Point::comp_div(&p1, &p2), ap1.comp_div(&ap2))); 172 | } 173 | 174 | let mut p = p1; 175 | *p.x_mut() = p1.x() + 10.0; 176 | *p.y_mut() = p2.y() + 20.0; 177 | *p.z_mut() = p3.z() + 30.0; 178 | assert_eq!(p, Point::new(p1.x() + 10.0, p2.y() + 20.0, p3.z() + 30.0)); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /mesh_to_sdf/src/point/impl_nalgebra.rs: -------------------------------------------------------------------------------- 1 | use super::Point; 2 | 3 | impl Point for nalgebra::Point3 { 4 | #[cfg(feature = "serde")] 5 | type Serde = Self; 6 | 7 | /// Create a new point. 8 | fn new(x: f32, y: f32, z: f32) -> Self { 9 | Self::new(x, y, z) 10 | } 11 | 12 | /// Get the x coordinate. 13 | fn x(&self) -> f32 { 14 | self.x 15 | } 16 | 17 | /// Get the y coordinate. 18 | fn y(&self) -> f32 { 19 | self.y 20 | } 21 | 22 | /// Get the z coordinate. 23 | fn z(&self) -> f32 { 24 | self.z 25 | } 26 | 27 | fn x_mut(&mut self) -> &mut f32 { 28 | &mut self.x 29 | } 30 | 31 | fn y_mut(&mut self) -> &mut f32 { 32 | &mut self.y 33 | } 34 | 35 | fn z_mut(&mut self) -> &mut f32 { 36 | &mut self.z 37 | } 38 | } 39 | 40 | impl Point for nalgebra::Vector3 { 41 | #[cfg(feature = "serde")] 42 | type Serde = Self; 43 | 44 | /// Create a new point. 45 | fn new(x: f32, y: f32, z: f32) -> Self { 46 | Self::new(x, y, z) 47 | } 48 | 49 | /// Get the x coordinate. 50 | fn x(&self) -> f32 { 51 | self.x 52 | } 53 | 54 | /// Get the y coordinate. 55 | fn y(&self) -> f32 { 56 | self.y 57 | } 58 | 59 | /// Get the z coordinate. 60 | fn z(&self) -> f32 { 61 | self.z 62 | } 63 | 64 | fn x_mut(&mut self) -> &mut f32 { 65 | &mut self.x 66 | } 67 | 68 | fn y_mut(&mut self) -> &mut f32 { 69 | &mut self.y 70 | } 71 | 72 | fn z_mut(&mut self) -> &mut f32 { 73 | &mut self.z 74 | } 75 | 76 | /// Add two points. 77 | fn add(&self, other: &Self) -> Self { 78 | self + other 79 | } 80 | /// Subtract two points. 81 | fn sub(&self, other: &Self) -> Self { 82 | self - other 83 | } 84 | /// Dot product of two points. 85 | fn dot(&self, other: &Self) -> f32 { 86 | self.dot(other) 87 | } 88 | /// Cross product of two points. 89 | fn cross(&self, other: &Self) -> Self { 90 | self.cross(other) 91 | } 92 | /// Length of the point. 93 | fn length(&self) -> f32 { 94 | self.norm() 95 | } 96 | /// Distance between two points. 97 | fn dist(&self, other: &Self) -> f32 { 98 | (self - other).norm() 99 | } 100 | /// Multiply a point by a scalar. 101 | fn fmul(&self, other: f32) -> Self { 102 | self * other 103 | } 104 | /// Divide two points by components. 105 | fn comp_div(&self, other: &Self) -> Self { 106 | self.component_div(other) 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | use proptest::prelude::*; 114 | 115 | proptest! { 116 | #[test] 117 | fn test_point_nalgebra_point3( 118 | p1 in prop::array::uniform3(-100.0f32..100.0), 119 | p2 in prop::array::uniform3(-100.0f32..100.0), 120 | 121 | ) { 122 | let cmp = |a: nalgebra::Point3, b: [f32;3]| { 123 | if a.x.is_nan() && b[0].is_nan() { 124 | return true; 125 | } 126 | if a.y.is_nan() && b[1].is_nan() { 127 | return true; 128 | } 129 | if a.z.is_nan() && b[2].is_nan() { 130 | return true; 131 | } 132 | float_cmp::approx_eq!(f32, a.x, b[0]) && float_cmp::approx_eq!(f32, a.y, b[1]) && float_cmp::approx_eq!(f32, a.z, b[2]) 133 | }; 134 | 135 | let ap1 = p1; 136 | let ap2 = p2; 137 | 138 | let p1 = nalgebra::Point3::new(p1[0], p1[1], p1[2]); 139 | let p2 = nalgebra::Point3::new(p2[0], p2[1], p2[2]); 140 | 141 | let p3: nalgebra::Point3 = Point::new(p1.x(), p1.y(), p1.z()); 142 | assert_eq!(p3.x(), ap1[0]); 143 | assert_eq!(p3.y(), ap1[1]); 144 | assert_eq!(p3.z(), ap1[2]); 145 | 146 | assert!(cmp(Point::add(&p1, &p2), ap1.add(&ap2))); 147 | assert!(cmp(Point::sub(&p1, &p2), ap1.sub(&ap2))); 148 | assert!(Point::dot(&p1, &p2) == ap1.dot(&ap2)); 149 | assert!(cmp(Point::cross(&p1, &p2), ap1.cross(&ap2))); 150 | assert!(Point::length(&p1) == ap1.length()); 151 | assert!(Point::dist(&p1, &p2) == ap1.dist(&ap2)); 152 | assert!(cmp(Point::fmul(&p1, 2.0), ap1.fmul(2.0))); 153 | if ap2[0] != 0.0 && ap2[1] != 0.0 && ap2[2] != 0.0 { 154 | assert!(cmp(Point::comp_div(&p1, &p2), ap1.comp_div(&ap2))); 155 | } 156 | 157 | let mut p = p1; 158 | *p.x_mut() = p1.x() + 10.0; 159 | *p.y_mut() = p2.y() + 20.0; 160 | *p.z_mut() = p3.z() + 30.0; 161 | assert_eq!(p, Point::new(p1.x() + 10.0, p2.y() + 20.0, p3.z() + 30.0)); 162 | } 163 | } 164 | 165 | proptest! { 166 | #[test] 167 | fn test_point_nalgebra_vector3( 168 | p1 in prop::array::uniform3(-100.0f32..100.0), 169 | p2 in prop::array::uniform3(-100.0f32..100.0), 170 | 171 | ) { 172 | let cmp = |a: nalgebra::Vector3, b: [f32;3]| { 173 | if a.x.is_nan() && b[0].is_nan() { 174 | return true; 175 | } 176 | if a.y.is_nan() && b[1].is_nan() { 177 | return true; 178 | } 179 | if a.z.is_nan() && b[2].is_nan() { 180 | return true; 181 | } 182 | float_cmp::approx_eq!(f32, a.x, b[0]) && float_cmp::approx_eq!(f32, a.y, b[1]) && float_cmp::approx_eq!(f32, a.z, b[2]) 183 | }; 184 | 185 | let ap1 = p1; 186 | let ap2 = p2; 187 | 188 | let p1 = nalgebra::Vector3::new(p1[0], p1[1], p1[2]); 189 | let p2 = nalgebra::Vector3::new(p2[0], p2[1], p2[2]); 190 | 191 | let p3: nalgebra::Vector3 = Point::new(p1.x(), p1.y(), p1.z()); 192 | assert_eq!(p3.x(), ap1[0]); 193 | assert_eq!(p3.y(), ap1[1]); 194 | assert_eq!(p3.z(), ap1[2]); 195 | 196 | assert!(cmp(Point::add(&p1, &p2), ap1.add(&ap2))); 197 | assert!(cmp(Point::sub(&p1, &p2), ap1.sub(&ap2))); 198 | assert!(Point::dot(&p1, &p2) == ap1.dot(&ap2)); 199 | assert!(cmp(Point::cross(&p1, &p2), ap1.cross(&ap2))); 200 | assert!(Point::length(&p1) == ap1.length()); 201 | assert!(Point::dist(&p1, &p2) == ap1.dist(&ap2)); 202 | assert!(cmp(Point::fmul(&p1, 2.0), ap1.fmul(2.0))); 203 | if ap2[0] != 0.0 && ap2[1] != 0.0 && ap2[2] != 0.0 { 204 | assert!(cmp(Point::comp_div(&p1, &p2), ap1.comp_div(&ap2))); 205 | } 206 | 207 | let mut p = p1; 208 | *p.x_mut() = p1.x() + 10.0; 209 | *p.y_mut() = p2.y() + 20.0; 210 | *p.z_mut() = p3.z() + 30.0; 211 | assert_eq!(p, Point::new(p1.x() + 10.0, p2.y() + 20.0, p3.z() + 30.0)); 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /mesh_to_sdf/tests/generate_python_baseline.py: -------------------------------------------------------------------------------- 1 | """This file is used to generate the "ground truth" SDFs for the crate tests.""" 2 | 3 | 4 | # Load some mesh (don't necessarily need trimesh) 5 | import trimesh 6 | mesh = trimesh.load("assets/suzanne.glb").geometry['Suzanne'] 7 | 8 | query_points = [[0, 0, 0], [1 ,1 ,1 ], [0.1, 0.2, 0.2]] 9 | 10 | # pysdf 11 | from pysdf import SDF 12 | sdf = SDF(mesh.vertices, mesh.faces); 13 | sdf_multi_point = sdf(query_points) 14 | print("pysdf", sdf_multi_point) 15 | #pysdf [0.45216727 -0.6997909 0.45411023] # negative is outside in pysdf 16 | 17 | 18 | import mesh_to_sdf 19 | import numpy as np 20 | query_points = np.array(query_points) 21 | sdf = mesh_to_sdf.mesh_to_sdf(mesh, query_points, surface_point_method='scan', sign_method='normal', bounding_radius=None, scan_count=100, scan_resolution=400, sample_point_count=10000000, normal_sample_count=11) 22 | print("mesh_to_sdf", sdf) 23 | # mesh_to_sdf [-0.40961263 0.6929414 -0.46345082] # negative is inside in mesh_to_sdf 24 | 25 | -------------------------------------------------------------------------------- /mesh_to_sdf/tests/sdf_generic_v1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf/tests/sdf_generic_v1.bin -------------------------------------------------------------------------------- /mesh_to_sdf/tests/sdf_grid_v1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf/tests/sdf_grid_v1.bin -------------------------------------------------------------------------------- /mesh_to_sdf_client/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | ## Mesh to SDF Client 2 | 3 | The program will hot reload shaders in debug native mode. In release mode, they are bundled in the executable. 4 | While the project started with[rust_wgpu_hot_reload](https://github.com/Azkellas/rust_wgpu_hot_reload/) as template, the rust hot reload code has been removed to keep it in a single package. 5 | 6 | --- 7 | 8 | The project is a bit of a mess currently as it was exported from another. Here's a breakdown of the architecture until it gets cleaned up: 9 | - `main`: Watch `shaders` folder in debug mode and start `runner`` 10 | - `runner`: Initializes winit, egui, and the renderer, run the event loop and calls `sdf_program` when needed 11 | - `sdf_program`: Contains the logic the program. Draws ui, renders the sdf, etc. 12 | - `sdf`: Call `mesh_to_sdf` to generate the sdf and stores it in a `Sdf` struct. Contains the uniforms for the render pass 13 | - other files should be small and self explanatory -------------------------------------------------------------------------------- /mesh_to_sdf_client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0] - 2024-09-06 4 | 5 | Update `mesh_to_sdf` dependency to `0.4.0`. 6 | 7 | ## [0.3.0] - 2024-09-06 8 | 9 | ### Added 10 | 11 | - Voxel and Raymarch visualizations now map the material of the mesh onto the new geometry. 12 | 13 | ### Changed 14 | 15 | - Parallelize loading of the mesh. 16 | 17 | ## [0.2.1] - 2024-02-18 18 | 19 | Update `mesh_to_sdf` dependency to `0.2.1` since it had a breaking change. 20 | 21 | 22 | ## [0.2.0] - 2024-02-16 23 | 24 | ### Added 25 | 26 | - Isosurface control for point cloud, voxels and raymarching visualization. 27 | - Expose `SignMethod` enum in UI. 28 | 29 | ## [0.1.0] - 2024-02-05 30 | 31 | First release of `mesh_to_sdf_client`. 32 | 33 | ### Added 34 | 35 | - Model visualization with a Blinn-Phong shader. 36 | - SDF visualization with a point cloud. 37 | - SDF point size and color customization. 38 | - Model + SDF visualization. 39 | - Raymarching visualization (snap, trilinear and tetrahedral interpolations). 40 | - Voxels visualization. 41 | - Model and SDF metrics. 42 | - Light control. 43 | - Grid parametrization (cell count, size, and bounding box extension). 44 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mesh_to_sdf_client" 3 | authors = ["Etienne Desbois "] 4 | version = "0.4.1" 5 | edition = "2021" 6 | homepage = "https://github.com/Azkellas/mesh_to_sdf" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/Azkellas/mesh_to_sdf" 9 | description = "A mesh to SDF converter and renderer" 10 | keywords = ["mesh", "sdf", "distance_field", "raymarching", "voxels"] 11 | categories = [ 12 | "algorithms", 13 | "rendering::data-formats", 14 | "game-development", 15 | "graphics", 16 | "data-structures", 17 | ] 18 | 19 | exclude = ["assets/*", "ARCHITECTURE.md", "tests/*"] 20 | 21 | [dependencies] 22 | mesh_to_sdf = { path = "../mesh_to_sdf", version = "0.4.0", features = [ 23 | "glam", 24 | ] } 25 | egui = "0.28.1" 26 | egui-gizmo = "0.16.2" 27 | wgpu = { version = "0.20.1", default-features = true } 28 | winit_input_helper = "0.16.0" 29 | bytemuck = { version = "1.18.0", features = ["derive"] } 30 | web-time = "1.1.0" 31 | itertools = "0.13.0" 32 | anyhow = "1.0.89" 33 | winit = "0.29.15" 34 | glam = { version = "0.29.0", default-features = true, features = [ 35 | "bytemuck", 36 | "mint", 37 | ] } 38 | image = "0.25.2" 39 | pollster = "0.3.0" 40 | egui-wgpu = "0.28.1" 41 | egui-winit = "0.28.1" 42 | log = "0.4.22" 43 | env_logger = "0.11.5" 44 | cfg-if = "1.0.0" 45 | float-cmp = "0.9.0" 46 | rayon = "1.10.0" 47 | base64 = "0.22.1" 48 | gltf = { version = "1.4.1", default-features = true, features = [ 49 | "KHR_lights_punctual", 50 | "names", 51 | "extras", 52 | ] } 53 | hashbrown = "0.14.5" 54 | 55 | [target.'cfg(not(target_family = "wasm"))'.dependencies] 56 | # Enable shader hot reload for native compilation. 57 | rust-embed = "8.5.0" 58 | notify = "6.1.1" 59 | 60 | [target.'cfg(target_family = "wasm")'.dependencies] 61 | # Embed shaders in wasm even in debug mode. 62 | rust-embed = { version = "8.4.0", features = ["debug-embed"] } 63 | wasm-bindgen-futures = "0.4.43" 64 | web-sys = "0.3.70" 65 | console_error_panic_hook = "0.1.7" 66 | console_log = "1.0.0" 67 | wasm-bindgen = "0.2.93" 68 | 69 | [target.'cfg(target_env = "musl")'.dependencies] 70 | rfd = { version = "0.14.1", default-features = false, features = [ 71 | "async-std", 72 | "xdg-portal", 73 | "file-handle-inner", 74 | ] } 75 | 76 | [target.'cfg(not(target_env = "musl"))'.dependencies] 77 | rfd = { version = "0.14.1", default-features = true, features = [ 78 | "file-handle-inner", 79 | ] } 80 | 81 | [features] 82 | # wgpu::DownlevelFlags::VERTEX_STORAGE is not supported on WebGL 83 | webgl = ["wgpu/webgl"] 84 | webgpu = ["wgpu/wgsl"] 85 | default = ["webgpu"] 86 | 87 | [lints] 88 | workspace = true 89 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/README.md: -------------------------------------------------------------------------------- 1 | # mesh_to_sdf_client 2 | 3 | Client application for the `mesh_to_sdf` project. 4 | 5 | ![example](https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/main/client.gif) 6 | 7 | A visualization client for the SDF of a mesh. 8 | Generates a SDF from a mesh and renders it. 9 | 10 | License: MIT OR Apache-2.0 11 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/shaders/draw_cubemap.wgsl: -------------------------------------------------------------------------------- 1 | struct ModelUniforms { 2 | transform: mat4x4, 3 | } 4 | 5 | @group(0) @binding(0) var model: ModelUniforms; 6 | 7 | // Vertex shader 8 | struct CameraUniform { 9 | view_proj: mat4x4, 10 | view: mat4x4, 11 | proj: mat4x4, 12 | view_inv: mat4x4, 13 | proj_inv: mat4x4, 14 | eye: vec4, 15 | resolution: vec2, 16 | z_near: f32, 17 | _padding: f32, 18 | }; 19 | @group(1) @binding(0) var camera: CameraUniform; 20 | 21 | @group(2) @binding(0) var albedo_map: texture_2d; 22 | @group(2) @binding(1) var albedo_sampler: sampler; 23 | 24 | struct VertexInput { 25 | @location(0) position: vec3, 26 | @location(1) normal: vec3, 27 | @location(2) uv: vec2, 28 | } 29 | 30 | struct VertexOutput { 31 | @builtin(position) svposition: vec4, 32 | @location(0) position: vec4, 33 | @location(1) normal: vec4, 34 | @location(2) uv: vec2, 35 | }; 36 | 37 | @vertex 38 | fn main_vs(in: VertexInput) -> VertexOutput { 39 | var out: VertexOutput; 40 | 41 | out.position = model.transform * vec4(in.position, 1.0); 42 | out.svposition = camera.view_proj * out.position; 43 | out.normal = normalize(model.transform * vec4(in.normal, 0.0)); 44 | out.uv = in.uv; 45 | 46 | // this way we don't clear the depth buffer to 1.0. 47 | out.svposition.z = 1.0 - out.svposition.z; 48 | 49 | return out; 50 | } 51 | 52 | const EPSILON: f32 = 0.0001; 53 | @fragment 54 | fn main_fs(in: VertexOutput) -> @location(0) vec4 { 55 | var color_rgba = textureSample(albedo_map, albedo_sampler, in.uv); 56 | 57 | if color_rgba.a < 0.5 { 58 | discard; 59 | } 60 | var color = color_rgba.rgb; 61 | 62 | return vec4(color, 1.0); 63 | } 64 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/shaders/draw_model.wgsl: -------------------------------------------------------------------------------- 1 | struct ModelUniforms { 2 | transform: mat4x4, 3 | } 4 | 5 | @group(0) @binding(0) var model: ModelUniforms; 6 | 7 | // Vertex shader 8 | struct CameraUniform { 9 | view_proj: mat4x4, 10 | view: mat4x4, 11 | proj: mat4x4, 12 | view_inv: mat4x4, 13 | proj_inv: mat4x4, 14 | eye: vec4, 15 | resolution: vec2, 16 | z_near: f32, 17 | _padding: f32, 18 | }; 19 | @group(1) @binding(0) var camera: CameraUniform; 20 | 21 | @group(2) @binding(0) var shadow_camera: CameraUniform; 22 | @group(2) @binding(1) var shadow_map: texture_2d; 23 | @group(2) @binding(2) var shadow_sampler: sampler; 24 | 25 | 26 | @group(3) @binding(0) var albedo_map: texture_2d; 27 | @group(3) @binding(1) var albedo_sampler: sampler; 28 | 29 | struct VertexInput { 30 | @location(0) position: vec3, 31 | @location(1) normal: vec3, 32 | @location(2) uv: vec2, 33 | } 34 | 35 | struct VertexOutput { 36 | @builtin(position) svposition: vec4, 37 | @location(0) position: vec4, 38 | @location(1) normal: vec4, 39 | @location(2) uv: vec2, 40 | }; 41 | 42 | @vertex 43 | fn main_vs(in: VertexInput) -> VertexOutput { 44 | var out: VertexOutput; 45 | 46 | out.position = model.transform * vec4(in.position, 1.0); 47 | out.svposition = camera.view_proj * out.position; 48 | out.normal = normalize(model.transform * vec4(in.normal, 0.0)); 49 | out.uv = in.uv; 50 | 51 | return out; 52 | } 53 | 54 | const EPSILON: f32 = 0.0001; 55 | @fragment 56 | fn main_fs(in: VertexOutput) -> @location(0) vec4 { 57 | var color_rgba = textureSample(albedo_map, albedo_sampler, in.uv); 58 | 59 | if color_rgba.a < 0.5 { 60 | discard; 61 | } 62 | var color = color_rgba.rgb; 63 | 64 | let light = shadow_camera.eye.xyz; 65 | let light_dir = normalize(light - in.position.xyz); 66 | let ambiant = 0.2; 67 | let diffuse = max(0.0, dot(in.normal.xyz, light_dir)); 68 | 69 | var shadow_uv = shadow_camera.view_proj * vec4(in.position.xyz, 1.0); 70 | shadow_uv /= shadow_uv.w; 71 | shadow_uv.x = shadow_uv.x * 0.5 + 0.5; 72 | shadow_uv.y = shadow_uv.y * -0.5 + 0.5; 73 | var depth = shadow_uv.z; 74 | 75 | let threshold = depth * (1.05); 76 | 77 | var diffuse_strength = 1.0; 78 | 79 | let PCF: bool = true; 80 | 81 | if PCF { 82 | var light_depth = 0.0; 83 | let inv_res = vec2(1.0 / f32(camera.resolution.x), 1.0 / f32(camera.resolution.y)); 84 | for (var y = -1.; y <= 1.; y += 1.0) { 85 | for (var x = -1.; x <= 1.; x += 1.0) { 86 | let pdepth = textureSample(shadow_map, shadow_sampler, shadow_uv.xy + vec2(x, y) * inv_res).x; 87 | light_depth += f32(pdepth < threshold); 88 | } 89 | } 90 | light_depth /= 9.0; 91 | 92 | diffuse_strength *= light_depth; 93 | } else { 94 | let pdepth = textureSample(shadow_map, shadow_sampler, shadow_uv.xy).x; 95 | diffuse_strength *= f32(pdepth < threshold); 96 | } 97 | 98 | 99 | let view_dir = normalize(camera.eye.xyz - in.position.xyz); 100 | let half_dir = normalize(view_dir + light_dir); 101 | let specular = max(0.0, dot(in.normal.xyz, half_dir)); 102 | 103 | let brightness = ambiant + (diffuse + specular) * diffuse_strength; 104 | 105 | // arbitrary attenuation 106 | color.r *= exp(-1.8 * (1.0 - brightness)); 107 | color.g *= exp(-1.9 * (1.0 - brightness)); 108 | color.b *= exp(-1.9 * (1.0 - brightness)); 109 | 110 | return vec4(color, 1.0); 111 | } 112 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/shaders/draw_sdf.wgsl: -------------------------------------------------------------------------------- 1 | struct SdfUniforms { 2 | // acts as bounding box 3 | start: vec4, 4 | end: vec4, 5 | cell_size: vec4, 6 | cell_count: vec4, 7 | } 8 | @group(0) @binding(0) var uniforms : SdfUniforms; 9 | @group(0) @binding(1) var sdf : array; 10 | 11 | // Vertex shader 12 | struct CameraUniform { 13 | view_proj: mat4x4, 14 | view: mat4x4, 15 | proj: mat4x4, 16 | view_inv: mat4x4, 17 | proj_inv: mat4x4, 18 | eye: vec4, 19 | resolution: vec2, 20 | _padding: vec2, 21 | }; 22 | @group(1) @binding(0) var camera: CameraUniform; 23 | 24 | struct VisUniforms { 25 | positive_color: vec4, 26 | negative_color: vec4, 27 | surface_color: vec4, 28 | positive_power: f32, 29 | negative_power: f32, 30 | surface_iso: f32, 31 | surface_power: f32, 32 | surface_width: f32, 33 | point_size: f32, 34 | raymarch_mode: u32, 35 | }; 36 | @group(2) @binding(0) var vis_uniforms: VisUniforms; 37 | 38 | struct VertexInput { 39 | @builtin(vertex_index) vertex_index: u32, 40 | // @location(0) vertex_position: vec2, 41 | @builtin(instance_index) instance_index: u32, 42 | }; 43 | 44 | struct VertexOutput { 45 | @builtin(position) svposition: vec4, 46 | @location(0) color: vec3, 47 | @location(1) center_pos: vec2, 48 | @location(2) radius: f32, 49 | }; 50 | 51 | 52 | @vertex 53 | fn main_vs( 54 | in: VertexInput, 55 | ) -> VertexOutput { 56 | let cell_count = uniforms.cell_count.xyz; 57 | let cell_size = uniforms.cell_size.xyz; 58 | 59 | // compute cell world position 60 | // index was generated via for x in 0..cell_x { for y in 0..cell_y { for z in 0..cell_z { ... } } } 61 | // so index is x*cell_y*cell_z + y*cell_z + z 62 | var index = in.instance_index; 63 | let cell_z = index % cell_count.z; 64 | index /= cell_count.z; 65 | let cell_y = index % cell_count.y; 66 | index /= cell_count.y; 67 | let cell_x = index; 68 | let cell_idx = vec3(f32(cell_x), f32(cell_y), f32(cell_z)); 69 | let cell = uniforms.start.xyz + cell_idx * cell_size; 70 | 71 | // cell view position 72 | let viewpos = camera.view * vec4(cell, 1.0); 73 | 74 | // we want the color to be homogeneous in the sphere 75 | // so we compute it here directly. 76 | // we subtract the iso value to get the distance to the isosurface and not the surface directly. 77 | let distance = sdf[in.instance_index] - vis_uniforms.surface_iso; 78 | 79 | // affect a score for a point in the sphere 80 | var positive_strength = 0.; 81 | var negative_strength = 0.; 82 | var surface_strength = 0.; 83 | 84 | let cell_radius = min(cell_size.x, min(cell_size.y, cell_size.z)) * 0.5; 85 | 86 | // positive strength is how "positive" is the point 87 | if distance > vis_uniforms.surface_width { 88 | positive_strength = saturate(vis_uniforms.positive_power * distance / cell_radius); 89 | } 90 | // negative strength is how "negative" is the point 91 | // we boost it arbitrarily to make it more visible since inside points are more likely to be small 92 | // and less visible since behind the surface 93 | if distance < - vis_uniforms.surface_width { 94 | negative_strength = saturate(- vis_uniforms.negative_power * distance / cell_radius); 95 | } 96 | // surface strength is how close to the surface is the point 97 | if abs(distance) < vis_uniforms.surface_width { 98 | surface_strength = saturate(vis_uniforms.surface_power * (1.0 - abs(distance) / vis_uniforms.surface_width)); 99 | } 100 | 101 | // compute vertex position 102 | // the equilateral triangle inscribing the unit circle has its vertices on the circle of radius 2. 103 | // the vertex position is 2d (in view space), but before the projection to keep perspective. 104 | // this allow us to draw unlit spheres cheaply. 105 | // see https://mastodon.gamedev.place/@Az/111824726660048628 106 | let PI_2_3 = 2.094395; // 2 * pi / 3 107 | let angle = PI_2_3 * f32(in.vertex_index); 108 | var vertex_position = 2.0 * vec4(cos(angle), sin(angle), 0.0, 0.0); 109 | 110 | // scale the vertex position to the cell size 111 | // we use the minimum dimension to keep the triangle equilateral and thus the sphere inscribed in the cell 112 | let min_dim = min(cell_size.x, min(cell_size.y, cell_size.z)); 113 | vertex_position *= min_dim; 114 | 115 | // color is a mix of the 3 colors 116 | // since we expect only one strength to be strong, we can just add them 117 | let positive_color = vis_uniforms.positive_color * positive_strength; 118 | let negative_color = vis_uniforms.negative_color * negative_strength; 119 | let surface_color = vis_uniforms.surface_color * surface_strength; 120 | let color = positive_color + negative_color + surface_color; 121 | 122 | // alpha is the sum of the strengths 123 | // only one should be "big" 124 | // this let us dim the point if the "big" strength was reduced by its power 125 | // note that while we call it alpha, it is actually a size factor 126 | // we don't have alpha blending in this shader since we have depth testing 127 | // so reducing the size acts as a dimming 128 | let alpha = positive_strength + negative_strength + surface_strength; 129 | let point_size = vis_uniforms.point_size * alpha * 0.5; 130 | // svposition is the position in screen space, normalized 131 | let svposition = camera.proj * (viewpos + vertex_position * point_size); 132 | 133 | // we need to know the distance between the drawn pixel in the fragment shader and the center pixel 134 | // so we can discard pixels outside of the inscribed circle 135 | // since the vertex returns a triangle. 136 | 137 | // center is the exact pixel position of the center of the sphere 138 | var center_svpos = camera.proj * viewpos; 139 | center_svpos = center_svpos / center_svpos.w; 140 | var center = (center_svpos.xy + 1.0) / 2.0 * vec2(camera.resolution); 141 | center.y = f32(camera.resolution.y) - center.y; // invert y 142 | 143 | // svpos is the exact pixel position of the drawn vertice 144 | var svpos = svposition.xy / svposition.w; 145 | svpos = (svpos + 1.0) / 2.0 * vec2(camera.resolution); 146 | svpos.y = f32(camera.resolution.y) - svpos.y; // invert y 147 | 148 | // delta between the two pixels 149 | let offset = svpos - center; 150 | // distance_squared / 2^2 since we go from circle 2 to circle 1. 151 | let radius = dot(offset, offset) * 0.25; 152 | 153 | // output 154 | var out: VertexOutput; 155 | out.svposition = svposition; 156 | out.color = color.xyz; 157 | out.center_pos = center; 158 | out.radius = radius; 159 | return out; 160 | } 161 | 162 | 163 | @fragment 164 | fn main_fs(in: VertexOutput) -> @location(0) vec4 { 165 | let offset = in.svposition.xy - in.center_pos; 166 | // discard pixels outside of the inscribed circle 167 | if dot(offset, offset) > in.radius { 168 | discard; 169 | } 170 | return vec4(in.color, 1.0); 171 | } 172 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/shaders/draw_shadowmap.wgsl: -------------------------------------------------------------------------------- 1 | // Vertex shader 2 | struct CameraUniform { 3 | view_proj: mat4x4, 4 | view: mat4x4, 5 | proj: mat4x4, 6 | view_inv: mat4x4, 7 | proj_inv: mat4x4, 8 | eye: vec4, 9 | resolution: vec2, 10 | znear: f32, 11 | _padding: f32, 12 | }; 13 | @group(0) @binding(0) var camera: CameraUniform; 14 | 15 | struct VertexInput { 16 | @location(0) position: vec3, 17 | @location(1) normal: vec3, 18 | @location(2) uv: vec2, 19 | } 20 | 21 | @vertex 22 | fn main_vs( 23 | in: VertexInput, 24 | ) -> @builtin(position) vec4 { 25 | return camera.view_proj * vec4(in.position, 1.0); 26 | } 27 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/shaders/draw_voxels.wgsl: -------------------------------------------------------------------------------- 1 | struct SdfUniforms { 2 | // acts as bounding box 3 | start: vec4, 4 | end: vec4, 5 | cell_size: vec4, 6 | cell_count: vec4, 7 | } 8 | @group(0) @binding(0) var uniforms : SdfUniforms; 9 | @group(0) @binding(1) var sdf : array; 10 | @group(0) @binding(2) var ordered_indices : array; 11 | 12 | // Vertex shader 13 | struct CameraUniform { 14 | view_proj: mat4x4, 15 | view: mat4x4, 16 | proj: mat4x4, 17 | view_inv: mat4x4, 18 | proj_inv: mat4x4, 19 | eye: vec4, 20 | resolution: vec2, 21 | _padding: vec2, 22 | }; 23 | @group(1) @binding(0) var camera: CameraUniform; 24 | 25 | struct VisUniforms { 26 | positive_color: vec4, 27 | negative_color: vec4, 28 | surface_color: vec4, 29 | positive_power: f32, 30 | negative_power: f32, 31 | surface_iso: f32, 32 | surface_power: f32, 33 | surface_width: f32, 34 | point_size: f32, 35 | raymarch_mode: u32, 36 | bounding_box_extension: f32, 37 | mesh_bounding_box_min: vec4, 38 | mesh_bounding_box_max: vec4, 39 | map_material: u32, 40 | }; 41 | @group(2) @binding(0) var vis_uniforms: VisUniforms; 42 | 43 | @group(3) @binding(0) var shadow_camera: CameraUniform; 44 | @group(3) @binding(1) var shadow_map: texture_2d; 45 | @group(3) @binding(2) var shadow_sampler: sampler; 46 | 47 | @group(4) @binding(0) var cubemap: texture_2d_array; 48 | @group(4) @binding(1) var cubemap_sampler: sampler; 49 | @group(4) @binding(2) var cubemap_depth: texture_2d_array; 50 | @group(4) @binding(3) var cubemap_depth_sampler: sampler; 51 | @group(4) @binding(4) var cubemap_viewprojs: array, 6>; 52 | 53 | struct VertexInput { 54 | @location(0) vertex_position: vec3, 55 | @location(1) normal: vec3, 56 | @location(2) uv: vec2, 57 | @builtin(instance_index) instance_index: u32, 58 | }; 59 | 60 | struct VertexOutput { 61 | @builtin(position) svposition: vec4, 62 | @location(0) position: vec4, 63 | @location(1) cell_center: vec4, 64 | @location(2) normal: vec4, 65 | @location(3) distance: f32, 66 | @location(4) cell_idx: u32, 67 | }; 68 | 69 | 70 | fn phong_lighting(k_d: f32, k_s: f32, alpha: f32, position: vec3, eye: vec3, light_pos: vec3, light_intensity: vec3, normal: vec3) -> vec3 { 71 | let N = normal; 72 | let L = normalize(light_pos - position); 73 | let V = normalize(eye - position); 74 | let R = normalize(reflect(-L, N)); 75 | 76 | let dotLN = dot(L, N); 77 | let dotRV = dot(R, V); 78 | 79 | if dotLN < 0.0 { 80 | // Light not visible from this point on the surface 81 | return vec3(0.0, 0.0, 0.0); 82 | } 83 | 84 | if dotRV < 0.0 { 85 | // Light reflection in opposite direction as viewer, apply only diffuse 86 | // component 87 | return light_intensity * (k_d * dotLN); 88 | } 89 | return light_intensity * (k_d * dotLN + k_s * pow(dotRV, alpha)); 90 | } 91 | 92 | 93 | @vertex 94 | fn main_vs( 95 | in: VertexInput, 96 | ) -> VertexOutput { 97 | let cell_count = uniforms.cell_count.xyz; 98 | let cell_size = uniforms.cell_size.xyz; 99 | 100 | // compute cell world position 101 | // index was generated via for x in 0..cell_x { for y in 0..cell_y { for z in 0..cell_z { ... } } } 102 | // so index is x*cell_y*cell_z + y*cell_z + z 103 | var index = ordered_indices[in.instance_index]; 104 | let distance = sdf[index] - vis_uniforms.surface_iso; 105 | 106 | let cell_z = index % cell_count.z; 107 | index /= cell_count.z; 108 | let cell_y = index % cell_count.y; 109 | index /= cell_count.y; 110 | let cell_x = index; 111 | let cell_idx = vec3(f32(cell_x), f32(cell_y), f32(cell_z)); 112 | let cell = uniforms.start.xyz + cell_idx * cell_size; 113 | 114 | let world_pos = cell + in.vertex_position * uniforms.cell_size.xyz * 0.5; 115 | 116 | // cell view position 117 | let svposition = camera.view_proj * vec4(world_pos, 1.0); 118 | 119 | 120 | // output 121 | var out: VertexOutput; 122 | out.svposition = svposition; 123 | out.position = vec4(world_pos, 1.0); 124 | out.cell_center = vec4(cell, 1.0); 125 | out.normal = vec4(in.normal, 0.0); 126 | out.distance = distance; 127 | out.cell_idx = ordered_indices[in.instance_index]; 128 | return out; 129 | } 130 | 131 | fn get_albedo(p: vec3) -> vec3 { 132 | let bbox_center = (vis_uniforms.mesh_bounding_box_min.xyz + vis_uniforms.mesh_bounding_box_max.xyz) * 0.5; 133 | let bbox_size = vis_uniforms.mesh_bounding_box_max.xyz - vis_uniforms.mesh_bounding_box_min.xyz; 134 | let bbox_min = bbox_center - bbox_size * 0.5; 135 | let bbox_max = bbox_center + bbox_size * 0.5; 136 | 137 | let pmin = p - bbox_min; 138 | let pmax = bbox_max - p; 139 | let mini = min(pmin, pmax); 140 | let min = min(mini.x, min(mini.y, mini.z)); 141 | 142 | var fars = array(bbox_size.x, bbox_size.x, bbox_size.z, bbox_size.z, bbox_size.y, bbox_size.y); 143 | var dists = array(pmin.x, pmax.x, pmin.z, pmax.z, pmin.y, pmax.y); 144 | 145 | var layer = -1; 146 | 147 | var min_dist = 1e10; 148 | for (var i = 0; i < 6; i = i + 1) { 149 | var projected = cubemap_viewprojs[i] * vec4(p, 1.0); 150 | projected /= projected.w; 151 | var uv = projected.xy * 0.5 + 0.5; 152 | uv.y = 1.0 - uv.y; 153 | let depth = textureSample(cubemap_depth, cubemap_depth_sampler, uv, i).x; 154 | let depth_lin = (1.0 - depth) * fars[i]; 155 | let delta = abs(depth_lin - projected.z); 156 | if delta < min_dist && depth > 0.0 { 157 | layer = i; 158 | min_dist = delta; 159 | } 160 | } 161 | 162 | 163 | var color = vec3(1.0, 0.0, 1.0); 164 | if layer >= 0 { 165 | var projected = cubemap_viewprojs[layer] * vec4(p, 1.0); 166 | projected /= projected.w; 167 | var uv = projected.xy * 0.5 + 0.5; 168 | uv.y = 1.0 - uv.y; 169 | color = textureSample(cubemap, cubemap_sampler, uv, layer).xyz; 170 | } 171 | 172 | return color; 173 | } 174 | 175 | @fragment 176 | fn main_fs(in: VertexOutput) -> @location(0) vec4 { 177 | // We send the cell center because we want a single color per cell. 178 | var color = mix(vec3(0.5, 0.5, 0.5), get_albedo(in.cell_center.xyz), f32(vis_uniforms.map_material > 0)); 179 | 180 | let light = shadow_camera.eye.xyz; 181 | let light_dir = normalize(light - in.position.xyz); 182 | let ambiant = 0.2; 183 | let diffuse = max(0.0, dot(in.normal.xyz, light_dir)); 184 | 185 | var diffuse_strength = 1.0; 186 | 187 | var shadow_uv = shadow_camera.view_proj * vec4(in.position.xyz, 1.0); 188 | shadow_uv /= shadow_uv.w; 189 | shadow_uv.x = shadow_uv.x * 0.5 + 0.5; 190 | shadow_uv.y = shadow_uv.y * -0.5 + 0.5; 191 | var depth = shadow_uv.z; 192 | 193 | let threshold = depth * 1.05; 194 | 195 | let PCF: bool = true; 196 | 197 | if PCF { 198 | var light_depth = 0.0; 199 | let inv_res = vec2(1.0 / f32(camera.resolution.x), 1.0 / f32(camera.resolution.y)); 200 | for (var y = -1.; y <= 1.; y += 1.0) { 201 | for (var x = -1.; x <= 1.; x += 1.0) { 202 | let pdepth = textureSample(shadow_map, shadow_sampler, shadow_uv.xy + vec2(x, y) * inv_res).x; 203 | light_depth += f32(pdepth < threshold); 204 | } 205 | } 206 | light_depth /= 9.0; 207 | 208 | diffuse_strength *= light_depth; 209 | } else { 210 | let pdepth = textureSample(shadow_map, shadow_sampler, shadow_uv.xy).x; 211 | diffuse_strength *= f32(pdepth < threshold); 212 | } 213 | 214 | 215 | let view_dir = normalize(camera.eye.xyz - in.position.xyz); 216 | let half_dir = normalize(view_dir + light_dir); 217 | let specular = max(0.0, dot(in.normal.xyz, half_dir)); 218 | 219 | let brightness = ambiant + (diffuse + 0.5 * specular) * diffuse_strength; 220 | 221 | // arbitrary attenuation 222 | color.r *= exp(-1.8 * (1.0 - brightness)); 223 | color.g *= exp(-1.9 * (1.0 - brightness)); 224 | color.b *= exp(-1.9 * (1.0 - brightness)); 225 | 226 | return vec4(color, 1.0); 227 | } 228 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/shaders/utility/mipmap_generation.wgsl: -------------------------------------------------------------------------------- 1 | // From https://github.com/gfx-rs/wgpu/blob/trunk/examples/src/mipmap/blit.wgsl 2 | 3 | struct VertexOutput { 4 | @builtin(position) position: vec4, 5 | @location(0) tex_coords: vec2, 6 | }; 7 | 8 | // meant to be called with 3 vertex indices: 0, 1, 2 9 | // draws one large triangle over the clip space like this: 10 | // (the asterisks represent the clip space bounds) 11 | //-1,1 1,1 12 | // --------------------------------- 13 | // | * . 14 | // | * . 15 | // | * . 16 | // | * . 17 | // | * . 18 | // | * . 19 | // |*************** 20 | // | . 1,-1 21 | // | . 22 | // | . 23 | // | . 24 | // | . 25 | // |. 26 | @vertex 27 | fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { 28 | var result: VertexOutput; 29 | let x = i32(vertex_index) / 2; 30 | let y = i32(vertex_index) & 1; 31 | let tc = vec2( 32 | f32(x) * 2.0, 33 | f32(y) * 2.0 34 | ); 35 | result.position = vec4( 36 | tc.x * 2.0 - 1.0, 37 | 1.0 - tc.y * 2.0, 38 | 0.0, 1.0 39 | ); 40 | result.tex_coords = tc; 41 | return result; 42 | } 43 | 44 | @group(0) 45 | @binding(0) 46 | var r_color: texture_2d; 47 | @group(0) 48 | @binding(1) 49 | var r_sampler: sampler; 50 | 51 | @fragment 52 | fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { 53 | return textureSample(r_color, r_sampler, vertex.tex_coords); 54 | } -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/camera.rs: -------------------------------------------------------------------------------- 1 | use wgpu::util::DeviceExt; 2 | 3 | use crate::camera_control::CameraLookAt; 4 | 5 | #[derive(Debug)] 6 | pub struct Camera { 7 | pub look_at: CameraLookAt, 8 | pub aspect: f32, 9 | pub fovy: f32, 10 | pub znear: f32, 11 | } 12 | 13 | impl Camera { 14 | pub fn get_view_matrix(&self) -> glam::Mat4 { 15 | self.look_at.get_view_matrix() 16 | } 17 | 18 | pub fn get_projection_matrix(&self) -> glam::Mat4 { 19 | // Note: we use reverse z. 20 | glam::Mat4::perspective_infinite_reverse_rh(self.fovy.to_radians(), self.aspect, self.znear) 21 | } 22 | 23 | pub fn build_view_projection_matrix(&self) -> glam::Mat4 { 24 | let view = self.get_view_matrix(); 25 | let proj = self.get_projection_matrix(); 26 | proj * view 27 | } 28 | 29 | pub fn update_resolution(&mut self, resolution: [u32; 2]) { 30 | self.aspect = resolution[0] as f32 / resolution[1] as f32; 31 | } 32 | } 33 | 34 | // We need this for Rust to store our data correctly for the shaders 35 | #[repr(C)] 36 | // This is so we can store this in a buffer 37 | #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 38 | pub struct CameraUniform { 39 | pub view_proj: glam::Mat4, 40 | pub view: glam::Mat4, 41 | pub proj: glam::Mat4, 42 | pub view_inv: glam::Mat4, 43 | pub proj_inv: glam::Mat4, 44 | pub eye: glam::Vec4, 45 | pub resolution: [u32; 2], 46 | pub znear: f32, 47 | pub padding: f32, 48 | } 49 | 50 | impl CameraUniform { 51 | pub const fn new() -> Self { 52 | Self { 53 | view_proj: glam::Mat4::IDENTITY, 54 | view: glam::Mat4::IDENTITY, 55 | proj: glam::Mat4::IDENTITY, 56 | view_inv: glam::Mat4::IDENTITY, 57 | proj_inv: glam::Mat4::IDENTITY, 58 | 59 | eye: glam::Vec4::ZERO, 60 | resolution: [800, 600], 61 | znear: 0.1, 62 | padding: 0.0, 63 | } 64 | } 65 | 66 | pub fn from_camera(camera: &Camera) -> Self { 67 | let mut res = Self::new(); 68 | res.update_view_proj(camera); 69 | res 70 | } 71 | 72 | /// Update the view and projection matrices. 73 | pub fn update_view_proj(&mut self, camera: &Camera) { 74 | self.view_proj = camera.build_view_projection_matrix(); 75 | self.view = camera.get_view_matrix(); 76 | self.proj = camera.get_projection_matrix(); 77 | self.view_inv = self.view.inverse(); 78 | self.proj_inv = self.proj.inverse(); 79 | self.eye = camera.look_at.get_eye(); 80 | self.znear = camera.znear; 81 | } 82 | 83 | /// Unproject a pixel coordinate to a ray in world space. 84 | pub fn unproject(&self, pixel: [f32; 2]) -> glam::Vec3 { 85 | let x = pixel[0] / self.resolution[0] as f32; 86 | let y = pixel[1] / self.resolution[1] as f32; 87 | 88 | let x = x * 2.0 - 1.0; 89 | let y = 1.0 - y * 2.0; 90 | 91 | let dir_eye = self.proj_inv.transform_point3(glam::Vec3::new(x, y, 0.0)); 92 | let dir_world = self.view_inv.transform_vector3(dir_eye); 93 | dir_world.normalize() 94 | } 95 | } 96 | 97 | #[derive(Debug)] 98 | pub struct CameraData { 99 | pub camera: Camera, 100 | pub uniform: CameraUniform, 101 | pub buffer: wgpu::Buffer, 102 | pub bind_group: wgpu::BindGroup, 103 | pub bind_group_layout: wgpu::BindGroupLayout, 104 | } 105 | 106 | impl CameraData { 107 | pub fn update_resolution(&mut self, resolution: [u32; 2]) { 108 | self.camera.update_resolution(resolution); 109 | self.uniform.resolution = resolution; 110 | } 111 | 112 | pub fn new(device: &wgpu::Device) -> Self { 113 | let camera = Camera { 114 | look_at: CameraLookAt { 115 | center: glam::Vec3 { 116 | x: 0.0, 117 | y: 0.0, 118 | z: 0.0, 119 | }, 120 | longitude: 6.06, 121 | latitude: 0.37, 122 | distance: 1.66, 123 | }, 124 | aspect: 800.0 / 600.0, 125 | fovy: 45.0, 126 | znear: 0.1, 127 | }; 128 | 129 | let camera_uniform = CameraUniform::from_camera(&camera); 130 | 131 | let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 132 | label: Some("Camera Buffer"), 133 | contents: bytemuck::cast_slice(&[camera_uniform]), 134 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 135 | }); 136 | 137 | let camera_bind_group_layout = 138 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 139 | entries: &[wgpu::BindGroupLayoutEntry { 140 | binding: 0, 141 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 142 | ty: wgpu::BindingType::Buffer { 143 | ty: wgpu::BufferBindingType::Uniform, 144 | has_dynamic_offset: false, 145 | min_binding_size: wgpu::BufferSize::new(camera_buffer.size()), 146 | }, 147 | count: None, 148 | }], 149 | label: Some("camera_bind_group_layout"), 150 | }); 151 | 152 | let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 153 | layout: &camera_bind_group_layout, 154 | entries: &[wgpu::BindGroupEntry { 155 | binding: 0, 156 | resource: camera_buffer.as_entire_binding(), 157 | }], 158 | label: Some("camera_bind_group"), 159 | }); 160 | 161 | Self { 162 | camera, 163 | uniform: camera_uniform, 164 | buffer: camera_buffer, 165 | bind_group: camera_bind_group, 166 | bind_group_layout: camera_bind_group_layout, 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/camera_control.rs: -------------------------------------------------------------------------------- 1 | use winit::event::MouseButton; 2 | use winit_input_helper::WinitInputHelper; 3 | 4 | // Naive look-at camera. 5 | // This version removes the use of quaternion to avoid adding a dependency. 6 | // To avoid having to do linear algebra ourselves, most computations are done in the shader. 7 | // This is sub-optimal. Improving this is left as an exercise to the reader. 8 | #[repr(C)] 9 | #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 10 | pub struct CameraLookAt { 11 | /// Object the camera is looking at. 12 | pub center: glam::Vec3, 13 | /// Angle around the object, in radians. 14 | pub longitude: f32, 15 | /// latitude between -PI/2 and PI/2, 0 is flat, PI/2 is zenith, -PI/2 is nadir 16 | pub latitude: f32, 17 | /// Distance from center 18 | pub distance: f32, 19 | } 20 | 21 | impl Default for CameraLookAt { 22 | fn default() -> Self { 23 | // See object in 0,0,0 from the front top left 24 | Self { 25 | center: glam::Vec3::ZERO, 26 | longitude: 2.0 * core::f32::consts::FRAC_PI_3, 27 | latitude: core::f32::consts::FRAC_PI_3, 28 | distance: 5.0, 29 | } 30 | } 31 | } 32 | 33 | impl CameraLookAt { 34 | /// Pan the camera with middle mouse click, zoom with scroll wheel, orbit with right mouse click. 35 | pub fn update(&mut self, input: &WinitInputHelper, window_size: [f32; 2]) -> bool { 36 | let mut captured = false; 37 | 38 | // change input mapping for orbit and panning here 39 | let orbit_button = MouseButton::Right; 40 | let translation_button = MouseButton::Middle; 41 | 42 | let mouse_delta = input.cursor_diff(); 43 | if mouse_delta.0 != 0.0 || mouse_delta.1 != 0.0 { 44 | if input.mouse_held(orbit_button) { 45 | // Rotate around the object 46 | let delta_x = mouse_delta.0 / window_size[0] * core::f32::consts::TAU; 47 | let delta_y = mouse_delta.1 / window_size[1] * core::f32::consts::PI; 48 | self.longitude += delta_x; 49 | self.latitude += delta_y; 50 | self.latitude = self.latitude.clamp( 51 | -core::f32::consts::FRAC_PI_2 + 0.001, 52 | core::f32::consts::FRAC_PI_2 - 0.001, 53 | ); 54 | 55 | captured = true; 56 | } 57 | 58 | if input.mouse_held(translation_button) { 59 | // Translate the center. 60 | let dir = self.get_view_direction(); 61 | let up = glam::Vec3::Y; 62 | let translation_dir = dir.cross(up).normalize(); 63 | let up = translation_dir.cross(dir).normalize(); 64 | 65 | // The further away we are, the faster we move. 66 | let translation_weight = mouse_delta.0 / window_size[0] * self.distance; 67 | 68 | self.center += translation_dir * translation_weight; 69 | self.center += up * mouse_delta.1 / window_size[1] * self.distance; 70 | 71 | captured = true; 72 | } 73 | } 74 | 75 | if input.scroll_diff().1 != 0.0 { 76 | // Zoom 77 | self.distance -= input.scroll_diff().1 * self.distance * 0.2; 78 | // Don't allow zoom to reach 0 or 1e6 to avoid getting stuck / in float precision issue realm. 79 | self.distance = self.distance.clamp(0.05, 1e6); 80 | 81 | captured = true; 82 | } 83 | 84 | captured 85 | } 86 | 87 | /// Get the view direction. 88 | pub fn get_view_direction(&self) -> glam::Vec3 { 89 | let dir = glam::Vec3::new( 90 | self.longitude.cos() * self.latitude.cos(), 91 | self.latitude.sin(), 92 | self.longitude.sin() * self.latitude.cos(), 93 | ); 94 | dir.normalize() 95 | } 96 | 97 | /// Get the view matrix. 98 | pub fn get_view_matrix(&self) -> glam::Mat4 { 99 | let eye = self.get_eye(); 100 | glam::Mat4::look_at_rh(eye.truncate(), self.center, glam::Vec3::Y) 101 | } 102 | 103 | /// Get the eye position. 104 | pub fn get_eye(&self) -> glam::Vec4 { 105 | let dir = glam::Vec3::new( 106 | self.longitude.cos() * self.latitude.cos(), 107 | self.latitude.sin(), 108 | self.longitude.sin() * self.latitude.cos(), 109 | ); 110 | let eye: glam::Vec3 = self.center + dir * self.distance; 111 | glam::Vec4::from((eye, 1.0)) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/frame_rate.rs: -------------------------------------------------------------------------------- 1 | /// Sliding window to give a smooth framerate. 2 | /// Sum the last `window_size` `frame_duration` to estimate the framerate. 3 | /// Implemented with a circular buffer. 4 | #[derive(Debug)] 5 | pub struct FrameRate { 6 | /// Store the last frame durations. 7 | window: Vec, 8 | /// Index of the oldest frame duration, 9 | /// next frame duration will be stored here. 10 | current_index: usize, 11 | } 12 | 13 | impl FrameRate { 14 | /// Create a new slicing window with the given size. 15 | pub fn new(window_size: usize) -> Self { 16 | Self { 17 | current_index: 0, 18 | window: vec![0.0; window_size], 19 | } 20 | } 21 | 22 | /// Add the latest `frame_duration` to the window 23 | /// by remplacing the oldest `frame_duration`. 24 | pub fn update(&mut self, frame_duration: f32) { 25 | self.window[self.current_index] = frame_duration; 26 | self.current_index = (self.current_index + 1) % self.window.len(); 27 | } 28 | 29 | /// Compute current `frame_rate` 30 | /// Since the mean of frame duration is `sum(window) / window_size` 31 | /// The number of frame per seconds is `1 / sum(window) / window_size` 32 | /// ie `window_size / sum(window)` 33 | pub fn get(&self) -> f32 { 34 | self.window.len() as f32 / self.window.iter().sum::() 35 | } 36 | 37 | /// Return current parity of the frame. 38 | pub const fn _get_parity(&self) -> bool { 39 | self.current_index % 2 == 0 40 | } 41 | } 42 | 43 | impl Default for FrameRate { 44 | /// Create a default `FrameRate` with a window size of 20. 45 | fn default() -> Self { 46 | Self::new(20) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/camera.rs: -------------------------------------------------------------------------------- 1 | use glam::{Mat4, Vec2, Vec3}; 2 | use gltf::camera::Projection as GltfProjection; 3 | 4 | /// Contains camera properties. 5 | #[derive(Clone, Debug)] 6 | pub struct Camera { 7 | /// Camera name. Requires the `names` feature. 8 | pub name: Option, 9 | 10 | /// Scene extra data. Requires the `extras` feature. 11 | pub extras: gltf::json::extras::Extras, 12 | 13 | /// Transform matrix (also called world to camera matrix) 14 | pub transform: Mat4, 15 | 16 | /// Projection type and specific parameters 17 | pub projection: Projection, 18 | 19 | /// The distance to the far clipping plane. 20 | /// 21 | /// For perspective projection, this may be infinite. 22 | pub zfar: f32, 23 | 24 | /// The distance to the near clipping plane. 25 | pub znear: f32, 26 | } 27 | 28 | /// Camera projections 29 | #[derive(Debug, Clone)] 30 | pub enum Projection { 31 | /// Perspective projection 32 | Perspective { 33 | /// Y-axis FOV, in radians 34 | yfov: f32, 35 | /// Aspect ratio, if specified 36 | aspect_ratio: Option, 37 | }, 38 | /// Orthographic projection 39 | Orthographic { 40 | /// Projection scale 41 | scale: Vec2, 42 | }, 43 | } 44 | impl Default for Projection { 45 | fn default() -> Self { 46 | Self::Perspective { 47 | yfov: 0.399, 48 | aspect_ratio: None, 49 | } 50 | } 51 | } 52 | 53 | impl Camera { 54 | /// Position of the camera. 55 | pub fn position(&self) -> Vec3 { 56 | self.transform.col(3).truncate() 57 | } 58 | 59 | /// Right vector of the camera. 60 | pub fn right(&self) -> Vec3 { 61 | self.transform.col(0).truncate().normalize() 62 | } 63 | 64 | /// Up vector of the camera. 65 | pub fn up(&self) -> Vec3 { 66 | self.transform.col(1).truncate().normalize() 67 | } 68 | 69 | /// Forward vector of the camera (backside direction). 70 | pub fn forward(&self) -> Vec3 { 71 | self.transform.col(2).truncate().normalize() 72 | } 73 | 74 | /// Apply the transformation matrix on a vector. 75 | /// 76 | /// # Example 77 | /// ``` 78 | /// # use easy_gltf::Camera; 79 | /// # use glam::*; 80 | /// # let cam = Camera::default(); 81 | /// let ray_dir = Vec3::new(1., 0., 0.); 82 | /// let ray_dir = cam.apply_transform_vector(&ray_dir); 83 | /// ``` 84 | pub fn apply_transform_vector(&self, pos: &Vec3) -> Vec3 { 85 | let res = self.transform * pos.extend(1.0); 86 | res.truncate() / res.w 87 | } 88 | 89 | pub(crate) fn load(gltf_cam: &gltf::Camera, transform: &Mat4) -> Self { 90 | let mut cam = Self { 91 | transform: *transform, 92 | ..Default::default() 93 | }; 94 | 95 | cam.name = gltf_cam.name().map(String::from); 96 | cam.extras.clone_from(gltf_cam.extras()); 97 | 98 | match gltf_cam.projection() { 99 | GltfProjection::Orthographic(ortho) => { 100 | cam.projection = Projection::Orthographic { 101 | scale: Vec2::new(ortho.xmag(), ortho.ymag()), 102 | }; 103 | cam.zfar = ortho.zfar(); 104 | cam.znear = ortho.znear(); 105 | } 106 | GltfProjection::Perspective(pers) => { 107 | cam.projection = Projection::Perspective { 108 | yfov: pers.yfov(), 109 | aspect_ratio: pers.aspect_ratio(), 110 | }; 111 | cam.zfar = pers.zfar().unwrap_or(f32::INFINITY); 112 | cam.znear = pers.znear(); 113 | } 114 | }; 115 | cam 116 | } 117 | } 118 | 119 | impl Default for Camera { 120 | fn default() -> Self { 121 | Self { 122 | name: None, 123 | extras: None, 124 | transform: Mat4::ZERO, 125 | projection: Projection::default(), 126 | zfar: f32::INFINITY, 127 | znear: 0.1, 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/light.rs: -------------------------------------------------------------------------------- 1 | use glam::{Mat4, Vec3, Vec4Swizzles}; 2 | use gltf::khr_lights_punctual::{Kind, Light as GltfLight}; 3 | 4 | /// Represents a light. 5 | #[derive(Clone, Debug)] 6 | pub enum Light { 7 | /// Directional lights are light sources that act as though they are 8 | /// infinitely far away and emit light in the `direction`. Because it is at 9 | /// an infinite distance, the light is not attenuated. Its intensity is 10 | /// defined in lumens per metre squared, or lux (lm/m2). 11 | Directional { 12 | /// Light name. Requires the `names` feature. 13 | name: Option, 14 | /// Light extra data. Requires the `extras` feature 15 | extras: gltf::json::extras::Extras, 16 | /// Direction of the directional light 17 | direction: Vec3, 18 | /// Color of the directional light 19 | color: Vec3, 20 | /// Intensity of the directional light 21 | intensity: f32, 22 | }, 23 | 24 | /// Point lights emit light in all directions from their `position` in space; 25 | /// The brightness of the light attenuates in a physically correct manner as 26 | /// distance increases from the light's position (i.e. brightness goes like 27 | /// the inverse square of the distance). Point light intensity is defined in 28 | /// candela, which is lumens per square radian (lm/sr). 29 | Point { 30 | /// Light name. Requires the `names` feature. 31 | name: Option, 32 | /// Light extra data. Requires the `extras` feature 33 | extras: gltf::json::extras::Extras, 34 | /// Position of the point light 35 | position: Vec3, 36 | /// Color of the point light 37 | color: Vec3, 38 | /// Intensity of the point light 39 | intensity: f32, 40 | }, 41 | 42 | /// Spot lights emit light in a cone in `direction`. The angle and falloff 43 | /// of the cone is defined using two numbers, the `inner_cone_angle` and 44 | /// `outer_cone_angle`. As with point lights, the brightness also attenuates 45 | /// in a physically correct manner as distance increases from the light's 46 | /// position (i.e. brightness goes like the inverse square of the distance). 47 | /// Spot light intensity refers to the brightness inside the 48 | /// `inner_cone_angle` (and at the location of the light) and is defined in 49 | /// candela, which is lumens per square radian (lm/sr). Engines that don't 50 | /// support two angles for spotlights should use `outer_cone_angle` as the 51 | /// spotlight angle (leaving `inner_cone_angle` to implicitly be `0`). 52 | Spot { 53 | /// Light name. Requires the `names` feature. 54 | name: Option, 55 | /// Light extra data. Requires the `extras` feature 56 | extras: gltf::json::extras::Extras, 57 | /// Position of the spot light 58 | position: Vec3, 59 | /// Direction of the spot light 60 | direction: Vec3, 61 | /// Color of the spot light 62 | color: Vec3, 63 | /// Intensity of the spot light 64 | intensity: f32, 65 | /// Inner cone angle of the spot light 66 | inner_cone_angle: f32, 67 | /// Outer cone angle of the spot light 68 | outer_cone_angle: f32, 69 | }, 70 | } 71 | 72 | impl Light { 73 | pub(crate) fn load(gltf_light: &GltfLight, transform: &Mat4) -> Self { 74 | match gltf_light.kind() { 75 | Kind::Directional => Self::Directional { 76 | name: gltf_light.name().map(String::from), 77 | extras: gltf_light.extras().clone(), 78 | direction: -1. * transform.col(2).xyz().normalize(), 79 | intensity: gltf_light.intensity(), 80 | color: Vec3::from(gltf_light.color()), 81 | }, 82 | Kind::Point => Self::Point { 83 | name: gltf_light.name().map(String::from), 84 | extras: gltf_light.extras().clone(), 85 | position: transform.col(3).xyz(), 86 | intensity: gltf_light.intensity(), 87 | color: Vec3::from(gltf_light.color()), 88 | }, 89 | Kind::Spot { 90 | inner_cone_angle, 91 | outer_cone_angle, 92 | } => Self::Spot { 93 | name: gltf_light.name().map(String::from), 94 | extras: gltf_light.extras().clone(), 95 | position: transform.col(3).xyz(), 96 | direction: -1. * transform.col(2).xyz().normalize(), 97 | intensity: gltf_light.intensity(), 98 | color: Vec3::from(gltf_light.color()), 99 | inner_cone_angle, 100 | outer_cone_angle, 101 | }, 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/mod.rs: -------------------------------------------------------------------------------- 1 | mod camera; 2 | mod light; 3 | /// Contains model and material 4 | /// # Usage 5 | /// Check [Model](struct.Model.html) for more information about how to use this module. 6 | pub mod model; 7 | 8 | pub use camera::Camera; 9 | use gltf::scene::Node; 10 | pub use light::Light; 11 | pub use model::{GltfModel, Material}; 12 | 13 | use super::utils::transform_to_matrix; 14 | 15 | /// Contains cameras, models and lights of a scene. 16 | #[derive(Default, Clone, Debug)] 17 | pub struct Scene { 18 | /// Scene name. Requires the `names` feature. 19 | pub name: Option, 20 | /// Scene extra data. Requires the `extras` feature. 21 | pub extras: gltf::json::extras::Extras, 22 | /// List of models in the scene 23 | pub models: Vec, 24 | /// List of cameras in the scene 25 | pub cameras: Vec, 26 | /// List of lights in the scene 27 | pub lights: Vec, 28 | /// Scene root not 29 | pub root_node: ModelNode, 30 | } 31 | 32 | /// A model in the scene graph 33 | #[derive(Debug, Clone, Default)] 34 | pub struct ModelNode { 35 | /// Model. 36 | pub model_id: Option, 37 | /// Node transform. 38 | pub transform: glam::Mat4, 39 | /// Node children. 40 | pub children: Vec, 41 | /// Name 42 | pub name: Option, 43 | } 44 | 45 | impl ModelNode { 46 | /// Debug print the node. 47 | pub fn debug(&self, level: usize) { 48 | for _ in 0..level { 49 | print!(" "); 50 | } 51 | log::debug!("{:?}", self.model_id); 52 | for child in &self.children { 53 | child.debug(level + 1); 54 | } 55 | } 56 | 57 | /// Simplify tree by removing nodes that only contain a single child. 58 | pub fn simplify_tree(&mut self) { 59 | // First simplify children 60 | for node in &mut self.children { 61 | node.simplify_tree(); 62 | } 63 | 64 | // self is only a transform node. No need to keep it. 65 | if self.model_id.is_none() && self.children.len() == 1 { 66 | let mut new_node = self.children.remove(0); 67 | new_node.transform = self.transform * new_node.transform; 68 | // Keep the parent name if present since the model name is ofter generic as it can be shared. 69 | if new_node.model_id.is_some() && self.name.is_some() { 70 | new_node.name = self.name.take(); 71 | } 72 | *self = new_node; 73 | } 74 | } 75 | } 76 | 77 | impl Scene { 78 | fn new(name: Option<&str>, extras: gltf::json::Extras) -> Self { 79 | Self { 80 | name: name.map(String::from), 81 | extras, 82 | ..Default::default() 83 | } 84 | } 85 | 86 | pub(crate) fn load(file_name: &str, gltf_scene: &gltf::Scene) -> Self { 87 | let mut scene = Self::new(gltf_scene.name(), gltf_scene.extras().clone()); 88 | 89 | for node in gltf_scene.nodes() { 90 | let mut new_root = ModelNode::default(); 91 | scene.read_node(&mut new_root, &node); 92 | scene.root_node.children.push(new_root); 93 | } 94 | 95 | // Simplify tree by removing nodes that only contain a single child and no model. 96 | scene.root_node.simplify_tree(); 97 | 98 | // Rename root node if it has no name with the file name. 99 | if scene.root_node.name.is_none() { 100 | scene.root_node.name = Some(file_name.to_string()); 101 | } 102 | 103 | scene 104 | } 105 | 106 | fn read_node(&mut self, parent_node: &mut ModelNode, node: &Node) { 107 | // Compute transform of the current node 108 | let mut new_node = ModelNode { 109 | name: node.name().map(String::from), 110 | ..Default::default() 111 | }; 112 | 113 | // Rename parent node if it has no name. Useful sinces meshes are often their own nodes. 114 | if parent_node.name.is_none() { 115 | parent_node.name.clone_from(&new_node.name); 116 | } 117 | 118 | let transform = transform_to_matrix(node.transform()); 119 | 120 | new_node.transform = transform; 121 | 122 | // Recurse on children 123 | for child in node.children() { 124 | self.read_node(&mut new_node, &child); 125 | } 126 | 127 | // Load camera 128 | if let Some(camera) = node.camera() { 129 | self.cameras.push(Camera::load(&camera, &transform)); 130 | } 131 | 132 | // Load light 133 | if let Some(light) = node.light() { 134 | self.lights.push(Light::load(&light, &transform)); 135 | } 136 | 137 | // Load model 138 | if let Some(mesh) = node.mesh() { 139 | let model_id = mesh.index(); 140 | self.models.push(model_id); 141 | new_node.model_id = Some(model_id); 142 | new_node.name = mesh.name().map(String::from).or(new_node.name); 143 | 144 | // Rename parent node if it has no name. Useful sinces meshes are often their own nodes. 145 | if parent_node.name.is_none() { 146 | parent_node.name.clone_from(&new_node.name); 147 | } 148 | } 149 | parent_node.children.push(new_node); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/model/material/emissive.rs: -------------------------------------------------------------------------------- 1 | use glam::Vec3; 2 | use image::RgbImage; 3 | use std::sync::Arc; 4 | 5 | use crate::gltf::GltfData; 6 | 7 | #[derive(Clone, Debug)] 8 | /// The emissive color of the material. 9 | pub struct Emissive { 10 | /// The `emissive_texture` refers to a texture that may be used to illuminate parts of the 11 | /// model surface: It defines the color of the light that is emitted from the surface 12 | pub texture: Option>, 13 | 14 | /// The `emissive_factor` contains scaling factors for the red, green and 15 | /// blue components of this texture. 16 | pub factor: Vec3, 17 | } 18 | 19 | impl Emissive { 20 | pub(crate) fn load(gltf_mat: &gltf::Material, data: &GltfData) -> Self { 21 | Self { 22 | texture: gltf_mat 23 | .emissive_texture() 24 | .map(|texture| data.get_rgb_image(&texture.texture())), 25 | factor: gltf_mat.emissive_factor().into(), 26 | } 27 | } 28 | } 29 | 30 | impl Default for Emissive { 31 | fn default() -> Self { 32 | Self { 33 | texture: None, 34 | factor: Vec3::ZERO, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/model/material/mod.rs: -------------------------------------------------------------------------------- 1 | mod emissive; 2 | mod normal; 3 | mod occlusion; 4 | mod pbr; 5 | 6 | use core::ops::Deref; 7 | use glam::{Vec2, Vec3, Vec4}; 8 | use image::{ImageBuffer, Pixel}; 9 | use std::sync::Arc; 10 | 11 | pub use emissive::Emissive; 12 | pub use normal::NormalMap; 13 | pub use occlusion::Occlusion; 14 | pub use pbr::PbrMaterial; 15 | 16 | use super::GltfData; 17 | 18 | /// Contains material properties of models. 19 | #[derive(Clone, Debug, Default)] 20 | pub struct Material { 21 | /// Material name. Requires the `names` feature. 22 | pub name: Option, 23 | 24 | /// Material extra data. Requires the `extras` feature. 25 | pub extras: gltf::json::extras::Extras, 26 | 27 | /// Parameter values that define the metallic-roughness material model from 28 | /// Physically-Based Rendering (PBR) methodology. 29 | pub pbr: PbrMaterial, 30 | 31 | /// Defines the normal texture of a material. 32 | pub normal: Option, 33 | 34 | /// Defines the occlusion texture of a material. 35 | pub occlusion: Option, 36 | 37 | /// The emissive color of the material. 38 | pub emissive: Emissive, 39 | } 40 | 41 | impl Material { 42 | /// Get the color base Rgb(A) (in RGB-color space) of the material given a 43 | /// texture coordinate. If no `base_color_texture` is available then the 44 | /// `base_color_factor` is returned. 45 | /// 46 | /// **Important**: `tex_coords` must contain values between `[0., 1.]` 47 | /// otherwise the function will fail. 48 | pub fn get_base_color_alpha(&self, tex_coords: Vec2) -> Vec4 { 49 | let mut res = self.pbr.base_color_factor; 50 | if let Some(texture) = &self.pbr.base_color_texture { 51 | let px_u = Self::get_pixel(tex_coords, texture); 52 | // Transform to float 53 | let mut px_f = Vec4::ZERO; 54 | for i in 0..4 { 55 | px_f[i] = f32::from(px_u[i]) / 255.; 56 | } 57 | // Convert sRGB to RGB 58 | let pixel = Vec4::new(px_f.x.powf(2.2), px_f.y.powf(2.2), px_f.z.powf(2.2), px_f.w); 59 | // Multiply to the scale factor 60 | for i in 0..4 { 61 | res[i] *= pixel[i]; 62 | } 63 | } 64 | res 65 | } 66 | 67 | /// Get the color base Rgb (in RGB-color space) of the material given a 68 | /// texture coordinate. If no `base_color_texture` is available then the 69 | /// `base_color_factor` is returned. 70 | /// 71 | /// **Important**: `tex_coords` must contain values between `[0., 1.]` 72 | /// otherwise the function will fail. 73 | pub fn get_base_color(&self, tex_coords: Vec2) -> Vec3 { 74 | self.get_base_color_alpha(tex_coords).truncate() 75 | } 76 | 77 | /// Get the metallic value of the material given a texture coordinate. If no 78 | /// `metallic_texture` is available then the `metallic_factor` is returned. 79 | /// 80 | /// **Important**: `tex_coords` must contain values between `[0., 1.]` 81 | /// otherwise the function will fail. 82 | pub fn get_metallic(&self, tex_coords: Vec2) -> f32 { 83 | self.pbr.metallic_factor 84 | * self.pbr.metallic_texture.as_ref().map_or(1., |texture| { 85 | f32::from(Self::get_pixel(tex_coords, texture)[0]) / 255. 86 | }) 87 | } 88 | 89 | /// Get the roughness value of the material given a texture coordinate. If no 90 | /// `roughness_texture` is available then the `roughness_factor` is returned. 91 | /// 92 | /// **Important**: `tex_coords` must contain values between `[0., 1.]` 93 | /// otherwise the function will fail. 94 | pub fn get_roughness(&self, tex_coords: Vec2) -> f32 { 95 | self.pbr.roughness_factor 96 | * self.pbr.roughness_texture.as_ref().map_or(1., |texture| { 97 | f32::from(Self::get_pixel(tex_coords, texture)[0]) / 255. 98 | }) 99 | } 100 | 101 | /// Get the normal vector of the material given a texture coordinate. If no 102 | /// `normal_texture` is available then `None` is returned. 103 | /// 104 | /// **Important**: `tex_coords` must contain values between `[0., 1.]` 105 | /// otherwise the function will fail. 106 | pub fn get_normal(&self, tex_coords: Vec2) -> Option { 107 | let normal = self.normal.as_ref()?; 108 | let pixel = Self::get_pixel(tex_coords, &normal.texture); 109 | Some( 110 | normal.factor 111 | * Vec3::new( 112 | f32::from(pixel[0]) / 127.5 - 1., 113 | f32::from(pixel[1]) / 127.5 - 1., 114 | f32::from(pixel[2]) / 127.5 - 1., 115 | ), 116 | ) 117 | } 118 | 119 | /// Get the occlusion value of the material given a texture coordinate. If no 120 | /// `occlusion_texture` is available then `None` is returned. 121 | /// 122 | /// **Important**: `tex_coords` must contain values between `[0., 1.]` 123 | /// otherwise the function will fail. 124 | pub fn get_occlusion(&self, tex_coords: Vec2) -> Option { 125 | let occlusion = self.occlusion.as_ref()?; 126 | Some( 127 | occlusion.factor * f32::from(Self::get_pixel(tex_coords, &occlusion.texture)[0]) / 255., 128 | ) 129 | } 130 | 131 | /// Get the emissive color Rgb of the material given a texture coordinate. 132 | /// If no `emissive_texture` is available then the `emissive_factor` is 133 | /// returned. 134 | /// 135 | /// **Important**: `tex_coords` must contain values between `[0., 1.]` 136 | /// otherwise the function will fail. 137 | pub fn get_emissive(&self, tex_coords: Vec2) -> Vec3 { 138 | let mut res = self.emissive.factor; 139 | if let Some(texture) = &self.emissive.texture { 140 | let pixel = Self::get_pixel(tex_coords, texture); 141 | for i in 0..3 { 142 | res[i] *= f32::from(pixel[i]) / 255.; 143 | } 144 | } 145 | res 146 | } 147 | 148 | fn get_pixel(tex_coords: Vec2, texture: &ImageBuffer) -> P 149 | where 150 | P: Pixel + 'static, 151 | P::Subpixel: 'static, 152 | Container: Deref, 153 | { 154 | let coords = Vec2 { 155 | x: tex_coords.x * texture.width() as f32, 156 | y: tex_coords.y * texture.height() as f32, 157 | }; 158 | 159 | texture[( 160 | (coords.x as i64).rem_euclid(i64::from(texture.width())) as u32, 161 | (coords.y as i64).rem_euclid(i64::from(texture.height())) as u32, 162 | )] 163 | } 164 | 165 | pub(crate) fn load(gltf_mat: &gltf::Material, data: &GltfData) -> Arc { 166 | if let Some(material) = data.materials.get(&gltf_mat.index()) { 167 | return Arc::clone(material); 168 | } 169 | 170 | let material = Arc::new(Self { 171 | name: gltf_mat.name().map(String::from), 172 | extras: gltf_mat.extras().clone(), 173 | 174 | pbr: PbrMaterial::load(&gltf_mat.pbr_metallic_roughness(), data), 175 | normal: NormalMap::load(gltf_mat, data), 176 | occlusion: Occlusion::load(gltf_mat, data), 177 | emissive: Emissive::load(gltf_mat, data), 178 | }); 179 | 180 | // Add to the collection 181 | material 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/model/material/normal.rs: -------------------------------------------------------------------------------- 1 | use super::GltfData; 2 | use image::RgbImage; 3 | use std::sync::Arc; 4 | 5 | #[derive(Clone, Debug)] 6 | /// Defines the normal texture of a material. 7 | pub struct NormalMap { 8 | /// A tangent space normal map. 9 | /// The texture contains RGB components in linear space. Each texel 10 | /// represents the XYZ components of a normal vector in tangent space. 11 | /// 12 | /// * Red [0 to 255] maps to X [-1 to 1]. 13 | /// * Green [0 to 255] maps to Y [-1 to 1]. 14 | /// * Blue [128 to 255] maps to Z [1/255 to 1]. 15 | /// 16 | /// The normal vectors use OpenGL conventions where +X is right, +Y is up, 17 | /// and +Z points toward the viewer. 18 | pub texture: Arc, 19 | 20 | /// The `normal_factor` is the normal strength to be applied to the 21 | /// texture value. 22 | pub factor: f32, 23 | } 24 | 25 | impl NormalMap { 26 | pub(crate) fn load(gltf_mat: &gltf::Material, data: &GltfData) -> Option { 27 | gltf_mat.normal_texture().map(|texture| Self { 28 | texture: data.get_rgb_image(&texture.texture()), 29 | factor: texture.scale(), 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/model/material/occlusion.rs: -------------------------------------------------------------------------------- 1 | use super::GltfData; 2 | use image::GrayImage; 3 | use std::sync::Arc; 4 | 5 | #[derive(Clone, Debug)] 6 | /// Defines the occlusion texture of a material. 7 | pub struct Occlusion { 8 | /// The `occlusion_texture` refers to a texture that defines areas of the 9 | /// surface that are occluded from light, and thus rendered darker. 10 | pub texture: Arc, 11 | 12 | /// The `occlusion_factor` is the occlusion strength to be applied to the 13 | /// texture value. 14 | pub factor: f32, 15 | } 16 | 17 | impl Occlusion { 18 | pub(crate) fn load(gltf_mat: &gltf::Material, data: &GltfData) -> Option { 19 | gltf_mat.occlusion_texture().map(|texture| Self { 20 | texture: data.get_gray_image(&texture.texture(), 0), 21 | factor: texture.strength(), 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/model/material/pbr.rs: -------------------------------------------------------------------------------- 1 | use super::GltfData; 2 | use glam::Vec4; 3 | use image::{GrayImage, RgbaImage}; 4 | use std::sync::Arc; 5 | 6 | #[derive(Clone, Debug)] 7 | /// A set of parameter values that are used to define the metallic-roughness 8 | /// material model from Physically-Based Rendering (PBR) methodology. 9 | pub struct PbrMaterial { 10 | /// The `base_color_factor` contains scaling factors for the red, green, 11 | /// blue and alpha component of the color. If no texture is used, these 12 | /// values will define the color of the whole object in **RGB** color space. 13 | pub base_color_factor: Vec4, 14 | 15 | /// The `base_color_texture` is the main texture that will be applied to the 16 | /// object. 17 | /// 18 | /// The texture contains RGB(A) components in **sRGB** color space. 19 | pub base_color_texture: Option>, 20 | 21 | /// Contains the metalness value 22 | pub metallic_texture: Option>, 23 | 24 | /// `metallic_factor` is multiply to the `metallic_texture` value. If no 25 | /// texture is given, then the factor define the metalness for the whole 26 | /// object. 27 | pub metallic_factor: f32, 28 | 29 | /// Contains the roughness value 30 | pub roughness_texture: Option>, 31 | 32 | /// `roughness_factor` is multiply to the `roughness_texture` value. If no 33 | /// texture is given, then the factor define the roughness for the whole 34 | /// object. 35 | pub roughness_factor: f32, 36 | } 37 | 38 | impl PbrMaterial { 39 | pub(crate) fn load(pbr: &gltf::material::PbrMetallicRoughness, data: &GltfData) -> Self { 40 | let mut material = Self { 41 | base_color_factor: pbr.base_color_factor().into(), 42 | ..Default::default() 43 | }; 44 | if let Some(texture) = pbr.base_color_texture() { 45 | material.base_color_texture = Some(data.get_base_color_image(&texture.texture())); 46 | } 47 | 48 | material.roughness_factor = pbr.roughness_factor(); 49 | material.metallic_factor = pbr.metallic_factor(); 50 | 51 | if let Some(texture) = pbr.metallic_roughness_texture() { 52 | if material.metallic_factor > 0. { 53 | material.metallic_texture = Some(data.get_gray_image(&texture.texture(), 2)); 54 | } 55 | if material.roughness_factor > 0. { 56 | material.roughness_texture = Some(data.get_gray_image(&texture.texture(), 1)); 57 | } 58 | } 59 | 60 | material 61 | } 62 | } 63 | 64 | impl Default for PbrMaterial { 65 | fn default() -> Self { 66 | Self { 67 | base_color_factor: Vec4::ONE, 68 | base_color_texture: None, 69 | metallic_factor: 0., 70 | metallic_texture: None, 71 | roughness_factor: 0., 72 | roughness_texture: None, 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/model/mode.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// The type of primitives to render. 4 | /// 5 | /// To find more information for each mode and how to render them check 6 | /// [Khronos Primitive Documentation](https://www.khronos.org/opengl/wiki/Primitive). 7 | #[derive(Clone, Debug, PartialEq, Eq, Default)] 8 | pub enum Mode { 9 | /// Corresponds to `GL_POINTS`. 10 | Points, 11 | /// Corresponds to `GL_LINES`. 12 | Lines, 13 | /// Corresponds to `GL_LINE_LOOP`. 14 | LineLoop, 15 | /// Corresponds to `GL_LINE_STRIP`. 16 | LineStrip, 17 | /// Corresponds to `GL_TRIANGLES`. 18 | #[default] 19 | Triangles, 20 | /// Corresponds to `GL_TRIANGLE_STRIP`. 21 | TriangleStrip, 22 | /// Corresponds to `GL_TRIANGLE_FAN`. 23 | TriangleFan, 24 | } 25 | 26 | impl From for Mode { 27 | fn from(mode: gltf::mesh::Mode) -> Self { 28 | match mode { 29 | gltf::mesh::Mode::Points => Self::Points, 30 | gltf::mesh::Mode::Lines => Self::Lines, 31 | gltf::mesh::Mode::LineLoop => Self::LineLoop, 32 | gltf::mesh::Mode::LineStrip => Self::LineStrip, 33 | gltf::mesh::Mode::Triangles => Self::Triangles, 34 | gltf::mesh::Mode::TriangleFan => Self::TriangleFan, 35 | gltf::mesh::Mode::TriangleStrip => Self::TriangleStrip, 36 | } 37 | } 38 | } 39 | 40 | /// Represents a runtime error. This error is triggered when an expected mode 41 | /// doesn't match the model mode . 42 | #[derive(Clone, Debug)] 43 | pub struct BadMode { 44 | /// The current mode of the model. 45 | pub mode: Mode, 46 | } 47 | 48 | impl fmt::Display for BadMode { 49 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 50 | write!(f, "Invalid mode \"{:?}\"", self.mode,) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/scene/model/vertex.rs: -------------------------------------------------------------------------------- 1 | use glam::{Vec2, Vec3, Vec4}; 2 | 3 | /// Represents the 3 vertices of a triangle. 4 | pub type Triangle = [Vertex; 3]; 5 | 6 | /// Represents the 2 vertices of a line. 7 | pub type Line = [Vertex; 2]; 8 | 9 | /// Contains a position, normal and texture coordinates vectors. 10 | #[repr(C)] 11 | #[derive(Clone, Copy, Debug, PartialEq)] 12 | pub struct Vertex { 13 | /// Position 14 | pub position: Vec3, 15 | /// Normalized normal 16 | pub normal: Vec3, 17 | /// Tangent normal 18 | /// The w component is the handedness of the tangent basis (can be -1 or 1) 19 | pub tangent: Vec4, 20 | /// Texture coordinates 21 | pub tex_coords: Vec2, 22 | } 23 | 24 | impl Default for Vertex { 25 | fn default() -> Self { 26 | Self { 27 | position: Vec3::ZERO, 28 | normal: Vec3::ZERO, 29 | tangent: Vec4::ZERO, 30 | tex_coords: Vec2::ZERO, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/utils/gltf_data.rs: -------------------------------------------------------------------------------- 1 | use base64::engine::general_purpose::URL_SAFE_NO_PAD; 2 | use base64::Engine; 3 | use gltf::image::Source; 4 | use hashbrown::HashMap; 5 | use image::{DynamicImage, GrayImage, ImageFormat, RgbImage, RgbaImage}; 6 | use std::path::{Path, PathBuf}; 7 | use std::sync::Arc; 8 | 9 | use crate::gltf::scene::GltfModel; 10 | use crate::gltf::Material; 11 | 12 | /// Helps to simplify the signature of import related functions. 13 | #[derive(Debug, Clone)] 14 | pub struct GltfData { 15 | /// Buffers of the glTF document. 16 | pub buffers: Vec, 17 | /// Base directory of the glTF document. 18 | pub base_dir: PathBuf, 19 | /// Models of the glTF document. 20 | pub models: HashMap>, 21 | /// Materials of the glTF document. 22 | pub materials: HashMap, Arc>, 23 | /// RGB images of the glTF document. 24 | pub rgb_images: HashMap>, 25 | /// RGBA images of the glTF document. 26 | pub rgba_images: HashMap>, 27 | /// Gray images of the glTF document. 28 | pub gray_images: HashMap<(usize, usize), Arc>, 29 | } 30 | 31 | impl GltfData { 32 | /// Create a new `GltfData` instance. 33 | pub fn new

(buffers: Vec, path: P) -> Self 34 | where 35 | P: AsRef, 36 | { 37 | let mut base_dir = PathBuf::from(path.as_ref()); 38 | base_dir.pop(); 39 | Self { 40 | buffers, 41 | base_dir, 42 | models: HashMap::default(), 43 | materials: HashMap::default(), 44 | rgb_images: HashMap::default(), 45 | rgba_images: HashMap::default(), 46 | gray_images: HashMap::default(), 47 | } 48 | } 49 | 50 | /// Get a rgb image from the glTF document. 51 | pub fn get_rgb_image(&self, texture: &gltf::Texture<'_>) -> Arc { 52 | Arc::clone( 53 | self.rgb_images 54 | .get(&texture.index()) 55 | .expect("Didn't preload this image"), 56 | ) 57 | } 58 | 59 | /// Get a base color image from the glTF document. 60 | pub fn get_base_color_image(&self, texture: &gltf::Texture<'_>) -> Arc { 61 | Arc::clone( 62 | self.rgba_images 63 | .get(&texture.index()) 64 | .expect("Didn't preload this image"), 65 | ) 66 | } 67 | 68 | /// Get a gray image from the glTF document. 69 | pub fn get_gray_image(&self, texture: &gltf::Texture<'_>, channel: usize) -> Arc { 70 | Arc::clone( 71 | self.gray_images 72 | .get(&(texture.index(), channel)) 73 | .expect("Didn't preload this image"), 74 | ) 75 | } 76 | 77 | /// Load a texture from the glTF document. 78 | pub fn load_texture(&self, texture: &gltf::Texture<'_>) -> DynamicImage { 79 | let g_img = texture.source(); 80 | let buffers = &self.buffers; 81 | match g_img.source() { 82 | Source::View { view, mime_type } => { 83 | let parent_buffer_data = &buffers[view.buffer().index()].0; 84 | let data = &parent_buffer_data[view.offset()..view.offset() + view.length()]; 85 | let mime_type = mime_type.replace('/', "."); 86 | image::load_from_memory_with_format( 87 | data, 88 | ImageFormat::from_path(mime_type).unwrap(), 89 | ) 90 | .unwrap() 91 | } 92 | Source::Uri { uri, mime_type } => { 93 | if uri.starts_with("data:") { 94 | let encoded = uri.split(',').nth(1).unwrap(); 95 | let data = URL_SAFE_NO_PAD.decode(encoded).unwrap(); 96 | let mime_type = mime_type.map_or_else( 97 | || { 98 | uri.split(',') 99 | .next() 100 | .unwrap() 101 | .split(':') 102 | .nth(1) 103 | .unwrap() 104 | .split(';') 105 | .next() 106 | .unwrap() 107 | }, 108 | |ty| ty, 109 | ); 110 | let mime_type = mime_type.replace('/', "."); 111 | image::load_from_memory_with_format( 112 | &data, 113 | ImageFormat::from_path(mime_type).unwrap(), 114 | ) 115 | .unwrap() 116 | } else { 117 | let path = self.base_dir.join(uri); 118 | image::open(path).unwrap() 119 | } 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/gltf/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod gltf_data; 2 | 3 | use std::sync::{Arc, RwLock}; 4 | 5 | use glam::Mat4; 6 | pub use gltf_data::GltfData; 7 | 8 | use gltf::scene::Transform; 9 | use image::GrayImage; 10 | use itertools::Itertools; 11 | use rayon::prelude::*; 12 | 13 | pub fn transform_to_matrix(transform: Transform) -> Mat4 { 14 | Mat4::from_cols_array_2d(&transform.matrix()) 15 | } 16 | 17 | /// Get all rgb images from the glTF document. 18 | /// rgb textures are: 19 | /// - normal textures 20 | /// - emissive textures 21 | pub fn get_rgb_textures(doc: &gltf::Document) -> Vec { 22 | let normal_textures = doc 23 | .materials() 24 | .filter_map(|mat| mat.normal_texture()) 25 | .map(|normal| normal.texture()); 26 | let emissive_textures = doc 27 | .materials() 28 | .filter_map(|mat| mat.emissive_texture()) 29 | .map(|emissive| emissive.texture()); 30 | 31 | // append normal and emissives 32 | normal_textures 33 | .chain(emissive_textures) 34 | .unique_by(gltf::Texture::index) 35 | .collect() 36 | } 37 | 38 | /// Load all rgb images from the glTF document. 39 | pub fn load_rgb_images(doc: &gltf::Document, data: &Arc>) { 40 | // Load rgb images 41 | get_rgb_textures(doc).par_iter().for_each(|tex| { 42 | let texture = { 43 | let data = data.read().unwrap(); 44 | data.load_texture(tex) 45 | }; 46 | 47 | let texture = Arc::new(texture.to_rgb8()); 48 | data.write() 49 | .unwrap() 50 | .rgb_images 51 | .insert(tex.index(), texture); 52 | }); 53 | } 54 | 55 | /// Get all rgba images from the glTF document. 56 | /// rgba textures are: 57 | /// - base color textures 58 | pub fn get_rgba_textures(doc: &gltf::Document) -> Vec { 59 | doc.materials() 60 | .filter_map(|mat| mat.pbr_metallic_roughness().base_color_texture()) 61 | .map(|info| info.texture()) 62 | .collect() 63 | } 64 | 65 | /// Load all rgba images from the glTF document. 66 | pub fn load_rbga_images(doc: &gltf::Document, data: &Arc>) { 67 | // Load rgb images 68 | get_rgba_textures(doc).par_iter().for_each(|tex| { 69 | let texture = { 70 | let data = data.read().unwrap(); 71 | data.load_texture(tex) 72 | }; 73 | 74 | let texture = Arc::new(texture.to_rgba8()); 75 | data.write() 76 | .unwrap() 77 | .rgba_images 78 | .insert(tex.index(), texture); 79 | }); 80 | } 81 | 82 | /// Get all rgba images from the glTF document. 83 | /// rgba textures are: 84 | /// - occlusion textures 85 | /// - metallic roughness textures 86 | pub fn get_gray_textures(doc: &gltf::Document) -> Vec { 87 | let occlusion_textures = doc 88 | .materials() 89 | .filter_map(|mat| mat.occlusion_texture()) 90 | .map(|normal| normal.texture()); 91 | let metallic_roughness_textures = doc 92 | .materials() 93 | .filter_map(|mat| mat.pbr_metallic_roughness().metallic_roughness_texture()) 94 | .map(|emissive| emissive.texture()); 95 | 96 | // Only keep unique textures indices. 97 | occlusion_textures 98 | .chain(metallic_roughness_textures) 99 | .unique_by(gltf::Texture::index) 100 | .collect() 101 | } 102 | 103 | /// Load all rgba images from the glTF document. 104 | pub fn load_gray_images(doc: &gltf::Document, data: &Arc>) { 105 | // Load rgb images 106 | get_gray_textures(doc).par_iter().for_each(|tex| { 107 | let texture = { 108 | let data = data.read().unwrap(); 109 | data.load_texture(tex) 110 | }; 111 | 112 | let img = texture.to_rgba8(); 113 | for channel in 0..4 { 114 | let mut extract_img = GrayImage::new(img.width(), img.height()); 115 | for (x, y, px) in img.enumerate_pixels() { 116 | extract_img[(x, y)][0] = px[channel]; 117 | } 118 | 119 | let img = Arc::new(extract_img); 120 | 121 | data.write() 122 | .unwrap() 123 | .gray_images 124 | .insert((tex.index(), channel), img); 125 | } 126 | }); 127 | } 128 | 129 | /// Load all images from the glTF document. 130 | pub fn load_all_images(doc: &gltf::Document, data: &Arc>) { 131 | load_rgb_images(doc, data); 132 | load_rbga_images(doc, data); 133 | load_gray_images(doc, data); 134 | } 135 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Client application for the `mesh_to_sdf` project. 2 | //! 3 | //! ![example](https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/main/client.gif) 4 | //! 5 | //! A visualization client for the SDF of a mesh. 6 | //! Generates a SDF from a mesh and renders it. 7 | mod camera; 8 | mod camera_control; 9 | mod cubemap; 10 | mod frame_rate; 11 | mod gltf; 12 | mod passes; 13 | mod pbr; 14 | mod reload_flags; 15 | mod runner; 16 | mod sdf; 17 | mod sdf_program; 18 | mod texture; 19 | mod utility; 20 | 21 | use std::sync::{Arc, Mutex}; 22 | 23 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 24 | use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; 25 | 26 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 27 | use std::path::Path; 28 | 29 | /// App entry point. 30 | fn main() { 31 | let data = Arc::new(Mutex::new(crate::reload_flags::ReloadFlags { 32 | shaders: vec![], 33 | })); 34 | 35 | // Watch shaders folder. 36 | // When a shader is saved, the pipelines will be recreated. 37 | // Only enabled in native debug mode. 38 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 39 | { 40 | let data = Arc::clone(&data); 41 | std::thread::spawn(move || { 42 | // first try to watch the mesh_to_sdf_client/shaders folder (runnin from the root of the project) 43 | // if that fails, try to watch the shaders folder (running from the client folder) 44 | let paths = ["mesh_to_sdf_client/shaders", "shaders"]; 45 | for path in paths { 46 | log::info!("Watching {path}"); 47 | if let Err(error) = watch(path, &data) { 48 | log::error!("Could not watch shaders folder: {error:?}"); 49 | } 50 | } 51 | }); 52 | } 53 | 54 | runner::start_app(data); 55 | } 56 | 57 | /// Watch shader folder. Only done in native debug mode. 58 | /// Everytime a shader is modified/added/deleted, 59 | /// it will update the `ReloadFlags` so the program can reload them. 60 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 61 | fn watch>( 62 | path: P, 63 | data: &Arc>, 64 | ) -> notify::Result<()> { 65 | let (tx, rx) = std::sync::mpsc::channel(); 66 | 67 | // Automatically select the best implementation for your platform. 68 | // You can also access each implementation directly e.g. INotifyWatcher. 69 | let mut watcher = RecommendedWatcher::new(tx, Config::default())?; 70 | 71 | // Add a path to be watched. All files and directories at that path and 72 | // below will be monitored for changes. 73 | watcher.watch(path.as_ref(), RecursiveMode::Recursive)?; 74 | 75 | for res in rx { 76 | match res { 77 | Ok(event) => { 78 | log::info!("Change: {:?}", event.paths); 79 | let mut data = data.lock().unwrap(); 80 | event.paths.iter().for_each(|p| { 81 | let shader_path = p.to_str().unwrap().to_owned(); 82 | data.shaders.push(shader_path); 83 | }); 84 | } 85 | Err(error) => log::error!("Error: {error:?}"), 86 | } 87 | } 88 | 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/passes/mip_generation_pass.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::utility::shader_builder::ShaderBuilder; 4 | 5 | #[derive(Debug)] 6 | pub struct MipGenerationPass { 7 | pub pipeline: wgpu::RenderPipeline, 8 | pub sampler: wgpu::Sampler, 9 | } 10 | 11 | impl MipGenerationPass { 12 | pub fn create_pipeline(device: &wgpu::Device) -> Result { 13 | let shader = ShaderBuilder::create_module(device, "utility/mipmap_generation.wgsl")?; 14 | 15 | let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 16 | label: Some("blit"), 17 | layout: None, 18 | vertex: wgpu::VertexState { 19 | module: &shader, 20 | entry_point: "vs_main", 21 | buffers: &[], 22 | compilation_options: wgpu::PipelineCompilationOptions::default(), 23 | }, 24 | fragment: Some(wgpu::FragmentState { 25 | module: &shader, 26 | entry_point: "fs_main", 27 | targets: &[Some(wgpu::TextureFormat::Rgba8UnormSrgb.into())], 28 | compilation_options: wgpu::PipelineCompilationOptions::default(), 29 | }), 30 | primitive: wgpu::PrimitiveState { 31 | topology: wgpu::PrimitiveTopology::TriangleList, 32 | ..Default::default() 33 | }, 34 | depth_stencil: None, 35 | multisample: wgpu::MultisampleState::default(), 36 | multiview: None, 37 | }); 38 | 39 | Ok(pipeline) 40 | } 41 | 42 | pub fn update_pipeline(&mut self, device: &wgpu::Device) -> Result<()> { 43 | self.pipeline = Self::create_pipeline(device)?; 44 | Ok(()) 45 | } 46 | 47 | pub fn new(device: &wgpu::Device) -> Result { 48 | let pipeline = Self::create_pipeline(device)?; 49 | 50 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 51 | label: Some("mip"), 52 | address_mode_u: wgpu::AddressMode::ClampToEdge, 53 | address_mode_v: wgpu::AddressMode::ClampToEdge, 54 | address_mode_w: wgpu::AddressMode::ClampToEdge, 55 | mag_filter: wgpu::FilterMode::Linear, 56 | min_filter: wgpu::FilterMode::Linear, 57 | mipmap_filter: wgpu::FilterMode::Nearest, 58 | ..Default::default() 59 | }); 60 | 61 | Ok(Self { pipeline, sampler }) 62 | } 63 | 64 | pub fn run( 65 | &self, 66 | device: &wgpu::Device, 67 | encoder: &mut wgpu::CommandEncoder, 68 | from_view: &wgpu::TextureView, 69 | to_view: &wgpu::TextureView, 70 | ) { 71 | let bind_group_layout = self.pipeline.get_bind_group_layout(0); 72 | 73 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 74 | layout: &bind_group_layout, 75 | entries: &[ 76 | wgpu::BindGroupEntry { 77 | binding: 0, 78 | resource: wgpu::BindingResource::TextureView(from_view), 79 | }, 80 | wgpu::BindGroupEntry { 81 | binding: 1, 82 | resource: wgpu::BindingResource::Sampler(&self.sampler), 83 | }, 84 | ], 85 | label: Some("generate_mipmaps::bind_group"), 86 | }); 87 | 88 | let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 89 | label: Some("generate_mipmaps::rpass"), 90 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 91 | view: to_view, 92 | resolve_target: None, 93 | ops: wgpu::Operations { 94 | load: wgpu::LoadOp::Load, 95 | store: wgpu::StoreOp::Store, 96 | }, 97 | })], 98 | depth_stencil_attachment: None, 99 | timestamp_writes: None, 100 | occlusion_query_set: None, 101 | }); 102 | rpass.set_pipeline(&self.pipeline); 103 | rpass.set_bind_group(0, &bind_group, &[]); 104 | rpass.draw(0..3, 0..1); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/passes/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains all the passes used in the renderer. 2 | pub mod cubemap_generation_pass; 3 | pub mod mip_generation_pass; 4 | pub mod model_render_pass; 5 | pub mod raymarch_pass; 6 | pub mod sdf_render_pass; 7 | pub mod shadow_pass; 8 | pub mod voxel_render_pass; 9 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/passes/sdf_render_pass.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::camera::CameraData; 4 | use crate::sdf::{Sdf, SdfUniforms}; 5 | use crate::texture::Texture; 6 | use crate::utility::shader_builder::ShaderBuilder; 7 | 8 | pub struct SdfRenderPass { 9 | pub render_pipeline: wgpu::RenderPipeline, 10 | pub sdf_bind_group_layout: wgpu::BindGroupLayout, 11 | } 12 | 13 | impl SdfRenderPass { 14 | fn create_pipeline( 15 | device: &wgpu::Device, 16 | view_format: wgpu::TextureFormat, 17 | camera: &CameraData, 18 | sdf_bind_group_layout: &wgpu::BindGroupLayout, 19 | settings_bind_group_layout: &wgpu::BindGroupLayout, 20 | ) -> Result { 21 | let draw_shader = ShaderBuilder::create_module(device, "draw_sdf.wgsl")?; 22 | 23 | let render_pipeline_layout = 24 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 25 | label: Some("SdfRenderPass::PipelineLayout"), 26 | bind_group_layouts: &[ 27 | sdf_bind_group_layout, 28 | &camera.bind_group_layout, 29 | settings_bind_group_layout, 30 | ], 31 | push_constant_ranges: &[], 32 | }); 33 | 34 | let primitive = wgpu::PrimitiveState { 35 | topology: wgpu::PrimitiveTopology::TriangleList, 36 | strip_index_format: None, 37 | front_face: wgpu::FrontFace::Ccw, 38 | cull_mode: None, 39 | polygon_mode: wgpu::PolygonMode::Fill, 40 | unclipped_depth: false, 41 | conservative: false, 42 | }; 43 | 44 | ShaderBuilder::create_render_pipeline( 45 | device, 46 | &wgpu::RenderPipelineDescriptor { 47 | label: Some("SdfRenderPass::RenderPipeline"), 48 | layout: Some(&render_pipeline_layout), 49 | vertex: wgpu::VertexState { 50 | module: &draw_shader, 51 | entry_point: "main_vs", 52 | buffers: &[], 53 | compilation_options: wgpu::PipelineCompilationOptions::default(), 54 | }, 55 | fragment: Some(wgpu::FragmentState { 56 | module: &draw_shader, 57 | entry_point: "main_fs", 58 | targets: &[Some(view_format.into())], 59 | compilation_options: wgpu::PipelineCompilationOptions::default(), 60 | }), 61 | primitive, 62 | depth_stencil: Some(wgpu::DepthStencilState { 63 | format: Texture::DEPTH_FORMAT, 64 | depth_write_enabled: true, 65 | depth_compare: wgpu::CompareFunction::Greater, 66 | stencil: wgpu::StencilState::default(), 67 | bias: wgpu::DepthBiasState::default(), 68 | }), 69 | multisample: wgpu::MultisampleState::default(), 70 | multiview: None, 71 | }, 72 | ) 73 | } 74 | 75 | pub fn get_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { 76 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 77 | label: Some("Sdf Bind Group Layout"), 78 | entries: &[ 79 | wgpu::BindGroupLayoutEntry { 80 | binding: 0, 81 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 82 | ty: wgpu::BindingType::Buffer { 83 | ty: wgpu::BufferBindingType::Uniform, 84 | has_dynamic_offset: false, 85 | min_binding_size: wgpu::BufferSize::new( 86 | core::mem::size_of::() as _, 87 | ), 88 | }, 89 | count: None, 90 | }, 91 | wgpu::BindGroupLayoutEntry { 92 | binding: 1, 93 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 94 | ty: wgpu::BindingType::Buffer { 95 | ty: wgpu::BufferBindingType::Storage { read_only: true }, 96 | has_dynamic_offset: false, 97 | min_binding_size: None, 98 | }, 99 | count: None, 100 | }, 101 | wgpu::BindGroupLayoutEntry { 102 | binding: 2, 103 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 104 | ty: wgpu::BindingType::Buffer { 105 | ty: wgpu::BufferBindingType::Storage { read_only: true }, 106 | has_dynamic_offset: false, 107 | min_binding_size: None, 108 | }, 109 | count: None, 110 | }, 111 | ], 112 | }) 113 | } 114 | 115 | pub fn update_pipeline( 116 | &mut self, 117 | device: &wgpu::Device, 118 | view_format: wgpu::TextureFormat, 119 | camera: &CameraData, 120 | settings_bind_group_layout: &wgpu::BindGroupLayout, 121 | ) -> Result<()> { 122 | self.render_pipeline = Self::create_pipeline( 123 | device, 124 | view_format, 125 | camera, 126 | &self.sdf_bind_group_layout, 127 | settings_bind_group_layout, 128 | )?; 129 | Ok(()) 130 | } 131 | pub fn new( 132 | device: &wgpu::Device, 133 | view_format: wgpu::TextureFormat, 134 | camera: &CameraData, 135 | settings_bind_group_layout: &wgpu::BindGroupLayout, 136 | ) -> Result { 137 | let sdf_bind_group_layout = Self::get_bind_group_layout(device); 138 | 139 | let render_pipeline = Self::create_pipeline( 140 | device, 141 | view_format, 142 | camera, 143 | &sdf_bind_group_layout, 144 | settings_bind_group_layout, 145 | )?; 146 | 147 | Ok(Self { 148 | render_pipeline, 149 | sdf_bind_group_layout, 150 | }) 151 | } 152 | 153 | pub fn run( 154 | &self, 155 | command_encoder: &mut wgpu::CommandEncoder, 156 | view: &wgpu::TextureView, 157 | depth_map: &Texture, 158 | camera: &CameraData, 159 | sdf: &Sdf, 160 | settings_bind_group: &wgpu::BindGroup, 161 | ) { 162 | // need to draw it each frame to update depth map. 163 | command_encoder.push_debug_group("render particles"); 164 | { 165 | let render_pass_descriptor = wgpu::RenderPassDescriptor { 166 | label: Some("SdfRenderPass::run::render_pass_descriptor"), 167 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 168 | view, 169 | resolve_target: None, 170 | ops: wgpu::Operations { 171 | load: wgpu::LoadOp::Load, 172 | store: wgpu::StoreOp::Store, 173 | }, 174 | })], 175 | depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 176 | view: &depth_map.view, 177 | depth_ops: Some(wgpu::Operations { 178 | load: wgpu::LoadOp::Load, 179 | store: wgpu::StoreOp::Store, 180 | }), 181 | stencil_ops: None, 182 | }), 183 | timestamp_writes: None, 184 | occlusion_query_set: None, 185 | }; 186 | 187 | // render pass 188 | let mut rpass = command_encoder.begin_render_pass(&render_pass_descriptor); 189 | rpass.set_pipeline(&self.render_pipeline); 190 | rpass.set_bind_group(0, &sdf.bind_group, &[]); 191 | rpass.set_bind_group(1, &camera.bind_group, &[]); 192 | rpass.set_bind_group(2, settings_bind_group, &[]); 193 | // vertices are computed in the shader directly to save bandwidth 194 | rpass.draw(0..3, 0..sdf.get_cell_count()); 195 | } 196 | command_encoder.pop_debug_group(); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/passes/shadow_pass.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::pbr::mesh::MeshVertex; 4 | use crate::pbr::model::Model; 5 | use crate::pbr::model_instance::ModelInstance; 6 | use crate::texture::Texture; 7 | use crate::utility::shader_builder::ShaderBuilder; 8 | 9 | use crate::pbr::shadow_map::ShadowMap; 10 | 11 | use crate::passes::model_render_pass::ModelRenderPass; 12 | 13 | pub struct ShadowPass { 14 | pub render_pipeline: wgpu::RenderPipeline, 15 | pub map: ShadowMap, 16 | } 17 | 18 | impl ShadowPass { 19 | fn create_pipeline( 20 | device: &wgpu::Device, 21 | shadow_map: &ShadowMap, 22 | ) -> Result { 23 | let draw_shader = ShaderBuilder::create_module(device, "draw_model.wgsl")?; 24 | 25 | let model_bind_group_layout = ModelRenderPass::create_model_bind_group_layout(device); 26 | 27 | let render_pipeline_layout = 28 | device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 29 | label: Some("shadow map"), 30 | bind_group_layouts: &[ 31 | &model_bind_group_layout, 32 | &shadow_map.light.bind_group_layout, 33 | ], 34 | push_constant_ranges: &[], 35 | }); 36 | 37 | ShaderBuilder::create_render_pipeline( 38 | device, 39 | &wgpu::RenderPipelineDescriptor { 40 | label: Some("shadow pass"), 41 | layout: Some(&render_pipeline_layout), 42 | vertex: wgpu::VertexState { 43 | module: &draw_shader, 44 | entry_point: "main_vs", 45 | buffers: &[MeshVertex::desc()], 46 | compilation_options: wgpu::PipelineCompilationOptions::default(), 47 | }, 48 | fragment: None, 49 | primitive: wgpu::PrimitiveState::default(), 50 | depth_stencil: Some(wgpu::DepthStencilState { 51 | format: Texture::DEPTH_FORMAT, 52 | depth_write_enabled: true, 53 | depth_compare: wgpu::CompareFunction::Greater, 54 | stencil: wgpu::StencilState::default(), 55 | bias: wgpu::DepthBiasState::default(), 56 | }), 57 | multisample: wgpu::MultisampleState::default(), 58 | multiview: None, 59 | }, 60 | ) 61 | } 62 | 63 | pub fn update_pipeline(&mut self, device: &wgpu::Device) -> Result<()> { 64 | self.render_pipeline = Self::create_pipeline(device, &self.map)?; 65 | Ok(()) 66 | } 67 | 68 | pub fn new(device: &wgpu::Device, shadow_map: ShadowMap) -> Result { 69 | Ok(Self { 70 | render_pipeline: Self::create_pipeline(device, &shadow_map)?, 71 | map: shadow_map, 72 | }) 73 | } 74 | 75 | pub fn run( 76 | &self, 77 | command_encoder: &mut wgpu::CommandEncoder, 78 | model: &Model, 79 | model_instance: &ModelInstance, 80 | ) { 81 | let render_pass_descriptor = wgpu::RenderPassDescriptor { 82 | label: Some("ShadowPass::run::render_pass_descriptor"), 83 | color_attachments: &[], 84 | depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { 85 | view: &self.map.texture.view, 86 | depth_ops: Some(wgpu::Operations { 87 | load: wgpu::LoadOp::Load, 88 | store: wgpu::StoreOp::Store, 89 | }), 90 | stencil_ops: None, 91 | }), 92 | timestamp_writes: None, 93 | occlusion_query_set: None, 94 | }; 95 | 96 | let mut rpass = command_encoder.begin_render_pass(&render_pass_descriptor); 97 | rpass.set_pipeline(&self.render_pipeline); 98 | rpass.set_bind_group(0, &model_instance.transform_bind_group, &[]); 99 | rpass.set_bind_group(1, &self.map.light.bind_group, &[]); 100 | rpass.set_vertex_buffer(0, model.mesh.vertex_buffer.slice(..)); 101 | rpass.set_index_buffer(model.mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); 102 | rpass.draw_indexed(0..model.mesh.index_count, 0, 0..1); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/pbr/mesh.rs: -------------------------------------------------------------------------------- 1 | use wgpu::util::DeviceExt; 2 | 3 | pub mod primitives; 4 | 5 | #[repr(C)] 6 | #[derive(Default, Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] 7 | pub struct MeshVertex { 8 | pub position: [f32; 3], 9 | pub normal: [f32; 3], 10 | pub tex_coords: [f32; 2], 11 | } 12 | 13 | impl MeshVertex { 14 | const ATTRIBUTES: [wgpu::VertexAttribute; 3] = 15 | wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x2]; 16 | 17 | pub const fn desc() -> wgpu::VertexBufferLayout<'static> { 18 | wgpu::VertexBufferLayout { 19 | array_stride: 8 * 4, 20 | step_mode: wgpu::VertexStepMode::Vertex, 21 | attributes: &Self::ATTRIBUTES, 22 | } 23 | } 24 | } 25 | 26 | pub struct Mesh { 27 | pub vertices: Vec, 28 | pub indices: Vec, 29 | 30 | pub vertex_buffer: wgpu::Buffer, 31 | pub index_buffer: wgpu::Buffer, 32 | 33 | pub index_count: u32, 34 | } 35 | 36 | impl Mesh { 37 | pub fn new(device: &wgpu::Device, vertices: Vec, indices: Vec) -> Self { 38 | let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 39 | label: Some("Voxel Vertex Buffer"), 40 | contents: bytemuck::cast_slice(&vertices), 41 | usage: wgpu::BufferUsages::VERTEX, 42 | }); 43 | 44 | let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 45 | label: Some("Voxel Index Buffer"), 46 | contents: bytemuck::cast_slice(&indices), 47 | usage: wgpu::BufferUsages::INDEX, 48 | }); 49 | 50 | let index_count = indices.len() as u32; 51 | Self { 52 | vertices, 53 | indices, 54 | 55 | vertex_buffer, 56 | index_buffer, 57 | 58 | index_count, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/pbr/mesh/primitives.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | use super::{Mesh, MeshVertex}; 4 | 5 | pub fn create_box(device: &wgpu::Device) -> Mesh { 6 | // Draw box. 7 | let positions = [ 8 | // front 9 | [-1.0, -1.0, 1.0], 10 | [1.0, -1.0, 1.0], 11 | [-1.0, 1.0, 1.0], 12 | [1.0, 1.0, 1.0], 13 | // back 14 | [-1.0, -1.0, -1.0], 15 | [1.0, -1.0, -1.0], 16 | [-1.0, 1.0, -1.0], 17 | [1.0, 1.0, -1.0], 18 | // left 19 | [-1.0, -1.0, -1.0], 20 | [-1.0, -1.0, 1.0], 21 | [-1.0, 1.0, -1.0], 22 | [-1.0, 1.0, 1.0], 23 | // right 24 | [1.0, -1.0, -1.0], 25 | [1.0, -1.0, 1.0], 26 | [1.0, 1.0, -1.0], 27 | [1.0, 1.0, 1.0], 28 | // top 29 | [-1.0, 1.0, -1.0], 30 | [1.0, 1.0, -1.0], 31 | [-1.0, 1.0, 1.0], 32 | [1.0, 1.0, 1.0], 33 | // bottom 34 | [-1.0, -1.0, -1.0], 35 | [1.0, -1.0, -1.0], 36 | [-1.0, -1.0, 1.0], 37 | [1.0, -1.0, 1.0], 38 | ]; 39 | let normals = [ 40 | [0.0, 0.0, 1.0], // front 41 | [0.0, 0.0, -1.0], // back 42 | [-1.0, 0.0, 0.0], // left 43 | [1.0, 0.0, 0.0], // right 44 | [0.0, 1.0, 0.0], // top 45 | [0.0, -1.0, 0.0], // bottom 46 | ]; 47 | 48 | let mut indices: Vec = vec![]; 49 | #[allow(clippy::identity_op)] 50 | for face in 0..6 { 51 | indices.append(&mut vec![face * 4 + 2, face * 4 + 1, face * 4 + 3]); 52 | indices.append(&mut vec![face * 4 + 0, face * 4 + 1, face * 4 + 2]); 53 | } 54 | 55 | let vertices = positions 56 | .into_iter() 57 | .zip(normals.iter().flat_map(|&n| core::iter::repeat(n).take(4))) 58 | .map(|(position, normal)| MeshVertex { 59 | position, 60 | normal, 61 | tex_coords: [0.0, 0.0], 62 | }) 63 | .collect_vec(); 64 | 65 | Mesh::new(device, vertices, indices) 66 | } 67 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/pbr/mod.rs: -------------------------------------------------------------------------------- 1 | //! While the name is PBR, it's not really PBR at-the-moment. 2 | //! It's just a simple model renderer, with albedo, blinn-phong shading, specular and shadows. 3 | pub mod mesh; 4 | pub mod model; 5 | pub mod model_instance; 6 | pub mod shadow_map; 7 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/pbr/model.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | use crate::{ 4 | gltf::{GltfModel, Material}, 5 | texture::Texture, 6 | }; 7 | 8 | use super::mesh::{Mesh, MeshVertex}; 9 | use crate::passes::model_render_pass::ModelRenderPass; 10 | 11 | pub struct Model { 12 | pub name: Option, 13 | 14 | pub mesh: Mesh, 15 | 16 | // TODO Material struct. 17 | pub albedo: Texture, 18 | 19 | pub textures_bind_group: wgpu::BindGroup, 20 | } 21 | 22 | impl Model { 23 | pub fn from_gtlf( 24 | device: &wgpu::Device, 25 | queue: &wgpu::Queue, 26 | model: &GltfModel, 27 | material: Option<&Material>, 28 | ) -> Self { 29 | let indices = model 30 | .indices() 31 | .cloned() 32 | .unwrap_or_else(|| (0..(model.vertices().len() as u32)).collect()); 33 | let vertices = model 34 | .vertices() 35 | .iter() 36 | .map(|v| MeshVertex { 37 | position: [v.position.x, v.position.y, v.position.z], 38 | normal: [v.normal.x, v.normal.y, v.normal.z], 39 | tex_coords: [v.tex_coords.x, v.tex_coords.y], 40 | }) 41 | .collect_vec(); 42 | 43 | println!("model: {:?}", model.mesh_name()); 44 | 45 | let mesh = Mesh::new(device, vertices, indices); 46 | 47 | // If no albedo is present, render as grey. 48 | let grey_albedo: image::ImageBuffer, std::vec::Vec> = 49 | image::ImageBuffer::from_pixel(2, 2, image::Rgba([128, 128, 128, 255])); 50 | 51 | let albedo = Texture::from_image( 52 | device, 53 | queue, 54 | material.map_or(&grey_albedo, |material| { 55 | material 56 | .pbr 57 | .base_color_texture 58 | .as_ref() 59 | .map_or(&grey_albedo, |x| x.as_ref()) 60 | }), 61 | Some("albedo"), 62 | ); 63 | 64 | let textures_bind_group_layout = ModelRenderPass::create_textures_bind_group_layout(device); 65 | 66 | let textures_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 67 | layout: &textures_bind_group_layout, 68 | entries: &[ 69 | wgpu::BindGroupEntry { 70 | binding: 0, 71 | resource: wgpu::BindingResource::TextureView(&albedo.view), 72 | }, 73 | wgpu::BindGroupEntry { 74 | binding: 1, 75 | resource: wgpu::BindingResource::Sampler(&albedo.sampler), 76 | }, 77 | ], 78 | label: Some("Model Bind group"), 79 | }); 80 | 81 | Self { 82 | name: model.mesh_name().map(std::borrow::ToOwned::to_owned), 83 | mesh, 84 | albedo, 85 | textures_bind_group, 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/pbr/model_instance.rs: -------------------------------------------------------------------------------- 1 | use wgpu::util::DeviceExt; 2 | 3 | use crate::passes::model_render_pass::ModelRenderPass; 4 | 5 | pub struct ModelInstance { 6 | pub model_id: usize, 7 | pub transform: glam::Mat4, 8 | pub transform_buffer: wgpu::Buffer, 9 | pub transform_bind_group: wgpu::BindGroup, 10 | pub transform_bind_group_layout: wgpu::BindGroupLayout, 11 | } 12 | 13 | impl ModelInstance { 14 | pub fn new(device: &wgpu::Device, model_id: usize, transform: glam::Mat4) -> Self { 15 | let transform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 16 | label: Some("Transform Buffer"), 17 | contents: bytemuck::cast_slice(&[transform]), 18 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 19 | }); 20 | 21 | let transform_bind_group_layout = ModelRenderPass::create_model_bind_group_layout(device); 22 | 23 | let transform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 24 | layout: &transform_bind_group_layout, 25 | entries: &[wgpu::BindGroupEntry { 26 | binding: 0, 27 | resource: transform_buffer.as_entire_binding(), 28 | }], 29 | label: Some("Model::Transform bind group"), 30 | }); 31 | 32 | Self { 33 | model_id, 34 | transform, 35 | transform_buffer, 36 | transform_bind_group, 37 | transform_bind_group_layout, 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/pbr/shadow_map.rs: -------------------------------------------------------------------------------- 1 | use wgpu::util::DeviceExt; 2 | use wgpu::Extent3d; 3 | 4 | use crate::camera::{Camera, CameraData, CameraUniform}; 5 | use crate::camera_control::CameraLookAt; 6 | use crate::texture::Texture; 7 | 8 | pub struct ShadowMap { 9 | pub light: CameraData, 10 | pub texture: Texture, 11 | } 12 | 13 | impl ShadowMap { 14 | pub fn new(device: &wgpu::Device) -> Self { 15 | let camera = Camera { 16 | look_at: CameraLookAt { 17 | distance: 24.0, 18 | latitude: 0.85, 19 | longitude: 6.10, 20 | ..Default::default() 21 | }, 22 | aspect: 800.0 / 600.0, 23 | fovy: 45.0, 24 | znear: 0.01, 25 | }; 26 | 27 | let camera_uniform = CameraUniform::from_camera(&camera); 28 | 29 | let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 30 | label: Some("Shadow Map Buffer"), 31 | contents: bytemuck::cast_slice(&[camera_uniform]), 32 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 33 | }); 34 | 35 | let camera_bind_group_layout = 36 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 37 | entries: &[wgpu::BindGroupLayoutEntry { 38 | binding: 0, 39 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 40 | ty: wgpu::BindingType::Buffer { 41 | ty: wgpu::BufferBindingType::Uniform, 42 | has_dynamic_offset: false, 43 | min_binding_size: wgpu::BufferSize::new(camera_buffer.size()), 44 | }, 45 | count: None, 46 | }], 47 | label: Some("camera_bind_group_layout"), 48 | }); 49 | 50 | let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 51 | layout: &camera_bind_group_layout, 52 | entries: &[wgpu::BindGroupEntry { 53 | binding: 0, 54 | resource: camera_buffer.as_entire_binding(), 55 | }], 56 | label: Some("camera_bind_group"), 57 | }); 58 | 59 | let camera_data = CameraData { 60 | camera, 61 | uniform: camera_uniform, 62 | buffer: camera_buffer, 63 | bind_group: camera_bind_group, 64 | bind_group_layout: camera_bind_group_layout, 65 | }; 66 | 67 | Self { 68 | light: camera_data, 69 | texture: Texture::create_depth_texture( 70 | device, 71 | Extent3d { 72 | width: 1024, 73 | height: 1024, 74 | depth_or_array_layers: 1, 75 | }, 76 | "shadow_map", 77 | ), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/reload_flags.rs: -------------------------------------------------------------------------------- 1 | /// Reload flags contain the state of the library / shader folder 2 | /// `shaders` contains the shaders that were updated until last rebuild 3 | /// `lib` is the state of the library 4 | #[derive(Debug)] 5 | pub struct ReloadFlags { 6 | pub shaders: Vec, 7 | } 8 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/sdf.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use mesh_to_sdf::SignMethod; 3 | use rayon::slice::ParallelSliceMut; 4 | use wgpu::util::DeviceExt; 5 | 6 | use crate::passes::sdf_render_pass::SdfRenderPass; 7 | 8 | #[repr(C)] 9 | #[derive(Default, Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] 10 | pub struct SdfUniforms { 11 | pub start: [f32; 4], 12 | pub end: [f32; 4], 13 | pub cell_size: [f32; 4], 14 | pub cell_count: [u32; 4], 15 | } 16 | 17 | #[derive(Debug)] 18 | pub struct Sdf { 19 | pub uniforms: SdfUniforms, 20 | pub uniforms_buffer: wgpu::Buffer, 21 | pub data: Vec, 22 | pub data_buffer: wgpu::Buffer, 23 | pub ordered_indices: Vec, 24 | pub ordered_buffer: wgpu::Buffer, 25 | 26 | pub grid: mesh_to_sdf::Grid, 27 | pub iso_limits: (f32, f32), 28 | 29 | pub bind_group: wgpu::BindGroup, 30 | pub time_taken: core::time::Duration, 31 | } 32 | 33 | impl Sdf { 34 | pub fn new( 35 | device: &wgpu::Device, 36 | vertices: &[glam::Vec3], 37 | indices: &[u32], 38 | start_cell: &glam::Vec3, 39 | end_cell: &glam::Vec3, 40 | cell_count: &[u32; 3], 41 | sign_method: SignMethod, 42 | ) -> Self { 43 | let ucell_count = [ 44 | cell_count[0] as usize, 45 | cell_count[1] as usize, 46 | cell_count[2] as usize, 47 | ]; 48 | let grid = mesh_to_sdf::Grid::from_bounding_box(start_cell, end_cell, ucell_count); 49 | 50 | let now = std::time::Instant::now(); 51 | let data = mesh_to_sdf::generate_grid_sdf( 52 | vertices, 53 | mesh_to_sdf::Topology::TriangleList(Some(indices)), 54 | &grid, 55 | sign_method, 56 | ); 57 | let time_taken = now.elapsed(); 58 | log::info!( 59 | "SDF generation took: {:.3}ms", 60 | time_taken.as_secs_f64() * 1000.0 61 | ); 62 | 63 | let now = std::time::Instant::now(); 64 | // sort cells by their distance to surface. 65 | // used in the voxel render pass to only draw valid cells. 66 | let mut ordered_indices = (0..data.len() as u32).collect_vec(); 67 | ordered_indices.par_sort_by(|i, j| data[*i as usize].total_cmp(&data[*j as usize])); 68 | log::info!( 69 | "voxel generation took: {:.3}ms", 70 | now.elapsed().as_secs_f64() * 1000.0 71 | ); 72 | 73 | let cell_size = grid.get_cell_size(); 74 | let first_cell = grid.get_first_cell(); 75 | let last_cell = grid.get_last_cell(); 76 | let uniforms = SdfUniforms { 77 | start: [first_cell[0], first_cell[1], first_cell[2], 0.0], 78 | end: [last_cell[0], last_cell[1], last_cell[2], 0.0], 79 | cell_size: [cell_size[0], cell_size[1], cell_size[2], 0.0], 80 | cell_count: [cell_count[0], cell_count[1], cell_count[2], 0], 81 | }; 82 | 83 | let uniforms_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 84 | label: Some("Sdf Uniforms Buffer"), 85 | contents: bytemuck::cast_slice(&[uniforms]), 86 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 87 | }); 88 | 89 | let data_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 90 | label: Some("Sdf Data Buffer"), 91 | contents: bytemuck::cast_slice(&data), 92 | usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, 93 | }); 94 | 95 | let ordered_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 96 | label: Some("Sdf Ordered Indices Buffer"), 97 | contents: bytemuck::cast_slice(&ordered_indices), 98 | usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, 99 | }); 100 | 101 | let bind_group_layout = SdfRenderPass::get_bind_group_layout(device); 102 | 103 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 104 | label: Some("Sdf Bind Group"), 105 | layout: &bind_group_layout, 106 | entries: &[ 107 | wgpu::BindGroupEntry { 108 | binding: 0, 109 | resource: uniforms_buffer.as_entire_binding(), 110 | }, 111 | wgpu::BindGroupEntry { 112 | binding: 1, 113 | resource: data_buffer.as_entire_binding(), 114 | }, 115 | wgpu::BindGroupEntry { 116 | binding: 2, 117 | resource: ordered_buffer.as_entire_binding(), 118 | }, 119 | ], 120 | }); 121 | 122 | let iso_limits = data.iter().copied().minmax().into_option().unwrap(); 123 | 124 | Self { 125 | uniforms, 126 | uniforms_buffer, 127 | data, 128 | data_buffer, 129 | ordered_indices, 130 | ordered_buffer, 131 | grid, 132 | iso_limits, 133 | bind_group, 134 | time_taken, 135 | } 136 | } 137 | 138 | pub const fn get_cell_count(&self) -> u32 { 139 | self.uniforms.cell_count[0] * self.uniforms.cell_count[1] * self.uniforms.cell_count[2] 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/sdf_program/command_stack.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use web_time::Instant; 4 | 5 | use super::{Parameters, Settings}; 6 | 7 | #[derive(Debug, Clone)] 8 | pub struct State { 9 | pub parameters: Parameters, 10 | pub settings: Settings, 11 | } 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct Command { 15 | pub old_state: State, 16 | pub new_state: State, 17 | } 18 | 19 | /// A poor-man's command stack. 20 | /// The label and timestamp are used to identify the command. 21 | /// When pushing a new command, it is compared to the last one. 22 | /// If the label are the same and the timestamp within a ten frame delta, the command is not pushed but updated. 23 | /// This is to avoid pushing a command for each drag. 24 | #[derive(Default)] 25 | pub struct CommandStack { 26 | /// Maximum number of commands to keep in the stack. 27 | stack_size: usize, 28 | /// Stack of commands that can be redone. 29 | redo_stack: VecDeque<(&'static str, Instant, Command)>, 30 | // Stack of commands that can be undone. 31 | undo_stack: VecDeque<(&'static str, Instant, Command)>, 32 | /// Current transaction, if any. 33 | /// A transaction is a command that is being edited or might be. 34 | /// It is not yet in the undo stack. 35 | current_transaction: Option<(&'static str, Instant, Command)>, 36 | } 37 | 38 | impl CommandStack { 39 | /// Create a new command stack. 40 | pub fn new(stack_size: usize) -> Self { 41 | Self { 42 | stack_size, 43 | redo_stack: VecDeque::with_capacity(stack_size), 44 | undo_stack: VecDeque::with_capacity(stack_size), 45 | current_transaction: None, 46 | } 47 | } 48 | 49 | /// Push a new command to the stack. 50 | /// If a `current_transaction` is present: 51 | /// - if the label are the same and timestamp is within 10 deltas, the transaction is updated. 52 | /// - otherwise, the transaction is pushed to the undo stack. 53 | /// and the current transaction is set to the new command. 54 | pub fn push(&mut self, label: &'static str, command: Command) { 55 | if let Some(transaction) = self.current_transaction.as_mut() { 56 | if transaction.0 == label && transaction.1.elapsed().as_secs_f32() < 10.0 / 60.0 { 57 | transaction.2.new_state = command.new_state; 58 | return; 59 | } 60 | 61 | self.redo_stack.clear(); 62 | self.undo_stack.push_back(transaction.clone()); 63 | self.current_transaction = None; 64 | } 65 | 66 | self.current_transaction = Some((label, Instant::now(), command)); 67 | } 68 | 69 | /// Undo the last command. Returns the command that was undone if any. 70 | pub fn undo(&mut self) -> Option { 71 | if self.current_transaction.is_some() { 72 | self.redo_stack.clear(); 73 | self.undo_stack 74 | .push_back(self.current_transaction.take().unwrap()); 75 | self.current_transaction = None; 76 | } 77 | 78 | if let Some(command) = self.undo_stack.pop_back() { 79 | self.redo_stack.push_back(command.clone()); 80 | Some(command.2) 81 | } else { 82 | None 83 | } 84 | } 85 | 86 | /// Redo the last command. Returns the command that was redone if any. 87 | pub fn redo(&mut self) -> Option { 88 | if self.current_transaction.is_some() { 89 | self.redo_stack.clear(); 90 | self.undo_stack 91 | .push_back(self.current_transaction.take().unwrap()); 92 | self.current_transaction = None; 93 | } 94 | 95 | if let Some(command) = self.redo_stack.pop_back() { 96 | self.undo_stack.push_back(command.clone()); 97 | Some(command.2) 98 | } else { 99 | None 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/texture.rs: -------------------------------------------------------------------------------- 1 | use crate::utility::mip_generation; 2 | 3 | #[expect(clippy::struct_field_names)] 4 | #[derive(Debug)] 5 | pub struct Texture { 6 | pub texture: wgpu::Texture, 7 | pub view: wgpu::TextureView, 8 | pub sampler: wgpu::Sampler, 9 | pub bind_group: wgpu::BindGroup, 10 | } 11 | 12 | impl Texture { 13 | pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; 14 | 15 | pub fn create_depth_texture(device: &wgpu::Device, size: wgpu::Extent3d, label: &str) -> Self { 16 | let desc = wgpu::TextureDescriptor { 17 | label: Some(label), 18 | size, 19 | mip_level_count: 1, 20 | sample_count: 1, 21 | dimension: wgpu::TextureDimension::D2, 22 | format: Self::DEPTH_FORMAT, 23 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, 24 | view_formats: &[], 25 | }; 26 | let texture = device.create_texture(&desc); 27 | 28 | let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); 29 | 30 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 31 | address_mode_u: wgpu::AddressMode::ClampToEdge, 32 | address_mode_v: wgpu::AddressMode::ClampToEdge, 33 | address_mode_w: wgpu::AddressMode::ClampToEdge, 34 | mag_filter: wgpu::FilterMode::Nearest, 35 | min_filter: wgpu::FilterMode::Nearest, 36 | mipmap_filter: wgpu::FilterMode::Nearest, 37 | compare: None, 38 | lod_min_clamp: 0.0, 39 | lod_max_clamp: 1.0, 40 | ..Default::default() 41 | }); 42 | 43 | let bind_group = Self::get_bind_group(device, &view, &sampler, &size, false); 44 | 45 | Self { 46 | texture, 47 | view, 48 | sampler, 49 | bind_group, 50 | } 51 | } 52 | 53 | pub fn create_render_target( 54 | device: &wgpu::Device, 55 | size: wgpu::Extent3d, 56 | label: Option<&str>, 57 | enable_mipmaps: bool, 58 | ) -> Self { 59 | let mip_level_count = if enable_mipmaps { 60 | size.max_mips(wgpu::TextureDimension::D2) 61 | } else { 62 | 1 63 | }; 64 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; 65 | let texture = device.create_texture(&wgpu::TextureDescriptor { 66 | label, 67 | size, 68 | mip_level_count, 69 | sample_count: 1, 70 | dimension: wgpu::TextureDimension::D2, 71 | format, 72 | usage: wgpu::TextureUsages::TEXTURE_BINDING 73 | | wgpu::TextureUsages::COPY_DST 74 | | wgpu::TextureUsages::COPY_SRC 75 | | wgpu::TextureUsages::RENDER_ATTACHMENT, 76 | view_formats: &[], 77 | }); 78 | 79 | let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); 80 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 81 | address_mode_u: wgpu::AddressMode::Repeat, 82 | address_mode_v: wgpu::AddressMode::Repeat, 83 | address_mode_w: wgpu::AddressMode::Repeat, 84 | mag_filter: wgpu::FilterMode::Linear, 85 | min_filter: wgpu::FilterMode::Linear, 86 | mipmap_filter: wgpu::FilterMode::Linear, 87 | ..Default::default() 88 | }); 89 | 90 | let bind_group = Self::get_bind_group(device, &view, &sampler, &size, true); 91 | 92 | Self { 93 | texture, 94 | view, 95 | sampler, 96 | bind_group, 97 | } 98 | } 99 | 100 | pub fn from_image( 101 | device: &wgpu::Device, 102 | queue: &wgpu::Queue, 103 | img: &image::ImageBuffer, std::vec::Vec>, 104 | label: Option<&str>, 105 | ) -> Self { 106 | let dimensions = img.dimensions(); 107 | 108 | let size = wgpu::Extent3d { 109 | width: dimensions.0, 110 | height: dimensions.1, 111 | depth_or_array_layers: 1, 112 | }; 113 | 114 | let texture = Self::create_render_target(device, size, label, true); 115 | 116 | queue.write_texture( 117 | wgpu::ImageCopyTexture { 118 | aspect: wgpu::TextureAspect::All, 119 | texture: &texture.texture, 120 | mip_level: 0, 121 | origin: wgpu::Origin3d::ZERO, 122 | }, 123 | img.as_ref(), 124 | wgpu::ImageDataLayout { 125 | offset: 0, 126 | bytes_per_row: Some(4 * dimensions.0), 127 | rows_per_image: Some(dimensions.1), 128 | }, 129 | size, 130 | ); 131 | 132 | let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { 133 | label: Some("compute_mips"), 134 | }); 135 | mip_generation::generate_mipmaps(device, &mut encoder, &texture.texture).unwrap(); 136 | queue.submit(Some(encoder.finish())); 137 | 138 | texture 139 | } 140 | 141 | pub fn get_bind_group_layout( 142 | device: &wgpu::Device, 143 | size: &wgpu::Extent3d, 144 | filterable: bool, 145 | ) -> wgpu::BindGroupLayout { 146 | let sample_binding_type = if filterable { 147 | wgpu::SamplerBindingType::Filtering 148 | } else { 149 | wgpu::SamplerBindingType::NonFiltering 150 | }; 151 | 152 | let view_dimension = if size.depth_or_array_layers > 1 { 153 | wgpu::TextureViewDimension::D2Array 154 | } else { 155 | wgpu::TextureViewDimension::D2 156 | }; 157 | 158 | device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 159 | entries: &[ 160 | wgpu::BindGroupLayoutEntry { 161 | binding: 0, 162 | visibility: wgpu::ShaderStages::FRAGMENT, 163 | ty: wgpu::BindingType::Texture { 164 | multisampled: false, 165 | view_dimension, 166 | sample_type: wgpu::TextureSampleType::Float { filterable }, 167 | }, 168 | count: None, 169 | }, 170 | wgpu::BindGroupLayoutEntry { 171 | binding: 1, 172 | visibility: wgpu::ShaderStages::FRAGMENT, 173 | ty: wgpu::BindingType::Sampler(sample_binding_type), 174 | count: None, 175 | }, 176 | ], 177 | label: Some("texture_bind_group_layout"), 178 | }) 179 | } 180 | 181 | pub fn get_bind_group( 182 | device: &wgpu::Device, 183 | view: &wgpu::TextureView, 184 | sampler: &wgpu::Sampler, 185 | size: &wgpu::Extent3d, 186 | filterable: bool, 187 | ) -> wgpu::BindGroup { 188 | device.create_bind_group(&wgpu::BindGroupDescriptor { 189 | layout: &Self::get_bind_group_layout(device, size, filterable), 190 | entries: &[ 191 | wgpu::BindGroupEntry { 192 | binding: 0, 193 | resource: wgpu::BindingResource::TextureView(view), 194 | }, 195 | wgpu::BindGroupEntry { 196 | binding: 1, 197 | resource: wgpu::BindingResource::Sampler(sampler), 198 | }, 199 | ], 200 | label: Some("texture_bind_group"), 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/utility/mip_generation.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::passes::mip_generation_pass::MipGenerationPass; 4 | 5 | pub fn generate_mipmaps( 6 | device: &wgpu::Device, 7 | encoder: &mut wgpu::CommandEncoder, 8 | texture: &wgpu::Texture, 9 | ) -> Result<()> { 10 | // TODO: only create the pass once. 11 | let pass = MipGenerationPass::new(device)?; 12 | 13 | let mip_count = texture.mip_level_count(); 14 | 15 | let layers = texture.depth_or_array_layers(); 16 | for layer in 0..layers { 17 | let views = (0..mip_count) 18 | .map(|mip| { 19 | texture.create_view(&wgpu::TextureViewDescriptor { 20 | label: Some("generate_mipmaps::mip_view"), 21 | format: None, 22 | dimension: None, 23 | aspect: wgpu::TextureAspect::All, 24 | base_mip_level: mip, 25 | mip_level_count: Some(1), 26 | base_array_layer: layer, 27 | array_layer_count: None, 28 | }) 29 | }) 30 | .collect::>(); 31 | 32 | for target_mip in 1..mip_count as usize { 33 | pass.run(device, encoder, &views[target_mip - 1], &views[target_mip]); 34 | } 35 | } 36 | 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/utility/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mip_generation; 2 | pub mod shader_builder; 3 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/src/utility/shader_builder.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use rust_embed::RustEmbed; 4 | 5 | use std::borrow::Cow; 6 | 7 | /// Shader helpers 8 | /// Will load from file in native debug mode to allow reloading at runtime 9 | /// and embed in binary in wasm/release mode. 10 | #[derive(RustEmbed)] 11 | #[folder = "shaders"] 12 | pub struct ShaderBuilder; 13 | 14 | impl ShaderBuilder { 15 | /// Load a shader file. 16 | /// Does not do any pre-processing here, but returns the raw content. 17 | pub fn load(name: &str) -> Result { 18 | // read file. 19 | Self::get(name) 20 | .ok_or_else(|| anyhow::anyhow!("Shader not found: {name}")) 21 | // Try parsing to utf8. 22 | .and_then(|file| { 23 | core::str::from_utf8(file.data.as_ref()) 24 | .map(std::borrow::ToOwned::to_owned) 25 | .map_err(|e| anyhow::anyhow!(e)) 26 | }) 27 | } 28 | 29 | /// Build a shader file by importing all its dependencies. 30 | pub fn build(name: &str) -> Result { 31 | Self::build_with_seen(name, &mut vec![]) 32 | } 33 | 34 | /// Create a shader module from a shader file. 35 | pub fn create_module(device: &wgpu::Device, name: &str) -> Result { 36 | let shader = Self::build(name)?; 37 | 38 | // device.create_shader_module panics if the shader is malformed 39 | // only check this on native debug builds. 40 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 41 | device.push_error_scope(wgpu::ErrorFilter::Validation); 42 | 43 | let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { 44 | label: Some(name), 45 | source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(shader.as_str())), 46 | }); 47 | 48 | // device.create_shader_module panics if the shader is malformed 49 | // only check this on native debug builds. 50 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 51 | if let Some(error) = pollster::block_on(device.pop_error_scope()) { 52 | anyhow::bail!("Shader {name} is malformed: {error}") 53 | } 54 | 55 | Ok(module) 56 | } 57 | 58 | pub fn create_compute_pipeline( 59 | device: &wgpu::Device, 60 | descriptor: &wgpu::ComputePipelineDescriptor, 61 | ) -> Result { 62 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 63 | device.push_error_scope(wgpu::ErrorFilter::Validation); 64 | 65 | let pipeline = device.create_compute_pipeline(descriptor); 66 | 67 | // device.create_shader_module panics if the shader is malformed 68 | // only check this on native debug builds. 69 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 70 | if let Some(error) = pollster::block_on(device.pop_error_scope()) { 71 | anyhow::bail!( 72 | "Compute pipeline {:?} is malformed: {error}", 73 | descriptor.label 74 | ) 75 | } 76 | 77 | Ok(pipeline) 78 | } 79 | 80 | pub fn create_render_pipeline( 81 | device: &wgpu::Device, 82 | descriptor: &wgpu::RenderPipelineDescriptor, 83 | ) -> Result { 84 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 85 | device.push_error_scope(wgpu::ErrorFilter::Validation); 86 | 87 | let pipeline = device.create_render_pipeline(descriptor); 88 | 89 | // device.create_shader_module panics if the shader is malformed 90 | // only check this on native debug builds. 91 | #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] 92 | if let Some(error) = pollster::block_on(device.pop_error_scope()) { 93 | anyhow::bail!( 94 | "Render pipeline {:?} is malformed: {error}", 95 | descriptor.label 96 | ) 97 | } 98 | 99 | Ok(pipeline) 100 | } 101 | 102 | /// Build a shader file by importing all its dependencies. 103 | /// We use seen to make sure we do not import the same file twice. 104 | /// Order of import does not matter in wgsl, as it does not in rust 105 | /// so we don't need to sort the imports depending on their dependencies. 106 | /// However we cannot define the same symbol twice, so we need to make sure 107 | /// we do not import the same file twice. 108 | fn build_with_seen(name: &str, seen: &mut Vec) -> Result { 109 | // File was already included, return empty string. 110 | let owned_name = name.to_owned(); 111 | if seen.contains(&owned_name) { 112 | return Ok(String::new()); 113 | } 114 | seen.push(owned_name); 115 | 116 | Self::load(name)? 117 | .lines() 118 | .map(|line| { 119 | // example of valid import: #import "common.wgsl" 120 | // note: this follow the bevy preprocessor syntax. 121 | // wgsl-analyzer is also based on the bevy preprocessor. 122 | // but does not support #import "file" as of August 2023. 123 | if line.starts_with("#import") { 124 | let include = line 125 | .split('"') 126 | .nth(1) 127 | .expect("Invalid import syntax: expected #import \"file\""); 128 | let include_content = Self::build_with_seen(include, seen)?; 129 | // We keep the import commented for debugging purposes. 130 | Ok(format!("//{line}\n {include_content}")) 131 | } else { 132 | Ok(format!("{line}\n")) 133 | } 134 | }) 135 | .collect::>() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/box_sparse.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/box_sparse.glb -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/complete.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/complete.glb -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/cube.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/cube.glb -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/cube.png -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/cube_classic.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/cube_classic.bin -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/cube_classic.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset" : { 3 | "generator" : "Khronos glTF Blender I/O v1.2.75", 4 | "version" : "2.0" 5 | }, 6 | "extensionsUsed" : [ 7 | "KHR_lights_punctual" 8 | ], 9 | "extensionsRequired" : [ 10 | "KHR_lights_punctual" 11 | ], 12 | "extensions" : { 13 | "KHR_lights_punctual" : { 14 | "lights" : [ 15 | { 16 | "color" : [ 17 | 1, 18 | 1, 19 | 1 20 | ], 21 | "intensity" : 1000, 22 | "type" : "point", 23 | "name" : "Light" 24 | } 25 | ] 26 | } 27 | }, 28 | "scene" : 0, 29 | "scenes" : [ 30 | { 31 | "name" : "Scene", 32 | "nodes" : [ 33 | 0, 34 | 2, 35 | 4 36 | ] 37 | } 38 | ], 39 | "nodes" : [ 40 | { 41 | "mesh" : 0, 42 | "name" : "Cube" 43 | }, 44 | { 45 | "extensions" : { 46 | "KHR_lights_punctual" : { 47 | "light" : 0 48 | } 49 | }, 50 | "name" : "Light_Orientation", 51 | "rotation" : [ 52 | -0.7071067690849304, 53 | 0, 54 | 0, 55 | 0.7071067690849304 56 | ] 57 | }, 58 | { 59 | "children" : [ 60 | 1 61 | ], 62 | "name" : "Light", 63 | "rotation" : [ 64 | 0.16907575726509094, 65 | 0.7558803558349609, 66 | -0.27217137813568115, 67 | 0.570947527885437 68 | ], 69 | "translation" : [ 70 | 4.076245307922363, 71 | 5.903861999511719, 72 | -1.0054539442062378 73 | ] 74 | }, 75 | { 76 | "camera" : 0, 77 | "name" : "Camera_Orientation", 78 | "rotation" : [ 79 | -0.7071067690849304, 80 | 0, 81 | 0, 82 | 0.7071067690849304 83 | ] 84 | }, 85 | { 86 | "children" : [ 87 | 3 88 | ], 89 | "name" : "Camera", 90 | "rotation" : [ 91 | 0.483536034822464, 92 | 0.33687159419059753, 93 | -0.20870360732078552, 94 | 0.7804827094078064 95 | ], 96 | "translation" : [ 97 | 7.358891487121582, 98 | 4.958309173583984, 99 | 6.925790786743164 100 | ] 101 | } 102 | ], 103 | "cameras" : [ 104 | { 105 | "name" : "Camera", 106 | "perspective" : { 107 | "yfov" : 0.39959652046304894, 108 | "zfar" : 100, 109 | "znear" : 0.10000000149011612 110 | }, 111 | "type" : "perspective" 112 | } 113 | ], 114 | "materials" : [ 115 | { 116 | "doubleSided" : true, 117 | "emissiveFactor" : [ 118 | 0, 119 | 0, 120 | 0 121 | ], 122 | "name" : "Material", 123 | "pbrMetallicRoughness" : { 124 | "baseColorTexture" : { 125 | "index" : 0, 126 | "texCoord" : 0 127 | }, 128 | "metallicFactor" : 0, 129 | "roughnessFactor" : 0.4000000059604645 130 | } 131 | } 132 | ], 133 | "meshes" : [ 134 | { 135 | "name" : "Cube", 136 | "primitives" : [ 137 | { 138 | "attributes" : { 139 | "POSITION" : 0, 140 | "NORMAL" : 1, 141 | "TEXCOORD_0" : 2 142 | }, 143 | "indices" : 3, 144 | "material" : 0 145 | } 146 | ] 147 | } 148 | ], 149 | "textures" : [ 150 | { 151 | "source" : 0 152 | } 153 | ], 154 | "images" : [ 155 | { 156 | "mimeType" : "image/png", 157 | "name" : "cube", 158 | "uri" : "cube.png" 159 | } 160 | ], 161 | "accessors" : [ 162 | { 163 | "bufferView" : 0, 164 | "componentType" : 5126, 165 | "count" : 24, 166 | "max" : [ 167 | 1, 168 | 1, 169 | 1 170 | ], 171 | "min" : [ 172 | -1, 173 | -1, 174 | -1 175 | ], 176 | "type" : "VEC3" 177 | }, 178 | { 179 | "bufferView" : 1, 180 | "componentType" : 5126, 181 | "count" : 24, 182 | "type" : "VEC3" 183 | }, 184 | { 185 | "bufferView" : 2, 186 | "componentType" : 5126, 187 | "count" : 24, 188 | "type" : "VEC2" 189 | }, 190 | { 191 | "bufferView" : 3, 192 | "componentType" : 5123, 193 | "count" : 36, 194 | "type" : "SCALAR" 195 | } 196 | ], 197 | "bufferViews" : [ 198 | { 199 | "buffer" : 0, 200 | "byteLength" : 288, 201 | "byteOffset" : 0 202 | }, 203 | { 204 | "buffer" : 0, 205 | "byteLength" : 288, 206 | "byteOffset" : 288 207 | }, 208 | { 209 | "buffer" : 0, 210 | "byteLength" : 192, 211 | "byteOffset" : 576 212 | }, 213 | { 214 | "buffer" : 0, 215 | "byteLength" : 72, 216 | "byteOffset" : 768 217 | } 218 | ], 219 | "buffers" : [ 220 | { 221 | "byteLength" : 840, 222 | "uri" : "cube_classic.bin" 223 | } 224 | ] 225 | } 226 | -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/dragon.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/dragon.glb -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/head.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/head.glb -------------------------------------------------------------------------------- /mesh_to_sdf_client/tests/suzanne.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azkellas/mesh_to_sdf/beea42bf76132ce0bda70e15c60adcb82677dd0e/mesh_to_sdf_client/tests/suzanne.glb -------------------------------------------------------------------------------- /update_web.bat: -------------------------------------------------------------------------------- 1 | cargo build --target wasm32-unknown-unknown 2 | wasm-bindgen --out-dir target/generated --web .\target\wasm32-unknown-unknown\%1\mesh_to_sdf_client.wasm --------------------------------------------------------------------------------