在 Windows 下开发,使用 glfw, glew 库,工具链是 VS 。
下载这些库后需要把 Include, DLL 和 Lib 放到 project dir 的对应目录下。
find_package(OpenGL REQUIRED)
add_library(glfw SHARED IMPORTED)
set_property(TARGET glfw PROPERTY IMPORTED_LOCATION "${PROJECT_SOURCE_DIR}/glfw3.dll")
set_property(TARGET glfw PROPERTY IMPORTED_IMPLIB "${PROJECT_SOURCE_DIR}/Lib/glfw3.lib")
add_library(glew SHARED IMPORTED)
set_property(TARGET glew PROPERTY IMPORTED_LOCATION "${PROJECT_SOURCE_DIR}/glew32.dll")
set_property(TARGET glew PROPERTY IMPORTED_IMPLIB "${PROJECT_SOURCE_DIR}/Lib/glew32.lib")
include_directories(Include)
add_executable(learn_cg main.cpp)
target_link_libraries(learn_cg
${OPENGL_LIBRARY} # filled by "find_package(OpenGL REQUIRED)"
glfw glew
)
另外运行的工作目录也要指定为 project dir
可以考虑研究一下 vcpkg
创建 shader 的步骤:读取 shader 源码,编译。
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
// id, source 数量, source 数组指针,source 长度
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
之后要和 Program 链接,注意之前传的源码 buffer 不能被销毁,否则会出错。
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
Program 创建完成后,可以把 shader 删除了。
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
之后我们可以用 program 的 id 去访问 shaders 中的 uniform 变量,并使用 shader 绘制顶点 (glUseProgram
)。
GLuint VAO, VBO, EBO;
static void CreateVertexBuffer() {
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), nullptr);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (void*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, NULL);
glBindVertexArray(NULL);
}
VBO: 顶点缓冲对象,存储顶点坐标
glGenBuffers
创建 Buffer
glBindBuffer
绑定指定类型的 Buffer (指向内存中我们创建的顶点、index 数组等)
使用 glVertexAttribPointer
可以设置顶点坐标的 layout (对应于 vertex shader 中的 layout)。设置好后使用 glEnableVertexAttribArray
启用。
EBO: 元素(index) 缓冲对象,存储顶点的绘制顺序。如果使用 EBO ,要用 glDrawElements
,否则使用 glDrawArrays
// 使用 EBO
glDrawElements(GL_TRIANGLES, sizeof(indices), GL_UNSIGNED_INT, nullptr);
// 不使用 EBO
glDrawArrays(GL_TRIANGLES, 0, sizeof(vertices));
VAO: 顶点数组对象,可以将我们配置的 VBO、EBO “打包”起来,将来绘制的时候只需要 glBindVertexArray
即可。
在创建和绑定 VBO, EBO 的时候,先创建和绑定 VAO (glBindVertexArray(VAO)
)。VBO 和 EBO 配置好后,需要调用 glBindVertexArray(NULL)
。
g-truc/glm: OpenGL Mathematics (GLM)
glm 可以进行向量、矩阵运算
以 vec4 为例,向量的结构体中包含了四个 T 类型的成员,依次表示 x, y, z, w 四个分量。
// Include/glm/detail/type_vec4.hpp
template<typename T, qualifier Q>
struct vec<4, T, Q>
{
union { T x, r, s; };
union { T y, g, t; };
union { T z, b, p; };
union { T w, a, q; };
使用 operator[]
访问的时候是这样的:
成员函数的实现都放在类型对应的
.inl
文件里面,由于这些函数都是inline
的,因此后缀名是.inl
// Include/glm/detail/type_vec4.inl
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER GLM_CONSTEXPR T& vec<4, T, Q>::operator[](typename vec<4, T, Q>::length_type i)
{
assert(i >= 0 && i < this->length());
switch(i)
{
default:
case 0:
return x;
case 1:
return y;
case 2:
return z;
case 3:
return w;
}
}
以 mat4 (4x4 矩阵)为例:
// Include/glm/detail/type_mat4x4.hpp
template<typename T, qualifier Q>
struct mat<4, 4, T, Q>
{
typedef vec<4, T, Q> col_type;
private:
col_type value[4];
矩阵以列优先存储,也就是包含了一个指向列向量的指针。
矩阵相乘:
// Include/glm/detail/type_mat4x4.inl
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> operator*(mat<4, 4, T, Q> const& m1, mat<4, 4, T, Q> const& m2)
{
typename mat<4, 4, T, Q>::col_type const SrcA0 = m1[0];
typename mat<4, 4, T, Q>::col_type const SrcA1 = m1[1];
typename mat<4, 4, T, Q>::col_type const SrcA2 = m1[2];
typename mat<4, 4, T, Q>::col_type const SrcA3 = m1[3];
typename mat<4, 4, T, Q>::col_type const SrcB0 = m2[0];
typename mat<4, 4, T, Q>::col_type const SrcB1 = m2[1];
typename mat<4, 4, T, Q>::col_type const SrcB2 = m2[2];
typename mat<4, 4, T, Q>::col_type const SrcB3 = m2[3];
mat<4, 4, T, Q> Result;
Result[0] = SrcA0 * SrcB0[0] + SrcA1 * SrcB0[1] + SrcA2 * SrcB0[2] + SrcA3 * SrcB0[3];
Result[1] = SrcA0 * SrcB1[0] + SrcA1 * SrcB1[1] + SrcA2 * SrcB1[2] + SrcA3 * SrcB1[3];
Result[2] = SrcA0 * SrcB2[0] + SrcA1 * SrcB2[1] + SrcA2 * SrcB2[2] + SrcA3 * SrcB2[3];
Result[3] = SrcA0 * SrcB3[0] + SrcA1 * SrcB3[1] + SrcA2 * SrcB3[2] + SrcA3 * SrcB3[3];
return Result;
}
上面的代码等价于:
可见仅仅是存储为列优先形式,并不是将矩阵转置进行存储。
但是在访问矩阵的时候是先列后行,如 a[2][1]
表示 1 行 2 列。
LearnOpenGL-CN/08 Coordinate Systems.md at new-theme · LearnOpenGL-CN/LearnOpenGL-CN
LearnOpenGL-CN/09 Camera.md at new-theme · LearnOpenGL-CN/LearnOpenGL-CN
OpenGL 的 vertex shader 输出的是一个标准化设备坐标,(x, y, z)
在 [-1.0, 1.0]
范围,分别表示横坐标、纵坐标和深度,这是我们最终在窗口中看到的坐标。
我们要自己将 3D 空间的图形变换到这个坐标系上。
vertex shader:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTex;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoord = aTex;
}
输入 aPos 是物体的局部坐标(Local Coordinate)
乘上 model 就变成在世界坐标(World Coordinate),即我们将物体的坐标变换到世界坐标系下,给它指定了位置、方向等。
再乘上 view 即观察坐标(View Coordinate),变换到了 Camera 的坐标系下。Camera 的 z 方向即观察的方向,xy 组成了投影面。
最后乘上 projection 得到裁剪坐标(Clip Coordinate),一般是一个投影变换矩阵,将 Camera 坐标系的三维坐标投影到投影面(xy 平面)上的 [-1,1]*[-1,1]
区域内。
假设摄像机在世界坐标系下的某个位置
接着还要确定摄像机的 x、y 轴方向。我们取一个参考向量“上向量”
假设观察坐标系(即摄像机的坐标系)中有一个坐标
利用仿射(透视?)坐标,也可以写成四阶矩阵和四维向量的形式:
左边就是观察坐标系到世界坐标系的变换矩阵。
由此,我们也可以得到世界坐标系到观察坐标系的反变换矩阵。由于
因此我们可以得到用于 view 的 lookAt 矩阵。
我们看看 glm 中的实现:
// Include/glm/ext/matrix_transform.inl
template<typename T, qualifier Q>
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> lookAtRH(vec<3, T, Q> const& eye, vec<3, T, Q> const& center, vec<3, T, Q> const& up)
{
vec<3, T, Q> const f(normalize(center - eye));
vec<3, T, Q> const s(normalize(cross(f, up)));
vec<3, T, Q> const u(cross(s, f));
mat<4, 4, T, Q> Result(1);
Result[0][0] = s.x;
Result[1][0] = s.y;
Result[2][0] = s.z;
Result[0][1] = u.x;
Result[1][1] = u.y;
Result[2][1] = u.z;
Result[0][2] =-f.x;
Result[1][2] =-f.y;
Result[2][2] =-f.z;
Result[3][0] =-dot(s, eye);
Result[3][1] =-dot(u, eye);
Result[3][2] = dot(f, eye);
return Result;
}
glm 中,我们只要提供摄像机坐标、观察点坐标和上向量就可以构造 lookAt 矩阵(此处是生成右手坐标系的)。
可见,eye
就是位置 center
是观察目标点, f = norm(center-eye)
就是 up
就是上向量
s, u
就是 f
是