-
Notifications
You must be signed in to change notification settings - Fork 15
3.2 Mesh
Once we load the model data from a [Wavefront .obj file](3.1 OBJ Files) (as discussed in the [next](3.3 Model) tutorial) we need to transform that data to a format that OpenGL understands so that we can render the objects. We learned from the previous tutorial that a mesh represents a single drawable entity so let's start by defining a mesh class of our own.
Let's review a bit of what we've learned so far to think about what a mesh should minimally have as its data. A mesh should at least need a set of vertices where each vertex contains a position vector, a normal vector and a texture coordinate vector. A mesh should also contain indices for indexed drawing and material data in the form of textures (diffuse/specular maps).
Now that we set the minimal requirements for a mesh class we can define a vertex in OpenGL:
type
TVertex = record
public
Position: TVector3;
Normal: TVector3;
TexCoords: TVector2;
end;
We store each of the required vectors in a record called TVertex
that we can use to index each of the vertex attributes. Aside from a TVertex
record we also want to organize the texture data in a TTexture
record:
type
TTextureKind = (Diffuse, Specular, Normal, Height);
type
TTexture = record
private
FId: GLuint;
FKind: TTextureKind;
public
property Id: GLuint read FId;
property Kind: TTextureKind read FKind;
end;
We store the Id
of the texture and its Kind
e.g. a diffuse texture or a specular texture.
Knowing the actual representation of a vertex and a texture we can start defining the structure of the mesh class:
type
TMesh = class
private
FShader: IShader;
FVertices: TArray<TVertex>;
FIndices: TArray<UInt16>;
FTextures: TArray<TTexture>;
FVertexArray: IVertexArray;
private
procedure SetupMesh;
public
constructor Create(const AVertices: TArray<TVertex>;
const AIndices: TArray<UInt16>; const ATextures: TArray<TTexture>;
const AShader: IShader);
procedure Draw;
end;
As you can see the class isn't too complicated. In the constructor we give the mesh all the necessary data, we initialize the buffers in the SetupMesh
method and finally draw the mesh via the Draw
method. Note that we pass a shader to the constructor, so we can use it to retrieve attribute and uniform locations.
The function content of the constructor is pretty straightforward. We simply set the class's fields with the constructor's corresponding argument variables. We also call the SetupMesh
method in the constructor:
constructor TMesh.Create(const AVertices: TArray<TVertex>;
const AIndices: TArray<UInt16>; const ATextures: TArray<TTexture>;
const AShader: IShader);
begin
inherited Create;
FShader := AShader;
FVertices := AVertices;
FIndices := AIndices;
FTextures := ATextures;
SetupMesh;
end;
Nothing special going on here. Let's delve right into the SetupMesh
method now.
Thanks to the constructor we now have large lists of mesh data that we can use for rendering. We do need to setup the appropriate buffers though and specify the vertex shader layout via vertex attribute pointers. By now you should have no trouble with these concepts:
procedure TMesh.SetupMesh;
var
VertexLayout: TVertexLayout;
begin
{ Create vertex layout to match TVertex type }
VertexLayout.Start(FShader)
.Add('position', 3)
.Add('normal', 3, False, True)
.Add('texCoords', 2, False, True);
{ Create vertex array (VAO, VBO and EBO) }
FVertexArray := TVertexArray.Create(VertexLayout,
FVertices[0], Length(FVertices) * SizeOf(TVertex), FIndices);
end;
The code is not much different than what you'd expect.
The last function we need to define for the TMesh
class to be complete is its Draw
method. Before actually rendering the mesh though we first want to bind the appropriate textures before calling glDrawElements
. However, this is actually slightly difficult since we don't know from the start how many (if any) textures the mesh has and what type they might have. So how do we set the texture units and samplers in the shaders?
To solve the issue we're going to assume a certain naming convention: each diffuse texture is named texture_diffuseN
and each specular texture should be named texture_specularN
where N
is any number ranging from 1 to the maximum number of texture samplers allowed. Let's say we have 3 diffuse textures and 2 specular textures for a particular mesh, their texture samplers should then be called:
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
By this convention we can define as many texture samplers as we want in the shaders and if a mesh actually does contain (so many) textures we know what their names are going to be. By this convention we can process any amount of textures on a single mesh and the developer is free to use as many of those as he wants by simply defining the proper samplers (although defining less would be a bit of a waste of bind and uniform calls).
ℹ️ There are many solutions to problems like this and if you don't like this particular solution it is up to you to get creative and come up with your own solution.
The resulting drawing code then becomes:
procedure TMesh.Draw;
var
Prog, DiffuseNr, SpecularNr, Nr: GLuint;
Location: GLint;
I: Integer;
Name: RawByteString;
begin
Prog := FShader.Handle;
DiffuseNr := 1;
SpecularNr := 1;
for I := 0 to Length(FTextures) - 1 do
begin
{ Active proper texture unit before binding }
glActiveTexture(GL_TEXTURE0 + I);
{ Retrieve texture number (the N in diffuse_textureN) }
case FTextures[I].Kind of
TTextureKind.Diffuse:
begin
Name := 'texture_diffuse';
Nr := DiffuseNr;
Inc(DiffuseNr);
end;
TTextureKind.Specular:
begin
Name := 'texture_specular';
Nr := SpecularNr;
Inc(SpecularNr);
end;
else
Assert(False);
Nr := 0;
end;
if (Nr > 0) then
begin
{ Now set the sampler to the correct texture unit }
Name := Name + UTF8Char(Ord('0') + Nr);
Location := glGetUniformLocation(Prog, MarshaledAString(Name));
if (Location >= 0) then
begin
glUniform1i(Location, I);
glBindTexture(GL_TEXTURE_2D, FTextures[I].Id);
end;
end;
end;
FVertexArray.Render;
end;
We first calculate the N-component per texture type and concatenate it to the texture's kind string to get the appropriate uniform name. We then locate the appropriate sampler, give it the location value to correspond with the currently active texture unit and bind the texture.
You can find the full source code of the TMesh
class here.
The TMesh
class we just defined is a neat abstraction for many of the topics we've discussed in the early tutorials. In the [next](3.3 Model) tutorial we'll create a model that acts as a container for several mesh objects and load it from a OBJ file.
⬅️ [3.1 OBJ Files](3.1 OBJ Files) | Contents | [3.3 Model](3.3 Model) ➡️ |
---|
Learn OpenGL(ES) with Delphi
-
- Getting Started
- OpenGL (ES)
- [Creating an OpenGL App](Creating an OpenGL App)
- [1.1 Hello Window](1.1 Hello Window)
- [1.2 Hello Triangle](1.2 Hello Triangle)
- [1.3 Shaders](1.3 Shaders)
- [1.4 Textures](1.4 Textures)
- [1.5 Transformations](1.5 Transformations)
- [1.6 Coordinate Systems](1.6 Coordinate Systems)
- [1.7 Camera](1.7 Camera)
- [Review](Getting Started Review)
-
- Lighting
- [2.1 Colors](2.1 Colors)
- [2.2 Basic Lighting](2.2 Basic Lighting)
- [2.3 Materials](2.3 Materials)
- [2.4 Lighting Maps](2.4 Lighting Maps)
- [2.5 Light Casters](2.5 Light Casters)
- [2.6 Multiple Lights](2.6 Multiple Lights)
- [Review](Lighting Review)
-
- Model Loading
- [3.1 OBJ Files](3.1 OBJ Files)
- [3.2 Mesh](3.2 Mesh)
- [3.3 Model](3.3 Model)
-
- Advanced OpenGL
- [4.1 Depth Testing](4.1 Depth Testing)
- [4.2 Stencil Testing](4.2 Stencil Testing)
- [4.3 Blending](4.3 Blending)
- [4.4 Face Culling](4.4 Face Culling)
- [4.5 Framebuffers](4.5 Framebuffers)
- [4.6 Cubemaps](4.6 Cubemaps)
- [4.7 Advanced Data](4.7 Advanced Data)
- [4.8 Advanced GLSL](4.8 Advanced GLSL)
- [4.9 Geometry Shader](4.9 Geometry Shader)
- 4.10Instancing
- [4.11 Anti Aliasing](4.11 Anti Aliasing)
-
- Advanced Lighting
- [5.1 Advanced Lighting](5.1 Advanced Lighting)
- [5.2 Gamma Correction](5.2 Gamma Correction)
- [5.3 Shadows](5.3 Shadows)
- [5.3.1 Shadow Mapping](5.3.1 Shadow Mapping)
- [5.3.2 Point Shadows](5.3.2 Point Shadows)
- [5.3.3 CSM](5.3.3 CSM)
- [5.4 Normal Mapping](5.4 Normal Mapping)
- [5.5 Parallax Mapping](5.5 Parallax Mapping)
- [5.6 HDR](5.6 HDR)
- [5.7 Bloom](5.7 Bloom)
- [5.8 Deferred Shading](5.8 Deferred Shading)
- [5.9 SSAO](5.9 SSAO)
-
- PBR
-
- In Practice
- [7.1 Debugging](7.1 Debugging)
- [Text Rendering](Text Rendering)
- [2D Game](2D Game)
- Breakout
- [Setting Up](Setting Up)
- [Rendering Sprites](Rendering Sprites)
- Levels
-
Collisions
- Ball
- [Collision Detection](Collision Detection)
- [Collision Resolution](Collision Resolution)
- Particles
- Postprocessing
- Powerups
- Audio
- [Render Text](Render Text)
- [Final Thoughts](Final Thoughts)