iOS特效之你家玻璃碎了

點(diǎn)擊獲取本文示例代碼

我是照騙

前言

最近逛博客看到了一篇帖子,里面介紹了自己如何設(shè)計(jì)一套星球大戰(zhàn)主題的UI驳庭,里面有一個(gè)界面破碎的特效活尊,看著很炫酷撼玄,那篇文章的作者使用了UIDynamics炬搭,UIKit蜈漓,OpenGL分別實(shí)現(xiàn)了效果。于是我就尋思如何使用Metal實(shí)現(xiàn)這樣的效果尚蝌。這是那篇博客的鏈接迎变。下面是Metal版本的效果預(yù)覽,目前還沒(méi)有和界面集成飘言,只是在一張靜態(tài)圖上做的破碎效果。我增加了一些邊界碰撞反彈驼侠,純屬娛樂(lè)姿鸿。

代碼

本文的代碼在BrokenGlassEffectView文件中谆吴,它繼承于MetalBaseViewMetalBaseView提供了使用Metal所需要的基礎(chǔ)方法苛预,BrokenGlassEffectView只需要在update和draw方法中實(shí)現(xiàn)邏輯刷新和繪制即可句狼。

原理

要做這樣的特效,主要分兩步热某,切割圖片腻菇,運(yùn)動(dòng)模擬。首先將圖片切割成小方塊昔馋,然后使用重力模型讓小方塊落下來(lái)筹吐。第一步切割可以使用兩種方式:

  1. 給每個(gè)小方塊創(chuàng)建一個(gè)四邊形,并配置好UV秘遏,顯示圖片對(duì)應(yīng)的部分丘薛。假設(shè)有n個(gè)小方塊,如果使用三角形繪制邦危,就需要6 * n個(gè)頂點(diǎn)洋侨。每個(gè)頂點(diǎn)有5個(gè)float,代表位置和uv倦蚪。
  2. 每個(gè)小方塊使用一個(gè)頂點(diǎn)繪制希坚,繪制時(shí)使用point繪制模式,將point_size設(shè)置成小方塊大小陵且,這樣只需要n個(gè)頂點(diǎn)吏够。本文采用的就是這種方式,這種方式唯一的缺點(diǎn)是小方塊只能是正方形滩报。
    第二步就很簡(jiǎn)單了锅知,只需要使用加速度即可。

頂點(diǎn)生成

我們計(jì)算出需要切割成幾行幾列脓钾,然后生成頂點(diǎn)數(shù)組售睹。

private func buildPointData() -> [Float] {
    var vertexDataArray: [Float] = []
    let pointSize: Float = 12
    let viewWidth: Float = Float(UIScreen.main.bounds.width)
    let viewHeight: Float = Float(UIScreen.main.bounds.height)
    let rowCount = Int(viewHeight / pointSize) + 1
    let colCount = Int(viewWidth / pointSize) + 1
    let sizeXInMetalTexcoord: Float = pointSize / viewWidth * 2.0
    let sizeYInMetalTexcoord: Float = pointSize / viewHeight * 2.0
    pointTransforms = [matrix_float4x4].init()
    pointMoveInfo = [PointMoveInfo].init()
    for row in 0..<rowCount {
        for col in 0..<colCount {
            let centerX = Float(col) * sizeXInMetalTexcoord + sizeXInMetalTexcoord / 2.0 - 1.0
            let centerY = Float(row) * sizeYInMetalTexcoord + sizeYInMetalTexcoord / 2.0 - 1.0
            vertexDataArray.append(centerX)
            vertexDataArray.append(centerY)
            vertexDataArray.append(0.0)
            vertexDataArray.append(Float(col) / Float(colCount))
            vertexDataArray.append(Float(row) / Float(rowCount))
            
            pointTransforms.append(GLKMatrix4Identity.toFloat4x4())
            pointMoveInfo.append(PointMoveInfo.defaultMoveInfo(centerX: centerX, centerY: centerY))
        }
    }
    
    uniforms.pointTexcoordScaleX = sizeXInMetalTexcoord / 2.0
    uniforms.pointTexcoordScaleY = sizeYInMetalTexcoord / 2.0
    uniforms.pointSizeInPixel = pointSize
    return vertexDataArray
}

這里有一點(diǎn)要注意,Metal里的坐標(biāo)系是x軸從-1(左)到1(右)可训,y軸從1(上)到-1(下)昌妹。所以我生成頂點(diǎn)坐標(biāo)時(shí)都把坐標(biāo)規(guī)范到了-1到1這個(gè)范圍。 這里除了生成頂點(diǎn)握截,還計(jì)算了點(diǎn)紋理坐標(biāo)需要的縮放量pointTexcoordScaleX飞崖,pointTexcoordScaleY,并且把點(diǎn)的像素大小傳遞給Uniforms谨胞。這個(gè)Uniforms會(huì)在后面?zhèn)鬟f給Shader固歪。關(guān)于點(diǎn)紋理坐標(biāo)需要的縮放量我會(huì)在后面介紹它的作用。pointTransformspointMoveInfo保存了每個(gè)點(diǎn)的運(yùn)動(dòng)信息,這里對(duì)他們進(jìn)行了初始化牢裳。
然后我們?cè)?code>setupRenderAssets中初始化頂點(diǎn)Buffer逢防。

// 構(gòu)建頂點(diǎn)
self.vertexArray = buildPointData()
let vertexBufferSize = MemoryLayout<Float>.size * self.vertexArray.count
self.vertexBuffer = device.makeBuffer(bytes: self.vertexArray, length: vertexBufferSize, options: MTLResourceOptions.cpuCacheModeWriteCombined)

更新運(yùn)動(dòng)信息

下面我們?cè)趗pdate方法中更新運(yùn)動(dòng)信息。每個(gè)點(diǎn)都有以下運(yùn)動(dòng)信息蒲讯。x忘朝,y軸的速度,x判帮,y軸的加速度局嘁,點(diǎn)最初的中心位置originCenterX,originCenterY晦墙,點(diǎn)的位移translateX悦昵,translateY。

struct PointMoveInfo {
    var xSpeed: Float
    var ySpeed: Float
    var xAccelerate: Float
    var yAccelerate: Float
    var originCenterX: Float
    var originCenterY: Float
    var translateX: Float
    var translateY: Float
  
    ...
}

我們使用這些信息就可以對(duì)點(diǎn)進(jìn)行運(yùn)動(dòng)模擬偎痛。首先我們處理y軸上的速度旱捧,每次update,速度會(huì)隨著加速度改變踩麦,如果超過(guò)了最大速度枚赡,那么就等于最大速度,因?yàn)槲疫@里的速度是負(fù)的谓谦,所以用的是小于贫橙。所以準(zhǔn)確來(lái)說(shuō)應(yīng)該是速度的絕對(duì)值超過(guò)了最大速度的絕對(duì)值。

pointMoveInfo[i].ySpeed += Float(deltaTime) * pointMoveInfo[i].yAccelerate
if pointMoveInfo[i].ySpeed < maxYSpeed {
    pointMoveInfo[i].ySpeed = maxYSpeed
}

然后是位移反粥。并且用位移數(shù)據(jù)生成Shader使用的矩陣卢肃。

pointMoveInfo[i].translateX += Float(deltaTime) * pointMoveInfo[i].xSpeed
pointMoveInfo[i].translateY += Float(deltaTime) * pointMoveInfo[i].ySpeed
let newMatrix = GLKMatrix4MakeTranslation(pointMoveInfo[i].translateX, pointMoveInfo[i].translateY, 0)
pointTransforms[i] = newMatrix.toFloat4x4()

最后我做了邊界檢測(cè),遇到邊界則反彈并且有衰減才顿。

