Unity: Creating a texture from a XML file
Posted by Dimitri | Nov 7th, 2011 | Filed under Programming
This Unity3D programming tutorial explains how to read a XML file from the Resources folder and use the pixel color data defined there to create a Texture2D object. Basically, it’s the same as creating a texture from an array, but this array will be obtained from a XML file, instead of being defined in the script.
Since even a small image uses a large amount of tags on the XML file, an example project is available for download at the end of the post. This sample project has a XML file with 4096 items, that creates a 64×64 texture.
So, the first thing to do is to create a XML file, like the one exemplified below:
<?xml version="1.0" encoding="utf-8"> <resources> <image width="2" height="2"> <item>FF0000FF</item><item>00FF00FF</item> <item>0000FFFF</item><item>FFFF00FF</item> </image> </resources>
The XML above is pretty simple: it has a resources tag that encapsulates a the image tag, which is defining the image size using the width and height attributes. In this example a 2×2 image is being declared, so this image must include color information to fill four pixels. This is achieved by the elements between the item tags. They are hexadecimal value pairs for each color component, in the RGBA (red, green, blue, alpha) format. For example, the first pixel is FF0000FF is an opaque red color (R:255, G:0, B:0, A: 255), while the last one, FFFF00FF corresponds to a yellow color (R: 255, G:255, B:0, A:255).
This may seem counter intuitive at first, but there are a few advantages in this approach as opposed to defining each color channel as a separate element. The first and most easy to notice is the size, since, if we were to write this XML file again using an item tag for each color channel, it would take sixteen item elements instead of just four (four pixels, multiplied by four color channels). The other advantage is a gain of performance on the script, since the XML parser has a smaller amount of data to process.
Some readers might have noticed the lack of the 0x prefix before the hexadecimal values. That’s because, later on the script, we will see that not including these two characters avoids an additional operation when processing the obtained data.
So, to make it work with the script that is going to be explained on the next paragraphs, place this XML file into the Resources folder. If this folder isn’t already placed on your project, simply create it by right clicking anywhere in the Project tab and select Create->Folder. Place the XML file there.
As for the C# script, it will load the XML file and parse the pixel data obtained from it. Then, a Texture2D object is created and the pixel data is processed as Color objects and assigned to the texture. Lastly, it sets it as the main texture of the attached game object material and updates those changes onto the GPU memory, so it can be rendered on the screen. Here’s the code:
using UnityEngine; using System.Collections; using System.Xml; public class XMLintoTexture : MonoBehaviour { //the texture2D object that will be created private Texture2D texture; //an object to read data from the XML file private XmlDocument xml; //an object to store a node private XmlNodeList xmlNL; //a IEnumerator, which will be used to cycle through the data obtained from the XML file private IEnumerator ienum; //an object that stores a group of attributes for a given tag private XmlAttributeCollection imageDimensions; //a byte array, to store pixel color information private byte[] b = new byte[4]; //an array that holds the color information for all the pixels of the texture private Color[] pixelColorArray; void Awake() { //initialize the XML parser this.xml = new XmlDocument(); } // Use this for initialization void Start() { //load the xml file xml.LoadXml(Resources.Load("arrays").ToString()); //find the list of nodes that contain the pixel information xmlNL = xml.SelectNodes("resources/image/item"); //initialize the pixel color array with the number of pixels pixelColorArray = new Color[xmlNL.Count]; //return a enumerator from the 'item' tags ienum = xmlNL.GetEnumerator(); //store the attributes that contains the image dimensions imageDimensions = xml.SelectSingleNode("resources/image").Attributes; //initialize the Texture2D object, passing the obtained image size as the parameters texture = new Texture2D(int.Parse(imageDimensions.Item(0).Value), int.Parse(imageDimensions.Item(1).Value)); //set the anisotropic level ant the filtering mode texture.anisoLevel = 0; texture.filterMode = FilterMode.Point; //initialize a new color object Color pixelColor = new Color(); //the only purpose of this counter is to upload the Color information to the pixelColorArray sequentially int counter = 0; //loop through this enumerator while (ienum.MoveNext()) { //convert the returned hex string into color elements for (byte i = 0; i < 4; i++) { int offset = (b.Length - 1 - i) * 8; b[i] = (byte) ((int.Parse(((XmlElement)ienum.Current).InnerXml, System.Globalization.NumberStyles.AllowHexSpecifier) >> offset) & 0xFF); //Convert the bytes into color elements pixelColor[i] = b[i]/(float)255; } //upload the current color to the pixelColorArray pixelColorArray[counter] = pixelColor; counter++; } //set the pixel at the pixelColorArray into the texture texture.SetPixels(pixelColorArray, 0); //assign the recently created texture to the material renderer.material.mainTexture = texture; //apply the current texture, recalculate mipmaps and make this texture unwrittable texture.Apply(true, true); } }
The first thing one notices while looking at this script is that everything is happening at the Awake() and Start() methods. This is due to the fact that the operations performed by this script are computationally expensive. If it’s necessary to use it on the Update() method, please consider using a control statement to avoid executing it every frame.
Back to the script explanation, there are seven member variables being declared at it’s beginning. The first one is a Texture2D object that will be later applied as the main texture at the attached material (line 8). The next four objects are being declared to obtain the pixel data from the XML file, retrieve the attributes from a specific tag and to go through each element returned by the parser (lines 11 through 17). It’s important to note that in order to declare the objects from XML related classes, the System.XML library must be imported (line 3).
Finally, there is a byte array and an array of Color objects. The former is needed to convert the hexadecimal color values from the XML file to multiple Color objects and the latter is just where these Color objects are going to be stored after the conversion.
On the Awake() method, the XML parser is being initialized (line 27), and that’s the only thing it does, to ensure that the parser is ready when the Start() method is called. At the mentioned method, the XML file is being loaded from the Resources folder, and all nodes with the tag item are being selected and stored in a list (lines 34 and 37). Next, the array that stores all colors as Color objects is being initialized with the number of elements in the node list. That way, the pixelColorArray will have the same size as the item elements in the XML document (line 40).
The following lines obtains a IEnumerator, so that we can loop through all obtained hexadecimal color values (line 43). Next, the attributes from the image tag are being obtained, just so that the Texture2D object can be initialized with the correct width and height (lines 46 and 49). With the texture initialized, it’s now possible to assign some of it’s settings, such as the anisotropic level and the filtering mode (lines 52 and 53). They are set to 0 and Point, because the hard edges from the pixels must be preserved for this specific case, since the image is a pixel art. Therefore, these two settings heavily depends on the type of texture that’s being generated.
Then, a Color object and a integer counter are being declared and initialized. They are needed for the while loop that’s right below them, which will process each found item tag (lines 56 and 59). The Color will temporarily hold the current item color value after it has being converted. The integer counter is used to sequentially populate these colors onto the pixelColorArray.
The while loop that goes through each found item tag, converting the obtained element value from a hexadecimal string to a byte. That way, it’s possible to separate the hexadecimal color values for each color channel and create a Color object from it. This object is later stored at the pixelColorArray. For example, FF0000FF will be separated as Red: 255, Blue: 0, Green:0 and Alpha:255 .
The for loop is the one that executes this conversion. It’s heavily based on the code featured on this page. The data type change is achieved by the bit-wise operations, and the exact details of how it works are way are out of this tutorial’s scope.
Although, there are some details that are worth explaining, such as line 68. The code ((XmlElement)ienum.Current).InnerXml returns the element defined at the item tags. This element is a string of hexadecimal values and to convert it to an integer, the int.Parse() method has to be called. In this script it is using an overload that takes two parameters: the value we want to convert as a string and the additional parameter System.Globalization.NumberStyles.AllowHexSpecifier which defines that the string must be interpreted as a hexadecimal value. This only works because the hexadecimal strings doesn’t have the 0x prefix. If the strings had this prefix, it would be required to strip them for each value before calling the int.Parse() method.
When the current hexadecimal values are obtained and the bit-wise operations are completed, the resulting color is divided by 255 so it can be at the 0-1 range and is stored in the pixelColor object (line 70). Outside the for loop the pixelColor is stored at the pixelColorArray, and the counter is incremented, to store the next Color object at the next position on the array (line 74 and 76).
Finally, the pixelColorArray is assigned to the Texture2D object (line 80) and this texture is set as the attached material main texture (line 83). The last thing to do is update the GPU with the new texture, which is done at line 86. This line also makes all mipmaps to be regenerated and tells the system that this texture is no longer writable, freeing some resources.
That’s it!
Here’s a screenshot of the example project, and the download link below:
Downloads
Please, don’t open the XML file with MonoDevelop as it might crash since the code highlight feature will try to mark all of the 8197 tags! Use a simple text editor to open it (one that doesn’t have this feature).
How to make XML file contains a Texture with easily??