当前位置: 首页 > news >正文

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);}

在这里插入图片描述

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

相关文章:

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

目录 模型加载Assimp网格模型及导入 深度测试深度值精度深度缓冲的可视化深度冲突 模板测试物体轮廓 GitHub主页&#xff1a;https://github.com/sdpyy1 OpenGL学习仓库:https://github.com/sdpyy1/CppLearn/tree/main/OpenGLtree/main/OpenGL):https://github.com/sdpyy1/CppL…...

自动化备份全网服务器数据平台

1.项目说明 1.1概述 该项目共分为2个子项目&#xff0c;由环境搭建和实施备份两部分组成 该项目旨在复习巩固系统服务部署使用、shell编程等知识&#xff0c;旨在让学生增加知识面&#xff0c;提高项目实习经历&#xff0c;充实简历 1.2项目组织方式及时间 时间&#xff1a…...

Trea CN多多与主流AI编程工具万字解析

Trea CN多多与主流AI编程工具万字解析 &#xff08;含数学建模、架构图、开发流程可视化&#xff09; 一、数学建模&#xff1a;代码生成效率量化模型 1.1 全链路效率公式 T total N ⋅ ( 1 λ C S ) T check (1) T_{\text{total}} N \cdot \left( \frac{1}{\lambda} \…...

Django从零搭建卖家中心注册页面实战

在电商系统开发中&#xff0c;卖家中心是一个重要的组成部分&#xff0c;而用户注册与登陆则是卖家中心的第一步。本文将详细介绍如何使用Django框架从零开始搭建一个功能完善的卖家注册页面&#xff0c;包括前端界面设计和后端逻辑实现。 一、项目概述 我们将创建一个名为sel…...

如何进行预算考核

✅ 一、预算考核体系总体架构 模块内容说明考核内容1. 预算目标/指标完成情况2. 预算编制/执行情况双轮驱动,目标 + 执行双考核考核对象高层、中层、基层、后台支持分层分类考核考核周期月度(滚动)+ 季度(校验)+ 年度(决算)提高适应性和准确性考核工具指标体系、差错率评…...

django相关面试题

django相关面试题 1.django的生命周期 2.django中的orm查询如何自定义方法 3.django中的中间件的作用 4.django中间件&#xff0c;request进来经过哪些中间件&#xff0c;顺序是怎么样的 5.django中的csrf是什么 6.django每访问一次数据库都会创建一个连接吗 7.uwsgi gunicorn…...

【Java面试系列】Spring Cloud微服务架构中的分布式事务实现与性能优化详解 - 3-5年Java开发必备知识

【Java面试系列】Spring Cloud微服务架构中的分布式事务实现与性能优化详解 - 3-5年Java开发必备知识 引言 在微服务架构中&#xff0c;分布式事务是一个不可避免的挑战。随着业务复杂度的提升&#xff0c;如何保证跨服务的数据一致性成为面试中的高频问题。本文将从基础到进…...

PostgreSQL 17深度解析(从17.0-17.4)

PostgreSQL 17自2024年9月发布以来,持续通过小版本迭代增强功能、优化性能并修复安全漏洞。本文将从17.0到17.4的每个版本切入,深度解析其新增特性、技术原理、性能提升及实践价值,帮助开发者、DBA及架构师全面掌握PostgreSQL 17的演进脉络。 一、PostgreSQL 17.0:基石奠定…...

人物4_Japanese

Now, I start my JaPan【Tokyo】 life, 【I go out of my country{China}, the reason is I want learn more new computer technologies in foreign, also it could let me know more different culture.】I like the place and most persons in here. The JaPan culture have…...

Go 语言中的 package main、 func main() 和main.go的使用规范

本文旨在解释 Go 语言中 package main 、 func main() 和main.go的关系及其使用规则&#xff0c;解决如下典型问题&#xff1a; 是否可以在一个项目中定义多个 func main()&#xff1f;是否可以在非 package main 中写 func main()&#xff1f;多个文件中都写 func main() 会冲…...

mac 终端 code 命令打开 vscode,修改 cursor占用

rm /usr/local/bin/code vim ~/.zshrc # 定义 cursor 函数&#xff0c;用于打开 Cursor 应用 function cursor {open -a "/Applications/Cursor.app" "$" }# 定义 code 函数&#xff0c;用于打开 Visual Studio Code function code {open -a "/Appli…...

【常用功能】下载文件和复制到剪切板

前言 前端人员在开发时经常会遇到&#xff1a; 后端给一个地址&#xff0c;需要去下载的需求。将页面的内容复制到剪切板 下载文件 我们先说下载文件&#xff0c;通常情况下我们会自己写上一个非常简单的工具函数。 思路如下&#xff1a; 创建一个a元素设置a元素跳转的链接…...

【ESP32-microros(vscode-Platformio)】

一、步骤 1、目前支持ESP32 2、同一个局域网 3、上位机要安装代理&#xff08;电脑或者linux设备&#xff09; 4、可直接通过USB下载&#xff0c;也可以使用官方烧录工具&#xff0c;具体的分区表地址要从USB烧录的时候日志查看&#xff0c;一共四个文件&#xff0c;第三个…...

如何使用AI辅助开发CSS3 - 通义灵码功能全解析

一、引言 CSS3 作为最新的 CSS 标准&#xff0c;引入了众多新特性&#xff0c;如弹性布局、网格布局等&#xff0c;极大地丰富了网页样式的设计能力。然而&#xff0c;CSS3 的样式规则繁多&#xff0c;记忆所有规则对于开发者来说几乎是不可能的任务。在实际开发中&#xff0c…...

OpenCV图像形态学详解

文章目录 一、什么是图像形态学&#xff1f;二、基本概念&#xff1a;结构元素三、基本形态学操作1. 腐蚀&#xff08;Erosion&#xff09;2. 膨胀&#xff08;Dilation&#xff09;3. 开运算&#xff08;Opening&#xff09;4. 闭运算&#xff08;Closing&#xff09; 四、高级…...

Java-servlet(完结篇)过滤器乱码解决与监听器

Java-servlet&#xff08;完结篇&#xff09;过滤器乱码解决与监听器 前言一、过滤器乱码解决二、监听器1. HttpSessionListener2. ServletContextListener3. ServletRequestListener 三、监听器的使用场景Java-servlet 结语 前言 在之前的 Java Servlet 学习中&#xff0c;我…...

【发布】dtns协议的js-sdk(实现a2a协议:agent2agent)

【发布】dtns协议的js-sdk&#xff08;实现a2a协议&#xff1a;agent2agent&#xff09; dtns协议简介 dtns协议是用于dtns.network分布式智体网络的通讯协议。主要目标是将各个独立的智体节点&#xff0c;通过dtns协议&#xff08;dtns.network&#xff09;连接在一起&#…...

深度学习总结(8)

模型工作流程 模型由许多层链接在一起组成&#xff0c;并将输入数据映射为预测值。随后&#xff0c;损失函数将这些预测值与目标值进行比较&#xff0c;得到一个损失值&#xff0c;用于衡量模型预测值与预期结果之间的匹配程度。优化器将利用这个损失值来更新模型权重。 下面是…...

[特殊字符] Hyperlane:为现代Web服务打造的高性能Rust文件上传解决方案

&#x1f680; Hyperlane&#xff1a;为现代Web服务打造的高性能Rust文件上传解决方案 ▎开发者必备&#xff1a;为什么选择Hyperlane处理大文件上传&#xff1f; 在实时数据爆炸式增长的时代&#xff0c;开发者面临两大核心挑战&#xff1a; 如何实现TB级大文件的可靠传输如…...

英伟达开源253B语言模型:Llama-3.1-Nemotron-Ultra-253B-v1 模型情况

Llama-3.1-Nemotron-Ultra-253B-v1 模型情况 1. 模型概述 Llama-3.1-Nemotron-Ultra-253B-v1 是一个基于 Meta Llama-3.1-405B-Instruct 的大型语言模型 (LLM)&#xff0c;专为推理、人类对话偏好和任务&#xff08;如 RAG 和工具调用&#xff09;而优化。该模型支持 128K 令…...

2025年智能合约玩法创新白皮书:九大核心模块与收益模型重构Web3经济范式

——从国库管理到动态激励的加密生态全栈解决方案 一、核心智能合约架构解析 1. 国库合约&#xff1a;生态财政中枢 作为协议的金库守卫者&#xff0c;国库合约通过多签冷钱包与跨链资产池实现资金沉淀。其创新点包括&#xff1a; 储备资产动态再平衡&#xff1a;采用预言机实…...

[250411] Meta 发布 Llama 4 系列 AI 模型 | Rust 1.86 引入重大语言特性

目录 Llama 4 家族登场&#xff1a;开启原生多模态 AI 创新新纪元Rust 1.86.0 版本发布亮点主要新特性与改进其他重要信息 Llama 4 家族登场&#xff1a;开启原生多模态 AI 创新新纪元 Meta AI 近日发布了其最新、最先进的 Llama 4 系列人工智能模型&#xff0c;标志着 AI 技术…...

缓存不只是加速器:深入理解 Redis 的底层机制

一、Redis 是什么&#xff1f;为什么我们需要它&#xff1f; Redis&#xff08;Remote Dictionary Server&#xff09;是一种高性能的内存型键值对数据库。 通俗地讲&#xff0c;它就像一个超快的、放在内存中的超级字典&#xff0c;你可以用它来存数据、查数据&#xff0c;而…...

windows虚拟内存

windows的虚拟内存只是 虚拟内存技术的一个拓展, 叫他分页文件更好, 真正的虚拟内存是 CPU 内存管理单元 用于调度物理内存和磁盘衍生出来的技术. 在此基础上, 虚拟内存会根据页表 去物理内存中找数据, 找不到就去磁盘找, 找到之后再登记到页表. 这里的磁盘就是window系统中所…...

Ajax------免刷新地前后端交互

本文略带PHP代码需要在PHP环境下使用 介绍 AJAX (Asynchronous JavaScript and XML) 是一种创建快速动态网页应用的开发技术&#xff0c;它允许网页在不重新加载整个页面的情况下&#xff0c;与服务器交换数据并更新部分网页内容。例如&#xff0c;在我们做爬虫的时候发现有些…...

python办公自动化---pdf文件的读取、添加水印

需要安装包&#xff1a;pdfminer、pypdf2 一、读取pdf中的内容 from pdfminer.converter import TextConverter from pdfminer.layout import LAParams from pdfminer.pdfdocument import PDFDocument from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterprete…...

下一代数据架构全景:云原生实践、行业解法与 AI 底座 | Databend Meetup 成都站回顾

3 月底&#xff0c;Databend 2025 开年首场 Meetup 在成都多点公司成功举办&#xff01;活动特别邀请到四位重量级嘉宾&#xff1a;多点科技数据库架构师王春涛、多点DMALL数据平台负责人李铭、Databend联合创始人吴炳锡&#xff0c;以及鹏城实验室王璞博士。在春日的蓉城&…...

Kubernetes容器编排与云原生实践

第一部分&#xff1a;Kubernetes基础架构与核心原理 第1章 容器技术的演进与Kubernetes的诞生 1.1 虚拟化技术的三次革命 物理机时代&#xff1a;资源浪费严重&#xff0c;利用率不足15% 虚拟机突破&#xff1a;VMware与Hyper-V实现硬件虚拟化&#xff0c;利用率提升至50% …...

vue项目使用html2canvas和jspdf将页面导出成PDF文件

一、需求&#xff1a; 页面上某一部分内容需要生成pdf并下载 二、技术方案&#xff1a; 使用html2canvas和jsPDF插件 三、js代码 // 页面导出为pdf格式 import html2Canvas from "html2canvas"; import jsPDF from "jspdf"; import { uploadImg } f…...

JAVA实现在H5页面中点击链接直接进入微信小程序

在普通的Html5页面中如何实现点击URL链接直接进入微信小程序&#xff0c;不需要扫描小程序二维码&#xff1f; 网上介绍的很多方法是在小程序后台设置Schema&#xff0c;不过我进入我的小程序后台在开发设置里面 没有找到设置小程序Schema的地方&#xff0c;我是通过调用API接口…...

深入剖析 Kafka 的零拷贝原理:从操作系统到 Java 实践

Kafka 作为一款高性能的分布式消息系统&#xff0c;其卓越的吞吐量和低延迟特性得益于多种优化技术&#xff0c;其中“零拷贝”&#xff08;Zero-Copy&#xff09;是核心之一。零拷贝通过减少用户态与内核态之间的数据拷贝&#xff0c;提升了 Kafka 在消息传输中的效率。本文将…...

深度学习:AI 大模型时代的智能引擎

当 Deepspeek 以逼真到难辨真假的语音合成和视频生成技术横空出世&#xff0c;瞬间引发了全球对 AI 伦理与技术边界的激烈讨论。从伪造名人演讲、制造虚假新闻&#xff0c;到影视行业的特效革新&#xff0c;这项技术以惊人的速度渗透进大众视野。但在 Deepspeek 强大功能的背后…...

【Flask开发】嘿马文学web完整flask项目第4篇:4.分类,4.分类【附代码文档】

教程总体简介&#xff1a;2. 目标 1.1产品与开发 1.2环境配置 1.3 运行方式 1.4目录说明 1.5数据库设计 2.用户认证 Json Web Token(JWT) 3.书架 4.1分类列表 5.搜索 5.3搜索-精准&高匹配&推荐 6.小说 6.4推荐-同类热门推荐 7.浏览记录 8.1配置-阅读偏好 8.配置 9.1项目…...

【MySQL】002.MySQL数据库基础

文章目录 数据库基础1.1 什么是数据库1.2 基本使用创建数据库创建数据表表中插入数据查询表中的数据 1.3 主流数据库1.4 服务器&#xff0c;数据库&#xff0c;表关系1.5 MySQL架构1.6 SQL分类1.7 存储引擎1.7.1 存储引擎1.7.2 查看存储引擎1.7.3 存储引擎对比 前言&#xff1a…...

Python爬取视频的架构方案,Python视频爬取入门教程

文章目录 前言方案概述架构设计详细实现步骤1.环境准备2. 网页请求模块3. 网页解析模块4. 视频下载模块5. 异常处理与日志模块 代码示例&#xff1a;性能优化注意事项 前言 以下是一个全面的使用 Python 爬取视频的架构方案&#xff0c;包含方案概述、架构设计、详细实现步骤、…...

ERC-20 代币标准

文章目录 一、什么是 ERC-20&#xff1f;核心价值&#xff1a;互操作性简化开发生态基石 二、ERC-20 的六大核心功能基础功能授权与代理转账事件通知 三、ERC-20 代币的典型应用场景四、ERC-20 的技术优势与局限性优势&#xff1a;局限性&#xff1a; 五、ERC-20 代币的创建步骤…...

SpringBoot实战1

SpringBoot实战1 一、开发环境&#xff0c;环境搭建-----创建项目 通过传统的Maven工程进行创建SpringBoot项目 &#xff08;1&#xff09;导入SpringBoot项目开发所需要的依赖 一个父依赖&#xff1a;&#xff08;工件ID为&#xff1a;spring-boot-starter-parent&#xf…...

GSO-YOLO:基于全局稳定性优化的建筑工地目标检测算法解析

论文地址:https://arxiv.org/pdf/2407.00906 1. 论文概述 《GSO-YOLO: Global Stability Optimization YOLO for Construction Site Detection》提出了一种针对建筑工地复杂场景优化的目标检测模型。通过融合全局优化模块(GOM)​、稳定捕捉模块(SCM)​和创新的AIoU损失函…...

解读json.loads函数参数

