本文为学习LearnOpenGL的学习笔记,如有书写和理解错误还请大佬扶正;

教程链接:

着色器 - LearnOpenGL CN​learnopengl-cn.github.io

一,基础概念

1,着色器

  • 着色器(Shader)是运行在GPU上的小程序;
  • 这些小程序为图形渲染管线的某个特定部分而运行;
  • 从基本意义上来说,着色器只是一种把输入转化为输出的程序;
  • 着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出;
  • 此教程的着色器是使用一种叫GLSL的类C语言写成的;

2,GLSL

  • GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性;
  • 着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数;
  • 每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中;

3,Uniform

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同;

  • 首先,uniform是全局的(Global)。味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问;
  • 第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新;

如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!

设置着色器中Uniform 的值

  1. 第一要查询设置值的位置;glGetUniformLocation(ID,"name");
  2. 第二设置值,根据设置值类型不同所调用函数也不同;glUniform();
int Location = glGetUniformLocation(shaderProgramID, "name");
glUniform4f(Location, value, value, value, value);

查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram);

4,GLSL数据类型

  • GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool;
  • GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix);

向量类型

  1. vecn 包含n个float分量的默认向量;
  2. bvecn 包含n个bool分量的向量;
  3. ivecn 包含n个int分量的向量;
  4. uvecn 包含n个unsigned int分量的向量;
  5. dvecn 包含n个double分量的向量;

5,GLSL特性,重组(Swizzling)

  • 一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量;
  • 你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4个分量;
  • GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量;
  • 例如:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

6,输入与输出

  • 着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递;
  • GLSL定义了in和out关键字专门来实现这个目的;
  • 每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去;

例如(伪代码)

#version version_number     //声明版本号
in type in_variable_name;   // 输入变量
in type in_variable_name;out type out_variable_name; //输出变量uniform type uniform_name;  //uniform变量int main()
{// 处理输入并进行一些图形操作...// 输出处理过的结果到输出变量out_variable_name = weird_stuff_we_processed;
}

二,创建着色器理论部分

1,顶点着色器

  • 顶点着色器的输入特殊在,它从顶点数据中直接接收输入;
  • 为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性;
  • 顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据,layout (location = 0);(每个顶点属性都会配置一个location[0,1,2..]这样才能再取顶点属性时有明确的位置)
  • 也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用;
//查询属性位置值(Location)
glGetAttribLocation();

  • 顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute);
  • 我们能声明的顶点属性是有上限的,它一般由硬件来决定;
  • OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限;
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

关于更多顶点属性声明与使用

//顶点数据
float vertices[] = {// 位置              // 颜色0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下-0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
};
....
//顶点着色器声明
#version 330 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
....

因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:

顶点位置属性依然为0 但是步长(两个顶点属性之间间隔)变为6,数据缓冲中起始位置的偏移量仍为0(因为在最前面);

顶点颜色位置属性为1,步长也为6,数据缓冲起始位置的偏移为3(他前面还有三个数值);

//属性指针应该更改为
// 位置属性
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);

2,片段着色器

  • 它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色;
  • 如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色);

3,链接着色器

  • 如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入;
  • 当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的);

最后代码类似这样:

//顶点
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0out vec4 vertexColor; // 为片段着色器指定一个颜色输出void main()
{gl_Position = vec4(aPos, 1.0); vertexColor = vec4(0.5, 0.0, 0.0, 1.0);
}
//片源
#version 330 core
out vec4 FragColor;in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)void main()
{FragColor = vertexColor;
}

三,创建着色器理实践部分

文件框架

1,封装Shader代码,主要封装顶点与片源编译以及链接着色器部分

文件ShaderBase代码:

与上一篇的编译链接着色器部分一样只不过单独封装成一个文件,方便使用;

#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h>   //包含glad来获取所用必须的OpenGL文件
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>class Shader
{
public://程序IDunsigned int ID;  // 构造函数 构建着色器Shader(const char* vertexPath, const char* fragmentPath){// 从文件路径中获取顶点/片源着色器std::string vertexCode;std::string fragmentCode;std::ifstream vShaderFile;std::ifstream fShaderFile;// 确保ifstream文件可以抛出异常vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);try{// 打开文件vShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// 读取文件的内容到数据缓冲流中vShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf();// 读取成功,关闭文件vShaderFile.close();fShaderFile.close();// 转换数据流内容为字符串vertexCode = vShaderStream.str();fragmentCode = fShaderStream.str();}catch (std::ifstream::failure e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;}const char* vShaderCode = vertexCode.c_str();const char * fShaderCode = fragmentCode.c_str();//编译着色器unsigned int vertex, fragment;// 顶点着色器//创建顶点着色器vertex = glCreateShader(GL_VERTEX_SHADER);//着色器源码附加到对象上,然后编译glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);//检查是否编译成功checkCompileErrors(vertex, "VERTEX");// 片源着色器fragment = glCreateShader(GL_FRAGMENT_SHADER);//着色器源码附加到对象上,然后编译glShaderSource(fragment, 1, &fShaderCode, NULL);glCompileShader(fragment);//检查是否编译成功checkCompileErrors(fragment, "FRAGMENT");//链接Shader,链接顶点与片源//创建链接对象ID = glCreateProgram();//着色器附加到了程序上,然后用glLinkProgram链接glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);//检查是否链接出错checkCompileErrors(ID, "PROGRAM");//把着色器对象链接到程序对象以后,删除着色器对象,不再需要它们glDeleteShader(vertex);glDeleteShader(fragment);}//激活链接程序,激活着色器,开始渲染void use(){glUseProgram(ID);}//设置Uniform变量的值//查询uniform地址不要求你之前使用过着色器程序//但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。void setBool(const std::string &name, bool value) const{glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);}// ------------------------------------------------------------------------void setInt(const std::string &name, int value) const{glUniform1i(glGetUniformLocation(ID, name.c_str()), value);}// ------------------------------------------------------------------------void setFloat(const std::string &name, float value) const{glUniform1f(glGetUniformLocation(ID, name.c_str()), value);}void setVec4(const std::string &name, float value01,float value02,float value03,float value04) const{glUniform4f(glGetUniformLocation(ID, name.c_str()), value01,value02,value03,value04);}private:// 检差是否出错 判断是顶点/片源 还是链接程序void checkCompileErrors(unsigned int shader, std::string type){int success;char infoLog[1024];if (type != "PROGRAM"){glGetShaderiv(shader, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(shader, 1024, NULL, infoLog);std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "n" << infoLog << "n -- --------------------------------------------------- -- " << std::endl;}}else{glGetProgramiv(shader, GL_LINK_STATUS, &success);if (!success){glGetProgramInfoLog(shader, 1024, NULL, infoLog);std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "n" << infoLog << "n -- --------------------------------------------------- -- " << std::endl;}}}
};
#endif

效果预览

更改渲染逻辑(LearnOpenGL.cpp文件)

s//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD。
#include <glad/glad.h>
#include <GLFW/glfw3.h>#include "ShaderBase.h"
#include <iostream>
using namespace std;/*******************************************定义常量************************************************///设置窗口的宽和高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;/*******************************************函数************************************************///响应键盘输入事件
void processInput(GLFWwindow* window)
{//glfwGetKey()用来判断一个键是否按下。第一个参数是GLFW窗口句柄,第二个参数是一个GLFW常量,代表一个键。//GLFW_KEY_ESCAPE表示Esc键。如果Esc键按下了,glfwGetKey将返回GLFW_PRESS(值为1),否则返回GLFW_RELEASE(值为0)。if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS){//glfwSetWindowShouldClose()函数,为窗口设置关闭标志。第一个参数是窗口句柄,第二个参数表示是否关闭//这里为GLFW_TRUE,表示关闭该窗口。//注意,这时窗口不会立即被关闭,但是glfwWindowShouldClose()将返回GLFW_TRUE,到了glfwTerminate()就会关闭窗口。glfwSetWindowShouldClose(window, true);}
}//当用户改变窗口的大小的时候,视口也应该被调整。
//对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{//OpenGL渲染窗口的尺寸大小//glViewport函数前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)glViewport(0, 0, width, height);
}/*******************************************主函数************************************************///主函数
int main()
{//初始化GLFWglfwInit();//声明版本与核心glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); //主版本号glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); //次版本号glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//创建窗口并设置其大小,名称,与检测是否创建成功GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", nullptr, nullptr);if (window == nullptr){cout << "Failed to create GLFW window" << endl;glfwTerminate();return -1;}//创建完毕之后,需要让当前窗口的环境在当前线程上成为当前环境,就是接下来的画图都会画在我们刚刚创建的窗口上glfwMakeContextCurrent(window);//告诉GLFW我们希望每当窗口调整大小的时候调用这个函数glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//glad寻找opengl的函数地址,调用opengl的函数前需要初始化gladif (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}/*******************************************着色器************************************************///构建和编译ShaderShader ourShader01("vs01.vs", "fs01.fs");Shader ourShader02("vs02.vs", "fs02.fs");/*******************************************顶点数据************************************************///设置顶点数据和顶点属性//第一个三角形数据float vertices01[] = {// 位置              顶点颜色0.1f, 0.9f, 0.0f,    0.0f,1.0f,0.0f,0.9f, 0.9f, 0.0f,    1.0f,1.0f,0.0f,0.9f, 0.1f, 0.0f,    1.0f,0.0f,0.0f,0.1f, 0.1f, 0.0f,    0.0f,0.0f,0.0f  };//索引unsigned int indices[] = {0, 1, 3,1, 2, 3};// 第二个三角形数据float vertices02[] = {-0.5f, 0.9f, 0.0f,-0.1f, 0.1f, 0.0f,-0.9f, 0.1f, 0.0f,};/*******************************************VAO/VBO/EBO************************************************///创建 VBO 顶点缓冲对象 VAO顶点数组对象 EBO索引缓冲对象unsigned int VBOs[2], VAOs[2], EBO;glGenVertexArrays(2, VAOs);glGenBuffers(2, VBOs);glGenBuffers(1, &EBO);//绑定VAO,VBO与EBO对象/*******************************************第一个************************************************/glBindVertexArray(VAOs[0]);glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);// 复制顶点数据到缓冲内存中glBufferData(GL_ARRAY_BUFFER, sizeof(vertices01), vertices01, GL_STATIC_DRAW);// 赋值顶点索引到缓冲内存中glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);//链接顶点属性,设置顶点属性指针//由于多了顶点颜色属性,偏移为6*sizeof(float)量glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);//顶点颜色//属性位置值为1的顶点属性//顶点颜色属性在位置之后 起始位置向后偏移 (void*)(3* sizeof(float))大小glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));//以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。glEnableVertexAttribArray(1);/*******************************************第二个************************************************///绑定VAO,VBO与EBO对象glBindVertexArray(VAOs[1]);glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]);//复制顶点数据到缓冲内存中glBufferData(GL_ARRAY_BUFFER, sizeof(vertices02), vertices02, GL_STATIC_DRAW);//链接顶点属性,设置顶点属性指针//顶点位置glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);//以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。glEnableVertexAttribArray(0);/*******************************************渲染循环************************************************///程序可以一直运行,直到用户关闭窗口。这样我们就需要创建一个循环,叫做游戏循环//glfwWindowShouldClose()检查窗口是否需要关闭。如果是,游戏循环就结束了,接下来我们将会清理资源,结束程序while (!glfwWindowShouldClose(window)){//响应键盘输入processInput(window);//设置清除颜色glClearColor(0.2f, 0.3f, 0.3f, 1.0f);//清除当前窗口,把颜色设置为清除颜色glClear(GL_COLOR_BUFFER_BIT);/*******************************************绘制************************************************///获取时间float timeValue = glfwGetTime();float greenValue = sin(timeValue) / 2.0f + 0.5f;//激活链接程序,激活着色器,开始渲染//绘制第一个四边形ourShader01.use();ourShader01.setFloat("YOffset", greenValue);//绑定VAOglBindVertexArray(VAOs[0]);//绘制四边形glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//绘制第二个ourShader02.use();ourShader02.setVec4("outColor", 0.0f,greenValue,0.0f,1.0f);glBindVertexArray(VAOs[1]);glDrawArrays(GL_TRIANGLES, 0, 3);/*******************************************结束************************************************///交换颜色缓冲 glfwSwapBuffers(window);//处理事件glfwPollEvents();}//解除绑定glDeleteVertexArrays(2, VAOs);glDeleteBuffers(2, VBOs);glDeleteBuffers(1, &EBO);//释放前面所申请的资源glfwTerminate();return 0;
}

vs01.vs代码为

