学习OpenGL ES之激光特效

本系列所有文章目录

获取示例代码


本文主要介绍如何使用2个四边形实现一个简单的激光效果。下面是最终效果图。


在了解激光实现原理之前,先介绍一下我对上一篇文章的代码进行的简单重构。我把OpenGL关键性的代码都集成到了GLContext类中。

#import <GLKit/GLKit.h>

@interface GLContext : NSObject
@property (assign, nonatomic) GLuint program;
+ (id)contextWithVertexShaderPath:(NSString *)vertexShaderPath fragmentShaderPath:(NSString *)fragmentShaderPath;
- (id)initWithVertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader;
- (void)active;

/// draw functions
- (void)drawTriangles:(GLfloat *)triangleData vertexCount:(GLint)vertexCount;

/// uniform setters
- (void)setUniform1i:(NSString *)uniformName value:(GLint)value;
- (void)setUniform1f:(NSString *)uniformName value:(GLfloat)value;
- (void)setUniform3fv:(NSString *)uniformName value:(GLKVector3)value;
- (void)setUniformMatrix4fv:(NSString *)uniformName value:(GLKMatrix4)value;

/// texture
- (void)bindTexture:(GLKTextureInfo *)textureInfo to:(GLenum)textureChannel uniformName:(NSString *)uniformName;

@end

这个类可以做以下事情:

  1. 编译链接Shader,生成program
  2. 调用active激活GLContext中的program
  3. 使用drawTriangles绘制三角形,后面会增加绘制三角带,线和点等等。
  4. setUniformXXX系列方法用来设置各种uniform的值。当然还可以增加更多。
  5. bindTexture用来绑定纹理到指定通道。

有了这个类之后,我们可以为不同的Shader建立不同的GLContext,这就意味着我们可以方便的在同一场景使用不同的Shader渲染不同的物体。GLContext的实现代码都是之前已有的代码,比较简单就不详述了。
回到激光特效实现的原理, 开头说到它是由两个四边形组成的,具体的形状如下。


两个四边形垂直相交,然后分别为他们加上一张纹理图。



渲染这两个四边形时,不能够使用前面介绍的光照模型,在Fragment Shader中直接返回纹理色即可,所以我们可以为激光单独创建一个Fragment Shader。

precision highp float;

varying vec2 fragUV;

uniform sampler2D diffuseMap;
uniform float life; // max: 1, min: 0
uniform float hue;

#define Max(a, b) (a > b ? a : b)
#define Min(a, b) (a < b ? a : b)

float hue2rgb(float p, float q, float t) {
    if(t < 0.0) t += 1.0;
    if(t > 1.0) t -= 1.0;
    if(t < 1.0/6.0) return p + (q - p) * 6.0 * t;
    if(t < 1.0/2.0) return q;
    if(t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
    return p;
}

vec3 hslToRgb(float h, float s, float l){
    float r, g, b;
    if(s == 0.0){
        r = g = b = l; // achromatic
    }else{
        float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
        float p = 2.0 * l - q;
        r = hue2rgb(p, q, h + 1.0/3.0);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1.0/3.0);
    }

    return vec3(r, g, b);
}

vec3 rgbToHsl(float r, float g, float b) {
    float max = Max(r, Max(g, b));
    float min = Min(r, Min(g, b));
    float h, s, l = (max + min) / 2.0;

    if(max == min){
        h = s = 0.0; // achromatic
    }else{
        float d = max - min;
        s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min);
        if (max == r) h = (g - b) / d + (g < b ? 6.0 : 0.0);
        if (max == g) h = (b - r) / d + 2.0;
        if (max == b) h = (r - g) / d + 4.0;
        h /= 6.0;
    }

    return vec3(h, s, l);
}

void main(void) {
    float v = (fragUV.y > 0.05 && fragUV.y < 0.95) ? 0.5 : fragUV.y;
    vec4 materialColor = texture2D(diffuseMap, vec2(fragUV.x, v));
    vec3 hsl = rgbToHsl(materialColor.x, materialColor.y, materialColor.z);
    hsl.x = hue;
    vec3 rgb = hslToRgb(hsl.x, hsl.y, hsl.z);
    gl_FragColor = vec4(rgb, materialColor.a * life);
}

