目录

图形渲染管线介绍

(一)顶点缓冲区

(二)VAO/VBO/IBO

(三)着色器

(四)索引缓冲区

绘制矩形完整代码 :

图形渲染管线介绍

1.图形渲染管线主要被划分成两个主要部分,第一部分是把物体的3D坐标转换成为2D坐标,第二部分是把2D坐标转变为有实际颜色的像素。一般步骤为:顶点着色器->图元装配->几何着色器->光栅化->片段着色器->Alpha测试与混合。

2.图形渲染管线高度专门化,所以大多数显卡可以并行执行,而在GPU上为每一个渲染管线阶段运行各自的小程序,这些小程序的名字就叫做着色器。而且着色器还可以用自己写的着色器(GLSL)代替默认的。


(一)顶点缓冲区

1.顶点缓冲区的概念与辨析 :基本就是去掉vertex,它只是一个内存缓冲区,一个内存字节数组,从字面上讲就是一块用来存字节的内存。

辨析:但是顶点缓冲区又和C++中像字符数组的内存缓冲区不太一样,它是OpenGL中的内存缓冲区,这意味着它实际上在显卡显存(Video RAM)上。

2.概念理解:所以这里的基本思路是我要定义一些数据来表示三角形,我要把它放入显卡VRAM中,然后还需要发出DrawCall绘制指令。实际上我们还需要告诉显卡如何读取和解释这些数据,以及如何把它放在我们的屏幕上,一旦我们发出DrawCall绘制指令,就得明确着色器的内存布局,怎么摆出来以及显卡需要怎么做,需要对显卡编程,就是着色器。着色器是一堆我们可以编写的在显卡上以一种非常特殊的方式运行的代码。

3.运行原理:“对显卡输送数据--->向显卡解释数据--->绘制目标步骤”。而OpenGL像一台状态机,我们要处理的一系列的状态而并非对象。这就是OpenGL渲染的流程。

4.代码实践:

声明:旧版OpenGL与新版OpenGL有实现上的差异。对于新版需要用到VBO和VAO绑定。

另外在生成缓冲区对象之前,需要检查glad是否可以获取地址,否则会无法生成缓冲区对象。

if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

//旧版处理方式:这种写法在glad版本的OpenGL中已经过时了,但是在glew.h下可以跑。
//(glad.h是glew.h进化版)
unsigned int buffer;
glGenBuffer(1,&buffer);
glBindBuffer(GL_VERTEX_BUFFER,buffer);
glBufferData(GL_ARRAY_BUFFER,6*sizeof(float),positions,GL_STATIC_DRAW);glEnableVertexAttribArray(0);
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*2,0);glBindBuffer(GL_ARRAY_BUFFER,0);

创建顶点缓冲区:

unsigned int buffer;
glGenBuffers(1,&buffer);//第一个参数: 指定需要生成几个已声明的缓冲区
//第二个参数: 指定返回整数的内存地址
//效果: 生成缓冲区的id,表征上看是一个整数,但是代指的是实际对象。
//当你想用这个对象时你就可以使用这个数字调用。

但是,我们要渲染三角形,就需要说明是用那个缓冲区来渲染三角形,即传递这个整数。

glBindBuffer(GL_ARRAY_BUFFER,buffer);

下一步是指定数据。一个简单的方式是在声明数据的时候直接把顶点数据填充进去

float positions[6]={-0.5f,-0.5f,0.0f ,0.5f,0.5f,-0.5f
};glBufferData(GL_ARRAY_BUFFER,6*sizeof(float),positions,GL_STATIC_DRAW);//然后我们一般需要创建索引缓冲区,没有索引缓冲区就可以调用glDrawArrays()来绘制指定图元glDrawArrays(GL_TRIANGLE,0,3);

这种选择类似于PS,选中图层,选中笔刷,而只影响该图层。OpenGL也一样,在使用它之前需要选择或绑定 必备的东西,这就是它的运行原理,它是上下文相关的状态机。

最后记得调用glEnableVertexAttribArray(),启用glVertexAttribPointer();

glEnableVertexAttribArray(0);

glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*2,0);

//效果:这两段代码告诉OpenGL缓冲区布局是什么,理论上有一个着色器就可以在屏幕上看到三角形了。


(二)VAO/VBO/IBO

1.新版本顶点缓冲区引入的几个概念

顶点数组对象 Vertex Array Object ,VAO

顶点缓冲对象 Vertex Buffer  Object ,VBO 【就相当于旧版的顶点缓冲区】

元素缓冲对象  Element    Buffer Object ,EBO/或索引缓冲对象 Index Buffer Object,IBO

阅读反思:

在LearnOpenGl的教程中,提到OpenGL的核心模式要求我们使用VAO。这句话的意思是必须。仔细阅读,甚至一个语气都可能影响到你对使用条件的把握。

     另外注意精确理解术语。比如当提到“对象"时,我们就可以参照C++里面的对象与属性的概念去理解。

     定性理解。顶点缓冲对象。顶点就是待输入的数据,对象就是一个状态的集成。这里的关键字就是”缓冲“,那么意思就是说,这是一块内存。这里指对象的内存,而并非内存的对象。

2.新旧版本剖析

//旧版处理方式:这种写法在glad版本的OpenGL中已经过时了。但是在glew.h下可以跑。(glad.h是glew.h进化版)//0.生成指定数量的缓冲区对象
unsigned int buffer;
glGenBuffer(1,&buffer);//1.复制对顶点数组到缓冲区中供OpenGL使用
glBindBuffer(GL_VERTEX_BUFFER,buffer);
glBufferData(GL_ARRAY_BUFFER,6*sizeof(float),positions,GL_STATIC_DRAW);//2.设置顶点属性指针
glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,sizeof(float)*2,0);
glEnableVertexAttribArray(0);//3.但我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);//4.绘制物体
glUseProgram(shaderProgram);//解绑:结束对象的生命周期
glBindVertexArray(0);

分析:通过旧版本对顶点数组的处理办法,我们可以观察到当我们绘制一个物体时必须要重复这一过程,但是如果有五个以上的顶点属性,上百个物体。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快变成一个麻烦事。这就是面向过程编程的劣势。

3.总结

//新版本处理方式//  初始化代码(只运行一次 (除非你的物体频繁改变))
// 1. 绑定VAO
glBindVertexArray(VAO);// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);[...]// 绘制代码(渲染循环中)
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

(1) VAO的设计思想 类似于面向对象的类的概念。我们用一个顶点数组对象去集成任意多个属性。当配置顶点属性指针时,只需要将那些调用执行一次,之后绘制物体时只需绑定相应的VA0就行了。

(2) VBO 但是想使用VAO,我们还需绑定与配置对应的VBO来说明这是那个对象的内存。VBO就像我们调用函数,会在栈上占用一块虚拟空间,(内存)会随着对象的生命周期结束后而释放(可以理解为失效)所以VBO与VAO拥有生命周期。

(3) VAO +  VBO= 一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。

4.配置顶点属性指针

//设置顶点属性指针,设置位置属性glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);  //设置颜色属性glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1);........我们还可以接下来配置纹理等属性

注释:

1.glVertexAttribPointer函数是使用来配置顶点属性的。

参数含义分别为,1顶点位置的起始位置,2几个一组,3数据类型,4是否需要标准化,5步长(以字节为单位),偏移量指针(字节为单位))

2.glEnableVertexAttribArray()用来激活对应属性的顶点着色器


(三)着色器

1.概念:着色器是一个运行在显卡上的程序代码。代码形式:以文本或字符串形式编写的。

编译方式:发送到GPU上编译链接运行。

CPU与GPU的配合:GPU处理图形的速度快的多,所以利用显卡的能力在屏幕上绘制图形。

而CPU也有擅长的部分,我们可以将结果数据发送给显卡同时仍然在CPU上处理。

2.类型对于大多数图形编程:重点放在两种着色器,顶点着色器和片段着色器。

(1)顶点着色器:他会被渲染的每个顶点调用,主要是告诉显卡我们计划这个顶点在屏幕空间的什么位置。而窗口基本上就是由像素组成的,指定的顶点就需要实际的像素填充,勾勒出了图形的轮廓。

(2)片段着色器:顶点着色器结束运行,进入片段着色器管道。片段着色器就是对需要填充的每个像素调用一次,主要决定像素是什么颜色的。相当于在勾勒的轮廓里上色。

对比:顶点着色器和片段着色器性质上是相似的,但是使用片段着色器的代价合并价值更高。

因为顶点着色器是一次性批量发送数据,但是是片段着色器是为每个像素运行,后者运行成本更大。并且,顶点着色处理输入信息,而片段着色器需要合成信息,计算的信息量不同。然而有些东西还必须按像素计算,比如光源。受光源,环境,纹理,提供表面的材质等因素影响,每个像素的颜色值都不同。而在这一些输入结束后,在片段着色器中的决定仅仅是单个像素的颜色。