#version 330 core    //版本号
layout (location = 0) in vec3 aPos; //顶点位置 位置0
layout (location = 1) in vec3 aColor; //顶点颜色 位置1out vec3 ourColor; //输出颜色
//out vec3 ourPosition;
uniform float YOffset; //声明一个偏移量void main()
{//偏移量改变顶点位置gl_Position = vec4(aPos.x, aPos.y -YOffset, aPos.z, 1.0);ourColor = aColor; //为输出颜色赋值 //ourPosition = aPos;}

fs01.fs代码为

#version 330 core   //版本号
out vec4 FragColor; //输出像素颜色in vec3 ourColor; //输入颜色 为上一步输出
//in vec3 ourPosition;void main()
{FragColor = vec4(ourColor, 1.0f); //输出  //FragColor = vec4(ourPosition, 1.0f);}

vs02.vs代码为

#version 330 core              //版本号
layout (location = 0) in vec3 aPos; //输入位置out vec3 ourPosition; //输出位置void main()
{gl_Position = vec4(aPos, 1.0);ourPosition = aPos; //为输出位置赋值
}

fs02.fs代码为

#version 330 core    //版本号
out vec4 FragColor; //输出in vec3 ourPosition; //输入位置  为上一步输出
uniform vec4 outColor; //声明一个颜色变量void main()
{FragColor = outColor + vec4(ourPosition, 1.0f); //赋值
}

opengl 设置每个点的颜色_OpenGL学习笔记(四)着色器相关推荐

  1. OpenGL超级宝典学习笔记:着色器存储区块、原子内存操作、内存屏障

    前言 本篇在讲什么 本篇为蓝宝书学习笔记 着色器存储区块 原子内存操作 内存屏障 本篇适合什么 适合初学Open的小白 本篇需要什么 对 C++语法有简单认知 对 OpenGL有简单认知 最好是有 O ...

  2. LearnOpenGL学习笔记——几何着色器

    几何着色器 在顶点和片段着色器之间有一个可选的几何着色器(Geometry Shader),几何着色器的输入是一个图元(如点或三角形)的一组顶点.几何着色器可以在顶点发送到下一着色器阶段之前对它们随意 ...

  3. 《深入理解java虚拟机》学习笔记四/垃圾收集器GC学习/一

    Grabage Collection      GC GC要完毕的三件事情: 哪些内存须要回收? 什么时候回收? 怎样回收? 内存运行时区域的各个部分中: 程序计数器.虚拟机栈.本地方法栈这3个区域随 ...

  4. dx12 龙书第十二章学习笔记 -- 几何着色器

    如果不启用曲面细分(tessellation)这一环节,那么几何着色器(geometry shader)这个可选阶段便会位于顶点着色器与像素着色器之间.顶点着色器以顶点作为输入数据,而几何着色器的输入 ...

  5. mysql root密码忘记2018_MySQL数据库之2018-03-28设置及修改mysql用户密码学习笔记

    本文主要向大家介绍了MySQL数据库之2018-03-28设置及修改mysql用户密码学习笔记 ,通过具体的内容向大家展现,希望对大家学习MySQL数据库有所帮助. 退出mysql方法 quit或者e ...

  6. IOS学习笔记(四)之UITextField和UITextView控件学习

    IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...

  7. JavaScript学习笔记(四)(DOM)

    JavaScript学习笔记(四) DOM 一.DOM概述 二.元素对象 2.1 获取方式 (1).通过ID获取一个元素对象,如果没有返回null (2).通过`标签名`获取一组元素对象,,如果没有返 ...

  8. JavaScript-WebGL2学习笔记四-蒙板

    stencil test(蒙板) demo的显示效果 这个例子由四个源文件构成 webgl.html <html> <head><!--Title: JavaScript ...

  9. 吴恩达《机器学习》学习笔记四——单变量线性回归(梯度下降法)代码

    吴恩达<机器学习>学习笔记四--单变量线性回归(梯度下降法)代码 一.问题介绍 二.解决过程及代码讲解 三.函数解释 1. pandas.read_csv()函数 2. DataFrame ...

最新文章

  1. Zend Studio 修改高亮变量的颜色、括号颜色
  2. win10系统下cmd输入一下安装的软件命令提示拒绝访问解决办法
  3. 计算机网络技术及应用 课程 英语,计算机网络应用—现代英语课堂中的第三种语言...
  4. 海外区域财务共享中心建设
  5. 揭秘ASP.NET 2.0的Eval方法
  6. 无法启动QPCore Service
  7. 图像融合(一)--概述
  8. SLAM会议笔记(一)LOAM
  9. springboot框架搭建
  10. python迅雷下载器_简单的迅雷VIP账号获取器(Python)
  11. 网络存储磁带库术语解释
  12. PyTorch搭建LSTM实现多变量多步长时间序列预测(四):多模型滚动预测
  13. 构建知识体系(3):建立体系6个步骤
  14. zbrush常用笔刷_教您在ZBrush中制作笔刷
  15. jupyter notebook误删怎么办
  16. UNIAPP实战项目笔记43 购物车页面修改收货地址和修改默认地址
  17. [Halcon] WriteImage保存图像崩溃问题
  18. 智云通CRM:销售的黄金法则,尊重客户的意见
  19. NSTextField限制输入框只能输入英文字母数字字符,不能输入中文
  20. [Shell]尚硅谷大数据技术之Shell--笔记(3)

热门文章

  1. iOS快速上手应用内购(IAP)附Demo
  2. ASIFormDataRequest实现post的代码示例
  3. C++ QT中的QSound使用方法
  4. ubuntu vim中文乱码问题
  5. SQL Server 数据岸问题
  6. 7、Altiris cms 7.0 软件管理 下
  7. 网络营销再掀波澜,微博独领风骚
  8. 7打开pycharm_Python+pycharm安装、关联教程
  9. 纯html css博客,纯HTML+CSS打造动画
  10. 应用市场自然量预估_VIVO市场ASO实战详解