OBJ Model Importing in Real-Time with Textures in Unity: A Step-by-Step Guide
Are you attempting to dynamically import 3D models into your Unity project without the need to download them onto your device?
Look no further! This article provides you with a step-by-step guide on how to import OBJ models in real-time using a URL directly to the model. Unlock the solution to seamlessly integrating models into your project without the hassle of storing them locally.
Prerequisites:
Latest version Unity
URL for an obj model
What is OBJ?
OBJ (object) file consists of a 3D model written as a code.
An OBJ file is downloaded with its MTL (material) file, the mtl file is liked with the obj file by a line in obj as: mtllib <MTL file reference>
Sample OBJ File -
raw.githubusercontent.com/DamanAhuja/cube/main/1/cube.obj
MTL File
An MTL File is a code file to define the materials used in the model.
It consists of:
Ka (Ambient color)
Kd (Diffuse color)
Ks (Specular color)
Ns (Specular Exponent)
Tr (Transparency)
Ni (Optical Density)
illum (Illumination Model)
The texture is mapped to the material by a command: map_kd <texture reference>
Sample MTL FIle -
raw.githubusercontent.com/DamanAhuja/cube/main/1/cube.mtl
Importing The Model in Real Time
Let's get started with our unity project.
First of all, create a new project in unity hub.
Then we have to import a unity package named "Runtime OBJ Importer".
You can get the package from here.
After Importing the package, you will have to different sample scenes in your Project. One is for importing by Path, which takes the file path as input and the other one as "import from stream" which imports a model from the given OBJ url.
Now if you have the OBJ, MTL, Texture files downloaded in your pc, you can simply import it in Run-Time by just entering the file path in the "Import by Path" scene.
But now as we try to import the model from stream, it will get the model imported but you will see that there will be no texture applied to the model. For getting the model with textures we will have to make some changes in the scripts present in the package.
OBJLoader Script
You will find an OBJLoader Script in your assets. Here I will tell you some changes to make in this script.
Search for the Method named "LoadMaterialLibrary()" in the script and then change the whole method by -
public void LoadMaterialLibrary(string mtlLibPath)
{
if (_objInfo != null)
{
// Construct path relative to OBJ model directory
string relativePath = Path.Combine(_objInfo.Directory.FullName, mtlLibPath);
Debug.Log("Checking MTL file at relative path: " + relativePath);
if (File.Exists(relativePath))
{
Materials = new MTLLoader().Load(relativePath);
return;
}
}
if (Uri.TryCreate(mtlLibPath, UriKind.Absolute, out Uri uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
{
Debug.Log("Loading MTL file from URL: " + mtlLibPath);
Materials = new MTLLoader().LoadFromURL(mtlLibPath);
return;
}
// Fallback: Check provided path directly
Debug.Log("Checking MTL file at provided path: " + mtlLibPath);
if (File.Exists(mtlLibPath))
{
Materials = new MTLLoader().Load(mtlLibPath);
return;
}
// Consider adding error handling for missing MTL file
Debug.LogError("MTL file not found: " + mtlLibPath);
Let me now explain the changes we just made.
First of all, we are adding script to check if the content after "mtllib" is an URL or a local path.
And if it is a URL then it calls a function from the MTLLoader file named LoadFromURL().
Now we will define the LoadFromURL() in the MTLLoader script
MTLLoader
Find the MTLLoader script in your assets and then add this code snippet in the already existing code.
public Dictionary<string, Material> LoadFromURL(string url)
{
try
{
WebClient client = new WebClient();
string mtlData = client.DownloadString(url);
using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(mtlData)))
{
return Load(stream);
}
}
catch (Exception e)
{
Debug.LogError("Error loading MTL file from URL: " + e.Message);
return null;
}
}
Now as you see, what this function does is reading the content from URL and making a text stream from its data. Then it calls the already existing Load() function which takes argument as a text stream.
Then find the Load() function in that script only, and then find the script -
//diffuse map
if (splitLine[0] == "map_Kd" || splitLine[0] == "map_kd")
{
string texturePath = GetTexPathFromMapStatement(processedLine, splitLine);
Debug.Log("loading texture from: " + texturePath);
if(texturePath == null)
{
continue; //invalid args or sth
}
Just after this script add an if condition -
if (Uri.TryCreate(texturePath, UriKind.Absolute, out Uri uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
{
Texture2D texture = LoadTextureFromURL(texturePath);
Debug.Log(texture);
if (texture != null)
{
currentMaterial.SetTexture("_MainTex",texture);
}
}
This condition checks if the reference to the texture is given as URL, and if it is true, a function named LoadTextureFromURL() is called.
Defining LoadTextureFromURL()
Now in the last step we will just define the required function which will load the texture by taking the argument as an URL.
public Texture2D LoadTextureFromURL(string url)
{
try
{
WebClient client = new WebClient();
byte[] textureData = client.DownloadData(url);
Texture2D texture = new Texture2D(2, 2);
if (texture.LoadImage(textureData))
{
return texture;
}
else
{
Debug.LogError("Failed to load texture from URL: " + url);
return null;
}
}
catch (Exception e)
{
Debug.LogError("Error loading texture from URL: " + e.Message);
return null;
}
}
Just define this function anywhere in the script but just inside the class.
Testing -
Now you are all done with changes to be made.
To try your newly made project, just open the LoadFromStream script from the assets and change the input URL from the default to -https://raw.githubusercontent.com/DamanAhuja/cube/main/1/cube.obj
Just run your scene and you will find a OBJ model imported in your scene.
Some Additional Tips:
Now you might be having issues with the textures of the model imported, the model might not be looking good.
This issue is happening because of the shader of the imported model.
We can change the shader back to standard after importing the model. To change it, we have to add a function in the LoadFromStream script.
private void ChangeShaderToStandard(GameObject obj)
{
if (obj != null)
{
Renderer[] renderers = obj.GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
{
Material[] materials = renderer.materials;
foreach (Material material in materials)
{
material.shader = Shader.Find("Standard");
}
}
}
}
Now call this function in Start() function by -
ChangeShaderToStandard(loadedobj);
Now you will get the model with proper textures.
For setting it up in VR
First we will set-up oculus interaction SDK in our project, to know more click here.
Now we will make some changes to optimise the script so that it does not lag in VR.
We will be adding a script to check if the the obj file is already saved, and if not then download it locally and them process it
Same for MTL and texture file.
To do that we will first add a function named DownloadAndLoadModel()
Which goes as
private IEnumerator DownloadandLoadModel()
{
string assetPath = Path.Combine(Application.persistentDataPath, Path.GetFileName(objURL));
if (!File.Exists(assetPath))
{
using (UnityWebRequest www = UnityWebRequest.Get(objURL))
{
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.LogError("Failed to download model: " + www.error);
yield break;
}
DebugHandler.Instance.Log("Model downloaded successfully");
}
}
else
{
Debug.Log("Model already exists offline at: " + assetPath);
DebugHandler.Instance.Log("Model already exists at: " + assetPath);
}
if (!File.Exists(assetPath))
{
Debug.LogError("Model file does not exist.");
DebugHandler.Instance.Log("Model does not exist");
StopCoroutine(DownloadandLoadModel());
}
modelPrefab = new OBJLoader().Load(assetPath);
modelPrefab.transform.localScale = scale;
modelPrefab.transform.position = spawnPosition;
AddComponents(modelPrefab);
for (int i = 0; i < numberOfInstances; i++)
{
GameObject modelInstance = Instantiate(modelPrefab, spawnPosition, Quaternion.identity);
DebugHandler.Instance.Log("Model Load Successfull");
}
}
With this function we have done many things,
1) Made it a coroutine so that it does not interrupt the the main thread while working
2) Added a check to see if the file already exists
3) If multiple models are needed then the one model contructed is initiated n times to have efficiency.
Now we will add the same check for mtl and texture file too
This is the new LoadFromURL function in MTLLoader file -
public Dictionary<string, Material> LoadFromURL(string url)
{
DebugHandler.Instance.Log("Texture loadfromURL function called");
try
{
string localFilePath = Path.Combine(Application.persistentDataPath, Path.GetFileName(url));
if (!File.Exists(localFilePath))
{
using (WebClient client = new WebClient())
{
string mtlData = client.DownloadString(url);
// Save MTL file locally
File.WriteAllText(localFilePath, mtlData);
//string mtlContent = File.ReadAllText(localFilePath);
//DebugHandler.Instance.Log("MTL File Contents:\n" + mtlContent);
DebugHandler.Instance.Log("MTL file downloaded and saved at: " + localFilePath);
// Load MTL data from local file
}
}
DebugHandler.Instance.Log("MTL File already exists");
using (var stream = new FileStream(localFilePath, FileMode.Open))
{
return Load(stream);
}
}
catch (Exception e)
{
Debug.LogError("Error loading MTL file from URL: " + e.Message);
return null;
}
}
And this is the new LoadTextureFromURL function
public Texture2D LoadTextureFromURL(string url)
{
try
{
// Generate a local path based on the texture URL
string localPath = Path.Combine(Application.persistentDataPath, Path.GetFileName(url));
if (!File.Exists(localPath))
{
WebClient client = new WebClient();
byte[] textureData = client.DownloadData(url);
// Save the texture data to the local path
File.WriteAllBytes(localPath, textureData);
}
// Load the texture from the local file
Texture2D texture = new Texture2D(2, 2);
byte[] fileData = File.ReadAllBytes(localPath);
if (texture.LoadImage(fileData))
{
return texture;
}
else
{
Debug.LogError("Failed to load texture from URL: " + url);
return null;
}
}
catch (Exception e)
{
Debug.LogError("Error loading texture from URL: " + e.Message);
return null;
}
}
Now the final Step,
instead of changing the shader of the model after importing it, we will change the shader at the time of loading model
For this
Search for the line -
var newMtl = new Material(Shader.Find("Standard (specular setup")) { name = materialName };
and change it to -
var newMtl = new Material(Shader.Find("Standard")) { name = materialName };
By this we are all set.
Subscribe to my newsletter
Read articles from Daman Ahuja directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by