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