本系列文章是对 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上.
下次见!