Skip to content

3.2 Mesh

Erik van Bilsen edited this page Dec 31, 2016 · 1 revision

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.

Initialization

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.

Rendering

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

    1. 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)
    1. 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)
    1. Model Loading
    • [3.1 OBJ Files](3.1 OBJ Files)
    • [3.2 Mesh](3.2 Mesh)
    • [3.3 Model](3.3 Model)
    1. 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)
    1. 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)
    1. PBR
    • Theory
    • [6.1 Lighting](6.1 Lighting)
    • IBL
      • [Diffuse Irradiance](Diffuse Irradiance)
      • [Specular IBL](Specular IBL)
    1. In Practice
    • [7.1 Debugging](7.1 Debugging)
    • [Text Rendering](Text Rendering)
    • [2D Game](2D Game)
Clone this wiki locally