Help with WRLLoader code

Hi all.

Recently I found a bug in the VMRLLoader available in threejs Github project. Due to the tricky way of program of this loader, I was unable to fix this issue.

The bug is related with the colors. Here more information about:

As WRL format (VMRL 2.0) is text tag-ordered based format, I tried to make my own convertor with nodejs.

The problem with my code is that it seems I am missing something multiplying matrices in cascade, but I cannot find my issue.

The idea of this conversor is to open a VRML file with hundreds|thousands of bufferGeometries and generate a simplified scene with only an object3d containing at maximum 3 kind of meshes: (Mesh, LineSegments, Points)

Could anybody have a look and help me to find the bug? I think it could be interesthing for people who will have to work with WRL format. (For eample people who needs to take models from CATIA)

Here you can see my code:

/*
 * 
 ******************************************************************************************************
 * PROGRAM: WRL_Loader_v2.4.js                                                                        *
 * TARGET: convert WRL file to json                                                                   *
 * AUTHOR   : Alejandro Insua Castro                                                                  *
 * DATE: 23/03/2019                                                                                   *
 * VERSION: 2.4                                                                                       *
 ******************************************************************************************************
 * LAST CHANGES:                                                                                      *
 *  2019/04/06: Program modifications                                                                 *
 *                                                                                                    *
 ******************************************************************************************************
 * ARGUMENTS : N/A                                                                                    *
 *                                                                                                    *
 ******************************************************************************************************
 * EXAMPLE OF USE:                                                                                    *
 *  set PATH=%HOME%\Documents\Viewer3d\node-v10.13.0-win-x64;%PATH%                                   *
 *  set IF=%HOME%\Documents\input.wrl                                     
 *  set OF=%HOME%\Documents\ouput.glb         
 *  node --max-old-space-size=32000 WRL_Loader_v2.4.js %IF% %OF%                                      *
 *                                                                                                    *
 *                                                                                                    * 
 ******************************************************************************************************
 * WHERE:                                                                                             *
 ******************************************************************************************************
 *  IF: Input folder where WRL are allocated                                                          *
 *  OF: Output File (GLB Draco compressed folder no simplification)                                   *
 ******************************************************************************************************
 *  
 */


console.log("START.....");
// ---------------------------------------------------------------------------------------------------------
// - BEFORE REQUIREMENTS:
// ---------------------------------------------------------------------------------------------------------
	var  start = Date.now();
	var  before = Date.now();

// ---------------------------------------------------------------------------------------------------------
// - REQUIREMENTS:
// ---------------------------------------------------------------------------------------------------------
	var  fs = require("fs")
	global.THREE = require('three');
	console.log("Time to load libraries took", Date.now() - before, "ms");

// ---------------------------------------------------------------------------------------------------------
// - ARGUMENTS:
// ---------------------------------------------------------------------------------------------------------
	var inFile = process.argv[2];
	var outFile = process.argv[3];

// ---------------------------------------------------------------------------------------------------------
// - VARIABLES:
// ---------------------------------------------------------------------------------------------------------
	var fileArray;

	console.log("Input file: " + inFile);
	console.log("Output file: " + outFile);

	var before = Date.now();
	
	
	// For control states:
	var startToGather = false;
	var inTransformLevelArray = [];
	var inScale = false;
	var inTranslation = false;
	var inRotation = false;
	var inChildrenLevelArray = [];
	var inGroupLevelArray = [];
	var inShape = false;
	var inAppearance = false;
	var inMaterial = false;
	var inGeometry = false;
	var inCoord = false;
	var inPoint = false;
	var inNormal = false;
	var inVector = false;
	var inCoordIndex = false;

	// Array for saving material, geometries and matrix
	var geometryArray = [];
	var materialArray = [];
	var matrixLevelArray = [];
	matrixLevelArray.push(new THREE.Matrix4());
	// For control depth level:
	var transformationLevel = 0;
	var groupLevel = 0;
	var level = 0;
	// Counters:
	var geometryCount = 0;
	
	//Current:
	var materialName;
	var geometryInfo;
	var scale = new THREE.Vector3(1,1,1);
	var quaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,0,0), 1);
	var translation = new THREE.Vector3(0,0,0);
	var matrix;

	var material = new THREE.MeshStandardMaterial();

	// Debug:
	var debug = true;
