[MetalKit]Ray tracing in a Swift playground2射线追踪2

本系列文章是对 http://metalkit.org 上面MetalKit内容的全面翻译和学习.

MetalKit系统文章目录


让我们继续上周的工作完成ray tracer射线追踪器.我还要感谢Caroline, Jessy, JeffMike为本项目提供了很有价植的反馈和性能改善建议.

首先,和往常一样,我们做一下代码清理.在第一部分中我们使用了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.这样,数组在本地函数之间就不需要被传递了.二,很简单就能实现,在本文中为了节省空间我们也将采用这种方法.我们需要做的是把两个函数整合起来,像这样:

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因为我们将来可能会在球体旁边创建其它类型的几何体.下一步,在objects.swift里我们需要创建一个新的结构体来表示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)
    }
}

下一步,我们需要创建一个协议命名为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的主页,看到新生成的图片:

raytracing3.png

很好!如果你仔细看就会注意到边缘的锯齿效应,这是因为我们没有对边缘像素使用任何颜色混合.要修复它,我们需要用随机生成值在一定范围内进行多次颜色采样,这样我们能把多个颜色混合在一起达到反锯齿效应的作用.

但是,首先,让我们在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主页面,看到新生成的图像:

raytracing4.png

看起来好多了! 但是,我们又注意到我们的渲染花了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主页面,看到新生成的图像:

raytracing5.png

如果你忘了将ns100送到10,你的渲染过程可能会花费大约18秒!但是,如果你已经减少了这个值,渲染时间降低到只有大约1.9秒,这对于一个漫反射表面的射线追踪器来说不算太差.

图像看起来很棒,但是我们还可以轻易去除那些小的波纹.留意在color函数中我们设置Tmin0.0,它似乎在某些情况下干扰了颜色的正确计算.如果我们设置Tmin为一个很小的正数,比如0.01,你会看到有明显不同!

raytracing6.png

现在,这个画面看起来非常漂亮!请期待本系列的下一部分,我们会深入研究如高光灯光,透明度,折射和反射.
源代码source code 已发布在Github上.
下次见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容