|
@@ -69,7 +69,8 @@ THREE.GLTFExporter.prototype = {
|
|
|
truncateDrawRange: true,
|
|
|
embedImages: true,
|
|
|
animations: [],
|
|
|
- forceIndices: false
|
|
|
+ forceIndices: false,
|
|
|
+ forcePowerOfTwoTexture: false
|
|
|
};
|
|
|
|
|
|
options = Object.assign( {}, DEFAULT_OPTIONS, options );
|
|
@@ -98,8 +99,8 @@ THREE.GLTFExporter.prototype = {
|
|
|
var skins = [];
|
|
|
var cachedData = {
|
|
|
|
|
|
- images: {},
|
|
|
- materials: {}
|
|
|
+ materials: {},
|
|
|
+ textures: {}
|
|
|
|
|
|
};
|
|
|
|
|
@@ -192,6 +193,19 @@ THREE.GLTFExporter.prototype = {
|
|
|
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Checks if image size is POT.
|
|
|
+ *
|
|
|
+ * @param {Image} image The image to be checked.
|
|
|
+ * @returns {Boolean} Returns true if image size is POT.
|
|
|
+ *
|
|
|
+ */
|
|
|
+ function isPowerOfTwo( image ) {
|
|
|
+
|
|
|
+ return THREE.Math.isPowerOfTwo( image.width ) && THREE.Math.isPowerOfTwo( image.height );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Get the required size + padding for a buffer, rounded to the next 4-byte boundary.
|
|
|
* https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment
|
|
@@ -205,7 +219,7 @@ THREE.GLTFExporter.prototype = {
|
|
|
return Math.ceil( bufferSize / 4 ) * 4;
|
|
|
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* Returns a buffer aligned to 4-byte boundary.
|
|
|
*
|
|
@@ -256,8 +270,8 @@ THREE.GLTFExporter.prototype = {
|
|
|
|
|
|
// Create a new dataview and dump the attribute's array into it
|
|
|
var byteLength = count * attribute.itemSize * componentSize;
|
|
|
-
|
|
|
- // adjust required size of array buffer with padding
|
|
|
+
|
|
|
+ // adjust required size of array buffer with padding
|
|
|
// to satisfy gltf requirement that the length is divisible by 4
|
|
|
byteLength = getPaddedBufferSize( byteLength );
|
|
|
|
|
@@ -448,11 +462,7 @@ THREE.GLTFExporter.prototype = {
|
|
|
*/
|
|
|
function processImage( map ) {
|
|
|
|
|
|
- if ( cachedData.images[ map.uuid ] !== undefined ) {
|
|
|
-
|
|
|
- return cachedData.images[ map.uuid ];
|
|
|
-
|
|
|
- }
|
|
|
+ // @TODO Cache
|
|
|
|
|
|
if ( ! outputJSON.images ) {
|
|
|
|
|
@@ -461,23 +471,34 @@ THREE.GLTFExporter.prototype = {
|
|
|
}
|
|
|
|
|
|
var mimeType = map.format === THREE.RGBAFormat ? 'image/png' : 'image/jpeg';
|
|
|
- var gltfImage = {mimeType: mimeType};
|
|
|
+ var gltfImage = { mimeType: mimeType };
|
|
|
|
|
|
if ( options.embedImages ) {
|
|
|
|
|
|
var canvas = cachedCanvas = cachedCanvas || document.createElement( 'canvas' );
|
|
|
+
|
|
|
canvas.width = map.image.width;
|
|
|
canvas.height = map.image.height;
|
|
|
+
|
|
|
+ if ( options.forcePowerOfTwoTexture && ! isPowerOfTwo( map.image ) ) {
|
|
|
+
|
|
|
+ console.warn( 'GLTFExporter: Resized non-power-of-two image.', map.image );
|
|
|
+
|
|
|
+ canvas.width = THREE.Math.floorPowerOfTwo( canvas.width );
|
|
|
+ canvas.height = THREE.Math.floorPowerOfTwo( canvas.height );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
var ctx = canvas.getContext( '2d' );
|
|
|
|
|
|
if ( map.flipY === true ) {
|
|
|
|
|
|
- ctx.translate( 0, map.image.height );
|
|
|
- ctx.scale( 1, -1 );
|
|
|
+ ctx.translate( 0, canvas.height );
|
|
|
+ ctx.scale( 1, - 1 );
|
|
|
|
|
|
}
|
|
|
|
|
|
- ctx.drawImage( map.image, 0, 0 );
|
|
|
+ ctx.drawImage( map.image, 0, 0, canvas.width, canvas.height );
|
|
|
|
|
|
// @TODO Embed in { bufferView } if options.binary set.
|
|
|
|
|
@@ -491,10 +512,7 @@ THREE.GLTFExporter.prototype = {
|
|
|
|
|
|
outputJSON.images.push( gltfImage );
|
|
|
|
|
|
- var index = outputJSON.images.length - 1;
|
|
|
- cachedData.images[ map.uuid ] = index;
|
|
|
-
|
|
|
- return index;
|
|
|
+ return outputJSON.images.length - 1;
|
|
|
|
|
|
}
|
|
|
|
|
@@ -533,6 +551,12 @@ THREE.GLTFExporter.prototype = {
|
|
|
*/
|
|
|
function processTexture( map ) {
|
|
|
|
|
|
+ if ( cachedData.textures[ map.uuid ] !== undefined ) {
|
|
|
+
|
|
|
+ return cachedData.textures[ map.uuid ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
if ( ! outputJSON.textures ) {
|
|
|
|
|
|
outputJSON.textures = [];
|
|
@@ -548,7 +572,10 @@ THREE.GLTFExporter.prototype = {
|
|
|
|
|
|
outputJSON.textures.push( gltfTexture );
|
|
|
|
|
|
- return outputJSON.textures.length - 1;
|
|
|
+ var index = outputJSON.textures.length - 1;
|
|
|
+ cachedData.textures[ map.uuid ] = index;
|
|
|
+
|
|
|
+ return index;
|
|
|
|
|
|
}
|
|
|
|
|
@@ -876,28 +903,87 @@ THREE.GLTFExporter.prototype = {
|
|
|
if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {
|
|
|
|
|
|
var weights = [];
|
|
|
+ var targetNames = [];
|
|
|
+ var reverseDictionary = {};
|
|
|
+
|
|
|
+ if ( mesh.morphTargetDictionary !== undefined ) {
|
|
|
+
|
|
|
+ for ( var key in mesh.morphTargetDictionary ) {
|
|
|
+
|
|
|
+ reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
gltfMesh.primitives[ 0 ].targets = [];
|
|
|
|
|
|
for ( var i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {
|
|
|
|
|
|
var target = {};
|
|
|
|
|
|
+ var warned = false;
|
|
|
+
|
|
|
for ( var attributeName in geometry.morphAttributes ) {
|
|
|
|
|
|
+ // glTF 2.0 morph supports only POSITION/NORMAL/TANGENT.
|
|
|
+ // Three.js doesn't support TANGENT yet.
|
|
|
+
|
|
|
+ if ( attributeName !== 'position' && attributeName !== 'normal' ) {
|
|
|
+
|
|
|
+ if ( ! warned ) {
|
|
|
+
|
|
|
+ console.warn( 'GLTFExporter: Only POSITION and NORMAL morph are supported.' );
|
|
|
+ warned = true;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ continue;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
var attribute = geometry.morphAttributes[ attributeName ][ i ];
|
|
|
- attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();
|
|
|
- target[ attributeName ] = processAccessor( attribute, geometry );
|
|
|
+
|
|
|
+ // Three.js morph attribute has absolute values while the one of glTF has relative values.
|
|
|
+ //
|
|
|
+ // glTF 2.0 Specification:
|
|
|
+ // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets
|
|
|
+
|
|
|
+ var baseAttribute = geometry.attributes[ attributeName ];
|
|
|
+ // Clones attribute not to override
|
|
|
+ var relativeAttribute = attribute.clone();
|
|
|
+
|
|
|
+ for ( var j = 0, jl = attribute.count; j < jl; j ++ ) {
|
|
|
+
|
|
|
+ relativeAttribute.setXYZ(
|
|
|
+ j,
|
|
|
+ attribute.getX( j ) - baseAttribute.getX( j ),
|
|
|
+ attribute.getY( j ) - baseAttribute.getY( j ),
|
|
|
+ attribute.getZ( j ) - baseAttribute.getZ( j )
|
|
|
+ );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ target[ attributeName.toUpperCase() ] = processAccessor( relativeAttribute, geometry );
|
|
|
|
|
|
}
|
|
|
|
|
|
gltfMesh.primitives[ 0 ].targets.push( target );
|
|
|
|
|
|
weights.push( mesh.morphTargetInfluences[ i ] );
|
|
|
+ if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );
|
|
|
|
|
|
}
|
|
|
|
|
|
gltfMesh.weights = weights;
|
|
|
|
|
|
+ if ( targetNames.length > 0 ) {
|
|
|
+
|
|
|
+ gltfMesh.extras = {};
|
|
|
+ gltfMesh.extras.targetNames = targetNames;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|
|
|
outputJSON.meshes.push( gltfMesh );
|
|
@@ -968,13 +1054,12 @@ THREE.GLTFExporter.prototype = {
|
|
|
*
|
|
|
* Status:
|
|
|
* - Only properties listed in PATH_PROPERTIES may be animated.
|
|
|
- * - Only LINEAR and STEP interpolation currently supported.
|
|
|
*
|
|
|
* @param {THREE.AnimationClip} clip
|
|
|
* @param {THREE.Object3D} root
|
|
|
* @return {number}
|
|
|
*/
|
|
|
- function processAnimation ( clip, root ) {
|
|
|
+ function processAnimation( clip, root ) {
|
|
|
|
|
|
if ( ! outputJSON.animations ) {
|
|
|
|
|
@@ -1022,11 +1107,37 @@ THREE.GLTFExporter.prototype = {
|
|
|
|
|
|
}
|
|
|
|
|
|
+ var interpolation;
|
|
|
+
|
|
|
+ // @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE
|
|
|
+
|
|
|
+ // Detecting glTF cubic spline interpolant by checking factory method's special property
|
|
|
+ // GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return
|
|
|
+ // valid value from .getInterpolation().
|
|
|
+ if ( track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true ) {
|
|
|
+
|
|
|
+ interpolation = 'CUBICSPLINE';
|
|
|
+
|
|
|
+ // itemSize of CUBICSPLINE keyframe is 9
|
|
|
+ // (VEC3 * 3: inTangent, splineVertex, and outTangent)
|
|
|
+ // but needs to be stored as VEC3 so dividing by 3 here.
|
|
|
+ outputItemSize /= 3;
|
|
|
+
|
|
|
+ } else if ( track.getInterpolation() === THREE.InterpolateDiscrete ) {
|
|
|
+
|
|
|
+ interpolation = 'STEP';
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ interpolation = 'LINEAR';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
samplers.push( {
|
|
|
|
|
|
input: processAccessor( new THREE.BufferAttribute( track.times, inputItemSize ) ),
|
|
|
output: processAccessor( new THREE.BufferAttribute( track.values, outputItemSize ) ),
|
|
|
- interpolation: track.getInterpolation() === THREE.InterpolateDiscrete ? 'STEP' : 'LINEAR'
|
|
|
+ interpolation: interpolation
|
|
|
|
|
|
} );
|
|
|
|
|
@@ -1151,7 +1262,8 @@ THREE.GLTFExporter.prototype = {
|
|
|
|
|
|
}
|
|
|
|
|
|
- if ( object.name ) {
|
|
|
+ // We don't export empty strings name because it represents no-name in Three.js.
|
|
|
+ if ( object.name !== '' ) {
|
|
|
|
|
|
gltfNode.name = String( object.name );
|
|
|
|
|
@@ -1434,4 +1546,4 @@ THREE.GLTFExporter.prototype = {
|
|
|
|
|
|
}
|
|
|
|
|
|
-};
|
|
|
+};
|