Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save andrew-raphael-lukasik/3559728d022a4c96f491924f8285e1bf to your computer and use it in GitHub Desktop.
Save andrew-raphael-lukasik/3559728d022a4c96f491924f8285e1bf to your computer and use it in GitHub Desktop.
OBJ file format vertex color importer for Unity

(Unity) vertex color importer for MeshLab's OBJ files

Update note:

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
@yong753
Copy link

yong753 commented Sep 25, 2023

Thank you @andrew-raphael-lukasik . I got a lot of help thanks to your code.

@boxibi24
Copy link

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

@andrew-raphael-lukasik
Copy link
Author

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.

@boxibi24
Copy link

boxibi24 commented May 5, 2025

Hi @andrew-raphael-lukasik,

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

@andrew-raphael-lukasik
Copy link
Author

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:
image

@boxibi24
Copy link

boxibi24 commented May 10, 2025

Hi @andrew-raphael-lukasik

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment