LearnOpenGL学习(模型加载 -- Assimp,网格,模型)
完整代码见:zaizai77/Cherno-OpenGL: OpenGL 小白学习之路
Assimp
3D建模工具如Blender、3DS Max在导出模型文件时,会自动生成所有的顶点坐标、顶点法线和纹理坐标。
.obj 格式只包含了模型数据和材质信息(颜色、贴图等)
Assimp是一个开源的模型导入库,支持数十种不同的3D模型格式。
使用Assimp导入模型时,通常会把模型加载入一个场景(Scene)对象,它包含了导入的模型/场景内的所有数据。Assimp会把场景载入为一系列的节点,每个节点包含了场景对象中存储数据的索引。
- 和材质和网格(Mesh)一样,所有的场景/模型数据都包含在Scene对象中。Scene对象也包含了场景根节点的引用。
- 场景的Root node(根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。Scene下的mMeshes数组储存了真正的Mesh对象,节点中的mMeshes数组保存的只是场景中网格数组的索引。(真正的Mesh数据存在Scene节点中,Scene节点本事在层级面板中不可见,根节点和子节点就像是层级面板中的父对象和子对象,他们不存储数据,只存储索引)
- 一个Mesh对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面(Face)和物体的材质。
- 一个网格包含了多个面。Face代表的是物体的渲染图元(Primitive)(三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的
- 最后,一个网格也包含了一个Material对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。
借助 Assimp 加载模型的步骤:
- 加载物体到 Scene 对象中
- 遍历所有节点,获取对应的 Mesh 对象
- 处理每个 Mesh 对象以获取渲染所需的数据
之后我们得到一系列的网格数据,我嫩会将他们包含在 Model 独享中
一个 Model 由若干个 Mesh 组成,一个 Mesh 是一个单独的形状,是 OpenGL 中绘制物体的最小单位
如果我们想要绘制一个模型,我们不需要将整个模型渲染为一个整体,只需要渲染组成模型的每个独立的网格就可以了。
Assimp 数据结构
struct aiNode{aiNode **mChildren; //子节点数组unsigned int *mMeshes; //网格数据的索引数组aiMetadata* mMetaData; //元数据数组aiString mName; //节点名unsigned int mNumChildren; //子节点数量unsigned int mNumMeshes; //网格数量aiNode *mParent; //父节点aiMatrix4x4 mTransformation; //变换矩阵
}
struct aiScene{aiAnimation** Animations; //可通过HasAnimations成员函数判断是否为0aiCamera** mCameras; //同上unsigned int mFlags;aiLight** mLights;aiMaterial** mMaterials;aiMesh** mMeshes;aiMetadata* mMetaData;aiString mName;unsigned int mNumAnimations;unsigned int mNumCameras;unsigned int mNumLights;unsigned int mNumMaterials;unsigned int mNumMeshes;unsigned int mNumTextures;aiNode* mRootNode;aiTexture **mTextures;
}
struct aiMesh{aiAnimMesh** mAnimMeshes;aiVector3D* mBitangents;aiBone** mBones;aiColor4D* mColors[AI_MAX_NUMBER_OF_COLOR_SETS];aiFaces* mFaces;unsigned int mMaterialIndex;unsigned int mMethod;aiString mName;aiVector3D* mNormals;unsigned int mNumAnimMeshes;unsigned int mNumBones;unsigned int mNumFaces;unsigned int mNumUVComponents[AI_MAX_NUMBER_OF_TEXTURECOORDS];unsigned int mNumVertices;unsigned int mPrimitiveTypes;aiVector3D* mTangents;aiVector3D* mTextureCoords[AI_MAX_NUMBER_OF_TEXTURECOORDS];aiString mTextureCoordsNames[AI_MAX_NUMBER_OF_TEXTURECOORDS];aiVector3D* mVertices;
}
网格
通过使用Assimp,我们可以加载不同的模型到程序中,但是载入后它们都被储存为Assimp的数据结构。我们需要将这些数据转换成 OpenGL 可以理解的格式
网格(Mesh)代表的是单个可绘制实体,它包含了顶点数据,索引和纹理
需要的属性:
- 顶点需要位置向量,法向量,纹理坐标
- 纹理对象需要 unsigned int 句柄,纹理类型(漫反射,高光贴图等),纹理路径
struct Vertex {glm::vec3 Position;glm::vec3 Normal;glm::vec2 TexCoords;
};struct Texture {unsigned int id;string type;string path;
};
网格类的结构:
class Mesh {public:/* 网格数据 */vector<Vertex> vertices;vector<unsigned int> indices;vector<Texture> textures;/* 函数 */Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures);void Draw(Shader shader);private:/* 渲染数据 */unsigned int VAO, VBO, EBO;/* 函数 */void setupMesh();
};
在构造函数中,我们将所有必须的数据赋予了网格,我们在setupMesh函数中初始化缓冲,并最终使用Draw函数来绘制网格。注意我们将一个着色器传入了Draw函数中,将着色器传入网格类中可以让我们在绘制之前设置一些uniform
构造函数的内容非常易于理解。我们只需要使用构造函数的参数设置类的公有变量就可以了。我们在构造函数中还调用了setupMesh函数:
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
{this->vertices = vertices;this->indices = indices;this->textures = textures;setupMesh();
}
初始化
void setupMesh()
{glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glGenBuffers(1, &EBO);glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO);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);// 顶点位置glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);// 顶点法线glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));// 顶点纹理坐标glEnableVertexAttribArray(2); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));glBindVertexArray(0);
}
C++结构体有一个很棒的特性,它们的内存布局是连续的(Sequential)。
Vertex vertex;
vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f);
vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = glm::vec2(1.0f, 0.0f);
// = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];
结构体的另外一个很好的用途是它的预处理指令 offsetof(Vertex,Normal),它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)。
渲染
绘制之前需要先绑定相应的纹理,但是一开始并不知道网格有多少纹理,为了解决这个问题,我们使用命名规则,
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
...uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
...
根据这个标准,我们可以在着色器中定义任意需要数量的纹理采样器,如果一个网格真的包含了(这么多)纹理,我们也能知道它们的名字是什么。根据这个标准,我们也能在一个网格中处理任意数量的纹理,开发者也可以自由选择需要使用的数量,他只需要定义正确的采样器就可以了(虽然定义少的话会有点浪费绑定和uniform调用)。
最终的渲染代码:
void Draw(Shader shader)
{unsigned int diffuseNr = 1;unsigned int specularNr = 1;for(unsigned int i = 0; i < textures.size(); i++){glActiveTexture(GL_TEXTURE0 + i); // 在绑定之前激活相应的纹理单元// 获取纹理序号(diffuse_textureN 中的 N)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++);shader.setInt(("material." + name + number).c_str(), i);glBindTexture(GL_TEXTURE_2D, textures[i].id);}glActiveTexture(GL_TEXTURE0);// 绘制网格glBindVertexArray(VAO);glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);glBindVertexArray(0);
}
Mesh 类的完整代码:
#ifndef MESH_H
#define MESH_H#include <glad/glad.h> // holds all OpenGL type declarations#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>#include <learnopengl/shader.h>#include <string>
#include <vector>
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 data unsigned 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
模型
创建另一个类来完整的表示一个模型。我们会使用 Assimp 来加载模型,并将它转换至多个 Mesh
对象
Model 类的结构
class Model
{public:/* 函数 */Model(char *path){loadModel(path);}void Draw(Shader shader); private:/* 模型数据 */vector<Mesh> meshes;string directory;/* 函数 */void loadModel(string path);void processNode(aiNode *node, const aiScene *scene);Mesh processMesh(aiMesh *mesh, const aiScene *scene);vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName);
};
遍历了所有网格,并调用它们各自的Draw函数。
void Draw(Shader &shader)
{for(unsigned int i = 0; i < meshes.size(); i++)meshes[i].Draw(shader);
}
导入3D模型到OpenGL
要想导入一个模型,并将它转换到我们自己的数据结构中的话,首先我们需要包含Assimp对应的头文件:
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
Importer 类用于加载模型文件:
void loadModel(string path){Assimp::Importer importer;//参数一为文件路径,参数二为后处理选项。此处意味:将所有图元转换为三角形|翻转纹理坐标以适应OpenGL设置//除此以外,还有://aiProcess_GenNormals - 生成法向量//aiProcess_SplitLargeMeshes - 分割大网格,防止超过顶点渲染限制//aiProcess_OptimizeMeshes - 合并小网格,减少Drawcallconst aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);//检查场景和根节点是否为null.//mFlags与特定宏求与,得到场景是否完全加载。这么做的目的是:位操作性能好if(!scene||scene->mFlags&AI_SCENE_FLAGS_INCOMPLETE||!scene->mRootNode){//导入期的GetErrorString()函数可得到错误信息cout<<"ERROR::ASSIMP::"<<import.GetErrorString()<<endl;return;}//剔除文件本身的名称,得到目录路径directory = path.substr(0,path.find_last_of('/')); //find_last_of:查找string最后出现的某字符的索引//由根节点开始,可以遍历到所有节点。所以首先处理根节点//processNode函数为递归函数processNode(scene->mRootNode,scene);
}
void processNode(aiNode *node, const aiScene* scene){//mNumMeshes指当前节点存储的网格数据数量for(unsigned int i=0;i<node->mNumMeshes;i++){//记住,节点只存放网格索引,场景中存放的才是真正的网格数据aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];meshes.push_back(processMesh(mesh,scene));}for(unsigned int i=0;i<node->mNumChildren;i++){//递归处理子节点processNode(node->mChildren[i],scene);}
}
之所以费这么多心思遍历子节点获取网格,而不是直接遍历aiScene的Mesh数组,是因为:
无论是在游戏引擎里还是在3D建模软件中,都存在类似层级面板的东西。在这里,网格之间有严格的父子关系,而节点之间的关系就体现了这一点。
如果单纯遍历Mesh数组,那网格之间的父子关系就被丢弃了。
ProcessMesh 函数用于把aiMesh对象转换为我们自己的Mesh类。实现这一步很简单,只需要访问aiMesh的所有属性,并把它们赋值给Mesh类的属性即可。
Mesh processMesh(aiMesh* mesh, const aiScene* scene){vector<Vertex> vertices;vector<Texture> textures;vector<unsigned int> indices;//处理顶点for(unsigned int i=0;i<mesh->mNumVertices;i++){Vertex vertex;glm::vec3 tmpVec;tmpVec.x = mesh->mVertices[i].x;tmpVec.y = mesh->mVertices[i].y;tmpVec.z = mesh->mVertices[i].z;vertex.Position = tmpVec;tmpVec.x = mesh->mNormals[i].x;tmpVec.y = mesh->mNormals[i].y;tmpVec.z = mesh->mNormals[i].z;vertex.Normal = tmpVec;glm::vec2 uv;//aiMesh结构体的mTexCoords可以被看作是二维数组。它的第一维是纹理的序号(Assimp允许同一个顶点上包含八个纹理的uv),第二维才是表示uv的二维向量。if(mesh->mTexCoords[0]){uv.x = mesh->mTexCoords[0][i].x;uv.y = mesh->mTexCoords[0][i].y;vertex.TexCoords = uv;}else{vertex.TexCoords = glm::vec2(0.0f,0.0f);}vertices.push_back(vertex);}//处理索引//每个网格包含了若干面,每个面包含了绘制这个面的顶点索引。for(unsigned int i=0;i<mesh->mNumFaces;i++){aiFace face = mesh->mFaces[i];for(unsigned int j=0;j<face.mNumIndices;j++){indices.push_back(face.mIndices[j]);}}//处理材质//一个网格只能使用一个材质,如果网格没有材质,mMaterialIndex为负数//和节点-网格的关系一样,网格本身只存储材质索引,场景对象才存储真正的aiMaterialif(mesh->mMaterialIndex>=0){aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];vector<Texture> diffuseMaps = loadMaterialTextures(material,aiTextureType_DIFFUSE,"texture_diffuse");//其实这里用for循环也行textures.insert(textures.end(),diffuseMaps.begin(),diffuseMaps.end());vector<Texture> specularMaps = loadMaterialTextures(material,aiTextureType_SPECULAR,"texture_specular");textures.insert(textures.end(),specularMaps.begin(),specularMaps.end());}
}
到这里,我们Mesh类的属性就都填充完毕了。接下来,我们要结合stbi_image库来加载材质中的纹理。
vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, string typeName){vector<Texture> textures;for(unsigned int i=0;i<mat->GetTextureCount(type);i++){aiString str;//这里获取到的str是纹理的文件名,而非路径mat->GetTexture(type,i,&str);bool skip = false;for(unsigned int j = 0; j < this->textures_loaded.size(); j++){//aiString.data()也可以用于获取const char*//这里匹配了当前纹理与textures_loaded数组中的内容。若发现匹配的,则直接跳过加载if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0){textures.push_back(textures_loaded[j]);skip = true; break;}}if(!skip){Texture texture;//aiString可以用C_Str()函数转化为const char*//这里的directory是模型所在的目录texture.id = TextureFromFile(str.C_Str(),this->directory); texture.type = typeName;texture.path = str;textures.push_back(texture);}}return textures;
}unsigned int TextureFromFile(const char* path, const string &directory){string filename = string(path);filename = directory + '/' + filename;unsigned int id;glGenTextures(1,&id);int width, height, channels;unsigned char* data = stbi_load(filename.c_str(), &width,&height,&channels,0);if(data){GLenum format;if(channels==1){format = GL_RED;}else if(channels==3){format = GL_RGB;}else if(channels==4){format = GL_RGBA;}glBindTexture(GL_TEXTURE_2D,id);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 id;
}
参考:Assimp - LearnOpenGL CN
LearnOpenGL学习笔记(七) - 模型导入 - Yoi's Home
相关文章:
LearnOpenGL学习(模型加载 -- Assimp,网格,模型)
完整代码见:zaizai77/Cherno-OpenGL: OpenGL 小白学习之路 Assimp 3D建模工具如Blender、3DS Max在导出模型文件时,会自动生成所有的顶点坐标、顶点法线和纹理坐标。 .obj 格式只包含了模型数据和材质信息(颜色、贴图等) Assi…...
基于Pyhton的人脸识别(Python 3.12+face_recognition库)
使用Python进行人脸编码和比较 简介 在这个教程中,我们将学习如何使用Python和face_recognition库来加载图像、提取人脸编码,并比较两个人脸是否相似。face_recognition库是一个强大的工具,它基于dlib的深度学习模型,可以轻松实…...
Scala的模式匹配(2)
package hfdobject Test34_2 {//match case匹配元组//能根据元组元素的个数来匹配,不是一一精确的匹配值def main(args: Array[String]): Unit {val d1(1,2,3) // val d1(1,2,3,4) // val d1(1,2,3,4,5)val d:Anyd1 //d的类型是Anyd match {case (x,y,z)>…...
Java 初学者的第一个 SpringBoot 系统
Java 初学者的第一个 SpringBoot 系统 对编程初学者而言,都存在一个 “第一个系统” 的问题。有些学习者找不到自己的 “第一个系统”,他们即使再努力也没有办法了解完整的系统,即使他们把教科书里的所有程序都跑通了。但是,面对…...
java IO面试题
目录 Java IO设计上使用了什么设计模式? 你怎么理解同步IO和异步IO? 字节流和字符流的区别? Java 流量有多少种类型? 如何将一个 java 将对象序列化到文件中? 如何实现 java 序列化? Filter流是什么? Filter流有哪些可用? 如何实现对象克隆? BIO、…...
Chocolatey软件包管理工具处理MSI升级的原理与实践
Chocolatey软件包管理工具处理MSI升级的原理与实践 在Windows环境下,Chocolatey作为一款强大的包管理工具,其处理MSI(Microsoft Installer)格式软件包升级的机制值得深入探讨。本文将详细介绍Chocolatey如何处理MSI升级ÿ…...
Navicat连接SQL Server及SpringBoot连接SQL Server(jtds)
Navicat连接SQL Server 安装自带的SQL Server客户端 去到Navicat安装目录,找到安装程序,安装即可。 安装对应版本的Microsoft ODBC Driver for SQL Server 打开Navicat输入对应的SQL Server相关信息 然后点测试连接,提示连接成功。 Spr…...
【Git】
博文将不断学习补充 Git下载 将下载链接复制到迅雷中,快速下载 Git安装 保持默认,傻瓜安装即可。 注册Gitee码云,设置公钥 在Git Bash中输入 ssh-keygen -t ed25519 -C "Gitee SSH Key" 找到对应的公钥文件 复制公钥,添…...
HttpServletRequest
HttpServletRequest 类确实是一个封装了完整 HTTP 请求信息的对象,而 Spring MVC 提供了更简化的方式来自动映射请求路径、请求参数等信息到控制器方法中。你不必直接使用 HttpServletRequest 来处理大部分常见的请求内容,因为 Spring MVC 会为你自动处理…...
Apache HttpClient 4和5访问没有有效证书的HTTPS
本文将展示如何配置Apache HttpClient 4和5以支持“接受所有”SSL。 目标很简单——访问没有有效证书的HTTPS URL。 SSLPeerUnverifiedException 在未配置SSL的情况下,尝试消费一个HTTPS URL时会遇到以下测试失败: Test void whenHttpsUrlIsConsumed…...
做异端中的异端 -- Emacs裸奔之路7: 怀念Vim的好
组合键的瑕疵 从Vim切换成Emacs之后,有一明显的不适就是感受Emacs的按键很硬, Vim移动是使用一个按完成的,而Emacs是组合键。 如果向前移动一个字,Vim只要在ESC模式下按w,而Emacs是Alt-f. 特别是当你对键盘改键之后不…...
asp.net core过滤器应用
筛选器类型 授权筛选器 授权过滤器是过滤器管道的第一个被执行的过滤器,用于系统授权。一般不会编写自定义的授权过滤器,而是配置授权策略或编写自定义授权策略。简单举个例子。 using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCo…...
VideoBooth: Diffusion-based Video Generation with Image Prompts
VideoBooth: Diffusion-based Video Generation with Image Prompts 概括 文章提出了一个视频生成模型VideoBooth,输入一张图片和一个文本提示词,即可输出保持图片中物体且符合文本提示词要求的视频。 方法 粗-细两阶段设计:1)…...
面阵相机的使用和注意事项
引言 面阵相机(Area Scan Camera)是一种广泛应用于工业视觉、医学成像、安防监控以及科研领域的图像采集设备。与线扫相机不同,面阵相机的传感器包含多行像素(例如1280x1024、1920x1080等),能够在一个曝光…...
人工智能与机器学习在智能扭矩系统中的应用
【大家好,我是唐Sun,唐Sun的唐,唐Sun的Sun。】 在当今科技飞速发展的时代,智能扭矩系统正经历着一场深刻的变革,而人工智能(AI)和机器学习算法的应用成为了推动这一变革的关键力量。 传统的扭矩…...
【附源码】基于环信鸿蒙IM SDK实现一个聊天Demo
项目背景 本项目基于环信IM 鸿蒙SDK 打造的鸿蒙IM Demo,完全适配HarmonyOS NEXT系统,实现了发送消息,添加好友等基础功能。代码开源,功能简洁,如果您有类似开发需求可以参考。 源码地址:https://github.c…...
【 AI技术赋能有限元分析与材料科学应用实践】Neo-Hookean 材料与深度学习结合的有限元分析
Neo-Hookean 材料模型是用于描述非线性弹性材料(如软组织和橡胶等)的经典模型,特别适用于大变形问题。其基本思想是通过应变能密度函数来描述材料的弹性行为。在该模型中,材料的应力-应变关系不仅依赖于应变能,还通过变…...
Origin快速拟合荧光寿命、PL Decay (TRPL)数据分析处理-方法二
1.先导入数据到origin 2.导入文件的时候注意:名字短的这个是,或者你打开后看哪个里面有800,因为我的激光重频是1.25Hz(应该是,不太确定单位是KHz还是MHz),所以对应的时间是800s。 3.选中两列直接…...
LeetCode Hot100 51~60
图论51. 岛屿问题52. 腐烂的橘子53. 课程表54. 前缀树55. 全排列56. 子集57. 电话号码58. 组合总和59. 括号生成60. 单词搜索 图论 51. 岛屿问题 经典洪水问题算法 class Solution { public:int numIslands(vector<vector<char>>& grid) {int nr grid.size…...
第一节:ORIN NX介绍与基于sdkmanager的镜像烧录(包含ubuntu文件系统/CUDA/OpenCV/cudnn/TensorRT)
ORIN NX技术参数 Orin NX版本对比 如上图所示,ORIN NX官方发布的版本有两个版本一个版本是70TOPS算力,DDR为8GB的版本低配版本,一个是100TOPS算法,DDR为16GB的高配版本。 Orin NX的外设框图 两个版本除了GPU和DDR的差距外,外设基本上没有区别,丰富的外设接口,后续开发…...
使用Pygame创建一个简单的消消乐游戏
消消乐游戏是一种经典的益智游戏,玩家通过交换相邻的方块来形成三个或更多相同颜色的连续方块,从而消除它们。本文将介绍如何使用Python的Pygame库来创建一个简单的消消乐游戏。 准备工作 在开始之前,请确保已安装Pygame库。可以通过以下命…...
node.js基础学习-JWT登录鉴权(十四)
一、前言 JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。它本质上是一个字符串,由三部分组成:头部(Header)、载荷(Payload&am…...
AbutionGraph-时序向量图谱数据库-快速安装部署
运行环境 1)操作系统 最好是使用CentOS7或者Ubuntu18以上系统,不满足的话请升级系统内核gcc版本至8以上版本。 支持所有国产主流操作系统银河麒麟、统信OS、深度等等,均做过兼容性测试; 2)CPU 为确保数据库每个进…...
翻译质量差对电子课程用户体验的影响
电子学习改变了教育交付方式,使全球不同受众更容易获得课程。然而,随着这种学习模式的发展,对周到地本地化和翻译的需求也在增长。如果做得好,翻译可以弥合文化和语言分歧,创造无缝和包容的学习体验。然而,…...
PS的功能学习(修复、画笔)
混合器画笔工具 就像,电子毛笔 关键功能有两个,自带一个混合器色板 清理画笔是全清,换一支新的毛笔,执行完之后在判断是否载入画笔 载入画笔就是把前景色上的颜色进行叠加处理,重新混入当前的混合色 (…...
Android 使用 Canvas 和 Paint 实现圆形图片
学习笔记 效果展示: 全部代码: public class YuanActivity extends AppCompatActivity {private ActivityYuanBinding binding;Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 通过 DataBinding 获取布局文件binding …...
vxe-table 键盘操作,设置按键编辑方式,支持覆盖方式与追加方式
vxe-table 全键盘操作,按键编辑方式设置,覆盖方式与追加方式; 通过 keyboard-config.editMode 设置按键编辑方式;支持覆盖方式编辑和追加方式编辑 安装 npm install vxe-pc-ui4.3.15 vxe-table4.9.15// ... import VxeUI from v…...
BUUCTF Pwn [HarekazeCTF2019]baby_rop 题解
下载 checksec 64位 用IDA64打开 定位main函数 栈溢出漏洞 SHIFTF12的字符串看到了binsh 以及函数窗口有system 因为是64位 找到rdi传参和ret栈平衡 构造exp: from pwn import *#p process(./babyrop) p remote("node5.buuoj.cn", 27869)addr_prr 0…...
什么是封装性?C++ 中如何实现封装?封装性的好处是什么?
一、引言 在面向对象编程中,封装性是一个重要的概念。封装可以帮助我们更好地组织和管理代码,提高代码的可维护性、可扩展性和安全性。本文将详细介绍什么是封装性,C 中如何实现封装以及封装性的好处。 二、什么是封装性? 封装…...
【MySQL】[42000][1071] Specified key was too long; max key length is 3072 bytes
问题描述 创建表时发生错误。 create table if not exists tbl_user(id int unsigned auto_increment comment 用户IDprimary key,username varchar(1023) not null comment 用户名,password varchar(1023) default 123456 …...
人工智能驱动的骗局会模仿熟悉的声音
由于人工智能技术的进步,各种现代骗局变得越来越复杂。 这些骗局现在包括人工智能驱动的网络钓鱼技术,即使用人工智能模仿家人或朋友的声音和视频。 诈骗者使用来自社交媒体的内容来制作深度伪造内容,要求提供金钱或个人信息。个人应该通过…...
实数与复数频谱掩蔽在音频分离中的应用
使用实数和复数频谱掩蔽进行音频分离 频谱掩蔽是指在音频信号的频谱表示中,通过选择性地增强或抑制某些频率成分来改善信号质量或实现信号分离的技术。频谱掩蔽可以分为两种类型:实数掩蔽和复数掩蔽。 实数频谱掩蔽 实数频谱掩蔽主要关注音频信号的幅…...
C++算法练习-day62——491.非递减子序列
题目来源:. - 力扣(LeetCode) 题目思路分析 这个问题要求找出数组 nums 中的所有非严格递增子序列,其中每个子序列至少包含两个元素。非严格递增子序列意味着子序列中的元素可以相等,但不允许递减。 为了解决这个问…...
golang实现单例日志对象
原文地址:golang实现单例日志对象 – 无敌牛 欢迎参观我的个人博客:无敌牛 – 技术/著作/典籍/分享等 介绍 golang有很多日志包,通过设置和修改都能实现日志轮转和自定义日志格式。例如:log、zap、golog、slog、log4go 等等。 …...
Redis——个人笔记留存
今日内容 1. redis1. 概念2. 下载安装3. 命令操作1. 数据结构4. 持久化操作5. 使用Java客户端操作redis Redis 1. 概念: redis是一款高性能的NOSQL系列的非关系型数据库 1.1.什么是NOSQLNoSQL(NoSQL Not Only SQL),意即“不仅仅是SQL”,是…...
c# Grpc取消
net6.0 通过CancellationTokenSource 客户端取消Grpc,服务端判断 IsCancellationRequested 是否取消。 proto: syntax "proto3";// 引用可空类型 import "google/protobuf/wrappers.proto";option csharp_namespace "Grpc.Common"…...
Flask 是什么?
近期开发chatbot 程序,过程中要使用Flask,所以收集资料记录这个套件的信息: Flask 是什么? Flask 是一个轻量级、模块化的 Python Web 框架,用于构建 Web 应用程序和 API。它被设计为简单、灵活且可扩展,…...
智能合约
06-智能合约 0 啥是智能合约? 定义 智能合约,又称加密合约,在一定条件下可直接控制数字货币或资产在各方之间转移的一种计算机程序。 角色 区块链网络可视为一个分布式存储服务,因为它存储了所有交易和智能合约的状态 智能合约还…...
【机器学习】分类器
在机器学习(Machine Learning,ML)中,分类器泛指算法或模型,用于将输入数据分为不同的类别或标签。分类器是监督学习的一部分,它依据已知的数据集中的特征和标签进行训练,并根据这些学习到的知识对新的未标记数据进行分…...
ASP 快速参考
ASP 快速参考 概述 ASP(Active Server Pages)是一种由微软开发的服务器端脚本环境,用于动态生成交互性网页。它允许开发者结合HTML、VBScript或JScript脚本语言来创建和运行动态网页或Web应用程序。本快速参考将提供ASP的基础知识、常用内置…...
支持向量机算法:原理、实现与应用
摘要: 本文深入探讨支持向量机(Support Vector Machine,SVM)算法,详细阐述其原理、数学模型、核函数机制以及在分类和回归问题中的应用方式。通过以 Python 和 C# 为例,展示 SVM 算法在不同编程环境下的具体…...
蓝桥杯分治
P1226 【模板】快速幂 题目描述 给你三个整数 𝑎,𝑏,𝑝a,b,p,求 𝑎𝑏 mod 𝑝abmodp。 输入格式 输入只有一行三个整数,分别代表 𝑎,𝑏,𝑝a,b,p。…...
群控系统服务端开发模式-应用开发-邮件工厂结构封装
首先在系统根目录下extend文件夹下创建邮件工厂文件夹并更名叫Mail。 一、邮件发送父类 在Mail目录下创建邮件发送父类并更名为MailSenderInterface.php,代码如下 <?php /*** 邮件发送父类* User: 龙哥三年风水* Date: 2024/12/5* Time: 14:22*/ namespace Ma…...
COCO数据集理解
COCO(Common Objects in Context)数据集是一个用于计算机视觉研究的广泛使用的数据集,特别是在物体检测、分割和图像标注等任务中。COCO数据集由微软研究院开发,其主要特点包括: 丰富的标签:COCO数据集包含…...
数据结构与算法学习笔记----堆
数据结构与算法学习笔记----堆 author: 明月清了个风 first publish time: 2024.12.2 revised: 2024.12.3 - 例题标题错误,已修改。 ps⛹从这里开始调整了文章结构,先讲解算法和数据结构基本原理,再给出例题,针对例题中的应用再…...
在玩“吃鸡”的时候游戏崩溃要如何解决?游戏运行时崩溃是什么原因?
“吃鸡”游戏崩溃问题深度解析与解决方案:原因、修复与预防 在紧张刺激的“吃鸡”(即《绝地求生》)游戏中,突然遭遇游戏崩溃无疑会让玩家倍感沮丧。作为一名经验丰富的软件开发从业者,我深知游戏崩溃可能由多种因素引…...
AndroidAutoSize实战教程:今日头条屏幕适配方案详解
如何在项目中结合 AndroidAutoSize 来进行今日头条屏幕适配,我会具体讲解如何用 AndroidAutoSize 实现屏幕适配,并结合 Kotlin 代码举例分析。 通过 AndroidAutoSize 库来实现屏幕适配,确保在不同的屏幕尺寸、分辨率、密度下,应用…...
学习threejs,通过设置纹理属性来修改纹理贴图的位置和大小
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️Texture 贴图 二、…...
图生3d 图生全景 学习笔记
目录 Aluciddreamer ZoeDepth 会自动下载模型: 图生全景图SD-T2I-360PanoImage: Aluciddreamer GitHub - luciddreamer-cvlab/LucidDreamer: Official code for the paper "LucidDreamer: Domain-free Generation of 3D Gaussian Splatting Sce…...
Delphi 实现键盘模拟、锁定键盘,锁定鼠标等操作
Delphi 模拟按键的方法 SendMessageA 说明: 调用一个窗口的窗口函数,将一条消息发给那个窗口。除非消息处理完毕,否则该函数不会返回SendMessage所包含4个参数: 1. hwnd 32位的窗口句柄窗口可以是任何类型的屏幕对象,因为Win32能够维护大多数…...