一句话总结:location 是 CPU 与 Shader 之间定位变量的一种机制。

为什么需要 location

shader 中是一些计算公式,计算就需要数据,数据来自 CPU。
数据有很多类型,意味着有很多的变量,数据在 CPU 中是变量,在 GPU 中也是变量。
如何把 CPU 的变量与 shader 的变量进行对应,就需要一种机制。

shader 中的变量:

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aUV;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

CPU 中的变量:

// Vertex Attribute
glVertexAttribPointer(0, ...);
glEnableVertexAttribArray(0);

glVertexAttribPointer(1, ...);
glEnableVertexAttribArray(1);

glVertexAttribPointer(2, ...);
glEnableVertexAttribArray(2);

// Uniform
GLint modelLocation =
    glGetUniformLocation(program, "model");

GLint viewLocation =
    glGetUniformLocation(program, "view");

GLint projectionLocation =
    glGetUniformLocation(program, "projection");

上面 3 个 attribute 的 location 是 shader 里写死的 0、1、2。
3 个 uniform 没有写 location,编号由链接器分配。
假如这次查询结果刚好是 0、1、2,对应关系就是:

                    Shader Program
                         │
          ┌──────────────┴──────────────┐
          │                             │
    Vertex Inputs                    Uniforms
          │                             │
   Attribute Location              Uniform Location
          │                             │
    ┌─────┼─────┐                 ┌─────┼─────┐
    0     1     2                 0     1     2
    │     │     │                 │     │     │
    ↓     ↓     ↓                 ↓     ↓     ↓
  aPos  aColor  aUV             model  view projection
    ↑     ↑     ↑                 ↑     ↑     ↑
    └─────┴─────┘                 └─────┴─────┘
          VBO                           CPU

两套编号各自独立,图里两边都出现 0、1、2,并不冲突。

两种类型的 location

Vertex Attribute Location 是“顶点数据入口”的编号。
Uniform Location 是“Uniform 参数”的编号。
两套 Location 独立存在,数字可以重复,但没有任何影响。

Vertex Attribute Location
0 → aPos
1 → aColor
2 → aUV

Uniform Location
0 → model
1 → view
2 → projection

Vertex Attribute Location

顶点数据如何给到 shader?
反过来说,shader 需要使用顶点数据。
按照逆向追踪数据来源的思路,先看 shader:

#version 330 core

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aUV;

out vec3 color;
out vec2 uv;

void main()
{
    gl_Position = vec4(aPos, 1.0);

    color = aColor;
    uv = aUV;
}

shader 定义了 3 个获取顶点数据的变量:aPos,aColor,aUV;并且分别给它们 location 的值设置为 0,1,2;
那么在 CPU 端必须存在对应的函数,与 0,1,2 进行关联;

接着看 OpenGL 对应函数:

// Location 0 → position
glVertexAttribPointer(
    0,                       // location 0
    3,                       // 3 个 float
    GL_FLOAT,
    GL_FALSE,
    8 * sizeof(float),       // stride
    (void*)0                 // offset
);
glEnableVertexAttribArray(0);

// Location 1 → color
glVertexAttribPointer(
    1,                       // location 1
    3,                       // 3 个 float
    GL_FLOAT,
    GL_FALSE,
    8 * sizeof(float),       // stride
    (void*)(3 * sizeof(float)) // offset
);
glEnableVertexAttribArray(1);

// Location 2 → UV
glVertexAttribPointer(
    2,                       // location 2
    2,                       // 2 个 float
    GL_FLOAT,
    GL_FALSE,
    8 * sizeof(float),       // stride
    (void*)(6 * sizeof(float)) // offset
);
glEnableVertexAttribArray(2);

与 0,1,2 关联的分别是 glVertexAttribPointer 函数的第一个参数,glEnableVertexAttribArray 函数的参数。
这 2 个函数记录的是 VAO 的状态。调用时必须先绑定一个 VAO,并且 VBO 已经绑定到 GL_ARRAY_BUFFER。
简单来说,就是记下“VBO 里的数据怎么读”,读出来的结果分别对应 location 0,1,2。

