本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.git
MetalKit系統文章目錄github
讓咱們繼續上週的工做完成ray tracer射線追蹤器
.我還要感謝Caroline
, Jessy
, Jeff
和Mike
爲本項目提供了頗有價植的反饋和性能改善建議.swift
首先,和往常同樣,咱們作一下代碼清理.在第一部分中咱們使用了vec3.swift類,由於咱們想要理解基礎的數據結構及內部操做,然而,其實已經有一個框架叫作simd能夠幫咱們完成全部的數學
計算.因此將vec3.swift
更名爲ray.swift,由於這個類將只包含ray
結構體相關的代碼.下一步,刪除vec3
結構體及底部的全部操做.你應該只保留ray結構體和color函數.數組
下一步,導入simd框架並用float3替換文件中全部的vec3
,而後到pixel.swift文件中重複這個步驟.如今咱們正式的只依賴於float3了!在pixel.swift
中咱們還須要關注另外一個問題:在兩個函數之間傳遞數組將會讓渲染變得至關慢.下面是如何計算playground中代碼的耗時:數據結構
let width = 800
let height = 400
let t0 = CFAbsoluteTimeGetCurrent()
var pixelSet = makePixelSet(width, height)
var image = imageFromPixels(pixelSet)
let t1 = CFAbsoluteTimeGetCurrent()
t1-t0
image
複製代碼
在個人電腦它花了5秒.這是由於在Swift
中數組其實是用結構體定義的,而在Swift中結構體是值傳遞
,也就是說當傳遞時數組須要複製,而複製一個大的數組是一個性能瓶頸.有兩種方法來修復它. 一,最簡單的方法是,包全部東西都包裝在class
中,讓數組成爲類的property
.這樣,數組在本地函數之間就不須要被傳遞了.二,很簡單就能實現,在本文中爲了節省空間咱們也將採用這種方法.咱們須要作的是把兩個函數整合起來,像這樣:app
public func imageFromPixels(width: Int, _ height: Int) -> CIImage {
var pixel = Pixel(red: 0, green: 0, blue: 0)
var pixels = [Pixel](count: width * height, repeatedValue: pixel)
let lower_left_corner = float3(x: -2.0, y: 1.0, z: -1.0) // Y is reversed
let horizontal = float3(x: 4.0, y: 0, z: 0)
let vertical = float3(x: 0, y: -2.0, z: 0)
let origin = float3()
for i in 0..<width {
for j in 0..<height {
let u = Float(i) / Float(width)
let v = Float(j) / Float(height)
let r = ray(origin: origin, direction: lower_left_corner + u * horizontal + v * vertical)
let col = color(r)
pixel = Pixel(red: UInt8(col.x * 255), green: UInt8(col.y * 255), blue: UInt8(col.z * 255))
pixels[i + j * width] = pixel
}
}
let bitsPerComponent = 8
let bitsPerPixel = 32
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue)
let providerRef = CGDataProviderCreateWithCFData(NSData(bytes: pixels, length: pixels.count * sizeof(Pixel)))
let image = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, width * sizeof(Pixel), rgbColorSpace, bitmapInfo, providerRef, nil, true, CGColorRenderingIntent.RenderingIntentDefault)
return CIImage(CGImage: image!)
}
複製代碼
再查看一次耗時:框架
let width = 800
let height = 400
let t0 = CFAbsoluteTimeGetCurrent()
let image = imageFromPixels(width, height)
let t1 = CFAbsoluteTimeGetCurrent()
t1-t0
image
複製代碼
很好!在個人電腦上運行時間從5秒
下降到了0.1秒.好了,代碼清理完成.讓咱們來畫點什麼! 咱們不止畫一個球體,可能畫不少個球體.畫一個足夠真實的巨大球體有個小花招就是模擬出地平線.而後咱們能夠把咱們的小球體放在上面,以達到放在地面上
的效果.dom
爲此,咱們須要抽取咱們當前球體的代碼到一個能用的類裏邊.命名爲objects.swift由於咱們未來可能會在球體旁邊建立其它類型的幾何體.下一步,在objects.swift
裏咱們須要建立一個新的結構體來表示hit
事件:ide
struct hit_record {
var t: Float
var p: float3
var normal: float3
init() {
t = 0.0
p = float3(x: 0.0, y: 0.0, z: 0.0)
normal = float3(x: 0.0, y: 0.0, z: 0.0)
}
}
複製代碼
下一步,咱們須要建立一個協議命名爲hitable這樣其餘各類類就能夠遵照這個協議.協議只包含了hit函數:函數
protocol hitable {
func hit(r: ray, _ tmin: Float, _ tmax: Float, inout _ rec: hit_record) -> Bool
}
複製代碼
下一步,很顯然該實現sphere類了:
class sphere: hitable {
var center = float3(x: 0.0, y: 0.0, z: 0.0)
var radius = Float(0.0)
init(c: float3, r: Float) {
center = c
radius = r
}
func hit(r: ray, _ tmin: Float, _ tmax: Float, inout _ rec: hit_record) -> Bool {
let oc = r.origin - center
let a = dot(r.direction, r.direction)
let b = dot(oc, r.direction)
let c = dot(oc, oc) - radius*radius
let discriminant = b*b - a*c
if discriminant > 0 {
var t = (-b - sqrt(discriminant) ) / a
if t < tmin {
t = (-b + sqrt(discriminant) ) / a
}
if tmin < t && t < tmax {
rec.t = t
rec.p = r.point_at_parameter(rec.t)
rec.normal = (rec.p - center) / float3(radius)
return true
}
}
return false
}
}
複製代碼
正如你看到的那樣,hit
函數很是相似咱們從ray.swift
中刪除的hit_sphere函數,不一樣的是咱們如今只關注那些處於區別tmax-tmin
內的撞擊.下一步,咱們須要一個方法把多個目標添加到一個列表裏.一個hitables
的數組彷佛是個正確的選擇:
class hitable_list: hitable {
var list = [hitable]()
func add(h: hitable) {
list.append(h)
}
func hit(r: ray, _ tmin: Float, _ tmax: Float, inout _ rec: hit_record) -> Bool {
var hit_anything = false
for item in list {
if (item.hit(r, tmin, tmax, &rec)) {
hit_anything = true
}
}
return hit_anything
}
}
複製代碼
回到ray.swift
,咱們須要修改color
函數引入一個hit-record
變量到顏色的計算中:
func color(r: ray, world: hitable) -> float3 {
var rec = hit_record()
if world.hit(r, 0.0, Float.infinity, &rec) {
return 0.5 * float3(rec.normal.x + 1, rec.normal.y + 1, rec.normal.z + 1);
} else {
let unit_direction = normalize(r.direction)
let t = 0.5 * (unit_direction.y + 1)
return (1.0 - t) * float3(x: 1, y: 1, z: 1) + t * float3(x: 0.5, y: 0.7, z: 1.0)
}
}
複製代碼
最後,回到pixel.swift
咱們須要更改imageFromPixels
函數,來容許導入更多對象:
public func imageFromPixels(width: Int, _ height: Int) -> CIImage {
...
let world = hitable_list()
var object = sphere(c: float3(x: 0, y: -100.5, z: -1), r: 100)
world.add(object)
object = sphere(c: float3(x: 0, y: 0, z: -1), r: 0.5)
world.add(object)
for i in 0..<width {
for j in 0..<height {
let u = Float(i) / Float(width)
let v = Float(j) / Float(height)
let r = ray(origin: origin, direction: lower_left_corner + u * horizontal + v * vertical)
let col = color(r, world: world)
pixel = Pixel(red: UInt8(col.x * 255), green: UInt8(col.y * 255), blue: UInt8(col.z * 255))
pixels[i + j * width] = pixel
}
}
...
}
複製代碼
在playground的主頁,看到新生成的圖片:
很好!若是你仔細看就會注意到邊緣的鋸齒
效應,這是由於咱們沒有對邊緣像素使用任何顏色混合.要修復它,咱們須要用隨機生成值在必定範圍內進行屢次顏色採樣,這樣咱們能把多個顏色混合在一塊兒達到反鋸齒
效應的做用.
可是,首先,讓咱們在ray.swift
裏面再建立一個camera類,稍後會用到.移動臨時的攝像機到imageFromPixels
函數裏面,放到正確的地方:
struct camera {
let lower_left_corner: float3
let horizontal: float3
let vertical: float3
let origin: float3
init() {
lower_left_corner = float3(x: -2.0, y: 1.0, z: -1.0)
horizontal = float3(x: 4.0, y: 0, z: 0)
vertical = float3(x: 0, y: -2.0, z: 0)
origin = float3()
}
func get_ray(u: Float, _ v: Float) -> ray {
return ray(origin: origin, direction: lower_left_corner + u * horizontal + v * vertical - origin);
}
}
複製代碼
imageFromPixels
函數如今是這個樣子:
public func imageFromPixels(width: Int, _ height: Int) -> CIImage {
...
let cam = camera()
for i in 0..<width {
for j in 0..<height {
let ns = 100
var col = float3()
for _ in 0..<ns {
let u = (Float(i) + Float(drand48())) / Float(width)
let v = (Float(j) + Float(drand48())) / Float(height)
let r = cam.get_ray(u, v)
col += color(r, world)
}
col /= float3(Float(ns));
pixel = Pixel(red: UInt8(col.x * 255), green: UInt8(col.y * 255), blue: UInt8(col.z * 255))
pixels[i + j * width] = pixel
}
}
...
}
複製代碼
注意咱們使用了一具名爲ns的變量並賦值爲100,這樣咱們就能夠用隨機生成值進行屢次顏色採樣,正像咱們上面討論的那樣.在playground主頁面,看到新生成的圖像:
看起來好多了! 可是,咱們又注意到咱們的渲染花了7秒時間,其實能夠經過使用更小的採樣值好比10來減小渲染時間.好了,如今咱們每一個像素有了多個射線,咱們終於能夠建立matte不光滑的
(漫反射)材料了.這種材料不會發射任何光線,一般吸取直射到上面的全部光線,並用本身的顏色與之混合.漫反射材料反射出的光線方向是隨機的.咱們能夠用objects.swift
中的這個函數來計算:
func random_in_unit_sphere() -> float3 {
var p = float3()
repeat {
p = 2.0 * float3(x: Float(drand48()), y: Float(drand48()), z: Float(drand48())) - float3(x: 1, y: 1, z: 1)
} while dot(p, p) >= 1.0
return p
}
複製代碼
而後,回到ray.swift
咱們須要修改color
函數,來引入新的隨機函數到顏色計算中:
func color(r: ray, _ world: hitable) -> float3 {
var rec = hit_record()
if world.hit(r, 0.0, Float.infinity, &rec) {
let target = rec.p + rec.normal + random_in_unit_sphere()
return 0.5 * color(ray(origin: rec.p, direction: target - rec.p), world)
} else {
let unit_direction = normalize(r.direction)
let t = 0.5 * (unit_direction.y + 1)
return (1.0 - t) * float3(x: 1, y: 1, z: 1) + t * float3(x: 0.5, y: 0.7, z: 1.0)
}
}
複製代碼
在playground主頁面,看到新生成的圖像:
若是你忘了將ns
從100
送到10
,你的渲染過程可能會花費大約18秒!可是,若是你已經減小了這個值,渲染時間下降到只有大約1.9秒,這對於一個漫反射表面的射線追蹤器來講不算太差.
圖像看起來很棒,可是咱們還能夠輕易去除那些小的波紋.留意在color
函數中咱們設置Tmin
爲0.0,它彷佛在某些狀況下干擾了顏色的正確計算.若是咱們設置Tmin
爲一個很小的正數,好比0.01,你會看到有明顯不一樣!
如今,這個畫面看起來很是漂亮!請期待本系列的下一部分,咱們會深刻研究如高光燈光,透明度,折射和反射. 源代碼source code 已發佈在Github上.
下次見!