Here is the final tutorial of this long series. We’re going to see how to apply a texture to a mesh by using mapping coordinates exported from Blender. If you’ve managed to understand the previous tutorials, it will just be some piece of cake to apply some textures. The main concept is once again to interpolate some data between each vertex. In the second part of this tutorial, we’ll see how to boost the performance of our rendering algorithm. For that, we’re going to only display visible faces by using a back-face culling approach. But to go even further, we will use our last secret weapon: the GPU. You will then understand why OpenGL/WebGL and DirectX technologies are so important to make realtime 3D games. They help to use the GPU instead of the CPU to render our 3D objects. To really see the differences, we will load the exact same JSON file inside a WebGL 3D engine named Babylon.JS. The rendering will be far better and the FPS will be without any comparison, especially on low-end devices!
At the end of this tutorial, you will have this final rendering inside our CPU-based 3D software engine:
Texture mapping
Concept
Let’s start by the Wikipedia definition: Texture mapping : “A texture map is applied (mapped) to the surface of a shape or polygon. This process is akin to applying patterned paper to a plain white box. Every vertex in a polygon is assigned a texture coordinate (which in the 2d case is also known as a UV coordinate) either via explicit assignment or by procedural definition. Image sampling locations are then interpolated across the face of a polygon to produce a visual result that seems to have more richness than could otherwise be achieved with a limited number of polygons.”
Let’s now try to understand what this means exactly.
The first time I’ve tried to imagine how we could apply a texture to a 3D mesh was by first thinking about a cube, the first mesh we’ve drawn in this series. I was then thinking about taking an image acting as our texture and map it to each cube’s faces. This could work well in such a simple case. But the first problem will be: what if I’d like to apply a different image/texture on each cube’s faces? A first idea could be to take 6 different images for the 6 sides of your cube. To be even more precise, take 6 images, split them into 2 triangles that will be mapped to the 12 triangles of a cube.
But there is a simpler more elegant approach you’ve probably already played with when you were a child. This image will help you:
The very same approach works perfectly well with 3D engine. Imagine this image as the texture that will be applied to our cube. View it as 2 dimensional array of color’s bytes. We will be able to affect some 2D coordinates moving into this array to each cube’s vertex in order to obtain something like that:
Image taken from this forum thread: Texturing a cube in Blender, and going to pull my hair out
These 2D texture coordinates are known as UV coordinates.
Note: I’ve asked to a 3D guru what were the reasons to name them U & V? The answer was amazingly obvious: “Well, it’s because it’s just before X, Y, Z.”. I was expecting a more complex answer! ;)
You’re now probably asking to yourself how to handle advanced complex meshes like Suzanne, our beautiful monkey’s head, aren’t you?
For this kind of mesh, we’re also going to use a single 2D image that will be mapped in 3D. To build the corresponding texture, we need a planned 2D view of your mesh. This operation is known as an unwrap operation. If you’re a poor developer like myself, trust me, you’ll need a brilliant 3D designer like my friend Michel Rousseau to help you in this phase! And this is exactly what I’ve done: asking for help. :)
Using the Suzanne model as an example, after the unwrap operation, the designer will obtain this kind of result:
The designer will then paint this planned 2D view and the result will be the texture ready to be used by our engine. In our case, Michel Rousseau has done this job for us and here is his own version of Suzanne:
I know this result may looks weird the first time you try to understand texture mapping. But you should already see something that looks like an eye in the bottom right of the texture. This part will be mapped in 3D to both Suzanne’s eyes using a simple symmetrical operation to differentiate both eyes.
You now known the basics of texture’s mapping. To definitely understand how it works, please read these additional resources I’ve found for you on the web:
– Tutorial 16 – Basic Texture Mapping, read the first part that will help to understand how to map the UV coordinates (living between 0 and 1) to the triangles of our meshes
– Blender 2.6 Manual – UV Mapping a Mesh, that describes the various mapping types
– Tutorial 5 – Texture mapping, read the first part that will definitely help you to at least know how to map a cube. :)
Code
We’re now ready to dig into the code. There are several tasks to be done :
1 – Create a Texture class that will load the image acting as the texture and return the color associated to the U & V coordinates interpolated per pixel
2 – Add/Pass the Texture information in the complete rendering flow
3 – Parse the JSON file exported by the Babylon Blender’s Add-on to load the UV coordinates
The Texture logic
In HTML5 with TypeScript/JavaScript, we’re going of course to loading the texture by dynamically creating a canvas element and getting it’s associated image data to obtain our color bytes array.
With C#/XAML, we’re going to create a WriteableBitmap, set its source with the image we will load and get its PixelBuffer property to obtain our color bytes array.
public class Texture { private byte[] internalBuffer; private int width; private int height; // Working with a fix sized texture (512x512, 1024x1024, etc.). public Texture(string filename, int width, int height) { this.width = width; this.height = height; Load(filename); } async void Load(string filename) { var file = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(filename); using (var stream = await file.OpenReadAsync()) { var bmp = new WriteableBitmap(width, height); bmp.SetSource(stream); internalBuffer = bmp.PixelBuffer.ToArray(); } } // Takes the U & V coordinates exported by Blender // and return the corresponding pixel color in the texture public Color4 Map(float tu, float tv) { // Image is not loaded yet if (internalBuffer == null) { return Color4.White; } // using a % operator to cycle/repeat the texture if needed int u = Math.Abs((int) (tu*width) % width); int v = Math.Abs((int) (tv*height) % height); int pos = (u + v * width) * 4; byte b = internalBuffer[pos]; byte g = internalBuffer[pos + 1]; byte r = internalBuffer[pos + 2]; byte a = internalBuffer[pos + 3]; return new Color4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); }
export class Texture { width: number; height: number; internalBuffer: ImageData; // Working with a fix sized texture (512x512, 1024x1024, etc.). constructor(filename: string, width: number, height: number) { this.width = width; this.height = height; this.load(filename); } public load(filename: string): void { var imageTexture = new Image(); imageTexture.height = this.height; imageTexture.width = this.width; imageTexture.onload = () => { var internalCanvas: HTMLCanvasElement = document.createElement("canvas"); internalCanvas.width = this.width; internalCanvas.height = this.height; var internalContext: CanvasRenderingContext2D = internalCanvas.getContext("2d"); internalContext.drawImage(imageTexture, 0, 0); this.internalBuffer = internalContext.getImageData(0, 0, this.width, this.height); }; imageTexture.src = filename; } // Takes the U & V coordinates exported by Blender // and return the corresponding pixel color in the texture public map(tu: number, tv: number): BABYLON.Color4 { if (this.internalBuffer) { // using a % operator to cycle/repeat the texture if needed var u = Math.abs(((tu * this.width) % this.width)) >> 0; var v = Math.abs(((tv * this.height) % this.height)) >> 0; var pos = (u + v * this.width) * 4; var r = this.internalBuffer.data[pos]; var g = this.internalBuffer.data[pos + 1]; var b = this.internalBuffer.data[pos + 2]; var a = this.internalBuffer.data[pos + 3]; return new BABYLON.Color4(r / 255.0, g / 255.0, b / 255.0, a / 255.0); } // Image is not loaded yet else { return new BABYLON.Color4(1, 1, 1, 1); } }
var Texture = (function () { // Working with a fix sized texture (512x512, 1024x1024, etc.). function Texture(filename, width, height) { this.width = width; this.height = height; this.load(filename); } Texture.prototype.load = function (filename) { var _this = this; var imageTexture = new Image(); imageTexture.height = this.height; imageTexture.width = this.width; imageTexture.onload = function () { var internalCanvas = document.createElement("canvas"); internalCanvas.width = _this.width; internalCanvas.height = _this.height; var internalContext = internalCanvas.getContext("2d"); internalContext.drawImage(imageTexture, 0, 0); _this.internalBuffer = internalContext.getImageData(0, 0, _this.width, _this.height); }; imageTexture.src = filename; }; // Takes the U & V coordinates exported by Blender // and return the corresponding pixel color in the texture Texture.prototype.map = function (tu, tv) { if (this.internalBuffer) { // using a % operator to cycle/repeat the texture if needed var u = Math.abs(((tu * this.width) % this.width)) >> 0; var v = Math.abs(((tv * this.height) % this.height)) >> 0; var pos = (u + v * this.width) * 4; var r = this.internalBuffer.data[pos]; var g = this.internalBuffer.data[pos + 1]; var b = this.internalBuffer.data[pos + 2]; var a = this.internalBuffer.data[pos + 3]; return new BABYLON.Color4(r / 255.0, g / 255.0, b / 255.0, a / 255.0); } // Image is not loaded yet else { return new BABYLON.Color4(1, 1, 1, 1); } }; return Texture; )(); oftEngine.Texture = Texture;
Pass the Texture information in the flow
I won’t dig into every detail as you’ve got the complete source to download a bit below. Let’s rather review globally what you need to do:
– add a Texture property to the Mesh class and a Vector2 property named TextureCoordinates to the Vertex structure
– update ScanLineData to embed 8 more floats/numbers : the UV coordinates per vertex (ua, ub, uc, ud & va, vb, vc, vd).
– update the Project method/function to return a new Vertex with the TextureCoordinates passed as-is (pass through)
– pass a Texture object as the last parameter to the ProcessScanLine, DrawTriangle methods/functions
– Fill the new ScanLineData structure in drawTriangle with the appropriate UV coordinates
– Interpolate the UV in ProcessScanLine on Y to have SU/SV & EU/EV (start U/start V/End U/End V) then interpolate U, V on X, find the corresponding color with it in the texture. This color texture will be mixed with the native object’s color (always white in our tutorials case) and the light quantity measured with the NDotL operation with the normal.
Note: our Project method could be seen as what we name a “Vertex Shader” in a 3D hardware engine and our ProcessScanLine could be seen as a “Pixel Shader”.
I’m sharing in this article only the new ProcessScanLine method which is really the main part to be updated:
void ProcessScanLine(ScanLineData data, Vertex va, Vertex vb, Vertex vc, Vertex vd, Color4 color, Texture texture) Vector3 pa = va.Coordinates; Vector3 pb = vb.Coordinates; Vector3 pc = vc.Coordinates; Vector3 pd = vd.Coordinates; // Thanks to current Y, we can compute the gradient to compute others values like // the starting X (sx) and ending X (ex) to draw between // if pa.Y == pb.Y or pc.Y == pd.Y, gradient is forced to 1 var gradient1 = pa.Y != pb.Y ? (data.currentY - pa.Y) / (pb.Y - pa.Y) : 1; var gradient2 = pc.Y != pd.Y ? (data.currentY - pc.Y) / (pd.Y - pc.Y) : 1; int sx = (int)Interpolate(pa.X, pb.X, gradient1); int ex = (int)Interpolate(pc.X, pd.X, gradient2); // starting Z & ending Z float z1 = Interpolate(pa.Z, pb.Z, gradient1); float z2 = Interpolate(pc.Z, pd.Z, gradient2); // Interpolating normals on Y var snl = Interpolate(data.ndotla, data.ndotlb, gradient1); var enl = Interpolate(data.ndotlc, data.ndotld, gradient2); // Interpolating texture coordinates on Y var su = Interpolate(data.ua, data.ub, gradient1); var eu = Interpolate(data.uc, data.ud, gradient2); var sv = Interpolate(data.va, data.vb, gradient1); var ev = Interpolate(data.vc, data.vd, gradient2); // drawing a line from left (sx) to right (ex) for (var x = sx; x < ex; x++) { float gradient = (x - sx) / (float)(ex - sx); // Interpolating Z, normal and texture coordinates on X var z = Interpolate(z1, z2, gradient); var ndotl = Interpolate(snl, enl, gradient); var u = Interpolate(su, eu, gradient); var v = Interpolate(sv, ev, gradient); Color4 textureColor; if (texture != null) textureColor = texture.Map(u, v); else textureColor = new Color4(1, 1, 1, 1); // changing the native color value using the cosine of the angle // between the light vector and the normal vector // and the texture color DrawPoint(new Vector3(x, data.currentY, z), color * ndotl * textureColor); }
public processScanLine(data: ScanLineData, va: Vertex, vb: Vertex, vc: Vertex, vd: Vertex, color: BABYLON.Color4, texture?: Texture): void { var pa = va.Coordinates; var pb = vb.Coordinates; var pc = vc.Coordinates; var pd = vd.Coordinates; // Thanks to current Y, we can compute the gradient to compute others values like // the starting X (sx) and ending X (ex) to draw between // if pa.Y == pb.Y or pc.Y == pd.Y, gradient is forced to 1 var gradient1 = pa.y != pb.y ? (data.currentY - pa.y) / (pb.y - pa.y) : 1; var gradient2 = pc.y != pd.y ? (data.currentY - pc.y) / (pd.y - pc.y) : 1; var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0; var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0; // starting Z & ending Z var z1: number = this.interpolate(pa.z, pb.z, gradient1); var z2: number = this.interpolate(pc.z, pd.z, gradient2); // Interpolating normals on Y var snl = this.interpolate(data.ndotla, data.ndotlb, gradient1); var enl = this.interpolate(data.ndotlc, data.ndotld, gradient2); // Interpolating texture coordinates on Y var su = this.interpolate(data.ua, data.ub, gradient1); var eu = this.interpolate(data.uc, data.ud, gradient2); var sv = this.interpolate(data.va, data.vb, gradient1); var ev = this.interpolate(data.vc, data.vd, gradient2); // drawing a line from left (sx) to right (ex) for (var x = sx; x < ex; x++) { var gradient: number = (x - sx) / (ex - sx); // Interpolating Z, normal and texture coordinates on X var z = this.interpolate(z1, z2, gradient); var ndotl = this.interpolate(snl, enl, gradient); var u = this.interpolate(su, eu, gradient); var v = this.interpolate(sv, ev, gradient); var textureColor; if (texture) textureColor = texture.map(u, v); else textureColor = new BABYLON.Color4(1, 1, 1, 1); // changing the native color value using the cosine of the angle // between the light vector and the normal vector // and the texture color this.drawPoint(new BABYLON.Vector3(x, data.currentY, z),
new BABYLON.Color4(color.r * ndotl * textureColor.r,
color.g * ndotl * textureColor.g,
color.b * ndotl * textureColor.b, 1)); }
Device.prototype.processScanLine = function (data, va, vb, vc, vd, color, texture) { var pa = va.Coordinates; var pb = vb.Coordinates; var pc = vc.Coordinates; var pd = vd.Coordinates; // Thanks to current Y, we can compute the gradient to compute others values like // the starting X (sx) and ending X (ex) to draw between // if pa.Y == pb.Y or pc.Y == pd.Y, gradient is forced to 1 var gradient1 = pa.y != pb.y ? (data.currentY - pa.y) / (pb.y - pa.y) : 1; var gradient2 = pc.y != pd.y ? (data.currentY - pc.y) / (pd.y - pc.y) : 1; var sx = this.interpolate(pa.x, pb.x, gradient1) >> 0; var ex = this.interpolate(pc.x, pd.x, gradient2) >> 0; // starting Z & ending Z var z1 = this.interpolate(pa.z, pb.z, gradient1); var z2 = this.interpolate(pc.z, pd.z, gradient2); // Interpolating normals on Y var snl = this.interpolate(data.ndotla, data.ndotlb, gradient1); var enl = this.interpolate(data.ndotlc, data.ndotld, gradient2); // Interpolating texture coordinates on Y var su = this.interpolate(data.ua, data.ub, gradient1); var eu = this.interpolate(data.uc, data.ud, gradient2); var sv = this.interpolate(data.va, data.vb, gradient1); var ev = this.interpolate(data.vc, data.vd, gradient2); // drawing a line from left (sx) to right (ex) for (var x = sx; x < ex; x++) { var gradient = (x - sx) / (ex - sx); // Interpolating Z, normal and texture coordinates on X var z = this.interpolate(z1, z2, gradient); var ndotl = this.interpolate(snl, enl, gradient); var u = this.interpolate(su, eu, gradient); var v = this.interpolate(sv, ev, gradient); var textureColor; if (texture) textureColor = texture.map(u, v); else textureColor = new BABYLON.Color4(1, 1, 1, 1); // changing the native color value using the cosine of the angle // between the light vector and the normal vector // and the texture color this.drawPoint(new BABYLON.Vector3(x, data.currentY, z),
new BABYLON.Color4(color.r * ndotl * textureColor.r,
color.g * ndotl * textureColor.g,
color.b * ndotl * textureColor.b, 1)); } ;
If you’ve followed all the previous tutorials to build your own version, please download the code of my solutions to review other slight modifications to put in your own project.
Loading the information from the Babylon JSON file format
To be able to have the nice rendering you’ve seen at the top of this article, you need to load a new version of Suzanne modified by Michel Rousseau and exported from Blender with the UV coordinates. For that, please download those 2 files:
– Suzanne Blender model with UV coordinates set: https://david.blob.core.windows.net/softengine3d/part6/monkey.babylon
– the 512×512 texture image to load with: https://david.blob.core.windows.net/softengine3d/part6/Suzanne.jpg
The Babylon.JSON format file of David Catuhe is containing a lot of details we won’t cover in this series. For instance, something that could be fun for you to play with is the material. Indeed, the designer can assign a particular material to a mesh. In our case, we’re only going to handle a diffuse texture. If you want to implement more of them, have a look to David Catuhe’s article as a base: Babylon.js: Unleash the StandardMaterial for your babylon.js game
Again, I’m sharing with you only the main part to change: the method/function loading & parsing the JSON file.
// Loading the JSON file in an asynchronous manner public async Task<Mesh[]> LoadJSONFileAsync(string fileName) var meshes = new List<Mesh>(); var materials = new Dictionary<String,Material>(); var file = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(fileName); var data = await Windows.Storage.FileIO.ReadTextAsync(file); dynamic jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject(data); for (var materialIndex = 0; materialIndex < jsonObject.materials.Count; materialIndex++) { var material = new Material(); material.Name = jsonObject.materials[materialIndex].name.Value; material.ID = jsonObject.materials[materialIndex].id.Value; if (jsonObject.materials[materialIndex].diffuseTexture != null) material.DiffuseTextureName = jsonObject.materials[materialIndex].diffuseTexture.name.Value; materials.Add(material.ID, material); } for (var meshIndex = 0; meshIndex < jsonObject.meshes.Count; meshIndex++) { var verticesArray = jsonObject.meshes[meshIndex].vertices; // Faces var indicesArray = jsonObject.meshes[meshIndex].indices; var uvCount = jsonObject.meshes[meshIndex].uvCount.Value; var verticesStep = 1; // Depending of the number of texture's coordinates per vertex // we're jumping in the vertices array by 6, 8 & 10 windows frame switch ((int)uvCount) { case 0: verticesStep = 6; break; case 1: verticesStep = 8; break; case 2: verticesStep = 10; break; } // the number of interesting vertices information for us var verticesCount = verticesArray.Count / verticesStep; // number of faces is logically the size of the array divided by 3 (A, B, C) var facesCount = indicesArray.Count / 3; var mesh = new Mesh(jsonObject.meshes[meshIndex].name.Value, verticesCount, facesCount); // Filling the Vertices array of our mesh first for (var index = 0; index < verticesCount; index++) { var x = (float)verticesArray[index * verticesStep].Value; var y = (float)verticesArray[index * verticesStep + 1].Value; var z = (float)verticesArray[index * verticesStep + 2].Value; // Loading the vertex normal exported by Blender var nx = (float)verticesArray[index * verticesStep + 3].Value; var ny = (float)verticesArray[index * verticesStep + 4].Value; var nz = (float)verticesArray[index * verticesStep + 5].Value; mesh.Vertices[index] = new Vertex { Coordinates = new Vector3(x, y, z), Normal = new Vector3(nx, ny, nz) }; if (uvCount > 0) { // Loading the texture coordinates float u = (float)verticesArray[index * verticesStep + 6].Value; float v = (float)verticesArray[index * verticesStep + 7].Value; mesh.Vertices[index].TextureCoordinates = new Vector2(u, v); } } // Then filling the Faces array for (var index = 0; index < facesCount; index++) { var a = (int)indicesArray[index * 3].Value; var b = (int)indicesArray[index * 3 + 1].Value; var c = (int)indicesArray[index * 3 + 2].Value; mesh.Faces[index] = new Face { A = a, B = b, C = c }; } // Getting the position you've set in Blender var position = jsonObject.meshes[meshIndex].position; mesh.Position = new Vector3((float)position[0].Value, (float)position[1].Value, (float)position[2].Value); if (uvCount > 0) { // Texture var meshTextureID = jsonObject.meshes[meshIndex].materialId.Value; var meshTextureName = materials[meshTextureID].DiffuseTextureName; mesh.Texture = new Texture(meshTextureName, 512, 512); } meshes.Add(mesh); } return meshes.ToArray();
private CreateMeshesFromJSON(jsonObject): Mesh[] { var meshes: Mesh[] = []; var materials: Material[] = []; for (var materialIndex = 0; materialIndex < jsonObject.materials.length; materialIndex++) { var material: Material = {}; material.Name = jsonObject.materials[materialIndex].name; material.ID = jsonObject.materials[materialIndex].id; if (jsonObject.materials[materialIndex].diffuseTexture) material.DiffuseTextureName = jsonObject.materials[materialIndex].diffuseTexture.name; materials[material.ID] = material; } for (var meshIndex = 0; meshIndex < jsonObject.meshes.length; meshIndex++) { var verticesArray: number[] = jsonObject.meshes[meshIndex].vertices; // Faces var indicesArray: number[] = jsonObject.meshes[meshIndex].indices; var uvCount: number = jsonObject.meshes[meshIndex].uvCount; var verticesStep = 1; // Depending of the number of texture's coordinates per vertex // we're jumping in the vertices array by 6, 8 & 10 windows frame switch (uvCount) { case 0: verticesStep = 6; break; case 1: verticesStep = 8; break; case 2: verticesStep = 10; break; } // the number of interesting vertices information for us var verticesCount = verticesArray.length / verticesStep; // number of faces is logically the size of the array divided by 3 (A, B, C) var facesCount = indicesArray.length / 3; var mesh = new SoftEngine.Mesh(jsonObject.meshes[meshIndex].name, verticesCount, facesCount); // Filling the Vertices array of our mesh first for (var index = 0; index < verticesCount; index++) { var x = verticesArray[index * verticesStep]; var y = verticesArray[index * verticesStep + 1]; var z = verticesArray[index * verticesStep + 2]; // Loading the vertex normal exported by Blender var nx = verticesArray[index * verticesStep + 3]; var ny = verticesArray[index * verticesStep + 4]; var nz = verticesArray[index * verticesStep + 5]; mesh.Vertices[index] = { Coordinates: new BABYLON.Vector3(x, y, z), Normal: new BABYLON.Vector3(nx, ny, nz) }; if (uvCount > 0) { // Loading the texture coordinates var u = verticesArray[index * verticesStep + 6]; var v = verticesArray[index * verticesStep + 7]; mesh.Vertices[index].TextureCoordinates = new BABYLON.Vector2(u, v); } else { mesh.Vertices[index].TextureCoordinates = new BABYLON.Vector2(0, 0); } } // Then filling the Faces array for (var index = 0; index < facesCount; index++) { var a = indicesArray[index * 3]; var b = indicesArray[index * 3 + 1]; var c = indicesArray[index * 3 + 2]; mesh.Faces[index] = { A: a, B: b, C: c }; } // Getting the position you've set in Blender var position = jsonObject.meshes[meshIndex].position; mesh.Position = new BABYLON.Vector3(position[0], position[1], position[2]); if (uvCount > 0) { var meshTextureID = jsonObject.meshes[meshIndex].materialId; var meshTextureName = materials[meshTextureID].DiffuseTextureName; mesh.Texture = new Texture(meshTextureName, 512, 512); } meshes.push(mesh); } return meshes;
Device.prototype.CreateMeshesFromJSON = function (jsonObject) { var meshes = []; var materials = []; for (var materialIndex = 0; materialIndex < jsonObject.materials.length; materialIndex++) { var material = {}; material.Name = jsonObject.materials[materialIndex].name; material.ID = jsonObject.materials[materialIndex].id; if (jsonObject.materials[materialIndex].diffuseTexture) material.DiffuseTextureName = jsonObject.materials[materialIndex].diffuseTexture.name; materials[material.ID] = material; } for (var meshIndex = 0; meshIndex < jsonObject.meshes.length; meshIndex++) { var verticesArray = jsonObject.meshes[meshIndex].vertices; // Faces var indicesArray = jsonObject.meshes[meshIndex].indices; var uvCount = jsonObject.meshes[meshIndex].uvCount; var verticesStep = 1; // Depending of the number of texture's coordinates per vertex // we're jumping in the vertices array by 6, 8 & 10 windows frame switch (uvCount) { case 0: verticesStep = 6; break; case 1: verticesStep = 8; break; case 2: verticesStep = 10; break; } // the number of interesting vertices information for us var verticesCount = verticesArray.length / verticesStep; // number of faces is logically the size of the array divided by 3 (A, B, C) var facesCount = indicesArray.length / 3; var mesh = new SoftEngine.Mesh(jsonObject.meshes[meshIndex].name, verticesCount, facesCount); // Filling the Vertices array of our mesh first for (var index = 0; index < verticesCount; index++) { var x = verticesArray[index * verticesStep]; var y = verticesArray[index * verticesStep + 1]; var z = verticesArray[index * verticesStep + 2]; // Loading the vertex normal exported by Blender var nx = verticesArray[index * verticesStep + 3]; var ny = verticesArray[index * verticesStep + 4]; var nz = verticesArray[index * verticesStep + 5]; mesh.Vertices[index] = { Coordinates: new BABYLON.Vector3(x, y, z), Normal: new BABYLON.Vector3(nx, ny, nz) }; if (uvCount > 0) { // Loading the texture coordinates var u = verticesArray[index * verticesStep + 6]; var v = verticesArray[index * verticesStep + 7]; mesh.Vertices[index].TextureCoordinates = new BABYLON.Vector2(u, v); } else { mesh.Vertices[index].TextureCoordinates = new BABYLON.Vector2(0, 0); } } // Then filling the Faces array for (var index = 0; index < facesCount; index++) { var a = indicesArray[index * 3]; var b = indicesArray[index * 3 + 1]; var c = indicesArray[index * 3 + 2]; mesh.Faces[index] = { A: a, B: b, C: c }; } // Getting the position you've set in Blender var position = jsonObject.meshes[meshIndex].position; mesh.Position = new BABYLON.Vector3(position[0], position[1], position[2]); if (uvCount > 0) { var meshTextureID = jsonObject.meshes[meshIndex].materialId; var meshTextureName = materials[meshTextureID].DiffuseTextureName; mesh.Texture = new Texture(meshTextureName, 512, 512); } meshes.push(mesh); } return meshes; ;
Thanks to all those modifications, we now have this beautiful rendering showing Suzanne textured with a gouraud shading algorithm:
3D Software engine : view Suzanne textured with a gouraud shading in HTML5 in your browser
You can download the solution implementing this Texture Mapping algorithm here:
– C# : SoftEngineCSharpPart6Sample1.zip
– TypeScript : SoftEngineTSPart6Sample1.zip
– JavaScript : SoftEngineJSPart6Sample1.zip or simply right-click –> view source on the above HTML5 demo
Performance is not huge. I’m running the C# version in 1600×900 at an average of 18 fps on my machine and the HTML5 version in 640×480 at an average of 15 fps in IE11.
But before requesting the help of the GPU, let’s have a look to the final optimization of your 3D software engine.
Back-face culling
Let’s start again by reading the definition from Wikipedia: Back-face culling : “In computer graphics, back-face culling determines whether a polygon of a graphical object is visible <…> One method of implementing back-face culling is by discarding all polygons where the dot product of their surface normal and the camera-to-polygon vector is greater than or equal to zero.”
The idea is our case is then to pre-compute each surface normal of a mesh during the JSON loading phase using the same algorithm used in the previous tutorial for flat shading. Once done, in Render method/function, we will transform the coordinates of the surface normal into the world view (the world viewed by the camera) and check its Z value. If it’s >= 0, we won’t draw the triangle at all as this means that this face is not visible from the camera view point.
3D Software engine : view Suzanne textured with a gouraud shading in HTML5 with back-face culling enabled
You can download the solution implementing this back-face algorithm here:
– C# : SoftEngineCSharpPart6Sample2.zip
– TypeScript : SoftEngineTSPart6Sample2.zip
– JavaScript : SoftEngineJSPart6Sample2.zip or simply right-click –> view source on the above HTML5 demo
Note: you’ll notice I’ve got a small rendering bug in my back-face culling implementation. A very few triangles are not drawn whereas they should be. This is because we should adjust the transformation of the normal to take into account the current perspective of the camera. The current algorithm makes the assumption that we have an orthogonal camera which is not the case. It could be a good exercise for you to fix that! :)
The performance boost is interesting and is around 66% as I’m switching from an average of 15 fps in IE11 to 25 fps with back-face culling enabled.
Rendering with WebGL thanks to Babylon.JS
Today’s modern 3D games are of course using the GPU. The aim of this series was really to understand the basics of 3D by building your own 3D software engine. Once you’ve been able to understand the 6 parts of the series, jumping into 3D engine using OpenGL/WebGL or DirectX will be much easier.
On our side, we’ve been working on a suite of frameworks in France to let developers building HTML5 3D games in an very easy way. The first step was the release of Babylon.JS built by David Catuhe. But we’re working on other cool frameworks on top of his awesome 3D engine to help you building your WebGL games.
David has started a tutorials series on his blog on how to use his 3D WebGL engine. The entry point is here: Babylon.js: a complete JavaScript framework for building 3D games with HTML 5 and WebGL
By taking this tutorial: Babylon.js: How to load a .babylon file produced with Blender, you’ll be able to reload our mesh used in this series and benefits from GPU hardware acceleration in the browser!
If you’ve got IE11, Chrome or Firefox or any WebGL compatible device/browser, you can test the result here:
Babylon.JS – 3D WebGL engine : view Suzanne textured and now hardware-accelerated!
Thanks to WebGL, we’re having a huge performance boost. For instance, on my Surface RT updated in Windows 8.1 preview, using IE11, I’m switching from less than 4 fps in 640×480 with my 3D soft engine to 60 FPS in 1366×768!
This series is now finished. I had a lot of pleasure to write it. I’ve received a lot of awesome feedbacks and some of you have ported the series in Java (by Yannick Comte), on Windows CE and in WPF! I’m so pleased to see it was useful to some of you and to discover forks of the code. Feel free to share your own version in the comments.
I’ll been soon writing a new series of tutorials on a framework we’re currently working on to build 3D games. Stay tuned!
Originally published: https://blogs.msdn.com/b/davrous/archive/2013/07/18/tutorial-part-6-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-texture-mapping-back-face-culling-amp-webgl.aspx. Reprinted here with permission of the author.
Frequently Asked Questions about 3D Software Engine and Texture Mapping
What is texture mapping in 3D graphics?
Texture mapping is a method used in 3D graphics to add detail, surface texture, or color to a 3D model. This technique involves applying a 2D image, known as a texture map, onto the surface of a 3D model. The texture map contains the color and pattern that will be applied to the model, giving it a more realistic appearance. This process is crucial in 3D modeling and animation, as it adds depth and detail to the models, making them more visually appealing.
How does texture mapping work in a 3D software engine?
In a 3D software engine, texture mapping works by mapping a 2D image onto a 3D object. The 2D image, or texture, is divided into a grid of small squares, known as texels. Each texel corresponds to a specific point on the 3D object’s surface. The software engine then uses algorithms to determine how the texels should be displayed on the object’s surface, taking into account factors like the object’s shape, the viewing angle, and the lighting conditions.
What are the different types of texture mapping?
There are several types of texture mapping used in 3D graphics, including diffuse mapping, specular mapping, bump mapping, normal mapping, and displacement mapping. Diffuse mapping is the most basic type, which involves applying a flat texture to the surface of a 3D object. Specular mapping adds shininess and reflectivity to the object’s surface. Bump mapping, normal mapping, and displacement mapping are more advanced techniques that add depth and detail to the object’s surface by simulating bumps and ridges.
How can I create my own texture maps?
Creating your own texture maps can be done using image editing software like Photoshop or GIMP. You can start by creating a 2D image that will serve as your texture. This image can be a photograph, a digital painting, or any other type of 2D artwork. Once you have your image, you can then edit it to fit the specific needs of your 3D model. This might involve adjusting the image’s size, color, and pattern, or adding special effects like noise or blur.
What are the challenges in texture mapping?
Texture mapping in 3D graphics can present several challenges. One of the main challenges is dealing with distortion. Since texture mapping involves applying a 2D image onto a 3D object, distortion can occur, especially on complex shapes. Another challenge is creating seamless textures, especially for objects that need to be tiled or repeated. Additionally, creating realistic textures can be difficult, as it requires a good understanding of how light interacts with different materials.
How can I improve the quality of my texture maps?
Improving the quality of your texture maps can be achieved by using high-resolution images for your textures, ensuring that your textures are seamless, and using appropriate mapping techniques for your specific needs. Additionally, using advanced mapping techniques like bump mapping, normal mapping, or displacement mapping can add depth and detail to your textures, making them more realistic.
What is the role of UV mapping in texture mapping?
UV mapping is a crucial part of the texture mapping process. It involves assigning each vertex of a 3D model a coordinate in the 2D texture map. This coordinate, known as a UV coordinate, determines which part of the texture map will be displayed on each vertex of the model. UV mapping ensures that the texture is correctly applied to the model, with minimal distortion.
How does lighting affect texture mapping?
Lighting plays a significant role in texture mapping. It affects how the texture appears on the 3D model, influencing factors like color, brightness, and contrast. Different lighting conditions can dramatically change the appearance of a texture, making it look more or less realistic. Therefore, when applying textures in a 3D software engine, it’s important to consider how the textures will look under different lighting conditions.
Can I use texture mapping in 2D graphics?
While texture mapping is most commonly used in 3D graphics, it can also be used in 2D graphics to add detail and depth to flat images. In 2D graphics, texture mapping involves applying a texture to a 2D shape or surface. This can be used to create effects like patterns, gradients, or noise.
What are some resources for learning more about texture mapping?
There are many resources available for learning more about texture mapping. Online tutorials, video courses, and textbooks on 3D graphics often cover texture mapping in detail. Websites like Stack Overflow and GitHub can also be useful resources, as they often have discussions and code examples related to texture mapping. Additionally, software documentation for 3D software engines often includes information on how to use texture mapping in that specific engine.
David Rousset is a Senior Program Manager at Microsoft, in charge of driving adoption of HTML5 standards. He has been a speaker at several famous web conferences such as Paris Web, CodeMotion, ReasonsTo or jQuery UK. He’s the co-author of the WebGL Babylon.js open-source engine. Read his blog on MSDN or follow him on Twitter.