看起来很长,因为除了实现读取纹理色之外,还实现了变色,渐隐渐现,防止拉伸过渡等功能。下面我来逐一解释这些功能。

渐隐渐现

该功能主要依靠uniform float life;实现,life的值从0到1。gl_FragColor = vec4(rgb, materialColor.a * life);所以最后相乘等到的颜色也是从全透明到原本的颜色,从而实现了渐隐渐现的效果。

变色

例子中贴图的颜色是蓝色的,为了不做更多颜色的贴图却可以实现不同颜色的激光效果,这里将颜色从RGB空间转换为HSL空间,然后根据uniform float hue;调整HSL的第一个组件值Hue就可以调整颜色了,更多关于HSL的知识可以看百度百科。相关代码如下。

uniform float hue;

#define Max(a, b) (a > b ? a : b)
#define Min(a, b) (a < b ? a : b)

float hue2rgb(float p, float q, float t) {
    if(t < 0.0) t += 1.0;
    if(t > 1.0) t -= 1.0;
    if(t < 1.0/6.0) return p + (q - p) * 6.0 * t;
    if(t < 1.0/2.0) return q;
    if(t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
    return p;
}

vec3 hslToRgb(float h, float s, float l){
    float r, g, b;
    if(s == 0.0){
        r = g = b = l; // achromatic
    }else{
        float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s;
        float p = 2.0 * l - q;
        r = hue2rgb(p, q, h + 1.0/3.0);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1.0/3.0);
    }

    return vec3(r, g, b);
}

vec3 rgbToHsl(float r, float g, float b) {
    float max = Max(r, Max(g, b));
    float min = Min(r, Min(g, b));
    float h, s, l = (max + min) / 2.0;

    if(max == min){
        h = s = 0.0; // achromatic
    }else{
        float d = max - min;
        s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min);
        if (max == r) h = (g - b) / d + (g < b ? 6.0 : 0.0);
        if (max == g) h = (b - r) / d + 2.0;
        if (max == b) h = (r - g) / d + 4.0;
        h /= 6.0;
    }

    return vec3(h, s, l);
}
void main(void) {
...
    vec3 hsl = rgbToHsl(materialColor.x, materialColor.y, materialColor.z);
    hsl.x = hue;
    vec3 rgb = hslToRgb(hsl.x, hsl.y, hsl.z);
...
}

HSL代表色相(H)、饱和度(S)、明度(L),其中色相是控制颜色的主要组件。

防止过度拉伸

这行代码主要就是防止贴图在y方向上过度拉伸,对于大于0.05小于0.95的值一律都按照0.5处理,当然也可以为x方向做同样的处理。这个和日常App开发中九宫格的原理是类似的。

float v = (fragUV.y > 0.05 && fragUV.y < 0.95) ? 0.5 : fragUV.y;

以上就是为激光编写的Fragment Shader,至于Vertex Shader,还可以复用原来的。回到OC代码。首先为激光的Shader创建GLContext

- (void)prepareLaserGLContext {
    NSString *vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"vertex" ofType:@".glsl"];
    NSString *fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"frg_laser" ofType:@".glsl"];
    self.laserContext = [GLContext contextWithVertexShaderPath:vertexShaderPath fragmentShaderPath:fragmentShaderPath];
}

接着,在绘制代码中就可以使用GLContext的相关方法代替原有的绘制代码了。

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    [super glkView:view drawInRect:rect];

    [self.laserContext active];
    [self.laserContext setUniform1f:@"elapsedTime" value:(GLfloat)self.elapsedTime];
    [self.laserContext setUniformMatrix4fv:@"projectionMatrix" value:self.projectionMatrix];
    [self.laserContext setUniformMatrix4fv:@"cameraMatrix" value:self.cameraMatrix];

    [self.laserContext setUniform3fv:@"lightDirection" value:self.lightDirection];

    [self.lasers enumerateObjectsUsingBlock:^(Laser *obj, NSUInteger idx, BOOL *stop) {
        [obj draw:self.laserContext];
    }];
}

