LearnOpenGL学习(模型加载 -- Assimp,网格,模型)

news2024/12/13 4:56:56

完整代码见: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成员函数判断是否为0
    aiCamera** 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 4

struct Vertex {
    // position
    glm::vec3 Position;
    // normal
    glm::vec3 Normal;
    // texCoords
    glm::vec2 TexCoords;
    // tangent
    glm::vec3 Tangent;
    // bitangent
    glm::vec3 Bitangent;
	//bone indexes which will influence this vertex
	int m_BoneIDs[MAX_BONE_INFLUENCE];
	//weights from each bone
	float m_Weights[MAX_BONE_INFLUENCE];
};

struct Texture {
    unsigned int id;
    string type;
    string path;
};

class Mesh {
public:
    // mesh Data
    vector<Vertex>       vertices;
    vector<unsigned int> indices;
    vector<Texture>      textures;
    unsigned int VAO;

    // constructor
    Mesh(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 mesh
    void Draw(Shader &shader) 
    {
        // bind appropriate textures
        unsigned 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 string
            else if(name == "texture_normal")
                number = std::to_string(normalNr++); // transfer unsigned int to string
             else if(name == "texture_height")
                number = std::to_string(heightNr++); // transfer unsigned int to string

            // now set the sampler to the correct texture unit
            glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);
            // and finally bind the texture
            glBindTexture(GL_TEXTURE_2D, textures[i].id);
        }
        
        // draw mesh
        glBindVertexArray(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/arrays
    void setupMesh()
    {
        // create buffers/arrays
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        glGenBuffers(1, &EBO);

        glBindVertexArray(VAO);
        // load data into vertex buffers
        glBindBuffer(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 Positions
        glEnableVertexAttribArray(0);	
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
        // vertex normals
        glEnableVertexAttribArray(1);	
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
        // vertex texture coords
        glEnableVertexAttribArray(2);	
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
        // vertex tangent
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
        // vertex bitangent
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
		// ids
		glEnableVertexAttribArray(5);
		glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));

		// weights
		glEnableVertexAttribArray(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 - 合并小网格,减少Drawcall
    const 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为负数
    //和节点-网格的关系一样,网格本身只存储材质索引,场景对象才存储真正的aiMaterial
    if(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

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

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

相关文章

qtcanpool 知 08:Docking

文章目录 前言口味改造后语 前言 很久以前&#xff0c;作者用 Qt 仿照前端 UI 设计了一个 ministack&#xff08;https://gitee.com/icanpool/qtcanpool/blob/release-1.x/src/libs/qcanpool/ministack.h&#xff09; 控件&#xff0c;这个控件可以折叠。部分用户体验后&#…

【Linux】文件管理必备知识和基本指令

【Linux】文件管理必备知识和基本指令 什么是操作系统什么是文件什么是路径01. ls 指令02. pwd命令03. cd 指令04. touch指令05.mkdir指令&#xff08;重要&#xff09;&#xff1a;06.rmdir指令 && rm 指令&#xff08;重要&#xff09;&#xff1a;rmdir指令rm指令 0…

R155 VTA 认证对汽车入侵检测系统(IDS)合规要求

续接上集“浅谈汽车网络安全车辆型式认证&#xff08;VTA&#xff09;的现状和未来发展”&#xff0c;有许多读者小伙伴有联系笔者来确认相关的R155 VTA网络安全审核要求&#xff0c;基于此&#xff0c;笔者将针对 R155 VTA 每一条网络安全审核细则来具体展开。 今天就先从汽车…

【PHP项目实战】活动报名系统

目录 项目介绍 开发语言 后端 前端 项目截图&#xff08;部分&#xff09; 首页 列表 详情 个人中心 后台管理 项目演示 项目介绍 本项目是一款基于手机浏览器的活动报名系统。它提供了一个方便快捷的活动报名解决方案&#xff0c;无需下载和安装任何APP&#xff0c…

【数据分享】1901-2023年我国省市县三级逐年最低气温数据(Shp/Excel格式)

之前我们分享过1901-2023年1km分辨率逐月最低气温栅格数据和Excel和Shp格式的省市县三级逐月最低气温数据&#xff0c;原始的逐月最低气温栅格数据来源于彭守璋学者在国家青藏高原科学数据中心平台上分享的数据&#xff01;基于逐月栅格数据我们采用求年平均值的方法得到逐年最…

使用伪装IP地址和MAC地址进行Nmap扫描

使用伪装IP地址和MAC地址进行Nmap扫描 在某些网络设置中&#xff0c;攻击者可以使用伪装的IP地址甚至伪装的MAC地址进行系统扫描。这种扫描方式只有在可以保证捕获响应的情况下才有意义。如果从某个随机的网络尝试使用伪装的IP地址进行扫描&#xff0c;很可能无法接收到任何响…

【趣题分享】赤壁之战每日演兵(原诸葛亮列传兵法题)求解算法

文章目录 序言1 求解算法代码&#xff08;python&#xff09;2 思路细节2.1 定义拼图与阵型2.2 穷举复杂度2.3 使用缓存进行改进&#xff08;&#xff09;2.3.1 LRU缓存2.3.2 将2.2的solve函数改写为可缓存装饰的 2.4 使用剪枝进行改进&#xff08;&#xff09;2.5 使用更好的状…

Java项目实战II基于微信小程序的私家车位共享系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着城市化进程的加速&…

STM32 实现 TCP 服务器与多个设备通信

目录 一、引言 二、硬件准备 三、软件准备 四、LWIP 协议栈的配置与初始化 五、创建 TCP 服务器 1.创建任务以及全局变量 2.创建 TCP 控制块 3.绑定端口 4. 进入监听状态 5.设置接收回调函数 六、处理多个客户端连接 七、总结 一、引言 在嵌入式系统开发中&…

LobeChat-46.6k星!顶级AI工具集,一键部署,界面美观易用,ApiSmart 是你肉身体验学习LLM 最好IDEA 工具

LobeChat LobeChat的开源&#xff0c;把AI功能集合到一起&#xff0c;真的太爽了。 我第一次发现LobeChat的时候&#xff0c;就是看到那炫酷的页面&#xff0c;这么强的前端真的是在秀肌肉啊&#xff01; 看下它的官网&#xff0c;整个网站的动效简直闪瞎我&#xff01; GitH…

计算机键盘的演变 | 键盘键名称及其功能 | 键盘指法

注&#xff1a;本篇为 “键盘的演变及其功能” 相关几篇文章合辑。 英文部分机翻未校。 The Evolution of Keyboards: From Typewriters to Tech Marvels 键盘的演变&#xff1a;从打字机到技术奇迹 Introduction 介绍 The keyboard has journeyed from a humble mechanical…

第三部分:进阶概念 7.数组与对象 --[JavaScript 新手村:开启编程之旅的第一步]

第三部分&#xff1a;进阶概念 7.数组与对象 --[JavaScript 新手村&#xff1a;开启编程之旅的第一步] 在 JavaScript 中&#xff0c;数组和对象是两种非常重要的数据结构&#xff0c;它们用于存储和组织数据。尽管它们都属于引用类型&#xff08;即它们存储的是对数据的引用而…

面试中遇到的一些有关进程的问题(有争议版)

一个进程最多可以创建多少个线程&#xff1f; 这个面经很有问题&#xff0c;没有说明是什么操作系统&#xff0c;以及是多少位操作系统。 因为不同的操作系统和不同位数的操作系统&#xff0c;虚拟内存可能是不一样多。 Windows 系统我不了解&#xff0c;我就说说 Linux 系统…

Excel技巧:如何批量调整excel表格中的图片?

插入到excel表格中的图片大小不一&#xff0c;如何做到每张图片都完美的与单元格大小相同&#xff1f;并且能够根据单元格来改变大小&#xff1f;今天分享&#xff0c;excel表格里的图片如何批量调整大小。 方法如下&#xff1a; 点击表格中的一个图片&#xff0c;然后按住Ct…

Stable Audio Open模型部署教程:用AI打造独家节拍,让声音焕发新活力!

Stable Audio Open 是一个开源的文本到音频模型&#xff0c;允许用户从简单的文本提示中生成长达 47 秒的高质量音频数据。该模型非常适合创建鼓点、乐器即兴演奏、环境声音、拟音录音和其他用于音乐制作和声音设计的音频样本。用户还可以根据他们的自定义音频数据微调模型&…

Linux上传代码的步骤与注意事项

最近因为工作需要&#xff0c;要上传代码到 DPDK 上&#xff0c;代码已经上传成功&#xff0c;记录一下过程&#xff0c;给大家提供一个参考。我这次需要上传的是pmd&#xff0c;即poll mode driver。 1 Coding Style 要上传代码&#xff0c;第一件事就是需要知道Coding Styl…

运费微服务和redis存热点数据

目录 运费模板微服务 接收前端发送的模板实体类 插入数据时使用的entity类对象 BaseEntity类 查询运费模板服务 新增和修改运费模块 整体流程 代码实现 运费计算 整体流程 总的代码 查找运费模板方法 计算重量方法 Redis存入热点数据 1.从nacos导入共享redis配置…

如何在windows10上部署WebDAV服务并通过内网穿透实现公网分享内部公共文件

WebDAV&#xff08;Web-based Distributed Authoring and Versioning&#xff09;是一种基于HTTP协议的应用层网络协议&#xff0c;它允许用户通过互联网进行文件的编辑和管理。这意味着&#xff0c;无论员工身处何地&#xff0c;只要连接到互联网&#xff0c;就能访问、编辑和…

gRPC 快速入门 — SpringBoot 实现(1)

目录 一、什么是 RPC 框架 &#xff1f; 二、什么是 gRPC 框架 &#xff1f; 三、传统 RPC 与 gRPC 对比 四、gRPC 的优势和适用场景 五、gRPC 在分布式系统中应用场景 六、什么是 Protocol Buffers&#xff08;ProtoBuf&#xff09;&#xff1f; 特点 使用场景 简单的…

深入浅出:SOME/IP-SD的工作原理与应用

目录 往期推荐 相关缩略语 SOME/IP 协议概述 协议介绍 SOME/IP TP 模块概述和 BSW 模块依赖性 原始 SOME/IP 消息的Header格式 SOME/IP-SD 模块概述 模块介绍 BSW modules依赖 客户端-服务器通信示例 Message 结构 用于SD服务的BSWM状态处理 往期推荐 ETAS工具…