OpenGL学习笔记(assimp封装、深度测试、模板测试)

目录

  • 模型加载
    • Assimp
    • 网格
    • 模型及导入
  • 深度测试
    • 深度值精度
    • 深度缓冲的可视化
    • 深度冲突
  • 模板测试
    • 物体轮廓

GitHub主页:https://github.com/sdpyy1
OpenGL学习仓库:https://github.com/sdpyy1/CppLearn/tree/main/OpenGLtree/main/OpenGL):https://github.com/sdpyy1/CppLearn/tree/main/OpenGL

模型加载

模型通常都由3D艺术家在Blender、3DS Max或者Maya这样的工具中精心制作。这些所谓的3D建模工具(3D Modeling Tool)可以让艺术家创建复杂的形状,并使用一种叫做UV映射(uv-mapping)的手段来应用贴图。这样子艺术家们即使不了解图形技术细节的情况下,也能拥有一套强大的工具来构建高品质的模型了。所有的技术细节都隐藏在了导出的模型文件中。但是,作为图形开发者,我们就必须要了解这些技术细节了。

  • 像是Wavefront.obj这样的模型格式,只包含了模型数据以及材质信息,像是模型颜色和漫反射/镜面光贴图。
  • 而以XML为基础的Collada文件格式则非常的丰富,包含模型、光照、多种材质、动画数据、摄像机、完整的场景信息等等。

Assimp

一个非常流行的模型导入库是Assimp,它是Open Asset Import Library(开放的资产导入库)的缩写。

当使用Assimp导入一个模型的时候,它通常会将整个模型加载进一个场景(Scene)对象,它会包含导入的模型/场景中的所有数据。Assimp会将场景载入为一系列的节点(Node),每个节点包含了场景对象中所储存数据的索引,每个节点都可以有任意数量的子节点。Assimp数据结构的(简化)模型如下
在这里插入图片描述

网格

通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。我们最终仍要将这些数据转换为OpenGL能够理解的格式,这样才能渲染这个物体。我们从上一节中学到,网格(Mesh)代表的是单个的可绘制实体,我们现在先来定义一个我们自己的网格类。这里就直接用它的代码就行。

//
// Created by Administrator on 2025/4/7.
//#ifndef OPENGL_MESH_H
#define OPENGL_MESH_H
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/vec3.hpp>
#include <glm/vec2.hpp>
#include <string>
#include "Shader.h"using namespace std;
#define MAX_BONE_INFLUENCE 4struct Vertex {// positionglm::vec3 Position;// normalglm::vec3 Normal;// texCoordsglm::vec2 TexCoords;// tangentglm::vec3 Tangent;// bitangentglm::vec3 Bitangent;//bone indexes which will influence this vertexint m_BoneIDs[MAX_BONE_INFLUENCE];//weights from each bonefloat m_Weights[MAX_BONE_INFLUENCE];
};struct Texture {unsigned int id;string type;string path;
};class Mesh {
public:// mesh Datavector<Vertex>       vertices;vector<unsigned int> indices;vector<Texture>      textures;unsigned int VAO;// constructorMesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures){this->vertices = vertices;this->indices = indices;this->textures = textures;// now that we have all the required data, set the vertex buffers and its attribute pointers.setupMesh();}// render the meshvoid Draw(Shader &shader){// bind appropriate texturesunsigned int diffuseNr  = 1;unsigned int specularNr = 1;unsigned int normalNr   = 1;unsigned int heightNr   = 1;for(unsigned int i = 0; i < textures.size(); i++){glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding// retrieve texture number (the N in diffuse_textureN)string number;string name = textures[i].type;if(name == "texture_diffuse")number = std::to_string(diffuseNr++);else if(name == "texture_specular")number = std::to_string(specularNr++); // transfer unsigned int to stringelse if(name == "texture_normal")number = std::to_string(normalNr++); // transfer unsigned int to stringelse if(name == "texture_height")number = std::to_string(heightNr++); // transfer unsigned int to string// now set the sampler to the correct texture unitglUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);// and finally bind the textureglBindTexture(GL_TEXTURE_2D, textures[i].id);}// draw meshglBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);glBindVertexArray(0);// always good practice to set everything back to defaults once configured.glActiveTexture(GL_TEXTURE0);}private:// render dataunsigned int VBO, EBO;// initializes all the buffer objects/arraysvoid setupMesh(){// create buffers/arraysglGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glGenBuffers(1, &EBO);glBindVertexArray(VAO);// load data into vertex buffersglBindBuffer(GL_ARRAY_BUFFER, VBO);// A great thing about structs is that their memory layout is sequential for all its items.// The effect is that we can simply pass a pointer to the struct and it translates perfectly to a glm::vec3/2 array which// again translates to 3/2 floats which translates to a byte array.glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);// set the vertex attribute pointers// vertex PositionsglEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);// vertex normalsglEnableVertexAttribArray(1);glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));// vertex texture coordsglEnableVertexAttribArray(2);glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));// vertex tangentglEnableVertexAttribArray(3);glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));// vertex bitangentglEnableVertexAttribArray(4);glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));// idsglEnableVertexAttribArray(5);glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));// weightsglEnableVertexAttribArray(6);glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));glBindVertexArray(0);}
};#endif //OPENGL_MESH_H

