iOS波浪效果-OpenGL實現(xiàn)篇

本文所用的代碼在https://github.com/SquarePants1991/WaveEffect

工作需要,最近要實現(xiàn)一個波浪效果,一般的做法是使用UIBezierPath生成sin曲線懦傍,通過CADisplayLink刷新曲線的相位或者幅度來達到波浪效果。本文要介紹另外一種方式,使用OpenGL來實現(xiàn)波浪效果佑笋。下面是效果圖,上面使用的是GLKView斑鼻,下面是CAShapeLayer蒋纬。這兩種方式我都做了簡單的遮罩效果。


接下來就重點介紹如何使用OpenGL實現(xiàn)這樣的效果坚弱。GLWaveViewGLContext里包含了所有的實現(xiàn)代碼蜀备。fragment.glslvertex.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)rectGLKView的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查看。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末仅仆,一起剝皮案震驚了整個濱河市器赞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌墓拜,老刑警劉巖港柜,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異咳榜,居然都是意外死亡夏醉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門涌韩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畔柔,“玉大人,你說我怎么就攤上這事臣樱“胁粒” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵雇毫,是天一觀的道長奢啥。 經常有香客問我,道長嘴拢,這世上最難降的妖魔是什么桩盲? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮席吴,結果婚禮上赌结,老公的妹妹穿的比我還像新娘。我一直安慰自己孝冒,他們只是感情好柬姚,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著庄涡,像睡著了一般量承。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天撕捍,我揣著相機與錄音拿穴,去河邊找鬼。 笑死忧风,一個胖子當著我的面吹牛默色,可吹牛的內容都是我干的。 我是一名探鬼主播狮腿,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼腿宰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了缘厢?” 一聲冷哼從身側響起吃度,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贴硫,沒想到半個月后规肴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡夜畴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年拖刃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贪绘。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡兑牡,死狀恐怖,靈堂內的尸體忽然破棺而出税灌,到底是詐尸還是另有隱情均函,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布菱涤,位于F島的核電站苞也,受9級特大地震影響,放射性物質發(fā)生泄漏粘秆。R本人自食惡果不足惜如迟,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望攻走。 院中可真熱鬧殷勘,春花似錦、人聲如沸昔搂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摘符。三九已至贤斜,卻和暖如春策吠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘩绒。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工猴抹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人草讶。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像炉菲,于是被迫代替她去往敵國和親堕战。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內容