let realY = pointMoveInfo[i].translateY + pointMoveInfo[i].originCenterY
let realX = pointMoveInfo[i].translateX + pointMoveInfo[i].originCenterX
if realY <= -1.0 {
    pointMoveInfo[i].ySpeed = -pointMoveInfo[i].ySpeed * 0.6
    if fabs(pointMoveInfo[i].ySpeed) < 0.01 {
        pointMoveInfo[i].ySpeed = 0
    }
}
if realX <= -1.0 || realX >= 1.0 {
    pointMoveInfo[i].xSpeed = -pointMoveInfo[i].xSpeed * 0.6
    if fabs(pointMoveInfo[i].xSpeed) < 0.01 {
        pointMoveInfo[i].xSpeed = 0
    }
}

渲染

頂點(diǎn)和運(yùn)動(dòng)信息萬(wàn)事具備莫湘,可以渲染了。我們把頂點(diǎn)Buffer郑气,紋理幅垮,Uniforms,運(yùn)動(dòng)信息pointTransforms都傳遞給Shader尾组,接下來(lái)就輪到Shader表演了忙芒。

override func draw(renderEncoder: MTLRenderCommandEncoder) {
    renderEncoder.setVertexBuffer(self.vertexBuffer, offset: 0, index: 0)
    renderEncoder.setFragmentTexture(self.imageTexture, index: 0)
    
    let uniformBuffer = device.makeBuffer(bytes: self.uniforms.data(), length: Uniforms.sizeInBytes(), options:
MTLResourceOptions.cpuCacheModeWriteCombined)
    renderEncoder.setVertexBuffer(uniformBuffer, offset: 0, index: 1)
    renderEncoder.setFragmentBuffer(uniformBuffer, offset: 0, index: 0)
    
    let transformsBufferSize = MemoryLayout<matrix_float4x4>.size * pointTransforms.count
    let transformsBuffer = device.makeBuffer(bytes: pointTransforms, length: transformsBufferSize, options:
MTLResourceOptions.cpuCacheModeWriteCombined)
    renderEncoder.setVertexBuffer(transformsBuffer, offset: 0, index: 2)
    
    renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: self.vertexArray.count / 5)
}

Shader

我們先來(lái)看看Shader中定義的結(jié)構(gòu)體。輸入的頂點(diǎn)VertexIn中包含位置和點(diǎn)所在位置的信息讳侨,點(diǎn)所在位置已經(jīng)被規(guī)范化到0到1的區(qū)間了呵萨。輸出到Fragment Shader的VertexOut結(jié)構(gòu)包含處理后的位置,點(diǎn)所在位置的信息和點(diǎn)的像素尺寸跨跨。Uniforms里包含點(diǎn)紋理坐標(biāo)的縮放量以及點(diǎn)的像素大小潮峦。

struct VertexIn
{
    packed_float3  position;
    packed_float2  pointPosition;
};

struct VertexOut
{
    float4  position [[position]];
    float2  pointPosition;
    float pointSize [[ point_size ]];
};

struct Uniforms
{
    packed_float2 pointTexcoordScale;
    float pointSizeInPixel;
};

接下來(lái)我們看看Vertex Shader。主要做了三件事情。

  1. 將輸入的位置信息使用運(yùn)動(dòng)信息transform進(jìn)行變換跑杭。
  2. 把點(diǎn)規(guī)范化后的位置信息原封不動(dòng)的傳給Fargment Shader铆帽。
  3. 把點(diǎn)的像素大小傳遞給point_size咆耿。
vertex VertexOut passThroughVertex(uint vid [[ vertex_id ]],
                                   const device VertexIn* vertexIn [[ buffer(0) ]],
                                   const device Uniforms& uniforms [[ buffer(1) ]],
                                   const device float4x4* transforms [[ buffer(2) ]])
{
    VertexOut outVertex;
    VertexIn inVertex = vertexIn[vid];
    outVertex.position = transforms[vid] * float4(inVertex.position, 1.0);
    outVertex.pointPosition = inVertex.pointPosition;
    outVertex.pointSize = uniforms.pointSizeInPixel;
    return outVertex;
};

最后輪到我們的Fragment Shader登場(chǎng)了德谅。這里的核心就是計(jì)算UV,將點(diǎn)紋理坐標(biāo)pointCoord在y軸上翻轉(zhuǎn)后乘以點(diǎn)紋理縮放量求解出額外的UV偏移萨螺。然后以點(diǎn)的位置信息為基礎(chǔ)UV窄做,兩者相加。最后將相加后的UV在Y軸上翻轉(zhuǎn)就得到可以使用的UV了慰技。從diffuse紋理上采樣椭盏,然后返回采樣到的顏色。

constexpr sampler s(coord::normalized, address::repeat, filter::linear);

fragment float4 passThroughFragment(VertexOut inFrag [[stage_in]],
                                    float2 pointCoord  [[point_coord]],
                                     texture2d<float> diffuse [[ texture(0) ]],
                                    const device Uniforms& uniforms [[ buffer(0) ]])
{
    float2 additionUV = float2((pointCoord[0]) * uniforms.pointTexcoordScale[0], (1.0 - pointCoord[1]) * uniforms.pointTexcoordScale[1]);
    float2 uv = inFrag.pointPosition + additionUV;
    uv = float2(uv[0], 1.0 - uv[1]);
    float4 finalColor = diffuse.sample(s, uv);
    return finalColor;
};

到此吻商,Shader就介紹完了掏颊,還是很簡(jiǎn)單的,代碼量并不大艾帐。主要流程就是VertexShader處理運(yùn)動(dòng)信息乌叶,F(xiàn)ragmentShader處理圖片在點(diǎn)上的著色。

總結(jié)

本文使用的方法類(lèi)似于一個(gè)小型的粒子系統(tǒng)柒爸,使用點(diǎn)精靈(Point Sprites)技術(shù)比較高效的實(shí)現(xiàn)了碎片的效果准浴。我們可以在update中使用其他的運(yùn)動(dòng)模擬算法實(shí)現(xiàn)類(lèi)似于爆炸,旋渦等效果捎稚,如果讀者有興趣乐横,可以自己嘗試一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末今野,一起剝皮案震驚了整個(gè)濱河市葡公,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌条霜,老刑警劉巖催什,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蛔外,居然都是意外死亡蛆楞,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)夹厌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)豹爹,“玉大人,你說(shuō)我怎么就攤上這事矛纹”哿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)孩等。 經(jīng)常有香客問(wèn)我艾君,道長(zhǎng),這世上最難降的妖魔是什么肄方? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任冰垄,我火速辦了婚禮,結(jié)果婚禮上权她,老公的妹妹穿的比我還像新娘虹茶。我一直安慰自己,他們只是感情好隅要,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布蝴罪。 她就那樣靜靜地躺著,像睡著了一般步清。 火紅的嫁衣襯著肌膚如雪要门。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,754評(píng)論 1 307
  • 那天廓啊,我揣著相機(jī)與錄音欢搜,去河邊找鬼。 笑死崖瞭,一個(gè)胖子當(dāng)著我的面吹牛狂巢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播书聚,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼唧领,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了雌续?” 一聲冷哼從身側(cè)響起斩个,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驯杜,沒(méi)想到半個(gè)月后受啥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸽心,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年滚局,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顽频。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡藤肢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出糯景,到底是詐尸還是另有隱情嘁圈,我是刑警寧澤省骂,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站最住,受9級(jí)特大地震影響钞澳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涨缚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一轧粟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仗岖,春花似錦逃延、人聲如沸览妖。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)讽膏。三九已至檩电,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間府树,已是汗流浹背俐末。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奄侠,地道東北人卓箫。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像垄潮,于是被迫代替她去往敵國(guó)和親烹卒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容