Learn OpenGL 10 Assimp+网格+模型

news2024/11/17 19:31:57

Assimp

一个非常流行的模型导入库是Assimp,它是Open Asset Import Library(开放的资产导入库)的缩写。Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。

导入

如图找到Nuget程序包

接着搜索assimp后点击安装就可以了

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

 创建Mesh类

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

一个网格应该至少需要一系列的顶点,每个顶点包含一个位置向量、一个法向量和一个纹理坐标向量。一个网格还应该包含用于索引绘制的索引以及纹理形式的材质数据(漫反射/镜面光贴图)。

创建Mesh类,生成Mesh.h和Mesh.cpp文件。

在Mesh.h文件定义我们需要的属性和函数

根据不同的需要设置了两个初始化缓冲区的函数以及两个绘制函数,当需要根据索引缓冲区绘制物体的时候,就调用DrawElements(Shader* shader),setupElementsMesh()这两个函数来绘制,否则调用DrawArray,setupArrayMesh()这两个函数来绘制

Mesh.h

#pragma once
#include <glm/glm.hpp>
#include <string>
#include <glad/glad.h>
#include <vector>
#include "Shader.h"
#include <GLFW/glfw3.h>
#include <iostream>

struct Vertex {
	glm::vec3 Position;
	glm::vec3 Normal;
	glm::vec2 TexCoords;
};

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

class Mesh
{
public:
	/*  网格数据  */
	std::vector<Vertex> vertices;
	std::vector<unsigned int> indices;
	std::vector<Texture> textures;
	/*  构造函数  */
	Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures);
	Mesh(float vertices[]);
	/*  绘制函数  */
	void DrawArray(Shader* shader, int diffuse, int specular, int emission);
	void DrawElements(Shader* shader);
private:
	/*  渲染数据  */
	unsigned int VAO, VBO, EBO;
	/*  函数  */
	void setupElementsMesh();//初始化缓冲
	void setupArrayMesh();
};


在Mesh.cpp文件中实现函数

C++结构体有一个很棒的特性,它们的内存布局是连续的(Sequential)。也就是说,如果我们将结构体作为一个数据数组使用,那么它将会以顺序排列结构体的变量,这将会直接转换为我们在数组缓冲中所需要的float(实际上是字节)数组。比如说,如果我们有一个填充后的Vertex结构体,那么它的内存布局将会等于:

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];

由于有了这个有用的特性,我们能够直接传入一大列的Vertex结构体的指针作为缓冲的数据,它们将会完美地转换为glBufferData所能用的参数:

glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

自然sizeof运算也可以用在结构体上来计算它的字节大小。这个应该是32字节的(8个float * 每个4字节)。

结构体的另外一个很好的用途是它的预处理指令offsetof(s, m),它的第一个参数是一个结构体,第二个参数是这个结构体中变量的名字。这个宏会返回那个变量距结构体头部的字节偏移量(Byte Offset)。这正好可以用在定义glVertexAttribPointer函数中的偏移参数:

glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal)); 

偏移量现在是使用offsetof来定义了,在这里它会将法向量的字节偏移量设置为结构体中法向量的偏移量,也就是3个float,即12字节。注意,我们同样将步长参数设置为了Vertex结构体的大小。

使用这样的一个结构体不仅能够提供可读性更高的代码,也允许我们很容易地拓展这个结构。如果我们希望添加另一个顶点属性,我们只需要将它添加到结构体中就可以了。由于它的灵活性,渲染的代码不会被破坏。

 这里绘制函数的实现和之前课程上的原理是一样的,需要根据纹理数组的大小分别设置纹理。

然后绑定对应的VAO进行绘制。 

Mesh.cpp

#include "Mesh.h"

Mesh::Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures)
{
	this->vertices = vertices;
	this->indices = indices;
	this->textures = textures;
	setupElementsMesh();
}

Mesh::Mesh(float vertices[])//绘制立方体(自定义顶点)
{
	this->vertices.resize(36);
	memcpy(&(this->vertices[0]), vertices, 36 * 8 * sizeof(float));

	setupArrayMesh();
}

void Mesh::DrawArray(Shader* shader, int diffuse, int specular, int emission)
{

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, diffuse);
	shader->setInt("material.diffuse", 0);


	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, specular);
	shader->setInt("material.specular", 1);

	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, emission);
	shader->setInt("material.emission", 2);

	// 绘制网格
	glBindVertexArray(VAO);
	glDrawArrays(GL_TRIANGLES, 0, 36);
	glBindVertexArray(0);
	glActiveTexture(GL_TEXTURE0);
}

void Mesh::DrawElements(Shader* shader)
{

	for (unsigned int i = 0; i < textures.size(); i++)
	{

		if (textures[i].type == "texture_diffuse") {
			glActiveTexture(GL_TEXTURE0);
			glBindTexture(GL_TEXTURE_2D, textures[i].id);
			shader->setInt("material.diffuse", i);


		}
		else if (textures[i].type == "texture_specular")
		{
			glActiveTexture(GL_TEXTURE1);
			glBindTexture(GL_TEXTURE_2D, textures[i].id);
			shader->setInt("material.specular", i);
		}
		else if (textures[i].type == "texture_emission")
		{
			glActiveTexture(GL_TEXTURE2);
			glBindTexture(GL_TEXTURE_2D, textures[i].id);
			shader->setInt("material.emission", i);
		}
	}
	// 绘制网格
	glBindVertexArray(VAO);
	glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
	glBindVertexArray(0);
	glActiveTexture(GL_TEXTURE0);

}



void Mesh::setupElementsMesh()
{
	glGenVertexArrays(1, &VAO);
	glBindVertexArray(VAO);

	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);


	glGenBuffers(1, &EBO);
	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);
}

void Mesh::setupArrayMesh()
{
	glGenVertexArrays(1, &VAO);
	glBindVertexArray(VAO);

	glGenBuffers(1, &VBO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[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);
}

创建Model类

现在是时候接触Assimp并创建实际的加载和转换代码了。这个教程的目标是创建另一个类来完整地表示一个模型,或者说是包含多个网格,甚至是多个物体的模型。一个包含木制阳台、塔楼、甚至游泳池的房子可能仍会被加载为一个模型。我们会使用Assimp来加载模型,并将它转换(Translate)至多个在上一节中创建的Mesh对象。

Model类包含了一个Mesh对象的vector,构造器需要我们给它一个文件路径。在构造器中,它会直接通过loadModel来加载文件。私有函数将会处理Assimp导入过程中的一部分,我们很快就会介绍它们。我们还将储存文件路径的目录,在之后加载纹理的时候还会用到它。

Draw函数没有什么特别之处,基本上就是遍历了所有网格,并调用它们各自的Draw函数。

Model.h

#pragma once
#include <vector>
#include <string>
#include "Mesh.h"
#include <iostream>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "stb_image.h"
class Model
{
public:
	/*  函数   */
	Model(std::string path);
	void Draw(Shader* shader);

private:
	/*  模型数据  */
	std::vector<Mesh> meshes;
	std::string directory;
	std::vector<Texture> textures_loaded;
	/*  函数   */
	void loadModel(std::string path);
	void processNode(aiNode* node, const aiScene* scene);
	Mesh processMesh(aiMesh* mesh, const aiScene* scene);
	std::vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName);
	unsigned int TextureFromFile(const char* path, const std::string& directory);
};


首先需要调用的函数是loadModel,它会从构造器中直接调用。在loadModel中,我们使用Assimp来加载模型至Assimp的一个叫做scene的数据结构中。你可能还记得在模型加载章节的第一节教程中,这是Assimp数据接口的根对象。一旦我们有了这个场景对象,我们就能访问到加载后的模型中所有所需的数据了。

首先声明了Assimp命名空间内的一个Importer,之后调用了它的ReadFile函数。这个函数需要一个文件路径,它的第二个参数是一些后期处理(Post-processing)的选项。除了加载文件之外,Assimp允许我们设定一些选项来强制它对导入的数据做一些额外的计算或操作。通过设定aiProcess_Triangulate,我们告诉Assimp,如果模型不是(全部)由三角形组成,它需要将模型所有的图元形状变换为三角形。aiProcess_FlipUVs将在处理的时候翻转y轴的纹理坐标(你可能还记得我们在纹理教程中说过,在OpenGL中大部分的图像的y轴都是反的,所以这个后期处理选项将会修复这个)。其它一些比较有用的选项有:

  • aiProcess_GenNormals:如果模型不包含法向量的话,就为每个顶点创建法线。
  • aiProcess_SplitLargeMeshes:将比较大的网格分割成更小的子网格,如果你的渲染有最大顶点数限制,只能渲染较小的网格,那么它会非常有用。
  • aiProcess_OptimizeMeshes:和上个选项相反,它会将多个小网格拼接为一个大的网格,减少绘制调用从而进行优化。

Assimp提供了很多有用的后期处理指令,你可以在这里找到全部的指令。实际上使用Assimp加载模型是非常容易的(你也可以看到)。困难的是之后使用返回的场景对象将加载的数据转换到一个Mesh对象的数组。

如果什么错误都没有发生,我们希望处理场景中的所有节点,对于每个节点,获取它们的网格。

每一个网格都是一个aiMesh对象,将一个aiMesh对象转化为我们自己的网格对象不是那么困难。我们要做的只是访问网格的相关属性并将它们储存到我们自己的对象中,依次获取他们的顶点,索引以及材质数据。最后返回一个Mesh对象。

Model.cpp 

#include "Model.h"


Model::Model(std::string path)
{
	loadModel(path);
}



void Model::loadModel(std::string path)
{
	Assimp::Importer import;
	//路径之后的三个参数的功能分别是将模型所有的图元形状变换为三角形,翻转y轴的纹理坐标,计算切线和副切线
	const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);
	//第二个是检查数据是否不完整或者有损坏
	if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
	{
		std::cout << "ERROR::ASSIMP::" << import.GetErrorString() << std::endl;
		return;
	}
	//第一个参数是截取的起始位置,第二个参数是截取的长度。在这里,它截取了从索引0开始,到最后一个斜杠之前的位置
	directory = path.substr(0, path.find_last_of('\\'));
	//std::cout << "Success!" << directory << std::endl;
	processNode(scene->mRootNode, scene);
}

void Model::processNode(aiNode* node, const aiScene* scene)
{
	// 处理节点所有的网格
	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);
	}
}
//处理网格
Mesh Model::processMesh(aiMesh* mesh, const aiScene* scene)
{
	std::vector<Vertex> vertices;
	std::vector<unsigned int> indices;
	std::vector<Texture> textures;

	// 处理顶点位置、法线和纹理坐标
	for (unsigned int i = 0; i < mesh->mNumVertices; i++)
	{
		Vertex vertex;
		glm::vec3 vector;
		// 位置
		vector.x = mesh->mVertices[i].x;
		vector.y = mesh->mVertices[i].y;
		vector.z = mesh->mVertices[i].z;
		vertex.Position = vector;
		// 法向
		vector.x = mesh->mNormals[i].x;
		vector.y = mesh->mNormals[i].y;
		vector.z = mesh->mNormals[i].z;
		vertex.Normal = vector;
		// 纹理坐标
		if (mesh->mTextureCoords[0]) // 是否存在
		{
			glm::vec2 vec;
			// Assimp允许一个模型在一个顶点上有最多8个不同的纹理坐标,
			// 我们不会用到那么多,我们只关心第一组纹理坐标。
			vec.x = mesh->mTextureCoords[0][i].x;
			vec.y = mesh->mTextureCoords[0][i].y;
			vertex.TexCoords = vec;
		}
		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]);
	}

	// 处理材质
	//mesh->mMaterialIndex返回的是材质的索引值,如果没有则一般返回-1
	if (mesh->mMaterialIndex >= 0)
	{
		aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];

		std::vector<Texture> diffuseMaps = loadMaterialTextures(material,
			aiTextureType_DIFFUSE, "texture_diffuse");
		textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());

		std::vector<Texture> specularMaps = loadMaterialTextures(material,
			aiTextureType_SPECULAR, "texture_specular");
		textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
	}

	// return a mesh object created from the extracted mesh data
	return Mesh(vertices, indices, textures);
}

std::vector<Texture>  Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName)
{

	std::vector<Texture> textures;
	for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
	{
		aiString str;
		mat->GetTexture(type, i, &str);
		bool 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;
				break;
			}
		}
		if (!skip)
		{   // 如果纹理还没有被加载,则加载它
			Texture texture;
			texture.id = TextureFromFile(str.C_Str(), directory);
			texture.type = typeName;
			texture.path = str.C_Str();
			textures.push_back(texture);
			textures_loaded.push_back(texture); // 添加到已加载的纹理中

		}
	}
	return textures;
}

unsigned int Model::TextureFromFile(const char* path, const std::string& directory)
{
	std::string filename = std::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;
}

void Model::Draw(Shader* shader)
{
	for (unsigned int i = 0; i < meshes.size(); i++)
		meshes[i].DrawElements(shader);
}

结果

渲染成功

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

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

相关文章

git基础命令(一)

目录 基础概念git statusgit addgit diffgit loggit commit文件可以处于以下三种状态之一远程存储库与本地存储库参考 用于知识记录。后续有新的的内容&#xff0c;例子&#xff0c;将持续更新本文档。 基础概念 工作树&#xff1a;git add 之前&#xff0c;变动内容的文件列表…

经典数组和指针笔试题解析——C语言

【本节内容】 1. 数组和指针笔试题解析 2. 指针运算笔试题解析 1. 数组和指针笔试题解析 1.1 一维数组 #include <stdio.h> int main() {int a[] { 1,2,3,4 };printf("%zd\n", sizeof(a));printf("%zd\n", sizeof(a 0));printf("%zd\n&qu…

【金三银四】掌趣科技24.3.7 H项目 服务端开发笔试题

考试题型&#xff1a; 不定项选择题 10 道 &#xff0c; 填空题 10 道 &#xff0c; 问答题 2 道 &#xff0c; 编程题 4 道 目录 不定项选择题 10 道填空题 10 道问答题 2 道编程题 4 道 不定项选择题 10 道 在TCP协议中&#xff0c;发送方的窗口大小是由两个关键因素共同决定…

三个表联合查询的场景分析-场景1:a表关联了b表和c表

本场景对应情景如下&#xff1a; 三个数据表&#xff0c;一个表的两个字段分别关联了另外两个表各自的id数据&#xff0c;可能包含多个id&#xff08;两个1对多关联&#xff09;。 目录 数据表准备 需求1、查询c表的列表数据&#xff0c;要求获得关联的b表中的name&#xf…

工业界真实的推荐系统(小红书)-涨指标的方法:召回、排序、多样性、特殊人群、利用交互行为

课程特点&#xff1a;系统、清晰、实用&#xff0c;原理和落地经验兼具 b站&#xff1a;https://www.bilibili.com/video/BV1HZ421U77y/?spm_id_from333.337.search-card.all.click&vd_sourceb60d8ab7e659b10ea6ea743ede0c5b48 讲义&#xff1a;https://github.com/wangsh…

CSDN 编辑器设置图片缩放和居中

CSDN 编辑器设置图片缩放和居中 文章目录 CSDN 编辑器设置图片缩放和居中对齐方式比例缩放 对齐方式 Markdown 编辑器插入图片的代码格式为 ![图片描述](图片路径)CSDN 的 Markdown 编辑器中插入图片&#xff0c;默认都是左对齐&#xff0c;需要设置居中对齐的话&#xff0c;…

项目性能优化—性能优化的指标、目标

项目性能优化—性能优化的指标、目标 性能优化的终极目标是什么 性能优化的目标实际上是为了更好的用户体验&#xff1a; 一般我们认为用户体验是下面的公式&#xff1a; 用户体验 产品设计&#xff08;非技术&#xff09; 系统性能 ≈ 系统性能 快 那什么样的体验叫快呢…

STM32第九节(中级篇):RCC——时钟树讲解(第一节)

目录 前言 STM32第九节&#xff08;中级篇&#xff09;&#xff1a;RCC——时钟树讲解 时钟树主系统时钟讲解 HSE时钟 HSI时钟 锁相环时钟 系统时钟 SW位控制 HCLK时钟 PCLKI时钟 PCLK2时钟 RTC时钟 MCO时钟输出 6.2.7时钟安全系统(CSS&#xff09; 小结 前言 从…

基于HarmonyOS ArkTS中秋国庆祝福程序、以代码之名,写阖家团圆祝福

中秋、国庆双节将至&#xff0c;作为程序员&#xff0c;以代码之名&#xff0c;表达对于阖家团圆的祝福。本节将演示如何在基于HarmonyOS ArkUI的SwiperController、Image、Swiper等组件来实现节日祝福轮播程序。 规则要求具体要求如下&#xff1a; 1、根据主题&#xff0c;用…

遗嘱消息(Will Message)介绍与示例 _ MQTT 5.0 特性详解

什么是 MQTT 遗嘱消息&#xff1f; 在现实世界中&#xff0c;一个人可以制定一份遗嘱&#xff0c;声明在他去世后应该如何分配他的财产以及应该采取什么行动。在他去世后&#xff0c;遗嘱执行人会将这份遗嘱公开&#xff0c;并执行遗嘱中的指示。 在 MQTT 中&#xff0c;客户端…

honle电源维修UV电源控制器维修EVG EPS60

好乐UV电源控制器维修&#xff1b;honle控制器维修&#xff1b;UV电源维修MUC-Steuermodul 2 LΛmpen D-82166 主要维修型号&#xff1a; EVG EPS 60/120、EVG EPS 100、EVG EPS200、EVG EPS 220、EVG EPS 340、EVG EPS40C-HMI、EVG EPS60 HONLE好乐uv电源维修故障包括&#…

Python实时追踪关键点组成人体模型

项目背景 最近遇到这样一个需求&#xff1a; 1&#xff1a;实时追踪关键点组成人体模型&#xff08;手臂包括三个点&#xff1a;手腕&#xff0c;肘关节&#xff0c;双肩&#xff1b;腿部包括胯骨&#xff0c;膝盖&#xff0c;脚踝&#xff09; 2&#xff1a;运用追踪到的关键…

【c++】string类的使用及模拟实现

1.我们为什么要学习string类&#xff1f; 1.1 c语言中的字符串 我们先了解一下什么是OOP思想 OOP思想&#xff0c;即面向对象编程&#xff08;Object-Oriented Programming&#xff09;的核心思想&#xff0c;主要包括“抽象”、“封装”、“继承”和“多态”四个方面。 抽象…

Day16 面向对象进阶——接Day15

Day16 面向对象进阶——接Day15 文章目录 Day16 面向对象进阶——接Day15一、抽象类及抽象方法二、接口三、多态四、对象转型五、内部类 一、抽象类及抽象方法 //抽象类 public abstract class 类名{//抽象方法public abstract void method(); }1、抽象方法交给非抽象的子类去…

轻松驾驭时间流:MYSQL日期与时间函数的实用技巧

​&#x1f308; 个人主页&#xff1a;danci_&#x1f525; 系列专栏&#xff1a;《MYSQL应用》&#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 轻松驾驭时间流&#xff1a;MYSQL日期与时间函数的实用技巧 MYSQL日期时间函数是数据库操作中不可…

第五十八回 吴用赚金铃吊挂 宋江闹西岳华山-飞桨图像分割套件PaddleSeg初探

鲁智深被贺太守抓住&#xff0c;押入死牢。武松得信后&#xff0c;正想回梁山报信&#xff0c;正好戴宗来了&#xff0c;就请戴宗赶快回梁山搬救兵。宋江说兄弟有难&#xff0c;怎能不救&#xff1f; 于是带了十六个头领来到少华山。 因为华州城池厚壮&#xff0c;宋江等无计可…

Selenium 自动化 —— 入门和 Hello World 实例

Selenium 是什么 Selenium 是一个用于自动化网页浏览器操作的工具&#xff0c;它支持多种浏览器和多种操作系统。主要用于测试 web 应用程序的功能&#xff0c;也可用于执行一些基本的浏览器操作任务&#xff0c;例如自动化表单填写、网页导航等。 Selenium 是一个开源项目&a…

扒带和扒谱的区别 FL Studio怎么扒带 扒带编曲制作 扒带简单歌曲

在许多业余音乐爱好者们的眼里&#xff0c;扒带和扒谱是同一种东西。诚然&#xff0c;扒带和扒谱的确非常相似&#xff0c;但是从严格的意义上来说&#xff0c;这二者还是有一定的区别。今天我们就来说一说扒带和扒谱的区别&#xff0c;FL Studio怎么扒带。 FL Studio21中文官网…

.Net使用ElasticSearch

文章目录 前言主体内容一.Kibana中ElasticSearch的基础操作1.GET&#xff08;查询&#xff09;1.POST&#xff08;新增&#xff09;1.PUT&#xff08;修改&#xff09;1.DELET&#xff08;删除&#xff09; 二.在.Net中&#xff0c;对ElasticSearch进行基础操作1.DotNet连接Ela…

Docker常见指令

1.docker search mysql &#xff1a;从docker镜像仓库搜索和mysql有关的镜像 docker search mysql 2.docker pull mysql &#xff1a;从docker仓库拉取mysql镜像 docker pull mysql 3.docker run mysql &#xff1a;启动mysql镜像 docker run mysql 4.docker ps &#xff…