├── .gitignore ├── .gitmodules ├── README.md └── to-move ├── renderer-demo ├── .gitignore ├── Makefile.toml ├── _static │ ├── _redirects │ └── wasm │ │ └── demo │ │ └── .gitkeep ├── build-utils │ └── local-media-server.js ├── jest.config.js ├── package.json ├── rust │ └── demo │ │ ├── Cargo.toml │ │ ├── Makefile.toml │ │ └── src │ │ ├── events │ │ ├── event_sender.rs │ │ ├── events.rs │ │ ├── handle_events.rs │ │ └── mod.rs │ │ ├── game_loop │ │ ├── game_loop.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ └── state │ │ ├── mod.rs │ │ └── state.rs ├── tsconfig.json ├── typescript │ ├── config │ │ └── config.ts │ ├── entry │ │ ├── index.css │ │ ├── index.html │ │ └── index.ts │ ├── events │ │ └── events.ts │ ├── state │ │ └── state.ts │ ├── tests │ │ └── sanity.spec.ts │ ├── tsconfig.json │ ├── ui │ │ ├── menu.ts │ │ ├── ui.css │ │ └── ui.ts │ └── utils │ │ ├── common.ts │ │ ├── path.ts │ │ └── window.ts ├── webpack.dev.js └── webpack.release.js └── renderer ├── Cargo.toml ├── README.md └── src ├── camera ├── camera.rs └── mod.rs ├── components ├── components.rs └── mod.rs ├── errors ├── errors.rs └── mod.rs ├── gltf ├── accessors.rs ├── buffer_view.rs ├── loader.rs ├── materials.rs ├── mod.rs └── processor.rs ├── lib.rs ├── nodes ├── mod.rs └── nodes.rs ├── primitives ├── mod.rs └── primitives.rs ├── renderer.rs ├── shaders ├── glsl │ ├── material.frag │ └── primitive.vert ├── mod.rs └── shaders.rs └── transform ├── mod.rs └── transform.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Netlify 2 | .netlify 3 | 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | #VSCode 8 | .vscode/* 9 | !.vscode/settings.json 10 | !.vscode/tasks.json 11 | !.vscode/launch.json 12 | !.vscode/extensions.json 13 | *.code-workspace 14 | 15 | # User-specific stuff 16 | .idea/**/workspace.xml 17 | .idea/**/tasks.xml 18 | .idea/**/usage.statistics.xml 19 | .idea/**/dictionaries 20 | .idea/**/shelf 21 | 22 | # Generated files 23 | .idea/**/contentModel.xml 24 | 25 | # Sensitive or high-churn files 26 | .idea/**/dataSources/ 27 | .idea/**/dataSources.ids 28 | .idea/**/dataSources.local.xml 29 | .idea/**/sqlDataSources.xml 30 | .idea/**/dynamic.xml 31 | .idea/**/uiDesigner.xml 32 | .idea/**/dbnavigator.xml 33 | 34 | # Gradle 35 | .idea/**/gradle.xml 36 | .idea/**/libraries 37 | 38 | # Gradle and Maven with auto-import 39 | # When using Gradle or Maven with auto-import, you should exclude module files, 40 | # since they will be recreated, and may cause churn. Uncomment if using 41 | # auto-import. 42 | # .idea/modules.xml 43 | # .idea/*.iml 44 | # .idea/modules 45 | 46 | # CMake 47 | cmake-build-*/ 48 | 49 | # Mongo Explorer plugin 50 | .idea/**/mongoSettings.xml 51 | 52 | # File-based project format 53 | *.iws 54 | 55 | # IntelliJ 56 | out/ 57 | 58 | # mpeltonen/sbt-idea plugin 59 | .idea_modules/ 60 | 61 | # JIRA plugin 62 | atlassian-ide-plugin.xml 63 | 64 | # Cursive Clojure plugin 65 | .idea/replstate.xml 66 | 67 | # Crashlytics plugin (for Android Studio and IntelliJ) 68 | com_crashlytics_export_strings.xml 69 | crashlytics.properties 70 | crashlytics-build.properties 71 | fabric.properties 72 | 73 | # Editor-based Rest Client 74 | .idea/httpRequests 75 | 76 | # Android studio 3.1+ serialized cache file 77 | .idea/caches/build_file_checksums.ser 78 | 79 | #dotenv 80 | .env 81 | 82 | # rollup-plugin-typescript2 83 | .rpt2_cache 84 | 85 | # rust/wasm 86 | .wasm 87 | target 88 | pkg 89 | Cargo.lock 90 | **/*.rs.bk 91 | 92 | # node 93 | npm-debug.log 94 | node_modules 95 | bower_components 96 | 97 | # build and deploy 98 | dist 99 | deploy 100 | 101 | # Swap 102 | [._]*.s[a-v][a-z] 103 | [._]*.sw[a-p] 104 | [._]s[a-v][a-z] 105 | [._]sw[a-p] 106 | 107 | # Session 108 | Session.vim 109 | 110 | # Temporary 111 | .netrwhist 112 | *~ 113 | # Auto-generated tag files 114 | tags 115 | 116 | #### OSX 117 | 118 | # General 119 | *.DS_Store 120 | .AppleDouble 121 | .LSOverride 122 | 123 | # Icon must end with two \r 124 | Icon 125 | 126 | 127 | # Thumbnails 128 | ._* 129 | 130 | # Files that might appear in the root of a volume 131 | .DocumentRevisions-V100 132 | .fseventsd 133 | .Spotlight-V100 134 | .TemporaryItems 135 | .Trashes 136 | .VolumeIcon.icns 137 | .com.apple.timemachine.donotpresent 138 | 139 | # Directories potentially created on remote AFP share 140 | .AppleDB 141 | .AppleDesktop 142 | Network Trash Folder 143 | Temporary Items 144 | .apdisk 145 | 146 | ##### Windows 147 | 148 | # Windows thumbnail cache files 149 | Thumbs.db 150 | ehthumbs.db 151 | ehthumbs_vista.db 152 | 153 | # Dump file 154 | *.stackdump 155 | 156 | # Folder config file 157 | Desktop.ini 158 | 159 | # Recycle Bin used on file shares 160 | $RECYCLE.BIN/ 161 | 162 | # Windows Installer files 163 | *.cab 164 | *.msi 165 | *.msm 166 | *.msp 167 | 168 | # Windows shortcuts 169 | *.lnk 170 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | awsm is a work-in-progress collection of crates for rust-powered wasm. The primary goal is 3d games, simulations, and other high-performance web apps. 4 | 5 | ## Crates 6 | 7 | * [awsm_web](https://github.com/dakom/awsm-web): The foundation for everything else 8 | [CRATES.IO](https://crates.io/crates/awsm_web) - [DOCS](https://docs.rs/awsm_web) 9 | 10 | 11 | * [awsm_renderer](https://github.com/dakom/awsm-renderer) (r&d / pre-production): a 3d renderer 12 | [CRATES.IO](https://crates.io/crates/awsm_renderer) - [DOCS](https://docs.rs/awsm_renderer) 13 | 14 | ## Live Coding 15 | 16 | As a way to self-motivate getting over the learning curve, I thought it might help to livestream the coding+learning sessions 17 | 18 | On twitch: https://www.twitch.tv/dakomz 19 | -------------------------------------------------------------------------------- /to-move/renderer-demo/.gitignore: -------------------------------------------------------------------------------- 1 | #dotenv 2 | .env 3 | 4 | # rollup-plugin-typescript2 5 | .rpt2_cache 6 | 7 | # 8 | # rust/wasm 9 | .wasm 10 | target 11 | pkg 12 | # apps should allow Cargo.lock, but libraries shouldn't. See https://doc.rust-lang.org/cargo/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries 13 | # Cargo.lock 14 | **/*.rs.bk 15 | 16 | # build and deploy 17 | dist 18 | deploy 19 | 20 | # Netlify 21 | .netlify 22 | 23 | # Logs 24 | logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | lerna-debug.log* 30 | 31 | # Diagnostic reports (https://nodejs.org/api/report.html) 32 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 33 | 34 | # Runtime data 35 | pids 36 | *.pid 37 | *.seed 38 | *.pid.lock 39 | 40 | # Directory for instrumented libs generated by jscoverage/JSCover 41 | lib-cov 42 | 43 | # Coverage directory used by tools like istanbul 44 | coverage 45 | *.lcov 46 | 47 | # nyc test coverage 48 | .nyc_output 49 | 50 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 51 | .grunt 52 | 53 | # Bower dependency directory (https://bower.io/) 54 | bower_components 55 | 56 | # node-waf configuration 57 | .lock-wscript 58 | 59 | # Compiled binary addons (https://nodejs.org/api/addons.html) 60 | build/Release 61 | 62 | # Dependency directories 63 | node_modules/ 64 | jspm_packages/ 65 | 66 | # TypeScript v1 declaration files 67 | typings/ 68 | 69 | # TypeScript cache 70 | *.tsbuildinfo 71 | 72 | # Optional npm cache directory 73 | .npm 74 | 75 | # Optional eslint cache 76 | .eslintcache 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variables file 88 | .env 89 | .env.test 90 | 91 | # parcel-bundler cache (https://parceljs.org/) 92 | .cache 93 | 94 | # next.js build output 95 | .next 96 | 97 | # nuxt.js build output 98 | .nuxt 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | 113 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 114 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 115 | 116 | 117 | # User-specific stuff 118 | .idea/**/workspace.xml 119 | .idea/**/tasks.xml 120 | .idea/**/usage.statistics.xml 121 | .idea/**/dictionaries 122 | .idea/**/shelf 123 | 124 | # Generated files 125 | .idea/**/contentModel.xml 126 | 127 | # Sensitive or high-churn files 128 | .idea/**/dataSources/ 129 | .idea/**/dataSources.ids 130 | .idea/**/dataSources.local.xml 131 | .idea/**/sqlDataSources.xml 132 | .idea/**/dynamic.xml 133 | .idea/**/uiDesigner.xml 134 | .idea/**/dbnavigator.xml 135 | 136 | # Gradle 137 | .idea/**/gradle.xml 138 | .idea/**/libraries 139 | 140 | # Gradle and Maven with auto-import 141 | # When using Gradle or Maven with auto-import, you should exclude module files, 142 | # since they will be recreated, and may cause churn. Uncomment if using 143 | # auto-import. 144 | # .idea/modules.xml 145 | # .idea/*.iml 146 | # .idea/modules 147 | 148 | # CMake 149 | cmake-build-*/ 150 | 151 | # Mongo Explorer plugin 152 | .idea/**/mongoSettings.xml 153 | 154 | # File-based project format 155 | *.iws 156 | 157 | # IntelliJ 158 | out/ 159 | 160 | # mpeltonen/sbt-idea plugin 161 | .idea_modules/ 162 | 163 | # JIRA plugin 164 | atlassian-ide-plugin.xml 165 | 166 | # Cursive Clojure plugin 167 | .idea/replstate.xml 168 | 169 | # Crashlytics plugin (for Android Studio and IntelliJ) 170 | com_crashlytics_export_strings.xml 171 | crashlytics.properties 172 | crashlytics-build.properties 173 | fabric.properties 174 | 175 | # Editor-based Rest Client 176 | .idea/httpRequests 177 | 178 | # Android studio 3.1+ serialized cache file 179 | .idea/caches/build_file_checksums.ser 180 | 181 | 182 | # Swap 183 | [._]*.s[a-v][a-z] 184 | [._]*.sw[a-p] 185 | [._]s[a-v][a-z] 186 | [._]sw[a-p] 187 | 188 | # Session 189 | Session.vim 190 | 191 | # Temporary 192 | .netrwhist 193 | *~ 194 | # Auto-generated tag files 195 | tags 196 | 197 | #### OSX 198 | 199 | # General 200 | *.DS_Store 201 | .AppleDouble 202 | .LSOverride 203 | 204 | # Icon must end with two \r 205 | Icon 206 | 207 | 208 | # Thumbnails 209 | ._* 210 | 211 | # Files that might appear in the root of a volume 212 | .DocumentRevisions-V100 213 | .fseventsd 214 | .Spotlight-V100 215 | .TemporaryItems 216 | .Trashes 217 | .VolumeIcon.icns 218 | .com.apple.timemachine.donotpresent 219 | 220 | # Directories potentially created on remote AFP share 221 | .AppleDB 222 | .AppleDesktop 223 | Network Trash Folder 224 | Temporary Items 225 | .apdisk 226 | 227 | ##### Windows 228 | 229 | # Windows thumbnail cache files 230 | Thumbs.db 231 | ehthumbs.db 232 | ehthumbs_vista.db 233 | 234 | # Dump file 235 | *.stackdump 236 | 237 | # Folder config file 238 | Desktop.ini 239 | 240 | # Recycle Bin used on file shares 241 | $RECYCLE.BIN/ 242 | 243 | # Windows Installer files 244 | *.cab 245 | *.msi 246 | *.msm 247 | *.msp 248 | 249 | # Windows shortcuts 250 | *.lnk 251 | -------------------------------------------------------------------------------- /to-move/renderer-demo/Makefile.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | skip_core_tasks = true 3 | 4 | ################## 5 | ## Main Entries ## 6 | ################## 7 | 8 | [tasks.build] 9 | run_task = [ 10 | {name = "build-development", condition = { profiles = [ "development"] } }, 11 | {name = "build-production", condition = { profiles = [ "production"] } }, 12 | ] 13 | 14 | ##See: https://github.com/sagiegurari/cargo-make/issues/355 15 | [tasks.serve] 16 | script = [''' 17 | echo waiting on https://github.com/sagiegurari/cargo-make/issues/355 18 | echo in the meantime use npm start 19 | '''] 20 | 21 | # [tasks.serve] 22 | # run_task = [{name = [ 23 | # "watch-rust", 24 | # "serve-webpack", 25 | # ], parallel = true}] 26 | 27 | [tasks.test] 28 | run_task = [{name = [ 29 | "test-rust-unit", 30 | "test-interop", 31 | ]}] 32 | 33 | [tasks.clean] 34 | run_task = [{name = [ 35 | "clean-rust", 36 | "clean-artifacts", 37 | ]}] 38 | 39 | ######################## 40 | ## Development Builds ## 41 | ######################## 42 | 43 | [tasks.build-development] 44 | run_task = [{name = [ 45 | "clean", 46 | "build-development-rust", 47 | "build-development-webpack", 48 | "copy-static" 49 | ]}] 50 | 51 | [tasks.build-development-rust] 52 | command = "cargo" 53 | args = ["make", "build", "--profile", "development"] 54 | cwd = "rust/demo" 55 | 56 | [tasks.build-development-webpack] 57 | script = ["npx webpack --progress --color --config webpack.dev.js"] 58 | 59 | ########################## 60 | ## Watchers and Servers ## 61 | ########################## 62 | 63 | [tasks.watch-rust] 64 | command = "cargo" 65 | args = ["make", "watch"] 66 | cwd = "rust/demo" 67 | 68 | ##See: https://github.com/sagiegurari/cargo-make/issues/355 69 | # [tasks.serve-webpack] 70 | # script = ["npx webpack-dev-server --config webpack.dev.js"] 71 | 72 | ####################### 73 | ## Production Builds ## 74 | ####################### 75 | 76 | [tasks.build-production] 77 | run_task = [{name = [ 78 | "clean", 79 | "build-production-rust", 80 | "build-production-webpack", 81 | "copy-static" 82 | ]}] 83 | 84 | [tasks.build-production-rust] 85 | command = "cargo" 86 | args = ["make", "build", "--profile", "production"] 87 | cwd = "rust/demo" 88 | 89 | 90 | [tasks.build-production-webpack] 91 | script = ["npx webpack --progress --color --config webpack.release.js"] 92 | 93 | ################## 94 | ## Copy Static ## 95 | ################# 96 | 97 | [tasks.copy-static] 98 | script_runner = "@shell" 99 | script = ["cp -R ./_static/* ./dist/"] 100 | 101 | ############### 102 | #### Tests #### 103 | ############### 104 | 105 | [tasks.test-rust-unit] 106 | command = "cargo" 107 | args = ["make", "test-unit"] 108 | cwd = "rust/demo" 109 | 110 | [tasks.test-interop] 111 | dependencies = ["build-rust-test-interop"] 112 | script = ["npx jest"] 113 | 114 | [tasks.build-rust-test-interop] 115 | command = "cargo" 116 | args = ["make", "build-test-interop"] 117 | cwd = "rust/demo" 118 | 119 | ############### 120 | #### Cleanup ## 121 | ############### 122 | 123 | [tasks.clean-rust] 124 | command = "cargo" 125 | args = ["make", "clean"] 126 | cwd = "rust/demo" 127 | 128 | [tasks.clean-artifacts] 129 | script_runner = "@shell" 130 | ignore_errors = true 131 | script = [ 132 | "rm -rf ./dist", 133 | "rm -rf ./_static/wasm/demo/pkg", 134 | ] -------------------------------------------------------------------------------- /to-move/renderer-demo/_static/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /to-move/renderer-demo/_static/wasm/demo/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /to-move/renderer-demo/build-utils/local-media-server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | if(!process.env.DEVELOPER || process.env.DEVELOPER === "") { 4 | console.log("Local CDN: set [DEVELOPER] in .env"); 5 | process.exit(0); 6 | } 7 | 8 | const paths = { 9 | david: (osPlatform) => { 10 | switch(osPlatform) { 11 | default: return `C:\\Users\\david\\Documents\\projects\\khronos\\glTF-Sample-Models\\2.0` 12 | } 13 | }, 14 | david_laptop: (osPlatform) => { 15 | switch(osPlatform) { 16 | default: return `C:\\Users\\david\\Documents\\github\\khronos\\glTF-Sample-Models\\2.0` 17 | } 18 | }, 19 | } 20 | 21 | const os = require('os'); 22 | const path = require('path'); 23 | const fs = require('fs'); 24 | 25 | const localPath = path.resolve( 26 | paths[process.env.DEVELOPER.toLowerCase()] (os.platform()) 27 | ); 28 | 29 | const express = require('express'); 30 | const cors = require('cors'); 31 | const serveIndex = require('serve-index'); 32 | 33 | const app = express(); 34 | 35 | app.options('*', cors()); 36 | app.use(cors()); 37 | app.use(express.static(localPath), serveIndex(localPath, {'icons': true})); 38 | 39 | 40 | //If you change it here - also change: 41 | //1. config/Config.ts 42 | //2. build-utils/transform-css.js (if exists) 43 | app.listen(4102, () => console.log('Local CDN Started!')) -------------------------------------------------------------------------------- /to-move/renderer-demo/jest.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: 'tsconfig.json' 8 | } 9 | } 10 | }; -------------------------------------------------------------------------------- /to-move/renderer-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my_app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cargo make build-development-rust && npm-run-all -p _start:rust _start:webpack", 8 | "_start:rust": "makers watch-rust", 9 | "_start:webpack": "webpack-dev-server --config webpack.dev.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/dakom/my_app.git" 14 | }, 15 | "author": "David Komer", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/dakom/my_app/issues" 19 | }, 20 | "homepage": "https://github.com/dakom/my_app#readme", 21 | "devDependencies": { 22 | "@types/dat.gui": "^0.7.5", 23 | "@types/jest": "^24.0.23", 24 | "cors": "^2.8.5", 25 | "cross-env": "^6.0.3", 26 | "css-loader": "^3.4.2", 27 | "dotenv": "^8.2.0", 28 | "eslint": "^6.7.2", 29 | "extract-loader": "^3.1.0", 30 | "fork-ts-checker-notifier-webpack-plugin": "^1.0.2", 31 | "fork-ts-checker-webpack-plugin": "^3.1.1", 32 | "html-webpack-plugin": "^3.2.0", 33 | "jest": "^24.9.0", 34 | "lit-css-loader": "0.0.3", 35 | "npm-run-all": "^4.1.5", 36 | "script-ext-html-webpack-plugin": "^2.1.4", 37 | "style-loader": "^1.1.3", 38 | "ts-jest": "^24.2.0", 39 | "ts-loader": "^6.2.1", 40 | "tslib": "^1.10.0", 41 | "typescript": "^3.7.3", 42 | "webpack": "^4.41.2", 43 | "webpack-cli": "^3.3.10", 44 | "webpack-dev-server": "^3.9.0" 45 | }, 46 | "dependencies": { 47 | "dat.gui": "^0.7.6", 48 | "lit-html": "^1.1.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "my_demo" 3 | version = "0.1.0" 4 | authors = ["David Komer "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "0.2.59" 12 | js-sys = "0.3.36" 13 | cfg-if = "0.1.10" 14 | log = "0.4.8" 15 | nalgebra = "0.20.0" 16 | float-cmp = "0.6.0" 17 | serde = { version = "1.0.104", features = ["derive"] } 18 | serde-wasm-bindgen = "0.1.3" 19 | num-traits = "0.2.11" 20 | num-derive = "0.3.0" 21 | wee_alloc = { version = "0.4.5", optional = true } 22 | shipyard = { git= "https://github.com/leudz/shipyard.git", features = ["proc"], default-features = false} 23 | awsm_renderer = { path="../../../../crates/renderer", version = "0.0.1"} 24 | awsm_web = { path="../../../../crates/web", features = ["tick"], default-features = false } 25 | wasm-bindgen-futures= "0.4.9" 26 | wasm-logger = { version = "0.2.0", optional = true } 27 | console_error_panic_hook = { version = "0.1.6", optional = true } 28 | web-sys = { version = "0.3.36", features = [ 'Event', 'HtmlCanvasElement' ] } 29 | 30 | [features] 31 | # TODO - remove these when we can really use dev feature (see https://users.rust-lang.org/t/dependencies-based-on-profile/32386/4) 32 | default = ["wee_alloc", "wasm-logger", "console_error_panic_hook"] 33 | dev = ["wee_alloc", "wasm-logger", "console_error_panic_hook"] 34 | -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/Makefile.toml: -------------------------------------------------------------------------------- 1 | [config] 2 | skip_core_tasks = true 3 | 4 | ################## 5 | ## Main Entries ## 6 | ################## 7 | 8 | [tasks.build] 9 | run_task = [ 10 | {name = "build-development", condition = { profiles = [ "development"] } }, 11 | {name = "build-production", condition = { profiles = [ "production"] } }, 12 | ] 13 | 14 | [tasks.watch] 15 | command = "watchexec" 16 | args = ["-w", "./src", "makers", "build-development"] 17 | # run_task = "build-development" 18 | # watch = true 19 | 20 | [tasks.test-unit] 21 | command = "cargo" 22 | args = ["test"] 23 | toolchain = "nightly" 24 | 25 | [tasks.clean] 26 | script_runner = "@shell" 27 | ignore_errors = true 28 | script = [ 29 | "rm -rf ./target", 30 | "rm -rf ./pkg" 31 | ] 32 | 33 | ################# 34 | ## Development ## 35 | ################# 36 | 37 | [tasks.build-development] 38 | run_task = [{name = [ 39 | "compile-development", 40 | "bindgen-development" 41 | ]}] 42 | 43 | [tasks.compile-development] 44 | command = "cargo" 45 | args = ["build","--features","dev","--target","wasm32-unknown-unknown"] 46 | toolchain = "nightly" 47 | 48 | [tasks.bindgen-development] 49 | command = "wasm-bindgen" 50 | args = ["./target/wasm32-unknown-unknown/debug/my_demo.wasm","--debug", "--keep-debug", "--target","web","--out-dir","../../_static/wasm/demo/pkg"] 51 | 52 | 53 | ################ 54 | ## Production ## 55 | ################ 56 | 57 | [tasks.build-production] 58 | run_task = [{name = [ 59 | "compile-production", 60 | "bindgen-production", 61 | "wasmopt-production", 62 | ]}] 63 | 64 | [tasks.compile-production] 65 | command = "cargo" 66 | args = ["build","--release","--target","wasm32-unknown-unknown"] 67 | toolchain = "nightly" 68 | 69 | [tasks.bindgen-production] 70 | command = "wasm-bindgen" 71 | args = ["./target/wasm32-unknown-unknown/release/my_demo.wasm", "--target","web","--out-dir","../../_static/wasm/demo/pkg"] 72 | 73 | [tasks.wasmopt-production] 74 | command = "wasm-opt" 75 | # if CI fails, try with BINARYEN_CORES=1 in an env 76 | args = ["-O3","-o","../../_static/wasm/demo/pkg/my_demo.wasm", "../../_static/wasm/demo/pkg/my_demo_bg.wasm"] 77 | 78 | ############### 79 | #### Tests #### 80 | ############### 81 | 82 | [tasks.build-test-interop] 83 | run_task = [{name = [ 84 | "compile-test-interop", 85 | "bindgen-test-interop", 86 | ]}] 87 | 88 | [tasks.compile-test-interop] 89 | command = "cargo" 90 | args = ["build","--features","ts_test","--target","wasm32-unknown-unknown"] 91 | toolchain = "nightly" 92 | 93 | [tasks.bindgen-test-interop] 94 | command = "wasm-bindgen" 95 | args = ["./target/wasm32-unknown-unknown/debug/my_demo.wasm", "--target", "nodejs", "--out-dir", "../../_static/wasm/demo/pkg"] -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/events/event_sender.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use super::{BridgeEvent}; 3 | 4 | 5 | #[derive(Clone)] 6 | pub struct EventSender { 7 | _send_event: js_sys::Function, 8 | } 9 | 10 | impl EventSender { 11 | pub fn new(send_event:js_sys::Function) -> Self { 12 | Self{ 13 | _send_event: send_event 14 | } 15 | } 16 | 17 | pub fn send(&self, evt:BridgeEvent) { 18 | let evt = match evt { 19 | BridgeEvent::GltfLoaded => Some((BridgeEvent::GltfLoaded, JsValue::UNDEFINED)), 20 | _ => None 21 | }; 22 | 23 | if let Some((evt_type, evt_data)) = evt { 24 | //Even though we're ultimately going from Rust -> rustc 25 | //We're going by way of a worker which uses plain JS objects 26 | //In the future maybe we can do shared memory! 27 | 28 | let evt_type:u32 = evt_type as u32; 29 | let evt_type = JsValue::from(evt_type); 30 | 31 | let this = JsValue::NULL; 32 | self._send_event.call2(&this, &evt_type, &evt_data).unwrap(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/events/events.rs: -------------------------------------------------------------------------------- 1 | use num_derive::FromPrimitive; 2 | use num_traits::FromPrimitive; 3 | use serde::{Serialize, Deserialize}; 4 | use std::convert::TryFrom; 5 | 6 | //the order must match typescript! 7 | #[derive(FromPrimitive)] 8 | #[repr(u32)] 9 | pub enum BridgeEvent { 10 | WindowSize, 11 | LoadGltf, 12 | GltfLoaded, 13 | CameraSettings, 14 | Clear 15 | } 16 | 17 | //Let's us get a BridgeEvent from the number which is sent from JS 18 | impl TryFrom for BridgeEvent { 19 | type Error = &'static str; 20 | 21 | fn try_from(value: u32) -> Result { 22 | FromPrimitive::from_u32(value).ok_or("BridgeEvent: outside of range!") 23 | } 24 | } 25 | 26 | //All the event data: 27 | 28 | #[derive(Serialize, Deserialize)] 29 | pub struct WindowSize { 30 | pub width: u32, 31 | pub height: u32 32 | } 33 | 34 | 35 | #[derive(Serialize, Deserialize)] 36 | pub struct CameraSettings { 37 | pub style: u32, 38 | pub xmag: Option, 39 | pub ymag: Option, 40 | pub znear: Option, 41 | pub zfar: Option, 42 | pub aspectRatio: Option, 43 | pub yfov: Option, 44 | pub positionX: f64, 45 | pub positionY: f64, 46 | pub positionZ: f64, 47 | } 48 | 49 | 50 | #[derive(FromPrimitive)] 51 | #[repr(u32)] 52 | pub enum CameraStyle { 53 | Orthographic, 54 | Perspective 55 | } 56 | impl TryFrom for CameraStyle { 57 | type Error = &'static str; 58 | 59 | fn try_from(value: u32) -> Result { 60 | FromPrimitive::from_u32(value).ok_or("CameraStyle: outside of range!") 61 | } 62 | } -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/events/handle_events.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use log::{info}; 3 | use std::convert::TryInto; 4 | use awsm_renderer::Renderer; 5 | use awsm_renderer::gltf::loader::{load_gltf}; 6 | use awsm_renderer::nodes::{NodeData}; 7 | use awsm_renderer::camera::{get_orthographic_projection, get_perspective_projection}; 8 | use awsm_renderer::transform::{Vector3, Matrix4, TransformValues, Quaternion}; 9 | use super::events; 10 | use super::event_sender::EventSender; 11 | use super::{BridgeEvent}; 12 | use crate::state::{CameraSettings, OrthographicCamera, PerspectiveCamera}; 13 | use crate::state::{State, self}; 14 | use std::rc::Rc; 15 | use std::cell::RefCell; 16 | use wasm_bindgen_futures::future_to_promise; 17 | //if result is Ok(true) then send the updated state back 18 | 19 | pub fn handle_event(evt_type:u32, evt_data: JsValue, state: Rc>, renderer:Rc>, event_sender:Rc) -> Result<(), JsValue> 20 | { 21 | let evt_type:BridgeEvent = evt_type.try_into()?; 22 | 23 | match evt_type { 24 | BridgeEvent::WindowSize => 25 | { 26 | let window_size:events::WindowSize = serde_wasm_bindgen::from_value(evt_data)?; 27 | state.borrow_mut().window_size = window_size; 28 | update_view(state, renderer)?; 29 | }, 30 | 31 | BridgeEvent::LoadGltf => 32 | { 33 | let filepath:String = serde_wasm_bindgen::from_value(evt_data)?; 34 | 35 | future_to_promise({ 36 | async move { 37 | let resource = load_gltf(&filepath, None).await?; 38 | let mut renderer = renderer.borrow_mut(); 39 | 40 | //maybe upload_gltf should return entity list so it can be mixed in... 41 | renderer.upload_gltf(&resource, None)?; 42 | 43 | renderer.set_scene_from_gltf(&resource.gltf); 44 | 45 | event_sender.send(BridgeEvent::GltfLoaded); 46 | Ok(JsValue::null()) 47 | } 48 | }); 49 | 50 | }, 51 | 52 | BridgeEvent::CameraSettings => 53 | { 54 | let camera_settings:events::CameraSettings = serde_wasm_bindgen::from_value(evt_data)?; 55 | match camera_settings.style.try_into()? { 56 | events::CameraStyle::Orthographic => { 57 | state.borrow_mut().camera_settings = Some(CameraSettings::Orthographic(OrthographicCamera{ 58 | xmag: camera_settings.xmag.unwrap(), 59 | ymag: camera_settings.ymag.unwrap(), 60 | znear: camera_settings.znear.unwrap(), 61 | zfar: camera_settings.zfar.unwrap(), 62 | translation: Vector3::new( camera_settings.positionX,camera_settings.positionY,camera_settings.positionZ) 63 | })); 64 | }, 65 | events::CameraStyle::Perspective => { 66 | state.borrow_mut().camera_settings = Some(CameraSettings::Perspective(PerspectiveCamera{ 67 | aspectRatio: camera_settings.aspectRatio.unwrap(), 68 | yfov: camera_settings.yfov.unwrap(), 69 | znear: camera_settings.znear.unwrap(), 70 | zfar: camera_settings.zfar.unwrap(), 71 | translation: Vector3::new( camera_settings.positionX,camera_settings.positionY,camera_settings.positionZ) 72 | })); 73 | } 74 | }; 75 | 76 | update_view(state, renderer)?; 77 | }, 78 | BridgeEvent::Clear => 79 | { 80 | info!("clearing all"); 81 | } 82 | _ => 83 | { 84 | info!("unknown event!"); 85 | } 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | fn update_view(state: Rc>, renderer:Rc>) -> Result<(), JsValue> { 92 | 93 | let mut renderer = renderer.borrow_mut(); 94 | let mut state = state.borrow_mut(); 95 | let events::WindowSize {width, height} = state.window_size; 96 | renderer.resize(width, height); 97 | 98 | if let Some((camera_translation, camera_projection)) = match &state.camera_settings { 99 | Some(CameraSettings::Orthographic(settings)) => { 100 | //scale ymag to keep things square with screen size 101 | let ratio = (height as f64) / (width as f64); 102 | let mut ymag = ratio * settings.ymag; 103 | 104 | //scale xmag and ymag by 2.0 just to fit content on screen probably 105 | let xmag = settings.xmag * 2.0; 106 | ymag *= 2.0; 107 | 108 | Some((settings.translation.clone(), get_orthographic_projection(xmag, ymag, settings.znear, settings.zfar))) 109 | }, 110 | Some(CameraSettings::Perspective(settings)) => { 111 | Some((settings.translation.clone(), get_perspective_projection(settings.aspectRatio, settings.yfov, settings.znear, Some(settings.zfar)))) 112 | }, 113 | None => None 114 | } { 115 | //let rotation = Some(Quaternion::new(0.0, 1.0, 0.0, 0.0)); 116 | let rotation = None; 117 | let translation = Some(camera_translation); 118 | if let Some(camera_node) = renderer.get_camera_node() { 119 | renderer.set_node_trs(camera_node, translation, rotation, None); 120 | renderer.update_camera_projection(None, camera_projection.as_ref()); 121 | } else { 122 | renderer.add_node(NodeData::Camera(camera_projection), None, translation, rotation, None)?; 123 | } 124 | } 125 | Ok(()) 126 | } -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | mod handle_events; 2 | mod events; 3 | mod event_sender; 4 | 5 | pub use self::handle_events::*; 6 | pub use self::events::*; 7 | pub use self::event_sender::*; -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/game_loop/game_loop.rs: -------------------------------------------------------------------------------- 1 | use awsm_web::tick::{MainLoop, MainLoopOptions, RafLoop}; 2 | use awsm_renderer::Renderer; 3 | 4 | use std::cell::RefCell; 5 | use std::rc::Rc; 6 | use wasm_bindgen::prelude::*; 7 | 8 | pub struct GameLoop { 9 | _raf_loop:RafLoop 10 | } 11 | 12 | impl GameLoop { 13 | pub fn new(renderer:Rc>) -> Result { 14 | // loop was ported from https://github.com/IceCreamYou/MainLoop.js#usage 15 | let begin = |_time, _delta| { }; 16 | 17 | let update = { 18 | let renderer = Rc::clone(&renderer); 19 | move |delta| { 20 | let mut renderer = renderer.borrow_mut(); 21 | renderer.animate(delta); 22 | } 23 | }; 24 | 25 | let draw = { 26 | let renderer = Rc::clone(&renderer); 27 | move |interpolation| { 28 | let mut renderer = renderer.borrow_mut(); 29 | 30 | renderer.clear(); 31 | renderer.render(Some(interpolation)); 32 | } 33 | }; 34 | 35 | let end = |_fps, _abort| { }; 36 | 37 | let raf_loop = RafLoop::start({ 38 | let mut main_loop = MainLoop::new(MainLoopOptions::default(), begin, update, draw, end); 39 | move |ts| { 40 | main_loop.tick(ts); 41 | } 42 | })?; 43 | 44 | Ok(Self{ 45 | _raf_loop: raf_loop 46 | }) 47 | } 48 | } -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/game_loop/mod.rs: -------------------------------------------------------------------------------- 1 | mod game_loop; 2 | 3 | pub use self::game_loop::*; -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/lib.rs: -------------------------------------------------------------------------------- 1 | use log::{info, Level}; 2 | use wasm_bindgen::prelude::*; 3 | use web_sys::{HtmlCanvasElement}; 4 | 5 | #[global_allocator] 6 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 7 | 8 | cfg_if::cfg_if! { 9 | if #[cfg(all(feature = "wasm-logger", feature = "console_error_panic_hook", debug_assertions))] { 10 | fn setup() { 11 | wasm_logger::init(wasm_logger::Config::default()); 12 | console_error_panic_hook::set_once(); 13 | log::info!("rust logging enabled!"); 14 | } 15 | } else { 16 | fn setup() { 17 | log::info!("rust logging disabled!"); //<-- won't be seen 18 | } 19 | } 20 | } 21 | 22 | pub fn run(canvas:HtmlCanvasElement, window_width: u32, window_height: u32, send_bridge_event:js_sys::Function) -> Result { 23 | Err(JsValue::from_str("resurecting!")) 24 | } 25 | 26 | /*#[allow(clippy::module_inception)] 27 | mod events; 28 | #[allow(clippy::module_inception)] 29 | mod game_loop; 30 | #[allow(clippy::module_inception)] 31 | mod state; 32 | 33 | use cfg_if::cfg_if; 34 | use log::{info, Level}; 35 | use wasm_bindgen::prelude::*; 36 | use std::rc::Rc; 37 | use std::cell::RefCell; 38 | use crate::game_loop::GameLoop; 39 | use crate::events::*; 40 | use crate::state::*; 41 | use awsm_renderer::{ Renderer}; 42 | use awsm_renderer::webgl::{ 43 | get_webgl_context_2, 44 | WebGlContextOptions, 45 | WebGl2Renderer, 46 | }; 47 | use web_sys::{HtmlCanvasElement}; 48 | 49 | #[global_allocator] 50 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 51 | 52 | cfg_if! { 53 | if #[cfg(all(feature = "wasm-logger", feature = "console_error_panic_hook", debug_assertions))] { 54 | fn setup() { 55 | wasm_logger::init(wasm_logger::Config::default()); 56 | console_error_panic_hook::set_once(); 57 | log::info!("rust logging enabled!"); 58 | } 59 | } else { 60 | fn setup() { 61 | log::info!("rust logging disabled!"); //<-- won't be seen 62 | } 63 | } 64 | } 65 | 66 | // Called by our JS entry point to run the example. 67 | #[wasm_bindgen] 68 | pub fn run(canvas:HtmlCanvasElement, window_width: u32, window_height: u32, send_bridge_event:js_sys::Function) -> Result { 69 | setup(); 70 | 71 | let event_sender = Rc::new(EventSender::new(send_bridge_event)); 72 | let webgl = get_webgl_context_2(&canvas, Some(&WebGlContextOptions{ 73 | alpha: false, 74 | ..WebGlContextOptions::default() 75 | }))?; 76 | let webgl = WebGl2Renderer::new(webgl)?; 77 | 78 | webgl.gl.clear_color(0.5, 0.5, 0.5, 1.0); 79 | 80 | let renderer = Renderer::new(Rc::new(RefCell::new(webgl)), None, window_width, window_height)?; 81 | let renderer = Rc::new(RefCell::new(renderer)); 82 | 83 | let game_loop = Box::new({ 84 | let renderer = Rc::clone(&renderer); 85 | GameLoop::new(renderer)? 86 | }); 87 | 88 | let state = Rc::new(RefCell::new(State{ 89 | camera_settings: None, 90 | window_size: WindowSize{ 91 | width: window_width, 92 | height: window_height 93 | } 94 | })); 95 | 96 | //Create a function which allows JS to send us events ad-hoc 97 | //We will need to get a handle and forget the Closure 98 | //See https://stackoverflow.com/a/53219594/784519 99 | let _send_event = Box::new({ 100 | move |evt_type:u32, data:JsValue| { 101 | let renderer = Rc::clone(&renderer); 102 | let event_sender = Rc::clone(&event_sender); 103 | let state = Rc::clone(&state); 104 | //The actual handling of events is in this function 105 | if let Err(reason) = handle_event(evt_type, data, state, renderer, event_sender) { 106 | info!("Error: {:?}", reason); 107 | } 108 | } 109 | }) as Box ()>; 110 | 111 | let _send_event = Closure::wrap(_send_event); 112 | 113 | let send_event = _send_event.as_ref().clone(); 114 | 115 | //forget the things that need to persist in memory 116 | std::mem::forget(game_loop); 117 | _send_event.forget(); 118 | 119 | Ok(send_event) 120 | } 121 | */ -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | mod state; 2 | 3 | pub use self::state::*; -------------------------------------------------------------------------------- /to-move/renderer-demo/rust/demo/src/state/state.rs: -------------------------------------------------------------------------------- 1 | use crate::events; 2 | use awsm_renderer::transform::Vector3; 3 | use shipyard::prelude::*; 4 | 5 | pub struct State { 6 | pub camera_settings: Option, 7 | pub window_size: events::WindowSize, 8 | } 9 | 10 | pub enum CameraSettings { 11 | Orthographic(OrthographicCamera), 12 | Perspective(PerspectiveCamera) 13 | } 14 | 15 | pub struct OrthographicCamera { 16 | pub xmag: f64, 17 | pub ymag: f64, 18 | pub znear: f64, 19 | pub zfar: f64, 20 | pub translation: Vector3, 21 | } 22 | 23 | pub struct PerspectiveCamera { 24 | pub aspectRatio: f64, 25 | pub yfov: f64, 26 | pub znear: f64, 27 | pub zfar: f64, 28 | pub translation: Vector3, 29 | } 30 | -------------------------------------------------------------------------------- /to-move/renderer-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "sourceMap": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "diagnostics": true, 9 | "outDir": "dist", 10 | "allowJs": true, 11 | "lib": [ 12 | "esnext", 13 | "es6", 14 | "es5", 15 | "dom" 16 | ], 17 | "baseUrl": ".", 18 | "paths": { 19 | "*": [ 20 | "node_modules/@types/*", 21 | "*" 22 | ], 23 | "@events/*": ["typescript/events/*"], 24 | "@state/*": ["typescript/state/*"], 25 | "@ui/*": ["typescript/ui/*"], 26 | "@utils/*": ["typescript/utils/*"], 27 | "@config/*": ["typescript/config/*"], 28 | }, 29 | }, 30 | "include": [ 31 | "./typescript/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules", 35 | "dist", 36 | "build", 37 | "coverage" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/config/config.ts: -------------------------------------------------------------------------------- 1 | export const buildMode = process.env['NODE_ENV']; 2 | export const buildVersion = process.env['BUILD_VERSION']; 3 | export const isProduction = buildMode === "production" ? true : false; 4 | 5 | export const SamplesUrlBase = !isProduction ? `http://localhost:4102` : "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0"; 6 | 7 | interface DebugSettings { 8 | model_idx: number; 9 | variant_idx: number; 10 | camera_style: "orthographic" | "perspective" 11 | } 12 | 13 | const devDebugSettings:DebugSettings = { 14 | model_idx: 51, //Triangle 15 | variant_idx: 0, //Embedded 16 | //variant_idx: 1, //Embedded 17 | camera_style: "perspective" 18 | } 19 | 20 | const prodDebugSettings:DebugSettings = { 21 | model_idx: 0, 22 | variant_idx: 0, 23 | camera_style: "perspective" 24 | } 25 | 26 | export const debug_settings = isProduction ? prodDebugSettings : devDebugSettings; 27 | -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/entry/index.css: -------------------------------------------------------------------------------- 1 | 2 | /* all tags */ 3 | html { 4 | box-sizing: border-box; 5 | } 6 | *, *:before, *:after { 7 | box-sizing: inherit; 8 | } 9 | 10 | html,body { 11 | padding: 0; 12 | margin: 0; 13 | font-family: Arial, Helvetica, sans-serif; 14 | } 15 | 16 | canvas { 17 | position: absolute; 18 | top: 0; 19 | left: 0; 20 | padding: 0; 21 | margin: 0; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | } 27 | .github-banner { 28 | position: absolute; 29 | top: 0; 30 | left: 0; 31 | z-index: 1000; 32 | } -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/entry/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | my_app 10 | 11 | 12 | 13 | 14 | 17 | 18 |
19 | 20 | Fork me on GitHub 21 | 22 |
23 |
24 | 34 | 35 | -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/entry/index.ts: -------------------------------------------------------------------------------- 1 | import { register_event_sender,send_event,send_bridge_event_from_rust_to_ts_unchecked, BridgeEvent } from "@events/events"; 2 | 3 | import {init_ui} from "@ui/ui"; 4 | import {init_menu} from "@ui/menu"; 5 | import {set_state} from "@state/state"; 6 | import { get_window_size } from "@utils/window"; 7 | import "./index.css"; 8 | 9 | init_ui(); 10 | (window as any).load_wasm(core => { 11 | const canvas_dom_element = document.getElementById("canvas"); 12 | const { width, height } = get_window_size(); 13 | window.onresize = () => { 14 | send_event([BridgeEvent.WindowSize, get_window_size()]); 15 | } 16 | 17 | //when the core has finished loading, it'll send an event (via send_bridge_event_to_ts which is just send_bridge_event on the rust side) 18 | //that event will cause a state transition and then we're off to the races 19 | register_event_sender(core.run(canvas_dom_element, width, height, send_bridge_event_from_rust_to_ts_unchecked)); 20 | 21 | init_menu(); 22 | }) -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/events/events.ts: -------------------------------------------------------------------------------- 1 | import {set_state} from "@state/state"; 2 | /** 3 | * Customize this for all the bridge event types 4 | * If there are any complex objects, create structs on the Rust side too! 5 | */ 6 | 7 | //The order of these must match the Rust BridgeEvent! 8 | export enum BridgeEvent { 9 | WindowSize, 10 | LoadGltf, 11 | GltfLoaded, 12 | CameraSettings, 13 | Clear 14 | } 15 | 16 | type ValidEvent = 17 | | [BridgeEvent.WindowSize, WindowSize] 18 | | BridgeEvent.GltfLoaded 19 | | [BridgeEvent.LoadGltf, string] 20 | | [BridgeEvent.CameraSettings, OrthographicCameraSettings | PerspectiveCameraSettings] 21 | | BridgeEvent.Clear 22 | 23 | interface WindowSize{ 24 | width: number; 25 | height: number; 26 | } 27 | 28 | export interface OrthographicCameraSettings { 29 | style: CameraStyle, 30 | xmag: number, 31 | ymag: number, 32 | znear: number, 33 | zfar: number, 34 | positionX: number, 35 | positionY: number, 36 | positionZ: number, 37 | } 38 | 39 | export interface PerspectiveCameraSettings { 40 | style: CameraStyle, 41 | aspectRatio: number; 42 | yfov: number; 43 | znear: number, 44 | zfar: number, 45 | positionX: number, 46 | positionY: number, 47 | positionZ: number, 48 | } 49 | export enum CameraStyle { 50 | ORTHOGRAPHIC, 51 | PERSPECTIVE 52 | } 53 | 54 | 55 | //this is loosely defined because the types are converted on the rust side 56 | type EventSender = (evt_type:number, evt_data:any) => unknown; 57 | let send_event_to_rust:EventSender; 58 | 59 | 60 | export const send_bridge_event_from_rust_to_ts_unchecked = (evt_type:BridgeEvent, evt_data?:any) => { 61 | switch(evt_type) { 62 | case BridgeEvent.GltfLoaded: set_state("loaded"); break; 63 | } 64 | } 65 | export const send_event = (event:ValidEvent) => { 66 | send_event_to_rust(event[0], event[1]); 67 | } 68 | 69 | export const register_event_sender = (_send_event_to_rust:EventSender) => { 70 | send_event_to_rust = _send_event_to_rust; 71 | } -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/state/state.ts: -------------------------------------------------------------------------------- 1 | import { render_ui } from "@ui/ui"; 2 | 3 | type State = "loading" | "loaded"; 4 | let _state:State = "loading"; 5 | 6 | export const set_state = (__state:State) => { 7 | _state = __state; 8 | render_ui(); 9 | }; 10 | export const get_state = ():State => _state; -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/tests/sanity.spec.ts: -------------------------------------------------------------------------------- 1 | test("sanity test", () => { 2 | expect(1+1).toBe(2); 3 | }); -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "sourceMap": true, 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "diagnostics": true, 9 | "outDir": "dist", 10 | "allowJs": true, 11 | "lib": [ 12 | "esnext", 13 | "es6", 14 | "es5", 15 | "dom" 16 | ], 17 | "baseUrl": ".", 18 | "paths": { 19 | "*": [ 20 | "node_modules/@types/*", 21 | "*" 22 | ], 23 | 24 | "@events/*": ["./events/*"], 25 | "@state/*": ["./state/*"], 26 | "@ui/*": ["./ui/*"], 27 | "@utils/*": ["./utils/*"], 28 | "@config/*": ["./config/*"], 29 | }, 30 | }, 31 | "include": [ 32 | "./**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "dist", 37 | "build", 38 | "coverage" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/ui/menu.ts: -------------------------------------------------------------------------------- 1 | import {samples_path} from "@utils/path"; 2 | import dat from "dat.gui"; 3 | import {debug_settings} from "@config/config"; 4 | import {BridgeEvent, send_event, CameraStyle} from "@events/events" 5 | import { set_state } from "@state/state"; 6 | 7 | /* 8 | const model_data:Array = 9 | samples_path("model-index.json") 10 | |> await fetch 11 | |> await json 12 | */ 13 | export const init_menu = async () => { 14 | const gui = new dat.GUI(); 15 | const model_data:Array = await (await fetch(samples_path("model-index.json"))).json(); 16 | 17 | gui.add({ CLEAR:() => send_event(BridgeEvent.Clear)}, "CLEAR"); 18 | 19 | init_model_menu (model_data); 20 | init_camera_menu (); 21 | 22 | //helper funcs 23 | interface ModelData { 24 | name: string; 25 | variants: Array 26 | } 27 | function init_model_menu(xs:Array) { 28 | const model_names = xs.map(({name}) => name); 29 | const variant_names = xs.map(({variants}) => Object.keys(variants)); 30 | const variant_values = xs.map(({variants}) => Object.values(variants)); 31 | let variant_name = variant_names[debug_settings.model_idx][debug_settings.variant_idx]; 32 | let model_name = model_names[debug_settings.model_idx]; 33 | 34 | const reload = () => { 35 | set_state("loading"); 36 | const model_idx = model_names.indexOf(model_name); 37 | const variant_idx = variant_names[model_idx].indexOf(variant_name); 38 | const variant_value = variant_values[model_idx][variant_idx]; 39 | 40 | const gltf_path = samples_path(`${model_name}/${variant_name}`); 41 | 42 | //console.log("model index is", model_idx, "variant index is", variant_idx); 43 | send_event([BridgeEvent.LoadGltf, `${gltf_path}/${variant_value}`]); 44 | } 45 | 46 | const opts = { 47 | model: model_name, 48 | variant: variant_name 49 | } 50 | 51 | 52 | const model_controller = gui.add(opts, "model", model_names); 53 | let variant_controller; 54 | const reset_variants = (model_idx:number) => { 55 | if(variant_controller) { 56 | gui.remove(variant_controller); 57 | } 58 | 59 | const vs = variant_names[model_idx]; 60 | variant_name = vs.indexOf(variant_name) === -1 ? vs[0] : variant_name; 61 | variant_controller = gui.add(opts, "variant", variant_names[model_idx]); 62 | variant_controller.setValue(variant_name); 63 | variant_controller.onChange(n => { 64 | variant_name = n; 65 | reload(); 66 | }); 67 | 68 | reload(); 69 | } 70 | 71 | model_controller.onChange(n => { 72 | model_name = n; 73 | const model_idx = model_names.indexOf(model_name); 74 | reset_variants(model_idx); 75 | }); 76 | 77 | //START WITH INITIAL SETTINGS 78 | (() => { 79 | reset_variants(debug_settings.model_idx); 80 | })(); 81 | } 82 | 83 | function init_camera_menu() { 84 | let cameraFolder; 85 | const reset_camera = (style:"orthographic" | "perspective") => { 86 | if(cameraFolder) { 87 | gui.removeFolder(cameraFolder); 88 | } 89 | 90 | const opts:any = {style}; 91 | 92 | cameraFolder = gui.addFolder("camera"); 93 | cameraFolder.open(); 94 | 95 | const camera_style = cameraFolder.add(opts, "style", ["orthographic", "perspective"]); 96 | camera_style.onChange(style => { 97 | reset_camera(style); 98 | }); 99 | 100 | const add_camera_menu_option = (label:string) => (value:any) => { 101 | opts[label] = value; 102 | cameraFolder 103 | .add(opts, label) 104 | .onChange(value => { 105 | send_camera_settings(); 106 | }); 107 | 108 | } 109 | const add_camera_menu_slider = (label:string) => (min:number) => (max:number) => (value:number) => { 110 | opts[label] = value; 111 | cameraFolder 112 | .add(opts, label, min, max) 113 | .onChange(value => { 114 | send_camera_settings(); 115 | }); 116 | 117 | } 118 | const setup_orthographic = () => { 119 | add_camera_menu_option("xmag") (1.0); 120 | add_camera_menu_option("ymag") (1.0); 121 | add_camera_menu_option("znear") (0.01); 122 | add_camera_menu_option("zfar") (1.0); 123 | add_camera_menu_slider("positionX") (-100.0) (100.0) (0.0); 124 | add_camera_menu_slider("positionY") (-100.0) (100.0) (0.0); 125 | add_camera_menu_slider("positionZ") (-100.0) (100.0) (1.0); 126 | } 127 | 128 | const setup_perspective = () => { 129 | add_camera_menu_option("aspectRatio") (1.0); 130 | add_camera_menu_option("yfov") (1.0); 131 | add_camera_menu_option("znear") (0.01); 132 | add_camera_menu_option("zfar") (100.0); 133 | add_camera_menu_slider("positionX") (-100.0) (100.0) (0.0); 134 | add_camera_menu_slider("positionY") (-100.0) (100.0) (0.0); 135 | add_camera_menu_slider("positionZ") (-100.0) (100.0) (1.0); 136 | } 137 | 138 | if (style === "orthographic") { 139 | setup_orthographic(); 140 | } else { 141 | setup_perspective(); 142 | } 143 | send_camera_settings(); 144 | 145 | function send_camera_settings() { 146 | if(style === "orthographic") { 147 | send_event([BridgeEvent.CameraSettings, { 148 | style: CameraStyle.ORTHOGRAPHIC, 149 | xmag: opts.xmag, 150 | ymag: opts.ymag, 151 | znear: opts.znear, 152 | zfar: opts.zfar, 153 | positionX: opts.positionX, 154 | positionY: opts.positionY, 155 | positionZ: opts.positionZ, 156 | }]) 157 | } else if(style === "perspective") { 158 | send_event([BridgeEvent.CameraSettings, { 159 | style: CameraStyle.PERSPECTIVE, 160 | aspectRatio: opts.aspectRatio, 161 | yfov: opts.yfov, 162 | znear: opts.znear, 163 | zfar: opts.zfar, 164 | positionX: opts.positionX, 165 | positionY: opts.positionY, 166 | positionZ: opts.positionZ, 167 | }]) 168 | } 169 | } 170 | } 171 | 172 | reset_camera(debug_settings.camera_style); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/ui/ui.css: -------------------------------------------------------------------------------- 1 | .ui { 2 | position: absolute; 3 | z-index: 2; 4 | } 5 | 6 | .loading { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | width: 100vw; 11 | height: 100vh; 12 | } 13 | 14 | .loading > .text { 15 | font-size: xx-large; 16 | text-align: center; 17 | } -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/ui/ui.ts: -------------------------------------------------------------------------------- 1 | import {render, html} from "lit-html"; 2 | import "./ui.css"; 3 | import { get_state, set_state } from "@state/state"; 4 | 5 | const ui_dom_element= document.getElementById("ui"); 6 | export const init_ui = () => { 7 | set_state("loading"); 8 | } 9 | 10 | export const render_ui = () => { 11 | const ui = get_state() === "loading" ? loading() : null; 12 | render( 13 | html` 14 |
15 | ${ui} 16 |
17 | `, ui_dom_element 18 | ); 19 | } 20 | 21 | const loading = () => html` 22 |
23 |
Loading...
24 |
`; -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const floats_equal = (f1:number) => (f2:number) => 2 | Math.abs(f1 - f2) < Number.EPSILON; 3 | 4 | export const find_map = (pred: (key:K) => boolean) => (m:Map):V | undefined => { 5 | for (var [k, v] of m) { 6 | if(pred(k)) { 7 | return v; 8 | } 9 | } 10 | 11 | return undefined; 12 | } 13 | 14 | export const find_object = (pred: (key:string) => boolean) => (obj:{[key:string]: V}):V | undefined => { 15 | const key = Object.keys(obj).find(key => pred(key)); 16 | 17 | return key ? obj[key] : undefined; 18 | } 19 | 20 | export const append_strings = (preds:Array<() => [string, boolean]>) => (initial:string) => 21 | preds.reduce((acc, x) => { 22 | const [value, flag] = x(); 23 | return flag ? `${acc} ${value}` : acc; 24 | }, initial) -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/utils/path.ts: -------------------------------------------------------------------------------- 1 | import {SamplesUrlBase} from "@config/config"; 2 | 3 | export const samples_path = (path:string) => `${SamplesUrlBase}/${path}`; -------------------------------------------------------------------------------- /to-move/renderer-demo/typescript/utils/window.ts: -------------------------------------------------------------------------------- 1 | export const get_window_size = () => ({ 2 | width: window.innerWidth, 3 | height: window.innerHeight 4 | }); -------------------------------------------------------------------------------- /to-move/renderer-demo/webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin'); 5 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 6 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | 9 | module.exports = { 10 | context: process.cwd(), // to automatically find tsconfig.json 11 | entry: "./typescript/entry/index.ts", 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: '[name].js', 15 | publicPath: "/" 16 | }, 17 | plugins: [ 18 | new ForkTsCheckerWebpackPlugin({ 19 | eslint: false 20 | }), 21 | new ForkTsCheckerNotifierWebpackPlugin({ title: 'TypeScript', excludeWarnings: false }), 22 | new HtmlWebpackPlugin({ 23 | inject: true, 24 | template: 'typescript/entry/index.html' 25 | }), 26 | new ScriptExtHtmlWebpackPlugin({ 27 | defaultAttribute: 'defer' 28 | }), 29 | //new webpack.HotModuleReplacementPlugin() 30 | ], 31 | module: { 32 | rules: [ 33 | { 34 | test: /.ts$/, 35 | use: [ 36 | { loader: 'ts-loader', options: { transpileOnly: true } } 37 | ] 38 | }, 39 | { 40 | test: /\.css$/i, 41 | use: ['style-loader', 'css-loader'], 42 | }, 43 | ] 44 | }, 45 | resolve: { 46 | extensions: [".ts", ".js", ".css"], 47 | alias: { 48 | "@events": path.resolve(__dirname, "typescript/events"), 49 | "@state": path.resolve(__dirname, "typescript/state"), 50 | "@ui": path.resolve(__dirname, "typescript/ui"), 51 | "@utils": path.resolve(__dirname, "typescript/utils"), 52 | "@config": path.resolve(__dirname, "typescript/config"), 53 | } 54 | }, 55 | devtool: 'inline-source-map', 56 | devServer: { 57 | contentBase: path.resolve(__dirname, './_static'), 58 | compress: true, 59 | clientLogLevel: 'warning', 60 | open: true, 61 | historyApiFallback: true, 62 | stats: 'errors-only', 63 | watchOptions: { 64 | ignored: ['node_modules', 'target', 'pkg', '**/*.rs'] 65 | }, 66 | watchContentBase: true, 67 | } 68 | }; -------------------------------------------------------------------------------- /to-move/renderer-demo/webpack.release.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | 8 | module.exports = { 9 | mode: "production", 10 | optimization: { 11 | minimizer: [new TerserPlugin({ 12 | parallel: true, 13 | //sourceMap: true 14 | })] 15 | }, 16 | context: process.cwd(), // to automatically find tsconfig.json 17 | entry: "./typescript/entry/index.ts", 18 | output: { 19 | path: path.join(process.cwd(), 'dist'), 20 | filename: '[name].js', 21 | }, 22 | plugins: [ 23 | new ForkTsCheckerWebpackPlugin({ 24 | async: false, 25 | useTypescriptIncrementalApi: true, 26 | memoryLimit: 4096 27 | }), 28 | new HtmlWebpackPlugin({ 29 | hash: true, 30 | inject: true, 31 | template: 'typescript/entry/index.html', 32 | minify: { 33 | removeComments: true, 34 | collapseWhitespace: true, 35 | removeRedundantAttributes: true, 36 | useShortDoctype: true, 37 | removeEmptyAttributes: true, 38 | removeStyleLinkTypeAttributes: true, 39 | keepClosingSlash: true, 40 | minifyJS: true, 41 | minifyCSS: true, 42 | minifyURLs: true, 43 | }, 44 | }), 45 | 46 | new ScriptExtHtmlWebpackPlugin({ 47 | defaultAttribute: 'defer' 48 | }), 49 | ], 50 | module: { 51 | rules: [ 52 | { 53 | test: /.ts$/, 54 | use: [ 55 | { loader: 'ts-loader', options: { transpileOnly: true } } 56 | ], 57 | }, 58 | { 59 | 60 | test: /\.css$/i, 61 | use: ['style-loader', 'css-loader'], 62 | }, 63 | ] 64 | }, 65 | resolve: { 66 | extensions: [".ts", ".js", ".css"], 67 | alias: { 68 | "@events": path.resolve(__dirname, "typescript/events"), 69 | "@state": path.resolve(__dirname, "typescript/state"), 70 | "@ui": path.resolve(__dirname, "typescript/ui"), 71 | "@utils": path.resolve(__dirname, "typescript/utils"), 72 | "@config": path.resolve(__dirname, "typescript/config"), 73 | } 74 | } 75 | }; -------------------------------------------------------------------------------- /to-move/renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "awsm_renderer" 3 | edition = "2018" 4 | version = "0.0.1" 5 | authors = ["David Komer "] 6 | license = "MIT" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/awsm/" 9 | repository = "https://github.com/dakom/awsm" 10 | homepage = "https://github.com/dakom/awsm" 11 | description = "Wrappers for WASM" 12 | categories = ["wasm"] 13 | keywords = ["gltf", "webgl", "renderer", "3d", "graphics", "gamedev"] 14 | 15 | [lib] 16 | crate-type = ["cdylib", "rlib"] 17 | 18 | [profile.release] 19 | lto = true 20 | opt-level = 's' 21 | 22 | [dependencies] 23 | wasm-bindgen = "0.2.55" 24 | web-sys = { version = "0.3.32", features = ["HtmlCanvasElement"] } 25 | js-sys = "0.3.32" 26 | log = "0.4.8" 27 | shipyard = { git= "https://github.com/leudz/shipyard.git", features = ["proc"], default-features = false} 28 | serde = { version = "1.0.104", features = ["derive"], optional = true } 29 | awsm_web = { path="../web", version = "0.0.13", features = ["webgl", "loaders"], default-features = false } 30 | futures = "0.3.1" 31 | 32 | [dependencies.gltf] 33 | # path = "../../../gltf" 34 | # version = "0.14" 35 | git = "https://github.com/gltf-rs/gltf" 36 | features = ["import", "utils", "names", "extras", "names", "KHR_lights_punctual", "KHR_materials_pbrSpecularGlossiness"] 37 | default-features = false 38 | 39 | [features] 40 | # default = ["awsm_web/debug_log", "awsm_web/disable_webgl_opt"] 41 | default = [] 42 | -------------------------------------------------------------------------------- /to-move/renderer/README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | awsm_renderer is a 3d renderer, intended to be used with other crates in the [awsm](https://github.com/dakom/awsm) ecosystem as well as with the [shipyard ECS](https://github.com/leudz/shipyard) 4 | 5 | The renderering itself ~~blatently rips off~~ borrows _heavily_ from the official Khronos [glTF-Sample-Viewer](https://github.com/KhronosGroup/glTF-Sample-Viewer) 6 | 7 | Transforms and other matrices use [nalgebra](https://www.nalgebra.org/) -------------------------------------------------------------------------------- /to-move/renderer/src/camera/camera.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::transform::*; 3 | use crate::renderer::Renderer; 4 | use shipyard::prelude::*; 5 | use awsm_web::webgl::{ WebGl2Renderer, ClearBufferMask, BufferData, BufferTarget, BufferUsage, Id}; 6 | 7 | pub struct CameraView(pub Matrix4); 8 | pub struct CameraProjection(pub Matrix4); 9 | 10 | pub fn get_orthographic_projection(xmag:f64, ymag: f64, znear: f64, zfar: f64) -> Matrix4 { 11 | let mut projection = Matrix4::default(); 12 | let mut values = projection.as_mut(); 13 | 14 | values[0] = 1.0/xmag; 15 | values[5] = 1.0/ymag; 16 | values[10] = 2.0/(znear - zfar); 17 | values[14] = (zfar+znear) / (znear-zfar); 18 | values[15] = 1.0; 19 | 20 | projection 21 | } 22 | 23 | pub fn get_perspective_projection(aspect_ratio:f64, yfov: f64, znear: f64, zfar: Option) -> Matrix4 { 24 | let mut projection = Matrix4::default(); 25 | let mut values = projection.as_mut(); 26 | 27 | match zfar { 28 | None => { 29 | values[10] = -1.0; 30 | values[14] = (-2.0 * znear); 31 | }, 32 | Some(zfar) => { 33 | values[10] = (zfar+znear)/(znear-zfar); 34 | values[14] = (2.0 * zfar * znear)/(znear - zfar); 35 | } 36 | }; 37 | 38 | values[0] = 1.0/(aspect_ratio * (0.5 * yfov).tan()); 39 | values[5] = 1.0/((0.5 * yfov).tan()); 40 | values[11] = -1.0; 41 | 42 | projection 43 | } 44 | 45 | impl Renderer { 46 | /// gets the first found camera node 47 | pub fn get_camera_node(&self) -> Option { 48 | let world = self.world.borrow(); 49 | world.run::<(&CameraView, &CameraProjection), _, _>(|(views, projs)| { 50 | (&views, &projs).iter().with_id().map(|(id, _, _)| id).next() 51 | }) 52 | } 53 | /// if no node is provided then the first camera node will be used 54 | pub fn update_camera_projection(&mut self, node: Option, projection:&[f64]) { 55 | let node = if node.is_none() { self.get_camera_node() } else { node }; 56 | if let Some(node) = node { 57 | let world = self.world.borrow_mut(); 58 | let proj = world.run::<&mut CameraProjection, _, _>(|mut projs| { 59 | if let Some(proj) = (&mut projs).get(node).iter_mut().next() { 60 | proj.0.as_mut().copy_from_slice(projection); 61 | } 62 | }); 63 | } 64 | } 65 | /// if no node is provided then the first camera node will be used 66 | pub fn update_camera_view(&mut self, node: Option) { 67 | let node = if node.is_none() { self.get_camera_node() } else { node }; 68 | if let Some(node) = node { 69 | let world = self.world.borrow_mut(); 70 | world.run::<(&mut CameraView, &LocalTransform), _, _>(|(mut views, local_mats)| { 71 | if let Some((view, local_mat)) = (&mut views, &local_mats).get(node).iter_mut().next() { 72 | let view = &mut view.0; 73 | let local_mat = &local_mat.0; 74 | view.copy_from_slice(local_mat.as_ref()); 75 | view.invert_mut().unwrap(); 76 | } 77 | }); 78 | } 79 | } 80 | 81 | /// if no node is provided then the first camera node will be used 82 | pub(crate) fn update_camera_ubo(&mut self, node:Option) { 83 | let node = if node.is_none() { self.get_camera_node() } else { node }; 84 | if let Some(node) = node { 85 | let world = self.world.borrow_mut(); 86 | let webgl = self.webgl.borrow_mut(); 87 | 88 | world.run::<(&CameraView, &CameraProjection), _, _>(|(views, projs)| { 89 | if let Some((view, proj)) = (&views, &projs).get(node).iter_mut().next() { 90 | let view = &view.0; 91 | let projection = &proj.0; 92 | 93 | let camera = vec![view.to_vec_f32(), projection.to_vec_f32()].concat(); 94 | webgl.upload_buffer( 95 | self.camera_buffer_id, 96 | BufferData::new( 97 | &camera, 98 | BufferTarget::UniformBuffer, 99 | BufferUsage::DynamicDraw, 100 | ), 101 | ).unwrap(); 102 | } 103 | }); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /to-move/renderer/src/camera/mod.rs: -------------------------------------------------------------------------------- 1 | mod camera; 2 | 3 | pub use self::camera::*; -------------------------------------------------------------------------------- /to-move/renderer/src/components/components.rs: -------------------------------------------------------------------------------- 1 | use shipyard::prelude::*; 2 | pub use crate::primitives::Primitive; 3 | pub use crate::transform::*; 4 | pub use crate::camera::*; 5 | pub use crate::nodes::{Node}; 6 | 7 | pub fn register_components(world:&mut World) { 8 | world.register::(); 9 | world.register::(); 10 | world.register::(); 11 | world.register::(); 12 | world.register::(); 13 | world.register::(); 14 | world.register::(); 15 | world.register::(); 16 | world.register::(); 17 | } -------------------------------------------------------------------------------- /to-move/renderer/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod components; 2 | 3 | pub use self::components::*; -------------------------------------------------------------------------------- /to-move/renderer/src/errors/errors.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use wasm_bindgen::prelude::JsValue; 3 | use awsm_web::errors::Error as AwsmWebError; 4 | 5 | pub enum Error { 6 | Empty, 7 | String(String), 8 | Js(JsValue), 9 | Native(NativeError), 10 | } 11 | 12 | pub enum NativeError { 13 | Internal, 14 | GltfLoader, 15 | SceneMissing, 16 | AccessorView, 17 | Wip, 18 | InvertMatrix, 19 | AttributeDimSize(String, u8, usize), 20 | NodeMissing(usize), 21 | } 22 | 23 | impl Error { 24 | pub fn to_js(self: &Self) -> JsValue { 25 | match self { 26 | Error::Empty => JsValue::null(), 27 | Error::String(s) => JsValue::from_str(&s[..]), 28 | Error::Js(jval) => jval.clone(), 29 | Error::Native(err) => JsValue::from_str(err.to_string().as_str()), 30 | } 31 | } 32 | } 33 | 34 | impl fmt::Debug for Error { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | match &self { 37 | Error::Empty => write!(f, "empty error"), 38 | _ => write!( 39 | f, 40 | "{}", 41 | self.to_js() 42 | .as_string() 43 | .unwrap_or("unknown error".to_string()) 44 | ), 45 | } 46 | } 47 | } 48 | 49 | impl fmt::Display for Error { 50 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 51 | match &self { 52 | Error::Empty => write!(f, "empty error"), 53 | _ => write!( 54 | f, 55 | "{}", 56 | self.to_js() 57 | .as_string() 58 | .unwrap_or("unknown error".to_string()) 59 | ), 60 | } 61 | } 62 | } 63 | 64 | impl NativeError { 65 | pub fn default_str(self: &Self) -> &'static str { 66 | match self { 67 | NativeError::Internal => "internal error", 68 | NativeError::GltfLoader => "unable to load gltf", 69 | NativeError::SceneMissing=> "no such scene", 70 | NativeError::AccessorView => "non-sparse accessor must have a buffer view", 71 | NativeError::Wip => "Work In Progress", 72 | NativeError::InvertMatrix => "Unable to invert matrix", 73 | NativeError::AttributeDimSize(_, _, _) => "wrong attribute dimension size", 74 | NativeError::NodeMissing(_) => "missing node", 75 | } 76 | } 77 | pub fn to_string(self: &Self) -> String { 78 | match self { 79 | NativeError::NodeMissing(index) => format!("missing node: {}", index), 80 | NativeError::AttributeDimSize(name, expected, got) => format!("wrong size for attribute {}: expected {} got {}", name, expected, got), 81 | _ => self.default_str().to_string(), 82 | } 83 | } 84 | } 85 | 86 | impl From for JsValue { 87 | fn from(err: Error) -> Self { 88 | err.to_js() 89 | } 90 | } 91 | 92 | impl From for Error { 93 | fn from(err: NativeError) -> Self { 94 | Error::Native(err) 95 | } 96 | } 97 | 98 | impl From for Error { 99 | fn from(err: JsValue) -> Self { 100 | Error::Js(err) 101 | } 102 | } 103 | 104 | impl From for Error { 105 | fn from(err: AwsmWebError) -> Self { 106 | Error::Js(err.to_js()) 107 | } 108 | } 109 | 110 | impl From for AwsmWebError { 111 | fn from(err: Error) -> Self { 112 | AwsmWebError::Js(err.to_js()) 113 | } 114 | } 115 | impl From for Error { 116 | fn from(err: js_sys::Error) -> Self { 117 | Error::Js(JsValue::from(err)) 118 | } 119 | } 120 | 121 | impl From for Error { 122 | fn from(err: String) -> Self { 123 | Error::String(err) 124 | } 125 | } 126 | 127 | impl From<&str> for Error { 128 | fn from(err: &str) -> Self { 129 | Error::String(String::from(err)) 130 | } 131 | } 132 | 133 | impl From for Error { 134 | fn from(err:gltf::Error) -> Self { 135 | Error::String(err.to_string()) 136 | } 137 | } 138 | 139 | impl From for gltf::Error { 140 | fn from(err:Error) -> Self { 141 | //gltf:Error doesn't seem to implement a From for string 142 | //but io:Other isn't too confusing and actually fits the usual case 143 | gltf::Error::from(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())) 144 | } 145 | } -------------------------------------------------------------------------------- /to-move/renderer/src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | 3 | pub use errors::*; 4 | -------------------------------------------------------------------------------- /to-move/renderer/src/gltf/accessors.rs: -------------------------------------------------------------------------------- 1 | use gltf::accessor::{DataType, Dimensions}; 2 | 3 | pub struct AccessorInfo { 4 | pub dim_size:usize, 5 | pub data_size:u8, 6 | pub webgl_data_type:awsm_web::webgl::DataType 7 | } 8 | 9 | impl AccessorInfo { 10 | pub fn new(accessor:&gltf::accessor::Accessor) -> Self { 11 | Self{ 12 | dim_size: get_accessor_dim_size(accessor.dimensions()), 13 | data_size: get_accessor_data_size(accessor.data_type()), 14 | webgl_data_type: get_accessor_webgl_data_type(accessor.data_type()), 15 | } 16 | } 17 | } 18 | 19 | fn get_accessor_dim_size(type_:gltf::accessor::Dimensions) -> usize { 20 | match type_ { 21 | Dimensions::Scalar => 1, 22 | Dimensions::Vec2 => 2, 23 | Dimensions::Vec3 => 3, 24 | Dimensions::Vec4 | Dimensions::Mat2 => 4, 25 | Dimensions::Mat3 => 9, 26 | Dimensions::Mat4 => 16, 27 | } 28 | } 29 | 30 | fn get_accessor_data_size(type_:DataType) -> u8 { 31 | match type_ { 32 | DataType::I8 | DataType::U8 => 1, //BYTE | UNSIGNED_BYTE 33 | DataType::I16 | DataType::U16 => 2, //SHORT | UNSIGNED_SHORT 34 | DataType::U32 | DataType::F32 => 4, //UNSIGNED_INT| FLOAT 35 | } 36 | } 37 | 38 | fn get_accessor_webgl_data_type(gltf_type:DataType) -> awsm_web::webgl::DataType { 39 | 40 | match gltf_type { 41 | DataType::I8 => awsm_web::webgl::DataType::Byte, 42 | DataType::U8 => awsm_web::webgl::DataType::UnsignedByte, 43 | DataType::I16 => awsm_web::webgl::DataType::Short, 44 | DataType::U16 => awsm_web::webgl::DataType::UnsignedShort, 45 | DataType::U32 => awsm_web::webgl::DataType::UnsignedInt, 46 | DataType::F32 => awsm_web::webgl::DataType::Float, 47 | } 48 | } 49 | 50 | /* 51 | fn element_byte_size(accessor:&gltf::accessor::Accessor) -> usize { 52 | dim_size(accessor.dimensions()) * component_size(accessor.data_type()) 53 | } 54 | 55 | fn get_byte_length(accessor:&gltf::accessor::Accessor) -> usize { 56 | accessor.count() * element_byte_size(accessor) 57 | } 58 | 59 | fn get_byte_offset(view:&gltf::buffer::View, accessor:&gltf::accessor::Accessor) -> usize { 60 | //TODO - followup with https://github.com/gltf-rs/gltf/issues/268 61 | view.offset() + accessor.offset() 62 | } 63 | 64 | fn get_byte_stride_len(view:&gltf::buffer::View, accessor:&gltf::accessor::Accessor) -> usize { 65 | match view.stride() { 66 | None => 0, 67 | Some(stride) => { 68 | stride * element_byte_size(accessor) 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug)] 74 | struct AccessorInfo { 75 | base: BaseAccessorInfo, 76 | sparse: Option 77 | } 78 | 79 | #[derive(Debug)] 80 | struct SparseAccessorInfo { 81 | indices: BaseAccessorInfo, 82 | values: BaseAccessorInfo 83 | } 84 | 85 | #[derive(Debug)] 86 | struct BaseAccessorInfo { 87 | len: usize, 88 | offset: usize, 89 | component_type: DataType, 90 | dim_type: Dimensions, 91 | buffer_id: Id, 92 | } 93 | 94 | //TODO - implement getting typed data 95 | // https://github.com/dakom/pure3d-typescript/blob/master/src/lib/internal/gltf/gltf-parse/Gltf-Parse-Data-Typed.ts 96 | // https://users.rust-lang.org/t/return-new-vec-or-slice/34542 97 | fn get_accessor_data<'a> (accessor:&gltf::accessor::Accessor, buffers:&'a Vec>) -> Cow<'a, [u8]> { 98 | 99 | //TODO - remove the temp Some wrapper 100 | //https://github.com/gltf-rs/gltf/issues/266 101 | match Some(accessor.view()) { 102 | Some(view) => { 103 | let byte_offset = get_byte_offset(&view, accessor); 104 | let byte_len = get_byte_length(accessor); 105 | let byte_end = byte_offset + byte_len; 106 | let full_buffer_data = &buffers[view.buffer().index()]; 107 | 108 | //info!("target length {} start {} end {}", full_buffer_data.len(), byte_offset, byte_end); 109 | 110 | Cow::Borrowed(&full_buffer_data[byte_offset..byte_end]) 111 | }, 112 | None => { 113 | let n_values = accessor.count() * dim_size(accessor.dimensions()); 114 | } 115 | } 116 | } 117 | 118 | fn make_accessor_info(webgl:&mut WebGl2Renderer, accessor:&gltf::accessor::Accessor, buffer_view_ids:&mut Vec) -> Result { 119 | let accessor_id = accessor.index(); 120 | 121 | let byte_len = get_byte_length(&accessor); 122 | //TODO - remove the temp Some wrapper 123 | //https://github.com/gltf-rs/gltf/issues/266 124 | match Some(accessor.view()) { 125 | None => { 126 | if accessor.sparse().is_none() { 127 | return Err(NativeError::AccessorSparse.into()) 128 | } 129 | 130 | let buffer_id = webgl.create_buffer()?; 131 | let data = get_accessor_data(accessor)?; 132 | //TODO - create and fill buffer with 0's 133 | 134 | 135 | Ok(AccessorInfo{ 136 | base: BaseAccessorInfo{ 137 | len: byte_len, 138 | offset: 0, 139 | component_type: accessor.data_type(), 140 | dim_type: accessor.dimensions(), 141 | buffer_id 142 | }, 143 | sparse: None 144 | }) 145 | }, 146 | Some(view) => { 147 | let offset = get_byte_offset(&view, &accessor); 148 | let stride_len = get_byte_stride_len(&view, &accessor); 149 | 150 | Ok(AccessorInfo{ 151 | base: BaseAccessorInfo { 152 | len: byte_len + stride_len, 153 | offset, 154 | component_type: accessor.data_type(), 155 | dim_type: accessor.dimensions(), 156 | buffer_id: buffer_view_ids[view.index()] 157 | }, 158 | sparse: None 159 | }) 160 | } 161 | } 162 | } 163 | 164 | */ -------------------------------------------------------------------------------- /to-move/renderer/src/gltf/buffer_view.rs: -------------------------------------------------------------------------------- 1 | use gltf::buffer::View; 2 | 3 | pub fn get_buffer_view_data <'a>(view:&View, buffers:&'a Vec>) -> &'a [u8] { 4 | let byte_offset = view.offset(); 5 | let byte_length = view.length(); 6 | let byte_end = byte_offset + byte_length; 7 | let full_buffer_data = &buffers[view.buffer().index()]; 8 | 9 | let data = &full_buffer_data[byte_offset..byte_end]; 10 | 11 | //log::info!("buffer view: target length {} start {} end {} data {:?}", full_buffer_data.len(), byte_offset, byte_end, data); 12 | 13 | data 14 | } -------------------------------------------------------------------------------- /to-move/renderer/src/gltf/loader.rs: -------------------------------------------------------------------------------- 1 | use web_sys::{HtmlImageElement}; 2 | use awsm_web::loaders::{fetch}; 3 | use crate::errors::{Error, NativeError}; 4 | use gltf::{Gltf, Document, buffer, image, Error as GltfError}; 5 | use futures::{Future}; 6 | use futures::future::{try_join_all, TryFutureExt}; 7 | use std::rc::Rc; 8 | use std::cell::RefCell; 9 | 10 | /// Web-specific adaptation of https://github.com/gltf-rs/gltf/blob/master/src/import.rs 11 | /// Main differences: 12 | /// 1. Everything is async 13 | /// 2. No image_data_reference feature (hence no base64/image crate dependencies) 14 | /// 3. Some error checking is removed since the web api does it inherently (e.g. mime type) 15 | /// 4. Adds awsm as a dependency 16 | /// 17 | /// instead of having awsm as a dependency, the loaders could accept 18 | /// generic functions like Fn(&str) -> impl Future 19 | 20 | type DataResult = Result, GltfError>; 21 | type ImageResult = Result; 22 | 23 | 24 | pub struct GltfResource { 25 | pub gltf: Document, 26 | pub buffers: Vec>, 27 | pub images: Vec 28 | } 29 | 30 | pub enum GltfFileType { 31 | Json, 32 | Glb, 33 | Draco //TODO 34 | } 35 | 36 | pub fn get_type_from_filename(_url:&str) -> Option { 37 | //todo - look for .gltf, .glb, etc. 38 | Some(GltfFileType::Json) 39 | } 40 | 41 | pub fn load_gltf(url:&str, file_type: Option) -> impl Future> { 42 | 43 | let future = { 44 | let url = url.to_owned(); 45 | let file_type = match file_type { 46 | Some(file_type) => file_type, 47 | None => get_type_from_filename(&url).unwrap_or(GltfFileType::Json) 48 | }; 49 | 50 | async move { 51 | let Gltf { document, blob } = match file_type { 52 | GltfFileType::Json => { 53 | let text = fetch::text(&url).await?; 54 | let bytes:&[u8] = text.as_bytes(); 55 | Gltf::from_slice(bytes) 56 | }, 57 | GltfFileType::Glb => { 58 | let bytes:Vec = fetch::vec_u8(&url).await?; 59 | Gltf::from_slice(&bytes) 60 | }, 61 | _ => return Err(Error::from(NativeError::GltfLoader)) 62 | }?; 63 | 64 | 65 | let base_path = get_base_path(&url); 66 | let buffers = import_buffer_data( &document, base_path, blob) 67 | .await.map_err(|err| Error::from(err))?; 68 | 69 | //info!("loaded {} buffers", buffer_data.len()); 70 | 71 | let images = import_image_data( &document, base_path, &buffers) 72 | .await.map_err(|err| Error::from(err))?; 73 | 74 | //info!("loaded {} images", image_data.len()); 75 | 76 | Ok(GltfResource{ gltf: document, buffers, images }) 77 | } 78 | }; 79 | 80 | future 81 | } 82 | 83 | fn get_base_path (url:&str) -> &str { 84 | let idx1:i32 = url.rfind('/').map(|n| n as i32).unwrap_or(-1) + 1; 85 | let idx2:i32 = url.rfind('\\').map(|n| n as i32).unwrap_or(-1) + 1; 86 | 87 | if idx1 == 0 && idx2 == 0 { 88 | url 89 | } else { 90 | &url[0..(std::cmp::max(idx1, idx2) as usize)] 91 | } 92 | } 93 | 94 | async fn import_buffer_data<'a>( document: &'a Document, base: &'a str, blob: Option>) -> Result>, GltfError> { 95 | 96 | let futures = get_buffer_futures(document, base, blob); 97 | 98 | let datas:Vec> = try_join_all(futures).await?; 99 | 100 | let mut buffers = Vec::new(); 101 | for (mut data, buffer) in datas.into_iter().zip(document.buffers()) { 102 | if data.len() < buffer.length() { 103 | return Err( 104 | GltfError::BufferLength { 105 | buffer: buffer.index(), 106 | expected: buffer.length(), 107 | actual: data.len(), 108 | } 109 | ); 110 | } 111 | while data.len() % 4 != 0 { 112 | data.push(0); 113 | } 114 | buffers.push(data); 115 | } 116 | Ok(buffers) 117 | } 118 | 119 | fn get_buffer_futures<'a>(document:&'a Document, base:&str, blob: Option>) -> Vec + 'a> { 120 | //these need to be owned by each future simultaneously 121 | let blob = Rc::new(RefCell::new(blob)); 122 | let base = Rc::new(base.to_owned()); 123 | 124 | document.buffers().map(|buffer| { 125 | let blob = Rc::clone(&blob); 126 | let base = Rc::clone(&base); 127 | 128 | async move { 129 | match buffer.source() { 130 | buffer::Source::Uri(uri) => { 131 | let url = get_url(base.as_ref(), uri)?; 132 | fetch::vec_u8(&url) 133 | .map_err(|err| GltfError::from(Error::from(err))) 134 | .await 135 | }, 136 | buffer::Source::Bin => { 137 | blob.borrow_mut().take().ok_or(GltfError::MissingBlob) 138 | } 139 | } 140 | } 141 | }).collect() 142 | } 143 | 144 | async fn import_image_data<'a>(document: &'a Document, base: &'a str, buffer_data:&'a [Vec]) -> Result, GltfError> { 145 | 146 | let futures = get_image_futures(document, base, buffer_data); 147 | 148 | try_join_all(futures).await 149 | } 150 | 151 | 152 | fn get_image_futures<'a>(document:&'a Document, base:&str, buffer_data:&'a [Vec]) -> Vec + 'a> { 153 | //these need to be owned by each future simultaneously 154 | let base = Rc::new(base.to_owned()); 155 | 156 | document.images().map(|image| { 157 | let base = Rc::clone(&base); 158 | async move { 159 | match image.source() { 160 | image::Source::Uri { uri, mime_type: _ } => { 161 | 162 | let url = get_url(base.as_ref(), uri)?; 163 | 164 | fetch::image(&url) 165 | .map_err(|err| GltfError::from(Error::from(err))) 166 | .await 167 | }, 168 | image::Source::View { view, mime_type } => { 169 | let parent_buffer_data = &buffer_data[view.buffer().index()]; 170 | let begin = view.offset(); 171 | let end = begin + view.length(); 172 | let encoded_image = &parent_buffer_data[begin..end]; 173 | fetch::image_u8(&encoded_image, &mime_type) 174 | .map_err(|err| GltfError::from(Error::from(err))) 175 | .await 176 | }, 177 | } 178 | } 179 | }).collect() 180 | } 181 | 182 | 183 | fn get_url(base:&str, uri: &str) -> Result { 184 | if uri.contains(":") { 185 | //absolute 186 | if uri.starts_with("data:") { 187 | Ok(uri.to_owned()) 188 | } else if uri.starts_with("http:") || uri.starts_with("https://") { 189 | Ok(uri.to_owned()) 190 | } else { 191 | Err(GltfError::UnsupportedScheme) 192 | } 193 | } else { 194 | //relative 195 | Ok(format!("{}{}", base, uri)) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /to-move/renderer/src/gltf/materials.rs: -------------------------------------------------------------------------------- 1 | /* 2 | use awsm_web::webgl::{ Id }; 3 | 4 | pub struct Material { 5 | shader_id: Id 6 | } 7 | */ -------------------------------------------------------------------------------- /to-move/renderer/src/gltf/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod loader; 2 | mod accessors; 3 | mod buffer_view; 4 | mod materials; 5 | pub(crate) mod processor; -------------------------------------------------------------------------------- /to-move/renderer/src/gltf/processor.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{Error, NativeError}; 2 | use crate::gltf::loader::{GltfResource}; 3 | use crate::primitives::*; 4 | use crate::shaders::compile_shader; 5 | use super::accessors::AccessorInfo; 6 | use crate::nodes::*; 7 | use shipyard::prelude::*; 8 | use awsm_web::webgl::{ 9 | Id, 10 | WebGl2Renderer, 11 | BufferData, 12 | BufferTarget, 13 | BufferUsage, 14 | AttributeOptions, 15 | VertexArray, 16 | BeginMode 17 | }; 18 | use std::convert::TryInto; 19 | 20 | pub struct ProcessState <'a> { 21 | pub resource:&'a GltfResource, 22 | pub world:&'a mut World, 23 | pub webgl:&'a mut WebGl2Renderer, 24 | 25 | //Just a local holder to help de-dup data 26 | buffer_view_ids:Vec>, 27 | } 28 | 29 | impl <'a> ProcessState<'a> { 30 | pub fn new(resource:&'a GltfResource, world:&'a mut World, webgl:&'a mut WebGl2Renderer) -> Self { 31 | let buffer_view_ids:Vec> = vec![None;resource.gltf.views().len()]; 32 | 33 | Self{ 34 | resource, 35 | world, 36 | webgl, 37 | buffer_view_ids 38 | } 39 | } 40 | } 41 | 42 | pub fn process_scene(state:ProcessState, scene:&gltf::scene::Scene) -> Result<(), Error> { 43 | let mut state = state; 44 | 45 | fn traverse_node_root(state:&mut ProcessState, node:&gltf::Node) -> Result<(), Error> 46 | { 47 | if let Some(mesh) = node.mesh() { 48 | process_mesh(state, &mesh)?; 49 | } 50 | for node in node.children() { 51 | traverse_node_root(state, &node)?; 52 | } 53 | Ok(()) 54 | }; 55 | 56 | for node in scene.nodes() { 57 | traverse_node_root(&mut state, &node)?; 58 | } 59 | Ok(()) 60 | } 61 | 62 | pub fn process_mesh(state:&mut ProcessState, mesh:&gltf::mesh::Mesh) -> Result<(), Error> { 63 | 64 | for primitive in mesh.primitives() { 65 | 66 | 67 | let shader_id = compile_shader(state.webgl)?; 68 | 69 | let vao_id = state.webgl.create_vertex_array()?; 70 | //Probably some way of making this just one iterator that exists early... 71 | let mut attributes = Vec::with_capacity(primitive.attributes().len()); 72 | 73 | for (semantic, accessor) in primitive.attributes() { 74 | let buffer_id = upload_accessor(state, &accessor, BufferTarget::ArrayBuffer)?; 75 | let accessor_info = AccessorInfo::new(&accessor); 76 | //TODO - figure out wtf this should really be... 77 | //OPTION 1: seems broken let opts = AttributeOptions::new(accessor_info.data_size, accessor_info.webgl_data_type); 78 | let opts = AttributeOptions::new(accessor.count().try_into().unwrap(), accessor_info.webgl_data_type); 79 | let attribute_name = match semantic { 80 | gltf::Semantic::Positions => "a_position", 81 | gltf::Semantic::Normals => "a_normal", 82 | gltf::Semantic::Tangents => "a_tangent", 83 | gltf::Semantic::Colors(_color) => "colors", 84 | gltf::Semantic::TexCoords(_coord) => "texcoords", 85 | gltf::Semantic::Joints(_joints) => "joints", 86 | gltf::Semantic::Weights(_weights) => "weights", 87 | gltf::Semantic::Extras(_extras) => "extras", 88 | }; 89 | 90 | //log::info!("dimensions for {} is {}", attribute_name, accessor_info.dim_size); 91 | //log::info!("attribute {} data buffer id is {:?} for accessor {}, primitive {}, count {}", attribute_name, buffer_id, accessor.index(), primitive.index(), accessor.count()); 92 | attributes.push((buffer_id, attribute_name, accessor_info, opts)); 93 | } 94 | 95 | let draw_mode = get_primitive_mode(&primitive); 96 | let (elements_id, draw_info) = match primitive.indices() { 97 | Some(accessor) => { 98 | let accessor_info = AccessorInfo::new(&accessor); 99 | let buffer_id = upload_accessor(state, &accessor, BufferTarget::ElementArrayBuffer)?; 100 | //log::info!("elements data buffer id is {:?} for accessor {}, primitive {}, count {}", buffer_id, accessor.index(), primitive.index(), accessor.count()); 101 | //TODO - figure out wtf this should really be... 102 | (Some(buffer_id), PrimitiveDraw::Elements(draw_mode, accessor.count().try_into().unwrap(), accessor_info.webgl_data_type, accessor.offset().try_into().unwrap())) 103 | }, 104 | 105 | //TODO 106 | None => (None, PrimitiveDraw::Direct(draw_mode, 36, 0)) 107 | }; 108 | 109 | 110 | /* 111 | Ideas: 112 | 1. We have info on the semantics in attributes - could use that here... 113 | 2. Maybe the attributes for this primitive should be changeable at runtime - so these could be a component? 114 | 3. Probably better to start with hardcoded / inline here and then work backwards 115 | */ 116 | 117 | 118 | let vertex_arrays = attributes 119 | .iter() 120 | .map(|(buffer_id, attribute_name, _dim_size, opts)| { 121 | VertexArray{ 122 | attribute_name, 123 | buffer_id: *buffer_id, 124 | opts: &opts 125 | } 126 | }) 127 | .collect::>(); 128 | 129 | if vertex_arrays.len() != attributes.iter().len() { 130 | return Err("lengths don't match".into()); 131 | } 132 | if vertex_arrays.len() == 0 { 133 | return Err("no elements!".into()); 134 | } 135 | 136 | log::info!("{:?}", &vertex_arrays); 137 | 138 | state.webgl.assign_vertex_array( 139 | vao_id, 140 | elements_id, 141 | &vertex_arrays 142 | )?; 143 | 144 | add_node(state.world, NodeData::Primitive(Primitive{shader_id, vao_id, draw_info, }), None, None, None, None); 145 | } 146 | 147 | Ok(()) 148 | } 149 | 150 | fn get_primitive_mode(primitive:&gltf::mesh::Primitive) -> BeginMode { 151 | match primitive.mode() { 152 | gltf::mesh::Mode::Points => BeginMode::Points, 153 | gltf::mesh::Mode::Lines => BeginMode::Lines, 154 | gltf::mesh::Mode::LineLoop => BeginMode::LineLoop, 155 | gltf::mesh::Mode::LineStrip => BeginMode::LineStrip, 156 | gltf::mesh::Mode::Triangles => BeginMode::Triangles, 157 | gltf::mesh::Mode::TriangleStrip => BeginMode::TriangleStrip, 158 | gltf::mesh::Mode::TriangleFan => BeginMode::TriangleFan, 159 | } 160 | } 161 | 162 | //If the view is non-sparse, upload as-is 163 | //Otherwise, create new data and upload 164 | //Either way, return the buffer id 165 | fn upload_accessor(state:&mut ProcessState, accessor:&gltf::accessor::Accessor, target:BufferTarget) -> Result { 166 | 167 | match accessor.sparse() { 168 | Some(_sparse) => { 169 | match accessor.view() { 170 | Some(_view) => { 171 | //TODO 172 | log::info!("get the typed data from buffer view"); 173 | }, 174 | None => { 175 | //TODO 176 | log::info!("create empty (filled with 0's) typed data from buffer view"); 177 | } 178 | } 179 | 180 | //TODO 181 | log::info!("replace typed data with sparse info"); 182 | 183 | Err(NativeError::Wip.into()) 184 | }, 185 | None => { 186 | let view = accessor.view().ok_or(Error::from(NativeError::AccessorView))?; 187 | upload_buffer_view(state, &view, target) 188 | } 189 | } 190 | } 191 | 192 | //Upload the buffer view if and only if there isn't already an id for that specific view 193 | //In either case, return the Id 194 | fn upload_buffer_view(state:&mut ProcessState, view:&gltf::buffer::View, target:BufferTarget) -> Result { 195 | 196 | let ProcessState {webgl, resource, buffer_view_ids, ..} = state; 197 | let GltfResource {buffers, ..} = resource; 198 | 199 | let buffer_view_id = view.index(); 200 | 201 | if buffer_view_ids[buffer_view_id].is_none() { 202 | 203 | let buffer_id = webgl.create_buffer()?; 204 | let raw_data = super::buffer_view::get_buffer_view_data(&view, &buffers); 205 | 206 | let data = BufferData::new( 207 | &raw_data, 208 | target, 209 | BufferUsage::StaticDraw 210 | ); 211 | 212 | webgl.upload_buffer(buffer_id, data)?; 213 | buffer_view_ids[buffer_view_id] = Some(buffer_id); 214 | } 215 | 216 | Ok(buffer_view_ids[buffer_view_id].unwrap()) 217 | } 218 | 219 | -------------------------------------------------------------------------------- /to-move/renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | #![feature(option_result_contains)] 3 | 4 | mod renderer; 5 | 6 | pub(crate) mod shaders; 7 | pub(crate) mod primitives; 8 | /// re-exported 9 | pub use awsm_web::*; 10 | 11 | /// exported 12 | pub mod transform; 13 | pub mod camera; 14 | pub mod components; 15 | pub mod gltf; 16 | pub mod errors; 17 | pub mod nodes; 18 | pub use self::renderer::*; 19 | */ -------------------------------------------------------------------------------- /to-move/renderer/src/nodes/mod.rs: -------------------------------------------------------------------------------- 1 | mod nodes; 2 | pub use self::nodes::*; -------------------------------------------------------------------------------- /to-move/renderer/src/nodes/nodes.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use crate::renderer::Renderer; 3 | use crate::transform::*; 4 | use crate::components::*; 5 | use shipyard::prelude::*; 6 | 7 | pub struct Node { 8 | //just an idea so far to try and keep flat list... might not pan out 9 | pub n_children: usize, 10 | } 11 | impl Node { 12 | pub fn new() -> Self { 13 | Self{ 14 | n_children: 0 15 | } 16 | } 17 | } 18 | 19 | pub enum NodeData { 20 | Empty, 21 | Camera(Matrix4), //Projection matrix. View Matrix is calculated from trs 22 | Primitive(Primitive) 23 | } 24 | 25 | impl Renderer { 26 | /// Adds a node to the scene 27 | pub fn add_node(&mut self, data:NodeData, parent:Option, translation: Option, rotation: Option, scale: Option) -> Result { 28 | add_node(&mut self.world.borrow_mut(), data, parent, translation, rotation, scale) 29 | } 30 | 31 | pub fn set_node_trs(&mut self, node:Key, translation: Option, rotation: Option, scale: Option) { 32 | let world = self.world.borrow_mut(); 33 | 34 | world.run::<(&mut Translation, &mut Rotation, &mut Scale), _, _>( 35 | |(mut translations, mut rotations, mut scales)| { 36 | if let Some((t,r,s)) = (&mut translations, &mut rotations, &mut scales).get(node).iter_mut().next() { 37 | if let Some(translation) = translation { 38 | t.0.copy_from(&translation); 39 | } 40 | if let Some(rotation) = rotation { 41 | r.0.copy_from(&rotation); 42 | } 43 | if let Some(scale) = scale { 44 | s.0.copy_from(&scale); 45 | } 46 | } 47 | } 48 | ); 49 | } 50 | } 51 | 52 | //Mostly for internal use - but can also be used to share the ECS outside of renderer 53 | pub fn add_node(world:&mut World, data:NodeData, parent:Option, translation: Option, rotation: Option, scale: Option) -> Result { 54 | let translation = translation.unwrap_or_default(); 55 | let rotation = rotation.unwrap_or_default(); 56 | let scale = scale.unwrap_or(Vector3::new(1.0, 1.0, 1.0)); 57 | let local_matrix = Matrix4::from_trs(&translation, &rotation, &scale); 58 | let world_matrix = Matrix4::default(); 59 | 60 | if let Some(parent) = parent { 61 | //TODO - re-arrange all the nodes? 62 | //probably do *not* need to mess with world matrix, just mark dirty and it'll be updated... 63 | } 64 | 65 | let node = match data { 66 | NodeData::Empty => { 67 | world.run::<( 68 | EntitiesMut, 69 | &mut Node, 70 | &mut Translation, 71 | &mut Rotation, 72 | &mut Scale, 73 | &mut LocalTransform, 74 | &mut WorldTransform, 75 | ), _, _>(|( 76 | mut entities, 77 | mut nodes, 78 | mut translations, 79 | mut rotations, 80 | mut scales, 81 | mut local_matrices, 82 | mut world_matrices, 83 | )| { 84 | Ok(entities.add_entity( 85 | ( 86 | &mut nodes, 87 | &mut translations, 88 | &mut rotations, 89 | &mut scales, 90 | &mut local_matrices, 91 | &mut world_matrices, 92 | ), 93 | ( 94 | Node::new(), 95 | Translation(translation), 96 | Rotation(rotation), 97 | Scale(scale), 98 | LocalTransform(local_matrix), 99 | WorldTransform(world_matrix), 100 | ) 101 | )) 102 | }) 103 | } 104 | NodeData::Camera(projection_matrix) => { 105 | let camera_view = Matrix4::invert_clone(&local_matrix)?; 106 | 107 | world.run::<( 108 | EntitiesMut, 109 | &mut Node, 110 | &mut CameraView, 111 | &mut CameraProjection, 112 | &mut Translation, 113 | &mut Rotation, 114 | &mut Scale, 115 | &mut LocalTransform, 116 | &mut WorldTransform, 117 | ), _, _>(|( 118 | mut entities, 119 | mut nodes, 120 | mut camera_views, 121 | mut camera_projections, 122 | mut translations, 123 | mut rotations, 124 | mut scales, 125 | mut local_matrices, 126 | mut world_matrices, 127 | )| { 128 | Ok(entities.add_entity( 129 | ( 130 | &mut nodes, 131 | &mut camera_views, 132 | &mut camera_projections, 133 | &mut translations, 134 | &mut rotations, 135 | &mut scales, 136 | &mut local_matrices, 137 | &mut world_matrices, 138 | ), 139 | ( 140 | Node::new(), 141 | CameraView(camera_view), 142 | CameraProjection(projection_matrix), 143 | Translation(translation), 144 | Rotation(rotation), 145 | Scale(scale), 146 | LocalTransform(local_matrix), 147 | WorldTransform(world_matrix), 148 | ) 149 | )) 150 | }) 151 | } 152 | 153 | NodeData::Primitive(primitive) => { 154 | world.run::<( 155 | EntitiesMut, 156 | &mut Node, 157 | &mut Primitive, 158 | &mut Translation, 159 | &mut Rotation, 160 | &mut Scale, 161 | &mut LocalTransform, 162 | &mut WorldTransform, 163 | ), _, _>(|( 164 | mut entities, 165 | mut nodes, 166 | mut primitives, 167 | mut translations, 168 | mut rotations, 169 | mut scales, 170 | mut local_matrices, 171 | mut world_matrices, 172 | )| { 173 | Ok(entities.add_entity( 174 | ( 175 | &mut nodes, 176 | &mut primitives, 177 | &mut translations, 178 | &mut rotations, 179 | &mut scales, 180 | &mut local_matrices, 181 | &mut world_matrices, 182 | ), 183 | ( 184 | Node::new(), 185 | primitive, 186 | Translation(translation), 187 | Rotation(rotation), 188 | Scale(scale), 189 | LocalTransform(local_matrix), 190 | WorldTransform(world_matrix), 191 | ) 192 | )) 193 | }) 194 | } 195 | }; 196 | 197 | node 198 | } -------------------------------------------------------------------------------- /to-move/renderer/src/primitives/mod.rs: -------------------------------------------------------------------------------- 1 | mod primitives; 2 | 3 | pub use self::primitives::*; -------------------------------------------------------------------------------- /to-move/renderer/src/primitives/primitives.rs: -------------------------------------------------------------------------------- 1 | use awsm_web::webgl::{Id, DataType, BeginMode}; 2 | use shipyard::prelude::*; 3 | 4 | pub struct Primitive { 5 | pub shader_id: Id, 6 | pub vao_id: Id, 7 | pub draw_info:PrimitiveDraw 8 | } 9 | 10 | pub enum PrimitiveDraw { 11 | //count, DataType, offset 12 | Elements(BeginMode, u32, DataType, u32), 13 | //TODO - update as needed... just copied elements for now 14 | Direct(BeginMode, u32, u32) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /to-move/renderer/src/renderer.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | use std::cell::RefCell; 3 | use awsm_web::webgl::{ WebGl2Renderer, ClearBufferMask, BufferData, BufferTarget, BufferUsage, Id}; 4 | use crate::errors::{Error, NativeError}; 5 | use crate::gltf::loader::GltfResource; 6 | use crate::components::*; 7 | use crate::primitives::PrimitiveDraw; 8 | use crate::gltf::processor::{ProcessState, process_scene}; 9 | 10 | use shipyard::prelude::*; 11 | 12 | pub struct Renderer { 13 | //This is Rc because other renderers might want to also own the context 14 | //for example a 2d renderer which gets passed to JS for whatever reason and must be 'static 15 | //There is almost no performance impact since it's only borrowed at the top of the core functions 16 | //- not deep in the iterators 17 | pub webgl:Rc>, 18 | pub world: Rc>, 19 | 20 | pub(crate) camera_buffer_id: Id, 21 | } 22 | 23 | impl Renderer { 24 | pub fn new(webgl:Rc>, world: Option>>, width: u32, height: u32) -> Result { 25 | let world = match world { 26 | Some(world) => world, 27 | None => Rc::new(RefCell::new(World::default())) 28 | }; 29 | 30 | let camera_buffer_id = webgl.borrow_mut().create_buffer()?; 31 | let mut ret = Self{webgl, world, camera_buffer_id}; 32 | 33 | { 34 | let mut world = ret.world.borrow_mut(); 35 | register_components(&mut world); 36 | } 37 | 38 | ret.resize(width, height); 39 | 40 | Ok(ret) 41 | } 42 | 43 | pub fn resize(&mut self, width: u32, height: u32) { 44 | let mut webgl = self.webgl.borrow_mut(); 45 | webgl.resize(width, height); 46 | } 47 | 48 | pub fn clear(&mut self) { 49 | let webgl = self.webgl.borrow_mut(); 50 | 51 | webgl.clear(&[ 52 | ClearBufferMask::ColorBufferBit, 53 | ClearBufferMask::DepthBufferBit, 54 | ]); 55 | } 56 | 57 | 58 | fn update_transforms(&mut self) { 59 | let mut webgl = self.webgl.borrow_mut(); 60 | let world = self.world.borrow_mut(); 61 | 62 | //Update all the LocalMatrices 63 | world.run::<(&Translation, &Rotation, &Scale, &mut LocalTransform), _, _>(|(translations, rotations, scales, local_matrices)| { 64 | for (translation, rotation, scale, mut local_matrix) in (translations, rotations, scales, local_matrices).iter() { 65 | let local_matrix = &mut local_matrix.0; 66 | let translation = &translation.0; 67 | let rotation = &rotation.0; 68 | let scale = &scale.0; 69 | local_matrix.from_trs_mut(translation, rotation, scale); 70 | } 71 | }); 72 | 73 | //Update all the WorldMatrices 74 | world.run::<(&Node, &LocalTransform, &mut WorldTransform), _, _>(|(nodes, local_matrices, world_matrices)| { 75 | let mut parent_matrix = Matrix4::default(); 76 | let mut child_index = 0; 77 | for (node, local_matrix, world_matrix) in (nodes, local_matrices, world_matrices).iter() { 78 | let local_matrix = &local_matrix.0; 79 | let world_matrix = &mut world_matrix.0; 80 | world_matrix.copy_from(&local_matrix); 81 | /* 82 | world_matrix.copy_from(&parent_matrix); 83 | world_matrix.mul_mut(local_matrix); 84 | 85 | if child_index == node.n_children { 86 | parent_matrix.copy_from(world_matrix); 87 | child_index = 0; 88 | } else { 89 | child_index += 1; 90 | } 91 | */ 92 | } 93 | }); 94 | } 95 | 96 | pub fn render(&mut self, _interpolation:Option) { 97 | self.update_transforms(); 98 | //mult-camera support will require changing this to Some 99 | //idea - have a Unique component which holds the active camera 100 | self.update_camera_view(None); 101 | self.update_camera_ubo(None); 102 | 103 | let mut webgl = self.webgl.borrow_mut(); 104 | let world = self.world.borrow_mut(); 105 | 106 | world.run::<(&Primitive, &WorldTransform), _, _>(|(primitives, model_matrices)| { 107 | for (primitive, model_matrix) in (primitives, model_matrices).iter() { 108 | let Primitive{shader_id, vao_id, draw_info} = primitive; 109 | 110 | webgl.activate_program(*shader_id).unwrap(); 111 | webgl.activate_uniform_buffer(self.camera_buffer_id, "camera").unwrap(); 112 | webgl.upload_uniform_mat_4("u_model", &model_matrix.0.to_vec_f32()).unwrap(); 113 | webgl.activate_vertex_array(*vao_id).unwrap(); 114 | 115 | match draw_info { 116 | PrimitiveDraw::Elements(draw_mode, count, data_type, offset) => { 117 | webgl.draw_elements(*draw_mode, *count, *data_type, *offset); 118 | //log::info!("draw mode: {}, count: {}, offset: {}", *draw_mode as u32, *count, *offset); 119 | }, 120 | PrimitiveDraw::Direct(draw_mode, count, offset) => { 121 | webgl.draw_arrays(*draw_mode, *offset, *count); 122 | } 123 | }; 124 | } 125 | }); 126 | } 127 | 128 | pub fn animate(&mut self, _delta:f64) { 129 | let _webgl = self.webgl.borrow_mut(); 130 | } 131 | 132 | //The scene will be determined by the following in order of preference 133 | //1. scene in argument 134 | //2. default scene set in gltf 135 | //3. first in scenes array 136 | //if none of these exist, it's an error (not supporting gltf as asset library atm) 137 | pub fn upload_gltf(&mut self, resource:&GltfResource, scene:Option) -> Result<(), Error> { 138 | let mut webgl = self.webgl.borrow_mut(); 139 | let mut world = self.world.borrow_mut(); 140 | 141 | let scene = 142 | scene.or( 143 | resource.gltf.default_scene().or( 144 | resource.gltf.scenes().next() 145 | ) 146 | ).ok_or(NativeError::SceneMissing)?; 147 | 148 | process_scene(ProcessState::new(resource,&mut world,&mut webgl), &scene)?; 149 | 150 | 151 | 152 | /* 153 | if let Some(mesh) = node.mesh() { 154 | mesh.primitives().any(|primitive| { 155 | 156 | if primitive.indices().map(|acc| acc.index()).contains(&accessor_id) { 157 | return true; 158 | } 159 | if primitive.attributes().any(|(_, attribute_accessor)| { 160 | attribute_accessor.index() == accessor_id 161 | }) { 162 | return true; 163 | } 164 | if primitive.morph_targets().any(|morph_target| { 165 | morph_target.positions().map(|acc| acc.index()).contains(&accessor_id) 166 | || morph_target.normals().map(|acc| acc.index()).contains(&accessor_id) 167 | || morph_target.tangents().map(|acc| acc.index()).contains(&accessor_id) 168 | }) { 169 | return true; 170 | } 171 | 172 | false 173 | }) 174 | } else { 175 | false 176 | } 177 | */ 178 | //let mut buffer_ids = gltf_renderer::buffer_view::upload_buffer_views(&mut webgl, &gltf, &buffers)?; 179 | //gltf_renderer::accessors::populate_accessors(&mut webgl, &mut world, &gltf, &mut buffer_ids, &buffers); 180 | //gltf_renderer::accessors::upload_accessors(&mut webgl, &gltf, buffers)?; 181 | 182 | Ok(()) 183 | } 184 | 185 | pub fn set_scene_from_gltf(&mut self, _gltf:&gltf::Document) { 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /to-move/renderer/src/shaders/glsl/material.frag: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | out vec4 final_color; 5 | 6 | void main() { 7 | final_color = vec4(1.0, 1.0, 1.0, 1.0); 8 | } -------------------------------------------------------------------------------- /to-move/renderer/src/shaders/glsl/primitive.vert: -------------------------------------------------------------------------------- 1 | #version 300 es 2 | precision mediump float; 3 | 4 | layout (std140) uniform camera { 5 | uniform mat4 u_view; 6 | uniform mat4 u_projection; 7 | }; 8 | 9 | uniform mat4 u_model; 10 | 11 | in vec3 a_position; 12 | 13 | void main() { 14 | mat4 vp = u_projection * u_view; 15 | mat4 mvp = vp * u_model; 16 | gl_Position = mvp * vec4(a_position, 1.0); 17 | } -------------------------------------------------------------------------------- /to-move/renderer/src/shaders/mod.rs: -------------------------------------------------------------------------------- 1 | mod shaders; 2 | 3 | pub use self::shaders::*; -------------------------------------------------------------------------------- /to-move/renderer/src/shaders/shaders.rs: -------------------------------------------------------------------------------- 1 | use awsm_web::webgl::{WebGl2Renderer, Id}; 2 | use crate::errors::{Error}; 3 | 4 | pub struct ShaderSettings{ 5 | pub has_position: bool 6 | } 7 | 8 | impl ShaderSettings { 9 | pub fn get_hash(&self) -> u32 { 10 | //TODO - implement hasher or derive 11 | 42 12 | } 13 | } 14 | 15 | const PRIMITIVE_VERT:&str = include_str!("glsl/primitive.vert"); 16 | 17 | const MATERIAL_FRAG:&str = include_str!("glsl/material.frag"); 18 | 19 | pub fn compile_shader(webgl:&mut WebGl2Renderer) -> Result { 20 | 21 | let shader_settings = ShaderSettings { 22 | has_position: true 23 | }; 24 | //TODO - look it up in hash map and return early if found 25 | let _shader_hash = shader_settings.get_hash(); 26 | 27 | let vertex_shader = PRIMITIVE_VERT; 28 | let fragment_shader = MATERIAL_FRAG; 29 | let program_id = webgl.compile_program(&vertex_shader, &fragment_shader)?; 30 | 31 | Ok(program_id) 32 | } -------------------------------------------------------------------------------- /to-move/renderer/src/transform/mod.rs: -------------------------------------------------------------------------------- 1 | mod transform; 2 | 3 | pub use self::transform::*; -------------------------------------------------------------------------------- /to-move/renderer/src/transform/transform.rs: -------------------------------------------------------------------------------- 1 | /* 2 | The math was taken and adapted from various places on the internet 3 | Specifically, from gl-matrix and the gltf-rs crate (which in turn took from cg_math) 4 | */ 5 | use crate::errors::{Error, NativeError}; 6 | pub struct Translation(pub Vector3); 7 | pub struct Rotation(pub Quaternion); 8 | pub struct Scale(pub Vector3); 9 | pub struct LocalTransform(pub Matrix4); 10 | pub struct WorldTransform(pub Matrix4); 11 | 12 | #[repr(C)] 13 | #[derive(Clone, PartialEq)] 14 | pub struct Vector3 { 15 | x: f64, 16 | y: f64, 17 | z: f64, 18 | } 19 | impl Vector3 { 20 | pub fn new(x: f64, y: f64, z: f64) -> Self { 21 | Self{x, y, z} 22 | } 23 | } 24 | 25 | impl Default for Vector3 { 26 | fn default() -> Self { 27 | Self::new(0.0, 0.0, 0.0) 28 | } 29 | } 30 | impl TransformValues for Vector3 { 31 | fn len(&self) -> usize { 3 } 32 | } 33 | 34 | #[repr(C)] 35 | #[derive(Clone, PartialEq)] 36 | pub struct Quaternion { 37 | x: f64, 38 | y: f64, 39 | z: f64, 40 | w: f64, 41 | } 42 | impl Quaternion { 43 | pub fn new(x: f64, y: f64, z: f64, w: f64) -> Self { 44 | Self{x, y, z, w} 45 | } 46 | 47 | 48 | } 49 | impl Default for Quaternion { 50 | fn default() -> Self { 51 | Self::new(0.0, 0.0, 0.0, 1.0) 52 | } 53 | } 54 | impl TransformValues for Quaternion { 55 | fn len(&self) -> usize { 4 } 56 | } 57 | 58 | #[repr(C)] 59 | #[derive(Clone, PartialEq)] 60 | pub struct Matrix4 ( 61 | f64, 62 | f64, 63 | f64, 64 | f64, 65 | f64, 66 | f64, 67 | f64, 68 | f64, 69 | f64, 70 | f64, 71 | f64, 72 | f64, 73 | f64, 74 | f64, 75 | f64, 76 | f64, 77 | ); 78 | 79 | impl Default for Matrix4 { 80 | fn default() -> Self { 81 | Self( 82 | 1.0,0.0,0.0,0.0, 83 | 0.0,1.0,0.0,0.0, 84 | 0.0,0.0,1.0,0.0, 85 | 0.0,0.0,0.0,1.0, 86 | ) 87 | } 88 | } 89 | 90 | impl Matrix4 { 91 | 92 | pub fn from_translation(v: &Vector3) -> Self { 93 | Self( 94 | 1.0, 0.0, 0.0, 0.0, 95 | 0.0, 1.0, 0.0, 0.0, 96 | 0.0, 0.0, 1.0, 0.0, 97 | v.x, v.y, v.z, 1.0, 98 | ) 99 | } 100 | pub fn from_rotation(r: &Quaternion) -> Self { 101 | let x2 = r.x + r.x; 102 | let y2 = r.y + r.y; 103 | let z2 = r.z + r.z; 104 | 105 | let xx2 = x2 * r.x; 106 | let xy2 = x2 * r.y; 107 | let xz2 = x2 * r.z; 108 | 109 | let yy2 = y2 * r.y; 110 | let yz2 = y2 * r.z; 111 | let zz2 = z2 * r.z; 112 | 113 | let sy2 = y2 * r.w; 114 | let sz2 = z2 * r.w; 115 | let sx2 = x2 * r.w; 116 | 117 | Self( 118 | 1.0 - yy2 - zz2, xy2 + sz2, xz2 - sy2, 0.0, 119 | xy2 - sz2, 1.0 - xx2 - zz2, yz2 + sx2, 0.0, 120 | xz2 + sy2, yz2 - sx2, 1.0 - xx2 - yy2, 0.0, 121 | 0.0, 0.0, 0.0, 1.0, 122 | ) 123 | } 124 | 125 | pub fn set_from_scale(&mut self, scale:&Vector3) { 126 | self.reset(); 127 | self.0 = scale.x; 128 | self.5 = scale.y; 129 | self.10 = scale.z; 130 | } 131 | pub fn from_scale(scale:&Vector3) -> Self { 132 | Self( 133 | scale.x, 0.0, 0.0, 0.0, 134 | 0.0, scale.y, 0.0, 0.0, 135 | 0.0, 0.0, scale.z, 0.0, 136 | 0.0, 0.0, 0.0, 1.0, 137 | ) 138 | } 139 | pub fn from_trs_mut(&mut self, translation:&Vector3, rotation:&Quaternion, scale:&Vector3) { 140 | self.set_from_scale(scale); 141 | self.mul_mut(&Self::from_rotation(rotation)); 142 | self.mul_mut(&Self::from_translation(translation)); 143 | } 144 | 145 | pub fn from_trs(translation:&Vector3, rotation:&Quaternion, scale:&Vector3) -> Self { 146 | let mut _self = Self::from_scale(scale); 147 | _self.mul_mut(&Self::from_rotation(rotation)); 148 | _self.mul_mut(&Self::from_translation(translation)); 149 | _self 150 | } 151 | 152 | pub fn mul_mut(&mut self, rhs: &Matrix4) { 153 | let a:&[f64] = self.as_ref(); 154 | let b:&[f64] = rhs.as_ref(); 155 | let a00 = a[0]; 156 | let a01 = a[1]; 157 | let a02 = a[2]; 158 | let a03 = a[3]; 159 | let a10 = a[4]; 160 | let a11 = a[5]; 161 | let a12 = a[6]; 162 | let a13 = a[7]; 163 | let a20 = a[8]; 164 | let a21 = a[9]; 165 | let a22 = a[10]; 166 | let a23 = a[11]; 167 | let a30 = a[12]; 168 | let a31 = a[13]; 169 | let a32 = a[14]; 170 | let a33 = a[15]; 171 | let mut b0 = b[0]; 172 | let mut b1 = b[1]; 173 | let mut b2 = b[2]; 174 | let mut b3 = b[3]; 175 | 176 | self.0 = b0*a00 + b1*a10 + b2*a20 + b3*a30; 177 | self.1 = b0*a01 + b1*a11 + b2*a21 + b3*a31; 178 | self.2 = b0*a02 + b1*a12 + b2*a22 + b3*a32; 179 | self.3 = b0*a03 + b1*a13 + b2*a23 + b3*a33; 180 | b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; 181 | self.4 = b0*a00 + b1*a10 + b2*a20 + b3*a30; 182 | self.5 = b0*a01 + b1*a11 + b2*a21 + b3*a31; 183 | self.6 = b0*a02 + b1*a12 + b2*a22 + b3*a32; 184 | self.7 = b0*a03 + b1*a13 + b2*a23 + b3*a33; 185 | b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; 186 | self.8 = b0*a00 + b1*a10 + b2*a20 + b3*a30; 187 | self.9 = b0*a01 + b1*a11 + b2*a21 + b3*a31; 188 | self.10 = b0*a02 + b1*a12 + b2*a22 + b3*a32; 189 | self.11 = b0*a03 + b1*a13 + b2*a23 + b3*a33; 190 | b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; 191 | self.12 = b0*a00 + b1*a10 + b2*a20 + b3*a30; 192 | self.13 = b0*a01 + b1*a11 + b2*a21 + b3*a31; 193 | self.14 = b0*a02 + b1*a12 + b2*a22 + b3*a32; 194 | self.15 = b0*a03 + b1*a13 + b2*a23 + b3*a33; 195 | } 196 | 197 | pub fn invert_clone(orig:&Self) -> Result { 198 | let mut clone = orig.clone(); 199 | clone.invert_mut()?; 200 | Ok(clone) 201 | } 202 | /// returns true if it was able to invert, false otherwise 203 | pub fn invert_mut(&mut self) -> Result<(), Error> { 204 | let a:&[f64] = self.as_ref(); 205 | let a00 = a[0]; 206 | let a01 = a[1]; 207 | let a02 = a[2]; 208 | let a03 = a[3]; 209 | let a10 = a[4]; 210 | let a11 = a[5]; 211 | let a12 = a[6]; 212 | let a13 = a[7]; 213 | let a20 = a[8]; 214 | let a21 = a[9]; 215 | let a22 = a[10]; 216 | let a23 = a[11]; 217 | let a30 = a[12]; 218 | let a31 = a[13]; 219 | let a32 = a[14]; 220 | let a33 = a[15]; 221 | let b00 = a00 * a11 - a01 * a10; 222 | let b01 = a00 * a12 - a02 * a10; 223 | let b02 = a00 * a13 - a03 * a10; 224 | let b03 = a01 * a12 - a02 * a11; 225 | let b04 = a01 * a13 - a03 * a11; 226 | let b05 = a02 * a13 - a03 * a12; 227 | let b06 = a20 * a31 - a21 * a30; 228 | let b07 = a20 * a32 - a22 * a30; 229 | let b08 = a20 * a33 - a23 * a30; 230 | let b09 = a21 * a32 - a22 * a31; 231 | let b10 = a21 * a33 - a23 * a31; 232 | let b11 = a22 * a33 - a23 * a32; 233 | // Calculate the determinant 234 | let mut det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; 235 | if det == 0.0 { 236 | Err(NativeError::InvertMatrix.into()) 237 | } else { 238 | det = 1.0 / det; 239 | self.0 = (a11 * b11 - a12 * b10 + a13 * b09) * det; 240 | self.1 = (a02 * b10 - a01 * b11 - a03 * b09) * det; 241 | self.2 = (a31 * b05 - a32 * b04 + a33 * b03) * det; 242 | self.3 = (a22 * b04 - a21 * b05 - a23 * b03) * det; 243 | self.4 = (a12 * b08 - a10 * b11 - a13 * b07) * det; 244 | self.5 = (a00 * b11 - a02 * b08 + a03 * b07) * det; 245 | self.6 = (a32 * b02 - a30 * b05 - a33 * b01) * det; 246 | self.7 = (a20 * b05 - a22 * b02 + a23 * b01) * det; 247 | self.8 = (a10 * b10 - a11 * b08 + a13 * b06) * det; 248 | self.9 = (a01 * b08 - a00 * b10 - a03 * b06) * det; 249 | self.10 = (a30 * b04 - a31 * b02 + a33 * b00) * det; 250 | self.11 = (a21 * b02 - a20 * b04 - a23 * b00) * det; 251 | self.12 = (a11 * b07 - a10 * b09 - a12 * b06) * det; 252 | self.13 = (a00 * b09 - a01 * b07 + a02 * b06) * det; 253 | self.14 = (a31 * b01 - a30 * b03 - a32 * b00) * det; 254 | self.15 = (a20 * b03 - a21 * b01 + a22 * b00) * det; 255 | Ok(()) 256 | } 257 | } 258 | } 259 | impl TransformValues for Matrix4 { 260 | fn len(&self) -> usize { 16 } 261 | } 262 | 263 | impl std::ops::Mul for Matrix4 { 264 | type Output = Matrix4; 265 | fn mul(self, rhs: Matrix4) -> Self::Output { 266 | let mut out = self.clone(); 267 | out.mul_mut(&rhs); 268 | out 269 | } 270 | } 271 | 272 | pub trait TransformValues: AsRef<[f64]> + AsMut<[f64]> + Default { 273 | fn len(self: &Self) -> usize; 274 | 275 | //TODO: cache! maybe Cow? 276 | fn to_vec_f32(self: &Self) -> Vec { 277 | self.as_ref().iter().map(|n| *n as f32).collect() 278 | } 279 | 280 | fn copy_from_slice(&mut self, values:&[f64]) { 281 | let curr:&mut [f64] = self.as_mut(); 282 | curr.copy_from_slice(values); 283 | } 284 | 285 | fn reset(&mut self) { 286 | //TODO: might be possible to keep this as like a static somehow? 287 | let _default = Self::default(); 288 | self.copy_from_slice(_default.as_ref()); 289 | } 290 | fn new_from_slice(values:&[f64]) -> Self { 291 | let mut _self = Self::default(); 292 | _self.copy_from_slice(values); 293 | _self 294 | } 295 | 296 | fn copy_from(&mut self, other:&Self) { 297 | self.copy_from_slice(other.as_ref()); 298 | } 299 | } 300 | macro_rules! impl_asref { 301 | ( $( $x:ty ),* ) => { 302 | $( 303 | 304 | impl AsRef<[f64]> for $x { 305 | //this is fast - no copy 306 | fn as_ref(&self) -> &[f64] { 307 | let pointer = self as *const Self as *const f64; 308 | let slice: &[f64] = unsafe { std::slice::from_raw_parts(pointer, self.len()) }; 309 | slice 310 | } 311 | } 312 | impl AsMut<[f64]> for $x { 313 | //this is fast - no copy 314 | fn as_mut(&mut self) -> &mut [f64] { 315 | let pointer = self as *const Self as *mut f64; 316 | let slice: &mut [f64] = unsafe { std::slice::from_raw_parts_mut(pointer, self.len()) }; 317 | slice 318 | } 319 | } 320 | )* 321 | }; 322 | } 323 | 324 | impl_asref!{Vector3, Quaternion, Matrix4} 325 | --------------------------------------------------------------------------------