Skip to content

Instantly share code, notes, and snippets.

@pharra
Forked from donmccurdy/README.md
Created June 7, 2021 13:11
Show Gist options
  • Save pharra/d074aff1f49c42942808619f856e6fbf to your computer and use it in GitHub Desktop.
Save pharra/d074aff1f49c42942808619f856e6fbf to your computer and use it in GitHub Desktop.
Convert legacy three.js (JSON) files to glTF 2.0

legacythree2gltf.js

Converts legacy JSON models (created by the three.js Blender exporter, for THREE.JSONLoader) to glTF 2.0. When original .blend files are available, prefer direct export from Blender 2.80+.

NOTE: JSON files created with .toJSON() use a newer JSON format, which isn't deprecated, and these are still supported by THREE.ObjectLoader. This converter does not support that newer type of JSON file.

Installation:

npm install canvas vblob three

Usage:

./legacythree2gltf.js model.json --optimize

Known issues:

  • Creates .gltf files with embedded Data URIs. Optimize to .glb using glTF-Pipeline to reduce file size.
  • Limited support for morph targets (mrdoob/three.js#15011)
#!/usr/bin/env node
const fs = require( 'fs' );
const path = require( 'path' );
const Canvas = require( 'canvas' );
const { Blob, FileReader } = require( 'vblob' );
THREE = global.THREE = require( 'three' );
require( 'three/examples/js/loaders/deprecated/LegacyJSONLoader.js' );
require( 'three/examples/js/exporters/GLTFExporter.js' );
require( 'three/examples/js/utils/BufferGeometryUtils.js' );
if ( process.argv.length <= 2 ) {
console.log( `Usage: ${path.basename( __filename )} model.json [ --optimize ]` );
process.exit( - 1 );
}
var file = process.argv[ 2 ];
var optimize = process.argv.indexOf( '--optimize' ) > 0;
var resourceDirectory = THREE.LoaderUtils.extractUrlBase( file );
//
// Patch global scope to imitate browser environment.
global.window = global;
global.Blob = Blob;
global.FileReader = FileReader;
global.THREE = THREE;
global.document = {
createElement: ( nodeName ) => {
if ( nodeName !== 'canvas' ) throw new Error( `Cannot create node ${nodeName}` );
const canvas = new Canvas( 256, 256 );
// This isn't working — currently need to avoid toBlob(), so export to embedded .gltf not .glb.
// canvas.toBlob = function () {
// return new Blob([this.toBuffer()]);
// };
return canvas;
}
};
//
// Load legacy JSON file and construct a mesh.
var jsonContent = fs.readFileSync( file, 'utf8' );
var loader = new THREE.LegacyJSONLoader();
var { geometry, materials } = loader.parse( JSON.parse( jsonContent ), resourceDirectory );
var mesh;
var boneDefs = geometry.bones || [];
var animations = geometry.animations || [];
var hasVertexColors = geometry.colors.length > 0;
geometry = new THREE.BufferGeometry().fromGeometry( geometry );
// Remove unnecessary vertex colors and groups added during BufferGeometry conversion.
if ( ! hasVertexColors ) geometry.removeAttribute( 'color' );
if ( geometry.groups.length === 1 ) geometry.clearGroups();
if ( ! materials ) {
materials = new THREE.MeshStandardMaterial( { color: 0x888888, roughness: 1, metalness: 0 } );
}
if ( optimize ) {
geometry = THREE.BufferGeometryUtils.mergeVertices( geometry );
}
if ( boneDefs.length ) {
var { roots, bones } = initBones( boneDefs );
mesh = new THREE.SkinnedMesh( geometry, materials );
roots.forEach( ( bone ) => mesh.add( bone ) );
mesh.updateMatrixWorld( true );
mesh.bind( new THREE.Skeleton( bones ), mesh.matrixWorld );
mesh.normalizeSkinWeights();
} else {
mesh = new THREE.Mesh( geometry, materials );
}
//
// Export to glTF.
var exporter = new THREE.GLTFExporter();
exporter.parse( mesh, ( json ) => {
var content = JSON.stringify( json );
fs.writeFileSync( path.basename( file, '.json' ) + '.gltf', content, 'utf8' );
}, { binary: false, animations } );
//
/** Previously SkinnedMesh.initBones(). */
function initBones( boneDefs ) {
var bones = [], bone, gbone;
var i, il;
// first, create array of 'Bone' objects from geometry data
for ( i = 0, il = boneDefs.length; i < il; i ++ ) {
gbone = boneDefs[ i ];
// create new 'Bone' object
bone = new THREE.Bone();
bones.push( bone );
// apply values
bone.name = gbone.name;
bone.position.fromArray( gbone.pos );
bone.quaternion.fromArray( gbone.rotq );
if ( gbone.scl !== undefined ) bone.scale.fromArray( gbone.scl );
}
// second, create bone hierarchy
var roots = [];
for ( i = 0, il = boneDefs.length; i < il; i ++ ) {
gbone = boneDefs[ i ];
if ( ( gbone.parent !== - 1 ) && ( gbone.parent !== null ) && ( bones[ gbone.parent ] !== undefined ) ) {
// subsequent bones in the hierarchy
bones[ gbone.parent ].add( bones[ i ] );
} else {
// topmost bone, immediate child of the skinned mesh
roots.push( bones[ i ] );
}
}
return { roots, bones };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment