├── .gitignore ├── LICENSE ├── README.md ├── examples ├── parcel-example │ ├── index.html │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── assets │ │ ├── cake.json │ │ ├── cake_bottom.png │ │ ├── cake_side.png │ │ └── cake_top.png │ │ └── index.js └── webpack-example │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── assets │ │ ├── cake.json │ │ ├── cake_bottom.png │ │ ├── cake_side.png │ │ └── cake_top.png │ └── index.js │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src ├── geometry.ts ├── index.ts ├── loader.ts ├── material.ts ├── mesh.ts ├── model.ts └── texture.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Lib 64 | lib 65 | 66 | 67 | examples/**/dist 68 | .cache 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Valentin Berlier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three-mcmodel 2 | 3 | > A library for working with Minecraft json models using three.js. 4 | 5 | **🚧 Work in progress, not stable yet 🚧** 6 | 7 | ```js 8 | import { MinecraftModelLoader, MinecraftTextureLoader } from 'three-mcmodel' 9 | 10 | new MinecraftModelLoader().load('model.json', mesh => { 11 | const textureLoader = new MinecraftTextureLoader() 12 | mesh.resolveTextures(path => textureLoader.load(`${path}.png`)) 13 | scene.add(mesh) 14 | }) 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/parcel-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | parcel-example 8 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/parcel-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-example", 3 | "license": "MIT", 4 | "scripts": { 5 | "dev": "parcel index.html", 6 | "build": "parcel build index.html" 7 | }, 8 | "dependencies": { 9 | "three": "^0.125.0", 10 | "three-mcmodel": "latest", 11 | "three-orbitcontrols": "^2.96.3" 12 | }, 13 | "devDependencies": { 14 | "parcel-bundler": "^1.10.3", 15 | "parcel-plugin-json-url-loader": "^0.1.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/parcel-example/src/assets/cake.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": { 3 | "particle": "block/cake_side", 4 | "bottom": "block/cake_bottom", 5 | "top": "block/cake_top", 6 | "side": "block/cake_side" 7 | }, 8 | "elements": [ 9 | { "from": [ 1, 0, 1 ], 10 | "to": [ 15, 8, 15 ], 11 | "faces": { 12 | "down": { "texture": "#bottom", "cullface": "down" }, 13 | "up": { "texture": "#top" }, 14 | "north": { "texture": "#side" }, 15 | "south": { "texture": "#side" }, 16 | "west": { "texture": "#side" }, 17 | "east": { "texture": "#side" } 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/parcel-example/src/assets/cake_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vberlier/three-mcmodel/b83fbe3108ec6035e8555ec32162acfe14a0f80c/examples/parcel-example/src/assets/cake_bottom.png -------------------------------------------------------------------------------- /examples/parcel-example/src/assets/cake_side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vberlier/three-mcmodel/b83fbe3108ec6035e8555ec32162acfe14a0f80c/examples/parcel-example/src/assets/cake_side.png -------------------------------------------------------------------------------- /examples/parcel-example/src/assets/cake_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vberlier/three-mcmodel/b83fbe3108ec6035e8555ec32162acfe14a0f80c/examples/parcel-example/src/assets/cake_top.png -------------------------------------------------------------------------------- /examples/parcel-example/src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, PerspectiveCamera, WebGLRenderer, 3 | CubeGeometry, EdgesGeometry, LineBasicMaterial, LineSegments 4 | } from 'three' 5 | import OrbitControls from 'three-orbitcontrols' 6 | import { MinecraftModelLoader, MinecraftTextureLoader } from 'three-mcmodel' 7 | 8 | let scene 9 | let camera 10 | let controls 11 | let renderer 12 | 13 | function init () { 14 | scene = new Scene() 15 | camera = new PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 1000) 16 | camera.position.set(16, 16, 64) 17 | 18 | renderer = new WebGLRenderer({ antialias: true, alpha: true }) 19 | document.body.appendChild(renderer.domElement) 20 | renderer.setSize(window.innerWidth, window.innerHeight) 21 | 22 | window.addEventListener('resize', () => { 23 | camera.aspect = window.innerWidth / window.innerHeight 24 | camera.updateProjectionMatrix() 25 | renderer.setSize(window.innerWidth, window.innerHeight) 26 | }) 27 | 28 | controls = new OrbitControls(camera, renderer.domElement) 29 | controls.enableKeys = false 30 | controls.screenSpacePanning = true 31 | 32 | scene.add(new LineSegments( 33 | new EdgesGeometry(new CubeGeometry(16, 16, 16)), 34 | new LineBasicMaterial({ color: 0x1111cc, linewidth: 3 }) 35 | )) 36 | 37 | loadModel() 38 | 39 | animate() 40 | } 41 | 42 | function loadModel () { 43 | const modelUrl = require('./assets/cake.json') 44 | const textureUrls = { 45 | 'block/cake_bottom': require('./assets/cake_bottom.png'), 46 | 'block/cake_side': require('./assets/cake_side.png'), 47 | 'block/cake_top': require('./assets/cake_top.png') 48 | } 49 | 50 | new MinecraftModelLoader().load(modelUrl, mesh => { 51 | const textureLoader = new MinecraftTextureLoader() 52 | mesh.resolveTextures(path => textureLoader.load(textureUrls[path])) 53 | scene.add(mesh) 54 | }) 55 | } 56 | 57 | function animate () { 58 | requestAnimationFrame(animate) 59 | controls.update() 60 | renderer.render(scene, camera) 61 | } 62 | 63 | init() 64 | -------------------------------------------------------------------------------- /examples/webpack-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | webpack-example 8 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/webpack-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-example", 3 | "license": "MIT", 4 | "scripts": { 5 | "dev": "webpack-dev-server --mode=development", 6 | "build": "webpack --mode=production" 7 | }, 8 | "dependencies": { 9 | "three": "^0.125.0", 10 | "three-mcmodel": "latest", 11 | "three-orbitcontrols": "^2.96.3" 12 | }, 13 | "devDependencies": { 14 | "file-loader": "^2.0.0", 15 | "html-webpack-plugin": "^4.0.0-beta.2", 16 | "webpack": "^4.24.0", 17 | "webpack-cli": "^3.1.2", 18 | "webpack-dev-server": "^3.1.14" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/webpack-example/src/assets/cake.json: -------------------------------------------------------------------------------- 1 | { 2 | "textures": { 3 | "particle": "block/cake_side", 4 | "bottom": "block/cake_bottom", 5 | "top": "block/cake_top", 6 | "side": "block/cake_side" 7 | }, 8 | "elements": [ 9 | { "from": [ 1, 0, 1 ], 10 | "to": [ 15, 8, 15 ], 11 | "faces": { 12 | "down": { "texture": "#bottom", "cullface": "down" }, 13 | "up": { "texture": "#top" }, 14 | "north": { "texture": "#side" }, 15 | "south": { "texture": "#side" }, 16 | "west": { "texture": "#side" }, 17 | "east": { "texture": "#side" } 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/webpack-example/src/assets/cake_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vberlier/three-mcmodel/b83fbe3108ec6035e8555ec32162acfe14a0f80c/examples/webpack-example/src/assets/cake_bottom.png -------------------------------------------------------------------------------- /examples/webpack-example/src/assets/cake_side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vberlier/three-mcmodel/b83fbe3108ec6035e8555ec32162acfe14a0f80c/examples/webpack-example/src/assets/cake_side.png -------------------------------------------------------------------------------- /examples/webpack-example/src/assets/cake_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vberlier/three-mcmodel/b83fbe3108ec6035e8555ec32162acfe14a0f80c/examples/webpack-example/src/assets/cake_top.png -------------------------------------------------------------------------------- /examples/webpack-example/src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, PerspectiveCamera, WebGLRenderer, 3 | CubeGeometry, EdgesGeometry, LineBasicMaterial, LineSegments 4 | } from 'three' 5 | import OrbitControls from 'three-orbitcontrols' 6 | import { MinecraftModelLoader, MinecraftTextureLoader } from 'three-mcmodel' 7 | 8 | // Create the scene and the camera 9 | const scene = new Scene() 10 | const camera = new PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 1000) 11 | camera.position.set(16, 16, 64) 12 | 13 | // Create a mesh from the json model and add it to the scene 14 | new MinecraftModelLoader().load(require('./assets/cake.json'), mesh => { 15 | const textureLoader = new MinecraftTextureLoader() 16 | mesh.resolveTextures(path => textureLoader.load(require(`./assets/${path.substr(6)}.png`))) 17 | scene.add(mesh) 18 | }) 19 | 20 | // Create cube indicator 21 | const wireframe = new LineSegments( 22 | new EdgesGeometry(new CubeGeometry(16, 16, 16)), 23 | new LineBasicMaterial({ color: 0x1111cc, linewidth: 3 }) 24 | ) 25 | scene.add(wireframe) 26 | 27 | // Create the renderer and append it to the document body 28 | const renderer = new WebGLRenderer({ antialias: true, alpha: true }) 29 | renderer.setSize(window.innerWidth, window.innerHeight) 30 | document.body.appendChild(renderer.domElement) 31 | 32 | // Create the controls 33 | const controls = new OrbitControls(camera, renderer.domElement) 34 | controls.enableKeys = false 35 | controls.screenSpacePanning = true 36 | 37 | // Update the dimensions of the viewport when the window gets resized 38 | window.addEventListener('resize', () => { 39 | camera.aspect = window.innerWidth / window.innerHeight 40 | camera.updateProjectionMatrix() 41 | renderer.setSize(window.innerWidth, window.innerHeight) 42 | }) 43 | 44 | // Start animation 45 | function animate () { 46 | requestAnimationFrame(animate) 47 | controls.update() 48 | renderer.render(scene, camera) 49 | } 50 | animate() 51 | -------------------------------------------------------------------------------- /examples/webpack-example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | 3 | module.exports = { 4 | module: { 5 | rules: [ 6 | { 7 | type: 'javascript/auto', 8 | test: /assets/, 9 | loader: 'file-loader' 10 | } 11 | ] 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: 'index.html' 16 | }) 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-mcmodel", 3 | "version": "0.2.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/three": { 8 | "version": "0.125.3", 9 | "resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz", 10 | "integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA==", 11 | "dev": true 12 | }, 13 | "balanced-match": { 14 | "version": "1.0.0", 15 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 16 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 17 | "dev": true 18 | }, 19 | "brace-expansion": { 20 | "version": "1.1.11", 21 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 22 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 23 | "dev": true, 24 | "requires": { 25 | "balanced-match": "^1.0.0", 26 | "concat-map": "0.0.1" 27 | } 28 | }, 29 | "concat-map": { 30 | "version": "0.0.1", 31 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 32 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 33 | "dev": true 34 | }, 35 | "fs.realpath": { 36 | "version": "1.0.0", 37 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 38 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 39 | "dev": true 40 | }, 41 | "glob": { 42 | "version": "7.1.6", 43 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 44 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 45 | "dev": true, 46 | "requires": { 47 | "fs.realpath": "^1.0.0", 48 | "inflight": "^1.0.4", 49 | "inherits": "2", 50 | "minimatch": "^3.0.4", 51 | "once": "^1.3.0", 52 | "path-is-absolute": "^1.0.0" 53 | } 54 | }, 55 | "inflight": { 56 | "version": "1.0.6", 57 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 58 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 59 | "dev": true, 60 | "requires": { 61 | "once": "^1.3.0", 62 | "wrappy": "1" 63 | } 64 | }, 65 | "inherits": { 66 | "version": "2.0.4", 67 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 68 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 69 | "dev": true 70 | }, 71 | "minimatch": { 72 | "version": "3.0.4", 73 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 74 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 75 | "dev": true, 76 | "requires": { 77 | "brace-expansion": "^1.1.7" 78 | } 79 | }, 80 | "once": { 81 | "version": "1.4.0", 82 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 83 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 84 | "dev": true, 85 | "requires": { 86 | "wrappy": "1" 87 | } 88 | }, 89 | "path-is-absolute": { 90 | "version": "1.0.1", 91 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 92 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 93 | "dev": true 94 | }, 95 | "rimraf": { 96 | "version": "3.0.2", 97 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 98 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 99 | "dev": true, 100 | "requires": { 101 | "glob": "^7.1.3" 102 | } 103 | }, 104 | "three": { 105 | "version": "0.126.0", 106 | "resolved": "https://registry.npmjs.org/three/-/three-0.126.0.tgz", 107 | "integrity": "sha512-/MecvboUefStCkUfXLImoJxthN+FoLPcEP7pz1r1Dd9i8BPGGuj+S1sOPRvW4Z+ViZjP2oWWm1inNC/MT52ybA==" 108 | }, 109 | "typescript": { 110 | "version": "4.2.2", 111 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz", 112 | "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==", 113 | "dev": true 114 | }, 115 | "wrappy": { 116 | "version": "1.0.2", 117 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 118 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 119 | "dev": true 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "three-mcmodel", 3 | "version": "0.2.1", 4 | "description": "A library for working with Minecraft json models using three.js.", 5 | "author": "Valentin Berlier ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "minecraft-model", 9 | "threejs", 10 | "minecraft", 11 | "minecraft-json-models", 12 | "3d", 13 | "threejs-loader", 14 | "three" 15 | ], 16 | "homepage": "https://github.com/vberlier/three-mcmodel#readme", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/vberlier/three-mcmodel.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/vberlier/three-mcmodel/issues" 23 | }, 24 | "main": "lib/index.js", 25 | "scripts": { 26 | "test": "echo \"Error: no test specified\" && exit 1", 27 | "dev": "tsc --watch", 28 | "build": "tsc", 29 | "clean": "rimraf lib", 30 | "prepare": "npm run clean && npm run build" 31 | }, 32 | "dependencies": { 33 | "three": "^0.126.0" 34 | }, 35 | "devDependencies": { 36 | "@types/three": "^0.125.3", 37 | "rimraf": "^3.0.0", 38 | "typescript": "^4.2.2" 39 | }, 40 | "files": [ 41 | "lib/**/*", 42 | "src/**/*" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/geometry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, Matrix3, Vector3, 3 | Float32BufferAttribute, Uint16BufferAttribute 4 | } from 'three' 5 | 6 | import { 7 | MinecraftModel, ArrayVector3, ArrayVector4, MinecraftModelFaceName, TextureRotationAngle, 8 | MinecraftModelElementRotation, ElementRotationAxis 9 | } from './model' 10 | 11 | const vertexMaps: { 12 | [name in MinecraftModelFaceName]: ArrayVector4 13 | } = { 14 | west: [0, 1, 2, 3], 15 | east: [4, 5, 6, 7], 16 | down: [0, 3, 4, 7], 17 | up: [2, 1, 6, 5], 18 | north: [7, 6, 1, 0], 19 | south: [3, 2, 5, 4] 20 | } 21 | 22 | function applyVertexMapRotation (rotation: TextureRotationAngle, [a, b, c, d]: ArrayVector4) { 23 | return ( 24 | rotation === 0 ? [a, b, c, d] : 25 | rotation === 90 ? [b, c, d, a] : 26 | rotation === 180 ? [c, d, a, b] : 27 | [d, a, b, c] 28 | ) as ArrayVector4 29 | } 30 | 31 | function buildMatrix (angle: number, scale: number, axis: ElementRotationAxis) { 32 | const a = Math.cos(angle) * scale 33 | const b = Math.sin(angle) * scale 34 | const matrix = new Matrix3() 35 | 36 | if (axis === 'x') { 37 | matrix.set( 38 | 1, 0, 0, 39 | 0, a, -b, 40 | 0, b, a 41 | ) 42 | } else if (axis === 'y') { 43 | matrix.set( 44 | a, 0, b, 45 | 0, 1, 0, 46 | -b, 0, a 47 | ) 48 | } else { 49 | matrix.set( 50 | a, -b, 0, 51 | b, a, 0, 52 | 0, 0, 1 53 | ) 54 | } 55 | 56 | return matrix 57 | } 58 | 59 | function rotateCubeCorners (corners: ArrayVector3[], rotation: MinecraftModelElementRotation) { 60 | const origin = new Vector3() 61 | .fromArray(rotation.origin) 62 | .subScalar(8) 63 | 64 | const angle = rotation.angle / 180 * Math.PI 65 | const scale = rotation.rescale ? Math.SQRT2 / Math.sqrt(Math.cos(angle || Math.PI / 4)**2 * 2) : 1 66 | const matrix = buildMatrix(angle, scale, rotation.axis) 67 | 68 | return corners.map( 69 | vertex => new Vector3() 70 | .fromArray(vertex) 71 | .sub(origin) 72 | .applyMatrix3(matrix) 73 | .add(origin) 74 | .toArray() 75 | ) as ArrayVector3[] 76 | } 77 | 78 | function getCornerVertices (from: ArrayVector3, to: ArrayVector3) { 79 | const [x1, y1, z1, x2, y2, z2] = from.concat(to).map(coordinate => coordinate - 8) 80 | 81 | return [ 82 | [x1, y1, z1], 83 | [x1, y2, z1], 84 | [x1, y2, z2], 85 | [x1, y1, z2], 86 | [x2, y1, z2], 87 | [x2, y2, z2], 88 | [x2, y2, z1], 89 | [x2, y1, z1] 90 | ] as ArrayVector3[] 91 | } 92 | 93 | function generateDefaultUvs (faceName: MinecraftModelFaceName, [x1, y1, z1]: ArrayVector3, [x2, y2, z2]: ArrayVector3) { 94 | return ( 95 | faceName === 'west' ? [z1, 16 - y2, z2, 16 - y1] : 96 | faceName === 'east' ? [16 - z2, 16 - y2, 16 - z1, 16 - y1] : 97 | faceName === 'down' ? [x1, 16 - z2, x2, 16 - z1] : 98 | faceName === 'up' ? [x1, z1, x2, z2] : 99 | faceName === 'north' ? [16 - x2, 16 - y2, 16 - x1, 16 - y1] : 100 | [x1, 16 - y2, x2, 16 - y1] 101 | ) as ArrayVector4 102 | } 103 | 104 | function computeNormalizedUvs (uvs: ArrayVector4) { 105 | return uvs.map((coordinate, i) => 106 | (i % 2 ? 16 - coordinate : coordinate) / 16 107 | ) as ArrayVector4 108 | } 109 | 110 | interface GroupAttributes { 111 | vertices: number[] 112 | uvs: number[] 113 | indices: number[] 114 | } 115 | 116 | class GroupedAttributesBuilder { 117 | private groups: { [path: string]: GroupAttributes } = {} 118 | private groupMapping: { [variable: string]: GroupAttributes } = {} 119 | private missingGroup: GroupAttributes = { vertices: [], uvs: [], indices: [] } 120 | 121 | constructor (textures: { [name: string]: string }) { 122 | for (const texturePath of new Set(Object.values(textures))) { 123 | this.groups[texturePath] = { vertices: [], uvs: [], indices: [] } 124 | } 125 | 126 | for (const variable in textures) { 127 | this.groupMapping['#' + variable] = this.groups[textures[variable]] 128 | } 129 | } 130 | 131 | public getContext (textureVariable: string) { 132 | return this.groupMapping[textureVariable] || this.missingGroup 133 | } 134 | 135 | public getAttributes () { 136 | let { vertices, uvs, indices } = this.missingGroup 137 | let indexCount = indices.length 138 | 139 | const groups = [{ start: 0, count: indexCount, materialIndex: 0 }] 140 | 141 | groups.push(...Object.keys(this.groups).sort().map((path, i) => { 142 | const group = this.groups[path] 143 | 144 | const start = indexCount 145 | const count = group.indices.length 146 | const offset = vertices.length / 3 147 | 148 | vertices = vertices.concat(group.vertices) 149 | uvs = uvs.concat(group.uvs) 150 | indices = indices.concat(group.indices.map(index => index + offset)) 151 | 152 | indexCount += count 153 | 154 | return { start, count, materialIndex: i + 1 } 155 | })) 156 | 157 | return { vertices, uvs, indices, groups } 158 | } 159 | } 160 | 161 | export class MinecraftModelGeometry extends BufferGeometry { 162 | constructor (model: MinecraftModel) { 163 | super() 164 | const { vertices, uvs, indices, groups } = MinecraftModelGeometry.computeAttributes(model) 165 | 166 | this.addAttribute('position', new Float32BufferAttribute(vertices, 3)) 167 | this.addAttribute('uv', new Float32BufferAttribute(uvs, 2)) 168 | this.setIndex(new Uint16BufferAttribute(indices, 1)) 169 | 170 | for (const { start, count, materialIndex } of groups) { 171 | this.addGroup(start, count, materialIndex) 172 | } 173 | } 174 | 175 | public static computeAttributes (model: MinecraftModel) { 176 | const builder = new GroupedAttributesBuilder(model.textures) 177 | 178 | for (const element of model.elements) { 179 | const { from, to, rotation } = element 180 | const cornerVertices = getCornerVertices(from, to) 181 | const rotatedVertices = rotation ? rotateCubeCorners(cornerVertices, rotation) : cornerVertices 182 | 183 | for (const name in element.faces) { 184 | const faceName = name as MinecraftModelFaceName 185 | const face = element.faces[faceName] 186 | 187 | if (face === undefined) { 188 | continue 189 | } 190 | 191 | const { vertices, uvs, indices } = builder.getContext(face.texture) 192 | 193 | const i = vertices.length / 3 194 | indices.push(i, i + 2, i + 1) 195 | indices.push(i, i + 3, i + 2) 196 | 197 | for (const index of applyVertexMapRotation(face.rotation || 0, vertexMaps[faceName])) { 198 | vertices.push(...rotatedVertices[index]) 199 | } 200 | 201 | const faceUvs = face.uv || generateDefaultUvs(faceName, from, to) 202 | const [u1, v1, u2, v2] = computeNormalizedUvs(faceUvs) 203 | 204 | uvs.push(u1, v2) 205 | uvs.push(u1, v1) 206 | uvs.push(u2, v1) 207 | uvs.push(u2, v2) 208 | } 209 | } 210 | 211 | return builder.getAttributes() 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { MinecraftModelGeometry } from './geometry' 2 | export { AbstractLoader } from './loader' 3 | export { MinecraftModelMaterial } from './material' 4 | export { MinecraftModelLoader, MinecraftModelMesh } from './mesh' 5 | export { 6 | ArrayVector3, isArrayVector3, 7 | ArrayVector4, isArrayVector4, 8 | TextureRotationAngle, 9 | MinecraftModelFaceName, MinecraftModelFace, isMinecraftModelFace, 10 | ElementRotationAngle, ElementRotationAxis, 11 | MinecraftModelElementRotation, isMinecraftModelElementRotation, 12 | MinecraftModelElement, isMinecraftModelElement, 13 | MinecraftModel, isMinecraftModel 14 | } from './model' 15 | export { 16 | MinecraftTextureLoader, MinecraftTexture, CHECKERBOARD_IMAGE 17 | } from './texture' 18 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | import { LoadingManager, DefaultLoadingManager } from 'three' 2 | 3 | export type OnLoad = (response: any) => void 4 | export type OnProgress = (request: ProgressEvent) => void 5 | export type OnError = (error: any) => void 6 | 7 | export abstract class AbstractLoader { 8 | public path = '' 9 | 10 | constructor (public manager: LoadingManager = DefaultLoadingManager) { } 11 | 12 | public abstract load (url: string, onLoad?: OnLoad, onProgress?: OnProgress, onError?: OnError): any 13 | 14 | public setPath (value: string) { 15 | this.path = value 16 | return this 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/material.ts: -------------------------------------------------------------------------------- 1 | import { MeshBasicMaterial } from 'three' 2 | 3 | import { MinecraftTexture } from './texture' 4 | 5 | export class MinecraftModelMaterial extends MeshBasicMaterial { 6 | constructor (map: MinecraftTexture = new MinecraftTexture()) { 7 | super({ 8 | map: map, 9 | transparent: true, 10 | alphaTest: 0.5 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/mesh.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, FileLoader } from 'three' 2 | 3 | import { MinecraftModelGeometry } from './geometry' 4 | import { AbstractLoader, OnProgress, OnError } from './loader' 5 | import { MinecraftModelMaterial } from './material' 6 | import { MinecraftModel, isMinecraftModel } from './model' 7 | import { MinecraftTexture } from './texture' 8 | 9 | type MaterialMapping = { [path: string]: MinecraftModelMaterial } 10 | 11 | export class MinecraftModelMesh extends Mesh { 12 | private materialMapping: MaterialMapping 13 | 14 | constructor (model: MinecraftModel | string | any) { 15 | if (typeof model === 'string') { 16 | model = JSON.parse(model) 17 | } 18 | 19 | if (!isMinecraftModel(model)) { 20 | throw new Error('Invalid model') 21 | } 22 | 23 | const geometry = new MinecraftModelGeometry(model) 24 | 25 | const sortedTextures = [...new Set(Object.values(model.textures))].sort() 26 | const mapping: MaterialMapping = {} 27 | const materials = sortedTextures 28 | .map(path => mapping[path] = new MinecraftModelMaterial()) 29 | 30 | super(geometry, [new MinecraftModelMaterial(), ...materials]) 31 | 32 | this.materialMapping = mapping 33 | } 34 | 35 | public resolveTextures (resolver: (path: string) => MinecraftTexture) { 36 | for (const path in this.materialMapping) { 37 | this.materialMapping[path].map = resolver(path) 38 | } 39 | } 40 | } 41 | 42 | type OnLoad = (mesh: MinecraftModelMesh) => void 43 | 44 | export class MinecraftModelLoader extends AbstractLoader { 45 | public load (url: string, onLoad?: OnLoad, onProgress?: OnProgress, onError?: OnError) { 46 | const loader = new FileLoader(this.manager) 47 | loader.setPath(this.path) 48 | loader.setResponseType('json') 49 | 50 | const handleLoad = (model: any) => { 51 | try { 52 | const mesh = new MinecraftModelMesh(model) 53 | 54 | if (onLoad) { 55 | onLoad(mesh) 56 | } 57 | } catch (err) { 58 | if (onError) { 59 | onError(err) 60 | } 61 | } 62 | } 63 | 64 | loader.load(url, handleLoad, onProgress, onError) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | export type ArrayVector3 = [number, number, number] 2 | 3 | export function isArrayVector3 (arrayVector: any): arrayVector is ArrayVector3 { 4 | return ( 5 | Array.isArray(arrayVector) && 6 | arrayVector.length === 3 && 7 | arrayVector.every(coordinate => typeof coordinate === 'number') 8 | ) 9 | } 10 | 11 | export type ArrayVector4 = [number, number, number, number] 12 | 13 | export function isArrayVector4 (arrayVector: any): arrayVector is ArrayVector4 { 14 | return ( 15 | Array.isArray(arrayVector) && 16 | arrayVector.length === 4 && 17 | arrayVector.every(coordinate => typeof coordinate === 'number') 18 | ) 19 | } 20 | 21 | export type TextureRotationAngle = 0 | 90 | 180 | 270 22 | 23 | export interface MinecraftModelFace { 24 | texture: string 25 | uv?: ArrayVector4 26 | rotation?: TextureRotationAngle 27 | } 28 | 29 | export function isMinecraftModelFace (face: any): face is MinecraftModelFace { 30 | return ( 31 | face && 32 | typeof face.texture === 'string' && 33 | face.texture.length >= 2 && 34 | face.texture[0] === '#' && 35 | (face.uv === undefined || isArrayVector4(face.uv)) && 36 | (face.rotation === undefined || [0, 90, 180, 270].includes(face.rotation)) 37 | ) 38 | } 39 | 40 | export type MinecraftModelFaceName = 'west' | 'east' | 'down' | 'up' | 'north' | 'south' 41 | 42 | export type ElementRotationAngle = -45 | -22.5 | 0 | 22.5 | 45 43 | export type ElementRotationAxis = 'x' | 'y' | 'z' 44 | 45 | export interface MinecraftModelElementRotation { 46 | origin: ArrayVector3 47 | angle: ElementRotationAngle 48 | axis: ElementRotationAxis 49 | rescale?: boolean 50 | } 51 | 52 | export function isMinecraftModelElementRotation (rotation: any): rotation is MinecraftModelElementRotation { 53 | return ( 54 | rotation && 55 | isArrayVector3(rotation.origin) && 56 | [-45, -22.5, 0, 22.5, 45].includes(rotation.angle) && 57 | ['x', 'y', 'z'].includes(rotation.axis) && 58 | (rotation.rescale === undefined || typeof rotation.rescale === 'boolean') 59 | ) 60 | } 61 | 62 | export interface MinecraftModelElement { 63 | from: ArrayVector3 64 | to: ArrayVector3 65 | rotation?: MinecraftModelElementRotation 66 | faces: { [name in MinecraftModelFaceName]?: MinecraftModelFace } 67 | } 68 | 69 | export function isMinecraftModelElement (element: any): element is MinecraftModelElement { 70 | let faceCount 71 | 72 | return ( 73 | element && 74 | isArrayVector3(element.from) && 75 | isArrayVector3(element.to) && 76 | (element.rotation === undefined || isMinecraftModelElementRotation(element.rotation)) && 77 | element.faces && 78 | (faceCount = Object.keys(element.faces).length) >= 1 && 79 | faceCount <= 6 && 80 | [ 81 | element.faces.down, element.faces.up, 82 | element.faces.north, element.faces.south, 83 | element.faces.west, element.faces.east 84 | ] 85 | .every((face: any) => 86 | face === undefined || isMinecraftModelFace(face) 87 | ) 88 | ) 89 | } 90 | 91 | export interface MinecraftModel { 92 | textures: { [name: string]: string } 93 | elements: MinecraftModelElement[] 94 | } 95 | 96 | export function isMinecraftModel (model: any): model is MinecraftModel { 97 | return ( 98 | model && 99 | model.textures && 100 | Object.entries(model.textures).every(([name, texture]) => 101 | typeof name === 'string' && typeof texture === 'string' 102 | ) && 103 | Array.isArray(model.elements) && 104 | model.elements.every(isMinecraftModelElement) 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /src/texture.ts: -------------------------------------------------------------------------------- 1 | import { NearestFilter, Texture, ImageLoader } from 'three' 2 | 3 | import { AbstractLoader, OnProgress, OnError } from './loader' 4 | 5 | export const CHECKERBOARD_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH4goSFSEEtucn/QAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAkSURBVCjPY2TAAX4w/MAqzsRAIhjVQAxgxBXeHAwco6FEPw0A+iAED8NWwMQAAAAASUVORK5CYII=' 6 | 7 | export class MinecraftTexture extends Texture { 8 | private _image?: HTMLImageElement 9 | 10 | constructor (image?: HTMLImageElement) { 11 | super() 12 | this.image = image 13 | this.magFilter = NearestFilter 14 | } 15 | 16 | get image () { 17 | return this._image 18 | } 19 | 20 | set image (value) { 21 | this._image = value && value.width === value.height ? value : new ImageLoader().load(CHECKERBOARD_IMAGE) 22 | this.needsUpdate = true 23 | } 24 | } 25 | 26 | type OnLoad = (texture: MinecraftTexture) => void 27 | 28 | export class MinecraftTextureLoader extends AbstractLoader { 29 | public crossOrigin = 'anonymous' 30 | 31 | public load (url: string, onLoad?: OnLoad, onProgress?: OnProgress, onError?: OnError) { 32 | const texture = new MinecraftTexture() 33 | 34 | const loader = new ImageLoader(this.manager) 35 | loader.setCrossOrigin(this.crossOrigin) 36 | loader.setPath(this.path) 37 | 38 | const handleLoad = (image: HTMLImageElement) => { 39 | texture.image = image 40 | 41 | if (onLoad) { 42 | onLoad(texture) 43 | } 44 | } 45 | 46 | loader.load(url, handleLoad, onProgress, onError) 47 | 48 | return texture 49 | } 50 | 51 | public setCrossOrigin (value: string) { 52 | this.crossOrigin = value 53 | return this 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["esnext", "dom"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "sourceMap": true, 9 | "outDir": "lib", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "esModuleInterop": true, 16 | "downlevelIteration": true 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------