├── OBJ └── src │ ├── FaceIndices.cs │ ├── GeometryBuffer.cs │ └── OBJ.cs ├── README └── objDemo.unity /OBJ/src/FaceIndices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | public struct FaceIndices 4 | { 5 | public int vi; 6 | public int vu; 7 | public int vn; 8 | } 9 | -------------------------------------------------------------------------------- /OBJ/src/GeometryBuffer.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | public class GeometryBuffer { 6 | 7 | private List objects; 8 | public List vertices; 9 | public List uvs; 10 | public List normals; 11 | public int unnamedGroupIndex = 1; // naming index for unnamed group. like "Unnamed-1" 12 | 13 | private ObjectData current; 14 | private class ObjectData { 15 | public string name; 16 | public List groups; 17 | public List allFaces; 18 | public int normalCount; 19 | public ObjectData() { 20 | groups = new List(); 21 | allFaces = new List(); 22 | normalCount = 0; 23 | } 24 | } 25 | 26 | private GroupData curgr; 27 | private class GroupData { 28 | public string name; 29 | public string materialName; 30 | public List faces; 31 | public GroupData() { 32 | faces = new List(); 33 | } 34 | public bool isEmpty { get { return faces.Count == 0; } } 35 | } 36 | 37 | public GeometryBuffer() { 38 | objects = new List(); 39 | ObjectData d = new ObjectData(); 40 | d.name = "default"; 41 | objects.Add(d); 42 | current = d; 43 | 44 | GroupData g = new GroupData(); 45 | g.name = "default"; 46 | d.groups.Add(g); 47 | curgr = g; 48 | 49 | vertices = new List(); 50 | uvs = new List(); 51 | normals = new List(); 52 | } 53 | 54 | public void PushObject(string name) { 55 | //Debug.Log("Adding new object " + name + ". Current is empty: " + isEmpty); 56 | if(isEmpty) objects.Remove(current); 57 | 58 | ObjectData n = new ObjectData(); 59 | n.name = name; 60 | objects.Add(n); 61 | 62 | GroupData g = new GroupData(); 63 | g.name = "default"; 64 | n.groups.Add(g); 65 | 66 | curgr = g; 67 | current = n; 68 | } 69 | 70 | public void PushGroup(string name) { 71 | if(curgr.isEmpty) current.groups.Remove(curgr); 72 | GroupData g = new GroupData(); 73 | if (name == null) { 74 | name = "Unnamed-"+unnamedGroupIndex; 75 | unnamedGroupIndex++; 76 | } 77 | g.name = name; 78 | current.groups.Add(g); 79 | curgr = g; 80 | } 81 | 82 | public void PushMaterialName(string name) { 83 | //Debug.Log("Pushing new material " + name + " with curgr.empty=" + curgr.isEmpty); 84 | if(!curgr.isEmpty) PushGroup(name); 85 | if(curgr.name == "default") curgr.name = name; 86 | curgr.materialName = name; 87 | } 88 | 89 | public void PushVertex(Vector3 v) { 90 | vertices.Add(v); 91 | } 92 | 93 | public void PushUV(Vector2 v) { 94 | uvs.Add(v); 95 | } 96 | 97 | public void PushNormal(Vector3 v) { 98 | normals.Add(v); 99 | } 100 | 101 | public void PushFace(FaceIndices f) { 102 | curgr.faces.Add(f); 103 | current.allFaces.Add(f); 104 | if (f.vn >= 0) { 105 | current.normalCount++; 106 | } 107 | } 108 | 109 | public void Trace() { 110 | Debug.Log("OBJ has " + objects.Count + " object(s)"); 111 | Debug.Log("OBJ has " + vertices.Count + " vertice(s)"); 112 | Debug.Log("OBJ has " + uvs.Count + " uv(s)"); 113 | Debug.Log("OBJ has " + normals.Count + " normal(s)"); 114 | foreach(ObjectData od in objects) { 115 | Debug.Log(od.name + " has " + od.groups.Count + " group(s)"); 116 | foreach(GroupData gd in od.groups) { 117 | Debug.Log(od.name + "/" + gd.name + " has " + gd.faces.Count + " faces(s)"); 118 | } 119 | } 120 | 121 | } 122 | 123 | public int numObjects { get { return objects.Count; } } 124 | public bool isEmpty { get { return vertices.Count == 0; } } 125 | public bool hasUVs { get { return uvs.Count > 0; } } 126 | public bool hasNormals { get { return normals.Count > 0; } } 127 | 128 | public static int MAX_VERTICES_LIMIT_FOR_A_MESH = 64999; 129 | 130 | public void PopulateMeshes(GameObject[] gs, Dictionary mats) { 131 | if(gs.Length != numObjects) return; // Should not happen unless obj file is corrupt... 132 | Debug.Log("PopulateMeshes GameObjects count:"+gs.Length); 133 | for(int i = 0; i < gs.Length; i++) { 134 | ObjectData od = objects[i]; 135 | bool objectHasNormals = (hasNormals && od.normalCount > 0); 136 | 137 | if(od.name != "default") gs[i].name = od.name; 138 | Debug.Log("PopulateMeshes object name:"+od.name); 139 | 140 | Vector3[] tvertices = new Vector3[od.allFaces.Count]; 141 | Vector2[] tuvs = new Vector2[od.allFaces.Count]; 142 | Vector3[] tnormals = new Vector3[od.allFaces.Count]; 143 | 144 | int k = 0; 145 | foreach(FaceIndices fi in od.allFaces) { 146 | if (k >= MAX_VERTICES_LIMIT_FOR_A_MESH) { 147 | Debug.LogWarning("maximum vertex number for a mesh exceeded for object:" + gs[i].name); 148 | break; 149 | } 150 | tvertices[k] = vertices[fi.vi]; 151 | if(hasUVs) tuvs[k] = uvs[fi.vu]; 152 | if(hasNormals && fi.vn >= 0) tnormals[k] = normals[fi.vn]; 153 | k++; 154 | } 155 | 156 | Mesh m = (gs[i].GetComponent(typeof(MeshFilter)) as MeshFilter).mesh; 157 | m.vertices = tvertices; 158 | if(hasUVs) m.uv = tuvs; 159 | if(objectHasNormals) m.normals = tnormals; 160 | 161 | if(od.groups.Count == 1) { 162 | Debug.Log("PopulateMeshes only one group: "+od.groups[0].name); 163 | GroupData gd = od.groups[0]; 164 | string matName = (gd.materialName != null) ? gd.materialName : "default"; // MAYBE: "default" may not enough. 165 | if (mats.ContainsKey(matName)) { 166 | gs[i].renderer.material = mats[matName]; 167 | Debug.Log("PopulateMeshes mat:"+matName+" set."); 168 | } 169 | else { 170 | Debug.LogWarning("PopulateMeshes mat:"+matName+" not found."); 171 | } 172 | int[] triangles = new int[gd.faces.Count]; 173 | for(int j = 0; j < triangles.Length; j++) triangles[j] = j; 174 | 175 | m.triangles = triangles; 176 | 177 | } else { 178 | int gl = od.groups.Count; 179 | Material[] materials = new Material[gl]; 180 | m.subMeshCount = gl; 181 | int c = 0; 182 | 183 | Debug.Log("PopulateMeshes group count:"+gl); 184 | for(int j = 0; j < gl; j++) { 185 | string matName = (od.groups[j].materialName != null) ? od.groups[j].materialName : "default"; // MAYBE: "default" may not enough. 186 | if (mats.ContainsKey(matName)) { 187 | materials[j] = mats[matName]; 188 | Debug.Log("PopulateMeshes mat:"+matName+" set."); 189 | } 190 | else { 191 | Debug.LogWarning("PopulateMeshes mat:"+matName+" not found."); 192 | } 193 | 194 | int[] triangles = new int[od.groups[j].faces.Count]; 195 | int l = od.groups[j].faces.Count + c; 196 | int s = 0; 197 | for(; c < l; c++, s++) triangles[s] = c; 198 | m.SetTriangles(triangles, j); 199 | } 200 | 201 | gs[i].renderer.materials = materials; 202 | } 203 | if (!objectHasNormals) { 204 | m.RecalculateNormals(); 205 | } 206 | } 207 | } 208 | } 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /OBJ/src/OBJ.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Text.RegularExpressions; 7 | using System.IO; 8 | 9 | public class OBJ : MonoBehaviour { 10 | 11 | public string objPath; 12 | 13 | /* OBJ file tags */ 14 | private const string O = "o"; 15 | private const string G = "g"; 16 | private const string V = "v"; 17 | private const string VT = "vt"; 18 | private const string VN = "vn"; 19 | private const string F = "f"; 20 | private const string MTL = "mtllib"; 21 | private const string UML = "usemtl"; 22 | 23 | /* MTL file tags */ 24 | private const string NML = "newmtl"; 25 | private const string NS = "Ns"; // Shininess 26 | private const string KA = "Ka"; // Ambient component (not supported) 27 | private const string KD = "Kd"; // Diffuse component 28 | private const string KS = "Ks"; // Specular component 29 | private const string D = "d"; // Transparency (not supported) 30 | private const string TR = "Tr"; // Same as 'd' 31 | private const string ILLUM = "illum"; // Illumination model. 1 - diffuse, 2 - specular 32 | private const string MAP_KA = "map_Ka"; // Ambient texture 33 | private const string MAP_KD = "map_Kd"; // Diffuse texture 34 | private const string MAP_KS = "map_Ks"; // Specular texture 35 | private const string MAP_KE = "map_Ke"; // Emissive texture 36 | private const string MAP_BUMP = "map_bump"; // Bump map texture 37 | private const string BUMP = "bump"; // Bump map texture 38 | 39 | private string basepath; 40 | private string mtllib; 41 | private GeometryBuffer buffer; 42 | 43 | void Start () 44 | { 45 | buffer = new GeometryBuffer (); 46 | StartCoroutine (Load (objPath)); 47 | } 48 | 49 | public IEnumerator Load(string path) { 50 | basepath = (path.IndexOf("/") == -1) ? "" : path.Substring(0, path.LastIndexOf("/") + 1); 51 | 52 | WWW loader = new WWW(path); 53 | yield return loader; 54 | SetGeometryData(loader.text); 55 | 56 | if(hasMaterials) { 57 | loader = new WWW(basepath + mtllib); 58 | Debug.Log("base path = "+basepath); 59 | Debug.Log("MTL path = "+(basepath + mtllib)); 60 | yield return loader; 61 | if (loader.error != null) { 62 | Debug.LogError(loader.error); 63 | } 64 | else { 65 | SetMaterialData(loader.text); 66 | } 67 | 68 | foreach(MaterialData m in materialData) { 69 | if(m.diffuseTexPath != null) { 70 | WWW texloader = GetTextureLoader(m, m.diffuseTexPath); 71 | yield return texloader; 72 | if (texloader.error != null) { 73 | Debug.LogError(texloader.error); 74 | } else { 75 | m.diffuseTex = texloader.texture; 76 | } 77 | } 78 | if(m.bumpTexPath != null) { 79 | WWW texloader = GetTextureLoader(m, m.bumpTexPath); 80 | yield return texloader; 81 | if (texloader.error != null) { 82 | Debug.LogError(texloader.error); 83 | } else { 84 | m.bumpTex = texloader.texture; 85 | } 86 | } 87 | } 88 | } 89 | 90 | Build(); 91 | 92 | } 93 | 94 | private WWW GetTextureLoader(MaterialData m, string texpath) { 95 | char[] separators = {'/', '\\'}; 96 | string[] components = texpath.Split(separators); 97 | string filename = components[components.Length-1]; 98 | string ext = Path.GetExtension(filename).ToLower(); 99 | if (ext != ".png" && ext != ".jpg") { 100 | Debug.LogWarning("maybe unsupported texture format:"+ext); 101 | } 102 | WWW texloader = new WWW(basepath + filename); 103 | Debug.Log("texture path for material("+m.name+") = "+(basepath + filename)); 104 | return texloader; 105 | } 106 | 107 | private void GetFaceIndicesByOneFaceLine(FaceIndices[] faces, string[] p, bool isFaceIndexPlus) { 108 | if (isFaceIndexPlus) { 109 | for(int j = 1; j < p.Length; j++) { 110 | string[] c = p[j].Trim().Split("/".ToCharArray()); 111 | FaceIndices fi = new FaceIndices(); 112 | // vertex 113 | int vi = ci(c[0]); 114 | fi.vi = vi-1; 115 | // uv 116 | if(c.Length > 1 && c[1] != "") { 117 | int vu = ci(c[1]); 118 | fi.vu = vu-1; 119 | } 120 | // normal 121 | if(c.Length > 2 && c[2] != "") { 122 | int vn = ci(c[2]); 123 | fi.vn = vn-1; 124 | } 125 | else { 126 | fi.vn = -1; 127 | } 128 | faces[j-1] = fi; 129 | } 130 | } 131 | else { // for minus index 132 | int vertexCount = buffer.vertices.Count; 133 | int uvCount = buffer.uvs.Count; 134 | for(int j = 1; j < p.Length; j++) { 135 | string[] c = p[j].Trim().Split("/".ToCharArray()); 136 | FaceIndices fi = new FaceIndices(); 137 | // vertex 138 | int vi = ci(c[0]); 139 | fi.vi = vertexCount + vi; 140 | // uv 141 | if(c.Length > 1 && c[1] != "") { 142 | int vu = ci(c[1]); 143 | fi.vu = uvCount + vu; 144 | } 145 | // normal 146 | if(c.Length > 2 && c[2] != "") { 147 | int vn = ci(c[2]); 148 | fi.vn = vertexCount + vn; 149 | } 150 | else { 151 | fi.vn = -1; 152 | } 153 | faces[j-1] = fi; 154 | } 155 | } 156 | } 157 | 158 | private void SetGeometryData(string data) { 159 | string[] lines = data.Split("\n".ToCharArray()); 160 | Regex regexWhitespaces = new Regex(@"\s+"); 161 | bool isFirstInGroup = true; 162 | bool isFaceIndexPlus = true; 163 | for(int i = 0; i < lines.Length; i++) { 164 | string l = lines[i].Trim(); 165 | 166 | if(l.IndexOf("#") != -1) { // comment line 167 | continue; 168 | } 169 | string[] p = regexWhitespaces.Split(l); 170 | switch(p[0]) { 171 | case O: 172 | buffer.PushObject(p[1].Trim()); 173 | isFirstInGroup = true; 174 | break; 175 | case G: 176 | string groupName = null; 177 | if (p.Length >= 2) { 178 | groupName = p[1].Trim(); 179 | } 180 | isFirstInGroup = true; 181 | buffer.PushGroup(groupName); 182 | break; 183 | case V: 184 | buffer.PushVertex( new Vector3( cf(p[1]), cf(p[2]), cf(p[3]) ) ); 185 | break; 186 | case VT: 187 | buffer.PushUV(new Vector2( cf(p[1]), cf(p[2]) )); 188 | break; 189 | case VN: 190 | buffer.PushNormal(new Vector3( cf(p[1]), cf(p[2]), cf(p[3]) )); 191 | break; 192 | case F: 193 | FaceIndices[] faces = new FaceIndices[p.Length-1]; 194 | if (isFirstInGroup) { 195 | isFirstInGroup = false; 196 | string[] c = p[1].Trim().Split("/".ToCharArray()); 197 | isFaceIndexPlus = (ci(c[0]) >= 0); 198 | } 199 | GetFaceIndicesByOneFaceLine(faces, p, isFaceIndexPlus); 200 | if (p.Length == 4) { 201 | buffer.PushFace(faces[0]); 202 | buffer.PushFace(faces[1]); 203 | buffer.PushFace(faces[2]); 204 | } 205 | else if (p.Length == 5) { 206 | buffer.PushFace(faces[0]); 207 | buffer.PushFace(faces[1]); 208 | buffer.PushFace(faces[3]); 209 | buffer.PushFace(faces[3]); 210 | buffer.PushFace(faces[1]); 211 | buffer.PushFace(faces[2]); 212 | } 213 | else { 214 | Debug.LogWarning("face vertex count :"+(p.Length-1)+" larger than 4:"); 215 | } 216 | break; 217 | case MTL: 218 | mtllib = l.Substring(p[0].Length+1).Trim(); 219 | break; 220 | case UML: 221 | buffer.PushMaterialName(p[1].Trim()); 222 | break; 223 | } 224 | } 225 | 226 | // buffer.Trace(); 227 | } 228 | 229 | private float cf(string v) { 230 | try { 231 | return float.Parse(v); 232 | } 233 | catch(Exception e) { 234 | print(e); 235 | return 0; 236 | } 237 | } 238 | 239 | private int ci(string v) { 240 | try { 241 | return int.Parse(v); 242 | } 243 | catch(Exception e) { 244 | print(e); 245 | return 0; 246 | } 247 | } 248 | 249 | private bool hasMaterials { 250 | get { 251 | return mtllib != null; 252 | } 253 | } 254 | 255 | /* ############## MATERIALS */ 256 | private List materialData; 257 | private class MaterialData { 258 | public string name; 259 | public Color ambient; 260 | public Color diffuse; 261 | public Color specular; 262 | public float shininess; 263 | public float alpha; 264 | public int illumType; 265 | public string diffuseTexPath; 266 | public string bumpTexPath; 267 | public Texture2D diffuseTex; 268 | public Texture2D bumpTex; 269 | } 270 | 271 | private void SetMaterialData(string data) { 272 | string[] lines = data.Split("\n".ToCharArray()); 273 | 274 | materialData = new List(); 275 | MaterialData current = new MaterialData(); 276 | Regex regexWhitespaces = new Regex(@"\s+"); 277 | 278 | for(int i = 0; i < lines.Length; i++) { 279 | string l = lines[i].Trim(); 280 | 281 | if(l.IndexOf("#") != -1) l = l.Substring(0, l.IndexOf("#")); 282 | string[] p = regexWhitespaces.Split(l); 283 | if (p[0].Trim() == "") continue; 284 | 285 | switch(p[0]) { 286 | case NML: 287 | current = new MaterialData(); 288 | current.name = p[1].Trim(); 289 | materialData.Add(current); 290 | break; 291 | case KA: 292 | current.ambient = gc(p); 293 | break; 294 | case KD: 295 | current.diffuse = gc(p); 296 | break; 297 | case KS: 298 | current.specular = gc(p); 299 | break; 300 | case NS: 301 | current.shininess = cf(p[1]) / 1000; 302 | break; 303 | case D: 304 | case TR: 305 | current.alpha = cf(p[1]); 306 | break; 307 | case MAP_KD: 308 | current.diffuseTexPath = p[p.Length-1].Trim(); 309 | break; 310 | case MAP_BUMP: 311 | case BUMP: 312 | BumpParameter(current, p); 313 | break; 314 | case ILLUM: 315 | current.illumType = ci(p[1]); 316 | break; 317 | default: 318 | Debug.Log("this line was not processed :" +l ); 319 | break; 320 | } 321 | } 322 | } 323 | 324 | private Material GetMaterial(MaterialData md) { 325 | Material m; 326 | 327 | if(md.illumType == 2) { 328 | string shaderName = (md.bumpTex != null)? "Bumped Specular" : "Specular"; 329 | m = new Material(Shader.Find(shaderName)); 330 | m.SetColor("_SpecColor", md.specular); 331 | m.SetFloat("_Shininess", md.shininess); 332 | } else { 333 | string shaderName = (md.bumpTex != null)? "Bumped Diffuse" : "Diffuse"; 334 | m = new Material(Shader.Find(shaderName)); 335 | } 336 | 337 | if(md.diffuseTex != null) { 338 | m.SetTexture("_MainTex", md.diffuseTex); 339 | } 340 | else { 341 | m.SetColor("_Color", md.diffuse); 342 | } 343 | if(md.bumpTex != null) m.SetTexture("_BumpMap", md.bumpTex); 344 | 345 | m.name = md.name; 346 | 347 | return m; 348 | } 349 | 350 | private class BumpParamDef { 351 | public string optionName; 352 | public string valueType; 353 | public int valueNumMin; 354 | public int valueNumMax; 355 | public BumpParamDef(string name, string type, int numMin, int numMax) { 356 | this.optionName = name; 357 | this.valueType = type; 358 | this.valueNumMin = numMin; 359 | this.valueNumMax = numMax; 360 | } 361 | } 362 | 363 | private void BumpParameter(MaterialData m, string[] p) { 364 | Regex regexNumber = new Regex(@"^[-+]?[0-9]*\.?[0-9]+$"); 365 | 366 | var bumpParams = new Dictionary(); 367 | bumpParams.Add("bm",new BumpParamDef("bm","string", 1, 1)); 368 | bumpParams.Add("clamp",new BumpParamDef("clamp", "string", 1,1)); 369 | bumpParams.Add("blendu",new BumpParamDef("blendu", "string", 1,1)); 370 | bumpParams.Add("blendv",new BumpParamDef("blendv", "string", 1,1)); 371 | bumpParams.Add("imfchan",new BumpParamDef("imfchan", "string", 1,1)); 372 | bumpParams.Add("mm",new BumpParamDef("mm", "string", 1,1)); 373 | bumpParams.Add("o",new BumpParamDef("o", "number", 1,3)); 374 | bumpParams.Add("s",new BumpParamDef("s", "number", 1,3)); 375 | bumpParams.Add("t",new BumpParamDef("t", "number", 1,3)); 376 | bumpParams.Add("texres",new BumpParamDef("texres", "string", 1,1)); 377 | int pos = 1; 378 | string filename = null; 379 | while (pos < p.Length) { 380 | if (!p[pos].StartsWith("-")) { 381 | filename = p[pos]; 382 | pos++; 383 | continue; 384 | } 385 | // option processing 386 | string optionName = p[pos].Substring(1); 387 | pos++; 388 | if (!bumpParams.ContainsKey(optionName)) { 389 | continue; 390 | } 391 | BumpParamDef def = bumpParams[optionName]; 392 | ArrayList args = new ArrayList(); 393 | int i=0; 394 | bool isOptionNotEnough = false; 395 | for (;i= p.Length) { 397 | isOptionNotEnough = true; 398 | break; 399 | } 400 | if (def.valueType == "number") { 401 | Match match = regexNumber.Match(p[pos]); 402 | if (!match.Success) { 403 | isOptionNotEnough = true; 404 | break; 405 | } 406 | } 407 | args.Add(p[pos]); 408 | } 409 | if (isOptionNotEnough) { 410 | Debug.Log("bump variable value not enough for option:"+optionName+" of material:"+m.name); 411 | continue; 412 | } 413 | for (;i materials = new Dictionary(); 436 | 437 | if(hasMaterials) { 438 | foreach(MaterialData md in materialData) { 439 | if (materials.ContainsKey(md.name)) { 440 | Debug.LogWarning("duplicate material found: "+ md.name+ ". ignored repeated occurences"); 441 | continue; 442 | } 443 | materials.Add(md.name, GetMaterial(md)); 444 | } 445 | } else { 446 | materials.Add("default", new Material(Shader.Find("VertexLit"))); 447 | } 448 | 449 | GameObject[] ms = new GameObject[buffer.numObjects]; 450 | 451 | if(buffer.numObjects == 1) { 452 | gameObject.AddComponent(typeof(MeshFilter)); 453 | gameObject.AddComponent(typeof(MeshRenderer)); 454 | ms[0] = gameObject; 455 | } else if(buffer.numObjects > 1) { 456 | for(int i = 0; i < buffer.numObjects; i++) { 457 | GameObject go = new GameObject(); 458 | go.transform.parent = gameObject.transform; 459 | go.AddComponent(typeof(MeshFilter)); 460 | go.AddComponent(typeof(MeshRenderer)); 461 | ms[i] = go; 462 | } 463 | } 464 | 465 | buffer.PopulateMeshes(ms, materials); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | << unity obj loader >> 2 | Project home: https://github.com/hammmm/unity-obj-loader 3 | 4 | This project is to achieve good-enough runtime OBJ file importing for Unity3D, 5 | based on Bartek Drozdz's OBJ library v1.2. 6 | http://www.everyday3d.com/blog/index.php/2010/05/24/loading-3d-models-runtime-unity3d/ 7 | 8 | Many thanks to the original author, Bartek Drozdz for publishing the code under MIT license. 9 | 10 | License: MIT 11 | 12 | Notes: 13 | - please put all texture files on the same directory of . obj file. 14 | - please use use this form of URL for local files. ex) file:///Users/someone/somepath/model.obj 15 | - Bump map is not correctly working. TODO: convert hight map to normal map, make tangent data. 16 | 17 | -------------------------------------------------------------------------------- /objDemo.unity: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hammmm/unity-obj-loader/f6e8e7eafaa935a65ae758919d2ecafdc84c40f6/objDemo.unity --------------------------------------------------------------------------------