OpenGL学习:模型加载-obj模型和AssImp模型

2023-10-14 23:30

本文主要是介绍OpenGL学习:模型加载-obj模型和AssImp模型,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前面介绍了光照基础内容,以及材质和lighting maps,和光源类型,我们对使用光照增强场景真实感有了一定了解。但是到目前为止,我们通过在程序中指定的立方体数据,绘制立方体,看起来还是很乏味。本节开始介绍模型加载,通过加载丰富的模型,能够丰富我们的场景,变得好玩。本节的示例代码均可以在我的github下载。

加载模型可以使用比较好的库,例如obj模型加载的库,Assimp加载库。本节作为入门篇,我们一开始不使用这些库加载很酷的模型,而是熟悉下模型以及模型加载的概念,然后我们封装一个简单的obj模型加载类,加载一个简单的立方体模型。 之后我们会使用Assimp库会加载一个酷炫的3d模型,但是首先还是注重多感受下模型加载的基础

通过本节可以了解到

  • Mesh的概念
  • Obj模型数据格式
  • Obj模型简单的加载类和加载实验
  • AssImp模型加载实验

模型的表达

在3d图形处理中,一个模型(model)通常由一个或者多个Mesh(网格)组成,一个Mesh是可绘制的独立实体。例如复杂的人物模型,可以分别划分为头部,四肢,服饰,武器等各个部分来建模,这些Mesh组合在一起最终形成人物模型。

Mesh由顶点、边、面Faces组成的,它包含绘制所需的数据,例如顶点位置、纹理坐标、法向量,材质属性等内容,它是OpenGL用来绘制的最小实体。Mesh的概念示意如下图所示(来自:What is a mesh in OpenGL?):

Mesh        Mesh2

Mesh可以包含多个Face,一个Face是Mesh中一个可绘制的基本图元,例如三角形,多边形,点。要想模型更加逼真,一般需要增加更多图元使Mesh更加精细,当然这也会受到硬件处理能力的限制,例如PC游戏的处理能力要强于移动设备。由于多边形都可以划分为三角形,而三角形是图形处理器中都支持的基本图元,因此使用得较多的就是三角形网格来建模。例如下面的图(来自:What is a mesh in OpenGL?)表达了使用越来越复杂的Mesh建模一只兔子的过程:

随着增加三角形个数,兔子模型变得越来越真实。

目前模型存储的格式很丰富,比较常用的,例如Wavefront .obj file,COLLADA等,要了解各个格式的特点,可以参考wiki 3D graphics file formats。在众多的格式中以obj格式比较通用,它内部是以文本形式表达的,接下来我们通过熟悉下obj格式,了解模型是如何定义的,以及如何加载到OpenGL中来渲染模型。

Obj模型数据格式

obj模型内部以文本存储,例如从Model loading处获取的一个立方体模型cube.obj的数据如下:

# Blender3D v249 OBJ File: untitled.blend
# www.blender3d.org
mtllib cube.mtl
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
...
vt 0.748573 0.750412
vt 0.749279 0.501284
vt 0.999110 0.501077
...
vn 0.000000 0.000000 -1.000000
vn -1.000000 -0.000000 -0.000000
vn -0.000000 -0.000000 1.000000
...
usemtl Material_ray.png
s off
f 5/1/1 1/2/1 4/3/1
f 5/1/1 4/3/1 8/4/1
f 3/5/2 7/6/2 8/7/2
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

对这个文本格式做一个简要说明:

  • usemtl和mtllib表示的材质相关数据,解析材质数据稍微繁琐,本节我们只是为了说明加载模型的原理,不做讨论。
  • o 引入一个新的object
  • v 表示顶点位置
  • vt 表示顶点纹理坐标
  • vn 表示顶点法向量
  • f 表示一个面,面使用1/2/8这样格式,表示顶点位置/纹理坐标/法向量的索引,这里索引的是前面用v,vt,vn定义的数据 注意这里Obj的索引是从1开始的,而不是0

