processMesh( mesh ) {

    		const cache = this.cache;
    		const json = this.json;

    		const meshCacheKeyParts = [ mesh.geometry.uuid ];

    		if ( Array.isArray( mesh.material ) ) {

    			for ( let i = 0, l = mesh.material.length; i < l; i ++ ) {

    				meshCacheKeyParts.push( mesh.material[ i ].uuid	);

    			}

    		} else {

    			meshCacheKeyParts.push( mesh.material.uuid );

    		}

    		const meshCacheKey = meshCacheKeyParts.join( ':' );

    		if ( cache.meshes.has( meshCacheKey ) ) return cache.meshes.get( meshCacheKey );

    		const geometry = mesh.geometry;
    		let mode;

    		// Use the correct mode
    		if ( mesh.isLineSegments ) {

    			mode = WEBGL_CONSTANTS.LINES;

    		} else if ( mesh.isLineLoop ) {

    			mode = WEBGL_CONSTANTS.LINE_LOOP;

    		} else if ( mesh.isLine ) {

    			mode = WEBGL_CONSTANTS.LINE_STRIP;

    		} else if ( mesh.isPoints ) {

    			mode = WEBGL_CONSTANTS.POINTS;

    		} else {

    			mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES;

    		}

    		if ( geometry.isBufferGeometry !== true ) {

    			throw new Error( 'THREE.GLTFExporter: Geometry is not of type THREE.BufferGeometry.' );

    		}

    		const meshDef = {};
    		const attributes = {};
    		const primitives = [];
    		const targets = [];

    		// Conversion between attributes names in threejs and gltf spec
    		const nameConversion = {
    			uv: 'TEXCOORD_0',
    			uv2: 'TEXCOORD_1',
    			color: 'COLOR_0',
    			skinWeight: 'WEIGHTS_0',
    			skinIndex: 'JOINTS_0'
    		};

    		const originalNormal = geometry.getAttribute( 'normal' );

    		if ( originalNormal !== undefined && ! this.isNormalizedNormalAttribute( originalNormal ) ) {

    			console.warn( 'THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.' );

    			geometry.setAttribute( 'normal', this.createNormalizedNormalAttribute( originalNormal ) );

    		}

    		// @QUESTION Detect if .vertexColors = true?
    		// For every attribute create an accessor
    		let modifiedAttribute = null;

    		for ( let attributeName in geometry.attributes ) {

    			// Ignore morph target attributes, which are exported later.
    			if ( attributeName.substr( 0, 5 ) === 'morph' ) continue;

    			const attribute = geometry.attributes[ attributeName ];
    			attributeName = nameConversion[ attributeName ] || attributeName.toUpperCase();

    			// Prefix all geometry attributes except the ones specifically
    			// listed in the spec; non-spec attributes are considered custom.
    			const validVertexAttributes =
    					/^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/;

    			if ( ! validVertexAttributes.test( attributeName ) ) attributeName = '_' + attributeName;

    			if ( cache.attributes.has( this.getUID( attribute ) ) ) {

    				attributes[ attributeName ] = cache.attributes.get( this.getUID( attribute ) );
    				continue;

    			}

    			// JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT.
    			modifiedAttribute = null;
    			const array = attribute.array;

    			if ( attributeName === 'JOINTS_0' &&
    				! ( array instanceof Uint16Array ) &&
    				! ( array instanceof Uint8Array ) ) {

    				console.warn( 'GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.' );
    				modifiedAttribute = new BufferAttribute( new Uint16Array( array ), attribute.itemSize, attribute.normalized );

    			}

    			const accessor = this.processAccessor( modifiedAttribute || attribute, geometry );

    			if ( accessor !== null ) {

    				attributes[ attributeName ] = accessor;
    				cache.attributes.set( this.getUID( attribute ), accessor );

    			}

    		}

    		if ( originalNormal !== undefined ) geometry.setAttribute( 'normal', originalNormal );

    		// Skip if no exportable attributes found
    		if ( Object.keys( attributes ).length === 0 ) return null;

    		// Morph targets
    		if ( mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0 ) {

    			const weights = [];
    			const targetNames = [];
    			const reverseDictionary = {};

    			if ( mesh.morphTargetDictionary !== undefined ) {

    				for ( const key in mesh.morphTargetDictionary ) {

    					reverseDictionary[ mesh.morphTargetDictionary[ key ] ] = key;

    				}

    			}

    			for ( let i = 0; i < mesh.morphTargetInfluences.length; ++ i ) {

    				const target = {};
    				let warned = false;

    				for ( const 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;

    					}

    					const attribute = geometry.morphAttributes[ attributeName ][ i ];
    					const gltfAttributeName = attributeName.toUpperCase();

    					// 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

    					const baseAttribute = geometry.attributes[ attributeName ];

    					if ( cache.attributes.has( this.getUID( attribute ) ) ) {

    						target[ gltfAttributeName ] = cache.attributes.get( this.getUID( attribute ) );
    						continue;

    					}

    					// Clones attribute not to override
    					const relativeAttribute = attribute.clone();

    					if ( ! geometry.morphTargetsRelative ) {

    						for ( let 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[ gltfAttributeName ] = this.processAccessor( relativeAttribute, geometry );
    					cache.attributes.set( this.getUID( baseAttribute ), target[ gltfAttributeName ] );

    				}

    				targets.push( target );

    				weights.push( mesh.morphTargetInfluences[ i ] );

    				if ( mesh.morphTargetDictionary !== undefined ) targetNames.push( reverseDictionary[ i ] );

    			}

    			meshDef.weights = weights;

    			if ( targetNames.length > 0 ) {

    				meshDef.extras = {};
    				meshDef.extras.targetNames = targetNames;

    			}

    		}

    		const isMultiMaterial = Array.isArray( mesh.material );

    		if ( isMultiMaterial && geometry.groups.length === 0 ) return null;

    		const materials = isMultiMaterial ? mesh.material : [ mesh.material ];
    		const groups = isMultiMaterial ? geometry.groups : [ { materialIndex: 0, start: undefined, count: undefined } ];

    		for ( let i = 0, il = groups.length; i < il; i ++ ) {

    			const primitive = {
    				mode: mode,
    				attributes: attributes,
    			};

    			this.serializeUserData( geometry, primitive );

    			if ( targets.length > 0 ) primitive.targets = targets;

    			if ( geometry.index !== null ) {

    				let cacheKey = this.getUID( geometry.index );

    				if ( groups[ i ].start !== undefined || groups[ i ].count !== undefined ) {

    					cacheKey += ':' + groups[ i ].start + ':' + groups[ i ].count;

    				}

    				if ( cache.attributes.has( cacheKey ) ) {

    					primitive.indices = cache.attributes.get( cacheKey );

    				} else {

    					primitive.indices = this.processAccessor( geometry.index, geometry, groups[ i ].start, groups[ i ].count );
    					cache.attributes.set( cacheKey, primitive.indices );

    				}

    				if ( primitive.indices === null ) delete primitive.indices;

    			}

    			const material = this.processMaterial( materials[ groups[ i ].materialIndex ] );

    			if ( material !== null ) primitive.material = material;

    			primitives.push( primitive );

    		}

    		meshDef.primitives = primitives;

    		if ( ! json.meshes ) json.meshes = [];

    		this._invokeAll( function ( ext ) {

    			ext.writeMesh && ext.writeMesh( mesh, meshDef );

    		} );

    		const index = json.meshes.push( meshDef ) - 1;
    		cache.meshes.set( meshCacheKey, index );
    		return index;

    	}
Process mesh
@param  {THREE.Mesh} mesh Mesh to process
@return {Integer|null} Index of the processed mesh in the "meshes" array