本文介绍 GLFW + GLAD 在 RayTracing in one weekend 的实现。
实验环境:Manjaro Linux 22.0.0
整体思路:
使用基于屏幕空间的光线跟踪算法,每个像素点代表一个光线。使用 GLSL 着色器语言编写顶点着色器,获取顶点在屏幕空间的坐标,经过函数计算得到 RGB 值并传输到片段着色器中。
目录
概述
输出第一张图片
顶点着色器代码
片段着色器代码
向场景中发射光线
顶点着色器代码
片段着色器代码
画一个实心球
顶点着色器代码
简单的着色——基于表面法向量
顶点着色器
首先设置窗口尺寸
const unsigned int SCR_WIDTH = 400;
const unsigned int SCR_HEIGHT = 400;
设置参数
// 确定顶点大小
const int point_size = 1;
// 均匀等距采样
const int sample_x = SCR_WIDTH / point_size;
const int sample_y = SCR_HEIGHT / point_size;
// 确定顶点数组
const int num_of_points = sample_y * sample_x;
float vertices[num_of_points * 3];
因为 GPU 绘制顶点的时候是要通过输入的顶点数组来计算屏幕空间中的顶点,因此在 main 函数中通过 CPU 设置顶点三维坐标信息。
平常我们认为图像的空间坐标系中,坐标从 0 开始一直到边界,但是 OpenGL 中,屏幕空间是一个坐标取值为 [-1.0, 1.0] 的二维空间,因此需要进行坐标的转换。
int dy = SCR_HEIGHT / sample_y; // 顶点在虚拟屏幕空间的 y 轴步长
int dx = SCR_WIDTH / sample_x; // 顶点在虚拟屏幕空间 x 轴步长
int idx = 0;
printf("dy: %d dx: %d\n", dy, dx);
/* 遍历虚拟屏幕空间,将 [0, SCR_HEIGHT] 坐标变换到 [-1.0, 1.0] 的
* 屏幕空间坐标并记录在 vertices 数组中 */
for (int y = 0; y < SCR_HEIGHT; y += dy) {
for (int x = 0; x < SCR_WIDTH; x += dx) {
float ny = (static_cast(y) / SCR_HEIGHT - 0.5) * 2.0f;
float nx = (static_cast(x) / SCR_WIDTH - 0.5) * 2.0f;
vertices[idx++] = nx;
vertices[idx++] = ny;
vertices[idx++] = 0.0f; // z 轴坐标,暂时设置为 0.0f
}
}
在渲染主循环中,我们需要将顶点数据发送到 GPU,因此使用 glDrawArrays() 函数 ,参数设置为顶点个数。
glPointSize(point_size);
glDrawArrays(GL_POINTS, 0, num_of_points);
为了将顶点坐标映射成颜色值,还需要在着色器程序中进行一点小小的调整,将 [-1.0, 1.0] 的坐标轴变换到 [0.0, 1.0] 范围内。
vsOutColor.x = aPos.x / 2 + 0.5;
vsOutColor.y = aPos.y / 2 + 0.5;
vsOutColor.z = 0.25;
最终结果图如下
// vertex shader
#version 460 core
layout (location = 0) in vec3 aPos;
out vec3 vsOutColor;
void main()
{
vsOutColor.x = aPos.x / 2 + 0.5;
vsOutColor.y = aPos.y / 2 + 0.5;
vsOutColor.z = 0.25;
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
};
// fragment shader
#version 460 core
out vec4 FragColor;
in vec3 vsOutColor;
void main()
{
FragColor = vec4(vsOutColor, 1.0f);
}
没有光,就看不到任何东西。假设有一个能够从镜头中发射光线的仪器,称为“相机”(显然并不现实)在场景中有固定的坐标。“相机”向其“底片”上的每个像素逐个发射一条光线,“底片” 的位置恰好位于镜头的焦距处。经过各种光学作用,“底片”能够显示出最终的图像。
Vec3 类的代码可以参考 Ray Tracing in One Weekend 的原文或者使用 glm::vec3 等实现,由于 GLSL 的 vec3 使用的是 float 类型,且类型检查严格,因此务必确保数值类型的对应。
首先需要布置相机的位置
// “底片”的尺寸
float viewport_height = 2.0;
float viewport_width = aspect_ratio * viewport_height;
float focal_length = 1.0; // 相机焦距
// “底片”的坐标
auto origin = Vec3(0, 0, 0);
auto horizontal = Vec3(viewport_width, 0, 0);
auto vertical = Vec3(0, viewport_height, 0);
auto lower_left_corner = origin - horizontal / 2 - vertical / 2 - Vec3(0, 0, focal_length);
为了计算像素值,这里在顶点着色器中增加一个函数
vec3 ray_color(vec3 direction)
{
// 计算单位向量
vec3 unit_direction = vec3(direction / length(direction));
// 归一化到 [0.0, 1.0]
float t = 0.5 * (unit_direction.y + 1.0);
vec3 blue = vec3(0.5, 0.7, 1.0);
vec3 white = vec3(1.0, 1.0, 1.0);
// 使用线性插值计算颜色值
return mix(white, blue, t);
}
将计算好的像素值命名为 RayColor 传输给片段着色器
in vec3 RayColor;
void main()
{
FragColor = vec4(RayColor, 1.0f);
}
最终结果如下
#version 460 core
layout (location = 0) in vec3 aPos;
uniform vec3 origin;
uniform vec3 lower_left_corner;
uniform vec3 horizontal;
uniform vec3 vertical;
out vec3 RayColor;
vec3 ray_color(vec3 direction)
{
vec3 unit_direction = vec3(direction / length(direction));
float t = 0.5 * (unit_direction.y + 1.0);
vec3 blue = vec3(0.5, 0.7, 1.0);
vec3 white = vec3(1.0, 1.0, 1.0);
return mix(white, blue, t);
}
void main()
{
float u = 0.5 * (aPos.x + 1.0);
float v = 0.5 * (aPos.y + 1.0);
vec3 dir = lower_left_corner + u * horizontal + v * vertical - origin;
RayColor = ray_color(dir);
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
#version 460 core
out vec4 FragColor;
in vec3 RayColor;
void main()
{
FragColor = vec4(RayColor, 1.0f);
}
我们都知道一个对于一个三维空间中的球,如果它的半径为 R ,圆心坐标为 C(x, y, z) ,则可以得到圆的标准方程如下
对于光线跟踪算法,我们需要知道空间中有哪些点需要上色,它的颜色值是什么。这里假设空间中有一个点 P(x, y, z) ,如果它在圆上或者在圆内,则将其颜色值设定为红色。
在这里需要注意,我们的 ray_color() 函数使用的参数是 vec3 direction ,即点 P 的参数化形式表示,而不是直接使用直角坐标的形式。因此在方程求解时需要将原式换成关于参数坐标 t 的方程。
经过推导后得到的公式如下
其中
从此转换为求关于 t 的一元二次方程的根的问题。
有了这些知识,我们就可以在顶点着色器中编写程序了
bool hit_sphere(vec3 center, float radius, vec3 direction)
{
vec3 oc = origin - center;
float a = dot(direction, direction);
float b = 2.0 * dot(oc, direction);
float c = dot(oc, oc) - radius * radius;
float discriminant = b*b - 4*a*c;
if (discriminant > 0)
return true;
else
return false;
}
在 ray_color() 函数中添加下面的代码
vec3 circ_center = vec3(0.0,0.0,-1);
vec3 red = vec3(1.0, 0.0, 0.0);
if(hit_sphere(circ_center, 0.5, direction))
return red;
渲染的结果如下
#version 460 core
layout (location = 0) in vec3 aPos;
uniform vec3 origin;
uniform vec3 lower_left_corner;
uniform vec3 horizontal;
uniform vec3 vertical;
out vec3 RayColor;
bool hit_sphere(vec3 center, float radius, vec3 direction)
{
vec3 oc = origin - center;
float a = dot(direction, direction);
float b = 2.0 * dot(oc, direction);
float c = dot(oc, oc) - radius * radius;
float discriminant = b*b - 4*a*c;
if (discriminant > 0)
return true;
else
return false;
}
vec3 ray_color(vec3 direction)
{
vec3 circ_center = vec3(0.0,0.0,-1);
vec3 red = vec3(1.0, 0.0, 0.0);
if(hit_sphere(circ_center, 0.5, direction))
return red;
vec3 unit_direction = vec3(direction / length(direction));
float t = 0.5 * (unit_direction.y + 1.0);
vec3 blue = vec3(0.5, 0.7, 1.0);
vec3 white = vec3(1.0, 1.0, 1.0);
return mix(white, blue, t);
}
void main()
{
float u = 0.5 * (aPos.x + 1.0);
float v = 0.5 * (aPos.y + 1.0);
vec3 dir = lower_left_corner + u * horizontal + v * vertical -
origin;
RayColor = ray_color(dir);
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
片段着色器代码不变
现在我们已经能在三维空间中表示一个球了,但是一个单色的球显然并不能满足我们的好奇心。因此现在试着给小球上色,我们想让颜色有变化。
为了产生变化,可以利用小球表面的数学性质:小球的表面是一个曲面,上面每一个点的法向量都不相同。
由于 RGB 颜色空间的表示范围是有限的([0.0, 1.0] 或 [0, 255]),因此需要将计算出的法向量归一化成单位向量,然后再映射到颜色值上。
因此,ray_color() 函数修改如下
vec3 ray_color(vec3 origin, vec3 direction)
{
vec3 ball_center = vec3(0.0,0.0,-1);
float t = hit_sphere(ball_center, 0.5, direction);
if(t > 0.0) {
vec3 P = origin + t * direction;
vec3 N = P - ball_center;
N = vec3(N / length(N));
return 0.5 * vec3(N + 1.0);
}
vec3 unit_direction = vec3(direction / length(direction));
t = 0.5 * (unit_direction.y + 1.0);
vec3 blue = vec3(0.5, 0.7, 1.0);
vec3 white = vec3(1.0, 1.0, 1.0);
return mix(white, blue, t);
}
hit_sphere() 函数现在需要计算出参数值 t,也就是一元二次方程的根。
float hit_sphere(vec3 center, float radius, vec3 direction)
{
vec3 oc = origin - center;
float a = dot(direction, direction);
float b = 2.0 * dot(oc, direction);
float c = dot(oc, oc) - radius * radius;
float discriminant = b*b - 4*a*c;
if (discriminant < 0)
return -1.0;
else
return (-b - sqrt(discriminant)) / (2.0*a);
}
最终结果
#version 460 core
layout (location = 0) in vec3 aPos;
uniform vec3 origin;
uniform vec3 lower_left_corner;
uniform vec3 horizontal;
uniform vec3 vertical;
out vec3 RayColor;
float hit_sphere(vec3 center, float radius, vec3 direction)
{
vec3 oc = origin - center;
float a = dot(direction, direction);
float b = 2.0 * dot(oc, direction);
float c = dot(oc, oc) - radius * radius;
float discriminant = b*b - 4*a*c;
if (discriminant < 0)
return -1.0;
else
return (-b - sqrt(discriminant)) / (2.0*a);
}
vec3 ray_color(vec3 origin, vec3 direction)
{
vec3 ball_center = vec3(0.0,0.0,-1);
float t = hit_sphere(ball_center, 0.5, direction);
if(t > 0.0) {
vec3 P = origin + t * direction;
vec3 N = P - ball_center;
N = vec3(N / length(N));
return 0.5 * vec3(N + 1.0);
}
vec3 unit_direction = vec3(direction / length(direction));
t = 0.5 * (unit_direction.y + 1.0);
vec3 blue = vec3(0.5, 0.7, 1.0);
vec3 white = vec3(1.0, 1.0, 1.0);
return mix(white, blue, t);
}
void main()
{
float u = 0.5 * (aPos.x + 1.0);
float v = 0.5 * (aPos.y + 1.0);
vec3 dir = lower_left_corner + u * horizontal + v * vertical -
origin;
RayColor = ray_color(dir);
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
至此,本文结束。
总结一下,本文使用 OpenGL 实现了屏幕空间的光线跟踪,并最终利用表面法向量绘制出一个带有颜色渐变的球。