模型及导入

目前只定义了一个渲染单位,下一步定义模型类

//
// Created by Administrator on 2025/4/7.
//#ifndef OPENGL_MODEL_H
#define OPENGL_MODEL_H
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/vec3.hpp>
#include <glm/vec2.hpp>
#include <string>
#include "Shader.h"
#include "mesh.h"
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
using namespace std;unsigned int TextureFromFile(const char *path, const string &directory, bool gamma = false);class Model
{
public:// model datavector<Texture> textures_loaded;	// stores all the textures loaded so far, optimization to make sure textures aren't loaded more than once.vector<Mesh>    meshes;string directory;bool gammaCorrection;// constructor, expects a filepath to a 3D model.Model(string const &path, bool gamma = false) : gammaCorrection(gamma){loadModel(path);}// draws the model, and thus all its meshesvoid Draw(Shader &shader){for(unsigned int i = 0; i < meshes.size(); i++)meshes[i].Draw(shader);}private:// loads a model with supported ASSIMP extensions from file and stores the resulting meshes in the meshes vector.void loadModel(string const &path){// read file via ASSIMPAssimp::Importer importer;const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);// check for errorsif(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero{cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << endl;return;}// retrieve the directory path of the filepathdirectory = path.substr(0, path.find_last_of('/'));// process ASSIMP's root node recursivelyprocessNode(scene->mRootNode, scene);}// processes a node in a recursive fashion. Processes each individual mesh located at the node and repeats this process on its children nodes (if any).void processNode(aiNode *node, const aiScene *scene){// process each mesh located at the current nodefor(unsigned int i = 0; i < node->mNumMeshes; i++){// the node object only contains indices to index the actual objects in the scene.// the scene contains all the data, node is just to keep stuff organized (like relations between nodes).aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];meshes.push_back(processMesh(mesh, scene));}// after we've processed all of the meshes (if any) we then recursively process each of the children nodesfor(unsigned int i = 0; i < node->mNumChildren; i++){processNode(node->mChildren[i], scene);}}Mesh processMesh(aiMesh *mesh, const aiScene *scene){// data to fillvector<Vertex> vertices;vector<unsigned int> indices;vector<Texture> textures;// walk through each of the mesh's verticesfor(unsigned int i = 0; i < mesh->mNumVertices; i++){Vertex vertex;glm::vec3 vector; // we declare a placeholder vector since assimp uses its own vector class that doesn't directly convert to glm's vec3 class so we transfer the data to this placeholder glm::vec3 first.// positionsvector.x = mesh->mVertices[i].x;vector.y = mesh->mVertices[i].y;vector.z = mesh->mVertices[i].z;vertex.Position = vector;// normalsif (mesh->HasNormals()){vector.x = mesh->mNormals[i].x;vector.y = mesh->mNormals[i].y;vector.z = mesh->mNormals[i].z;vertex.Normal = vector;}// texture coordinatesif(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?{glm::vec2 vec;// a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't// use models where a vertex can have multiple texture coordinates so we always take the first set (0).vec.x = mesh->mTextureCoords[0][i].x;vec.y = mesh->mTextureCoords[0][i].y;vertex.TexCoords = vec;// tangentvector.x = mesh->mTangents[i].x;vector.y = mesh->mTangents[i].y;vector.z = mesh->mTangents[i].z;vertex.Tangent = vector;// bitangentvector.x = mesh->mBitangents[i].x;vector.y = mesh->mBitangents[i].y;vector.z = mesh->mBitangents[i].z;vertex.Bitangent = vector;}elsevertex.TexCoords = glm::vec2(0.0f, 0.0f);vertices.push_back(vertex);}// now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices.for(unsigned int i = 0; i < mesh->mNumFaces; i++){aiFace face = mesh->mFaces[i];// retrieve all indices of the face and store them in the indices vectorfor(unsigned int j = 0; j < face.mNumIndices; j++)indices.push_back(face.mIndices[j]);}// process materialsaiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];// we assume a convention for sampler names in the shaders. Each diffuse texture should be named// as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER.// Same applies to other texture as the following list summarizes:// diffuse: texture_diffuseN// specular: texture_specularN// normal: texture_normalN// 1. diffuse mapsvector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());// 2. specular mapsvector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());// 3. normal mapsstd::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());// 4. height mapsstd::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());// return a mesh object created from the extracted mesh datareturn Mesh(vertices, indices, textures);}// checks all material textures of a given type and loads the textures if they're not loaded yet.// the required info is returned as a Texture struct.vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName){vector<Texture> textures;for(unsigned int i = 0; i < mat->GetTextureCount(type); i++){aiString str;mat->GetTexture(type, i, &str);// check if texture was loaded before and if so, continue to next iteration: skip loading a new texturebool skip = false;for(unsigned int j = 0; j < textures_loaded.size(); j++){if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0){textures.push_back(textures_loaded[j]);skip = true; // a texture with the same filepath has already been loaded, continue to next one. (optimization)break;}}if(!skip){   // if texture hasn't been loaded already, load itTexture texture;texture.id = TextureFromFile(str.C_Str(), this->directory);texture.type = typeName;texture.path = str.C_Str();textures.push_back(texture);textures_loaded.push_back(texture);  // store it as texture loaded for entire model, to ensure we won't unnecessary load duplicate textures.}}return textures;}
};unsigned int TextureFromFile(const char *path, const string &directory, bool gamma)
{string filename = string(path);filename = directory + '/' + filename;unsigned int textureID;glGenTextures(1, &textureID);int width, height, nrComponents;unsigned char *data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);if (data){GLenum format;if (nrComponents == 1)format = GL_RED;else if (nrComponents == 3)format = GL_RGB;else if (nrComponents == 4)format = GL_RGBA;glBindTexture(GL_TEXTURE_2D, textureID);glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);glGenerateMipmap(GL_TEXTURE_2D);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);stbi_image_free(data);}else{std::cout << "Texture failed to load at path: " << path << std::endl;stbi_image_free(data);}return textureID;
}
#endif //OPENGL_MODEL_H

