OpenGL 3D渲染技术:渲染glTF模型

12,749 阅读10分钟

大家好,我是程序员kenney,在我之前的一篇文章《OpenGL 3D渲染技术:glTF基础知识》中,跟大家介绍了glTF模型格式,今天这篇文章我们来渲染glTF模型。

glTF模型相当复杂,其实上一篇文章也只介绍了glTF模型的一些比较常用的字段而已,如果要渲染出glTF模型的所有效果,还是相当复杂的,本篇文章先来带大家看看一些基础的渲染。

glTF模型的sample可以这里下载:github.com/KhronosGrou…

我们以一个简单的模型BoxTextured为例,它的效果很简单,一个带纹理的立方体,效果是这样的:

企业微信20210328-154411@2x.png

我们来看一下它的配置:

{
    "asset": {
        "generator": "COLLADA2GLTF",
        "version": "2.0"
    },
    "scene": 0,
    "scenes": [
        {
            "nodes": [
                0
            ]
        }
    ],
    "nodes": [
        {
            "children": [
                1
            ],
            "matrix": [
                1.0,
                0.0,
                0.0,
                0.0,
                0.0,
                0.0,
                -1.0,
                0.0,
                0.0,
                1.0,
                0.0,
                0.0,
                0.0,
                0.0,
                0.0,
                1.0
            ]
        },
        {
            "mesh": 0
        }
    ],
    "meshes": [
        {
            "primitives": [
                {
                    "attributes": {
                        "NORMAL": 1,
                        "POSITION": 2,
                        "TEXCOORD_0": 3
                    },
                    "indices": 0,
                    "mode": 4,
                    "material": 0
                }
            ],
            "name": "Mesh"
        }
    ],
    "accessors": [
        {
            "bufferView": 0,
            "byteOffset": 0,
            "componentType": 5123,
            "count": 36,
            "max": [
                23
            ],
            "min": [
                0
            ],
            "type": "SCALAR"
        },
        {
            "bufferView": 1,
            "byteOffset": 0,
            "componentType": 5126,
            "count": 24,
            "max": [
                1.0,
                1.0,
                1.0
            ],
            "min": [
                -1.0,
                -1.0,
                -1.0
            ],
            "type": "VEC3"
        },
        {
            "bufferView": 1,
            "byteOffset": 288,
            "componentType": 5126,
            "count": 24,
            "max": [
                0.5,
                0.5,
                0.5
            ],
            "min": [
                -0.5,
                -0.5,
                -0.5
            ],
            "type": "VEC3"
        },
        {
            "bufferView": 2,
            "byteOffset": 0,
            "componentType": 5126,
            "count": 24,
            "max": [
                6.0,
                1.0
            ],
            "min": [
                0.0,
                0.0
            ],
            "type": "VEC2"
        }
    ],
    "materials": [
        {
            "pbrMetallicRoughness": {
                "baseColorTexture": {
                    "index": 0
                },
                "metallicFactor": 0.0
            },
            "name": "Texture"
        }
    ],
    "textures": [
        {
            "sampler": 0,
            "source": 0
        }
    ],
    "images": [
        {
            "uri": "CesiumLogoFlat.png"
        }
    ],
    "samplers": [
        {
            "magFilter": 9729,
            "minFilter": 9986,
            "wrapS": 10497,
            "wrapT": 10497
        }
    ],
    "bufferViews": [
        {
            "buffer": 0,
            "byteOffset": 768,
            "byteLength": 72,
            "target": 34963
        },
        {
            "buffer": 0,
            "byteOffset": 0,
            "byteLength": 576,
            "byteStride": 12,
            "target": 34962
        },
        {
            "buffer": 0,
            "byteOffset": 576,
            "byteLength": 192,
            "byteStride": 8,
            "target": 34962
        }
    ],
    "buffers": [
        {
            "byteLength": 840,
            "uri": "BoxTextured0.bin"
        }
    ]
}

下面我们来打造一个小型3D引擎来渲染它,其中的类名和作用保持和glTF中的一致,主要有EngineSceneNodeMeshPrimitiveMaterial

glTF模型解析

glTF模型的解析有一些开源库可以使用,这里我们使用tinygltf这个库,这个库挺好用的,很小巧,接入简单,我们先来看一下解析代码:

void Engine::loadGLTF(const std::string &path) {
  tinygltf::TinyGLTF loader;
  std::string err;
  std::string warn;
  loader.LoadASCIIFromFile(&model_, &err, &warn, path);
}

