This part will cover loading texture coordinates and normals from a file, calculating normals for geometry that doesn't come with them (such as shapes we're generating through code) and loading basic materials from a file.
(UPDATED 2015-11-24 to fix some issues with loading normals from a file)
(UPDATED 2015-12-16 to make the MTL file have materials that look nicer under lighting)
(UPDATED 2018-11-06 to make compatible with input handling changes in Tutorial 5)
First, we need to lay some groundwork for the rest of the tutorial. We will be changing the Volume class to store normals (and their associated information).
The normals will be stored as an array defined in the Volume class. Why isn't it like the vertex, color, or texture data? To make things convenient, a function will be added to the Volume class to calculate the normals, but if each subclass stored normals differently, the function would need to be redefined in each one. Add the array to the class:
Vector3[] Normals = new Vector3[0];
Now add a new property to the Volume class to store the number of normals the volume has. This might not always be equal to the number of vertices or indices, so it's best to make it simply linked to the array we stored the normals in:
public virtual int NormalCount { get { return Normals.Length; } }
To make our normals data available in the rest of the program, add a function to the class as well (the Normals array could've just been made public, but this makes it match up nicely with the existing code):
public virtual Vector3[] GetNormals() { return Normals; }
A "normal" is a vector perpendicular to a surface. In our case, the surface will be the triangles forming the shape of a model.
Normals on a curved surface (from Wikimedia Commons) |
Fortunately, the normal vector is very easy to calculate. The cross product of two vectors is a third vector perpendicular to both of the other two vectors. This means that we can calculate the normal vector by finding the cross product of two vectors making up the edges of our triangles.
Keep in mind that this may result in either "flat" looking shading, or objects being shaded like they don't have edges when the normals are generated this way. Normals output by a 3D modelling program will allow for better control, including things like smoothing groups.
Add this function to the Volume class:
public void CalculateNormals() { Vector3[] normals = new Vector3[VertCount]; Vector3[] verts = GetVerts(); int[] inds = GetIndices(); // Compute normals for each face for (int i = 0; i < IndiceCount; i += 3) { Vector3 v1 = verts[inds[i]]; Vector3 v2 = verts[inds[i + 1]]; Vector3 v3 = verts[inds[i + 2]]; // The normal is the cross product of two sides of the triangle normals[inds[i]] += Vector3.Cross(v2 - v1, v3 - v1); normals[inds[i + 1]] += Vector3.Cross(v2 - v1, v3 - v1); normals[inds[i + 2]] += Vector3.Cross(v2 - v1, v3 - v1); } for (int i = 0; i < NormalCount; i++) { normals[i] = normals[i].Normalized(); } Normals = normals; }
Now that we have normals included, we have one more change to make in the Volume class. Replace the existing definition for TextureCoordsCount with the following:
public virtual int TextureCoordsCount { get; set; }
This adds some additional flexibility required for the improved OBJ loader, which is the next step.
Improving the OBJ loader will also require some work before the main course. Add the following class to the end (but still inside the namespace!) of the ObjVolume.cs file:
class FaceVertex { public Vector3 Position; public Vector3 Normal; public Vector2 TextureCoord; public FaceVertex(Vector3 pos, Vector3 norm, Vector2 texcoord) { Position = pos; Normal = norm; TextureCoord = texcoord; } }
In the new OBJ loader, each face uses three vertices with a position, normal vector and a texture coordinate. As these might not line up nicely, we'll need to store information about each vertex of each face.
To begin updating the ObjVolume class to use this class, we need to replace the definition for the faces variable. Replace it with the following:
private List<Tuple<FaceVertex, FaceVertex, FaceVertex>> faces = new List<Tuple<FaceVertex, FaceVertex, FaceVertex>>();
The GetVerts, GetIndices, GetColorData, and GetTextureCoords functions need to be adapted too:
/// <summary> /// Get vertice data for this object /// </summary> /// <returns></returns> public override Vector3[] GetVerts() { List<Vector3> verts = new List<Vector3>(); foreach (var face in faces) { verts.Add(face.Item1.Position); verts.Add(face.Item2.Position); verts.Add(face.Item3.Position); } return verts.ToArray(); } /// <summary> /// Get indices /// </summary> /// <param name="offset"></param> /// <returns></returns> public override int[] GetIndices(int offset = 0) { return Enumerable.Range(offset, IndiceCount).ToArray(); } /// <summary> /// Get color data. /// </summary> /// <returns></returns> public override Vector3[] GetColorData() { return new Vector3[ColorDataCount]; } /// <summary> /// Get texture coordinates. /// </summary> /// <returns></returns> public override Vector2[] GetTextureCoords() { List<Vector2> coords = new List<Vector2>(); foreach (var face in faces) { coords.Add(face.Item1.TextureCoord); coords.Add(face.Item2.TextureCoord); coords.Add(face.Item3.TextureCoord); } return coords.ToArray(); }
Now, replace the existing LoadFromString function in the ObjVolume class with the following (warning, long code ahead!):
public static ObjVolume LoadFromString(string obj) { // Seperate lines from the file List<String> lines = new List<string>(obj.Split('\n')); // Lists to hold model data List<Vector3> verts = new List<Vector3>(); List<Vector3> normals = new List<Vector3>(); List<Vector2> texs = new List<Vector2>(); List<Tuple<TempVertex, TempVertex, TempVertex>> faces = new List<Tuple<TempVertex, TempVertex, TempVertex>>(); // Base values verts.Add(new Vector3()); texs.Add(new Vector2()); normals.Add(new Vector3()); int currentindice = 0; // Read file line by line foreach (String line in lines) { if (line.StartsWith("v ")) // Vertex definition { // Cut off beginning of line String temp = line.Substring(2); Vector3 vec = new Vector3(); if (temp.Trim().Count((char c) => c == ' ') == 2) // Check if there's enough elements for a vertex { String[] vertparts = temp.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // Attempt to parse each part of the vertice bool success = float.TryParse(vertparts[0], out vec.X); success |= float.TryParse(vertparts[1], out vec.Y); success |= float.TryParse(vertparts[2], out vec.Z); // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing vertex: {0}", line); } } else { Console.WriteLine("Error parsing vertex: {0}", line); } verts.Add(vec); } else if (line.StartsWith("vt ")) // Texture coordinate { // Cut off beginning of line String temp = line.Substring(2); Vector2 vec = new Vector2(); if (temp.Trim().Count((char c) => c == ' ') > 0) // Check if there's enough elements for a vertex { String[] texcoordparts = temp.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // Attempt to parse each part of the vertice bool success = float.TryParse(texcoordparts[0], out vec.X); success |= float.TryParse(texcoordparts[1], out vec.Y); // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing texture coordinate: {0}", line); } } else { Console.WriteLine("Error parsing texture coordinate: {0}", line); } texs.Add(vec); } else if (line.StartsWith("vn ")) // Normal vector { // Cut off beginning of line String temp = line.Substring(2); Vector3 vec = new Vector3(); if (temp.Trim().Count((char c) => c == ' ') == 2) // Check if there's enough elements for a normal { String[] vertparts = temp.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // Attempt to parse each part of the vertice bool success = float.TryParse(vertparts[0], out vec.X); success |= float.TryParse(vertparts[1], out vec.Y); success |= float.TryParse(vertparts[2], out vec.Z); // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing normal: {0}", line); } } else { Console.WriteLine("Error parsing normal: {0}", line); } normals.Add(vec); } else if (line.StartsWith("f ")) // Face definition { // Cut off beginning of line String temp = line.Substring(2); Tuple<TempVertex, TempVertex, TempVertex> face = new Tuple<TempVertex, TempVertex, TempVertex>(new TempVertex(), new TempVertex(), new TempVertex()); if (temp.Trim().Count((char c) => c == ' ') == 2) // Check if there's enough elements for a face { String[] faceparts = temp.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); int v1, v2, v3; int t1, t2, t3; int n1, n2, n3; // Attempt to parse each part of the face bool success = int.TryParse(faceparts[0].Split('/')[0], out v1); success |= int.TryParse(faceparts[1].Split('/')[0], out v2); success |= int.TryParse(faceparts[2].Split('/')[0], out v3); if (faceparts[0].Count((char c) => c == '/') >= 2) { success |= int.TryParse(faceparts[0].Split('/')[1], out t1); success |= int.TryParse(faceparts[1].Split('/')[1], out t2); success |= int.TryParse(faceparts[2].Split('/')[1], out t3); success |= int.TryParse(faceparts[0].Split('/')[2], out n1); success |= int.TryParse(faceparts[1].Split('/')[2], out n2); success |= int.TryParse(faceparts[2].Split('/')[2], out n3); } else { if (texs.Count > v1 && texs.Count > v2 && texs.Count > v3) { t1 = v1; t2 = v2; t3 = v3; } else { t1 = 0; t2 = 0; t3 = 0; } if (normals.Count > v1 && normals.Count > v2 && normals.Count > v3) { n1 = v1; n2 = v2; n3 = v3; } else { n1 = 0; n2 = 0; n3 = 0; } } // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing face: {0}", line); } else { TempVertex tv1 = new TempVertex(v1, n1, t1); TempVertex tv2 = new TempVertex(v2, n2, t2); TempVertex tv3 = new TempVertex(v3, n3, t3); face = new Tuple<TempVertex, TempVertex, TempVertex>(tv1, tv2, tv3); faces.Add(face); } } else { Console.WriteLine("Error parsing face: {0}", line); } } } // Create the ObjVolume ObjVolume vol = new ObjVolume(); foreach (var face in faces) { FaceVertex v1 = new FaceVertex(verts[face.Item1.Vertex], normals[face.Item1.Normal], texs[face.Item1.Texcoord]); FaceVertex v2 = new FaceVertex(verts[face.Item2.Vertex], normals[face.Item2.Normal], texs[face.Item2.Texcoord]); FaceVertex v3 = new FaceVertex(verts[face.Item3.Vertex], normals[face.Item3.Normal], texs[face.Item3.Texcoord]); vol.faces.Add(new Tuple<FaceVertex, FaceVertex, FaceVertex>(v1, v2, v3)); } return vol; } private class TempVertex { public int Vertex; public int Normal; public int Texcoord; public TempVertex(int vert = 0, int norm = 0, int tex = 0) { Vertex = vert; Normal = norm; Texcoord = tex; } } public override Vector3[] GetNormals() { if (base.GetNormals().Length > 0) { return base.GetNormals(); } List<Vector3> normals = new List<Vector3>(); foreach (var face in faces) { normals.Add(face.Item1.Normal); normals.Add(face.Item2.Normal); normals.Add(face.Item3.Normal); } return normals.ToArray(); } public override int NormalCount { get { return faces.Count * 3; } }
This code loads the normal and texture data found in most OBJ files. The TempVertex class is used to make it easier to pass around the data for a vertex. There's also code included to make the ObjVolume class use the loaded normals, but revert to the computed ones if they exist (it assumes that if you've set a new list of normals, you want that one instead of the ones from the file).
Now let's add a parser for MTL files. Create a new class called Material. This class will store information about a material, and handle loading materials from MTL files.
First we'll have the basics of the Material class (don't forget to add a using directive for the OpenTK classes):
/// <summary> /// Stores information about a material applied to a <c>Volume</c> /// </summary> public class Material { public Vector3 AmbientColor = new Vector3(); public Vector3 DiffuseColor = new Vector3(); public Vector3 SpecularColor = new Vector3(); public float SpecularExponent = 1; public float Opacity = 1.0f; public String AmbientMap = ""; public String DiffuseMap = ""; public String SpecularMap = ""; public String OpacityMap = ""; public String NormalMap = ""; public Material() { } public Material(Vector3 ambient, Vector3 diffuse, Vector3 specular, float specexponent = 1.0f, float opacity = 1.0f) { AmbientColor = ambient; DiffuseColor = diffuse; SpecularColor = specular; SpecularExponent = specexponent; Opacity = opacity; } }
The "maps" here are effectively textures. The most interesting of these is the normal map, which can be used to create an illusion of greater detail in a model. Most of these maps won't be used (we'll only be using diffuse for now) in this tutorial, but are included in case they need to be used later.
The only thing left for this class is the actual code to load the material itself! Add the following functions to the Material class.
public static Dictionary<String, Material> LoadFromFile(string filename) { Dictionary<String, Material> mats = new Dictionary<String, Material>(); try { String currentmat = ""; using (StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read))) { String currentLine; while (!reader.EndOfStream) { currentLine = reader.ReadLine(); if (!currentLine.StartsWith("newmtl")) { if (currentmat.StartsWith("newmtl")) { currentmat += currentLine + "\n"; } } else { if (currentmat.Length > 0) { Material newMat = new Material(); String newMatName = ""; newMat = LoadFromString(currentmat, out newMatName); mats.Add(newMatName, newMat); } currentmat = currentLine + "\n"; } } } // Add final material if (currentmat.Count((char c) => c == '\n') > 0) { Material newMat = new Material(); String newMatName = ""; newMat = LoadFromString(currentmat, out newMatName); mats.Add(newMatName, newMat); } } catch (FileNotFoundException) { Console.WriteLine("File not found: {0}", filename); } catch (Exception) { Console.WriteLine("Error loading file: {0}", filename); } return mats; } public static Material LoadFromString(string mat, out string name) { Material output = new Material(); name = ""; List<String> lines = mat.Split('\n').ToList(); // Skip until the material definition starts lines = lines.SkipWhile(s => !s.StartsWith("newmtl ")).ToList(); // Make sure an actual material was included if (lines.Count != 0) { // Get name from first line name = lines[0].Substring("newmtl ".Length); } // Remove leading whitespace lines = lines.Select((string s) => s.Trim()).ToList(); // Read material properties foreach (String line in lines) { // Skip comments and blank lines if (line.Length < 3 || line.StartsWith("//") || line.StartsWith("#")) { continue; } // Parse ambient color if (line.StartsWith("Ka")) { String[] colorparts = line.Substring(3).Split(' '); // Check that all vector fields are present if (colorparts.Length < 3) { throw new ArgumentException("Invalid color data"); } Vector3 vec = new Vector3(); // Attempt to parse each part of the color bool success = float.TryParse(colorparts[0], out vec.X); success |= float.TryParse(colorparts[1], out vec.Y); success |= float.TryParse(colorparts[2], out vec.Z); output.AmbientColor = new Vector3(float.Parse(colorparts[0]), float.Parse(colorparts[1]), float.Parse(colorparts[2])); // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing color: {0}", line); } } // Parse diffuse color if (line.StartsWith("Kd")) { String[] colorparts = line.Substring(3).Split(' '); // Check that all vector fields are present if (colorparts.Length < 3) { throw new ArgumentException("Invalid color data"); } Vector3 vec = new Vector3(); // Attempt to parse each part of the color bool success = float.TryParse(colorparts[0], out vec.X); success |= float.TryParse(colorparts[1], out vec.Y); success |= float.TryParse(colorparts[2], out vec.Z); output.DiffuseColor = new Vector3(float.Parse(colorparts[0]), float.Parse(colorparts[1]), float.Parse(colorparts[2])); // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing color: {0}", line); } } // Parse specular color if (line.StartsWith("Ks")) { String[] colorparts = line.Substring(3).Split(' '); // Check that all vector fields are present if (colorparts.Length < 3) { throw new ArgumentException("Invalid color data"); } Vector3 vec = new Vector3(); // Attempt to parse each part of the color bool success = float.TryParse(colorparts[0], out vec.X); success |= float.TryParse(colorparts[1], out vec.Y); success |= float.TryParse(colorparts[2], out vec.Z); output.SpecularColor = new Vector3(float.Parse(colorparts[0]), float.Parse(colorparts[1]), float.Parse(colorparts[2])); // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing color: {0}", line); } } // Parse specular exponent if (line.StartsWith("Ns")) { // Attempt to parse each part of the color float exponent = 0.0f; bool success = float.TryParse(line.Substring(3), out exponent); output.SpecularExponent = exponent; // If any of the parses failed, report the error if (!success) { Console.WriteLine("Error parsing specular exponent: {0}", line); } } // Parse ambient map if (line.StartsWith("map_Ka")) { // Check that file name is present if (line.Length > "map_Ka".Length + 6) { output.AmbientMap = line.Substring("map_Ka".Length + 1); } } // Parse diffuse map if (line.StartsWith("map_Kd")) { // Check that file name is present if (line.Length > "map_Kd".Length + 6) { output.DiffuseMap = line.Substring("map_Kd".Length + 1); } } // Parse specular map if (line.StartsWith("map_Ks")) { // Check that file name is present if (line.Length > "map_Ks".Length + 6) { output.SpecularMap = line.Substring("map_Ks".Length + 1); } } // Parse normal map if (line.StartsWith("map_normal")) { // Check that file name is present if (line.Length > "map_normal".Length + 6) { output.NormalMap = line.Substring("map_normal".Length + 1); } } // Parse opacity map if (line.StartsWith("map_opacity")) { // Check that file name is present if (line.Length > "map_opacity".Length + 6) { output.OpacityMap = line.Substring("map_opacity".Length + 1); } } } return output; }
The Dictionary<String, Material> LoadFromFile returns is the set of materials defined in the file, with the names of the materials as the keys.
At this point we've added normals, we've added better OBJ file loading, and we've added MTL file loading. Now let's actually do something with these features.
First, we'll use the new MTL loader to dynamically load textures instead of having everything hardcoded.
In the Game class, add the following new member:
Dictionary<String, Material> materials = new Dictionary<string, Material>();
This dictionary will store information about materials loaded from files. Next, we'll add a method to populate it. To do this, add the following method to the Game class:
private void loadMaterials(String filename) { foreach (var mat in Material.LoadFromFile(filename)) { if (!materials.ContainsKey(mat.Key)) { materials.Add(mat.Key, mat.Value); } } // Load textures foreach (Material mat in materials.Values) { if (File.Exists(mat.AmbientMap) && !textures.ContainsKey(mat.AmbientMap)) { textures.Add(mat.AmbientMap, loadImage(mat.AmbientMap)); } if (File.Exists(mat.DiffuseMap) && !textures.ContainsKey(mat.DiffuseMap)) { textures.Add(mat.DiffuseMap, loadImage(mat.DiffuseMap)); } if (File.Exists(mat.SpecularMap) && !textures.ContainsKey(mat.SpecularMap)) { textures.Add(mat.SpecularMap, loadImage(mat.SpecularMap)); } if (File.Exists(mat.NormalMap) && !textures.ContainsKey(mat.NormalMap)) { textures.Add(mat.NormalMap, loadImage(mat.NormalMap)); } if (File.Exists(mat.OpacityMap) && !textures.ContainsKey(mat.OpacityMap)) { textures.Add(mat.OpacityMap, loadImage(mat.OpacityMap)); } } }
This will load the materials from a file, add them, and load the maps in the file, if they're not already loaded. Now we need one more thing to use this: an MTL file. Create opentk.mtl, set "Copy to Output Directory" to "Copy Always", and put the following inside it:
newmtl opentk1 Ns 10.0000 Ni 1.5000 d 1.0000 Tr 0.0000 Tf 1.0000 1.0000 1.0000 illum 2 Ka 0.1880 0.1880 0.1880 Kd 1.0000 1.0000 1.0000 Ks 0.1000 0.1000 0.1000 map_Ka opentksquare.png map_Kd opentksquare.png newmtl opentk2 Ns 10.0000 Ni 1.5000 d 1.0000 Tr 0.0000 Tf 1.0000 1.0000 1.0000 illum 2 Ka 0.1880 0.1880 0.1880 Kd 1.0000 1.0000 1.0000 Ks 0.1000 0.1000 0.1000 map_Ka opentksquare2.png map_Kd opentksquare2.png
This file includes the materials of the two textured cubes we had already used in previous tutorials. To use this, replace the initProgram method in the Game class with the following;
void initProgram() { lastMousePos = new Vector2(Mouse.X, Mouse.Y); GL.GenBuffers(1, out ibo_elements); // Load shaders from file shaders.Add("default", new ShaderProgram("vs.glsl", "fs.glsl", true)); shaders.Add("textured", new ShaderProgram("vs_tex.glsl", "fs_tex.glsl", true)); activeShader = "textured"; loadMaterials("opentk.mtl"); // Create our objects TexturedCube tc = new TexturedCube(); tc.TextureID = textures[materials["opentk1"].DiffuseMap]; tc.CalculateNormals(); objects.Add(tc); TexturedCube tc2 = new TexturedCube(); tc2.Position += new Vector3(1f, 1f, 1f); tc2.TextureID = textures[materials["opentk2"].DiffuseMap]; tc2.CalculateNormals(); objects.Add(tc2); // Move camera away from origin cam.Position += new Vector3(0f, 0f, 3f); }
Now we're back where we were two tutorials ago, but with everything somewhat more flexible.
One last thing before the end of this part. We've calculated normals, but we're not doing anything with them. Let's visualize them with a new shader!
Create vs_norm.glsl (set it to be copied to the output directory, as usual), and put the following in it:
#version 330 in vec3 vPosition; in vec3 vNormal; out vec3 v_norm; uniform mat4 modelview; void main() { gl_Position = modelview * vec4(vPosition, 1.0); v_norm = normalize(mat3(modelview) * vNormal); v_norm = vNormal; }
Also create fs_norm.glsl, for the fragment shader:
#version 330 in vec3 v_norm; out vec4 outputColor; void main() { vec3 n = normalize(v_norm); outputColor = vec4( 0.5 + 0.5 * n, 1.0); }
This shader will color each fragment based on the normal at that point. Why doesn't it directly draw the normal, instead of doing that additional math? Some normals will have negative components, and that wouldn't be drawn properly on the screen.
The OnUpdateFrame method in the Game class needs to be updated to send the normal information to this shader. Replace it with the following:
protected override void OnUpdateFrame(FrameEventArgs e) { base.OnUpdateFrame(e); ProcessInput(); List<Vector3> verts = new List<Vector3>(); List<int> inds = new List<int>(); List<Vector3> colors = new List<Vector3>(); List<Vector2> texcoords = new List<Vector2>(); List<Vector3> normals = new List<Vector3>(); // Assemble vertex and indice data for all volumes int vertcount = 0; foreach (Volume v in objects) { verts.AddRange(v.GetVerts().ToList()); inds.AddRange(v.GetIndices(vertcount).ToList()); colors.AddRange(v.GetColorData().ToList()); texcoords.AddRange(v.GetTextureCoords()); normals.AddRange(v.GetNormals().ToList()); vertcount += v.VertCount; } vertdata = verts.ToArray(); indicedata = inds.ToArray(); coldata = colors.ToArray(); texcoorddata = texcoords.ToArray(); normdata = normals.ToArray(); GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vPosition")); GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(vertdata.Length * Vector3.SizeInBytes), vertdata, BufferUsageHint.StaticDraw); GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vPosition"), 3, VertexAttribPointerType.Float, false, 0, 0); // Buffer vertex color if shader supports it if (shaders[activeShader].GetAttribute("vColor") != -1) { GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vColor")); GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(coldata.Length * Vector3.SizeInBytes), coldata, BufferUsageHint.StaticDraw); GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vColor"), 3, VertexAttribPointerType.Float, true, 0, 0); } // Buffer texture coordinates if shader supports it if (shaders[activeShader].GetAttribute("texcoord") != -1) { GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("texcoord")); GL.BufferData<Vector2>(BufferTarget.ArrayBuffer, (IntPtr)(texcoorddata.Length * Vector2.SizeInBytes), texcoorddata, BufferUsageHint.StaticDraw); GL.VertexAttribPointer(shaders[activeShader].GetAttribute("texcoord"), 2, VertexAttribPointerType.Float, true, 0, 0); } if (shaders[activeShader].GetAttribute("vNormal") != -1) { GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vNormal")); GL.BufferData<Vector3>(BufferTarget.ArrayBuffer, (IntPtr)(normdata.Length * Vector3.SizeInBytes), normdata, BufferUsageHint.StaticDraw); GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vNormal"), 3, VertexAttribPointerType.Float, true, 0, 0); } // Update object positions time += (float)e.Time; objects[0].Position = new Vector3(0.3f, -0.5f + (float)Math.Sin(time), -3.0f); objects[0].Rotation = new Vector3(0.55f * time, 0.25f * time, 0); objects[0].Scale = new Vector3(0.5f, 0.5f, 0.5f); objects[1].Position = new Vector3(-1f, 0.5f + (float)Math.Cos(time), -2.0f); objects[1].Rotation = new Vector3(-0.25f * time, -0.35f * time, 0); objects[1].Scale = new Vector3(0.7f, 0.7f, 0.7f); // Update model view matrices foreach (Volume v in objects) { v.CalculateModelMatrix(); v.ViewProjectionMatrix = cam.GetViewMatrix() * Matrix4.CreatePerspectiveFieldOfView(1.3f, ClientSize.Width / (float)ClientSize.Height, 1.0f, 40.0f); v.ModelViewProjectionMatrix = v.ModelMatrix * v.ViewProjectionMatrix; } GL.UseProgram(shaders[activeShader].ProgramID); GL.BindBuffer(BufferTarget.ArrayBuffer, 0); // Buffer index data GL.BindBuffer(BufferTarget.ElementArrayBuffer, ibo_elements); GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(indicedata.Length * sizeof(int)), indicedata, BufferUsageHint.StaticDraw); }
We also need to add a new variable to the Game class itself to store normal information:
Vector3[] normdata;
Now that the right data is being calculated and buffered, and the shaders are written, we need to load and use the shaders.
In the initProgram method, add the following (put it next to the other shaders being loaded):
shaders.Add("normal", new ShaderProgram("vs_norm.glsl", "fs_norm.glsl", true));
And the last thing we need to do is to switch the active shader (also in initProgram):
activeShader = "normal";
Now you can run the code and see the normals on our two cubes. Each face, being flat, will have one normal and therefore one color when this shader is used.
Stay tuned for Part 2, where we use these normals to shade an object loaded from an OBJ file.
This comment has been removed by the author.
ReplyDeleteLoving this series!!
ReplyDeleteAny chance of a 110 version of the shaders?
ReplyDeletev_norm = normalize(mat3(modelview) * vNormal);
spits this: "GLSL 110 does not allow sub- or super-matrix constructors".
110 rewrites are required for people following this series on MacOS. It's not that MacOS can't run 110+, but that your C# code requires modifications to run the 330 shaders. Which I can't figure out, hence the 110. Thanks again for the hard work man, these are so helpful
Nm, got it:
ReplyDelete#version 110
attribute vec3 vPosition;
attribute vec3 vNormal;
varying vec3 v_norm;
uniform mat4 modelview;
void
main()
{
gl_Position = modelview * vec4(vPosition, 1.0);
mat3 NicolBolas = mat3(modelview[0].xyz, modelview[1].xyz, modelview[2].xyz);
v_norm = normalize(NicolBolas * vNormal);
v_norm = vNormal;
}
Congrate for the artivcle (very helpfull) ... I'm using the tutorial number 3 as base for develop a application, and Im using, inside onUpdateFrame:
ReplyDeleteGL.BindBuffer(BufferTarget.ArrayBuffer, vbo_position);
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(vertdata.Length * Vector3.SizeInBytes), vertdata, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(attribute_vpos, 3, VertexAttribPointerType.Float, false, 0, 0);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo_color);
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(coldata.Length * Vector3.SizeInBytes), coldata, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(attribute_vcol, 3, VertexAttribPointerType.Float, true, 0, 0);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, ibo_elements);
GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(indicedata.Length * sizeof(int)), indicedata, BufferUsageHint.StaticDraw);
it's work fine, but when I add...:
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo_normal);
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(Normals.Length * Vector3.SizeInBytes), Normals, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(attribute_normal, 3, VertexAttribPointerType.Float, false, 0, 0);
...to try draw the normal its doesnt work...
thanks for the help....
Detail: I have the normals correctly calculated in the Normals array ;)
DeleteHi Vinicius,
DeleteCan you please put your whole Game.cs file up on Pastebin? It would help me figure out what the issue is.
Hi Kabuto, Thanks soo much for your answer....
ReplyDeleteBefore my Game.cs, let me exply: I have a class called "triangulation", with my vertex, index, color and normal information. I want create a terrain with, at the same time, color and normal, but I only can put one or another...
Only color: https://onedrive.live.com/redir?resid=64A6A5CC5630D388!639&authkey=!AIw0foZZjQ25vbQ&v=3&ithint=photo%2cpng
Only Normal: https://onedrive.live.com/redir?resid=64A6A5CC5630D388!640&authkey=!AIJeqMOtWwFS27Q&v=3&ithint=photo%2cpng
I need this (this image generated with code in XNA embedded in a windows form): https://onedrive.live.com/redir?resid=64A6A5CC5630D388!638&authkey=!AMOx08owsMgeZx8&v=3&ithint=photo%2cpng
My Game.cs:
public Game()
Delete: base(512, 512, new GraphicsMode(32, 24, 0, 4))
{
}
Matrix4 modelview;
triangulation triangulation = new triangulation();
Vector3[] vertdata;
Vector3[] coldata;
Vector3[] normals;
int[] indicedata;
int ibo_elements;
Camera cam = new Camera();
Dictionary textures = new Dictionary();
Dictionary shaders = new Dictionary();
string activeShader = "default";
void initProgram()
{
GL.GenBuffers(1, out ibo_elements);
// Load shaders from file
shaders.Add("default", new ShaderProgram("vs.glsl", "fs.glsl", true));
shaders.Add("normal", new ShaderProgram("vs_norm.glsl", "fs_norm.glsl", true));
activeShader = "default";
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
initProgram();
Title = "Hello OpenTK!";
GL.ClearColor(Color.CornflowerBlue);
GL.PointSize(5f);
}
protected override void OnRenderFrame(FrameEventArgs e)
{
base.OnRenderFrame(e);
GL.Viewport(0, 0, Width, Height);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.Enable(EnableCap.DepthTest);
shaders[activeShader].EnableVertexAttribArrays();
GL.UniformMatrix4(shaders[activeShader].GetUniform("modelview"), false, ref modelview);
GL.DrawElements(BeginMode.Triangles, triangulation.getIndex().Count, DrawElementsType.UnsignedInt, 0);
shaders[activeShader].DisableVertexAttribArrays();
GL.Flush();
SwapBuffers();
}
protected override void OnUpdateFrame(FrameEventArgs e)
Delete{
base.OnUpdateFrame(e);
// Assemble vertex and indice data for all volumes
vertdata = triangulation.getVertex().ToArray();
indicedata = triangulation.getIndex().ToArray();
coldata = triangulation.getColor().ToArray();
normals = triangulation.CalculateNormals().ToArray();
GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vPosition"));
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(vertdata.Length * Vector3.SizeInBytes), vertdata, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vPosition"), 3, VertexAttribPointerType.Float, false, 0, 0);
// Buffer vertex color if shader supports it
if (shaders[activeShader].GetAttribute("vColor") != -1)
{
GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vColor"));
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(coldata.Length * Vector3.SizeInBytes), coldata, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vColor"), 3, VertexAttribPointerType.Float, true, 0, 0);
}
if (shaders[activeShader].GetAttribute("vNormal") != -1)
{
GL.BindBuffer(BufferTarget.ArrayBuffer, shaders[activeShader].GetBuffer("vNormal"));
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(normals.Length * Vector3.SizeInBytes), normals, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(shaders[activeShader].GetAttribute("vNormal"), 3, VertexAttribPointerType.Float, true, 0, 0);
}
modelview = Matrix4.LookAt(cam.getCamaraPosition(), cam.target, cam.getUp()) *
Matrix4.CreatePerspectiveFieldOfView((float)Math.PI / 4, ClientSize.Width / (float)ClientSize.Height, 0.1f, 10000f);
GL.UseProgram(shaders[activeShader].ProgramID);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
// Buffer index data
GL.BindBuffer(BufferTarget.ElementArrayBuffer, ibo_elements);
GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(indicedata.Length * sizeof(int)), indicedata, BufferUsageHint.StaticDraw);
}
Thanks for the help... if you give me permission, letme ask two more asks:
Delete1) Is posible draw strings in the screen?
2) OpenTK has a Viewport.Project, like XNA?
thanks again!!
Hi Vinicius,
DeleteIt's not possible to draw strings directly with OpenTK. But you can write your own function to do it. Some people use the C# Graphics object to make a bitmap image with the text you want and then load that as a texture to draw. You could also create a pre-rendered image of all the characters in a font and use it as a texture atlas to get the characters you want (but then each character is drawn separately).
Viewport.Project seems to do the same thing we do in the vertex shader with the mvp matrix. You can do this with OpenTK by using Vector4.Transform.
I was able to get the code you posted working on my end (I had to fill in a little bit because Blogger comments ate the parts with angle brackets; that's why I recommend http://pastebin.com/ for posting code). The problem may be in your shader or the Triangulation class. Can you please post those as well?
DeleteHi Kabuto.... the triangularion class is only a class with the vectore's and index... and the shader are the sames from your previous post... the vs.glsl, fs.glsl, vs_norm.glsl and fs_norm.glsl.
Deletethanks
Hi Vinicius,
DeleteThat would explain the issue. One of those shaders shows colors, the other shows normals. You would need to create a shader that displays both.
Hi, Kabuto, I didn't know whether to post it here or part 2 but I'm having issues with my material.
ReplyDeleteError CS1061 'TexturedCube' does not contain a definition for 'Material' and no extension method 'Material' accepting a first argument of type 'TexturedCube' could be found (are you missing a using directive or an assembly reference?)
when trying to do tc.Material = materials["opentk1"];
Likewise
Error CS1061 'Volume' does not contain a definition for 'Material' and no extension method 'Material' accepting a first argument of type 'Volume' could be found (are you missing a using directive or an assembly reference?)
GL.Uniform3(shaders[activeShader].GetUniform("material_ambient"), ref v.Material.AmbientColor);
this is true for the rest of material shaders (diffuse, specularColor, specularExponent, etc..)
http://pastebin.com/2Tuj0j7c //Game.cs
http://pastebin.com/UUJRzhEN //Volume.cs
http://pastebin.com/grCxyT6T //Material.cs
Hi NNYC,
DeleteThanks for pointing this out. Looks like I missed a line. I'll go add it to the tutorial now.
In the Volume class, add:
public Material Material = new Material();
Thank you for the help.
DeleteI am following this guide concurrently while learning C# so some stuff are a bit foreign to me. Especially (at the time) the idea of separating classes into different files when until recently I just had it in one just very large file.
Again thank you for making this tutorial it has been very informative about shaders and graphics in general so far.
Hi Kabuto!
ReplyDeleteThank you for this great tutorial. I've been following your tutorial and I'm a bit confused, how did we get the colors in the
public static ObjVolume LoadFromString(string obj) ?
we previously have this line:
// Dummy color/texture coordinates for now
colors.Add(new Vector3((float) Math.Sin(vec.Z), (float) Math.Sin(vec.Z), (float) Math.Sin(vec.Z)));
texs.Add(new Vector2((float) Math.Sin(vec.Z), (float) Math.Sin(vec.Z)));
but in this page I can't seem to find anywhere that we add colors.
This line is still in our OnUpdateFrame
colors.AddRange(v.GetColorData().ToList());
but our GetColorData returns an array uninitialized values (new Vector3)
public override Vector3[] GetColorData()
{
return new Vector3[ColorDataCount];
}
Am I missing something?
This comment has been removed by the author.
ReplyDeleteHello Kabuto
ReplyDeleteJust wondering if your material file works with any object? Keep up the tutorials man :) looking forward to #9!
Should work with any object, as long as it's loaded properly.
DeleteHello Kabuto. Please tell me How to calculate indices, and texture coordinates if known only to the coordinates of the vertices?
ReplyDeleteHello,
DeleteUnfortunately, there's no easy way to do that. It would be different for each model. Unless you have more information about the model (e.g. you know that it is convex, or you know the vertices are each used once and are in order), you can't know enough to determine indices.
Texture coordinates are similar, but they may be possible if you don't need the specific wrapping for the model and have the indices (e.g. spherical wrapping can make a usable set of texture coordinates: https://www.mvps.org/directx/articles/spheremap.htm - sadly I could only find a DirectX example, but the math is the same)
Im found cool way, i use your method Sin(z) and this worked)
DeleteHello.
ReplyDeleteI tried running this code, but program crashes hard ("vshost.exe stopped working") when I use normal shader. I triple checked step by step in debugger, and all C# objects seem to be created properly, UpdateFrame loop works correctly - debugger doesn't show anything wrong or sinister. Yet when I step out of debugger, it instantly crashes.
Any ideas on that behavior?
Update: I ran debugger through OnRenderFrame, and it appears that the program crashes on GL.DrawElements
DeleteUpdate: Nevermind, I was actually a moron and just forgot to copy OnUpdateFrame. That mistake was a result of me backing up from initial pasting of that code to change something that was fishy to find out what was going on, then after correction I forgot to copy it back again...
Delete