那么 VAO 如何解读 VBO 的数据?
先看 VBO 数据:

// 准备顶点数据
float vertices[] = {
    // position        // color        // UV
     0.0f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  0.5f, 1.0f,
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  0.0f, 0.0f,
     0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f,  1.0f, 0.0f
};

// 创建 VBO 并顶点数据关联
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(
    GL_ARRAY_BUFFER,
    sizeof(vertices),
    vertices,
    GL_STATIC_DRAW
);

分析顶点数据 vertices 可以发现:

glVertexAttribPointer 连接 vertices 与 shader:

简单来讲,glVertexAttribPointer 记下了从 vertices 取值的规则。真正把数据送进 shader,发生在绘制的时候。

一个完整的链路图:

                  Vertex Shader
              ┌─────────────────┐
              │                 │
location 0 ──→│ aPos            │
location 1 ──→│ aColor          │
location 2 ──→│ aUV             │
              │                 │
              └─────────────────┘
                    ↑
                    │
                  VAO
                    ↑
                    │
                   VBO
                    │
       ┌────────────┼────────────┐
       ↓            ↓            ↓
    position       color         UV

Uniform Location

一次绘制里,uniform 对所有顶点都是同一个值,且数据量少。
顶点 attribute 则是每个顶点各有一份,数据量大。
所以 uniform 从 CPU 到 shader,不需要像顶点数据那样做格式解读。

先看 shader 代码:

#version 330 core

layout(location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position =
        projection *
        view *
        model *
        vec4(aPos, 1.0);
}

可以看到,shader 定义了 3 个 uniform 变量:model、view、projection,它们没有像顶点变量那样设置 location。
#version 330 也不能给 uniform 写 layout(location)。这个写法要到 GLSL 4.30。
那么 CPU 如何对应这 3 个 uniform 变量呢?

使用 glGetUniformLocation 获取 uniform location 值:

GLint modelLocation =
    glGetUniformLocation(program, "model");

GLint viewLocation =
    glGetUniformLocation(program, "view");

GLint projectionLocation =
    glGetUniformLocation(program, "projection");

很明显,glGetUniformLocation 函数的第 2 个参数直接关联了 shader 中定义的 uniform 变量。
返回值是这个 program 里的编号,换一个 program 就要重新查询。
变量声明了但实际没用到时,链接器会把它优化掉,这里返回 -1。

接着,应该还有对应的变量和函数与 modelLocation、viewLocation、projectionLocation 进行关联才对。
比如 glUniformMatrix4fv 函数:

// 假如存在三个矩阵数据
glm::mat4 model;
glm::mat4 view;
glm::mat4 projection;

// 把矩阵数据与 uniform location 关联
glUseProgram(program);

glUniformMatrix4fv(
    modelLocation,
    1,
    GL_FALSE,
    glm::value_ptr(model)
);

glUniformMatrix4fv(
    viewLocation,
    1,
    GL_FALSE,
    glm::value_ptr(view)
);

glUniformMatrix4fv(
    projectionLocation,
    1,
    GL_FALSE,
    glm::value_ptr(projection)
);

glUniformMatrix4fv 写入的是当前正在使用的 program。一个 mat4 只占 1 个 uniform location。
location 为 -1 时,这次调用会被忽略。

此时,一个完整的 uniform location 链路就完成了。
编号由链接器分配。假如这次查到的是:

modelLocation      = 0
viewLocation       = 1
projectionLocation = 2

链路就是这样的:

CPU
 │
 ├── glUniformMatrix4fv(0, ...)
 │         ↓
 │       model
 │
 ├── glUniformMatrix4fv(1, ...)
 │         ↓
 │       view
 │
 └── glUniformMatrix4fv(2, ...)
           ↓
       projection