把这些代码背下来也没什么用,用的时候能理解它怎么处理即可
在这里插入图片描述

深度测试

其实就是之前学习的ZBuffer,但有一点不同,OpenGL是在片段着色器之后执行深度测试的,这与理解不同,因为完全可以先判断需不需要渲染再执行渲染,后来看文章解释到,因为在片段着色器中可以修改深度值,如果提前判断是不合理的。

现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段。当使用提前深度测试时,片段着色器的一个限制是你不能写入片段的深度值。如果一个片段着色器对它的深度值进行了写入,提前深度测试是不可能的。OpenGL不能提前知道深度值。
这里只介绍OpenGL对深度测试比较特殊的地方。OpenGL允许我们修改深度测试中使用的比较运算符。

glDepthFunc(GL_LESS);

在这里插入图片描述

深度值精度

深度缓冲包含了一个介于0.0和1.0之间的深度值,它将会与观察者视角所看见的场景中所有物体的z值进行比较。在观察空间中(视图变换之后)z值可能是投影平截头体的近平面(Near)和远平面(Far)之间的任何值。我们需要一种方式来将这些观察空间的z值变换到[0, 1]范围之间,其中的一种方式就是将它们线性变换到[0, 1]范围之间。
在这里插入图片描述
就是求z在两面之间的比例。这种变化是线性的。
在这里插入图片描述
然而,在实践中是几乎永远不会使用这样的线性深度缓冲(Linear Depth Buffer)的。用下面的方程能达到更好的效果,因为离摄像机很远的地方并不需要多大的精度,而近处需要更大的精度(即z轴距离很近的点反映到深度缓存中值差距也很大,这样就更好区分谁前谁后)
在这里插入图片描述
可以看到,深度值很大一部分是由很小的z值所决定的,这给了近处的物体很大的深度精度。
在这里插入图片描述
这里可以看出深度缓存中值为0.5,在观察空间中并不是中点。
这个方程是嵌入到投影矩阵的,投影矩阵执行完后得到裁剪空间,裁剪空间进行透视除法得到NDC空间。