3.代码实践

主要完成的任务有三个,创建着色器绑定至程序,编译着色器源码为程序,调用着色器。

如何管理shader文件?首先将不同功能的代码文件分类以便于我们管理。因此我们将shader的源码单独放在.shader文件中,其中vertex shader和fragment shader文件可以分成两个,也可以写成一个。但是如果以后比如一个顶点着色器要搭配多个片段着色器,还是会依照功能分成多个文件。

另外,shader独立成文件后,需要以读取文件的方式建立链接。

//shader 源码
#shader vertex
#version 330 corelayout(location = 0) in vec4 position;void main()
{gl_Position = position;
};#shader fragment
#version 330 corelayout(location = 0) out vec4 color;void main()
{color = vec4(1.0, 0.0, 0.0, 1.0);
};

然后我们需要用parse逐行解析shader,解析到#shader就去判断是什么类型的shader,然后去填入string stream中。

struct ShaderProgramSource
{std::string VertexSource;std::string FragmentSource;
};static ShaderProgramSource ParseShader(const std::string& filepath)
{std::ifstream stream(filepath);  //从硬盘到内存enum class ShaderType{NONE=-1,VERTEX=0,FRAGMENT=1};std::stringstream ss[2];ShaderType type=ShaderType::NONE;std::string line;while (getline(stream, line)){if (line.find("#shader") != std::string::npos){if (line.find("vertex") != std::string::npos)type = ShaderType::VERTEX;else  if (line.find("fragment") != std::string::npos)type = ShaderType::FRAGMENT;}else{ss[(int)type] << line << "\n";}}return { ss[0].str(),ss[1].str() };
}

4.以上我们完成了着色器的生成,编译,读取。接下来实现着色器程序的调用。 然后我们需要创建一个缓冲区,生成指定个数的缓冲区对象;绑定缓冲区 ;把内存中的数据拷贝到GPU的显存中;激活着色器;按照属性指定着色器的内存布局;解析shader源码;创建shader程序;激活程序。

运行结果

 完整代码


#include"glad/glad.h"
#include"GLFW/glfw3.h"
#include<iostream>
#include<fstream>
#include<sstream>struct ShaderProgramSource
{std::string VertexSource;std::string FragmentSource;
};static ShaderProgramSource ParseShader(const std::string& filepath)
{std::ifstream stream(filepath);  //从硬盘到内存enum class ShaderType{NONE=-1,VERTEX=0,FRAGMENT=1};std::stringstream ss[2];ShaderType type=ShaderType::NONE;std::string line;while (getline(stream, line)){if (line.find("#shader") != std::string::npos){if (line.find("vertex") != std::string::npos)type = ShaderType::VERTEX;else  if (line.find("fragment") != std::string::npos)type = ShaderType::FRAGMENT;}else{ss[(int)type] << line << "\n";}}return { ss[0].str(),ss[1].str() };
}
static unsigned int CompileShader(unsigned int type, const std::string& source)
{unsigned int id = glCreateShader(type);const char* src = source.c_str();glShaderSource(id, 1, &src, nullptr);glCompileShader(id);//to do error handingint result;glGetShaderiv(id, GL_COMPILE_STATUS, &result);if (result == GL_FALSE){int length;glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);char* message = (char*)alloca(length * sizeof(char));//C函数,在栈上分配glGetShaderInfoLog(id, length, &length, message);std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment")<< " shader" << std::endl;std::cout << message << std::endl;glDeleteShader(id);return 0;}return id;
}static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{unsigned int program = glCreateProgram();unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);glAttachShader(program, vs);glAttachShader(program, fs);glLinkProgram(program);glValidateProgram(program);glDeleteShader(vs);glDeleteShader(fs);return program;
}int main(void)
{GLFWwindow* window;glfwInit();if (!glfwInit()) return -1;glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);window = glfwCreateWindow(480, 480, "hello", NULL, NULL);if (!window){std::cout << "error" << std::endl;glfwTerminate();return -1;}glfwMakeContextCurrent(window);if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}float positions[6] = {-0.5f,-0.5f,0.0f, 0.5f,0.5f, -0.5f};unsigned int VBO, VAO;glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, 6* sizeof(float) , positions, GL_STATIC_DRAW);glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,8,(const void*)0);glEnableVertexAttribArray(0);ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");unsigned int shader = CreateShader(source.VertexSource,source.FragmentSource);glUseProgram(shader);while (!glfwWindowShouldClose(window)){glClear(GL_COLOR_BUFFER_BIT);glDrawArrays(GL_TRIANGLES, 0, 3);glfwSwapBuffers(window);glfwPollEvents();}glDeleteVertexArrays(1, &VAO);glDeleteBuffers(1, &VBO);glDeleteProgram(shader);glfwTerminate();return 0;
} 

VAO,VBO,IBO模型图解


(四)索引缓冲区

(1)背景

举一反三,回顾绘制一个三角形的过程,我们联想如何画一个四边形呢?一个角度上我们可以认为所有的多边形都是由三角形组成的,那么我们可以以同样的方法在不同的位置绘制若干三角形以此来拼接出我们的目标多边形。但是一旦模型复杂起来,就会有许多重复的顶点,这种办法就完全不现实。

另一个角度,按照“环节”去考虑,我们应该在顶点着色器管道就勾勒出轮廓,然后在片段着色器管道里上色。显然第一个角度不符合OpenGL的设计理念。

而顶点缓冲区的目的就是不提供冗余或重复的顶点位置。

 (2)概念:

删除任何重复的顶点,在顶点缓冲区得到了完全唯一的顶点,之后创建一个索引以便多次绘制顶点,然后我们用IBO绑定代码把索引缓冲区发送给显卡;最终我们使用glDrawElement()绘制图形。

 (3)代码实现:

 unsigned int IBO;glGenBuffers(1, &IBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indicesGL_STATIC_DRAW);//rendering glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,nullptr);


绘制矩形完整代码 :

//main.cpp
#include"glad/glad.h"
#include"GLFW/glfw3.h"
#include<iostream>
#include<fstream>
#include<sstream>struct ShaderProgramSource
{std::string VertexSource;std::string FragmentSource;
};static ShaderProgramSource ParseShader(const std::string& filepath)
{std::ifstream stream(filepath);  //从硬盘到内存enum class ShaderType{NONE=-1,VERTEX=0,FRAGMENT=1};std::stringstream ss[2];ShaderType type=ShaderType::NONE;std::string line;while (getline(stream, line)){if (line.find("#shader") != std::string::npos){if (line.find("vertex") != std::string::npos)type = ShaderType::VERTEX;else  if (line.find("fragment") != std::string::npos)type = ShaderType::FRAGMENT;}else{ss[(int)type] << line << "\n";}}return { ss[0].str(),ss[1].str() };
}
static unsigned int CompileShader(unsigned int type, const std::string& source)
{unsigned int id = glCreateShader(type);const char* src = source.c_str();glShaderSource(id, 1, &src, nullptr);glCompileShader(id);//to do error handingint result;glGetShaderiv(id, GL_COMPILE_STATUS, &result);if (result == GL_FALSE){int length;glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);char* message = (char*)alloca(length * sizeof(char));//C函数,在栈上分配glGetShaderInfoLog(id, length, &length, message);std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment")<< " shader" << std::endl;std::cout << message << std::endl;glDeleteShader(id);return 0;}return id;
}static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{unsigned int program = glCreateProgram();unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);glAttachShader(program, vs);glAttachShader(program, fs);glLinkProgram(program);glValidateProgram(program);glDeleteShader(vs);glDeleteShader(fs);return program;
}int main(void)
{GLFWwindow* window;glfwInit();if (!glfwInit()) return -1;glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);window = glfwCreateWindow(480, 480, "hello", NULL, NULL);if (!window){std::cout << "error" << std::endl;glfwTerminate();return -1;}glfwMakeContextCurrent(window);if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}//顶点属性float positions[] = {-0.5f,-0.5f,0.5, -0.5f,0.5f, 0.5f,-0.5f,0.5f};unsigned int indices[] ={0,1,2,2,3,0};unsigned int VAO,VBO;glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, 6 * 2 * sizeof(float), positions, GL_STATIC_DRAW);glEnableVertexAttribArray(0);glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,8,(const void*)0);unsigned int IBO;glGenBuffers(1, &IBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");unsigned int shader = CreateShader(source.VertexSource,source.FragmentSource);glUseProgram(shader);while (!glfwWindowShouldClose(window)){glClear(GL_COLOR_BUFFER_BIT);glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//线框模式glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);glfwSwapBuffers(window);glfwPollEvents();}glDeleteProgram(shader);glfwTerminate();return 0;
} 

顶点缓冲区与着色器 (The Cherno + LeranOpenGL)笔记相关推荐

  1. 【Unity Shader】学习顶点/片元着色器

    上一篇博客重点放在了Unity Shader的基本结构,分别介绍了它包含的三个语义块,最后简单介绍了Unity Shader的形式:表面着色器.顶点/片元着色器和固定函数着色器. 趁热打铁,今天接着上 ...

  2. 第四章:缓冲区、着色器、GLSL

    原文链接: http://www.rastertek.com/gl40tut04.html Tutorial 4: Buffers, Shaders, and GLSL 第四章:缓冲区.着色器.GLS ...

  3. UnityShader入门精要笔记1——顶点/片元着色器结构与BRDF(基本光照模型)——实现漫反射

    文章目录 BRDF(基本光照模型) 实现漫反射 光线强度的计算 好现在开始写Shader 新建Shader 添加一个Properties语义块 添加SubShader和Pass. 使用CG/HLSL语 ...

  4. Vertex and FragmentShader顶点与片段着色器

    一.顶点与片段着色器简介 Vertex and FragmentShader:最强大的Shader类型,也是本系列的重点,下文中简称V&FShader,属于可编程渲染管线.使用的是CG/HLS ...

  5. 缓冲区、着色器和HLSL

    教程 4:缓冲区.着色器和 HLSL 本教程将介绍在 DirectX 11 中编写顶点和像素着色器.还将介绍在 DirectX 11 中使用顶点和索引缓冲区.这些是您需要理解和利用来渲染 3D 图形的 ...

  6. UnityShader6:最简单的顶点/片元着色器

    一.顶点/片元着色器基本结构 直接上代码: 这个着色器可以得到蓝色的纯色输出,如果顶点着色器得出了错误的裁剪空间坐标,那么会出现很明显表现错误 Shader "Jaihk662/NewSur ...

  7. Unity 3D 图形学 Shader之顶点与片段着色器(五)

    通过实现一个只有颜色属性可调节的简单材质效果更好的了解顶点与片段着色器 一.顶点着色器 顶点着色器就是处理顶点的着色器,每个顶点都会执行一次顶点着色器.我们先认识下顶点函数的结构: 顶点着色器函数的名 ...

  8. 初识顶点/片元着色器

    5.2 一个最简单的顶点/片元着色器 5.2.1 顶点/片元着色器的基本结构 一个 Unity Shader 的基本结构.它包含了 Shader.Properties.SubShader.Fallba ...

  9. 顶点与片段着色器的例子

    视窗坐标 Shader "Custom/WindowCoordinates/Base" {SubShader {Pass {CGPROGRAM#pragma vertex vert ...

最新文章

  1. linux和aix设置时间
  2. springboot 实现微信小程序授权并解密手机号
  3. java截取字符串拼接_java截取字符串并拼接
  4. 学习进度条——第八周
  5. 一天学一个模式_第一天:策略模式
  6. (47)fs创建多级目录
  7. php 付款,php – 接受付款最佳做法
  8. [转载]WSDL 教程
  9. 检验杜宾 瓦森检验法R语言_2018年9-11月高级计量经济学主要授课内容概要
  10. 【DM】DMHS的安装部署及DM7的二节点同步
  11. v65智慧屏是真的鸿蒙,华为智慧屏V65深测:真智慧or增智慧?
  12. ModelAndView使用
  13. 解决办法:My Endnote Library.enl constrains an incorrect path
  14. git 提交代码报错,提示邮箱格式不正确
  15. 国产DSP,32位双核CPU,pin2pin替代TMS320F280049C,高频400MHz
  16. 如何区分电脑上的硬件和软件?
  17. 关闭占用端口号的程序
  18. I-SOON CTF
  19. 勇敢者游戏(捡石子问题)
  20. Android9--android 10.0 去掉未知来源弹窗 默认授予安装未知来源权限

热门文章

  1. Ubuntu下bazel卸载与安装
  2. P4745 B’s problem(b)
  3. 联系书商出译著的流程
  4. Java好学吗?现在待遇如何?
  5. MySQL增加新的分区
  6. PyQt5端口映射TCP/UDP工具
  7. 从零开始安装搭建win10与ubuntu20.04双系统开发环境——集安装、配置、软件、美化、常见问题等于一体的——超详细教程
  8. B站黑马程序员Oracle学习——数据的增删改
  9. 使用useState因异步导致数据不能及时更新的问题
  10. 建筑八大员培训湖北劳务员培训施工现场劳务人员多元化管理