当前位置: 首页 > news >正文

OpenGL Chan视频学习-9 Index Buffers inOpenGL

bilibili视频链接:

【最好的OpenGL教程之一】https://www.bilibili.com/video/BV1MJ411u7Bc?p=5&vd_source=44b77bde056381262ee55e448b9b1973

函数网站:

docs.gl

说明:

1.之后就不再单独整理网站具体函数了,网站直接翻译会更直观也会有更多注意点。直接通过csdn索引查找反而会慢。

2.代码区域会单独注释功能参数返回值和相关注意事项。

3.课程学习从4-本节,如果有些函数没有注释可以看专栏里面的前面发表的文章,一般有解释。

4.如果觉得代码注释白色字体不太直观可以直接copy到相应软件看。

5.有两种版本的可供查看:注释全面的和注释简洁版的,可以在索引里面找到相关代码查看。

6.希望能帮到你。

7.有错误请跟我说明一下,可能整理的时候没有检查好。

一、知识点整理

1.1 画方形

1.1.1方法一

1.1.1.1代码
//准备数据
float position[] = {0.5f, 0.5f,-0.5f, -0.5f,0.5f, -0.5f,-0.5f,0.5f,-0.5f,-0.5f,0.5f,-0.5f};//定义缓冲区对象
unsigned int buffer;
//功能:生成缓冲区对象,并将数据写入缓冲区
glGenBuffers(1, &buffer);
//功能:将缓冲区对象绑定到目标
glBindBuffer(GL_ARRAY_BUFFER, buffer);
//功能:将数据写入缓冲区
glBufferData(GL_ARRAY_BUFFER, 2 * 6 * sizeof(float), position, GL_STATIC_DRAW);//功能:配置顶点属性指针
glEnableVertexAttribArray(0);
//功能:指定顶点属性数组的索引、大小、数据类型、是否归一化、偏移量、数据指针
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
1.1.1.2运行结果

1.1.1.3解释

在float数组里面直接添加组成绘制方形的两个三角形所需的六个顶点。

1.1.1.4缺点

顶点冗余

1.1.2方法二——索引缓冲

1.1.1.1代码
//准备数据
float position[] = {-0.5f, -0.5f,0.5f, -0.5f,0.5f,0.5f,-0.5f,0.5f,
};//定义顶点索引缓存,用于标定顶点顺序
unsigned int indices[] = {0,1,2,2,3,0
};//定义缓冲区对象
unsigned int buffer;
//功能:生成缓冲区对象,并将数据写入缓冲区
glGenBuffers(1, &buffer);
//功能:将缓冲区对象绑定到目标
glBindBuffer(GL_ARRAY_BUFFER, buffer);
//功能:将数据写入缓冲区
glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(float), position, GL_STATIC_DRAW);//功能:配置顶点属性指针
glEnableVertexAttribArray(0);
//功能:指定顶点属性数组的索引、大小、数据类型、是否归一化、偏移量、数据指针
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);//索引缓冲区对象
unsigned int ibo;
//功能:生成缓冲区对象,并将数据写入缓冲区
glGenBuffers(1, &ibo);
//功能:将缓冲区对象绑定到目标.没有绑定为数组缓冲区
//参数:1.GL_ELEMENT_ARRAY_BUFFER:指定目标为索引缓冲区对象
//2.ibo:索引缓冲区对象ID
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
//功能:将数据写入缓冲区
//参数:1.GL_ELEMENT_ARRAY_BUFFER:指定目标为索引缓冲区对象
//2.6*sizeof(unsigned int):索引数据大小
//3.indices:索引数据指针
//4.GL_STATIC_DRAW:指定数据不会改变
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);

while绘制时改动

//glDrawArrays(GL_TRIANGLES, 0, 6);
//功能:绘制三角形
//参数:1.GL_TRIANGLES:绘制三角形
//2.6:顶点数量
//3.GL_UNSIGNED_INT:索引数据类型
//4.nullptr:索引数据指针
//因为索引缓冲区已经绑定到GL_ELEMENT_ARRAY_BUFFER目标,所以这里不需要再传入索引数据指针
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
1.1.1.2运行结果

如果不用unsigned,如:

glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr);

会绘制错误

1.1.1.3解释

利用索引缓冲。避免方法一的重复撰写,减少内存占用(在实际应用中顶点还包含了颜色,法线等信息)。顶点数从6->4。

QS:position的坐标点怎么和indices联系起来的

1. 顶点数据 (position 数组)

float position[] = {-0.5f, -0.5f,  // 顶点 00.5f, -0.5f,   // 顶点 10.5f, 0.5f,    // 顶点 2-0.5f, 0.5f    // 顶点 3
};
  • position数组存储了四个顶点的坐标。
  • 每个顶点由两个浮点数表示,分别是x和y坐标。
  • 顶点的顺序是:左下、右下、右上、左上。

2. 索引数据 (indices 数组)

unsigned int indices[] = {0, 1, 2,  // 三角形 1: 左下 -> 右下 -> 右上2, 3, 0   // 三角形 2: 右上 -> 左上 -> 左下
};
  • indices数组存储了绘制两个三角形所需的顶点索引。
  • 索引值对应position数组中顶点的顺序。
  • 例如:
    • 第一个三角形的顶点索引是 0, 1, 2,这意味着使用 position 数组中的第0个、第1个和第2个顶点来绘制一个三角形。
    • 第二个三角形的顶点索引是 2, 3, 0,这意味着使用 position 数组中的第2个、第3个和第0个顶点来绘制另一个三角形。

3. 顶点缓冲区对象 (buffer)

unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(float), position, GL_STATIC_DRAW);
  • 生成一个顶点缓冲区对象(VBO),用于存储顶点数据。
  • 将顶点数据绑定到GL_ARRAY_BUFFER,这是一个用于存储顶点属性(如位置、颜色等)的缓冲区目标。
  • 使用glBufferData将顶点数据传输到GPU的缓冲区中。

4. 索引缓冲区对象 (ibo)

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);
  • 生成一个索引缓冲区对象(IBO),用于存储索引数据。
  • 将索引数据绑定到GL_ELEMENT_ARRAY_BUFFER,这是一个用于存储顶点索引的缓冲区目标。
  • 使用glBufferData将索引数据传输到GPU的缓冲区中。

如何联系起来

  • 顶点缓冲区对象 (buffer) 存储了顶点的位置数据。
  • 索引缓冲区对象 (ibo) 存储了顶点的索引数据,这些索引数据告诉OpenGL如何使用顶点缓冲区中的顶点来绘制几何图形。

main函数中,使用glDrawElements函数来绘制几何图形:

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
  • GL_TRIANGLES:指定绘制的图形类型为三角形。
  • 6:指定索引的数量(即indices数组的长度)。
  • GL_UNSIGNED_INT:指定索引数据的类型为无符号整数。
  • nullptr:指定索引数据在缓冲区中的偏移量为0。因为索引缓冲区已经绑定到GL_ELEMENT_ARRAY_BUFFER,OpenGL会自动从该缓冲区中读取索引数据。

详细联系过程

  1. 顶点数据绑定

    • position数组中存储了四个顶点的坐标。
    • 使用glVertexAttribPointer配置顶点属性指针:
      glEnableVertexAttribArray(0);
      glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);
      
    • 这里,0表示顶点属性的索引,2表示每个顶点有两个分量(x和y坐标),GL_FLOAT表示数据类型为浮点数。
  2. 索引数据绑定

    • indices数组中存储了绘制两个三角形所需的顶点索引。
    • 使用glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)将索引数据绑定到GL_ELEMENT_ARRAY_BUFFER
  3. 绘制过程

    • 当调用glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr)时,OpenGL会按照indices数组中的索引顺序从position数组中获取顶点数据。
    • 例如:
      • 第一个三角形的索引是 0, 1, 2,OpenGL会获取position数组中的第0个、第1个和第2个顶点来绘制第一个三角形。
      • 第二个三角形的索引是 2, 3, 0,OpenGL会获取position数组中的第2个、第3个和第0个顶点来绘制第二个三角形。

总结

  • 顶点缓冲区对象 (buffer) 和 索引缓冲区对象 (ibo) 分别存储顶点数据和索引数据。
  • glDrawElements 函数使用索引数据来决定如何从顶点缓冲区中提取顶点数据来绘制几何图形。
  • 这种方法可以减少顶点数据的冗余存储,提高内存效率。

