本系列文章是对 上面MetalKit内容的全面翻译和学习.
让我们继续上周的工作完成ray tracer射线追踪器
.我还要感谢Caroline
, Jessy
, Jeff
和Mike
为本项目提供了很有价植的反馈和性能改善建议.
首先,和往常一样,我们做一下代码清理.在第一部分中我们使用了vec3.swift类,因为我们想要理解基础的数据结构及内部操作,然而,其实已经有一个框架叫做simd可以帮我们完成所有的数学
计算.所以将vec3.swift
改名为ray.swift,因为这个类将只包含ray
结构体相关的代码.下一步,删除vec3
结构体及底部的所有操作.你应该只保留ray结构体和color函数.
下一步,导入simd框架并用float3替换文件中所有的vec3
,然后到pixel.swift文件中重复这个步骤.现在我们正式的只依赖于float3了!在pixel.swift
中我们还需要关注另一个问题:在两个函数之间传递数组将会让渲染变得相当慢.下面是如何计算playground中代码的耗时:
let width = 800let height = 400let t0 = CFAbsoluteTimeGetCurrent()var pixelSet = makePixelSet(width, height)var image = imageFromPixels(pixelSet)let t1 = CFAbsoluteTimeGetCurrent()t1-t0image复制代码
在我的电脑它花了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..
再查看一次耗时:
let width = 800let height = 400let t0 = CFAbsoluteTimeGetCurrent()let image = imageFromPixels(width, height)let t1 = CFAbsoluteTimeGetCurrent()t1-t0image复制代码
很好!在我的电脑上运行时间从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..
在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..
注意我们使用了一具名为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,你会看到有明显不同!
现在,这个画面看起来非常漂亮!请期待本系列的下一部分,我们会深入研究如高光灯光,透明度,折射和反射. 源代码 已发布在Github上.
下次见!