介紹
在這個教程中绩衷,我們會做一個可以渲染Mandelbrot Set的應用程序,我們可以縮放和平鋪它來看分形那令人驚嘆的復雜之美喷好。最終的結果如下:
著色程序的代碼
void main() {
#define iterations 128
vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
vec2 z = position; // z.x is the real component z.y is the imaginary component
// Rescale the position to the intervals [-2,1] [-1,1]
z *= vec2(3.0,2.0);
z -= vec2(2.0,1.0);
vec2 c = z;
float it = 0.0; // Keep track of what iteration we reached
for (int i = 0;i < iterations; ++i) {
// zn = zn-1 ^ 2 + c
// (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += c;
if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
break;
}
it += 1.0;
}
if (it < float(iterations)) {
color.x = sin(it / 3.0);
color.y = cos(it / 6.0);
color.z = cos(it / 12.0 + 3.14 / 4.0);
}
gl_FragColor = vec4(color,1.0);
}
你可以下載起始版本跟著教程一起做,也可以在本文結尾找到最終版本的代碼。
Gamescene.sks文件里包含一個名為fractal的子畫面围橡,它填充了整個界面并且著色程序程序Fractal.fsh也附在它上。
Fractal.fsh包含了上面著色程序的代碼
GameViewController.swift包含了設置游戲場景的代碼
GameScene.swift為空
如果你現(xiàn)在運行代碼缕贡,你將會得到如下的結果:
請注意縱橫比固定為3/2翁授,我們需要先根據(jù)屏幕大小調節(jié)它。
并且由于畫面是靜態(tài)的晾咪,所以你不可能與它有任何方式的交互收擦。
我們將用一個透明的scrollview來處理平鋪縮放。scrollview將自動跟蹤我們的位置以及我們在分形中的縮放程度谍倦。
打開`Main.storyboard`文件炬守,拖進去一個scrollview。將scrollview設置成fill the view剂跟,并對它的寬度减途,到頂部距離,到底部距離設置限制曹洽。
將scrollview的最大縮放程度設置為100000鳍置,意味著我們將可以把分享放大到十萬倍!我們不能再放大更多了因為已經接近了`float`類型的準確極限送淆。
拖一個view(畫面)到scrollview里税产,它將用作處理縮放。這個view本身不會展示任何東西偷崩,我們將用到它的contentOffset和scrollView的zoom屬性來更新我們的著色程序辟拷。要確保這個畫面可以填滿scrollView,并且設定好寬度,到頂部底部左右距離的限制阐斜。將畫面的背景色設置為 Clear Color (透明色)衫冻。
接下來我們將連接我們所需要的outlet和scrollView的代理。
給scrollView和scrollView的contentView拖進outlet谒出。
class GameViewController: UIViewController, UIScrollViewDelegate? {
@IBOutlet weak var contentView: UIView!
@IBOutlet weak var scrollView: UIScrollView!
...
}
接下來我們去掉代理方法隅俘,并且實現(xiàn)viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView?這個方法
class GameViewController: UIViewController, UIScrollViewDelegate? {
...
func scrollViewDidScroll(scrollView: UIScrollView) {
}
func scrollViewDidZoom(scrollView: UIScrollView) {
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return contentView
}
...
}
著色程序可以從你的swift代碼里的uniform變量里獲得數(shù)據(jù)邻奠。uniform變量可以在SpriteKit編輯器里聲明。那現(xiàn)在我們來聲明一下uniform變量为居。
打開GameScene.sks文件碌宴,選擇 mandelbrote sprite。將insepctor拖到底部蒙畴,在“Custom shader Uniforms”里添加兩項:float類型的zoom贰镣,值為1, 以及vec2類型的offset膳凝。我們將用這兩項uniform變量儲存scrollView的contentOffset以及zoom屬性八孝。
警告:Xcode 6.3的uniform變量有bug。它不能直接在編輯器里賦值初始化鸠项,你必須在代碼里初始化它們干跛。
我們可以通過shader屬性來獲取節(jié)點上(node)著色程序,用theuniformedName()方法來從著色程序得到uniform變量祟绊。以下是我們獲取zoom uniform變量的例子:
let zoomUniform = node.shader!.uniformNamed("zoom")!
Once we have a uniform we can change its value via one of of the properties
當我們有了uniform變量后楼入,我們可以通過它的屬性來改變它的值。
var textureValue: SKTexture!
var floatValue: Float
var floatVector2Value: GLKVector2
var floatVector3Value: GLKVector3
var floatVector4Value: GLKVector4
var floatMatrix2Value: GLKMatrix2
var floatMatrix3Value: GLKMatrix3
var floatMatrix4Value: GLKMatrix4
We’re only interested in usingfloatValueandfloatVector2Valuefor this tutorial.
在本教程里牧抽,我們只對floatValue和floatVector2Value感興趣嘉熊。
Ex: to set the zoom to 2 we use
例子:將zoom的值設置成2
zoomUniform.floatValue = 2
Coordinate systems and mapping intervals
我們將在保持比例的基礎上映射不同的坐標系。我們將用這個來轉化scrollview的坐標到復平面扬舒。
讓我們先看一下一維的情況:
將x從區(qū)間[0,a]映射到區(qū)間[0,1]阐肤,我們只需要除以區(qū)間長度x' = x / a。
將x從區(qū)間[0,1]映射到區(qū)間[a,b]讲坎,我們可以乘上區(qū)間長度孕惜,然后再加上區(qū)間起始值,x' = x * (b - a) + a晨炕。
舉個例子衫画,比如iPhone4的x坐標,x坐標為0到480之間瓮栗。映射x到[0削罩,1], 我們用x' = x / 480。映射x'從[0,1]到[-2,2]费奸,我們用x'' = x' * 4 - 2
如果我們屏幕上有一點x弥激,坐標值為120,那么對應到區(qū)間[0,1]將成為120 / 480 = 0.25愿阐,以及在區(qū)間[-2,2]微服,如下所見它將成為0.25 * 4 - 2 = -1。
Mapping between the scrollview and the complex plane
我們需要講scrollView上的點轉換到復平面换况。第一步职辨,先將scrollView上的點轉換到區(qū)間[0,1]盗蟆。通過將contentOffset除以contentSize可以將contentOffset轉換到區(qū)間[0,1]戈二。
var offset = scrollView.contentOffset
offset.x /= scrollView.contentSize.width
offset.y /= scrollView.contentSize.height
我們著色程序x舒裤,y坐標都有點在區(qū)間[0,1],所以我們要在scrollView的contentView里映射出這些店觉吭。
標準化過的contentView為1.0 / zoom腾供,所以contentView里標準化過的點坐標講在區(qū)間[contentOffset / contentSize,contentOffset / contentSize + 1.0 / zoom]。
還有我們必須牢記的是鲜滩,y軸的點在GLSL上伴鳖,而點(0,0)在左下角,所以我們必須翻轉y軸來對應我們的scrollView徙硅。
下面的GLSL代碼轉換scrollView的contentView里點的位置榜聂。
// Fractal.fsh
void main {
vec2 position = v_tex_coord;
position.y = 1.0 - position.y; // flip y coordinate
vec2 z = offset + position / zoom;
...
}
如下你可以看見藍色的scrollView的contentView在標準化與未標準化過的邊框。contentSize = (960,640)嗓蘑,contentOffset = (240,160)须肆,zoom = 2.0
標準化過的ScrollView
最后我們將點映射到復平面。為了在mandelbrot里得到好看的效果桩皿,我們將希望映射區(qū)域[-1.5,0.5] x [-1,1]復平面豌汇。
我們還想使縱橫比正確。現(xiàn)在我們的x泄隔、y軸的比例一樣拒贱,我們要乘以x和縱橫比使得圖片不會變形。
縱橫比是什么
縱橫比是屏幕寬度和高度的比例佛嬉。
// Fractal.fsh
void main {
...
z *= 2.0;
z -= vec2(1.5,1.0);
float aspectRatio = u_sprite_size.x / u_sprite_size.y;
z.x *= aspectRatio;
...
}
下面你可以看到我們scrollView的contentView映射到的平復面以及糾正過縱橫比的結果逻澳。
為了整合上面所有代碼,我們建了一個新的方法叫updateShader,它可以傳一個contentView坐標到著色程序暖呕。我們所需要做的就是在scrollView的代理方法里調用updateShader方法赡盘。
class GameViewController: UIViewController, UIScrollViewDelegate? {
...
func updateShader(scrollView: UIScrollView) {
let zoomUniform = node.shader!.uniformNamed("zoom")!
let offsetUniform = node.shader!.uniformNamed("offset")!
var offset = scrollView.contentOffset
offset.x /= scrollView.contentSize.width
offset.y /= scrollView.contentSize.height
zoomUniform.floatValue = Float(scrollView.zoomScale)
offsetUniform.floatVector2Value = GLKVector2Make(Float(offset.x), Float(offset.y))
}
func scrollViewDidScroll(scrollView: UIScrollView) {
updateShader(scrollView)
}
func scrollViewDidZoom(scrollView: UIScrollView) {
updateShader(scrollView)
}
...
}
同時也別忘了當view出現(xiàn)時調用updateShader方法,這樣你才可以初始化uniform變量缰揪。
class ViewController {
...
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
updateShader(scrollView)
}
...
}
最終著色程序的如下所示:
void main() {
#define iterations 128
vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
position.y = 1.0 - position.y;
vec2 z = offset + position / zoom;
z *= 2.0;
z -= vec2(1.5,1.0);
float aspectRatio = u_sprite_size.x / u_sprite_size.y;
z.x *= aspectRatio;
vec2 c = z;
float it = 0.0; // Keep track of what iteration we reached
for (int i = 0;i < iterations; ++i) {
// zn = zn-1 ^ 2 + c
// (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += c;
if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
break;
}
it += 1.0;
}
vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
if (it < float(iterations)) {
color.x = sin(it / 3.0);
color.y = cos(it / 6.0);
color.z = cos(it / 12.0 + 3.14 / 4.0);
}
gl_FragColor = vec4(color,1.0);
}
1 . 優(yōu)化
黑色部分渲染的最慢陨享。幸好根據(jù)下圖,我們可以很快知道一個點是否在兩塊黑色部分之一里 (心形部分或者區(qū)域2)钝腺。這里你可以找到如何判斷點是否在兩塊黑色區(qū)域之一里的方法抛姑。加上這些代碼來改進著色程序,它們只會在點不在這兩個區(qū)域里執(zhí)行mandelbrot循環(huán)艳狐。這將大幅度提高app在這些區(qū)域可見時的表現(xiàn)定硝。
見下圖,主要的心形為紅色毫目,區(qū)域2為綠色蔬啡。
Hint
提示
只當點在這些區(qū)域中的一個以外的時候執(zhí)行mandelbrot循環(huán)诲侮。
Solution
答案
void main() {
#define iterations 128
vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
position.y = 1.0 - position.y;
vec2 z = offset + position / zoom;
z *= 2.0;
z -= vec2(1.5,1.0);
float aspectRatio = u_sprite_size.x / u_sprite_size.y;
z.x *= aspectRatio;
vec2 c = z;
bool skipPoint = false;
//? ? cardioid checking
if ((z.x + 1.0) * (z.x + 1.0) + z.y * z.y < 0.0625) {
skipPoint = true;
}
//? ? period 2 checking
float q = (z.x - 0.25) * (z.x - 0.25) + z.y * z.y;
if (q * (q + (z.x - 0.25)) < 0.25 * z.y * z.y) {
skipPoint = true;
}
float it = 0.0; // Keep track of what iteration we reached
if (!skipPoint) {
for (int i = 0;i < iterations; ++i) {
// zn = zn-1 ^ 2 + c
// (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += c;
if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
break;
}
it += 1.0;
}
}
vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
if (it < float(iterations) && !skipPoint) {
color.x = sin(it / 3.0);
color.y = cos(it / 6.0);
color.z = cos(it / 12.0 + 3.14 / 4.0);
}
gl_FragColor = vec4(color,1.0);
}
2 . 做一個類似的app,可以讓你探索Julia set 的某點c箱蟆。
例子: vec2 c = vec2(-0.76, 0.15);
Solution
答案
void main() {
#define iterations 128
vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
position.y = 1.0 - position.y;
vec2 z = offset + position / zoom;
z *= 2.0;
z -= vec2(1.0,1.0);
float aspectRatio = u_sprite_size.x / u_sprite_size.y;
z.x *= aspectRatio;
vec2 c = vec2(-0.76, 0.15);
float it = 0.0; // Keep track of what iteration we reached
for (int i = 0;i < iterations; ++i) {
// zn = zn-1 ^ 2 + c
// (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += c;
if (dot(z,z) > 4.0) { // dot(z,z) == length(z) ^ 2 only faster to compute
break;
}
it += 1.0;
}
vec3 color = vec3(0.0,0.0,0.0); // initialize color to black
if (it < float(iterations)) {
color.x = sin(it / 3.0);
color.y = cos(it / 6.0);
color.z = cos(it / 12.0 + 3.14 / 4.0);
}
gl_FragColor = vec4(color,1.0);
}
3 . 添加一個點c的uniform變量沟绪,使用戶可以用兩個手指改變其值。
提示
用UIPanGestureRecognizer來檢測兩個手指的范圍空猜。你需要標準化手勢識別器傳來的結果绽慈。
答案
class GameViewController: UIViewController, UIScrollViewDelegate {
...
var c: GLKVector2 = GLKVector2Make(0, 0)
override func viewDidLoad() {
...
let panGr = UIPanGestureRecognizer(target: self, action: "didPan:")
panGr.minimumNumberOfTouches = 2
view.addGestureRecognizer(panGr)
}
func didPan(panGR: UIPanGestureRecognizer) {
var translation = panGR.translationInView(view)
translation.x /= view.frame.size.width
translation.y /= view.frame.size.height
c = GLKVector2Make(Float(translation.x) + c.x, Float(translation.y) + c.y)
let cUniform = node.shader!.uniformNamed("c")!
cUniform.floatVector2Value = c
panGR.setTranslation(CGPointZero, inView: view)
}
}
4 . 用一個圖片來給julia分形涂色。有很多方法都可以實現(xiàn)辈毯,其中有一個很有意思的方法如下:
每一次循環(huán)都從圖片里得到對應z的顏色坝疼。如果顏色不是透明的就跳出循環(huán)。
如果跑完所有循環(huán)谆沃,得到的顏色依舊不是透明的钝凶,那么就用它來填色對應的像素。
如果是透明的唁影,那么就用另外一個公式來填點的顏色耕陷。比如標準化過的循環(huán)次數(shù)。
下面是一個用兔子照片來填色的julia分形夭咬。
Hint
提示
你需要再添加一個Texture類型的uniform變量啃炸,命名為image。你可以用vec4 color = texture2D(image,p)來得到texture在p位置的顏色卓舵。
答案
class GameViewController: UIViewController, UIScrollViewDelegate {
...
override func viewDidLoad() {
...
let imageUniform = node.shader!.uniformNamed("image")!
imageUniform.textureValue = SKTexture(imageNamed: "bunny")
}
...
}
vec4 getColor(vec2 p) {
if (p.x > 0.99 || p.y > 0.99 || p.x < 0.01 || p.y < 0.01) {
return vec4(0.0);
}
return texture2D(image,p);
}
void main() {
#define iterations 128
vec2 position = v_tex_coord; // gets the location of the current pixel in the intervals [0..1] [0..1]
position.y = 1.0 - position.y;
vec2 z = offset + position / zoom;
z *= 2.0;
z -= vec2(1.0,1.0);
float aspectRatio = u_sprite_size.x / u_sprite_size.y;
z.x *= aspectRatio;
vec2 c = vec2(-0.76, 0.15);
vec4 color = vec4(0.0); // initialize color to black
float it = 0.0; // Keep track of what iteration we reached
for (int i = 0;i < iterations; ++i) {
// zn = zn-1 ^ 2 + c
// (x + yi) ^ 2 = x ^ 2 - y ^ 2 + 2xyi
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += c;
color = getColor(z);
if (dot(z,z) > 4.0 || color.w > 0.1) { // dot(z,z) == length(z) ^ 2 only faster to compute
break;
}
it += 1.0;
}
if (color.w < 0.1) {
float s = it / 80.0;
color = vec4(s,s,s,1.0);
}
gl_FragColor = color;
}
5 .類比分形南用,實驗一下Mandelbrot的公式。這個是開放性的挑戰(zhàn)掏湾。以下提供了兩個例子
燃燒之船的分形
Formulazn = abs(zn-12 + c)
公式zn = abs(zn-12 + c)
GLSL
GLSL
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += c;
z = abs(z);
Sierpinski Julia
公式zn = zn-12 + 0.5 * c / (zn-12)
GLSLS
vec2 powc(vec2 z,float p) {
vec2 polar = vec2(length(z),atan(z.y,z.x));
polar.x = pow(polar.x,p);
polar.y *= p;
return vec2(polar.x * cos(polar.y),polar.x * sin(polar.y));
}
void main() {
...
z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
z += 0.5 * c * powc(z,-2.0);
...
}