解析后会得到一个tinygltf::Model,里面的成员变量的名字和层级与glTF里的保持一致,用起来非常友好,并且还包括了bin数据读取、图片数据读取,而不仅仅是字段的解析,连数据都帮你读好了。

数据加载

有了model之后,我们来根据这个model里的数据创建相应的GL资源备用。

我们先来看buffer

"buffers": [
    {
        "byteLength": 840,
        "uri": "BoxTextured0.bin"
    }
]

在这个立方体模型中,只有一个buffer,我们来把它加载到GL buffer中去,也就是常说的vbo

std::shared_ptr<std::vector<GLuint>>
Engine::buildBuffers(const tinygltf::Model &model) {
  auto buffers = std::make_shared<std::vector<GLuint>>(model.buffers.size(), 0);
  GL_CHECK(glGenBuffers(buffers->size(), buffers->data()));
  for (auto i = 0; i < model.buffers.size(); ++i) {
    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, buffers->at(i)));
    GL_CHECK(glBufferData(GL_ARRAY_BUFFER, model.buffers[i].data.size(),
                          model.buffers[i].data.data(), GL_STATIC_DRAW));
  }
  GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
  return buffers;
}

这里vbo里装的就是这个glTF模型所有的数值数据了,包括顶点、纹理坐标、法向量、索引等都在这了。

接下来我们来加载纹理:

std::shared_ptr<std::vector<GLuint>>
Engine::buildTextures(const tinygltf::Model &model) {
  auto textures = std::make_shared<std::vector<GLuint>>(model.textures.size());
  GL_CHECK(glGenTextures(textures->size(), textures->data()));
  for (auto i = 0; i < textures->size(); ++i) {
    GL_CHECK(glBindTexture(GL_TEXTURE_2D, textures->at(i)));
    const auto &texture = model.textures[i];
    const auto &image = model.images[texture.source];
    auto minFilter =
        texture.sampler >= 0 && model.samplers[texture.sampler].minFilter != -1
            ? model.samplers[texture.sampler].minFilter
            : GL_LINEAR;
    auto magFilter =
        texture.sampler >= 0 && model.samplers[texture.sampler].magFilter != -1
            ? model.samplers[texture.sampler].magFilter
            : GL_LINEAR;
    auto wrapS = texture.sampler >= 0 ? model.samplers[texture.sampler].wrapS
                                      : GL_REPEAT;
    auto wrapT = texture.sampler >= 0 ? model.samplers[texture.sampler].wrapT
                                      : GL_REPEAT;
    GL_CHECK(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height,
                          0, GL_RGBA, image.pixel_type, image.image.data()));
    GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter));
    GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter));
    GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapS));
    GL_CHECK(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapT));
    if (minFilter == GL_NEAREST_MIPMAP_NEAREST ||
        minFilter == GL_NEAREST_MIPMAP_LINEAR ||
        minFilter == GL_LINEAR_MIPMAP_NEAREST ||
        minFilter == GL_LINEAR_MIPMAP_LINEAR) {
      GL_CHECK(glGenerateMipmap(GL_TEXTURE_2D));
    }
  }
  GL_CHECK(glBindTexture(GL_TEXTURE_2D, 0));
  return textures;
}

都是常规操作,没太多可说的,主要是把数据load到纹理上,并设置filter和wrap方式,这里需要注意的是glTF里有些字段是可以不配的,并且不配的时候tinygltf这个库也不会给你生成默认值,比如minFilter

TypeDescriptionRequired
magFilterintegerMagnification filter.No
minFilterintegerMinification filter.No
wrapSintegers wrapping mode.No, default: 10497
wrapTintegert wrapping mode.No, default: 10497

参见:github.com/KhronosGrou…

tinygltf库注释:

// glTF 2.0 spec does not define default value for `minFilter` and
// `magFilter`. Set -1 in TinyGLTF(issue #186)
int minFilter =
    -1;  // optional. -1 = no filter defined. ["NEAREST", "LINEAR",
         // "NEAREST_MIPMAP_LINEAR", "LINEAR_MIPMAP_NEAREST",
         // "NEAREST_MIPMAP_LINEAR", "LINEAR_MIPMAP_LINEAR"]

所以需要处理一下不配时候的情况,自己设置好默认值,否则可能会导致纹理渲染不出来。

至此我们的数值数据以及纹理数据就加载好了。

场景创建

下面我们来创建SceneScene里面有会包含NodeNode里又有MeshMesh里有可以有好几个部分,每个部分是一个Primitive,这个层次关系是glTF规范里约定的。

