本系列文章是對(duì) http://metalkit.org 上面MetalKit內(nèi)容的全面翻譯和學(xué)習(xí).
今天我們從Peter Shirley’s mini book導(dǎo)入ray tracer射線追蹤器
到Swift
playground中.我將不會(huì)解釋什么是Ray Tracing射線追蹤及它是怎么工作的,我會(huì)請(qǐng)你自己先去讀一讀這本書因?yàn)樗鼘?duì)Kindle訂閱者是免費(fèi)的.如果你不是訂閱者,只需要像我一樣購買這本書就行了.如果你對(duì)這個(gè)話題感興趣,那花費(fèi)$2.99是絕對(duì)值得的.
我們要做的第一件事就是創(chuàng)建一個(gè)數(shù)據(jù)結(jié)構(gòu)體來保存像素信息.在playground中,在Sources
文件夾下創(chuàng)建一個(gè)新文件命名為pixel.swift.下一步,編寫Pixel結(jié)構(gòu)體.它只是一個(gè)簡單結(jié)構(gòu)體,各用一個(gè)變量來保存 RGBA 通道.我們初始化alpha
通道為255,這意味著在[0~255]
范圍的完全不透明:
public struct Pixel {
var r: UInt8
var g: UInt8
var b: UInt8
var a: UInt8
init(red: UInt8, green: UInt8, blue: UInt8) {
r = red
g = green
b = blue
a = 255
}
}
下一步,我們需要?jiǎng)?chuàng)建一個(gè)數(shù)組來保存屏幕上的像素.為了計(jì)算每個(gè)像素的顏色,我們只需要給所有像素的Red
設(shè)置為0,同時(shí)Green
則從屏幕左下角的0(沒有任何綠色)漸變到屏幕右上角的255(純綠色).同樣,Blue
顏色從屏幕頂部的0漸變到屏幕底部的255.
public func makePixelSet(width: Int, _ height: Int) -> ([Pixel], Int, Int) {
var pixel = Pixel(red: 0, green: 0, blue: 0)
var pixels = [Pixel](count: width * height, repeatedValue: pixel)
for i in 0..<width {
for j in 0..<height {
pixel = Pixel(red: 0, green: UInt8(Double(i * 255 / width)), blue: UInt8(Double(j * 255 / height)))
pixels[i + j * width] = pixel
}
}
return (pixels, width, height)
}
最后,我們需要一個(gè)方法來從像素創(chuàng)建出一個(gè)可繪制的圖片.Core Image框架提供了CGImageCreate()方法,它只需要幾個(gè)參數(shù)就可以渲染圖片:
public func imageFromPixels(pixels: ([Pixel], width: Int, height: Int)) -> CIImage {
let bitsPerComponent = 8
let bitsPerPixel = 32
let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue) // alpha is last
let providerRef = CGDataProviderCreateWithCFData(NSData(bytes: pixels.0, length: pixels.0.count * sizeof(Pixel)))
let image = CGImageCreate(pixels.1, pixels.2, bitsPerComponent, bitsPerPixel, pixels.1 * sizeof(Pixel), rgbColorSpace, bitmapInfo, providerRef, nil, true, CGColorRenderingIntent.RenderingIntentDefault)
return CIImage(CGImage: image!)
}
下一步,在playground主頁中用給定的width
和height
創(chuàng)建一個(gè)窗口像素的集合,并用這個(gè)集合來創(chuàng)建渲染出的圖片:
let width = 800
let height = 400
var pixelSet = makePixelSet(width, height)
var image = imageFromPixels(pixelSet)
image
你應(yīng)該能看到下面的圖面:
OK,你可能會(huì)好奇,但是ray tracing射線追蹤
在哪里呢?我們稍后再解釋這個(gè).在Sources
文件夾下面,讓我們創(chuàng)建一個(gè)便利的類命名為vec3.swift,在里面放一些數(shù)學(xué)
工具方法:
struct vec3 {
var x = 0.0
var y = 0.0
var z = 0.0
}
func * (left: Double, right: vec3) -> vec3 {
return vec3(x: left * right.x, y: left * right.y, z: left * right.z)
}
func + (left: vec3, right: vec3) -> vec3 {
return vec3(x: left.x + right.x, y: left.y + right.y, z: left.z + right.z)
}
func - (left: vec3, right: vec3) -> vec3 {
return vec3(x: left.x - right.x, y: left.y - right.y, z: left.z - right.z)
}
func dot (left: vec3, _ right: vec3) -> Double {
return left.x * right.x + left.y * right.y + left.z * right.z
}
func unit_vector(v: vec3) -> vec3 {
let length : Double = sqrt(dot(v, v))
return vec3(x: v.x/length, y: v.y/length, z: v.z/length)
}
下一步,我們需要?jiǎng)?chuàng)建一個(gè)ray射線結(jié)構(gòu)體.它擁有一個(gè)origin原點(diǎn)
成員,一個(gè)direction方向
,還有一個(gè)方法可以計(jì)算任意給定參數(shù)的ray tracing
方程式:
struct ray {
var origin: vec3
var direction: vec3
func point_at_parameter(t: Double) -> vec3 {
return origin + t * direction
}
}
然后我們需要計(jì)算顏色,基于ray射線
是否接觸到我們在屏幕中間創(chuàng)建的sphere球體
:
func color(r: ray) -> vec3 {
let minusZ = vec3(x: 0, y: 0, z: -1.0)
var t = hit_sphere(minusZ, 0.5, r)
if t > 0.0 {
let norm = unit_vector(r.point_at_parameter(t) - minusZ)
return 0.5 * vec3(x: norm.x + 1.0, y: norm.y + 1.0, z: norm.z + 1.0)
}
let unit_direction = unit_vector(r.direction)
t = 0.5 * (unit_direction.y + 1.0)
return (1.0 - t) * vec3(x: 1.0, y: 1.0, z: 1.0) + t * vec3(x: 0.5, y: 0.7, z: 1.0)
}
你注意到我們用到了另一個(gè)方法,叫hit_sphere(),來確定我們的射線是撞到了球體,或者沒有接觸球體:
func hit_sphere(center: vec3, _ radius: Double, _ r: ray) -> Double {
let oc = r.origin - center
let a = dot(r.direction, r.direction)
let b = 2.0 * dot(oc, r.direction)
let c = dot(oc, oc) - radius * radius
let discriminant = b * b - 4 * a * c
if discriminant < 0 {
return -1.0
} else {
return (-b - sqrt(discriminant)) / (2.0 * a)
}
}
回到pixel.swift文件,更改makePixelSet(:),使其在每個(gè)像素被加入集合前,給每個(gè)像素創(chuàng)建一個(gè)ray射線
并計(jì)算它的color顏色
:
public func makePixelSet(width: Int, _ height: Int) -> ([Pixel], Int, Int) {
var pixel = Pixel(red: 0, green: 0, blue: 0)
var pixels = [Pixel](count: width * height, repeatedValue: pixel)
let lower_left_corner = vec3(x: -2.0, y: 1.0, z: -1.0)
let horizontal = vec3(x: 4.0, y: 0, z: 0)
let vertical = vec3(x: 0, y: -2.0, z: 0)
let origin = vec3()
for i in 0..<width {
for j in 0..<height {
let u = Double(i) / Double(width)
let v = Double(j) / Double(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
}
}
return (pixels, width, height)
}
在playground的主頁面,看看產(chǎn)生的新圖像:
敬請(qǐng)關(guān)注本文的第2部分,我們將會(huì)計(jì)算燈光和陰影,產(chǎn)生更真實(shí)的圖像渲染.
源代碼source code 已發(fā)布在Github上.
下次見!