工作需要,最近要實現(xiàn)一個波浪效果,一般的做法是使用UIBezierPath生成sin曲線懦傍,通過CADisplayLink刷新曲線的相位或者幅度來達到波浪效果。本文要介紹另外一種方式,使用OpenGL來實現(xiàn)波浪效果佑笋。下面是效果圖,上面使用的是GLKView斑鼻,下面是CAShapeLayer蒋纬。這兩種方式我都做了簡單的遮罩效果。
接下來就重點介紹如何使用OpenGL實現(xiàn)這樣的效果坚弱。GLWaveView
和GLContext
里包含了所有的實現(xiàn)代碼蜀备。fragment.glsl
和vertex.glsl
兩個著色器是整個效果的核心。先來看看GLWaveView
的代碼吧荒叶。
GLWaveView
GLWaveView
繼承自GLKView
碾阁,所以可以很方便的初始化OpenGL相關環(huán)境。
static EAGLContext *eaglContext;
if (eaglContext == nil) {
eaglContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:eaglContext];
}
self.context = eaglContext;
self.drawableMultisample = GLKViewDrawableMultisample4X;
因為EAGLContext
當前線程只能設置一個些楣,所以使用靜態(tài)變量來表示脂凶,接著為GLWaveView
設置好EAGLContext
和多重采樣率GLKViewDrawableMultisample4X
泊脐,多重采樣可以讓鋸齒更平滑喉钢。為了實現(xiàn)動畫,需要一個循環(huán)來運行渲染代碼笆环,這里使用的是CADisplayLink
鹅很。
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(ticked)];
displayLink.preferredFramesPerSecond = 60;
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
lastTime = [[NSDate date] timeIntervalSince1970];
- (void)ticked {
NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
currentTime += now - lastTime;
lastTime = now;
[self display];
}
可以為CADisplayLink
指定需要的幀率preferredFramesPerSecond
嘶居,這里我設定的是60fps。lastTime
用來保存上一次更新的時間戳促煮,ticked
會被循環(huán)調用邮屁,每調用一次整袁,都會計算當前經過的總時長currentTime
。[self display];
調用后會觸發(fā)重繪佑吝。執(zhí)行下面的代碼坐昙。下面的代碼主要就是繪制了一個撐滿當前View的四邊形,并且綁定了一張圖片到diffuseMap
芋忿。這個會在Shader中用來當遮罩圖民珍。
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
[self.glContext active];
glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);
static GLfloat triangleData[] = {
-1, 1, 0.5, 0, 0, 1, 0, 0,
-1, -1, 0.5, 0, 0, 1, 0, 1,
1, -1, 0.5, 0, 0, 1, 1, 1,
1, -1, 0.5, 0, 0, 1, 1, 1,
1, 1, 0.5, 0, 0, 1, 1, 0,
-1, 1, 0.5, 0, 0, 1, 0, 0,
};
[self.glContext setUniform1f:@"time" value:currentTime];
[self.glContext bindTexture:self.diffuseMap to:GL_TEXTURE0 uniformName:@"diffuseMap"];
[self.glContext drawTriangles:triangleData vertexCount:6];
}
在此之前,你還需要設置
self.delegate = self;
盗飒,- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
是GLKView
的delegate中的一個方法嚷量。同時初始化OpenGL的工具類GLContext也是必要的。綜合起來初始化代碼如下逆趣。
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
static EAGLContext *eaglContext;
if (eaglContext == nil) {
eaglContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:eaglContext];
}
self.context = eaglContext;
self.drawableMultisample = GLKViewDrawableMultisample4X;
self.delegate = self;
self.layer.backgroundColor = [UIColor clearColor].CGColor;
self.layer.opaque = NO;
NSString *vertexShader = [[NSBundle mainBundle] pathForResource:@"vertex" ofType:@"glsl"];
NSString *fragmentShader = [[NSBundle mainBundle] pathForResource:@"fragment" ofType:@"glsl"];
NSString *vertexShaderContent = [NSString stringWithContentsOfFile:vertexShader encoding:NSUTF8StringEncoding error:nil];
NSString *fragmentShaderContent = [NSString stringWithContentsOfFile:fragmentShader encoding:NSUTF8StringEncoding error:nil];
self.glContext = [[GLContext alloc] initWithVertexShader:vertexShaderContent fragmentShader:fragmentShaderContent];
self.diffuseMap = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"mask.png"].CGImage options:nil error:nil];
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(ticked)];
displayLink.preferredFramesPerSecond = 60;
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
lastTime = [[NSDate date] timeIntervalSince1970];
}
return self;
}
如果你對OpenGL不熟悉蝶溶,可以去閱讀學習OpenGL ES系列文章,GLContext這個類是從那邊直接拿過來的宣渗。主要封裝了OpenGL的一些基本操作抖所。
有了上面這些,就可以使用OpenGL繪制一個四邊形了痕囱。接下來就該Fragment Shader出場了田轧。
Fragment Shader
先來說一下思路,用uv當做坐標值鞍恢,計算當前uv.x對應的正弦值sin(uv.x)傻粘,如果uv.y在sin(uv.x)之下,就給gl_FragColor
著色帮掉。這樣就能繪制出一個基本的波浪弦悉。判斷當前點是否著色的代碼如下。
bool shouldBeColored(float waveAmplitude, float waveHeight, float phase, float period) {
float x = fragUV.x * period; // x軸范圍0~period
float y = 1.0 - fragUV.y;
float topY = (sin(x + phase) + 1.0) / 2.0 * waveAmplitude - waveAmplitude / 2.0 + waveHeight;
return y <= topY;
}
waveAmplitude
是波峰和波谷的間距蟆炊,waveHeight
是波峰的位置稽莉,phase
是計算正弦的初始相位,period
是可見范圍的周期涩搓。下面是示意圖污秆。因為uv的y是從0到1的,所以先進行翻轉float y = 1.0 - fragUV.y;
昧甘,最后如果翻轉后的y在topY之下即可著色良拼。
下面是完整代碼。
precision highp float;
varying vec3 fragNormal;
varying vec2 fragUV;
uniform sampler2D diffuseMap;
uniform float time;
bool shouldBeColored(float waveAmplitude, float waveHeight, float phase, float period) {
float x = fragUV.x * period; // x軸范圍0~period
float y = 1.0 - fragUV.y;
float topY = (sin(x + phase) + 1.0) / 2.0 * waveAmplitude - waveAmplitude / 2.0 + waveHeight;
return y <= topY;
}
void main(void) {
vec4 color = texture2D(diffuseMap, fragUV);
float baseFactor = (sin(time / 4.5) + 1.0) / 2.0;
float heightFactor = baseFactor;
float phaseFactor = time * 3.14;
float period = 3.14 * 1.4; // 周期
vec4 finalColor = vec4(0.2, 0.2, 0.2, 1.0);
if (shouldBeColored(0.07, heightFactor, phaseFactor, period)) {
finalColor = finalColor * 0.0 + vec4(1.0, 0.4, 0.4, 1.0);
}
if (shouldBeColored(0.05, heightFactor - 0.02, phaseFactor - 0.25, period)) {
finalColor = finalColor * 0.4 + vec4(1.0, 0.1, 0.1, 1.0) * 0.6;
}
gl_FragColor = finalColor * color.a;
}
上面一共繪制了兩個波浪疾层,第二個波浪賦予了0.6的Alpha值将饺,采用SrcColor * SrcAlpha + DstColor * (1 - SrcAlpha)的混合算法贡避。兩個波浪的有0.02的振幅差痛黎,和0.02的高度差予弧,相位差0.25。高度和相位都會隨著時間改變而改變湖饱。周期是1.4Pi掖蛤。
最后finalColor * color.a;
將會使遮罩圖上alpha為0的部分不可見,從而達到遮罩的效果井厌。
GitHub上的代碼中也包含UIBezierPath
實現(xiàn)的版本CAWaveView
蚓庭,有興趣的可以自己clone查看。