├── src ├── DamagedHelmet.glb ├── ts │ ├── wasm.d.ts │ ├── tsconfig.json │ ├── package.json │ ├── index.ts │ └── webpack.config.js ├── flatten_gltf.h ├── import_gltf.h ├── tiny_gltf.cpp ├── gltf_accessor.h ├── gltf_buffer_view.h ├── gltf_mesh.h ├── gltf_mesh.cpp ├── gltf_primitive.h ├── gltf.wgsl ├── gltf_node.h ├── gltf_buffer_view.cpp ├── gltf_accessor.cpp ├── arcball_camera.h ├── gltf_node.cpp ├── gltf_primitive.cpp ├── import_gltf.cpp ├── arcball_camera.cpp ├── flatten_gltf.cpp ├── gltf_util.h ├── CMakeLists.txt └── main.cpp ├── web ├── src │ ├── wasm.d.ts │ └── index.ts ├── tsconfig.json ├── package.json ├── index.html └── webpack.config.js ├── .vscode ├── settings.json ├── launch.json ├── c_cpp_properties.json └── tasks.json ├── .gitignore ├── README.md ├── CMakeLists.txt ├── cmake ├── glm.cmake ├── EmbedFile.cmake ├── BinToH.cmake └── FindDawn.cmake ├── LICENSE.md ├── .github └── workflows │ └── deploy-page.yml └── .clang-format /src/DamagedHelmet.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Twinklebear/webgpu-cpp-gltf/HEAD/src/DamagedHelmet.glb -------------------------------------------------------------------------------- /src/ts/wasm.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.wasm" 2 | { 3 | const content: any; 4 | export default content; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /web/src/wasm.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.wasm" 2 | { 3 | const content: any; 4 | export default content; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureOnOpen": false, 3 | "C_Cpp.default.compilerPath": "/opt/homebrew/opt/llvm/bin/clang++" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.raw 3 | *.spv 4 | shaders/embedded_shaders.js 5 | models/ 6 | *.wasm 7 | js/liblas.js 8 | js/liblas_wrapper.js 9 | .DS_Store 10 | glslc.exe 11 | node_modules 12 | dist/ 13 | cmake-build*/ 14 | web/src/cpp/ 15 | .cache/ 16 | compile_commands.json 17 | web/dbg 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug", 6 | "type": "chrome", 7 | "request": "launch", 8 | "url": "http://localhost:8080", 9 | "preLaunchTask": "run_app" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [WebGPU C++/Wasm glTF Renderer](https://github.com/Twinklebear/webgpu-cpp-gltf) 2 | 3 | This is a glTF Renderer written using C++ and WebGPU that is compiled to 4 | WebAssembly to run in the browser. Try it out [online!](https://www.willusher.io/webgpu-cpp-gltf/). 5 | Uses [tinygltf](https://github.com/syoyo/tinygltf) to load glTF files 6 | -------------------------------------------------------------------------------- /src/flatten_gltf.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "tiny_gltf.h" 4 | #include 5 | 6 | glm::mat4 read_node_transform(const tinygltf::Node &n); 7 | 8 | // Check if the GLTF scene contains only single-level instancing 9 | bool gltf_is_single_level(const tinygltf::Model &model); 10 | 11 | /* Convert the potentially multi-level instanced GLTf scene to one which uses only 12 | * single level instancing. 13 | */ 14 | void flatten_gltf(tinygltf::Model &model); 15 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "ES6", 6 | "target": "ES6", 7 | "allowJs": true, 8 | "moduleResolution": "Node", 9 | "allowSyntheticDefaultImports": true, 10 | "sourceMap": true, 11 | "typeRoots": [ 12 | "./node_modules/@webgpu/types", 13 | "./node_modules/@types" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/import_gltf.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "flatten_gltf.h" 4 | #include "gltf_buffer_view.h" 5 | #include "gltf_mesh.h" 6 | #include "gltf_node.h" 7 | #include "tiny_gltf.h" 8 | 9 | struct GLTFRenderData { 10 | tinygltf::Model model; 11 | std::vector buffers; 12 | std::vector meshes; 13 | std::vector nodes; 14 | }; 15 | 16 | // Import a GLTF binary file 17 | std::unique_ptr import_gltf(const uint8_t *glb, const size_t glb_size); 18 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.27) 2 | project(wgpu-cpp-wasm) 3 | # Library version for our npm package 4 | set(LIBRARY_VERSION "0.2.3") 5 | 6 | if(NOT WIN32) 7 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic") 8 | endif() 9 | 10 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake") 11 | include(ExternalProject) 12 | include(${CMAKE_CURRENT_LIST_DIR}/cmake/EmbedFile.cmake) 13 | 14 | include(cmake/glm.cmake) 15 | 16 | add_definitions(-DGLM_ENABLE_EXPERIMENTAL) 17 | 18 | add_subdirectory(src) 19 | -------------------------------------------------------------------------------- /src/tiny_gltf.cpp: -------------------------------------------------------------------------------- 1 | #define TINYGLTF_IMPLEMENTATION 2 | #define STB_IMAGE_IMPLEMENTATION 3 | #define TINYGLTF_NO_STB_IMAGE_WRITE 4 | #include "tiny_gltf.h" 5 | 6 | // We're not going to ever write out gltf files, so just stub this out 7 | namespace tinygltf { 8 | bool WriteImageData(const std::string *, 9 | const std::string *, 10 | const Image *, 11 | bool, 12 | const URICallbacks *, 13 | std::string *, 14 | void *) 15 | { 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "ES6", 6 | "target": "ES6", 7 | "lib": [ 8 | "ES6", "DOM" 9 | ], 10 | "allowJs": true, 11 | "moduleResolution": "Node", 12 | "sourceMap": true, 13 | "declaration": true, 14 | "typeRoots": [ 15 | "./node_modules/@types", 16 | "./node_modules/@webgpu/types" 17 | ], 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "files": [ 21 | "index.ts", 22 | "wasm.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/gltf_accessor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "gltf_buffer_view.h" 4 | #include "tiny_gltf.h" 5 | #include 6 | 7 | struct GLTFAccessor { 8 | const GLTFBufferView *view = nullptr; 9 | const tinygltf::Accessor *accessor = nullptr; 10 | 11 | GLTFAccessor() = default; 12 | 13 | GLTFAccessor(const GLTFBufferView *view, const tinygltf::Accessor *accessor); 14 | 15 | size_t offset() const; 16 | 17 | size_t size() const; 18 | 19 | size_t byte_length() const; 20 | 21 | size_t stride() const; 22 | 23 | wgpu::VertexFormat vertex_format() const; 24 | 25 | wgpu::IndexFormat index_format() const; 26 | }; 27 | -------------------------------------------------------------------------------- /src/gltf_buffer_view.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "tiny_gltf.h" 5 | 6 | struct GLTFBufferView { 7 | const tinygltf::BufferView *view = nullptr; 8 | const uint8_t *buf = nullptr; 9 | 10 | bool needs_upload = false; 11 | wgpu::Buffer gpu_buffer; 12 | uint32_t usage_flags = 0; 13 | 14 | GLTFBufferView() = default; 15 | 16 | GLTFBufferView(const tinygltf::BufferView *view, const tinygltf::Buffer &buffer); 17 | 18 | size_t byte_length() const; 19 | 20 | size_t stride() const; 21 | 22 | void add_usage(const wgpu::BufferUsage usage); 23 | 24 | void upload(wgpu::Device &device); 25 | }; 26 | -------------------------------------------------------------------------------- /cmake/glm.cmake: -------------------------------------------------------------------------------- 1 | ExternalProject_Add(glm_ext 2 | PREFIX glm 3 | DOWNLOAD_DIR glm 4 | STAMP_DIR glm/stamp 5 | SOURCE_DIR glm/src 6 | BINARY_DIR glm 7 | URL "https://github.com/g-truc/glm/releases/download/0.9.9.8/glm-0.9.9.8.zip" 8 | URL_HASH "SHA256=37e2a3d62ea3322e43593c34bae29f57e3e251ea89f4067506c94043769ade4c" 9 | CONFIGURE_COMMAND "" 10 | BUILD_COMMAND "" 11 | INSTALL_COMMAND "" 12 | BUILD_ALWAYS OFF 13 | ) 14 | 15 | set(GLM_INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/glm/src) 16 | 17 | add_library(glm INTERFACE) 18 | 19 | add_dependencies(glm glm_ext) 20 | 21 | target_include_directories(glm INTERFACE 22 | ${GLM_INCLUDE_DIRS}) 23 | 24 | 25 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Emscripten", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "/opt/homebrew/Cellar/emscripten/3.1.50/libexec/cache/sysroot/include/" 8 | ], 9 | "defines": [ 10 | "EMSCRIPTEN" 11 | ], 12 | "macFrameworkPath": [ 13 | "/Library/Developer/CommandLineTools/SDKs/MacOSX14.sdk/System/Library/Frameworks" 14 | ], 15 | "cStandard": "c17", 16 | "cppStandard": "c++17", 17 | "intelliSenseMode": "${default}" 18 | } 19 | ], 20 | "version": 4 21 | } -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgpu-cpp-frontend", 3 | "version": "1.0.0", 4 | "description": "WebGPU C++ Frontend app", 5 | "main": "main.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "serve": "webpack server", 9 | "deploy": "webpack --mode=production" 10 | }, 11 | "keywords": [], 12 | "author": "Will Usher ", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "copy-webpack-plugin": "^13.0.0", 16 | "html-webpack-plugin": "^5.5.3", 17 | "ts-loader": "^9.5.2", 18 | "typescript": "^5.8.3", 19 | "webpack": "^5.98.0", 20 | "webpack-cli": "^6.0.1", 21 | "webpack-dev-server": "^4.15.1" 22 | }, 23 | "dependencies": { 24 | "@twinklebear/webgpu_cpp_gltf": "^0.1.0", 25 | "@webgpu/types": "^0.1.60" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/gltf_mesh.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "gltf_primitive.h" 5 | 6 | class GLTFMesh { 7 | std::string name; 8 | std::vector primitives; 9 | 10 | public: 11 | GLTFMesh() = default; 12 | 13 | // Primitives should be moved into the GLTFMesh 14 | GLTFMesh(const std::string &name, std::vector primitives); 15 | 16 | void build_render_pipeline(wgpu::Device &device, 17 | const wgpu::ShaderModule &shader_module, 18 | const std::vector &color_targets, 19 | const wgpu::DepthStencilState &depth_state, 20 | const std::vector &bind_group_layouts); 21 | 22 | void render(wgpu::RenderPassEncoder &pass); 23 | 24 | const std::string &get_name() const; 25 | }; 26 | -------------------------------------------------------------------------------- /src/gltf_mesh.cpp: -------------------------------------------------------------------------------- 1 | #include "gltf_mesh.h" 2 | 3 | GLTFMesh::GLTFMesh(const std::string &name, std::vector primitives) 4 | : name(name), primitives(std::move(primitives)) 5 | { 6 | } 7 | 8 | void GLTFMesh::build_render_pipeline( 9 | wgpu::Device &device, 10 | const wgpu::ShaderModule &shader_module, 11 | const std::vector &color_targets, 12 | const wgpu::DepthStencilState &depth_state, 13 | const std::vector &bind_group_layouts) 14 | { 15 | for (auto &p : primitives) { 16 | p.build_render_pipeline( 17 | device, shader_module, color_targets, depth_state, bind_group_layouts); 18 | } 19 | } 20 | 21 | void GLTFMesh::render(wgpu::RenderPassEncoder &pass) 22 | { 23 | for (auto &p : primitives) { 24 | p.render(pass); 25 | } 26 | } 27 | 28 | const std::string &GLTFMesh::get_name() const 29 | { 30 | return name; 31 | } 32 | -------------------------------------------------------------------------------- /src/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@twinklebear/webgpu_cpp_gltf", 3 | "version": "@LIBRARY_VERSION@", 4 | "description": "WebGPU GLTF Renderer using Wasm", 5 | "readme": "README.md", 6 | "main": "index.js", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "build": "webpack" 10 | }, 11 | "keywords": [], 12 | "author": "Will Usher", 13 | "email": "will@willusher.io", 14 | "repository": { 15 | "url": "git+https://github.com/Twinklebear/webgpu-cpp-gltf.git" 16 | }, 17 | "license": "MIT", 18 | "devDependencies": { 19 | "copy-webpack-plugin": "^13.0.0", 20 | "webpack": "^5.98.0", 21 | "webpack-cli": "^6.0.1", 22 | "typescript": "^5.8.3", 23 | "ts-loader": "^9.5.2" 24 | }, 25 | "dependencies": { 26 | "@webgpu/types": "^0.1.60" 27 | }, 28 | "files": [ 29 | "*.js", 30 | "*.d.ts", 31 | "*.wasm" 32 | ] 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/gltf_primitive.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "gltf_accessor.h" 5 | #include "tiny_gltf.h" 6 | #include 7 | 8 | class GLTFPrimitive { 9 | const tinygltf::Primitive *primitive = nullptr; 10 | GLTFAccessor positions; 11 | GLTFAccessor indices; 12 | 13 | wgpu::RenderPipeline render_pipeline; 14 | 15 | public: 16 | GLTFPrimitive() = default; 17 | 18 | GLTFPrimitive(const GLTFAccessor &positions, 19 | const GLTFAccessor &indices, 20 | const tinygltf::Primitive *primitive); 21 | 22 | void build_render_pipeline(wgpu::Device &device, 23 | const wgpu::ShaderModule &shader_module, 24 | const std::vector &color_targets, 25 | const wgpu::DepthStencilState &depth_state, 26 | const std::vector &bind_group_layouts); 27 | 28 | void render(wgpu::RenderPassEncoder &pass); 29 | }; 30 | -------------------------------------------------------------------------------- /src/gltf.wgsl: -------------------------------------------------------------------------------- 1 | alias float3 = vec3; 2 | alias float4 = vec4; 3 | 4 | struct VertexInput { 5 | @location(0) position: float3, 6 | }; 7 | 8 | struct VertexOutput { 9 | @builtin(position) position: float4, 10 | @location(0) world_pos: float3, 11 | }; 12 | 13 | struct ViewParams { 14 | view_proj: mat4x4, 15 | }; 16 | @group(0) @binding(0) 17 | var view_params: ViewParams; 18 | 19 | struct NodeParams { 20 | transform: mat4x4, 21 | }; 22 | @group(1) @binding(0) 23 | var node_params: NodeParams; 24 | 25 | @vertex 26 | fn vertex_main(in: VertexInput) -> VertexOutput { 27 | var out: VertexOutput; 28 | out.position = view_params.view_proj * node_params.transform * float4(in.position, 1.0); 29 | out.world_pos = in.position.xyz; 30 | return out; 31 | }; 32 | 33 | @fragment 34 | fn fragment_main(in: VertexOutput) -> @location(0) float4 { 35 | let dx = dpdx(in.world_pos); 36 | let dy = dpdy(in.world_pos); 37 | let n = normalize(cross(dx, dy)); 38 | return float4((n + 1.0) * 0.5, 1.0); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/gltf_node.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "gltf_mesh.h" 6 | #include 7 | 8 | class GLTFNode { 9 | std::string name; 10 | glm::mat4 transform; 11 | // Reference to the shared set of meshes 12 | GLTFMesh *mesh = nullptr; 13 | 14 | wgpu::Buffer node_params_buf; 15 | wgpu::BindGroupLayout node_params_bg_layout; 16 | wgpu::BindGroup node_params_bg; 17 | 18 | public: 19 | GLTFNode() = default; 20 | 21 | GLTFNode(const std::string &name, const glm::mat4 &transform, GLTFMesh *mesh); 22 | 23 | void build_render_pipeline(wgpu::Device &device, 24 | const wgpu::ShaderModule &shader_module, 25 | const std::vector &color_targets, 26 | const wgpu::DepthStencilState &depth_state, 27 | const std::vector &bind_group_layouts); 28 | 29 | void render(wgpu::RenderPassEncoder &pass); 30 | 31 | const std::string &get_name() const; 32 | }; 33 | -------------------------------------------------------------------------------- /src/gltf_buffer_view.cpp: -------------------------------------------------------------------------------- 1 | #include "gltf_buffer_view.h" 2 | #include 3 | #include 4 | #include "gltf_util.h" 5 | 6 | GLTFBufferView::GLTFBufferView(const tinygltf::BufferView *view, 7 | const tinygltf::Buffer &buffer) 8 | : view(view), buf(buffer.data.data() + view->byteOffset) 9 | { 10 | } 11 | 12 | size_t GLTFBufferView::byte_length() const 13 | { 14 | return view->byteLength; 15 | } 16 | 17 | size_t GLTFBufferView::stride() const 18 | { 19 | return view->byteStride; 20 | } 21 | 22 | void GLTFBufferView::add_usage(const wgpu::BufferUsage usage) 23 | { 24 | usage_flags |= static_cast(usage); 25 | } 26 | 27 | void GLTFBufferView::upload(wgpu::Device &device) 28 | { 29 | needs_upload = false; 30 | 31 | wgpu::BufferDescriptor desc; 32 | desc.size = align_to(byte_length(), 4); 33 | desc.usage = static_cast(usage_flags); 34 | desc.mappedAtCreation = true; 35 | 36 | gpu_buffer = device.CreateBuffer(&desc); 37 | 38 | std::memcpy(gpu_buffer.GetMappedRange(), buf, byte_length()); 39 | 40 | gpu_buffer.Unmap(); 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Will Usher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ts/index.ts: -------------------------------------------------------------------------------- 1 | // The wgpu_app.js file is generated by Emscript 2 | import App from "./wgpu_app.js"; 3 | 4 | type WasmModule = Awaited>; 5 | 6 | export class WGPUApp { 7 | wasm: WasmModule; 8 | 9 | constructor(wasm: WasmModule) { 10 | this.wasm = wasm; 11 | } 12 | 13 | public callMain(canvasId: string, fixed_dpi: number = 0) { 14 | this.wasm.callMain([canvasId, `${fixed_dpi}`]); 15 | } 16 | 17 | public loadGLTFBuffer(data: Uint8Array) { 18 | // Allocate memory for the data and copy it in 19 | const ptr = this.wasm._malloc(data.byteLength); 20 | this.wasm.HEAPU8.set(data, ptr); 21 | this.wasm.loadGLTFBuffer(ptr, data.byteLength); 22 | // Release the memory we allocated in the Wasm, it's no longer needed 23 | this.wasm._free(ptr); 24 | } 25 | } 26 | 27 | export async function loadApp(args: any = {}) { 28 | const adapter = await navigator.gpu.requestAdapter(); 29 | const device = await adapter.requestDevice(); 30 | 31 | const app = await App({ 32 | preinitializedWebGPUDevice: device, 33 | ...args 34 | }); 35 | 36 | app.loadGLTFBuffer = app.cwrap("load_gltf_buffer", null, ["number", "number"]); 37 | 38 | return new WGPUApp(app); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/ts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const packageInfo = require("./package.json"); 3 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 4 | 5 | const rules = [ 6 | { 7 | test: /\.(png|jpg|jpeg)$/i, 8 | type: "asset/resource", 9 | }, 10 | { 11 | test: /\.tsx?$/, 12 | use: "ts-loader", 13 | exclude: /node_modules/, 14 | }, 15 | { 16 | test: /\.wasm$/, 17 | type: "asset/resource", 18 | } 19 | ]; 20 | 21 | const resolve = { 22 | extensions: [".tsx", ".ts", ".js"], 23 | }; 24 | 25 | // Maybe I don't need the copy webpack at this step 26 | // for the wasm file? I was just copying the dwarf 27 | // file which I won't ship 28 | const plugins = [ 29 | new CopyWebpackPlugin({ 30 | patterns: [{ 31 | from: "./*.dwarf", 32 | to() { 33 | return "[name][ext]"; 34 | }, 35 | noErrorOnMissing: true 36 | }] 37 | }) 38 | ] 39 | 40 | const browser_config = { 41 | entry: "./index.ts", 42 | mode: "development", 43 | devtool: "inline-source-map", 44 | target: "web", 45 | output: { 46 | filename: "index.js", 47 | path: path.resolve(__dirname, "dist"), 48 | globalObject: "this", 49 | library: { 50 | name: "webgpu_cpp_gltf", 51 | type: "umd", 52 | } 53 | }, 54 | module: { 55 | rules: rules, 56 | }, 57 | resolve: resolve, 58 | plugins: plugins, 59 | }; 60 | 61 | module.exports = [browser_config]; 62 | 63 | -------------------------------------------------------------------------------- /src/gltf_accessor.cpp: -------------------------------------------------------------------------------- 1 | #include "gltf_accessor.h" 2 | #include "gltf_util.h" 3 | 4 | GLTFAccessor::GLTFAccessor(const GLTFBufferView *view, const tinygltf::Accessor *accessor) 5 | : view(view), accessor(accessor) 6 | { 7 | } 8 | 9 | size_t GLTFAccessor::offset() const 10 | { 11 | return accessor ? accessor->byteOffset : 0; 12 | } 13 | 14 | size_t GLTFAccessor::size() const 15 | { 16 | return accessor ? accessor->count : 0; 17 | } 18 | 19 | size_t GLTFAccessor::byte_length() const 20 | { 21 | return stride() * size(); 22 | } 23 | 24 | size_t GLTFAccessor::stride() const 25 | { 26 | return accessor ? accessor->ByteStride(*view->view) : 0; 27 | } 28 | 29 | wgpu::VertexFormat GLTFAccessor::vertex_format() const 30 | { 31 | return accessor ? gltf_wgpu_vertex_type(accessor->componentType, accessor->type) 32 | : wgpu::VertexFormat::Undefined; 33 | } 34 | 35 | wgpu::IndexFormat GLTFAccessor::index_format() const 36 | { 37 | if (!accessor || accessor->type != TINYGLTF_TYPE_SCALAR) { 38 | return wgpu::IndexFormat::Undefined; 39 | } 40 | switch (accessor->componentType) { 41 | case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: 42 | return wgpu::IndexFormat::Uint16; 43 | case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: 44 | return wgpu::IndexFormat::Uint32; 45 | default: 46 | return wgpu::IndexFormat::Undefined; 47 | } 48 | return wgpu::IndexFormat::Undefined; 49 | } 50 | -------------------------------------------------------------------------------- /cmake/EmbedFile.cmake: -------------------------------------------------------------------------------- 1 | # find_file(BINTOH NAME BinToH.cmake PATHS ${CMAKE_MODULE_PATH} 2 | # ${CMAKE_CURRENT_LIST_DIR}) 3 | get_filename_component(BINTOH "${CMAKE_CURRENT_LIST_DIR}/BinToH.cmake" ABSOLUTE) 4 | 5 | function(embed_files) 6 | cmake_parse_arguments(PARSE_ARGV 1 EMBED_FILE "" "" "${options}") 7 | 8 | # Resolve the full path to each file 9 | set(EMBED_FILE_LIST "") 10 | foreach(FIN IN LISTS EMBED_FILE_UNPARSED_ARGUMENTS) 11 | get_filename_component(FULL_FILE_PATH "${CMAKE_CURRENT_LIST_DIR}/${FIN}" 12 | ABSOLUTE) 13 | list(APPEND EMBED_FILE_LIST ${FULL_FILE_PATH}) 14 | 15 | get_filename_component(FNAME ${FULL_FILE_PATH} NAME_WE) 16 | endforeach() 17 | 18 | string(REPLACE ";" "," EMBED_FILE_STR "${EMBED_FILE_LIST}") 19 | 20 | set(EMBED_HEADER_FILE "${CMAKE_CURRENT_BINARY_DIR}/${ARGV0}.h") 21 | set(EMBED_CPP_FILE "${CMAKE_CURRENT_BINARY_DIR}/${ARGV0}.cpp") 22 | add_custom_command( 23 | OUTPUT ${EMBED_CPP_FILE} 24 | COMMAND 25 | ${CMAKE_COMMAND} -DBIN_TO_H_INPUT_FILES=${EMBED_FILE_STR} 26 | -DBIN_TO_H_HEADER_FILE=${EMBED_HEADER_FILE} 27 | -DBIN_TO_H_CPP_FILE=${EMBED_CPP_FILE} -P ${BINTOH} 28 | DEPENDS ${EMBED_FILE_LIST} 29 | COMMENT "Embedding ${EMBED_FILE_UNPARSED_ARGUMENTS} in ${EMBED_HEADER_FILE}" 30 | ) 31 | 32 | add_library(${ARGV0} ${EMBED_CPP_FILE}) 33 | target_include_directories(${ARGV0} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) 34 | endfunction() 35 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "cmake", 8 | "args": [ 9 | "--build", 10 | "." 11 | ], 12 | "options": { 13 | "cwd": "${workspaceFolder}/cmake-build" 14 | }, 15 | "problemMatcher": [ 16 | "$gcc" 17 | ] 18 | }, 19 | { 20 | "label": "npm_install", 21 | "type": "shell", 22 | "command": "npm i", 23 | "isBuildCommand": true, 24 | "options": { 25 | "cwd": "${workspaceFolder}/web" 26 | } 27 | }, 28 | { 29 | "label": "run_app", 30 | "type": "shell", 31 | "command": "npm run serve", 32 | "options": { 33 | "cwd": "${workspaceFolder}/web" 34 | }, 35 | "dependsOn": [ 36 | "npm_install", 37 | "build" 38 | ], 39 | "isBackground": true, 40 | "problemMatcher": { 41 | "fileLocation": "autodetect", 42 | "pattern": [ 43 | { 44 | "regexp": ".* (\\w+) in (.*)\\((\\d+),(\\d+)\\)", 45 | "message": 0, 46 | "severity": 1, 47 | "file": 2, 48 | "line": 3, 49 | "column": 4 50 | } 51 | ], 52 | "background": { 53 | "activeOnStart": true, 54 | "beginsPattern": ".*", 55 | "endsPattern": "webpack .* compiled.*" 56 | } 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /web/src/index.ts: -------------------------------------------------------------------------------- 1 | import { loadApp } from "@twinklebear/webgpu_cpp_gltf" 2 | 3 | (async () => { 4 | const canvas = document.getElementById("webgpu-canvas") 5 | if (navigator.gpu === undefined) { 6 | canvas.setAttribute("style", "display:none;"); 7 | document.getElementById("no-webgpu").setAttribute("style", "display:block;"); 8 | return; 9 | } 10 | 11 | // Block right click so we can use right click + drag to pan 12 | canvas.addEventListener("contextmenu", (evt) => { 13 | evt.preventDefault(); 14 | }); 15 | 16 | const app = await loadApp(); 17 | 18 | try { 19 | app.callMain("#webgpu-canvas"); 20 | } catch (e) { 21 | console.error(e.stack); 22 | } 23 | 24 | const loadingText = document.getElementById("loading-text"); 25 | 26 | // Setup listener to upload new GLB files now that the app is running 27 | document.getElementById("uploadGLB").onchange = (evt: Event) => { 28 | // When we get a new file we read it into an array buffer, then allocate room in the Wasm 29 | // memory and copy the array buffer in to pass it to the C++ code 30 | const picker = evt.target as HTMLInputElement; 31 | if (picker.files.length === 0) { 32 | return; 33 | } 34 | loadingText.hidden = false; 35 | 36 | const reader = new FileReader(); 37 | reader.onerror = () => { throw Error(`Error reading file ${picker.files[0].name}`); }; 38 | 39 | reader.onload = () => { 40 | const start = performance.now(); 41 | const buf = new Uint8Array(reader.result as ArrayBuffer); 42 | 43 | app.loadGLTFBuffer(buf); 44 | 45 | loadingText.hidden = true; 46 | 47 | const end = performance.now(); 48 | console.log(`Import took ${end - start}ms`); 49 | }; 50 | 51 | reader.readAsArrayBuffer(picker.files[0]); 52 | }; 53 | })(); 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/deploy-page.yml: -------------------------------------------------------------------------------- 1 | name: Deploy App 2 | 3 | on: ["push"] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: "latest" 13 | 14 | - name: Setup Emscripten SDK 15 | run: | 16 | git clone https://github.com/emscripten-core/emsdk.git 17 | cd emsdk 18 | ./emsdk install 4.0.3 19 | ./emsdk activate 4.0.3 20 | 21 | - name: Configure 22 | run: | 23 | source ./emsdk/emsdk_env.sh 24 | mkdir cmake-build 25 | cd cmake-build 26 | emcmake cmake .. -DCMAKE_BUILD_TYPE=Release 27 | 28 | - name: Build C++ 29 | working-directory: ${{ github.workspace }}/cmake-build 30 | run: | 31 | source ../emsdk/emsdk_env.sh 32 | make 33 | 34 | # Add plausible analytics, curious how many people 35 | # visit each demo 36 | - name: Add Stats 37 | working-directory: ${{ github.workspace}}/web/ 38 | run: | 39 | sed -i.bk "s/<\/body>/