[Manjaro] OpenGL 配合着色器实现光线跟踪之引入光线

概述

本文介绍 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;

最终结果图如下

[Manjaro] OpenGL 配合着色器实现光线跟踪之引入光线_第1张图片

顶点着色器代码

// 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);
}

最终结果如下

[Manjaro] OpenGL 配合着色器实现光线跟踪之引入光线_第2张图片

顶点着色器代码

#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) ,则可以得到圆的标准方程如下

(x-C_x)^2+(y-C_y)^2+(z-C_z)^2 = R^2

对于光线跟踪算法,我们需要知道空间中有哪些点需要上色,它的颜色值是什么。这里假设空间中有一个点 P(x, y, z) ,如果它在圆上或者在圆内,则将其颜色值设定为红色。

在这里需要注意,我们的 ray_color() 函数使用的参数是 vec3 direction ,即点 P 的参数化形式表示,而不是直接使用直角坐标的形式。因此在方程求解时需要将原式换成关于参数坐标 t 的方程。

经过推导后得到的公式如下

b\cdot b\ t^2 + 2b(A-C) t + (A-C)\cdot (A-C) - R^2 = 0

其中

b = direction \ vector, (A-C) = (origin \ point) - (center \ point)

从此转换为求关于 t 的一元二次方程的根的问题。

  • 如果判别式大于0,则射线与圆存在两个交点;
  • 如果判别式等于零,则射线与圆有一个交点;
  • 如果判别式小于零,则射线与圆没有交点。

有了这些知识,我们就可以在顶点着色器中编写程序了

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;

渲染的结果如下

[Manjaro] OpenGL 配合着色器实现光线跟踪之引入光线_第3张图片

顶点着色器代码

#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);
}

最终结果

[Manjaro] OpenGL 配合着色器实现光线跟踪之引入光线_第4张图片

顶点着色器

#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 实现了屏幕空间的光线跟踪,并最终利用表面法向量绘制出一个带有颜色渐变的球。

你可能感兴趣的:(着色器,图形渲染,c++)