深度缓冲的可视化

在片段着色器中,有内建参数gl_FragCoord向量的z值,包含了该像素的深度值,可以把这个深度值输出为颜色,深度值范围为[0,1]

void main()
{FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
}

修改后发现模型全白
在这里插入图片描述
这是因为深度值是非线性的,在z值很大的时候精度很低,所以看上去都接近1了,因为摄像机离得比较远,只有贴这摄像机的部分才会变化很大。这样我们慢慢靠近让z值变小过程就会发现模型逐渐变成灰色
在这里插入图片描述
这里可以看出z值比较小的情况下,移动一点就会让z值变化很大
在这里插入图片描述
学到这我大概知道了为什么z插值时需要透视矫正了,因为z值变化是非线性的,而插值是一个线性的过程。
我们也可以通过一些处理来让非线性变化转为线性的。这也就意味着我们需要首先将深度值从[0, 1]范围重新变换到[-1, 1]范围的标准化设备坐标,紧接着反过来使用投影矩阵来还原

float LinearizeDepth(float depth)
{float z = depth * 2.0 - 1.0; // 转换为 NDCreturn  (2.0 * near * far) / (far + near - z * (far - near));
}void main()
{float depth = LinearizeDepth(gl_FragCoord.z) / far; FragColor = vec4(vec3(depth), 1.0);
}

在这里插入图片描述
离得越近颜色越暗,可以看出变化是线性的
在这里插入图片描述

深度冲突

一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序。这个现象叫做深度冲突(Z-fighting)。
根据前边学到的z值的非线性变化,当z值很大时,精度很低,z-fighting现象会更明显

防止z-fighting的方法:

  • 永远不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠
  • 尽可能将近平面设置远一些
  • 使用更高精度的深度缓冲

模板测试

模板测试在片段着色器之后,深度测试之前。他的效果大概就是对每个像素进行了一次if操作
在这里插入图片描述

通过在渲染时修改模板缓冲的内容,我们写入了模板缓冲。。在同一个(或者接下来的)帧中,我们可以读取这些值,来决定丢弃还是保留某个片段。使用模板缓冲的时候你可以尽情发挥
启动模板测试

glEnable(GL_STENCIL_TEST);

注意在循环中也需要clear模板缓冲,类似深度缓冲

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

可以用glStencilMask来控制缓冲的写入方式,他的执行方法就是glStencilMask的属于与要写入的数据进行AND操作,来得到最终的写入数据

glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会变成0(禁用写入)

和深度测试一样,我们对模板缓冲应该通过还是失败,以及它应该如何影响模板缓冲,也是有一定控制的。一共有两个函数能够用来配置模板测试:glStencilFuncglStencilOp
glStencilFunc(GLenum func, GLint ref, GLuint mask)一共包含三个参数:

  • func:设置模板测试函数(Stencil Test Function),GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。
  • ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。
  • mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。

