├── .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 | 
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 |
--------------------------------------------------------------------------------