二、完整代码

2.1完全注释代码

#include <GL/glew.h>
#include <GLFW/glfw3.h>#include<iostream>
#include<fstream>
#include<string>
#include<sstream>//功能:定义ShaderProgramSource结构体,用于存储着色器代码
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::string line;//该变量用于存储着色器类型std::stringstream ss[2];//该变量是当前着色器类型ShaderType type = ShaderType::NONE;//功能:读取文件中的每一行内容,直到文件结束while (getline(stream, line)){//如果当前行包含#shader,则说明接下来是着色器代码if (line.find("#shader") != std::string::npos){//如果当前行包含vertex,则说明接下来是顶点着色器代码if (line.find("vertex") != std::string::npos){type = ShaderType::VERTEX;}else if (line.find("fragment") != std::string::npos){type = ShaderType::FRAGMENT;}}else{//否则,将当前行添加到对应着色器代码的stringstream中ss[(int)type] << line << '\n';}}//返回ShaderProgramSource结构体return { ss[0].str(), ss[1].str() };
}//功能:编译着色器代码
static unsigned int CompilesShader(unsigned int type, const std::string& source)
{//功能:创建着色器对象unsigned int id = glCreateShader(type);//功能:设置着色器源代码.const char* src = source.c_str();//功能:替换着色器对象中的源代码。将该id的指定着色器的源代码设置为src指针指向的字符串glShaderSource(id, 1, &src, nullptr);//功能:编译着色器对象的源代码glCompileShader(id);//设置返回着色器的对象IDint 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));//功能:获取编译错误信息glGetShaderInfoLog(id, length, &length, message);std::cout << "Failed to compile shader!" << (type == GL_VERTEX_SHADER? "Vertex" : "Fragment") << "shader!" << std::endl;std::cout << message << std::endl;//删除着色器对象glDeleteShader(id);return 0;}//TODO:错误处理ingreturn id;
}//功能:创建着色器程序
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{//创建程序对象unsigned int program = glCreateProgram();//编译顶点着色器对象unsigned int vs = CompilesShader(GL_VERTEX_SHADER, vertexShader);//编译片段着色器对象unsigned int fs = CompilesShader(GL_FRAGMENT_SHADER, fragmentShader);//功能:将编译好的着色器对象附加到程序对象中glAttachShader(program, vs);glAttachShader(program, fs);//功能:链接程序对象glLinkProgram(program);//功能:验证着色器程序对象是否可以在当前OpenGL状态中执行。检查着色器程序的完整性和可执行性。glValidateProgram(program);//删除着色器对象,因为它们已经被链接到程序对象中glDeleteShader(vs);glDeleteShader(fs);//返回着色器程序return program;
}int main(void)
{GLFWwindow* window;//初始化glfwif (!glfwInit())return -1;//创建一个窗口模式的窗口并设置OpenGL上下文window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);if (!window)//如果窗口创建失败,则终止程序{glfwTerminate();//释放glfw资源return -1;}//设置当前窗口的上下文,之后所有的OpenGL调用都会在这个上下文中进行glfwMakeContextCurrent(window);//初始化GLEWif (glewInit() != GLEW_OK)std::cout << "Error!" << std::endl;//打印OpenGL版本信息std::cout << glGetString(GL_VERSION) << std::endl;//准备数据float position[] = {-0.5f, -0.5f,0.5f, -0.5f,0.5f,0.5f,-0.5f,0.5f,};//定义顶点索引缓存,用于标定顶点顺序unsigned int indices[] = {0,1,2,2,3,0};//定义缓冲区对象unsigned int buffer;//功能:生成缓冲区对象,并将数据写入缓冲区glGenBuffers(1, &buffer);//功能:将缓冲区对象绑定到目标glBindBuffer(GL_ARRAY_BUFFER, buffer);//功能:将数据写入缓冲区glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(float), position, GL_STATIC_DRAW);//功能:配置顶点属性指针glEnableVertexAttribArray(0);//功能:指定顶点属性数组的索引、大小、数据类型、是否归一化、偏移量、数据指针glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);//索引缓冲区对象unsigned int ibo;//功能:生成缓冲区对象,并将数据写入缓冲区glGenBuffers(1, &ibo);//功能:将缓冲区对象绑定到目标.没有绑定为数组缓冲区//参数:1.GL_ELEMENT_ARRAY_BUFFER:指定目标为索引缓冲区对象//2.ibo:索引缓冲区对象IDglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);//功能:将数据写入缓冲区//参数:1.GL_ELEMENT_ARRAY_BUFFER:指定目标为索引缓冲区对象//2.6*sizeof(unsigned int):索引数据大小//3.indices:索引数据指针//4.GL_STATIC_DRAW:指定数据不会改变glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);//解析着色器代码文件ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");std::string vertexShader = source.VertexSource;std::string fragmentShader = source.FragmentSource;//创建着色器程序unsigned int shader = CreateShader(vertexShader, fragmentShader);//使用着色器程序glUseProgram(shader);//渲染循环,直到窗口被关闭while (!glfwWindowShouldClose(window)){//清除颜色缓冲区glClear(GL_COLOR_BUFFER_BIT);//glDrawArrays(GL_TRIANGLES, 0, 6);//功能:绘制三角形//参数:1.GL_TRIANGLES:绘制三角形//2.6:顶点数量//3.GL_UNSIGNED_INT:索引数据类型//4.nullptr:索引数据指针//因为索引缓冲区已经绑定到GL_ELEMENT_ARRAY_BUFFER目标,所以这里不需要再传入索引数据指针glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);//刷新缓冲区并交换窗口glfwSwapBuffers(window);//处理窗口事件,如键盘输入、鼠标移动等glfwPollEvents();}//删除着色器程序//glDeleteProgram(shader);//释放 GLFW 库占用的所有资源。glfwTerminate();return 0;
}

