(Unity) vertex color importer for MeshLab's OBJ files
This toy code worked fine in the older Unity version but stopped working correctly in the newer ones. Something about the vertex data order. I suggest using different format - FBX for example.
/// src* https://gist.github.com/andrew-raphael-lukasik/3559728d022a4c96f491924f8285e1bf | |
/// | |
/// Copyright (C) 2023 Andrzej Rafał Łukasik (also known as: Andrew Raphael Lukasik) | |
/// | |
/// This program is free software: you can redistribute it and/or modify | |
/// it under the terms of the GNU General Public License as published by | |
/// the Free Software Foundation, version 3 of the License. | |
/// | |
/// This program is distributed in the hope that it will be useful, | |
/// but WITHOUT ANY WARRANTY; without even the implied warranty of | |
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
/// See the GNU General Public License for details https://www.gnu.org/licenses/ | |
/// | |
#if UNITY_EDITOR | |
using System.Collections.Generic; | |
using UnityEditor; | |
using UnityEngine; | |
using IO = System.IO; | |
using NumberStyles = System.Globalization.NumberStyles; | |
using CultureInfo = System.Globalization.CultureInfo; | |
public class ObjFileFormatVertexColorImporter : AssetPostprocessor | |
{ | |
static Dictionary<string,List<Color>> _rawColors; | |
static List<int> _rawIndices; | |
void OnPreprocessModel () | |
{ | |
if( !assetPath.EndsWith(".obj") ) return; | |
var stopwatch = System.Diagnostics.Stopwatch.StartNew(); | |
string path = Application.dataPath + assetPath.Replace("Assets",""); | |
var fs = new IO.FileStream(path, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.ReadWrite); | |
var reader = new IO.StreamReader(fs); | |
ReadColorData(reader); | |
reader.Dispose(); | |
fs.Dispose(); | |
double totalSeconds = new System.TimeSpan(stopwatch.ElapsedTicks).TotalSeconds; | |
if( totalSeconds>0.1 ) Debug.LogWarning($"{GetType().Name}::{nameof(OnPreprocessModel)}() took {totalSeconds:0.00} seconds, `{assetPath}` asset"); | |
} | |
void ReadColorData ( IO.StreamReader reader ) | |
{ | |
List<Color> current_rawColor = new (); | |
Dictionary<int,int> current_rawNormalHash = new (); | |
Dictionary<int,int> current_rawUvHash = new (); | |
HashSet<Vector3Int> current_geometrySharedVerticesDetector = new (); | |
_rawIndices = new (); | |
_rawColors = new (){ { "default" , current_rawColor } }; | |
Dictionary<string,Dictionary<int,int>> rawNormalHashes = new (){ { "default" , current_rawNormalHash } }; | |
Dictionary<string,Dictionary<int,int>> rawUvHashes = new (){ { "default" , current_rawUvHash } }; | |
Dictionary<string,HashSet<Vector3Int>> sharedVertexDetector = new (){ { "default" , current_geometrySharedVerticesDetector } }; | |
int numVertexColorVertices = 0; | |
int v_index = 1, vn_index = 1, vt_index = 1; | |
string line; | |
while( (line = reader.ReadLine())!=null ) | |
{ | |
string[] words = line.Split( ' ' , System.StringSplitOptions.RemoveEmptyEntries ); | |
byte wordsLen = (byte) words.Length; | |
if( wordsLen!=0 ) | |
switch( words[0] ) | |
{ | |
// parse vertex data (vertex color only) | |
case "v": | |
{ | |
float r = float.Parse( words[4] , NumberStyles.Number , CultureInfo.InvariantCulture ); | |
float g = float.Parse( words[5] , NumberStyles.Number , CultureInfo.InvariantCulture ); | |
float b = float.Parse( words[6] , NumberStyles.Number , CultureInfo.InvariantCulture ); | |
current_rawColor.Add( new Color(r,g,b) ); | |
v_index++; | |
numVertexColorVertices++; | |
break; | |
} | |
// parse vertex normal data | |
case "vn": | |
{ | |
int hash = 17; | |
unchecked | |
{ | |
hash = hash * 23 + words[1].GetHashCode(); | |
hash = hash * 23 + words[2].GetHashCode(); | |
hash = hash * 23 + words[3].GetHashCode(); | |
} | |
//Debug.Log($"vn #{vn_index} ( {words[1]} , {words[2]} , {words[3]} ) added as #{hash}"); | |
current_rawNormalHash.Add( vn_index++ , hash ); | |
break; | |
} | |
// parse texture UV data | |
case "vt": | |
{ | |
int hash = 17; | |
unchecked | |
{ | |
hash = hash * 23 + words[1].GetHashCode(); | |
hash = hash * 23 + words[2].GetHashCode(); | |
} | |
//Debug.Log($"vt #{vn_index} ( {words[1]} , {words[2]} ) added as #{hash}"); | |
current_rawUvHash.Add( vt_index++ , hash ); | |
break; | |
} | |
// parse face data | |
case "f": | |
{ | |
if( words[1].Contains('/') )// format: pos/uv/normal indices | |
{ | |
for( int i=1 ; i<wordsLen ; i++ ) | |
{ | |
string[] v_vt_vn = words[i].Split('/'); | |
if( v_vt_vn.Length==3 ) | |
{ | |
string v = v_vt_vn[0];// position index | |
string vt = v_vt_vn[1];// uv index | |
string vn = v_vt_vn[2];// normal index | |
int vertexIndex = int.Parse( v , NumberStyles.Number , CultureInfo.InvariantCulture ); | |
int normalHash; | |
if( vn.Length!=0 ) | |
{ | |
int normalIndex = int.Parse( vn , NumberStyles.Number , CultureInfo.InvariantCulture ); | |
if( !current_rawNormalHash.TryGetValue(normalIndex,out normalHash) ) | |
{ | |
normalHash = -1; | |
Debug.LogError($"Face definition points to normal data that is missing from this OBJ file. Line: {line}"); | |
} | |
} | |
else normalHash = -1;// vertex with no normal data | |
int uvHash; | |
if( vt.Length!=0 ) | |
{ | |
int uvIndex = int.Parse( vt , NumberStyles.Number , CultureInfo.InvariantCulture ); | |
if( !current_rawUvHash.TryGetValue(uvIndex,out uvHash) ) | |
{ | |
normalHash = -1; | |
Debug.LogError($"Face definition points to uv data that is missing from this OBJ file. Line: {line}"); | |
} | |
} | |
else uvHash = -1;// vertex with no uv data | |
if( current_geometrySharedVerticesDetector.Add(new Vector3Int(vertexIndex,uvHash,normalHash)) ) | |
{ | |
// Debug.Log($"new vertex index added: ( {vertexIndex} #{uvHash} , #{normalHash} )"); | |
_rawIndices.Add(vertexIndex); | |
} | |
// else Debug.Log($"ignoring vertex as shared: ( {vertexIndex} , #{normalHash} )"); | |
} | |
} | |
} | |
else if( wordsLen>=4 )// face indices only | |
{ | |
for( int i=1 ; i<wordsLen ; i++ ) | |
{ | |
string v = words[i]; | |
int vertexIndex = int.Parse( v , NumberStyles.Number , CultureInfo.InvariantCulture ); | |
_rawIndices.Add(vertexIndex); | |
} | |
} | |
break; | |
} | |
// parse g | |
case "g": | |
{ | |
string meshName = line.Substring(1).Trim(' ').Replace(' ','_'); | |
current_rawColor = new(); | |
current_rawNormalHash = new(); | |
current_rawUvHash = new(); | |
current_geometrySharedVerticesDetector = new(); | |
_rawColors.Add( meshName , current_rawColor ); | |
rawNormalHashes.Add( meshName , current_rawNormalHash ); | |
rawUvHashes.Add( meshName , current_rawUvHash ); | |
sharedVertexDetector.Add( meshName , current_geometrySharedVerticesDetector ); | |
break; | |
} | |
} | |
} | |
reader.Dispose(); | |
if( numVertexColorVertices!=0 )// was any vertex color data found ? | |
{ | |
// change importer settings | |
var importer = (ModelImporter)assetImporter; | |
importer.optimizeMeshVertices = false; | |
importer.optimizeMeshPolygons = false; | |
importer.weldVertices = false; | |
importer.importNormals = ModelImporterNormals.Import; | |
importer.importTangents = ModelImporterTangents.CalculateMikk; | |
importer.importAnimation = false; | |
importer.animationType = ModelImporterAnimationType.None; | |
importer.materialImportMode = ModelImporterMaterialImportMode.None; | |
} | |
} | |
void OnPostprocessModel ( GameObject gameObject ) | |
{ | |
if( !assetPath.EndsWith(".obj") ) return; | |
if( _rawColors!=null ) | |
{ | |
Debug.Assert( _rawColors.Count!=0 , $"{nameof(_rawColors)}.Count is zero" ); | |
var stopwatch = System.Diagnostics.Stopwatch.StartNew(); | |
MeshFilter[] meshFilters = gameObject.GetComponentsInChildren<MeshFilter>(); | |
foreach( var mf in meshFilters ) | |
{ | |
Mesh mesh = mf.sharedMesh; | |
List<Color> rawColors = _rawColors[mesh.name]; | |
int meshVertexCount = mesh.vertexCount; | |
Color[] finalColors = new Color[meshVertexCount]; | |
for( int i=0 ; i<meshVertexCount ; i++ ) | |
{ | |
int rawIndex = _rawIndices[i]-1;// "-1" because raw OBJ face indices are in 1..N and not 0..N space | |
finalColors[i] = rawColors[rawIndex]; | |
} | |
if( finalColors.Length!=meshVertexCount ) Debug.LogError($"Invalid color data length {finalColors.Length} - while mesh \"{mesh.name}\" expects {meshVertexCount} entries",gameObject); | |
mesh.SetColors( finalColors ); | |
foreach( var vec in mesh.vertices ) | |
{ | |
Debug.Log(vec); | |
} | |
} | |
double totalSeconds = new System.TimeSpan(stopwatch.ElapsedTicks).TotalSeconds; | |
if( totalSeconds>0.1 ) Debug.LogWarning($"{GetType().Name}::{nameof(OnPostprocessModel)}() took {totalSeconds:0.00} seconds, `{assetPath}` asset",gameObject); | |
// release static refs | |
_rawColors = null; | |
_rawIndices = null; | |
} | |
} | |
} | |
#endif |
Hello, I would like to modify and use this script to import obj files generated by 'Trimesh'.
What will be the license for this script?
Hi @yong753
Cool. That would be GNU General Public License. Feel free to use it as you see fit.
Thank you @andrew-raphael-lukasik . I got a lot of help thanks to your code.
Hi @andrew-raphael-lukasik,
I’m encountering the same errors that @yong753 mentioned above. I’ve tried using a variety of different OBJ files, but the error persists. It seems like my vertex color count and vertices count are always mismatched by a small number for some reason.
Invalid color data length! mesh.name:'default', vertexColor.Length:463249, mesh.vertexCount:463210
The model I tried with. https://sketchfab.com/3d-models/transposition-of-the-great-arteries-long-axis-f7c8acca3614485499af11a01e52f89d
Hi @boxibi24 !
I added few minor improvements. Try it out. But to know what the issue is I need to put my hands on the obj that is giving this error. The link you provided allows me to download file formats but don't see obj among them.
Thanks for the code changes. I tried out the new version, but the same errors are still appearing. I think it might be related to the mesh. Here are the error details:
[Error] Invalid color data length! mesh.name: 'default', vertexColor.Length: 463249, mesh.vertexCount: 463210
ObjFileFormatVertexColorImporter.OnPostprocessModel() at /Test/ObjColorImporter.cs:279
GUIUtility.ProcessEvent()
[Error] Mesh.colors is out of bounds. The supplied array needs to be the same size as the Mesh.vertices array.
Mesh.SetColors()
ObjFileFormatVertexColorImporter.OnPostprocessModel() at /Test/ObjColorImporter.cs:280
GUIUtility.ProcessEvent()
FYI, I downloaded the model as FBX, imported it into Blender, and then exported it as an OBJ. Here’s the converted model.
https://drive.google.com/file/d/1IZbrtospMUgBdBZSRvU-eYZJ-3qduHRM/view?usp=sharing
Let me know if you have any thoughts on how to resolve this! Thank you
Dear @boxibi24,
I tried making this script work with the sample you provided but more I looked into it more I realized that fixing this is simply not worth it for me as it requires a lot of guessing what Unity's closed-source code does to the vertices after import. It worked fine some time ago when I wrote it -> then Unity changed something -> and now it's broken 🤷♂️
But - please use FBX if you have an option to! I tried it and this is the FBX from the sketchfab website you linked, imported, works great:
Thank you for taking the time to look into this. I totally agree with you on Unity being inconsistent at times with the updates, so no worries on dropping support for this case.
FYI, I’m working on a clinical app called Cardioscape, and it currently supports .obj format as an import type. I’m working to make it (model with vertex color) compatible with .obj, but due to limitations with vertex color data, I’m also writing support for .fbx as you suggested.
I just wanted to express my appreciation for the script and all the other tools in your repo (I checked a few out, and they’re all awesome). Keep up the great work!
Hi @wangjinhoon ! Thanks for letting me know. I looked into it, fixed it and should be working fine now.
Here is an OBJ mesh from MeshLab and few other test case OBJ meshes to make sure it works with different data inputs as well: