Whitted-Style

516 阅读26分钟

源码:github.com/buglas/shad…

1-Whitted-Style 简介

Whitted-Style 是一种光线追踪模型,由Turner Whitted提出,可以制作反射和折射效果。。

Turner Whitted 在1979 就将光线追踪引入到计算机图形学,现在已经加入了 NVidia 研究院。

我们可以通过下图来理解Whitted-Style。

image-20221102194534411

学过Ray Marching 的同学会很容易的理解这张图。

首先,我们的眼睛看向栅格图像的某一个像素时,会形成一条primary ray,我暂且将其翻译成初始射线了。

当primary ray 碰撞到物体时,会发生反射或者折射,这种反射或者折射后的射线就叫做secondary rays,我们暂且将其翻译为二次射线。当然,这种二次射线还可能再碰撞到其它物体,发生三次、四次反射或折射。

除此之外,我们还要判断初始射线在反射或折射的过程中,所碰撞到的着色点是否在阴影中。这就需要向光源打一条射线,这种射线就叫做shadow rays。

Whitted-Style 的原理很简单,其中的重点算法就是反射和折射。

2-反射的概念

在此为了照顾像我一样高中没有学过物理的同学,咱们在敲代码之前,先把概念说一下。

2-1-基本定义

反射是指某种波在传播到不同物质时,在分界面上改变传播方向,并返回到原来物质中的现象。

常见的波有光波、水波和声波,而我们这里要说就是光波。

反射定律表明,对于镜面反射(例如在镜子处) ,波入射到表面的角度等于它被反射的角度。

2-2-物理定律

Reflection_angles

已知:

  • 镜面mirror,其法线为normal
  • 入射光线PO
  • PO在O点撞击镜面
  • 反射光线为OQ
  • 入射角θi
  • 反射角θr

反射的物理定律为:

  • 入射光线PO、反射光线OQ 和法线normal 都在同一平面内
  • 入射光线PO、反射光线OQ 分居法线normal 两侧
  • 入射角θi等于反射角θr
  • 在反射中,光路可逆。

道理已通,接下来咱们可以做个反射球玩玩。

3-反射球

3-1-一次反射

假设球体是镜面发射的,而地面只有漫反射。

接下来我们做一个球体反射一次地面的效果:

反射

整体代码如下:

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

// 球体的半径
#define SPHERE_R 1.2
// 球体的球心位置
#define SPHERE_POS vec3(0, SPHERE_R, -4)
// 球体的漫反射系数
#define SPHERE_KD vec3(0,0.6,0.9)

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

//基向量c,视线
#define C normalize(CAMERA_POS - CAMERA_TARGET)
//基向量a,视线和上方向的垂线
#define A cross(CAMERA_UP, C)
//基向量b,修正上方向
#define B cross(C, A)
// 相机旋转矩阵
#define  CAMERA_ROTATE mat3(A,B,C)

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

// 点光源位置
#define LIGHT_POS vec3(4,5, -3)

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

// 栅格图像的z位置
#define SCREEN_Z -1.

// 要渲染的对象集合
float SDFArray[2];

/* 
距离场最小的物体:
0 地面
1 球体
 */
int minObj = 0;

// RayMarch 数据的结构体
struct RayMarchData {
  // 是否碰撞到物体  
  bool crash;
  // 射线碰撞到的物体
  int obj;
  // 射线碰撞到的着色点位置
  vec3 ro;
  // 射线碰撞到着色点时的反射方向
  vec3 reflect;
  // 射线碰撞到的着色点的颜色
  vec3 color; 
};

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

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

// 水平面的SDF模型
float SDFPlane(vec3 coord) {
  return coord.y;
}

// 所有的SDF模型
float SDFAll(vec3 coord) {
  SDFArray[0] = SDFPlane(coord);
  SDFArray[1] = SDFSphere(coord);
  float min = SDFArray[0];
  minObj = 0;
  for(int i = 1; i < 2; i++) {
    if(min > SDFArray[i]) {
      min = SDFArray[i];
      minObj = i;
    }
  }
  return min;
}

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

// 软投影
float SoftShadow(in vec3 ro, in vec3 rd, float k) {
  float res = 1.;
  for(float t = RAYMARCH_NEAR; t < RAYMARCH_FAR;) {
    float h = SDFAll(ro + rd * t);
    if(h < RAYMARCH_PRECISION) {
      return 0.;
    }
    res = min(res, k * h / t);
    t += h;
  }
  return res;
}


// 棋盘格
float Checkers(in vec2 uv) {
  vec2 grid = floor(uv*2.);
  return mod(grid.x + grid.y, 2.);
}

// 获取漫反射系数
vec3 getKD(vec3 pos){
  if(minObj == 0) {
    float check = Checkers(pos.xz);
    return vec3(check * 0.8 + 0.2);
  } else if(minObj == 1) {
    return SPHERE_KD;
  }
}

// 打光
vec3 AddLight(vec3 positon,vec3 n,vec3 kd) {
  // 当前着色点到光源的方向
  vec3 lightDir = normalize(LIGHT_POS - positon);
  // 漫反射
  vec3 diffuse = kd * max(dot(lightDir, n), 0.);
  // 投影
  float shadow = SoftShadow(positon, lightDir, 8.);
  diffuse *= shadow;
  // 最终颜色
  return diffuse;
}

// 将RayMarch与渲染分离
RayMarchData RayMarch(vec3 ro, vec3 rd) {
  // 最近距离
  float d = RAYMARCH_NEAR;
  // 建立RayMarchData数据
  RayMarchData rm;
  rm = RayMarchData(false,0,vec3(0),vec3(0),vec3(0));
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = ro + d * rd;
    // 光线推进后的点位到模型的有向距离
    float curD = SDFAll(p);
    // 若有向距离小于一定的精度,默认此点在模型表面
    if(curD < RAYMARCH_PRECISION) {
      // 发生碰撞
      rm.crash=true;
      // 碰撞到的物体
      rm.obj=minObj;
      // 光源
      rm.ro=p;
      // 当前着色点的法线
      vec3 n = SDFNormal(p);
      // 光线反射方向
      rm.reflect=reflect(rd,n);
      // 碰到的着色点的漫反射系数
      vec3 kd=getKD(p);
      // 碰到的着色点的颜色
      rm.color=AddLight(p,n,kd);
      break;
    }
    // 距离累加
    d += curD;
  }
  return rm;
}


// 渲染
vec3 Render(vec3 rd) {
  // 初始RayMarch数据
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd);
  // 颜色
  vec3 color=rm0.color;
  // 球体
  if(rm0.crash&&rm0.obj==1){
    // 反射数据
    RayMarchData rmNext = RayMarch(rm0.ro, rm0.reflect); 
    if(rmNext.crash){
      color=color*0.4+rmNext.color*0.6;
    }
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 Render_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);
      // 光线推进的方向
      vec3 rd = normalize(CAMERA_ROTATE * vec3(coord, SCREEN_Z));
      // 累加周围片元的颜色
      color += Render(rd);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

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

解释一下其实现过程。

1.定义要渲染的对象集合。

float SDFArray[2];

当前只有地面和球体2个对象。

2.声明RayMarching 中距离场最小的对象。

/* 
距离场最小的物体:
0 地面
1 球体
 */
int minObj = 0;

minObj 会随着射线与物体的碰撞而更新。

3.定义 RayMarch 数据的结构体

struct RayMarchData {
  // 是否碰撞到物体  
  bool crash;
  // 射线碰撞到的物体
  int obj;
  // 射线碰撞到的着色点位置
  vec3 ro;
  // 射线碰撞到着色点时的反射方向
  vec3 reflect;
  // 射线碰撞到的着色点的颜色
  vec3 color; 
};

4.构建SDF模型

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

// 水平面的SDF模型
float SDFPlane(vec3 coord) {
  return coord.y;
}

// 所有的SDF模型
float SDFAll(vec3 coord) {
  SDFArray[0] = SDFPlane(coord);
  SDFArray[1] = SDFSphere(coord);
  float min = SDFArray[0];
  minObj = 0;
  for(int i = 1; i < 2; i++) {
    if(min > SDFArray[i]) {
      min = SDFArray[i];
      minObj = i;
    }
  }
  return min;
}

SDFAll()中除了会返回所有SDF中的最小距离,还会记录这个最小距离属于哪个物体。

5.建立RayMarch方法

// 将RayMarch与渲染分离
RayMarchData RayMarch(vec3 ro, vec3 rd) {
  // 最近距离
  float d = RAYMARCH_NEAR;
  // 建立RayMarchData数据
  RayMarchData rm;
  rm = RayMarchData(false,0,vec3(0),vec3(0),vec3(0));
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = ro + d * rd;
    // 光线推进后的点位到模型的有向距离
    float curD = SDFAll(p);
    // 若有向距离小于一定的精度,默认此点在模型表面
    if(curD < RAYMARCH_PRECISION) {
      // 发生碰撞
      rm.crash=true;
      // 碰撞到的物体
      rm.obj=minObj;
      // 光源
      rm.ro=p;
      // 当前着色点的法线
      vec3 n = SDFNormal(p);
      // 光线反射方向
      rm.reflect=reflect(rd,n);
      // 碰到的着色点的漫反射系数
      vec3 kd=getKD(p);
      // 碰到的着色点的颜色
      rm.color=AddLight(p,n,kd);
      break;
    }
    // 距离累加
    d += curD;
  }
  return rm;
}

RayMarch()方法会将射线的推进状态存储到RayMarchData中,然后返回。

reflect(i,n)方法是glsl内置方法,它可以基于入射光线和着色点的法线计算反射光线。

image-20221122215638882

上图是我从《WebGL编程指南》里截的。

rm.color 存储的是计算完光照和阴影后的颜色。

6.在渲染方法中,根据Ray Marching 信息计算片元原色。

// 渲染
vec3 Render(vec3 rd) {
  // 初始RayMarch数据
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd);
  // 颜色
  vec3 color=rm0.color;
  // 球体
  if(rm0.crash&&rm0.obj==1){
    // 反射数据
    RayMarchData rmNext = RayMarch(rm0.ro, rm0.reflect); 
    if(rmNext.crash){
      color=color*0.4+rmNext.color*0.6;
    }
  }
  return color;
}

若primary ray 碰撞的物体是球体,即rm0.obj==1,就反射。

若primary ray 碰撞的物体是地面,那就不做反射,直接取primary ray 所射到的着色点的颜色。

反射的时候,以当前的碰撞点rm0.ro 为起点,以反射方向为方向进行RayMarching。

若反射到其它着色点上,就按照一定比例,让其与上一次碰撞到的着色点做合成。

当前的球体还只是做了一次反射,实际上它是可以进行多次反射的。

3-2-多次反射

现在为了观察效果,我在场景里又添加了一个box,并且假设所有物体都是镜面反射的,并且可以反射多次,效果如下:

image-20230921140007627

多次反射的原理就是,在实现碰到一个物体上时,再进行反弹。在一定反弹次数内,将碰的到物体颜色按照一定的权重进行累积。

代码如下:

// 渲染
vec3 Render(vec3 rd) {
  // 初始RayMarch数据
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd);
  // 颜色
  vec3 color=rm0.color;
  // 光线反射的衰减系数
  float ratio=0.6;
  // 暂存数据
  vec3 curRo=rm0.ro;
  vec3 curRd=rm0.reflect;
  for(int i=0;i<4;i++){
    // 下一次RayMarch数据
    RayMarchData rmNext = RayMarch(curRo, curRd); 
    if(rmNext.crash){
      color=color*(1.-ratio)+rmNext.color*ratio;
      curRo=rmNext.ro;
      curRd=rmNext.reflect;
      ratio*=ratio;
    }else{
      break;
    }
  }
  return color;
}

当前是反弹了4次,其颜色就是按照一定的衰减系数累加。

当前这个衰减系数是没有物理依据的,只是对反射规律的感性模拟,之后我们会说比较严谨的光照度量学。

当前实例的整体代码如下:

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

// 球体的半径
#define SPHERE_R 1.2
// 球体的球心位置
#define SPHERE_POS vec3(1.3, SPHERE_R, 0)
// 球体的漫反射系数
#define SPHERE_KD vec3(0,0.6,0.9)

// 长方体的中心位置
#define RECT_POS vec3(-1.3, 0, 0)
// 长方体的尺寸
#define RECT_SIZE vec3(.2,2.6,2.)
// 长方体的漫反射系数
#define RECT_KD vec3(1,1,0)

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

//基向量c,视线
#define C normalize(CAMERA_POS - CAMERA_TARGET)
//基向量a,视线和上方向的垂线
#define A cross(CAMERA_UP, C)
//基向量b,修正上方向
#define B cross(C, A)
// 相机旋转矩阵
#define  CAMERA_ROTATE mat3(A,B,C)

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

