IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> openGL细分着色器详解 -> 正文阅读

[游戏开发]openGL细分着色器详解

openGL系列文章目录

前言

术语Tessellation(镶嵌)是指一大类设计活动,通常是指在平坦的表面上,用各种几何
形状的瓷砖相邻排列以形成图案。它的目的可以是艺术性的或实用性的,很多例子可以追
溯到几千年前[TS16]。
在3D 图形学中,Tessellation 指的是有点不同的东西(曲面细分),但显然是由它的经典
对应物(镶嵌)启发而成的。在这里,曲面细分指的是生成并且操控大量三角形以渲染复
杂的形状和表面,尤其是使用硬件进行渲染。曲面细分是OpenGL 核心近期才增加的新功
能,在2010 年的4.0 版本中出现。①

一、细分曲面着色器

1.细分曲面Patch

细分曲面过程并不对OpenGL典型的几何图元(点、线和三角形)进行操作,而是使用一个新的图元(在OpenGL 4.0版本中新增的),称为patch。patch由流水线中所有活动的着色阶段处理。相比起来,其它图元类型仅仅被顶点、片段和几何着色器处理,而旁通细分曲面阶段。实际上,如果有任一细分曲面着色器是活跃的,那么传递任何其它几何类型会产生一个GL_INVALID_OPERATION错误。相反地,如果企图渲染一个patch而没有任何细分曲面着色器(明确地说是一个细分曲面计算着色器;我们会看到细分曲面控制着色器是可选的),那么将也会得到一个GL_INVALID_OPERATION错误。

patch仅仅是传入到OpenGL的一列顶点列表,该列表在处理期间保存它们的次序。当用细分曲面与patch进行渲染时,使用像glDrawArrays()这样的渲染命令,并指定从绑定的顶点缓存对象(VBO)将被读出的顶点的总数,然后为该绘制调用进行处理。当用其它的OpenGL图元进行渲染时,OpenGL基于在绘制调用中所指定的图元类型而隐式地知道要使用多少顶点,比如使用三个顶点来绘制一个三角形。然后,当使用一个patch时,需要告诉OpenGL顶点数组中要使用多少个顶点来组成一个patch,而这可以通过使用glPatchParameteri()进行指定。由同一个绘制调用所处理的patch,它们的尺寸(即每个patch的顶点个数)将是相同的。

void glPatchParameteri(GLenum pname, GLint value);
/**
 * 使用value来指定一个patch中的顶点个数。pname必须设置为GL_PATCH_VERTICES。
 * 如果value小于零或大于GL_MAX_PATCH_VERTICES,将会产一个GL_INVALID_ENUM的错误。
 * 一个patch的默认顶点个数是三。如果一个patch的顶点个数小于参数value值,那么该patch将被忽略,从而不会有几何图形产生。
*/

要指定一个patch,使用类型GL_PATCHES输入到任一OpenGL绘制命令。以下代码描述了发射两个patch,每个patch含有四个顶点,然后通过glDrawArrays绘制命令进行渲染。

GLfloat vertices[][2] = {
    {-0.75f, -0.25f}, {-0.25f, -0.25f}, {-0.25f, 0.25f}, {-0.75f, 0.25f},
    {0.25f, -0.25f}, {0.75f, -0.25f}, {0.75f, -0.25f}, {0.75f, 0.25f}, {0.25f, 0.25f}
};

glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(vPos, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
glPatchParameteri(GL_PATCH_VERTICES, 4);
glDrawArrays(GL_PATCHES, 0, 8);

什么是面片
是一个传递给OpenGL的顶点列表
使用面片绘制的区别:
如果我们不使用面片绘制图形 我们就是使用默认的图元类型 这些类型都是已知图元类型和顶点数量的对应关系 例如三个顶点对应于一个三角形 或者两个顶点对应于一条线
但是如果使用面片 我们无法知道一个面片包含了多少个顶点 所以就要使用glPatchParameteri()

细分控制
细分控制着色器负责控制要渲染的输出面片类型 也就是细分发生的区域
OpenGL有三种细分域:四边形 三角形 等值线集合

内侧细分层级
设置的是细分区域的内部划分方式 保存在gl_TessLevelInner中
内测细分层级的值设置了区域内水平和垂直方向上各自有多少“区域”

外侧细分层级
负责控制细分区域的周长 保存在gl_TessLevelOuter数组中
外侧细分层级的值于周长上每条边的段数是对应的

细分计算
每个通过图元生成的细分坐标都需要执行一次细分计算着色器
细分计算着色器的输入变量
在这里插入图片描述
细分计算着色器中输出的顶点数据被存储在如下的接口块中

out gl_PerVertex
{
	vec4 gl_Positionl;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
}

在这里插入图片描述
在这里插入图片描述
例子
在这里插入图片描述

gl_TessLevelOuter[0] = 2.0;
gl_TessLevelOuter[1] = 3.0;
gl_TessLevelOuter[2] = 2.0;
gl_TessLevelOuter[3] = 5.0;
gl_TessLevelInner[0] = 3.0;
gl_TessLevelInner[1] = 4.0;

在这里插入图片描述

Example 9.4 Tessellation Levels for an Isoline Domain Tessellation Shown in Figure 9.2
gl_TessLevelOuter[0] = 6;
gl_TessLevelOuter[1] = 8;

在这里插入图片描述

Example 9.5 Tessellation Levels for a Triangular Domain Tessellation Shown in Figure 9.3 .
gl_TessLevelOuter[0] = 6;
gl_TessLevelOuter[1] = 5;
gl_TessLevelOuter[2] = 8;
gl_TessLevelInner[0] = 5;

在这里插入图片描述

左边为gl_TessLevelInner[0] = 奇数时 右边为gl_TessLevelInner[0] = 偶数时

细分着色器有两个着色阶段 细分控制着色器,细分计算着色器
1.首先要指定面片 也就是顶点的有序列表
2.渲染面片时 将首先执行细分控制着色器,处理面片顶点,并设置面片中要生成多少集合数据
3.细分控制着色器结束后 细分计算着色器将负责把生成网格的顶点放置到细分坐标指定的位置 并将他们发送到光栅化阶段 或发给几何着色器

所有管线中的着色器都能处理片元这个图元类型
gl_in的结构体

in gl_PerVertex
{
	vec4 gl_Positionl;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
}gl_in[gl_PatchVerticesIn];

函数
(设置面片顶点数)glPatchParameteri(GLenum paname,GLint value)
设置一个面片中的顶点数为vlaue panme必须设置为GL_PATCH_VERTICES
一个面片默认值是3 如果面片的顶点数量小于这个值 那么将忽略这个面片 不会产生几何体

(设置内测和外侧细分层级因数)glPatchParameterfv(GLenum panme, const GLfloat* values)
如果没有绑定细分控制着色器 则用这个函数来设置内测和外侧细分层级因数
pname必须时GL_PATCH_DEFAULT_OUTER_LEVEL或者GL_PATCH_DEPCH_INNER_LEVEL

内置变量
(获取gl_in的元素个数)gl_PatchVerticesIn
(当前细分着色器在控制的顶点索引)gl_InvocationID
(gl_in的大小 输入面片的顶点数量)gl_PatchVerticesIn
(gl_out的大小 输出面片的顶点数量)gl_PatchVerticesOut
(细分坐标)gl_TessCoord
评估着色器对曲面细分器生成的每个顶点执行一次。可以使用内置变量gl_TessCoord 访
问顶点位置。曲面细分网格的朝向使得它位于X-Z 平面中,因此gl_TessCoord 的X 和Y 分
量被应用于网格的X 和Z 坐标。网格坐标,以及gl_TessCoord 的值,范围为0.0~1.0(这在
计算纹理坐标时会很方便)。然后,评估着色器使用MVP 矩阵定向每个顶点(这在前面章
节的示例中,是由顶点着色器完成的)。
最后,片段着色器只为每个像素输出一个恒定的黄色。当然,我们也可以使用它来为我
们的场景应用纹理或光照,就像我们在前面的章节中看到的那样。
(细分控制着色器的输出顶点)gl_out[i].gl_Position
这是细分计算着色器中调用gl_out[i].gl_Position可以获取从细分控制着色器输进来的顶点坐标

#version 430 core
layout (vertices = 4) out;
void main()
{
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

例子

1.c++主程序


```cpp
#include "glew/glew.h"
#include "glfw/glfw3.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "Utils.h"
#include "camera.h"
#include <iostream>
#include <string>
#include <fstream>

using namespace std;

static const float pai = 3.1415926f;
float toRadians(float degrees) { return degrees * 2.f * pai / (float)360.f; }

static const int screenWidth = 1920;
static const int screenHeight = 1080;

static const int numVAOs = 1;
static const int numVBOs = 4;

GLuint renderingProgram = 0;

GLuint vao[numVAOs] = { 0 };
GLuint vbo[numVBOs] = { 0 };

float cameraX = 0.f, cameraY = 0.f, cameraZ = 0.f;
float terLocX = 0.f, terLocY = 0.f, terLocZ = 0.f;

GLuint mvpLoc = 0;

int width = 0, height = 0;

float aspect = 0.f;
glm::mat4 mMat(1.f), vMat(1.f), pMat(1.f), mvMat(1.f), mvpMat(1.f);
float tessInner = 30.f;
float tessOuter = 20.f;
GLuint floorTexture = 0;


Camera camera(glm::vec3(0.f, 0.f, 5.f));
//float cameraX = 0.f, cameraY = 0.f, cameraZ = 5.f;
GLboolean keys[1024] = { GL_FALSE };
GLboolean b_firstMouse = GL_TRUE;
float deltaTime = 0.f;

float lastFrame = 0.f;
float lastLocX = 0.f;
float lastLocY = 0.f;

void do_movement()
{
	if (keys[GLFW_KEY_W])
	{
		camera.ProcessKeyboard(FORWARD, deltaTime);
	}
	if (keys[GLFW_KEY_S])
	{
		camera.ProcessKeyboard(BACKWARD, deltaTime);
	}
	if (keys[GLFW_KEY_A])
	{
		camera.ProcessKeyboard(LEFT, deltaTime);
	}
	if (keys[GLFW_KEY_D])
	{
		camera.ProcessKeyboard(RIGHT, deltaTime);
	}
	/*if (keys[GLFW_KEY_ESCAPE])
	{
		glfwSetWindowShouldClose(window, GL_TRUE);
	}*/
}

void key_press_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
	if ((key == GLFW_KEY_ESCAPE) && (action == GLFW_PRESS))
	{
		glfwSetWindowShouldClose(window, GL_TRUE);
	}
	if (action == GLFW_PRESS)
	{
		keys[key] = GLFW_TRUE;  //这里一定一定不能写成“==“,否则  按键WSAD按键失效!!!!!!!
	}
	else if (action == GLFW_RELEASE)
	{
		keys[key] = GLFW_FALSE;    //这里一定一定不能写成“==“,否则  按键WSAD按键失效!!!!!!!
	}
}

void mouse_move_callback(GLFWwindow* window, double xPos, double yPos)
{
	if (b_firstMouse)
	{
		lastLocX = xPos;
		lastLocY = yPos;
		b_firstMouse = GL_FALSE;
	}

	float xOffset = xPos - lastLocX;
	float yOffset = lastLocY - yPos;
	lastLocX = xPos;
	lastLocY = yPos;

	camera.ProcessMouseMovement(xOffset, yOffset);

}

void mouse_scroll_callback(GLFWwindow* window, double xPos, double yPos)
{
	camera.ProcessMouseScroll(yPos);
}


void init(GLFWwindow* window)
{
	/*.vert - 顶点着色器
	  .tesc - 曲面细分控制着色器
	  .tese - 曲面细分评估着色器
	  .geom - 几何着色器
	  .frag - 片段着色器
		.comp - 计算着色器*/
	renderingProgram = Utils::createShaderProgram("vertShader.vert", "tessCShader.tesc", "tessEShader.tese", "fragShader.frag");
	cameraX = 0.f, cameraY = 0.f, cameraZ = 4.f;
	terLocX = 0.f, terLocY = 0.f, terLocZ = 0.f;

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(toRadians(45.f), aspect, 0.1f, 1000.f);

	floorTexture = Utils::loadTexture("floor_color.jpg");
	glGenVertexArrays(numVAOs, vao);
	glBindVertexArray(vao[0]);
}

void display(GLFWwindow* window, double currentTime)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glClearColor(0.1f, 0.2f, 0.5f, 1.f);

	glUseProgram(renderingProgram);  //这句不能少,否则渲染不出曲面

	deltaTime = currentTime - lastFrame;
	lastFrame = currentTime;

	do_movement();
	//这句必须要有,否则鼠标中键失效
	pMat = glm::perspective(camera.Zoom, aspect, 0.1f, 1000.f);

	//没有这句,背景就没在相机视点上了,把圆环移到相机的位置
	//mMat = glm::translate(glm::mat4(1.f), glm::vec3(cameraX, cameraY, 4.5f));
	vMat = camera.GetViewMatrix();



	//vMat = glm::translate(glm::mat4(1.f), glm::vec3(-cameraX, -cameraY, -cameraZ));
	mMat = glm::translate(glm::mat4(1.f), glm::vec3(terLocX, terLocY, terLocZ));
	mMat = glm::rotate(mMat, toRadians(45.f), glm::vec3(1.f, 0.f, 0.f));  //mMat;
	mMat = glm::rotate(mMat, toRadians(30.f), glm::vec3(0.f, 1.f, 0.f));
	mvpMat = pMat * vMat * mMat;

	mvpLoc = glGetUniformLocation(renderingProgram, "mvp_matrix");

	glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, glm::value_ptr(mvpMat));

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, floorTexture);

	glFrontFace(GL_CCW);

	//指定补丁原语的参数, pname指定要修改的参数,并且必须为GL_PATCH_VERTICES。 value为pname指定的参数指定新值。
	//当pname为GL_PATCH_VERTICES时,value指定将用于构成单个面片基元的顶点数。修补程序基元由镶嵌细分控制着色器(如果存在)消耗,随后用于镶嵌细分。
	//使用glDrawArrays或类似函数指定图元时,每个面片均由参数控制点组成,每个控制点均由从enabeld顶点数组中提取的顶点表示。
	//参数必须大于零,并且小于或等于GL_MAX_PATCH_VERTICES的值。
	glPatchParameteri(GL_PATCH_VERTICES, 16);
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	glDrawArrays(GL_PATCHES, 0, 16);
}

void window_size_callback(GLFWwindow* window, int newWidth, int newHeight)
{
	glViewport(0, 0, newWidth, newHeight);
	aspect = (float)newWidth / (float)newHeight;
	pMat = glm::perspective(toRadians(45.f), aspect, 0.1f, 1000.f);
}

int main(int argc, char** argv)
{
	int glfwState = glfwInit();
	if (glfwState == GLFW_FALSE)
	{
		cout << "GLFW initialize failed, invoke glfwInit()......Error file:" << __FILE__ << "......Error line:" << __LINE__ << endl;
		glfwTerminate();
		exit(EXIT_FAILURE);
	}

	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
	glfwWindowHint(GLFW_OPENGL_CORE_PROFILE, GLFW_OPENGL_PROFILE);
	glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);

	GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "Tessellation Bezier", nullptr, nullptr);
	if (!window)
	{
		cout << "GLFW create window failed, invoke glfwCreateWindow()......Error file:" << __FILE__ << "......Error line:" << __LINE__ << endl;
		glfwTerminate();
		exit(EXIT_FAILURE);
	}

	glfwMakeContextCurrent(window);

	

	int glewState = glewInit();
	if (glewState != GLEW_OK)
	{
		cout << "GLEW initialize failed, invoke glewInit()......Error file:" << __FILE__ << "......Error line:" << __LINE__ << endl;
		glfwTerminate();
		exit(EXIT_FAILURE);
	}

	glfwSetWindowSizeCallback(window, window_size_callback);
	glfwSetCursorPosCallback(window, mouse_move_callback);
	glfwSetScrollCallback(window, mouse_scroll_callback);
	glfwSetKeyCallback(window, key_press_callback);

	glfwSwapInterval(1);

	init(window);

	while (!glfwWindowShouldClose(window))
	{
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);

	return 0;
}



##  着色器程序
### 1.顶点着色器

```cpp
#version 460 core

out vec2 tc;

uniform mat4 mvp_matrix;

layout(binding = 0) uniform sampler2D tex_color;

void main(void)
{
	const vec4 vertices[] = 
		vec4[] (vec4(-1.0, 0.5, -1.0, 1.0),
				vec4(-0.5, 0.5, -1.0, 1.0),
				vec4( 0.5, 0.5, -1.0, 1.0),
				vec4( 1.0, 0.5, -1.0, 1.0),
				
				vec4(-1.0, 0.0, -0.5, 1.0),
				vec4(-0.5, 0.0, -0.5, 1.0),
				vec4( 0.5, 0.0, -0.5, 1.0),
				vec4( 1.0, 0.0, -0.5, 1.0),
				
				vec4(-1.0, 0.0,  0.5, 1.0),
				vec4(-0.5, 0.0,  0.5, 1.0),
				vec4( 0.5, 0.0,  0.5, 1.0),
				vec4( 1.0, 0.0,  0.5, 1.0),
				
				vec4(-1.0,-0.5,  1.0, 1.0),
				vec4(-0.5, 0.3,  1.0, 1.0),
				vec4( 0.5, 0.3,  1.0, 1.0),
				vec4( 1.0, 0.3,  1.0, 1.0));

	tc = vec2((vertices[gl_VertexID].x + 1.f) / 2.f, (vertices[gl_VertexID].z + 1.f) / 2.f);

	gl_Position = vertices[gl_VertexID];
}

2.片元着色器

#version 460 core

in  vec2 tes_out;
out vec4 color;
uniform mat4 mvp_matrix;

layout(binding = 0) uniform sampler2D tex_color;

void main(void)
{
	color = texture(tex_color, tes_out);
}

3.曲面细分控制着色器

#version 460 core

in vec2 tc[];
out vec2 tcs_out[];

uniform mat4 mvp_matrix;
layout(binding = 0) uniform sampler2D tex_color;
layout(vertices = 16) out;

void main(void)
{
	int TL = 32; // tessellation levels
	if(gl_InvocationID == 0)
	{
		gl_TessLevelOuter[0] = TL;
		gl_TessLevelOuter[1] = TL;
		gl_TessLevelOuter[2] = TL;
		gl_TessLevelOuter[3] = TL;
		gl_TessLevelInner[0] = TL;
		gl_TessLevelInner[1] = TL;
//		gl_TessLevelInner[2] = TL;  //超出了边界	
	}

	tcs_out[gl_InvocationID] = tc[gl_InvocationID];
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

4.曲面细分评估着色器

#version 460 core

layout(quads, equal_spacing, ccw) in;

uniform mat4 mvp_matrix;
layout(binding = 0) uniform sampler2D tex_color;

in vec2 tcs_out[];
out vec2 tes_out;

void main(void)
{
	vec3 p00 = (gl_in[0].gl_Position).xyz;
	vec3 p10 = (gl_in[1].gl_Position).xyz;
	vec3 p20 = (gl_in[2].gl_Position).xyz;
	vec3 p30 = (gl_in[3].gl_Position).xyz;
	vec3 p01 = (gl_in[4].gl_Position).xyz;
	vec3 p11 = (gl_in[5].gl_Position).xyz;
	vec3 p21 = (gl_in[6].gl_Position).xyz;
	vec3 p31 = (gl_in[7].gl_Position).xyz;
	vec3 p02 = (gl_in[8].gl_Position).xyz;
	vec3 p12 = (gl_in[9].gl_Position).xyz;
	vec3 p22 = (gl_in[10].gl_Position).xyz;
	vec3 p32 = (gl_in[11].gl_Position).xyz;
	vec3 p03 = (gl_in[12].gl_Position).xyz;
	vec3 p13 = (gl_in[13].gl_Position).xyz;
	vec3 p23 = (gl_in[14].gl_Position).xyz;
	vec3 p33 = (gl_in[15].gl_Position).xyz;
//	vec3 p33 = (gl_in[32].gl_Position).xyz;  //gl_in数组边界越界

//	vec3 p04 = (gl_in[16].gl_Position).xyz;

	float u = gl_TessCoord.x;
	float v = gl_TessCoord.y;

	// cubic Bezier basis functions
	float bu0 = (1.0-u)  * (1.0-u)  * (1.0-u);	//(1-u)^3
	float bu1 = 3.0 * u * (1.0-u) * (1.0-u);	//3u(1-u)^2  
	float bu2 = 3. * u * u * (1.0-u);			//3u^2(1-u)
	float bu3 = u * u * u;						//u^3
	float bv0 = (1.0-v)  * (1.0-v)  * (1.0-v);	//(1-v)^3
	float bv1 = 3.0 * v * (1.0-v) * (1.0-v);	//3v(1-v)^2  
	float bv2 = 3. * v * v * (1.0-v);			//3v^2(1-v)
	float bv3 = v * v * v;						//v^3

	// output the position of this vertex in the tessellated patch
	vec3 outputPosition =
		  bu0 * ( bv0*p00 + bv1*p01 + bv2*p02 + bv3*p03 )
		+ bu1 * ( bv0*p10 + bv1*p11 + bv2*p12 + bv3*p13 )
		+ bu2 * ( bv0*p20 + bv1*p21 + bv2*p22 + bv3*p23 )
		+ bu3 * ( bv0*p30 + bv1*p31 + bv2*p32 + bv3*p33 );
	gl_Position = mvp_matrix * vec4(outputPosition,1.0f);   // shows bezier curve
//	gl_Position = mvp_matrix * vec4(u,0,v,1);               // shows original grid (pick one)
	
	// output the interpolated texture coordinates
	vec2 tc1 = mix(tcs_out[0], tcs_out[3], gl_TessCoord.x);
	vec2 tc2 = mix(tcs_out[12], tcs_out[15], gl_TessCoord.x);
	vec2 tc = mix(tc2, tc1, gl_TessCoord.y);
	tes_out = tc;
}


运行结果

在这里插入图片描述

源码下载

源码下载地址

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-04-15 00:35:02  更:2022-04-15 00:38:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 17:09:19-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码