为了让创建多个激光变得简单,我将激光的相关方法封装到了Laser类中。我们在ViewController中要做的事情就是实例化多个Laser。并且在update中更新他们的状态。

- (void)prepareLasers {
    Laser *laser = [[Laser alloc] initWithLaserImage:[UIImage imageNamed:@"laser.png"]];
    laser.position = GLKVector3Make(0, 0, -40);
    laser.direction = GLKVector3Make(0.08, 0.08, 1);
    laser.length = 60;
    laser.radius = 1;
    [self.lasers addObject:laser];

    laser = [[Laser alloc] initWithLaserImage:[UIImage imageNamed:@"laser.png"]];
    laser.position = GLKVector3Make(0, 0, -40);
    laser.direction = GLKVector3Make(-0.08, -0.08, 1);
    laser.length = 60;
    laser.radius = 1;
    [self.lasers addObject:laser];

    laser = [[Laser alloc] initWithLaserImage:[UIImage imageNamed:@"laser.png"]];
    laser.position = GLKVector3Make(0, 0, -40);
    laser.direction = GLKVector3Make(-0.08, -0.08, 1);
    laser.length = 60;
    laser.radius = 1;
    [self.lasers addObject:laser];

    laser = [[Laser alloc] initWithLaserImage:[UIImage imageNamed:@"laser.png"]];
    laser.position = GLKVector3Make(0, 0, -40);
    laser.direction = GLKVector3Make(-0.08, -0.08, 1);
    laser.length = 60;
    laser.radius = 1;
    [self.lasers addObject:laser];
}

- (void)update {
    [super update];
    [self.lasers enumerateObjectsUsingBlock:^(Laser *obj, NSUInteger idx, BOOL *stop) {
        [obj update:self.timeSinceLastUpdate];
    }];
}

最后我们来看看Laser类中的具体实现。

#import <GLKit/GLKit.h>

@class GLContext;

@interface Laser : NSObject
@property (assign, nonatomic) GLfloat life;
@property (assign, nonatomic) GLKVector3 position;
@property (assign, nonatomic) GLKVector3 direction;
@property (assign, nonatomic) float length;
@property (assign, nonatomic) float radius;

- (id)initWithLaserImage:(UIImage *)image;
- (void)update:(NSTimeInterval)timeSinceLastUpdate;
- (void)draw:(GLContext *)glContext;
@end

life就是之前提到的控制渐隐渐现的参数,position表示激光发射的位置,direction表示激光发射的方向,length是长度,radius是直径。初始化的时候将纹理图片传入即可。

update中计算对应的modelMatrix

- (void)update:(NSTimeInterval)timeSinceLastUpdate {
    self.life -= timeSinceLastUpdate;
    if (self.life <= 0) {
        self.life = 1;
        float x = rand() / (float)RAND_MAX * 0.1 - 0.05;
        float y = rand() / (float)RAND_MAX * 0.1 - 0.05;
        self.direction = GLKVector3Normalize(GLKVector3Make(x, y, 1));
        self.hue = rand() / (float)RAND_MAX * 1.0;
    }

    GLKVector3 defaultForward = GLKVector3Make(0, 0, 1);
    GLKVector3 rotateAxis = GLKVector3CrossProduct(defaultForward, self.direction);
    float cosAngle = GLKVector3DotProduct(defaultForward, self.direction);
    float angle = acos(cosAngle);

    GLKMatrix4 scaleMatrix = GLKMatrix4MakeScale(self.length, self.radius, self.radius);
    GLKMatrix4 rotateToZMatrix = GLKMatrix4MakeRotation(M_PI / 2, 0, 1, 0);
    GLKMatrix4 translateMatrix = GLKMatrix4MakeTranslation(0, 0, self.length / 2);
    GLKMatrix4 rotateMatrix = GLKMatrix4MakeRotation(angle, rotateAxis.x, rotateAxis.y, rotateAxis.z);
    GLKMatrix4 positionTranslateMatrix = GLKMatrix4MakeTranslation(self.position.x, self.position.y, self.position.z);
    self.modelMatrix = GLKMatrix4Multiply(rotateToZMatrix, scaleMatrix);
    self.modelMatrix = GLKMatrix4Multiply(translateMatrix, self.modelMatrix);
    self.modelMatrix = GLKMatrix4Multiply(rotateMatrix, self.modelMatrix);
    self.modelMatrix = GLKMatrix4Multiply(positionTranslateMatrix, self.modelMatrix);
}

