Skip to content

Instantly share code, notes, and snippets.

@supachailllpay
Last active May 21, 2024 05:42
Show Gist options
  • Save supachailllpay/893cd5b0c31dff3bb025 to your computer and use it in GitHub Desktop.
Save supachailllpay/893cd5b0c31dff3bb025 to your computer and use it in GitHub Desktop.
Load .obj and .mtl in Unity at runtime
Load .obj and .mtl in Unity at runtime
using UnityEngine;
using System.Collections;
public class Manager : MonoBehaviour {
public GameObject model;
void Start () {
ObjectLoader loader = model.AddComponent <ObjectLoader> ();
loader.Load (@"/model/path/", "model.obj");
}
}
/*
* The .obj and .mtl files must follow Wavefront OBJ Specifications.
*
* OBJ Supported List
*
* Vertex Data
* - v Geometric vertices (not support w)
* - vt Texture vertices (not support w)
* - vn Vertex normals
*
* Elements
* - f Face (only support triangulate faces)
*
* Grouping
* - o Object name
*
* Display
* - mtllib Material library (not support multiple files)
* - usemtl Material name
*
* MTL Supported List
*
* Material Name
* - newmtl Material name
*
* Texture Map
* - map_Kd Texture file is linked to the diffuse (not support options)
*/
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FileReader {
public struct ObjectFile {
public string o;
public string mtllib;
public List<string> usemtl;
public List<Vector3> v;
public List<Vector3> vn;
public List<Vector2> vt;
public List<List<int[]>> f;
}
public struct MaterialFile {
public List<string> newmtl;
public List<string> mapKd;
}
public static ObjectFile ReadObjectFile (string path) {
ObjectFile obj = new ObjectFile ();
string[] lines = File.ReadAllLines (path);
obj.usemtl = new List<string> ();
obj.v = new List<Vector3> ();
obj.vn = new List<Vector3> ();
obj.vt = new List<Vector2> ();
obj.f = new List<List<int[]>> ();
foreach (string line in lines) {
if (line == "" || line.StartsWith ("#"))
continue;
string[] token = line.Split (' ');
switch (token [0]) {
case ("o"):
obj.o = token [1];
break;
case ("mtllib"):
obj.mtllib = token [1];
break;
case ("usemtl"):
obj.usemtl.Add (token [1]);
obj.f.Add (new List<int[]> ());
break;
case ("v"):
obj.v.Add (new Vector3 (
float.Parse (token [1]),
float.Parse (token [2]),
float.Parse (token [3])));
break;
case ("vn"):
obj.vn.Add (new Vector3 (
float.Parse (token [1]),
float.Parse (token [2]),
float.Parse (token [3])));
break;
case ("vt"):
obj.vt.Add (new Vector3 (
float.Parse (token [1]),
float.Parse (token [2])));
break;
case ("f"):
for (int i = 1; i < 4; i += 1) {
int[] triplet = Array.ConvertAll (token [i].Split ('/'), x => {
if (String.IsNullOrEmpty (x))
return 0;
return int.Parse (x);
});
obj.f [obj.f.Count - 1].Add (triplet);
}
break;
}
}
return obj;
}
public static MaterialFile ReadMaterialFile (string path) {
MaterialFile mtl = new MaterialFile ();
string[] lines = File.ReadAllLines (path);
mtl.newmtl = new List<string> ();
mtl.mapKd = new List<string> ();
foreach (string line in lines) {
if (line == "" || line.StartsWith ("#"))
continue;
string[] token = line.Split (' ');
switch (token [0]) {
case ("newmtl"):
mtl.newmtl.Add (token [1]);
break;
case ("map_Kd"):
mtl.mapKd.Add (token [1]);
break;
}
}
return mtl;
}
}
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectLoader : MonoBehaviour {
public string directoryPath;
public bool isLoaded;
void Awake () {
isLoaded = true;
}
public void Load (string path, string filename) {
if (!isLoaded)
return;
directoryPath = path;
StartCoroutine (ConstructModel (filename));
}
IEnumerator ConstructModel (string filename) {
isLoaded = false;
FileReader.ObjectFile obj = FileReader.ReadObjectFile (directoryPath + filename);
FileReader.MaterialFile mtl = FileReader.ReadMaterialFile (directoryPath + obj.mtllib);
MeshFilter filter = gameObject.AddComponent<MeshFilter> ();
MeshRenderer renderer = gameObject.AddComponent<MeshRenderer> ();
filter.mesh = PopulateMesh (obj);
renderer.materials = DefineMaterial (obj, mtl);
isLoaded = true;
yield return null;
}
Mesh PopulateMesh (FileReader.ObjectFile obj) {
Mesh mesh = new Mesh ();
List<int[]> triplets = new List<int[]> ();
List<int> submeshes = new List<int> ();
for (int i = 0; i < obj.f.Count; i += 1) {
for (int j = 0; j < obj.f [i].Count; j += 1) {
triplets.Add (obj.f [i] [j]);
}
submeshes.Add (obj.f [i].Count);
}
Vector3[] vertices = new Vector3[triplets.Count];
Vector3[] normals = new Vector3[triplets.Count];
Vector2[] uvs = new Vector2[triplets.Count];
for (int i = 0; i < triplets.Count; i += 1) {
vertices [i] = obj.v [triplets [i] [0] - 1];
normals [i] = obj.vn [triplets [i] [2] - 1];
if (triplets [i] [1] > 0)
uvs [i] = obj.vt [triplets [i] [1] - 1];
}
mesh.name = obj.o;
mesh.vertices = vertices;
mesh.normals = normals;
mesh.uv = uvs;
mesh.subMeshCount = submeshes.Count;
int vertex = 0;
for (int i = 0; i < submeshes.Count; i += 1) {
int[] triangles = new int[submeshes [i]];
for (int j = 0; j < submeshes [i]; j += 1) {
triangles [j] = vertex;
vertex += 1;
}
mesh.SetTriangles (triangles, i);
}
mesh.RecalculateBounds ();
mesh.Optimize ();
return mesh;
}
Material[] DefineMaterial (FileReader.ObjectFile obj, FileReader.MaterialFile mtl) {
Material[] materials = new Material[obj.usemtl.Count];
for (int i = 0; i < obj.usemtl.Count; i += 1) {
int index = mtl.newmtl.IndexOf (obj.usemtl [i]);
Texture2D texture = new Texture2D (1, 1);
texture.LoadImage (File.ReadAllBytes (directoryPath + mtl.mapKd [index]));
materials [i] = new Material (Shader.Find ("Diffuse"));
materials [i].name = mtl.newmtl [index];
materials [i].mainTexture = texture;
}
return materials;
}
}
@NotLazy
Copy link

NotLazy commented Jun 24, 2021

Thank you a billion times over. I had to make some changes to this to make it work in my unity project, such as adding multi-object support, but without this code, I wouldn't have been able to add a vital feature to my game. Thank you. Thank you. Thank you.

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