// 点光源位置
#define LIGHT_POS vec3(4,5, 3)

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

// 栅格图像的z位置
#define SCREEN_Z -1.

// 要渲染的对象集合
float SDFArray[3];

/* 
距离场最小的物体:
0 地面
1 球体
 */
int minObj = 0;

// RayMarch 数据的结构体
struct RayMarchData {
  // 是否碰撞到物体  
  bool crash;
  // 射线碰撞到的物体
  int obj;
  // 射线碰撞到的着色点位置
  vec3 ro;
  // 射线碰撞到着色点时的反射方向
  vec3 reflect;
  // 射线碰撞到的着色点的颜色
  vec3 color; 
};

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

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

// 长方体的的SDF模型
float SDFRect(vec3 coord) {
  vec3 d = abs(coord - RECT_POS) - RECT_SIZE;
  return length(max(d, 0.)) + min(max(d.x, max(d.y, d.z)), 0.);
}

// 水平面的SDF模型
float SDFPlane(vec3 coord) {
  return coord.y;
}

// 所有的SDF模型
float SDFAll(vec3 coord) {
  SDFArray[0] = SDFPlane(coord);
  SDFArray[1] = SDFSphere(coord);
  SDFArray[2] = SDFRect(coord);
  float min = SDFArray[0];
  minObj = 0;
  for(int i = 1; i < 3; i++) {
    if(min > SDFArray[i]) {
      min = SDFArray[i];
      minObj = i;
    }
  }
  return min;
}

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

// 软投影
float SoftShadow(in vec3 ro, in vec3 rd, float k) {
  float res = 1.;
  for(float t = RAYMARCH_NEAR; t < RAYMARCH_FAR;) {
    float h = SDFAll(ro + rd * t);
    if(h < RAYMARCH_PRECISION) {
      return 0.;
    }
    res = min(res, k * h / t);
    t += h;
  }
  return res;
}


// 棋盘格
float Checkers(in vec2 uv) {
  vec2 grid = floor(uv*2.);
  return mod(grid.x + grid.y, 2.);
}

// 获取漫反射系数
vec3 getKD(vec3 pos){
  if(minObj == 0) {
    float check = Checkers(pos.xz);
    return vec3(check * 0.8 + 0.2);
  } else if(minObj == 1) {
    return SPHERE_KD;
  }else if(minObj == 2){
    return RECT_KD;
  }
}

// 打光
vec3 AddLight(vec3 positon,vec3 n,vec3 kd) {
  // 当前着色点到光源的方向
  vec3 lightDir = normalize(LIGHT_POS - positon);
  // 漫反射
  vec3 diffuse = kd * max(dot(lightDir, n), 0.);
  // 投影
  float shadow = SoftShadow(positon, lightDir, 8.);
  diffuse *= shadow;
  // 最终颜色
  return diffuse;
}

// 将RayMarch与渲染分离
RayMarchData RayMarch(vec3 ro, vec3 rd) {
  // 最近距离
  float d = RAYMARCH_NEAR;
  // 建立RayMarchData数据
  RayMarchData rm;
  rm = RayMarchData(false,0,vec3(0),vec3(0),vec3(0));
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = ro + d * rd;
    // 光线推进后的点位到模型的有向距离
    float curD = SDFAll(p);
    // 若有向距离小于一定的精度,默认此点在模型表面
    if(curD < RAYMARCH_PRECISION) {
      // 发生碰撞
      rm.crash=true;
      // 碰撞到的物体
      rm.obj=minObj;
      // 光源
      rm.ro=p;
      // 当前着色点的法线
      vec3 n = SDFNormal(p);
      // 光线反射方向
      rm.reflect=reflect(rd,n);
      // 碰到的着色点的漫反射系数
      vec3 kd=getKD(p);
      // 碰到的着色点的颜色
      rm.color=AddLight(p,n,kd);
      break;
    }
    // 距离累加
    d += curD;
  }
  return rm;
}

// 渲染
vec3 Render(vec3 rd) {
  // 初始RayMarch数据
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd);
  // 颜色
  vec3 color=rm0.color;
  // 光线反射的衰减系数
  float ratio=0.6;
  // 暂存数据
  vec3 curRo=rm0.ro;
  vec3 curRd=rm0.reflect;
  for(int i=0;i<4;i++){
    // 下一次RayMarch数据
    RayMarchData rmNext = RayMarch(curRo, curRd); 
    if(rmNext.crash){
      color=color*(1.-ratio)+rmNext.color*ratio;
      curRo=rmNext.ro;
      curRd=rmNext.reflect;
      ratio*=ratio;
    }else{
      break;
    }
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 Render_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);
      // 光线推进的方向
      vec3 rd = normalize(CAMERA_ROTATE * vec3(coord, SCREEN_Z));
      // 累加周围片元的颜色
      color += Render(rd);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

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

关于Whitted-Style 中的反射我们就说到这,接下来我们再说折射。

4-折射的概念

4-1-基本定义

折射是波从一种介质传播到另一种介质时的重定向。

Refraction_photo

波的重定向可能是由波的速度变化或介质变化引起的。

光波的折射是最常见的,声波和水波等其它波也会发生折射,我们这里只说光波。

4-2-物理定律

波的折射的程度取决于两方面:

  • 波速的变化
  • 波传播的初始方向相对于速度变化的方向

这么说大家可能不太好理解,咱们图解。

Snells_law

已知:

  • 左侧的白色区域和右侧的蓝色区域是两种介质
  • 光在左右两种介质的折射率分别是n1、n2
  • 光在左右两种介质中的速度分别是v1、v2
  • normal 是两种介质之间的介面的法线
  • 入射光线PO
  • PO在O点撞击介面
  • 折射光线为OQ
  • 入射角θ1
  • 折射角θ2

则折射存在以下等式:

image-20221212163536871

折射的物理定律为:

  • 入射光线、折射光线和法线在同一平面上。
  • 入射光线和折射光线分居法线两侧。
  • 入射光线和折射光线分居介面两侧。
  • 当入射光从速度大的介质射入速度小的介质时,折射角小于入射角;反之,折射角大于入射角。
  • 入射角的大小和折射角的大小成正比。
  • 当光线垂直射向介质表面时,传播方向不变。
  • 在折射中,光路可逆。

折射的基本概念就是这样,接下来咱们画个折射球。

5-折射球

通过上面的折射概念我们可以知道,光线要穿透一个玻璃球,会发生2次折射:

  • 光线从空气到玻璃的折射,这样折射出的光叫入射光。
  • 光线从玻璃到空气的折射,这样折射出的光叫出射光。

为了循序渐进,我们先写个从空气到玻璃的折射试试。

5-1-从空气到玻璃的折射

效果如下:

shadertoy

整体代码如下:

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

// 球体的半径
#define SPHERE_R 1.2
// 球体的球心位置
#define SPHERE_POS vec3(0, SPHERE_R, -4)
// 球体的漫反射系数
#define SPHERE_KD vec3(0,0.6,0.9)

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

//基向量c,视线
#define C normalize(CAMERA_POS - CAMERA_TARGET)
//基向量a,视线和上方向的垂线
#define A cross(CAMERA_UP, C)
//基向量b,修正上方向
#define B cross(C, A)
// 相机旋转矩阵
#define  CAMERA_ROTATE mat3(A,B,C)

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

// 点光源位置
#define LIGHT_POS vec3(4,5, -3)

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

// 栅格图像的z位置
#define SCREEN_Z -1.

// 要渲染的对象集合
float SDFArray[2];

/* 
距离场最小的物体:
0 地面
1 球体
 */
int minObj = 0;


// RayMarch 数据的结构体
struct RayMarchData {
  // 是否碰撞到物体  
  bool crash;
  // 射线碰撞到的物体
  int obj;
  // 射线碰撞到的着色点位置
  vec3 ro;
  // 射线碰撞到着色点时的弹射方向
  vec3 reflect;
  // 射线碰撞到的着色点的颜色
  vec3 color; 
  // 法线
  vec3 normal;
};

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

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

// 水平面的SDF模型
float SDFPlane(vec3 coord) {
  return coord.y;
}

// 所有的SDF模型
float SDFAll(vec3 coord) {
  SDFArray[0] = SDFPlane(coord);
  SDFArray[1] = SDFSphere(coord);
  float min = SDFArray[0];
  minObj = 0;
  for(int i = 1; i < 2; i++) {
    if(min > SDFArray[i]) {
      min = SDFArray[i];
      minObj = i;
    }
  }
  return min;
}

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

// 棋盘格
float Checkers(in vec2 uv) {
  vec2 grid = floor(uv*2.);
  return mod(grid.x + grid.y, 2.);
}

// 获取漫反射系数
vec3 getKD(vec3 pos){
  if(minObj == 0) {
    float check = Checkers(pos.xz);
    return vec3(check * 0.8 + 0.2);
  } else if(minObj == 1) {
    return SPHERE_KD;
  }
}

// 打光
vec3 AddLight(vec3 positon,vec3 n,vec3 kd) {
  // 当前着色点到光源的方向
  vec3 lightDir = normalize(LIGHT_POS - positon);
  // 漫反射
  vec3 diffuse = kd * max(dot(lightDir, n), 0.);
  // 最终颜色
  return diffuse;
}

// 根据索引计算SDF
float getSDFbyInd(int ind,vec3 p){
  if(ind==0){
    return SDFPlane(p);
  }else if(ind==1){
    return SDFSphere(p);
  }else{
    return SDFAll(p);
  }
}

// 将RayMarch与渲染分离
RayMarchData RayMarch(vec3 ro, vec3 rd,int curSDF) {
  // 最近距离
  float d = RAYMARCH_NEAR;
  // 建立RayMarchData数据
  RayMarchData rm;
  rm = RayMarchData(false,0,vec3(0),vec3(0),vec3(0),vec3(0));
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = ro + d * rd;
    // 光线推进后的点位到模型的有向距离
    float curD = getSDFbyInd(curSDF,p);
    // 若有向距离小于一定的精度,默认此点在模型表面
    if(curD < RAYMARCH_PRECISION) {
      // 发生碰撞
      rm.crash=true;
      // 碰撞到的物体
      rm.obj=minObj;
      // 光源
      rm.ro=p;
      // 当前着色点的法线
      vec3 n = SDFNormal(p);
      rm.normal=n;
      // 光线反射方向
      rm.reflect=reflect(rd,n);
      // 碰到的着色点的漫反射系数
      vec3 kd=getKD(p);
      // 碰到的着色点的颜色
      rm.color=AddLight(p,n,kd);
      break;
    }
    // 距离累加
    d += curD;
  }
  return rm;
}

// 渲染
vec3 Render(vec3 rd) {
  // 初始RayMarch数据
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd,-1);
  // 颜色
  vec3 color=rm0.color;
  // 球体
  if(rm0.crash&&rm0.obj==1){
    // 计算光线进入球体时的入射方向
    vec3 incidentDir=refract(rm0.reflect,rm0.normal,1./1.2);
    // 基于入射角,追踪平面
    RayMarchData rm1 = RayMarch(rm0.ro,incidentDir,0);
    if(rm1.crash){
      color=rm1.color;
    }
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 Render_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);
      // 光线推进的方向
      vec3 rd = normalize(CAMERA_ROTATE * vec3(coord, SCREEN_Z));
      // 累加周围片元的颜色
      color += Render(rd);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

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

在上面的代码中,RayMarch 方法和之前有所不同,我又给它增加了一个参数curSDF。

curSDF 会告诉RayMarch 方法要追踪那个物体。

我们通过渲染方法解释一下其折射过程。

// 渲染
vec3 Render(vec3 rd) {
  // 初始RayMarch数据
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd,-1);
  // 颜色
  vec3 color=rm0.color;
  // 球体
  if(rm0.crash&&rm0.obj==1){
    // 计算光线进入球体时的入射方向
    vec3 incidentDir=refract(rm0.reflect,rm0.normal,1./1.2);
    // 以入射角为方向追踪平面
    RayMarchData rm1 = RayMarch(rm0.ro,incidentDir,0);
    if(rm1.crash){
      color=rm1.color;
    }
  }
  return color;
}

rm0 打出了好多条光。

有的打到地面上,我们就渲染地面。

有的打到了球体上,那我们就让它做折射(incidentDir),这样折射的方向我们就叫它入射方向。

折射出的光线打到其它物体上时,我们就按照一定比例,把其它物体的颜色和之前球体的颜色进行合成。

refract() 是glsl的内置的折射方法。

我们可以在《WebGL 编程指南》中找到其用法:

image-20221211112356929

我当前的这种做法其实是不对的,因为入射光从玻璃球中出来的时候还会再次折射。

5-2-从玻璃到空气的折射

效果如下:

refract

整体代码如下:

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

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

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

