RayMarching中的相机

314 阅读2分钟

源码:github.com/buglas/shad…

在传统的顶点建模中,相机要考虑视图矩阵和投影矩阵。

在RayMarching中,其相机天生就具备了透视相机的部分功能,比如透视投影,所以接下来重点要考虑的就是相机的旋转变换。

01-相机测试

首先咱们基于上一节“03-球体的抗锯齿.glsl”做个试验。

在webgl 中,通常默认相机的视点在零点,然后看向-z的方向,所以我先对之前的代码做下调整。

1.把球心放在z=-2的位置。

// 球体的球心位置
#define SPHERE_POS vec3(0, 0, -2)

2.把相机的视点放在零点。

// 相机视点位
#define CAMERA_POS vec3(0, 0, 0)

3.把栅格图像放在z=-1的位置。

//从相机视点到片元的射线
vec3 RayDir(in vec2 coord) {
  return normalize(vec3(coord, -1) - CAMERA_POS);
}

4.给点光源一个固定位置。

// 点光源位置
#define LIGHT_POS vec3(1, 1, 0)

效果如下:

image-20220908124804474

接下来,咱们调整一下相机视点看看。

5.把相机视点往右移1个单位。

#define CAMERA_POS vec3(1, 0, 0)

效果如下:

image-20220914165823920

从上面的效果中,我们可以发现球体发生了两个变化:

  • 球体往右移动了
  • 球体变长了

这个现象出现的原理如下:

image-20220914223949666

通过上图可知,线段b在线段a的右侧,且比a要长。

而我们想要都是:相机从另一侧看向球体的时候,球体就在视图中心,不产生拉伸,大小会随透视而变,球体会换一个角度显示。

在此我们就需要把思维分解开想象了。

02-根据相机旋转光线推进方向

1.基于视线2旋转视点1处默认的光线推进方向。

image-20220915095720334

2.从视点2,基于旋转后的光线推进方向,推进光线。

image-20220915100928576

这样我们就可以看到一个正常的球体。

注:在视点2处看到的球体要比在视点1处看到的球体小,即b<a,因为球体到视点2的距离变远了。

03-代码实现

1.声明相机数据。

// 相机视点位
#define CAMERA_POS vec3(1, 0, 0)
// 相机目标点
#define CAMERA_TARGET vec3(0, 0, -2)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

对于视点、目标点和上方向的概念如下图所示:

image-20211109204350902

我在WebGL的视图矩阵里对相关概念有详细讲解。

2.根据相机的状态,构建用于旋转光线的旋转矩阵。

// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

上面的向量a、b、c 和相机视点、目标点、上方向的关系如下图所示:

image-20211126112152482

其运算原理,我在WebGL的视图矩阵里同样有详细讲解。

注:RotateMatrix()方法返回的并不是视图矩阵,但它所涉及的知识点是视图矩阵的一部分。

3.用上面的旋转矩阵,旋转默认(视点在零点)的光线推进方向。

// 光线推进
vec3 RayMarch(vec2 coord) {
  ……
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  ……
}

在后面的代码里,便会从相机视点,基于旋转后的光线推进方向,推进光线。

// 光线推进
vec3 RayMarch(vec2 coord) {
  ……
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  ……
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    ……
  }
  ……
}

效果如下:

image-20220915104340464

由上图我们可以看到,球体就在视图中心,没有发生拉伸,大小会随透视变小了一点,球体换了一个角度显示。

4.我们还可以改变目标点测试一下。

#define CAMERA_TARGET vec3(.5, 0, -2)

效果如下:

image-20220915104650023

相机往右看,则球体往左移动。

5.我们还可以让相机视点绕球心处的y轴逆时针旋转。

#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(1, 0, 0)-SPHERE_POS)+SPHERE_POS

效果如下:

2

好了,现在我们已经把透视相机写出来了。

整体代码如下:

// 坐标系缩放
#define PROJECTION_SCALE  1.

// 球体的球心位置
#define SPHERE_POS vec3(0, 0, -2)
// 球体的半径
#define SPHERE_R 1.0
// 球体的漫反射系数
#define SPHERE_KD vec3(1)

// 相机视点位
#define CAMERA_POS mat3(cos(iTime),0,sin(iTime),0,1,0,-sin(iTime),0,cos(iTime))*(vec3(1, 0, 0)-SPHERE_POS)+SPHERE_POS

