├── .eslintrc ├── .gitignore ├── .prettierignore ├── LICENSE ├── README.md ├── aframe.js ├── examples ├── index.html ├── playground.1.2.0.js ├── playground.html └── smoothvoxels.css ├── index.js ├── package-lock.json ├── package.json ├── scripts ├── build-aframe.mjs ├── build-worker.mjs └── build.mjs ├── src ├── img-to-svox │ └── index.js ├── smoothvoxels │ ├── aframe.js │ ├── aocalculator.js │ ├── basematerial.js │ ├── bits.js │ ├── boundingbox.js │ ├── buffers.js │ ├── color.js │ ├── colorcombiner.js │ ├── constants.js │ ├── deformer.js │ ├── facealigner.js │ ├── light.js │ ├── lightscalculator.js │ ├── material.js │ ├── materiallist.js │ ├── matrix.js │ ├── model.js │ ├── modelreader.js │ ├── modelwriter.js │ ├── noise.js │ ├── normalscalculator.js │ ├── planar.js │ ├── simplifier.js │ ├── smoothvoxel.js │ ├── svox.worker.js │ ├── svoxbuffergeometry.js │ ├── svoxmeshgenerator.js │ ├── svoxtothreemeshconverter.js │ ├── uvassigner.js │ ├── vertexlinker.js │ ├── vertextransformer.js │ ├── voxels.js │ └── workerpool.js └── vox-to-svox │ ├── IMAP.js │ ├── LAYR.js │ ├── MATL.js │ ├── MATT.js │ ├── PACK.js │ ├── RGBA.js │ ├── SIZE.js │ ├── SKIP.js │ ├── XYZI.js │ ├── constants.js │ ├── getChunkData.js │ ├── index.js │ ├── nGRP.js │ ├── nSHP.js │ ├── nTRN.js │ ├── rOBJ.js │ ├── read-dict.js │ ├── readId.js │ ├── recReadChunksInRange.js │ └── useDefaultPalette.js ├── three.js └── worker.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2018, 9 | "sourceType": "module" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | node_modules/ 4 | build/ 5 | thumbs.db 6 | .idea/ 7 | .env 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git/* 2 | .idea/* 3 | .next/* 4 | .vscode/* 5 | .yarn/* 6 | assets/* 7 | build/* 8 | coverage/* 9 | dist/* 10 | node_modules/* 11 | public/* 12 | storage/* 13 | vendor/* 14 | package-lock.json 15 | yarn.lock 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Smooth Voxels 2 | 3 | ![Voxel fruit](https://cdn.glitch.com/7426b469-4cb2-4027-abe8-f01d443ec980%2FApple.webp?v=1622117366719) 4 | 5 | Command line tools for converting VOX to SVOX and SVOX to GLTF are now available here: https://github.com/jel-app/svox-tools 6 | 7 | NPM Package: https://www.npmjs.com/package/smoothvoxels 8 | 9 | This is an optimized and npm packaged fork of the [Smooth Voxels](https://svox.glitch.me/) library by Samuel van Egmond. 10 | 11 | It has been updated to use Typed Arrays and bitfields which results in a 5x or so speedup. All features from the main project are supported. 12 | 13 | A copy of the Smooth Voxel Playground using this library can be found here: 14 | 15 | https://gfodor.github.io/smoothvoxels/ 16 | 17 | Full documentation can be found here (which applies to both libraries, other than the section on Shells) 18 | 19 | https://svox.glitch.me/ 20 | -------------------------------------------------------------------------------- /aframe.js: -------------------------------------------------------------------------------- 1 | import './src/smoothvoxels/smoothvoxel' 2 | -------------------------------------------------------------------------------- /examples/smoothvoxels.css: -------------------------------------------------------------------------------- 1 | /******************/ 2 | /* General styles */ 3 | /******************/ 4 | 5 | * { 6 | -moz-box-sizing: border-box; 7 | -webkit-box-sizing: border-box; 8 | box-sizing: border-box; 9 | } 10 | 11 | html { font-family: Helvetica, sans-serif; font-size: 16px; scroll-behavior: smooth; } 12 | 13 | body { margin:0; padding:0; font-family: Helvetica, sans-serif; font-size: 16px; /* overflow:hidden; */ background-color:#8AC; } 14 | 15 | div.full { max-width:50em; margin:auto; padding:0 1em 1em 1em; overflow-y:hidden; background-color:#FFFDFA; } 16 | 17 | h1 { color:#007dc1; margin: 0.25em 0em 0.25em 0em; } 18 | h2 { color:#007dc1; margin: 1em 0em 0.25em 0em; } 19 | h3 { color:#007dc1; margin: 1em 0em 0.0em 0em; } 20 | 21 | .button { 22 | box-shadow:inset 0px 1px 0px 0px #54a3f7; 23 | background:linear-gradient(to bottom, #007dc1 5%, #0061a7 100%); 24 | background-color:#007dc1; 25 | border-radius:5px; 26 | border:1px solid #124d77; 27 | display:inline-block; 28 | cursor:pointer; 29 | color:#ffffff; 30 | padding:2px 24px; 31 | text-decoration:none; 32 | text-shadow:0px 1px 0px #154682; 33 | margin: 0.1em; 34 | } 35 | 36 | div.info { color:#888; font-style:italic; } 37 | 38 | .hide { display:none } 39 | 40 | a { 41 | color: #007dc1; 42 | } 43 | 44 | /**********************/ 45 | /* Cheat sheet styles */ 46 | /**********************/ 47 | 48 | pre.cheatsheetcode { 49 | font-family: monospace; font-size:0.75rem; font-weight:700; 50 | width:auto; border-radius:2px; padding:0.5em; 51 | background-color:#EEE; border: solid 1px #888; overflow: auto; 52 | } 53 | 54 | /************************/ 55 | /* Documentation Styles */ 56 | /************************/ 57 | 58 | div.frontpage { 59 | display:none; 60 | width:100%; 61 | margin:0; 62 | padding:0; 63 | background-color:black; 64 | color:white; 65 | } 66 | 67 | .frontpage div.title { /* UNUSED? */ 68 | font-size: x-large; 69 | display:block; 70 | width:100%; 71 | text-align:center; 72 | } 73 | 74 | .frontpage div.author { /* UNUSED? */ 75 | display:block; 76 | width:100%; 77 | text-align:center; 78 | } 79 | 80 | div.logo { 81 | max-width:50em; margin:auto; padding:0; overflow-y:hidden; 82 | background: rgb(0,0,0); 83 | background: linear-gradient(0deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 50%, rgba(255,154,53,1) 50%, rgba(255,154,53,1) 100%); 84 | } 85 | 86 | #ToC, #ToC li a, #ToC li a:visited { 87 | list-style-type: none; 88 | padding-left: 0.5em; 89 | font-size: 20px; 90 | color:#007dc1; 91 | text-decoration: none; 92 | } 93 | 94 | a.totoc, a.totoc:visited { 95 | color:#007dc1; 96 | text-decoration: none; 97 | clear:both; float:right; margin:-2em 0em 0em 0em; 98 | } 99 | 100 | pre.example { font-family: monospace; font-size:1rem; font-weight:400; width:100%; margin-top:0.75em; border-radius:2px; padding:0.5em; background-size:75vmin auto; background-repeat:no-repeat; background-position: right bottom; background-color:#adf; border: solid 1px #888; overflow: auto; 101 | text-shadow: 102 | -2px -2px 2px #adf, 0px -2px 2px #adf, 2px -2px 2px #adf, 103 | -2px 0px 2px #adf, 0px 0px 2px #adf, 2px 0px 2px #adf, 104 | -2px 2px 2px #adf, 0px 2px 2px #adf, 2px 2px 2px #adf 105 | } 106 | .button.pre { position: relative; top:2.5em; float:right; right:0.75em; box-shadow: 0px 2px 4px 0px rgba(0,0,0, 0.5); } 107 | .button.pre:active { top:2.6em; box-shadow: unset; } 108 | span.pre { font-family: monospace; font-size:1rem; white-space: nowrap; border-radius:100px; padding:0 0.5em; background-color:#F0F0F0; border: solid 1px #AAA; } 109 | pre.syntax { background-color:#F0F0F0; text-shadow: none; border-radius:2px; border: solid 1px #AAA; padding:0.5em; } 110 | 111 | table { border-collapse: collapse; } 112 | th { border: solid 1px #AAA; padding: 0.5em } 113 | td { border: solid 1px #AAA; padding: 0.5em } 114 | 115 | .note { 116 | margin-top: 15px; 117 | margin-bottom: 15px; 118 | padding: 4px 12px; 119 | background-color: #FFF8D0; 120 | border-left: 6px solid #FFCC00; 121 | } 122 | 123 | @media print { 124 | div.frontpage { display:block; } 125 | ::-webkit-scrollbar { display:none; } 126 | body { background-color:unset; } 127 | h2 { page-break-before: always; } 128 | .pagebreak { page-break-before: always; } 129 | a { text-decoration:unset; color:unset; } 130 | a.totoc { display:none; } 131 | .button.pre { display:none; } 132 | pre { font-size:smaller; } 133 | #visitors { display:none; } 134 | } 135 | @page { margin:1cm; } 136 | 137 | /* Highlighting styles */ 138 | 139 | pre { background-color:#FFF8F0; font-size:80%; width:100%; border-radius:2px; padding:0.5em; border: solid 1px #888; overflow: auto; } 140 | pre span.tag { color:#00F; font-weight:400; } 141 | pre span.string { color:#A00; } 142 | pre span.comment { color:#080; font-style: italic; font-weight:300; } 143 | pre span.color { color:#A0A; font-weight:400; } 144 | 145 | /*********************/ 146 | /* Playground styles */ 147 | /*********************/ 148 | 149 | .inactive { display:none !important; overflow:hidden; } 150 | div.playgroundleft { width:50vw; height:100vh; float:left; padding:0 1em 1em 1em; overflow-y:auto; overflow-x:visible; background-color:#FFFDFA; } 151 | div.playgroundright { width:50vw; height:100vh; float:right; background-color:#8cf; } 152 | div.buttonbar { width:100%; padding:0.33em; margin:1em 0 0.5em 0; background-color:#EEE; text-align:right; box-shadow:inset 0px 0px 1px 1px #CCC; border-radius:2px; } 153 | hr { border: 0; border-top: solid 1px #CCC; margin:0 0.5em 0 0.5em; } 154 | 155 | .button.green { 156 | box-shadow:inset 0px 1px 0px 0px #54f7a3; 157 | background:linear-gradient(to bottom, #00c17d 5%, #00a761 100%); 158 | border:1px solid #12774d; 159 | text-shadow:0px 1px 0px #158246; 160 | background-color:#00c17d; 161 | } 162 | .button:hover { 163 | background:linear-gradient(to bottom, #0061a7 5%, #007dc1 100%); 164 | text-decoration:none; 165 | background-color:#0061a7; 166 | } 167 | .button.green:hover { 168 | background:linear-gradient(to bottom, #00a761 5%, #00c17d 100%); 169 | background-color:#00a761; 170 | } 171 | .button:active { 172 | position:relative; 173 | top:1px; 174 | } 175 | 176 | /* Ripple effect */ 177 | .ripple { 178 | background-position: center; 179 | transition: background 0.4s; 180 | } 181 | .ripple:hover { 182 | background: #47a7f5 radial-gradient(circle, transparent 1%, #47a7f5 1%) center/15000%; 183 | } 184 | .ripple:active { 185 | background-color: #6eb9f7; 186 | background-size: 100%; 187 | transition: background 0s; 188 | } 189 | 190 | div.buttonbarpart { display:inline-block; margin: 0.1em; } 191 | select { padding:1px 2px 2px 2px} 192 | textarea { width:100%; min-width:100%; max-width:100%; height:50%; margin-top:0.25em; border-radius:2px; padding:0.5em; outline:none; white-space: pre; overflow: auto; } 193 | textarea:focus { box-shadow: inset 0 0 1px 1px #09F; } 194 | #formula { height:10%; } 195 | div.errortext { color:red; } 196 | 197 | /* The container
- needed to position the dropdown content */ 198 | .dropdown { 199 | position: relative; 200 | display: inline-block; 201 | text-align:left; 202 | } 203 | 204 | /* Dropdown Content (Hidden by Default) */ 205 | .dropdown-content { 206 | display: none; 207 | border: 1px solid #c0c0c0; 208 | position: absolute; 209 | left: -50px; 210 | background-color: #f0f0f0; 211 | min-width: 160px; 212 | box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); 213 | z-index: 1; 214 | } 215 | 216 | /* Links inside the dropdown */ 217 | .dropdown-content a { 218 | color: black; 219 | padding: 4px 16px; 220 | text-decoration: none; 221 | display: block; 222 | white-space: nowrap; 223 | } 224 | 225 | /* Change color of dropdown links on hover */ 226 | .dropdown-content a:hover {background-color: #8CF;} 227 | 228 | /* Show the dropdown menu on hover */ 229 | .dropdown:hover .dropdown-content {display: block;} 230 | 231 | /* Change the background color of the dropdown button when the dropdown content is shown */ 232 | .dropdown:hover .a.button {background-color: #3e8e41;} 233 | 234 | .editor { 235 | width:100%; 236 | min-height:20px; 237 | border:solid 1px silver; 238 | border-radius:3px; 239 | resize:vertical; 240 | overflow:hidden; 241 | z-index:0; 242 | } 243 | 244 | /* Hide cursor when the editor does not have focus */ 245 | .ace_hidden-cursors { 246 | opacity: 0; 247 | } 248 | 249 | .fullscreen { 250 | position:absolute; 251 | width:58px; 252 | height:34px; 253 | line-height:28px; 254 | right:5px; top:5px; 255 | background-color: white; 256 | border: 3.2px solid #6f90a6; 257 | border-radius: 8px; 258 | z-index:1; 259 | color:#6f90a6; 260 | text-align:center; 261 | cursor: pointer; 262 | } 263 | 264 | .fullscreen:hover { 265 | color:red; 266 | border-color:red; 267 | } 268 | 269 | div.playgroundright.displayfullscreen { 270 | position:absolute; 271 | width:100vw; 272 | } 273 | 274 | /**********************/ 275 | /* Scroll bars styles */ 276 | /**********************/ 277 | 278 | ::-webkit-scrollbar { 279 | width: 11px; 280 | height: 11px; 281 | } 282 | ::-webkit-scrollbar-button { 283 | width: 0px; 284 | height: 0px; 285 | } 286 | ::-webkit-scrollbar-thumb { 287 | background: #706040; 288 | border: 85px none #ffffff; 289 | border-radius: 50px; 290 | } 291 | ::-webkit-scrollbar-thumb:hover { 292 | background: #ff8000; 293 | } 294 | ::-webkit-scrollbar-thumb:active { 295 | background: #ff8000; 296 | } 297 | ::-webkit-scrollbar-track { 298 | background: #c0b8a8; 299 | border: 0px none #ffffff; 300 | border-radius: 100px; 301 | } 302 | ::-webkit-scrollbar-track:hover { 303 | background: #c0b8a8; 304 | } 305 | ::-webkit-scrollbar-track:active { 306 | background: #c0b8a8; 307 | } 308 | ::-webkit-scrollbar-corner { 309 | background: transparent; 310 | } 311 | 312 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import BaseMaterial from './src/smoothvoxels/basematerial' 2 | import Bits from './src/smoothvoxels/bits' 3 | import BoundingBox from './src/smoothvoxels/boundingbox' 4 | import Color from './src/smoothvoxels/color' 5 | import Light from './src/smoothvoxels/light' 6 | import Material from './src/smoothvoxels/material' 7 | import MaterialList from './src/smoothvoxels/materiallist' 8 | import SvoxMeshGenerator from './src/smoothvoxels/svoxmeshgenerator' 9 | import Model from './src/smoothvoxels/model' 10 | import ModelReader from './src/smoothvoxels/modelreader' 11 | import ModelWriter from './src/smoothvoxels/modelwriter' 12 | import Buffers from './src/smoothvoxels/buffers' 13 | import Noise from './src/smoothvoxels/noise' 14 | import Voxels, { VOXEL_FILTERS, MAX_SIZE, xyzRangeForSize, shiftForSize, voxColorForRGBT, voxBGRForHex, rgbtForVoxColor, REMOVE_VOXEL_COLOR } from './src/smoothvoxels/voxels' 15 | 16 | import voxToSvox from './src/vox-to-svox' 17 | import imgToSvox from './src/img-to-svox' 18 | 19 | export { 20 | BaseMaterial, 21 | Bits, 22 | BoundingBox, 23 | Color, 24 | Light, 25 | Noise, 26 | Material, 27 | MaterialList, 28 | SvoxMeshGenerator, 29 | Model, 30 | ModelReader, 31 | ModelWriter, 32 | Buffers, 33 | Voxels, 34 | xyzRangeForSize, 35 | shiftForSize, 36 | voxColorForRGBT, 37 | voxBGRForHex, 38 | rgbtForVoxColor, 39 | voxToSvox, 40 | imgToSvox, 41 | REMOVE_VOXEL_COLOR, 42 | MAX_SIZE, 43 | VOXEL_FILTERS 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smoothvoxels", 3 | "version": "1.2.8", 4 | "description": "Smooth Voxels", 5 | "scripts": { 6 | "build": "node scripts/build.mjs --production; node scripts/build-worker.mjs --production ; node scripts/build-aframe.mjs --production", 7 | "test": "exit 0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jel-app/svox.git" 12 | }, 13 | "keywords": [], 14 | "author": "Samuel van Egmond + Greg Fodor", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/jel-app/svox/issues" 18 | }, 19 | "homepage": "https://github.com/jel-app/svox#readme", 20 | "devDependencies": { 21 | "esbuild": "^0.14.54", 22 | "esbuild-plugin-inline-worker": "^0.1.1", 23 | "prettier": "^2.7.1", 24 | "prettier-standard": "^15.0.1", 25 | "standard": "^17.0.0", 26 | "tinyify": "^3.1.0" 27 | }, 28 | "peerDependencies": { 29 | "aframe": "^1.3.0" 30 | }, 31 | "dependencies": { 32 | "buffer": "^6.0.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/build-aframe.mjs: -------------------------------------------------------------------------------- 1 | import { join, basename, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { readFileSync, writeFileSync, renameSync } from 'fs' 4 | import { spawnSync } from 'child_process' 5 | import esbuild from 'esbuild' 6 | import inlineWorkerPlugin from 'esbuild-plugin-inline-worker' 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)) 9 | console.log('base path', join(__dirname, '..')) 10 | 11 | const buildConfig = { 12 | basePath: join(__dirname, '..'), 13 | bundle: true, 14 | constants: {}, 15 | entry: 'aframe.js', 16 | format: 'esm', 17 | minify: true, 18 | outdir: 'dist', 19 | sourcemap: false, 20 | platform: { name: 'browser', target: 'chrome', version: 96 } 21 | } 22 | 23 | const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')) 24 | 25 | class Builder { 26 | config = { 27 | production: false, 28 | verbose: false 29 | } 30 | 31 | write (msg) { 32 | process.stdout.write(`${msg}`.toString()) 33 | } 34 | 35 | writeln (msg) { 36 | this.write(`${msg}\n`) 37 | } 38 | 39 | async compile () { 40 | const result = await esbuild.build({ 41 | absWorkingDir: buildConfig.basePath, 42 | allowOverwrite: true, 43 | bundle: buildConfig.bundle, 44 | define: { 45 | __APP_VERSION__: `'${pkg.version}'`, 46 | __COMPILED_AT__: `'${new Date().toUTCString()}'`, 47 | ...buildConfig.constants 48 | }, 49 | entryPoints: [buildConfig.entry], 50 | format: buildConfig.format, 51 | logLevel: 'silent', 52 | metafile: true, 53 | minify: buildConfig.minify, 54 | outdir: buildConfig.outdir, 55 | platform: buildConfig.platform.name, 56 | sourcemap: buildConfig.sourcemap, 57 | plugins: [inlineWorkerPlugin()], 58 | target: `${buildConfig.platform.target}${buildConfig.platform.version}` 59 | }) 60 | 61 | return new Promise(resolve => resolve(result)) 62 | } 63 | 64 | sizeForDisplay (bytes) { 65 | return `${bytes / 1024}`.slice(0, 4) + ' kb' 66 | } 67 | 68 | reportCompileResults (results) { 69 | results.errors.forEach(errorMsg => this.writeln(`* Error: ${errorMsg}`)) 70 | results.warnings.forEach(msg => this.writeln(`* Warning: ${msg}`)) 71 | 72 | Object.keys(results.metafile.outputs).forEach(fn => { 73 | this.writeln(`* » created '${fn}' (${this.sizeForDisplay(results.metafile.outputs[fn].bytes)})`) 74 | }) 75 | } 76 | 77 | processArgv () { 78 | const argMap = { 79 | '--prod': { name: 'production', value: true }, 80 | '--production': { name: 'production', value: true }, 81 | '--verbose': { name: 'verbose', value: true }, 82 | '-p': { name: 'production', value: true }, 83 | '-v': { name: 'verbose', value: true } 84 | } 85 | 86 | process.argv 87 | .slice(2) 88 | .map(arg => { 89 | const hasMappedArg = typeof argMap[arg] === 'undefined' 90 | return hasMappedArg ? { name: arg.replace(/^-+/, ''), value: true } : argMap[arg] 91 | }) 92 | .forEach(data => (this.config[data.name] = data.value)) 93 | } 94 | 95 | convertToProductionFile () { 96 | const filename = basename(buildConfig.entry) 97 | const newFilename = `${pkg.name}-aframe.js` 98 | const contents = readFileSync(`${buildConfig.outdir}/${filename}`, { encoding: 'utf-8' }) 99 | 100 | spawnSync('chmod', ['+x', `${buildConfig.outdir}/${filename}`], { stdio: 'ignore' }) 101 | writeFileSync(`${buildConfig.outdir}/${filename}`, contents, { encoding: 'utf-8' }) 102 | renameSync(`${buildConfig.outdir}/${filename}`, `${buildConfig.outdir}/${newFilename}`) 103 | } 104 | 105 | async run () { 106 | this.processArgv() 107 | 108 | if (this.config.verbose) { 109 | this.writeln(`* Using esbuild v${esbuild.version}.`) 110 | } 111 | 112 | this.write(`* Compiling application...${this.config.verbose ? '\n' : ''}`) 113 | 114 | const startedTs = new Date().getTime() 115 | const results = await this.compile() 116 | const finishedTs = new Date().getTime() 117 | 118 | if (this.config.verbose) { 119 | this.reportCompileResults(results) 120 | } 121 | 122 | this.writeln((this.config.verbose ? '* D' : 'd') + `one. (${finishedTs - startedTs} ms)`) 123 | 124 | if (this.config.production) { 125 | this.convertToProductionFile() 126 | } 127 | } 128 | } 129 | 130 | new Builder().run() 131 | -------------------------------------------------------------------------------- /scripts/build-worker.mjs: -------------------------------------------------------------------------------- 1 | import { join, basename, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { readFileSync, writeFileSync, renameSync } from 'fs' 4 | import { spawnSync } from 'child_process' 5 | import esbuild from 'esbuild' 6 | import inlineWorkerPlugin from 'esbuild-plugin-inline-worker' 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)) 9 | console.log('base path', join(__dirname, '..')) 10 | 11 | const buildConfig = { 12 | basePath: join(__dirname, '..'), 13 | bundle: true, 14 | constants: {}, 15 | entry: 'worker.js', 16 | format: 'esm', 17 | minify: true, 18 | outdir: 'dist', 19 | sourcemap: false, 20 | platform: { name: 'browser', target: 'chrome', version: 96 } 21 | } 22 | 23 | const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')) 24 | 25 | class Builder { 26 | config = { 27 | production: false, 28 | verbose: false 29 | } 30 | 31 | write (msg) { 32 | process.stdout.write(`${msg}`.toString()) 33 | } 34 | 35 | writeln (msg) { 36 | this.write(`${msg}\n`) 37 | } 38 | 39 | async compile () { 40 | const result = await esbuild.build({ 41 | absWorkingDir: buildConfig.basePath, 42 | allowOverwrite: true, 43 | bundle: buildConfig.bundle, 44 | define: { 45 | __APP_VERSION__: `'${pkg.version}'`, 46 | __COMPILED_AT__: `'${new Date().toUTCString()}'`, 47 | ...buildConfig.constants 48 | }, 49 | entryPoints: [buildConfig.entry], 50 | format: buildConfig.format, 51 | logLevel: 'silent', 52 | metafile: true, 53 | minify: buildConfig.minify, 54 | outdir: buildConfig.outdir, 55 | platform: buildConfig.platform.name, 56 | sourcemap: buildConfig.sourcemap, 57 | plugins: [inlineWorkerPlugin()], 58 | target: `${buildConfig.platform.target}${buildConfig.platform.version}` 59 | }) 60 | 61 | return new Promise(resolve => resolve(result)) 62 | } 63 | 64 | sizeForDisplay (bytes) { 65 | return `${bytes / 1024}`.slice(0, 4) + ' kb' 66 | } 67 | 68 | reportCompileResults (results) { 69 | results.errors.forEach(errorMsg => this.writeln(`* Error: ${errorMsg}`)) 70 | results.warnings.forEach(msg => this.writeln(`* Warning: ${msg}`)) 71 | 72 | Object.keys(results.metafile.outputs).forEach(fn => { 73 | this.writeln(`* » created '${fn}' (${this.sizeForDisplay(results.metafile.outputs[fn].bytes)})`) 74 | }) 75 | } 76 | 77 | processArgv () { 78 | const argMap = { 79 | '--prod': { name: 'production', value: true }, 80 | '--production': { name: 'production', value: true }, 81 | '--verbose': { name: 'verbose', value: true }, 82 | '-p': { name: 'production', value: true }, 83 | '-v': { name: 'verbose', value: true } 84 | } 85 | 86 | process.argv 87 | .slice(2) 88 | .map(arg => { 89 | const hasMappedArg = typeof argMap[arg] === 'undefined' 90 | return hasMappedArg ? { name: arg.replace(/^-+/, ''), value: true } : argMap[arg] 91 | }) 92 | .forEach(data => (this.config[data.name] = data.value)) 93 | } 94 | 95 | convertToProductionFile () { 96 | const filename = basename(buildConfig.entry) 97 | const newFilename = `${pkg.name}-worker.js` 98 | const contents = readFileSync(`${buildConfig.outdir}/${filename}`, { encoding: 'utf-8' }) 99 | 100 | spawnSync('chmod', ['+x', `${buildConfig.outdir}/${filename}`], { stdio: 'ignore' }) 101 | writeFileSync(`${buildConfig.outdir}/${filename}`, contents, { encoding: 'utf-8' }) 102 | renameSync(`${buildConfig.outdir}/${filename}`, `${buildConfig.outdir}/${newFilename}`) 103 | } 104 | 105 | async run () { 106 | this.processArgv() 107 | 108 | if (this.config.verbose) { 109 | this.writeln(`* Using esbuild v${esbuild.version}.`) 110 | } 111 | 112 | this.write(`* Compiling application...${this.config.verbose ? '\n' : ''}`) 113 | 114 | const startedTs = new Date().getTime() 115 | const results = await this.compile() 116 | const finishedTs = new Date().getTime() 117 | 118 | if (this.config.verbose) { 119 | this.reportCompileResults(results) 120 | } 121 | 122 | this.writeln((this.config.verbose ? '* D' : 'd') + `one. (${finishedTs - startedTs} ms)`) 123 | 124 | if (this.config.production) { 125 | this.convertToProductionFile() 126 | } 127 | } 128 | } 129 | 130 | new Builder().run() 131 | -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { join, basename, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { readFileSync, writeFileSync, renameSync } from 'fs' 4 | import { spawnSync } from 'child_process' 5 | import esbuild from 'esbuild' 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)) 8 | console.log('base path', join(__dirname, '..')) 9 | 10 | const buildConfig = { 11 | basePath: join(__dirname, '..'), 12 | bundle: true, 13 | constants: {}, 14 | entry: 'index.js', 15 | format: 'esm', 16 | minify: true, 17 | outdir: 'dist', 18 | sourcemap: false, 19 | platform: { name: 'browser', target: 'chrome', version: 96 } 20 | } 21 | 22 | const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')) 23 | 24 | class Builder { 25 | config = { 26 | production: false, 27 | verbose: false 28 | } 29 | 30 | write (msg) { 31 | process.stdout.write(`${msg}`.toString()) 32 | } 33 | 34 | writeln (msg) { 35 | this.write(`${msg}\n`) 36 | } 37 | 38 | async compile () { 39 | const result = await esbuild.build({ 40 | absWorkingDir: buildConfig.basePath, 41 | allowOverwrite: true, 42 | bundle: buildConfig.bundle, 43 | define: { 44 | __APP_VERSION__: `'${pkg.version}'`, 45 | __COMPILED_AT__: `'${new Date().toUTCString()}'`, 46 | ...buildConfig.constants 47 | }, 48 | entryPoints: [buildConfig.entry], 49 | format: buildConfig.format, 50 | logLevel: 'silent', 51 | metafile: true, 52 | minify: buildConfig.minify, 53 | outdir: buildConfig.outdir, 54 | platform: buildConfig.platform.name, 55 | sourcemap: buildConfig.sourcemap, 56 | target: `${buildConfig.platform.target}${buildConfig.platform.version}` 57 | }) 58 | 59 | return new Promise(resolve => resolve(result)) 60 | } 61 | 62 | sizeForDisplay (bytes) { 63 | return `${bytes / 1024}`.slice(0, 4) + ' kb' 64 | } 65 | 66 | reportCompileResults (results) { 67 | results.errors.forEach(errorMsg => this.writeln(`* Error: ${errorMsg}`)) 68 | results.warnings.forEach(msg => this.writeln(`* Warning: ${msg}`)) 69 | 70 | Object.keys(results.metafile.outputs).forEach(fn => { 71 | this.writeln(`* » created '${fn}' (${this.sizeForDisplay(results.metafile.outputs[fn].bytes)})`) 72 | }) 73 | } 74 | 75 | processArgv () { 76 | const argMap = { 77 | '--prod': { name: 'production', value: true }, 78 | '--production': { name: 'production', value: true }, 79 | '--verbose': { name: 'verbose', value: true }, 80 | '-p': { name: 'production', value: true }, 81 | '-v': { name: 'verbose', value: true } 82 | } 83 | 84 | process.argv 85 | .slice(2) 86 | .map(arg => { 87 | const hasMappedArg = typeof argMap[arg] === 'undefined' 88 | return hasMappedArg ? { name: arg.replace(/^-+/, ''), value: true } : argMap[arg] 89 | }) 90 | .forEach(data => (this.config[data.name] = data.value)) 91 | } 92 | 93 | convertToProductionFile () { 94 | const filename = basename(buildConfig.entry) 95 | const newFilename = `${pkg.name}.js` 96 | const contents = readFileSync(`${buildConfig.outdir}/${filename}`, { encoding: 'utf-8' }) 97 | 98 | spawnSync('chmod', ['+x', `${buildConfig.outdir}/${filename}`], { stdio: 'ignore' }) 99 | writeFileSync(`${buildConfig.outdir}/${filename}`, contents, { encoding: 'utf-8' }) 100 | renameSync(`${buildConfig.outdir}/${filename}`, `${buildConfig.outdir}/${newFilename}`) 101 | } 102 | 103 | async run () { 104 | this.processArgv() 105 | 106 | if (this.config.verbose) { 107 | this.writeln(`* Using esbuild v${esbuild.version}.`) 108 | } 109 | 110 | this.write(`* Compiling application...${this.config.verbose ? '\n' : ''}`) 111 | 112 | const startedTs = new Date().getTime() 113 | const results = await this.compile() 114 | const finishedTs = new Date().getTime() 115 | 116 | if (this.config.verbose) { 117 | this.reportCompileResults(results) 118 | } 119 | 120 | this.writeln((this.config.verbose ? '* D' : 'd') + `one. (${finishedTs - startedTs} ms)`) 121 | 122 | if (this.config.production) { 123 | this.convertToProductionFile() 124 | } 125 | } 126 | } 127 | 128 | new Builder().run() 129 | -------------------------------------------------------------------------------- /src/img-to-svox/index.js: -------------------------------------------------------------------------------- 1 | import { MATSTANDARD, BOTH } from '../smoothvoxels/constants' 2 | import Model from '../smoothvoxels/model' 3 | import Voxels, { MAX_SIZE, voxBGRForHex, shiftForSize } from '../smoothvoxels/voxels' 4 | 5 | export default function imgToSvox (img, model = null, tempCanvas = null) { 6 | tempCanvas = tempCanvas || document.createElement('canvas') 7 | tempCanvas.id = 'tempCanvas' 8 | 9 | if (img.width >= img.height) { 10 | tempCanvas.width = Math.min(MAX_SIZE, img.width) 11 | tempCanvas.height = Math.floor(Math.min(MAX_SIZE, img.width) * (img.height / img.width)) 12 | } else { 13 | tempCanvas.width = Math.floor(Math.min(MAX_SIZE, img.height) * (img.width / img.height)) 14 | tempCanvas.height = Math.min(MAX_SIZE, img.height) 15 | } 16 | 17 | const ctx = tempCanvas.getContext('2d') 18 | ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, tempCanvas.width, tempCanvas.height) 19 | const pixels = ctx.getImageData(0, 0, tempCanvas.width, tempCanvas.height) 20 | 21 | const sizeX = Math.min(MAX_SIZE, tempCanvas.width) 22 | const sizeZ = Math.min(MAX_SIZE, tempCanvas.height) 23 | const scaleFactor = 1 / (Math.max(sizeX, sizeZ)) 24 | 25 | if (model === null) { 26 | model = new Model() 27 | model.scale = { x: scaleFactor, y: 0.1, z: scaleFactor } 28 | model.size = { x: sizeX, y: 1, z: sizeZ } 29 | model.origin = '+z' 30 | model.rotation = { x: 90, y: 0, z: 0 } 31 | } 32 | 33 | const material = model.materials.createMaterial(MATSTANDARD, BOTH, 0.5, 0, true) 34 | material.setDeform(10) 35 | material.clamp = 'y' 36 | const materialIndex = model.materials.materials.indexOf(material) 37 | 38 | let pixel = 0 39 | const pixColToVoxColor = new Map() 40 | 41 | for (let z = 0; z < tempCanvas.height; z++) { 42 | for (let x = 0; x < tempCanvas.width; x++) { 43 | if (x < MAX_SIZE && z < MAX_SIZE) { 44 | // 4096 Colors 45 | // let col = ((pixels.data[pixel+0]&0xF0)<<5) + ((pixels.data[pixel+1]&0xD0)) + ((pixels.data[pixel+2]&0xF0)>>4); 46 | 47 | // 512 Colors 48 | const pixCol = ((pixels.data[pixel + 0] & 0xE0) << 4) + ((pixels.data[pixel + 1] & 0xE0)) + ((pixels.data[pixel + 2] & 0xE0) >> 4) 49 | let hex = '000' + Number(pixCol).toString(16) 50 | hex = '#' + hex.substr(hex.length - 3, 3) 51 | 52 | if (pixels.data[pixel + 3] > 0) { 53 | if (!pixColToVoxColor.has(pixCol)) { 54 | const voxBgr = voxBGRForHex(hex) 55 | const voxColor = (voxBgr | (materialIndex << 24)) >>> 0 56 | pixColToVoxColor.set(pixCol, voxColor) 57 | } 58 | } 59 | } 60 | 61 | pixel += 4 62 | } 63 | } 64 | 65 | pixel = 0 66 | 67 | const numberOfColors = pixColToVoxColor.size 68 | let paletteBits = 1 69 | 70 | if (numberOfColors >= 2) { paletteBits = 2 } 71 | if (numberOfColors >= 4) { paletteBits = 4 } 72 | if (numberOfColors >= 16) { paletteBits = 8 } 73 | 74 | model.voxels = new Voxels([model.size.x, model.size.y, model.size.z], paletteBits) 75 | 76 | const xShift = shiftForSize(model.size.x) 77 | const zShift = shiftForSize(model.size.z) 78 | 79 | for (let z = 0; z < tempCanvas.height; z++) { 80 | for (let x = 0; x < tempCanvas.width; x++) { 81 | if (x < MAX_SIZE && z < MAX_SIZE) { 82 | // 4096 Colors 83 | // const pixCol = ((pixels.data[pixel+0]&0xF0)<<5) + ((pixels.data[pixel+1]&0xD0)) + ((pixels.data[pixel+2]&0xF0)>>4); 84 | 85 | // 512 Colors 86 | const pixCol = ((pixels.data[pixel + 0] & 0xE0) << 4) + ((pixels.data[pixel + 1] & 0xE0)) + ((pixels.data[pixel + 2] & 0xE0) >> 4) 87 | 88 | if (pixels.data[pixel + 3] > 0) { 89 | model.voxels.setColorAt(x - xShift, 0, z - zShift, pixColToVoxColor.get(pixCol)) 90 | } 91 | } 92 | 93 | pixel += 4 94 | } 95 | } 96 | 97 | return model 98 | } 99 | -------------------------------------------------------------------------------- /src/smoothvoxels/aframe.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME */ 2 | 3 | import ModelReader from './modelreader' 4 | import Buffers from './buffers' 5 | import SvoxMeshGenerator from './svoxmeshgenerator' 6 | import SvoxToThreeMeshConverter from './svoxtothreemeshconverter' 7 | import WorkerPool from './workerpool' 8 | 9 | // We are combining this file with others in the minified version that will be used also in the worker. 10 | // Do not register the svox component inside the worker 11 | if (typeof window !== 'undefined') { 12 | if (typeof AFRAME !== 'undefined') { 13 | /* ******************************** 14 | * TODO: 15 | * - Cleanup playground HTML and Code 16 | * - Multiple models combined in a scene 17 | * - Model layers (combine multiple layers, e.g. weapon models) 18 | * - Model animation? (including layers?) 19 | * 20 | ***********************************/ 21 | 22 | let WORKERPOOL = null 23 | 24 | /** 25 | * Smooth Voxels component for A-Frame. 26 | */ 27 | AFRAME.registerComponent('svox', { 28 | schema: { 29 | model: { type: 'string' }, 30 | worker: { type: 'boolean', default: true } 31 | }, 32 | 33 | /** 34 | * Set if component needs multiple instancing. 35 | */ 36 | multiple: false, 37 | 38 | _MISSING: 'model size=9,scale=0.05,material lighting=flat,colors=A:#FFFFFF B:#FF8800 C:#FF0000,voxels 10B7-2B-C3-C-2B2-C-C2-2B3-C3-2B2-C-C2-2B-C3-C-2B7-11B7-B-6(7A2-)7A-B7-2B-C3-C-B-7A-C7AC-2(7A2-)7A-C7AC-7A-B-C3-C-2B2-C-C2-B-7A2-2(7A-C7AC-)7A2-7A-B2-C-C2-2B3-C3-B-2(7A2-)7A-C7AC-2(7A2-)7A-B3-C3-2B2-C-C2-B-7A2-2(7A-C7AC-)7A2-7A-B2-C-C2-2B-C3-C-B-7A-C7AC-2(7A2-)7A-C7AC-7A-B-C3-C-2B7-B-6(7A2-)7A-B7-11B7-2B-C3-C-2B2-C-C2-2B3-C3-2B2-C-C2-2B-C3-C-2B7-10B', 39 | _ERROR: 'model size=9,scale=0.05,material lighting=flat,colors=B:#FF8800 C:#FF0000 A:#FFFFFF,voxels 10B7-2(2B2-3C2-2B4-C2-)2B2-3C2-2B7-11B7-B-6(7A2-)7A-B7-2B2-3C2-B-6(7A2-)7A-B2-3C2-2B2-C4-B-2(7A-C7A2C)7A-C7AC-7A-B2-C4-2B2-3C2-B3(-7A-C7AC)-7A-B2-3C2-2B2-C4-B-7A-C2(7AC-7A2C)7AC-7A-B2-C4-2B2-3C2-B-6(7A2-)7A-B2-3C2-2B7-B-6(7A2-)7A-B7-11B7-2(2B2-3C2-2B2-C4-)2B2-3C2-2B7-10B', 40 | _workerPool: null, 41 | 42 | /** 43 | * Called once when component is attached. Generally for initial setup. 44 | */ 45 | init: function () { 46 | const el = this.el 47 | const data = this.data 48 | let useWorker = data.worker 49 | let error = false 50 | 51 | const modelName = data.model 52 | let modelString = window.SVOX.models[modelName] 53 | if (!modelString) { 54 | this._logError({ name: 'ConfigError', message: 'Model not found' }) 55 | modelString = this._MISSING 56 | error = true 57 | useWorker = false 58 | } 59 | 60 | if (!useWorker) { 61 | this._generateModel(modelString, el, error) 62 | } else { 63 | this._generateModelInWorker(modelString, el) 64 | } 65 | }, 66 | 67 | _generateModel: function (modelString, el, error) { 68 | let model 69 | model = window.model = ModelReader.readFromString(modelString) 70 | 71 | // let meshGenerator = new MeshGenerator(); 72 | // this.mesh = meshGenerator.generate(model); 73 | 74 | // for (let i = 0; i < 5; i++) { 75 | // SvoxMeshGenerator.generate(model); 76 | // //SvoxToThreeMeshConverter.generate(svoxmesh); 77 | // } 78 | 79 | // Set based on magicavoxel menger 80 | const buffers = new Buffers(1024 * 768 * 2) 81 | const t0 = performance.now() 82 | const svoxmesh = SvoxMeshGenerator.generate(model, buffers) 83 | // console.log('SvoxMeshGenerator.generate took ' + (performance.now() - t0) + ' ms.') 84 | const t1 = performance.now() 85 | this.mesh = SvoxToThreeMeshConverter.generate(svoxmesh) 86 | 87 | // Log stats 88 | const statsText = `Time: ${Math.round(t1 - t0)}ms. Verts:${svoxmesh.maxIndex + 1} Faces:${svoxmesh.indices.length / 3} Materials:${this.mesh.material.length}` 89 | // console.log(`SVOX ${this.data.model}: ${statsText}`); 90 | const statsEl = document.getElementById('svoxstats') 91 | if (statsEl && !error) { statsEl.innerHTML = 'Last render: ' + statsText } 92 | 93 | el.setObject3D('mesh', this.mesh) 94 | }, 95 | 96 | _generateModelInWorker: function (svoxmodel, el) { 97 | // Make sure the element has an Id, create a task in the task array and process it 98 | if (!el.id) { el.id = new Date().valueOf().toString(36) + Math.random().toString(36).substr(2) } 99 | const task = { svoxmodel, elementId: el.id } 100 | 101 | if (!WORKERPOOL) { 102 | WORKERPOOL = new WorkerPool(this, this._processResult) 103 | } 104 | WORKERPOOL.executeTask(task) 105 | }, 106 | 107 | _processResult: function (data) { 108 | if (data.svoxmesh.error) { 109 | this._logError(data.svoxmesh.error) 110 | } else { 111 | const mesh = SvoxToThreeMeshConverter.generate(data.svoxmesh) 112 | const el = document.querySelector('#' + data.elementId) 113 | 114 | el.setObject3D('mesh', mesh) 115 | } 116 | }, 117 | 118 | _toSharedArrayBuffer (floatArray) { 119 | const buffer = new Float32Array(new ArrayBuffer(floatArray.length * 4)) 120 | buffer.set(floatArray, 0) 121 | return buffer 122 | }, 123 | 124 | /** 125 | * Log errors to the console and an optional div #svoxerrors (as in the playground) 126 | * @param {modelName} The name of the model being loaded 127 | * @param {error} Error object with name and message 128 | */ 129 | _logError: function (error) { 130 | const errorText = error.name + ': ' + error.message 131 | const errorElement = document.getElementById('svoxerrors') 132 | if (errorElement) { errorElement.innerHTML = errorText } 133 | console.error(`SVOXERROR (${this.data.model}) ${errorText}`) 134 | }, 135 | 136 | /** 137 | * Called when component is attached and when component data changes. 138 | * Generally modifies the entity based on the data. 139 | * @param {object} oldData The previous version of the data 140 | */ 141 | update: function (oldData) { }, 142 | 143 | /** 144 | * Called when a component is removed (e.g., via removeAttribute). 145 | */ 146 | remove: function () { 147 | const maps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'matcap'] 148 | 149 | if (this.mesh) { // TODO: Test 150 | while (this.mesh.material.length > 0) { 151 | maps.forEach(function (map) { 152 | if (this.mesh.material[0][map]) { 153 | this.mesh.material[0][map].dispose() 154 | } 155 | }, this) 156 | 157 | this.mesh.material[0].dispose() 158 | this.mesh.material.shift() 159 | } 160 | 161 | this.mesh.geometry.dispose() 162 | this.el.removeObject3D('mesh') 163 | delete this.mesh 164 | } 165 | }, 166 | 167 | /** 168 | * Called on each scene tick. 169 | */ 170 | // tick: function (t) { }, 171 | 172 | /** 173 | * Called when entity pauses. 174 | * Use to stop or remove any dynamic or background behavior such as events. 175 | */ 176 | pause: function () { }, 177 | 178 | /** 179 | * Called when entity resumes. 180 | * Use to continue or add any dynamic or background behavior such as events. 181 | */ 182 | play: function () { }, 183 | 184 | /** 185 | * Event handlers that automatically get attached or detached based on scene state. 186 | */ 187 | events: { 188 | // click: function (evt) { } 189 | } 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/smoothvoxels/basematerial.js: -------------------------------------------------------------------------------- 1 | import { MATSTANDARD, MATBASIC, MATPHONG, MATTOON, MATMATCAP, MATNORMAL, MATLAMBERT, FRONT, BACK, DOUBLE } from './constants' 2 | import Color from './color' 3 | 4 | export default class BaseMaterial { 5 | constructor (type, roughness, metalness, 6 | opacity, alphaTest, transparent, refractionRatio, wireframe, side, 7 | emissiveColor, emissiveIntensity, fog, 8 | map, normalMap, roughnessMap, metalnessMap, emissiveMap, matcap, 9 | reflectionMap, refractionMap, 10 | uscale, vscale, uoffset, voffset, rotation) { 11 | type = type || MATSTANDARD 12 | 13 | switch (type) { 14 | case MATSTANDARD: 15 | case MATBASIC: 16 | case MATLAMBERT: 17 | case MATPHONG: 18 | case MATTOON: 19 | case MATMATCAP: 20 | case MATNORMAL: 21 | // Type is ok 22 | break 23 | default: { 24 | throw new Error('SyntaxError: Unknown material type: ' + type) 25 | } 26 | } 27 | this.type = type 28 | 29 | if (((map && map.cube) || (normalMap && normalMap.cube) || (roughnessMap && roughnessMap.cube) || (metalnessMap && metalnessMap.cube) || (emissiveMap && emissiveMap.cube)) && 30 | !(uscale === -1 && vscale === -1)) { 31 | throw new Error('SyntaxError: Cube textures can not be combined with maptransform') 32 | } 33 | 34 | if (reflectionMap && refractionMap) { 35 | throw new Error('SyntaxError: One material can have a reflectionmap or a refractionmap, but not both') 36 | } 37 | 38 | this.index = 0 39 | 40 | // Standard material values 41 | this.roughness = typeof roughness === 'number' ? roughness : 1 42 | this.metalness = typeof metalness === 'number' ? metalness : 0 43 | this.opacity = typeof opacity === 'number' ? opacity : 1 44 | this.alphaTest = typeof alphaTest === 'number' ? alphaTest : 0 45 | this.transparent = !!transparent 46 | this.refractionRatio = typeof refractionRatio === 'number' ? refractionRatio : 0.9 47 | this.wireframe = !!wireframe 48 | this.side = side || FRONT 49 | if (![FRONT, BACK, DOUBLE].includes(this.side)) { this.side = FRONT } 50 | this.setEmissive(emissiveColor, emissiveIntensity) 51 | this.fog = typeof fog === 'boolean' ? fog : true 52 | 53 | this.map = map 54 | this.normalMap = normalMap 55 | this.roughnessMap = roughnessMap 56 | this.metalnessMap = metalnessMap 57 | this.emissiveMap = emissiveMap 58 | this.matcap = matcap 59 | this.reflectionMap = reflectionMap 60 | this.refractionMap = refractionMap 61 | this.mapTransform = { 62 | uscale: uscale || -1, 63 | vscale: vscale || -1, 64 | uoffset: uoffset || 0, 65 | voffset: voffset || 0, 66 | rotation: rotation || 0 67 | } 68 | 69 | this.aoActive = false 70 | } 71 | 72 | get baseId () { 73 | if (this._baseId === undefined) { 74 | this._baseId = `${this.type}|${this.roughness}|${this.metalness}|` + 75 | `${this.opacity}|${this.alphaTest}|${this.transparent ? 1 : 0}|` + 76 | `${this.refractionRatio}|${this.wireframe ? 1 : 0}|${this.side}|` + 77 | (this.emissive ? `${this.emissive.color}|${this.emissive.intensity}|` : '||') + 78 | `${this.fog ? 1 : 0}|` + 79 | (this.map ? `${this.map.id}|` : '|') + 80 | (this.normalMap ? `${this.normalMap.id}|` : '|') + 81 | (this.roughnessMap ? `${this.roughnessMap.id}|` : '|') + 82 | (this.metalnessMap ? `${this.metalnessMap.id}|` : '|') + 83 | (this.emissiveMap ? `${this.emissiveMap.id}|` : '|') + 84 | (this.matcap ? `${this.matcap.id}|` : '|') + 85 | (this.reflectionMap ? `${this.reflectionMap.id}|` : '|') + 86 | (this.refractionMap ? `${this.refractionMap.id}|` : '|') + 87 | `${this.mapTransform.uscale}|${this.mapTransform.vscale}|` + 88 | `${this.mapTransform.uoffset}|${this.mapTransform.voffset}|` + 89 | `${this.mapTransform.rotation}` 90 | } 91 | 92 | return this._baseId 93 | } 94 | 95 | get isTransparent () { 96 | return this.transparent || this.opacity < 1.0 97 | } 98 | 99 | setEmissive (color, intensity) { 100 | if (color === undefined || color === '#000' || color === '#000000' || !(intensity || 0)) { this._emissive = undefined } else { this._emissive = { color: Color.fromHex(color), intensity } } 101 | } 102 | 103 | get emissive () { 104 | return this._emissive 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/smoothvoxels/bits.js: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/inolen/bit-buffer/blob/master/bit-buffer.js 2 | /* eslint-disable max-classes-per-file */ 3 | function set (offset, value, bits, view) { 4 | let bufOffset = bits * offset 5 | 6 | for (let i = 0; i < bits;) { 7 | const bitOffset = bufOffset & 7 8 | const byteOffset = bufOffset >> 3 9 | const remaining = bits - i 10 | const residual = 8 - bitOffset 11 | 12 | const wrote = remaining < residual ? remaining : residual 13 | 14 | // create a mask with the correct bit width 15 | const mask = ~(0xFF << wrote) 16 | 17 | // shift the bits we want to the start of the byte and mask of the rest 18 | const writeBits = value & mask 19 | value >>= wrote 20 | const destMask = ~(mask << bitOffset) 21 | view[byteOffset] = (view[byteOffset] & destMask) | (writeBits << bitOffset) 22 | bufOffset += wrote 23 | i += wrote 24 | } 25 | } 26 | 27 | class Bits1 { 28 | constructor (view) { this.view = view } 29 | 30 | get (offset) { return ((this.view[offset >> 3] >> (offset & 7)) & 0x1) } 31 | 32 | set (offset, value) { return set(offset, value, 1, this.view) } 33 | 34 | clear () { this.view.fill(0) } 35 | } 36 | 37 | class Bits2 { 38 | constructor (view) { this.view = view } 39 | 40 | get (offset) { return ((this.view[offset >> 2] >> ((offset & 3) << 1)) & 0x3) } 41 | 42 | set (offset, value) { return set(offset, value, 2, this.view) } 43 | 44 | clear () { this.view.fill(0) } 45 | } 46 | 47 | class Bits4 { 48 | constructor (view) { this.view = view } 49 | 50 | get (offset) { return ((this.view[offset >> 1] >> ((offset & 1) << 2)) & 0xF) } 51 | 52 | set (offset, value) { return set(offset, value, 4, this.view) } 53 | 54 | clear () { this.view.fill(0) } 55 | } 56 | 57 | class Bits8 { 58 | constructor (view) { this.view = view } 59 | 60 | get (offset) { return this.view[offset] >>> 0 } 61 | 62 | set (offset, value) { return set(offset, value, 8, this.view) } 63 | 64 | clear () { this.view.fill(0) } 65 | } 66 | 67 | class BitsN { 68 | constructor (view, bits) { 69 | this.view = view 70 | this.bits = bits 71 | } 72 | 73 | get (offset) { 74 | const { view, bits } = this 75 | let bufOffset = offset * bits 76 | let value = 0 77 | for (let i = 0; i < bits;) { 78 | const bitOffset = bufOffset & 7 79 | const byteOffset = bufOffset >> 3 80 | 81 | const remaining = bits - i 82 | const residual = 8 - bitOffset 83 | 84 | const read = remaining < residual ? remaining : residual 85 | const currentByte = view[byteOffset] 86 | 87 | const mask = ~(0xFF << read) 88 | const readBits = (currentByte >> bitOffset) & mask 89 | value |= readBits << i 90 | 91 | bufOffset += read 92 | i += read 93 | } 94 | 95 | return value >>> 0 96 | } 97 | 98 | set (offset, value) { 99 | set(offset, value, this.bits, this.view) 100 | } 101 | 102 | clear () { 103 | this.view.fill(0) 104 | } 105 | } 106 | 107 | export default class Bits { 108 | static create (buffer, bits, offset, length = null) { 109 | const view = length ? new Uint8Array(buffer, offset, length) : new Uint8Array(buffer, offset) 110 | 111 | switch (bits) { 112 | case 1: 113 | return new Bits1(view) 114 | case 2: 115 | return new Bits2(view) 116 | case 4: 117 | return new Bits4(view) 118 | case 8: 119 | return new Bits8(view) 120 | default: 121 | return new BitsN(view) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/smoothvoxels/boundingbox.js: -------------------------------------------------------------------------------- 1 | export default class BoundingBox { 2 | get size () { 3 | if (this.minX > this.maxX) { return { x: 0, y: 0, z: 0 } } else { 4 | return { 5 | x: this.maxX - this.minX + 1, 6 | y: this.maxY - this.minY + 1, 7 | z: this.maxZ - this.minZ + 1 8 | } 9 | } 10 | } 11 | 12 | constructor () { 13 | this.reset() 14 | } 15 | 16 | reset () { 17 | this.minX = Number.POSITIVE_INFINITY 18 | this.minY = Number.POSITIVE_INFINITY 19 | this.minZ = Number.POSITIVE_INFINITY 20 | this.maxX = Number.NEGATIVE_INFINITY 21 | this.maxY = Number.NEGATIVE_INFINITY 22 | this.maxZ = Number.NEGATIVE_INFINITY 23 | } 24 | 25 | set (x, y, z) { 26 | this.minX = Math.min(this.minX, x) 27 | this.minY = Math.min(this.minY, y) 28 | this.minZ = Math.min(this.minZ, z) 29 | this.maxX = Math.max(this.maxX, x) 30 | this.maxY = Math.max(this.maxY, y) 31 | this.maxZ = Math.max(this.maxZ, z) 32 | } 33 | 34 | // End of class BoundingBox 35 | } 36 | -------------------------------------------------------------------------------- /src/smoothvoxels/buffers.js: -------------------------------------------------------------------------------- 1 | import Bits from './bits' 2 | 3 | export default class Buffers { 4 | constructor (maxVerts) { 5 | const maxVertBits = Math.floor(maxVerts / 8) 6 | const maxFaces = maxVerts / 4 7 | const maxFaceBits = Math.floor(maxFaces / 8) 8 | const maxFaceVerts = maxFaces * 4 9 | 10 | this.maxFaces = maxFaces 11 | 12 | this.tmpVertIndexLookup = new Map() 13 | 14 | this.vertX = new Float32Array(maxVerts) 15 | this.vertY = new Float32Array(maxVerts) 16 | this.vertZ = new Float32Array(maxVerts) 17 | 18 | // Used for deform 19 | this.vertTmpX = new Float32Array(maxVerts) 20 | this.vertTmpY = new Float32Array(maxVerts) 21 | this.vertTmpZ = new Float32Array(maxVerts) 22 | this.vertHasTmp = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 23 | 24 | this.vertColorR = new Float32Array(maxVerts * 6) 25 | this.vertColorG = new Float32Array(maxVerts * 6) 26 | this.vertColorB = new Float32Array(maxVerts * 6) 27 | this.vertColorCount = new Uint8Array(maxVerts) 28 | 29 | this.vertSmoothNormalX = new Float32Array(maxVerts) 30 | this.vertSmoothNormalY = new Float32Array(maxVerts) 31 | this.vertSmoothNormalZ = new Float32Array(maxVerts) 32 | this.vertBothNormalX = new Float32Array(maxVerts) 33 | this.vertBothNormalY = new Float32Array(maxVerts) 34 | this.vertBothNormalZ = new Float32Array(maxVerts) 35 | this.vertFlattenedX = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 36 | this.vertFlattenedY = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 37 | this.vertFlattenedZ = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 38 | this.vertClampedX = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 39 | this.vertClampedY = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 40 | this.vertClampedZ = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 41 | this.vertFullyClamped = Bits.create(new Uint8Array(maxVertBits).buffer, 1, 0) 42 | this.vertDeformCount = new Uint8Array(maxVerts) 43 | this.vertDeformDamping = new Float32Array(maxVerts) 44 | this.vertDeformStrength = new Float32Array(maxVerts) 45 | this.vertWarpAmplitude = new Float32Array(maxVerts) 46 | this.vertWarpFrequency = new Float32Array(maxVerts) 47 | this.vertScatter = new Float32Array(maxVerts) 48 | this.vertRing = new Float32Array(maxVerts) 49 | this.vertNrOfClampedLinks = new Uint8Array(maxVerts) 50 | this.vertLinkCounts = new Uint8Array(maxVerts) // A vert can be linked to up to 6 other verts 51 | this.vertLinkIndices = new Uint32Array(maxVerts * 6) 52 | 53 | this.faceFlattened = Bits.create(new Uint8Array(maxFaceBits).buffer, 1, 0) 54 | this.faceClamped = Bits.create(new Uint8Array(maxFaceBits).buffer, 1, 0) 55 | this.faceSmooth = Bits.create(new Uint8Array(maxFaceBits).buffer, 1, 0) 56 | this.faceEquidistant = Bits.create(new Uint8Array(maxFaceBits).buffer, 1, 0) 57 | this.faceCulled = Bits.create(new Uint8Array(maxFaceBits).buffer, 1, 0) // Bits for removed faces from simplify 58 | this.faceNameIndices = new Uint8Array(maxFaces) 59 | this.faceMaterials = new Uint8Array(maxFaces) 60 | 61 | this.faceVertIndices = new Uint32Array(maxFaceVerts) 62 | this.faceVertNormalX = new Float32Array(maxFaceVerts) 63 | this.faceVertNormalY = new Float32Array(maxFaceVerts) 64 | this.faceVertNormalZ = new Float32Array(maxFaceVerts) 65 | this.faceVertFlatNormalX = new Float32Array(maxFaceVerts) 66 | this.faceVertFlatNormalY = new Float32Array(maxFaceVerts) 67 | this.faceVertFlatNormalZ = new Float32Array(maxFaceVerts) 68 | this.faceVertSmoothNormalX = new Float32Array(maxFaceVerts) 69 | this.faceVertSmoothNormalY = new Float32Array(maxFaceVerts) 70 | this.faceVertSmoothNormalZ = new Float32Array(maxFaceVerts) 71 | this.faceVertBothNormalX = new Float32Array(maxFaceVerts) 72 | this.faceVertBothNormalY = new Float32Array(maxFaceVerts) 73 | this.faceVertBothNormalZ = new Float32Array(maxFaceVerts) 74 | this.faceVertColorR = new Float32Array(maxFaceVerts) 75 | this.faceVertColorG = new Float32Array(maxFaceVerts) 76 | this.faceVertColorB = new Float32Array(maxFaceVerts) 77 | this.faceVertLightR = new Float32Array(maxFaceVerts) 78 | this.faceVertLightG = new Float32Array(maxFaceVerts) 79 | this.faceVertLightB = new Float32Array(maxFaceVerts) 80 | this.faceVertAO = new Float32Array(maxFaceVerts) 81 | this.faceVertUs = new Float32Array(maxFaceVerts) 82 | this.faceVertVs = new Float32Array(maxFaceVerts) 83 | 84 | this.tmpVoxelXZYFaceIndices = Array(maxFaces).fill(0) 85 | this.tmpVoxelXYZFaceIndices = Array(maxFaces).fill(0) 86 | this.tmpVoxelYZXFaceIndices = Array(maxFaces).fill(0) 87 | this.voxelXZYFaceIndices = null 88 | this.voxelXYZFaceIndices = null 89 | this.voxelYZXFaceIndices = null 90 | } 91 | 92 | clear () { 93 | this.tmpVertIndexLookup.clear() 94 | this.vertX.fill(0) 95 | this.vertY.fill(0) 96 | this.vertZ.fill(0) 97 | 98 | this.vertTmpX.fill(0) 99 | this.vertTmpY.fill(0) 100 | this.vertTmpZ.fill(0) 101 | this.vertHasTmp.clear() 102 | 103 | this.vertColorR.fill(0) 104 | this.vertColorG.fill(0) 105 | this.vertColorB.fill(0) 106 | this.vertColorCount.fill(0) 107 | 108 | this.vertSmoothNormalX.fill(0) 109 | this.vertSmoothNormalY.fill(0) 110 | this.vertSmoothNormalZ.fill(0) 111 | this.vertBothNormalX.fill(0) 112 | this.vertBothNormalY.fill(0) 113 | this.vertBothNormalZ.fill(0) 114 | this.vertFlattenedX.clear() 115 | this.vertFlattenedY.clear() 116 | this.vertFlattenedZ.clear() 117 | this.vertClampedX.clear() 118 | this.vertClampedY.clear() 119 | this.vertClampedZ.clear() 120 | this.vertFullyClamped.clear() 121 | this.vertDeformCount.fill(0) 122 | this.vertDeformDamping.fill(0) 123 | this.vertDeformStrength.fill(0) 124 | this.vertWarpAmplitude.fill(0) 125 | this.vertWarpFrequency.fill(0) 126 | this.vertScatter.fill(0) 127 | this.vertRing.fill(0) 128 | this.vertNrOfClampedLinks.fill(0) 129 | this.vertLinkCounts.fill(0) 130 | this.vertLinkIndices.fill(0) 131 | 132 | this.faceFlattened.clear() 133 | this.faceClamped.clear() 134 | this.faceSmooth.clear() 135 | this.faceEquidistant.clear() 136 | this.faceCulled.clear() 137 | this.faceNameIndices.fill(0) 138 | this.faceMaterials.fill(0) 139 | 140 | this.faceVertIndices.fill(0) 141 | this.faceVertNormalX.fill(0) 142 | this.faceVertNormalY.fill(0) 143 | this.faceVertNormalZ.fill(0) 144 | this.faceVertFlatNormalX.fill(0) 145 | this.faceVertFlatNormalY.fill(0) 146 | this.faceVertFlatNormalZ.fill(0) 147 | this.faceVertSmoothNormalX.fill(0) 148 | this.faceVertSmoothNormalY.fill(0) 149 | this.faceVertSmoothNormalZ.fill(0) 150 | this.faceVertBothNormalX.fill(0) 151 | this.faceVertBothNormalY.fill(0) 152 | this.faceVertBothNormalZ.fill(0) 153 | this.faceVertColorR.fill(0) 154 | this.faceVertColorG.fill(0) 155 | this.faceVertColorB.fill(0) 156 | this.faceVertLightR.fill(0) 157 | this.faceVertLightG.fill(0) 158 | this.faceVertLightB.fill(0) 159 | this.faceVertAO.fill(0) 160 | this.faceVertUs.fill(0) 161 | this.faceVertVs.fill(0) 162 | 163 | this.tmpVoxelXZYFaceIndices.length = 0 164 | this.tmpVoxelXYZFaceIndices.length = 0 165 | this.tmpVoxelYZXFaceIndices.length = 0 166 | this.voxelXZYFaceIndices = null 167 | this.voxelXYZFaceIndices = null 168 | this.voxelYZXFaceIndices = null 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/smoothvoxels/color.js: -------------------------------------------------------------------------------- 1 | const clamp = (num, min, max) => Math.min(Math.max(num, min), max) 2 | 3 | /* Note, the Color class only supports hexadecimal colors like #FFF or #FFFFFF. */ 4 | /* Its r, g and b members are stored as floats between 0 and 1. */ 5 | 6 | export default class Color { 7 | static fromHex (hex) { 8 | const color = new Color() 9 | color._set(hex) 10 | 11 | color.id = '' 12 | color.exId = null // Used for MagicaVoxel color index 13 | color.count = 0 14 | 15 | return color 16 | } 17 | 18 | // r, g, b from 0 to 1 !! 19 | static fromRgb (r, g, b) { 20 | r = Math.round(clamp(r, 0, 1) * 255) 21 | g = Math.round(clamp(g, 0, 1) * 255) 22 | b = Math.round(clamp(b, 0, 1) * 255) 23 | const color = '#' + 24 | (r < 16 ? '0' : '') + r.toString(16) + 25 | (g < 16 ? '0' : '') + g.toString(16) + 26 | (b < 16 ? '0' : '') + b.toString(16) 27 | return Color.fromHex(color) 28 | } 29 | 30 | clone () { 31 | const clone = new Color() 32 | clone._color = this._color 33 | clone.r = this.r 34 | clone.g = this.g 35 | clone.b = this.b 36 | clone._material = this._material 37 | return clone 38 | } 39 | 40 | multiply (factor) { 41 | if (factor instanceof Color) { return Color.fromRgb(this.r * factor.r, this.g * factor.g, this.b * factor.b) } else { return Color.fromRgb(this.r * factor, this.g * factor, this.b * factor) } 42 | } 43 | 44 | normalize () { 45 | const d = Math.sqrt(this.r * this.r + this.g * this.g + this.b * this.b) 46 | return this.multiply(1 / d) 47 | } 48 | 49 | add (...colors) { 50 | const r = this.r + colors.reduce((sum, color) => sum + color.r, 0) 51 | const g = this.g + colors.reduce((sum, color) => sum + color.g, 0) 52 | const b = this.b + colors.reduce((sum, color) => sum + color.b, 0) 53 | return Color.fromRgb(r, g, b) 54 | } 55 | 56 | _setMaterial (material) { 57 | if (this._material !== undefined) { throw new Error('A Color can only be added once.') } 58 | 59 | this._material = material 60 | this.count = 0 61 | } 62 | 63 | get material () { 64 | return this._material 65 | } 66 | 67 | _set (colorValue) { 68 | let color = colorValue 69 | if (typeof color === 'string' || color instanceof String) { 70 | color = color.trim().toUpperCase() 71 | if (color.match(/^#([0-9a-fA-F]{3}|#?[0-9a-fA-F]{6})$/)) { 72 | color = color.replace('#', '') 73 | 74 | this._color = '#' + color 75 | 76 | if (color.length === 3) { 77 | color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2] 78 | } 79 | 80 | // Populate .r .g and .b 81 | const value = parseInt(color, 16) 82 | this.r = ((value >> 16) & 255) / 255 83 | this.g = ((value >> 8) & 255) / 255 84 | this.b = (value & 255) / 255 85 | 86 | return 87 | } 88 | } 89 | 90 | throw new Error(`SyntaxError: Color ${colorValue} is not a hexadecimal color of the form #000 or #000000.`) 91 | } 92 | 93 | toString () { 94 | return this._color 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/smoothvoxels/colorcombiner.js: -------------------------------------------------------------------------------- 1 | export default class ColorCombiner { 2 | static combineColors (model, buffers) { 3 | const { vertColorR, vertColorG, vertColorB, vertColorCount, faceVertColorR, faceVertColorG, faceVertColorB, faceVertLightR, faceVertLightG, faceVertLightB, faceVertIndices, faceMaterials, faceVertAO } = buffers 4 | const materials = model.materials.materials 5 | 6 | // No need to fade colors when there is no material with fade 7 | const fadeAny = !!model.materials.find(m => m.fade) 8 | 9 | const fadeMaterials = Array(materials.length).fill(false) 10 | 11 | for (let m = 0, l = materials.length; m < l; m++) { 12 | if (fadeAny && materials[m].fade) { 13 | fadeMaterials[m] = true 14 | } 15 | } 16 | 17 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 18 | const fadeFace = fadeMaterials[faceMaterials[faceIndex]] 19 | 20 | if (fadeFace) { 21 | // Fade vertex colors 22 | for (let v = 0; v < 4; v++) { 23 | let r = 0 24 | let g = 0 25 | let b = 0 26 | let count = 0 27 | 28 | const faceVertOffset = faceIndex * 4 + v 29 | const vertIndex = faceVertIndices[faceVertOffset] 30 | const colorCount = vertColorCount[vertIndex] 31 | 32 | for (let c = 0; c < colorCount; c++) { 33 | const faceColorOffset = vertIndex * 6 + c 34 | r += vertColorR[faceColorOffset] 35 | g += vertColorG[faceColorOffset] 36 | b += vertColorB[faceColorOffset] 37 | count++ 38 | } 39 | 40 | const d = 1.0 / count 41 | faceVertColorR[faceVertOffset] = r * d 42 | faceVertColorG[faceVertOffset] = g * d 43 | faceVertColorB[faceVertOffset] = b * d 44 | } 45 | } 46 | } 47 | 48 | const doAo = model.ao || model.materials.find(function (m) { return m.ao }) 49 | const doLights = model.lights.length > 0 50 | 51 | if (doAo && doLights) { 52 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 53 | const material = materials[faceMaterials[faceIndex]] 54 | const vAoShared = material.ao || model.ao 55 | const vAoSharedColor = vAoShared ? vAoShared.color : null 56 | 57 | // Face colors are already set to voxel color during model load 58 | for (let v = 0; v < 4; v++) { 59 | const faceVertOffset = faceIndex * 4 + v 60 | const vR = faceVertColorR[faceVertOffset] 61 | const vG = faceVertColorG[faceVertOffset] 62 | const vB = faceVertColorB[faceVertOffset] 63 | 64 | const vAoColorR = vAoSharedColor ? vAoSharedColor.r : vR 65 | const vAoColorG = vAoSharedColor ? vAoSharedColor.g : vG 66 | const vAoColorB = vAoSharedColor ? vAoSharedColor.b : vB 67 | const vAo = 1 - faceVertAO[faceVertOffset] 68 | 69 | faceVertColorR[faceVertOffset] = vR * faceVertLightR[faceVertOffset] * vAo + vAoColorR * (1 - vAo) 70 | faceVertColorG[faceVertOffset] = vG * faceVertLightG[faceVertOffset] * vAo + vAoColorG * (1 - vAo) 71 | faceVertColorB[faceVertOffset] = vB * faceVertLightB[faceVertOffset] * vAo + vAoColorB * (1 - vAo) 72 | } 73 | } 74 | } else if (doLights && !doAo) { 75 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 76 | // Face colors are already set to voxel color during model load 77 | for (let v = 0; v < 4; v++) { 78 | const faceVertOffset = faceIndex * 4 + v 79 | faceVertColorR[faceVertOffset] = faceVertColorR[faceVertOffset] * faceVertLightR[faceVertOffset] 80 | faceVertColorG[faceVertOffset] = faceVertColorG[faceVertOffset] * faceVertLightG[faceVertOffset] 81 | faceVertColorB[faceVertOffset] = faceVertColorB[faceVertOffset] * faceVertLightB[faceVertOffset] 82 | } 83 | } 84 | } else if (!doLights && doAo) { 85 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 86 | const material = materials[faceMaterials[faceIndex]] 87 | const vAoShared = material.ao || model.ao 88 | if (!vAoShared) continue 89 | const vAoSharedColor = vAoShared.color 90 | 91 | // Face colors are already set to voxel color during model load 92 | for (let v = 0; v < 4; v++) { 93 | const faceVertOffset = faceIndex * 4 + v 94 | const vR = faceVertColorR[faceVertOffset] 95 | const vG = faceVertColorG[faceVertOffset] 96 | const vB = faceVertColorB[faceVertOffset] 97 | 98 | const vAoColorR = vAoSharedColor ? vAoSharedColor.r : vR 99 | const vAoColorG = vAoSharedColor ? vAoSharedColor.g : vG 100 | const vAoColorB = vAoSharedColor ? vAoSharedColor.b : vB 101 | const vAo = 1 - faceVertAO[faceVertOffset] 102 | 103 | faceVertColorR[faceVertOffset] = vAo * vR + vAoColorR * (1 - vAo) 104 | faceVertColorG[faceVertOffset] = vAo * vG + vAoColorG * (1 - vAo) 105 | faceVertColorB[faceVertOffset] = vAo * vB + vAoColorB * (1 - vAo) 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/smoothvoxels/constants.js: -------------------------------------------------------------------------------- 1 | // Material type constants 2 | export const MATSTANDARD = 'standard' 3 | export const MATBASIC = 'basic' 4 | export const MATLAMBERT = 'lambert' 5 | export const MATPHONG = 'phong' 6 | export const MATMATCAP = 'matcap' 7 | export const MATTOON = 'toon' 8 | export const MATNORMAL = 'normal' 9 | 10 | // Material resize constants 11 | export const BOUNDS = 'bounds' // Resize the bounds to fit the model 12 | export const MODEL = 'model' // Resize the model to fit the bounds 13 | export const SKIP = 'skip' // skip all resizing 14 | 15 | // Material lighting constants 16 | export const FLAT = 'flat' // Flat shaded triangles 17 | export const QUAD = 'quad' // Flat shaded quads 18 | export const SMOOTH = 'smooth' // Smooth shaded triangles 19 | export const BOTH = 'both' // Smooth shaded, but flat shaded clamped / flattened 20 | 21 | // Material side constants 22 | export const FRONT = 'front' // Show only front side of the material 23 | export const BACK = 'back' // Show only back side of the material 24 | export const DOUBLE = 'double' // Show both sides of the material 25 | 26 | export const _FACES = ['nx', 'px', 'ny', 'py', 'nz', 'pz'] 27 | 28 | // Vertex numbering per side. 29 | // The shared vertices for side nx (negative x) and pz (positive z) indicated as example: 30 | // 31 | // -------- 32 | // |1 2| 33 | // | py | 34 | // |0 3| 35 | // ----------------------------- 36 | // |1 [2|1] 2|1 2|1 2| nx shares vertext 2 & 3 37 | // | nx | pz | px | nz | 38 | // |0 [3|0] 3|0 3|0 3| with vertex 1 & 0 of pz 39 | // ----------------------------- 40 | // |1 2| 41 | // | ny | 42 | // |0 3| 43 | // -------- 44 | 45 | // Define the vertex offsets for each side. 46 | 47 | export const _VERTEX_OFFSETS = [ 48 | // nx 49 | [[0, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], 50 | // px 51 | [[1, 0, 1], [1, 1, 1], [1, 1, 0], [1, 0, 0]], 52 | // ny 53 | [[0, 0, 0], [0, 0, 1], [1, 0, 1], [1, 0, 0]], 54 | // py 55 | [[0, 1, 1], [0, 1, 0], [1, 1, 0], [1, 1, 1]], 56 | // nz 57 | [[1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0]], 58 | // pz 59 | [[0, 0, 1], [0, 1, 1], [1, 1, 1], [1, 0, 1]] 60 | ] 61 | 62 | // Define the neighbor voxels for each face 63 | export const _NEIGHBORS = [ 64 | [-1, 0, 0], // nx 65 | [+1, 0, 0], // px 66 | [0, -1, 0], // ny 67 | [0, +1, 0], // py 68 | [0, 0, -1], // nz 69 | [0, 0, +1] // pz 70 | ] 71 | 72 | // Define the uv's for each face 73 | // Textures can be shown on all sides of all voxels (allows scaling and rotating) 74 | // Or a textures with the layout below can be projected on all model sides (no scaling or rotating allowed) 75 | // NOTE: To cover a model, ensure that the model fits the voxel matrix, i.e has no empty voxels next to it 76 | // (export the model to remove unused space). 77 | // 78 | // 0.0 0.25 0.5 0.75 1.0 79 | // 1.0 ----------------------------- 80 | // | |o | | | 81 | // | | py | | ny | 82 | // | | | |o | 83 | // 0.5 ----------------------------- 84 | // | | | | | 85 | // | nx | pz | px | nz | 86 | // |o | | o| | 87 | // 0.0 ----------------------------- 88 | // 89 | export const _FACEINDEXUVS = [ 90 | { u: 'z', v: 'y', order: [0, 1, 2, 3], ud: 1, vd: 1, uo: 0.00, vo: 0.00 }, 91 | { u: 'z', v: 'y', order: [3, 2, 1, 0], ud: -1, vd: 1, uo: 0.75, vo: 0.00 }, 92 | { u: 'x', v: 'z', order: [0, 1, 2, 3], ud: 1, vd: 1, uo: 0.75, vo: 0.50 }, 93 | { u: 'x', v: 'z', order: [1, 0, 3, 2], ud: 1, vd: -1, uo: 0.25, vo: 1.00 }, 94 | { u: 'x', v: 'y', order: [3, 2, 1, 0], ud: -1, vd: 1, uo: 1.00, vo: 0.00 }, 95 | { u: 'x', v: 'y', order: [0, 1, 2, 3], ud: 1, vd: 1, uo: 0.25, vo: 0.00 } 96 | ] 97 | 98 | // Optimization over above 99 | export const _FACEINDEXUV_MULTIPLIERS = [ 100 | [[0, 0, 1], [0, 1, 0]], 101 | [[0, 0, 1], [0, 1, 0]], 102 | [[1, 0, 0], [0, 0, 1]], 103 | [[1, 0, 0], [0, 0, 1]], 104 | [[1, 0, 0], [0, 1, 0]], 105 | [[1, 0, 0], [0, 1, 0]] 106 | ] 107 | -------------------------------------------------------------------------------- /src/smoothvoxels/deformer.js: -------------------------------------------------------------------------------- 1 | import { xyzRangeForSize } from './voxels' 2 | import Noise from './noise' 3 | 4 | export default class Deformer { 5 | static changeShape (model, buffers, shape) { 6 | const { faceEquidistant } = buffers 7 | 8 | switch (shape) { 9 | case 'sphere' : this._circularDeform(model, buffers, 1, 1, 1); break 10 | case 'cylinder-x' : this._circularDeform(model, buffers, 0, 1, 1); break 11 | case 'cylinder-y' : this._circularDeform(model, buffers, 1, 0, 1); break 12 | case 'cylinder-z' : this._circularDeform(model, buffers, 1, 1, 0); break 13 | default: 14 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 15 | faceEquidistant.set(faceIndex, 0) 16 | } 17 | break 18 | } 19 | } 20 | 21 | static _circularDeform (model, buffers, xStrength, yStrength, zStrength) { 22 | const [minX, maxX, minY, maxY, minZ, maxZ] = xyzRangeForSize(model.voxels.size) 23 | 24 | const xMid = (minX + maxX) / 2 + 0.5 25 | const yMid = (minY + maxY) / 2 + 0.5 26 | const zMid = (minZ + maxZ) / 2 + 0.5 27 | 28 | const { vertX, vertY, vertZ, vertRing } = buffers 29 | 30 | for (let vertIndex = 0, c = model.vertCount; vertIndex < c; vertIndex++) { 31 | const vx = vertX[vertIndex] 32 | const vy = vertY[vertIndex] 33 | const vz = vertZ[vertIndex] 34 | 35 | const x = (vx - xMid) 36 | const y = (vy - yMid) 37 | const z = (vz - zMid) 38 | 39 | const sphereSize = Math.max(Math.abs(x * xStrength), Math.abs(y * yStrength), Math.abs(z * zStrength)) 40 | const vertexDistance = Math.sqrt(x * x * xStrength + y * y * yStrength + z * z * zStrength) 41 | if (vertexDistance === 0) continue 42 | const factor = sphereSize / vertexDistance 43 | 44 | vertX[vertIndex] = x * ((1 - xStrength) + (xStrength) * factor) + xMid 45 | vertY[vertIndex] = y * ((1 - yStrength) + (yStrength) * factor) + yMid 46 | vertZ[vertIndex] = z * ((1 - zStrength) + (zStrength) * factor) + zMid 47 | vertRing[vertIndex] = sphereSize 48 | } 49 | 50 | this._markEquidistantFaces(model, buffers) 51 | } 52 | 53 | static _markEquidistantFaces (model, buffers) { 54 | const { faceVertIndices, vertRing, faceEquidistant } = buffers 55 | 56 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 57 | const faceVertIndex0 = faceIndex * 4 58 | const faceVertIndex1 = faceVertIndex0 + 1 59 | const faceVertIndex2 = faceVertIndex0 + 2 60 | const faceVertIndex3 = faceVertIndex0 + 3 61 | 62 | faceEquidistant.set(faceIndex, vertRing[faceVertIndices[faceVertIndex0]] === vertRing[faceVertIndices[faceVertIndex1]] && 63 | vertRing[faceVertIndices[faceVertIndex0]] === vertRing[faceVertIndices[faceVertIndex2]] && 64 | vertRing[faceVertIndices[faceVertIndex0]] === vertRing[faceVertIndices[faceVertIndex3]] 65 | ? 1 66 | : 0) 67 | } 68 | } 69 | 70 | static maximumDeformCount (model) { 71 | let maximumCount = 0 72 | model.materials.forEach(function (material) { 73 | if (material.deform) { maximumCount = Math.max(maximumCount, material.deform.count) } 74 | }) 75 | return maximumCount 76 | } 77 | 78 | static deform (model, buffers, maximumDeformCount) { 79 | const { vertLinkIndices, vertLinkCounts, vertDeformCount, vertDeformDamping, vertDeformStrength, vertFlattenedX, vertFlattenedY, vertFlattenedZ, vertClampedX, vertClampedY, vertClampedZ, vertX, vertY, vertZ, vertTmpX, vertTmpY, vertTmpZ, vertHasTmp } = buffers 80 | 81 | for (let step = 0; step < maximumDeformCount; step++) { 82 | let hasDeforms = false 83 | 84 | for (let vertIndex = 0, c = model.vertCount; vertIndex < c; vertIndex++) { 85 | const deformCount = vertDeformCount[vertIndex] 86 | if (deformCount <= step) continue 87 | 88 | const vertLinkCount = vertLinkCounts[vertIndex] 89 | if (vertLinkCount === 0) continue 90 | 91 | hasDeforms = true 92 | 93 | const vx = vertX[vertIndex] 94 | const vy = vertY[vertIndex] 95 | const vz = vertZ[vertIndex] 96 | 97 | const deformDamping = vertDeformDamping[vertIndex] 98 | const deformStrength = vertDeformStrength[vertIndex] 99 | const notClampOrFlattenX = 1 - (vertClampedX.get(vertIndex) | vertFlattenedX.get(vertIndex)) 100 | const notClampOrFlattenY = 1 - (vertClampedY.get(vertIndex) | vertFlattenedY.get(vertIndex)) 101 | const notClampOrFlattenZ = 1 - (vertClampedZ.get(vertIndex) | vertFlattenedZ.get(vertIndex)) 102 | 103 | let x = 0; let y = 0; let z = 0 104 | 105 | for (let i = 0; i < vertLinkCount; i++) { 106 | const linkIndex = vertLinkIndices[vertIndex * 6 + i] 107 | x += vertX[linkIndex] 108 | y += vertY[linkIndex] 109 | z += vertZ[linkIndex] 110 | } 111 | 112 | const strength = Math.pow(deformDamping, step) * deformStrength 113 | 114 | const offsetX = x / vertLinkCount - vx 115 | const offsetY = y / vertLinkCount - vy 116 | const offsetZ = z / vertLinkCount - vz 117 | 118 | vertTmpX[vertIndex] = vx + notClampOrFlattenX * offsetX * strength 119 | vertTmpY[vertIndex] = vy + notClampOrFlattenY * offsetY * strength 120 | vertTmpZ[vertIndex] = vz + notClampOrFlattenZ * offsetZ * strength 121 | vertHasTmp.set(vertIndex, notClampOrFlattenX | notClampOrFlattenY | notClampOrFlattenZ) 122 | } 123 | 124 | if (hasDeforms) { 125 | for (let vertIndex = 0, c = model.vertCount; vertIndex < c; vertIndex++) { 126 | if (vertHasTmp.get(vertIndex) === 0) continue 127 | 128 | vertX[vertIndex] = vertTmpX[vertIndex] 129 | vertY[vertIndex] = vertTmpY[vertIndex] 130 | vertZ[vertIndex] = vertTmpZ[vertIndex] 131 | } 132 | 133 | vertHasTmp.clear() 134 | } 135 | } 136 | } 137 | 138 | static warpAndScatter (model, buffers) { 139 | const noise = Noise().noise 140 | const { nx: tnx, px: tpx, ny: tny, py: tpy, nz: tnz, pz: tpz } = model._tile 141 | let [vxMinX, vxMaxX, vxMinY, vxMaxY, vxMinZ, vxMaxZ] = xyzRangeForSize(model.voxels.size) 142 | 143 | const { vertX, vertY, vertZ, vertWarpAmplitude, vertWarpFrequency, vertScatter, vertFlattenedX, vertFlattenedY, vertFlattenedZ, vertClampedX, vertClampedY, vertClampedZ } = buffers 144 | 145 | vxMinX += 0.1 146 | vxMinY += 0.1 147 | vxMinZ += 0.1 148 | vxMaxX += 0.9 149 | vxMaxY += 0.9 150 | vxMaxZ += 0.9 151 | 152 | for (let vertIndex = 0, c = model.vertCount; vertIndex < c; vertIndex++) { 153 | const vx = vertX[vertIndex] 154 | const vy = vertY[vertIndex] 155 | const vz = vertZ[vertIndex] 156 | 157 | // In case of tiling, do not warp or scatter the edges 158 | if ((tnx && vx < vxMinX) || 159 | (tpx && vx > vxMaxX) || 160 | (tny && vy < vxMinY) || 161 | (tpy && vy > vxMaxY) || 162 | (tnz && vz < vxMinZ) || 163 | (tpz && vz > vxMaxZ)) { continue } 164 | 165 | const amplitude = vertWarpAmplitude[vertIndex] 166 | const frequency = vertWarpFrequency[vertIndex] 167 | const scatter = vertScatter[vertIndex] 168 | const hasAmplitude = amplitude > 0 169 | const hasScatter = scatter > 0 170 | 171 | if (hasAmplitude || hasScatter) { 172 | let xOffset = 0; let yOffset = 0; let zOffset = 0 173 | 174 | if (hasAmplitude) { 175 | xOffset = noise((vx + 0.19) * frequency, vy * frequency, vz * frequency) * amplitude 176 | yOffset = noise((vy + 0.17) * frequency, vz * frequency, vx * frequency) * amplitude 177 | zOffset = noise((vz + 0.13) * frequency, vx * frequency, vy * frequency) * amplitude 178 | } 179 | 180 | if (hasScatter) { 181 | xOffset += (Math.random() * 2 - 1) * scatter 182 | yOffset += (Math.random() * 2 - 1) * scatter 183 | zOffset += (Math.random() * 2 - 1) * scatter 184 | } 185 | 186 | const notClampOrFlattenX = 1 - (vertClampedX.get(vertIndex) | vertFlattenedX.get(vertIndex)) 187 | const notClampOrFlattenY = 1 - (vertClampedY.get(vertIndex) | vertFlattenedY.get(vertIndex)) 188 | const notClampOrFlattenZ = 1 - (vertClampedZ.get(vertIndex) | vertFlattenedZ.get(vertIndex)) 189 | 190 | vertX[vertIndex] = vx + notClampOrFlattenX * xOffset 191 | vertY[vertIndex] = vy + notClampOrFlattenY * yOffset 192 | vertZ[vertIndex] = vz + notClampOrFlattenZ * zOffset 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/smoothvoxels/facealigner.js: -------------------------------------------------------------------------------- 1 | export default class FaceAligner { 2 | // Align all 'quad' diagonals to the center, making most models look more symmetrical 3 | static alignFaceDiagonals (model, buffers) { 4 | // TODO skip culled faces 5 | let maxDist = 0.1 * Math.min(model.scale.x, model.scale.y, model.scale.z) 6 | maxDist *= maxDist // No need to use sqrt for the distances 7 | 8 | const { faceCulled, faceVertIndices, vertX, vertY, vertZ, faceVertFlatNormalX, faceVertFlatNormalY, faceVertFlatNormalZ, faceVertSmoothNormalX, faceVertSmoothNormalY, faceVertSmoothNormalZ, faceVertBothNormalX, faceVertBothNormalY, faceVertBothNormalZ, faceVertUs, faceVertVs, faceVertColorR, faceVertColorG, faceVertColorB, faceVertNormalX, faceVertNormalY, faceVertNormalZ } = buffers 9 | 10 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 11 | if (faceCulled.get(faceIndex) === 1) continue 12 | 13 | const faceVertOffset = faceIndex * 4 14 | const vert0Index = faceVertIndices[faceVertOffset] 15 | const vert1Index = faceVertIndices[faceVertOffset + 1] 16 | const vert2Index = faceVertIndices[faceVertOffset + 2] 17 | const vert3Index = faceVertIndices[faceVertOffset + 3] 18 | 19 | let vert0X = vertX[vert0Index] 20 | let vert0Y = vertY[vert0Index] 21 | let vert0Z = vertZ[vert0Index] 22 | let vert1X = vertX[vert1Index] 23 | let vert1Y = vertY[vert1Index] 24 | let vert1Z = vertZ[vert1Index] 25 | let vert2X = vertX[vert2Index] 26 | let vert2Y = vertY[vert2Index] 27 | let vert2Z = vertZ[vert2Index] 28 | let vert3X = vertX[vert3Index] 29 | let vert3Y = vertY[vert3Index] 30 | let vert3Z = vertZ[vert3Index] 31 | 32 | // Determine the diagonal for v0 - v2 mid point and the distances from v1 and v3 to that mid point 33 | const mid02X = (vert0X + vert2X) / 2 34 | const mid02Y = (vert0Y + vert2Y) / 2 35 | const mid02Z = (vert0Z + vert2Z) / 2 36 | const distance1toMid = (vert1X - mid02X) * (vert1X - mid02X) + 37 | (vert1Y - mid02Y) * (vert1Y - mid02Y) + 38 | (vert1Z - mid02Z) * (vert1Z - mid02Z) 39 | const distance3toMid = (vert3X - mid02X) * (vert3X - mid02X) + 40 | (vert3Y - mid02Y) * (vert3Y - mid02Y) + 41 | (vert3Z - mid02Z) * (vert3Z - mid02Z) 42 | 43 | const mid13X = (vert1X + vert3X) / 2 44 | const mid13Y = (vert1Y + vert3Y) / 2 45 | const mid13Z = (vert1Z + vert3Z) / 2 46 | const distance0toMid = (vert0X - mid13X) * (vert0X - mid13X) + 47 | (vert0Y - mid13Y) * (vert0Y - mid13Y) + 48 | (vert0Z - mid13Z) * (vert0Z - mid13Z) 49 | const distance2toMid = (vert2X - mid13X) * (vert2X - mid13X) + 50 | (vert2Y - mid13Y) * (vert2Y - mid13Y) + 51 | (vert2Z - mid13Z) * (vert2Z - mid13Z) 52 | 53 | // NOTE: The check below is not an actual check for concave quads but 54 | // checks whether one of the vertices is close to the midpoint of te other diagonal. 55 | // This can happen in certain cases when deforming, when the vertex itself is not moved, 56 | // but two vertices it is dependant on are moved in the 'wrong' direction, resulting 57 | // in a concave quad. Since deforming should not make the quad very badly concave 58 | // this seems enough to prevent ugly artefacts in these edge cases. 59 | 60 | if (distance1toMid < maxDist || distance3toMid < maxDist) { 61 | // If v1 or v3 is close to the mid point we may have a rare concave quad. 62 | // Switch the default triangles so this does not show up 63 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertIndices) 64 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertNormalX) 65 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertNormalY) 66 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertNormalZ) 67 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertFlatNormalX) 68 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertFlatNormalY) 69 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertFlatNormalZ) 70 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertSmoothNormalX) 71 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertSmoothNormalY) 72 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertSmoothNormalZ) 73 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertBothNormalX) 74 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertBothNormalY) 75 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertBothNormalZ) 76 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertUs) 77 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertVs) 78 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertColorR) 79 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertColorG) 80 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertColorB) 81 | // face.ao.push(face.ao.shift()); 82 | } else if (distance0toMid < maxDist || distance2toMid < maxDist) { } // eslint-disable-line 83 | // If v0 or v2 is close to the mid point we may have a rare concave quad. 84 | // Keep the default triangles so this does not show up. 85 | // 86 | /* else if (face.ao && 87 | Math.min(face.ao[0], face.ao[1], face.ao[2], face.ao[3]) !== 88 | Math.max(face.ao[0], face.ao[1], face.ao[2], face.ao[3])) { 89 | // This is a 'standard' quad but with an ao gradient 90 | // Rotate the vertices so they connect the highest contrast 91 | let ao02 = Math.abs(face.ao[0] - face.ao[2]); 92 | let ao13 = Math.abs(face.ao[1] - face.ao[3]); 93 | if (ao02 < ao13) { 94 | face.vertices.push(face.vertices.shift()); 95 | //face.normals.push(face.normals.shift()); 96 | face.flatNormals.push(face.flatNormals.shift()); 97 | face.smoothNormals.push(face.smoothNormals.shift()); 98 | face.bothNormals.push(face.bothNormals.shift()); 99 | face.ao.push(face.ao.shift()); 100 | face.uv.push(face.uv.shift()); 101 | if (face.vertexColors) 102 | face.vertexColors.push(face.vertexColors.shift()); 103 | } 104 | } */ 105 | else { 106 | // This is a 'standard' quad. 107 | // Rotate the vertices so they align to the center 108 | // For symetric models this improves the end result 109 | let min = this._getVertexSumInline(vert0X, vert0Y, vert0Z) 110 | 111 | while (this._getVertexSumInline(vert1X, vert1Y, vert1Z) < min || 112 | this._getVertexSumInline(vert2X, vert2Y, vert2Z) < min || 113 | this._getVertexSumInline(vert3X, vert3Y, vert3Z) < min) { 114 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertIndices) 115 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertNormalX) 116 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertNormalY) 117 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertNormalZ) 118 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertFlatNormalX) 119 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertFlatNormalY) 120 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertFlatNormalZ) 121 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertSmoothNormalX) 122 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertSmoothNormalY) 123 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertSmoothNormalZ) 124 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertBothNormalX) 125 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertBothNormalY) 126 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertBothNormalZ) 127 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertUs) 128 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertVs) 129 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertColorR) 130 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertColorG) 131 | this._shiftFaceVertsAtOffset(faceVertOffset, faceVertColorB) 132 | 133 | const tx = vert0X 134 | const ty = vert0Y 135 | const tz = vert0Z 136 | vert0X = vert1X 137 | vert0Y = vert1Y 138 | vert0Z = vert1Z 139 | vert1X = vert2X 140 | vert1Y = vert2Y 141 | vert1Z = vert2Z 142 | vert2X = vert3X 143 | vert2Y = vert3Y 144 | vert2Z = vert3Z 145 | vert3X = tx 146 | vert3Y = ty 147 | vert3Z = tz 148 | 149 | min = this._getVertexSumInline(vert0X, vert0Y, vert0Z) 150 | } 151 | } 152 | } 153 | } 154 | 155 | static _getVertexSumInline (vx, vy, vz) { 156 | return Math.abs(vx) + Math.abs(vy) + Math.abs(vz) 157 | } 158 | 159 | static _shiftFaceVertsAtOffset (offset, arr) { 160 | const t = arr[offset] 161 | arr[offset] = arr[offset + 1] 162 | arr[offset + 1] = arr[offset + 2] 163 | arr[offset + 2] = arr[offset + 3] 164 | arr[offset + 3] = t 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/smoothvoxels/light.js: -------------------------------------------------------------------------------- 1 | export default class Light { 2 | constructor (color, strength, direction, position, distance, size, detail) { 3 | this.color = color 4 | this.strength = strength 5 | this.direction = direction 6 | this.position = position 7 | this.distance = distance 8 | this.size = size 9 | this.detail = detail 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/smoothvoxels/lightscalculator.js: -------------------------------------------------------------------------------- 1 | export default class LightsCalculator { 2 | static calculateLights (model, buffers) { 3 | const lights = model.lights 4 | if (lights.length === 0) { return } 5 | 6 | for (const light of lights) { 7 | if (light.direction && !light.normalizedDirection) { 8 | const length = Math.sqrt(light.direction.x * light.direction.x + light.direction.y * light.direction.y + light.direction.z * light.direction.z) 9 | light.normalizedDirection = { x: light.direction.x, y: light.direction.y, z: light.direction.z } 10 | 11 | if (length > 0) { 12 | const d = 1.0 / length 13 | light.normalizedDirection.x *= d 14 | } 15 | } 16 | } 17 | 18 | const materials = model.materials.materials 19 | 20 | const { faceMaterials, faceVertNormalX, faceVertNormalY, faceVertNormalZ, faceVertIndices, vertX, vertY, vertZ, faceVertLightR, faceVertLightG, faceVertLightB } = buffers 21 | 22 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 23 | const material = materials[faceMaterials[faceIndex]] 24 | const faceOffset = faceIndex * 4 25 | 26 | if (!material.lights) { 27 | for (let v = 0; v < 4; v++) { 28 | const faceVertOffset = faceOffset + v 29 | 30 | faceVertLightR[faceVertOffset] = 1 31 | faceVertLightG[faceVertOffset] = 1 32 | faceVertLightB[faceVertOffset] = 1 33 | } 34 | } else { 35 | for (let v = 0; v < 4; v++) { 36 | const faceVertOffset = faceOffset + v 37 | 38 | const vertIndex = faceVertIndices[faceVertOffset] 39 | const vx = vertX[vertIndex] 40 | const vy = vertY[vertIndex] 41 | const vz = vertZ[vertIndex] 42 | 43 | const nx = faceVertNormalX[faceVertOffset] 44 | const ny = faceVertNormalY[faceVertOffset] 45 | const nz = faceVertNormalZ[faceVertOffset] 46 | 47 | faceVertLightR[faceVertOffset] = 0 48 | faceVertLightG[faceVertOffset] = 0 49 | faceVertLightB[faceVertOffset] = 0 50 | 51 | for (const light of lights) { 52 | const { color, strength, distance, normalizedDirection, position } = light 53 | 54 | let exposure = strength 55 | 56 | let length = 0 57 | 58 | if (position) { 59 | const lvx = position.x - vx 60 | const lvy = position.y - vy 61 | const lvz = position.z - vz 62 | 63 | length = Math.sqrt(lvx * lvx + lvy * lvy + lvz * lvz) 64 | const d = 1.0 / length 65 | 66 | exposure = strength * Math.max(nx * lvx * d + ny * lvy * d + nz * lvz * d, 0.0) 67 | } else if (normalizedDirection) { 68 | exposure = strength * 69 | Math.max(nx * normalizedDirection.x + 70 | ny * normalizedDirection.y + 71 | nz * normalizedDirection.z, 0.0) 72 | } 73 | 74 | if (position && distance) { 75 | exposure = exposure * (1 - Math.min(length / distance, 1)) 76 | } 77 | 78 | faceVertLightR[faceVertOffset] += color.r * exposure 79 | faceVertLightG[faceVertOffset] += color.g * exposure 80 | faceVertLightB[faceVertOffset] += color.b * exposure 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/smoothvoxels/material.js: -------------------------------------------------------------------------------- 1 | import Planar from './planar' 2 | import BoundingBox from './boundingbox' 3 | 4 | export default class Material { 5 | constructor (baseMaterial, lighting, fade, simplify, side) { 6 | this._baseMaterial = baseMaterial 7 | 8 | // lighting, smooth, flat or both 9 | this.lighting = lighting 10 | this.fade = !!fade 11 | this.simplify = simplify !== false 12 | 13 | // Preset the shape modifiers 14 | this._deform = undefined 15 | this._warp = undefined 16 | this._scatter = undefined 17 | 18 | this._flatten = Planar.parse('') 19 | this._clamp = Planar.parse('') 20 | this._skip = Planar.parse('') 21 | 22 | this._ao = undefined 23 | this.lights = true 24 | 25 | this._side = side 26 | 27 | this._colors = [] 28 | 29 | this.bounds = new BoundingBox() 30 | } 31 | 32 | get baseId () { 33 | return this._baseMaterial.baseId 34 | } 35 | 36 | get index () { 37 | return this._baseMaterial.index 38 | } 39 | 40 | get colors () { 41 | return this._colors 42 | } 43 | 44 | get colorCount () { 45 | return this._baseMaterial.colorCount 46 | } 47 | 48 | get type () { 49 | return this._baseMaterial.type 50 | } 51 | 52 | get roughness () { 53 | return this._baseMaterial.roughness 54 | } 55 | 56 | get metalness () { 57 | return this._baseMaterial.metalness 58 | } 59 | 60 | get opacity () { 61 | return this._baseMaterial.opacity 62 | } 63 | 64 | get alphaTest () { 65 | return this._baseMaterial.alphaTest 66 | } 67 | 68 | get transparent () { 69 | return this._baseMaterial.transparent 70 | } 71 | 72 | get isTransparent () { 73 | return this._baseMaterial.isTransparent 74 | } 75 | 76 | get refractionRatio () { 77 | return this._baseMaterial.refractionRatio 78 | } 79 | 80 | get emissive () { 81 | return this._baseMaterial.emissive 82 | } 83 | 84 | get side () { 85 | return this._side 86 | } 87 | 88 | get fog () { 89 | // Emissive materials shine through fog (in case fog used as darkness) 90 | return this._baseMaterial.fog 91 | } 92 | 93 | get map () { 94 | return this._baseMaterial.map 95 | } 96 | 97 | get normalMap () { 98 | return this._baseMaterial.normalMap 99 | } 100 | 101 | get roughnessMap () { 102 | return this._baseMaterial.roughnessMap 103 | } 104 | 105 | get metalnessMap () { 106 | return this._baseMaterial.metalnessMap 107 | } 108 | 109 | get emissiveMap () { 110 | return this._baseMaterial.emissiveMap 111 | } 112 | 113 | get matcap () { 114 | return this._baseMaterial.matcap 115 | } 116 | 117 | get reflectionMap () { 118 | return this._baseMaterial.reflectionMap 119 | } 120 | 121 | get refractionMap () { 122 | return this._baseMaterial.refractionMap 123 | } 124 | 125 | get mapTransform () { 126 | return this._baseMaterial.mapTransform 127 | } 128 | 129 | setDeform (count, strength, damping) { 130 | count = Math.max((count === null || count === undefined) ? 1 : count, 0) 131 | strength = (strength === null || strength === undefined) ? 1.0 : strength 132 | damping = (damping === null || damping === undefined) ? 1.0 : damping 133 | if (count > 0 && strength !== 0.0) { this._deform = { count, strength, damping } } else { this._deform = { count: 0, strength: 0, damping: 0 } } 134 | } 135 | 136 | get deform () { 137 | return this._deform 138 | } 139 | 140 | setWarp (amplitude, frequency) { 141 | amplitude = amplitude === undefined ? 1.0 : Math.abs(amplitude) 142 | frequency = frequency === undefined ? 1.0 : Math.abs(frequency) 143 | if (amplitude > 0.001 && frequency > 0.001) { this._warp = { amplitude, frequency } } else { this._warp = undefined } 144 | } 145 | 146 | get warp () { 147 | return this._warp 148 | } 149 | 150 | set scatter (value) { 151 | if (value === 0.0) { value = undefined } 152 | this._scatter = Math.abs(value) 153 | } 154 | 155 | get scatter () { 156 | return this._scatter 157 | } 158 | 159 | // Getters and setters for planar handling 160 | set flatten (flatten) { this._flatten = Planar.parse(flatten) } 161 | get flatten () { return Planar.toString(this._flatten) } 162 | set clamp (clamp) { this._clamp = Planar.parse(clamp) } 163 | get clamp () { return Planar.toString(this._clamp) } 164 | set skip (skip) { this._skip = Planar.parse(skip) } 165 | get skip () { return Planar.toString(this._skip) } 166 | 167 | // Set AO as { color, maxDistance, strength, angle } 168 | setAo (ao) { 169 | this._ao = ao 170 | } 171 | 172 | get ao () { 173 | return this._ao 174 | } 175 | 176 | set aoSides (sides) { this._aoSides = Planar.parse(sides) } 177 | get aoSides () { return Planar.toString(this._aoSides) } 178 | } 179 | -------------------------------------------------------------------------------- /src/smoothvoxels/materiallist.js: -------------------------------------------------------------------------------- 1 | import { FRONT, BACK, DOUBLE } from './constants' 2 | import BaseMaterial from './basematerial' 3 | import Material from './material' 4 | 5 | export default class MaterialList { 6 | constructor () { 7 | this.baseMaterials = [] 8 | this.materials = [] 9 | } 10 | 11 | createMaterial (type, lighting, roughness, metalness, 12 | fade, simplify, opacity, alphaTest, transparent, refractionRatio, wireframe, side, 13 | emissiveColor, emissiveIntensity, fog, 14 | map, normalMap, roughnessMap, metalnessMap, emissiveMap, matcap, 15 | reflectionmap, refractionmap, 16 | uscale, vscale, uoffset, voffset, rotation) { 17 | // Since the mesh generator reverses the faces a front and back side material are the same base material 18 | side = side || FRONT 19 | if (![FRONT, BACK, DOUBLE].includes(side)) { side = FRONT } 20 | const baseSide = (side === DOUBLE) ? DOUBLE : FRONT 21 | 22 | let baseMaterial = new BaseMaterial(type, roughness, metalness, 23 | opacity, alphaTest, transparent, refractionRatio, wireframe, baseSide, 24 | emissiveColor, emissiveIntensity, fog, 25 | map, normalMap, roughnessMap, metalnessMap, emissiveMap, matcap, 26 | reflectionmap, refractionmap, 27 | uscale, vscale, uoffset, voffset, rotation) 28 | const baseId = baseMaterial.baseId 29 | const existingBase = this.baseMaterials.find(m => m.baseId === baseId) 30 | 31 | if (existingBase) { 32 | baseMaterial = existingBase 33 | } else { 34 | this.baseMaterials.push(baseMaterial) 35 | } 36 | 37 | const material = new Material(baseMaterial, lighting, fade, simplify, side) 38 | this.materials.push(material) 39 | 40 | return material 41 | } 42 | 43 | clearMaterials () { 44 | this.materials.length = 0 45 | } 46 | 47 | forEach (func, thisArg, baseOnly) { 48 | if (baseOnly) { 49 | this.baseMaterials.foreach(func, thisArg) 50 | } else { 51 | this.materials.forEach(func, thisArg) 52 | } 53 | } 54 | 55 | find (func) { 56 | return this.materials.find(func) 57 | } 58 | 59 | getMaterialListIndex (material) { 60 | return this.materials.indexOf(material) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/smoothvoxels/matrix.js: -------------------------------------------------------------------------------- 1 | // Single Matrix class adapted from https://github.com/evanw/lightgl.js 2 | // Simplified to only the parts needed 3 | 4 | // Represents a 4x4 matrix stored in row-major order that uses Float32Arrays 5 | // when available. Matrix operations can either be done using convenient 6 | // methods that return a new matrix for the result or optimized methods 7 | // that store the result in an existing matrix to avoid generating garbage. 8 | 9 | // ### new Matrix() 10 | // 11 | // This constructor creates an identity matrix. 12 | export default class Matrix { 13 | constructor () { 14 | const m = [ 15 | 1, 0, 0, 0, 16 | 0, 1, 0, 0, 17 | 0, 0, 1, 0, 18 | 0, 0, 0, 1 19 | ] 20 | this.m = new Float32Array(m) 21 | } 22 | 23 | // ### .transformPoint(point) 24 | // 25 | // Transforms the vector as a point with a w coordinate of 1. This 26 | // means translations will have an effect, for example. 27 | transformPoint (v) { 28 | const m = this.m 29 | const div = m[12] * v.x + m[13] * v.y + m[14] * v.z + m[15] 30 | const x = (m[0] * v.x + m[1] * v.y + m[2] * v.z + m[3]) / div 31 | const y = (m[4] * v.x + m[5] * v.y + m[6] * v.z + m[7]) / div 32 | const z = (m[8] * v.x + m[9] * v.y + m[10] * v.z + m[11]) / div 33 | v.x = x 34 | v.y = y 35 | v.z = z 36 | } 37 | 38 | transformPointInline (xs, ys, zs, index) { 39 | const vx = xs[index] 40 | const vy = ys[index] 41 | const vz = zs[index] 42 | 43 | const m = this.m 44 | const div = m[12] * vx + m[13] * vy + m[14] * vz + m[15] 45 | const x = (m[0] * vx + m[1] * vy + m[2] * vz + m[3]) / div 46 | const y = (m[4] * vx + m[5] * vy + m[6] * vz + m[7]) / div 47 | const z = (m[8] * vx + m[9] * vy + m[10] * vz + m[11]) / div 48 | xs[index] = x 49 | ys[index] = y 50 | zs[index] = z 51 | } 52 | 53 | // ### .transformVector(vector) 54 | // 55 | // Transforms the vector as a vector with a w coordinate of 0. This 56 | // means translations will have no effect, for example. 57 | transformVector (v) { 58 | const m = this.m 59 | const x = (m[0] * v.x + m[1] * v.y + m[2] * v.z) 60 | const y = (m[4] * v.x + m[5] * v.y + m[6] * v.z) 61 | const z = (m[8] * v.x + m[9] * v.y + m[10] * v.z) 62 | v.x = x 63 | v.y = y 64 | v.z = z 65 | } 66 | 67 | transformVectorInline (xs, ys, zs, index) { 68 | const vx = xs[index] 69 | const vy = ys[index] 70 | const vz = zs[index] 71 | 72 | const m = this.m 73 | const x = (m[0] * vx + m[1] * vy + m[2] * vz) 74 | const y = (m[4] * vx + m[5] * vy + m[6] * vz) 75 | const z = (m[8] * vx + m[9] * vy + m[10] * vz) 76 | xs[index] = x 77 | ys[index] = y 78 | zs[index] = z 79 | } 80 | 81 | // ### Matrix.identity([result]) 82 | // 83 | // Returns an identity matrix. You can optionally pass an existing matrix in 84 | // `result` to avoid allocating a new matrix. 85 | static identity (result) { 86 | result = result || new Matrix() 87 | const m = result.m 88 | m[0] = m[5] = m[10] = m[15] = 1 89 | m[1] = m[2] = m[3] = m[4] = m[6] = m[7] = m[8] = m[9] = m[11] = m[12] = m[13] = m[14] = 0 90 | return result 91 | } 92 | 93 | // ### Matrix.multiply(left, right[, result]) 94 | // 95 | // Returns the concatenation of the transforms for `left` and `right`. You can 96 | // optionally pass an existing matrix in `result` to avoid allocating a new 97 | // matrix. 98 | static multiply (left, right, result) { 99 | result = result || new Matrix() 100 | const a = left.m; const b = right.m; const r = result.m 101 | 102 | r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12] 103 | r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13] 104 | r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14] 105 | r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15] 106 | 107 | r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12] 108 | r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13] 109 | r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14] 110 | r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15] 111 | 112 | r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12] 113 | r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13] 114 | r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14] 115 | r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15] 116 | 117 | r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12] 118 | r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13] 119 | r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14] 120 | r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15] 121 | 122 | return result 123 | } 124 | 125 | // ### Matrix.transpose(matrix[, result]) 126 | // 127 | // Returns `matrix`, exchanging columns for rows. You can optionally pass an 128 | // existing matrix in `result` to avoid allocating a new matrix. 129 | static transpose (matrix, result) { 130 | result = result || new Matrix() 131 | const m = matrix.m; const r = result.m 132 | r[0] = m[0]; r[1] = m[4]; r[2] = m[8]; r[3] = m[12] 133 | r[4] = m[1]; r[5] = m[5]; r[6] = m[9]; r[7] = m[13] 134 | r[8] = m[2]; r[9] = m[6]; r[10] = m[10]; r[11] = m[14] 135 | r[12] = m[3]; r[13] = m[7]; r[14] = m[11]; r[15] = m[15] 136 | return result 137 | } 138 | 139 | // ### Matrix.inverse(matrix[, result]) 140 | // 141 | // Returns the matrix that when multiplied with `matrix` results in the 142 | // identity matrix. You can optionally pass an existing matrix in `result` 143 | // to avoid allocating a new matrix. This implementation is from the Mesa 144 | // OpenGL function `__gluInvertMatrixd()` found in `project.c`. 145 | static inverse (matrix, result) { 146 | result = result || new Matrix() 147 | const m = matrix.m; const r = result.m 148 | 149 | r[0] = m[5] * m[10] * m[15] - m[5] * m[14] * m[11] - m[6] * m[9] * m[15] + m[6] * m[13] * m[11] + m[7] * m[9] * m[14] - m[7] * m[13] * m[10] 150 | r[1] = -m[1] * m[10] * m[15] + m[1] * m[14] * m[11] + m[2] * m[9] * m[15] - m[2] * m[13] * m[11] - m[3] * m[9] * m[14] + m[3] * m[13] * m[10] 151 | r[2] = m[1] * m[6] * m[15] - m[1] * m[14] * m[7] - m[2] * m[5] * m[15] + m[2] * m[13] * m[7] + m[3] * m[5] * m[14] - m[3] * m[13] * m[6] 152 | r[3] = -m[1] * m[6] * m[11] + m[1] * m[10] * m[7] + m[2] * m[5] * m[11] - m[2] * m[9] * m[7] - m[3] * m[5] * m[10] + m[3] * m[9] * m[6] 153 | 154 | r[4] = -m[4] * m[10] * m[15] + m[4] * m[14] * m[11] + m[6] * m[8] * m[15] - m[6] * m[12] * m[11] - m[7] * m[8] * m[14] + m[7] * m[12] * m[10] 155 | r[5] = m[0] * m[10] * m[15] - m[0] * m[14] * m[11] - m[2] * m[8] * m[15] + m[2] * m[12] * m[11] + m[3] * m[8] * m[14] - m[3] * m[12] * m[10] 156 | r[6] = -m[0] * m[6] * m[15] + m[0] * m[14] * m[7] + m[2] * m[4] * m[15] - m[2] * m[12] * m[7] - m[3] * m[4] * m[14] + m[3] * m[12] * m[6] 157 | r[7] = m[0] * m[6] * m[11] - m[0] * m[10] * m[7] - m[2] * m[4] * m[11] + m[2] * m[8] * m[7] + m[3] * m[4] * m[10] - m[3] * m[8] * m[6] 158 | 159 | r[8] = m[4] * m[9] * m[15] - m[4] * m[13] * m[11] - m[5] * m[8] * m[15] + m[5] * m[12] * m[11] + m[7] * m[8] * m[13] - m[7] * m[12] * m[9] 160 | r[9] = -m[0] * m[9] * m[15] + m[0] * m[13] * m[11] + m[1] * m[8] * m[15] - m[1] * m[12] * m[11] - m[3] * m[8] * m[13] + m[3] * m[12] * m[9] 161 | r[10] = m[0] * m[5] * m[15] - m[0] * m[13] * m[7] - m[1] * m[4] * m[15] + m[1] * m[12] * m[7] + m[3] * m[4] * m[13] - m[3] * m[12] * m[5] 162 | r[11] = -m[0] * m[5] * m[11] + m[0] * m[9] * m[7] + m[1] * m[4] * m[11] - m[1] * m[8] * m[7] - m[3] * m[4] * m[9] + m[3] * m[8] * m[5] 163 | 164 | r[12] = -m[4] * m[9] * m[14] + m[4] * m[13] * m[10] + m[5] * m[8] * m[14] - m[5] * m[12] * m[10] - m[6] * m[8] * m[13] + m[6] * m[12] * m[9] 165 | r[13] = m[0] * m[9] * m[14] - m[0] * m[13] * m[10] - m[1] * m[8] * m[14] + m[1] * m[12] * m[10] + m[2] * m[8] * m[13] - m[2] * m[12] * m[9] 166 | r[14] = -m[0] * m[5] * m[14] + m[0] * m[13] * m[6] + m[1] * m[4] * m[14] - m[1] * m[12] * m[6] - m[2] * m[4] * m[13] + m[2] * m[12] * m[5] 167 | r[15] = m[0] * m[5] * m[10] - m[0] * m[9] * m[6] - m[1] * m[4] * m[10] + m[1] * m[8] * m[6] + m[2] * m[4] * m[9] - m[2] * m[8] * m[5] 168 | 169 | const det = m[0] * r[0] + m[1] * r[4] + m[2] * r[8] + m[3] * r[12] 170 | for (let i = 0; i < 16; i++) r[i] /= det 171 | return result 172 | } 173 | 174 | // ### Matrix.scale(x, y, z[, result]) 175 | // 176 | // Create a scaling matrix. You can optionally pass an 177 | // existing matrix in `result` to avoid allocating a new matrix. 178 | static scale (x, y, z, result) { 179 | result = result || new Matrix() 180 | const m = result.m 181 | 182 | m[0] = x 183 | m[1] = 0 184 | m[2] = 0 185 | m[3] = 0 186 | 187 | m[4] = 0 188 | m[5] = y 189 | m[6] = 0 190 | m[7] = 0 191 | 192 | m[8] = 0 193 | m[9] = 0 194 | m[10] = z 195 | m[11] = 0 196 | 197 | m[12] = 0 198 | m[13] = 0 199 | m[14] = 0 200 | m[15] = 1 201 | 202 | return result 203 | } 204 | 205 | // ### Matrix.translate(x, y, z[, result]) 206 | // 207 | // Create a translation matrix. You can optionally pass 208 | // an existing matrix in `result` to avoid allocating a new matrix. 209 | static translate (x, y, z, result) { 210 | result = result || new Matrix() 211 | const m = result.m 212 | 213 | m[0] = 1 214 | m[1] = 0 215 | m[2] = 0 216 | m[3] = x 217 | 218 | m[4] = 0 219 | m[5] = 1 220 | m[6] = 0 221 | m[7] = y 222 | 223 | m[8] = 0 224 | m[9] = 0 225 | m[10] = 1 226 | m[11] = z 227 | 228 | m[12] = 0 229 | m[13] = 0 230 | m[14] = 0 231 | m[15] = 1 232 | 233 | return result 234 | } 235 | 236 | // ### Matrix.rotate(a, x, y, z[, result]) 237 | // 238 | // Create a rotation matrix that rotates by `a` degrees around the vector `x, y, z`. 239 | // You can optionally pass an existing matrix in `result` to avoid allocating 240 | // a new matrix. This emulates the OpenGL function `glRotate()`. 241 | static rotate (a, x, y, z, result) { 242 | if (!a || (!x && !y && !z)) { 243 | return Matrix.identity(result) 244 | } 245 | 246 | result = result || new Matrix() 247 | const m = result.m 248 | 249 | const d = Math.sqrt(x * x + y * y + z * z) 250 | a *= Math.PI / 180; x /= d; y /= d; z /= d 251 | const c = Math.cos(a); const s = Math.sin(a); const t = 1 - c 252 | 253 | m[0] = x * x * t + c 254 | m[1] = x * y * t - z * s 255 | m[2] = x * z * t + y * s 256 | m[3] = 0 257 | 258 | m[4] = y * x * t + z * s 259 | m[5] = y * y * t + c 260 | m[6] = y * z * t - x * s 261 | m[7] = 0 262 | 263 | m[8] = z * x * t - y * s 264 | m[9] = z * y * t + x * s 265 | m[10] = z * z * t + c 266 | m[11] = 0 267 | 268 | m[12] = 0 269 | m[13] = 0 270 | m[14] = 0 271 | m[15] = 1 272 | 273 | return result 274 | } 275 | 276 | // ### Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz[, result]) 277 | // 278 | // Returns a matrix that puts the camera at the eye point `ex, ey, ez` looking 279 | // toward the center point `cx, cy, cz` with an up direction of `ux, uy, uz`. 280 | // You can optionally pass an existing matrix in `result` to avoid allocating 281 | // a new matrix. This emulates the OpenGL function `gluLookAt()`. 282 | static lookAtORIGINAL (ex, ey, ez, cx, cy, cz, ux, uy, uz, result) { 283 | result = result || new Matrix() 284 | const m = result.m 285 | 286 | // f = e.subtract(c).unit() 287 | let fx = ex - cx; let fy = ey - cy; let fz = ez - cz 288 | let d = Math.sqrt(fx * fx + fy * fy + fz * fz) 289 | fx /= d; fy /= d; fz /= d 290 | 291 | // s = u.cross(f).unit() 292 | let sx = uy * fz - uz * fy 293 | let sy = uz * fx - ux * fz 294 | let sz = ux * fy - uy * fx 295 | d = Math.sqrt(sx * sx + sy * sy + sz * sz) 296 | sx /= d; sy /= d; sz /= d 297 | 298 | // t = f.cross(s).unit() 299 | let tx = fy * sz - fz * sy 300 | let ty = fz * sx - fx * sz 301 | let tz = fx * sy - fy * sx 302 | d = Math.sqrt(tx * tx + ty * ty + tz * tz) 303 | tx /= d; ty /= d; tz /= d 304 | 305 | m[0] = sx 306 | m[1] = sy 307 | m[2] = sz 308 | m[3] = -(sx * ex + sy * ey + sz * ez) // -s.dot(e) 309 | 310 | m[4] = tx 311 | m[5] = ty 312 | m[6] = tz 313 | m[7] = -(tx * ex + ty * ey + tz * ez) // -t.dot(e) 314 | 315 | m[8] = fx 316 | m[9] = fy 317 | m[10] = fz 318 | m[11] = -(fx * ex + fy * ey + fz * ez) // -f.dot(e) 319 | 320 | m[12] = 0 321 | m[13] = 0 322 | m[14] = 0 323 | m[15] = 1 324 | 325 | return result 326 | }; 327 | 328 | // ### Matrix.lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz[, result]) 329 | // 330 | // Returns a matrix that puts the camera at the eye point `ex, ey, ez` looking 331 | // toward the center point `cx, cy, cz` with an up direction of `ux, uy, uz`. 332 | // You can optionally pass an existing matrix in `result` to avoid allocating 333 | // a new matrix. This emulates the OpenGL function `gluLookAt()`. 334 | static lookAtTRYOUT (nx, ny, nz, result) { 335 | result = result || new Matrix() 336 | const m = result.m 337 | 338 | const len = Math.sqrt(nx * nx + nz * nz) 339 | 340 | m[0] = nz / len 341 | m[1] = 0 342 | m[2] = -nx / len 343 | m[3] = 0 344 | 345 | m[4] = nx * ny / len 346 | m[5] = -len 347 | m[6] = nz * ny / len 348 | m[7] = 0 349 | 350 | m[8] = nx 351 | m[9] = ny 352 | m[10] = nz 353 | m[11] = 0 354 | 355 | m[12] = 0 356 | m[13] = 0 357 | m[14] = 0 358 | m[15] = 1 359 | 360 | return result 361 | }; 362 | 363 | static lookAt (nx, ny, nz, result) { 364 | result = result || new Matrix() 365 | const m = result.m 366 | 367 | const len = Math.sqrt(nx * nx + nz * nz) 368 | 369 | /* Find cosθ and sinθ; if gimbal lock, choose (1,0) arbitrarily */ 370 | const c2 = len ? nx / len : 1.0 371 | const s2 = len ? nz / len : 0.0 372 | 373 | m[0] = nx 374 | m[1] = -s2 375 | m[2] = -nz * c2 376 | m[3] = 0 377 | 378 | m[4] = ny 379 | m[5] = 0 380 | m[6] = len 381 | m[7] = 0 382 | 383 | m[8] = nz 384 | m[9] = c2 385 | m[10] = -nz * s2 386 | m[11] = 0 387 | 388 | m[12] = 0 389 | m[13] = 0 390 | m[14] = 0 391 | m[15] = 1 392 | 393 | return result 394 | }; 395 | } 396 | -------------------------------------------------------------------------------- /src/smoothvoxels/modelwriter.js: -------------------------------------------------------------------------------- 1 | import { MATSTANDARD, FLAT, FRONT } from './constants.js' 2 | import { xyzRangeForSize } from './voxels.js' 3 | 4 | const SINGLE_HEX_VALUES = new Map() 5 | SINGLE_HEX_VALUES.set(0, 0) 6 | SINGLE_HEX_VALUES.set(0x11, 1) 7 | SINGLE_HEX_VALUES.set(0x22, 2) 8 | SINGLE_HEX_VALUES.set(0x33, 3) 9 | SINGLE_HEX_VALUES.set(0x44, 4) 10 | SINGLE_HEX_VALUES.set(0x55, 5) 11 | SINGLE_HEX_VALUES.set(0x66, 6) 12 | SINGLE_HEX_VALUES.set(0x77, 7) 13 | SINGLE_HEX_VALUES.set(0x88, 8) 14 | SINGLE_HEX_VALUES.set(0x99, 9) 15 | SINGLE_HEX_VALUES.set(0xaa, 10) 16 | SINGLE_HEX_VALUES.set(0xbb, 11) 17 | SINGLE_HEX_VALUES.set(0xcc, 12) 18 | SINGLE_HEX_VALUES.set(0xdd, 13) 19 | SINGLE_HEX_VALUES.set(0xee, 14) 20 | SINGLE_HEX_VALUES.set(0xff, 15) 21 | 22 | export default class ModelWriter { 23 | /** 24 | * Serialize the model to a string. 25 | * When repeat is used, compressed is ignored. 26 | * @param model The model data. 27 | * @param compressed Wether the voxels need to be compressed using Recursive Runlength Encoding. 28 | * @param repeat An integer specifying whether to repeat the voxels to double or tripple the size, default is 1. 29 | */ 30 | static writeToString (model, compressed, repeat, modelLine = null, materialLine = null, extraOptionalModelFields = {}, skipVoxels = false) { 31 | repeat = Math.round(repeat || 1) 32 | 33 | const voxColorToCount = new Map() 34 | const voxColorToHex = new Map() 35 | 36 | const { voxels, voxColorToColorId } = model 37 | 38 | // Include all shell materials 39 | for (const shell of (model.shell || [])) { 40 | for (const voxColor of voxColorToColorId.keys()) { 41 | const materialIndex = (voxColor >> 24) & 0xff 42 | if (materialIndex === shell.materialIndex) { 43 | voxColorToCount.set(voxColor, 1) 44 | } 45 | } 46 | } 47 | 48 | model.materials.forEach(function (material, index) { 49 | for (const shell of (material.shell || [])) { 50 | for (const voxColor of voxColorToColorId.keys()) { 51 | const materialIndex = (voxColor >> 24) & 0xff 52 | if (materialIndex === shell.materialIndex) { 53 | voxColorToCount.set(voxColor, 1) 54 | } 55 | } 56 | } 57 | }) 58 | 59 | for (const [voxColor, count] of voxels.getVoxColorCounts()) { 60 | if (!voxColorToCount.has(voxColor)) { 61 | const r = voxColor & 0xff 62 | const g = (voxColor >> 8) & 0xff 63 | const b = (voxColor >> 16) & 0xff 64 | 65 | let hex 66 | 67 | if (SINGLE_HEX_VALUES.has(r) && SINGLE_HEX_VALUES.has(g) && SINGLE_HEX_VALUES.has(b)) { 68 | hex = '#' + (SINGLE_HEX_VALUES.get(b) + SINGLE_HEX_VALUES.get(g) * 16 + SINGLE_HEX_VALUES.get(r) * 256).toString(16).padStart(3, '0') 69 | } else { 70 | hex = '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0') 71 | } 72 | 73 | voxColorToHex.set(voxColor, hex.toUpperCase()) 74 | } 75 | 76 | const c = voxColorToCount.get(voxColor) || 0 77 | voxColorToCount.set(voxColor, c + count) 78 | } 79 | 80 | for (const voxColor of voxColorToCount.keys()) { 81 | const r = voxColor & 0xff 82 | const g = (voxColor >> 8) & 0xff 83 | const b = (voxColor >> 16) & 0xff 84 | 85 | let hex 86 | 87 | if (SINGLE_HEX_VALUES.has(r) && SINGLE_HEX_VALUES.has(g) && SINGLE_HEX_VALUES.has(b)) { 88 | hex = '#' + (SINGLE_HEX_VALUES.get(b) + SINGLE_HEX_VALUES.get(g) * 16 + SINGLE_HEX_VALUES.get(r) * 256).toString(16).padStart(3, '0') 89 | } else { 90 | hex = '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0') 91 | } 92 | 93 | voxColorToHex.set(voxColor, hex.toUpperCase()) 94 | } 95 | 96 | const sortedVoxColors = [...voxColorToCount.keys()].sort((a, b) => { 97 | return voxColorToCount.get(b) - voxColorToCount.get(a) 98 | }) 99 | 100 | let index = 0 101 | let maxIdLength = 0 102 | 103 | for (let c = 0; c < sortedVoxColors.length; c++) { 104 | const voxColor = sortedVoxColors[c] 105 | 106 | if (!voxColorToColorId.has(voxColor)) { 107 | let colorId 108 | do { 109 | colorId = this._colorIdForIndex(index++) 110 | } while ([...voxColorToColorId.values()].includes(colorId)) 111 | 112 | voxColorToColorId.set(voxColor, colorId) 113 | } 114 | 115 | maxIdLength = Math.max(voxColorToColorId.get(voxColor).length, maxIdLength) 116 | } 117 | 118 | // If multi character color Id's (2 or 3 long) are used, use extra spaces for the '-' for empty voxels 119 | const voxelWidth = compressed || maxIdLength === 1 || maxIdLength > 3 ? 1 : maxIdLength 120 | 121 | /// / Add the textures to the result 122 | let result = this._serializeTextures(model) 123 | 124 | /// / Add the lights to the result 125 | result += this._serializeLights(model) 126 | 127 | result += 'model\r\n' 128 | 129 | if (modelLine) { 130 | result += modelLine + '\r\n' 131 | } else { 132 | for (const key of Object.keys(extraOptionalModelFields)) { 133 | if (model[key]) { 134 | result += `${key} = ${model[key]}\r\n` 135 | } 136 | } 137 | 138 | // Add the size to the result 139 | const [sizeX, sizeY, sizeZ] = model.voxels.size 140 | if (sizeY === sizeX && sizeZ === sizeX) { result += `size = ${sizeX * repeat}\r\n` } else { result += `size = ${sizeX * repeat} ${sizeY * repeat} ${sizeZ * repeat}\r\n` } 141 | 142 | if (model.shape !== 'box') { result += `shape = ${model.shape}\r\n` } 143 | 144 | // Add the scale 145 | if (model.scale.x !== 1 || model.scale.y !== 1 || model.scale.z !== 1 || repeat !== 1) { 146 | if (model.scale.y === model.scale.x && model.scale.z === model.scale.x) { result += `scale = ${model.scale.x / repeat}\r\n` } else { result += `scale = ${model.scale.x / repeat} ${model.scale.y / repeat} ${model.scale.z / repeat}\r\n` } 147 | } 148 | 149 | if (model.resize) { result += `resize = ${model.resize}\r\n` } 150 | 151 | // Add the rotation (degrees) 152 | if (model.rotation.x !== 0 || model.rotation.y !== 0 || model.rotation.z !== 0) { 153 | result += `rotation = ${model.rotation.x} ${model.rotation.y} ${model.rotation.z}\r\n` 154 | } 155 | 156 | // Add the position (in world scale) 157 | if (model.position.x !== 0 || model.position.y !== 0 || model.position.z !== 0) { 158 | result += `position = ${model.position.x} ${model.position.y} ${model.position.z}\r\n` 159 | } 160 | 161 | if (model.origin) result += `origin = ${model.origin}\r\n` 162 | if (model.flatten) result += `flatten = ${model.flatten}\r\n` 163 | if (model.clamp) result += `clamp = ${model.clamp}\r\n` 164 | if (model.skip) result += `skip = ${model.skip}\r\n` 165 | if (model.tile) result += `tile = ${model.tile}\r\n` 166 | 167 | if (model.ao) result += `ao =${model.ao.color.toString() !== '#000' ? ' ' + model.ao.color : ''} ${model.ao.maxDistance} ${model.ao.strength}${model.ao.angle !== 70 ? ' ' + model.ao.angle : ''}\r\n` 168 | if (model.asSides) result += `aosides = ${model.aoSides}\r\n` 169 | if (model.asSamples) result += `aosamples = ${model.aoSamples}\r\n` 170 | 171 | if (model.wireframe) result += 'wireframe = true\r\n' 172 | 173 | if (!model.simplify) result += 'simplify = false\r\n' 174 | 175 | if (model.data) result += `data = ${this._serializeVertexData(model.data)}\r\n` 176 | 177 | if (model.shell) result += `shell = ${this._getShell(model.shell)}\r\n` 178 | } 179 | 180 | // Add the materials and colors to the result 181 | result += this._serializeMaterials(model, sortedVoxColors, voxColorToHex, materialLine) 182 | 183 | if (!skipVoxels) { 184 | // Add the voxels to the result 185 | if (!compressed || repeat !== 1) { result += this._serializeVoxels(model, repeat, voxelWidth) } else { result += this._serializeVoxelsRLE(model, 100) } 186 | } 187 | 188 | return result 189 | } 190 | 191 | static _serializeVertexData (data) { 192 | let result = null 193 | if (data && data.length > 0) { 194 | result = '' 195 | for (let d = 0; d < data.length; d++) { 196 | result += data[d].name + ' ' 197 | for (let v = 0; v < data[d].values.length; v++) { 198 | result += data[d].values[v] + ' ' 199 | } 200 | } 201 | } 202 | return result 203 | } 204 | 205 | /** 206 | * Serialize the textures of the model. 207 | * @param model The model data, including the textures. 208 | */ 209 | static _serializeTextures (model) { 210 | let result = '' 211 | let newLine = '' 212 | Object.getOwnPropertyNames(model.textures).forEach(function (textureName) { 213 | const texture = model.textures[textureName] 214 | 215 | const settings = [] 216 | settings.push(`id = ${texture.id}`) 217 | if (texture.cube) { settings.push('cube = true') } 218 | settings.push(`image = ${texture.image}`) 219 | 220 | result += `texture ${settings.join(', ')}\r\n` 221 | newLine = '\r\n' 222 | }) 223 | 224 | result += newLine 225 | 226 | return result 227 | } 228 | 229 | /** 230 | * Serialize the lights of the model. 231 | * @param model The model data, including the lights. 232 | */ 233 | static _serializeLights (model) { 234 | let result = '' 235 | let newLine = '' 236 | model.lights.forEach(function (light) { 237 | const settings = [] 238 | let colorAndStrength = `${light.color}` 239 | if (light.strength !== 1) { colorAndStrength += ` ${light.strength}` } 240 | settings.push(`color = ${colorAndStrength}`) 241 | if (light.direction) settings.push(`direction = ${light.direction.x} ${light.direction.y} ${light.direction.z}`) 242 | if (light.position) settings.push(`position = ${light.position.x} ${light.position.y} ${light.position.z}`) 243 | if (light.distance) settings.push(`distance = ${light.distance}`) 244 | if (light.size) { 245 | settings.push(`size = ${light.size}`) 246 | if (light.detail !== 1) settings.push(`detail = ${light.detail}`) 247 | } 248 | 249 | result += `light ${settings.join(', ')}\r\n` 250 | newLine = '\r\n' 251 | }) 252 | 253 | result += newLine 254 | 255 | return result 256 | } 257 | 258 | /** 259 | * Serialize the materials of the model. 260 | * @param model The model data, including the materials. 261 | */ 262 | static _serializeMaterials (model, sortedVoxColors, voxColorToHex, materialLine = null) { 263 | let result = '' 264 | model.materials.forEach(function (material) { 265 | const settings = [] 266 | const colorParts = [] 267 | const materialIndex = model.materials.materials.indexOf(material) 268 | 269 | for (const voxColor of sortedVoxColors) { 270 | if (((voxColor >> 24) & 0xFF) !== materialIndex) continue 271 | colorParts.push(`${model.voxColorToColorId.get(voxColor)}:${voxColorToHex.get(voxColor)}`) 272 | } 273 | 274 | if (colorParts.length === 0) return 275 | 276 | if (material.type !== MATSTANDARD) settings.push(`type = ${material.type}`) 277 | if (material.lighting !== FLAT) settings.push(`lighting = ${material.lighting}`) 278 | if (material.wireframe) settings.push('wireframe = true') 279 | if (material.roughness !== 1.0) settings.push(`roughness = ${material.roughness}`) 280 | if (material.metalness !== 0.0) settings.push(`metalness = ${material.metalness}`) 281 | if (material.fade) settings.push('fade = true') 282 | if (material.simplify !== null && material.simplify !== model.simplify) settings.push(`simplify = ${material.simplify}`) 283 | if (material.opacity !== 1.0) settings.push(`opacity = ${material.opacity}`) 284 | if (material.transparent) settings.push('transparent = true') 285 | if (material.refractionRatio !== 0.9) settings.push(`refractionRatio = ${material.refractionRatio}`) 286 | if (material.emissive) settings.push(`emissive = ${material.emissive.color} ${material.emissive.intensity}`) 287 | if (!material.fog) settings.push('fog = false') 288 | if (material.side !== FRONT) settings.push(`side = ${material.side}`) 289 | 290 | if (material.deform) settings.push(`deform = ${material.deform.count} ${material.deform.strength}${material.deform.damping !== 1 ? ' ' + material.deform.damping : ''}`) 291 | if (material.warp) settings.push(`warp = ${material.warp.amplitude} ${material.warp.frequency}`) 292 | if (material.scatter) settings.push(`scatter = ${material.scatter}`) 293 | 294 | if (material.ao) settings.push(`ao =${material.ao.color !== '#000' ? ' ' + material.ao.color : ''} ${material.ao.maxDistance} ${material.ao.strength}${material.ao.angle !== 70 ? ' ' + material.ao.angle : ''}`) 295 | if (model.lights.length > 0 && !material.lights) settings.push('lights = false') 296 | 297 | if (material.flatten) settings.push(`flatten = ${material.flatten}`) 298 | if (material.clamp) settings.push(`clamp = ${material.clamp}`) 299 | if (material.skip) settings.push(`skip = ${material.skip}`) 300 | 301 | if (material.map) settings.push(`map = ${material.map.id}`) 302 | if (material.normalMap) settings.push(`normalmap = ${material.normalMap.id}`) 303 | if (material.roughnessMap) settings.push(`roughnessmap = ${material.roughnessMap.id}`) 304 | if (material.metalnessMap) settings.push(`metalnessmap = ${material.metalnessMap.id}`) 305 | if (material.emissiveMap) settings.push(`emissivemap = ${material.emissiveMap.id}`) 306 | if (material.matcap) settings.push(`matcap = ${material.matcap.id}`) 307 | 308 | if (material.reflectionMap) settings.push(`reflectionmap = ${material.reflectionMap.id}`) 309 | if (material.refractionMap) settings.push(`refractionmap = ${material.refractionMap.id}`) 310 | 311 | if (material.mapTransform.uscale !== -1 || material.mapTransform.vscale !== -1) { 312 | let transform = 'maptransform =' 313 | transform += ` ${material.mapTransform.uscale} ${material.mapTransform.vscale}` 314 | if (material.mapTransform.uoffset !== 0 || material.mapTransform.voffset !== 0 || material.mapTransform.rotation !== 0) { 315 | transform += ` ${material.mapTransform.uoffset} ${material.mapTransform.voffset}` 316 | if (material.mapTransform.rotation !== 0) { transform += ` ${material.mapTransform.rotation}` } 317 | } 318 | settings.push(transform) 319 | } 320 | 321 | if (material.data) settings.push(`data = ${this._serializeVertexData(material.data)}`) 322 | 323 | if (material.shell) settings.push(`shell = ${this._getShell(material.shell)}`) 324 | 325 | result += 'material ' + (materialLine || settings.join(', ')) + '\r\n' 326 | 327 | if (colorParts.length > 0) { 328 | result += ' colors = ' 329 | result += colorParts.join(' ') 330 | } 331 | 332 | result += '\r\n' 333 | }, this) 334 | 335 | return result 336 | } 337 | 338 | /** 339 | * Calculate the color Id, after sorting the colors on usage. 340 | * This ensures often used colors are encoded as one character A-Z. 341 | * If there are more then 26 colors used the other colors are Aa, Ab, ... Ba, Bb, etc. or even Aaa, Aab, etc. 342 | * @param model The sorted index of the color. 343 | */ 344 | static _colorIdForIndex (index) { 345 | let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 346 | let id = '' 347 | do { 348 | const mod = index % 26 349 | id = chars[mod] + id.toLowerCase() 350 | index = (index - mod) / 26 351 | if (index < 26) { chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ' } 352 | } while (index > 0) 353 | return id 354 | } 355 | 356 | /** 357 | * Create shell string to write 358 | * @param shell array of shells 359 | */ 360 | static _getShell (shell) { 361 | if (shell.length === 0) { return 'none' } 362 | 363 | let result = '' 364 | for (let sh = 0; sh < shell.length; sh++) { 365 | result += `${shell[sh].colorId} ${shell[sh].distance} ` 366 | } 367 | return result.trim() 368 | } 369 | 370 | /** 371 | * Serialize the voxels without runlength encoding. 372 | * This results in a recognizable manualy editable syntax 373 | * @param model The model data 374 | */ 375 | static _serializeVoxels (model, repeat, voxelWidth) { 376 | const { voxColorToColorId } = model 377 | 378 | const emptyVoxel = '-' + ' '.repeat(Math.max(voxelWidth - 1)) 379 | const gutter = ' '.repeat(voxelWidth) 380 | 381 | const voxels = model.voxels 382 | 383 | const [minX, maxX, minY, maxY, minZ, maxZ] = xyzRangeForSize(voxels.size) 384 | 385 | const resultBuffer = new Uint8Array(voxels.size[0] * voxels.size[1] * voxels.size[2] * voxelWidth * repeat * 2) 386 | let resultOffset = 0 387 | 388 | resultBuffer[resultOffset++] = 0x76 // v 389 | resultBuffer[resultOffset++] = 0x6F // o 390 | resultBuffer[resultOffset++] = 0x78 // x 391 | resultBuffer[resultOffset++] = 0x65 // e 392 | resultBuffer[resultOffset++] = 0x6C // l 393 | resultBuffer[resultOffset++] = 0x73 // s 394 | resultBuffer[resultOffset++] = 0x0D // \r 395 | resultBuffer[resultOffset++] = 0x0A // \n 396 | 397 | for (let z = minZ; z <= maxZ; z++) { 398 | for (let zr = 0; zr < repeat; zr++) { 399 | for (let y = minY; y <= maxY; y++) { 400 | for (let yr = 0; yr < repeat; yr++) { 401 | for (let x = minX; x <= maxX; x++) { 402 | const paletteIndex = voxels.getPaletteIndexAt(x, y, z) 403 | for (let xr = 0; xr < repeat; xr++) { 404 | if (paletteIndex !== 0) { 405 | const colorId = voxColorToColorId.get(voxels.getColorAt(x, y, z)) 406 | 407 | for (let i = 0, l = colorId.length; i < l; i++) { 408 | resultBuffer[resultOffset++] = colorId.charCodeAt(i) 409 | } 410 | 411 | let l = colorId.length 412 | while (l++ < voxelWidth) { resultBuffer[resultOffset++] = 0x20 } 413 | } else { 414 | for (let i = 0, l = emptyVoxel.length; i < l; i++) { 415 | resultBuffer[resultOffset++] = emptyVoxel.charCodeAt(i) 416 | } 417 | } 418 | } 419 | } 420 | 421 | for (let i = 0, l = gutter.length; i < l; i++) { 422 | resultBuffer[resultOffset++] = gutter.charCodeAt(i) 423 | } 424 | } 425 | } 426 | resultBuffer[resultOffset++] = 0x0D // \r 427 | resultBuffer[resultOffset++] = 0x0A // \n 428 | } 429 | } 430 | 431 | resultBuffer[resultOffset++] = 0x0 // \r 432 | return (new TextDecoder('utf-8')).decode(resultBuffer.subarray(0, resultOffset - 1)) 433 | } 434 | 435 | /** 436 | * Serialize the voxels with runlength encoding. 437 | * Recognizing repeated patterns only in the compression window size 438 | * @param model The model data. 439 | * @param compressionWindow Typical values are from 10 to 100. 440 | */ 441 | static _serializeVoxelsRLE (model, compressionWindow) { 442 | const queue = [] 443 | let count = 0 444 | let lastVoxColor 445 | 446 | const { voxels, voxColorToColorId } = model 447 | const [minX, maxX, minY, maxY, minZ, maxZ] = xyzRangeForSize(voxels.size) 448 | 449 | for (let z = minZ; z <= maxZ; z++) { 450 | for (let y = minY; y <= maxY; y++) { 451 | for (let x = minX; x <= maxX; x++) { 452 | const paletteIndex = voxels.getPaletteIndexAt(x, y, z) 453 | const voxColor = paletteIndex === 0 ? null : voxels.getColorAt(x, y, z) 454 | 455 | if (voxColor === lastVoxColor) { 456 | count++ 457 | } else { 458 | this._addRleChunk(voxColorToColorId, queue, lastVoxColor, count, compressionWindow) 459 | lastVoxColor = voxColor 460 | count = 1 461 | } 462 | } 463 | } 464 | } 465 | 466 | // Add the last chunk to the RLE queue 467 | this._addRleChunk(voxColorToColorId, queue, lastVoxColor, count, compressionWindow) 468 | 469 | // Create the final result string 470 | let result = '' 471 | for (const item of queue) { 472 | result += this._rleToString(item) 473 | } 474 | 475 | return 'voxels\r\n' + result + '\r\n' // .match(/.{1,100}/g).join('\r\n') + '\r\n'; 476 | } 477 | 478 | /** 479 | * Add a chunk (repeat count + color ID, e.g. 13A, 24Aa or 35-) the RLE queue. 480 | * @param queue The RLE queue. 481 | * @param color The color to add. 482 | * @param count The number of times this color is repeated over the voxels. 483 | * @param compressionWindow Typical values are from 10 to 100. 484 | */ 485 | static _addRleChunk (voxColorToColorId, queue, voxColor, count, compressionWindow) { 486 | if (count === 0) { return } 487 | 488 | // Add the chunk to the RLE queue 489 | let chunk = count > 1 ? count.toString() : '' 490 | chunk += voxColor === null ? '-' : voxColorToColorId.get(voxColor) 491 | queue.push([chunk, 1, chunk]) 492 | 493 | // Check for repeating patterns of length 1 to the compression window 494 | for (let k = Math.max(0, queue.length - compressionWindow * 2); k <= queue.length - 2; k++) { 495 | const item = queue[k][0] 496 | 497 | // First cherk if there is a repeating pattern 498 | for (let j = 1; j < compressionWindow; j++) { 499 | if (k + 2 * j > queue.length) { break } 500 | let repeating = true 501 | for (let i = 0; i <= j - 1; i++) { 502 | repeating = queue[k + i][2] === queue[k + i + j][2] 503 | if (!repeating) break 504 | } 505 | if (repeating) { 506 | // Combine the repeating pattern into a sub array and remove the two occurences 507 | const arr = queue.splice(k, j) 508 | queue.splice(k, j - 1) 509 | queue[k] = [arr, 2, null] 510 | queue[k][2] = JSON.stringify(queue[k]) // Update for easy string comparison 511 | k = queue.length 512 | break 513 | } 514 | } 515 | 516 | if (Array.isArray(item) && queue.length > k + item.length) { 517 | // This was already a repeating pattern, check if it repeats again 518 | const array = item 519 | let repeating = true 520 | for (let i = 0; i < array.length; i++) { 521 | repeating = array[i][2] === queue[k + 1 + i][2] 522 | if (!repeating) break 523 | } 524 | if (repeating) { 525 | // Eemove the extra pattern and increase the repeat count 526 | queue.splice(k + 1, array.length) 527 | queue[k][1]++ 528 | queue[k][2] = null 529 | queue[k][2] = JSON.stringify(queue[k]) // Update for easy string comparison 530 | k = queue.length 531 | } 532 | } 533 | } 534 | } 535 | 536 | /** 537 | * Converts one (recursive RLE) chunk to a string. 538 | * @param chunk the entire RLE queue to start then recursivly the nested chunks. 539 | */ 540 | static _rleToString (chunk) { 541 | let result = chunk[1] === 1 ? '' : chunk[1].toString() 542 | const value = chunk[0] 543 | if (Array.isArray(value)) { 544 | result += '(' 545 | for (const sub of value) { 546 | result += this._rleToString(sub) 547 | } 548 | result += ')' 549 | } else { 550 | result += value 551 | } 552 | 553 | return result 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /src/smoothvoxels/noise.js: -------------------------------------------------------------------------------- 1 | // http://mrl.nyu.edu/~perlin/noise/ 2 | 3 | // This is the Improved Noise from the examples of Three.js. 4 | // It was adapted to change the permutation array from hard coded to generated. 5 | 6 | export default function () { 7 | const p = [] 8 | for (let i = 0; i < 256; i++) { 9 | p[i] = Math.floor(Math.random() * 256) 10 | p[i + 256] = p[i] 11 | } 12 | 13 | function fade (t) { 14 | return t * t * t * (t * (t * 6 - 15) + 10) 15 | } 16 | 17 | function lerp (t, a, b) { 18 | return a + t * (b - a) 19 | } 20 | 21 | function grad (hash, x, y, z) { 22 | const h = hash & 15 23 | const u = h < 8 ? x : y; const v = h < 4 ? y : h === 12 || h === 14 ? x : z 24 | return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v) 25 | } 26 | 27 | return { 28 | 29 | noise: function (x, y, z) { 30 | const floorX = Math.floor(x) 31 | const floorY = Math.floor(y) 32 | const floorZ = Math.floor(z) 33 | 34 | const X = floorX & 255 35 | const Y = floorY & 255 36 | const Z = floorZ & 255 37 | 38 | x -= floorX 39 | y -= floorY 40 | z -= floorZ 41 | 42 | const xMinus1 = x - 1; const yMinus1 = y - 1; const zMinus1 = z - 1 43 | const u = fade(x); const v = fade(y); const w = fade(z) 44 | const A = p[X] + Y 45 | const AA = p[A] + Z 46 | const AB = p[A + 1] + Z 47 | const B = p[X + 1] + Y 48 | const BA = p[B] + Z 49 | const BB = p[B + 1] + Z 50 | 51 | return lerp(w, 52 | lerp(v, 53 | lerp(u, grad(p[AA], x, y, z), 54 | grad(p[BA], xMinus1, y, z)), 55 | lerp(u, grad(p[AB], x, yMinus1, z), 56 | grad(p[BB], xMinus1, yMinus1, z)) 57 | ), 58 | lerp(v, 59 | lerp(u, grad(p[AA + 1], x, y, zMinus1), 60 | grad(p[BA + 1], xMinus1, y, z - 1)), 61 | lerp(u, grad(p[AB + 1], x, yMinus1, zMinus1), 62 | grad(p[BB + 1], xMinus1, yMinus1, zMinus1)) 63 | ) 64 | ) 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/smoothvoxels/normalscalculator.js: -------------------------------------------------------------------------------- 1 | import { SMOOTH, BOTH } from './constants' 2 | import { xyzRangeForSize } from './voxels' 3 | 4 | export default class NormalsCalculator { 5 | static calculateNormals (model, buffers) { 6 | const tile = model.tile 7 | 8 | const { faceNameIndices, faceEquidistant, faceSmooth, faceFlattened, faceClamped, vertX, vertY, vertZ, faceVertFlatNormalX, faceVertFlatNormalY, faceVertFlatNormalZ, faceVertSmoothNormalX, faceVertSmoothNormalY, faceVertSmoothNormalZ, faceVertBothNormalX, faceVertBothNormalY, faceVertBothNormalZ, faceVertNormalX, faceVertNormalY, faceVertNormalZ, faceMaterials, faceVertIndices, vertSmoothNormalX, vertSmoothNormalY, vertSmoothNormalZ, vertBothNormalX, vertBothNormalY, vertBothNormalZ } = buffers 9 | 10 | const [minX, maxX, minY, maxY, minZ, maxZ] = xyzRangeForSize(model.voxels.size) 11 | 12 | // Zero out smooth + both normals because buffers may be re-rused 13 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 14 | const faceOffset = faceIndex * 4 15 | 16 | for (let v = 0; v < 4; v++) { 17 | const vertIndex = faceVertIndices[faceOffset + v] 18 | vertSmoothNormalX[vertIndex] = 0 19 | vertSmoothNormalY[vertIndex] = 0 20 | vertSmoothNormalZ[vertIndex] = 0 21 | 22 | vertBothNormalX[vertIndex] = 0 23 | vertBothNormalY[vertIndex] = 0 24 | vertBothNormalZ[vertIndex] = 0 25 | } 26 | } 27 | 28 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 29 | // Compute face vertex normals 30 | const faceNameIndex = faceNameIndices[faceIndex] 31 | const equidistant = faceEquidistant.get(faceIndex) 32 | const flattened = faceFlattened.get(faceIndex) 33 | const clamped = faceClamped.get(faceIndex) 34 | 35 | // equidistant || (!flattened && !clamped) 36 | const faceSmoothValue = equidistant | (1 - (flattened | clamped)) 37 | faceSmooth.set(faceIndex, faceSmoothValue) 38 | 39 | const vert1Index = faceVertIndices[faceIndex * 4] 40 | const vert2Index = faceVertIndices[faceIndex * 4 + 1] 41 | const vert3Index = faceVertIndices[faceIndex * 4 + 2] 42 | const vert4Index = faceVertIndices[faceIndex * 4 + 3] 43 | 44 | const vmidX = (vertX[vert1Index] + vertX[vert2Index] + vertX[vert3Index] + vertX[vert4Index]) / 4 45 | const vmidY = (vertY[vert1Index] + vertY[vert2Index] + vertY[vert3Index] + vertY[vert4Index]) / 4 46 | const vmidZ = (vertZ[vert1Index] + vertZ[vert2Index] + vertZ[vert3Index] + vertZ[vert4Index]) / 4 47 | 48 | for (let v = 0; v < 4; v++) { 49 | const vertIndex = faceVertIndices[faceIndex * 4 + v] 50 | const prevVertIndex = faceVertIndices[faceIndex * 4 + ((v + 3) % 4)] 51 | 52 | const vX = vertX[vertIndex] 53 | const vXPrev = vertX[prevVertIndex] 54 | 55 | const vY = vertY[vertIndex] 56 | const vYPrev = vertY[prevVertIndex] 57 | 58 | const vZ = vertZ[vertIndex] 59 | const vZPrev = vertZ[prevVertIndex] 60 | 61 | let smoothX = vertSmoothNormalX[vertIndex] 62 | let smoothY = vertSmoothNormalY[vertIndex] 63 | let smoothZ = vertSmoothNormalZ[vertIndex] 64 | 65 | let bothX = vertBothNormalX[vertIndex] 66 | let bothY = vertBothNormalY[vertIndex] 67 | let bothZ = vertBothNormalZ[vertIndex] 68 | 69 | // e1 is diff between two verts 70 | let e1X = vXPrev - vX 71 | let e1Y = vYPrev - vY 72 | let e1Z = vZPrev - vZ 73 | 74 | // e2 is diff between vert and mid 75 | let e2X = vmidX - vX 76 | let e2Y = vmidY - vY 77 | let e2Z = vmidZ - vZ 78 | 79 | // Normalize e1 + e2 80 | let e1l = Math.sqrt(e1X * e1X + e1Y * e1Y + e1Z * e1Z) 81 | let e2l = Math.sqrt(e2X * e2X + e2Y * e2Y + e2Z * e2Z) 82 | e1l = e1l === 0 ? 1 : e1l 83 | e2l = e2l === 0 ? 1 : e2l 84 | 85 | const e1d = 1 / e1l 86 | e1X *= e1d 87 | e1Y *= e1d 88 | e1Z *= e1d 89 | 90 | const e2d = 1 / e2l 91 | e2X *= e2d 92 | e2Y *= e2d 93 | e2Z *= e2d 94 | 95 | // Calculate cross product to start normal 96 | let normalX = e1Y * e2Z - e1Z * e2Y 97 | let normalY = e1Z * e2X - e1X * e2Z 98 | let normalZ = e1X * e2Y - e1Y * e2X 99 | 100 | const voxMinXBuf = minX + 0.1 101 | const voxMaxXBuf = maxX + 0.9 102 | const voxMinYBuf = minY + 0.1 103 | const voxMaxYBuf = maxY + 0.9 104 | const voxMinZBuf = minZ + 0.1 105 | const voxMaxZBuf = maxZ + 0.9 106 | 107 | // In case of tiling, make normals peripendicular on edges 108 | if (tile) { 109 | if (((tile.nx && faceNameIndex === 0) || (tile.px && faceNameIndex === 1)) && 110 | (vY < voxMinYBuf || vY > voxMaxYBuf || 111 | vZ < voxMinZBuf || vZ > voxMaxZBuf)) { 112 | normalY = 0; normalZ = 0 113 | }; 114 | if (((tile.ny && faceNameIndex === 2) || (tile.py && faceNameIndex === 3)) && 115 | (vX < voxMinXBuf || vX > voxMaxXBuf || 116 | vZ < voxMinZBuf || vZ > voxMaxZBuf)) { 117 | normalX = 0; normalZ = 0 118 | }; 119 | if (((tile.nz && faceNameIndex === 4) || (tile.pz && faceNameIndex === 5)) && 120 | (vX < voxMinXBuf || vX > voxMaxXBuf || 121 | vY < voxMinYBuf || vY > voxMaxYBuf)) { 122 | normalX = 0; normalY = 0 123 | }; 124 | } 125 | 126 | // Normalize normal 127 | let nl = Math.sqrt(normalX * normalX + normalY * normalY + normalZ * normalZ) 128 | nl = nl === 0 ? 1 : nl 129 | 130 | const nd = 1 / nl 131 | normalX *= nd 132 | normalY *= nd 133 | normalZ *= nd 134 | 135 | // Store the normal for all 4 vertices (used for flat lighting) 136 | faceVertFlatNormalX[faceIndex * 4 + v] = normalX 137 | faceVertFlatNormalY[faceIndex * 4 + v] = normalY 138 | faceVertFlatNormalZ[faceIndex * 4 + v] = normalZ 139 | 140 | // Average the normals weighed by angle (i.e. wide adjacent faces contribute more than narrow adjacent faces) 141 | // Since we're using the mid point we can be wrong on strongly deformed quads, but not noticable 142 | const mul = e1X * e2X + e1Y * e2Y + e1Z * e2Z 143 | const angle = Math.acos(mul) 144 | 145 | // Always count towards the smoothNormal 146 | smoothX += normalX * angle 147 | smoothY += normalY * angle 148 | smoothZ += normalZ * angle 149 | 150 | // But only add this normal to bothNormal when the face uses smooth lighting 151 | bothX += faceSmoothValue * (normalX * angle) 152 | bothY += faceSmoothValue * (normalY * angle) 153 | bothZ += faceSmoothValue * (normalZ * angle) 154 | 155 | vertSmoothNormalX[vertIndex] = smoothX 156 | vertSmoothNormalY[vertIndex] = smoothY 157 | vertSmoothNormalZ[vertIndex] = smoothZ 158 | 159 | vertBothNormalX[vertIndex] = bothX 160 | vertBothNormalY[vertIndex] = bothY 161 | vertBothNormalZ[vertIndex] = bothZ 162 | } 163 | } 164 | 165 | // Normalize the smooth + both vertex normals 166 | for (let vertIndex = 0, c = model.vertCount; vertIndex < c; vertIndex++) { 167 | const smoothX = vertSmoothNormalX[vertIndex] 168 | const smoothY = vertSmoothNormalY[vertIndex] 169 | const smoothZ = vertSmoothNormalZ[vertIndex] 170 | 171 | const bothX = vertBothNormalX[vertIndex] 172 | const bothY = vertBothNormalY[vertIndex] 173 | const bothZ = vertBothNormalZ[vertIndex] 174 | 175 | const sl = Math.sqrt(smoothX * smoothX + smoothY * smoothY + smoothZ * smoothZ) 176 | const bl = Math.sqrt(bothX * bothX + bothY * bothY + bothZ * bothZ) 177 | 178 | if (sl !== 0) { 179 | vertSmoothNormalX[vertIndex] = smoothX / sl 180 | vertSmoothNormalY[vertIndex] = smoothY / sl 181 | vertSmoothNormalZ[vertIndex] = smoothZ / sl 182 | } 183 | 184 | if (bl !== 0) { 185 | vertBothNormalX[vertIndex] = bothX / bl 186 | vertBothNormalY[vertIndex] = bothY / bl 187 | vertBothNormalZ[vertIndex] = bothZ / bl 188 | } 189 | } 190 | 191 | const materials = model.materials.materials 192 | 193 | // Use flat normals if as both normals for faces if both is not set or isn't smooth 194 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 195 | const isSmooth = faceSmooth.get(faceIndex) === 1 196 | const material = materials[faceMaterials[faceIndex]] 197 | 198 | for (let i = 0; i < 4; i++) { 199 | const faceVertNormalIndex = faceIndex * 4 + i 200 | const vertIndex = faceVertIndices[faceIndex * 4 + i] 201 | faceVertSmoothNormalX[faceVertNormalIndex] = vertSmoothNormalX[vertIndex] 202 | faceVertSmoothNormalY[faceVertNormalIndex] = vertSmoothNormalY[vertIndex] 203 | faceVertSmoothNormalZ[faceVertNormalIndex] = vertSmoothNormalZ[vertIndex] 204 | 205 | faceVertBothNormalX[faceVertNormalIndex] = !isSmooth || vertBothNormalX[vertIndex] === 0 ? faceVertFlatNormalX[faceVertNormalIndex] : vertBothNormalX[vertIndex] 206 | faceVertBothNormalY[faceVertNormalIndex] = !isSmooth || vertBothNormalY[vertIndex] === 0 ? faceVertFlatNormalY[faceVertNormalIndex] : vertBothNormalY[vertIndex] 207 | faceVertBothNormalZ[faceVertNormalIndex] = !isSmooth || vertBothNormalZ[vertIndex] === 0 ? faceVertFlatNormalZ[faceVertNormalIndex] : vertBothNormalZ[vertIndex] 208 | 209 | switch (material.lighting) { 210 | case SMOOTH: 211 | faceVertNormalX[faceVertNormalIndex] = faceVertSmoothNormalX[faceVertNormalIndex] 212 | faceVertNormalY[faceVertNormalIndex] = faceVertSmoothNormalY[faceVertNormalIndex] 213 | faceVertNormalZ[faceVertNormalIndex] = faceVertSmoothNormalZ[faceVertNormalIndex] 214 | break 215 | case BOTH: 216 | faceVertNormalX[faceVertNormalIndex] = faceVertBothNormalX[faceVertNormalIndex] 217 | faceVertNormalY[faceVertNormalIndex] = faceVertBothNormalY[faceVertNormalIndex] 218 | faceVertNormalZ[faceVertNormalIndex] = faceVertBothNormalZ[faceVertNormalIndex] 219 | break 220 | default: 221 | faceVertNormalX[faceVertNormalIndex] = faceVertFlatNormalX[faceVertNormalIndex] 222 | faceVertNormalY[faceVertNormalIndex] = faceVertFlatNormalY[faceVertNormalIndex] 223 | faceVertNormalZ[faceVertNormalIndex] = faceVertFlatNormalZ[faceVertNormalIndex] 224 | break 225 | } 226 | } 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/smoothvoxels/planar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Planars are the representaions of origin, clamp and skip 3 | */ 4 | export default class Planar { 5 | /** 6 | * Parse a planar representation from a string. 7 | * @param {string} value The string containing the planar settings. 8 | * @returns {object} An object with the planar values. 9 | */ 10 | static parse (value) { 11 | if (!value) { return undefined } 12 | 13 | value = ' ' + (value || '').toLowerCase() 14 | 15 | if (value !== ' ' && !/^(?!$)(\s+(?:none|-x|x|\+x|-y|y|\+y|-z|z|\+z|\s))+\s*$/.test(value)) { 16 | throw new Error(`SyntaxError: Planar expression '${value}' is only allowed to be 'none' or contain -x x +x -y y +y -z z +z.`) 17 | } 18 | 19 | const none = value.includes('none') 20 | return { 21 | nx: !none && value.includes('-x'), 22 | x: !none && value.includes(' x'), 23 | px: !none && value.includes('+x'), 24 | ny: !none && value.includes('-y'), 25 | y: !none && value.includes(' y'), 26 | py: !none && value.includes('+y'), 27 | nz: !none && value.includes('-z'), 28 | z: !none && value.includes(' z'), 29 | pz: !none && value.includes('+z') 30 | } 31 | } 32 | 33 | /** 34 | * Returns a planar as a string. 35 | * @param {object} planar The planar object. 36 | * @returns {string} The planar string. 37 | */ 38 | static toString (planar) { 39 | if (!planar) { return undefined } 40 | 41 | const result = '' + 42 | (planar.nx ? ' -x' : '') + (planar.x ? ' x' : '') + (planar.px ? ' +x' : '') + 43 | (planar.ny ? ' -y' : '') + (planar.y ? ' y' : '') + (planar.py ? ' +y' : '') + 44 | (planar.nz ? ' -z' : '') + (planar.z ? ' z' : '') + (planar.pz ? ' +z' : '') 45 | return result.trim() 46 | } 47 | 48 | /** 49 | * Combines two planars. 50 | * @param {object} planar1 The first planar object. 51 | * @param {object} planar2 The first planar object. 52 | * @param {object} defaultPlanar The default returned when planar1 and planar2 are both not set. 53 | * @returns {object} An object with the combined planar values. 54 | */ 55 | static combine (planar1, planar2, defaultPlanar) { 56 | if (!planar1 && !planar2) { return defaultPlanar } 57 | if (!planar1) { return planar2 } 58 | if (!planar2) { return planar1 } 59 | if (planar1 === planar2) { return planar1 } 60 | return { 61 | nx: planar1.nx || planar2.nx, 62 | x: planar1.x || planar2.x, 63 | px: planar1.px || planar2.px, 64 | ny: planar1.ny || planar2.ny, 65 | y: planar1.y || planar2.y, 66 | py: planar1.py || planar2.py, 67 | nz: planar1.nz || planar2.nz, 68 | z: planar1.z || planar2.z, 69 | pz: planar1.pz || planar2.pz 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/smoothvoxels/simplifier.js: -------------------------------------------------------------------------------- 1 | const EPS = 0.0001 2 | 3 | const contexti1 = { 4 | filled: false, 5 | lastVoxelAxis1: 0, 6 | lastVoxelAxis2: 0, 7 | maxVoxelAxis3: 0, 8 | lastFaceIndex: 0 9 | } 10 | 11 | const contexti2 = { 12 | filled: false, 13 | lastVoxelAxis1: 0, 14 | lastVoxelAxis2: 0, 15 | maxVoxelAxis3: 0, 16 | lastFaceIndex: 0 17 | } 18 | 19 | const contexti3 = { 20 | filled: false, 21 | lastVoxelAxis1: 0, 22 | lastVoxelAxis2: 0, 23 | maxVoxelAxis3: 0, 24 | lastFaceIndex: 0 25 | } 26 | 27 | const contexti4 = { 28 | filled: false, 29 | lastVoxelAxis1: 0, 30 | lastVoxelAxis2: 0, 31 | maxVoxelAxis3: 0, 32 | lastFaceIndex: 0 33 | } 34 | 35 | export default class Simplifier { 36 | // Combine all faces which are coplanar, have the same normals, colors, etc. 37 | static simplify (model, buffers) { 38 | if (!model.simplify) { return } 39 | 40 | const clearContexts = function () { 41 | contexti1.filled = false 42 | contexti2.filled = false 43 | contexti3.filled = false 44 | contexti4.filled = false 45 | } 46 | 47 | const materials = model.materials.materials 48 | const { faceCulled, faceNameIndices, vertX, vertY, vertZ, voxelXZYFaceIndices, voxelXYZFaceIndices, voxelYZXFaceIndices } = buffers 49 | 50 | // Combine nx, px, nz and pz faces vertical up 51 | for (let i = voxelXZYFaceIndices.length - model.faceCount, l = voxelXZYFaceIndices.length; i < l; i++) { 52 | const key = voxelXZYFaceIndices[i] 53 | const faceIndex = key & ((1 << 28) - 1) 54 | if (faceCulled.get(faceIndex)) continue 55 | 56 | const xzy = key / (1 << 28) 57 | const x = xzy >> 16 & 0xFF 58 | const z = xzy >> 8 & 0xFF 59 | const y = xzy & 0xFF 60 | const faceNameIndex = faceNameIndices[faceIndex] 61 | 62 | switch (faceNameIndex) { 63 | case 0: // nx 64 | this._mergeFaces(materials, model, buffers, contexti1, faceIndex, x, z, y, vertX, vertZ, vertY, 0, 1, 2, 3) 65 | break 66 | case 1: // px 67 | this._mergeFaces(materials, model, buffers, contexti2, faceIndex, x, z, y, vertX, vertZ, vertY, 0, 1, 2, 3) 68 | break 69 | case 4: // nz 70 | this._mergeFaces(materials, model, buffers, contexti3, faceIndex, x, z, y, vertX, vertZ, vertY, 0, 1, 2, 3) 71 | break 72 | case 5: // pz 73 | this._mergeFaces(materials, model, buffers, contexti4, faceIndex, x, z, y, vertX, vertZ, vertY, 0, 1, 2, 3) 74 | break 75 | } 76 | } 77 | 78 | clearContexts() 79 | 80 | // Combine nx, px, ny and py faces from back to front 81 | for (let i = voxelXYZFaceIndices.length - model.faceCount, l = voxelXYZFaceIndices.length; i < l; i++) { 82 | const key = voxelXYZFaceIndices[i] 83 | const faceIndex = key & ((1 << 28) - 1) 84 | if (faceCulled.get(faceIndex)) continue 85 | 86 | const xyz = key / (1 << 28) 87 | const x = xyz >> 16 & 0xFF 88 | const y = xyz >> 8 & 0xFF 89 | const z = xyz & 0xFF 90 | 91 | const faceNameIndex = faceNameIndices[faceIndex] 92 | 93 | switch (faceNameIndex) { 94 | case 0: // nx 95 | this._mergeFaces(materials, model, buffers, contexti1, faceIndex, x, y, z, vertX, vertY, vertZ, 1, 2, 3, 0) 96 | break 97 | case 1: // px 98 | this._mergeFaces(materials, model, buffers, contexti2, faceIndex, x, y, z, vertX, vertY, vertZ, 3, 0, 1, 2) 99 | break 100 | case 2: // ny 101 | this._mergeFaces(materials, model, buffers, contexti3, faceIndex, x, y, z, vertX, vertY, vertZ, 0, 1, 2, 3) 102 | break 103 | case 3: // py 104 | this._mergeFaces(materials, model, buffers, contexti4, faceIndex, x, y, z, vertX, vertY, vertZ, 2, 3, 0, 1) 105 | break 106 | } 107 | } 108 | 109 | clearContexts() 110 | 111 | // Combine ny, py, nz and pz faces from left to right 112 | for (let i = voxelYZXFaceIndices.length - model.faceCount, l = voxelYZXFaceIndices.length; i < l; i++) { 113 | const key = voxelYZXFaceIndices[i] 114 | const faceIndex = key & ((1 << 28) - 1) 115 | if (faceCulled.get(faceIndex)) continue 116 | 117 | const yzx = key / (1 << 28) 118 | const y = yzx >> 16 & 0xFF 119 | const z = yzx >> 8 & 0xFF 120 | const x = yzx & 0xFF 121 | 122 | const faceNameIndex = faceNameIndices[faceIndex] 123 | 124 | switch (faceNameIndex) { 125 | case 2: // ny 126 | this._mergeFaces(materials, model, buffers, contexti1, faceIndex, y, z, x, vertY, vertZ, vertX, 1, 2, 3, 0) 127 | break 128 | case 3: // py 129 | this._mergeFaces(materials, model, buffers, contexti2, faceIndex, y, z, x, vertY, vertZ, vertX, 1, 2, 3, 0) 130 | break 131 | case 4: // nz 132 | this._mergeFaces(materials, model, buffers, contexti3, faceIndex, y, z, x, vertY, vertZ, vertX, 3, 0, 1, 2) 133 | break 134 | case 5: // pz 135 | this._mergeFaces(materials, model, buffers, contexti4, faceIndex, y, z, x, vertY, vertZ, vertX, 1, 2, 3, 0) 136 | break 137 | } 138 | } 139 | 140 | clearContexts() 141 | } 142 | 143 | static _mergeFaces (materials, model, buffers, context, faceIndex, vaxis1, vaxis2, vaxis3, axis1Arr, axis2Arr, axis3Arr, v0, v1, v2, v3) { 144 | const { faceCulled, faceMaterials, vertX, vertY, vertZ, faceVertIndices, faceVertNormalX, faceVertNormalY, faceVertNormalZ, faceVertColorR, faceVertColorG, faceVertColorB, faceVertUs, faceVertVs, faceVertFlatNormalX, faceVertFlatNormalY, faceVertFlatNormalZ, faceVertSmoothNormalX, faceVertSmoothNormalY, faceVertSmoothNormalZ, faceVertBothNormalX, faceVertBothNormalY, faceVertBothNormalZ } = buffers 145 | const materialIndex = faceMaterials[faceIndex] 146 | const material = materials[materialIndex] 147 | 148 | if (context.filled && 149 | context.lastVoxelAxis1 === vaxis1 && context.lastVoxelAxis2 === vaxis2 && 150 | (material.simplify === true || (material.simplify === null && model.simplify === true)) && 151 | faceCulled.get(faceIndex) === 0) { 152 | if (context.maxVoxelAxis3 !== vaxis3 - 1) { 153 | // Voxel was skipped, reset context and continue. 154 | context.filled = true 155 | context.lastVoxelAxis1 = vaxis1 156 | context.lastVoxelAxis2 = vaxis2 157 | context.maxVoxelAxis3 = vaxis3 158 | context.lastFaceIndex = faceIndex 159 | return 160 | } 161 | 162 | const faceOffset = faceIndex * 4 163 | const lastFaceIndex = context.lastFaceIndex 164 | const lastFaceOffset = lastFaceIndex * 4 165 | if (faceMaterials[lastFaceIndex] !== materialIndex) return 166 | 167 | const faceVertNormal0X = faceVertNormalX[faceOffset] 168 | const faceVertNormal0Y = faceVertNormalY[faceOffset] 169 | const faceVertNormal0Z = faceVertNormalZ[faceOffset] 170 | const faceVertNormal1X = faceVertNormalX[faceOffset + 1] 171 | const faceVertNormal1Y = faceVertNormalY[faceOffset + 1] 172 | const faceVertNormal1Z = faceVertNormalZ[faceOffset + 1] 173 | const faceVertNormal2X = faceVertNormalX[faceOffset + 2] 174 | const faceVertNormal2Y = faceVertNormalY[faceOffset + 2] 175 | const faceVertNormal2Z = faceVertNormalZ[faceOffset + 2] 176 | const faceVertNormal3X = faceVertNormalX[faceOffset + 3] 177 | const faceVertNormal3Y = faceVertNormalY[faceOffset + 3] 178 | const faceVertNormal3Z = faceVertNormalZ[faceOffset + 3] 179 | 180 | const lastFaceVertNormal0X = faceVertNormalX[lastFaceOffset] 181 | const lastFaceVertNormal0Y = faceVertNormalY[lastFaceOffset] 182 | const lastFaceVertNormal0Z = faceVertNormalZ[lastFaceOffset] 183 | const lastFaceVertNormal1X = faceVertNormalX[lastFaceOffset + 1] 184 | const lastFaceVertNormal1Y = faceVertNormalY[lastFaceOffset + 1] 185 | const lastFaceVertNormal1Z = faceVertNormalZ[lastFaceOffset + 1] 186 | const lastFaceVertNormal2X = faceVertNormalX[lastFaceOffset + 2] 187 | const lastFaceVertNormal2Y = faceVertNormalY[lastFaceOffset + 2] 188 | const lastFaceVertNormal2Z = faceVertNormalZ[lastFaceOffset + 2] 189 | const lastFaceVertNormal3X = faceVertNormalX[lastFaceOffset + 3] 190 | const lastFaceVertNormal3Y = faceVertNormalY[lastFaceOffset + 3] 191 | const lastFaceVertNormal3Z = faceVertNormalZ[lastFaceOffset + 3] 192 | 193 | const normalsEqual = 194 | this._normalEquals(faceVertNormal0X, faceVertNormal0Y, faceVertNormal0Z, lastFaceVertNormal0X, lastFaceVertNormal0Y, lastFaceVertNormal0Z) && 195 | this._normalEquals(faceVertNormal1X, faceVertNormal1Y, faceVertNormal1Z, lastFaceVertNormal1X, lastFaceVertNormal1Y, lastFaceVertNormal1Z) && 196 | this._normalEquals(faceVertNormal2X, faceVertNormal2Y, faceVertNormal2Z, lastFaceVertNormal2X, lastFaceVertNormal2Y, lastFaceVertNormal2Z) && 197 | this._normalEquals(faceVertNormal3X, faceVertNormal3Y, faceVertNormal3Z, lastFaceVertNormal3X, lastFaceVertNormal3Y, lastFaceVertNormal3Z) 198 | 199 | // Normals not equal, can't merge 200 | if (!normalsEqual) return 201 | 202 | const faceVertColor0R = faceVertColorR[faceOffset] 203 | const faceVertColor0G = faceVertColorG[faceOffset] 204 | const faceVertColor0B = faceVertColorB[faceOffset] 205 | const faceVertColor1R = faceVertColorR[faceOffset + 1] 206 | const faceVertColor1G = faceVertColorG[faceOffset + 1] 207 | const faceVertColor1B = faceVertColorB[faceOffset + 1] 208 | const faceVertColor2R = faceVertColorR[faceOffset + 2] 209 | const faceVertColor2G = faceVertColorG[faceOffset + 2] 210 | const faceVertColor2B = faceVertColorB[faceOffset + 2] 211 | const faceVertColor3R = faceVertColorR[faceOffset + 3] 212 | const faceVertColor3G = faceVertColorG[faceOffset + 3] 213 | const faceVertColor3B = faceVertColorB[faceOffset + 3] 214 | 215 | const lastFaceVertColor0R = faceVertColorR[lastFaceOffset] 216 | const lastFaceVertColor0G = faceVertColorG[lastFaceOffset] 217 | const lastFaceVertColor0B = faceVertColorB[lastFaceOffset] 218 | const lastFaceVertColor1R = faceVertColorR[lastFaceOffset + 1] 219 | const lastFaceVertColor1G = faceVertColorG[lastFaceOffset + 1] 220 | const lastFaceVertColor1B = faceVertColorB[lastFaceOffset + 1] 221 | const lastFaceVertColor2R = faceVertColorR[lastFaceOffset + 2] 222 | const lastFaceVertColor2G = faceVertColorG[lastFaceOffset + 2] 223 | const lastFaceVertColor2B = faceVertColorB[lastFaceOffset + 2] 224 | const lastFaceVertColor3R = faceVertColorR[lastFaceOffset + 3] 225 | const lastFaceVertColor3G = faceVertColorG[lastFaceOffset + 3] 226 | const lastFaceVertColor3B = faceVertColorB[lastFaceOffset + 3] 227 | 228 | const colorsEqual = faceVertColor0R === lastFaceVertColor0R && faceVertColor0G === lastFaceVertColor0G && faceVertColor0B === lastFaceVertColor0B && 229 | faceVertColor1R === lastFaceVertColor1R && faceVertColor1G === lastFaceVertColor1G && faceVertColor1B === lastFaceVertColor1B && 230 | faceVertColor2R === lastFaceVertColor2R && faceVertColor2G === lastFaceVertColor2G && faceVertColor2B === lastFaceVertColor2B && 231 | faceVertColor3R === lastFaceVertColor3R && faceVertColor3G === lastFaceVertColor3G && faceVertColor3B === lastFaceVertColor3B 232 | 233 | // Colors not equal, can't merge 234 | if (!colorsEqual) return 235 | 236 | const faceVertIndexV0 = faceVertIndices[faceOffset + v0] 237 | const faceVertIndexV1 = faceVertIndices[faceOffset + v1] 238 | const faceVertIndexV2 = faceVertIndices[faceOffset + v2] 239 | const faceVertIndexV3 = faceVertIndices[faceOffset + v3] 240 | 241 | const faceVertV0X = vertX[faceVertIndexV0] 242 | const faceVertV0Y = vertY[faceVertIndexV0] 243 | const faceVertV0Z = vertZ[faceVertIndexV0] 244 | const faceVertV1X = vertX[faceVertIndexV1] 245 | const faceVertV1Y = vertY[faceVertIndexV1] 246 | const faceVertV1Z = vertZ[faceVertIndexV1] 247 | 248 | const lastFaceVertIndexV0 = faceVertIndices[lastFaceOffset + v0] 249 | const lastFaceVertIndexV1 = faceVertIndices[lastFaceOffset + v1] 250 | const lastFaceVertIndexV2 = faceVertIndices[lastFaceOffset + v2] 251 | const lastFaceVertIndexV3 = faceVertIndices[lastFaceOffset + v3] 252 | 253 | const lastFaceVertV0X = vertX[lastFaceVertIndexV0] 254 | const lastFaceVertV0Y = vertY[lastFaceVertIndexV0] 255 | const lastFaceVertV0Z = vertZ[lastFaceVertIndexV0] 256 | 257 | // Calculate the ratio between the face length and the total face length (in case they are combined) 258 | const faceLength = Math.sqrt( 259 | (faceVertV1X - faceVertV0X) * (faceVertV1X - faceVertV0X) + 260 | (faceVertV1Y - faceVertV0Y) * (faceVertV1Y - faceVertV0Y) + 261 | (faceVertV1Z - faceVertV0Z) * (faceVertV1Z - faceVertV0Z) 262 | ) 263 | const totalLength = Math.sqrt( 264 | (faceVertV1X - lastFaceVertV0X) * (faceVertV1X - lastFaceVertV0X) + 265 | (faceVertV1Y - lastFaceVertV0Y) * (faceVertV1Y - lastFaceVertV0Y) + 266 | (faceVertV1Z - lastFaceVertV0Z) * (faceVertV1Z - lastFaceVertV0Z) 267 | ) 268 | 269 | const ratio = faceLength / totalLength 270 | 271 | /* TODO JEL faceAo[0] === lastFaceAo[0] && 272 | faceAo[1] === lastFaceAo[1] && 273 | faceAo[2] === lastFaceAo[2] && 274 | faceAo[3] === lastFaceAo[3] && */ 275 | 276 | const positionsEqual = Math.abs(axis1Arr[lastFaceVertIndexV1] - (1 - ratio) * axis1Arr[faceVertIndexV1] - ratio * axis1Arr[lastFaceVertIndexV0]) <= EPS && 277 | Math.abs(axis2Arr[lastFaceVertIndexV1] - (1 - ratio) * axis2Arr[faceVertIndexV1] - ratio * axis2Arr[lastFaceVertIndexV0]) <= EPS && 278 | Math.abs(axis3Arr[lastFaceVertIndexV1] - (1 - ratio) * axis3Arr[faceVertIndexV1] - ratio * axis3Arr[lastFaceVertIndexV0]) <= EPS && 279 | Math.abs(axis1Arr[lastFaceVertIndexV2] - (1 - ratio) * axis1Arr[faceVertIndexV2] - ratio * axis1Arr[lastFaceVertIndexV3]) <= EPS && 280 | Math.abs(axis2Arr[lastFaceVertIndexV2] - (1 - ratio) * axis2Arr[faceVertIndexV2] - ratio * axis2Arr[lastFaceVertIndexV3]) <= EPS && 281 | Math.abs(axis3Arr[lastFaceVertIndexV2] - (1 - ratio) * axis3Arr[faceVertIndexV2] - ratio * axis3Arr[lastFaceVertIndexV3]) <= EPS 282 | 283 | if (!positionsEqual) return 284 | 285 | // console.log("merging faces", faceIndex, lastFaceIndex, faceOffset, lastFaceOffset, v1, v2); 286 | // Everything checks out, so add this face to the last one 287 | // console.log(`MERGE: ${this._faceVerticesToString(lastFaceVertices)}`); 288 | // console.log(` AND: ${this._faceVerticesToString(faceVertices)}`); 289 | // console.log("change", faceVertIndices[lastFaceOffset + v1], " to ", faceVertIndexV1); 290 | // console.log("change", faceVertIndices[lastFaceOffset + v2], " to ", faceVertIndexV2); 291 | 292 | faceVertIndices[lastFaceOffset + v1] = faceVertIndexV1 293 | faceVertIndices[lastFaceOffset + v2] = faceVertIndexV2 294 | 295 | // console.log(` TO: ${this._faceVerticesToString(lastFaceVertices)}`); 296 | 297 | faceVertUs[lastFaceOffset + v1] = faceVertUs[faceOffset + v1] 298 | faceVertVs[lastFaceOffset + v1] = faceVertVs[faceOffset + v1] 299 | 300 | faceVertUs[lastFaceOffset + v2] = faceVertUs[faceOffset + v2] 301 | faceVertVs[lastFaceOffset + v2] = faceVertVs[faceOffset + v2] 302 | 303 | faceVertFlatNormalX[lastFaceOffset + v1] = faceVertFlatNormalX[faceOffset + v1] 304 | faceVertFlatNormalY[lastFaceOffset + v1] = faceVertFlatNormalY[faceOffset + v1] 305 | faceVertFlatNormalZ[lastFaceOffset + v1] = faceVertFlatNormalZ[faceOffset + v1] 306 | faceVertFlatNormalX[lastFaceOffset + v2] = faceVertFlatNormalX[faceOffset + v2] 307 | faceVertFlatNormalY[lastFaceOffset + v2] = faceVertFlatNormalY[faceOffset + v2] 308 | faceVertFlatNormalZ[lastFaceOffset + v2] = faceVertFlatNormalZ[faceOffset + v2] 309 | 310 | faceVertSmoothNormalX[lastFaceOffset + v1] = faceVertSmoothNormalX[faceOffset + v1] 311 | faceVertSmoothNormalY[lastFaceOffset + v1] = faceVertSmoothNormalY[faceOffset + v1] 312 | faceVertSmoothNormalZ[lastFaceOffset + v1] = faceVertSmoothNormalZ[faceOffset + v1] 313 | faceVertSmoothNormalX[lastFaceOffset + v2] = faceVertSmoothNormalX[faceOffset + v2] 314 | faceVertSmoothNormalY[lastFaceOffset + v2] = faceVertSmoothNormalY[faceOffset + v2] 315 | faceVertSmoothNormalZ[lastFaceOffset + v2] = faceVertSmoothNormalZ[faceOffset + v2] 316 | 317 | faceVertBothNormalX[lastFaceOffset + v1] = faceVertBothNormalX[faceOffset + v1] 318 | faceVertBothNormalY[lastFaceOffset + v1] = faceVertBothNormalY[faceOffset + v1] 319 | faceVertBothNormalZ[lastFaceOffset + v1] = faceVertBothNormalZ[faceOffset + v1] 320 | faceVertBothNormalX[lastFaceOffset + v2] = faceVertBothNormalX[faceOffset + v2] 321 | faceVertBothNormalY[lastFaceOffset + v2] = faceVertBothNormalY[faceOffset + v2] 322 | faceVertBothNormalZ[lastFaceOffset + v2] = faceVertBothNormalZ[faceOffset + v2] 323 | 324 | context.maxVoxelAxis3 = vaxis3 325 | 326 | // And remove this face 327 | faceCulled.set(faceIndex, 1) 328 | model.nonCulledFaceCount-- 329 | 330 | return true 331 | } 332 | 333 | context.filled = true 334 | context.lastVoxelAxis1 = vaxis1 335 | context.lastVoxelAxis2 = vaxis2 336 | context.maxVoxelAxis3 = vaxis3 337 | context.lastFaceIndex = faceIndex 338 | return false 339 | } 340 | 341 | static _normalEquals (n1x, n1y, n1z, n2x, n2y, n2z) { 342 | return Math.abs(n1x - n2x) < 0.01 && // Allow for minimal differences 343 | Math.abs(n1y - n2y) < 0.01 && 344 | Math.abs(n1z - n2z) < 0.01 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/smoothvoxels/smoothvoxel.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME */ 2 | 3 | import ModelReader from './modelreader' 4 | import Buffers from './buffers' 5 | import SvoxMeshGenerator from './svoxmeshgenerator' 6 | import SvoxToThreeMeshConverter from './svoxtothreemeshconverter' 7 | import WorkerPool from './workerpool' 8 | 9 | // We are combining this file with others in the minified version that will be used also in the worker. 10 | // Do not register the svox component inside the worker 11 | if (typeof window !== 'undefined') { 12 | if (typeof AFRAME !== 'undefined') { 13 | /* ******************************** 14 | * TODO: 15 | * - Cleanup playground HTML and Code 16 | * - Multiple models combined in a scene 17 | * - Model layers (combine multiple layers, e.g. weapon models) 18 | * - Model animation? (including layers?) 19 | * 20 | ***********************************/ 21 | 22 | let WORKERPOOL = null 23 | 24 | /** 25 | * Smooth Voxels component for A-Frame. 26 | */ 27 | AFRAME.registerComponent('svox', { 28 | schema: { 29 | model: { type: 'string' }, 30 | worker: { type: 'boolean', default: false } 31 | }, 32 | 33 | /** 34 | * Set if component needs multiple instancing. 35 | */ 36 | multiple: false, 37 | 38 | _MISSING: 'model size=9,scale=0.05,material lighting=flat,colors=A:#FFFFFF B:#FF8800 C:#FF0000,voxels 10B7-2B-C3-C-2B2-C-C2-2B3-C3-2B2-C-C2-2B-C3-C-2B7-11B7-B-6(7A2-)7A-B7-2B-C3-C-B-7A-C7AC-2(7A2-)7A-C7AC-7A-B-C3-C-2B2-C-C2-B-7A2-2(7A-C7AC-)7A2-7A-B2-C-C2-2B3-C3-B-2(7A2-)7A-C7AC-2(7A2-)7A-B3-C3-2B2-C-C2-B-7A2-2(7A-C7AC-)7A2-7A-B2-C-C2-2B-C3-C-B-7A-C7AC-2(7A2-)7A-C7AC-7A-B-C3-C-2B7-B-6(7A2-)7A-B7-11B7-2B-C3-C-2B2-C-C2-2B3-C3-2B2-C-C2-2B-C3-C-2B7-10B', 39 | _ERROR: 'model size=9,scale=0.05,material lighting=flat,colors=B:#FF8800 C:#FF0000 A:#FFFFFF,voxels 10B7-2(2B2-3C2-2B4-C2-)2B2-3C2-2B7-11B7-B-6(7A2-)7A-B7-2B2-3C2-B-6(7A2-)7A-B2-3C2-2B2-C4-B-2(7A-C7A2C)7A-C7AC-7A-B2-C4-2B2-3C2-B3(-7A-C7AC)-7A-B2-3C2-2B2-C4-B-7A-C2(7AC-7A2C)7AC-7A-B2-C4-2B2-3C2-B-6(7A2-)7A-B2-3C2-2B7-B-6(7A2-)7A-B7-11B7-2(2B2-3C2-2B2-C4-)2B2-3C2-2B7-10B', 40 | _workerPool: null, 41 | 42 | /** 43 | * Called once when component is attached. Generally for initial setup. 44 | */ 45 | init: function () { 46 | const el = this.el 47 | const data = this.data 48 | let useWorker = data.worker 49 | let error = false 50 | 51 | const modelName = data.model 52 | let modelString = window.SVOX.models[modelName] 53 | if (!modelString) { 54 | this._logError({ name: 'ConfigError', message: 'Model not found' }) 55 | modelString = this._MISSING 56 | error = true 57 | useWorker = false 58 | } 59 | 60 | if (!useWorker) { 61 | this._generateModel(modelString, el, error) 62 | } else { 63 | this._generateModelInWorker(modelString, el) 64 | } 65 | }, 66 | 67 | _generateModel: function (modelString, el, error) { 68 | let model 69 | model = window.model = ModelReader.readFromString(modelString) 70 | 71 | // let meshGenerator = new MeshGenerator(); 72 | // this.mesh = meshGenerator.generate(model); 73 | 74 | // for (let i = 0; i < 5; i++) { 75 | // SvoxMeshGenerator.generate(model); 76 | // //SvoxToThreeMeshConverter.generate(svoxmesh); 77 | // } 78 | 79 | // Set based on magicavoxel menger 80 | const buffers = new Buffers(1024 * 768 * 2) 81 | const t0 = performance.now() 82 | const svoxmesh = SvoxMeshGenerator.generate(model, buffers) 83 | // console.log('SvoxMeshGenerator.generate took ' + (performance.now() - t0) + ' ms.') 84 | const t1 = performance.now() 85 | this.mesh = SvoxToThreeMeshConverter.generate(svoxmesh) 86 | 87 | // Log stats 88 | const statsText = `Time: ${Math.round(t1 - t0)}ms. Verts:${svoxmesh.maxIndex + 1} Faces:${svoxmesh.indices.length / 3} Materials:${this.mesh.material.length}` 89 | // console.log(`SVOX ${this.data.model}: ${statsText}`); 90 | const statsEl = document.getElementById('svoxstats') 91 | if (statsEl && !error) { statsEl.innerHTML = 'Last render: ' + statsText } 92 | 93 | el.setObject3D('mesh', this.mesh) 94 | }, 95 | 96 | _generateModelInWorker: function (svoxmodel, el) { 97 | // Make sure the element has an Id, create a task in the task array and process it 98 | if (!el.id) { el.id = new Date().valueOf().toString(36) + Math.random().toString(36).substr(2) } 99 | const task = { svoxmodel, elementId: el.id } 100 | 101 | if (!WORKERPOOL) { 102 | WORKERPOOL = new WorkerPool(this, this._processResult) 103 | } 104 | WORKERPOOL.executeTask(task) 105 | }, 106 | 107 | _processResult: function (data) { 108 | if (data.svoxmesh.error) { 109 | this._logError(data.svoxmesh.error) 110 | } else { 111 | const mesh = SvoxToThreeMeshConverter.generate(data.svoxmesh) 112 | const el = document.querySelector('#' + data.elementId) 113 | 114 | el.setObject3D('mesh', mesh) 115 | } 116 | }, 117 | 118 | _toSharedArrayBuffer (floatArray) { 119 | const buffer = new Float32Array(new ArrayBuffer(floatArray.length * 4)) 120 | buffer.set(floatArray, 0) 121 | return buffer 122 | }, 123 | 124 | /** 125 | * Log errors to the console and an optional div #svoxerrors (as in the playground) 126 | * @param {modelName} The name of the model being loaded 127 | * @param {error} Error object with name and message 128 | */ 129 | _logError: function (error) { 130 | const errorText = error.name + ': ' + error.message 131 | const errorElement = document.getElementById('svoxerrors') 132 | if (errorElement) { errorElement.innerHTML = errorText } 133 | console.error(`SVOXERROR (${this.data.model}) ${errorText}`) 134 | }, 135 | 136 | /** 137 | * Called when component is attached and when component data changes. 138 | * Generally modifies the entity based on the data. 139 | * @param {object} oldData The previous version of the data 140 | */ 141 | update: function (oldData) { }, 142 | 143 | /** 144 | * Called when a component is removed (e.g., via removeAttribute). 145 | */ 146 | remove: function () { 147 | const maps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'matcap'] 148 | 149 | if (this.mesh) { // TODO: Test 150 | while (this.mesh.material.length > 0) { 151 | maps.forEach(function (map) { 152 | if (this.mesh.material[0][map]) { 153 | this.mesh.material[0][map].dispose() 154 | } 155 | }, this) 156 | 157 | this.mesh.material[0].dispose() 158 | this.mesh.material.shift() 159 | } 160 | 161 | this.mesh.geometry.dispose() 162 | this.el.removeObject3D('mesh') 163 | delete this.mesh 164 | } 165 | }, 166 | 167 | /** 168 | * Called on each scene tick. 169 | */ 170 | // tick: function (t) { }, 171 | 172 | /** 173 | * Called when entity pauses. 174 | * Use to stop or remove any dynamic or background behavior such as events. 175 | */ 176 | pause: function () { }, 177 | 178 | /** 179 | * Called when entity resumes. 180 | * Use to continue or add any dynamic or background behavior such as events. 181 | */ 182 | play: function () { }, 183 | 184 | /** 185 | * Event handlers that automatically get attached or detached based on scene state. 186 | */ 187 | events: { 188 | // click: function (evt) { } 189 | } 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/smoothvoxels/svox.worker.js: -------------------------------------------------------------------------------- 1 | /* global postMessage */ 2 | 3 | import ModelReader from './modelreader' 4 | import SvoxMeshGenerator from './svoxmeshgenerator' 5 | import Buffers from './buffers' 6 | 7 | // Set based on magicavoxel menger - this is a lot of memory :P 8 | const buffers = new Buffers(1024 * 768 * 2) 9 | 10 | onmessage = function (event) { // eslint-disable-line 11 | try { 12 | const svoxmesh = generateModel(event.data.svoxmodel) 13 | 14 | postMessage( 15 | { svoxmesh, elementId: event.data.elementId, worker: event.data.worker }, 16 | [svoxmesh.positions.buffer, svoxmesh.normals.buffer, svoxmesh.colors.buffer, svoxmesh.indices.buffer, svoxmesh.uvs.buffer] 17 | ) 18 | } catch (e) { 19 | console.error(e) 20 | } 21 | } 22 | 23 | function generateModel (svoxmodel) { 24 | const _MISSING = 'model size=9,scale=0.05,material lighting=flat,colors=B:#FF8800 C:#FF0000 A:#FFFFFF,voxels 10B7-2(2B2-3C2-2B4-C2-)2B2-3C2-2B7-11B7-B-6(7A2-)7A-B7-2B2-3C2-B-6(7A2-)7A-B2-3C2-2B2-C4-B-2(7A-C7A2C)7A-C7AC-7A-B2-C4-2B2-3C2-B3(-7A-C7AC)-7A-B2-3C2-2B2-C4-B-7A-C2(7AC-7A2C)7AC-7A-B2-C4-2B2-3C2-B-6(7A2-)7A-B2-3C2-2B7-B-6(7A2-)7A-B7-11B7-2(2B2-3C2-2B2-C4-)2B2-3C2-2B7-10B' 25 | const _ERROR = 'model size=9,scale=0.05,material lighting=flat,colors=A:#FFFFFF B:#FF8800 C:#FF0000,voxels 10B7-2B-C3-C-2B2-C-C2-2B3-C3-2B2-C-C2-2B-C3-C-2B7-11B7-B-6(7A2-)7A-B7-2B-C3-C-B-7A-C7AC-2(7A2-)7A-C7AC-7A-B-C3-C-2B2-C-C2-B-7A2-2(7A-C7AC-)7A2-7A-B2-C-C2-2B3-C3-B-2(7A2-)7A-C7AC-2(7A2-)7A-B3-C3-2B2-C-C2-B-7A2-2(7A-C7AC-)7A2-7A-B2-C-C2-2B-C3-C-B-7A-C7AC-2(7A2-)7A-C7AC-7A-B-C3-C-2B7-B-6(7A2-)7A-B7-11B7-2B-C3-C-2B2-C-C2-2B3-C3-2B2-C-C2-2B-C3-C-2B7-10B' 26 | 27 | let error 28 | if (!svoxmodel || svoxmodel.trim() === '') { 29 | error = { name: 'ConfigError', message: 'Model not found' } 30 | svoxmodel = _MISSING 31 | } 32 | 33 | let model = null 34 | try { 35 | model = ModelReader.readFromString(svoxmodel) 36 | } catch (err) { 37 | error = err 38 | model = ModelReader.readFromString(_ERROR) 39 | } 40 | 41 | const svoxmesh = SvoxMeshGenerator.generate(model, buffers) 42 | svoxmesh.error = error 43 | 44 | return svoxmesh 45 | } 46 | -------------------------------------------------------------------------------- /src/smoothvoxels/svoxbuffergeometry.js: -------------------------------------------------------------------------------- 1 | /* global THREE */ 2 | 3 | export default class SvoxBufferGeometry extends THREE.BufferGeometry { 4 | constructor () { 5 | super() 6 | this.type = 'SvoxBufferGeometry' 7 | } 8 | 9 | // Updates the geometry with the specified svox model 10 | update (svoxMesh, addGroups = true) { 11 | const { positions, normals, colors, bounds, uvs, data, indices } = svoxMesh 12 | 13 | this.freeMemory() 14 | 15 | let boundingBox = this.boundingBox 16 | let boundingSphere = this.boundingSphere 17 | 18 | if (!boundingBox) { 19 | boundingBox = this.boundingBox = new THREE.Box3() 20 | } 21 | 22 | if (!boundingSphere) { 23 | boundingSphere = this.boundingSphere = new THREE.Sphere() 24 | } 25 | 26 | boundingBox.min.set(bounds.minX, bounds.minY, bounds.minZ) 27 | boundingBox.max.set(bounds.maxX, bounds.maxY, bounds.maxZ) 28 | boundingSphere.center.set(bounds.centerX, bounds.centerY, bounds.centerZ) 29 | boundingSphere.radius = bounds.radius 30 | 31 | // Set the this attribute buffers from the model 32 | this.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)) 33 | this.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)) 34 | this.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)) 35 | 36 | if (uvs) { 37 | this.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)) 38 | } 39 | 40 | if (data) { 41 | for (let d = 0; d < data.length; d++) { 42 | this.setAttribute(data[d].name, new THREE.Float32BufferAttribute(data[d].values, data[d].width)) 43 | } 44 | } 45 | 46 | this.setIndex(new THREE.BufferAttribute(indices, 1)) 47 | this.clearGroups() 48 | 49 | if (addGroups) { 50 | // Add the groups for each material 51 | svoxMesh.groups.forEach(function (group) { 52 | this.addGroup(group.start, group.count, group.materialIndex) 53 | }, this) 54 | } else { 55 | this.setDrawRange(0, indices.length) 56 | } 57 | 58 | this.uvsNeedUpdate = true 59 | } 60 | 61 | dispose () { 62 | this.freeMemory() 63 | 64 | super.dispose() 65 | } 66 | 67 | freeMemory () { 68 | for (const attribute of Object.keys(this.attributes)) { 69 | this.deleteAttribute(attribute) 70 | } 71 | 72 | this.clearGroups() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/smoothvoxels/svoxtothreemeshconverter.js: -------------------------------------------------------------------------------- 1 | /* global THREE */ 2 | 3 | export default class SvoxToThreeMeshConverter { 4 | static generate (svoxMesh) { 5 | const materials = [] 6 | 7 | svoxMesh.materials.forEach(function (material) { 8 | materials.push(SvoxToThreeMeshConverter._generateMaterial(material)) 9 | }, this) 10 | 11 | const geometry = new THREE.BufferGeometry() 12 | SvoxToThreeMeshConverter.updateGeometry(svoxMesh, geometry, true) 13 | const mesh = new THREE.Mesh(geometry, materials) 14 | // return new THREE.VertexNormalsHelper(mesh, 0.1); 15 | // return new THREE.FaceNormalsHelper(mesh, 0.1); 16 | 17 | return mesh 18 | } 19 | 20 | static updateGeometry (svoxMesh, geometry, addGroups = true) { 21 | for (const attribute of Object.keys(geometry.attributes)) { 22 | geometry.deleteAttribute(attribute) 23 | } 24 | 25 | let boundingBox = geometry.boundingBox 26 | let boundingSphere = geometry.boundingSphere 27 | 28 | const { positions, normals, colors, bounds, uvs, data, indices } = svoxMesh 29 | 30 | if (!boundingBox) { 31 | boundingBox = geometry.boundingBox = new THREE.Box3() 32 | } 33 | 34 | if (!boundingSphere) { 35 | boundingSphere = geometry.boundingSphere = new THREE.Sphere() 36 | } 37 | 38 | boundingBox.min.set(bounds.minX, bounds.minY, bounds.minZ) 39 | boundingBox.max.set(bounds.minX, bounds.minY, bounds.minZ) 40 | boundingSphere.center.set(bounds.centerX, bounds.centerY, bounds.centerZ) 41 | boundingSphere.radius = bounds.radius 42 | 43 | // Set the geometry attribute buffers from the model 44 | geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)) 45 | geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3)) 46 | geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)) 47 | 48 | if (uvs) { 49 | geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2)) 50 | } 51 | 52 | if (data) { 53 | for (let d = 0; d < data.length; d++) { 54 | geometry.setAttribute(data[d].name, new THREE.Float32BufferAttribute(data[d].values, data[d].width)) 55 | } 56 | } 57 | 58 | geometry.setIndex(new THREE.BufferAttribute(indices, 1)) 59 | geometry.clearGroups() 60 | 61 | if (addGroups) { 62 | // Add the groups for each material 63 | svoxMesh.groups.forEach(function (group) { 64 | geometry.addGroup(group.start, group.count, group.materialIndex) 65 | }, this) 66 | } else { 67 | geometry.setDrawRange(0, indices.length) 68 | } 69 | 70 | geometry.uvsNeedUpdate = true 71 | } 72 | 73 | static _generateMaterial (definition) { 74 | // Create reflectivity from roughness 75 | definition.reflectivity = (1 - definition.roughness) * (definition.metalness * 0.95 + 0.05) 76 | 77 | // Create shininess from roughness 78 | definition.shininess = Math.pow(10, 5 * Math.pow(1 - definition.roughness, 1.1)) * 0.1 79 | 80 | switch (definition.side) { 81 | case 'back': definition.side = THREE.BackSide; break // Should never occur, faces are reversed instead 82 | case 'double': definition.side = THREE.DoubleSide; break 83 | default: definition.side = THREE.FrontSide; break 84 | } 85 | 86 | // Color encodings according to https://www.donmccurdy.com/2020/06/17/color-management-in-threejs/ 87 | // TODO: Should color management be addressed aywhere else? 88 | 89 | if (definition.map) { 90 | definition.map = SvoxToThreeMeshConverter._generateTexture(definition.map.image, THREE.sRGBEncoding, 91 | definition.map.uscale, definition.map.vscale, 92 | definition.map.uoffset, definition.map.voffset, definition.map.rotation) 93 | } 94 | 95 | if (definition.normalMap) { 96 | definition.normalMap = SvoxToThreeMeshConverter._generateTexture(definition.normalMap.image, THREE.LinearEncoding, 97 | definition.normalMap.uscale, definition.normalMap.vscale, 98 | definition.normalMap.uoffset, definition.normalMap.voffset, definition.normalMap.rotation) 99 | } 100 | 101 | if (definition.roughnessMap) { 102 | definition.roughnessMap = SvoxToThreeMeshConverter._generateTexture(definition.roughnessMap.image, THREE.LinearEncoding, 103 | definition.roughnessMap.uscale, definition.roughnessMap.vscale, 104 | definition.roughnessMap.uoffset, definition.roughnessMap.voffset, definition.roughnessMap.rotation) 105 | } 106 | 107 | if (definition.metalnessMap) { 108 | definition.metalnessMap = SvoxToThreeMeshConverter._generateTexture(definition.metalnessMap.image, THREE.LinearEncoding, 109 | definition.metalnessMap.uscale, definition.metalnessMap.vscale, 110 | definition.metalnessMap.uoffset, definition.metalnessMap.voffset, definition.metalnessMap.rotation) 111 | } 112 | 113 | if (definition.emissiveMap) { 114 | definition.emissiveMap = SvoxToThreeMeshConverter._generateTexture(definition.emissiveMap.image, THREE.sRGBEncoding, 115 | definition.emissiveMap.uscale, definition.emissiveMap.vscale, 116 | definition.emissiveMap.uoffset, definition.emissiveMap.voffset, definition.emissiveMap.rotation) 117 | } 118 | 119 | if (definition.matcap) { 120 | definition.matcap = SvoxToThreeMeshConverter._generateTexture(definition.matcap.image, THREE.sRGBEncoding) 121 | } 122 | 123 | if (definition.reflectionMap) { 124 | definition.envMap = new THREE.TextureLoader().load(definition.reflectionMap.image) 125 | definition.envMap.encoding = THREE.sRGBEncoding 126 | definition.envMap.mapping = THREE.EquirectangularReflectionMapping 127 | delete definition.reflectionMap 128 | } 129 | 130 | if (definition.refractionMap) { 131 | definition.envMap = new THREE.TextureLoader().load(definition.refractionMap.image) 132 | definition.envMap.encoding = THREE.sRGBEncoding 133 | definition.envMap.mapping = THREE.EquirectangularRefractionMapping 134 | delete definition.refractionMap 135 | } 136 | 137 | let material = null 138 | const type = definition.type 139 | delete definition.index 140 | delete definition.type 141 | switch (type) { 142 | // case 'physical': 143 | // Supported on A-Frame 1.3.0 for much better (single surface) refractive and reflective glass 144 | // Use for instance metalness 0.1 and roughness 0.1 with the below settings 145 | // definition.transmission = 1; 146 | // definition.thickness = 1.5; 147 | // material = new THREE.MeshPhysicalMaterial(definition); 148 | // break; 149 | case 'standard': 150 | delete definition.reflectivity 151 | delete definition.shininess 152 | material = new THREE.MeshStandardMaterial(definition) 153 | break 154 | 155 | case 'basic': 156 | delete definition.roughness 157 | delete definition.metalness 158 | delete definition.shininess 159 | delete definition.emissive 160 | delete definition.emissiveIntensity 161 | delete definition.roughnessMap 162 | delete definition.metalnessMap 163 | delete definition.emissiveMap 164 | material = new THREE.MeshBasicMaterial(definition) 165 | break 166 | 167 | case 'lambert': 168 | delete definition.roughness 169 | delete definition.metalness 170 | delete definition.shininess 171 | delete definition.roughnessMap 172 | delete definition.metalnessMap 173 | material = new THREE.MeshLambertMaterial(definition) 174 | break 175 | 176 | case 'phong': 177 | delete definition.roughness 178 | delete definition.metalness 179 | delete definition.roughnessMap 180 | delete definition.metalnessMap 181 | material = new THREE.MeshPhongMaterial(definition) 182 | break 183 | 184 | case 'matcap': 185 | delete definition.roughness 186 | delete definition.metalness 187 | delete definition.wireframe 188 | delete definition.reflectivity 189 | delete definition.shininess 190 | delete definition.emissive 191 | delete definition.emissiveIntensity 192 | delete definition.envMap 193 | delete definition.roughnessMap 194 | delete definition.metalnessMap 195 | delete definition.emissiveMap 196 | delete definition.reflectionMap 197 | delete definition.refractionMap 198 | delete definition.refractionRatio 199 | material = new THREE.MeshMatcapMaterial(definition) 200 | break 201 | 202 | case 'toon': 203 | delete definition.roughness 204 | delete definition.metalness 205 | delete definition.reflectivity 206 | delete definition.shininess 207 | delete definition.emissive 208 | delete definition.emissiveIntensity 209 | delete definition.envMap 210 | delete definition.roughnessMap 211 | delete definition.metalnessMap 212 | delete definition.reflectionMap 213 | delete definition.refractionMap 214 | delete definition.refractionRatio 215 | material = new THREE.MeshToonMaterial(definition) 216 | break 217 | 218 | case 'normal': 219 | delete definition.roughness 220 | delete definition.metalness 221 | delete definition.reflectivity 222 | delete definition.shininess 223 | delete definition.emissive 224 | delete definition.emissiveIntensity 225 | delete definition.map 226 | delete definition.envMap 227 | delete definition.roughnessMap 228 | delete definition.metalnessMap 229 | delete definition.emissiveMap 230 | delete definition.reflectionMap 231 | delete definition.refractionMap 232 | delete definition.refractionRatio 233 | material = new THREE.MeshNormalMaterial(definition) 234 | break 235 | 236 | default: { 237 | throw new Error(`SyntaxError: Unknown material type ${type}`) 238 | } 239 | } 240 | 241 | return material 242 | } 243 | 244 | static _generateTexture (image, encoding, uscale, vscale, uoffset, voffset, rotation) { 245 | const threetexture = new THREE.TextureLoader().load(image) 246 | threetexture.encoding = encoding 247 | threetexture.repeat.set(1 / uscale, 1 / vscale) 248 | threetexture.wrapS = THREE.RepeatWrapping 249 | threetexture.wrapT = THREE.RepeatWrapping 250 | threetexture.offset = new THREE.Vector2(uoffset, voffset) 251 | threetexture.rotation = rotation * Math.PI / 180 252 | return threetexture 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/smoothvoxels/uvassigner.js: -------------------------------------------------------------------------------- 1 | import { _FACEINDEXUVS } from './constants' 2 | 3 | export default class UVAssigner { 4 | static assignUVs (model, buffers) { 5 | const { faceMaterials, faceNameIndices, faceVertUs, faceVertVs } = buffers 6 | 7 | const materialUseOffsets = [] 8 | const materialUScales = [] 9 | const materialVScales = [] 10 | 11 | const materials = model.materials.materials 12 | 13 | for (let materialIndex = 0; materialIndex < materials.length; materialIndex++) { 14 | const material = materials[materialIndex] 15 | 16 | let useOffset = 0 // Simple (per voxel) textures don't need offsets per side 17 | let uscale = 1 18 | let vscale = 1 19 | 20 | if (material.map || material.normalMap || material.roughnessMap || material.metalnessMap || material.emissiveMap) { 21 | const sizeX = model.voxels.size[0] 22 | const sizeY = model.voxels.size[1] 23 | const sizeZ = model.voxels.size[2] 24 | 25 | if (material.mapTransform.uscale === -1) { 26 | uscale = 1 / Math.max(sizeX, sizeY, sizeZ) 27 | } 28 | 29 | if (material.mapTransform.vscale === -1) { 30 | vscale = 1 / Math.max(sizeX, sizeY, sizeZ) 31 | } 32 | 33 | if ((material.map && material.map.cube) || 34 | (material.normalMap && material.normalMap.cube) || 35 | (material.roughnessMap && material.roughnessMap.cube) || 36 | (material.metalnessMap && material.metalnessMap.cube) || 37 | (material.emissiveMap && material.emissiveMap.cube)) { 38 | useOffset = 1 // Use the offsets per face in the cube texture 39 | uscale = uscale / 4 // The cube texture is 4 x 2 40 | vscale = vscale / 2 41 | } 42 | } 43 | 44 | materialUseOffsets.push(useOffset) 45 | materialUScales.push(uscale) 46 | materialVScales.push(vscale) 47 | } 48 | 49 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 50 | const faceMaterialIndex = faceMaterials[faceIndex] 51 | const useOffset = materialUseOffsets[faceMaterialIndex] 52 | const uscale = materialUScales[faceMaterialIndex] 53 | const vscale = materialVScales[faceMaterialIndex] 54 | 55 | const faceUVs = _FACEINDEXUVS[faceNameIndices[faceIndex]] 56 | 57 | // model initializes the UV arrays to the proper vox x, y, z value 58 | const faceOffset = faceIndex * 4 59 | 60 | const voxU0 = faceVertUs[faceOffset + faceUVs.order[0]] 61 | const voxV0 = faceVertVs[faceOffset + faceUVs.order[0]] 62 | const voxU1 = faceVertUs[faceOffset + faceUVs.order[1]] 63 | const voxV1 = faceVertVs[faceOffset + faceUVs.order[1]] 64 | const voxU2 = faceVertUs[faceOffset + faceUVs.order[2]] 65 | const voxV2 = faceVertVs[faceOffset + faceUVs.order[2]] 66 | const voxU3 = faceVertUs[faceOffset + faceUVs.order[3]] 67 | const voxV3 = faceVertVs[faceOffset + faceUVs.order[3]] 68 | 69 | const uv1 = faceOffset + faceUVs.order[0] 70 | const uv2 = faceOffset + faceUVs.order[1] 71 | const uv3 = faceOffset + faceUVs.order[2] 72 | const uv4 = faceOffset + faceUVs.order[3] 73 | const uOffset = useOffset * faceUVs.uo 74 | const vOffset = useOffset * faceUVs.vo 75 | const uScale = faceUVs.ud * uscale 76 | const vScale = faceUVs.vd * vscale 77 | 78 | faceVertUs[uv1] = uOffset + (voxU0 + 0.0001) * uScale 79 | faceVertVs[uv1] = vOffset + (voxV0 + 0.0001) * vScale 80 | 81 | faceVertUs[uv2] = uOffset + (voxU1 + 0.0001) * uScale 82 | faceVertVs[uv2] = vOffset + (voxV1 + 0.9999) * vScale 83 | 84 | faceVertUs[uv3] = uOffset + (voxU2 + 0.9999) * uScale 85 | faceVertVs[uv3] = vOffset + (voxV2 + 0.9999) * vScale 86 | 87 | faceVertUs[uv4] = uOffset + (voxU3 + 0.9999) * uScale 88 | faceVertVs[uv4] = vOffset + (voxV3 + 0.0001) * vScale 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/smoothvoxels/vertexlinker.js: -------------------------------------------------------------------------------- 1 | export default class VertexLinker { 2 | static linkVertices (model, buffers, faceIndex) { 3 | const { faceClamped, vertNrOfClampedLinks, faceVertIndices, vertLinkIndices, vertLinkCounts } = buffers 4 | 5 | const clamped = faceClamped.get(faceIndex) 6 | 7 | if (clamped === 1) { 8 | // Do not link clamped face vertices so the do not pull in the sides on deform. 9 | // But now this leaves these vertices with only 3 links, which offsets the average. 10 | // Add the vertex itself to compensate the average. 11 | // This, for instance, results in straight 45 degree roofs when clamping the sides. 12 | // This is the only difference in handling flatten vs clamp. 13 | for (let v = 0; v < 4; v++) { 14 | const vertIndex = faceVertIndices[faceIndex * 4 + v] 15 | 16 | let hasSelfLink = false 17 | 18 | for (let l = 0, c = vertLinkCounts[vertIndex]; l < c; l++) { 19 | if (vertLinkIndices[vertIndex * 6 + l] === vertIndex) { 20 | hasSelfLink = true 21 | break 22 | } 23 | } 24 | 25 | if (!hasSelfLink) { 26 | vertLinkIndices[vertIndex * 6 + vertLinkCounts[vertIndex]] = vertIndex 27 | vertLinkCounts[vertIndex]++ 28 | vertNrOfClampedLinks[vertIndex]++ 29 | } 30 | } 31 | } else { 32 | // Link each vertex with its neighbor and back (so not diagonally) 33 | for (let v = 0; v < 4; v++) { 34 | const vertIndexFrom = faceVertIndices[faceIndex * 4 + v] 35 | const vertIndexTo = faceVertIndices[faceIndex * 4 + (v + 1) % 4] 36 | 37 | let hasForwardLink = false 38 | 39 | for (let l = 0, c = vertLinkCounts[vertIndexFrom]; l < c; l++) { 40 | if (vertLinkIndices[vertIndexFrom * 6 + l] === vertIndexTo) { 41 | hasForwardLink = true 42 | break 43 | } 44 | } 45 | 46 | if (!hasForwardLink) { 47 | vertLinkIndices[vertIndexFrom * 6 + vertLinkCounts[vertIndexFrom]] = vertIndexTo 48 | vertLinkCounts[vertIndexFrom]++ 49 | } 50 | 51 | let hasBackwardLink = false 52 | 53 | for (let l = 0, c = vertLinkCounts[vertIndexTo]; l < c; l++) { 54 | if (vertLinkIndices[vertIndexTo * 6 + l] === vertIndexFrom) { 55 | hasBackwardLink = true 56 | break 57 | } 58 | } 59 | 60 | if (!hasBackwardLink) { 61 | vertLinkIndices[vertIndexTo * 6 + vertLinkCounts[vertIndexTo]] = vertIndexFrom 62 | vertLinkCounts[vertIndexTo]++ 63 | } 64 | } 65 | } 66 | } 67 | 68 | static fixClampedLinks (model, buffers) { 69 | const { faceVertIndices, vertNrOfClampedLinks, vertFullyClamped, vertLinkCounts, vertLinkIndices } = buffers 70 | 71 | // Clamped sides are ignored when deforming so the clamped side does not pull in the other sodes. 72 | // This results in the other sides ending up nice and peripendicular to the clamped sides. 73 | // However, this als makes all of the vertices of the clamped side not deform. 74 | // This then results in the corners of these sides sticking out sharply with high deform counts. 75 | 76 | // Find all vertices that are fully clamped (i.e. not at the edge of the clamped side) 77 | for (let vertIndex = 0, c = model.vertCount; vertIndex < c; vertIndex++) { 78 | const nrOfClampedLinks = vertNrOfClampedLinks[vertIndex] 79 | const nrOfLinks = vertLinkCounts[vertIndex] 80 | 81 | if (nrOfClampedLinks === nrOfLinks) { 82 | vertFullyClamped.set(vertIndex, 1) 83 | vertLinkCounts[vertIndex] = 0 84 | } 85 | } 86 | 87 | // For these fully clamped vertices add links for normal deforming 88 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 89 | for (let v = 0; v < 4; v++) { 90 | const vertIndexFrom = faceVertIndices[faceIndex * 4 + v] 91 | const vertIndexTo = faceVertIndices[faceIndex * 4 + (v + 1) % 4] 92 | 93 | if (vertFullyClamped.get(vertIndexFrom) === 1) { 94 | let hasForwardLink = false 95 | 96 | for (let l = 0, c = vertLinkCounts[vertIndexFrom]; l < c; l++) { 97 | if (vertLinkIndices[vertIndexFrom * 6 + l] === vertIndexTo) { 98 | hasForwardLink = true 99 | break 100 | } 101 | } 102 | 103 | if (!hasForwardLink) { 104 | vertLinkIndices[vertIndexFrom * 6 + vertLinkCounts[vertIndexFrom]] = vertIndexTo 105 | vertLinkCounts[vertIndexFrom]++ 106 | } 107 | } 108 | 109 | if (vertFullyClamped.get(vertIndexTo) === 1) { 110 | let hasBackwardLink = false 111 | 112 | for (let l = 0, c = vertLinkCounts[vertIndexTo]; l < c; l++) { 113 | if (vertLinkIndices[vertIndexTo * 6 + l] === vertIndexFrom) { 114 | hasBackwardLink = true 115 | break 116 | } 117 | } 118 | 119 | if (!hasBackwardLink) { 120 | vertLinkIndices[vertIndexTo * 6 + vertLinkCounts[vertIndexTo]] = vertIndexFrom 121 | vertLinkCounts[vertIndexTo]++ 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/smoothvoxels/vertextransformer.js: -------------------------------------------------------------------------------- 1 | import Matrix from './matrix' 2 | 3 | const normalXs = [null, null, null, null] 4 | const normalYs = [null, null, null, null] 5 | const normalZs = [null, null, null, null] 6 | 7 | export default class VertexTransformer { 8 | static transformVertices (model, buffers) { 9 | const { vertX, vertY, vertZ, faceVertNormalX, faceVertFlatNormalX, faceVertNormalY, faceVertFlatNormalY, faceVertNormalZ, faceVertFlatNormalZ, faceVertSmoothNormalX, faceVertSmoothNormalY, faceVertSmoothNormalZ, faceVertBothNormalX, faceVertBothNormalY, faceVertBothNormalZ } = buffers 10 | const bor = model.determineBoundsOffsetAndRescale(model.resize, buffers) 11 | 12 | // Define the transformation in reverse order to how they are carried out 13 | let vertexTransform = new Matrix() 14 | 15 | vertexTransform = Matrix.multiply(vertexTransform, Matrix.translate(model.position.x, model.position.y, model.position.z)) 16 | vertexTransform = Matrix.multiply(vertexTransform, Matrix.rotate(model.rotation.z, 0, 0, 1)) 17 | vertexTransform = Matrix.multiply(vertexTransform, Matrix.rotate(model.rotation.y, 0, 1, 0)) 18 | vertexTransform = Matrix.multiply(vertexTransform, Matrix.rotate(model.rotation.x, 1, 0, 0)) 19 | vertexTransform = Matrix.multiply(vertexTransform, Matrix.scale(model.scale.x, model.scale.y, model.scale.z)) 20 | vertexTransform = Matrix.multiply(vertexTransform, Matrix.scale(bor.rescale, bor.rescale, bor.rescale)) 21 | vertexTransform = Matrix.multiply(vertexTransform, Matrix.translate(bor.offset.x, bor.offset.y, bor.offset.z)) 22 | 23 | // Convert the vertex transform matrix in a normal transform matrix 24 | let normalTransform = Matrix.inverse(vertexTransform) 25 | normalTransform = Matrix.transpose(normalTransform) 26 | 27 | // Now move all vertices to their new position and transform the average normals 28 | for (let vertIndex = 0, c = model.vertCount; vertIndex < c; vertIndex++) { 29 | vertexTransform.transformPointInline(vertX, vertY, vertZ, vertIndex) 30 | } 31 | 32 | normalXs[0] = faceVertNormalX 33 | normalYs[0] = faceVertNormalY 34 | normalZs[0] = faceVertNormalZ 35 | normalXs[1] = faceVertFlatNormalX 36 | normalYs[1] = faceVertFlatNormalY 37 | normalZs[1] = faceVertFlatNormalZ 38 | normalXs[2] = faceVertSmoothNormalX 39 | normalYs[2] = faceVertSmoothNormalY 40 | normalZs[2] = faceVertSmoothNormalZ 41 | normalXs[3] = faceVertBothNormalX 42 | normalYs[3] = faceVertBothNormalY 43 | normalZs[3] = faceVertBothNormalZ 44 | 45 | // Transform all normals 46 | for (let faceIndex = 0, c = model.faceCount; faceIndex < c; faceIndex++) { 47 | const faceOffset = faceIndex * 4 48 | 49 | for (let normalIndex = 0; normalIndex < 4; normalIndex++) { 50 | for (let normalType = 0, c = normalXs.length; normalType < c; normalType++) { 51 | const xs = normalXs[normalType] 52 | const ys = normalYs[normalType] 53 | const zs = normalZs[normalType] 54 | 55 | const idx = faceOffset + normalIndex 56 | normalTransform.transformVectorInline(xs, ys, zs, idx) 57 | 58 | // Normalize 59 | const normalX = xs[idx] 60 | const normalY = ys[idx] 61 | const normalZ = zs[idx] 62 | 63 | const normalLength = Math.sqrt(normalX * normalX + normalY * normalY + normalZ * normalZ) 64 | 65 | if (normalLength > 0) { 66 | const d = 1 / normalLength 67 | xs[idx] = normalX * d 68 | ys[idx] = normalY * d 69 | zs[idx] = normalZ * d 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/smoothvoxels/workerpool.js: -------------------------------------------------------------------------------- 1 | /* global event */ 2 | 3 | import Worker from './svox.worker.js' 4 | 5 | export default class WorkerPool { 6 | // workerfile: e.g. "/smoothvoxelworker.js" 7 | constructor (resultHandler, resultCallback) { 8 | this._resultHandler = resultHandler 9 | this._resultCallback = resultCallback 10 | this._nrOfWorkers = window.navigator.hardwareConcurrency 11 | this._workers = [] // The actual workers 12 | this._free = [] // Array of free worker indexes 13 | this._tasks = [] // Array of tasks to perform 14 | } 15 | 16 | executeTask (task) { 17 | // Create max nrOfWorkers web workers 18 | if (this._workers.length < this._nrOfWorkers) { 19 | // Create a new worker and mark it as free by adding its index to the free array 20 | const worker = new Worker() 21 | 22 | // On message handler 23 | const _this = this 24 | worker.onmessage = function (task) { 25 | // Mark the worker as free again, process the next task and process the result 26 | _this._free.push(event.data.worker) 27 | _this._processNextTask() 28 | _this._resultCallback.apply(_this._resultHandler, [event.data]) 29 | } 30 | 31 | this._free.push(this._workers.length) 32 | this._workers.push(worker) 33 | } 34 | 35 | this._tasks.push(task) 36 | 37 | this._processNextTask() 38 | } 39 | 40 | _processNextTask () { 41 | if (this._tasks.length > 0 && this._free.length > 0) { 42 | const task = this._tasks.shift() 43 | task.worker = this._free.shift() 44 | const worker = this._workers[task.worker] 45 | worker.postMessage(task) 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/vox-to-svox/IMAP.js: -------------------------------------------------------------------------------- 1 | export default function IMAPHandler (state, startIndex, endIndex) { 2 | const ret = {} 3 | ret.pal_indices = [] 4 | 5 | for (let i = 0; i < 256; i++) { 6 | ret.pal_indices.push(state.Buffer[state.readByteIndex++]) 7 | } 8 | 9 | return ret 10 | } 11 | -------------------------------------------------------------------------------- /src/vox-to-svox/LAYR.js: -------------------------------------------------------------------------------- 1 | import readDict from './read-dict.js' 2 | 3 | export default function LAYRHandler (state, startIndex, endIndex) { 4 | const ret = {} 5 | 6 | // node id 7 | ret.id = state.Buffer.readInt32LE(state.readByteIndex) 8 | state.readByteIndex += 4 9 | 10 | // DICT node attributes 11 | ret.attributes = readDict(state) 12 | 13 | ret.reserved_id = state.Buffer.readInt32LE(state.readByteIndex) 14 | console.assert(ret.reserved_id === -1, 'LAYR reserved_id must be -1') 15 | state.readByteIndex += 4 16 | 17 | return ret 18 | } 19 | -------------------------------------------------------------------------------- /src/vox-to-svox/MATL.js: -------------------------------------------------------------------------------- 1 | import readDict from './read-dict.js' 2 | 3 | export default function MATLHandler (state, startIndex, endIndex) { 4 | const ret = {} 5 | 6 | // node id 7 | ret.id = state.Buffer.readInt32LE(state.readByteIndex) 8 | state.readByteIndex += 4 9 | 10 | ret.properties = readDict(state) 11 | 12 | return ret 13 | }; 14 | -------------------------------------------------------------------------------- /src/vox-to-svox/MATT.js: -------------------------------------------------------------------------------- 1 | /* global totalEndIndex */ 2 | 3 | export default function MATTHandler (state, startIndex, endIndex) { 4 | const ret = {} 5 | 6 | ret.id = state.Buffer.readInt32LE(state.readByteIndex) 7 | state.readByteIndex += 4 8 | 9 | ret.materialType = state.Buffer.readInt32LE(state.readByteIndex) 10 | state.readByteIndex += 4 11 | 12 | ret.materialWeight = state.Buffer.readFloatLE(state.readByteIndex) 13 | state.readByteIndex += 4 14 | 15 | ret.propertyBits = state.Buffer.readInt32LE(state.readByteIndex) 16 | state.readByteIndex += 4 17 | 18 | ret.normalizedPropertyValues = [] 19 | while (state.readByteIndex < totalEndIndex) { 20 | ret.normalizedPropertyValues.push(state.Buffer.readFloatLE(state.readByteIndex)) 21 | state.readByteIndex += 4 22 | } 23 | 24 | return ret 25 | }; 26 | -------------------------------------------------------------------------------- /src/vox-to-svox/PACK.js: -------------------------------------------------------------------------------- 1 | export default function PACKHandler (state) { 2 | return state.Buffer.readInt32LE(state.readByteIndex) 3 | }; 4 | -------------------------------------------------------------------------------- /src/vox-to-svox/RGBA.js: -------------------------------------------------------------------------------- 1 | export default function RGBAHandler (state, startIndex, endIndex) { 2 | const colors = [] 3 | for (let n = 0; n < 256; n++) { 4 | colors[n] = { 5 | r: state.Buffer[state.readByteIndex++], 6 | g: state.Buffer[state.readByteIndex++], 7 | b: state.Buffer[state.readByteIndex++], 8 | a: state.Buffer[state.readByteIndex++] 9 | } 10 | } 11 | return colors 12 | }; 13 | -------------------------------------------------------------------------------- /src/vox-to-svox/SIZE.js: -------------------------------------------------------------------------------- 1 | export default function SIZEHandler (state, startIndex, endIndex) { 2 | const sizex = state.Buffer.readInt32LE(state.readByteIndex) 3 | state.readByteIndex += 4 4 | 5 | const sizey = state.Buffer.readInt32LE(state.readByteIndex) 6 | state.readByteIndex += 4 7 | 8 | const sizez = state.Buffer.readInt32LE(state.readByteIndex) 9 | state.readByteIndex += 4 10 | 11 | console.assert(state.readByteIndex === endIndex, "Chunk handler didn't reach end") 12 | 13 | return { 14 | x: sizex, 15 | y: sizey, 16 | z: sizez 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/vox-to-svox/SKIP.js: -------------------------------------------------------------------------------- 1 | // Skip this chunk. 2 | export default function SKIPHandler (state, startIndex, endIndex) { 3 | state.readByteIndex = endIndex 4 | return { error: 'Unsupported chunk type' } 5 | } 6 | -------------------------------------------------------------------------------- /src/vox-to-svox/XYZI.js: -------------------------------------------------------------------------------- 1 | export default function XYZIHandler (state, startIndex, endIndex) { 2 | const numVoxels = Math.abs(state.Buffer.readInt32LE(state.readByteIndex)) 3 | state.readByteIndex += 4 4 | 5 | const voxelData = [] 6 | for (let n = 0; n < numVoxels; n++) { 7 | voxelData[n] = { 8 | x: state.Buffer[state.readByteIndex++] & 0xFF, 9 | y: state.Buffer[state.readByteIndex++] & 0xFF, 10 | z: state.Buffer[state.readByteIndex++] & 0xFF, 11 | c: state.Buffer[state.readByteIndex++] & 0xFF // color index in RGBA 12 | } 13 | } 14 | 15 | console.assert(state.readByteIndex === endIndex, 'XYZI chunk did not fully read') 16 | return voxelData 17 | }; 18 | -------------------------------------------------------------------------------- /src/vox-to-svox/constants.js: -------------------------------------------------------------------------------- 1 | export const intByteLength = 4 2 | -------------------------------------------------------------------------------- /src/vox-to-svox/getChunkData.js: -------------------------------------------------------------------------------- 1 | import SIZEHandler from './SIZE' 2 | import XYZIHandler from './XYZI' 3 | import RGBAHandler from './RGBA' 4 | import PACKHandler from './PACK' 5 | import MATTHandler from './MATT' 6 | import nTRNHandler from './nTRN' 7 | import nGRPHandler from './nGRP' 8 | import nSHPHandler from './nSHP' 9 | import LAYRHandler from './LAYR' 10 | import MATLHandler from './MATL' 11 | import rOBJHandler from './rOBJ' 12 | import IMAPHandler from './IMAP' 13 | import SKIPHandler from './SKIP' 14 | 15 | const chunkHandlers = { 16 | SIZE: SIZEHandler, 17 | XYZI: XYZIHandler, 18 | RGBA: RGBAHandler, 19 | PACK: PACKHandler, 20 | MATT: MATTHandler, 21 | nTRN: nTRNHandler, 22 | nGRP: nGRPHandler, 23 | nSHP: nSHPHandler, 24 | LAYR: LAYRHandler, 25 | MATL: MATLHandler, 26 | rOBJ: rOBJHandler, 27 | IMAP: IMAPHandler 28 | } 29 | 30 | export default function getChunkData (state, id, startIndex, endIndex) { 31 | if (!chunkHandlers[id]) { 32 | console.log('Unsupported chunk type ' + id) 33 | return SKIPHandler(state, startIndex, endIndex) 34 | } 35 | return chunkHandlers[id](state, startIndex, endIndex) 36 | }; 37 | -------------------------------------------------------------------------------- /src/vox-to-svox/index.js: -------------------------------------------------------------------------------- 1 | import { intByteLength } from './constants' 2 | import recReadChunksInRange from './recReadChunksInRange' 3 | import readId from './readId' 4 | import useDefaultPalette from './useDefaultPalette' 5 | import { Buffer as BrowserBuffer } from 'buffer' 6 | import Voxels, { shiftForSize, voxColorForRGBT } from '../smoothvoxels/voxels' 7 | import { MATSTANDARD, FLAT } from '../smoothvoxels/constants' 8 | import Model from '../smoothvoxels/model' 9 | 10 | const BufferImpl = typeof (Buffer) !== 'undefined' ? Buffer : BrowserBuffer 11 | 12 | function parseHeader (buffer) { 13 | const ret = {} 14 | const state = { 15 | Buffer: buffer, 16 | readByteIndex: 0 17 | } 18 | ret[readId(state)] = buffer.readInt32LE(intByteLength) 19 | return ret 20 | }; 21 | 22 | function parseMagicaVoxel (BufferLikeData) { 23 | let buffer = BufferLikeData 24 | buffer = BufferImpl.from(new Uint8Array(BufferLikeData)) // eslint-disable-line 25 | 26 | const header = parseHeader(buffer) 27 | const body = recReadChunksInRange( 28 | buffer, 29 | 8, // start on the 8th byte as the header dosen't follow RIFF pattern. 30 | buffer.length, 31 | header 32 | ) 33 | 34 | if (!body.RGBA) { 35 | body.RGBA = useDefaultPalette() 36 | } 37 | 38 | return Object.assign(header, body) 39 | }; 40 | 41 | export default function (bufferData, model = null) { 42 | const vox = parseMagicaVoxel(bufferData) 43 | 44 | // Palette map (since palette indices can be moved in Magica Voxel by CTRL-Drag) 45 | const iMap = [] 46 | if (vox.IMAP) { 47 | for (let i = 1; i <= vox.IMAP.pal_indices.length; i++) { 48 | iMap[vox.IMAP.pal_indices[i - 1]] = i 49 | } 50 | } 51 | 52 | let minX = Infinity; let minY = Infinity; let minZ = Infinity 53 | let maxX = -Infinity; let maxY = -Infinity; let maxZ = -Infinity 54 | 55 | // Only use first xyzi chunk 56 | const xyziChunk = Array.isArray(vox.XYZI[0]) ? vox.XYZI[0] : vox.XYZI 57 | 58 | xyziChunk.forEach(function (v) { 59 | const { x, y, z } = v 60 | minX = Math.min(minX, x) 61 | minY = Math.min(minY, y) 62 | minZ = Math.min(minZ, z) 63 | maxX = Math.max(maxX, x) 64 | maxY = Math.max(maxY, y) 65 | maxZ = Math.max(maxZ, z) 66 | }) 67 | 68 | const voxSizeX = maxX - minX + 1 69 | const voxSizeY = maxY - minY + 1 70 | const voxSizeZ = maxZ - minZ + 1 71 | 72 | if (model === null) { 73 | model = new Model() 74 | model.origin = '-y' 75 | model.scale = { x: 0.05, y: 0.05, z: 0.05 } 76 | model.size = { x: voxSizeX, y: voxSizeZ, z: voxSizeY } 77 | 78 | let paletteBits = 1 79 | 80 | const seenColors = new Set() 81 | 82 | xyziChunk.forEach(({ c }) => seenColors.add(c)) 83 | 84 | const numberOfColors = seenColors.size 85 | if (numberOfColors >= 2) { paletteBits = 2 } 86 | if (numberOfColors >= 4) { paletteBits = 4 } 87 | if (numberOfColors >= 16) { paletteBits = 8 } 88 | 89 | model.voxels = new Voxels([model.size.x, model.size.y, model.size.z], paletteBits) 90 | } 91 | // Alpha channel is unused(?) in Magica voxel, so just use the same material for all 92 | // If all colors are already available this new material will have no colors and not be written by the modelwriter 93 | const newMaterial = model.materials.createMaterial(MATSTANDARD, FLAT) 94 | const newMaterialIndex = model.materials.materials.indexOf(newMaterial) 95 | 96 | const shiftX = shiftForSize(model.size.x) 97 | const shiftY = shiftForSize(model.size.y) 98 | const shiftZ = shiftForSize(model.size.z) 99 | 100 | const voxels = model.voxels 101 | const RGBA = vox.RGBA 102 | 103 | const hz = Math.floor(voxSizeY / 2) 104 | 105 | xyziChunk.forEach(function (v) { 106 | let { x: vx, y: vz, z: vy, c } = v 107 | // ?? not sure why this is needed but objects come in mirrored 108 | vz = -(vz - minY - hz) + hz + minY 109 | 110 | const { r, g, b } = RGBA[c - 1] 111 | const svoxColor = voxColorForRGBT(r, g, b, newMaterialIndex) 112 | 113 | voxels.setColorAt(vx - shiftX - minX, vy - shiftY - minZ, vz - shiftZ - minY, svoxColor) 114 | }) 115 | 116 | return model 117 | } 118 | -------------------------------------------------------------------------------- /src/vox-to-svox/nGRP.js: -------------------------------------------------------------------------------- 1 | import readDict from './read-dict.js' 2 | 3 | export default function nGRPHandler (state, startIndex, endIndex) { 4 | const ret = {} 5 | 6 | // node id 7 | ret.id = state.Buffer.readInt32LE(state.readByteIndex) 8 | state.readByteIndex += 4 9 | 10 | // DICT node attributes 11 | ret.attributes = readDict(state) 12 | 13 | ret.num_of_children = state.Buffer.readInt32LE(state.readByteIndex) 14 | state.readByteIndex += 4 15 | 16 | ret.child_ids = [] 17 | for (let i = 0; i < ret.num_of_children; i++) { 18 | ret.child_ids.push(state.Buffer.readInt32LE(state.readByteIndex)) 19 | state.readByteIndex += 4 20 | } 21 | 22 | console.assert(state.readByteIndex === endIndex, `nGRP chunk length mismatch: ${state.readByteIndex} ${endIndex}`) 23 | return ret 24 | } 25 | -------------------------------------------------------------------------------- /src/vox-to-svox/nSHP.js: -------------------------------------------------------------------------------- 1 | import readDict from './read-dict.js' 2 | 3 | export default function nSHPHandler (state, startIndex, endIndex) { 4 | const ret = {} 5 | 6 | // node id 7 | ret.id = state.Buffer.readInt32LE(state.readByteIndex) 8 | state.readByteIndex += 4 9 | 10 | // DICT node attributes 11 | ret.attributes = readDict(state) 12 | 13 | ret.num_of_models = state.Buffer.readInt32LE(state.readByteIndex) 14 | console.assert(ret.num_of_models >= 1, 'nSHP num of models must be 1') 15 | state.readByteIndex += 4 16 | 17 | ret.models = [] 18 | for (let i = 0; i < ret.num_of_models; i++) { 19 | const model = {} 20 | model.id = state.Buffer.readInt32LE(state.readByteIndex) 21 | state.readByteIndex += 4 22 | 23 | // supposed to be a DICT here but marked as reserved in docs 24 | // https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox-extension.txt#L103 25 | // might not be valid 26 | model.attributes = readDict(state) 27 | 28 | ret.models.push(model) 29 | } 30 | 31 | console.assert(state.readByteIndex === endIndex, `nSHP chunk length mismatch: ${state.readByteIndex} ${endIndex}`) 32 | return ret 33 | } 34 | -------------------------------------------------------------------------------- /src/vox-to-svox/nTRN.js: -------------------------------------------------------------------------------- 1 | import readDict from './read-dict.js' 2 | 3 | export default function nTRNHandler (state, startIndex, endIndex) { 4 | const ret = {} 5 | 6 | // node id 7 | ret.node_id = state.Buffer.readInt32LE(state.readByteIndex) 8 | state.readByteIndex += 4 9 | 10 | // DICT node attributes 11 | ret.attributes = readDict(state) 12 | // child node id 13 | ret.child_id = state.Buffer.readInt32LE(state.readByteIndex) 14 | state.readByteIndex += 4 15 | 16 | // reserved id 17 | ret.reserved_id = state.Buffer.readInt32LE(state.readByteIndex) 18 | console.assert(ret.reserved_id === -1, 'reserved id must be -1') 19 | state.readByteIndex += 4 20 | 21 | // layer id 22 | ret.layer_id = state.Buffer.readInt32LE(state.readByteIndex) 23 | state.readByteIndex += 4 24 | 25 | // num of frames 26 | ret.num_of_frames = state.Buffer.readInt32LE(state.readByteIndex) 27 | console.assert(ret.num_of_frames >= 1, 'num frames must be 1') 28 | state.readByteIndex += 4 29 | 30 | ret.frame_transforms = [] 31 | for (let i = 0; i < ret.num_of_frames; i++) { 32 | ret.frame_transforms.push(readDict(state)) 33 | } 34 | 35 | console.assert(state.readByteIndex === endIndex, `nTRN chunk length mismatch: ${state.readByteIndex} ${endIndex}`) 36 | 37 | return ret 38 | }; 39 | -------------------------------------------------------------------------------- /src/vox-to-svox/rOBJ.js: -------------------------------------------------------------------------------- 1 | import readDict from './read-dict.js' 2 | 3 | export default function rOBJHandler (state, startIndex, endIndex) { 4 | let ret = {} 5 | 6 | // DICT node attributes 7 | ret = readDict(state) 8 | return ret 9 | } 10 | -------------------------------------------------------------------------------- /src/vox-to-svox/read-dict.js: -------------------------------------------------------------------------------- 1 | export default function readDict (state) { 2 | const ret = {} 3 | 4 | const attributePairLength = state.Buffer.readInt32LE(state.readByteIndex) 5 | state.readByteIndex += 4 6 | 7 | // Read all the key value pairs of the DICT 8 | for (let i = 0; i < attributePairLength; i++) { 9 | const keyByteLength = state.Buffer.readInt32LE(state.readByteIndex) 10 | state.readByteIndex += 4 11 | 12 | const key = state.Buffer.readInt8(keyByteLength) 13 | state.readByteIndex += 1 * keyByteLength 14 | 15 | const valueByteLength = state.Buffer.readInt32LE(state.readByteIndex) 16 | state.readByteIndex += 4 17 | 18 | const value = state.Buffer.readInt8(valueByteLength) 19 | state.readByteIndex += 1 * valueByteLength 20 | 21 | ret[key] = value 22 | } 23 | 24 | return ret 25 | } 26 | -------------------------------------------------------------------------------- /src/vox-to-svox/readId.js: -------------------------------------------------------------------------------- 1 | export default function readId (state) { 2 | const id = String.fromCharCode(parseInt(state.Buffer[state.readByteIndex++])) + 3 | String.fromCharCode(parseInt(state.Buffer[state.readByteIndex++])) + 4 | String.fromCharCode(parseInt(state.Buffer[state.readByteIndex++])) + 5 | String.fromCharCode(parseInt(state.Buffer[state.readByteIndex++])) 6 | 7 | return id 8 | }; 9 | -------------------------------------------------------------------------------- /src/vox-to-svox/recReadChunksInRange.js: -------------------------------------------------------------------------------- 1 | import readId from './readId' 2 | import { intByteLength } from './constants' 3 | import getChunkData from './getChunkData' 4 | 5 | export default function recReadChunksInRange (Buffer, bufferStartIndex, bufferEndIndex, accum) { 6 | const state = { 7 | Buffer, 8 | readByteIndex: bufferStartIndex 9 | } 10 | 11 | const id = readId(state, bufferStartIndex) 12 | 13 | const chunkContentByteLength = Buffer.readInt32LE(state.readByteIndex) 14 | state.readByteIndex += intByteLength 15 | 16 | const childContentByteLength = Buffer.readInt32LE(state.readByteIndex) 17 | state.readByteIndex += intByteLength 18 | 19 | const definitionEndIndex = state.readByteIndex 20 | const contentByteLength = chunkContentByteLength 21 | const totalChunkEndIndex = state.readByteIndex + chunkContentByteLength + childContentByteLength 22 | 23 | if (contentByteLength === 0 && childContentByteLength === 0) { 24 | console.log('no content or children for', id) 25 | return accum 26 | } 27 | 28 | if (contentByteLength && id) { 29 | const chunkContent = getChunkData(state, id, definitionEndIndex, totalChunkEndIndex) 30 | console.assert(state.readByteIndex === totalChunkEndIndex, `${id} length mismatch ${state.readByteIndex}:${totalChunkEndIndex}`) 31 | if (!accum[id]) { 32 | accum[id] = chunkContent 33 | } else if (accum[id] && !accum[id][0]?.length) { 34 | accum[id] = [accum[id], chunkContent] 35 | } else if (accum[id] && accum[id][0]?.length) { 36 | accum[id].push(chunkContent) 37 | } 38 | } 39 | 40 | // read children 41 | if (childContentByteLength > 0) { 42 | return recReadChunksInRange(Buffer, 43 | definitionEndIndex + contentByteLength, 44 | bufferEndIndex, 45 | {}) 46 | } 47 | 48 | // accumulate siblings 49 | if (totalChunkEndIndex !== bufferEndIndex) { 50 | return recReadChunksInRange(Buffer, 51 | totalChunkEndIndex, 52 | bufferEndIndex, 53 | accum) 54 | } 55 | 56 | return accum 57 | }; 58 | -------------------------------------------------------------------------------- /src/vox-to-svox/useDefaultPalette.js: -------------------------------------------------------------------------------- 1 | const defaultPalette = [ 2 | 0x000000, 0xffffff, 0xccffff, 0x99ffff, 0x66ffff, 0x33ffff, 0x00ffff, 0xffccff, 0xccccff, 0x99ccff, 0x66ccff, 0x33ccff, 0x00ccff, 0xff99ff, 0xcc99ff, 0x9999ff, 3 | 0x6699ff, 0x3399ff, 0x0099ff, 0xff66ff, 0xcc66ff, 0x9966ff, 0x6666ff, 0x3366ff, 0x0066ff, 0xff33ff, 0xcc33ff, 0x9933ff, 0x6633ff, 0x3333ff, 0x0033ff, 0xff00ff, 4 | 0xcc00ff, 0x9900ff, 0x6600ff, 0x3300ff, 0x0000ff, 0xffffcc, 0xccffcc, 0x99ffcc, 0x66ffcc, 0x33ffcc, 0x00ffcc, 0xffcccc, 0xcccccc, 0x99cccc, 0x66cccc, 0x33cccc, 5 | 0x00cccc, 0xff99cc, 0xcc99cc, 0x9999cc, 0x6699cc, 0x3399cc, 0x0099cc, 0xff66cc, 0xcc66cc, 0x9966cc, 0x6666cc, 0x3366cc, 0x0066cc, 0xff33cc, 0xcc33cc, 0x9933cc, 6 | 0x6633cc, 0x3333cc, 0x0033cc, 0xff00cc, 0xcc00cc, 0x9900cc, 0x6600cc, 0x3300cc, 0x0000cc, 0xffff99, 0xccff99, 0x99ff99, 0x66ff99, 0x33ff99, 0x00ff99, 0xffcc99, 7 | 0xcccc99, 0x99cc99, 0x66cc99, 0x33cc99, 0x00cc99, 0xff9999, 0xcc9999, 0x999999, 0x669999, 0x339999, 0x009999, 0xff6699, 0xcc6699, 0x996699, 0x666699, 0x336699, 8 | 0x006699, 0xff3399, 0xcc3399, 0x993399, 0x663399, 0x333399, 0x003399, 0xff0099, 0xcc0099, 0x990099, 0x660099, 0x330099, 0x000099, 0xffff66, 0xccff66, 0x99ff66, 9 | 0x66ff66, 0x33ff66, 0x00ff66, 0xffcc66, 0xcccc66, 0x99cc66, 0x66cc66, 0x33cc66, 0x00cc66, 0xff9966, 0xcc9966, 0x999966, 0x669966, 0x339966, 0x009966, 0xff6666, 10 | 0xcc6666, 0x996666, 0x666666, 0x336666, 0x006666, 0xff3366, 0xcc3366, 0x993366, 0x663366, 0x333366, 0x003366, 0xff0066, 0xcc0066, 0x990066, 0x660066, 0x330066, 11 | 0x000066, 0xffff33, 0xccff33, 0x99ff33, 0x66ff33, 0x33ff33, 0x00ff33, 0xffcc33, 0xcccc33, 0x99cc33, 0x66cc33, 0x33cc33, 0x00cc33, 0xff9933, 0xcc9933, 0x999933, 12 | 0x669933, 0x339933, 0x009933, 0xff6633, 0xcc6633, 0x996633, 0x666633, 0x336633, 0x006633, 0xff3333, 0xcc3333, 0x993333, 0x663333, 0x333333, 0x003333, 0xff0033, 13 | 0xcc0033, 0x990033, 0x660033, 0x330033, 0x000033, 0xffff00, 0xccff00, 0x99ff00, 0x66ff00, 0x33ff00, 0x00ff00, 0xffcc00, 0xcccc00, 0x99cc00, 0x66cc00, 0x33cc00, 14 | 0x00cc00, 0xff9900, 0xcc9900, 0x999900, 0x669900, 0x339900, 0x009900, 0xff6600, 0xcc6600, 0x996600, 0x666600, 0x336600, 0x006600, 0xff3300, 0xcc3300, 0x993300, 15 | 0x663300, 0x333300, 0x003300, 0xff0000, 0xcc0000, 0x990000, 0x660000, 0x330000, 0x0000ee, 0x0000dd, 0x0000bb, 0x0000aa, 0x000088, 0x000077, 0x000055, 0x000044, 16 | 0x000022, 0x000011, 0x00ee00, 0x00dd00, 0x00bb00, 0x00aa00, 0x008800, 0x007700, 0x005500, 0x004400, 0x002200, 0x001100, 0xee0000, 0xdd0000, 0xbb0000, 0xaa0000, 17 | 0x880000, 0x770000, 0x550000, 0x440000, 0x220000, 0x110000, 0xeeeeee, 0xdddddd, 0xbbbbbb, 0xaaaaaa, 0x888888, 0x777777, 0x555555, 0x444444, 0x222222, 0x111111 18 | ] 19 | 20 | export default function useDefaultPalette () { 21 | const colors = defaultPalette.map(function (hex) { 22 | return { 23 | b: (hex & 0xff0000) >> 16, 24 | g: (hex & 0x00ff00) >> 8, 25 | r: (hex & 0x0000ff), 26 | a: 1 27 | } 28 | }) 29 | 30 | return colors 31 | }; 32 | -------------------------------------------------------------------------------- /three.js: -------------------------------------------------------------------------------- 1 | import SvoxBufferGeometry from './src/smoothvoxels/svoxbuffergeometry' 2 | import SvoxToThreeMeshConverter from './src/smoothvoxels/svoxtothreemeshconverter' 3 | 4 | export { 5 | SvoxToThreeMeshConverter, 6 | SvoxBufferGeometry 7 | } 8 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | import WorkerPool from './src/smoothvoxels/workerpool' 2 | 3 | export { 4 | WorkerPool 5 | } 6 | --------------------------------------------------------------------------------