创建所有场景:

void Engine::buildScenes() {
  auto buffers = buildBuffers(model_);
  auto textures = buildTextures(model_);
  scenes_.resize(model_.scenes.size());
  for (auto i = 0; i < model_.scenes.size(); ++i) {
    scenes_[i] = buildScene(model_, i, buffers, textures);
  }
}

std::shared_ptr<Scene>
Engine::buildScene(const tinygltf::Model &model, unsigned int sceneIndex,
                   const std::shared_ptr<std::vector<GLuint>> &buffers,
                   const std::shared_ptr<std::vector<GLuint>> &textures) {
  auto scene = std::make_shared<triangle::Scene>();
  for (auto i = 0; i < model.scenes[sceneIndex].nodes.size(); ++i) {
    scene->addNode(
        buildNode(model, model.scenes[sceneIndex].nodes[i], buffers, textures));
  }
  return scene;
}

节点创建:

std::shared_ptr<Node>
Engine::buildNode(const tinygltf::Model &model, unsigned int nodeIndex,
                  const std::shared_ptr<std::vector<GLuint>> &buffers,
                  const std::shared_ptr<std::vector<GLuint>> &textures,
                  std::shared_ptr<Node> parent) {
  auto node = std::make_shared<Node>(parent);
  auto nodeMatrix = model.nodes[nodeIndex].matrix;
  glm::mat4 matrix(1.0f);
  if (nodeMatrix.size() == 16) {
    matrix[0].x = nodeMatrix[0], matrix[0].y = nodeMatrix[1],
    matrix[0].z = nodeMatrix[2], matrix[0].w = nodeMatrix[3];
    matrix[1].x = nodeMatrix[4], matrix[1].y = nodeMatrix[5],
    matrix[1].z = nodeMatrix[6], matrix[1].w = nodeMatrix[7];
    matrix[2].x = nodeMatrix[8], matrix[2].y = nodeMatrix[9],
    matrix[2].z = nodeMatrix[10], matrix[2].w = nodeMatrix[11];
    matrix[3].x = nodeMatrix[12], matrix[3].y = nodeMatrix[13],
    matrix[3].z = nodeMatrix[14], matrix[3].w = nodeMatrix[15];
  } else {
    if (model.nodes[nodeIndex].translation.size() == 3) {
      glm::translate(matrix, glm::vec3(model.nodes[nodeIndex].translation[0],
                                       model.nodes[nodeIndex].translation[1],
                                       model.nodes[nodeIndex].translation[2]));
    }
    if (model.nodes[nodeIndex].rotation.size() == 4) {
      matrix *= glm::mat4_cast(glm::quat(model.nodes[nodeIndex].rotation[3],
                                         model.nodes[nodeIndex].rotation[0],
                                         model.nodes[nodeIndex].rotation[1],
                                         model.nodes[nodeIndex].rotation[2]));
    }
    if (model.nodes[nodeIndex].scale.size() == 3) {
      glm::scale(matrix, glm::vec3(model.nodes[nodeIndex].scale[0],
                                   model.nodes[nodeIndex].scale[1],
                                   model.nodes[nodeIndex].scale[2]));
    }
  }
  node->setMatrix(matrix);
  if (model.nodes[nodeIndex].mesh >= 0) {
    node->setMesh(
        buildMesh(model, model.nodes[nodeIndex].mesh, buffers, textures));
  }
  for (auto &childNodeIndex : model.nodes[nodeIndex].children) {
    node->addChild(buildNode(model, childNodeIndex, buffers, textures, node));
  }
  return node;
}

注意节点的变换即可以用matrix的方式给出,也可以用translationrotationscale的方式给出:

Any node can define a local space transformation either by supplying a matrix property, or any of translation, rotation, and scale properties (also known as TRS properties). translation and scale are FLOAT_VEC3 values in the local coordinate system. rotation is a FLOAT_VEC4 unit quaternion value, (x, y, z, w), in the local coordinate system.

参见:github.com/KhronosGrou…

另外注意当以translationrotationscale方式给出时,变换顺序是TRS,在shader里矩阵是左乘顶点,也就是说先做缩放变换,再做旋转变换,最后做平移变换。

节点里可以包含meshmaterial,也可以是空节点,注意判断是否配置了。接着再对子节点继续递归创建。

下面来看Mesh的创建:

我们可以看到,Mesh是由Primitive组成的:

"meshes": [
  {
    "primitives": [
      {
        "attributes": {
          "NORMAL": 1,
          "POSITION": 2,
          "TEXCOORD_0": 3
        },
        "indices": 0,
        "mode": 4,
        "material": 0
      }
    ],
    "name": "Mesh"
  }
]

这个Primitive是什么东西呢?我们来看glTF文档的解释:

In glTF, meshes are defined as arrays of primitives. Primitives correspond to the data required for GPU draw calls. Primitives specify one or more attributes, corresponding to the vertex attributes used in the draw calls. Indexed primitives also define an indices property. Attributes and indices are defined as references to accessors containing corresponding data. Each primitive also specifies a material and a primitive type that corresponds to the GPU primitive type (e.g., triangle set).

参见:github.com/KhronosGrou…

可以把它认为是Mesh的一个组成部分,就是说一个大Mesh可以再细节成几个子部分,每个部分可以做为一次draw call的单位,当然也可以不分,像这个模型里就没有分,它只有一个PrimitivePrimitive里描述了attribute的构成,以及通过indices来指定这些attribute的来源是什么,这个indices就是accessor的索引。

std::shared_ptr<Mesh>
Engine::buildMesh(const tinygltf::Model &model, unsigned int meshIndex,
                  const std::shared_ptr<std::vector<GLuint>> &buffers,
                  const std::shared_ptr<std::vector<GLuint>> &textures) {
  auto meshPrimitives =
      std::make_shared<std::vector<std::shared_ptr<Primitive>>>();
  const auto &primitives = model.meshes[meshIndex].primitives;
  auto vaos = std::make_shared<std::vector<GLuint>>(primitives.size());
  GL_CHECK(glGenVertexArrays(vaos->size(), vaos->data()));
  for (auto i = 0; i < primitives.size(); ++i) {
    GL_CHECK(glBindVertexArray(vaos->at(i)));
    meshPrimitives->push_back(
        buildPrimitive(model, meshIndex, i, vaos, buffers, textures));
  }
  GL_CHECK(glBindVertexArray(0));
  return std::make_shared<Mesh>(meshPrimitives);
}

std::shared_ptr<Primitive>
Engine::buildPrimitive(const tinygltf::Model &model, unsigned int meshIndex,
                       unsigned int primitiveIndex,
                       const std::shared_ptr<std::vector<GLuint>> &vaos,
                       const std::shared_ptr<std::vector<GLuint>> &buffers,
                       const std::shared_ptr<std::vector<GLuint>> &textures) {
  const auto &primitive = model.meshes[meshIndex].primitives[primitiveIndex];
  for (auto &attribute : preDefinedAttributes) {
    const auto &attributeName = attribute.first;
    const auto &attributeLocation = attribute.second;
    const auto iterator = primitive.attributes.find(attributeName);
    if (iterator == primitive.attributes.end()) {
      continue;
    }
    const auto &accessor = model.accessors[(*iterator).second];
    const auto &bufferView = model.bufferViews[accessor.bufferView];
    const auto bufferIdx = bufferView.buffer;

    GL_CHECK(glEnableVertexAttribArray(attributeLocation));
    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, buffers->at(bufferIdx)));

    const auto byteOffset = accessor.byteOffset + bufferView.byteOffset;
    GL_CHECK(glVertexAttribPointer(
        attributeLocation, accessor.type, accessor.componentType, GL_FALSE,
        bufferView.byteStride, (const GLvoid *)byteOffset));
  }
  std::shared_ptr<Primitive> meshPrimitive;
  if (primitive.indices >= 0) {
    const auto &accessor = model.accessors[primitive.indices];
    const auto &bufferView = model.bufferViews[accessor.bufferView];
    const auto bufferIndex = bufferView.buffer;
    GL_CHECK(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffers->at(bufferIndex)));
    meshPrimitive = std::make_shared<Primitive>(
        vaos->at(primitiveIndex), primitive.mode, accessor.count,
        accessor.componentType, accessor.byteOffset + bufferView.byteOffset);
  } else {
    const auto accessorIndex = (*begin(primitive.attributes)).second;
    const auto &accessor = model.accessors[accessorIndex];
    meshPrimitive =
        std::make_shared<Primitive>(vaos->at(primitiveIndex), primitive.mode,
                                    accessor.count, accessor.componentType);
  }
  meshPrimitive->setMaterial(
      buildMaterial(model, primitive.material, textures));
  return meshPrimitive;
}