// ---------------------------------------------------------------------------------------------------------
// - START OF PROGRAM:
// ---------------------------------------------------------------------------------------------------------
	var scene = LoadVMRL(inFile);
	showLog(scene);
// ---------------------------------------------------------------------------------------------------------
// - END OF PROGRAM:
// ---------------------------------------------------------------------------------------------------------


	// FUNCTIONS : 
	function showLog(msg) {
		if (debug == true) {
			console.log(msg);
		}
	}

	

	function LoadVMRL(inFile) {
		var fileArray = fs.readFileSync(inFile, 'utf8').toString().split("\r\n");
		for (var i=0; i<fileArray.length; i++) {
			var lineText  = fileArray[i].trim().replace(/[,/[ ,[\t, ,\,]+/g, ' ').replace(/diffuseColor|emissiveColor/g, 'colorMaterial');
			var lineFields = lineText.split(" ");
			var firstField = lineFields[0];
			if (firstField == "Transform") {
				startToGather = true;
			}
			if (startToGather == true) {
				if (isNumber(firstField) == false) {
					showLog(firstField);
				}
				switch (firstField) {
					case "Transform": // {}
						transformationLevel += 1;
						inTransformLevelArray[transformationLevel] = true;
						scale = new THREE.Vector3(1,1,1);
						quaternion = new THREE.Quaternion();
						translation = new THREE.Vector3();		
						break;
					case "scale": // val val val
						scale = new THREE.Vector3(parseFloat(lineFields[1]), parseFloat(lineFields[2]), parseFloat(lineFields[3]));
						break;
					case "translation": // val val val
						translation = new THREE.Vector3(parseFloat(lineFields[1]), parseFloat(lineFields[2]), parseFloat(lineFields[3]));
						break;
					case "rotation": // val val val val
						quaternion = new THREE.Quaternion().setFromAxisAngle( new THREE.Vector3(parseFloat(lineFields[1]), parseFloat(lineFields[2]), parseFloat(lineFields[3])), parseFloat(lineFields[4]) );
						break;
					case "children": //  []
						level += 1;
						inChildrenLevelArray[level] = true;
						//showLog("level", level, "scale", scale, "quaternion", quaternion, "translation",  translation);
						if (inTransformLevelArray[transformationLevel] == true) {
							showLog("transformationLevel", transformationLevel, "scale", scale, "quaternion", quaternion, "translation",  translation);
							matrix = new THREE.Matrix4().compose(translation, quaternion, scale);
							showLog("matrixLevelArray:" , matrixLevelArray);
							showLog("matrix:" , matrix);    							
							var combinedMatrix = new THREE.Matrix4().identity();
							matrixLevelArray[transformationLevel] = combinedMatrix.multiplyMatrices( matrixLevelArray[transformationLevel-1], matrix);    							
						}
						
						break;
					case "Group": // {}
						groupLevel += 1;
						inGroupLevelArray[groupLevel] = true;
						break;
					case "DEF": // Same than Group
						groupLevel += 1;
						inGroupLevelArray[groupLevel] = true;
						 break;
					case "Shape": // {}
						inShape = true;
						geometryInfo = {
							name: undefined, // numero de mesh
							type: undefined, // PointSet, IndexedLineSet, IndexedFaceSet
							color: undefined, // color del material
							matrix: matrixLevelArray[transformationLevel],
							position: [], // arrayOf vertex need to compute normals
							index: [] // array of Index
						}
						 break;
					case "appearance": //{}
						inAppearance = true;
						 break;
					case "material": // {}
						inMaterial = true;
						materialName  = lineFields[2].replace('_', '');   						
						showLog("level: ", level, "; matrix: ", matrixLevelArray[level]);
						showLog("matrixLevelArray: ", matrixLevelArray);
						geometryInfo.name = materialName;    						
						// check if material is being defined or already exists:
						if (lineFields[1] == "DEF") {
							// Material being defined
							materialArray[materialName] = undefined;
						} else {
							geometryInfo.color = materialArray[materialName];
						}
						geometryCount += 1;
						break;
					case "colorMaterial":
						materialArray[materialName] = [parseFloat(lineFields[1]), parseFloat(lineFields[2]), parseFloat(lineFields[3])];
						geometryInfo.color = materialArray[materialName];
						break;
					case "}":
						if (inMaterial == true) {
							inMaterial = false;
							showLog("end of inMaterial");
						} else if (inAppearance == true) {
							inAppearance = false;
							showLog("end of inAppearance");
						} else if (inCoord == true ) {
							inCoord = false;
							showLog("end of inCoord");
						} else if (inNormal == true) {
							inNormal = false;
						} else if (inGeometry == true) {
							inGeometry = false;
							showLog("end of inGeometry");
						} else if (inShape == true) {
							inShape = false;
							showLog("end of inShape");
							createGeometry(geometryInfo);
						} else if (inGroupLevelArray[groupLevel] == true) {
							inGroupLevelArray[groupLevel] = false;
							showLog("end of inGroupLevelArray[" + groupLevel + "]");
							groupLevel -= 1;
						} else if (inTransformLevelArray[transformationLevel] == true) {
							inTransformLevelArray[transformationLevel] = false;
							showLog("end of inTransformLevelArray[" + transformationLevel + "]");
							inTransformLevelArray[transformationLevel] == false
							transformationLevel -= 1;
						}
						break;
					case "geometry":
						geometryInfo.type = lineFields[1];    						
						break;
					case "solid":
						break;
					case "coord":
						inCoord = true;
						break;
					case "point":
						inPoint = true;
						break;
					case "normal": // {}
						inNormal = true;
						break;
					case "vector": // []
						inVector = true;
						break;
					case "}coordIndex": // sometimes it can be found in this way
						inCoord = false;
						showLog("end of inCoord");
					case "coordIndex":
						inCoordIndex = true;
						if (lineFields.length > 1) {
							for (var k=1; k<lineFields.length; k++) {
								if (isNumber(lineFields[k]) == true ) {
									var value = parseInt(lineFields[k]);
									if (value != -1) {
										geometryInfo.index.push(value);
									}
								} else if (lineFields[k] == ']') {
									inCoordIndex = false;
								}
							}
						}
						break;
					case "]":
						if (inPoint == true) {
							inPoint = false;
							showLog("end of inPoint");
						} else if (inCoordIndex == true) {
							inCoordIndex = false;
						} else if (inVector == true) {
							inVector = false;
						} else if (inChildrenLevelArray[level] == true) {
							inChildrenLevelArray[level] = false;
							showLog("end of inChildrenLevelArray[" + level + "]");
							level -= 1;
						}
						break;
					default:
						// Any other data
						if (isNumber(firstField) == true) {
							// numeric data, find if we are in Point or coordIndex Section I always take the 3 first values parsed to Float
							if (inPoint == true){
								geometryInfo.position.push(parseFloat(lineFields[0]));
								geometryInfo.position.push(parseFloat(lineFields[1]));
								geometryInfo.position.push(parseFloat(lineFields[2]));
							} else if (inCoordIndex == true) {
								//showLog("inserting index", geometryInfo.type);
								for (var k=0; k<lineFields.length; k++) {
									if (isNumber(lineFields[k]) == true ) {
										var value = parseInt(lineFields[k]);
										if (value != -1) {
											geometryInfo.index.push(value);
										}
									} else if (lineFields[k] == ']') {
										inCoordIndex = false;
									}
								}
							}
						}
						break;
				}
			}
		}
		
		// types: PointSet, IndexedLineSet, IndexedFaceSet:
		var name = getFileName(outFile).replace('.glb', '');
		var scene2 = new THREE.Scene();
		scene2.name =  name;
		var object3d = new THREE.Object3D();
		object3d.name =  "O_" + name;
		scene2.add(object3d);
        
		if (geometryArray["PointSet"] != undefined) {
			var mergedPointGeos = mergeBufferGeometries(geometryArray["PointSet"]);
			var pointMesh = new THREE.Points(mergedPointGeos, material);
			pointMesh.name = "P_" + name;
			object3d.add(pointMesh)
		}
		if (geometryArray["IndexedLineSet"] != undefined) {
			var mergedLineGeos = mergeBufferGeometries(geometryArray["IndexedLineSet"]);
			var lineMesh = new THREE.LineSegments(mergedLineGeos, material);
			lineMesh.name = "L_" + name;
			object3d.add(lineMesh)
			//showLog(lineMesh);
		}
		if (geometryArray["IndexedFaceSet"] != undefined) {
			var mergedMeshGeos = mergeBufferGeometries(geometryArray["IndexedFaceSet"]);
			var faceMesh = new THREE.Mesh(mergedMeshGeos, material);
			faceMesh.name = "M_" + name;
			object3d.add(faceMesh)
		}
		return (scene2);
	}

	function getFileName(filePath) {
	   return filePath.split('\\').reverse()[0];
	}

	function getExtension(filePath) {
		var i = filePath.lastIndexOf('.');
		return (i < 0) ? '' : filePath.substr(i+1);
	}

	function getBaseName(filePath){
		var i = filePath.lastIndexOf('\\');
		var j = filePath.lastIndexOf('.');
		return (filePath.substr(i+1, j-i-1));
	}

	function mergeBufferGeometries(geometries) {
		var indexLength = 0,
			verticesLength = 0,
			attributesInfos = {},
			geometriesInfos = [],
			geometryInfo,
			referenceAttributesKeys = [],
			attributesKeys,
			countVertices,
			i,
			j,
			k;
		var vertex_index = [];
		var length_index = [];
		// read the geometries and attributes information, calculate indexLength and verticesLength
		if (geometries.length > 0 ){
			for (i = 0; i < geometries.length; i++) {
				length_index.push(indexLength);
				vertex_index.push(verticesLength);
				attributesKeys = Object.keys(geometries[i].attributes);
				if ( geometries[i].attributes[attributesKeys[0]] != undefined ) {
					geometryInfo = {
						indexed: geometries[i].index !== null,
						vertices: geometries[i].attributes[attributesKeys[0]].count
					};
					geometriesInfos.push(geometryInfo);
					if (geometryInfo.indexed) {
						indexLength += geometries[i].index.count;
					} else {
						indexLength += geometryInfo.vertices;
					}
					verticesLength += geometryInfo.vertices;
					for (j = 0; j < attributesKeys.length; j++) {
						if (referenceAttributesKeys.indexOf(attributesKeys[j]) === -1) {
							referenceAttributesKeys.push(attributesKeys[j]);
							attributesInfos[attributesKeys[j]] = {
								array: null,
								constructor: geometries[i].attributes[attributesKeys[j]].array.constructor,
								itemSize: geometries[i].attributes[attributesKeys[j]].itemSize
							};
						}
					}
				}
			}
			
			// prepare the new BufferGeometry and its attributes
			var newGeometry = new THREE.BufferGeometry();
			
			indexArray = verticesLength > 0xFFFF ? new Uint32Array(indexLength) : new Uint16Array(indexLength);
			for (i = 0; i < referenceAttributesKeys.length; i++) {
				attributesInfos[referenceAttributesKeys[i]].array = new (attributesInfos[referenceAttributesKeys[i]].constructor)(
					verticesLength * attributesInfos[referenceAttributesKeys[i]].itemSize
				);
				newGeometry.addAttribute(referenceAttributesKeys[i], new THREE.BufferAttribute(
					attributesInfos[referenceAttributesKeys[i]].array,
					attributesInfos[referenceAttributesKeys[i]].itemSize
				));
			}
			// copy all the data in the new BufferGeometry
			var offsetIndices = 0,
				offsetVertices = 0,
				offsetAttribute;
			for (i = 0; i < geometries.length; i++) {
				geometryInfo = geometriesInfos[i];
				if (geometryInfo != undefined) {
					if (geometryInfo.indexed) {
						for (j = 0; j < geometries[i].index.count; j++) {
							indexArray[offsetIndices + j] = offsetVertices + geometries[i].index.array[j];
						}
						offsetIndices += geometries[i].index.count;
					} else {
						for (j = 0; j < geometryInfo.vertices; j++) {
							indexArray[offsetIndices + j] = offsetVertices + j;
						}
						offsetIndices += geometryInfo.vertices;
					}
					for (j = 0; j < referenceAttributesKeys.length; j++) {
						offsetAttribute = offsetVertices * attributesInfos[referenceAttributesKeys[j]].itemSize;
						if (geometries[i].attributes[referenceAttributesKeys[j]]) {
							attributesInfos[referenceAttributesKeys[j]].array.set(geometries[i].attributes[referenceAttributesKeys[j]].array, offsetAttribute);
						}
					}
					offsetVertices += geometryInfo.vertices;
				}
			}
			newGeometry.setIndex(new THREE.BufferAttribute(indexArray, 1));
			for (i=0; i<geometries.length; i++){
				geometries[i].dispose();
			}
			geometries = [];
			return newGeometry;
		} else {
			return new THREE.BufferGeometry();
		}
	}

	function isNumber(str) {
		var out = true;
		if(isNaN(str)){
			out = false;
		}
		return out;
	}

	function createGeometry(geometryInfo) {
		showLog("geometryInfo:");
		showLog("----------------------------------");
		showLog(geometryInfo);
		showLog(matrixLevelArray);
		showLog("----------------------------------");
		var geo = new THREE.BufferGeometry();
		var positionTypedArray = new Float32Array(geometryInfo.position);
		var length = positionTypedArray.length; 
		var numberOfVertex = length / 3;
		var colorTypedArray = new Float32Array(length);
		var indexTypedArray = new Uint16Array(geometryInfo.index);
		var color =  geometryInfo.color;
		var type = geometryInfo.type;
		if (geometryArray[type] == undefined){
			geometryArray[type] = [];
		}
		switch (type) {
			case "IndexedFaceSet":
				geo.addAttribute( 'position', new THREE.Float32BufferAttribute( positionTypedArray, 3, false ));
				geo.addAttribute( 'color', new THREE.Float32BufferAttribute( positionTypedArray, 3, true ));
				geo.index = new THREE.Uint16BufferAttribute( indexTypedArray, 1, false );
				for (j=0; j<numberOfVertex; j++) {
					geo.attributes.color.array[j*3] = color[0];
					geo.attributes.color.array[j*3+1] = color[1];
					geo.attributes.color.array[j*3+2] = color[2];
				}
				if (geo != undefined) {
					geo.applyMatrix(geometryInfo.matrix);
					geo.computeBoundingBox();
					geo.computeBoundingSphere();
					geo.computeVertexNormals();
					geo.normalizeNormals();
					geometryArray[type].push(geo);
				}
				break;
			case "IndexedLineSet":
				geo.addAttribute( 'position', new THREE.Float32BufferAttribute( positionTypedArray, 3, false ));
				geo.addAttribute( 'color', new THREE.Float32BufferAttribute( positionTypedArray, 3, true ));
				geo.index = new THREE.Uint16BufferAttribute( indexTypedArray, 1, false );
				for (j=0; j<numberOfVertex; j++) {
					geo.attributes.color.array[j*3] = color[0];
					geo.attributes.color.array[j*3+1] = color[1];
					geo.attributes.color.array[j*3+2] = color[2];
				}
				if (geo != undefined) {
					geo.applyMatrix(geometryInfo.matrix);
					geo.computeBoundingBox();
					geo.computeBoundingSphere();
					geometryArray[type].push(geo);
				}
				break;
			case "PointSet":
				geo.addAttribute( 'position', new THREE.Float32BufferAttribute( positionTypedArray, 3, false ));
				geo.addAttribute( 'color', new THREE.Float32BufferAttribute( positionTypedArray, 3, true ));
				for (j=0; j<numberOfVertex; j++) {
					geo.attributes.color.array[j*3] = color[0];
					geo.attributes.color.array[j*3+1] = color[1];
					geo.attributes.color.array[j*3+2] = color[2];
				}
				if (geo != undefined) {
					geo.applyMatrix(geometryInfo.matrix);
					geo.computeBoundingBox();
					geo.computeBoundingSphere();
					geometryArray[type].push(geo);
				}
				break;
		}
	}

Best regards