本系列文章是对 http://metalkit.org 上面MetalKit内容的全面翻译和学习.
MetalKit系统文章目录
让我们继续上周的工作完成ray tracer射线追踪器
.首先,还是像往常一样做一些代码清理.我在之前把所有类用结构体替换了,并且本次使用了合适的命名规则(如首字母大写).你可以在本周的代码仓库里看到修改后的代码.简短起见,本次不再详细介绍清理过程,但是你会发现改变其实是很小的.
上次我们重点关注了lambertian
和metal
材料是如何被渲染的.最后一种我们需要关注的材料类型叫dielectric介电质,就是当你看水或者某个玻璃物体时看到的那样.当dielectric
被射线撞击时,射线会分成两部分:一个反射
(反弹)射线和一个折射
(新增)射线.折射
用斯涅尔定律 Snell’s law来描述.让我们来看看这个定律怎么翻译成代码.在material.swift
创建一个refract()函数:
func refract(v: float3, n: float3, ni_over_nt: Float) -> float3? {
let uv = normalize(v)
let dt = dot(uv, n)
let discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt)
if discriminant > 0 {
return ni_over_nt * (uv - n * dt) - n * sqrt(discriminant)
}
return nil
}
下一步,让我们创建一个Dielectric结构体.注意attenuation衰减
总是1因为dielectrics
不吸收入射射线:
struct Dielectric: Material {
var ref_index: Float = 1
func scatter(ray_in: Ray, _ rec: Hit_record, inout _ attenuation: float3, inout _ scattered: Ray) -> Bool {
var ni_over_nt: Float = 1
var outward_normal = float3()
let reflected = reflect(ray_in.direction, n: rec.normal)
attenuation = float3(1, 1, 1)
if dot(ray_in.direction, rec.normal) > 0 {
outward_normal = -rec.normal
ni_over_nt = ref_index
} else {
outward_normal = rec.normal
ni_over_nt = 1 / ref_index
}
let refracted = refract(ray_in.direction, n: outward_normal, ni_over_nt: ni_over_nt)
if refracted != nil {
scattered = Ray(origin: rec.p, direction: refracted!)
} else {
scattered = Ray(origin: rec.p, direction: reflected)
return false
}
return true
}
}
我们先计算向外的法线-可以用射线和碰撞点的点积正负来判断-然后用它来计算折射射线.当它是nil
时,我们反射射线,否则我们折射射线.在pixel.swift
中替换第二个metal
球体,换成dielectric
的:
object = sphere(c: float3(x: -1, y: 0, z: -1), r: 0.5, m: Dielectric())
在playground主页面中,看看新生成的图像:
玻璃表面的反射率随角度变化而不同.当你垂直观察它时,反射率几乎没有.随着观察点角度变小,反射率升高,并且外界其它物体在玻璃表面的镜面反射越来越清楚.这个效应可以用Schlick 多项式近似来计算:
func schlick(cosine: Float, _ index: Float) -> Float {
var r0 = (1 - index) / (1 + index)
r0 = r0 * r0
return r0 + (1 - r0) * powf(1 - cosine, 5)
}
scatter()函数需要用这个近似值来修正:
func scatter(ray_in: Ray, _ rec: Hit_record, inout _ attenuation: float3, inout _ scattered: Ray) -> Bool {
var reflect_prob: Float = 1
var cosine: Float = 1
var ni_over_nt: Float = 1
var outward_normal = float3()
let reflected = reflect(ray_in.direction, n: rec.normal)
attenuation = float3(1, 1, 1)
if dot(ray_in.direction, rec.normal) > 0 {
outward_normal = -rec.normal
ni_over_nt = ref_index
cosine = ref_index * dot(ray_in.direction, rec.normal) / length(ray_in.direction)
} else {
outward_normal = rec.normal
ni_over_nt = 1 / ref_index
cosine = -dot(ray_in.direction, rec.normal) / length(ray_in.direction)
}
let refracted = refract(ray_in.direction, n: outward_normal, ni_over_nt: ni_over_nt)
if refracted != nil {
reflect_prob = schlick(cosine, ref_index)
} else {
scattered = Ray(origin: rec.p, direction: reflected)
reflect_prob = 1.0
}
if Float(drand48()) < reflect_prob {
scattered = Ray(origin: rec.p, direction: reflected)
} else {
scattered = Ray(origin: rec.p, direction: refracted!)
}
return true
}
注意我们现在折射是基于反射阈值设置为1.有一个得到凹玻璃球的简单方式(小把戏).如果半径是负的,尽管几何体不受影响,但法线会指向内部,就得到一个漂亮的凹玻璃球.让我们再加一个半径为负值的dielectric
球体:
object = sphere(c: float3(x: -1, y: 0, z: -1), r: -0.49, m: Dielectric())
现在你应该能看到凹玻璃球了.在总结之前,我们还需要去做一件事-修复摄像机,这样我们从不同角度和距离来观察物体了.首先,我们需要给摄像机一个field of view视场
.然后,我们需要一个lookFrom
点和一个lookAt
点来设置摄像机的观察方向.最后,我们需要一个up上方向
向量,这样就可以沿着这个方向旋转摄像机同时总是保持up
向量方向不变.在ray.swift
中让我们用下面的代码替换旧摄像机:
struct Camera {
let lower_left_corner, horizontal, vertical, origin, u, v, w: float3
var lens_radius: Float = 0.0
init(lookFrom: float3, lookAt: float3, vup: float3, vfov: Float, aspect: Float) {
let theta = vfov * Float(M_PI) / 180
let half_height = tan(theta / 2)
let half_width = aspect * half_height
origin = lookFrom
w = normalize(lookFrom - lookAt)
u = normalize(cross(vup, w))
v = cross(w, u)
lower_left_corner = origin - half_width * u - half_height * v - w
horizontal = 2 * half_width * u
vertical = 2 * half_height * v
}
func get_ray(s: Float, _ t: Float) -> Ray {
return Ray(origin: origin, direction: lower_left_corner + s * horizontal + t * vertical - origin)
}
}
在pixel.swift
中,用下面代码替换调用摄像机的代码:
let lookFrom = float3(0, 1, -4)
let lookAt = float3()
let vup = float3(0, -1, 0)
let cam = Camera(lookFrom: lookFrom, lookAt: lookAt, vup: vup, vfov: 50, aspect: Float(width) / Float(height))
在playground主页面中,看看新生成的图像:
源代码 source code 已发布在Github上.
下次见!