关键点是对于attributes所指定的字段的索引号,以及indices的索引号,通过accessor得到buffer数据块中不同部分的访问方式,再结合buffer view来确定这个字段所对应的数据,比如:

// accessor 0
{
  "bufferView": 0,
  "byteOffset": 0,
  "componentType": 5123,
  "count": 36,
  "max": [
    23
  ],
  "min": [
    0
  ],
  "type": "SCALAR"
}

// buffer view 0
{
  "buffer": 0,
  "byteOffset": 768,
  "byteLength": 72,
  "target": 34963
}

这里accessor 0又指向了buffer view 0accessor 0中指定了偏移量为0个字节,buffer view 0中指定了偏移量为768个字节,最终在数据块上取数据的偏移量就是两个相加也就是768字节。componentType对应的是GL_UNSIGNED_SHORTaccessor 0中指定取36个值,buffer view 0中指定取长度为72个字节的数据,而一个GL_UNSIGNED_SHORT占用2个字段,36个正好是72个字节,这就对应上了。而target值34963对应GL_ELEMENT_ARRAY_BUFFER,就也就顶点索引,因此accessor 0实际上提供就是顶点索引数组。

可以把它打印出来看看验证一下:

std::shared_ptr<std::vector<GLuint>> buildBuffers(const tinygltf::Model &model)
{
  auto buffers = std::make_shared<std::vector<GLuint>>(model.buffers.size(), 0);
  GL_CHECK(glGenBuffers(buffers->size(), buffers->data()));
  for (auto i = 0; i < model.buffers.size(); ++i) {
    GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, buffers->at(i)));
    GL_CHECK(glBufferData(GL_ARRAY_BUFFER, model.buffers[i].data.size(),
        model.buffers[i].data.data(), GL_STATIC_DRAW));
  }
  for (int i = 768; i < 768 + 72; i += 2) {
    unsigned int index;
    memcpy(&index, model.buffers[0].data.data() + i, 2);
    // print
  }
  GL_CHECK(glBindBuffer(GL_ARRAY_BUFFER, 0));
  return buffers;
}
index 0 = 0
index 1 = 1
index 2 = 2
index 3 = 3
index 4 = 2
index 5 = 1
index 6 = 4
index 7 = 5
index 8 = 6
index 9 = 7
index 10 = 6
index 11 = 5
index 12 = 8
index 13 = 9
index 14 = 10
index 15 = 11
index 16 = 10
index 17 = 9
index 18 = 12
index 19 = 13
index 20 = 14
index 21 = 15
index 22 = 14
index 23 = 13
index 24 = 16
index 25 = 17
index 26 = 18
index 27 = 19
index 28 = 18
index 29 = 17
index 30 = 20
index 31 = 21
index 32 = 22
index 33 = 23
index 34 = 22
index 35 = 21

可以看到,这36个索引号,范围是从0~23,一共24个,正方体6个面,每个面2个三角形,每个三角形有3个顶点索引,这两个三角形有2个顶点是共用的,因此索引数组共有3*2*6=36个值,索引号共有4*6=24个

下面来看attributes,对于Primitive中的attribute

"attributes": {
  "NORMAL": 1,
  "POSITION": 2,
  "TEXCOORD_0": 3
}

后面的编号也是Accessor的索引,和上面说的indices道理是一样的。对于attributes这里通过glVertexAttribPointer中给每个attribute指定数据typestrideoffset,并且这里我们使用了vao,通过vao就能一次性地把所有attributeglVertexAttribPointer配置给记录下来,使用时只需绑定vao即可,否则渲染前需要对所有attribute都重新来一遍glVertexAttribPointer配置,就比较麻烦。

至此就完成了Primitive的创建,此时还只创建了形状,并没有纹理,接下来我们来创建Material,里面就包含了纹理信息:

std::shared_ptr<Material>
Engine::buildMaterial(const tinygltf::Model &model, unsigned int materialIndex,
                      const std::shared_ptr<std::vector<GLuint>> &textures) {
  auto baseColorIndex = model.materials[materialIndex]
                            .pbrMetallicRoughness.baseColorTexture.index;
  auto baseColorTexture =
      (baseColorIndex >= 0 ? textures->at(baseColorIndex)
                           : buildDefaultBaseColorTexture(model));
  const auto baseColorTextureLocation = GL_CHECK(
      glGetUniformLocation(program_->getProgram(), UNIFORM_BASE_COLOR_TEXTURE));
  return std::make_shared<Material>(baseColorTexture, baseColorTextureLocation);
}

glTF中的Material比较复杂,参见:github.com/KhronosGrou…

