threejs-avatar-demo / tools /gltf-avatar-merge.js
Umama-at-Bluchip's picture
Upload 92 files
73c6193 verified
raw
history blame contribute delete
13.1 kB
const commandLineArgs = require('command-line-args');
const commandLineUsage = require('command-line-usage');
// const fs = require('fs');
const fs = require('fs-extra');
const path = require('path');
const PNG = require('pngjs').PNG;
// node .\tools\gltf-avatar-merge.js -s .\models\gltf\saber-body-walk\saber-body-walk.gltf -a .\models\gltf\saber-maid-hair\saber-maid-hair.gltf .\models\gltf\saber-maid-dress\saber-maid-dress.gltf -f models/merge
// node .\tools\gltf-avatar-merge.js -s .\models\gltf\saber-body-mixamo-animations\saber-body-animations.gltf -a .\models\gltf\saber-maid-hair-mixamo\saber-maid-hair.gltf .\models\gltf\saber-dress-mixamo\saber-dress.gltf -f models/merge
// node .\tools\gltf-avatar-merge.js -s .\models\gltf\saber-stand-pose\saber-stand-pose.gltf -a .\models\gltf\saber-maid-hair-mixamo\saber-maid-hair.gltf .\models\gltf\saber-dress-mixamo\saber-dress.gltf -f models/merge
const optionDefinitions = [
{
name: 'help',
alias: 'h',
type: Boolean,
description: 'Display this usage guide.'
},
{
name: 'skeletonFilePath',
alias: 's',
type: String,
description: '/path/to/accessory-file/skeleton-gltf-file'
},
{
name: 'accessoriesFilePath',
alias: 'a',
type: String,
multiple: true,
// defaultValue: true,
description: '/path/to/accessory-gltf-file [multiple]'
},
{
name: 'outputFolder',
alias: 'f',
type: String,
defaultValue: './',
description: '/path/to/outputFolder'
},
{
name: 'outputFilename',
alias: 'o',
type: String,
defaultValue: 'output.gltf',
description: 'output filename'
}
];
const options = commandLineArgs(optionDefinitions);
if (options.help) {
const usage = commandLineUsage([
{
header: 'glAvatar Merge test',
content: 'Merge skeleton and accessories gltf into one gltf file'
},
{
header: 'Options',
optionList: optionDefinitions
}
]);
console.log(usage);
return;
}
// // temp
// // TODO: cmd line arg
// var skeletonGltfPath = 'demo/models/saber-body-walk/';
// var skeletonGltfFilename = 'saber-body-walk.gltf';
// var accessoryGltfPath = 'demo/models/saber-maid-hair/';
// var accessoryGltfFilename = 'saber-maid-hair.gltf';
// var outputFilename = 'models/merged/output.gltf';
// var skeleton = JSON.parse(fs.readFileSync(skeletonGltfPath + skeletonGltfFilename));
// var accessory = JSON.parse(fs.readFileSync(accessoryGltfPath + accessoryGltfFilename));
// preprocess
// TODO: all sanity checks should happen here
var skeletonGltfDir = path.dirname(options.skeletonFilePath);
var accessoryFilepaths = options.accessoriesFilePath;
console.log('skeleton filename: ', options.skeletonFilePath);
console.log('skin filenames: ');
for (var i = 0, len = accessoryFilepaths.length; i < len; i++) {
console.log(accessoryFilepaths[i]);
}
var skeleton = JSON.parse(fs.readFileSync(options.skeletonFilePath));
if(!skeleton.extensions) {
skeleton.extensions = {};
}
if (!skeleton.extensions.gl_avatar) {
skeleton.extensions.gl_avatar = {};
}
if (!skeleton.extensions.gl_avatar.visibility) {
skeleton.extensions.gl_avatar.visibility = [];
}
var textureWithVisibility = null;
var bodyIdLUTTexture = null;
var visiblityMaterial = null;
// bake visibility
// modify body texture, let its alpha = 0
// look at alphaMode: Mask, alphaCutOff
// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#alpha-coverage
function visibilityAndOperation(vi) {
if (skeleton.extensions.gl_avatar.visibility.length === 0) {
skeleton.extensions.gl_avatar.visibility = vi.slice(0);
return;
}
var v = skeleton.extensions.gl_avatar.visibility;
var vl = v.length;
for (var i = 0, len = vi.length; i < len; i++) {
if (vl <= i) {
v[i] = vi[i];
} else {
v[i] = v[i] && vi[i];
}
}
}
/**
*
* @param {*} skeleton gltf json
* @param {*} skin gltf json
*/
function merge(skeleton, skin) {
var i, len, j, lenj;
var linkedSkeletons = skin.extensions.gl_avatar.linkedSkeletons || [];
// buffers
var bufferBaseId = skeleton.buffers.length;
// for (i = 0, len = skin.buffers.length; i < len; i++) {
// skeleton.buffers.push(skin.buffers[i]);
// }
skeleton.buffers = skeleton.buffers.concat(skin.buffers);
// bufferViews
var bufferViewBaseId = skeleton.bufferViews.length;
for (i = 0, len = skin.bufferViews.length; i < len; i++) {
skeleton.bufferViews.push(skin.bufferViews[i]);
skeleton.bufferViews[i + bufferViewBaseId].buffer += bufferBaseId;
}
// accessors
var accessorBaseId = skeleton.accessors.length;
for (i = 0, len = skin.accessors.length; i < len; i++) {
skeleton.accessors.push(skin.accessors[i]);
skeleton.accessors[i + accessorBaseId].bufferView += bufferViewBaseId;
}
// images
var imageBaseId = skeleton.images.length;
skeleton.images = skeleton.images.concat(skin.images);
// samplers
var samplerBaseId = skeleton.samplers.length;
skeleton.samplers = skeleton.samplers.concat(skin.samplers);
// textures
var textureBaseId = skeleton.textures.length;
skeleton.textures = skeleton.textures.concat(skin.textures);
for (i = 0, len = skin.textures.length; i < len; i++) {
var t = skeleton.textures[i + textureBaseId];
if (t.sampler !== undefined) {
t.sampler += samplerBaseId;
}
if (t.source !== undefined) {
t.source += imageBaseId;
}
}
// materials
var materialBaseId = skeleton.materials.length;
// find texture with bodyIdLUT
for (i = 0, len = skeleton.materials.length; i < len; i++) {
var m = skeleton.materials[i];
if (m.extensions && m.extensions.gl_avatar && m.extensions.gl_avatar.bodyIdLUT !== undefined) {
// m has pbr related texture
visiblityMaterial = m;
bodyIdLUTTexture = skeleton.textures[m.extensions.gl_avatar.bodyIdLUT];
textureWithVisibility = skeleton.textures[m.pbrMetallicRoughness.baseColorTexture.index];
}
}
for (i = 0, len = skin.materials.length; i < len; i++) {
skeleton.materials.push(skin.materials[i]);
var m = skeleton.materials[i + materialBaseId];
if (m.pbrMetallicRoughness !== undefined) {
if (m.pbrMetallicRoughness.baseColorTexture !== undefined) {
var bt = m.pbrMetallicRoughness.baseColorTexture;
for (var tt in bt) {
bt[tt] += imageBaseId;
}
}
}
}
// meshes
var meshBaseId = skeleton.meshes.length;
for (i = 0, len = skin.meshes.length; i < len; i++) {
skeleton.meshes.push(skin.meshes[i]);
var m = skeleton.meshes[i + meshBaseId];
if (m.primitives !== undefined) {
for (j = 0, lenj = m.primitives.length; j < lenj; j++) {
var p = m.primitives[j];
if (p.indices !== undefined) {
p.indices += accessorBaseId;
}
if (p.material !== undefined) {
p.material += materialBaseId;
}
if (p.attributes !== undefined) {
var a = p.attributes;
for (var att in a) {
a[att] += accessorBaseId;
}
}
if (p.extensions !== undefined) {
if (p.extensions.gl_avatar.attributes) {
var ea = p.extensions.gl_avatar.attributes;
if (!p.attributes) {
p.attributes = {};
}
for (var att2 in ea) {
p.attributes[att2] = ea[att2] + accessorBaseId;
}
}
delete p.extensions;
}
}
}
}
// nodes
var nodeBaseId = skeleton.nodes.length;
// var numLinkedSkin = 0;
for (i = 0, len = skin.nodes.length; i < len; i++) {
skeleton.nodes.push(skin.nodes[i]);
var n = skeleton.nodes[i + nodeBaseId];
if (n.children !== undefined) {
var c = n.children;
for (j = 0, lenj = c.length; j < lenj; j++) {
c[j] += nodeBaseId;
}
}
if (n.mesh !== undefined) {
n.mesh += meshBaseId;
}
// skins link
if (n.extensions) {
// create a new skin copy of skin linked
// replace inverseBindMatrices
if (n.extensions.gl_avatar && n.extensions.gl_avatar.skin !== undefined) {
// linked skin
// assume linkedSkeletons exists
var linkedSkinInfo = linkedSkeletons[n.extensions.gl_avatar.skin];
var skinKey = linkedSkinInfo.skeleton;
var newSkin = Object.assign({}, skeleton.skins[skeleton.extensions.gl_avatar.skins[skinKey]]);
skeleton.skins.push(newSkin);
// numLinkedSkin++;
n.skin = skeleton.skins.length - 1;
newSkin.inverseBindMatrices = linkedSkinInfo.inverseBindMatrices + accessorBaseId;
}
delete n.extensions;
}
}
// scenes (assume only one scene)
var sceneNodeBaseId = skeleton.scenes[0].nodes.length;
skeleton.scenes[0].nodes = skeleton.scenes[0].nodes.concat(skin.scenes[0].nodes);
for (i = 0, len = skin.scenes[0].nodes.length; i < len; i++) {
skeleton.scenes[0].nodes[i + sceneNodeBaseId] += nodeBaseId;
}
// TODO: animations, cameras...
// extensions: visibility array
visibilityAndOperation(skin.extensions.gl_avatar.visibility);
}
function ensureDirectoryExistence(filePath) {
var dirname = path.dirname(filePath);
if (fs.existsSync(dirname)) {
return true;
}
ensureDirectoryExistence(dirname);
fs.mkdirSync(dirname);
}
function copyAssets(inputFolder, outputFolder) {
// TODO: copy recursively using ncp
var assets = fs.readdirSync(inputFolder);
for (var i = 0, len = assets.length; i < len; i++) {
if (path.extname(assets[i]) !== '.gltf') {
fs.copySync( path.join(inputFolder, assets[i]), path.join(outputFolder, assets[i]) );
}
}
}
function bakeVisibility(texPath, bodyIdLUTPath, visibilty) {
console.log('Texture with visibility path: ', texPath);
console.log('body Id LUT texture path: ', bodyIdLUTPath);
visiblityMaterial.alphaMode = "MASK";
visiblityMaterial.alphaCutOff = 0.5;
fs.createReadStream(texPath)
.pipe(new PNG({filterType: 4}))
.on('parsed', function() {
var tex = this;
fs.createReadStream(bodyIdLUTPath)
.pipe(new PNG({filterType: 0}))
.on('parsed', function() {
var lut = this;
// temp assume same size
for (var y = 0; y < this.height; y++) {
for (var x = 0; x < this.width; x++) {
var idx = (this.width * y + x) << 2;
var bodyId = lut.data[idx];
if (visibilty[bodyId] === 0) {
tex.data[idx + 3] = 0;
}
}
}
tex.pack().pipe(fs.createWriteStream(texPath));
});
});
}
var outputFilename = path.join(options.outputFolder, options.outputFilename);
ensureDirectoryExistence(outputFilename);
copyAssets(skeletonGltfDir, options.outputFolder);
for (var i = 0, len = accessoryFilepaths.length; i < len; i++) {
merge(skeleton, JSON.parse(fs.readFileSync(accessoryFilepaths[i])));
copyAssets( path.dirname(accessoryFilepaths[i]), options.outputFolder );
}
// modify body texture to reflect visibility
if (textureWithVisibility && bodyIdLUTTexture){
var textureWithVisibilityPath = path.join(options.outputFolder, skeleton.images[textureWithVisibility.source].uri);
var textureBodyIdLUTPath = path.join(options.outputFolder, skeleton.images[bodyIdLUTTexture.source].uri);
bakeVisibility(textureWithVisibilityPath, textureBodyIdLUTPath, skeleton.extensions.gl_avatar.visibility);
}
// TODO: delete gltf extensions
fs.writeFileSync(outputFilename, JSON.stringify(skeleton));