├── .eslintrc.js ├── .gitignore ├── README.md ├── nuxt.config.js ├── package-lock.json ├── package.json ├── preview └── 0.gif └── src ├── assets └── scss │ └── styles.scss ├── components ├── Editor │ ├── Editor.options.vue │ └── Editor.vue └── Scene │ ├── Scene.options.vue │ ├── Scene.vue │ └── js │ └── Scene.init.js ├── layouts └── default.vue ├── pages └── index.vue ├── plugins └── sayHello.js ├── static ├── favicon.ico └── model │ ├── scene.bin │ ├── scene.gltf │ └── textures │ ├── initialShadingGroup_baseColor.jpeg │ ├── initialShadingGroup_metallicRoughness.png │ └── initialShadingGroup_normal.png └── utils └── index.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | extends: [ 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | 'plugin:vue/essential' 14 | ], 15 | // required to lint *.vue files 16 | plugins: [ 17 | 'vue' 18 | ], 19 | // add your custom rules here 20 | rules: {} 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3D Headphones Configurator 2 | 3 | 3D Headphones Configurator with THREE.js. 4 | 5 | Medium tutorial: https://medium.com/p/6d40da0209e0/ 6 | 7 | Just upload your model to the static/model folder and enjoy. 8 | 9 | ![](preview/0.gif) 10 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | 3 | const routerBase = process.env.DEPLOY_ENV === 'GH_PAGES' 4 | ? { 5 | router: { 6 | base: '/3d-headphones/' 7 | } 8 | } 9 | : {}; 10 | 11 | export default { 12 | mode: 'spa', 13 | /* 14 | ** Headers of the page 15 | */ 16 | head: { 17 | title: '3D Headphones Configurator', 18 | meta: [ 19 | { charset: 'utf-8' }, 20 | { 21 | name: 'viewport', 22 | content: 'width=device-width, initial-scale=1' 23 | }, 24 | { 25 | hid: 'description', 26 | name: 'description', 27 | content: process.env.npm_package_description || '' 28 | } 29 | ], 30 | link: [{ rel: 'icon', type: 'image/x-icon', href: 'favicon.ico' }] 31 | }, 32 | ...routerBase, 33 | /* 34 | ** Source directory 35 | */ 36 | srcDir: 'src', 37 | /* 38 | ** Customize the progress-bar color 39 | */ 40 | loading: { color: '#000000' }, 41 | /* 42 | ** Global CSS 43 | */ 44 | css: ['~/assets/scss/styles.scss'], 45 | /* 46 | ** Plugins to load before mounting the App 47 | */ 48 | plugins: [{ src: '~/plugins/sayHello', ssr: false }], 49 | /* 50 | ** Nuxt.js dev-modules 51 | */ 52 | buildModules: [], 53 | /* 54 | ** Nuxt.js modules 55 | */ 56 | modules: [ 57 | '@nuxtjs/vuetify', 58 | [ 59 | 'nuxt-compress', 60 | { 61 | gzip: { 62 | cache: true 63 | }, 64 | brotli: { 65 | threshold: 10240 66 | } 67 | } 68 | ] 69 | ], 70 | /* 71 | ** Build configuration 72 | */ 73 | build: { 74 | /* 75 | ** You can extend webpack config here 76 | */ 77 | extend(config, ctx) { 78 | config.plugins.push(new webpack.ProvidePlugin({ 79 | THREE: 'three' 80 | })); 81 | 82 | // Run ESLint on save 83 | if (ctx.isDev && ctx.isClient) { 84 | config.module.rules.push({ 85 | enforce: 'pre', 86 | test: /\.(js|vue)$/, 87 | loader: 'eslint-loader', 88 | exclude: /(node_modules)/ 89 | // options: { 90 | // fix: true 91 | // } 92 | }); 93 | } 94 | } 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3d-headphones-configurator", 3 | "version": "1.0.0", 4 | "description": "3D Headphones Configurator with Three.js", 5 | "author": "osorin ", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/iosorin/3d-headphones" 9 | }, 10 | "keywords": [ 11 | "3d nuxt", 12 | "configurator", 13 | "3d configurator", 14 | "3d configurator nuxt", 15 | "nuxt", 16 | "vuetify", 17 | "threejs" 18 | ], 19 | "private": true, 20 | "scripts": { 21 | "dev": "nuxt", 22 | "build": "nuxt build", 23 | "start": "nuxt start", 24 | "generate": "cross-env DEPLOY_ENV=GH_PAGES nuxt generate", 25 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 26 | "deploy": "push-dir --dir=dist --branch=gh-pages --cleanup", 27 | "precommit": "npm run lint" 28 | }, 29 | "dependencies": { 30 | "@nuxtjs/vuetify": "^1.11.2", 31 | "nuxt": "^2.0.0", 32 | "three": "^0.116.1" 33 | }, 34 | "devDependencies": { 35 | "@babel/cli": "^7.8.4", 36 | "babel-eslint": "^10.1.0", 37 | "cross-env": "^7.0.2", 38 | "eslint": "^6.8.0", 39 | "eslint-config-airbnb": "^18.1.0", 40 | "eslint-friendly-formatter": "^4.0.1", 41 | "eslint-import-resolver-alias": "^1.1.2", 42 | "eslint-loader": "^2.1.1", 43 | "eslint-plugin-html": "^6.0.2", 44 | "eslint-plugin-import": "^2.20.2", 45 | "eslint-plugin-vue": "^6.2.2", 46 | "nuxt-compress": "^1.1.0", 47 | "push-dir": "^0.4.1" 48 | } 49 | } -------------------------------------------------------------------------------- /preview/0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iosorin/3d-headphones/69708311de511341dac1a6aa22ed1e82efa38080/preview/0.gif -------------------------------------------------------------------------------- /src/assets/scss/styles.scss: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100vh; 3 | overflow: hidden !important; 4 | } 5 | 6 | :focus { 7 | outline:none; 8 | border: none; 9 | -webkit-box-shadow: none; 10 | box-shadow: none; 11 | } 12 | 13 | .scene { 14 | cursor: grab; 15 | 16 | &:active { 17 | cursor: grabbing; 18 | } 19 | } 20 | 21 | .coverdiv { 22 | position: absolute; 23 | width: 100%; 24 | height: 100%; 25 | top: 0; 26 | left: 0; 27 | } 28 | 29 | .options { 30 | display: flex; 31 | position: absolute; 32 | bottom: 12px; 33 | left: 14px; 34 | right: 14px; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.options.vue: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 120 | 121 | 132 | -------------------------------------------------------------------------------- /src/components/Scene/Scene.options.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 83 | -------------------------------------------------------------------------------- /src/components/Scene/Scene.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 197 | -------------------------------------------------------------------------------- /src/components/Scene/js/Scene.init.js: -------------------------------------------------------------------------------- 1 | import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | 4 | class SceneInit { 5 | constructor({ rootEl, autoRotate }) { 6 | this.canvas = document.createElement('canvas'); 7 | 8 | this.root = rootEl; 9 | this.width = rootEl.clientWidth; 10 | this.height = rootEl.clientHeight; 11 | 12 | this.background = 0xEEEEEE; 13 | 14 | this.autoRotate = autoRotate; 15 | 16 | this.init(); 17 | this.update(); 18 | this.bindEvents(); 19 | } 20 | 21 | init() { 22 | this.initScene(); 23 | this.initLights(); 24 | this.initCamera(); 25 | this.initRenderer(); 26 | this.initControls(); 27 | 28 | this.root.appendChild(this.canvas); 29 | } 30 | 31 | initScene() { 32 | this.scene = new THREE.Scene(); 33 | } 34 | 35 | initLights() { 36 | const ambient = new THREE.AmbientLight(0xFFFFFF, 0.9); 37 | const point = new THREE.PointLight(0xCCCCCC, 0.1, 10); 38 | const directional = new THREE.DirectionalLight(0xFFFFFF, 0.5); 39 | 40 | this.scene.add(ambient); 41 | this.scene.add(point); 42 | this.scene.add(directional); 43 | } 44 | 45 | initCamera() { 46 | const aspect = this.width / this.height; 47 | 48 | this.camera = new THREE.PerspectiveCamera( 49 | 45, 50 | aspect, 51 | 1, 52 | 1000 53 | ); 54 | 55 | this.camera.position.z = 15; 56 | this.camera.aspect = aspect; 57 | this.camera.updateProjectionMatrix(); 58 | } 59 | 60 | initRenderer() { 61 | this.renderer = new THREE.WebGLRenderer({ antialias: true }); 62 | this.renderer.setSize(this.width, this.height); 63 | this.renderer.setClearColor(this.background, 1); 64 | 65 | this.canvas = this.renderer.domElement; 66 | } 67 | 68 | initControls() { 69 | this.controls = new OrbitControls( 70 | this.camera, 71 | this.canvas 72 | ); 73 | 74 | this.controls.minPolarAngle = (Math.PI * 1) / 6; 75 | this.controls.maxPolarAngle = (Math.PI * 3) / 4; 76 | 77 | this.controls.smooth = true; 78 | this.controls.smoothspeed = 0.95; 79 | this.controls.autoRotateSpeed = 2; 80 | this.controls.maxDistance = 20; 81 | this.controls.minDistance = 12; 82 | 83 | this.controls.update(); 84 | } 85 | 86 | render() { 87 | this.camera.lookAt(this.scene.position); 88 | 89 | this.renderer.render(this.scene, this.camera); 90 | } 91 | 92 | update() { 93 | requestAnimationFrame(() => this.update()); 94 | 95 | this.controls.autoRotate = this.autoRotate; 96 | 97 | this.controls.update(); 98 | 99 | this.render(); 100 | } 101 | 102 | loadModel(model, callback) { 103 | this.loader = new GLTFLoader(); 104 | 105 | this.loader.load(model, (gltf) => { 106 | if (typeof callback === 'function') { 107 | callback(gltf.scene); 108 | } 109 | 110 | this.scene.add(gltf.scene); 111 | }); 112 | } 113 | 114 | add(model) { 115 | this.scene.add(model); 116 | } 117 | 118 | remove(objName) { 119 | const object = this.scene.getObjectByName(objName); 120 | 121 | if (object) { 122 | this.scene.remove(object); 123 | } 124 | } 125 | 126 | onResize() { 127 | this.width = this.root.clientWidth; 128 | this.height = this.root.clientHeight; 129 | 130 | this.renderer.setSize(this.width, this.height); 131 | 132 | this.camera.aspect = this.width / this.height; 133 | this.camera.updateProjectionMatrix(); 134 | } 135 | 136 | bindEvents() { 137 | window.addEventListener('resize', () => this.onResize()); 138 | } 139 | } 140 | 141 | // To call our class as a function 142 | const sceneInit = args => new SceneInit(args); 143 | 144 | export default sceneInit; 145 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 47 | 48 | 66 | -------------------------------------------------------------------------------- /src/plugins/sayHello.js: -------------------------------------------------------------------------------- 1 | const sayHello = () => { 2 | if (window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1) { 3 | const args = ['\n %c Made with ❤️ by Mir Osorin 🚀 %c http://iosorin.github.io/ \n', 'border: 1px solid #000;color: #fff; background: #171717; padding:5px 0;', 'color: #fff; background: #1c1c1c; padding:5px 0;']; 4 | window.console.log.apply(console, args); 5 | } 6 | else if (window.console) { 7 | window.console.log('Made with love ❤️ by Mir Osorin 🚀 - http://iosorin.github.io/'); 8 | } 9 | }; 10 | 11 | export default sayHello; 12 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iosorin/3d-headphones/69708311de511341dac1a6aa22ed1e82efa38080/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/model/scene.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iosorin/3d-headphones/69708311de511341dac1a6aa22ed1e82efa38080/src/static/model/scene.bin -------------------------------------------------------------------------------- /src/static/model/scene.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 2, 5 | "componentType": 5126, 6 | "count": 2892, 7 | "max": [ 8 | 3.7867729663848877, 9 | 1.1773710250854492, 10 | 1.7257610559463501 11 | ], 12 | "min": [ 13 | -3.7867729663848877, 14 | -1.1768310070037842, 15 | -3.979935884475708 16 | ], 17 | "type": "VEC3" 18 | }, 19 | { 20 | "bufferView": 2, 21 | "byteOffset": 34704, 22 | "componentType": 5126, 23 | "count": 2892, 24 | "max": [ 25 | 1, 26 | 1, 27 | 1 28 | ], 29 | "min": [ 30 | -1, 31 | -1, 32 | -1 33 | ], 34 | "type": "VEC3" 35 | }, 36 | { 37 | "bufferView": 3, 38 | "componentType": 5126, 39 | "count": 2892, 40 | "max": [ 41 | 1, 42 | 1, 43 | 1, 44 | 1 45 | ], 46 | "min": [ 47 | -1, 48 | -1, 49 | -1, 50 | 1 51 | ], 52 | "type": "VEC4" 53 | }, 54 | { 55 | "bufferView": 1, 56 | "componentType": 5126, 57 | "count": 2892, 58 | "max": [ 59 | 0.99082297086715698, 60 | 0.99014002084732056 61 | ], 62 | "min": [ 63 | 0.0055100000463426113, 64 | 0.0055100000463426113 65 | ], 66 | "type": "VEC2" 67 | }, 68 | { 69 | "bufferView": 0, 70 | "componentType": 5125, 71 | "count": 14688, 72 | "max": [ 73 | 2891 74 | ], 75 | "min": [ 76 | 0 77 | ], 78 | "type": "SCALAR" 79 | }, 80 | { 81 | "bufferView": 2, 82 | "byteOffset": 69408, 83 | "componentType": 5126, 84 | "count": 2364, 85 | "max": [ 86 | 3.5273680686950684, 87 | 1.1143029928207397, 88 | 3.1037919521331787 89 | ], 90 | "min": [ 91 | -3.5273680686950684, 92 | -1.1137650012969971, 93 | -3.9888889789581299 94 | ], 95 | "type": "VEC3" 96 | }, 97 | { 98 | "bufferView": 2, 99 | "byteOffset": 97776, 100 | "componentType": 5126, 101 | "count": 2364, 102 | "max": [ 103 | 0.99943065643310547, 104 | 0.99930101633071899, 105 | 0.99061721563339233 106 | ], 107 | "min": [ 108 | -0.99943035840988159, 109 | -0.99930047988891602, 110 | -0.99923747777938843 111 | ], 112 | "type": "VEC3" 113 | }, 114 | { 115 | "bufferView": 3, 116 | "byteOffset": 46272, 117 | "componentType": 5126, 118 | "count": 2364, 119 | "max": [ 120 | 0.99795985221862793, 121 | 0.99928855895996094, 122 | 0.99998444318771362, 123 | 1 124 | ], 125 | "min": [ 126 | -0.99761378765106201, 127 | -1, 128 | -0.97841787338256836, 129 | 1 130 | ], 131 | "type": "VEC4" 132 | }, 133 | { 134 | "bufferView": 1, 135 | "byteOffset": 23136, 136 | "componentType": 5126, 137 | "count": 2364, 138 | "max": [ 139 | 0.9122849702835083, 140 | 0.91572397947311401 141 | ], 142 | "min": [ 143 | 0.050094000995159149, 144 | 0.1570730060338974 145 | ], 146 | "type": "VEC2" 147 | }, 148 | { 149 | "bufferView": 0, 150 | "byteOffset": 58752, 151 | "componentType": 5125, 152 | "count": 12960, 153 | "max": [ 154 | 2363 155 | ], 156 | "min": [ 157 | 0 158 | ], 159 | "type": "SCALAR" 160 | }, 161 | { 162 | "bufferView": 2, 163 | "byteOffset": 126144, 164 | "componentType": 5126, 165 | "count": 2755, 166 | "max": [ 167 | 3.8927350044250488, 168 | 1.5935790538787842, 169 | 1.176954984664917 170 | ], 171 | "min": [ 172 | -3.1193389892578125, 173 | -1.5930379629135132, 174 | -4.324364185333252 175 | ], 176 | "type": "VEC3" 177 | }, 178 | { 179 | "bufferView": 2, 180 | "byteOffset": 159204, 181 | "componentType": 5126, 182 | "count": 2755, 183 | "max": [ 184 | 0.99995100498199463, 185 | 1, 186 | 0.99998760223388672 187 | ], 188 | "min": [ 189 | -0.99998390674591064, 190 | -1, 191 | -0.99998235702514648 192 | ], 193 | "type": "VEC3" 194 | }, 195 | { 196 | "bufferView": 3, 197 | "byteOffset": 84096, 198 | "componentType": 5126, 199 | "count": 2755, 200 | "max": [ 201 | 0.99814325571060181, 202 | 1, 203 | 0.99496173858642578, 204 | 1 205 | ], 206 | "min": [ 207 | -0.99448871612548828, 208 | -1, 209 | -0.96300077438354492, 210 | -1 211 | ], 212 | "type": "VEC4" 213 | }, 214 | { 215 | "bufferView": 1, 216 | "byteOffset": 42048, 217 | "componentType": 5126, 218 | "count": 2755, 219 | "max": [ 220 | 0.97451800107955933, 221 | 0.98990899324417114 222 | ], 223 | "min": [ 224 | 0.0055100000463426113, 225 | 0.07879900187253952 226 | ], 227 | "type": "VEC2" 228 | }, 229 | { 230 | "bufferView": 0, 231 | "byteOffset": 110592, 232 | "componentType": 5125, 233 | "count": 13968, 234 | "max": [ 235 | 2754 236 | ], 237 | "min": [ 238 | 0 239 | ], 240 | "type": "SCALAR" 241 | }, 242 | { 243 | "bufferView": 2, 244 | "byteOffset": 192264, 245 | "componentType": 5126, 246 | "count": 2936, 247 | "max": [ 248 | 3.800260066986084, 249 | 1.702360987663269, 250 | 1.176954984664917 251 | ], 252 | "min": [ 253 | -3.8927350044250488, 254 | -1.702360987663269, 255 | -4.324364185333252 256 | ], 257 | "type": "VEC3" 258 | }, 259 | { 260 | "bufferView": 2, 261 | "byteOffset": 227496, 262 | "componentType": 5126, 263 | "count": 2936, 264 | "max": [ 265 | 0.99999064207077026, 266 | 1, 267 | 0.99989485740661621 268 | ], 269 | "min": [ 270 | -0.99999934434890747, 271 | -1, 272 | -0.99999988079071045 273 | ], 274 | "type": "VEC3" 275 | }, 276 | { 277 | "bufferView": 3, 278 | "byteOffset": 128176, 279 | "componentType": 5126, 280 | "count": 2936, 281 | "max": [ 282 | 0.99989438056945801, 283 | 1, 284 | 0.98171645402908325, 285 | 1 286 | ], 287 | "min": [ 288 | -0.99994421005249023, 289 | -1, 290 | -0.97968494892120361, 291 | -1 292 | ], 293 | "type": "VEC4" 294 | }, 295 | { 296 | "bufferView": 1, 297 | "byteOffset": 64088, 298 | "componentType": 5126, 299 | "count": 2936, 300 | "max": [ 301 | 0.96381497383117676, 302 | 0.989795982837677 303 | ], 304 | "min": [ 305 | 0.0055100000463426113, 306 | 0.096418999135494232 307 | ], 308 | "type": "VEC2" 309 | }, 310 | { 311 | "bufferView": 0, 312 | "byteOffset": 166464, 313 | "componentType": 5125, 314 | "count": 14736, 315 | "max": [ 316 | 2935 317 | ], 318 | "min": [ 319 | 0 320 | ], 321 | "type": "SCALAR" 322 | }, 323 | { 324 | "bufferView": 2, 325 | "byteOffset": 262728, 326 | "componentType": 5126, 327 | "count": 2003, 328 | "max": [ 329 | 2.5653150081634521, 330 | 1.702360987663269, 331 | 1.1509619951248169 332 | ], 333 | "min": [ 334 | -3.800260066986084, 335 | -1.702360987663269, 336 | -4.2871379852294922 337 | ], 338 | "type": "VEC3" 339 | }, 340 | { 341 | "bufferView": 2, 342 | "byteOffset": 286764, 343 | "componentType": 5126, 344 | "count": 2003, 345 | "max": [ 346 | 0.99999934434890747, 347 | 0.99999988079071045, 348 | 0.99989485740661621 349 | ], 350 | "min": [ 351 | -0.99999064207077026, 352 | -1, 353 | -0.99999988079071045 354 | ], 355 | "type": "VEC3" 356 | }, 357 | { 358 | "bufferView": 3, 359 | "byteOffset": 175152, 360 | "componentType": 5126, 361 | "count": 2003, 362 | "max": [ 363 | 0.99989461898803711, 364 | 1, 365 | 0.97969025373458862, 366 | 1 367 | ], 368 | "min": [ 369 | -0.99994397163391113, 370 | -1, 371 | -0.99943935871124268, 372 | 1 373 | ], 374 | "type": "VEC4" 375 | }, 376 | { 377 | "bufferView": 1, 378 | "byteOffset": 87576, 379 | "componentType": 5126, 380 | "count": 2003, 381 | "max": [ 382 | 0.99107497930526733, 383 | 0.99155700206756592 384 | ], 385 | "min": [ 386 | 0.0060720001347362995, 387 | 0.038775000721216202 388 | ], 389 | "type": "VEC2" 390 | }, 391 | { 392 | "bufferView": 0, 393 | "byteOffset": 225408, 394 | "componentType": 5125, 395 | "count": 9984, 396 | "max": [ 397 | 2002 398 | ], 399 | "min": [ 400 | 0 401 | ], 402 | "type": "SCALAR" 403 | }, 404 | { 405 | "bufferView": 2, 406 | "byteOffset": 310800, 407 | "componentType": 5126, 408 | "count": 1217, 409 | "max": [ 410 | 3.7923660278320312, 411 | 0.65131300687789917, 412 | 4.4611759185791016 413 | ], 414 | "min": [ 415 | -3.7923660278320312, 416 | -0.65117502212524414, 417 | 1.1293530464172363 418 | ], 419 | "type": "VEC3" 420 | }, 421 | { 422 | "bufferView": 2, 423 | "byteOffset": 325404, 424 | "componentType": 5126, 425 | "count": 1217, 426 | "max": [ 427 | 0.98565661907196045, 428 | 0.99999988079071045, 429 | 1 430 | ], 431 | "min": [ 432 | -0.9856608510017395, 433 | -1, 434 | -1 435 | ], 436 | "type": "VEC3" 437 | }, 438 | { 439 | "bufferView": 3, 440 | "byteOffset": 207200, 441 | "componentType": 5126, 442 | "count": 1217, 443 | "max": [ 444 | 1, 445 | 0.87887567281723022, 446 | 0.99662816524505615, 447 | 1 448 | ], 449 | "min": [ 450 | -0.99940621852874756, 451 | -0.95592206716537476, 452 | -0.99660122394561768, 453 | 1 454 | ], 455 | "type": "VEC4" 456 | }, 457 | { 458 | "bufferView": 1, 459 | "byteOffset": 103600, 460 | "componentType": 5126, 461 | "count": 1217, 462 | "max": [ 463 | 0.85237401723861694, 464 | 0.62625801563262939 465 | ], 466 | "min": [ 467 | 0.0055959997698664665, 468 | 0.0056210001930594444 469 | ], 470 | "type": "VEC2" 471 | }, 472 | { 473 | "bufferView": 0, 474 | "byteOffset": 265344, 475 | "componentType": 5125, 476 | "count": 6240, 477 | "max": [ 478 | 1216 479 | ], 480 | "min": [ 481 | 0 482 | ], 483 | "type": "SCALAR" 484 | }, 485 | { 486 | "bufferView": 2, 487 | "byteOffset": 340008, 488 | "componentType": 5126, 489 | "count": 1768, 490 | "max": [ 491 | 3.8674080371856689, 492 | 0.67645400762557983, 493 | 4.5584120750427246 494 | ], 495 | "min": [ 496 | -3.8674080371856689, 497 | -0.67591398954391479, 498 | 1.158877968788147 499 | ], 500 | "type": "VEC3" 501 | }, 502 | { 503 | "bufferView": 2, 504 | "byteOffset": 361224, 505 | "componentType": 5126, 506 | "count": 1768, 507 | "max": [ 508 | 0.99915486574172974, 509 | 1, 510 | 1 511 | ], 512 | "min": [ 513 | -0.99915534257888794, 514 | -1, 515 | -1 516 | ], 517 | "type": "VEC3" 518 | }, 519 | { 520 | "bufferView": 3, 521 | "byteOffset": 226672, 522 | "componentType": 5126, 523 | "count": 1768, 524 | "max": [ 525 | 0.9983670711517334, 526 | 1, 527 | 0.9301484227180481, 528 | 1 529 | ], 530 | "min": [ 531 | -0.99998527765274048, 532 | -1, 533 | -0.92995434999465942, 534 | 1 535 | ], 536 | "type": "VEC4" 537 | }, 538 | { 539 | "bufferView": 1, 540 | "byteOffset": 113336, 541 | "componentType": 5126, 542 | "count": 1768, 543 | "max": [ 544 | 0.80839300155639648, 545 | 0.92549002170562744 546 | ], 547 | "min": [ 548 | 0.18753500282764435, 549 | 0.12949000298976898 550 | ], 551 | "type": "VEC2" 552 | }, 553 | { 554 | "bufferView": 0, 555 | "byteOffset": 290304, 556 | "componentType": 5125, 557 | "count": 9600, 558 | "max": [ 559 | 1767 560 | ], 561 | "min": [ 562 | 0 563 | ], 564 | "type": "SCALAR" 565 | } 566 | ], 567 | "asset": { 568 | "extras": { 569 | "author": "ivanlynx (https://sketchfab.com/ivanlynx)", 570 | "license": "CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)", 571 | "source": "https://sketchfab.com/models/4993f221600e4e24ae48b301dd6fc17f", 572 | "title": "Headphones" 573 | }, 574 | "generator": "Sketchfab-3.19.6", 575 | "version": "2.0" 576 | }, 577 | "bufferViews": [ 578 | { 579 | "buffer": 0, 580 | "byteLength": 328704, 581 | "byteOffset": 0, 582 | "name": "floatBufferViews", 583 | "target": 34963 584 | }, 585 | { 586 | "buffer": 0, 587 | "byteLength": 127480, 588 | "byteOffset": 328704, 589 | "byteStride": 8, 590 | "name": "floatBufferViews", 591 | "target": 34962 592 | }, 593 | { 594 | "buffer": 0, 595 | "byteLength": 382440, 596 | "byteOffset": 456184, 597 | "byteStride": 12, 598 | "name": "floatBufferViews", 599 | "target": 34962 600 | }, 601 | { 602 | "buffer": 0, 603 | "byteLength": 254960, 604 | "byteOffset": 838624, 605 | "byteStride": 16, 606 | "name": "floatBufferViews", 607 | "target": 34962 608 | } 609 | ], 610 | "buffers": [ 611 | { 612 | "byteLength": 1093584, 613 | "uri": "scene.bin" 614 | } 615 | ], 616 | "images": [ 617 | { 618 | "uri": "textures/initialShadingGroup_metallicRoughness.png" 619 | }, 620 | { 621 | "uri": "textures/initialShadingGroup_normal.png" 622 | }, 623 | { 624 | "uri": "textures/initialShadingGroup_baseColor.jpeg" 625 | } 626 | ], 627 | "materials": [ 628 | { 629 | "doubleSided": true, 630 | "emissiveFactor": [ 631 | 0, 632 | 0, 633 | 0 634 | ], 635 | "name": "initialShadingGroup", 636 | "normalTexture": { 637 | "index": 1, 638 | "scale": 1, 639 | "texCoord": 0 640 | }, 641 | "occlusionTexture": { 642 | "index": 0, 643 | "strength": 1, 644 | "texCoord": 0 645 | }, 646 | "pbrMetallicRoughness": { 647 | "baseColorFactor": [ 648 | 1, 649 | 1, 650 | 1, 651 | 1 652 | ], 653 | "baseColorTexture": { 654 | "index": 2, 655 | "texCoord": 0 656 | }, 657 | "metallicFactor": 1, 658 | "metallicRoughnessTexture": { 659 | "index": 0, 660 | "texCoord": 0 661 | }, 662 | "roughnessFactor": 1 663 | } 664 | } 665 | ], 666 | "meshes": [ 667 | { 668 | "primitives": [ 669 | { 670 | "attributes": { 671 | "NORMAL": 1, 672 | "POSITION": 0, 673 | "TANGENT": 2, 674 | "TEXCOORD_0": 3 675 | }, 676 | "indices": 4, 677 | "material": 0, 678 | "mode": 4 679 | } 680 | ] 681 | }, 682 | { 683 | "primitives": [ 684 | { 685 | "attributes": { 686 | "NORMAL": 6, 687 | "POSITION": 5, 688 | "TANGENT": 7, 689 | "TEXCOORD_0": 8 690 | }, 691 | "indices": 9, 692 | "material": 0, 693 | "mode": 4 694 | } 695 | ] 696 | }, 697 | { 698 | "primitives": [ 699 | { 700 | "attributes": { 701 | "NORMAL": 11, 702 | "POSITION": 10, 703 | "TANGENT": 12, 704 | "TEXCOORD_0": 13 705 | }, 706 | "indices": 14, 707 | "material": 0, 708 | "mode": 4 709 | } 710 | ] 711 | }, 712 | { 713 | "primitives": [ 714 | { 715 | "attributes": { 716 | "NORMAL": 16, 717 | "POSITION": 15, 718 | "TANGENT": 17, 719 | "TEXCOORD_0": 18 720 | }, 721 | "indices": 19, 722 | "material": 0, 723 | "mode": 4 724 | } 725 | ] 726 | }, 727 | { 728 | "primitives": [ 729 | { 730 | "attributes": { 731 | "NORMAL": 21, 732 | "POSITION": 20, 733 | "TANGENT": 22, 734 | "TEXCOORD_0": 23 735 | }, 736 | "indices": 24, 737 | "material": 0, 738 | "mode": 4 739 | } 740 | ] 741 | }, 742 | { 743 | "primitives": [ 744 | { 745 | "attributes": { 746 | "NORMAL": 26, 747 | "POSITION": 25, 748 | "TANGENT": 27, 749 | "TEXCOORD_0": 28 750 | }, 751 | "indices": 29, 752 | "material": 0, 753 | "mode": 4 754 | } 755 | ] 756 | }, 757 | { 758 | "primitives": [ 759 | { 760 | "attributes": { 761 | "NORMAL": 31, 762 | "POSITION": 30, 763 | "TANGENT": 32, 764 | "TEXCOORD_0": 33 765 | }, 766 | "indices": 34, 767 | "material": 0, 768 | "mode": 4 769 | } 770 | ] 771 | } 772 | ], 773 | "nodes": [ 774 | { 775 | "children": [ 776 | 1 777 | ], 778 | "name": "RootNode (gltf orientation matrix)", 779 | "rotation": [ 780 | -0.70710678118654746, 781 | -0, 782 | -0, 783 | 0.70710678118654757 784 | ] 785 | }, 786 | { 787 | "children": [ 788 | 2 789 | ], 790 | "name": "RootNode (model correction matrix)" 791 | }, 792 | { 793 | "children": [ 794 | 3, 795 | 4, 796 | 5, 797 | 6, 798 | 7, 799 | 8, 800 | 9 801 | ], 802 | "name": "Beats_Final_sketchfab_01.obj.cleaner.materialmerger.gles" 803 | }, 804 | { 805 | "mesh": 0, 806 | "name": "" 807 | }, 808 | { 809 | "mesh": 1, 810 | "name": "" 811 | }, 812 | { 813 | "mesh": 2, 814 | "name": "" 815 | }, 816 | { 817 | "mesh": 3, 818 | "name": "" 819 | }, 820 | { 821 | "mesh": 4, 822 | "name": "" 823 | }, 824 | { 825 | "mesh": 5, 826 | "name": "" 827 | }, 828 | { 829 | "mesh": 6, 830 | "name": "" 831 | } 832 | ], 833 | "samplers": [ 834 | { 835 | "magFilter": 9729, 836 | "minFilter": 9987, 837 | "wrapS": 10497, 838 | "wrapT": 10497 839 | } 840 | ], 841 | "scene": 0, 842 | "scenes": [ 843 | { 844 | "name": "OSG_Scene", 845 | "nodes": [ 846 | 0 847 | ] 848 | } 849 | ], 850 | "textures": [ 851 | { 852 | "sampler": 0, 853 | "source": 0 854 | }, 855 | { 856 | "sampler": 0, 857 | "source": 1 858 | }, 859 | { 860 | "sampler": 0, 861 | "source": 2 862 | } 863 | ] 864 | } 865 | 866 | -------------------------------------------------------------------------------- /src/static/model/textures/initialShadingGroup_baseColor.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iosorin/3d-headphones/69708311de511341dac1a6aa22ed1e82efa38080/src/static/model/textures/initialShadingGroup_baseColor.jpeg -------------------------------------------------------------------------------- /src/static/model/textures/initialShadingGroup_metallicRoughness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iosorin/3d-headphones/69708311de511341dac1a6aa22ed1e82efa38080/src/static/model/textures/initialShadingGroup_metallicRoughness.png -------------------------------------------------------------------------------- /src/static/model/textures/initialShadingGroup_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iosorin/3d-headphones/69708311de511341dac1a6aa22ed1e82efa38080/src/static/model/textures/initialShadingGroup_normal.png -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * findArraySibling - Find a sibling of the current element in array 3 | * 4 | * @param {Array} arr Array 5 | * @param {Object} current Current element 6 | * @param {String} pName Property name 7 | * @param {Boolean} forward Which way to search (next element or previous) 8 | * 9 | * @return {Object} Array element 10 | */ 11 | const findArraySibling = ({ 12 | arr = [], current = {}, forward = true, pName = 'name' 13 | } = {}) => { 14 | const currentIdx = arr.findIndex(i => i[pName] === current[pName]); 15 | 16 | const lastIdx = arr.length - 1; 17 | 18 | const nextIdx = () => { 19 | if (forward) { 20 | return currentIdx === lastIdx ? 0 : currentIdx + 1; 21 | } 22 | 23 | return currentIdx === 0 ? lastIdx : currentIdx - 1; 24 | }; 25 | 26 | return arr[nextIdx()]; 27 | }; 28 | 29 | export { 30 | findArraySibling 31 | }; 32 | --------------------------------------------------------------------------------