//基向量c,视线
#define C normalize(CAMERA_POS - CAMERA_TARGET)
//基向量a,视线和上方向的垂线
#define A cross(CAMERA_UP, C)
//基向量b,修正上方向
#define B cross(C, A)
// 相机旋转矩阵
#define  CAMERA_ROTATE mat3(A,B,C)

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

// 点光源位置
#define LIGHT_POS vec3(4,5, -3)

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

// 栅格图像的z位置
#define SCREEN_Z -1.

// 玻璃折射率
#define GLASS_REFRACTIVITY 1.5
#define GLASS_REFRACTIVITY_INVERT 1./GLASS_REFRACTIVITY

// 要渲染的对象集合
float SDFArray[2];

/* 
距离场最小的物体:
0 地面
1 球体
 */
int minObj = 0;


// RayMarch 数据的结构体
struct RayMarchData {
  // 是否碰撞到物体  
  bool crash;
  // 射线碰撞到的物体
  int obj;
  // 射线碰撞到的着色点位置
  vec3 ro;
  // 射线碰撞到着色点时的反射方向
  vec3 reflect;
  // 射线碰撞到的着色点的颜色
  vec3 color; 
  // 法线
  vec3 normal;
};

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

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

// 水平面的SDF模型
float SDFPlane(vec3 coord) {
  return coord.y;
}

// 所有的SDF模型
float SDFAll(vec3 coord) {
  SDFArray[0] = SDFPlane(coord);
  SDFArray[1] = SDFSphere(coord);
  
  float min = SDFArray[0];
  minObj = 0;
  if(SDFArray[1]<SDFArray[0]){
    min = SDFArray[1];
    minObj = 1;
  }
  return min;
}

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

// 软投影
float SoftShadow(in vec3 ro, in vec3 rd, float k) {
  float res = 1.;
  for(float t = RAYMARCH_NEAR; t < RAYMARCH_FAR;) {
    float h = SDFAll(ro + rd * t);
    if(h < RAYMARCH_PRECISION) {
      return 0.;
    }
    res = min(res, k * h / t);
    t += h;
  }
  return res;
}


// 棋盘格
float Checkers(in vec2 uv) {
  vec2 grid = floor(uv*2.);
  return mod(grid.x + grid.y, 2.);
}

// 获取漫反射系数
vec3 getKD(vec3 pos){
  if(minObj == 0) {
    float check = Checkers(pos.xz);
    return vec3(check * 0.8 + 0.2);
  } else if(minObj == 1) {
    return SPHERE_KD;
  }
}

// 打光
vec3 AddLight(vec3 positon,vec3 n,vec3 kd) {
  // 当前着色点到光源的方向
  vec3 lightDir = normalize(LIGHT_POS - positon);
  // 漫反射
  vec3 diffuse = kd * max(dot(lightDir, n), 0.);
  // 投影
  float shadow = SoftShadow(positon, lightDir, 16.);
  diffuse *= shadow+0.2;
  // 最终颜色
  return diffuse;
}

// 根据索引计算SDF
float getSDFbyInd(int ind,vec3 p){
  if(ind==0){
    return SDFPlane(p);
  }else if(ind==1){
    return SDFSphere(p);
  }else{
    return SDFAll(p);
  }
}

// 将RayMarch与渲染分离
RayMarchData RayMarch(vec3 ro, vec3 rd,int curSDF) {
  // 最近距离
  float d = RAYMARCH_NEAR;
  // 建立RayMarchData数据
  RayMarchData rm;
  rm = RayMarchData(false,0,vec3(0),vec3(0),vec3(0),vec3(0));
  for(int i = 0; i < RAYMARCH_TIME && d < RAYMARCH_FAR; i++) {
    // 光线推进后的点位
    vec3 p = ro + d * rd;
    // 光线推进后的点位到模型的有向距离
    float curD = getSDFbyInd(curSDF,p);
    curD=abs(curD);
    // 若有向距离小于一定的精度,默认此点在模型表面
    if(curD < RAYMARCH_PRECISION) {
      // 发生碰撞
      rm.crash=true;
      // 碰撞到的物体
      rm.obj=minObj;
      // 光源
      rm.ro=p;
      // 当前着色点的法线
      vec3 n = SDFNormal(p);
      rm.normal=n;
      // 光线反射方向
      rm.reflect=reflect(rd,n);
      // 碰到的着色点的漫反射系数
      vec3 kd=getKD(p);
      // 碰到的着色点的颜色
      rm.color=AddLight(p,n,kd);
      break;
    }
    // 距离累加
    d += curD;
  }
  return rm;
}

