练习手写一个光线追踪渲染器

练习手写一个光线追踪渲染器

  1. 非webgl版本: 使用cpu执行,和web worker加速
  2. webgl版本: 会调用gpu渲染 ,需要使用gpu编程语言shader,大部分流程由webgl来管理

非webgl版本渲染

光线追踪需要准备的对象有这些

  • 画布: 用于把场景的物体渲染到目标的位置
  • 视口: 用于决定每一条光线的投射方向
  • 渲染对象: 场景中的几何体
  • 光源: 用于点亮场景的物体
  • 相机: 每一条光线的投射起始点

准备完成接下来就可以开始渲染了


大体流程

从相机发射n条光线向场景,光线如果与场景的物体相交,取最近的那个交点 之后从这个点向光源方向连一条线,如果最近的物体就是这个光源(之间没有遮挡物)那么 就可以取与光线相交物体的颜色之后写入到画布,这样完成了画布上一个像素的渲染 之后其他的像素也是一样的方法,只是从相机发出的射线的方向需要变更

详细流程

使用光线追踪追踪每一个像素的颜色

就如同光线追踪名字一样,我们从相机位置开始追踪,之后会追踪到场景中的某个物体, 之后在那个物体上的那个点来追踪光线

定义画布大小和视口

假设现在的画布为320 x 320 大小,为简单计算fov,视口vw和vh分别为2, 视口的平面与xy平面平行并且距离为1,如下图

定义相机的位置和视口

相机在(0,0,0)位置,相机朝向-z方向也就是(0,0,-1),相机向上方向为(0,1,0)

因为画布像素为320 x 320,所以需要把视口水平和垂直都可以各自分为320份 假设我么渲染第i行,j列的位置的像素,那么得到的视口的位置为如下

js 复制代码
for (var i = 0; i < 320; i++) {
    for (var j = 0; j < 320; j++) {
        const y_pos = (1 - (i / 320)) * 2 - 1
        const x_pos = ((i / 320)) * 2 - 1
    }
}

之后就是构造光线的传播方向向量,向量的x和y就是刚刚获取到的在视口平面上的 x_pos 和 y_pos 之后加上一个相机和视口的方向也就是-1,最后归一化这个向量为单位向量, 最后我们就有了光线的定义了

js 复制代码
const ray_dir = (vec3(x_pos, y_pos, 0) + vec3(0, 0, -1)).normalize()
const camera_pos = vec3(0, 0, 0)
const ray = new ray({camera_pos, ray_dir})
渲染第一个像素

比如渲染左上角的第一个像素

在场景中有一个球,位于世界空间(-5,0,-5)的位置,半径为1.

ini 复制代码
接下来就是求这个光线和球的交点

光线的定义 P = O + tD
D 为光线方向 o为光线原点 , 在t时候 处于P的位置


球的定义 | P - C | = R 
球心c到球上任何一点p的距离为r


t^2 <D,D> + 2t <CO,D> + <CO,CO> - r^2 = 0
人生苦短,公式的证明就不说了,最后需要解这个一元二次方程

可以用求根公式  (-b +- sqrt(b^2 - 4ac)) / 2a
可以看出 
    a = dot(D,D)
    b = 2(dot(CO,D))
    c = dot(CO,CO) - R^2
    
其中
    CO = O - C
    

最后解出a,b和c,不过有多种可能

  • 无解 光线没有和这个物体相交,我们给一个默认的颜色,比如黑色
  • 一个解 光线正好与球边缘相交,我们需要计算这个物体的颜色
  • 两个解 光线与球相交有前后两个交点,我们同样需要计算这个物体的颜色,但是我们应该取t小 的那个点,因为那个点离我们更近,也就是球的正面
定义点光源并着色

得到了交点之后就可以计算这个像素着色了,传统的着色模型有很多,但是都会遵循 渲染方程的定义,目前主要流向的是PBR和NPR,但是这些模型还是很复杂,这里只用一个 简单的版本冯模型

这里选一个常见的组合 ambient + diffuse + specular

首先计算环境光 ambient 什么是环境光,现实世界中房间的角落即便没有收到太阳的光照,但他却不是黑的 因为他接收到了环境中其他物体反射过来的光照,因为光线会多次弹射 但是如果真实的计算这一步是需要使用全局光照技术的,需要非常大的计算量 这里简化为一个常数

之后计算漫反射 diffuse

真实世界的物体接受到了入射光,之后反射出去,最后被眼睛看到的那一部分反射的 能量就是物体的颜色.物体着色点有一个表面的法线,如果光线与法线垂直,那么接受不到任何 能量.如果平行,那么接收到了光线的全部能量

js 复制代码
const normal_at_shader_point = vec3()
const light_dir = vec3()

const theta = Math.max(0, dot(normal_at_shader_point, light_dir))
//  theta 的范围在 [0,1] 

const light_color = vec3()
const hit_obj_color = vec3()

const diffuse_color = mul(light_color, hit_obj_color).scale(theta) 

最后是高光 specular 高光与我们的观察方向有关,光线到达物体表面后经过一次弹射,最后与我们相机的夹角 决定了我们观察到的高光反射 因为高光反射不是线性的,我们看到的高光只有很小一点 绿色是原本的,蓝色是 theta^20

js 复制代码
const theta = Math.max(0, dot(reflect(light_dir, normal), view_dir))
const shine = 20
const diffuse_color = mul(light_color, hit_obj_color).scale(theta ** shine)

我们把这三个种光照的颜色加在一起得到的和就是这个点的颜色

模拟光线的多次弹射

考虑到物体不仅仅有亚光物体,还有抛光过的金属这种,他会不仅会接受光源的光照,他还会 接受其他点给与他的反射光照

js 复制代码
function ray_tracing(ray, deep = 3) {
    let color = vec3.ZEOR()
    color += calc_light_from_light_source(hit_record, ray)

    if (hit_record.hit_obj.material.type === "metal") {
        if (deep < 3) {
            let dir = reflect(ray.dir, hit_record.hit_point_normal).normalize()
            ray_tracing(
                createRay({source: hit_record.hit_point, dir}),
                deep - 1
            )
        }
    }

    return color
}
绘制阴影

我们看到了这个点,但是光源却没能看见这个点,那么这个点就处于阴影中 也就是我们不计算这个点的光照着色,但是需要计算间接光的反射

js 复制代码
const toLightDir = light.position.subtractTo(hitPoint).normalize();
let withBlock = getNearestIntersection(createRay(hitPoint.clone(), toLightDir), scene);

if (withBlock) {
    return
}
把这个颜色写入到到画布上
js 复制代码
for (let i = 0; i < height; i++) {
    for (let j = 0; j < width; j++) {
        const x = (j / width) * 2 - 1;
        const y = (1 - (i / height)) * 2 - 1;

        const ray_direction = vec3(x, y, -1).normalize();

        const color = ray_tracing(ray_origin, ray_direction, scene, 3);

        setColorAt(j, i, color.toArray().concat(255))
    }
}

const setColorAt = (x, y, color = [0, 0, 0, 0]) => {
    const start = (imageData.width * y * 4) + (x * 4)
    let [r, g, b, a] = color;
    imageData.data[start] = r
    imageData.data[start + 1] = g
    imageData.data[start + 2] = b
    imageData.data[start + 3] = a
}

最后的效果完成图

参考

相关推荐
发呆小天才yy4 小时前
uniapp 微信小程序使用图表
前端·微信小程序·uni-app·echarts
@PHARAOH5 小时前
HOW - 在 Mac 上的 Chrome 浏览器中调试 Windows 场景下的前端页面
前端·chrome·macos
月月大王7 小时前
easyexcel导出动态写入标题和数据
java·服务器·前端
JC_You_Know8 小时前
多语言网站的 UX 陷阱与国际化实践陷阱清单
前端·ux
Python智慧行囊8 小时前
前端三大件---CSS
前端·css
Jinuss9 小时前
源码分析之Leaflet中Marker
前端·leaflet
成都渲染101云渲染66669 小时前
blender云渲染指南2025版
前端·javascript·网络·blender·maya
聆听+自律9 小时前
css实现渐变色圆角边框,背景色自定义
前端·javascript·css
牛马程序小猿猴10 小时前
17.thinkphp的分页功能
前端·数据库
huohuopro10 小时前
Vue3快速入门/Vue3基础速通
前端·javascript·vue.js·前端框架