// 相机目标点
#define CAMERA_TARGET vec3(0, 0, -2)
// 上方向
#define CAMERA_UP vec3(0, 1, 0)

// 光线推进的起始距离 
#define RAYMARCH_NEAR 0.1
// 光线推进的最远距离
#define RAYMARCH_FAR 128.
// 光线推进次数
#define RAYMARCH_TIME 20
// 当推进后的点位距离物体表面小于RAYMARCH_PRECISION时,默认此点为物体表面的点
#define RAYMARCH_PRECISION 0.001

// 点光源位置
#define LIGHT_POS vec3(1, 1, 0)

// 相邻点的抗锯齿的行列数
#define AA 3

// 投影坐标系
vec2 ProjectionCoord(in vec2 coord) {
  return PROJECTION_SCALE * 2. * (coord - 0.5 * iResolution.xy) / min(iResolution.x, iResolution.y);
}

//从相机视点到片元的射线
vec3 RayDir(in vec2 coord) {
  return normalize(vec3(coord, 0) - CAMERA_POS);
}

//球体的SDF模型
float SDFSphere(vec3 coord) {
  return length(coord - SPHERE_POS) - SPHERE_R;
}

// 计算球体的法线
vec3 SDFNormal(in vec3 p) {
  const float h = 0.0001;
  const vec2 k = vec2(1, -1);
  return normalize(k.xyy * SDFSphere(p + k.xyy * h) +
    k.yyx * SDFSphere(p + k.yyx * h) +
    k.yxy * SDFSphere(p + k.yxy * h) +
    k.xxx * SDFSphere(p + k.xxx * h));
}

// 打光
vec3 AddLight(vec3 positon) {
  // 当前着色点的法线
  vec3 n = SDFNormal(positon);
  // 当前着色点到光源的方向
  vec3 lightDir = normalize(LIGHT_POS - positon);
  // 漫反射
  vec3 diffuse = SPHERE_KD * max(dot(lightDir, n), 0.);
  // 环境光
  float amb = 0.15 + dot(-lightDir, n) * 0.2;
  // 最终颜色
  return diffuse + amb;
}

// 视图旋转矩阵
mat3 RotateMatrix() {
  //基向量c,视线
  vec3 c = normalize(CAMERA_POS - CAMERA_TARGET);
  //基向量a,视线和上方向的垂线
  vec3 a = cross(CAMERA_UP, c);
  //基向量b,修正上方向
  vec3 b = cross(c, a);
  //正交旋转矩阵
  return mat3(a, b, c);
}

// 光线推进
vec3 RayMarch(vec2 coord) {
  float d = RAYMARCH_NEAR;
  // 从相机视点到当前片元的射线
  vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));
  // 片元颜色
  vec3 color = vec3(0);
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = CAMERA_POS + d * rd;
    // 光线推进后的点位到球体的有向距离
    float curD = SDFSphere(p);
    // 若有向距离小于一定的精度,默认此点在球体表面
    if(curD < RAYMARCH_PRECISION) {
      color = AddLight(p);
      break;
    }
    // 距离累加
    d += curD;
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 RayMarch_anti(vec2 fragCoord) {
  // 初始颜色
  vec3 color = vec3(0);
  // 行列的一半
  float aa2 = float(AA / 2);
  // 逐行列遍历
  for(int y = 0; y < AA; y++) {
    for(int x = 0; x < AA; x++) {
      // 基于像素的偏移距离
      vec2 offset = vec2(float(x), float(y)) / float(AA) - aa2;
      // 投影坐标位
      vec2 coord = ProjectionCoord(fragCoord + offset);
      // 累加周围片元的颜色
      color += RayMarch(coord);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

/* 绘图函数,画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 光线推进
  vec3 color = RayMarch_anti(fragCoord);
  // 最终颜色
  fragColor = vec4(color, 1);
}

扩展

熟悉相机的同学肯定会想到透视相机的视锥体的垂直夹角,以及正交相机。

透视相机的视锥体的垂直夹角跟栅格图像的z值有关,即下面的-1:

vec3 rd = normalize(RotateMatrix() * vec3(coord, -1));

我们可以利用三角函数,基于夹角和栅格图像的高度来计算这个z值。

至于正交相机会更加简单,直接默认光线推进的初始方向都为-z即可。

对于这两个概念我就先不写了,反正原理想通了就好了。

大家没事也可以自己练练,可以将其与传统相机的视图矩阵、投影矩阵相互印证,巩固所学。

参考链接:space.bilibili.com/10707223/ch…