2.2简洁注释代码

#include <GL/glew.h>
#include <GLFW/glfw3.h>#include<iostream>
#include<fstream>
#include<string>
#include<sstream>//功能:定义ShaderProgramSource结构体,用于存储着色器代码
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::string line;//该变量用于存储着色器类型std::stringstream ss[2];//该变量是当前着色器类型ShaderType type = ShaderType::NONE;//功能:读取文件中的每一行内容,直到文件结束while (getline(stream, line)){//如果当前行包含#shader,则说明接下来是着色器代码if (line.find("#shader") != std::string::npos){//如果当前行包含vertex,则说明接下来是顶点着色器代码if (line.find("vertex") != std::string::npos){type = ShaderType::VERTEX;}else if (line.find("fragment") != std::string::npos){type = ShaderType::FRAGMENT;}}else{//否则,将当前行添加到对应着色器代码的stringstream中ss[(int)type] << line << '\n';}}//返回ShaderProgramSource结构体return { ss[0].str(), ss[1].str() };
}//功能:编译着色器代码
static unsigned int CompilesShader(unsigned int type, const std::string& source)
{//功能:创建着色器对象unsigned int id = glCreateShader(type);//功能:设置着色器源代码.const char* src = source.c_str();//功能:替换着色器对象中的源代码。将该id的指定着色器的源代码设置为src指针指向的字符串glShaderSource(id, 1, &src, nullptr);//功能:编译着色器对象的源代码glCompileShader(id);//设置返回着色器的对象IDint 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));//功能:获取编译错误信息glGetShaderInfoLog(id, length, &length, message);std::cout << "Failed to compile shader!" << (type == GL_VERTEX_SHADER? "Vertex" : "Fragment") << "shader!" << std::endl;std::cout << message << std::endl;//删除着色器对象glDeleteShader(id);return 0;}//TODO:错误处理ingreturn id;
}//功能:创建着色器程序
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{//创建程序对象unsigned int program = glCreateProgram();//编译顶点着色器对象unsigned int vs = CompilesShader(GL_VERTEX_SHADER, vertexShader);//编译片段着色器对象unsigned int fs = CompilesShader(GL_FRAGMENT_SHADER, fragmentShader);//功能:将编译好的着色器对象附加到程序对象中glAttachShader(program, vs);glAttachShader(program, fs);//功能:链接程序对象glLinkProgram(program);//功能:验证着色器程序对象是否可以在当前OpenGL状态中执行。检查着色器程序的完整性和可执行性。glValidateProgram(program);//删除着色器对象,因为它们已经被链接到程序对象中glDeleteShader(vs);glDeleteShader(fs);//返回着色器程序return program;
}int main(void)
{GLFWwindow* window;//初始化glfwif (!glfwInit())return -1;//创建一个窗口模式的窗口并设置OpenGL上下文window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);if (!window)//如果窗口创建失败,则终止程序{glfwTerminate();//释放glfw资源return -1;}//设置当前窗口的上下文,之后所有的OpenGL调用都会在这个上下文中进行glfwMakeContextCurrent(window);//初始化GLEWif (glewInit() != GLEW_OK)std::cout << "Error!" << std::endl;//打印OpenGL版本信息std::cout << glGetString(GL_VERSION) << std::endl;//准备数据float position[] = {-0.5f, -0.5f,0.5f, -0.5f,0.5f,0.5f,-0.5f,0.5f,};//定义顶点索引缓存,用于标定顶点顺序unsigned int indices[] = {0,1,2,2,3,0};//定义缓冲区对象unsigned int buffer;//功能:生成缓冲区对象,并将数据写入缓冲区glGenBuffers(1, &buffer);//功能:将缓冲区对象绑定到目标glBindBuffer(GL_ARRAY_BUFFER, buffer);//功能:将数据写入缓冲区glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(float), position, GL_STATIC_DRAW);//功能:配置顶点属性指针glEnableVertexAttribArray(0);//功能:指定顶点属性数组的索引、大小、数据类型、是否归一化、偏移量、数据指针glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);//索引缓冲区对象unsigned int ibo;//功能:生成缓冲区对象,并将数据写入缓冲区glGenBuffers(1, &ibo);//功能:将缓冲区对象绑定到目标.没有绑定为数组缓冲区//参数:1.GL_ELEMENT_ARRAY_BUFFER:指定目标为索引缓冲区对象//2.ibo:索引缓冲区对象IDglBindBuffer(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");std::string vertexShader = source.VertexSource;std::string fragmentShader = source.FragmentSource;//创建着色器程序unsigned int shader = CreateShader(vertexShader, fragmentShader);//使用着色器程序glUseProgram(shader);//渲染循环,直到窗口被关闭while (!glfwWindowShouldClose(window)){//清除颜色缓冲区glClear(GL_COLOR_BUFFER_BIT);//glDrawArrays(GL_TRIANGLES, 0, 6);//功能:绘制三角形//参数:1.GL_TRIANGLES:绘制三角形glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);//刷新缓冲区并交换窗口glfwSwapBuffers(window);//处理窗口事件,如键盘输入、鼠标移动等glfwPollEvents();}//删除着色器程序//glDeleteProgram(shader);//释放 GLFW 库占用的所有资源。glfwTerminate();return 0;
}

