本系列文章是對 metalkit.org 上面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上.下次見!