three
Version:
JavaScript 3D library
1,963 lines (1,200 loc) • 36.4 kB
JavaScript
import {
Color,
ColorManagement,
MathUtils,
Matrix4,
Vector3,
SRGBColorSpace
} from 'three';
/**
* Utility functions for parsing
*/
function getElementsByTagName( xml, name ) {
// Non recursive xml.getElementsByTagName() ...
const array = [];
const childNodes = xml.childNodes;
for ( let i = 0, l = childNodes.length; i < l; i ++ ) {
const child = childNodes[ i ];
if ( child.nodeName === name ) {
array.push( child );
}
}
return array;
}
function parseStrings( text ) {
if ( text.length === 0 ) return [];
return text.trim().split( /\s+/ );
}
function parseFloats( text ) {
if ( text.length === 0 ) return [];
return text.trim().split( /\s+/ ).map( parseFloat );
}
function parseInts( text ) {
if ( text.length === 0 ) return [];
return text.trim().split( /\s+/ ).map( s => parseInt( s ) );
}
function parseId( text ) {
return text.substring( 1 );
}
/**
* ColladaParser handles XML parsing and converts Collada XML to intermediate data structures.
*/
class ColladaParser {
constructor() {
this.count = 0;
}
generateId() {
return 'three_default_' + ( this.count ++ );
}
parse( text ) {
if ( text.length === 0 ) {
return null;
}
const xml = new DOMParser().parseFromString( text, 'application/xml' );
const collada = getElementsByTagName( xml, 'COLLADA' )[ 0 ];
const parserError = xml.getElementsByTagName( 'parsererror' )[ 0 ];
if ( parserError !== undefined ) {
// Chrome will return parser error with a div in it
const errorElement = getElementsByTagName( parserError, 'div' )[ 0 ];
let errorText;
if ( errorElement ) {
errorText = errorElement.textContent;
} else {
errorText = this.parserErrorToText( parserError );
}
console.error( 'THREE.ColladaLoader: Failed to parse collada file.\n', errorText );
return null;
}
// metadata
const version = collada.getAttribute( 'version' );
console.debug( 'THREE.ColladaLoader: File version', version );
const asset = this.parseAsset( getElementsByTagName( collada, 'asset' )[ 0 ] );
//
const library = {
animations: {},
clips: {},
controllers: {},
images: {},
effects: {},
materials: {},
cameras: {},
lights: {},
geometries: {},
nodes: {},
visualScenes: {},
kinematicsModels: {},
physicsModels: {},
kinematicsScenes: {}
};
this.library = library;
this.collada = collada;
this.parseLibrary( collada, 'library_animations', 'animation', this.parseAnimation.bind( this ) );
this.parseLibrary( collada, 'library_animation_clips', 'animation_clip', this.parseAnimationClip.bind( this ) );
this.parseLibrary( collada, 'library_controllers', 'controller', this.parseController.bind( this ) );
this.parseLibrary( collada, 'library_images', 'image', this.parseImage.bind( this ) );
this.parseLibrary( collada, 'library_effects', 'effect', this.parseEffect.bind( this ) );
this.parseLibrary( collada, 'library_materials', 'material', this.parseMaterial.bind( this ) );
this.parseLibrary( collada, 'library_cameras', 'camera', this.parseCamera.bind( this ) );
this.parseLibrary( collada, 'library_lights', 'light', this.parseLight.bind( this ) );
this.parseLibrary( collada, 'library_geometries', 'geometry', this.parseGeometry.bind( this ) );
this.parseLibrary( collada, 'library_nodes', 'node', this.parseNode.bind( this ) );
this.parseLibrary( collada, 'library_visual_scenes', 'visual_scene', this.parseVisualScene.bind( this ) );
this.parseLibrary( collada, 'library_kinematics_models', 'kinematics_model', this.parseKinematicsModel.bind( this ) );
this.parseLibrary( collada, 'library_physics_models', 'physics_model', this.parsePhysicsModel.bind( this ) );
this.parseLibrary( collada, 'scene', 'instance_kinematics_scene', this.parseKinematicsScene.bind( this ) );
return {
library: library,
asset: asset,
collada: collada
};
}
// convert the parser error element into text with each child elements text
// separated by new lines.
parserErrorToText( parserError ) {
const parts = [];
const stack = [ parserError ];
while ( stack.length ) {
const node = stack.shift();
if ( node.nodeType === Node.TEXT_NODE ) {
parts.push( node.textContent );
} else {
parts.push( '\n' );
stack.push( ...node.childNodes );
}
}
return parts.join( '' ).trim();
}
// asset
parseAsset( xml ) {
return {
unit: this.parseAssetUnit( getElementsByTagName( xml, 'unit' )[ 0 ] ),
upAxis: this.parseAssetUpAxis( getElementsByTagName( xml, 'up_axis' )[ 0 ] )
};
}
parseAssetUnit( xml ) {
if ( ( xml !== undefined ) && ( xml.hasAttribute( 'meter' ) === true ) ) {
return parseFloat( xml.getAttribute( 'meter' ) );
} else {
return 1; // default 1 meter
}
}
parseAssetUpAxis( xml ) {
return xml !== undefined ? xml.textContent : 'Y_UP';
}
// library
parseLibrary( xml, libraryName, nodeName, parser ) {
const library = getElementsByTagName( xml, libraryName )[ 0 ];
if ( library !== undefined ) {
const elements = getElementsByTagName( library, nodeName );
for ( let i = 0; i < elements.length; i ++ ) {
parser( elements[ i ] );
}
}
}
// animation
parseAnimation( xml ) {
const data = {
sources: {},
samplers: {},
channels: {}
};
let hasChildren = false;
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
let id;
switch ( child.nodeName ) {
case 'source':
id = child.getAttribute( 'id' );
data.sources[ id ] = this.parseSource( child );
break;
case 'sampler':
id = child.getAttribute( 'id' );
data.samplers[ id ] = this.parseAnimationSampler( child );
break;
case 'channel':
id = child.getAttribute( 'target' );
data.channels[ id ] = this.parseAnimationChannel( child );
break;
case 'animation':
// hierarchy of related animations
this.parseAnimation( child );
hasChildren = true;
break;
default:
}
}
if ( hasChildren === false ) {
// since 'id' attributes can be optional, it's necessary to generate a UUID for unique assignment
this.library.animations[ xml.getAttribute( 'id' ) || MathUtils.generateUUID() ] = data;
}
}
parseAnimationSampler( xml ) {
const data = {
inputs: {},
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'input':
const id = parseId( child.getAttribute( 'source' ) );
const semantic = child.getAttribute( 'semantic' );
data.inputs[ semantic ] = id;
break;
}
}
return data;
}
parseAnimationChannel( xml ) {
const data = {};
const target = xml.getAttribute( 'target' );
// parsing SID Addressing Syntax
let parts = target.split( '/' );
const id = parts.shift();
let sid = parts.shift();
// check selection syntax
const arraySyntax = ( sid.indexOf( '(' ) !== - 1 );
const memberSyntax = ( sid.indexOf( '.' ) !== - 1 );
if ( memberSyntax ) {
// member selection access
parts = sid.split( '.' );
sid = parts.shift();
data.member = parts.shift();
} else if ( arraySyntax ) {
// array-access syntax. can be used to express fields in one-dimensional vectors or two-dimensional matrices.
const indices = sid.split( '(' );
sid = indices.shift();
for ( let i = 0; i < indices.length; i ++ ) {
indices[ i ] = parseInt( indices[ i ].replace( /\)/, '' ) );
}
data.indices = indices;
}
data.id = id;
data.sid = sid;
data.arraySyntax = arraySyntax;
data.memberSyntax = memberSyntax;
data.sampler = parseId( xml.getAttribute( 'source' ) );
return data;
}
// animation clips
parseAnimationClip( xml ) {
const data = {
name: xml.getAttribute( 'id' ) || 'default',
start: parseFloat( xml.getAttribute( 'start' ) || 0 ),
end: parseFloat( xml.getAttribute( 'end' ) || 0 ),
animations: []
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'instance_animation':
data.animations.push( parseId( child.getAttribute( 'url' ) ) );
break;
}
}
this.library.clips[ xml.getAttribute( 'id' ) ] = data;
}
// controller
parseController( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'skin':
// there is exactly one skin per controller
data.id = parseId( child.getAttribute( 'source' ) );
data.skin = this.parseSkin( child );
break;
case 'morph':
data.id = parseId( child.getAttribute( 'source' ) );
console.warn( 'THREE.ColladaLoader: Morph target animation not supported yet.' );
break;
}
}
this.library.controllers[ xml.getAttribute( 'id' ) ] = data;
}
parseSkin( xml ) {
const data = {
sources: {}
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'bind_shape_matrix':
data.bindShapeMatrix = parseFloats( child.textContent );
break;
case 'source':
const id = child.getAttribute( 'id' );
data.sources[ id ] = this.parseSource( child );
break;
case 'joints':
data.joints = this.parseJoints( child );
break;
case 'vertex_weights':
data.vertexWeights = this.parseVertexWeights( child );
break;
}
}
return data;
}
parseJoints( xml ) {
const data = {
inputs: {}
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'input':
const semantic = child.getAttribute( 'semantic' );
const id = parseId( child.getAttribute( 'source' ) );
data.inputs[ semantic ] = id;
break;
}
}
return data;
}
parseVertexWeights( xml ) {
const data = {
inputs: {}
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'input':
const semantic = child.getAttribute( 'semantic' );
const id = parseId( child.getAttribute( 'source' ) );
const offset = parseInt( child.getAttribute( 'offset' ) );
data.inputs[ semantic ] = { id: id, offset: offset };
break;
case 'vcount':
data.vcount = parseInts( child.textContent );
break;
case 'v':
data.v = parseInts( child.textContent );
break;
}
}
return data;
}
// image
parseImage( xml ) {
const data = {
init_from: getElementsByTagName( xml, 'init_from' )[ 0 ].textContent
};
this.library.images[ xml.getAttribute( 'id' ) ] = data;
}
// effect
parseEffect( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'profile_COMMON':
data.profile = this.parseEffectProfileCOMMON( child );
break;
}
}
this.library.effects[ xml.getAttribute( 'id' ) ] = data;
}
parseEffectProfileCOMMON( xml ) {
const data = {
surfaces: {},
samplers: {}
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'newparam':
this.parseEffectNewparam( child, data );
break;
case 'technique':
data.technique = this.parseEffectTechnique( child );
break;
case 'extra':
data.extra = this.parseEffectExtra( child );
break;
}
}
return data;
}
parseEffectNewparam( xml, data ) {
const sid = xml.getAttribute( 'sid' );
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'surface':
data.surfaces[ sid ] = this.parseEffectSurface( child );
break;
case 'sampler2D':
data.samplers[ sid ] = this.parseEffectSampler( child );
break;
}
}
}
parseEffectSurface( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'init_from':
data.init_from = child.textContent;
break;
}
}
return data;
}
parseEffectSampler( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'source':
data.source = child.textContent;
break;
}
}
return data;
}
parseEffectTechnique( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'constant':
case 'lambert':
case 'blinn':
case 'phong':
data.type = child.nodeName;
data.parameters = this.parseEffectParameters( child );
break;
case 'extra':
data.extra = this.parseEffectExtra( child );
break;
}
}
return data;
}
parseEffectParameters( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'emission':
case 'diffuse':
case 'specular':
case 'bump':
case 'ambient':
case 'shininess':
case 'transparency':
data[ child.nodeName ] = this.parseEffectParameter( child );
break;
case 'transparent':
data[ child.nodeName ] = {
opaque: child.hasAttribute( 'opaque' ) ? child.getAttribute( 'opaque' ) : 'A_ONE',
data: this.parseEffectParameter( child )
};
break;
}
}
return data;
}
parseEffectParameter( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'color':
data[ child.nodeName ] = parseFloats( child.textContent );
break;
case 'float':
data[ child.nodeName ] = parseFloat( child.textContent );
break;
case 'texture':
data[ child.nodeName ] = { id: child.getAttribute( 'texture' ), extra: this.parseEffectParameterTexture( child ) };
break;
}
}
return data;
}
parseEffectParameterTexture( xml ) {
const data = {
technique: {}
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'extra':
this.parseEffectParameterTextureExtra( child, data );
break;
}
}
return data;
}
parseEffectParameterTextureExtra( xml, data ) {
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'technique':
this.parseEffectParameterTextureExtraTechnique( child, data );
break;
}
}
}
parseEffectParameterTextureExtraTechnique( xml, data ) {
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'repeatU':
case 'repeatV':
case 'offsetU':
case 'offsetV':
data.technique[ child.nodeName ] = parseFloat( child.textContent );
break;
case 'wrapU':
case 'wrapV':
// some files have values for wrapU/wrapV which become NaN via parseInt
if ( child.textContent.toUpperCase() === 'TRUE' ) {
data.technique[ child.nodeName ] = 1;
} else if ( child.textContent.toUpperCase() === 'FALSE' ) {
data.technique[ child.nodeName ] = 0;
} else {
data.technique[ child.nodeName ] = parseInt( child.textContent );
}
break;
case 'bump':
data[ child.nodeName ] = this.parseEffectExtraTechniqueBump( child );
break;
}
}
}
parseEffectExtra( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'technique':
data.technique = this.parseEffectExtraTechnique( child );
break;
}
}
return data;
}
parseEffectExtraTechnique( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'double_sided':
data[ child.nodeName ] = parseInt( child.textContent );
break;
case 'bump':
data[ child.nodeName ] = this.parseEffectExtraTechniqueBump( child );
break;
}
}
return data;
}
parseEffectExtraTechniqueBump( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'texture':
data[ child.nodeName ] = { id: child.getAttribute( 'texture' ), texcoord: child.getAttribute( 'texcoord' ), extra: this.parseEffectParameterTexture( child ) };
break;
}
}
return data;
}
// material
parseMaterial( xml ) {
const data = {
name: xml.getAttribute( 'name' )
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'instance_effect':
data.url = parseId( child.getAttribute( 'url' ) );
break;
}
}
this.library.materials[ xml.getAttribute( 'id' ) ] = data;
}
// camera
parseCamera( xml ) {
const data = {
name: xml.getAttribute( 'name' )
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'optics':
data.optics = this.parseCameraOptics( child );
break;
}
}
this.library.cameras[ xml.getAttribute( 'id' ) ] = data;
}
parseCameraOptics( xml ) {
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
switch ( child.nodeName ) {
case 'technique_common':
return this.parseCameraTechnique( child );
}
}
return {};
}
parseCameraTechnique( xml ) {
const data = {};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
switch ( child.nodeName ) {
case 'perspective':
case 'orthographic':
data.technique = child.nodeName;
data.parameters = this.parseCameraParameters( child );
break;
}
}
return data;
}
parseCameraParameters( xml ) {
const data = {};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
switch ( child.nodeName ) {
case 'xfov':
case 'yfov':
case 'xmag':
case 'ymag':
case 'znear':
case 'zfar':
case 'aspect_ratio':
data[ child.nodeName ] = parseFloat( child.textContent );
break;
}
}
return data;
}
// light
parseLight( xml ) {
let data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'technique_common':
data = this.parseLightTechnique( child );
break;
}
}
this.library.lights[ xml.getAttribute( 'id' ) ] = data;
}
parseLightTechnique( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'directional':
case 'point':
case 'spot':
case 'ambient':
data.technique = child.nodeName;
data.parameters = this.parseLightParameters( child );
break;
}
}
return data;
}
parseLightParameters( xml ) {
const data = {};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'color':
const array = parseFloats( child.textContent );
data.color = new Color().fromArray( array );
ColorManagement.colorSpaceToWorking( data.color, SRGBColorSpace );
break;
case 'falloff_angle':
data.falloffAngle = parseFloat( child.textContent );
break;
case 'quadratic_attenuation':
const f = parseFloat( child.textContent );
data.distance = f ? Math.sqrt( 1 / f ) : 0;
break;
}
}
return data;
}
// geometry
parseGeometry( xml ) {
const data = {
name: xml.getAttribute( 'name' ),
sources: {},
vertices: {},
primitives: []
};
const mesh = getElementsByTagName( xml, 'mesh' )[ 0 ];
// the following tags inside geometry are not supported yet (see https://github.com/mrdoob/three.js/pull/12606): convex_mesh, spline, brep
if ( mesh === undefined ) return;
for ( let i = 0; i < mesh.childNodes.length; i ++ ) {
const child = mesh.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
const id = child.getAttribute( 'id' );
switch ( child.nodeName ) {
case 'source':
data.sources[ id ] = this.parseSource( child );
break;
case 'vertices':
// data.sources[ id ] = data.sources[ parseId( getElementsByTagName( child, 'input' )[ 0 ].getAttribute( 'source' ) ) ];
data.vertices = this.parseGeometryVertices( child );
break;
case 'polygons':
console.warn( 'THREE.ColladaLoader: Unsupported primitive type: ', child.nodeName );
break;
case 'lines':
case 'linestrips':
case 'polylist':
case 'triangles':
data.primitives.push( this.parseGeometryPrimitive( child ) );
break;
default:
}
}
this.library.geometries[ xml.getAttribute( 'id' ) ] = data;
}
parseSource( xml ) {
const data = {
array: [],
stride: 3
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'float_array':
data.array = parseFloats( child.textContent );
break;
case 'Name_array':
data.array = parseStrings( child.textContent );
break;
case 'technique_common':
const accessor = getElementsByTagName( child, 'accessor' )[ 0 ];
if ( accessor !== undefined ) {
data.stride = parseInt( accessor.getAttribute( 'stride' ) );
}
break;
}
}
return data;
}
parseGeometryVertices( xml ) {
const data = {};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
data[ child.getAttribute( 'semantic' ) ] = parseId( child.getAttribute( 'source' ) );
}
return data;
}
parseGeometryPrimitive( xml ) {
const primitive = {
type: xml.nodeName,
material: xml.getAttribute( 'material' ),
count: parseInt( xml.getAttribute( 'count' ) ),
inputs: {},
stride: 0,
hasUV: false
};
for ( let i = 0, l = xml.childNodes.length; i < l; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'input':
const id = parseId( child.getAttribute( 'source' ) );
const semantic = child.getAttribute( 'semantic' );
const offset = parseInt( child.getAttribute( 'offset' ) );
const set = parseInt( child.getAttribute( 'set' ) );
const inputname = ( set > 0 ? semantic + set : semantic );
primitive.inputs[ inputname ] = { id: id, offset: offset };
primitive.stride = Math.max( primitive.stride, offset + 1 );
if ( semantic === 'TEXCOORD' ) primitive.hasUV = true;
break;
case 'vcount':
primitive.vcount = parseInts( child.textContent );
break;
case 'p':
primitive.p = parseInts( child.textContent );
break;
}
}
return primitive;
}
// kinematics
parseKinematicsModel( xml ) {
const data = {
name: xml.getAttribute( 'name' ) || '',
joints: {},
links: []
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'technique_common':
this.parseKinematicsTechniqueCommon( child, data );
break;
}
}
this.library.kinematicsModels[ xml.getAttribute( 'id' ) ] = data;
}
parseKinematicsTechniqueCommon( xml, data ) {
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'joint':
data.joints[ child.getAttribute( 'sid' ) ] = this.parseKinematicsJoint( child );
break;
case 'link':
data.links.push( this.parseKinematicsLink( child ) );
break;
}
}
}
parseKinematicsJoint( xml ) {
let data;
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'prismatic':
case 'revolute':
data = this.parseKinematicsJointParameter( child );
break;
}
}
return data;
}
parseKinematicsJointParameter( xml ) {
const data = {
sid: xml.getAttribute( 'sid' ),
name: xml.getAttribute( 'name' ) || '',
axis: new Vector3(),
limits: {
min: 0,
max: 0
},
type: xml.nodeName,
static: false,
zeroPosition: 0,
middlePosition: 0
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'axis':
const array = parseFloats( child.textContent );
data.axis.fromArray( array );
break;
case 'limits':
const max = child.getElementsByTagName( 'max' )[ 0 ];
const min = child.getElementsByTagName( 'min' )[ 0 ];
data.limits.max = parseFloat( max.textContent );
data.limits.min = parseFloat( min.textContent );
break;
}
}
// if min is equal to or greater than max, consider the joint static
if ( data.limits.min >= data.limits.max ) {
data.static = true;
}
// calculate middle position
data.middlePosition = ( data.limits.min + data.limits.max ) / 2.0;
return data;
}
parseKinematicsLink( xml ) {
const data = {
sid: xml.getAttribute( 'sid' ),
name: xml.getAttribute( 'name' ) || '',
attachments: [],
transforms: []
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'attachment_full':
data.attachments.push( this.parseKinematicsAttachment( child ) );
break;
case 'matrix':
case 'translate':
case 'rotate':
data.transforms.push( this.parseKinematicsTransform( child ) );
break;
}
}
return data;
}
parseKinematicsAttachment( xml ) {
const data = {
joint: xml.getAttribute( 'joint' ).split( '/' ).pop(),
transforms: [],
links: []
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'link':
data.links.push( this.parseKinematicsLink( child ) );
break;
case 'matrix':
case 'translate':
case 'rotate':
data.transforms.push( this.parseKinematicsTransform( child ) );
break;
}
}
return data;
}
parseKinematicsTransform( xml ) {
const data = {
type: xml.nodeName
};
const array = parseFloats( xml.textContent );
switch ( data.type ) {
case 'matrix':
data.obj = new Matrix4();
data.obj.fromArray( array ).transpose();
break;
case 'translate':
data.obj = new Vector3();
data.obj.fromArray( array );
break;
case 'rotate':
data.obj = new Vector3();
data.obj.fromArray( array );
data.angle = MathUtils.degToRad( array[ 3 ] );
break;
}
return data;
}
// physics
parsePhysicsModel( xml ) {
const data = {
name: xml.getAttribute( 'name' ) || '',
rigidBodies: {}
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'rigid_body':
data.rigidBodies[ child.getAttribute( 'name' ) ] = {};
this.parsePhysicsRigidBody( child, data.rigidBodies[ child.getAttribute( 'name' ) ] );
break;
}
}
this.library.physicsModels[ xml.getAttribute( 'id' ) ] = data;
}
parsePhysicsRigidBody( xml, data ) {
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'technique_common':
this.parsePhysicsTechniqueCommon( child, data );
break;
}
}
}
parsePhysicsTechniqueCommon( xml, data ) {
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'inertia':
data.inertia = parseFloats( child.textContent );
break;
case 'mass':
data.mass = parseFloats( child.textContent )[ 0 ];
break;
}
}
}
// scene
parseKinematicsScene( xml ) {
const data = {
bindJointAxis: []
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'bind_joint_axis':
data.bindJointAxis.push( this.parseKinematicsBindJointAxis( child ) );
break;
}
}
this.library.kinematicsScenes[ parseId( xml.getAttribute( 'url' ) ) ] = data;
}
parseKinematicsBindJointAxis( xml ) {
const data = {
target: xml.getAttribute( 'target' ).split( '/' ).pop()
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
switch ( child.nodeName ) {
case 'axis':
const param = child.getElementsByTagName( 'param' )[ 0 ];
data.axis = param.textContent;
const tmpJointIndex = data.axis.split( 'inst_' ).pop().split( 'axis' )[ 0 ];
data.jointIndex = tmpJointIndex.substring( 0, tmpJointIndex.length - 1 );
break;
}
}
return data;
}
// nodes
prepareNodes( xml ) {
const elements = xml.getElementsByTagName( 'node' );
// ensure all node elements have id attributes
for ( let i = 0; i < elements.length; i ++ ) {
const element = elements[ i ];
if ( element.hasAttribute( 'id' ) === false ) {
element.setAttribute( 'id', this.generateId() );
}
}
}
parseNode( xml ) {
const matrix = new Matrix4();
const vector = new Vector3();
const data = {
name: xml.getAttribute( 'name' ) || '',
type: xml.getAttribute( 'type' ),
id: xml.getAttribute( 'id' ),
sid: xml.getAttribute( 'sid' ),
matrix: new Matrix4(),
nodes: [],
instanceCameras: [],
instanceControllers: [],
instanceLights: [],
instanceGeometries: [],
instanceNodes: [],
transforms: {},
transformData: {},
transformOrder: []
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
if ( child.nodeType !== 1 ) continue;
let array;
switch ( child.nodeName ) {
case 'node':
data.nodes.push( child.getAttribute( 'id' ) );
this.parseNode( child );
break;
case 'instance_camera':
data.instanceCameras.push( parseId( child.getAttribute( 'url' ) ) );
break;
case 'instance_controller':
data.instanceControllers.push( this.parseNodeInstance( child ) );
break;
case 'instance_light':
data.instanceLights.push( parseId( child.getAttribute( 'url' ) ) );
break;
case 'instance_geometry':
data.instanceGeometries.push( this.parseNodeInstance( child ) );
break;
case 'instance_node':
data.instanceNodes.push( parseId( child.getAttribute( 'url' ) ) );
break;
case 'matrix':
array = parseFloats( child.textContent );
data.matrix.multiply( matrix.fromArray( array ).transpose() );
{
const sid = child.getAttribute( 'sid' );
data.transforms[ sid ] = child.nodeName;
data.transformData[ sid ] = { type: 'matrix', array: array };
data.transformOrder.push( sid );
}
break;
case 'translate':
array = parseFloats( child.textContent );
vector.fromArray( array );
data.matrix.multiply( matrix.makeTranslation( vector.x, vector.y, vector.z ) );
{
const sid = child.getAttribute( 'sid' );
data.transforms[ sid ] = child.nodeName;
data.transformData[ sid ] = { type: 'translate', x: array[ 0 ], y: array[ 1 ], z: array[ 2 ] };
data.transformOrder.push( sid );
}
break;
case 'rotate':
array = parseFloats( child.textContent );
{
const angle = MathUtils.degToRad( array[ 3 ] );
data.matrix.multiply( matrix.makeRotationAxis( vector.fromArray( array ), angle ) );
const sid = child.getAttribute( 'sid' );
data.transforms[ sid ] = child.nodeName;
data.transformData[ sid ] = { type: 'rotate', axis: [ array[ 0 ], array[ 1 ], array[ 2 ] ], angle: array[ 3 ] };
data.transformOrder.push( sid );
}
break;
case 'scale':
array = parseFloats( child.textContent );
data.matrix.scale( vector.fromArray( array ) );
{
const sid = child.getAttribute( 'sid' );
data.transforms[ sid ] = child.nodeName;
data.transformData[ sid ] = { type: 'scale', x: array[ 0 ], y: array[ 1 ], z: array[ 2 ] };
data.transformOrder.push( sid );
}
break;
case 'extra':
break;
default:
}
}
if ( this.hasNode( data.id ) ) {
console.warn( 'THREE.ColladaLoader: There is already a node with ID %s. Exclude current node from further processing.', data.id );
} else {
this.library.nodes[ data.id ] = data;
}
return data;
}
parseNodeInstance( xml ) {
const data = {
id: parseId( xml.getAttribute( 'url' ) ),
materials: {},
skeletons: []
};
for ( let i = 0; i < xml.childNodes.length; i ++ ) {
const child = xml.childNodes[ i ];
switch ( child.nodeName ) {
case 'bind_material':
const instances = child.getElementsByTagName( 'instance_material' );
for ( let j = 0; j < instances.length; j ++ ) {
const instance = instances[ j ];
const symbol = instance.getAttribute( 'symbol' );
const target = instance.getAttribute( 'target' );
data.materials[ symbol ] = parseId( target );
}
break;
case 'skeleton':
data.skeletons.push( parseId( child.textContent ) );
break;
default:
break;
}
}
return data;
}
// visual scenes
parseVisualScene( xml ) {
const data = {
name: xml.getAttribute( 'name' ),
children: []
};
this.prepareNodes( xml );
const elements = getElementsByTagName( xml, 'node' );
for ( let i = 0; i < elements.length; i ++ ) {
data.children.push( this.parseNode( elements[ i ] ) );
}
this.library.visualScenes[ xml.getAttribute( 'id' ) ] = data;
}
hasNode( id ) {
return this.library.nodes[ id ] !== undefined;
}
}
export { ColladaParser, getElementsByTagName, parseStrings, parseFloats, parseInts, parseId };