模型一般通过3d建模软件,例如Blender, 3DS Max 或者 Maya等工具建模,导出时的数据格式变化较大,:将一种模型数据文件表示的模型,转换为OpenGL可以利用的数据。例如上面的Obj文件中,我们需要解析顶点位置,纹理坐标等数据,构成OpenGL可以渲染的Mesh对象。

从Obj到OpenGL可以理解的Mesh

上面说明了Obj的数据格式,那么在OpenGL中我们怎么表达Mesh呢?首先定义顶点属性数据如下所示:

 // 表示一个顶点属性
struct Vertex
{glm::vec3 position;  // 顶点位置glm::vec2 texCoords; // 纹理坐标glm::vec3 normal;  // 法向量
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Mesh中包含顶点属性,纹理对象等信息,本节我们定义Mesh数据结构如下所示:


// 表示一个OpenGL渲染的最小实体
class Mesh
{
public:void draw(Shader& shader) // 绘制MeshMesh(const std::vector<Vertex>& vertData, GLint textureId) // 构造一个Mesh
private:std::vector<Vertex> vertData;// 顶点数据GLuint VAOId, VBOId; // 缓存对象GLint textureId; // 纹理对象idvoid setupMesh();  // 建立VAO,VBO等缓冲区
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

载入obj模型的过程,就是读取obj文件,并转换为上面Mesh对象的过程。这个过程的思路大致是这样的,读取文件的每一行,根据行首部的指示,确定数据类型,然后加载到mesh的vertData里面去,这个框架是这样:

std::ifstream file(objFilePath);
while (getline(file, line))
{if (line.substr(0, 2) == "vt") // 顶点纹理坐标数据{// 解析顶点纹理数据}else if (line.substr(0, 2) == "vn") // 顶点法向量数据{// 解析法向量数据}else if (line.substr(0, 1) == "v") // 顶点位置数据{// 解析顶点位置数据}else if (line.substr(0, 1) == "f") // 面数据{// 解析面数据}else if (line[0] == '#') // 注释忽略{ }else  {// 其余内容 暂时不处理}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

上面提供了一个读取obj文件格式的框架,例如解析纹理坐标数据如下:

if (line.substr(0, 2) == "vt") // 顶点纹理坐标数据
{std::istringstream s(line.substr(2));glm::vec2 v;s >> v.x; s >> v.y;v.y = -v.y;  // 注意这里加载的dds纹理 要对y进行反转temp_textCoords.push_back(v);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

其余的也类似处理。读取到数据后,在Mesh对象里面需要向前面绘制物体时一样建立缓冲数据,如下:

void setupMesh()  // 建立VAO,VBO等缓冲区
{glGenVertexArrays(1, &this->VAOId);glGenBuffers(1, &this->VBOId);glBindVertexArray(this->VAOId);glBindBuffer(GL_ARRAY_BUFFER, this->VBOId);glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex)* this->vertData.size(),&this->vertData[0], GL_STATIC_DRAW);// 顶点位置属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,sizeof(Vertex), (GLvoid*)0);glEnableVertexAttribArray(0);// 顶点纹理坐标glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,sizeof(Vertex), (GLvoid*)(3 * sizeof(GL_FLOAT)));glEnableVertexAttribArray(1);// 顶点法向量属性glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE,sizeof(Vertex), (GLvoid*)(5 * sizeof(GL_FLOAT)));glEnableVertexAttribArray(2);glBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

建立缓冲区的同时,本节我们使用的立方体模型cube.dds纹理如下图所示: 
dds

这与以前使用的png纹理不一样,这里我用C++重新改编了Model loading处的加载dds纹理的函数,加载纹理不是本节的重点,具体可以查看github代码。加载纹理后,可以渲染这个obj表达的立方体模型,整个过程如下:

//Section1 从obj文件加载数据
std::vector<Vertex> vertData;
ObjLoader::loadFromFile("cube.obj", vertData)// Section2 准备纹理
GLint textureId = TextureHelper::loadDDS("cube.dds");// Section3 建立Mesh对象
Mesh mesh(vertData, textureId);// Section4 准备着色器程序
Shader shader("cube.vertex", "cube.frag");// 在游戏主循环中渲染立方体
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里我们可以看到,与以往在程序中通过数值指定立方体模型相比,我们的代码更简洁,后面介绍使用Assimp加载库后,可以加载更多丰富的模型,当然要比这个立方体好看。但是本节还是看一下最终立方体的效果吧,如下: 
obj加载

说明

在使用dds纹理的时候,要注意纹理的y轴相对于OpenGL是进行反转的,因此需要使用( coord.u, 1.0-coord.v) 来访问,这可以在加载obj时做,也可以在着色器里面做。没有使用反转的v坐标将导致,无法正常渲染,这也是困住我的一个地方。后来使用数据比对格式发现了这个错误,如下图,左边是反转了的数据,右边是未反转的数据:

v导致的错误

在使用blender软件导出模型时,即使勾选了includ UVs,输出时仍然没有纹理坐标,这是因为除了勾选这些选项外,还需要一个uv map操作,关于这一点也是容易产生错误的,详细可以参考Add UV Mapped texture coordinates to OBJ file?。uv mappring这个操作的过程比较繁琐,就不再这里介绍了,感兴趣地可以参考UV Mapping a Mesh

最后本节的加载obj程序只是一个示例,并没有解析材质mtl部分。当没有使用纹理数据绘制经典的Suzanne 模型如下图所示: 
Suzanne

这里缺少了纹理和光照,所以模型看起来不真实,下面节介绍使用Assimp加载库时将会改善这一点。

下载和安装AssImp

AssImp是一个模型加载库,它将不同格式的模型数据转换为统一的抽象的数据类型,因而支持较多的模型文件格式。下载和编译这个库的过程,你可以参考官方文档。在linux下可以直接apt-get安装: apt-get install libassimp-dev。

OpenGL需要的数据结构

加载模型的任务就是将抽象的模型数据转换为OpenGL可以处理的VBO,EBO,纹理数据。在程序内部我们定义了Mesh,Model结构来作为内部格式。Mesh表达是绘制的最小实体,它包含顶点属性数据、材质数据;Model则是包含1个或者多个Mesh的模型。定义Mesh结构如下:

// 表示一个顶点属性
struct Vertex
{glm::vec3 position;glm::vec2 texCoords;glm::vec3 normal;
};
// 表示一个Texture
struct Texture
{GLuint id;aiTextureType type;std::string path;
};
// 表示一个用于渲染的最小实体
class Mesh
{
public:void draw(const Shader& shader) const;// 绘制MeshMesh():VAOId(0), VBOId(0), EBOId(0){}Mesh(const std::vector<Vertex>& vertData, const std::vector<Texture> & textures,const std::vector<GLuint>& indices); // 构造一个Meshvoid final() const; // 释放VBO等空间
private:std::vector<Vertex> vertData;  // 顶点属性数据std::vector<GLuint> indices;    // 索引数据 std::vector<Texture> textures;  // 纹理数据GLuint VAOId, VBOId, EBOId;void setupMesh();  // 建立VAO,VBO等缓冲区
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

为了简化程序,这里我们只处理了材质中的纹理数据。Model则是一个包含多个Mesh的类,定义如下:

// 代表一个模型 模型可以包含一个或多个Mesh
class Model
{
public:void draw(const Shader& shader) const{for (mesh in meshes){mesh->draw(shader);}}bool loadModel(const std::string& filePath);~Model(){for (mesh in meshes){mesh->final();}}
private:bool processNode(const aiNode* node, const aiScene* sceneObjPtr); bool processMesh(const aiMesh* meshPtr, const aiScene* sceneObjPtr, Mesh& meshObj);bool processMaterial(const aiMaterial* matPtr, const aiScene* sceneObjPtr, Material& material);
private:std::vector<Mesh> meshes; // 保存Meshstd::string modelFileDir; // 保存模型文件的文件夹路径typedef std::map<std::string, Texture> LoadedTextMapType; // key = texture file pathLoadedTextMapType loadedTextureMap; // 保存已经加载的纹理
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

需要注意的是模型文件所在文件路径我们通过modelFileDir保存起来,因为模型中纹理数据可能使用相对路径来表示纹理,通过modelFileDir加上这个相对路径才能找到纹理图片的正确路径。

AssImp加载模型

加载模型时首先创建 Assimp::Importer的示例,然后通过它的l Assimp::Importer::ReadFile()方法加载模型,如下所示:

#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>Assimp::Importer importer;
const aiScene* sceneObjPtr = importer.ReadFile(filePath, aiProcess_Triangulate | aiProcess_FlipUVs);
if (!sceneObjPtr|| sceneObjPtr->mFlags == AI_SCENE_FLAGS_INCOMPLETE|| !sceneObjPtr->mRootNode)
{std::cerr << "Error:Model::loadModel, description: " << importer.GetErrorString() << std::endl;return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

ReadFile函数中第二个参数就是后处理选项,它是一个枚举类型aiPostProcessSteps,可以使用位或操作包含多个选项,例如选项aiProcess_MakeLeftHanded表示将默认的右手系坐标数据转换为左手系坐标数据,aiProcess_Triangulate选项将索引数据多余3个的多边形划分为多个三角形,方便我们使用三角形进行绘制。完整的后处理选项列表,可以参考官方文档。

通过上面的加载我们获取到了模型的根结构数据aiScene,接下来的工作就是:从aiScene获取OpenGL所需要的VBO,EBO,纹理数据

AssImp中数据通过aiNode组织父子结点,包含了层次信息,我们可以忽略这些信息,直接读取所有我们需要的VBO,EBO,纹理数据,但是这种父子结构信息在后面制作骨骼动画时会再次用到,因此这里还是按照层次的方式来解析aiScene数据。

所谓结点就是包含一个多个Mesh的部位,例如一个人物角色,可能包含头部,颈部,手臂,胸部等多个结点,每个结点也可以包含更多的细化结点。解析aiScene这种父子结点的层次数据,直观的方法就是使用递归,递归就是一个函数直接调用自己,一层一层调用下去,当遇到一个合适条件时终止调用,函数一层层返回。从aiScene解析模型数据获取OpenGL所需数据的框架大概是这样的:

bool loadModel(const std::string& filePath)
{// 加载模型 得到aiScene
const aiScene* sceneObjPtr = importer.ReadFile(filePath, aiProcess_Triangulate | aiProcess_FlipUVs);// 递归处理结点return this->processNode(sceneObjPtr->mRootNode, sceneObjPtr);
}
bool processNode(const aiNode* node, const aiScene* sceneObjPtr)
{for (size_t i = 0; i < node->mNumMeshes; ++i) // 先处理自身结点{// 注意node中的mesh是对sceneObject中mesh的索引const aiMesh* meshPtr = sceneObjPtr->mMeshes[node->mMeshes[i]]; this->processMesh(meshPtr, sceneObjPtr, meshObj); // 处理Mesh}for (size_t i = 0; i < node->mNumChildren; ++i) // 再处理孩子结点{this->processNode(node->mChildren[i], sceneObjPtr);}return true;
}
bool processMesh(const aiMesh* meshPtr, const aiScene* sceneObjPtr, Mesh& meshObj)
{// 从Mesh得到顶点数据、法向量、纹理数据for (size_t i = 0; i < meshPtr->mNumVertices; ++i){...}// 获取索引数据for (size_t i = 0; i < meshPtr->mNumFaces; ++i){...}// 获取纹理数据if (meshPtr->mMaterialIndex >= 0){this->processMaterial(materialPtr, sceneObjPtr, aiTextureType_DIFFUSE, diffuseTexture);this->processMaterial(materialPtr, sceneObjPtr, aiTextureType_SPECULAR, specularTexture);}return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

上面的框架给出了从aiScene获取数据,建立内部格式Model和Mesh的思路,具体实现细节可以参考程序源码。

加载纳米战斗服模型

到这里,我们可以来欣赏酷炫的模型了,首先加载一个从learnopengl获取的纳米战斗服模型nanosuit,效果如下图左所示: 
有光照效果没有光照效果

这里没有使用光照,上图右是实现了一个点光源的效果. 可以从机器人胸部的高光部分看到,实现光照时的区别。

加载模型需要注意的地方

1.加载模型后,需要适当设置模型变换矩阵,否则模型显示在奇怪的位置。这个模型变换矩阵,目前还没找到合适的方法从模型数据中获取。 
2.下载的模型,有些路径是不正确的,本文统一采用绝对路径方式。路径不正确或者文件缺失时的错误提示 
3.部分纹理图片的格式,模型的格式目前并未处理,不支持加载。

还需要改进的地方

上面加载的模型,已经让人很兴奋了,但是还不够真实,高效。在实验过程中,思考还需要通过以下方面进行改进:

1.我们这里的材质只处理了纹理部分,实际上模型中如果没有通过纹理定义材质,还需要获取ambient等颜色表示的材质。而且纹理可能不止一个,本文目前只处理了一个纹理(主要原因是下载的素材里面没有找不到更多的纹理坐标)。可以通过定义下面的材质结构体,并处理这个材质数据来丰富场景:

struct Material // 表示材质属性
{glm::vec3 ambient;glm::vec3 diffuse;glm::vec3 specular;float shininess;std::vector<Texture> textures;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.模型中要通过光源和相机加以改善。目前在模型中通过以下方式:

if (sceneObjPtr->HasLights() 
&& !this->processLightSource(sceneObjPtr))
{
std::cerr 
<< "Error:Model::loadModel, process lights failed."<< std::endl;return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

获取光源数据时,大量从网络上下载的模型中并没有找到光源数据,比较可惜。

3.实际模型的材质中包含了map_Bump数据,但目前还未学习处理方法。 
4.目前通过Model加载模型时耗时非常多,效率不高,需要进一步提高模型加载和渲染的速度


这篇关于OpenGL学习:模型加载-obj模型和AssImp模型的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/213871

相关文章

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

Spring AI Alibaba接入大模型时的依赖问题小结

《SpringAIAlibaba接入大模型时的依赖问题小结》文章介绍了如何在pom.xml文件中配置SpringAIAlibaba依赖,并提供了一个示例pom.xml文件,同时,建议将Maven仓... 目录(一)pom.XML文件:(二)application.yml配置文件(一)pom.xml文件:首

如何在本地部署 DeepSeek Janus Pro 文生图大模型

《如何在本地部署DeepSeekJanusPro文生图大模型》DeepSeekJanusPro模型在本地成功部署,支持图片理解和文生图功能,通过Gradio界面进行交互,展示了其强大的多模态处... 目录什么是 Janus Pro1. 安装 conda2. 创建 python 虚拟环境3. 克隆 janus

本地私有化部署DeepSeek模型的详细教程

《本地私有化部署DeepSeek模型的详细教程》DeepSeek模型是一种强大的语言模型,本地私有化部署可以让用户在自己的环境中安全、高效地使用该模型,避免数据传输到外部带来的安全风险,同时也能根据自... 目录一、引言二、环境准备(一)硬件要求(二)软件要求(三)创建虚拟环境三、安装依赖库四、获取 Dee

DeepSeek模型本地部署的详细教程

《DeepSeek模型本地部署的详细教程》DeepSeek作为一款开源且性能强大的大语言模型,提供了灵活的本地部署方案,让用户能够在本地环境中高效运行模型,同时保护数据隐私,在本地成功部署DeepSe... 目录一、环境准备(一)硬件需求(二)软件依赖二、安装Ollama三、下载并部署DeepSeek模型选

Golang的CSP模型简介(最新推荐)

《Golang的CSP模型简介(最新推荐)》Golang采用了CSP(CommunicatingSequentialProcesses,通信顺序进程)并发模型,通过goroutine和channe... 目录前言一、介绍1. 什么是 CSP 模型2. Goroutine3. Channel4. Channe

SpringBoot项目启动后自动加载系统配置的多种实现方式

《SpringBoot项目启动后自动加载系统配置的多种实现方式》:本文主要介绍SpringBoot项目启动后自动加载系统配置的多种实现方式,并通过代码示例讲解的非常详细,对大家的学习或工作有一定的... 目录1. 使用 CommandLineRunner实现方式:2. 使用 ApplicationRunne