具体的测试方法就是:(ref & mask) xxxxx (stencil_value & mask) 其中xxxx就是在第一个参数设置的测试函数

glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)一共包含三个选项,我们能够设定每个选项应该采取的行为:主要是针对模板缓冲是该如何修改

  • sfail:模板测试失败时采取的行为。
  • dpfail:模板测试通过,但深度测试失败时采取的行为。
  • dppass:模板测试和深度测试都通过时采取的行为。
    在这里插入图片描述
    所以,我们可以通过glStencilFunc设置如何比较,用glStencilOp设置缓存数据如何修改

物体轮廓

在这里插入图片描述

  1. 启用模板写入。
  2. 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
  3. 渲染物体。
  4. 禁用模板写入以及深度测试。
  5. 将每个物体缩放一点点(其实是扩大一点点,这样外边框的模板缓存中是0,内部都是1)。
  6. 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
  7. 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
  8. 再次启用模板写入和深度测试。

// 写出来的循环是这样的

    while (!glfwWindowShouldClose(window)){auto currentFrame = static_cast<float>(glfwGetTime());deltaTime = currentFrame - lastFrame;lastFrame = currentFrame;processInput(window);// 清理窗口glClearColor(0.05f, 0.05f, 0.05f, 1.0f);// 启动模板测试glEnable(GL_STENCIL_TEST);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);// 绘制物体glStencilFunc(GL_ALWAYS, 1, 0xFF); // 缓存会与1进行比较,但比较运算这里设置的永远通过glStencilMask(0xFF); // 启用模板缓冲写入bagShader.use();drawModel(bagShader, model,{0.f,0.f,0.f},{1.0f,1.0f,1.0f});// 绘制新的glStencilFunc(GL_NOTEQUAL, 1, 0xFF); // 缓存与1比较,与1不相同才能通过glStencilMask(0x00); // 禁止模板缓冲的写入glDisable(GL_DEPTH_TEST);layoutShader.use();drawModel(layoutShader,model,{0.f,0.f,0.f},{1.1f,1.1f,1.1f});glEnable(GL_DEPTH_TEST);glStencilMask(0xFF);  // 这一行代码有坑,如果你不写,下次循环的clear就没法清空缓存,出现BUG// 事件处理glfwPollEvents();// 双缓冲glfwSwapBuffers(window);}

在这里插入图片描述
在这里插入图片描述
要想实现穿过其他物体来显示边框,需要注意渲染其他物体时,要关闭模板测试,渲染完再打开,否则会污染模板缓存

    while (!glfwWindowShouldClose(window)){auto currentFrame = static_cast<float>(glfwGetTime());deltaTime = currentFrame - lastFrame;lastFrame = currentFrame;processInput(window);// 清理窗口glClearColor(0.05f, 0.05f, 0.05f, 1.0f);// 启动模板测试glEnable(GL_STENCIL_TEST);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);// 绘制物体glStencilFunc(GL_ALWAYS, 1, 0xFF); // 缓存会与1进行比较,但比较运算这里设置的永远通过glStencilMask(0xFF); // 启用模板缓冲写入bagShader.use();drawModel(bagShader, model,{1.f,0.f,-3.f},{1.0f,1.0f,1.0f});glDisable(GL_STENCIL_TEST);drawModel(bagShader, model,{0.f,0.f,0.f},{1.0f,1.0f,1.0f});glEnable(GL_STENCIL_TEST);// 绘制新的glStencilFunc(GL_NOTEQUAL, 1, 0xFF); // 缓存与1比较,与1不相同才能通过glStencilMask(0x00); // 禁止模板缓冲的写入glDisable(GL_DEPTH_TEST);layoutShader.use();drawModel(layoutShader, model,{1.f,0.f,-3.f},{1.1f,1.1f,1.1f});glEnable(GL_DEPTH_TEST);glStencilMask(0xFF);// 事件处理glfwPollEvents();// 双缓冲glfwSwapBuffers(window);}

在这里插入图片描述

我觉得他这里对模板测试的作用描述不是很清晰,我理解是第一次渲染时对每个像素设置模板缓冲,第二次渲染时利用第一次渲染的模板缓冲来实现各种效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/76534.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通过AWS EKS 生成并部署容器化应用

今天给大家分享一个实战例子&#xff0c;如何在EKS上创建容器化应用并通过ALB来发布。先介绍一下几个基本概念&#xff1a; IAM, OpenID Connect (OIDC) 2014 年&#xff0c;AWS Identity and Access Management 增加了使用 OpenID Connect (OIDC) 的联合身份支持。此功能允许…

入侵检测snort功能概述

1. 数据包嗅探与日志记录 网络流量监控&#xff1a;实时捕获和分析网络数据包&#xff08;支持以太网、无线等&#xff09;。 日志记录&#xff1a;将数据包以二进制格式&#xff08;pcap&#xff09;或文本格式存储&#xff0c;供后续分析。 2. 协议分析与解码 深度协议解析…

【Easylive】定时任务-每日数据统计和临时文件清理

【Easylive】项目常见问题解答&#xff08;自用&持续更新中…&#xff09; 汇总版 这个定时任务系统主要包含两个核心功能&#xff1a;每日数据统计和临时文件清理。下面我将详细解析这两个定时任务的实现逻辑和技术要点&#xff1a; Component Slf4j public class SysTas…

蓝桥杯 15g

班级活动 问题描述 小明的老师准备组织一次班级活动。班上一共有 nn 名 (nn 为偶数) 同学&#xff0c;老师想把所有的同学进行分组&#xff0c;每两名同学一组。为了公平&#xff0c;老师给每名同学随机分配了一个 nn 以内的正整数作为 idid&#xff0c;第 ii 名同学的 idid 为…

如何使用AI辅助开发R语言

R语言是一种用于统计计算和图形生成的编程语言和软件环境&#xff0c;很多学术研究和数据分析的科学家和统计学家更青睐于它。但对与没有编程基础的初学者而言&#xff0c;R语言也是有一定使用难度的。不过现在有了通义灵码辅助编写R语言代码&#xff0c;我们完全可以用自然语言…

CISCO组建RIP V2路由网络

1.实验准备&#xff1a; 2.具体配置&#xff1a; 2.1根据分配好的IP地址配置静态IP&#xff1a; 2.1.1PC配置&#xff1a; PC0&#xff1a; PC1&#xff1a; PC2&#xff1a; 2.1.2路由器配置&#xff1a; R0&#xff1a; Router>en Router#conf t Enter configuration…

React + TipTap 富文本编辑器 实现消息列表展示,类似Slack,Deepseek等对话框功能

经过几天折腾再折腾&#xff0c;弄出来了&#xff0c;弄出来了&#xff01;&#xff01;&#xff01; 消息展示 在位编辑功能。 两个tiptap实例1个用来展示 消息列表&#xff0c;一个用来在位编辑消息。 tiptap灵活富文本编辑器&#xff0c;拓展性太好了!!! !!! 关键点&#x…

Ubuntu搭建Pytorch环境

Ubuntu搭建Pytorch环境 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 Ubuntu搭建Pytorch环境前言一、Anaconda二、Cuda1.安装流程2、环境变量&#…

Sping Cloud配置和注册中心

1.Nacos实现原理了解吗&#xff1f; Nacos是注册中心&#xff0c;主要是帮助我们管理服务列表。Nacos的实现原理大概可以从下面三个方面来讲&#xff1a; 服务注册与发现&#xff1a;当一个服务实例启动时&#xff0c;它会向Nacos Server发送注册请求&#xff0c;将自己的信息…

C++笔记之父类引用是否可以访问到子类特有的属性?

C++笔记之父类引用是否可以访问到子类特有的属性? code review! 参考笔记 1.C++笔记之在基类和派生类之间进行类型转换的所有方法 文章目录 C++笔记之父类引用是否可以访问到子类特有的属性?1.主要原因2.示例代码3.说明4.如何访问子类特有的属性5.注意事项6.总结在 C++ 中,…

JavaScript逆向工程:如何判断对称加密与非对称加密

在现代Web应用安全分析中&#xff0c;加密算法的识别是JavaScript逆向工程的关键环节。本文将详细介绍如何在逆向工程中判断JavaScript代码使用的是对称加密还是非对称加密。 一、加密算法基础概念 1. 对称加密 (Symmetric Encryption) 特点&#xff1a;加密和解密使用相同的…

物理备份工具 BRM vs gs_probackup

什么是BRM 上一篇文章讲了openGauss的物理备份工具gs_probackup&#xff0c;今天来说说BRM备份工具。 BRM备份恢复工具全称为&#xff1a;Backup and Recovery Manager&#xff0c;是MogDB基于opengauss的备份工具 gs_probackup 做了一些封装和优化,面向MogDB数据库实现备份和…

问问lua怎么写DeepSeek,,,,,

很坦白说&#xff0c;这十年&#xff0c;我几乎没办法从互联网找到这个这样的代码&#xff0c;互联网引擎找不到&#xff0c;我也没有很大的“追求”要传承&#xff0c;或者要宣传什么&#xff1b;直到DeepSeek的出现 兄弟&#xff0c;Deepseek现在已经比你更了解你楼下的超市…

react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR

需求背景 在开发过程中可能会存在用户上传一张图片后下方需要自己识别出来文字数字等信息&#xff0c;有的时候会通过后端来识别后返回&#xff0c;但是也会存在纯前端去识别的情况&#xff0c;这个时候就需要使用到Tesseract.js这个库了 附Tesseract.js官方&#xff08;htt…

蓝桥杯考前复盘

明天就是考试了&#xff0c;适当的停下刷题的步伐。 静静回望、思考、总结一下&#xff0c;我走过的步伐。 考试不是结束&#xff0c;他只是检测这一段时间学习成果的工具。 该继续走的路&#xff0c;还是要继续走的。 只是最近&#xff0c;我偶尔会感到迷惘&#xff0c;看…

前端-Vue3

1. Vue3简介 2020年9月18日&#xff0c;Vue.js发布版3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;n 经历了&#xff1a;4800次提交、40个RFC、600次PR、300贡献者 官方发版地址&#xff1a;Release v3.0.0 One Piece vuejs/core 截止2023年10月&#xff0c;最…

[ctfshow web入门] web39

信息收集 题目发生了微妙的变化&#xff0c;只过滤flag&#xff0c;include后固定跟上了.php。且没有了echo $flag;&#xff0c;虽说本来就没什么用 if(isset($_GET[c])){$c $_GET[c];if(!preg_match("/flag/i", $c)){include($c.".php");} }else{…

【动手学深度学习】LeNet:卷积神经网络的开山之作

【动手学深度学习】LeNet&#xff1a;卷积神经网络的开山之作 1&#xff0c;LeNet卷积神经网络简介2&#xff0c;Fashion-MNIST图像分类数据集3&#xff0c;LeNet总体架构4&#xff0c;LeNet代码实现4.1&#xff0c;定义LeNet模型4.2&#xff0c;定义模型评估函数4.3&#xff0…

代码随想录第15天:(二叉树)

一、二叉搜索树的最小绝对差&#xff08;Leetcode 530&#xff09; 思路1 &#xff1a;中序遍历将二叉树转化为有序数组&#xff0c;然后暴力求解。 class Solution:def __init__(self):# 初始化一个空的列表&#xff0c;用于保存树的节点值self.vec []def traversal(self, r…

计算机操作系统-【死锁】

文章目录 一、什么是死锁&#xff1f;死锁产生的原因&#xff1f;死锁产生的必要条件&#xff1f;互斥条件请求并保持不可剥夺环路等待 二、处理死锁的基本方法死锁的预防摒弃请求和保持条件摒弃不可剥夺条件摒弃环路等待条件 死锁的避免银行家算法案例 提示&#xff1a;以下是…