├── rustfmt.toml ├── demo ├── .gitignore ├── src │ ├── types.js │ ├── index.js │ ├── worker.js │ └── OffscreenTileLayer.js ├── README.md ├── package.json ├── index.html ├── vite.config.js └── package-lock.json ├── .gitignore ├── tests ├── pbf │ └── osm_4_8_5.pbf ├── render_test.rs └── utils.rs ├── rust-toolchain ├── .cargo └── config.toml ├── .github ├── actions │ ├── install-graphics-driver │ │ └── action.yml │ ├── post-build-env │ │ └── action.yml │ └── prepare-build-env │ │ └── action.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── webdriver.json ├── pkg └── package.json ├── LICENSE ├── src ├── ressource │ ├── tile │ │ ├── line.rs │ │ ├── point.rs │ │ ├── fill.rs │ │ └── mod.rs │ ├── material │ │ ├── fill.rs │ │ ├── line.rs │ │ ├── point.rs │ │ ├── shader │ │ │ └── common.wgsl │ │ └── mod.rs │ ├── view.rs │ └── mod.rs ├── tessellation │ ├── shader │ │ └── line.wgsl │ └── mod.rs ├── renderer.rs └── lib.rs ├── Cargo.toml ├── README.md └── examples └── window.rs /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /target 3 | **/*.rs.bk 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /tests/pbf/osm_4_8_5.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeart1st/wgpu-layers/HEAD/tests/pbf/osm_4_8_5.pbf -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2025-01-03" 3 | components = ["clippy", "rustfmt", "rust-src"] 4 | targets = ["wasm32-unknown-unknown"] 5 | profile = "minimal" -------------------------------------------------------------------------------- /demo/src/types.js: -------------------------------------------------------------------------------- 1 | export const READY = 'ready' 2 | export const STARTED = 'started' 3 | export const CANVAS = 'canvas' 4 | export const SHARED_ARRAY_BUFFER = 'sab' 5 | export const PBF_DATA = 'pbf-data' -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # How to use 2 | 3 | ```bash 4 | # because of https://github.com/npm/cli/issues/6033 5 | # and https://github.com/rustwasm/wasm-pack/issues/1206 6 | npm install --install-links=false 7 | npm run dev 8 | ``` -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ["-C", "target-feature=+simd128,+atomics,+bulk-memory,+mutable-globals", "--cfg=web_sys_unstable_apis"] 3 | 4 | [unstable] 5 | build-std = ["panic_abort", "std"] -------------------------------------------------------------------------------- /.github/actions/install-graphics-driver/action.yml: -------------------------------------------------------------------------------- 1 | name: Install graphics driver 2 | description: Ensures that all necessary Mesa libraries are installed 3 | 4 | runs: 5 | using: "composite" 6 | steps: 7 | - name: Install mesa dependencies 8 | shell: bash 9 | run: sudo apt-get update -qq && sudo apt-get install -y libegl1-mesa mesa-vulkan-drivers xvfb 10 | -------------------------------------------------------------------------------- /webdriver.json: -------------------------------------------------------------------------------- 1 | { 2 | "moz:firefoxOptions": { 3 | "binary": "/tmp/firefox-nightly/firefox/firefox", 4 | "prefs": { 5 | "dom.webgpu.enabled": true 6 | }, 7 | "args": [] 8 | }, 9 | "goog:chromeOptions": { 10 | "args": [ 11 | "--enable-unsafe-webgpu", 12 | "--enable-features=Vulkan,VulkanFromANGLE", 13 | "--use-angle=swiftshader" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wgpu-layers-demo", 3 | "version": "0.1.0", 4 | "author": "Paul Lange ", 5 | "license": "MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "ol": "10.6.1", 14 | "wgpu-layers": "file:../pkg" 15 | }, 16 | "devDependencies": { 17 | "vite": "7.1.11" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | wgpu-layers 6 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wgpu-layers", 3 | "type": "module", 4 | "collaborators": [ 5 | "Paul Lange " 6 | ], 7 | "description": "WebGPU mapping renderer for OpenLayers", 8 | "version": "0.1.0", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/codeart1st/wgpu-layers" 13 | }, 14 | "files": [ 15 | "wgpu_layers_bg.wasm", 16 | "wgpu_layers.js", 17 | "wgpu_layers.d.ts" 18 | ], 19 | "main": "wgpu_layers.js", 20 | "types": "wgpu_layers.d.ts", 21 | "sideEffects": [ 22 | "./snippets/*" 23 | ] 24 | } -------------------------------------------------------------------------------- /demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, searchForWorkspaceRoot } from 'vite' 2 | 3 | export default defineConfig({ 4 | server: { 5 | headers: { 6 | 'Cross-Origin-Opener-Policy': 'same-origin', 7 | 'Cross-Origin-Embedder-Policy': 'require-corp' 8 | }, 9 | proxy: { 10 | '/tegola': { 11 | target: 'https://tegola-osm-demo.go-spatial.org', 12 | changeOrigin: true, 13 | rewrite: path => path.replace(/^\/tegola/, ''), 14 | }, 15 | }, 16 | fs: { 17 | allow: [ 18 | searchForWorkspaceRoot(process.cwd()), 19 | '../pkg' 20 | ] 21 | }, 22 | port: 8080 23 | } 24 | }) -------------------------------------------------------------------------------- /.github/actions/post-build-env/action.yml: -------------------------------------------------------------------------------- 1 | name: Post build environment 2 | description: Clean up build environment actions 3 | 4 | inputs: 5 | cargo-cache-primary-key: 6 | description: Primary key of cargo cache restore action 7 | required: true 8 | cargo-cache-hit: 9 | description: A boolean value to indicate an exact match was found for the cargo cache restore action 10 | required: true 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Cache cargo output (save) 16 | if: inputs.cargo-cache-hit != 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') 17 | uses: actions/cache/save@v4.2.4 18 | with: 19 | path: | 20 | ~/.cargo/bin/ 21 | ~/.cargo/.crates.toml 22 | ~/.cargo/.crates2.json 23 | ~/.cargo/.package-cache 24 | ~/.cargo/registry/ 25 | ~/.cargo/git/db/ 26 | target/ 27 | key: ${{ inputs.cargo-cache-primary-key }} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-now Paul Lange 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/ressource/tile/line.rs: -------------------------------------------------------------------------------- 1 | use mvt_reader::feature::Feature; 2 | 3 | use crate::ressource::{BindGroupScope, RessourceManager, material::MaterialType}; 4 | 5 | use super::{Bucket, BucketType, Tile, TileUniform}; 6 | 7 | impl Bucket for Tile { 8 | fn new(ressource_manager: &RessourceManager, extent: [f32; 4]) -> Self { 9 | let tile_uniform = TileUniform::default(); 10 | let tile_uniform_buffer = 11 | ressource_manager.create_buffer_init(&wgpu::util::BufferInitDescriptor { 12 | label: None, 13 | contents: bytemuck::cast_slice(&[tile_uniform]), 14 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 15 | }); 16 | let bind_group = 17 | ressource_manager.create_bind_group(&BindGroupScope::Model, &[wgpu::BindGroupEntry { 18 | binding: 0, 19 | resource: tile_uniform_buffer.as_entire_binding(), 20 | }]); 21 | 22 | Self { 23 | material: ressource_manager.get_material(MaterialType::Line), 24 | bind_group, 25 | tile_uniform_buffer, 26 | vertex_wgpu_buffer: None, 27 | vertex_buffer: Vec::with_capacity(0), 28 | index_wgpu_buffer: None, 29 | index_buffer: Vec::with_capacity(0), 30 | instance_wgpu_buffer: None, 31 | extent, 32 | bucket_type: BucketType::Line, 33 | } 34 | } 35 | 36 | fn add_features(&mut self, _: &mut Vec, _: &RessourceManager) {} 37 | } 38 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import 'ol/ol.css' 2 | 3 | import { Map, View } from 'ol' 4 | import { Tile } from 'ol/layer' 5 | import { createXYZ } from 'ol/tilegrid' 6 | import { TileDebug, VectorTile } from 'ol/source' 7 | 8 | import { OffscreenTileLayer } from './OffscreenTileLayer' 9 | 10 | function start() { 11 | const offscreenTileLayer = new OffscreenTileLayer() 12 | const tileGrid = createXYZ({ maxZoom: 22 }) 13 | const vectorTileSource = new VectorTile({ 14 | tileGrid, 15 | zDirection: 0, // same as TileDebug 16 | tileLoadFunction: async (tile, src) => { 17 | const { tileCoord, extent } = tile 18 | if (tileCoord[0] != 4) { 19 | tile.setFeatures([]) // finish tile loading 20 | return 21 | } 22 | const response = await fetch(src) 23 | const pbf = await response.arrayBuffer() 24 | await offscreenTileLayer.pushPbfTileData(pbf, tileCoord, extent) 25 | tile.setFeatures([]) // finish tile loading 26 | }, 27 | url: 'https://tegola-osm-demo.go-spatial.org/v1/maps/osm/{z}/{x}/{y}' 28 | }) 29 | vectorTileSource.getTileGridForProjection = () => { // override for better debug purposes 30 | return tileGrid 31 | } 32 | offscreenTileLayer.setSource(vectorTileSource) 33 | new Map({ 34 | target: 'map', 35 | layers: [ 36 | offscreenTileLayer, 37 | new Tile({ 38 | source: new TileDebug({ 39 | tileGrid 40 | }) 41 | }) 42 | ], 43 | view: new View({ 44 | center: [1489199.332673, 6894017.412561], 45 | zoom: 4 46 | }) 47 | }) 48 | } 49 | 50 | start() -------------------------------------------------------------------------------- /src/tessellation/shader/line.wgsl: -------------------------------------------------------------------------------- 1 | struct OutputVertex { 2 | position: vec2, 3 | normal: vec2, 4 | } 5 | 6 | @group(0) @binding(0) 7 | var vertices : array>; 8 | 9 | @group(0) @binding(1) 10 | var indices : array; 11 | 12 | @group(0) @binding(2) 13 | var line_vertices : array; 14 | 15 | @group(0) @binding(3) 16 | var line_indices : array; 17 | 18 | @compute @workgroup_size(256, 1) 19 | fn main(@builtin(global_invocation_id) global_id : vec3) { 20 | if (global_id.x >= arrayLength(&indices)) { 21 | return; 22 | } 23 | 24 | let i1 = indices[global_id.x]; 25 | let i2 = indices[global_id.x + 1u]; 26 | 27 | if (i1 == i2 || (global_id.x > 0u && indices[global_id.x - 1u] == i1)) { // separate linestring from the next one 28 | return; 29 | } 30 | 31 | let v1 = vertices[i1]; 32 | let v2 = vertices[i2]; 33 | 34 | let dx = v2.x - v1.x; 35 | let dy = v2.y - v1.y; 36 | let n1 = normalize(vec2(-dy, dx)); 37 | let n2 = normalize(vec2(dy, -dx)); 38 | 39 | let ii1 = global_id.x * 4u; 40 | let ii2 = ii1 + 1u; 41 | let ii3 = ii1 + 2u; 42 | let ii4 = ii1 + 3u; 43 | 44 | line_vertices[ii1] = OutputVertex(v1, n1); 45 | line_vertices[ii2] = OutputVertex(v1, n2); 46 | line_vertices[ii3] = OutputVertex(v2, n1); 47 | line_vertices[ii4] = OutputVertex(v2, n2); 48 | 49 | let offset = global_id.x * 6u; 50 | line_indices[offset + 0u] = ii1; 51 | line_indices[offset + 1u] = ii2; 52 | line_indices[offset + 2u] = ii3; 53 | line_indices[offset + 3u] = ii3; 54 | line_indices[offset + 4u] = ii2; 55 | line_indices[offset + 5u] = ii4; 56 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgpu-layers" 3 | version = "0.1.0" 4 | description = "WebGPU mapping renderer for OpenLayers" 5 | authors = ["Paul Lange "] 6 | repository = "https://github.com/codeart1st/wgpu-layers" 7 | license = "MIT" 8 | edition = "2024" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [features] 14 | multithreaded = [] 15 | 16 | [dependencies] 17 | log = "0.4.28" 18 | wgpu = { version = "25.0.2", default-features = false, features = ["wgsl", "webgpu", "vulkan"]} 19 | rayon = "1.11.0" 20 | futures = "0.3.31" 21 | geo-types = "0.7.17" 22 | bytemuck = "1.23.2" 23 | bytemuck_derive = "1.10.1" 24 | mvt-reader = "2.1.0" 25 | earcutr = "0.5.0" 26 | glam = { version = "0.30.5", default-features = false, features = ["libm", "bytemuck"] } 27 | 28 | [target.'cfg(target_arch = "wasm32")'.dependencies] 29 | wasm-bindgen = "0.2.99" 30 | wasm-bindgen-futures = "0.4.49" 31 | wasm-bindgen-rayon = "1.3.0" # alternative https://github.com/chemicstry/wasm_thread 32 | console_log = { version = "1.0.0", optional = true } 33 | console_error_panic_hook = { version = "0.1.7", optional = true } 34 | web-sys = { version = "0.3.76", features = [ 35 | "OffscreenCanvas", 36 | "HtmlElement", # from here all features are for integrationtests 37 | "CssStyleDeclaration", 38 | "HtmlCanvasElement", 39 | "FileReader", 40 | "Blob" 41 | ]} 42 | 43 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 44 | pollster = "0.4.0" 45 | env_logger = "0.11.6" 46 | winit = "0.30.12" 47 | 48 | [dev-dependencies] 49 | wasm-bindgen-test = "0.3.49" 50 | pdqhash = "0.1.1" 51 | js-sys = "0.3.76" 52 | 53 | [profile.release] 54 | opt-level = "s" 55 | lto = true 56 | 57 | [package.metadata.wasm-pack.profile.release] 58 | wasm-opt = false # version in wasm-pack is to old for simd128 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: cargo 5 | directory: / 6 | schedule: 7 | interval: daily 8 | target-branch: "develop" 9 | open-pull-requests-limit: 10 10 | groups: 11 | development-dependencies: 12 | dependency-type: "development" 13 | 14 | - package-ecosystem: npm 15 | directory: /demo 16 | schedule: 17 | interval: daily 18 | target-branch: "develop" 19 | open-pull-requests-limit: 10 20 | groups: 21 | development-dependencies: 22 | dependency-type: "development" 23 | production-dependencies: 24 | dependency-type: "production" 25 | 26 | - package-ecosystem: github-actions 27 | directory: / 28 | schedule: 29 | interval: daily 30 | target-branch: "develop" 31 | open-pull-requests-limit: 10 32 | groups: 33 | actions: 34 | dependency-type: "production" 35 | 36 | - package-ecosystem: github-actions 37 | directory: /.github/actions/install-graphics-driver 38 | schedule: 39 | interval: daily 40 | target-branch: "develop" 41 | open-pull-requests-limit: 10 42 | groups: 43 | actions: 44 | dependency-type: "production" 45 | 46 | - package-ecosystem: github-actions 47 | directory: /.github/actions/prepare-build-env 48 | schedule: 49 | interval: daily 50 | target-branch: "develop" 51 | open-pull-requests-limit: 10 52 | groups: 53 | actions: 54 | dependency-type: "production" 55 | 56 | - package-ecosystem: github-actions 57 | directory: /.github/actions/post-build-env 58 | schedule: 59 | interval: daily 60 | target-branch: "develop" 61 | open-pull-requests-limit: 10 62 | groups: 63 | actions: 64 | dependency-type: "production" 65 | -------------------------------------------------------------------------------- /src/ressource/material/fill.rs: -------------------------------------------------------------------------------- 1 | use crate::ressource::{BindGroupScope, RessourceManager}; 2 | 3 | use super::{CreatePipeline, Material, MaterialType, Style}; 4 | 5 | impl CreatePipeline<{ MaterialType::Fill }> for Material { 6 | fn new(ressource_manager: &RessourceManager, shader_module: &wgpu::ShaderModule) -> Self { 7 | let vertex_state = wgpu::VertexState { 8 | module: shader_module, 9 | entry_point: Some("vs_fill"), 10 | buffers: &[wgpu::VertexBufferLayout { 11 | array_stride: 8, 12 | step_mode: wgpu::VertexStepMode::Vertex, 13 | attributes: &wgpu::vertex_attr_array![0 => Float32x2], 14 | }], 15 | compilation_options: wgpu::PipelineCompilationOptions::default(), 16 | }; 17 | let fragment_state = wgpu::FragmentState { 18 | module: shader_module, 19 | entry_point: Some("fs_fill"), 20 | targets: &[Some(wgpu::ColorTargetState { 21 | format: ressource_manager.texture_format, 22 | blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 23 | write_mask: wgpu::ColorWrites::default(), 24 | })], 25 | compilation_options: wgpu::PipelineCompilationOptions::default(), 26 | }; 27 | let pipeline = ressource_manager.create_render_pipeline(vertex_state, fragment_state); 28 | 29 | let style = Style { 30 | fill_color: [0.506, 0.694, 0.31, 1.0], 31 | stroke_color: [0.0, 0.0, 0.0, 1.0], 32 | stroke_width: 0.0, 33 | _pad: [0, 0, 0], 34 | }; 35 | let style_buffer = ressource_manager.create_buffer_init(&wgpu::util::BufferInitDescriptor { 36 | label: None, 37 | contents: bytemuck::cast_slice(&[style]), 38 | usage: wgpu::BufferUsages::UNIFORM, 39 | }); 40 | let bind_group = 41 | ressource_manager.create_bind_group(&BindGroupScope::Material, &[wgpu::BindGroupEntry { 42 | binding: 0, 43 | resource: style_buffer.as_entire_binding(), 44 | }]); 45 | 46 | Self { 47 | pipeline, 48 | bind_group, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ressource/material/line.rs: -------------------------------------------------------------------------------- 1 | use crate::ressource::{BindGroupScope, RessourceManager}; 2 | 3 | use super::{CreatePipeline, Material, MaterialType, Style}; 4 | 5 | impl CreatePipeline<{ MaterialType::Line }> for Material { 6 | fn new(ressource_manager: &RessourceManager, shader_module: &wgpu::ShaderModule) -> Self { 7 | let vertex_state = wgpu::VertexState { 8 | module: shader_module, 9 | entry_point: Some("vs_stroke"), 10 | buffers: &[wgpu::VertexBufferLayout { 11 | array_stride: 16, 12 | step_mode: wgpu::VertexStepMode::Vertex, 13 | attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2], 14 | }], 15 | compilation_options: wgpu::PipelineCompilationOptions::default(), 16 | }; 17 | let fragment_state = wgpu::FragmentState { 18 | module: shader_module, 19 | entry_point: Some("fs_stroke"), 20 | targets: &[Some(wgpu::ColorTargetState { 21 | format: ressource_manager.texture_format, 22 | blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 23 | write_mask: wgpu::ColorWrites::default(), 24 | })], 25 | compilation_options: wgpu::PipelineCompilationOptions::default(), 26 | }; 27 | let pipeline = ressource_manager.create_render_pipeline(vertex_state, fragment_state); 28 | 29 | let stroke_width = 2.5; 30 | let style = Style { 31 | fill_color: [0.0, 0.0, 0.0, 1.0], 32 | stroke_color: [0.0, 0.0, 0.0, 1.0], 33 | stroke_width: stroke_width * 0.5, // multiply by half because of double sided buffer 34 | _pad: [0, 0, 0], 35 | }; 36 | let style_buffer = ressource_manager.create_buffer_init(&wgpu::util::BufferInitDescriptor { 37 | label: None, 38 | contents: bytemuck::cast_slice(&[style]), 39 | usage: wgpu::BufferUsages::UNIFORM, 40 | }); 41 | let bind_group = 42 | ressource_manager.create_bind_group(&BindGroupScope::Material, &[wgpu::BindGroupEntry { 43 | binding: 0, 44 | resource: style_buffer.as_entire_binding(), 45 | }]); 46 | 47 | Self { 48 | pipeline, 49 | bind_group, 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ressource/material/point.rs: -------------------------------------------------------------------------------- 1 | use crate::ressource::{BindGroupScope, RessourceManager}; 2 | 3 | use super::{CreatePipeline, Material, MaterialType, Style}; 4 | 5 | impl CreatePipeline<{ MaterialType::Point }> for Material { 6 | fn new(ressource_manager: &RessourceManager, shader_module: &wgpu::ShaderModule) -> Self { 7 | let vertex_state = wgpu::VertexState { 8 | module: shader_module, 9 | entry_point: Some("vs_point"), 10 | buffers: &[ 11 | wgpu::VertexBufferLayout { 12 | array_stride: 8, 13 | step_mode: wgpu::VertexStepMode::Vertex, 14 | attributes: &wgpu::vertex_attr_array![0 => Float32x2], 15 | }, 16 | wgpu::VertexBufferLayout { 17 | array_stride: 8, 18 | step_mode: wgpu::VertexStepMode::Instance, 19 | attributes: &wgpu::vertex_attr_array![1 => Float32x2], 20 | }, 21 | ], 22 | compilation_options: wgpu::PipelineCompilationOptions::default(), 23 | }; 24 | let fragment_state = wgpu::FragmentState { 25 | module: shader_module, 26 | entry_point: Some("fs_fill"), 27 | targets: &[Some(wgpu::ColorTargetState { 28 | format: ressource_manager.texture_format, 29 | blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 30 | write_mask: wgpu::ColorWrites::default(), 31 | })], 32 | compilation_options: wgpu::PipelineCompilationOptions::default(), 33 | }; 34 | let pipeline = ressource_manager.create_render_pipeline(vertex_state, fragment_state); 35 | 36 | let style = Style { 37 | fill_color: [1.0, 0.0, 0.0, 1.0], 38 | stroke_color: [0.0, 0.0, 0.0, 1.0], 39 | stroke_width: 0.0, 40 | _pad: [0, 0, 0], 41 | }; 42 | let style_buffer = ressource_manager.create_buffer_init(&wgpu::util::BufferInitDescriptor { 43 | label: None, 44 | contents: bytemuck::cast_slice(&[style]), 45 | usage: wgpu::BufferUsages::UNIFORM, 46 | }); 47 | let bind_group = 48 | ressource_manager.create_bind_group(&BindGroupScope::Material, &[wgpu::BindGroupEntry { 49 | binding: 0, 50 | resource: style_buffer.as_entire_binding(), 51 | }]); 52 | 53 | Self { 54 | pipeline, 55 | bind_group, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Preview 2 | 3 | ![image](https://user-images.githubusercontent.com/581407/205440021-d99e2a8e-b83f-4032-a237-2b8d0fca2c6d.png) 4 | 5 | --- 6 |
7 | WebGPU mapping renderer for OpenLayers 8 |
9 |
10 | Currently only a playground for rust, wgpu, openlayers web mapping combo 11 |
12 |
13 | 21 | 22 | # Build the project 23 | 24 | ```sh 25 | wasm-pack build --release --all-features --target web 26 | ``` 27 | 28 | # Next goals 29 | 30 | - [x] Fill 31 | - [x] Initial support 32 | - [ ] Line 33 | - [x] Initial support 34 | - [x] Anti aliasing 35 | - [ ] Line joins 36 | - [ ] Line caps 37 | - [ ] Points 38 | - [x] Initial support 39 | - [ ] Shapes 40 | - [ ] Move polygon triangulation to worker threads 41 | - [ ] Architecture overhaul 42 | - [ ] Combine tiles in buckets with same material 43 | - [ ] Split code in smaller chunks 44 | - [ ] CI 45 | - [x] Initial 46 | - [ ] Deployment 47 | - [ ] Publishing 48 | - [ ] OpenLayers integration 49 | - [ ] Smooth frame sync 50 | 51 | # Run a native example 52 | 53 | ```sh 54 | cargo run --example window --target `rustc -vV | sed -n 's|host: ||p'` 55 | ``` 56 | 57 | # Run tests 58 | 59 | Native unit tests 60 | ```sh 61 | LIBGL_ALWAYS_SOFTWARE=true cargo test --target `rustc -vV | sed -n 's|host: ||p'` -- --nocapture 62 | ``` 63 | 64 | WASM Browser Integration tests 65 | ```sh 66 | wasm-pack test --chrome --release --features console_log,console_error_panic_hook --test '*' 67 | ``` 68 | 69 | # Useful environment variables 70 | 71 | | Name | Example | 72 | | --------------------- | ------- | 73 | | WGPU_BACKEND | gl | 74 | | LIBGL_ALWAYS_SOFTWARE | true | 75 | | WINIT_UNIX_BACKEND | x11 | 76 | | VK_ICD_FILENAMES | /usr/share/vulkan/icd.d/lvp_icd.x86_64.json | 77 | -------------------------------------------------------------------------------- /tests/render_test.rs: -------------------------------------------------------------------------------- 1 | #![feature(thread_local)] 2 | #![allow(clippy::await_holding_refcell_ref)] 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | mod utils; 6 | 7 | use wasm_bindgen_test::*; 8 | 9 | use utils::*; 10 | 11 | wasm_bindgen_test_configure!(run_in_browser); 12 | 13 | #[wasm_bindgen_test] 14 | async fn osm_pbf() { 15 | initialize(); 16 | 17 | // arrange 18 | let canvas_ref = CANVAS.borrow(); 19 | let canvas = canvas_ref.as_ref().unwrap(); 20 | wgpu_layers::wasm::start_with_canvas(canvas).await; 21 | wgpu_layers::add_pbf_tile_data( 22 | include_bytes!("pbf/osm_4_8_5.pbf").to_vec(), 23 | vec![4, 8, 5], 24 | vec![0.0, 5009377.085697312, 2_504_688.5, 7_514_065.5], 25 | ) 26 | .await; 27 | 28 | // act 29 | wgpu_layers::render(get_view_matrix(), vec![CANVAS_SIZE.0, CANVAS_SIZE.1]); 30 | timeout(500).await; // wait for compute shader 31 | wgpu_layers::render(get_view_matrix(), vec![CANVAS_SIZE.0, CANVAS_SIZE.1]); 32 | timeout(1500).await; // wait to render 33 | 34 | // assert 35 | let image_data = get_canvas_image_data(canvas).await; 36 | let image = pdqhash::image::load_from_memory(&image_data[..]).unwrap(); 37 | let (hash, _) = pdqhash::generate_pdq_full_size(&image); 38 | 39 | assert_eq!( 40 | [ 41 | 155, 210, 99, 215, 34, 140, 167, 36, 126, 218, 111, 44, 241, 6, 10, 205, 9, 179, 75, 237, 42 | 159, 35, 148, 131, 139, 45, 212, 131, 162, 190, 87, 48 43 | ], 44 | hash 45 | ); 46 | } 47 | 48 | #[wasm_bindgen_test] 49 | async fn empty() { 50 | initialize(); 51 | 52 | // arrange 53 | let canvas_ref = CANVAS.borrow(); 54 | let canvas = canvas_ref.as_ref().unwrap(); 55 | wgpu_layers::wasm::start_with_canvas(canvas).await; 56 | 57 | // act 58 | wgpu_layers::render(get_view_matrix(), vec![CANVAS_SIZE.0, CANVAS_SIZE.1]); 59 | timeout(1500).await; // wait to render 60 | 61 | // assert 62 | let image_data = get_canvas_image_data(canvas).await; 63 | let image = pdqhash::image::load_from_memory(&image_data[..]).unwrap(); 64 | let (hash, _) = pdqhash::generate_pdq_full_size(&image); 65 | 66 | assert_eq!( 67 | [ 68 | 171, 170, 84, 117, 171, 170, 84, 81, 171, 138, 84, 84, 171, 170, 171, 138, 171, 138, 84, 81, 69 | 85, 255, 84, 113, 171, 174, 84, 84, 171, 170, 84, 85 70 | ], 71 | hash 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /.github/actions/prepare-build-env/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare build environment 2 | description: Ensures that everything is prepared for our build jobs 3 | 4 | outputs: 5 | cargo-cache-primary-key: 6 | description: Primary key of cargo cache restore action 7 | value: ${{ steps.cargo-cache.outputs.cache-primary-key }} 8 | cargo-cache-hit: 9 | description: A boolean value to indicate an exact match was found for the cargo cache restore action 10 | value: ${{ steps.cargo-cache.outputs.cache-hit }} 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Use protobuf-compiler latest 16 | shell: bash 17 | run: sudo apt-get install -y protobuf-compiler 18 | 19 | - name: Use wasm-pack latest 20 | shell: bash 21 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 22 | 23 | - name: Use brotli latest 24 | shell: bash 25 | run: sudo apt-get install -y brotli 26 | 27 | - name: Cache rust-toolchain (restore) 28 | id: rustup-cache 29 | uses: actions/cache/restore@v4.2.4 30 | with: 31 | path: | 32 | ~/.rustup/toolchains 33 | ~/.rustup/update-hashes 34 | ~/.rustup/settings.toml 35 | key: toolchain-${{ hashFiles('rust-toolchain') }}-1 36 | 37 | - name: Use rust-toolchain nightly 38 | if: steps.rustup-cache.outputs.cache-hit != 'true' 39 | shell: bash 40 | run: | 41 | rustup toolchain uninstall stable 42 | rustup show # force install of rust-toolchain TOML 43 | 44 | - name: Get rust version 45 | id: rustup 46 | shell: bash 47 | run: | 48 | rustup show 49 | echo "version=$(rustc --version | cut -d ' ' -f 2)" >> $GITHUB_OUTPUT 50 | 51 | - name: Cache rust-toolchain (save) 52 | uses: actions/cache/save@v4.2.4 53 | if: steps.rustup-cache.outputs.cache-hit != 'true' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') 54 | with: 55 | path: | 56 | ~/.rustup/toolchains 57 | ~/.rustup/update-hashes 58 | ~/.rustup/settings.toml 59 | key: ${{ steps.rustup-cache.outputs.cache-primary-key }} 60 | 61 | - name: Cache cargo output (restore) 62 | id: cargo-cache 63 | uses: actions/cache/restore@v4.2.4 64 | with: 65 | path: | 66 | ~/.cargo/bin/ 67 | ~/.cargo/.crates.toml 68 | ~/.cargo/.crates2.json 69 | ~/.cargo/.package-cache 70 | ~/.cargo/registry/ 71 | ~/.cargo/git/db/ 72 | target/ 73 | key: ${{ runner.os }}-${{ steps.rustup.outputs.version }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ github.job }}-1 74 | -------------------------------------------------------------------------------- /src/ressource/material/shader/common.wgsl: -------------------------------------------------------------------------------- 1 | struct Tile { 2 | model_view_matrix: mat4x4, 3 | clipping_rect: vec4, 4 | } 5 | 6 | struct View { 7 | view_matrix: mat4x4, 8 | width: u32, 9 | height: u32, 10 | } 11 | 12 | struct Style { 13 | fill_color: vec4, 14 | stroke_color: vec4, 15 | stroke_width: f32, 16 | } 17 | 18 | struct VertexInput { 19 | @location(0) position: vec2, 20 | @location(1) normal: vec2, 21 | } 22 | 23 | struct FragmentOutput { 24 | @location(0) color: vec4, 25 | @builtin(sample_mask) mask_out: u32, 26 | } 27 | 28 | struct FragmentInput { 29 | @builtin(position) position: vec4, 30 | @location(0) @interpolate(linear, center) normal: vec2, 31 | } 32 | 33 | @group(0) @binding(0) 34 | var view: View; 35 | 36 | @group(1) @binding(0) 37 | var style: Style; 38 | 39 | @group(2) @binding(0) 40 | var tile: Tile; 41 | 42 | const GLOBAL_SCALE: f32 = 0.0032; // can this be calculated? 43 | 44 | @vertex 45 | fn vs_fill( 46 | @location(0) pos: vec2 47 | ) -> @builtin(position) vec4 { 48 | return tile.model_view_matrix * vec4(pos, 0.0, 1.0); 49 | } 50 | 51 | @vertex 52 | fn vs_stroke(vertex: VertexInput) -> FragmentInput { 53 | // TODO: precalculate (scale * GLOBAL_SCALE * style.stroke_width) on cpu 54 | var scale = 1.0 / (view.view_matrix[0][0] * f32(view.width)); 55 | var delta = vec2(vertex.normal * scale * GLOBAL_SCALE * style.stroke_width); 56 | var position = tile.model_view_matrix * vec4(vertex.position + delta, 0.0, 1.0); 57 | return FragmentInput(position, vertex.normal); 58 | } 59 | 60 | @vertex 61 | fn vs_point(@location(0) pos: vec2, @location(1) point_location: vec2) -> @builtin(position) vec4 { 62 | var scale = 1.0 / (view.view_matrix[0][0] * f32(view.width)); 63 | return tile.model_view_matrix * vec4((pos * 0.02 * scale) + point_location, 0.0, 1.0); 64 | } 65 | 66 | fn clipping_and_premul_alpha(position: vec4, input_color: vec4) -> FragmentOutput { 67 | var color = input_color.a * vec4(input_color.rgb, 1.0); // pre-multiplied alpha 68 | var fragment_output = FragmentOutput(color, 0xFFFFFFFFu); 69 | 70 | if ( 71 | position.x < tile.clipping_rect[0] || 72 | position.y < tile.clipping_rect[1] || 73 | position.x > tile.clipping_rect[2] || 74 | position.y > tile.clipping_rect[3] 75 | ) { 76 | fragment_output.mask_out = 0u; 77 | } 78 | return fragment_output; 79 | } 80 | 81 | @fragment 82 | fn fs_fill(@builtin(position) position: vec4) -> FragmentOutput { 83 | return clipping_and_premul_alpha(position, style.fill_color); 84 | } 85 | 86 | @fragment 87 | fn fs_stroke(input: FragmentInput) -> FragmentOutput { 88 | var distance = length(input.normal); 89 | var blur = 0.8; 90 | var alpha = (1.0 - distance) / (blur / style.stroke_width); 91 | var color = vec4(style.stroke_color.rgb, alpha); 92 | return clipping_and_premul_alpha(input.position, color); 93 | } -------------------------------------------------------------------------------- /demo/src/worker.js: -------------------------------------------------------------------------------- 1 | import init, { initThreadPool, startWithOffscreenCanvas, render, addPbfTileData } from 'wgpu-layers' 2 | import { create, makeInverse } from 'ol/transform' 3 | 4 | import { READY, STARTED, CANVAS, SHARED_ARRAY_BUFFER, PBF_DATA } from './types' 5 | 6 | let shared_state, ready = false 7 | 8 | self.onmessage = async ({ data: { type, payload } }) => { 9 | switch (type) { 10 | case CANVAS: 11 | await startWithOffscreenCanvas(payload.canvas) 12 | self.postMessage({ type: STARTED }) 13 | ready = true 14 | loop() 15 | break 16 | case SHARED_ARRAY_BUFFER: 17 | shared_state = payload 18 | break 19 | case PBF_DATA: 20 | const { data, tileCoord, extent } = payload 21 | if (ready) { 22 | await addPbfTileData(new Uint8Array(data), tileCoord, extent) 23 | } 24 | break 25 | } 26 | } 27 | 28 | function loop() { 29 | Atomics.wait(new Int32Array(shared_state), 6, 0) // wait until notify 30 | 31 | const { size, viewState } = getFrameState() 32 | 33 | const [width, height] = size 34 | render( 35 | getViewMatrix(viewState, width, height), 36 | size 37 | ) 38 | 39 | setTimeout(loop) 40 | } 41 | 42 | function getFrameState() { 43 | const slice = new Uint32Array(shared_state) 44 | 45 | const buffer = new ArrayBuffer(shared_state.byteLength) 46 | const f32_buffer = new Float32Array(buffer) 47 | const uint32_buffer = new Uint32Array(buffer) 48 | 49 | for (let i = 0; i < uint32_buffer.length; i++) { 50 | uint32_buffer[i] = Atomics.load(slice, i) 51 | } 52 | 53 | return { 54 | size: [uint32_buffer[0], uint32_buffer[1]], 55 | viewState: { 56 | center: [f32_buffer[2], f32_buffer[3]], 57 | resolution: f32_buffer[4], 58 | rotation: f32_buffer[5] 59 | } 60 | } 61 | } 62 | 63 | function getViewMatrix(viewState, width, height) { 64 | const halfWidth = width * .5 65 | const halfHeight = height * .5 66 | 67 | const scaleX = viewState.resolution * halfWidth 68 | const scaleY = viewState.resolution * halfHeight 69 | 70 | const viewTransform = create() 71 | 72 | const sin = Math.sin(viewState.rotation) 73 | const cos = Math.cos(viewState.rotation) 74 | 75 | viewTransform[0] = scaleX * cos 76 | viewTransform[1] = scaleX * sin 77 | viewTransform[2] = scaleY * -sin 78 | viewTransform[3] = scaleY * cos 79 | viewTransform[4] = viewState.center[0] 80 | viewTransform[5] = viewState.center[1] 81 | 82 | const viewMatrix = makeInverse( 83 | create(), 84 | viewTransform 85 | ) 86 | 87 | return [ 88 | viewMatrix[0], viewMatrix[1], 0.0, 0.0, 89 | viewMatrix[2], viewMatrix[3], 0.0, 0.0, 90 | 0.0, 0.0, 1.0, 0.0, 91 | viewMatrix[4], viewMatrix[5], 0.0, 1.0 92 | ] 93 | } 94 | 95 | async function run() { 96 | await init() 97 | await initThreadPool(navigator.hardwareConcurrency - 1) 98 | 99 | self.postMessage({ type: READY }) 100 | } 101 | 102 | run() 103 | -------------------------------------------------------------------------------- /src/ressource/tile/point.rs: -------------------------------------------------------------------------------- 1 | use geo_types::Geometry::{MultiPoint, Point}; 2 | use log::info; 3 | use mvt_reader::feature::Feature; 4 | 5 | use crate::ressource::{BindGroupScope, RessourceManager, material::MaterialType}; 6 | 7 | use super::{Bucket, BucketType, Tile, TileUniform}; 8 | 9 | const DIMENSIONS: usize = 2; 10 | 11 | const RECT_VERTEX_BUFFER: [f32; 8] = [-0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5]; 12 | const RECT_INDICES_BUFFER: [u32; 6] = [0, 1, 2, 2, 3, 0]; 13 | 14 | impl Bucket for Tile { 15 | fn new(ressource_manager: &RessourceManager, extent: [f32; 4]) -> Self { 16 | let tile_uniform = TileUniform::default(); 17 | let tile_uniform_buffer = 18 | ressource_manager.create_buffer_init(&wgpu::util::BufferInitDescriptor { 19 | label: None, 20 | contents: bytemuck::cast_slice(&[tile_uniform]), 21 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 22 | }); 23 | let bind_group = 24 | ressource_manager.create_bind_group(&BindGroupScope::Model, &[wgpu::BindGroupEntry { 25 | binding: 0, 26 | resource: tile_uniform_buffer.as_entire_binding(), 27 | }]); 28 | 29 | let vertex_wgpu_buffer = Some(ressource_manager.create_buffer_init( 30 | &wgpu::util::BufferInitDescriptor { 31 | label: None, 32 | contents: bytemuck::cast_slice(&RECT_VERTEX_BUFFER), 33 | usage: wgpu::BufferUsages::VERTEX, 34 | }, 35 | )); 36 | 37 | let index_wgpu_buffer = Some(ressource_manager.create_buffer_init( 38 | &wgpu::util::BufferInitDescriptor { 39 | label: None, 40 | contents: bytemuck::cast_slice(&RECT_INDICES_BUFFER), 41 | usage: wgpu::BufferUsages::INDEX, 42 | }, 43 | )); 44 | 45 | Self { 46 | material: ressource_manager.get_material(MaterialType::Point), 47 | bind_group, 48 | tile_uniform_buffer, 49 | vertex_wgpu_buffer, 50 | vertex_buffer: Vec::with_capacity(0), 51 | index_wgpu_buffer, 52 | index_buffer: Vec::with_capacity(0), 53 | instance_wgpu_buffer: None, 54 | extent, 55 | bucket_type: BucketType::Point, 56 | } 57 | } 58 | 59 | fn add_features(&mut self, features: &mut Vec, ressource_manager: &RessourceManager) { 60 | for feature in features.iter() { 61 | match feature.get_geometry() { 62 | Point(point) => { 63 | let mut vertices = Vec::with_capacity(DIMENSIONS); 64 | vertices.push(point.x()); 65 | vertices.push(point.y()); 66 | self.vertex_buffer.append(&mut vertices); 67 | } 68 | MultiPoint(multi_point) => { 69 | let mut vertices = Vec::with_capacity(multi_point.0.len() * DIMENSIONS); 70 | for point in multi_point.iter() { 71 | vertices.push(point.x()); 72 | vertices.push(point.y()); 73 | } 74 | self.vertex_buffer.append(&mut vertices); 75 | } 76 | _ => { 77 | info!("Geometry type currently not supported"); 78 | } 79 | } 80 | } 81 | 82 | self.instance_wgpu_buffer = Some(ressource_manager.create_buffer_init( 83 | &wgpu::util::BufferInitDescriptor { 84 | label: None, 85 | contents: bytemuck::cast_slice(&self.vertex_buffer), 86 | usage: wgpu::BufferUsages::VERTEX, 87 | }, 88 | )); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ressource/view.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | use super::{BindGroupScope, RessourceManager}; 4 | 5 | #[repr(C)] 6 | #[derive(Copy, Clone, bytemuck_derive::Pod, bytemuck_derive::Zeroable)] 7 | struct ViewBuffer { 8 | /// transformation matrix world-space to view-space 9 | view_matrix: glam::Mat4, 10 | 11 | width: u32, 12 | 13 | height: u32, 14 | 15 | _pad: [u32; 2], 16 | } 17 | 18 | pub struct View { 19 | bind_group: wgpu::BindGroup, 20 | 21 | /// width of surface 22 | width: u32, 23 | 24 | /// height of surface 25 | height: u32, 26 | 27 | /// half width of surface 28 | half_width: f32, 29 | 30 | /// half height of surface 31 | half_height: f32, 32 | 33 | view_buffer: ViewBuffer, 34 | 35 | view_matrix_buffer: wgpu::Buffer, 36 | } 37 | 38 | impl View { 39 | pub fn new((width, height): (u32, u32), ressource_manager: &mut RessourceManager) -> Self { 40 | let view_matrix = ViewBuffer { 41 | view_matrix: glam::Mat4::IDENTITY, 42 | width, 43 | height, 44 | _pad: [0, 0], 45 | }; 46 | let view_matrix_buffer = 47 | ressource_manager.create_buffer_init(&wgpu::util::BufferInitDescriptor { 48 | label: None, 49 | contents: bytemuck::cast_slice(&[view_matrix]), 50 | usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 51 | }); 52 | 53 | ressource_manager.register_bind_group_layout( 54 | BindGroupScope::Global, 55 | &wgpu::BindGroupLayoutDescriptor { 56 | label: None, 57 | entries: &[wgpu::BindGroupLayoutEntry { 58 | binding: 0, 59 | visibility: wgpu::ShaderStages::VERTEX, 60 | ty: wgpu::BindingType::Buffer { 61 | ty: wgpu::BufferBindingType::Uniform, 62 | has_dynamic_offset: false, 63 | min_binding_size: wgpu::BufferSize::new(mem::size_of::() as _), 64 | }, 65 | count: None, 66 | }], 67 | }, 68 | ); 69 | 70 | let bind_group = 71 | ressource_manager.create_bind_group(&BindGroupScope::Global, &[wgpu::BindGroupEntry { 72 | binding: 0, 73 | resource: view_matrix_buffer.as_entire_binding(), 74 | }]); 75 | 76 | Self { 77 | bind_group, 78 | width, 79 | height, 80 | half_width: width as f32 * 0.5, 81 | half_height: height as f32 * 0.5, 82 | view_buffer: view_matrix, 83 | view_matrix_buffer, 84 | } 85 | } 86 | 87 | pub fn set<'frame>( 88 | &'frame self, 89 | render_pass: &mut wgpu::RenderPass<'frame>, 90 | queue: &wgpu::Queue, 91 | ) { 92 | render_pass.set_bind_group(BindGroupScope::Global as u32, Some(&self.bind_group), &[]); 93 | 94 | queue.write_buffer( 95 | &self.view_matrix_buffer, 96 | 0, 97 | bytemuck::cast_slice(&[self.view_buffer]), 98 | ); 99 | } 100 | 101 | pub fn set_size(&mut self, (width, height): (u32, u32)) { 102 | self.width = width; 103 | self.height = height; 104 | self.half_width = width as f32 * 0.5; 105 | self.half_height = height as f32 * 0.5; 106 | self.view_buffer.width = width; 107 | self.view_buffer.height = height; 108 | } 109 | 110 | pub fn set_view_matrix(&mut self, view_matrix: glam::Mat4) { 111 | self.view_buffer.view_matrix = view_matrix; 112 | } 113 | 114 | pub fn get_view_matrix(&self) -> glam::Mat4 { 115 | self.view_buffer.view_matrix 116 | } 117 | 118 | pub fn get_size(&self) -> (u32, u32) { 119 | (self.width, self.height) 120 | } 121 | 122 | pub fn get_half_size(&self) -> (f32, f32) { 123 | (self.half_width, self.half_height) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/ressource/material/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, marker::ConstParamTy, mem, sync::Arc}; 2 | 3 | use super::{BindGroupScope, RessourceManager, ShaderModuleScope}; 4 | 5 | mod fill; 6 | mod line; 7 | mod point; 8 | 9 | #[repr(C)] 10 | #[derive(Copy, Clone, bytemuck_derive::Pod, bytemuck_derive::Zeroable)] 11 | struct Style { 12 | /// fill color 13 | fill_color: [f32; 4], 14 | 15 | /// stroke color 16 | stroke_color: [f32; 4], 17 | 18 | /// stroke width 19 | stroke_width: f32, 20 | 21 | _pad: [u32; 3], 22 | } 23 | 24 | #[derive(PartialEq, Eq, Hash, Clone, ConstParamTy)] 25 | pub enum MaterialType { 26 | Fill, 27 | Line, 28 | Point, 29 | } 30 | 31 | pub struct Material { 32 | /// wgpu pipeline 33 | pipeline: wgpu::RenderPipeline, 34 | 35 | /// wgpu bind group 36 | bind_group: wgpu::BindGroup, 37 | } 38 | 39 | impl Material { 40 | pub fn set<'frame>(&'frame self, render_pass: &mut wgpu::RenderPass<'frame>) { 41 | render_pass.set_pipeline(&self.pipeline); 42 | render_pass.set_bind_group(BindGroupScope::Material as u32, Some(&self.bind_group), &[]); 43 | } 44 | } 45 | 46 | pub trait CreatePipeline 47 | where 48 | Self: Sized, 49 | { 50 | fn new(ressource_manager: &RessourceManager, shader_module: &wgpu::ShaderModule) -> Self; 51 | } 52 | 53 | pub struct MaterialManager { 54 | shader_module: wgpu::ShaderModule, 55 | 56 | materials: HashMap>, 57 | } 58 | 59 | impl MaterialManager { 60 | pub fn new(ressource_manager: &mut RessourceManager) -> Self { 61 | let shader_module = ressource_manager.create_shader_module( 62 | ShaderModuleScope::Common, 63 | std::borrow::Cow::Borrowed(include_str!("shader/common.wgsl")), 64 | ); 65 | 66 | ressource_manager.register_bind_group_layout( 67 | BindGroupScope::Material, 68 | &wgpu::BindGroupLayoutDescriptor { 69 | label: None, 70 | entries: &[wgpu::BindGroupLayoutEntry { 71 | binding: 0, 72 | visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 73 | ty: wgpu::BindingType::Buffer { 74 | ty: wgpu::BufferBindingType::Uniform, 75 | has_dynamic_offset: false, 76 | min_binding_size: wgpu::BufferSize::new(mem::size_of::