2.3运行结果

相关文章:

  • Netty创新架构突破链接数瓶颈技术,如何应用于新能源汽车智慧充电桩?
  • Elasticsearch的运维
  • etcd:高可用,分布式的key-value存储系统
  • 5 WPF中的Page页面的使用
  • 【NLP基础知识系列课程-Tokenizer的前世今生第一课】Tokenizer 是什么?为什么重要?
  • React 第四十八节 Router中 useMatch 的使用详细介绍及案例分析
  • LVS 负载均衡群集
  • Kotlin 中 Lambda 表达式的语法结构及简化推导
  • 前端 reconnecting-websocket 包
  • Windows逆向工程提升之IMAGE_TLS_DIRECTORY
  • 三、OrcaSlicer预设显示
  • 交换机 路由器
  • Python训练打卡Day35
  • C++23 新成员函数与字符串类型的改动
  • idea配置android--以idea2023为例
  • 一则doris数据不一致问题
  • gcc clang
  • 详解srs流媒体服务器的集群
  • sharding jdbc的使用,如何在Spring中实现数据库的主从分离、分库分表等功能
  • day022-定时任务-故障案例与发送邮件
  • 民宿网站建设问卷调查/百度sem运营
  • 北京网站建设熊掌号/百度seo关键词排名价格
  • wordpress企业官网主题/seo查询百科
  • 淘宝 网站建设教程视频/怎么自己弄一个平台
  • 网站宣传软文/推广营销大的公司
  • 重庆秀山网站建设费用/搜索引擎外部链接优化