这里只实现了base color texture,尚未包含PBR等效果,后续文章我们再来完善,这里注意base color texture也是可以不配的,这里我们创建一个白色纹理来做为默认的颜色。

这样我们渲染前所需要的数据就准备好了。

渲染

我们先来看shader

// vertex shader
#version 300 es
precision mediump float;
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_normal;
layout(location = 2) in vec2 a_texCoord0;
out vec2 v_texCoord0;
uniform mat4 u_modelViewProjectMatrix;
void main() {
    gl_Position = u_modelViewProjectMatrix * a_position;
    v_texCoord0 = a_texCoord0;
}

// fragment shader
#version 300 es
precision mediump float;
in vec2 v_texCoord0;
out vec4 outColor;
uniform sampler2D u_baseColorTexture;
void main() {
    outColor = texture(u_baseColorTexture, v_texCoord0);
}

由于我们现在只实现了base color,所以shader很简单,vertex shader中有model-view-project矩阵变换,fragment shader中就简单地取base color texture就行了。

前面提到Primitive是一次draw call的单位,因此我们对Primitive来执行draw call

void Primitive::draw() {
  material_->bind();
  GL_CHECK(glBindVertexArray(vao_));
  if (offset_ >= 0) {
    GL_CHECK(glDrawElements(mode_, count_, componentType_, (void *)offset_));
  } else {
    GL_CHECK(glDrawArrays(mode_, 0, count_));
  }
  GL_CHECK(glBindVertexArray(0));
}

根据glTF的配置,如果是采用了顶点索引的方式,我们就用glDrawElements渲染,否则采用glDrawArrays

The index of the accessor that contains mesh indices. When this is not defined, the primitives should be rendered without indices using drawArrays().

参见:github.com/KhronosGrou…

下面我们来遍历所有Node进行渲染,在Engine里先绑定program,并开启深度测试。

Node遍历时计算model-view-project矩阵,这里注意Node的变换矩阵是要一直乘到根节点。

Nodedraw()最终会调到Primitive的渲染。

void Engine::drawFrame() {
  if (!initialized_) {
    init();
    initialized_ = true;
  }
  program_->bind();
  GL_CHECK(glEnable(GL_DEPTH_TEST));
  GL_CHECK(glViewport(0, 0, width, height));
  GL_CHECK(glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
  GL_CHECK(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
  for (auto &scene : scenes_) {
    scene->traverse([&](const std::shared_ptr<Node> &node) {
      auto modelViewProjectMatrix = camera_->getProjectMatrix() *
                                    camera_->getViewMatrix() *
                                    node->getWorldMatrix();
      auto location = GL_CHECK(glGetUniformLocation(
          program_->getProgram(), UNIFORM_MODEL_VIEW_PROJECT_MATRIX));
      GL_CHECK(glUniformMatrix4fv(location, 1, false,
                                  glm::value_ptr(modelViewProjectMatrix)));
      node->draw();
    });
  }
}

为简单起见这里我们使用一个自定义的Camera,其实glTF里也可以定义Camera。我们让Camera做圆周运动,围绕观察点(0,0,0)旋转,这样就是一个围绕glTF模型观察的效果:

extern "C" JNIEXPORT void
JNICALL Java_io_github_kenneycode_triangle_example_MainActivity_drawFrame(JNIEnv *env, jobject /* this */, jint width, jint height, jlong timestamp) {
    if (engine == nullptr) {
        engine = std::make_shared<triangle::Engine>(width, height);
        camera = std::make_shared<triangle::Camera>(glm::vec3(0.0f, 0.0f, R), glm::vec3(0.0f, 0.0f, 0.0f),
                                                    glm::vec3(0.0f, 1.0f, 0.0f), 70.0f, float(width) / height, 1.0f, 10.0f);
        engine->setDefaultCamera(camera);
        engine->loadGLTF("/sdcard/Duck/glTF/Duck.gltf");
    }
    auto theta = -timestamp % 100000 / 1000.0f;
    x = R * cos(theta);
    z = R * sin(theta);
    camera->setPosition(glm::vec3(x, 0.0f, z));
    engine->drawFrame();
}

下面是一些模型的效果,第一个就是本文一开始贴出来的那个glTF。由于只用了base color texture,没有PBR效果,没有光照,效果还比较粗糙:

demo4.gif

demo0.gif

demo2.gif

谢谢阅读!如有疑问,欢迎在评论区交流~

代码在我的github上:github.com/kenneycode/…