點(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
文件中谆吴,它繼承于MetalBaseView
,MetalBaseView
提供了使用Metal所需要的基礎(chǔ)方法苛预,BrokenGlassEffectView
只需要在update和draw方法中實(shí)現(xiàn)邏輯刷新和繪制即可句狼。
原理
要做這樣的特效,主要分兩步热某,切割圖片腻菇,運(yùn)動(dòng)模擬。首先將圖片切割成小方塊昔馋,然后使用重力模型讓小方塊落下來(lái)筹吐。第一步切割可以使用兩種方式:
- 給每個(gè)小方塊創(chuàng)建一個(gè)四邊形,并配置好UV秘遏,顯示圖片對(duì)應(yīng)的部分丘薛。假設(shè)有n個(gè)小方塊,如果使用三角形繪制邦危,就需要6 * n個(gè)頂點(diǎn)洋侨。每個(gè)頂點(diǎn)有5個(gè)float,代表位置和uv倦蚪。
- 每個(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ì)在后面介紹它的作用。pointTransforms
和pointMoveInfo
保存了每個(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。主要做了三件事情。
- 將輸入的位置信息使用運(yùn)動(dòng)信息transform進(jìn)行變換跑杭。
- 把點(diǎn)規(guī)范化后的位置信息原封不動(dòng)的傳給Fargment Shader铆帽。
- 把點(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)似于爆炸,旋渦等效果捎稚,如果讀者有興趣乐横,可以自己嘗試一下。