1-Whitted-Style 简介
Whitted-Style 是一种光线追踪模型,由Turner Whitted提出,可以制作反射和折射效果。。
Turner Whitted 在1979 就将光线追踪引入到计算机图形学,现在已经加入了 NVidia 研究院。
我们可以通过下图来理解Whitted-Style。
学过Ray Marching 的同学会很容易的理解这张图。
首先,我们的眼睛看向栅格图像的某一个像素时,会形成一条primary ray,我暂且将其翻译成初始射线了。
当primary ray 碰撞到物体时,会发生反射或者折射,这种反射或者折射后的射线就叫做secondary rays,我们暂且将其翻译为二次射线。当然,这种二次射线还可能再碰撞到其它物体,发生三次、四次反射或折射。
除此之外,我们还要判断初始射线在反射或折射的过程中,所碰撞到的着色点是否在阴影中。这就需要向光源打一条射线,这种射线就叫做shadow rays。
Whitted-Style 的原理很简单,其中的重点算法就是反射和折射。
2-反射的概念
在此为了照顾像我一样高中没有学过物理的同学,咱们在敲代码之前,先把概念说一下。
2-1-基本定义
反射是指某种波在传播到不同物质时,在分界面上改变传播方向,并返回到原来物质中的现象。
常见的波有光波、水波和声波,而我们这里要说就是光波。
反射定律表明,对于镜面反射(例如在镜子处) ,波入射到表面的角度等于它被反射的角度。
2-2-物理定律
已知:
- 镜面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内置方法,它可以基于入射光线和着色点的法线计算反射光线。
上图是我从《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,并且假设所有物体都是镜面反射的,并且可以反射多次,效果如下:
多次反射的原理就是,在实现碰到一个物体上时,再进行反弹。在一定反弹次数内,将碰的到物体颜色按照一定的权重进行累积。
代码如下:
// 渲染
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-基本定义
折射是波从一种介质传播到另一种介质时的重定向。
波的重定向可能是由波的速度变化或介质变化引起的。
光波的折射是最常见的,声波和水波等其它波也会发生折射,我们这里只说光波。
4-2-物理定律
波的折射的程度取决于两方面:
- 波速的变化
- 波传播的初始方向相对于速度变化的方向
这么说大家可能不太好理解,咱们图解。
已知:
- 左侧的白色区域和右侧的蓝色区域是两种介质
- 光在左右两种介质的折射率分别是n1、n2
- 光在左右两种介质中的速度分别是v1、v2
- normal 是两种介质之间的介面的法线
- 入射光线PO
- PO在O点撞击介面
- 折射光线为OQ
- 入射角θ1
- 折射角θ2
则折射存在以下等式:
折射的物理定律为:
- 入射光线、折射光线和法线在同一平面上。
- 入射光线和折射光线分居法线两侧。
- 入射光线和折射光线分居介面两侧。
- 当入射光从速度大的介质射入速度小的介质时,折射角小于入射角;反之,折射角大于入射角。
- 入射角的大小和折射角的大小成正比。
- 当光线垂直射向介质表面时,传播方向不变。
- 在折射中,光路可逆。
折射的基本概念就是这样,接下来咱们画个折射球。
5-折射球
通过上面的折射概念我们可以知道,光线要穿透一个玻璃球,会发生2次折射:
- 光线从空气到玻璃的折射,这样折射出的光叫入射光。
- 光线从玻璃到空气的折射,这样折射出的光叫出射光。
为了循序渐进,我们先写个从空气到玻璃的折射试试。
5-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;
// 法线
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 编程指南》中找到其用法:
我当前的这种做法其实是不对的,因为入射光从玻璃球中出来的时候还会再次折射。
5-2-从玻璃到空气的折射
效果如下:
整体代码如下:
// 坐标系缩放
#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 我们就说到这,下一章我们说一下球天的制作,它会给场景一个环境光。
参考链接: