├── _config.yml ├── bin ├── barrel │ ├── barrel.png │ ├── barrel.blend │ ├── output │ │ ├── barrel.glb │ │ ├── Batchedbarrel │ │ │ ├── barrel.b3dm │ │ │ └── tileset.json │ │ ├── Instancedbarrel │ │ │ ├── barrel.i3dm │ │ │ └── tileset.json │ │ ├── BatchedTilesets │ │ │ ├── barrel_withCustomBatchTable.b3dm │ │ │ ├── barrel_withDefaultBatchTable.b3dm │ │ │ └── tileset.json │ │ └── barrel_batchTable.json │ ├── barrel.mtl │ ├── customTilesetOptions.json │ ├── customI3dmBatchTable.json │ ├── customFeatureTable.json │ └── customBatchtable.json └── obj23dtiles.js ├── pics ├── useOcclusion.png └── boundingvolume.png ├── .eslintrc.js ├── lib ├── Texture.js ├── outsideDirectory.js ├── tilesetOptionsUtility.js ├── getBufferPadded.js ├── readLines.js ├── getJsonBufferPadded.js ├── getBufferPadded8Byte.js ├── getJsonBufferPadded8Byte.js ├── gltfToGlb.js ├── createI3dm.js ├── obj2b3dm.js ├── ArrayStorage.js ├── loadTexture.js ├── createB3dm.js ├── combineTileset.js ├── createSingleTileset.js ├── writeGltf.js ├── obj23dtiles.js ├── obj2Tileset.js ├── obj2gltf.js ├── obj2I3dm.js ├── createGltf.js ├── loadObj.js └── loadMtl.js ├── tools ├── BatchConvert.bat └── test.js ├── package.json ├── .gitignore ├── README_CN.md ├── NODEUSAGE.md ├── README.md └── LICENSE /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | show_downloads: true -------------------------------------------------------------------------------- /bin/barrel/barrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/bin/barrel/barrel.png -------------------------------------------------------------------------------- /pics/useOcclusion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/pics/useOcclusion.png -------------------------------------------------------------------------------- /bin/barrel/barrel.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/bin/barrel/barrel.blend -------------------------------------------------------------------------------- /pics/boundingvolume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/pics/boundingvolume.png -------------------------------------------------------------------------------- /bin/barrel/output/barrel.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/bin/barrel/output/barrel.glb -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "cesium/node", 3 | "globals": { 4 | "Promise": true 5 | } 6 | }; -------------------------------------------------------------------------------- /bin/barrel/output/Batchedbarrel/barrel.b3dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/bin/barrel/output/Batchedbarrel/barrel.b3dm -------------------------------------------------------------------------------- /bin/barrel/output/Instancedbarrel/barrel.i3dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/bin/barrel/output/Instancedbarrel/barrel.i3dm -------------------------------------------------------------------------------- /bin/barrel/output/BatchedTilesets/barrel_withCustomBatchTable.b3dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/bin/barrel/output/BatchedTilesets/barrel_withCustomBatchTable.b3dm -------------------------------------------------------------------------------- /bin/barrel/output/BatchedTilesets/barrel_withDefaultBatchTable.b3dm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrincessGod/objTo3d-tiles/HEAD/bin/barrel/output/BatchedTilesets/barrel_withDefaultBatchTable.b3dm -------------------------------------------------------------------------------- /bin/barrel/barrel.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'barrel.blend' 2 | # Material Count: 1 3 | 4 | newmtl wood 5 | Ns 96.078431 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.640000 0.336538 0.171980 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | map_Kd barrel.png 14 | -------------------------------------------------------------------------------- /bin/barrel/customTilesetOptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "longitude": -1.31968, 3 | "latitude": 0.698874, 4 | "transHeight": 0.0, 5 | "minHeight": 0.0, 6 | "maxHeight": 40.0, 7 | "tileWidth": 200.0, 8 | "tileHeight": 200.0, 9 | "geometricError": 200.0, 10 | "region": true, 11 | "box": false, 12 | "sphere": false 13 | } -------------------------------------------------------------------------------- /lib/Texture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Texture; 4 | 5 | /** 6 | * An object containing information about a texture. 7 | * 8 | * @private 9 | */ 10 | function Texture() { 11 | this.transparent = false; 12 | this.source = undefined; 13 | this.name = undefined; 14 | this.extension = undefined; 15 | this.path = undefined; 16 | this.pixels = undefined; 17 | this.width = undefined; 18 | this.height = undefined; 19 | } 20 | -------------------------------------------------------------------------------- /bin/barrel/customI3dmBatchTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": [ 3 | "center", 4 | "right", 5 | "left", 6 | "top", 7 | "bottom", 8 | "up", 9 | "right-top", 10 | "right-bottom", 11 | "left-top", 12 | "left-bottom" 13 | ], 14 | "id": [ 15 | 0, 16 | 1, 17 | 2, 18 | 3, 19 | 4, 20 | 5, 21 | 6, 22 | 7, 23 | 8, 24 | 9 25 | ] 26 | } -------------------------------------------------------------------------------- /lib/outsideDirectory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | 4 | module.exports = outsideDirectory; 5 | 6 | /** 7 | * Checks if a file is outside of a directory. 8 | * 9 | * @param {String} file Path to the file. 10 | * @param {String} directory Path to the directory. 11 | * @returns {Boolean} Whether the file is outside of the directory. 12 | * 13 | * @private 14 | */ 15 | function outsideDirectory(file, directory) { 16 | return (path.relative(directory, file).indexOf('..') === 0); 17 | } 18 | -------------------------------------------------------------------------------- /bin/barrel/output/barrel_batchTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "batchId": [ 3 | 0, 4 | 1, 5 | 2, 6 | 3, 7 | 4, 8 | 5, 9 | 6, 10 | 7, 11 | 8, 12 | 9, 13 | 10, 14 | 11, 15 | 12, 16 | 13 17 | ], 18 | "name": [ 19 | "Barrel.013", 20 | "Barrel.012", 21 | "Barrel.011", 22 | "Barrel.010", 23 | "Barrel.009", 24 | "Barrel.008", 25 | "Barrel.007", 26 | "Barrel.006", 27 | "Barrel.005", 28 | "Barrel.004", 29 | "Barrel.003", 30 | "Barrel.002", 31 | "Barrel.001", 32 | "Barrel" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /lib/tilesetOptionsUtility.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | getPoint3MinMax : getPoint3MinMax 5 | }; 6 | 7 | function getPoint3MinMax(points) { 8 | var min = new Array(3).fill(Number.POSITIVE_INFINITY); 9 | var max = new Array(3).fill(Number.NEGATIVE_INFINITY); 10 | for(var i = 0; i < points.length; i ++) { 11 | for (var j = 0; j < 3; j ++) { 12 | min[j] = Math.min(min[j], points[i][j]); 13 | max[j] = Math.max(max[j], points[i][j]); 14 | } 15 | } 16 | return { 17 | min : min, 18 | max : max 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /lib/getBufferPadded.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = getBufferPadded; 3 | 4 | /** 5 | * Pad the buffer to the next 4-byte boundary to ensure proper alignment for the section that follows. 6 | * 7 | * @param {Buffer} buffer The buffer. 8 | * @returns {Buffer} The padded buffer. 9 | * 10 | * @private 11 | */ 12 | function getBufferPadded(buffer) { 13 | var boundary = 4; 14 | var byteLength = buffer.length; 15 | var remainder = byteLength % boundary; 16 | if (remainder === 0) { 17 | return buffer; 18 | } 19 | var padding = (remainder === 0) ? 0 : boundary - remainder; 20 | var emptyBuffer = Buffer.alloc(padding); 21 | return Buffer.concat([buffer, emptyBuffer]); 22 | } 23 | -------------------------------------------------------------------------------- /bin/barrel/customFeatureTable.json: -------------------------------------------------------------------------------- 1 | { 2 | "position": [ 3 | [0, 0, 0], 4 | [20, 0, 0], 5 | [-20, 0, 0], 6 | [0, 20, 0], 7 | [0, -20, 0], 8 | [0, 0, 20], 9 | [20, 20, 0], 10 | [20, -20, 0], 11 | [-20, 20, 0], 12 | [-20, -20, 0] 13 | ], 14 | "orientation": [ 15 | [0, 0, 0], 16 | [0, 0, 0], 17 | [0, 0, 0], 18 | [0, 0, 0], 19 | [0, 0, 0], 20 | [0, 0, 0], 21 | [0, 0, 0], 22 | [0, 0, 0], 23 | [0, 0, 0], 24 | [0, 0, 0] 25 | ], 26 | "scale": [ 27 | [1, 1, 1], 28 | [1, 1, 1], 29 | [1, 1, 1], 30 | [1, 1, 1], 31 | [1, 1, 1], 32 | [1, 1, 1], 33 | [1, 1, 1], 34 | [1, 1, 1], 35 | [1, 1, 1], 36 | [1, 1, 1] 37 | ] 38 | } -------------------------------------------------------------------------------- /lib/readLines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fsExtra = require('fs-extra'); 3 | var Promise = require('bluebird'); 4 | var readline = require('readline'); 5 | 6 | module.exports = readLines; 7 | 8 | /** 9 | * Read a file line-by-line. 10 | * 11 | * @param {String} path Path to the file. 12 | * @param {Function} callback Function to call when reading each line. 13 | * @returns {Promise} A promise when the reader is finished. 14 | * 15 | * @private 16 | */ 17 | function readLines(path, callback) { 18 | return new Promise(function(resolve, reject) { 19 | var stream = fsExtra.createReadStream(path); 20 | stream.on('error', reject); 21 | stream.on('end', resolve); 22 | 23 | var lineReader = readline.createInterface({ 24 | input : stream 25 | }); 26 | lineReader.on('line', callback); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /lib/getJsonBufferPadded.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = getJsonBufferPadded; 3 | 4 | /** 5 | * Convert the JSON object to a padded buffer. 6 | * 7 | * Pad the JSON with extra whitespace to fit the next 4-byte boundary. This ensures proper alignment 8 | * for the section that follows. 9 | * 10 | * @param {Object} [json] The JSON object. 11 | * @returns {Buffer} The padded JSON buffer. 12 | * 13 | * @private 14 | */ 15 | function getJsonBufferPadded(json) { 16 | var string = JSON.stringify(json); 17 | 18 | var boundary = 4; 19 | var byteLength = Buffer.byteLength(string); 20 | var remainder = byteLength % boundary; 21 | var padding = (remainder === 0) ? 0 : boundary - remainder; 22 | var whitespace = ''; 23 | for (var i = 0; i < padding; ++i) { 24 | whitespace += ' '; 25 | } 26 | string += whitespace; 27 | 28 | return Buffer.from(string); 29 | } 30 | -------------------------------------------------------------------------------- /tools/BatchConvert.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 3 | echo batch convert obj To gltf 4 | echo 请选择要执行的操作,目前支持转换模型到gltf、glb、b3dm、b3dm瓦片,分别对应操作顺序1-4 5 | echo 其他操作还不支持 6 | echo q 退出 7 | echo. 8 | :cho 9 | set num= 10 | set para= 11 | set /p num=请选择需要执行的操作 12 | if "%num%"=="1" ( 13 | echo 转换为gltf 14 | ) else ( 15 | if "%num%"=="2" ( set para=-b ) else ( 16 | if "%num%"=="3" ( set para=--b3dm ) else ( 17 | if "%num%"=="4" ( set para=--tileset ) else ( 18 | if "%num%"=="q" ( exit ) else ( 19 | echo 选择无效,请重新输入 20 | goto cho 21 | ) 22 | ) 23 | ) 24 | ) 25 | ) 26 | 27 | echo 脚本中写入对应的路径 28 | set dirPath= 29 | set /p dirPath= 30 | echo 路径为:%dirPath% 31 | echo 开始转换 32 | for /r %dirPath% %%i in (*.obj) do ( 33 | echo %%i 34 | call obj23dtiles -i %%i %para% 35 | ) 36 | echo 转换完毕 37 | echo 继续选择 r ,其他则退出 38 | set again= 39 | set /p again= 40 | if "%again%"=="r" goto cho else exit 41 | pause 42 | -------------------------------------------------------------------------------- /bin/barrel/output/Batchedbarrel/tileset.json: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "version": "0.0", 4 | "tilesetVersion": "1.0.0-obj23dtiles", 5 | "gltfUpAxis": "Y" 6 | }, 7 | "geometricError": 200, 8 | "root": { 9 | "transform": [ 10 | 0.9686356343768792, 11 | 0.24848542777253732, 12 | 0, 13 | 0, 14 | -0.15986460744316267, 15 | 0.6231776117948786, 16 | 0.7655670914065438, 17 | 0, 18 | 0.19023226619673222, 19 | -0.7415555652426398, 20 | 0.6433560666966038, 21 | 0, 22 | 1215011.9192183807, 23 | -4736309.294663747, 24 | 4081601.9621787807, 25 | 1 26 | ], 27 | "boundingVolume": { 28 | "region": [ 29 | -1.3196812388922754, 30 | 0.6988730441359575, 31 | -1.3196785765456007, 32 | 0.6988749388279575, 33 | 0, 34 | 12.060076788067818 35 | ] 36 | }, 37 | "geometricError": 0, 38 | "refine": "ADD", 39 | "content": { 40 | "url": "barrel.b3dm" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bin/barrel/output/Instancedbarrel/tileset.json: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "version": "0.0", 4 | "tilesetVersion": "1.0.0-obj23dtiles", 5 | "gltfUpAxis": "Y" 6 | }, 7 | "geometricError": 200, 8 | "root": { 9 | "transform": [ 10 | 0.9686356343768792, 11 | 0.24848542777253732, 12 | 0, 13 | 0, 14 | -0.15986460744316267, 15 | 0.6231776117948786, 16 | 0.7655670914065438, 17 | 0, 18 | 0.19023226619673222, 19 | -0.7415555652426398, 20 | 0.6433560666966038, 21 | 0, 22 | 1215011.9192183807, 23 | -4736309.294663747, 24 | 4081601.9621787807, 25 | 1 26 | ], 27 | "boundingVolume": { 28 | "region": [ 29 | -1.3196853348102364, 30 | 0.6988698863159575, 31 | -1.3196744806276397, 32 | 0.6988780966479575, 33 | 0, 34 | 32.06007678806782 35 | ] 36 | }, 37 | "geometricError": 0, 38 | "refine": "ADD", 39 | "content": { 40 | "url": "barrel.i3dm" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obj23dtiles", 3 | "version": "1.0.0", 4 | "description": "obj to 3d tiles", 5 | "main": "lib/obj23dtiles.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "node ./tools/test.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/PrincessGod/objTo3d-tiles.git" 15 | }, 16 | "keywords": [ 17 | "3d-tiles", 18 | "cesium" 19 | ], 20 | "preferGlobal": true, 21 | "bin": { 22 | "obj23dtiles" : "bin/obj23dtiles.js" 23 | }, 24 | "author": "princessgod", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/PrincessGod/objTo3d-tiles/issues" 28 | }, 29 | "homepage": "https://github.com/PrincessGod/objTo3d-tiles#readme", 30 | "dependencies": { 31 | "bluebird": "^3.5.1", 32 | "cesium": "^1.38.0", 33 | "fs-extra": "^4.0.2", 34 | "jpeg-js": "^0.3.3", 35 | "mime": "^2.0.3", 36 | "pngjs": "^3.3.0", 37 | "yargs": "^10.0.3" 38 | }, 39 | "devDependencies": { 40 | "eslint": "^4.9.0", 41 | "eslint-config-cesium": "^2.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/getBufferPadded8Byte.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | 4 | var defaultValue = Cesium.defaultValue; 5 | var defined = Cesium.defined; 6 | 7 | module.exports = getBufferPadded; 8 | 9 | /** 10 | * Pad the buffer to the next 8-byte boundary to ensure proper alignment for the section that follows. 11 | * Padding is not required by the 3D Tiles spec but is important when using Typed Arrays in JavaScript. 12 | * 13 | * @param {Buffer} buffer The buffer. 14 | * @param {Number} [byteOffset=0] The byte offset on which the buffer starts. 15 | * @returns {Buffer} The padded buffer. 16 | * 17 | * @private 18 | */ 19 | function getBufferPadded(buffer, byteOffset) { 20 | if (!defined(buffer)) { 21 | return Buffer.alloc(0); 22 | } 23 | 24 | byteOffset = defaultValue(byteOffset, 0); 25 | 26 | var boundary = 8; 27 | var byteLength = buffer.length; 28 | var remainder = (byteOffset + byteLength) % boundary; 29 | var padding = (remainder === 0) ? 0 : boundary - remainder; 30 | var emptyBuffer = Buffer.alloc(padding); 31 | return Buffer.concat([buffer, emptyBuffer]); 32 | } 33 | -------------------------------------------------------------------------------- /bin/barrel/customBatchtable.json: -------------------------------------------------------------------------------- 1 | { 2 | "batchId": [ 3 | 0, 4 | 1, 5 | 2, 6 | 3, 7 | 4, 8 | 5, 9 | 6, 10 | 7, 11 | 8, 12 | 9, 13 | 10, 14 | 11, 15 | 12, 16 | 13 17 | ], 18 | "batchIdnew": [ 19 | 0, 20 | 1, 21 | 2, 22 | 3, 23 | 4, 24 | 5, 25 | 6, 26 | 7, 27 | 8, 28 | 9, 29 | 10, 30 | 11, 31 | 12, 32 | 13 33 | ], 34 | "newname": [ 35 | [ 36 | "Barrel" 37 | ], 38 | [ 39 | "Barrel" 40 | ], 41 | [ 42 | "Barrel" 43 | ], 44 | [ 45 | "Barrel" 46 | ], 47 | [ 48 | "Barrel" 49 | ], 50 | [ 51 | "Barrel" 52 | ], 53 | [ 54 | "Barrel" 55 | ], 56 | [ 57 | "Barrel" 58 | ], 59 | [ 60 | "Barrel" 61 | ], 62 | [ 63 | "Barrel" 64 | ], 65 | [ 66 | "Barrel" 67 | ], 68 | [ 69 | "Barrel" 70 | ], 71 | [ 72 | "Barrel" 73 | ], 74 | [ 75 | "Barrel" 76 | ] 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /.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 (http://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 | # Debug file 61 | bin/debug.js 62 | .vscode 63 | -------------------------------------------------------------------------------- /lib/getJsonBufferPadded8Byte.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | 4 | var defaultValue = Cesium.defaultValue; 5 | var defined = Cesium.defined; 6 | 7 | module.exports = getJsonBufferPadded; 8 | 9 | /** 10 | * Convert the JSON object to a padded buffer. 11 | * 12 | * Pad the JSON with extra whitespace to fit the next 8-byte boundary. This ensures proper alignment 13 | * for the section that follows (for example, batch table binary or feature table binary). 14 | * Padding is not required by the 3D Tiles spec but is important when using Typed Arrays in JavaScript. 15 | * 16 | * @param {Object} [json] The JSON object. 17 | * @param {Number} [byteOffset=0] The byte offset on which the buffer starts. 18 | * @returns {Buffer} The padded JSON buffer. 19 | * 20 | * @private 21 | */ 22 | function getJsonBufferPadded(json, byteOffset) { 23 | // Check for undefined or empty 24 | if (!defined(json) || Object.keys(json).length === 0) { 25 | return Buffer.alloc(0); 26 | } 27 | 28 | byteOffset = defaultValue(byteOffset, 0); 29 | var string = JSON.stringify(json); 30 | 31 | var boundary = 8; 32 | var byteLength = Buffer.byteLength(string); 33 | var remainder = (byteOffset + byteLength) % boundary; 34 | var padding = (remainder === 0) ? 0 : boundary - remainder; 35 | var whitespace = ''; 36 | for (var i = 0; i < padding; ++i) { 37 | whitespace += ' '; 38 | } 39 | string += whitespace; 40 | 41 | return Buffer.from(string); 42 | } 43 | -------------------------------------------------------------------------------- /tools/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var exec = util.promisify(require('child_process').exec); 5 | 6 | var commands = [ 7 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj', 8 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj -b', 9 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj --b3dm', 10 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj --b3dm --outputBatchTable', 11 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj -c ./bin/barrel/customBatchTable.json --b3dm', 12 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj -f ./bin/barrel/customFeatureTable.json --i3dm', 13 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj -f ./bin/barrel/customFeatureTable.json -c ./bin/barrel/customI3dmBatchTable.json --i3dm', 14 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj --tileset', 15 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj --tileset -p ./bin/barrel/customTilesetOptions.json -c ./bin/barrel/customBatchTable.json', 16 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj --tileset --i3dm -f ./bin/barrel/customFeatureTable.json', 17 | 'node ./bin/obj23dtiles.js -i ./bin/barrel/barrel.obj --tileset --i3dm -f ./bin/barrel/customFeatureTable.json -p ./bin/barrel/customTilesetOptions.json -c ./bin/barrel/customI3dmBatchTable.json', 18 | ]; 19 | 20 | var errcount = 0; 21 | var finished = 0; 22 | function logout(stdout, error, command) { 23 | if(error || stdout.stderr) { 24 | console.error(command , error, stdout.stderr); 25 | errcount ++; 26 | } else { 27 | console.log(command, stdout.stdout); 28 | } 29 | 30 | finished ++; 31 | if(finished === commands.length) {console.log('all down! ' + (errcount ? ('error: ' + error) : 'passed.'));} 32 | } 33 | 34 | function test() { 35 | commands.forEach( function(command) { 36 | exec(command).then( function(out, err) { logout(out, err, command); }); 37 | }); 38 | } 39 | 40 | test(); 41 | -------------------------------------------------------------------------------- /lib/gltfToGlb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var getJsonBufferPadded = require('./getJsonBufferPadded'); 4 | 5 | var defined = Cesium.defined; 6 | 7 | module.exports = gltfToGlb; 8 | 9 | /** 10 | * Convert a glTF to binary glTF. 11 | * 12 | * The glTF is expected to have a single buffer and all embedded resources stored in bufferViews. 13 | * 14 | * @param {Object} gltf The glTF asset. 15 | * @param {Buffer} binaryBuffer The binary buffer. 16 | * @returns {Buffer} The glb buffer. 17 | * 18 | * @private 19 | */ 20 | function gltfToGlb(gltf, binaryBuffer) { 21 | var buffer = gltf.buffers[0]; 22 | if (defined(buffer.uri)) { 23 | binaryBuffer = Buffer.alloc(0); 24 | } 25 | 26 | // Create padded binary scene string 27 | var jsonBuffer = getJsonBufferPadded(gltf); 28 | 29 | // Allocate buffer (Global header) + (JSON chunk header) + (JSON chunk) + (Binary chunk header) + (Binary chunk) 30 | var glbLength = 12 + 8 + jsonBuffer.length + 8 + binaryBuffer.length; 31 | var glb = Buffer.alloc(glbLength); 32 | 33 | // Write binary glTF header (magic, version, length) 34 | var byteOffset = 0; 35 | glb.writeUInt32LE(0x46546C67, byteOffset); 36 | byteOffset += 4; 37 | glb.writeUInt32LE(2, byteOffset); 38 | byteOffset += 4; 39 | glb.writeUInt32LE(glbLength, byteOffset); 40 | byteOffset += 4; 41 | 42 | // Write JSON Chunk header (length, type) 43 | glb.writeUInt32LE(jsonBuffer.length, byteOffset); 44 | byteOffset += 4; 45 | glb.writeUInt32LE(0x4E4F534A, byteOffset); // JSON 46 | byteOffset += 4; 47 | 48 | // Write JSON Chunk 49 | jsonBuffer.copy(glb, byteOffset); 50 | byteOffset += jsonBuffer.length; 51 | 52 | // Write Binary Chunk header (length, type) 53 | glb.writeUInt32LE(binaryBuffer.length, byteOffset); 54 | byteOffset += 4; 55 | glb.writeUInt32LE(0x004E4942, byteOffset); // BIN 56 | byteOffset += 4; 57 | 58 | // Write Binary Chunk 59 | binaryBuffer.copy(glb, byteOffset); 60 | return glb; 61 | } 62 | -------------------------------------------------------------------------------- /bin/barrel/output/BatchedTilesets/tileset.json: -------------------------------------------------------------------------------- 1 | { 2 | "asset": 3 | { 4 | "version": "0.0" 5 | }, 6 | "geometricError": 20, 7 | "root": 8 | { 9 | "boundingVolume": 10 | { 11 | "box": [ 12 | 0, 13 | 0, 14 | 15, 15 | 30, 16 | 0, 17 | 0, 18 | 0, 19 | 30, 20 | 0, 21 | 0, 22 | 0, 23 | 15 24 | ] 25 | }, 26 | "transform": [ 27 | 0.9686356343768792, 28 | 0.24848542777253735, 29 | 0, 30 | 0, -0.15986460744966327, 31 | 0.623177611820219, 32 | 0.765567091384559, 33 | 0, 34 | 0.19023226619126932, -0.7415555652213445, 35 | 0.6433560667227647, 36 | 0, 37 | 1215011.9317263428, -4736309.3434217675, 38 | 4081602.0044800863, 39 | 1 40 | ], 41 | "geometricError": 70, 42 | "refine": "ADD", 43 | "content": 44 | { 45 | "url": "barrel_withCustomBatchTable.b3dm" 46 | }, 47 | "children": [ 48 | { 49 | "boundingVolume": 50 | { 51 | "box": [ 52 | 0, 53 | 0, 54 | 10, 55 | 10, 56 | 0, 57 | 0, 58 | 0, 59 | 10, 60 | 0, 61 | 0, 62 | 0, 63 | 10 64 | ] 65 | }, 66 | "transform": [ 67 | 1.0, 0.0, 0.0, 0.0, 68 | 0.0, 1.0, 0.0, 0.0, 69 | 0.0, 0.0, 1.0, 0.0, 70 | -15.0, 0.0, 0.0, 1.0 71 | ], 72 | "geometricError": 0, 73 | "content": 74 | { 75 | "url": "barrel_withDefaultBatchTable.b3dm" 76 | } 77 | }] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/createI3dm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | 4 | var getJsonBufferPadded = require('./getJsonBufferPadded8Byte'); 5 | var getBufferPadded = require('./getBufferPadded8Byte'); 6 | 7 | var defined = Cesium.defined; 8 | 9 | module.exports = createI3dm; 10 | 11 | 12 | /** 13 | * 14 | * @param {Object} options An object contains following properties: 15 | * @param {Object} options.featureTableJson The feature table JSON. 16 | * @param {Buffer} options.featureTableBinary The feature table binary. 17 | * @param {Object} [options.batchTableJson] Batch table JSON. 18 | * @param {Buffer} [options.batchTableBianry] The batch table binary. 19 | * @param {Buffer} [options.glb] The binary glTF buffer. 20 | * @param {String} [options.url] Url to an external glTF model when options.glb is not specified. 21 | * @returns {Buffer} I3dm buffer. 22 | */ 23 | function createI3dm(options) { 24 | var featureTableJson = getJsonBufferPadded(options.featureTableJson); 25 | var featureTableBinary = getBufferPadded(options.featureTableBinary); 26 | var batchTableJson = getJsonBufferPadded(options.batchTableJson); 27 | var batchTableBianry = getBufferPadded(options.batchTableBianry); 28 | 29 | var gltfFormat = defined(options.glb) ? 1 : 0; 30 | var gltfBuffer = defined(options.glb) ? options.glb : getGltfUrlBuffer(options.url); 31 | 32 | var verison = 1; 33 | var headerByteLength = 32; 34 | var featureTableJsonByteLength = featureTableJson.length; 35 | var featureTableBinaryByteLength = featureTableBinary.length; 36 | var batchTableJsonByteLength = batchTableJson.length; 37 | var batchTableBianryByteLength = batchTableBianry.length; 38 | var gltfByteLength = gltfBuffer.length; 39 | var byteLength = headerByteLength + featureTableJsonByteLength + featureTableBinaryByteLength + batchTableJsonByteLength + batchTableBianryByteLength + gltfByteLength; 40 | 41 | var header = Buffer.alloc(headerByteLength); 42 | header.write('i3dm', 0); 43 | header.writeUInt32LE(verison, 4); 44 | header.writeUInt32LE(byteLength, 8); 45 | header.writeUInt32LE(featureTableJsonByteLength, 12); 46 | header.writeUInt32LE(featureTableBinaryByteLength, 16); 47 | header.writeUInt32LE(batchTableJsonByteLength, 20); 48 | header.writeUInt32LE(batchTableBianryByteLength, 24); 49 | header.writeUInt32LE(gltfFormat, 28); 50 | 51 | return Buffer.concat([header, featureTableJson, featureTableBinary, batchTableJson, batchTableBianry, gltfBuffer]); 52 | } 53 | 54 | function getGltfUrlBuffer(url) { 55 | url = url.replace(/\\/g, '/'); 56 | return Buffer.from(url); 57 | } 58 | -------------------------------------------------------------------------------- /lib/obj2b3dm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var createB3dm = require('./createB3dm'); 3 | var obj2gltf = require('./obj2gltf'); 4 | 5 | module.exports = obj2B3dm; 6 | 7 | /** 8 | * Convert obj model to b3dm file. 9 | * 10 | * @param {String} objPath The obj model file path. 11 | * @param {String} outputPath Output file path. 12 | * @param {Object} options Optional parameters. 13 | */ 14 | function obj2B3dm(objPath, options) { 15 | return obj2gltf(objPath, options) 16 | .then(function (result) { 17 | var glb = result.gltf; 18 | var batchTableJson = result.batchTableJson; 19 | var customBatchTable = batchTableJson; 20 | var length = batchTableJson.maxPoint.length; // maxPoint always be there 21 | return new Promise(function (resolve, reject) { 22 | if (options.customBatchTable) { 23 | length = options.customBatchTable[Object.keys(options.customBatchTable)[0]].length; 24 | if (length !== batchTableJson.maxPoint.length ) { 25 | reject('Custom BatchTable properties\'s length should be equals to default BatchTable \'batchId\' length.'); 26 | } 27 | customBatchTable = options.customBatchTable; 28 | } 29 | resolve({ 30 | b3dm : createB3dm({ 31 | glb: glb, 32 | featureTableJson: { 33 | BATCH_LENGTH: length 34 | }, 35 | batchTableJson: customBatchTable 36 | }), 37 | batchTableJson : batchTableJson 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | /** 44 | * Default value for optional pramater. 45 | */ 46 | obj2B3dm.defaults = { 47 | /** 48 | * Gets or sets whether add the _BATCHID semantic to gltf per-model's attributes. 49 | * If true, _BATCHID begin from 0 for first mesh and add one for the next. 50 | * @type Boolean 51 | * @default false 52 | */ 53 | batchId: false, 54 | /** 55 | * Gets or sets whether create b3dm model file, with _BATCHID and default batch table per-mesh. 56 | * @type Boolean 57 | * @default false 58 | */ 59 | b3dm: false, 60 | /** 61 | * Gets or sets whether create BtchTable Json file. 62 | * @type Boolean 63 | * @default false 64 | */ 65 | outputBatchTable: false, 66 | /** 67 | * Sets the default BatchTable object, should have proper property "batchId" Array. 68 | * @type Object 69 | * @default undefined 70 | */ 71 | customBatchTable: undefined 72 | }; 73 | -------------------------------------------------------------------------------- /lib/ArrayStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | 4 | var ComponentDatatype = Cesium.ComponentDatatype; 5 | 6 | module.exports = ArrayStorage; 7 | 8 | var initialLength = 1024; // 2^10 9 | var doublingThreshold = 33554432; // 2^25 (~134 MB for a Float32Array) 10 | var fixedExpansionLength = 33554432; // 2^25 (~134 MB for a Float32Array) 11 | 12 | /** 13 | * Provides expandable typed array storage for geometry data. This is preferable to JS arrays which are 14 | * stored with double precision. The resizing mechanism is similar to std::vector. 15 | * 16 | * @param {ComponentDatatype} componentDatatype The data type. 17 | * 18 | * @private 19 | */ 20 | function ArrayStorage(componentDatatype) { 21 | this.componentDatatype = componentDatatype; 22 | this.typedArray = ComponentDatatype.createTypedArray(componentDatatype, 0); 23 | this.length = 0; 24 | } 25 | 26 | function resize(storage, length) { 27 | var typedArray = ComponentDatatype.createTypedArray(storage.componentDatatype, length); 28 | typedArray.set(storage.typedArray); 29 | storage.typedArray = typedArray; 30 | } 31 | 32 | ArrayStorage.prototype.push = function(value) { 33 | var length = this.length; 34 | var typedArrayLength = this.typedArray.length; 35 | 36 | if (length === 0) { 37 | resize(this, initialLength); 38 | } else if (length === typedArrayLength) { 39 | if (length < doublingThreshold) { 40 | resize(this, typedArrayLength * 2); 41 | } else { 42 | resize(this, typedArrayLength + fixedExpansionLength); 43 | } 44 | } 45 | 46 | this.typedArray[this.length++] = value; 47 | }; 48 | 49 | ArrayStorage.prototype.get = function(index) { 50 | return this.typedArray[index]; 51 | }; 52 | 53 | var sizeOfUint16 = 2; 54 | var sizeOfUint32 = 4; 55 | var sizeOfFloat = 4; 56 | 57 | ArrayStorage.prototype.toUint16Buffer = function() { 58 | var length = this.length; 59 | var typedArray = this.typedArray; 60 | var paddedLength = length + ((length % 2 === 0) ? 0 : 1); // Round to next multiple of 2 61 | var buffer = Buffer.alloc(paddedLength * sizeOfUint16); 62 | for (var i = 0; i < length; ++i) { 63 | buffer.writeUInt16LE(typedArray[i], i * sizeOfUint16); 64 | } 65 | return buffer; 66 | }; 67 | 68 | ArrayStorage.prototype.toUint32Buffer = function() { 69 | var length = this.length; 70 | var typedArray = this.typedArray; 71 | var buffer = Buffer.alloc(length * sizeOfUint32); 72 | for (var i = 0; i < length; ++i) { 73 | buffer.writeUInt32LE(typedArray[i], i * sizeOfUint32); 74 | } 75 | return buffer; 76 | }; 77 | 78 | ArrayStorage.prototype.toFloatBuffer = function() { 79 | var length = this.length; 80 | var typedArray = this.typedArray; 81 | var buffer = Buffer.alloc(length * sizeOfFloat); 82 | for (var i = 0; i < length; ++i) { 83 | buffer.writeFloatLE(typedArray[i], i * sizeOfFloat); 84 | } 85 | return buffer; 86 | }; 87 | 88 | ArrayStorage.prototype.getMinMax = function(components) { 89 | var length = this.length; 90 | var typedArray = this.typedArray; 91 | var count = length / components; 92 | var min = new Array(components).fill(Number.POSITIVE_INFINITY); 93 | var max = new Array(components).fill(Number.NEGATIVE_INFINITY); 94 | for (var i = 0; i < count; ++i) { 95 | for (var j = 0; j < components; ++j) { 96 | var index = i * components + j; 97 | var value = typedArray[index]; 98 | min[j] = Math.min(min[j], value); 99 | max[j] = Math.max(max[j], value); 100 | } 101 | } 102 | return { 103 | min : min, 104 | max : max 105 | }; 106 | }; 107 | -------------------------------------------------------------------------------- /lib/loadTexture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var fsExtra = require('fs-extra'); 4 | var jpeg = require('jpeg-js'); 5 | var path = require('path'); 6 | var PNG = require('pngjs').PNG; 7 | var Promise = require('bluebird'); 8 | var Texture = require('./Texture'); 9 | 10 | var defaultValue = Cesium.defaultValue; 11 | var defined = Cesium.defined; 12 | 13 | module.exports = loadTexture; 14 | 15 | /** 16 | * Load a texture file. 17 | * 18 | * @param {String} texturePath Path to the texture file. 19 | * @param {Object} [options] An object with the following properties: 20 | * @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. 21 | * @param {Boolean} [options.decode=false] Whether to decode the texture. 22 | * @returns {Promise} A promise resolving to a Texture object. 23 | * 24 | * @private 25 | */ 26 | function loadTexture(texturePath, options) { 27 | options = defaultValue(options, {}); 28 | options.checkTransparency = defaultValue(options.checkTransparency, false); 29 | options.decode = defaultValue(options.decode, false); 30 | 31 | return fsExtra.readFile(texturePath) 32 | .then(function(source) { 33 | var name = path.basename(texturePath, path.extname(texturePath)); 34 | var extension = path.extname(texturePath).toLowerCase(); 35 | var texture = new Texture(); 36 | texture.source = source; 37 | texture.name = name; 38 | texture.extension = extension; 39 | texture.path = texturePath; 40 | 41 | var decodePromise; 42 | if (extension === '.png') { 43 | decodePromise = decodePng(texture, options); 44 | } else if (extension === '.jpg' || extension === '.jpeg') { 45 | decodePromise = decodeJpeg(texture, options); 46 | } 47 | 48 | if (defined(decodePromise)) { 49 | return decodePromise.thenReturn(texture); 50 | } 51 | 52 | return texture; 53 | }); 54 | } 55 | 56 | function hasTransparency(pixels) { 57 | var pixelsLength = pixels.length / 4; 58 | for (var i = 0; i < pixelsLength; ++i) { 59 | if (pixels[i * 4 + 3] < 255) { 60 | return true; 61 | } 62 | } 63 | return false; 64 | } 65 | 66 | function getChannels(colorType) { 67 | switch (colorType) { 68 | case 0: // greyscale 69 | return 1; 70 | case 2: // RGB 71 | return 3; 72 | case 4: // greyscale + alpha 73 | return 2; 74 | case 6: // RGB + alpha 75 | return 4; 76 | default: 77 | return 3; 78 | } 79 | } 80 | 81 | function parsePng(data) { 82 | return new Promise(function(resolve, reject) { 83 | new PNG().parse(data, function(error, decodedResults) { 84 | if (defined(error)) { 85 | reject(error); 86 | return; 87 | } 88 | resolve(decodedResults); 89 | }); 90 | }); 91 | } 92 | 93 | function decodePng(texture, options) { 94 | // Color type is encoded in the 25th bit of the png 95 | var source = texture.source; 96 | var colorType = source[25]; 97 | var channels = getChannels(colorType); 98 | 99 | var checkTransparency = (channels === 4 && options.checkTransparency); 100 | var decode = options.decode || checkTransparency; 101 | 102 | if (decode) { 103 | return parsePng(source) 104 | .then(function(decodedResults) { 105 | if (options.checkTransparency) { 106 | texture.transparent = hasTransparency(decodedResults.data); 107 | } 108 | if (options.decode) { 109 | texture.pixels = decodedResults.data; 110 | texture.width = decodedResults.width; 111 | texture.height = decodedResults.height; 112 | texture.source = undefined; // Unload resources 113 | } 114 | }); 115 | } 116 | } 117 | 118 | function decodeJpeg(texture, options) { 119 | if (options.decode) { 120 | var source = texture.source; 121 | var decodedResults = jpeg.decode(source); 122 | texture.pixels = decodedResults.data; 123 | texture.width = decodedResults.width; 124 | texture.height = decodedResults.height; 125 | texture.source = undefined; // Unload resources 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/createB3dm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var getBufferPadded = require('./getBufferPadded8Byte'); 4 | var getJsonBufferPadded = require('./getJsonBufferPadded8Byte'); 5 | 6 | var defaultValue = Cesium.defaultValue; 7 | 8 | module.exports = createB3dm; 9 | 10 | /** 11 | * Create a Batched 3D Model (b3dm) tile from a binary glTF and per-feature metadata. 12 | * 13 | * @param {Object} options An object with the following properties: 14 | * @param {Buffer} options.glb The binary glTF buffer. 15 | * @param {Object} [options.featureTableJson] Feature table JSON. 16 | * @param {Buffer} [options.featureTableBinary] Feature table binary. 17 | * @param {Object} [options.batchTableJson] Batch table describing the per-feature metadata. 18 | * @param {Buffer} [options.batchTableBinary] The batch table binary. 19 | * @param {Boolean} [options.deprecated1=false] Save the b3dm with the deprecated 20-byte header. 20 | * @param {Boolean} [options.deprecated2=false] Save the b3dm with the deprecated 24-byte header. 21 | * @returns {Buffer} The generated b3dm tile buffer. 22 | */ 23 | function createB3dm(options) { 24 | var glb = options.glb; 25 | var defaultFeatureTable = { 26 | BATCH_LENGTH : 0 27 | }; 28 | var featureTableJson = defaultValue(options.featureTableJson, defaultFeatureTable); 29 | var batchLength = featureTableJson.BATCH_LENGTH; 30 | 31 | var headerByteLength = 28; 32 | var featureTableJsonBuffer = getJsonBufferPadded(featureTableJson, headerByteLength); 33 | var featureTableBinary = getBufferPadded(options.featureTableBinary); 34 | var batchTableJsonBuffer = getJsonBufferPadded(options.batchTableJson); 35 | var batchTableBinary = getBufferPadded(options.batchTableBinary); 36 | 37 | var deprecated1 = defaultValue(options.deprecated1, false); 38 | var deprecated2 = defaultValue(options.deprecated2, false); 39 | 40 | if (deprecated1) { 41 | return createB3dmDeprecated1(glb, batchLength, batchTableJsonBuffer); 42 | } else if (deprecated2) { 43 | return createB3dmDeprecated2(glb, batchLength, batchTableJsonBuffer, batchTableBinary); 44 | } 45 | 46 | return createB3dmCurrent(glb, featureTableJsonBuffer, featureTableBinary, batchTableJsonBuffer, batchTableBinary); 47 | } 48 | 49 | function createB3dmCurrent(glb, featureTableJson, featureTableBinary, batchTableJson, batchTableBinary) { 50 | var version = 1; 51 | var headerByteLength = 28; 52 | var featureTableJsonByteLength = featureTableJson.length; 53 | var featureTableBinaryByteLength = featureTableBinary.length; 54 | var batchTableJsonByteLength = batchTableJson.length; 55 | var batchTableBinaryByteLength = batchTableBinary.length; 56 | var gltfByteLength = glb.length; 57 | var byteLength = headerByteLength + featureTableJsonByteLength + featureTableBinaryByteLength + batchTableJsonByteLength + batchTableBinaryByteLength + gltfByteLength; 58 | 59 | var header = Buffer.alloc(headerByteLength); 60 | header.write('b3dm', 0); 61 | header.writeUInt32LE(version, 4); 62 | header.writeUInt32LE(byteLength, 8); 63 | header.writeUInt32LE(featureTableJsonByteLength, 12); 64 | header.writeUInt32LE(featureTableBinaryByteLength, 16); 65 | header.writeUInt32LE(batchTableJsonByteLength, 20); 66 | header.writeUInt32LE(batchTableBinaryByteLength, 24); 67 | 68 | return Buffer.concat([header, featureTableJson, featureTableBinary, batchTableJson, batchTableBinary, glb]); 69 | } 70 | 71 | function createB3dmDeprecated1(glb, batchLength, batchTableJson) { 72 | var version = 1; 73 | var headerByteLength = 20; 74 | var batchTableJsonByteLength = batchTableJson.length; 75 | var gltfByteLength = glb.length; 76 | var byteLength = headerByteLength + batchTableJsonByteLength + gltfByteLength; 77 | 78 | var header = Buffer.alloc(headerByteLength); 79 | header.write('b3dm', 0); 80 | header.writeUInt32LE(version, 4); 81 | header.writeUInt32LE(byteLength, 8); 82 | header.writeUInt32LE(batchLength, 12); 83 | header.writeUInt32LE(batchTableJsonByteLength, 16); 84 | 85 | return Buffer.concat([header, batchTableJson, glb]); 86 | } 87 | 88 | function createB3dmDeprecated2(glb, batchLength, batchTableJson, batchTableBinary) { 89 | var version = 1; 90 | var headerByteLength = 24; 91 | var batchTableJsonByteLength = batchTableJson.length; 92 | var batchTableBinaryByteLength = batchTableBinary.length; 93 | var gltfByteLength = glb.length; 94 | var byteLength = headerByteLength + batchTableJsonByteLength + batchTableBinaryByteLength + gltfByteLength; 95 | 96 | var header = Buffer.alloc(headerByteLength); 97 | header.write('b3dm', 0); 98 | header.writeUInt32LE(version, 4); 99 | header.writeUInt32LE(byteLength, 8); 100 | header.writeUInt32LE(batchTableJsonByteLength, 12); 101 | header.writeUInt32LE(batchTableBinaryByteLength, 16); 102 | header.writeUInt32LE(batchLength, 20); 103 | 104 | return Buffer.concat([header, batchTableJson, batchTableBinary, glb]); 105 | } 106 | -------------------------------------------------------------------------------- /lib/combineTileset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var fsExtra = require('fs-extra'); 4 | var path = require('path'); 5 | 6 | var defaultValue = Cesium.defaultValue; 7 | var defined = Cesium.defined; 8 | 9 | module.exports = combineTileset; 10 | 11 | /** 12 | * Combie tileset into one tileset json. 13 | * 14 | * @param {Object} options Object with following properties. 15 | * @param {String} options.inputDir Input directory include tilesets. 16 | * @param {String} [options.outputTileset="tileset.json"] Output tileset file path. 17 | */ 18 | function combineTileset(options) { 19 | var west = Number.POSITIVE_INFINITY; 20 | var south = Number.POSITIVE_INFINITY; 21 | var north = Number.NEGATIVE_INFINITY; 22 | var east = Number.NEGATIVE_INFINITY; 23 | var minheight = Number.POSITIVE_INFINITY; 24 | var maxheight = Number.NEGATIVE_INFINITY; 25 | var inputDir = defaultValue(options.inputDir, './'); 26 | var outputTileset = defaultValue(options.outputDir, path.join(inputDir, 'tileset.json')); 27 | 28 | var geometricError = 500; 29 | var children = []; 30 | var promises = []; 31 | var jsonFiles = []; 32 | inputDir = path.normalize(inputDir); 33 | outputTileset = path.normalize(outputTileset); 34 | var outputDir = path.dirname(outputTileset); 35 | 36 | getJsonFiles(inputDir, jsonFiles); 37 | jsonFiles.forEach(function(jsonFile) { 38 | var promise = fsExtra.readJson(jsonFile) 39 | .then(function(json) { 40 | if(!json.root) {return Promise.resolve();} 41 | var boundingVolume = json.root.boundingVolume; 42 | var geometricError = json.geometricError; 43 | var refine = json.root.refine; 44 | 45 | if (defined(boundingVolume) && defined(geometricError)) { 46 | // Use external tileset instand of b3dm. 47 | var url = path.relative(outputDir, jsonFile); 48 | url = url.replace(/\\/g, '/'); 49 | 50 | // Only support region for now. 51 | if(boundingVolume.region) { 52 | west = Math.min(west, boundingVolume.region[0]); 53 | south = Math.min(south, boundingVolume.region[1]); 54 | east = Math.max(east, boundingVolume.region[2]); 55 | north = Math.max(north, boundingVolume.region[3]); 56 | minheight = Math.min(minheight, boundingVolume.region[4]); 57 | maxheight = Math.max(maxheight, boundingVolume.region[5]); 58 | } 59 | 60 | var child = { 61 | 'boundingVolume': boundingVolume, 62 | 'geometricError': geometricError, 63 | 'refine': refine, 64 | 'content': { 65 | 'url': url 66 | } 67 | }; 68 | children.push(child); 69 | } 70 | }) 71 | .catch(function(err) { 72 | throw Error(err); 73 | }); 74 | 75 | promises.push(promise); 76 | }); 77 | 78 | return Promise.all(promises).then(function() { 79 | var tileset = { 80 | 'asset': { 81 | 'version': '0.0', 82 | 'tilesetVersion': '1.0.0-obj23dtiles', 83 | }, 84 | 'geometricError': geometricError, 85 | 'root': { 86 | 'boundingVolume': { 87 | 'region': [ 88 | west, 89 | south, 90 | east, 91 | north, 92 | minheight, 93 | maxheight 94 | ] 95 | }, 96 | 'refine': 'ADD', 97 | 'geometricError': geometricError, 98 | 'children': children 99 | } 100 | }; 101 | 102 | return Promise.resolve({ 103 | tileset: tileset, 104 | output: outputTileset 105 | }); 106 | }); 107 | } 108 | 109 | function getJsonFiles(dir, jsonFiles) { 110 | var files = fsExtra.readdirSync(dir); 111 | files.forEach(function (itm) { 112 | var fullpath = path.join(dir, itm); 113 | var stat = fsExtra.statSync(fullpath); 114 | if (stat.isDirectory()) { 115 | readFileList(fullpath, jsonFiles); 116 | } 117 | }); 118 | } 119 | 120 | function readFileList(dir, jsonFiles) { 121 | var files = fsExtra.readdirSync(dir); 122 | files.forEach(function (itm) { 123 | var fullpath = path.join(dir, itm); 124 | var stat = fsExtra.statSync(fullpath); 125 | if (stat.isDirectory()) { 126 | readFileList(fullpath, jsonFiles); 127 | } else { 128 | var ext = path.extname(fullpath); 129 | if (ext === '.json'){ 130 | jsonFiles.push(fullpath); 131 | } 132 | } 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /lib/createSingleTileset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Cesium = require('cesium'); 4 | 5 | var Cartesian3 = Cesium.Cartesian3; 6 | var defined = Cesium.defined; 7 | var defaultValue = Cesium.defaultValue; 8 | var HeadingPitchRoll = Cesium.HeadingPitchRoll; 9 | var Matrix4 = Cesium.Matrix4; 10 | var Transforms = Cesium.Transforms; 11 | 12 | module.exports = createSingleTileset; 13 | 14 | /** 15 | * Create a tileset JSON. 16 | * 17 | * @param {Object} options The object have follow properties. 18 | * @param {String} options.tileName The tile name of root. 19 | * @param {Number} [options.longitude=-1.31968] The longitute of tile origin point. 20 | * @param {Number} [options.latitude=0.698874] The latitute of tile origin point. 21 | * @param {Number} [options.minHeight=0.0] The minimum height of the tile. 22 | * @param {Number} [options.maxHeight=40.0] The maximum height of the tile. 23 | * @param {Number} [options.tileWidth=200.0] The horizontal length (cross longitude) of tile. 24 | * @param {Number} [options.tileHeight=200.0] The virtical length (cross latitude) of tile. 25 | * @param {Number} [options.transHeight=0.0] The transform height of the tile. 26 | * @param {String} [options.gltfUpAxis="Y"] The up axis of model. 27 | * @param {Object} [options.properties] Pre-model properties. 28 | * @param {Number} [options.geometricError = 200.0] The geometric error of tile. 29 | * @param {Matrix4} [options.transfrom] The tile transform. 30 | * @param {Boolean} [options.region = true] Using bounding region for tile. 31 | * @param {Boolean} [options.box] Using bounding box for tile. 32 | * @param {Boolean} [options.sphere] Using bounding sphere for tile. 33 | * 34 | */ 35 | function createSingleTileset(options) { 36 | var longitude = defaultValue(options.longitude, -1.31968); 37 | var latitude = defaultValue(options.latitude, 0.698874); 38 | var minHeight = defaultValue(options.minHeight, 0.0); 39 | var maxHeight = defaultValue(options.maxHeight, 40.0); 40 | var transHeight = defaultValue(options.transHeight, 0.0); 41 | var tileWidth = defaultValue(options.tileWidth, 200.0); 42 | var tileHeight = defaultValue(options.tileHeight, 200.0); 43 | var offsetX = defaultValue(options.offsetX, 0.0); 44 | var offsetY = defaultValue(options.offsetY, 0.0); 45 | var upAxis = defaultValue(options.gltfUpAxis, 'Y'); 46 | var properties = defaultValue(options.properties, undefined); 47 | var geometricError = defaultValue(options.geometricError, 200.0); 48 | var tileTransform = wgs84Transform(longitude, latitude, transHeight); 49 | var transform = defaultValue(options.transfrom, tileTransform); 50 | var transformArray = (defined(transform) && !Matrix4.equals(transform, Matrix4.IDENTITY)) ? Matrix4.pack(transform, new Array(16)) : undefined; 51 | var height = maxHeight - minHeight; 52 | 53 | if(!(options.region||options.box||options.sphere)) { 54 | options.region = true; 55 | } 56 | var boundingVolume; 57 | if(options.region) { 58 | var longitudeExtent = metersToLongitude(tileWidth, latitude); 59 | var latitudeExtent = metersToLatitude(tileHeight); 60 | 61 | var west = longitude - longitudeExtent / 2 + offsetX / tileWidth * longitudeExtent; 62 | var south = latitude - latitudeExtent / 2 - offsetY / tileHeight * latitudeExtent; 63 | var east = longitude + longitudeExtent / 2 + offsetX / tileWidth * longitudeExtent; 64 | var north = latitude + latitudeExtent / 2 - offsetY / tileHeight * latitudeExtent; 65 | 66 | boundingVolume = { 67 | region : [ 68 | west, 69 | south, 70 | east, 71 | north, 72 | minHeight, 73 | maxHeight 74 | ] 75 | }; 76 | } 77 | else if (options.box) { 78 | boundingVolume = { 79 | box : [ 80 | offsetX, -offsetY, height / 2 + minHeight, // center 81 | tileWidth / 2, 0, 0, // width 82 | 0, tileHeight / 2, 0, // depth 83 | 0, 0, height / 2 // height 84 | ] 85 | }; 86 | } 87 | else if (options.sphere) { 88 | boundingVolume = { 89 | sphere : [ 90 | offsetX, -offsetY, height / 2 + minHeight, 91 | Math.sqrt(tileWidth * tileWidth / 4 + tileHeight * tileHeight / 4 + height * height / 4) 92 | ] 93 | }; 94 | } 95 | 96 | var tilesetJson = { 97 | asset : { 98 | version : '0.0', 99 | tilesetVersion : '1.0.0-obj23dtiles', 100 | gltfUpAxis : upAxis 101 | }, 102 | properties : properties, 103 | geometricError : geometricError, 104 | root : { 105 | transform : transformArray, 106 | boundingVolume : boundingVolume, 107 | geometricError : 0.0, 108 | refine : 'ADD', 109 | content : { 110 | url : options.tileName 111 | } 112 | } 113 | }; 114 | 115 | return tilesetJson; 116 | } 117 | 118 | function metersToLongitude(meters, latitude) { 119 | return meters * 0.000000156785 / Math.cos(latitude); 120 | } 121 | 122 | function metersToLatitude(meters) { 123 | return meters * 0.000000157891; 124 | } 125 | 126 | function wgs84Transform(longitude, latitude, height) { 127 | return Transforms.headingPitchRollToFixedFrame(Cartesian3.fromRadians(longitude, latitude, height), new HeadingPitchRoll()); 128 | } 129 | -------------------------------------------------------------------------------- /lib/writeGltf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var mime = require('mime'); 4 | var PNG = require('pngjs').PNG; 5 | var Promise = require('bluebird'); 6 | var getBufferPadded = require('./getBufferPadded'); 7 | var gltfToGlb = require('./gltfToGlb'); 8 | 9 | var defined = Cesium.defined; 10 | var RuntimeError = Cesium.RuntimeError; 11 | 12 | module.exports = writeGltf; 13 | 14 | /** 15 | * Write glTF resources as embedded data uris or external files. 16 | * 17 | * @param {Object} gltf The glTF asset. 18 | * @param {Object} options The options object passed along from lib/obj2gltf.js 19 | * @returns {Promise} A promise that resolves to the glTF JSON or glb buffer. 20 | * 21 | * @private 22 | */ 23 | function writeGltf(gltf, options) { 24 | return encodeTextures(gltf) 25 | .then(function() { 26 | var binary = options.binary; 27 | var separate = options.separate; 28 | var separateTextures = options.separateTextures; 29 | 30 | var promises = []; 31 | if (separateTextures) { 32 | promises.push(writeSeparateTextures(gltf, options)); 33 | } else { 34 | writeEmbeddedTextures(gltf); 35 | } 36 | 37 | if (separate) { 38 | promises.push(writeSeparateBuffer(gltf, options)); 39 | } else if (!binary) { 40 | writeEmbeddedBuffer(gltf); 41 | } 42 | 43 | var binaryBuffer = gltf.buffers[0].extras._obj2gltf.source; 44 | 45 | return Promise.all(promises) 46 | .then(function() { 47 | deleteExtras(gltf); 48 | removeEmpty(gltf); 49 | if (binary) { 50 | return gltfToGlb(gltf, binaryBuffer); 51 | } 52 | return gltf; 53 | }); 54 | }); 55 | } 56 | 57 | function encodePng(texture) { 58 | // Constants defined by pngjs 59 | var rgbColorType = 2; 60 | var rgbaColorType = 6; 61 | 62 | var png = new PNG({ 63 | width : texture.width, 64 | height : texture.height, 65 | colorType : texture.transparent ? rgbaColorType : rgbColorType, 66 | inputColorType : rgbaColorType, 67 | inputHasAlpha : true 68 | }); 69 | 70 | png.data = texture.pixels; 71 | 72 | return new Promise(function(resolve, reject) { 73 | var chunks = []; 74 | var stream = png.pack(); 75 | stream.on('data', function(chunk) { 76 | chunks.push(chunk); 77 | }); 78 | stream.on('end', function() { 79 | resolve(Buffer.concat(chunks)); 80 | }); 81 | stream.on('error', reject); 82 | }); 83 | } 84 | 85 | function encodeTexture(texture) { 86 | if (!defined(texture.source) && defined(texture.pixels) && texture.extension === '.png') { 87 | return encodePng(texture) 88 | .then(function(encoded) { 89 | texture.source = encoded; 90 | }); 91 | } 92 | } 93 | 94 | function encodeTextures(gltf) { 95 | // Dynamically generated PBR textures need to be encoded to png prior to being saved 96 | var encodePromises = []; 97 | var images = gltf.images; 98 | var length = images.length; 99 | for (var i = 0; i < length; ++i) { 100 | encodePromises.push(encodeTexture(images[i].extras._obj2gltf)); 101 | } 102 | return Promise.all(encodePromises); 103 | } 104 | 105 | function deleteExtras(gltf) { 106 | var buffer = gltf.buffers[0]; 107 | delete buffer.extras; 108 | 109 | var images = gltf.images; 110 | var imagesLength = images.length; 111 | for (var i = 0; i < imagesLength; ++i) { 112 | delete images[i].extras; 113 | } 114 | } 115 | 116 | function removeEmpty(json) { 117 | Object.keys(json).forEach(function(key) { 118 | if (!defined(json[key]) || (Array.isArray(json[key]) && json[key].length === 0)) { 119 | delete json[key]; // Delete values that are undefined or [] 120 | } else if (typeof json[key] === 'object') { 121 | removeEmpty(json[key]); 122 | } 123 | }); 124 | } 125 | 126 | function writeSeparateBuffer(gltf, options) { 127 | var buffer = gltf.buffers[0]; 128 | var source = buffer.extras._obj2gltf.source; 129 | var bufferUri = buffer.name + '.bin'; 130 | buffer.uri = bufferUri; 131 | return options.writer(bufferUri, source); 132 | } 133 | 134 | function writeSeparateTextures(gltf, options) { 135 | var images = gltf.images; 136 | return Promise.map(images, function(image) { 137 | var texture = image.extras._obj2gltf; 138 | var imageUri = image.name + texture.extension; 139 | image.uri = imageUri; 140 | return options.writer(imageUri, texture.source); 141 | }, {concurrency : 10}); 142 | } 143 | 144 | function writeEmbeddedBuffer(gltf) { 145 | var buffer = gltf.buffers[0]; 146 | var source = buffer.extras._obj2gltf.source; 147 | 148 | // Buffers larger than ~192MB cannot be base64 encoded due to a NodeJS limitation. Source: https://github.com/nodejs/node/issues/4266 149 | if (source.length > 201326580) { 150 | throw new RuntimeError('Buffer is too large to embed in the glTF. Use the --separate flag instead.'); 151 | } 152 | 153 | buffer.uri = 'data:application/octet-stream;base64,' + source.toString('base64'); 154 | } 155 | 156 | function writeEmbeddedTextures(gltf) { 157 | var buffer = gltf.buffers[0]; 158 | var bufferExtras = buffer.extras._obj2gltf; 159 | var bufferSource = bufferExtras.source; 160 | var images = gltf.images; 161 | var imagesLength = images.length; 162 | var sources = [bufferSource]; 163 | var byteOffset = bufferSource.length; 164 | 165 | for (var i = 0; i < imagesLength; ++i) { 166 | var image = images[i]; 167 | var texture = image.extras._obj2gltf; 168 | var textureSource = texture.source; 169 | var textureByteLength = textureSource.length; 170 | 171 | image.mimeType = mime.getType(texture.extension); 172 | image.bufferView = gltf.bufferViews.length; 173 | gltf.bufferViews.push({ 174 | buffer : 0, 175 | byteOffset : byteOffset, 176 | byteLength : textureByteLength 177 | }); 178 | byteOffset += textureByteLength; 179 | sources.push(textureSource); 180 | } 181 | 182 | var source = getBufferPadded(Buffer.concat(sources)); 183 | bufferExtras.source = source; 184 | buffer.byteLength = source.length; 185 | } 186 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # objTo3d-tiles 2 | 将 obj 模型转换为 3D Tiles 的 Node 命令行工具以及 Node 模块, 基于[obj2gltf](https://github.com/AnalyticalGraphicsInc/obj2gltf)。 3 | 4 | [在线示例](https://princessgod.github.io/plc/batchedTileset.html) 5 | 6 | >注意: 目前只支持 `.b3dm` 和 `.i3dm` ! 7 | > 8 | >请使用 Cesium v1.37 或以后版本, 因为这里的 3D Tiles 使用 glTF2.0 。 9 | 10 | ## 开始使用 11 | 12 | 确保已经安装 [Node](https://nodejs.org/en/) , 然后 13 | 14 | ``` 15 | npm install -g obj23dtiles 16 | ``` 17 | 18 | ### 基本用法 19 | 20 | * 转换 `.obj` 为 `.gltf` 21 | 22 | ``` 23 | obj23dtiles -i ./bin/barrel/barrel.obj 24 | // 在模型目录导出 barrel.gltf 25 | ``` 26 | 27 | * 转换 `.obj` 为 `.glb` 28 | 29 | ``` 30 | obj23dtiles -i ./bin/barrel/barrel.obj -b 31 | // 在模型目录导出 barrel.glb 32 | ``` 33 | 34 | >注意: 更多 `.gltf` 和 `.glb` 的转换信息可以在 [obj2gltf](https://github.com/AnalyticalGraphicsInc/obj2gltf) 查看。 35 | 36 | >注意: 如果你的模型中包含透明纹理,请添加 `--checkTransparency` 参数。 37 | 38 | >注意: 如果的模型使用 blinn-phong 材质, 当转换为PBR材质时使用遮蔽贴图会使模型看起来变暗。 39 | >所以 `useOcclusion` (使用遮蔽贴图) 默认为 `false`, 如果你的模型本身就准备使用PBR材质,请加 `--useOcclusion` 参数,这里有一些对比图。 40 | 41 |