上面的代码在每一次life小于等于0时,重置激光方向和色相Hue。计算modelMatrix的步骤如下:

  1. 根据方向计算旋转轴和旋转角。
  2. 计算缩放矩阵scaleMatrix将激光缩放到长self.length,直径self.radius
  3. 因为默认较长的方向是x轴,所以再计算旋转到z轴的旋转矩阵rotateToZMatrix
  4. 旋转到z轴后,将一端移至(0,0,0)点,计算出translateMatrix
  5. 在根据刚开始计算的旋转角和旋转轴计算旋转矩阵rotateMatrix
  6. 最后计算将激光移至self.position的矩阵positionTranslateMatrix

将上述矩阵相乘即可得到modelMatrix,注意是从下往上乘。

具体的绘制代码如下。lifehue都在- (void)draw:(GLContext *)glContext里传递给了Shader。- (void)drawLaser:(GLContext *)glContext中绘制了两个垂直平面,并且在绘制过程中禁用了DepthMask

- (void)draw:(GLContext *)glContext {
    [glContext setUniformMatrix4fv:@"modelMatrix" value:self.modelMatrix];
    bool canInvert;
    GLKMatrix4 normalMatrix = GLKMatrix4InvertAndTranspose(self.modelMatrix, &canInvert);
    [glContext setUniformMatrix4fv:@"normalMatrix" value:canInvert ? normalMatrix : GLKMatrix4Identity];

    [glContext setUniform1f:@"life" value:self.life];
    [glContext bindTexture:self.diffuseTexture to:GL_TEXTURE0 uniformName:@"diffuseMap"];
    [glContext setUniform1f:@"hue" value:self.hue];

    [self drawLaser: glContext];
}

- (void)drawLaser:(GLContext *)glContext{
    glDepthMask(GL_FALSE);

    static GLfloat plane1[] = {
        -0.5, 0.5f, 0, 1, 0, 0,     1, 0, // x, y, z, r, g, b,每一行存储一个点的信息,位置和颜色
        -0.5f, -0.5f, 0, 0, 1, 0,   0, 0,
        0.5f, -0.5f, 0, 0, 0, 1,    0, 1,
        0.5, -0.5f, 0, 0, 0, 1,     0, 1,
        0.5f, 0.5f, 0, 0, 1, 0,     1, 1,
        -0.5f, 0.5f, 0, 1, 0, 0,    1, 0,
    };
    [glContext drawTriangles:plane1 vertexCount:6];
    
    static GLfloat plane2[] = {
        -0.5,0, 0.5f,  1, 0, 0,     1, 0, // x, y, z, r, g, b,每一行存储一个点的信息,位置和颜色
        -0.5f,0, -0.5f,  0, 1, 0,   0, 0,
        0.5f, 0, -0.5f, 0, 0, 1,    0, 1,
        0.5,0, -0.5f,  0, 0, 1,     0, 1,
        0.5f, 0, 0.5f, 0, 1, 0,     1, 1,
        -0.5f, 0,0.5f,  1, 0, 0,    1, 0,
    };
    [glContext drawTriangles:plane2 vertexCount:6];

    glDepthMask(GL_TRUE);
}

本文的例子中使用的BlendFunc是glBlendFunc (GL_SRC_ALPHA, GL_DST_ALPHA);,这种混合方式可以让激光显得更明亮一些。

到此,激光的效果就介绍完了,这个效果涉及到了之前大部分的知识,算是一个阶段性总结了吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容