[MetalKit]Ray tracing in a Swift playground2射線追蹤2

本系列文章是對 http://metalkit.org 上面MetalKit內(nèi)容的全面翻譯和學(xué)習(xí).

MetalKit系統(tǒng)文章目錄


讓我們繼續(xù)上周的工作完成ray tracer射線追蹤器.我還要感謝Caroline, Jessy, JeffMike為本項目提供了很有價植的反饋和性能改善建議.

首先,和往常一樣,我們做一下代碼清理.在第一部分中我們使用了vec3.swift類,因為我們想要理解基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)及內(nèi)部操作,然而,其實已經(jīng)有一個框架叫做simd可以幫我們完成所有的數(shù)學(xué)計算.所以將vec3.swift改名為ray.swift,因為這個類將只包含ray結(jié)構(gòu)體相關(guān)的代碼.下一步,刪除vec3結(jié)構(gòu)體及底部的所有操作.你應(yīng)該只保留ray結(jié)構(gòu)體和color函數(shù).

下一步,導(dǎo)入simd框架并用float3替換文件中所有的vec3,然后到pixel.swift文件中重復(fù)這個步驟.現(xiàn)在我們正式的只依賴于float3了!在pixel.swift中我們還需要關(guān)注另一個問題:在兩個函數(shù)之間傳遞數(shù)組將會讓渲染變得相當慢.下面是如何計算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中數(shù)組實際上是用結(jié)構(gòu)體定義的,而在Swift中結(jié)構(gòu)體是值傳遞,也就是說當傳遞時數(shù)組需要復(fù)制,而復(fù)制一個大的數(shù)組是一個性能瓶頸.有兩種方法來修復(fù)它. 一,最簡單的方法是,包所有東西都包裝在class中,讓數(shù)組成為類的property.這樣,數(shù)組在本地函數(shù)之間就不需要被傳遞了.二,很簡單就能實現(xiàn),在本文中為了節(jié)省空間我們也將采用這種方法.我們需要做的是把兩個函數(shù)整合起來,像這樣:

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秒.好了,代碼清理完成.讓我們來畫點什么! 我們不止畫一個球體,可能畫很多個球體.畫一個足夠真實的巨大球體有個小花招就是模擬出地平線.然后我們可以把我們的小球體放在上面,以達到放在地面上的效果.

為此,我們需要抽取我們當前球體的代碼到一個能用的類里邊.命名為objects.swift因為我們將來可能會在球體旁邊創(chuàng)建其它類型的幾何體.下一步,在objects.swift里我們需要創(chuàng)建一個新的結(jié)構(gòu)體來表示hit事件:

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)
    }
}

下一步,我們需要創(chuàng)建一個協(xié)議命名為hitable這樣其他各種類就可以遵守這個協(xié)議.協(xié)議只包含了hit函數(shù):

protocol hitable {
    func hit(r: ray, _ tmin: Float, _ tmax: Float, inout _ rec: hit_record) -> Bool
}

下一步,很顯然該實現(xiàn)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函數(shù)非常類似我們從ray.swift中刪除的hit_sphere函數(shù),不同的是我們現(xiàn)在只關(guān)注那些處于區(qū)別tmax-tmin內(nèi)的撞擊.下一步,我們需要一個方法把多個目標添加到一個列表里.一個hitables的數(shù)組似乎是個正確的選擇:

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函數(shù)引入一個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函數(shù),來允許導(dǎo)入更多對象:

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的主頁,看到新生成的圖片:

raytracing3.png

很好!如果你仔細看就會注意到邊緣的鋸齒效應(yīng),這是因為我們沒有對邊緣像素使用任何顏色混合.要修復(fù)它,我們需要用隨機生成值在一定范圍內(nèi)進行多次顏色采樣,這樣我們能把多個顏色混合在一起達到反鋸齒效應(yīng)的作用.

但是,首先,讓我們在ray.swift里面再創(chuàng)建一個camera類,稍后會用到.移動臨時的攝像機到imageFromPixels函數(shù)里面,放到正確的地方:

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函數(shù)現(xiàn)在是這個樣子:

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主頁面,看到新生成的圖像:

raytracing4.png

看起來好多了! 但是,我們又注意到我們的渲染花了7秒時間,其實可以通過使用更小的采樣值比如10來減少渲染時間.好了,現(xiàn)在我們每個像素有了多個射線,我們終于可以創(chuàng)建matte不光滑的(漫反射)材料了.這種材料不會發(fā)射任何光線,通常吸收直射到上面的所有光線,并用自己的顏色與之混合.漫反射材料反射出的光線方向是隨機的.我們可以用objects.swift中的這個函數(shù)來計算:

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函數(shù),來引入新的隨機函數(shù)到顏色計算中:

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主頁面,看到新生成的圖像:

raytracing5.png

如果你忘了將ns100送到10,你的渲染過程可能會花費大約18秒!但是,如果你已經(jīng)減少了這個值,渲染時間降低到只有大約1.9秒,這對于一個漫反射表面的射線追蹤器來說不算太差.

圖像看起來很棒,但是我們還可以輕易去除那些小的波紋.留意在color函數(shù)中我們設(shè)置Tmin0.0,它似乎在某些情況下干擾了顏色的正確計算.如果我們設(shè)置Tmin為一個很小的正數(shù),比如0.01,你會看到有明顯不同!

raytracing6.png

現(xiàn)在,這個畫面看起來非常漂亮!請期待本系列的下一部分,我們會深入研究如高光燈光,透明度,折射和反射.
源代碼source code 已發(fā)布在Github上.
下次見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容