three
Version:
JavaScript 3D library
2,160 lines (1,422 loc) • 74.6 kB
JavaScript
( function () {
class VRMLLoader extends THREE.Loader {
constructor( manager ) {
super( manager ); // dependency check
if ( typeof chevrotain === 'undefined' ) {
// eslint-disable-line no-undef
throw Error( 'THREE.VRMLLoader: External library chevrotain.min.js required.' );
}
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const path = scope.path === '' ? THREE.LoaderUtils.extractUrlBase( url ) : scope.path;
const loader = new THREE.FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text, path ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( data, path ) {
const nodeMap = {};
function generateVRMLTree( data ) {
// create lexer, parser and visitor
const tokenData = createTokens();
const lexer = new VRMLLexer( tokenData.tokens );
const parser = new VRMLParser( tokenData.tokenVocabulary );
const visitor = createVisitor( parser.getBaseCstVisitorConstructor() ); // lexing
const lexingResult = lexer.lex( data );
parser.input = lexingResult.tokens; // parsing
const cstOutput = parser.vrml();
if ( parser.errors.length > 0 ) {
console.error( parser.errors );
throw Error( 'THREE.VRMLLoader: Parsing errors detected.' );
} // actions
const ast = visitor.visit( cstOutput );
return ast;
}
function createTokens() {
const createToken = chevrotain.createToken; // eslint-disable-line no-undef
// from http://gun.teipir.gr/VRML-amgem/spec/part1/concepts.html#SyntaxBasics
const RouteIdentifier = createToken( {
name: 'RouteIdentifier',
pattern: /[^\x30-\x39\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d][^\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d]*[\.][^\x30-\x39\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d][^\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d]*/
} );
const Identifier = createToken( {
name: 'Identifier',
pattern: /[^\x30-\x39\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d][^\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d]*/,
longer_alt: RouteIdentifier
} ); // from http://gun.teipir.gr/VRML-amgem/spec/part1/nodesRef.html
const nodeTypes = [ 'Anchor', 'Billboard', 'Collision', 'Group', 'Transform', // grouping nodes
'Inline', 'LOD', 'Switch', // special groups
'AudioClip', 'DirectionalLight', 'PointLight', 'Script', 'Shape', 'Sound', 'SpotLight', 'WorldInfo', // common nodes
'CylinderSensor', 'PlaneSensor', 'ProximitySensor', 'SphereSensor', 'TimeSensor', 'TouchSensor', 'VisibilitySensor', // sensors
'Box', 'Cone', 'Cylinder', 'ElevationGrid', 'Extrusion', 'IndexedFaceSet', 'IndexedLineSet', 'PointSet', 'Sphere', // geometries
'Color', 'Coordinate', 'Normal', 'TextureCoordinate', // geometric properties
'Appearance', 'FontStyle', 'ImageTexture', 'Material', 'MovieTexture', 'PixelTexture', 'TextureTransform', // appearance
'ColorInterpolator', 'CoordinateInterpolator', 'NormalInterpolator', 'OrientationInterpolator', 'PositionInterpolator', 'ScalarInterpolator', // interpolators
'Background', 'Fog', 'NavigationInfo', 'Viewpoint', // bindable nodes
'Text' // Text must be placed at the end of the regex so there are no matches for TextureTransform and TextureCoordinate
]; //
const Version = createToken( {
name: 'Version',
pattern: /#VRML.*/,
longer_alt: Identifier
} );
const NodeName = createToken( {
name: 'NodeName',
pattern: new RegExp( nodeTypes.join( '|' ) ),
longer_alt: Identifier
} );
const DEF = createToken( {
name: 'DEF',
pattern: /DEF/,
longer_alt: Identifier
} );
const USE = createToken( {
name: 'USE',
pattern: /USE/,
longer_alt: Identifier
} );
const ROUTE = createToken( {
name: 'ROUTE',
pattern: /ROUTE/,
longer_alt: Identifier
} );
const TO = createToken( {
name: 'TO',
pattern: /TO/,
longer_alt: Identifier
} ); //
const StringLiteral = createToken( {
name: 'StringLiteral',
pattern: /"(?:[^\\"\n\r]|\\[bfnrtv"\\/]|\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])*"/
} );
const HexLiteral = createToken( {
name: 'HexLiteral',
pattern: /0[xX][0-9a-fA-F]+/
} );
const NumberLiteral = createToken( {
name: 'NumberLiteral',
pattern: /[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/
} );
const TrueLiteral = createToken( {
name: 'TrueLiteral',
pattern: /TRUE/
} );
const FalseLiteral = createToken( {
name: 'FalseLiteral',
pattern: /FALSE/
} );
const NullLiteral = createToken( {
name: 'NullLiteral',
pattern: /NULL/
} );
const LSquare = createToken( {
name: 'LSquare',
pattern: /\[/
} );
const RSquare = createToken( {
name: 'RSquare',
pattern: /]/
} );
const LCurly = createToken( {
name: 'LCurly',
pattern: /{/
} );
const RCurly = createToken( {
name: 'RCurly',
pattern: /}/
} );
const Comment = createToken( {
name: 'Comment',
pattern: /#.*/,
group: chevrotain.Lexer.SKIPPED // eslint-disable-line no-undef
} ); // commas, blanks, tabs, newlines and carriage returns are whitespace characters wherever they appear outside of string fields
const WhiteSpace = createToken( {
name: 'WhiteSpace',
pattern: /[ ,\s]/,
group: chevrotain.Lexer.SKIPPED // eslint-disable-line no-undef
} );
const tokens = [ WhiteSpace, // keywords appear before the Identifier
NodeName, DEF, USE, ROUTE, TO, TrueLiteral, FalseLiteral, NullLiteral, // the Identifier must appear after the keywords because all keywords are valid identifiers
Version, Identifier, RouteIdentifier, StringLiteral, HexLiteral, NumberLiteral, LSquare, RSquare, LCurly, RCurly, Comment ];
const tokenVocabulary = {};
for ( let i = 0, l = tokens.length; i < l; i ++ ) {
const token = tokens[ i ];
tokenVocabulary[ token.name ] = token;
}
return {
tokens: tokens,
tokenVocabulary: tokenVocabulary
};
}
function createVisitor( BaseVRMLVisitor ) {
// the visitor is created dynmaically based on the given base class
function VRMLToASTVisitor() {
BaseVRMLVisitor.call( this );
this.validateVisitor();
}
VRMLToASTVisitor.prototype = Object.assign( Object.create( BaseVRMLVisitor.prototype ), {
constructor: VRMLToASTVisitor,
vrml: function ( ctx ) {
const data = {
version: this.visit( ctx.version ),
nodes: [],
routes: []
};
for ( let i = 0, l = ctx.node.length; i < l; i ++ ) {
const node = ctx.node[ i ];
data.nodes.push( this.visit( node ) );
}
if ( ctx.route ) {
for ( let i = 0, l = ctx.route.length; i < l; i ++ ) {
const route = ctx.route[ i ];
data.routes.push( this.visit( route ) );
}
}
return data;
},
version: function ( ctx ) {
return ctx.Version[ 0 ].image;
},
node: function ( ctx ) {
const data = {
name: ctx.NodeName[ 0 ].image,
fields: []
};
if ( ctx.field ) {
for ( let i = 0, l = ctx.field.length; i < l; i ++ ) {
const field = ctx.field[ i ];
data.fields.push( this.visit( field ) );
}
} // DEF
if ( ctx.def ) {
data.DEF = this.visit( ctx.def[ 0 ] );
}
return data;
},
field: function ( ctx ) {
const data = {
name: ctx.Identifier[ 0 ].image,
type: null,
values: null
};
let result; // SFValue
if ( ctx.singleFieldValue ) {
result = this.visit( ctx.singleFieldValue[ 0 ] );
} // MFValue
if ( ctx.multiFieldValue ) {
result = this.visit( ctx.multiFieldValue[ 0 ] );
}
data.type = result.type;
data.values = result.values;
return data;
},
def: function ( ctx ) {
return ( ctx.Identifier || ctx.NodeName )[ 0 ].image;
},
use: function ( ctx ) {
return {
USE: ( ctx.Identifier || ctx.NodeName )[ 0 ].image
};
},
singleFieldValue: function ( ctx ) {
return processField( this, ctx );
},
multiFieldValue: function ( ctx ) {
return processField( this, ctx );
},
route: function ( ctx ) {
const data = {
FROM: ctx.RouteIdentifier[ 0 ].image,
TO: ctx.RouteIdentifier[ 1 ].image
};
return data;
}
} );
function processField( scope, ctx ) {
const field = {
type: null,
values: []
};
if ( ctx.node ) {
field.type = 'node';
for ( let i = 0, l = ctx.node.length; i < l; i ++ ) {
const node = ctx.node[ i ];
field.values.push( scope.visit( node ) );
}
}
if ( ctx.use ) {
field.type = 'use';
for ( let i = 0, l = ctx.use.length; i < l; i ++ ) {
const use = ctx.use[ i ];
field.values.push( scope.visit( use ) );
}
}
if ( ctx.StringLiteral ) {
field.type = 'string';
for ( let i = 0, l = ctx.StringLiteral.length; i < l; i ++ ) {
const stringLiteral = ctx.StringLiteral[ i ];
field.values.push( stringLiteral.image.replace( /'|"/g, '' ) );
}
}
if ( ctx.NumberLiteral ) {
field.type = 'number';
for ( let i = 0, l = ctx.NumberLiteral.length; i < l; i ++ ) {
const numberLiteral = ctx.NumberLiteral[ i ];
field.values.push( parseFloat( numberLiteral.image ) );
}
}
if ( ctx.HexLiteral ) {
field.type = 'hex';
for ( let i = 0, l = ctx.HexLiteral.length; i < l; i ++ ) {
const hexLiteral = ctx.HexLiteral[ i ];
field.values.push( hexLiteral.image );
}
}
if ( ctx.TrueLiteral ) {
field.type = 'boolean';
for ( let i = 0, l = ctx.TrueLiteral.length; i < l; i ++ ) {
const trueLiteral = ctx.TrueLiteral[ i ];
if ( trueLiteral.image === 'TRUE' ) field.values.push( true );
}
}
if ( ctx.FalseLiteral ) {
field.type = 'boolean';
for ( let i = 0, l = ctx.FalseLiteral.length; i < l; i ++ ) {
const falseLiteral = ctx.FalseLiteral[ i ];
if ( falseLiteral.image === 'FALSE' ) field.values.push( false );
}
}
if ( ctx.NullLiteral ) {
field.type = 'null';
ctx.NullLiteral.forEach( function () {
field.values.push( null );
} );
}
return field;
}
return new VRMLToASTVisitor();
}
function parseTree( tree ) {
// console.log( JSON.stringify( tree, null, 2 ) );
const nodes = tree.nodes;
const scene = new THREE.Scene(); // first iteration: build nodemap based on DEF statements
for ( let i = 0, l = nodes.length; i < l; i ++ ) {
const node = nodes[ i ];
buildNodeMap( node );
} // second iteration: build nodes
for ( let i = 0, l = nodes.length; i < l; i ++ ) {
const node = nodes[ i ];
const object = getNode( node );
if ( object instanceof THREE.Object3D ) scene.add( object );
if ( node.name === 'WorldInfo' ) scene.userData.worldInfo = object;
}
return scene;
}
function buildNodeMap( node ) {
if ( node.DEF ) {
nodeMap[ node.DEF ] = node;
}
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
if ( field.type === 'node' ) {
const fieldValues = field.values;
for ( let j = 0, jl = fieldValues.length; j < jl; j ++ ) {
buildNodeMap( fieldValues[ j ] );
}
}
}
}
function getNode( node ) {
// handle case where a node refers to a different one
if ( node.USE ) {
return resolveUSE( node.USE );
}
if ( node.build !== undefined ) return node.build;
node.build = buildNode( node );
return node.build;
} // node builder
function buildNode( node ) {
const nodeName = node.name;
let build;
switch ( nodeName ) {
case 'Group':
case 'Transform':
case 'Collision':
build = buildGroupingNode( node );
break;
case 'Background':
build = buildBackgroundNode( node );
break;
case 'Shape':
build = buildShapeNode( node );
break;
case 'Appearance':
build = buildAppearanceNode( node );
break;
case 'Material':
build = buildMaterialNode( node );
break;
case 'ImageTexture':
build = buildImageTextureNode( node );
break;
case 'PixelTexture':
build = buildPixelTextureNode( node );
break;
case 'TextureTransform':
build = buildTextureTransformNode( node );
break;
case 'IndexedFaceSet':
build = buildIndexedFaceSetNode( node );
break;
case 'IndexedLineSet':
build = buildIndexedLineSetNode( node );
break;
case 'PointSet':
build = buildPointSetNode( node );
break;
case 'Box':
build = buildBoxNode( node );
break;
case 'Cone':
build = buildConeNode( node );
break;
case 'Cylinder':
build = buildCylinderNode( node );
break;
case 'Sphere':
build = buildSphereNode( node );
break;
case 'ElevationGrid':
build = buildElevationGridNode( node );
break;
case 'Extrusion':
build = buildExtrusionNode( node );
break;
case 'Color':
case 'Coordinate':
case 'Normal':
case 'TextureCoordinate':
build = buildGeometricNode( node );
break;
case 'WorldInfo':
build = buildWorldInfoNode( node );
break;
case 'Anchor':
case 'Billboard':
case 'Inline':
case 'LOD':
case 'Switch':
case 'AudioClip':
case 'DirectionalLight':
case 'PointLight':
case 'Script':
case 'Sound':
case 'SpotLight':
case 'CylinderSensor':
case 'PlaneSensor':
case 'ProximitySensor':
case 'SphereSensor':
case 'TimeSensor':
case 'TouchSensor':
case 'VisibilitySensor':
case 'Text':
case 'FontStyle':
case 'MovieTexture':
case 'ColorInterpolator':
case 'CoordinateInterpolator':
case 'NormalInterpolator':
case 'OrientationInterpolator':
case 'PositionInterpolator':
case 'ScalarInterpolator':
case 'Fog':
case 'NavigationInfo':
case 'Viewpoint':
// node not supported yet
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown node:', nodeName );
break;
}
if ( build !== undefined && node.DEF !== undefined && build.hasOwnProperty( 'name' ) === true ) {
build.name = node.DEF;
}
return build;
}
function buildGroupingNode( node ) {
const object = new THREE.Group(); //
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'bboxCenter':
// field not supported
break;
case 'bboxSize':
// field not supported
break;
case 'center':
// field not supported
break;
case 'children':
parseFieldChildren( fieldValues, object );
break;
case 'collide':
// field not supported
break;
case 'rotation':
const axis = new THREE.Vector3( fieldValues[ 0 ], fieldValues[ 1 ], fieldValues[ 2 ] );
const angle = fieldValues[ 3 ];
object.quaternion.setFromAxisAngle( axis, angle );
break;
case 'scale':
object.scale.set( fieldValues[ 0 ], fieldValues[ 1 ], fieldValues[ 2 ] );
break;
case 'scaleOrientation':
// field not supported
break;
case 'translation':
object.position.set( fieldValues[ 0 ], fieldValues[ 1 ], fieldValues[ 2 ] );
break;
case 'proxy':
// field not supported
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
return object;
}
function buildBackgroundNode( node ) {
const group = new THREE.Group();
let groundAngle, groundColor;
let skyAngle, skyColor;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'groundAngle':
groundAngle = fieldValues;
break;
case 'groundColor':
groundColor = fieldValues;
break;
case 'backUrl':
// field not supported
break;
case 'bottomUrl':
// field not supported
break;
case 'frontUrl':
// field not supported
break;
case 'leftUrl':
// field not supported
break;
case 'rightUrl':
// field not supported
break;
case 'topUrl':
// field not supported
break;
case 'skyAngle':
skyAngle = fieldValues;
break;
case 'skyColor':
skyColor = fieldValues;
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
const radius = 10000; // sky
if ( skyColor ) {
const skyGeometry = new THREE.SphereGeometry( radius, 32, 16 );
const skyMaterial = new THREE.MeshBasicMaterial( {
fog: false,
side: THREE.BackSide,
depthWrite: false,
depthTest: false
} );
if ( skyColor.length > 3 ) {
paintFaces( skyGeometry, radius, skyAngle, toColorArray( skyColor ), true );
skyMaterial.vertexColors = true;
} else {
skyMaterial.color.setRGB( skyColor[ 0 ], skyColor[ 1 ], skyColor[ 2 ] );
}
const sky = new THREE.Mesh( skyGeometry, skyMaterial );
group.add( sky );
} // ground
if ( groundColor ) {
if ( groundColor.length > 0 ) {
const groundGeometry = new THREE.SphereGeometry( radius, 32, 16, 0, 2 * Math.PI, 0.5 * Math.PI, 1.5 * Math.PI );
const groundMaterial = new THREE.MeshBasicMaterial( {
fog: false,
side: THREE.BackSide,
vertexColors: true,
depthWrite: false,
depthTest: false
} );
paintFaces( groundGeometry, radius, groundAngle, toColorArray( groundColor ), false );
const ground = new THREE.Mesh( groundGeometry, groundMaterial );
group.add( ground );
}
} // render background group first
group.renderOrder = - Infinity;
return group;
}
function buildShapeNode( node ) {
const fields = node.fields; // if the appearance field is NULL or unspecified, lighting is off and the unlit object color is (0, 0, 0)
let material = new THREE.MeshBasicMaterial( {
color: 0x000000
} );
let geometry;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'appearance':
if ( fieldValues[ 0 ] !== null ) {
material = getNode( fieldValues[ 0 ] );
}
break;
case 'geometry':
if ( fieldValues[ 0 ] !== null ) {
geometry = getNode( fieldValues[ 0 ] );
}
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
} // build 3D object
let object;
if ( geometry && geometry.attributes.position ) {
const type = geometry._type;
if ( type === 'points' ) {
// points
const pointsMaterial = new THREE.PointsMaterial( {
color: 0xffffff
} );
if ( geometry.attributes.color !== undefined ) {
pointsMaterial.vertexColors = true;
} else {
// if the color field is NULL and there is a material defined for the appearance affecting this PointSet, then use the emissiveColor of the material to draw the points
if ( material.isMeshPhongMaterial ) {
pointsMaterial.color.copy( material.emissive );
}
}
object = new THREE.Points( geometry, pointsMaterial );
} else if ( type === 'line' ) {
// lines
const lineMaterial = new THREE.LineBasicMaterial( {
color: 0xffffff
} );
if ( geometry.attributes.color !== undefined ) {
lineMaterial.vertexColors = true;
} else {
// if the color field is NULL and there is a material defined for the appearance affecting this IndexedLineSet, then use the emissiveColor of the material to draw the lines
if ( material.isMeshPhongMaterial ) {
lineMaterial.color.copy( material.emissive );
}
}
object = new THREE.LineSegments( geometry, lineMaterial );
} else {
// consider meshes
// check "solid" hint (it's placed in the geometry but affects the material)
if ( geometry._solid !== undefined ) {
material.side = geometry._solid ? THREE.FrontSide : THREE.DoubleSide;
} // check for vertex colors
if ( geometry.attributes.color !== undefined ) {
material.vertexColors = true;
}
object = new THREE.Mesh( geometry, material );
}
} else {
object = new THREE.Object3D(); // if the geometry field is NULL or no vertices are defined the object is not drawn
object.visible = false;
}
return object;
}
function buildAppearanceNode( node ) {
let material = new THREE.MeshPhongMaterial();
let transformData;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'material':
if ( fieldValues[ 0 ] !== null ) {
const materialData = getNode( fieldValues[ 0 ] );
if ( materialData.diffuseColor ) material.color.copy( materialData.diffuseColor );
if ( materialData.emissiveColor ) material.emissive.copy( materialData.emissiveColor );
if ( materialData.shininess ) material.shininess = materialData.shininess;
if ( materialData.specularColor ) material.specular.copy( materialData.specularColor );
if ( materialData.transparency ) material.opacity = 1 - materialData.transparency;
if ( materialData.transparency > 0 ) material.transparent = true;
} else {
// if the material field is NULL or unspecified, lighting is off and the unlit object color is (0, 0, 0)
material = new THREE.MeshBasicMaterial( {
color: 0x000000
} );
}
break;
case 'texture':
const textureNode = fieldValues[ 0 ];
if ( textureNode !== null ) {
if ( textureNode.name === 'ImageTexture' || textureNode.name === 'PixelTexture' ) {
material.map = getNode( textureNode );
} else { // MovieTexture not supported yet
}
}
break;
case 'textureTransform':
if ( fieldValues[ 0 ] !== null ) {
transformData = getNode( fieldValues[ 0 ] );
}
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
} // only apply texture transform data if a texture was defined
if ( material.map ) {
// respect VRML lighting model
if ( material.map.__type ) {
switch ( material.map.__type ) {
case TEXTURE_TYPE.INTENSITY_ALPHA:
material.opacity = 1; // ignore transparency
break;
case TEXTURE_TYPE.RGB:
material.color.set( 0xffffff ); // ignore material color
break;
case TEXTURE_TYPE.RGBA:
material.color.set( 0xffffff ); // ignore material color
material.opacity = 1; // ignore transparency
break;
default:
}
delete material.map.__type;
} // apply texture transform
if ( transformData ) {
material.map.center.copy( transformData.center );
material.map.rotation = transformData.rotation;
material.map.repeat.copy( transformData.scale );
material.map.offset.copy( transformData.translation );
}
}
return material;
}
function buildMaterialNode( node ) {
const materialData = {};
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'ambientIntensity':
// field not supported
break;
case 'diffuseColor':
materialData.diffuseColor = new THREE.Color( fieldValues[ 0 ], fieldValues[ 1 ], fieldValues[ 2 ] );
break;
case 'emissiveColor':
materialData.emissiveColor = new THREE.Color( fieldValues[ 0 ], fieldValues[ 1 ], fieldValues[ 2 ] );
break;
case 'shininess':
materialData.shininess = fieldValues[ 0 ];
break;
case 'specularColor':
materialData.emissiveColor = new THREE.Color( fieldValues[ 0 ], fieldValues[ 1 ], fieldValues[ 2 ] );
break;
case 'transparency':
materialData.transparency = fieldValues[ 0 ];
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
return materialData;
}
function parseHexColor( hex, textureType, color ) {
let value;
switch ( textureType ) {
case TEXTURE_TYPE.INTENSITY:
// Intensity texture: A one-component image specifies one-byte hexadecimal or integer values representing the intensity of the image
value = parseInt( hex );
color.r = value;
color.g = value;
color.b = value;
color.a = 1;
break;
case TEXTURE_TYPE.INTENSITY_ALPHA:
// Intensity+Alpha texture: A two-component image specifies the intensity in the first (high) byte and the alpha opacity in the second (low) byte.
value = parseInt( '0x' + hex.substring( 2, 4 ) );
color.r = value;
color.g = value;
color.b = value;
color.a = parseInt( '0x' + hex.substring( 4, 6 ) );
break;
case TEXTURE_TYPE.RGB:
// RGB texture: Pixels in a three-component image specify the red component in the first (high) byte, followed by the green and blue components
color.r = parseInt( '0x' + hex.substring( 2, 4 ) );
color.g = parseInt( '0x' + hex.substring( 4, 6 ) );
color.b = parseInt( '0x' + hex.substring( 6, 8 ) );
color.a = 1;
break;
case TEXTURE_TYPE.RGBA:
// RGBA texture: Four-component images specify the alpha opacity byte after red/green/blue
color.r = parseInt( '0x' + hex.substring( 2, 4 ) );
color.g = parseInt( '0x' + hex.substring( 4, 6 ) );
color.b = parseInt( '0x' + hex.substring( 6, 8 ) );
color.a = parseInt( '0x' + hex.substring( 8, 10 ) );
break;
default:
}
}
function getTextureType( num_components ) {
let type;
switch ( num_components ) {
case 1:
type = TEXTURE_TYPE.INTENSITY;
break;
case 2:
type = TEXTURE_TYPE.INTENSITY_ALPHA;
break;
case 3:
type = TEXTURE_TYPE.RGB;
break;
case 4:
type = TEXTURE_TYPE.RGBA;
break;
default:
}
return type;
}
function buildPixelTextureNode( node ) {
let texture;
let wrapS = THREE.RepeatWrapping;
let wrapT = THREE.RepeatWrapping;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'image':
const width = fieldValues[ 0 ];
const height = fieldValues[ 1 ];
const num_components = fieldValues[ 2 ];
const textureType = getTextureType( num_components );
const data = new Uint8Array( 4 * width * height );
const color = {
r: 0,
g: 0,
b: 0,
a: 0
};
for ( let j = 3, k = 0, jl = fieldValues.length; j < jl; j ++, k ++ ) {
parseHexColor( fieldValues[ j ], textureType, color );
const stride = k * 4;
data[ stride + 0 ] = color.r;
data[ stride + 1 ] = color.g;
data[ stride + 2 ] = color.b;
data[ stride + 3 ] = color.a;
}
texture = new THREE.DataTexture( data, width, height );
texture.needsUpdate = true;
texture.__type = textureType; // needed for material modifications
break;
case 'repeatS':
if ( fieldValues[ 0 ] === false ) wrapS = THREE.ClampToEdgeWrapping;
break;
case 'repeatT':
if ( fieldValues[ 0 ] === false ) wrapT = THREE.ClampToEdgeWrapping;
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
if ( texture ) {
texture.wrapS = wrapS;
texture.wrapT = wrapT;
}
return texture;
}
function buildImageTextureNode( node ) {
let texture;
let wrapS = THREE.RepeatWrapping;
let wrapT = THREE.RepeatWrapping;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'url':
const url = fieldValues[ 0 ];
if ( url ) texture = textureLoader.load( url );
break;
case 'repeatS':
if ( fieldValues[ 0 ] === false ) wrapS = THREE.ClampToEdgeWrapping;
break;
case 'repeatT':
if ( fieldValues[ 0 ] === false ) wrapT = THREE.ClampToEdgeWrapping;
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
if ( texture ) {
texture.wrapS = wrapS;
texture.wrapT = wrapT;
}
return texture;
}
function buildTextureTransformNode( node ) {
const transformData = {
center: new THREE.Vector2(),
rotation: new THREE.Vector2(),
scale: new THREE.Vector2(),
translation: new THREE.Vector2()
};
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'center':
transformData.center.set( fieldValues[ 0 ], fieldValues[ 1 ] );
break;
case 'rotation':
transformData.rotation = fieldValues[ 0 ];
break;
case 'scale':
transformData.scale.set( fieldValues[ 0 ], fieldValues[ 1 ] );
break;
case 'translation':
transformData.translation.set( fieldValues[ 0 ], fieldValues[ 1 ] );
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
return transformData;
}
function buildGeometricNode( node ) {
return node.fields[ 0 ].values;
}
function buildWorldInfoNode( node ) {
const worldInfo = {};
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'title':
worldInfo.title = fieldValues[ 0 ];
break;
case 'info':
worldInfo.info = fieldValues;
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
return worldInfo;
}
function buildIndexedFaceSetNode( node ) {
let color, coord, normal, texCoord;
let ccw = true,
solid = true,
creaseAngle = 0;
let colorIndex, coordIndex, normalIndex, texCoordIndex;
let colorPerVertex = true,
normalPerVertex = true;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'color':
const colorNode = fieldValues[ 0 ];
if ( colorNode !== null ) {
color = getNode( colorNode );
}
break;
case 'coord':
const coordNode = fieldValues[ 0 ];
if ( coordNode !== null ) {
coord = getNode( coordNode );
}
break;
case 'normal':
const normalNode = fieldValues[ 0 ];
if ( normalNode !== null ) {
normal = getNode( normalNode );
}
break;
case 'texCoord':
const texCoordNode = fieldValues[ 0 ];
if ( texCoordNode !== null ) {
texCoord = getNode( texCoordNode );
}
break;
case 'ccw':
ccw = fieldValues[ 0 ];
break;
case 'colorIndex':
colorIndex = fieldValues;
break;
case 'colorPerVertex':
colorPerVertex = fieldValues[ 0 ];
break;
case 'convex':
// field not supported
break;
case 'coordIndex':
coordIndex = fieldValues;
break;
case 'creaseAngle':
creaseAngle = fieldValues[ 0 ];
break;
case 'normalIndex':
normalIndex = fieldValues;
break;
case 'normalPerVertex':
normalPerVertex = fieldValues[ 0 ];
break;
case 'solid':
solid = fieldValues[ 0 ];
break;
case 'texCoordIndex':
texCoordIndex = fieldValues;
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
if ( coordIndex === undefined ) {
console.warn( 'THREE.VRMLLoader: Missing coordIndex.' );
return new THREE.BufferGeometry(); // handle VRML files with incomplete geometry definition
}
const triangulatedCoordIndex = triangulateFaceIndex( coordIndex, ccw );
let colorAttribute;
let normalAttribute;
let uvAttribute;
if ( color ) {
if ( colorPerVertex === true ) {
if ( colorIndex && colorIndex.length > 0 ) {
// if the colorIndex field is not empty, then it is used to choose colors for each vertex of the IndexedFaceSet.
const triangulatedColorIndex = triangulateFaceIndex( colorIndex, ccw );
colorAttribute = computeAttributeFromIndexedData( triangulatedCoordIndex, triangulatedColorIndex, color, 3 );
} else {
// if the colorIndex field is empty, then the coordIndex field is used to choose colors from the THREE.Color node
colorAttribute = toNonIndexedAttribute( triangulatedCoordIndex, new THREE.Float32BufferAttribute( color, 3 ) );
}
} else {
if ( colorIndex && colorIndex.length > 0 ) {
// if the colorIndex field is not empty, then they are used to choose one color for each face of the IndexedFaceSet
const flattenFaceColors = flattenData( color, colorIndex );
const triangulatedFaceColors = triangulateFaceData( flattenFaceColors, coordIndex );
colorAttribute = computeAttributeFromFaceData( triangulatedCoordIndex, triangulatedFaceColors );
} else {
// if the colorIndex field is empty, then the color are applied to each face of the IndexedFaceSet in order
const triangulatedFaceColors = triangulateFaceData( color, coordIndex );
colorAttribute = computeAttributeFromFaceData( triangulatedCoordIndex, triangulatedFaceColors );
}
}
}
if ( normal ) {
if ( normalPerVertex === true ) {
// consider vertex normals
if ( normalIndex && normalIndex.length > 0 ) {
// if the normalIndex field is not empty, then it is used to choose normals for each vertex of the IndexedFaceSet.
const triangulatedNormalIndex = triangulateFaceIndex( normalIndex, ccw );
normalAttribute = computeAttributeFromIndexedData( triangulatedCoordIndex, triangulatedNormalIndex, normal, 3 );
} else {
// if the normalIndex field is empty, then the coordIndex field is used to choose normals from the Normal node
normalAttribute = toNonIndexedAttribute( triangulatedCoordIndex, new THREE.Float32BufferAttribute( normal, 3 ) );
}
} else {
// consider face normals
if ( normalIndex && normalIndex.length > 0 ) {
// if the normalIndex field is not empty, then they are used to choose one normal for each face of the IndexedFaceSet
const flattenFaceNormals = flattenData( normal, normalIndex );
const triangulatedFaceNormals = triangulateFaceData( flattenFaceNormals, coordIndex );
normalAttribute = computeAttributeFromFaceData( triangulatedCoordIndex, triangulatedFaceNormals );
} else {
// if the normalIndex field is empty, then the normals are applied to each face of the IndexedFaceSet in order
const triangulatedFaceNormals = triangulateFaceData( normal, coordIndex );
normalAttribute = computeAttributeFromFaceData( triangulatedCoordIndex, triangulatedFaceNormals );
}
}
} else {
// if the normal field is NULL, then the loader should automatically generate normals, using creaseAngle to determine if and how normals are smoothed across shared vertices
normalAttribute = computeNormalAttribute( triangulatedCoordIndex, coord, creaseAngle );
}
if ( texCoord ) {
// texture coordinates are always defined on vertex level
if ( texCoordIndex && texCoordIndex.length > 0 ) {
// if the texCoordIndex field is not empty, then it is used to choose texture coordinates for each vertex of the IndexedFaceSet.
const triangulatedTexCoordIndex = triangulateFaceIndex( texCoordIndex, ccw );
uvAttribute = computeAttributeFromIndexedData( triangulatedCoordIndex, triangulatedTexCoordIndex, texCoord, 2 );
} else {
// if the texCoordIndex field is empty, then the coordIndex array is used to choose texture coordinates from the TextureCoordinate node
uvAttribute = toNonIndexedAttribute( triangulatedCoordIndex, new THREE.Float32BufferAttribute( texCoord, 2 ) );
}
}
const geometry = new THREE.BufferGeometry();
const positionAttribute = toNonIndexedAttribute( triangulatedCoordIndex, new THREE.Float32BufferAttribute( coord, 3 ) );
geometry.setAttribute( 'position', positionAttribute );
geometry.setAttribute( 'normal', normalAttribute ); // optional attributes
if ( colorAttribute ) geometry.setAttribute( 'color', colorAttribute );
if ( uvAttribute ) geometry.setAttribute( 'uv', uvAttribute ); // "solid" influences the material so let's store it for later use
geometry._solid = solid;
geometry._type = 'mesh';
return geometry;
}
function buildIndexedLineSetNode( node ) {
let color, coord;
let colorIndex, coordIndex;
let colorPerVertex = true;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'color':
const colorNode = fieldValues[ 0 ];
if ( colorNode !== null ) {
color = getNode( colorNode );
}
break;
case 'coord':
const coordNode = fieldValues[ 0 ];
if ( coordNode !== null ) {
coord = getNode( coordNode );
}
break;
case 'colorIndex':
colorIndex = fieldValues;
break;
case 'colorPerVertex':
colorPerVertex = fieldValues[ 0 ];
break;
case 'coordIndex':
coordIndex = fieldValues;
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
} // build lines
let colorAttribute;
const expandedLineIndex = expandLineIndex( coordIndex ); // create an index for three.js's linesegment primitive
if ( color ) {
if ( colorPerVertex === true ) {
if ( colorIndex.length > 0 ) {
// if the colorIndex field is not empty, then one color is used for each polyline of the IndexedLineSet.
const expandedColorIndex = expandLineIndex( colorIndex ); // compute colors for each line segment (rendering primitve)
colorAttribute = computeAttributeFromIndexedData( expandedLineIndex, expandedColorIndex, color, 3 ); // compute data on vertex level
} else {
// if the colorIndex field is empty, then the colors are applied to each polyline of the IndexedLineSet in order.
colorAttribute = toNonIndexedAttribute( expandedLineIndex, new THREE.Float32BufferAttribute( color, 3 ) );
}
} else {
if ( colorIndex.length > 0 ) {
// if the colorIndex field is not empty, then colors are applied to each vertex of the IndexedLineSet
const flattenLineColors = flattenData( color, colorIndex ); // compute colors for each VRML primitve
const expandedLineColors = expandLineData( flattenLineColors, coordIndex ); // compute colors for each line segment (rendering primitve)
colorAttribute = computeAttributeFromLineData( expandedLineIndex, expandedLineColors ); // compute data on vertex level
} else {
// if the colorIndex field is empty, then the coordIndex field is used to choose colors from the THREE.Color node
const expandedLineColors = expandLineData( color, coordIndex ); // compute colors for each line segment (rendering primitve)
colorAttribute = computeAttributeFromLineData( expandedLineIndex, expandedLineColors ); // compute data on vertex level
}
}
} //
const geometry = new THREE.BufferGeometry();
const positionAttribute = toNonIndexedAttribute( expandedLineIndex, new THREE.Float32BufferAttribute( coord, 3 ) );
geometry.setAttribute( 'position', positionAttribute );
if ( colorAttribute ) geometry.setAttribute( 'color', colorAttribute );
geometry._type = 'line';
return geometry;
}
function buildPointSetNode( node ) {
let color, coord;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'color':
const colorNode = fieldValues[ 0 ];
if ( colorNode !== null ) {
color = getNode( colorNode );
}
break;
case 'coord':
const coordNode = fieldValues[ 0 ];
if ( coordNode !== null ) {
coord = getNode( coordNode );
}
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( coord, 3 ) );
if ( color ) geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( color, 3 ) );
geometry._type = 'points';
return geometry;
}
function buildBoxNode( node ) {
const size = new THREE.Vector3( 2, 2, 2 );
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'size':
size.x = fieldValues[ 0 ];
size.y = fieldValues[ 1 ];
size.z = fieldValues[ 2 ];
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
const geometry = new THREE.BoxGeometry( size.x, size.y, size.z );
return geometry;
}
function buildConeNode( node ) {
let radius = 1,
height = 2,
openEnded = false;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'bottom':
openEnded = ! fieldValues[ 0 ];
break;
case 'bottomRadius':
radius = fieldValues[ 0 ];
break;
case 'height':
height = fieldValues[ 0 ];
break;
case 'side':
// field not supported
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
const geometry = new THREE.ConeGeometry( radius, height, 16, 1, openEnded );
return geometry;
}
function buildCylinderNode( node ) {
let radius = 1,
height = 2;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'bottom':
// field not supported
break;
case 'radius':
radius = fieldValues[ 0 ];
break;
case 'height':
height = fieldValues[ 0 ];
break;
case 'side':
// field not supported
break;
case 'top':
// field not supported
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
const geometry = new THREE.CylinderGeometry( radius, radius, height, 16, 1 );
return geometry;
}
function buildSphereNode( node ) {
let radius = 1;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'radius':
radius = fieldValues[ 0 ];
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
}
const geometry = new THREE.SphereGeometry( radius, 16, 16 );
return geometry;
}
function buildElevationGridNode( node ) {
let color;
let normal;
let texCoord;
let height;
let colorPerVertex = true;
let normalPerVertex = true;
let solid = true;
let ccw = true;
let creaseAngle = 0;
let xDimension = 2;
let zDimension = 2;
let xSpacing = 1;
let zSpacing = 1;
const fields = node.fields;
for ( let i = 0, l = fields.length; i < l; i ++ ) {
const field = fields[ i ];
const fieldName = field.name;
const fieldValues = field.values;
switch ( fieldName ) {
case 'color':
const colorNode = fieldValues[ 0 ];
if ( colorNode !== null ) {
color = getNode( colorNode );
}
break;
case 'normal':
const normalNode = fieldValues[ 0 ];
if ( normalNode !== null ) {
normal = getNode( normalNode );
}
break;
case 'texCoord':
const texCoordNode = fieldValues[ 0 ];
if ( texCoordNode !== null ) {
texCoord = getNode( texCoordNode );
}
break;
case 'height':
height = fieldValues;
break;
case 'ccw':
ccw = fieldValues[ 0 ];
break;
case 'colorPerVertex':
colorPerVertex = fieldValues[ 0 ];
break;
case 'creaseAngle':
creaseAngle = fieldValues[ 0 ];
break;
case 'normalPerVertex':
normalPerVertex = fieldValues[ 0 ];
break;
case 'solid':
solid = fieldValues[ 0 ];
break;
case 'xDimension':
xDimension = fieldValues[ 0 ];
break;
case 'xSpacing':
xSpacing = fieldValues[ 0 ];
break;
case 'zDimension':
zDimension = fieldValues[ 0 ];
break;
case 'zSpacing':
zSpacing = fieldValues[ 0 ];
break;
default:
console.warn( 'THREE.VRMLLoader: Unknown field:', fieldName );
break;
}
} // vertex data
const vertices = [];
const normals = [];
const colors = [];
const uvs = [];
for ( let i = 0; i < zDimension; i ++ ) {
for ( let j = 0; j < xDimension; j ++ ) {
// compute a row major index
const index = i * xDimension + j; // vertices
const x = xSpacing * i;
const y = height[ index ];
const z = zSpacing * j;
vertices.push( x, y, z ); // colors
if ( color && colorPerVertex === true ) {
const r = color[ index * 3 + 0 ];
const g = color[ index * 3 + 1 ];
const b = color[ index * 3 + 2 ];
colors.push( r, g, b );
} // normals
if ( normal && normalPerVertex === true ) {
const xn = normal[ index * 3 + 0 ];
const yn = normal[ index * 3 + 1 ];
const zn = normal[ index * 3 + 2 ];
normals.push( xn, yn, zn );
} // uvs
if ( texCoord ) {
const s = texCoord[ index * 2 + 0 ];
const t = texCoord[ index * 2 + 1 ];
uvs.push( s, t );
} else {
uvs.push( i / ( xDimension - 1 ), j / ( zDimension - 1 ) );
}
}
} // indices
const indices = [];
for ( let i = 0; i < xDimension - 1; i ++ ) {
for ( let j = 0; j < zDimension - 1; j ++ ) {
// from https://tecfa.unige.ch/guides/vrml/vrml97/spec/part1/nodesRef.html#ElevationGrid
const a = i