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