// 渲染
vec3 Render(vec3 rd) {
  // 初始RayMarch数据,可以打到球体和平面表面
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd,-1);
  // 颜色
  vec3 color=rm0.color;
  // 球体
  if(rm0.crash&&rm0.obj==1){
    // 光线反射的衰减系数
    float ratio0=0.7;
    float ratio1=1.-ratio0;

    // 计算光线进入球体时的入射方向
    vec3 incidentDir=refract(rm0.reflect,rm0.normal,GLASS_REFRACTIVITY_INVERT);
    // 基于入射方向,在球体内部追踪球体
    RayMarchData rm1 = RayMarch(rm0.ro, incidentDir,1);
    if(rm1.crash){
      // 计算光线出球体时的出射方向
      vec3 outDir=refract(rm1.reflect,rm1.normal,GLASS_REFRACTIVITY);
      // 基于出射方向,追踪平面
      RayMarchData rm2 = RayMarch(rm1.ro, outDir,0);
      if(rm2.crash){
        // 折射颜色
        color=color*ratio1+rm2.color*color;
      }
    }

    // 反射
    RayMarchData rmNext = RayMarch(rm0.ro, rm0.reflect,-1); 
    if(rmNext.crash){
      // 在折射颜色的基础上,合成反射颜色
      color=color*ratio1+rmNext.color*ratio0;
    }
  }
  return color;
}

// 抗锯齿 Anti-Aliasing
vec3 Render_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);
      // 光线推进的方向
      vec3 rd = normalize(CAMERA_ROTATE * vec3(coord, SCREEN_Z));
      // 累加周围片元的颜色
      color += Render(rd);
    }
  }
  // 返回周围颜色的均值
  return color / float(AA * AA);
}

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

上面代码里的RayMarch方法和之前有点不一样。

我在求出有向距离的时候,又取了个绝对值。

// 光线推进后的点位到模型的有向距离
float curD = getSDFbyInd(curSDF,p);
curD=abs(curD);
// 若有向距离小于一定的精度,默认此点在模型表面
if(curD < RAYMARCH_PRECISION) {
……
}

这是因为光线在球体内部推进的时候,其距离场会一直小于0,这是无法结合RAYMARCH_PRECISION 做碰撞判断的。

我们通过渲染方法解释一下其折射过程。

vec3 Render(vec3 rd) {
  // 初始RayMarch数据,可以打到球体和平面表面
  RayMarchData rm0 = RayMarch(CAMERA_POS, rd,-1);
  // 颜色
  vec3 color=rm0.color;
  // 球体
  if(rm0.crash&&rm0.obj==1){
    // 光线反射的衰减系数
    float ratio0=0.7;
    float ratio1=1.-ratio0;

    // 计算光线进入球体时的入射方向
    vec3 incidentDir=refract(rm0.reflect,rm0.normal,GLASS_REFRACTIVITY_INVERT);
    // 基于入射方向,在球体内部追踪球体
    RayMarchData rm1 = RayMarch(rm0.ro, incidentDir,1);
    if(rm1.crash){
      // 计算光线出球体时的出射方向
      vec3 outDir=refract(rm1.reflect,rm1.normal,GLASS_REFRACTIVITY);
      // 基于出射方向,追踪平面
      RayMarchData rm2 = RayMarch(rm1.ro, outDir,0);
      if(rm2.crash){
        // 折射颜色
        color=color*ratio1+rm2.color*color;
      }
    }

    // 反射
    RayMarchData rmNext = RayMarch(rm0.ro, rm0.reflect,-1); 
    if(rmNext.crash){
      // 在折射颜色的基础上,合成反射颜色
      color=color*ratio1+rmNext.color*ratio0;
    }
  }
  return color;
}

incidentDir 的概念我们之前说过了,它是入射方向。

当入射光线穿透玻璃球的时候,它会再次折射,这时的折射方向就是出射方向outDir。

当出射光碰撞到其它物体的时候,我们再让此物体的颜色与球体原本的颜色进行合成。

后我们还在折射颜色的基础上,合成了反射颜色。

总结

关于Whitted-Style 我们就说到这,下一章我们说一下球天的制作,它会给场景一个环境光。

参考链接:

www.bilibili.com/video/BV1X7…