json.loads()函数是解码JSON字符串为Python对象的核心工具。本文将深入探讨json.loads()函数的各个参数。 一、基本功能与输入类型 1. 功能概述 json.loads(s, *, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=N…...

2025.04.10-拼多多春招笔试第一题

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 01. 神奇公园的福娃占卜 问题描述 LYA在一个神奇的公园里发现了一条特殊的小径,小径上排列着一群可爱的福娃玩偶。这条小径有 n n...

【学习笔记】CPU 的“超线程”是什么?

1. 什么是超线程&#xff1f; 超线程&#xff08;Hyper-Threading&#xff09;是Intel的技术&#xff0c;让一个物理CPU核心模拟出两个逻辑核心。 效果&#xff1a;4核CPU在系统中显示为8线程。 本质&#xff1a;通过复用空闲的硬件单元&#xff08;如ALU、FPU&#xff09;&a…...

Docker Harbor

下载Harbor安装包 wget https://github.com/goharbor/harbor/releases/download/v2.5.0/harbor-offline-installer-v2.5.0.tgz 解压安装包 tar xvf harbor-offline-installer-v2.5.0.tgz cd harbor 配置harbor vi harbor.yml hostname: registry.example.com # Harbor …...

天梯集训笔记整理

1.着色问题 直接标注哪些行和列是被标注过的&#xff0c;安全格子的数量就是未标注的行*列 #include <bits/stdc.h> using namespace std;const int N 1e510; int hang[N],lie[N];int main(){int n,m;cin>>n>>m;int q;cin>>q;while(q--){int x,y;ci…...

C语言复习笔记--指针(4)

在经过前几篇的复习后我们已经了解了大部分指针的类型,今天让我们继续复习指针的其他内容吧. 函数指针变量 函数指针变量的创建 什么是函数指针变量呢&#xff1f;函数指针变量应该是⽤来存放函数地址的&#xff0c;未来通过地址能够调⽤函数的。 那么函数是否有地址呢&#x…...

008二分答案+贪心判断——算法备赛

二分答案贪心判断 有些问题&#xff0c;从已知信息推出答案&#xff0c;细节太多&#xff0c;过程繁杂&#xff0c;不易解答。 从猜答案出发&#xff0c;贪心地判断该答案是否合法是个不错的思路&#xff0c;这要求所有可能的答案是单调的&#xff08;例&#xff1a;x满足条件…...

Netty之内存池的原理和实战

深入理解Netty的内存池机制及其应用实践 在高性能网络编程中&#xff0c;内存管理对于系统的稳定性和性能至关重要。Netty作为一个高效的网络通信框架&#xff0c;通过引入内存池机制有效地解决了内存分配和回收频繁带来的性能瓶颈和内存碎片问题。本文将深入探讨Netty内存池的…...

Elasticsearch 向量数据库,原生支持 Google Cloud Vertex AI 平台

作者&#xff1a;来自 Elastic Valerio Arvizzigno Elasticsearch 将作为第一个第三方原生语义对齐引擎&#xff0c;支持 Google Cloud 的 Vertex AI 平台和 Google 的 Gemini 模型。这使得联合用户能够基于企业数据构建完全可定制的生成式 AI 体验&#xff0c;并借助 Elastics…...

《算法笔记》3.1小节——入门模拟->简单模拟

1001 害死人不偿命的(3n1)猜想 #include <iostream> using namespace std;int main() {int n,count0;cin>>n;while(n!1){if(n%20) n/2;else n(3*n1)/2;count1;}std::cout <<count;return 0; }1032 挖掘机技术哪家强 #include <iostream> using namespa…...

每日一题(小白)暴力娱乐篇24

由题已知这是一个匹配题目&#xff0c;题目已经说了三阶幻方是给定的&#xff0c;经过镜像和旋转&#xff0c;镜像*2旋转*4&#xff1b; 总共八种方案&#xff0c;然后接收每次的数据去匹配&#xff08;跳过0&#xff09;&#xff0c;如果匹配就输出匹配的数组&#xff0c;如果…...

C++:函数模板类模板

程序员Amin &#x1f648;作者简介&#xff1a;练习时长两年半&#xff0c;全栈up主 &#x1f649;个人主页&#xff1a;程序员Amin &#x1f64a; P   S : 点赞是免费的&#xff0c;却可以让写博客的作者开心好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全…...