42 | 43 | 44 | * 转换 `.obj` 为 `.b3dm` 同时带有基础的[属性表](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/BatchTable/README.md), 包含 `batchId` 和 `name` 属性, `name` 就是模型建模时的名字。 45 | 46 | ``` 47 | obj23dtiles -i ./bin/barrel/barrel.obj --b3dm 48 | // 在模型目录导出 barrel.b3dm 49 | ``` 50 | 51 | * 转换 `.obj` 为 `.b3dm`,同时导出默认的[属性表](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/BatchTable/README.md) (一个 JSON 文件)。可以从这个表中获取相关信息以便制作自定义属性表。 52 | 53 | ``` 54 | obj23dtiles -i ./bin/barrel/barrel.obj --b3dm --outputBatchTable 55 | // 在模型目录导出 barrel.b3dm 和 barrel_batchTable.json 56 | ``` 57 | 58 | * 转换 `.obj` 为 `.b3dm`,使用自定义[属性表](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/BatchTable/README.md)。属性和模型对应关系靠 `batchId` 进行连接。 59 | 60 | ``` 61 | obj23dtiles -i ./bin/barrel/barrel.obj -c ./bin/barrel/customBatchTable.json --b3dm 62 | // 在模型目录导出 barrel.b3dm 63 | ``` 64 | 65 | * 转换 `.obj` 为 `.i3dm`,使用自定义[要素表](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Instanced3DModel/README.md#feature-table)。 66 | 67 | ``` 68 | obj23dtiles -i ./bin/barrel/barrel.obj -f ./bin/barrel/customFeatureTable.json --i3dm 69 | // 在模型目录导出 barrel.i3dm 70 | ``` 71 | 72 | * 转换 `.obj` 为 `.i3dm`,使用自定义[要素表](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Instanced3DModel/README.md#feature-table)和[属性表](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Instanced3DModel/README.md#batch-table)。 73 | 74 | ``` 75 | obj23dtiles -i ./bin/barrel/barrel.obj -f ./bin/barrel/customFeatureTable.json 76 | -c ./bin/barrel/customI3dmBatchTable.json --i3dm 77 | // 在模型目录导出 barrel.i3dm 78 | ``` 79 | 80 | 要素表目前可以使用以下属性控制模型 : `position`(位置),`orientation`(旋转),`scale`(缩放)。 81 | 82 | 83 | ### 创建单个瓦片 84 | 85 | * 创建一个 `.b3dm` 瓦片。 86 | 87 | ``` 88 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset 89 | // 在模型目录导出 Batchedbarrel 文件夹 90 | ``` 91 | 92 | * 创建一个 `.b3dm` 瓦片,并自定义瓦片参数和属性表。 93 | 94 | ``` 95 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset 96 | -p ./bin/barrel/customTilesetOptions.json -c ./bin/barrel/customBatchTable.json 97 | // 在模型目录导出 Batchedbarrel 文件夹 98 | ``` 99 | 100 | * 创建一个 `.i3dm` 瓦片。 101 | 102 | ``` 103 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset --i3dm 104 | -f ./bin/barrel/customFeatureTable.json 105 | // 在模型目录导出 Instancedbarrel 文件夹 106 | ``` 107 | 108 | * 创建一个 `.i3dm` 瓦片,并自定义瓦片参数和属性表。 109 | 110 | ``` 111 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset --i3dm 112 | -f ./bin/barrel/customFeatureTable.json -p ./bin/barrel/customTilesetOptions.json 113 | -c ./bin/barrel/customI3dmBatchTable.json 114 | // 在模型目录导出 Instancedbarrel 文件夹 115 | ``` 116 | 117 | `customTilesetOptions.json` 配置文件可以包含以下信息, 这些都是虚拟值,请在文件中包含自己想修改的属性,没有出现的属性会根据模型自动计算。 118 | ``` 119 | { 120 | "longitude": -1.31968, // 瓦片原点(模型原点 (0,0,0)) 经度的弧度值。 121 | "latitude": 0.698874, // 瓦片原点维度的弧度值。 122 | "transHeight": 0.0, // 瓦片原点所在高度,单位为米。 123 | "region": true, // 使用 region 作为外包体。 124 | "box": false, // 使用 box 作为外包体。 125 | "sphere": false // 使用 sphere 作为外包体。 126 | } 127 | ``` 128 | >注意: 如果你没有指明 `transHeight` 属性,你的模型会被放置在高度为0的地表,无论你模型最低点是什么值。比如你有一个飞机模型,它在1000单位高度,那就会被放置在地面上,同样的道理如果你有一个潜艇,都在原点以下,也会被抬升到地面上。所以如果你想保留原始模型的相对高度,可以设置 `transHeight = 0.0`。 129 | 130 | 这是不用的外包体示意图。 131 |

132 | 133 | ### 捆绑瓦片 134 | 你可以将多个瓦片捆绑为一个瓦片,每个瓦片作为外置瓦片集合到一个 `tileset.json` 中。 135 | 136 | ``` 137 | obj23dtiles combine -i ./bin/barrel/output/ 138 | ``` 139 | 140 | ## 作为 Node 模块使用 141 | 如果你想调试此工具或者在node中使用,可以看看[如何作为Node模块使用](NODEUSAGE.md)。 142 | 143 | ## 测试 144 | 导航到项目文件夹下,运行 145 | ``` 146 | npm run test 147 | ``` 148 | 149 | ## 问题定位 150 | 首先,确保你的 `.obj` 文件是完整的,通常情况下包含 `.obj`, `.mtl` 和纹理文件比如 `.jpg` 或 `.png`。 151 | 如果你使用的 Win10, 可以使用自带的模型浏览工具 “Mixed Reality Viewer” 浏览你的 `.obj` 文件, 152 | 或者使用这个[在线浏览工具](https://3dviewer.net/)。 153 |
154 |
155 | 其次,使用此工具导出 `.glb` 然后查看是否现实正常,你可以使用[Cesium](https://www.virtualgis.io/gltfviewer/) 或者 [Three.js](https://gltf-viewer.donmccurdy.com/) gltf 浏览器。 156 |
157 |
158 | 最后,直接导出 `.b3dm` 或瓦片然后在 Cesium 中加载。 159 | 160 | ## 样例数据 161 | 示例代码中的样例数据在 `.bin\barrel\` 文件夹下。 162 | 163 | ``` 164 | barrel\ 165 | | 166 | - barrel.blend -- 167 | | |- Blender 工程文件和纹理 168 | - barrel.png -- 169 | | 170 | - barrel.obj -- 171 | | |- Obj 模型文件 172 | - barrel.mtl -- 173 | | 174 | - customBatchTable.json ---- b3dm 使用的属性表例子 175 | | 176 | - customTilesetOptions.json ---- 自定义瓦片配置文件 177 | | 178 | - customFeatureTable.json ---- i3dm 使用的要素表例子 179 | | 180 | - customI3dmBatchTable.json ---- i3dm 使用的属性表例子 181 | | 182 | - output\ ---- 导出的数据 183 | | 184 | - barrel.glb 185 | | 186 | - barrel.gltf 187 | | 188 | - barrel_batchTable.json ---- 默认属性表 189 | | 190 | - Batchedbarrel\ ---- 使用 b3dm 的瓦片 191 | | | 192 | | - tileset.json 193 | | | 194 | | - barrel.b3dm 195 | | 196 | - Instancedbarrel\ ---- 使用 i3dm 的瓦片 197 | | | 198 | | - tileset.json 199 | | | 200 | | - barrel.i3dm 201 | | 202 | - BatchedTilesets\ ---- 自定义配置文件的瓦片 203 | | 204 | - tileset.json 205 | | 206 | - barrel_withDefaultBatchTable.b3dm 207 | | 208 | - barrel_withCustonBatchTable.b3dm 209 | ``` 210 | 211 | ## 相关资源 212 | * 在线 glTF 浏览工具。 [Cesium](https://www.virtualgis.io/gltfviewer/), [Three.js](https://gltf-viewer.donmccurdy.com/)。 213 | * [Cesium](https://github.com/AnalyticalGraphicsInc/cesium) 214 | * [3D Tiles](https://github.com/AnalyticalGraphicsInc/3d-tiles) 215 | * [glTF](https://github.com/KhronosGroup/glTF) 216 | -------------------------------------------------------------------------------- /NODEUSAGE.md: -------------------------------------------------------------------------------- 1 | # Using as module in node. 2 | 3 | Install package from npm. 4 | 5 | ``` 6 | npm install obj23dtiles 7 | ``` 8 | 9 | ## Convert to `.gltf` 10 | 11 | ```javascript 12 | var obj23dtiles = require('obj23dtiles'); 13 | 14 | var objPath = './bin/barrel/barrel.obj'; 15 | var gltfPath = './bin/barrel/barrel.gltf'; 16 | obj23dtiles(objPath, gltfPath); 17 | ``` 18 | 19 | ## Convert to `.glb` 20 | 21 | ```javascript 22 | var obj23dtiles = require('obj23dtiles'); 23 | 24 | var objPath = './bin/barrel/barrel.obj'; 25 | var glbPath = './bin/barrel/barrel.glb'; 26 | obj23dtiles(objPath, glbPath, {binary: true}); 27 | ``` 28 | 29 | ## Convert to `.b3dm` 30 | 31 | ```javascript 32 | var obj23dtiles = require('obj23dtiles'); 33 | 34 | var objPath = './bin/barrel/barrel.obj'; 35 | var b3dmPath = './bin/barrel/barrel.b3dm'; 36 | obj23dtiles(objPath, b3dmPath, {b3dm: true}); 37 | ``` 38 | 39 | Or use custom BatchTable. 40 | 41 | ```javascript 42 | var obj23dtiles = require('obj23dtiles'); 43 | 44 | var objPath = './bin/barrel/barrel.obj'; 45 | var b3dmPath = './bin/barrel/barrel.b3dm'; 46 | var customBatchTable = './bin/barrel/customBatchTable.json' // file or JS Object. 47 | obj23dtiles(objPath, b3dmPath, { 48 | b3dm: true, 49 | customBatchTable: customBatchTable 50 | }); 51 | ``` 52 | 53 | ## Convert to `.i3dm` 54 | 55 | ```javascript 56 | var obj23dtiles = require('obj23dtiles'); 57 | 58 | var objPath = './bin/barrel/barrel.obj'; 59 | var i3dmPath = './bin/barrel/barrel.i3dm'; 60 | obj23dtiles(objPath, i3dmPath, { 61 | i3dm: true, 62 | customFeatureTable: { 63 | position: [ 64 | [0, 0, 0], 65 | [20, 0, 0] 66 | ], 67 | orientation: [ 68 | [0, 0, 0], 69 | [0, 0, 45] 70 | ], 71 | scale: [ 72 | [1, 1, 1], 73 | [0.8, 0.8, 0.8] 74 | ] 75 | } 76 | }); 77 | ``` 78 | 79 | Or use custom BatchTable. 80 | 81 | ```javascript 82 | var obj23dtiles = require('obj23dtiles'); 83 | 84 | var objPath = './bin/barrel/barrel.obj'; 85 | var i3dmPath = './bin/barrel/barrel.i3dm'; 86 | obj23dtiles(objPath, i3dmPath, { 87 | i3dm: true, 88 | customFeatureTable: { 89 | position: [ 90 | [0, 0, 0], 91 | [20, 0, 0] 92 | ], 93 | orientation: [ 94 | [0, 0, 0], 95 | [0, 0, 45] 96 | ], 97 | scale: [ 98 | [1, 1, 1], 99 | [0.8, 0.8, 0.8] 100 | ] 101 | }, 102 | customBatchTable: { 103 | name: [ 104 | 'modelNormal', 105 | 'modelModified' 106 | ], 107 | id: [ 108 | 0, 109 | 1 110 | ] 111 | } 112 | }); 113 | ``` 114 | 115 | ## Convert to tileset 116 | 117 | * Convert to `.b3dm` tileset. 118 | 119 | ```javascript 120 | var obj23dtiles = require('obj23dtiles'); 121 | 122 | var objPath = './bin/barrel/barrel.obj'; 123 | var tilesetPath = './bin/barrel/barrel.b3dm'; 124 | obj23dtiles(objPath, tilesetPath, {tileset: true}); 125 | ``` 126 | 127 | Or use custom tileset options and BatchTable. 128 | 129 | ```javascript 130 | var obj23dtiles = require('obj23dtiles'); 131 | 132 | var objPath = './bin/barrel/barrel.obj'; 133 | var tilesetPath = './bin/barrel/barrel.b3dm'; 134 | obj23dtiles(objPath, tilesetPath, { 135 | tileset: true, 136 | tilesetOptions: { 137 | longitude: -1.31968, 138 | latitude: 0.698874, 139 | transHeight: 0.0, 140 | minHeight: 0.0, 141 | maxHeight: 40.0, 142 | tileWidth: 200.0, 143 | tileHeight: 200.0, 144 | geometricError: 200.0, 145 | region: true 146 | }, 147 | customBatchTable: { // Cause default BatchTable 'batchId' length is 14 148 | name: [ 149 | 'model1', 150 | 'model2', 151 | 'model3', 152 | 'model4', 153 | 'model5', 154 | 'model6', 155 | 'model7', 156 | 'model8', 157 | 'model9', 158 | 'model10', 159 | 'model11', 160 | 'model12', 161 | 'model13', 162 | 'model14' 163 | ] 164 | } 165 | }); 166 | ``` 167 | 168 | * Convert to `.i3dm` tileset. 169 | 170 | ```javascript 171 | var obj23dtiles = require('obj23dtiles'); 172 | 173 | var objPath = './bin/barrel/barrel.obj'; 174 | var tilesetPath = './bin/barrel/barrel.i3dm'; 175 | obj23dtiles(objPath, tilesetPath, { 176 | tileset: true, 177 | i3dm: true, 178 | customFeatureTable: { 179 | position: [ 180 | [0, 0, 0], 181 | [20, 0, 0] 182 | ], 183 | orientation: [ 184 | [0, 0, 0], 185 | [0, 0, 45] 186 | ], 187 | scale: [ 188 | [1, 1, 1], 189 | [0.8, 0.8, 0.8] 190 | ] 191 | } 192 | }); 193 | ``` 194 | 195 | Or use custom tileset options and BatchTable. 196 | 197 | ```javascript 198 | var obj23dtiles = require('obj23dtiles'); 199 | 200 | var objPath = './bin/barrel/barrel.obj'; 201 | var tilesetPath = './bin/barrel/barrel.i3dm'; 202 | obj23dtiles(objPath, tilesetPath, { 203 | tileset: true, 204 | i3dm: true, 205 | customFeatureTable: { 206 | position: [ 207 | [0, 0, 0], 208 | [20, 0, 0] 209 | ], 210 | orientation: [ 211 | [0, 0, 0], 212 | [0, 0, 45] 213 | ], 214 | scale: [ 215 | [1, 1, 1], 216 | [0.8, 0.8, 0.8] 217 | ] 218 | }, 219 | tilesetOptions: { 220 | longitude: -1.31968, 221 | latitude: 0.698874, 222 | transHeight: 0.0, 223 | minHeight: 0.0, 224 | maxHeight: 40.0, 225 | tileWidth: 200.0, 226 | tileHeight: 200.0, 227 | geometricError: 200.0, 228 | region: true 229 | }, 230 | customBatchTable: { 231 | name: [ 232 | 'model1', 233 | 'model2' 234 | ], 235 | id: [ 236 | 0, 237 | 1 238 | ] 239 | } 240 | }); 241 | ``` 242 | 243 | ## Combine tilesets 244 | 245 | ```javascript 246 | var obj23dtiles = require('obj23dtiles'); 247 | var fs = require('fs'); 248 | 249 | var combine = obj23dtiles.combine; 250 | var outputPath = './bin/barrel/output/tileset.json'; 251 | 252 | combine({inputDir : './bin/barrel/output'}) 253 | .then(function(result) { 254 | fs.writeFile(outputPath, JSON.stringify(result.tileset), 'utf8'); 255 | }) 256 | .catch(function(err) { 257 | console.log(err); 258 | }); 259 | ``` 260 | -------------------------------------------------------------------------------- /lib/obj23dtiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var fsExtra = require('fs-extra'); 4 | var obj2gltf = require('./obj2gltf'); 5 | var obj2B3dm = require('./obj2B3dm'); 6 | var obj2I3dm = require('./obj2I3dm'); 7 | var obj2Tileset = require('./obj2Tileset'); 8 | var combine = require('./combineTileset'); 9 | 10 | module.exports = obj23dtiles; 11 | obj23dtiles.combine = combine; 12 | 13 | function obj23dtiles(objPath, outputPath, options) { 14 | console.time('Total'); 15 | 16 | if(typeof options.tilesetOptions === 'string') { 17 | options.tilesetOptions = fsExtra.readJsonSync(options.tilesetOptions); 18 | } 19 | if(typeof options.customBatchTable === 'string') { 20 | options.customBatchTable = fsExtra.readJsonSync(options.customBatchTable); 21 | } 22 | if (typeof options.customFeatureTable === 'string') { 23 | options.customFeatureTable = fsExtra.readJsonSync(options.customFeatureTable); 24 | } 25 | 26 | if (options && options.tileset) { 27 | if(!options.i3dm) { 28 | options.binary = true; 29 | options.batchId = true; 30 | options.b3dm = true; 31 | 32 | obj2Tileset(objPath, outputPath, options) 33 | .then(function(result) { 34 | var b3dm = result.b3dm; 35 | var batchTableJson = result.batchTableJson; 36 | var tileset = result.tilesetJson; 37 | var tilePath = result.tilePath; 38 | var tilesetPath = result.tilesetPath; 39 | 40 | if(options.outputBatchTable) { 41 | var batchTableJsonPath = tilePath.replace(/\.[^/.]+$/, '') + '_batchTable.json'; 42 | fsExtra.ensureDirSync(path.dirname(batchTableJsonPath)); 43 | fsExtra.writeJsonSync(batchTableJsonPath, batchTableJson, {spaces: 2}); 44 | } 45 | 46 | var tasks = []; 47 | fsExtra.ensureDirSync(path.dirname(tilePath)); 48 | tasks.push(fsExtra.outputFile(tilePath, b3dm)); 49 | tasks.push(fsExtra.writeJson(tilesetPath, tileset, {spaces: 2})); 50 | return Promise.all(tasks); 51 | }) 52 | .then(function() { 53 | console.timeEnd('Total'); 54 | }) 55 | .catch(function(error) { 56 | console.log(error.message || error); 57 | process.exit(1); 58 | }); 59 | } else if (options.i3dm) { 60 | options.binary = true; 61 | options.batchId = false; 62 | if (!options.customFeatureTable) { 63 | console.log('Convert to i3dm need a custom FeatureTable.'); 64 | process.exit(1); 65 | } 66 | 67 | obj2Tileset(objPath, outputPath, options) 68 | .then(function(result) { 69 | var i3dm = result.i3dm; 70 | var batchTableJson = result.batchTableJson; 71 | var tileset = result.tilesetJson; 72 | var tilePath = result.tilePath; 73 | var tilesetPath = result.tilesetPath; 74 | 75 | if(options.outputBatchTable) { 76 | var batchTableJsonPath = tilePath.replace(/\.[^/.]+$/, '') + '_batchTable.json'; 77 | fsExtra.ensureDirSync(path.dirname(batchTableJsonPath)); 78 | fsExtra.writeJsonSync(batchTableJsonPath, batchTableJson, {spaces: 2}); 79 | } 80 | 81 | var tasks = []; 82 | fsExtra.ensureDirSync(path.dirname(tilePath)); 83 | tasks.push(fsExtra.outputFile(tilePath, i3dm)); 84 | tasks.push(fsExtra.writeJson(tilesetPath, tileset, {spaces: 2})); 85 | return Promise.all(tasks); 86 | }) 87 | .then(function() { 88 | console.timeEnd('Total'); 89 | }) 90 | .catch(function(error) { 91 | console.log(error.message || error); 92 | process.exit(1); 93 | }); 94 | } 95 | } 96 | else if (options && options.b3dm) { 97 | options.binary = true; 98 | options.batchId = true; 99 | obj2B3dm(objPath, options) 100 | .then(function(result){ 101 | var b3dm = result.b3dm; 102 | var batchTableJson = result.batchTableJson; 103 | 104 | if(options.outputBatchTable) { 105 | var batchTableJsonPath = outputPath.replace(/\.[^/.]+$/, '') + '_batchTable.json'; 106 | fsExtra.ensureDirSync(path.dirname(batchTableJsonPath)); 107 | fsExtra.writeJsonSync(batchTableJsonPath, batchTableJson, {spaces: 2}); 108 | } 109 | fsExtra.ensureDirSync(path.dirname(outputPath)); 110 | return fsExtra.outputFile(outputPath, b3dm); 111 | }) 112 | .then(function() { 113 | console.timeEnd('Total'); 114 | }) 115 | .catch(function(error) { 116 | console.log(error.message || error); 117 | process.exit(1); 118 | }); 119 | } 120 | else if(options && options.i3dm) { 121 | options.binary = true; 122 | options.batchId = false; 123 | if (!options.customFeatureTable) { 124 | console.log('Convert to i3dm need a custom FeatureTable.'); 125 | process.exit(1); 126 | } 127 | obj2I3dm(objPath, options) 128 | .then(function(result){ 129 | var i3dm = result.i3dm; 130 | var batchTableJson = result.batchTableJson; 131 | 132 | if(options.outputBatchTable) { 133 | var batchTableJsonPath = outputPath.replace(/\.[^/.]+$/, '') + '_batchTable.json'; 134 | fsExtra.ensureDirSync(path.dirname(batchTableJsonPath)); 135 | fsExtra.writeJsonSync(batchTableJsonPath, batchTableJson, {spaces: 2}); 136 | } 137 | fsExtra.ensureDirSync(path.dirname(outputPath)); 138 | return fsExtra.outputFile(outputPath, i3dm); 139 | }) 140 | .then(function() { 141 | console.timeEnd('Total'); 142 | }) 143 | .catch(function(error) { 144 | console.log(error.message || error); 145 | process.exit(1); 146 | }); 147 | } 148 | else { 149 | obj2gltf(objPath, options) 150 | .then(function(result){ 151 | var gltf = result.gltf; 152 | if (options && options.binary) { 153 | // gltf is a glb buffer 154 | return fsExtra.outputFile(outputPath, gltf); 155 | } 156 | var jsonOptions = { 157 | spaces : 2 158 | }; 159 | return fsExtra.outputJson(outputPath, gltf, jsonOptions); 160 | }) 161 | .then(function() { 162 | console.timeEnd('Total'); 163 | }) 164 | .catch(function(error) { 165 | console.log(error.message || error); 166 | process.exit(1); 167 | }); 168 | } 169 | } 170 | 171 | /** 172 | * Default values that will used when call obj23dtiles to use. 173 | */ 174 | obj23dtiles.defaults = JSON.parse(JSON.stringify(obj2gltf.defaults)); 175 | Object.assign(obj23dtiles.defaults, JSON.parse(JSON.stringify(obj2B3dm.defaults))); 176 | Object.assign(obj23dtiles.defaults, JSON.parse(JSON.stringify(obj2I3dm.defaults))); 177 | Object.assign(obj23dtiles.defaults, JSON.parse(JSON.stringify(obj2Tileset.defaults))); 178 | -------------------------------------------------------------------------------- /lib/obj2Tileset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var Cesium = require('cesium'); 4 | var createSingleTileset = require('./createSingleTileset'); 5 | var tilesetOptionsUtility = require('./tilesetOptionsUtility'); 6 | var obj2B3dm = require('./obj2B3dm'); 7 | var obj2I3dm = require('./obj2I3dm'); 8 | var Cartesian3 = Cesium.Cartesian3; 9 | var Matrix3 = Cesium.Matrix3; 10 | var CMath = Cesium.Math; 11 | 12 | var defaultValue = Cesium.defaultValue; 13 | var getPoint3MinMax = tilesetOptionsUtility.getPoint3MinMax; 14 | 15 | module.exports = obj2Tileset; 16 | 17 | function obj2Tileset(objPath, outputpath, options) { 18 | var folder = path.dirname(outputpath); 19 | var tileFullName = path.basename(outputpath); 20 | var folderPrifix = options.b3dm ? 'Batched' : 'Instanced'; 21 | var tilesetFolderName = folderPrifix + path.basename(objPath, '.obj'); 22 | var tilePath = path.join(folder, tilesetFolderName, tileFullName); 23 | var tilesetPath = path.join(folder, tilesetFolderName, 'tileset.json'); 24 | var tilesetOptions = options.tilesetOptions || {}; 25 | if (options.b3dm) { 26 | return obj2B3dm(objPath, options) 27 | .then(function(result){ 28 | var batchTableJson = result.batchTableJson; 29 | var minmaxPoint = getPoint3MinMax(batchTableJson.minPoint.concat(batchTableJson.maxPoint)); 30 | var width = minmaxPoint.max[0] - minmaxPoint.min[0]; 31 | var height = minmaxPoint.max[2] - minmaxPoint.min[2]; 32 | width = Math.ceil(width); 33 | height = Math.ceil(height); 34 | var offsetX = width / 2 + minmaxPoint.min[0]; 35 | var offsetY = height / 2 + minmaxPoint.min[2]; 36 | return new Promise(function(resolve) { 37 | tilesetOptions.tileName = tileFullName; 38 | tilesetOptions.tileWidth = defaultValue(tilesetOptions.tileWidth, width); 39 | tilesetOptions.tileHeight = defaultValue(tilesetOptions.tileHeight, height); 40 | tilesetOptions.transHeight = defaultValue(tilesetOptions.transHeight, -minmaxPoint.min[1]); 41 | tilesetOptions.minHeight = defaultValue(tilesetOptions.minHeight, minmaxPoint.min[1] + tilesetOptions.transHeight); 42 | tilesetOptions.maxHeight = defaultValue(tilesetOptions.maxHeight, minmaxPoint.max[1] + tilesetOptions.transHeight); 43 | tilesetOptions.offsetX = defaultValue(tilesetOptions.offsetX, offsetX); 44 | tilesetOptions.offsetY = defaultValue(tilesetOptions.offsetY, offsetY); 45 | return resolve({ 46 | b3dm : result.b3dm, 47 | batchTableJson: result.batchTableJson, 48 | tilesetJson : createSingleTileset(tilesetOptions), 49 | tilePath : tilePath, 50 | tilesetPath : tilesetPath 51 | }); 52 | }); 53 | }); 54 | } else if (options.i3dm) { 55 | return obj2I3dm(objPath, options) 56 | .then(function(result) { 57 | var batchTableJson = result.batchTableJson; 58 | var minmaxPoint = getPoint3MinMax(batchTableJson.minPoint.concat(batchTableJson.maxPoint)); 59 | minmaxPoint.min = [minmaxPoint.min[0], minmaxPoint.min[2], minmaxPoint.min[1]]; 60 | minmaxPoint.max = [minmaxPoint.max[0], minmaxPoint.max[2], minmaxPoint.max[1]]; 61 | var featureTable = options.customFeatureTable; 62 | var tempPoints = []; 63 | var i; 64 | var j; 65 | var position = featureTable.position; 66 | var length = position.length; 67 | for (i = 0; i < length; i ++) { 68 | tempPoints.push([minmaxPoint.min[0] + position[i][0], minmaxPoint.min[1] + position[i][1], minmaxPoint.min[2] + position[i][2]]); 69 | tempPoints.push([minmaxPoint.min[0] + position[i][0], minmaxPoint.max[1] + position[i][1], minmaxPoint.min[2] + position[i][2]]); 70 | tempPoints.push([minmaxPoint.max[0] + position[i][0], minmaxPoint.min[1] + position[i][1], minmaxPoint.min[2] + position[i][2]]); 71 | tempPoints.push([minmaxPoint.max[0] + position[i][0], minmaxPoint.max[1] + position[i][1], minmaxPoint.min[2] + position[i][2]]); 72 | 73 | tempPoints.push([minmaxPoint.max[0] + position[i][0], minmaxPoint.max[1] + position[i][1], minmaxPoint.max[2] + position[i][2]]); 74 | tempPoints.push([minmaxPoint.max[0] + position[i][0], minmaxPoint.min[1] + position[i][1], minmaxPoint.max[2] + position[i][2]]); 75 | tempPoints.push([minmaxPoint.min[0] + position[i][0], minmaxPoint.max[1] + position[i][1], minmaxPoint.max[2] + position[i][2]]); 76 | tempPoints.push([minmaxPoint.min[0] + position[i][0], minmaxPoint.min[1] + position[i][1], minmaxPoint.max[2] + position[i][2]]); 77 | } 78 | if (featureTable.scale) { 79 | var scale = featureTable.scale; 80 | for (i = 0; i < length; i ++) { 81 | for (j = 0; j < 8; j ++) { 82 | tempPoints[i * 8 + j] = [tempPoints[i * 8 + j][0] * scale[i][0], tempPoints[i * 8 + j][1] * scale[i][1], tempPoints[i * 8 + j][2] * scale[i][2]]; 83 | } 84 | } 85 | } 86 | if (featureTable.orientation) { 87 | var orientation = featureTable.orientation; 88 | var ps = new Array(8); 89 | var m; 90 | var rotate; 91 | for (i = 0; i < length; i ++) { 92 | rotate = orientation[i]; 93 | for (j = 0; j < 8; j ++) { 94 | ps[j] = new Cartesian3(tempPoints[i * 8 + j][0], tempPoints[i * 8 + j][1], tempPoints[i * 8 + j][2]); 95 | } 96 | m = Matrix3.fromRotationZ(CMath.toRadians(rotate[2])); 97 | for (j = 0; j < 8; j ++) { 98 | ps[j] = Matrix3.multiplyByVector(m, ps[j], new Cartesian3()); 99 | } 100 | m = Matrix3.fromRotationX(-CMath.toRadians(rotate[0])); 101 | for (j = 0; j < 8; j ++) { 102 | ps[j] = Matrix3.multiplyByVector(m, ps[j], new Cartesian3()); 103 | } 104 | m = Matrix3.fromRotationY(CMath.toRadians(rotate[1])); 105 | for (j = 0; j < 8; j ++) { 106 | ps[j] = Matrix3.multiplyByVector(m, ps[j], new Cartesian3()); 107 | } 108 | for (j = 0; j < 8; j ++) { 109 | tempPoints[i * 8 + j] = [ps[j].x, ps[j].y, ps[j].z]; 110 | } 111 | } 112 | } 113 | 114 | minmaxPoint = getPoint3MinMax(tempPoints); 115 | var width = minmaxPoint.max[0] - minmaxPoint.min[0]; 116 | var height = minmaxPoint.max[1] - minmaxPoint.min[1]; 117 | width = Math.ceil(width); 118 | height = Math.ceil(height); 119 | var offsetX = width / 2 + minmaxPoint.min[0]; 120 | var offsetY = height / 2 + minmaxPoint.min[1]; 121 | 122 | return new Promise(function(resolve) { 123 | tilesetOptions.tileName = tileFullName; 124 | tilesetOptions.tileWidth = defaultValue(tilesetOptions.tileWidth, width); 125 | tilesetOptions.tileHeight = defaultValue(tilesetOptions.tileHeight, height); 126 | tilesetOptions.transHeight = defaultValue(tilesetOptions.transHeight, -minmaxPoint.min[2]); 127 | tilesetOptions.minHeight = defaultValue(tilesetOptions.minHeight, minmaxPoint.min[2] + tilesetOptions.transHeight); 128 | tilesetOptions.maxHeight = defaultValue(tilesetOptions.maxHeight, minmaxPoint.max[2] + tilesetOptions.transHeight); 129 | tilesetOptions.offsetX = defaultValue(tilesetOptions.offsetX, offsetX); 130 | tilesetOptions.offsetY = defaultValue(tilesetOptions.offsetY, offsetY); 131 | return resolve({ 132 | i3dm : result.i3dm, 133 | batchTableJson: result.batchTableJson, 134 | tilesetJson : createSingleTileset(tilesetOptions), 135 | tilePath : tilePath, 136 | tilesetPath : tilesetPath 137 | }); 138 | }); 139 | }); 140 | } 141 | } 142 | 143 | /** 144 | * Default pramaters used in this moudle. 145 | */ 146 | obj2Tileset.defaults = { 147 | /** 148 | * Gets or set whether create a tileset. 149 | * 150 | * @type Boolean 151 | * @default false 152 | */ 153 | tileset: false, 154 | /** 155 | * Gets or set the tileset optional parameters. 156 | * 157 | * @type Object 158 | * @default undefined 159 | */ 160 | tilesetOptions: undefined 161 | }; 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # objTo3d-tiles 2 | 3 | > WARNING: THIS REPO IS NO LONGER MAINTANING, MAYBE NOT SUPPORT NEWERST CESIUM. 4 | 5 | Node command line tool and module convert obj model file to 3D Tiles, based on [obj2gltf](https://github.com/AnalyticalGraphicsInc/obj2gltf). 6 | 7 | [Online Demonstration](https://princessgod.github.io/plc/batchedTileset.html) 8 | 9 | [简体中文](README_CN.md) 10 | 11 | >NOTE: Only support `.b3dm` and `.i3dm` for now! 12 | > 13 | >Please use Cesium after v1.37, cause this 3d tile use glTF2.0. 14 | 15 | ## Getting Start 16 | 17 | Make sure you have [Node](https://nodejs.org/en/) installed, and then 18 | 19 | ``` 20 | npm install -g obj23dtiles 21 | ``` 22 | 23 | ### Basic Usage 24 | 25 | * Convert `.obj` to `.gltf` 26 | 27 | ``` 28 | obj23dtiles -i ./bin/barrel/barrel.obj 29 | // Export barrel.gltf at obj folder. 30 | ``` 31 | 32 | * Convert `.obj` to `.glb` 33 | 34 | ``` 35 | obj23dtiles -i ./bin/barrel/barrel.obj -b 36 | // Export barrel.glb at obj folder. 37 | ``` 38 | 39 | >NOTE: More detial to convert `.gltf` and `.glb` can find at [obj2gltf](https://github.com/AnalyticalGraphicsInc/obj2gltf). 40 | 41 | >NOTE: If your model have tarnsparency texture please add `--checkTransparency` parameter. 42 | 43 | >NOTE: If your model using blinn-phong material, and use occlusion when convert to PBR material, the model will looks darker. 44 | >The `useOcclusion` default is false, remember adding `--useOcclusion` if your model using PBR material. Here are some showcase about it. 45 | 46 |

47 | 48 | 49 | * Convert `.obj` to `.b3dm` with default [BatchTable](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/BatchTable/README.md), which have `batchId` and `name` property, and `name` is model's name. 50 | 51 | ``` 52 | obj23dtiles -i ./bin/barrel/barrel.obj --b3dm 53 | // Export barrel.b3dm at obj folder. 54 | ``` 55 | 56 | * Convert `.obj` to `.b3dm` with default [BatchTable](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/BatchTable/README.md) and export default BatchTable (a JSON file). Maybe get information for custom BatchTable. 57 | 58 | ``` 59 | obj23dtiles -i ./bin/barrel/barrel.obj --b3dm --outputBatchTable 60 | // Export barrel.b3dm and barrel_batchTable.json at obj folder. 61 | ``` 62 | 63 | * Convert `.obj` to `.b3dm` with custom [BatchTable](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/BatchTable/README.md). 64 | 65 | ``` 66 | obj23dtiles -i ./bin/barrel/barrel.obj -c ./bin/barrel/customBatchTable.json --b3dm 67 | // Export barrel.b3dm with custom batch table at obj folder. 68 | ``` 69 | 70 | * Convert `.obj` to `.i3dm` width [FeatureTable](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Instanced3DModel/README.md#feature-table). 71 | 72 | ``` 73 | obj23dtiles -i ./bin/barrel/barrel.obj -f ./bin/barrel/customFeatureTable.json --i3dm 74 | // Export barrel.i3dm at obj folder. 75 | ``` 76 | 77 | * Convert `.obj` to `.i3dm` with [FeatureTable](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Instanced3DModel/README.md#feature-table) and [BatchTable](https://github.com/AnalyticalGraphicsInc/3d-tiles/blob/master/TileFormats/Instanced3DModel/README.md#batch-table). 78 | 79 | ``` 80 | obj23dtiles -i ./bin/barrel/barrel.obj -f ./bin/barrel/customFeatureTable.json 81 | -c ./bin/barrel/customI3dmBatchTable.json --i3dm 82 | // Export barrel.i3dm with BatchTable at obj folder. 83 | ``` 84 | 85 | FeatureTable support following parameters : `position`, `orientation`, `scale`. 86 | 87 | ### Create Tileset 88 | 89 | * Create a single tileset with `.b3dm` tile. 90 | 91 | ``` 92 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset 93 | // Export ./Batchedbarrel folder at obj folder which is a tileset. 94 | ``` 95 | 96 | * Create a single tileset with `.b3dm` tile and custom tileset options, custom BatchTable. 97 | 98 | ``` 99 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset 100 | -p ./bin/barrel/customTilesetOptions.json -c ./bin/barrel/customBatchTable.json 101 | // Export ./Batchedbarrel folder at obj folder which is a tileset with custom tileset options. 102 | ``` 103 | 104 | * Create a single tileset with `.i3dm` tile. 105 | 106 | ``` 107 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset --i3dm 108 | -f ./bin/barrel/customFeatureTable.json 109 | // Export ./Instancedbarrel folder at obj folder which is a tileset. 110 | ``` 111 | 112 | * Create a single tileset with `.i3dm` tile and custom tileset options, custom BatchTable. 113 | 114 | ``` 115 | obj23dtiles -i ./bin/barrel/barrel.obj --tileset --i3dm 116 | -f ./bin/barrel/customFeatureTable.json -p ./bin/barrel/customTilesetOptions.json 117 | -c ./bin/barrel/customI3dmBatchTable.json 118 | // Export ./Instancedbarrel folder at obj folder which is a tileset. 119 | ``` 120 | 121 | The `customTilesetOptions.json` can have options bellow, and these are fake values, please only add properties you need, other value will be auto calculate through `.obj` file. 122 | 123 | ``` 124 | { 125 | "longitude": -1.31968, // Tile origin's(models' point (0,0,0)) longitude in radian. 126 | "latitude": 0.698874, // Tile origin's latitude in radian. 127 | "transHeight": 0.0, // Tile origin's height in meters. 128 | "region": true, // Using region bounding volume. 129 | "box": false, // Using box bounding volume. 130 | "sphere": false // Using sphere bounding volume. 131 | } 132 | ``` 133 | >NOTE: If you are not specify the `transHeight` option, your model will be place at earth ground surface, which means no matter what the height your models are, 134 | >the lowerest point of your models will be place at `height = 0.0` on the earth. But if you want keep origin heigth you just need specify `transHeight = 0.0`. 135 | 136 | Here are different bounding volumes. 137 |

138 | 139 | ### Combine tilesets 140 | You can combine tilesets into one `tileset.json` as external tileset. 141 | 142 | ``` 143 | obj23dtiles combine -i ./bin/barrel/output 144 | ``` 145 | 146 | ## Using as node module 147 | If you want to use this tool in node or debug, check out [how to use as node module](NODEUSAGE.md). 148 | 149 | ## Test 150 | Navigate to this project folder and run 151 | ``` 152 | npm run test 153 | ``` 154 | 155 | ## Troubleshooting 156 | First, make sure your `.obj` file is complete, normally include `.obj`, `.mtl` and textures like `.jpg` or `.png`. 157 | You can preview your `.obj` model via "Mixed Reality Viewer" if you are in windows 10. 158 | Otherwise you can use this [online viewer](https://3dviewer.net/). 159 |
160 |
161 | Second, export `.glb` and check if it display correctly. You can use 162 | [Cesium](https://www.virtualgis.io/gltfviewer/) or [Three.js](https://gltf-viewer.donmccurdy.com/) gltf viewer. 163 |
164 |
165 | In the end, just export `.b3dm` or tileset and load in Cesium. 166 | 167 | ## Sample Data 168 | Sample data under the `.bin\barrel\` folder. 169 | 170 | ``` 171 | barrel\ 172 | | 173 | - barrel.blend -- 174 | | |- Blender project file with texture. 175 | - barrel.png -- 176 | | 177 | - barrel.obj -- 178 | | |- Obj model files. 179 | - barrel.mtl -- 180 | | 181 | - customBatchTable.json ---- Custom batchtable for b3dm. 182 | | 183 | - customTilesetOptions.json ---- Custom tileset optional parameters. 184 | | 185 | - customFeatureTable.json ---- Custom FeatureTable for i3dm. 186 | | 187 | - customI3dmBatchTable.json ---- Custom BatchTable for i3dm. 188 | | 189 | - output\ ---- Export data by using upper files. 190 | | 191 | - barrel.glb 192 | | 193 | - barrel.gltf 194 | | 195 | - barrel_batchTable.json ---- Default batch table. 196 | | 197 | - Batchedbarrel\ ---- Tileset use b3dm 198 | | | 199 | | - tileset.json 200 | | | 201 | | - barrel.b3dm 202 | | 203 | - Instancedbarrel\ ---- Tileset use i3dm 204 | | | 205 | | - tileset.json 206 | | | 207 | | - barrel.i3dm 208 | | 209 | - BatchedTilesets\ ---- Tileset with custom tileset.json 210 | | 211 | - tileset.json 212 | | 213 | - barrel_withDefaultBatchTable.b3dm 214 | | 215 | - barrel_withCustonBatchTable.b3dm 216 | ``` 217 | 218 | ## Resources 219 | * Online glTF viewer, make sure your glTF is correct. [Cesium](https://www.virtualgis.io/gltfviewer/), [Three.js](https://gltf-viewer.donmccurdy.com/). 220 | * [Cesium](https://github.com/AnalyticalGraphicsInc/cesium) 221 | * [3D Tiles](https://github.com/AnalyticalGraphicsInc/3d-tiles) 222 | * [glTF](https://github.com/KhronosGroup/glTF) 223 | 224 | ## Credits 225 | Great thanks to Sean Lilley([@lilleyse](https://github.com/lilleyse)) for helping and advising. 226 | 227 | Thanks [AnalyticalGraphicsInc](https://github.com/AnalyticalGraphicsInc) provide a lot of open source project (like [Cesium](https://github.com/AnalyticalGraphicsInc/cesium) and [3D Tiles](https://github.com/AnalyticalGraphicsInc/3d-tiles)) and creat a great GIS environment. 228 | 229 | ## License 230 | [Apache License 2.0](https://github.com/PrincessGod/objTo3d-tiles/blob/master/LICENSE) 231 | -------------------------------------------------------------------------------- /lib/obj2gltf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var fsExtra = require('fs-extra'); 4 | var path = require('path'); 5 | var createGltf = require('./createGltf'); 6 | var loadObj = require('./loadObj'); 7 | var writeGltf = require('./writeGltf'); 8 | 9 | var defaultValue = Cesium.defaultValue; 10 | var defined = Cesium.defined; 11 | var DeveloperError = Cesium.DeveloperError; 12 | 13 | module.exports = obj2gltf; 14 | 15 | /** 16 | * Converts an obj file to a glTF or glb. 17 | * 18 | * @param {String} objPath Path to the obj file. 19 | * @param {Object} [options] An object with the following properties: 20 | * @param {Boolean} [options.binary=false] Convert to binary glTF. 21 | * @param {Boolean} [options.separate=false] Write out separate buffer files and textures instead of embedding them in the glTF. 22 | * @param {Boolean} [options.separateTextures=false] Write out separate textures only. 23 | * @param {Boolean} [options.checkTransparency=false] Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. 24 | * @param {Boolean} [options.secure=false] Prevent the converter from reading textures or mtl files outside of the input obj directory. 25 | * @param {Boolean} [options.packOcclusion=false] Pack the occlusion texture in the red channel of the metallic-roughness texture. 26 | * @param {Boolean} [options.metallicRoughness=false] The values in the mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots. 27 | * @param {Boolean} [options.specularGlossiness=false] The values in the mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. 28 | * @param {Boolean} [options.materialsCommon=false] The glTF will be saved with the KHR_materials_common extension. 29 | * @param {Object} [options.overridingTextures] An object containing texture paths that override textures defined in the .mtl file. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material. 30 | * @param {String} [options.overridingTextures.metallicRoughnessOcclusionTexture] Path to the metallic-roughness-occlusion texture, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. 31 | * @param {String} [options.overridingTextures.specularGlossinessTexture] Path to the specular-glossiness texture, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension. 32 | * @param {String} [options.overridingTextures.occlusionTexture] Path to the occlusion texture. Ignored if metallicRoughnessOcclusionTexture is also set. 33 | * @param {String} [options.overridingTextures.normalTexture] Path to the normal texture. 34 | * @param {String} [options.overridingTextures.baseColorTexture] Path to the baseColor/diffuse texture. 35 | * @param {String} [options.overridingTextures.emissiveTexture] Path to the emissive texture. 36 | * @param {String} [options.overridingTextures.alphaTexture] Path to the alpha texture. 37 | * @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. 38 | * @param {Writer} [options.writer] A callback function that writes files that are saved as separate resources. 39 | * @param {String} [options.outputDirectory] Output directory for writing separate resources when options.writer is not defined. 40 | * @return {Promise} A promise that resolves to the glTF JSON or glb buffer. 41 | */ 42 | function obj2gltf(objPath, options) { 43 | var defaults = obj2gltf.defaults; 44 | options = defaultValue(options, {}); 45 | options.binary = defaultValue(options.binary, defaults.binary); 46 | options.separate = defaultValue(options.separate, defaults.separate); 47 | options.separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || options.separate; 48 | options.checkTransparency = defaultValue(options.checkTransparency, defaults.checkTransparency); 49 | options.secure = defaultValue(options.secure, defaults.secure); 50 | options.packOcclusion = defaultValue(options.packOcclusion, defaults.packOcclusion); 51 | options.metallicRoughness = defaultValue(options.metallicRoughness, defaults.metallicRoughness); 52 | options.specularGlossiness = defaultValue(options.specularGlossiness, defaults.specularGlossiness); 53 | options.materialsCommon = defaultValue(options.materialsCommon, defaults.materialsCommon); 54 | options.overridingTextures = defaultValue(options.overridingTextures, defaultValue.EMPTY_OBJECT); 55 | options.logger = defaultValue(options.logger, getDefaultLogger()); 56 | options.writer = defaultValue(options.writer, getDefaultWriter(options.outputDirectory)); 57 | 58 | if (!defined(objPath)) { 59 | throw new DeveloperError('objPath is required'); 60 | } 61 | 62 | if (options.separateTextures && !defined(options.writer)) { 63 | throw new DeveloperError('Either options.writer or options.outputDirectory must be defined when writing separate resources.'); 64 | } 65 | 66 | if (options.metallicRoughness + options.specularGlossiness + options.materialsCommon > 1) { 67 | throw new DeveloperError('Only one material type may be set from [metallicRoughness, specularGlossiness, materialsCommon].'); 68 | } 69 | 70 | if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture) && defined(options.overridingTextures.specularGlossinessTexture)) { 71 | throw new DeveloperError('metallicRoughnessOcclusionTexture and specularGlossinessTexture cannot both be defined.'); 72 | } 73 | 74 | if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture)) { 75 | options.metallicRoughness = true; 76 | options.specularGlossiness = false; 77 | options.materialsCommon = false; 78 | options.packOcclusion = true; 79 | } 80 | 81 | if (defined(options.overridingTextures.specularGlossinessTexture)) { 82 | options.metallicRoughness = false; 83 | options.specularGlossiness = true; 84 | options.materialsCommon = false; 85 | } 86 | 87 | return loadObj(objPath, options) 88 | .then(function(objData) { 89 | return createGltf(objData, options); 90 | }) 91 | .then(function(result) { 92 | return writeGltf(result.gltf, options) 93 | .then(function(gltf) { 94 | return { 95 | gltf : gltf, 96 | batchTableJson : result.batchTableJson 97 | }; 98 | }); 99 | }); 100 | } 101 | 102 | function getDefaultLogger() { 103 | return function(message) { 104 | console.log(message); 105 | }; 106 | } 107 | 108 | function getDefaultWriter(outputDirectory) { 109 | if (defined(outputDirectory)) { 110 | return function(file, data) { 111 | var outputFile = path.join(outputDirectory, file); 112 | return fsExtra.outputFile(outputFile, data); 113 | }; 114 | } 115 | } 116 | 117 | /** 118 | * Default values that will be used when calling obj2gltf(options) unless specified in the options object. 119 | */ 120 | obj2gltf.defaults = { 121 | /** 122 | * Gets or sets whether the converter will return a glb. 123 | * @type Boolean 124 | * @default false 125 | */ 126 | binary : false, 127 | /** 128 | * Gets or sets whether to write out separate buffer and texture, 129 | * shader files, and textures instead of embedding them in the glTF. 130 | * @type Boolean 131 | * @default false 132 | */ 133 | separate : false, 134 | /** 135 | * Gets or sets if glTF use occlusition texture. 136 | * Tips : If origin use blinn-phong material, use occlusition will make model looks darker. 137 | */ 138 | useOcclusion : false, 139 | /** 140 | * Gets or sets whether to write out separate textures only. 141 | * @type Boolean 142 | * @default false 143 | */ 144 | separateTextures : false, 145 | /** 146 | * Gets or sets whether the converter will do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. 147 | * @type Boolean 148 | * @default false 149 | */ 150 | checkTransparency : false, 151 | /** 152 | * Gets or sets whether the source model can reference paths outside of its directory. 153 | * @type Boolean 154 | * @default false 155 | */ 156 | secure : false, 157 | /** 158 | * Gets or sets whether to pack the occlusion texture in the red channel of the metallic-roughness texture. 159 | * @type Boolean 160 | * @default false 161 | */ 162 | packOcclusion : false, 163 | /** 164 | * Gets or sets whether rhe values in the .mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots. 165 | * @type Boolean 166 | * @default false 167 | */ 168 | metallicRoughness : false, 169 | /** 170 | * Gets or sets whether the values in the .mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension. 171 | * @type Boolean 172 | * @default false 173 | */ 174 | specularGlossiness : false, 175 | /** 176 | * Gets or sets whether the glTF will be saved with the KHR_materials_common extension. 177 | * @type Boolean 178 | * @default false 179 | */ 180 | materialsCommon : false 181 | }; 182 | 183 | /** 184 | * A callback function that logs messages. 185 | * @callback Logger 186 | * 187 | * @param {String} message The message to log. 188 | */ 189 | 190 | /** 191 | * A callback function that writes files that are saved as separate resources. 192 | * @callback Writer 193 | * 194 | * @param {String} file The relative path of the file. 195 | * @param {Buffer} data The file data to write. 196 | * @returns {Promise} A promise that resolves when the file is written. 197 | */ 198 | -------------------------------------------------------------------------------- /bin/obj23dtiles.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | var Cesium = require('cesium'); 4 | var path = require('path'); 5 | var yargs = require('yargs'); 6 | var fsExtra = require('fs-extra'); 7 | var combine = require('../lib/combineTileset'); 8 | var obj23dtiles = require('../lib/obj23dtiles'); 9 | 10 | var defined = Cesium.defined; 11 | 12 | var defaults = obj23dtiles.defaults; 13 | 14 | var args = process.argv; 15 | 16 | var argv = yargs 17 | .usage('Usage: node $0 -i inputPath -o outputPath') 18 | .example('node $0 -i ./specs/data/box/box.obj -o box.gltf') 19 | .help('h') 20 | .alias('h', 'help') 21 | .options({ 22 | input : { 23 | alias : 'i', 24 | describe : 'Path to the obj file.', 25 | type : 'string', 26 | normalize : true, 27 | demandOption : true 28 | }, 29 | output : { 30 | alias : 'o', 31 | describe : 'Path of the converted glTF or glb file.', 32 | type : 'string', 33 | normalize : true 34 | }, 35 | binary : { 36 | alias : 'b', 37 | describe : 'Save as binary glTF (.glb)', 38 | type : 'boolean', 39 | default : defaults.binary 40 | }, 41 | batchId : { 42 | describe : 'Add _BTACHID to glTF or glb file.', 43 | type : 'boolean', 44 | default : defaults.batchId 45 | }, 46 | b3dm : { 47 | describe : 'Convert to b3dm model file, with _BATCHID and default batch table per-mesh.', 48 | type : 'boolean', 49 | default : defaults.b3dm 50 | }, 51 | i2dm : { 52 | describe : 'Convert to i3dm model file, with custom FeatureTable and BatchTable.', 53 | type : 'boolean', 54 | default : defaults.i3dm 55 | }, 56 | tileset : { 57 | describe : 'Convert to b3dm with a single tileset.json.', 58 | type : 'boolean', 59 | default : defaults.tileset 60 | }, 61 | tilesetOptions : { 62 | alias : 'p', 63 | describe : 'Tileset options config file.', 64 | type : 'string', 65 | normalize : true 66 | }, 67 | outputBatchTable : { 68 | describe : 'Output BatchTable Json file.', 69 | type : 'boolean', 70 | default : defaults.outputBatchTable 71 | }, 72 | customBatchTable : { 73 | alias : 'c', 74 | describe : 'Custom BatchTable Json file.', 75 | type : 'string', 76 | normalize : true 77 | }, 78 | customFeatureTable : { 79 | alias : 'f', 80 | describe : 'Custom FeatureTable Json file.', 81 | type: 'string', 82 | normalize : true 83 | }, 84 | useOcclusion : { 85 | describe : 'Use occlusition texture in MetallicRoughnessMaterial.', 86 | type : 'boolean', 87 | default : defaults.useOcclusion 88 | }, 89 | separate : { 90 | alias : 's', 91 | describe : 'Write separate buffers and textures instead of embedding them in the glTF.', 92 | type : 'boolean', 93 | default : defaults.separate 94 | }, 95 | separateTextures : { 96 | alias : 't', 97 | describe : 'Write out separate textures only.', 98 | type : 'boolean', 99 | default : defaults.separateTextures 100 | }, 101 | checkTransparency : { 102 | describe : 'Do a more exhaustive check for texture transparency by looking at the alpha channel of each pixel. By default textures are considered to be opaque.', 103 | type : 'boolean', 104 | default : defaults.checkTransparency 105 | }, 106 | secure : { 107 | describe : 'Prevent the converter from reading textures or mtl files outside of the input obj directory.', 108 | type : 'boolean', 109 | default : defaults.secure 110 | }, 111 | packOcclusion : { 112 | describe : 'Pack the occlusion texture in the red channel of metallic-roughness texture.', 113 | type : 'boolean', 114 | default : defaults.packOcclusion 115 | }, 116 | metallicRoughness : { 117 | describe : 'The values in the .mtl file are already metallic-roughness PBR values and no conversion step should be applied. Metallic is stored in the Ks and map_Ks slots and roughness is stored in the Ns and map_Ns slots.', 118 | type : 'boolean', 119 | default : defaults.metallicRoughness 120 | }, 121 | specularGlossiness : { 122 | describe : 'The values in the .mtl file are already specular-glossiness PBR values and no conversion step should be applied. Specular is stored in the Ks and map_Ks slots and glossiness is stored in the Ns and map_Ns slots. The glTF will be saved with the KHR_materials_pbrSpecularGlossiness extension.', 123 | type : 'boolean', 124 | default : defaults.specularGlossiness 125 | }, 126 | materialsCommon : { 127 | describe : 'The glTF will be saved with the KHR_materials_common extension.', 128 | type : 'boolean', 129 | default : defaults.materialsCommon 130 | }, 131 | metallicRoughnessOcclusionTexture : { 132 | describe : 'Path to the metallic-roughness-occlusion texture that should override textures in the .mtl file, where occlusion is stored in the red channel, roughness is stored in the green channel, and metallic is stored in the blue channel. The model will be saved with a pbrMetallicRoughness material. This is often convenient in workflows where the .mtl does not exist or is not set up to use PBR materials. Intended for models with a single material', 133 | type : 'string', 134 | normalize : true 135 | }, 136 | specularGlossinessTexture : { 137 | describe : 'Path to the specular-glossiness texture that should override textures in the .mtl file, where specular color is stored in the red, green, and blue channels and specular glossiness is stored in the alpha channel. The model will be saved with a material using the KHR_materials_pbrSpecularGlossiness extension.', 138 | type : 'string', 139 | normalize : true 140 | }, 141 | occlusionTexture : { 142 | describe : 'Path to the occlusion texture that should override textures in the .mtl file.', 143 | type : 'string', 144 | normalize : true 145 | }, 146 | normalTexture : { 147 | describe : 'Path to the normal texture that should override textures in the .mtl file.', 148 | type : 'string', 149 | normalize : true 150 | }, 151 | baseColorTexture : { 152 | describe : 'Path to the baseColor/diffuse texture that should override textures in the .mtl file.', 153 | type : 'string', 154 | normalize : true 155 | }, 156 | emissiveTexture : { 157 | describe : 'Path to the emissive texture that should override textures in the .mtl file.', 158 | type : 'string', 159 | normalize : true 160 | }, 161 | alphaTexture : { 162 | describe : 'Path to the alpha texture that should override textures in the .mtl file.' 163 | } 164 | }) 165 | .command('combine', 'Combine tilesets in to one tileset.json.') 166 | .parse(args); 167 | 168 | if(argv._[2] === 'combine') { 169 | console.time('Total'); 170 | return combine({ 171 | inputDir: argv.input, 172 | outputTileset: argv.output, 173 | }) 174 | .then(function(result) { 175 | var tileset = result.tileset; 176 | var output = result.output; 177 | return fsExtra.writeJson(output, tileset, {spaces: 2}); 178 | }) 179 | .then(function() { 180 | console.timeEnd('Total'); 181 | }) 182 | .catch(function(err) { 183 | console.log(err); 184 | }); 185 | } 186 | 187 | if (argv.metallicRoughness + argv.specularGlossiness + argv.materialsCommon > 1) { 188 | console.error('Only one material type may be set from [--metallicRoughness, --specularGlossiness, --materialsCommon].'); 189 | process.exit(1); 190 | } 191 | 192 | if (defined(argv.metallicRoughnessOcclusionTexture) && defined(argv.specularGlossinessTexture)) { 193 | console.error('--metallicRoughnessOcclusionTexture and --specularGlossinessTexture cannot both be set.'); 194 | process.exit(1); 195 | } 196 | 197 | var objPath = argv.input; 198 | var outputPath = argv.output; 199 | 200 | var name = path.basename(objPath, path.extname(objPath)); 201 | 202 | if (!defined(outputPath)) { 203 | outputPath = path.join(path.dirname(objPath), name + '.gltf'); 204 | } 205 | 206 | var outputDirectory = path.dirname(outputPath); 207 | var extension = path.extname(outputPath).toLowerCase(); 208 | if (argv.binary || extension === '.glb') { 209 | argv.binary = true; 210 | extension = '.glb'; 211 | } 212 | if (argv.tileset || argv.b3dm || extension === '.b3dm') { 213 | argv.binary = true; 214 | argv.batchId = true; 215 | argv.b3dm = true; 216 | extension = '.b3dm'; 217 | } 218 | if (argv.i3dm || extension === '.i3dm') { 219 | argv.binary = true; 220 | argv.batchId = false; 221 | argv.b3dm = false; 222 | argv.i3dm = true; 223 | extension = '.i3dm'; 224 | } 225 | outputPath = path.join(outputDirectory, name + extension); 226 | 227 | var overridingTextures = { 228 | metallicRoughnessOcclusionTexture : argv.metallicRoughnessOcclusionTexture, 229 | specularGlossinessTexture : argv.specularGlossinessTexture, 230 | occlusionTexture : argv.occlusionTexture, 231 | normalTexture : argv.normalTexture, 232 | baseColorTexture : argv.baseColorTexture, 233 | emissiveTexture : argv.emissiveTexture, 234 | alphaTexture : argv.alphaTexture 235 | }; 236 | 237 | var options = { 238 | binary : argv.binary, 239 | batchId: argv.batchId, 240 | b3dm: argv.b3dm, 241 | i3dm: argv.i3dm, 242 | outputBatchTable : argv.outputBatchTable, 243 | customBatchTable : argv.customBatchTable, 244 | customFeatureTable : argv.customFeatureTable, 245 | tileset : argv.tileset, 246 | tilesetOptions : argv.tilesetOptions, 247 | useOcclusion : argv.useOcclusion, 248 | separate : argv.separate, 249 | separateTextures : argv.separateTextures, 250 | checkTransparency : argv.checkTransparency, 251 | secure : argv.secure, 252 | packOcclusion : argv.packOcclusion, 253 | metallicRoughness : argv.metallicRoughness, 254 | specularGlossiness : argv.specularGlossiness, 255 | materialsCommon : argv.materialsCommon, 256 | overridingTextures : overridingTextures, 257 | outputDirectory : outputDirectory 258 | }; 259 | 260 | obj23dtiles(objPath, outputPath, options); 261 | -------------------------------------------------------------------------------- /lib/obj2I3dm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Cesium = require('cesium'); 4 | 5 | var createI3dm = require('./createI3dm'); 6 | var obj2gltf = require('./obj2gltf'); 7 | var defaultValue = Cesium.defaultValue; 8 | var Cartesian3 = Cesium.Cartesian3; 9 | var Matrix3 = Cesium.Matrix3; 10 | var CMath = Cesium.Math; 11 | 12 | module.exports = obj2I3dm; 13 | 14 | var sizeOfUint8 = 1; 15 | var sizeOfUint16 = 2; 16 | var sizeOfUint32 = 4; 17 | var sizeOfFloat32 = 4; 18 | 19 | /** 20 | * Convert obj file to i3dm with custon FeatureTable and BatchTable. 21 | * 22 | * @param {String} objPath The obj file path. 23 | * @param {Object} options Optional parameters. 24 | */ 25 | function obj2I3dm(objPath, options){ 26 | return obj2gltf(objPath, options) 27 | .then(function(result) { 28 | var glb = result.gltf; 29 | var defaultBatchTable = result.batchTableJson; 30 | var featureTable = defaultValue(options.customFeatureTable, undefined); 31 | var featureTableJson = {}; 32 | var featureTableBinary; 33 | var batchTableJson = defaultValue(options.customBatchTable, undefined); 34 | 35 | return new Promise(function(resolve, reject) { 36 | if (featureTable.position && Array.isArray(featureTable.position)) { 37 | var position = featureTable.position; 38 | var length = position.length; 39 | featureTableJson.INSTANCES_LENGTH = position.length; 40 | var attributes = []; 41 | attributes.push(getPositions(position)); 42 | attributes.push(getBatchIds(position.length)); 43 | if (featureTable.orientation) { 44 | if (featureTable.orientation.length !== length) { 45 | if (featureTable.orientation.length > length) { 46 | featureTable.orientation = featureTable.orientation.slice(0, length); 47 | console.log('FeatureTable Array length inconsistent. \'orientation\' and \'position\' have different length.'); 48 | } else { 49 | reject('FeatureTable Array length inconsistent. \'orientation\' and \'position\' have different length.'); 50 | } 51 | } 52 | attributes = attributes.concat(getOrientations(featureTable.orientation)); 53 | } 54 | if (featureTable.scale) { 55 | if (featureTable.scale.length !== length) { 56 | if (featureTable.scale.length > length) { 57 | featureTable.scale = featureTable.scale.slice(0, length); 58 | console.log('FeatureTable Array length inconsistent. \'scale\' and \'position\' have different length.'); 59 | } else { 60 | reject('FeatureTable Array length inconsistent. \'scale\' and \'position\' have different length.'); 61 | } 62 | } 63 | attributes.push(getScales(featureTable.scale)); 64 | } 65 | 66 | var i; 67 | var attribute; 68 | var byteOffset = 0; 69 | var attributesLength = attributes.length; 70 | for(i = 0; i < attributesLength; i++) { 71 | attribute = attributes[i]; 72 | var byteAlignment = attribute.byteAlignment; 73 | byteOffset = Math.ceil(byteOffset / byteAlignment) * byteAlignment; 74 | attribute.byteOffset = byteOffset; 75 | byteOffset += attribute.buffer.length; 76 | } 77 | 78 | featureTableBinary = Buffer.alloc(byteOffset); 79 | 80 | for (i = 0; i < attributesLength; i ++) { 81 | attribute = attributes[i]; 82 | featureTableJson[attribute.propertyName] = { 83 | byteOffset : attribute.byteOffset, 84 | componentType: attribute.componentType 85 | }; 86 | attribute.buffer.copy(featureTableBinary, attribute.byteOffset); 87 | } 88 | 89 | var i3dm = createI3dm({ 90 | featureTableJson: featureTableJson, 91 | featureTableBinary: featureTableBinary, 92 | batchTableJson: batchTableJson, 93 | glb: glb 94 | }); 95 | 96 | resolve({ 97 | i3dm : i3dm, 98 | batchTableJson: defaultBatchTable 99 | }); 100 | } 101 | reject('Invalued FeatureTable.'); 102 | }); 103 | 104 | 105 | function getPositions(instancePositions) { 106 | var instanceLength = instancePositions.length; 107 | var buffer = Buffer.alloc(instanceLength * 3 * sizeOfFloat32); 108 | 109 | for(var i = 0; i < instanceLength; i ++) { 110 | var position = instancePositions[i]; 111 | buffer.writeFloatLE(position[0], (i * 3) * sizeOfFloat32); 112 | buffer.writeFloatLE(position[1], (i * 3 + 1) * sizeOfFloat32); 113 | buffer.writeFloatLE(position[2], (i * 3 + 2) * sizeOfFloat32); 114 | } 115 | 116 | return { 117 | buffer : buffer, 118 | propertyName: 'POSITION', 119 | byteAlignment: sizeOfFloat32 120 | }; 121 | } 122 | 123 | function getBatchIds(instancesLength) { 124 | var i; 125 | var buffer; 126 | var componentType; 127 | var byteAlignment; 128 | 129 | if (instancesLength < 256) { 130 | buffer = Buffer.alloc(instancesLength * sizeOfUint8); 131 | for (i = 0; i < instancesLength; ++i) { 132 | buffer.writeUInt8(i, i * sizeOfUint8); 133 | } 134 | componentType = 'UNSIGNED_BYTE'; 135 | byteAlignment = sizeOfUint8; 136 | } else if (instancesLength < 65536) { 137 | buffer = Buffer.alloc(instancesLength * sizeOfUint16); 138 | for (i = 0; i < instancesLength; ++i) { 139 | buffer.writeUInt16LE(i, i * sizeOfUint16); 140 | } 141 | componentType = 'UNSIGNED_SHORT'; 142 | byteAlignment = sizeOfUint16; 143 | } else { 144 | buffer = Buffer.alloc(instancesLength * sizeOfUint32); 145 | for (i = 0; i < instancesLength; ++i) { 146 | buffer.writeUInt32LE(i, i * sizeOfUint32); 147 | } 148 | componentType = 'UNSIGNED_INT'; 149 | byteAlignment = sizeOfUint32; 150 | } 151 | 152 | return { 153 | buffer : buffer, 154 | componentType : componentType, 155 | propertyName : 'BATCH_ID', 156 | byteAlignment : byteAlignment 157 | }; 158 | } 159 | 160 | function getOrientations(orientations) { 161 | var length = orientations.length; 162 | var normalsUpBuffer = Buffer.alloc(length * 3 * sizeOfFloat32); 163 | var normalsRightBuffer = Buffer.alloc(length * 3 * sizeOfFloat32); 164 | 165 | for(var i = 0; i < length; i ++) { 166 | var rotate = orientations[i]; 167 | var up = new Cartesian3(0, 1, 0); 168 | var right = new Cartesian3(1, 0, 0); 169 | var m = Matrix3.fromRotationZ(CMath.toRadians(rotate[2])); 170 | up = Matrix3.multiplyByVector(m, up, new Cartesian3()); 171 | right = Matrix3.multiplyByVector(m, right, new Cartesian3()); 172 | m = Matrix3.fromRotationX(CMath.toRadians(rotate[0])); 173 | up = Matrix3.multiplyByVector(m, up, new Cartesian3()); 174 | right = Matrix3.multiplyByVector(m, right, new Cartesian3()); 175 | m = Matrix3.fromRotationY(CMath.toRadians(rotate[1])); 176 | up = Matrix3.multiplyByVector(m, up, new Cartesian3()); 177 | right = Matrix3.multiplyByVector(m, right, new Cartesian3()); 178 | up = Cartesian3.normalize(up, up); 179 | right = Cartesian3.normalize(right, right); 180 | 181 | normalsUpBuffer.writeFloatLE(up.x, (i * 3) * sizeOfFloat32); 182 | normalsUpBuffer.writeFloatLE(up.y, (i * 3 + 1) * sizeOfFloat32); 183 | normalsUpBuffer.writeFloatLE(up.z, (i * 3 + 2) * sizeOfFloat32); 184 | 185 | normalsRightBuffer.writeFloatLE(right.x, (i * 3) * sizeOfFloat32); 186 | normalsRightBuffer.writeFloatLE(right.y, (i * 3 + 1) * sizeOfFloat32); 187 | normalsRightBuffer.writeFloatLE(right.z, (i * 3 + 2) * sizeOfFloat32); 188 | } 189 | 190 | return [ 191 | { 192 | buffer : normalsUpBuffer, 193 | propertyName : 'NORMAL_UP', 194 | byteAlignment : sizeOfFloat32 195 | }, 196 | { 197 | buffer : normalsRightBuffer, 198 | propertyName : 'NORMAL_RIGHT', 199 | byteAlignment : sizeOfFloat32 200 | } 201 | ]; 202 | } 203 | 204 | function getScales(scale) { 205 | var length = scale.length; 206 | var buffer = Buffer.alloc(length * 3 * sizeOfFloat32); 207 | for(var i = 0; i < length; i++) { 208 | var s = scale[i]; 209 | buffer.writeFloatLE(s[0], (i * 3) * sizeOfFloat32); 210 | buffer.writeFloatLE(s[1], (i * 3 + 1) * sizeOfFloat32); 211 | buffer.writeFloatLE(s[2], (i * 3 + 2) * sizeOfFloat32); 212 | } 213 | 214 | return { 215 | buffer : buffer, 216 | propertyName : 'SCALE_NON_UNIFORM', 217 | byteAlignment : sizeOfFloat32 218 | }; 219 | } 220 | }); 221 | } 222 | 223 | obj2I3dm.defaults = { 224 | /** 225 | * Gets or sets whether create i3dm model file, with custom FeatureTable and BatchTable. 226 | * @type Boolean 227 | * @default false 228 | */ 229 | i3dm: false, 230 | /** 231 | * Sets the default FeatureTable json file or object. 232 | * @type Object 233 | * @default undefined 234 | */ 235 | customFeatureTable: undefined, 236 | /** 237 | * Sets the default BatchTable json file or object. 238 | * @type Object 239 | * @default undefined 240 | */ 241 | customBatchTable: undefined 242 | }; 243 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/createGltf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var getBufferPadded = require('./getBufferPadded'); 4 | var getDefaultMaterial = require('./loadMtl').getDefaultMaterial; 5 | var Texture = require('./Texture'); 6 | var ArrayStorage = require('./ArrayStorage'); 7 | 8 | var ComponentDatatype = Cesium.ComponentDatatype; 9 | var defined = Cesium.defined; 10 | var WebGLConstants = Cesium.WebGLConstants; 11 | 12 | module.exports = createGltf; 13 | 14 | var nameRegtex = /^[^_-]+/g; 15 | 16 | /** 17 | * Create a glTF from obj data. 18 | * 19 | * @param {Object} objData An object containing an array of nodes containing geometry information and an array of materials. 20 | * @param {Object} options The options object passed along from lib/obj2gltf.js 21 | * @returns {Object} A glTF asset. 22 | * 23 | * @private 24 | */ 25 | function createGltf(objData, options) { 26 | var nodes = objData.nodes; 27 | var materials = objData.materials; 28 | var name = objData.name; 29 | 30 | var gltf = { 31 | accessors : [], 32 | asset : {}, 33 | buffers : [], 34 | bufferViews : [], 35 | extensionsUsed : [], 36 | extensionsRequired : [], 37 | images : [], 38 | materials : [], 39 | meshes : [], 40 | nodes : [], 41 | samplers : [], 42 | scene : 0, 43 | scenes : [], 44 | textures : [] 45 | }; 46 | 47 | gltf.asset = { 48 | generator : 'obj2gltf', 49 | version: '2.0' 50 | }; 51 | 52 | gltf.scenes.push({ 53 | nodes : [] 54 | }); 55 | 56 | var bufferState = { 57 | positionBuffers : [], 58 | normalBuffers : [], 59 | uvBuffers : [], 60 | indexBuffers : [], 61 | positionAccessors : [], 62 | normalAccessors : [], 63 | uvAccessors : [], 64 | indexAccessors : [], 65 | batchIdBuffers: [], 66 | batchIdAccessors: [], 67 | currentBatchId: 0, 68 | batchTableJson: { 69 | batchId: [], 70 | name: [], 71 | maxPoint: [], 72 | minPoint: [] 73 | }, 74 | }; 75 | 76 | var uint32Indices = requiresUint32Indices(nodes); 77 | 78 | var nodesLength = nodes.length; 79 | for (var i = 0; i < nodesLength; ++i) { 80 | var node = nodes[i]; 81 | var meshes = node.meshes; 82 | var meshesLength = meshes.length; 83 | var meshIndex; 84 | 85 | if (meshesLength === 1) { 86 | meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, meshes[0], options); 87 | addNode(gltf, node.name, meshIndex, undefined); 88 | } else { 89 | // Add meshes as child nodes 90 | var parentIndex = addNode(gltf, node.name); 91 | for (var j = 0; j < meshesLength; ++j) { 92 | var mesh = meshes[j]; 93 | meshIndex = addMesh(gltf, materials, bufferState, uint32Indices, mesh, options); 94 | addNode(gltf, mesh.name, meshIndex, parentIndex); 95 | } 96 | } 97 | } 98 | 99 | if (gltf.images.length > 0) { 100 | gltf.samplers.push({ 101 | magFilter : WebGLConstants.LINEAR, 102 | minFilter : WebGLConstants.NEAREST_MIPMAP_LINEAR, 103 | wrapS : WebGLConstants.REPEAT, 104 | wrapT : WebGLConstants.REPEAT 105 | }); 106 | } 107 | 108 | addBuffers(gltf, bufferState, name); 109 | 110 | if (options.specularGlossiness) { 111 | gltf.extensionsUsed.push('KHR_materials_pbrSpecularGlossiness'); 112 | gltf.extensionsRequired.push('KHR_materials_pbrSpecularGlossiness'); 113 | } else if (options.materialsCommon) { 114 | gltf.extensionsUsed.push('KHR_materials_common'); 115 | gltf.extensionsRequired.push('KHR_materials_common'); 116 | } 117 | 118 | return { 119 | gltf: gltf, 120 | batchTableJson: bufferState.batchTableJson 121 | }; 122 | } 123 | 124 | function addBufferView(gltf, buffers, accessors, byteStride, target) { 125 | var length = buffers.length; 126 | if (length === 0) { 127 | return; 128 | } 129 | var bufferViewIndex = gltf.bufferViews.length; 130 | var previousBufferView = gltf.bufferViews[bufferViewIndex - 1]; 131 | var byteOffset = defined(previousBufferView) ? previousBufferView.byteOffset + previousBufferView.byteLength : 0; 132 | var byteLength = 0; 133 | for (var i = 0; i < length; ++i) { 134 | var accessor = gltf.accessors[accessors[i]]; 135 | accessor.bufferView = bufferViewIndex; 136 | accessor.byteOffset = byteLength; 137 | byteLength += buffers[i].length; 138 | } 139 | gltf.bufferViews.push({ 140 | name : 'bufferView_' + bufferViewIndex, 141 | buffer : 0, 142 | byteLength : byteLength, 143 | byteOffset : byteOffset, 144 | byteStride : byteStride, 145 | target : target 146 | }); 147 | } 148 | 149 | function addBuffers(gltf, bufferState, name) { 150 | // Positions and normals share the same byte stride so they can share the same bufferView 151 | var positionsAndNormalsAccessors = bufferState.positionAccessors.concat(bufferState.normalAccessors); 152 | var positionsAndNormalsBuffers = bufferState.positionBuffers.concat(bufferState.normalBuffers); 153 | addBufferView(gltf, positionsAndNormalsBuffers, positionsAndNormalsAccessors, 12, WebGLConstants.ARRAY_BUFFER); 154 | addBufferView(gltf, bufferState.uvBuffers, bufferState.uvAccessors, 8, WebGLConstants.ARRAY_BUFFER); 155 | addBufferView(gltf, bufferState.indexBuffers, bufferState.indexAccessors, undefined, WebGLConstants.ELEMENT_ARRAY_BUFFER); 156 | addBufferView(gltf, bufferState.batchIdBuffers, bufferState.batchIdAccessors, 0, WebGLConstants.ARRAY_BUFFER); 157 | 158 | var buffers = []; 159 | buffers = buffers.concat(bufferState.positionBuffers, bufferState.normalBuffers, bufferState.uvBuffers, bufferState.indexBuffers, bufferState.batchIdBuffers); 160 | var buffer = getBufferPadded(Buffer.concat(buffers)); 161 | 162 | gltf.buffers.push({ 163 | name : name, 164 | byteLength : buffer.length, 165 | extras : { 166 | _obj2gltf : { 167 | source : buffer 168 | } 169 | } 170 | }); 171 | } 172 | 173 | function addTexture(gltf, texture) { 174 | var imageName = texture.name; 175 | var textureName = texture.name; 176 | var imageIndex = gltf.images.length; 177 | var textureIndex = gltf.textures.length; 178 | 179 | gltf.images.push({ 180 | name : imageName, 181 | extras : { 182 | _obj2gltf : texture 183 | } 184 | }); 185 | 186 | gltf.textures.push({ 187 | name : textureName, 188 | sampler : 0, 189 | source : imageIndex 190 | }); 191 | 192 | return textureIndex; 193 | } 194 | 195 | function getTexture(gltf, texture) { 196 | var textureIndex; 197 | var name = texture.name; 198 | var textures = gltf.textures; 199 | var length = textures.length; 200 | for (var i = 0; i < length; ++i) { 201 | if (textures[i].name === name) { 202 | textureIndex = i; 203 | break; 204 | } 205 | } 206 | 207 | if (!defined(textureIndex)) { 208 | textureIndex = addTexture(gltf, texture); 209 | } 210 | 211 | return { 212 | index : textureIndex 213 | }; 214 | } 215 | 216 | function resolveTextures(gltf, material) { 217 | for (var name in material) { 218 | if (material.hasOwnProperty(name)) { 219 | var property = material[name]; 220 | if (property instanceof Texture) { 221 | material[name] = getTexture(gltf, property); 222 | } else if (!Array.isArray(property) && (typeof property === 'object')) { 223 | resolveTextures(gltf, property); 224 | } 225 | } 226 | } 227 | } 228 | 229 | function addMaterial(gltf, material, options) { 230 | resolveTextures(gltf, material); 231 | var materialIndex = gltf.materials.length; 232 | if (!options.useOcclusion) { 233 | material.occlusionTexture = undefined; 234 | } 235 | gltf.materials.push(material); 236 | return materialIndex; 237 | } 238 | 239 | function getMaterial(gltf, materials, materialName, options) { 240 | if (!defined(materialName)) { 241 | // Create a default material if the primitive does not specify one 242 | materialName = 'default'; 243 | } 244 | 245 | var i; 246 | var material; 247 | var materialsLength = materials.length; 248 | for (i = 0; i < materialsLength; ++i) { 249 | if (materials[i].name === materialName) { 250 | material = materials[i]; 251 | break; 252 | } 253 | } 254 | 255 | if (!defined(material)) { 256 | material = getDefaultMaterial(options); 257 | material.name = materialName; 258 | } 259 | 260 | var materialIndex; 261 | materialsLength = gltf.materials.length; 262 | for (i = 0; i < materialsLength; ++i) { 263 | if (gltf.materials[i].name === materialName) { 264 | materialIndex = i; 265 | break; 266 | } 267 | } 268 | 269 | if (!defined(materialIndex)) { 270 | materialIndex = addMaterial(gltf, material, options); 271 | } 272 | 273 | return materialIndex; 274 | } 275 | 276 | function addBacthIdAttribute(gltf, batchId, count, name) { 277 | var accessor = { 278 | name: name, 279 | componentType: WebGLConstants.UNSIGNED_SHORT, 280 | count: count, 281 | min: batchId, 282 | max: batchId, 283 | type: 'SCALAR' 284 | }; 285 | 286 | var accessorIndex = gltf.accessors.length; 287 | gltf.accessors.push(accessor); 288 | return accessorIndex; 289 | } 290 | 291 | function addVertexAttribute(gltf, array, components, name) { 292 | var count = array.length / components; 293 | var minMax = array.getMinMax(components); 294 | var type = (components === 3 ? 'VEC3' : 'VEC2'); 295 | 296 | var accessor = { 297 | name : name, 298 | componentType : WebGLConstants.FLOAT, 299 | count : count, 300 | min : minMax.min, 301 | max : minMax.max, 302 | type : type 303 | }; 304 | 305 | var accessorIndex = gltf.accessors.length; 306 | gltf.accessors.push(accessor); 307 | return accessorIndex; 308 | } 309 | 310 | function addIndexArray(gltf, array, uint32Indices, name) { 311 | var componentType = uint32Indices ? WebGLConstants.UNSIGNED_INT : WebGLConstants.UNSIGNED_SHORT; 312 | var count = array.length; 313 | var minMax = array.getMinMax(1); 314 | 315 | var accessor = { 316 | name : name, 317 | componentType : componentType, 318 | count : count, 319 | min : minMax.min, 320 | max : minMax.max, 321 | type : 'SCALAR' 322 | }; 323 | 324 | var accessorIndex = gltf.accessors.length; 325 | gltf.accessors.push(accessor); 326 | return accessorIndex; 327 | } 328 | 329 | function requiresUint32Indices(nodes) { 330 | var nodesLength = nodes.length; 331 | for (var i = 0; i < nodesLength; ++i) { 332 | var meshes = nodes[i].meshes; 333 | var meshesLength = meshes.length; 334 | for (var j = 0; j < meshesLength; ++j) { 335 | // Reserve the 65535 index for primitive restart 336 | var vertexCount = meshes[j].positions.length / 3; 337 | if (vertexCount > 65534) { 338 | return true; 339 | } 340 | } 341 | } 342 | return false; 343 | } 344 | 345 | function addMesh(gltf, materials, bufferState, uint32Indices, mesh, options) { 346 | var hasPositions = mesh.positions.length > 0; 347 | var hasNormals = mesh.normals.length > 0; 348 | var hasUVs = mesh.uvs.length > 0; 349 | var useBatchId = options.batchId; 350 | 351 | // Vertex attributes are shared by all primitives in the mesh 352 | var accessorIndex; 353 | var attributes = {}; 354 | if (hasPositions) { 355 | accessorIndex = addVertexAttribute(gltf, mesh.positions, 3, mesh.name + '_positions'); 356 | attributes.POSITION = accessorIndex; 357 | bufferState.positionBuffers.push(mesh.positions.toFloatBuffer()); 358 | bufferState.positionAccessors.push(accessorIndex); 359 | bufferState.batchTableJson.maxPoint.push(gltf.accessors[accessorIndex].max); 360 | bufferState.batchTableJson.minPoint.push(gltf.accessors[accessorIndex].min); 361 | } 362 | if (hasNormals) { 363 | accessorIndex = addVertexAttribute(gltf, mesh.normals, 3, mesh.name + '_normals'); 364 | attributes.NORMAL = accessorIndex; 365 | bufferState.normalBuffers.push(mesh.normals.toFloatBuffer()); 366 | bufferState.normalAccessors.push(accessorIndex); 367 | } 368 | if (hasUVs) { 369 | accessorIndex = addVertexAttribute(gltf, mesh.uvs, 2, mesh.name + '_texcoords'); 370 | attributes.TEXCOORD_0 = accessorIndex; 371 | bufferState.uvBuffers.push(mesh.uvs.toFloatBuffer()); 372 | bufferState.uvAccessors.push(accessorIndex); 373 | } 374 | if(useBatchId) { 375 | var batchIdCount = mesh.positions.length / 3; 376 | accessorIndex = addBacthIdAttribute(gltf, bufferState.currentBatchId, batchIdCount, mesh.name + '_batchId'); 377 | attributes._BATCHID = accessorIndex; 378 | var batchIdArray = new ArrayStorage(ComponentDatatype.UNSIGNED_SHORT); 379 | for(var j = 0; j < batchIdCount; j++){ 380 | batchIdArray.push(bufferState.currentBatchId); 381 | } 382 | bufferState.batchIdBuffers.push(batchIdArray.toUint16Buffer()); 383 | bufferState.batchIdAccessors.push(accessorIndex); 384 | bufferState.batchTableJson.batchId.push(bufferState.currentBatchId); 385 | bufferState.batchTableJson.name.push(mesh.name); 386 | bufferState.currentBatchId ++; 387 | } 388 | 389 | // Unload resources 390 | mesh.positions = undefined; 391 | mesh.normals = undefined; 392 | mesh.uvs = undefined; 393 | 394 | var gltfPrimitives = []; 395 | var primitives = mesh.primitives; 396 | var primitivesLength = primitives.length; 397 | for (var i = 0; i < primitivesLength; ++i) { 398 | var primitive = primitives[i]; 399 | var indexAccessorIndex = addIndexArray(gltf, primitive.indices, uint32Indices, mesh.name + '_' + i + '_indices'); 400 | var indexBuffer = uint32Indices ? primitive.indices.toUint32Buffer() : primitive.indices.toUint16Buffer(); 401 | bufferState.indexBuffers.push(indexBuffer); 402 | bufferState.indexAccessors.push(indexAccessorIndex); 403 | 404 | primitive.indices = undefined; // Unload resources 405 | 406 | var materialIndex = getMaterial(gltf, materials, primitive.material, options); 407 | 408 | gltfPrimitives.push({ 409 | attributes : attributes, 410 | indices : indexAccessorIndex, 411 | material : materialIndex, 412 | mode : WebGLConstants.TRIANGLES 413 | }); 414 | } 415 | 416 | var gltfMesh = { 417 | name : mesh.name, 418 | primitives : gltfPrimitives 419 | }; 420 | 421 | var meshIndex = gltf.meshes.length; 422 | gltf.meshes.push(gltfMesh); 423 | return meshIndex; 424 | } 425 | 426 | function addNode(gltf, name, meshIndex, parentIndex) { 427 | var node = { 428 | name : name, 429 | mesh : meshIndex 430 | }; 431 | 432 | var nodeIndex = gltf.nodes.length; 433 | gltf.nodes.push(node); 434 | 435 | if (defined(parentIndex)) { 436 | var parentNode = gltf.nodes[parentIndex]; 437 | if (!defined(parentNode.children)) { 438 | parentNode.children = []; 439 | } 440 | parentNode.children.push(nodeIndex); 441 | } else { 442 | gltf.scenes[gltf.scene].nodes.push(nodeIndex); 443 | } 444 | 445 | return nodeIndex; 446 | } 447 | -------------------------------------------------------------------------------- /lib/loadObj.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var path = require('path'); 4 | var Promise = require('bluebird'); 5 | 6 | var ArrayStorage = require('./ArrayStorage'); 7 | var loadMtl = require('./loadMtl'); 8 | var outsideDirectory = require('./outsideDirectory'); 9 | var readLines = require('./readLines'); 10 | 11 | var Cartesian2 = Cesium.Cartesian2; 12 | var Cartesian3 = Cesium.Cartesian3; 13 | var ComponentDatatype = Cesium.ComponentDatatype; 14 | var defaultValue = Cesium.defaultValue; 15 | var defined = Cesium.defined; 16 | var IntersectionTests = Cesium.IntersectionTests; 17 | var Matrix3 = Cesium.Matrix3; 18 | var OrientedBoundingBox = Cesium.OrientedBoundingBox; 19 | var Plane = Cesium.Plane; 20 | var PolygonPipeline = Cesium.PolygonPipeline; 21 | var Ray = Cesium.Ray; 22 | var RuntimeError = Cesium.RuntimeError; 23 | var WindingOrder = Cesium.WindingOrder; 24 | 25 | module.exports = loadObj; 26 | 27 | // Object name (o) -> node 28 | // Group name (g) -> mesh 29 | // Material name (usemtl) -> primitive 30 | 31 | function Node() { 32 | this.name = undefined; 33 | this.meshes = []; 34 | } 35 | 36 | function Mesh() { 37 | this.name = undefined; 38 | this.primitives = []; 39 | this.positions = new ArrayStorage(ComponentDatatype.FLOAT); 40 | this.normals = new ArrayStorage(ComponentDatatype.FLOAT); 41 | this.uvs = new ArrayStorage(ComponentDatatype.FLOAT); 42 | } 43 | 44 | function Primitive() { 45 | this.material = undefined; 46 | this.indices = new ArrayStorage(ComponentDatatype.UNSIGNED_INT); 47 | } 48 | 49 | // OBJ regex patterns are modified from ThreeJS (https://github.com/mrdoob/three.js/blob/master/examples/js/loaders/OBJLoader.js) 50 | var vertexPattern = /v( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // v float float float 51 | var normalPattern = /vn( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vn float float float 52 | var uvPattern = /vt( +[\d|\.|\+|\-|e|E]+)( +[\d|\.|\+|\-|e|E]+)/; // vt float float 53 | var facePattern = /(-?\d+)\/?(-?\d*)\/?(-?\d*)/g; // for any face format "f v", "f v/v", "f v//v", "f v/v/v" 54 | 55 | /** 56 | * Parse an obj file. 57 | * 58 | * @param {String} objPath Path to the obj file. 59 | * @param {Object} options The options object passed along from lib/obj2gltf.js 60 | * @returns {Promise} A promise resolving to the obj data, which includes an array of nodes containing geometry information and an array of materials. 61 | * 62 | * @private 63 | */ 64 | function loadObj(objPath, options) { 65 | // Global store of vertex attributes listed in the obj file 66 | var positions = new ArrayStorage(ComponentDatatype.FLOAT); 67 | var normals = new ArrayStorage(ComponentDatatype.FLOAT); 68 | var uvs = new ArrayStorage(ComponentDatatype.FLOAT); 69 | 70 | // The current node, mesh, and primitive 71 | var node; 72 | var mesh; 73 | var primitive; 74 | var activeMaterial; 75 | 76 | // All nodes seen in the obj 77 | var nodes = []; 78 | 79 | // Used to build the indices. The vertex cache is unique to each mesh. 80 | var vertexCache = {}; 81 | var vertexCacheLimit = 1000000; 82 | var vertexCacheCount = 0; 83 | var vertexCount = 0; 84 | 85 | // All mtl paths seen in the obj 86 | var mtlPaths = []; 87 | 88 | // Buffers for face data that spans multiple lines 89 | var lineBuffer = ''; 90 | 91 | // Used for parsing face data 92 | var faceVertices = []; 93 | var facePositions = []; 94 | var faceUvs = []; 95 | var faceNormals = []; 96 | 97 | var vertexIndices = []; 98 | 99 | function getName(name) { 100 | return (name === '' ? undefined : name); 101 | } 102 | 103 | function addNode(name) { 104 | node = new Node(); 105 | node.name = getName(name); 106 | nodes.push(node); 107 | addMesh(); 108 | } 109 | 110 | function addMesh(name) { 111 | mesh = new Mesh(); 112 | mesh.name = getName(name); 113 | node.meshes.push(mesh); 114 | addPrimitive(); 115 | 116 | // Clear the vertex cache for each new mesh 117 | vertexCache = {}; 118 | vertexCacheCount = 0; 119 | vertexCount = 0; 120 | } 121 | 122 | function addPrimitive() { 123 | primitive = new Primitive(); 124 | primitive.material = activeMaterial; 125 | mesh.primitives.push(primitive); 126 | } 127 | 128 | function useMaterial(name) { 129 | var material = getName(name); 130 | activeMaterial = material; 131 | 132 | // Look to see if this material has already been used by a primitive in the mesh 133 | var primitives = mesh.primitives; 134 | var primitivesLength = primitives.length; 135 | for (var i = 0; i < primitivesLength; ++i) { 136 | if (primitives[i].material === material) { 137 | primitive = primitives[i]; 138 | return; 139 | } 140 | } 141 | // Add a new primitive with this material 142 | addPrimitive(); 143 | } 144 | 145 | function getOffset(a, attributeData, components) { 146 | var i = parseInt(a); 147 | if (i < 0) { 148 | // Negative vertex indexes reference the vertices immediately above it 149 | return (attributeData.length / components + i) * components; 150 | } 151 | return (i - 1) * components; 152 | } 153 | 154 | function createVertex(p, u, n) { 155 | // Positions 156 | if (p.length > 0) { 157 | var pi = getOffset(p, positions, 3); 158 | var px = positions.get(pi + 0); 159 | var py = positions.get(pi + 1); 160 | var pz = positions.get(pi + 2); 161 | mesh.positions.push(px); 162 | mesh.positions.push(py); 163 | mesh.positions.push(pz); 164 | } 165 | 166 | // Normals 167 | if (n.length > 0) { 168 | var ni = getOffset(n, normals, 3); 169 | var nx = normals.get(ni + 0); 170 | var ny = normals.get(ni + 1); 171 | var nz = normals.get(ni + 2); 172 | mesh.normals.push(nx); 173 | mesh.normals.push(ny); 174 | mesh.normals.push(nz); 175 | } 176 | 177 | // UVs 178 | if (u.length > 0) { 179 | var ui = getOffset(u, uvs, 2); 180 | var ux = uvs.get(ui + 0); 181 | var uy = uvs.get(ui + 1); 182 | mesh.uvs.push(ux); 183 | mesh.uvs.push(uy); 184 | } 185 | } 186 | 187 | function addVertex(v, p, u, n) { 188 | var index = vertexCache[v]; 189 | if (!defined(index)) { 190 | index = vertexCount++; 191 | vertexCache[v] = index; 192 | createVertex(p, u, n); 193 | 194 | // Prevent the vertex cache from growing too large. As a result of clearing the cache there 195 | // may be some duplicate vertices. 196 | vertexCacheCount++; 197 | if (vertexCacheCount > vertexCacheLimit) { 198 | vertexCacheCount = 0; 199 | vertexCache = {}; 200 | } 201 | } 202 | return index; 203 | } 204 | 205 | // Given a set of 3D points, project them onto whichever axis will produce the least distortion. 206 | var scratchIntersectionPoint = new Cartesian3(); 207 | var scratchXAxis = new Cartesian3(); 208 | var scratchYAxis = new Cartesian3(); 209 | var scratchZAxis = new Cartesian3(); 210 | var scratchOrigin = new Cartesian3(); 211 | var scratchNormal = new Cartesian3(); 212 | var scratchRay = new Ray(); 213 | var scratchPlane = new Plane(Cesium.Cartesian3.UNIT_X, 0); 214 | var scratchPositions2D = [new Cartesian2(), new Cartesian2(), new Cartesian2()]; 215 | function projectTo2D(positions) { 216 | var positions2D = []; 217 | var obb = OrientedBoundingBox.fromPoints(positions); 218 | var halfAxes = obb.halfAxes; 219 | Matrix3.getColumn(halfAxes, 0, scratchXAxis); 220 | Matrix3.getColumn(halfAxes, 1, scratchYAxis); 221 | Matrix3.getColumn(halfAxes, 2, scratchZAxis); 222 | 223 | var xMag = Cartesian3.magnitude(scratchXAxis); 224 | var yMag = Cartesian3.magnitude(scratchYAxis); 225 | var zMag = Cartesian3.magnitude(scratchZAxis); 226 | var min = Math.min(xMag, yMag, zMag); 227 | 228 | var i; 229 | // If all the points are on a line, just remove one of the zero dimensions 230 | if (xMag === 0 && (yMag === 0 || zMag === 0)) { 231 | for (i = 0; i < positions.length; i++) { 232 | if (i === scratchPositions2D.length) { 233 | scratchPositions2D.push(new Cartesian2()); 234 | } 235 | positions2D[i] = new Cartesian2.fromElements(positions[i].y, positions[i].z, scratchPositions2D[i]); 236 | } 237 | return positions2D; 238 | } else if (yMag === 0 && zMag === 0) { 239 | for (i = 0; i < positions.length; i++) { 240 | if (i === scratchPositions2D.length) { 241 | scratchPositions2D.push(new Cartesian2()); 242 | } 243 | positions2D[i] = new Cartesian2.fromElements(positions[i].x, positions[i].y, scratchPositions2D[i]); 244 | } 245 | return positions2D; 246 | } 247 | 248 | var center = obb.center; 249 | var planeXAxis; 250 | var planeYAxis; 251 | if (min === xMag) { 252 | if (!scratchXAxis.equals(Cartesian3.ZERO)) { 253 | Cartesian3.add(center, scratchXAxis, scratchOrigin); 254 | Cartesian3.normalize(scratchXAxis, scratchNormal); 255 | } 256 | planeXAxis = Cartesian3.normalize(scratchYAxis, scratchYAxis); 257 | planeYAxis = Cartesian3.normalize(scratchZAxis, scratchZAxis); 258 | } else if (min === yMag) { 259 | if (!scratchYAxis.equals(Cartesian3.ZERO)) { 260 | Cartesian3.add(center, scratchYAxis, scratchOrigin); 261 | Cartesian3.normalize(scratchYAxis, scratchNormal); 262 | } 263 | planeXAxis = Cartesian3.normalize(scratchXAxis, scratchXAxis); 264 | planeYAxis = Cartesian3.normalize(scratchZAxis, scratchZAxis); 265 | } else { 266 | if (!scratchZAxis.equals(Cartesian3.ZERO)) { 267 | Cartesian3.add(center, scratchZAxis, scratchOrigin); 268 | Cartesian3.normalize(scratchZAxis, scratchNormal); 269 | } 270 | planeXAxis = Cartesian3.normalize(scratchXAxis, scratchXAxis); 271 | planeYAxis = Cartesian3.normalize(scratchYAxis, scratchYAxis); 272 | } 273 | 274 | if (min === 0) { 275 | scratchNormal = Cartesian3.cross(planeXAxis, planeYAxis, scratchNormal); 276 | scratchNormal = Cartesian3.normalize(scratchNormal, scratchNormal); 277 | } 278 | 279 | Plane.fromPointNormal(scratchOrigin, scratchNormal, scratchPlane); 280 | scratchRay.direction = scratchNormal; 281 | 282 | for (i = 0; i < positions.length; i++) { 283 | scratchRay.origin = positions[i]; 284 | 285 | var intersectionPoint = IntersectionTests.rayPlane(scratchRay, scratchPlane, scratchIntersectionPoint); 286 | 287 | if (!defined(intersectionPoint)) { 288 | Cartesian3.negate(scratchRay.direction, scratchRay.direction); 289 | intersectionPoint = IntersectionTests.rayPlane(scratchRay, scratchPlane, scratchIntersectionPoint); 290 | } 291 | var v = Cartesian3.subtract(intersectionPoint, scratchOrigin, intersectionPoint); 292 | var x = Cartesian3.dot(planeXAxis, v); 293 | var y = Cartesian3.dot(planeYAxis, v); 294 | 295 | if (i === scratchPositions2D.length) { 296 | scratchPositions2D.push(new Cartesian2()); 297 | } 298 | 299 | positions2D[i] = new Cartesian2.fromElements(x, y, scratchPositions2D[i]); 300 | } 301 | 302 | return positions2D; 303 | } 304 | 305 | function get3DPoint(index, result) { 306 | var pi = getOffset(index, positions, 3); 307 | var px = positions.get(pi + 0); 308 | var py = positions.get(pi + 1); 309 | var pz = positions.get(pi + 2); 310 | return Cartesian3.fromElements(px, py, pz, result); 311 | } 312 | 313 | function get3DNormal(index, result) { 314 | var ni = getOffset(index, normals, 3); 315 | var nx = normals.get(ni + 0); 316 | var ny = normals.get(ni + 1); 317 | var nz = normals.get(ni + 2); 318 | return Cartesian3.fromElements(nx, ny, nz, result); 319 | } 320 | 321 | // Given a sequence of three points A B C, determine whether vector BC 322 | // "turns" clockwise (positive) or counter-clockwise (negative) from vector AB 323 | var scratch1 = new Cartesian3(); 324 | var scratch2 = new Cartesian3(); 325 | function getTurnDirection(pointA, pointB, pointC) { 326 | var vector1 = Cartesian2.subtract(pointA, pointB, scratch1); 327 | var vector2 = Cartesian2.subtract(pointC, pointB, scratch2); 328 | return vector1.x * vector2.y - vector1.y * vector2.x; 329 | } 330 | 331 | // Given the cartesian 2 vertices of a polygon, determine if convex 332 | function isConvex(positions2D) { 333 | var length = position2D.length; 334 | var turnDirection = getTurnDirection(positions2D[0], positions2D[1], positions2D[2]); 335 | for (var i=1; i < length-2; ++i) { 336 | var currentTurnDirection = getTurnDirection(positions2D[i], positions2D[i+1], positions2D[i+2]); 337 | if (turnDirection * currentTurnDirection < 0) { 338 | return false; 339 | } 340 | } 341 | var currentTurnDirection = getTurnDirection(position2D[length-2], position2D[length-1], position2D[0]); 342 | if (turnDirection * currentTurnDirection < 0) { 343 | return false; 344 | } 345 | currentTurnDirection = getTurnDirection(position2D[length-1], position2D[0], position2D[1]); 346 | if (turnDirection * currentTurnDirection < 0) { 347 | return false; 348 | } 349 | return true; 350 | } 351 | 352 | var scratch3 = new Cartesian3(); 353 | var scratch4 = new Cartesian3(); 354 | var scratch5 = new Cartesian3(); 355 | // Checks if winding order matches the given normal. 356 | function checkWindingCorrect(positionIndex1, positionIndex2, positionIndex3, normal) { 357 | var A = get3DPoint(positionIndex1, scratch1); 358 | var B = get3DPoint(positionIndex2, scratch2); 359 | var C = get3DPoint(positionIndex3, scratch3); 360 | 361 | var BA = Cartesian3.subtract(B, A, scratch4); 362 | var CA = Cartesian3.subtract(C, A, scratch5); 363 | var cross = Cartesian3.cross(BA, CA, scratch3); 364 | 365 | return (Cartesian3.dot(normal, cross) >= 0); 366 | } 367 | 368 | function addTriangle(index1, index2, index3, correctWinding) { 369 | if (correctWinding) { 370 | primitive.indices.push(index1); 371 | primitive.indices.push(index2); 372 | primitive.indices.push(index3); 373 | } else { 374 | primitive.indices.push(index1); 375 | primitive.indices.push(index3); 376 | primitive.indices.push(index2); 377 | } 378 | } 379 | 380 | var scratchPositions3D = [new Cartesian3(), new Cartesian3(), new Cartesian3()]; 381 | function addFace(vertices, positions, uvs, normals) { 382 | var isWindingCorrect = true; 383 | var faceNormal; 384 | 385 | // If normals are defined, find a face normal to use in winding order sanitization. 386 | // If no face normal, we have to assume the winding is correct. 387 | if (normals[0].length > 0) { 388 | faceNormal = get3DNormal(normals[0], scratchNormal); 389 | isWindingCorrect = checkWindingCorrect(positions[0], positions[1], positions[2], faceNormal); 390 | } 391 | 392 | if (vertices.length === 3) { 393 | var index1 = addVertex(vertices[0], positions[0], uvs[0], normals[0]); 394 | var index2 = addVertex(vertices[1], positions[1], uvs[1], normals[1]); 395 | var index3 = addVertex(vertices[2], positions[2], uvs[2], normals[2]); 396 | addTriangle(index1, index2, index3, isWindingCorrect); 397 | } else { // Triangulate if the face is not a triangle 398 | var positions3D = []; 399 | vertexIndices.length = 0; 400 | 401 | var i; 402 | for (i = 0; i < vertices.length; ++i) { 403 | var index = addVertex(vertices[i], positions[i], uvs[i], normals[i]); 404 | vertexIndices.push(index); 405 | 406 | // Collect the vertex positions as 3D points 407 | if (i === scratchPositions3D.length) { 408 | scratchPositions3D.push(new Cartesian3()); 409 | } 410 | positions3D.push(get3DPoint(positions[i], scratchPositions3D[i])); 411 | } 412 | 413 | var positions2D = projectTo2D(positions3D); 414 | 415 | if (isConvex(positions2D)) { 416 | for (i=1; i < vertices.length-1; ++i) { 417 | addTriangle(vertexIndices[0], vertexIndices[i], vertexIndices[i+1], isWindingCorrect); 418 | } 419 | } else { 420 | // Since the projection doesn't preserve winding order, reverse the order of 421 | // the vertices before triangulating to enforce counter clockwise. 422 | var projectedWindingOrder = PolygonPipeline.computeWindingOrder2D(positions2D); 423 | if (projectedWindingOrder === WindingOrder.CLOCKWISE) { 424 | positions2D.reverse(); 425 | } 426 | 427 | // Use an ear-clipping algorithm to triangulate 428 | var positionIndices = PolygonPipeline.triangulate(positions2D); 429 | for (i = 0; i < positionIndices.length-2; i += 3) { 430 | addTriangle(vertexIndices[positionIndices[i]], vertexIndices[positionIndices[i+1]], vertexIndices[positionIndices[i+2]], isWindingCorrect); 431 | } 432 | } 433 | } 434 | } 435 | 436 | function parseLine(line) { 437 | line = line.trim(); 438 | var result; 439 | 440 | if ((line.length === 0) || (line.charAt(0) === '#')) { 441 | // Don't process empty lines or comments 442 | } else if (/^o\s/i.test(line)) { 443 | var objectName = line.substring(2).trim(); 444 | addNode(objectName); 445 | } else if (/^g\s/i.test(line)) { 446 | var groupName = line.substring(2).trim(); 447 | addMesh(groupName); 448 | } else if (/^usemtl\s/i.test(line)) { 449 | var materialName = line.substring(7).trim(); 450 | useMaterial(materialName); 451 | } else if (/^mtllib/i.test(line)) { 452 | var mtllibLine = line.substring(7).trim(); 453 | mtlPaths = mtlPaths.concat(getMtlPaths(mtllibLine)); 454 | } else if ((result = vertexPattern.exec(line)) !== null) { 455 | positions.push(parseFloat(result[1])); 456 | positions.push(parseFloat(result[2])); 457 | positions.push(parseFloat(result[3])); 458 | } else if ((result = normalPattern.exec(line) ) !== null) { 459 | var normal = Cartesian3.fromElements(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]), scratchNormal); 460 | if (Cartesian3.equals(normal, Cartesian3.ZERO)) { 461 | Cartesian3.clone(Cartesian3.UNIT_Z, normal); 462 | } else { 463 | Cartesian3.normalize(normal, normal); 464 | } 465 | normals.push(normal.x); 466 | normals.push(normal.y); 467 | normals.push(normal.z); 468 | } else if ((result = uvPattern.exec(line)) !== null) { 469 | uvs.push(parseFloat(result[1])); 470 | uvs.push(1.0 - parseFloat(result[2])); // Flip y so 0.0 is the bottom of the image 471 | } else { // face line or invalid line 472 | // Because face lines can contain n vertices, we use a line buffer in case the face data spans multiple lines. 473 | // If there's a line continuation don't create face yet 474 | if (line.slice(-1) === '\\') { 475 | lineBuffer += line.substring(0, line.length-1); 476 | return; 477 | } 478 | lineBuffer += line; 479 | if (lineBuffer.substring(0, 2) === 'f ') { 480 | while ((result = facePattern.exec(lineBuffer)) !== null) { 481 | faceVertices.push(result[0]); 482 | facePositions.push(result[1]); 483 | faceUvs.push(result[2]); 484 | faceNormals.push(result[3]); 485 | } 486 | if (faceVertices.length > 2) { 487 | addFace(faceVertices, facePositions, faceUvs, faceNormals); 488 | } 489 | 490 | faceVertices.length = 0; 491 | facePositions.length = 0; 492 | faceNormals.length = 0; 493 | faceUvs.length = 0; 494 | } 495 | lineBuffer = ''; 496 | } 497 | } 498 | 499 | // Create a default node in case there are no o/g/usemtl lines in the obj 500 | addNode(); 501 | 502 | // Parse the obj file 503 | return readLines(objPath, parseLine) 504 | .then(function() { 505 | // Add hasNormals to options object for loadMtl 506 | options.hasNormals = normals.length > 0; 507 | 508 | // Unload resources 509 | positions = undefined; 510 | normals = undefined; 511 | uvs = undefined; 512 | 513 | // Load materials and textures 514 | return finishLoading(nodes, mtlPaths, objPath, options); 515 | }); 516 | } 517 | 518 | function getMtlPaths(mtllibLine) { 519 | // Handle paths with spaces. E.g. mtllib my material file.mtl 520 | var mtlPaths = []; 521 | var splits = mtllibLine.split(' '); 522 | var length = splits.length; 523 | var startIndex = 0; 524 | for (var i = 0; i < length; ++i) { 525 | if (path.extname(splits[i]) !== '.mtl') { 526 | continue; 527 | } 528 | var mtlPath = splits.slice(startIndex, i + 1).join(' '); 529 | mtlPaths.push(mtlPath); 530 | startIndex = i + 1; 531 | } 532 | return mtlPaths; 533 | } 534 | 535 | function finishLoading(nodes, mtlPaths, objPath, options) { 536 | nodes = cleanNodes(nodes); 537 | if (nodes.length === 0) { 538 | throw new RuntimeError(objPath + ' does not have any geometry data'); 539 | } 540 | var name = path.basename(objPath, path.extname(objPath)); 541 | return loadMtls(mtlPaths, objPath, options) 542 | .then(function(materials) { 543 | assignDefaultMaterial(nodes, materials); 544 | return { 545 | nodes : nodes, 546 | materials : materials, 547 | name : name 548 | }; 549 | }); 550 | } 551 | 552 | function loadMtls(mtlPaths, objPath, options) { 553 | var objDirectory = path.dirname(objPath); 554 | var materials = []; 555 | 556 | // Remove duplicates 557 | mtlPaths = mtlPaths.filter(function(value, index, self) { 558 | return self.indexOf(value) === index; 559 | }); 560 | 561 | return Promise.map(mtlPaths, function(mtlPath) { 562 | mtlPath = path.resolve(objDirectory, mtlPath); 563 | var shallowPath = path.resolve(path.join(objDirectory, path.basename(mtlPath))); 564 | if (options.secure && outsideDirectory(mtlPath, objDirectory)) { 565 | // Try looking for the .mtl in the same directory as the obj 566 | options.logger('The material file is outside of the obj directory and the secure flag is true. Attempting to read the material file from within the obj directory instead.'); 567 | return loadMtl(shallowPath, options) 568 | .then(function(materialsInMtl) { 569 | materials = materials.concat(materialsInMtl); 570 | }) 571 | .catch(function(error) { 572 | options.logger(error.message); 573 | options.logger('Could not read material file at ' + shallowPath + '. Using default material instead.'); 574 | }); 575 | } 576 | 577 | return loadMtl(mtlPath, options) 578 | .catch(function(error) { 579 | // Try looking for the .mtl in the same directory as the obj 580 | options.logger(error.message); 581 | options.logger('Could not read material file at ' + mtlPath + '. Attempting to read the material file from within the obj directory instead.'); 582 | return loadMtl(shallowPath, options); 583 | }) 584 | .then(function(materialsInMtl) { 585 | materials = materials.concat(materialsInMtl); 586 | }) 587 | .catch(function(error) { 588 | options.logger(error.message); 589 | options.logger('Could not read material file at ' + shallowPath + '. Using default material instead.'); 590 | }); 591 | }, {concurrency : 10}) 592 | .then(function() { 593 | return materials; 594 | }); 595 | } 596 | 597 | function assignDefaultMaterial(nodes, materials) { 598 | if (materials.length === 0) { 599 | return; 600 | } 601 | var defaultMaterial = materials[0].name; 602 | var nodesLength = nodes.length; 603 | for (var i = 0; i < nodesLength; ++i) { 604 | var meshes = nodes[i].meshes; 605 | var meshesLength = meshes.length; 606 | for (var j = 0; j < meshesLength; ++j) { 607 | var primitives = meshes[j].primitives; 608 | var primitivesLength = primitives.length; 609 | for (var k = 0; k < primitivesLength; ++k) { 610 | var primitive = primitives[k]; 611 | primitive.material = defaultValue(primitive.material, defaultMaterial); 612 | } 613 | } 614 | } 615 | } 616 | 617 | function removeEmptyMeshes(meshes) { 618 | return meshes.filter(function(mesh) { 619 | // Remove empty primitives 620 | mesh.primitives = mesh.primitives.filter(function(primitive) { 621 | return primitive.indices.length > 0; 622 | }); 623 | // Valid meshes must have at least one primitive and contain positions 624 | return (mesh.primitives.length > 0) && (mesh.positions.length > 0); 625 | }); 626 | } 627 | 628 | function meshesHaveNames(meshes) { 629 | var meshesLength = meshes.length; 630 | for (var i = 0; i < meshesLength; ++i) { 631 | if (defined(meshes[i].name)) { 632 | return true; 633 | } 634 | } 635 | return false; 636 | } 637 | 638 | function removeEmptyNodes(nodes) { 639 | var final = []; 640 | var nodesLength = nodes.length; 641 | for (var i = 0; i < nodesLength; ++i) { 642 | var node = nodes[i]; 643 | var meshes = removeEmptyMeshes(node.meshes); 644 | if (meshes.length === 0) { 645 | continue; 646 | } 647 | node.meshes = meshes; 648 | if (!defined(node.name) && meshesHaveNames(meshes)) { 649 | // If the obj has groups (g) but not object groups (o) then convert meshes to nodes 650 | var meshesLength = meshes.length; 651 | for (var j = 0; j < meshesLength; ++j) { 652 | var mesh = meshes[j]; 653 | var convertedNode = new Node(); 654 | convertedNode.name = mesh.name; 655 | convertedNode.meshes = [mesh]; 656 | final.push(convertedNode); 657 | } 658 | } else { 659 | final.push(node); 660 | } 661 | } 662 | return final; 663 | } 664 | 665 | function setDefaultNames(items, defaultName, usedNames) { 666 | var itemsLength = items.length; 667 | for (var i = 0; i < itemsLength; ++i) { 668 | var item = items[i]; 669 | var name = defaultValue(item.name, defaultName); 670 | var occurrences = usedNames[name]; 671 | if (defined(occurrences)) { 672 | usedNames[name]++; 673 | name = name + '_' + occurrences; 674 | } else { 675 | usedNames[name] = 1; 676 | } 677 | item.name = name; 678 | } 679 | } 680 | 681 | function setDefaults(nodes) { 682 | var usedNames = {}; 683 | setDefaultNames(nodes, 'Node', usedNames); 684 | var nodesLength = nodes.length; 685 | for (var i = 0; i < nodesLength; ++i) { 686 | var node = nodes[i]; 687 | setDefaultNames(node.meshes, node.name + '-Mesh', usedNames); 688 | } 689 | } 690 | 691 | function cleanNodes(nodes) { 692 | nodes = removeEmptyNodes(nodes); 693 | setDefaults(nodes); 694 | return nodes; 695 | } 696 | -------------------------------------------------------------------------------- /lib/loadMtl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Cesium = require('cesium'); 3 | var path = require('path'); 4 | var Promise = require('bluebird'); 5 | var loadTexture = require('./loadTexture'); 6 | var outsideDirectory = require('./outsideDirectory'); 7 | var readLines = require('./readLines'); 8 | var Texture = require('./Texture'); 9 | 10 | var CesiumMath = Cesium.Math; 11 | var combine = Cesium.combine; 12 | var defaultValue = Cesium.defaultValue; 13 | var defined = Cesium.defined; 14 | 15 | module.exports = loadMtl; 16 | 17 | /** 18 | * Parse a .mtl file and load textures referenced within. Returns an array of glTF materials with Texture 19 | * objects stored in the texture slots. 20 | *

21 | * Packed PBR textures (like metallicRoughnessOcclusion and specularGlossiness) require all input textures to be decoded before hand. 22 | * If a texture is of an unsupported format like .gif or .tga it can't be packed and a metallicRoughness texture will not be created. 23 | * Similarly if a texture cannot be found it will be ignored and a default value will be used instead. 24 | *

25 | * 26 | * @param {String} mtlPath Path to the .mtl file. 27 | * @param {Object} options The options object passed along from lib/obj2gltf.js 28 | * @param {Boolean} options.hasNormals Whether the model has normals. 29 | * @returns {Promise} A promise resolving to an array of glTF materials with Texture objects stored in the texture slots. 30 | * 31 | * @private 32 | */ 33 | function loadMtl(mtlPath, options) { 34 | var material; 35 | var values; 36 | var value; 37 | 38 | var mtlDirectory = path.dirname(mtlPath); 39 | var materials = []; 40 | var texturePromiseMap = {}; // Maps texture paths to load promises so that no texture is loaded twice 41 | var texturePromises = []; 42 | 43 | var overridingTextures = options.overridingTextures; 44 | var overridingSpecularTexture = defaultValue(overridingTextures.metallicRoughnessOcclusionTexture, overridingTextures.specularGlossinessTexture); 45 | var overridingSpecularShininessTexture = defaultValue(overridingTextures.metallicRoughnessOcclusionTexture, overridingTextures.specularGlossinessTexture); 46 | var overridingAmbientTexture = defaultValue(overridingTextures.metallicRoughnessOcclusionTexture, overridingTextures.occlusionTexture); 47 | var overridingNormalTexture = overridingTextures.normalTexture; 48 | var overridingDiffuseTexture = overridingTextures.baseColorTexture; 49 | var overridingEmissiveTexture = overridingTextures.emissiveTexture; 50 | var overridingAlphaTexture = overridingTextures.alphaTexture; 51 | 52 | // Textures that are packed into PBR textures need to be decoded first 53 | var decodeOptions = options.materialsCommon ? undefined : { 54 | decode : true 55 | }; 56 | 57 | var diffuseTextureOptions = { 58 | checkTransparency : options.checkTransparency 59 | }; 60 | 61 | var ambientTextureOptions = defined(overridingAmbientTexture) ? undefined : (options.packOcclusion ? decodeOptions : undefined); 62 | var specularTextureOptions = defined(overridingSpecularTexture) ? undefined : decodeOptions; 63 | var specularShinessTextureOptions = defined(overridingSpecularShininessTexture) ? undefined : decodeOptions; 64 | var emissiveTextureOptions; 65 | var normalTextureOptions; 66 | var alphaTextureOptions = { 67 | decode : true 68 | }; 69 | 70 | function createMaterial(name) { 71 | material = new Material(); 72 | material.name = name; 73 | material.specularShininess = options.metallicRoughness ? 1.0 : 0.0; 74 | material.specularTexture = overridingSpecularTexture; 75 | material.specularShininessTexture = overridingSpecularShininessTexture; 76 | material.diffuseTexture = overridingDiffuseTexture; 77 | material.ambientTexture = overridingAmbientTexture; 78 | material.normalTexture = overridingNormalTexture; 79 | material.emissiveTexture = overridingEmissiveTexture; 80 | material.alphaTexture = overridingAlphaTexture; 81 | materials.push(material); 82 | } 83 | 84 | /** 85 | * Removes texture options from texture name 86 | * NOTE: assumes no spaces in texture name 87 | * 88 | * @param {String} name 89 | * @returns {String} The clean texture name 90 | */ 91 | function cleanTextureName (name) { 92 | var re = /-(bm|t|s|o|blendu|blendv|boost|mm|texres|clamp|imfchan|type)/; 93 | if (re.test(name)) { 94 | return name.split(/\s+/).pop(); 95 | } 96 | return name; 97 | } 98 | 99 | function parseLine(line) { 100 | line = line.trim(); 101 | if (/^newmtl /i.test(line)) { 102 | var name = line.substring(7).trim(); 103 | createMaterial(name); 104 | } else if (/^Ka /i.test(line)) { 105 | values = line.substring(3).trim().split(' '); 106 | material.ambientColor = [ 107 | parseFloat(values[0]), 108 | parseFloat(values[1]), 109 | parseFloat(values[2]), 110 | 1.0 111 | ]; 112 | } else if (/^Ke /i.test(line)) { 113 | values = line.substring(3).trim().split(' '); 114 | material.emissiveColor = [ 115 | parseFloat(values[0]), 116 | parseFloat(values[1]), 117 | parseFloat(values[2]), 118 | 1.0 119 | ]; 120 | } else if (/^Kd /i.test(line)) { 121 | values = line.substring(3).trim().split(' '); 122 | material.diffuseColor = [ 123 | parseFloat(values[0]), 124 | parseFloat(values[1]), 125 | parseFloat(values[2]), 126 | 1.0 127 | ]; 128 | } else if (/^Ks /i.test(line)) { 129 | values = line.substring(3).trim().split(' '); 130 | material.specularColor = [ 131 | parseFloat(values[0]), 132 | parseFloat(values[1]), 133 | parseFloat(values[2]), 134 | 1.0 135 | ]; 136 | } else if (/^Ns /i.test(line)) { 137 | value = line.substring(3).trim(); 138 | material.specularShininess = parseFloat(value); 139 | } else if (/^d /i.test(line)) { 140 | value = line.substring(2).trim(); 141 | material.alpha = correctAlpha(parseFloat(value)); 142 | } else if (/^Tr /i.test(line)) { 143 | value = line.substring(3).trim(); 144 | material.alpha = correctAlpha(1.0 - parseFloat(value)); 145 | } else if (/^map_Ka /i.test(line)) { 146 | if (!defined(overridingAmbientTexture)) { 147 | material.ambientTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); 148 | } 149 | } else if (/^map_Ke /i.test(line)) { 150 | if (!defined(overridingEmissiveTexture)) { 151 | material.emissiveTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); 152 | } 153 | } else if (/^map_Kd /i.test(line)) { 154 | if (!defined(overridingDiffuseTexture)) { 155 | material.diffuseTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); 156 | } 157 | } else if (/^map_Ks /i.test(line)) { 158 | if (!defined(overridingSpecularTexture)) { 159 | material.specularTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); 160 | } 161 | } else if (/^map_Ns /i.test(line)) { 162 | if (!defined(overridingSpecularShininessTexture)) { 163 | material.specularShininessTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(7).trim())); 164 | } 165 | } else if (/^map_Bump /i.test(line)) { 166 | if (!defined(overridingNormalTexture)) { 167 | material.normalTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(9).trim())); 168 | } 169 | } else if (/^map_d /i.test(line)) { 170 | if (!defined(overridingAlphaTexture)) { 171 | material.alphaTexture = path.resolve(mtlDirectory, cleanTextureName(line.substring(6).trim())); 172 | } 173 | } 174 | } 175 | 176 | function loadMaterialTextures(material) { 177 | // If an alpha texture is present the diffuse texture needs to be decoded so they can be packed together 178 | var diffuseAlphaTextureOptions = defined(material.alphaTexture) ? alphaTextureOptions : diffuseTextureOptions; 179 | 180 | if (material.diffuseTexture === material.ambientTexture) { 181 | // OBJ models are often exported with the same texture in the diffuse and ambient slots but this is typically not desirable, particularly 182 | // when saving with PBR materials where the ambient texture is treated as the occlusion texture. 183 | material.ambientTexture = undefined; 184 | } 185 | 186 | loadMaterialTexture(material, 'diffuseTexture', diffuseAlphaTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); 187 | loadMaterialTexture(material, 'ambientTexture', ambientTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); 188 | loadMaterialTexture(material, 'emissiveTexture', emissiveTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); 189 | loadMaterialTexture(material, 'specularTexture', specularTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); 190 | loadMaterialTexture(material, 'specularShininessTexture', specularShinessTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); 191 | loadMaterialTexture(material, 'normalTexture', normalTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); 192 | loadMaterialTexture(material, 'alphaTexture', alphaTextureOptions, mtlDirectory, texturePromiseMap, texturePromises, options); 193 | } 194 | 195 | return readLines(mtlPath, parseLine) 196 | .then(function() { 197 | var length = materials.length; 198 | for (var i = 0; i < length; ++i) { 199 | loadMaterialTextures(materials[i]); 200 | } 201 | return Promise.all(texturePromises); 202 | }) 203 | .then(function() { 204 | return convertMaterials(materials, options); 205 | }); 206 | } 207 | 208 | function correctAlpha(alpha) { 209 | // An alpha of 0.0 usually implies a problem in the export, change to 1.0 instead 210 | return alpha === 0.0 ? 1.0 : alpha; 211 | } 212 | 213 | function Material() { 214 | this.name = undefined; 215 | this.ambientColor = [0.0, 0.0, 0.0, 1.0]; // Ka 216 | this.emissiveColor = [0.0, 0.0, 0.0, 1.0]; // Ke 217 | this.diffuseColor = [0.5, 0.5, 0.5, 1.0]; // Kd 218 | this.specularColor = [0.0, 0.0, 0.0, 1.0]; // Ks 219 | this.specularShininess = 0.0; // Ns 220 | this.alpha = 1.0; // d / Tr 221 | this.ambientTexture = undefined; // map_Ka 222 | this.emissiveTexture = undefined; // map_Ke 223 | this.diffuseTexture = undefined; // map_Kd 224 | this.specularTexture = undefined; // map_Ks 225 | this.specularShininessTexture = undefined; // map_Ns 226 | this.normalTexture = undefined; // map_Bump 227 | this.alphaTexture = undefined; // map_d 228 | } 229 | 230 | loadMtl.getDefaultMaterial = function(options) { 231 | return convertMaterial(new Material(), options); 232 | }; 233 | 234 | // Exposed for testing 235 | loadMtl._createMaterial = function(materialOptions, options) { 236 | return convertMaterial(combine(materialOptions, new Material()), options); 237 | }; 238 | 239 | function loadMaterialTexture(material, name, textureOptions, mtlDirectory, texturePromiseMap, texturePromises, options) { 240 | var texturePath = material[name]; 241 | if (!defined(texturePath)) { 242 | return; 243 | } 244 | 245 | var texturePromise = texturePromiseMap[texturePath]; 246 | if (!defined(texturePromise)) { 247 | var shallowPath = path.resolve(path.join(mtlDirectory, path.basename(texturePath))); 248 | if (options.secure && outsideDirectory(texturePath, mtlDirectory)) { 249 | // Try looking for the texture in the same directory as the obj 250 | options.logger('Texture file is outside of the mtl directory and the secure flag is true. Attempting to read the texture file from within the obj directory instead.'); 251 | texturePromise = loadTexture(shallowPath, textureOptions) 252 | .catch(function(error) { 253 | options.logger(error.message); 254 | options.logger('Could not read texture file at ' + shallowPath + '. This texture will be ignored'); 255 | }); 256 | } else { 257 | texturePromise = loadTexture(texturePath, textureOptions) 258 | .catch(function(error) { 259 | // Try looking for the texture in the same directory as the obj 260 | options.logger(error.message); 261 | options.logger('Could not read texture file at ' + texturePath + '. Attempting to read the texture file from within the obj directory instead.'); 262 | return loadTexture(shallowPath, textureOptions); 263 | }) 264 | .catch(function(error) { 265 | options.logger(error.message); 266 | options.logger('Could not read texture file at ' + shallowPath + '. This texture will be ignored.'); 267 | }); 268 | } 269 | texturePromiseMap[texturePath] = texturePromise; 270 | } 271 | 272 | texturePromises.push(texturePromise 273 | .then(function(texture) { 274 | material[name] = texture; 275 | })); 276 | } 277 | 278 | function convertMaterial(material, options) { 279 | if (options.specularGlossiness) { 280 | return createSpecularGlossinessMaterial(material, options); 281 | } else if (options.metallicRoughness) { 282 | return createMetallicRoughnessMaterial(material, options); 283 | } else if (options.materialsCommon) { 284 | return createMaterialsCommonMaterial(material, options); 285 | } 286 | 287 | // No material type specified, convert the material to metallic roughness 288 | convertTraditionalToMetallicRoughness(material); 289 | return createMetallicRoughnessMaterial(material, options); 290 | } 291 | 292 | function convertMaterials(materials, options) { 293 | return materials.map(function(material) { 294 | return convertMaterial(material, options); 295 | }); 296 | } 297 | 298 | function resizeChannel(sourcePixels, sourceWidth, sourceHeight, targetPixels, targetWidth, targetHeight) { 299 | // Nearest neighbor sampling 300 | var widthRatio = sourceWidth / targetWidth; 301 | var heightRatio = sourceHeight / targetHeight; 302 | 303 | for (var y = 0; y < targetHeight; ++y) { 304 | for (var x = 0; x < targetWidth; ++x) { 305 | var targetIndex = y * targetWidth + x; 306 | var sourceY = Math.round(y * heightRatio); 307 | var sourceX = Math.round(x * widthRatio); 308 | var sourceIndex = sourceY * sourceWidth + sourceX; 309 | var sourceValue = sourcePixels.readUInt8(sourceIndex); 310 | targetPixels.writeUInt8(sourceValue, targetIndex); 311 | } 312 | } 313 | return targetPixels; 314 | } 315 | 316 | var scratchResizeChannel; 317 | 318 | function getTextureChannel(texture, index, targetWidth, targetHeight, targetChannel) { 319 | var pixels = texture.pixels; // RGBA 320 | var sourceWidth = texture.width; 321 | var sourceHeight = texture.height; 322 | var sourcePixelsLength = sourceWidth * sourceHeight; 323 | var targetPixelsLength = targetWidth * targetHeight; 324 | 325 | // Allocate the scratchResizeChannel on demand if the texture needs to be resized 326 | var sourceChannel = targetChannel; 327 | if (sourcePixelsLength > targetPixelsLength) { 328 | if (!defined(scratchResizeChannel) || (sourcePixelsLength > scratchResizeChannel.length)) { 329 | scratchResizeChannel = Buffer.alloc(sourcePixelsLength); 330 | } 331 | sourceChannel = scratchResizeChannel; 332 | } 333 | 334 | for (var i = 0; i < sourcePixelsLength; ++i) { 335 | var value = pixels.readUInt8(i * 4 + index); 336 | sourceChannel.writeUInt8(value, i); 337 | } 338 | 339 | if (sourcePixelsLength > targetPixelsLength) { 340 | resizeChannel(sourceChannel, sourceWidth, sourceHeight, targetChannel, targetWidth, targetHeight); 341 | } 342 | 343 | return targetChannel; 344 | } 345 | 346 | function writeChannel(pixels, channel, index) { 347 | var pixelsLength = pixels.length / 4; 348 | for (var i = 0; i < pixelsLength; ++i) { 349 | var value = channel.readUInt8(i); 350 | pixels.writeUInt8(value, i * 4 + index); 351 | } 352 | } 353 | 354 | function getMinimumDimensions(textures, options) { 355 | var i; 356 | var texture; 357 | var width = Number.POSITIVE_INFINITY; 358 | var height = Number.POSITIVE_INFINITY; 359 | 360 | var length = textures.length; 361 | for (i = 0; i < length; ++i) { 362 | texture = textures[i]; 363 | width = Math.min(texture.width, width); 364 | height = Math.min(texture.height, height); 365 | } 366 | 367 | for (i = 0; i < length; ++i) { 368 | texture = textures[i]; 369 | if (texture.width !== width || texture.height !== height) { 370 | options.logger('Texture ' + texture.path + ' will be scaled from ' + texture.width + 'x' + texture.height + ' to ' + width + 'x' + height + '.'); 371 | } 372 | } 373 | 374 | return [width, height]; 375 | } 376 | 377 | function isChannelSingleColor(buffer) { 378 | var first = buffer.readUInt8(0); 379 | var length = buffer.length; 380 | for (var i = 1; i < length; ++i) { 381 | if (buffer[i] !== first) { 382 | return false; 383 | } 384 | } 385 | return true; 386 | } 387 | 388 | function createDiffuseAlphaTexture(diffuseTexture, alphaTexture, options) { 389 | var packDiffuse = defined(diffuseTexture); 390 | var packAlpha = defined(alphaTexture); 391 | 392 | if (!packDiffuse) { 393 | return undefined; 394 | } 395 | 396 | if (!packAlpha) { 397 | return diffuseTexture; 398 | } 399 | 400 | if (!defined(diffuseTexture.pixels) || !defined(alphaTexture.pixels)) { 401 | options.logger('Could not get decoded texture data for ' + diffuseTexture.path + ' or ' + alphaTexture.path + '. The material will be created without an alpha texture.'); 402 | return diffuseTexture; 403 | } 404 | 405 | var packedTextures = [diffuseTexture, alphaTexture]; 406 | var dimensions = getMinimumDimensions(packedTextures, options); 407 | var width = dimensions[0]; 408 | var height = dimensions[1]; 409 | var pixelsLength = width * height; 410 | var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels 411 | var scratchChannel = Buffer.alloc(pixelsLength); 412 | 413 | // Write into the R, G, B channels 414 | var redChannel = getTextureChannel(diffuseTexture, 0, width, height, scratchChannel); 415 | writeChannel(pixels, redChannel, 0); 416 | var greenChannel = getTextureChannel(diffuseTexture, 1, width, height, scratchChannel); 417 | writeChannel(pixels, greenChannel, 1); 418 | var blueChannel = getTextureChannel(diffuseTexture, 2, width, height, scratchChannel); 419 | writeChannel(pixels, blueChannel, 2); 420 | 421 | // First try reading the alpha component from the alpha channel, but if it is a single color read from the red channel instead. 422 | var alphaChannel = getTextureChannel(alphaTexture, 3, width, height, scratchChannel); 423 | if (isChannelSingleColor(alphaChannel)) { 424 | alphaChannel = getTextureChannel(alphaTexture, 0, width, height, scratchChannel); 425 | } 426 | writeChannel(pixels, alphaChannel, 3); 427 | 428 | var texture = new Texture(); 429 | texture.name = diffuseTexture.name; 430 | texture.extension = '.png'; 431 | texture.pixels = pixels; 432 | texture.width = width; 433 | texture.height = height; 434 | texture.transparent = true; 435 | 436 | return texture; 437 | } 438 | 439 | function createMetallicRoughnessTexture(metallicTexture, roughnessTexture, occlusionTexture, options) { 440 | if (defined(options.overridingTextures.metallicRoughnessOcclusionTexture)) { 441 | return metallicTexture; 442 | } 443 | 444 | var packMetallic = defined(metallicTexture); 445 | var packRoughness = defined(roughnessTexture); 446 | var packOcclusion = defined(occlusionTexture) && options.packOcclusion; 447 | 448 | if (!packMetallic && !packRoughness) { 449 | return undefined; 450 | } 451 | 452 | if (packMetallic && !defined(metallicTexture.pixels)) { 453 | options.logger('Could not get decoded texture data for ' + metallicTexture.path + '. The material will be created without a metallicRoughness texture.'); 454 | return undefined; 455 | } 456 | 457 | if (packRoughness && !defined(roughnessTexture.pixels)) { 458 | options.logger('Could not get decoded texture data for ' + roughnessTexture.path + '. The material will be created without a metallicRoughness texture.'); 459 | return undefined; 460 | } 461 | 462 | if (packOcclusion && !defined(occlusionTexture.pixels)) { 463 | options.logger('Could not get decoded texture data for ' + occlusionTexture.path + '. The occlusion texture will not be packed in the metallicRoughness texture.'); 464 | return undefined; 465 | } 466 | 467 | var packedTextures = [metallicTexture, roughnessTexture, occlusionTexture].filter(function(texture) { 468 | return defined(texture) && defined(texture.pixels); 469 | }); 470 | 471 | var dimensions = getMinimumDimensions(packedTextures, options); 472 | var width = dimensions[0]; 473 | var height = dimensions[1]; 474 | var pixelsLength = width * height; 475 | var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white 476 | var scratchChannel = Buffer.alloc(pixelsLength); 477 | 478 | if (packMetallic) { 479 | // Write into the B channel 480 | var metallicChannel = getTextureChannel(metallicTexture, 0, width, height, scratchChannel); 481 | writeChannel(pixels, metallicChannel, 2); 482 | } 483 | 484 | if (packRoughness) { 485 | // Write into the G channel 486 | var roughnessChannel = getTextureChannel(roughnessTexture, 0, width, height, scratchChannel); 487 | writeChannel(pixels, roughnessChannel, 1); 488 | } 489 | 490 | if (packOcclusion) { 491 | // Write into the R channel 492 | var occlusionChannel = getTextureChannel(occlusionTexture, 0, width, height, scratchChannel); 493 | writeChannel(pixels, occlusionChannel, 0); 494 | } 495 | 496 | var length = packedTextures.length; 497 | var names = new Array(length); 498 | for (var i = 0; i < length; ++i) { 499 | names[i] = packedTextures[i].name; 500 | } 501 | var name = names.join('_'); 502 | 503 | var texture = new Texture(); 504 | texture.name = name; 505 | texture.extension = '.png'; 506 | texture.pixels = pixels; 507 | texture.width = width; 508 | texture.height = height; 509 | 510 | return texture; 511 | } 512 | 513 | function createSpecularGlossinessTexture(specularTexture, glossinessTexture, options) { 514 | if (defined(options.overridingTextures.specularGlossinessTexture)) { 515 | return specularTexture; 516 | } 517 | 518 | var packSpecular = defined(specularTexture); 519 | var packGlossiness = defined(glossinessTexture); 520 | 521 | if (!packSpecular && !packGlossiness) { 522 | return undefined; 523 | } 524 | 525 | if (packSpecular && !defined(specularTexture.pixels)) { 526 | options.logger('Could not get decoded texture data for ' + specularTexture.path + '. The material will be created without a specularGlossiness texture.'); 527 | return undefined; 528 | } 529 | 530 | if (packGlossiness && !defined(glossinessTexture.pixels)) { 531 | options.logger('Could not get decoded texture data for ' + glossinessTexture.path + '. The material will be created without a specularGlossiness texture.'); 532 | return undefined; 533 | } 534 | 535 | var packedTextures = [specularTexture, glossinessTexture].filter(function(texture) { 536 | return defined(texture) && defined(texture.pixels); 537 | }); 538 | 539 | var dimensions = getMinimumDimensions(packedTextures, options); 540 | var width = dimensions[0]; 541 | var height = dimensions[1]; 542 | var pixelsLength = width * height; 543 | var pixels = Buffer.alloc(pixelsLength * 4, 0xFF); // Initialize with 4 channels, unused channels will be white 544 | var scratchChannel = Buffer.alloc(pixelsLength); 545 | 546 | if (packSpecular) { 547 | // Write into the R, G, B channels 548 | var redChannel = getTextureChannel(specularTexture, 0, width, height, scratchChannel); 549 | writeChannel(pixels, redChannel, 0); 550 | var greenChannel = getTextureChannel(specularTexture, 1, width, height, scratchChannel); 551 | writeChannel(pixels, greenChannel, 1); 552 | var blueChannel = getTextureChannel(specularTexture, 2, width, height, scratchChannel); 553 | writeChannel(pixels, blueChannel, 2); 554 | } 555 | 556 | if (packGlossiness) { 557 | // Write into the A channel 558 | var glossinessChannel = getTextureChannel(glossinessTexture, 0, width, height, scratchChannel); 559 | writeChannel(pixels, glossinessChannel, 3); 560 | } 561 | 562 | var length = packedTextures.length; 563 | var names = new Array(length); 564 | for (var i = 0; i < length; ++i) { 565 | names[i] = packedTextures[i].name; 566 | } 567 | var name = names.join('_'); 568 | 569 | var texture = new Texture(); 570 | texture.name = name; 571 | texture.extension = '.png'; 572 | texture.pixels = pixels; 573 | texture.width = width; 574 | texture.height = height; 575 | 576 | return texture; 577 | } 578 | 579 | function createSpecularGlossinessMaterial(material, options) { 580 | var emissiveTexture = material.emissiveTexture; 581 | var normalTexture = material.normalTexture; 582 | var occlusionTexture = material.ambientTexture; 583 | var diffuseTexture = material.diffuseTexture; 584 | var alphaTexture = material.alphaTexture; 585 | var specularTexture = material.specularTexture; 586 | var glossinessTexture = material.specularShininessTexture; 587 | var specularGlossinessTexture = createSpecularGlossinessTexture(specularTexture, glossinessTexture, options); 588 | var diffuseAlphaTexture = createDiffuseAlphaTexture(diffuseTexture, alphaTexture, options); 589 | 590 | var emissiveFactor = material.emissiveColor.slice(0, 3); 591 | var diffuseFactor = material.diffuseColor; 592 | var specularFactor = material.specularColor.slice(0, 3); 593 | var glossinessFactor = material.specularShininess; 594 | 595 | if (defined(emissiveTexture)) { 596 | emissiveFactor = [1.0, 1.0, 1.0]; 597 | } 598 | 599 | if (defined(diffuseTexture)) { 600 | diffuseFactor = [1.0, 1.0, 1.0, 1.0]; 601 | } 602 | 603 | if (defined(specularTexture)) { 604 | specularFactor = [1.0, 1.0, 1.0]; 605 | } 606 | 607 | if (defined(glossinessTexture)) { 608 | glossinessFactor = 1.0; 609 | } 610 | 611 | var transparent = false; 612 | if (defined(alphaTexture)) { 613 | transparent = true; 614 | } else { 615 | var alpha = material.alpha; 616 | diffuseFactor[3] = alpha; 617 | transparent = alpha < 1.0; 618 | } 619 | 620 | if (defined(diffuseTexture)) { 621 | transparent = transparent || diffuseTexture.transparent; 622 | } 623 | 624 | var doubleSided = transparent; 625 | var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; 626 | 627 | return { 628 | name : material.name, 629 | extensions : { 630 | KHR_materials_pbrSpecularGlossiness: { 631 | diffuseTexture : diffuseAlphaTexture, 632 | specularGlossinessTexture : specularGlossinessTexture, 633 | diffuseFactor : diffuseFactor, 634 | specularFactor : specularFactor, 635 | glossinessFactor : glossinessFactor 636 | } 637 | }, 638 | emissiveTexture : emissiveTexture, 639 | normalTexture : normalTexture, 640 | occlusionTexture : occlusionTexture, 641 | emissiveFactor : emissiveFactor, 642 | alphaMode : alphaMode, 643 | doubleSided : doubleSided 644 | }; 645 | } 646 | 647 | function createMetallicRoughnessMaterial(material, options) { 648 | var emissiveTexture = material.emissiveTexture; 649 | var normalTexture = material.normalTexture; 650 | var occlusionTexture = material.ambientTexture; 651 | var baseColorTexture = material.diffuseTexture; 652 | var alphaTexture = material.alphaTexture; 653 | var metallicTexture = material.specularTexture; 654 | var roughnessTexture = material.specularShininessTexture; 655 | var metallicRoughnessTexture = createMetallicRoughnessTexture(metallicTexture, roughnessTexture, occlusionTexture, options); 656 | var diffuseAlphaTexture = createDiffuseAlphaTexture(baseColorTexture, alphaTexture, options); 657 | 658 | if (options.packOcclusion) { 659 | occlusionTexture = metallicRoughnessTexture; 660 | } 661 | 662 | var emissiveFactor = material.emissiveColor.slice(0, 3); 663 | var baseColorFactor = material.diffuseColor; 664 | var metallicFactor = material.specularColor[0]; 665 | var roughnessFactor = material.specularShininess; 666 | 667 | if (defined(emissiveTexture)) { 668 | emissiveFactor = [1.0, 1.0, 1.0]; 669 | } 670 | 671 | if (defined(baseColorTexture)) { 672 | baseColorFactor = [1.0, 1.0, 1.0, 1.0]; 673 | } 674 | 675 | if (defined(metallicTexture)) { 676 | metallicFactor = 1.0; 677 | } 678 | 679 | if (defined(roughnessTexture)) { 680 | roughnessFactor = 1.0; 681 | } 682 | 683 | var transparent = false; 684 | if (defined(alphaTexture)) { 685 | transparent = true; 686 | } else { 687 | var alpha = material.alpha; 688 | baseColorFactor[3] = alpha; 689 | transparent = alpha < 1.0; 690 | } 691 | 692 | if (defined(baseColorTexture)) { 693 | transparent = transparent || baseColorTexture.transparent; 694 | } 695 | 696 | var doubleSided = transparent; 697 | var alphaMode = transparent ? 'BLEND' : 'OPAQUE'; 698 | 699 | return { 700 | name : material.name, 701 | pbrMetallicRoughness : { 702 | baseColorTexture : diffuseAlphaTexture, 703 | metallicRoughnessTexture : metallicRoughnessTexture, 704 | baseColorFactor : baseColorFactor, 705 | metallicFactor : metallicFactor, 706 | roughnessFactor : roughnessFactor 707 | }, 708 | emissiveTexture : emissiveTexture, 709 | normalTexture : normalTexture, 710 | occlusionTexture : occlusionTexture, 711 | emissiveFactor : emissiveFactor, 712 | alphaMode : alphaMode, 713 | doubleSided : doubleSided 714 | }; 715 | } 716 | 717 | function luminance(color) { 718 | return color[0] * 0.2125 + color[1] * 0.7154 + color[2] * 0.0721; 719 | } 720 | 721 | function convertTraditionalToMetallicRoughness(material) { 722 | // Translate the blinn-phong model to the pbr metallic-roughness model 723 | // Roughness factor is a combination of specular intensity and shininess 724 | // Metallic factor is 0.0 725 | // Textures are not converted for now 726 | var specularIntensity = luminance(material.specularColor); 727 | 728 | // Transform from 0-1000 range to 0-1 range. Then invert. 729 | var roughnessFactor = material.specularShininess; 730 | roughnessFactor = roughnessFactor / 1000.0; 731 | roughnessFactor = 1.0 - roughnessFactor; 732 | roughnessFactor = CesiumMath.clamp(roughnessFactor, 0.0, 1.0); 733 | 734 | // Low specular intensity values should produce a rough material even if shininess is high. 735 | if (specularIntensity < 0.1) { 736 | roughnessFactor *= (1.0 - specularIntensity); 737 | } 738 | 739 | var metallicFactor = 0.0; 740 | 741 | material.specularColor = [metallicFactor, metallicFactor, metallicFactor, 1.0]; 742 | material.specularShininess = roughnessFactor; 743 | } 744 | 745 | function createMaterialsCommonMaterial(material, options) { 746 | var diffuseAlphaTexture = createDiffuseAlphaTexture(material.diffuseTexture, material.alphaTexture, options); 747 | var ambient = defaultValue(material.ambientTexture, material.ambientColor); 748 | var diffuse = defaultValue(diffuseAlphaTexture, material.diffuseColor); 749 | var emission = defaultValue(material.emissiveTexture, material.emissiveColor); 750 | var specular = defaultValue(material.specularTexture, material.specularColor); 751 | 752 | var alpha = material.alpha; 753 | var shininess = material.specularShininess; 754 | var hasSpecular = (shininess > 0.0) && (defined(material.specularTexture) || (specular[0] > 0.0 || specular[1] > 0.0 || specular[2] > 0.0)); 755 | 756 | var transparent; 757 | var transparency = 1.0; 758 | if (defined(material.alphaTexture)) { 759 | transparent = true; 760 | } else if (defined(material.diffuseTexture)) { 761 | transparency = alpha; 762 | transparent = material.diffuseTexture.transparent || (transparency < 1.0); 763 | } else { 764 | diffuse[3] = alpha; 765 | transparent = alpha < 1.0; 766 | } 767 | 768 | if (!defined(material.ambientTexture)) { 769 | // If ambient color is [1, 1, 1] assume it is a multiplier and instead change to [0, 0, 0] 770 | if (ambient[0] === 1.0 && ambient[1] === 1.0 && ambient[2] === 1.0) { 771 | ambient = [0.0, 0.0, 0.0, 1.0]; 772 | } 773 | } 774 | 775 | var doubleSided = transparent; 776 | var technique = hasSpecular ? 'PHONG' : 'LAMBERT'; 777 | 778 | if (!options.hasNormals) { 779 | // Constant technique only factors in ambient and emission sources - set emission to diffuse 780 | emission = diffuse; 781 | technique = 'CONSTANT'; 782 | } 783 | 784 | return { 785 | name : material.name, 786 | extensions : { 787 | KHR_materials_common : { 788 | technique : technique, 789 | transparent : transparent, 790 | doubleSided : doubleSided, 791 | values : { 792 | ambient : ambient, 793 | diffuse : diffuse, 794 | emission : emission, 795 | specular : specular, 796 | shininess : shininess, 797 | transparency : transparency, 798 | transparent : transparent, 799 | doubleSided : doubleSided 800 | } 801 | } 802 | } 803 | }; 804 | } 805 | --------------------------------------------------------------------------------