美文网首页iOS 砖家纪实录OpenGL绘制
iOS开发-OpenGL ES画图应用

iOS开发-OpenGL ES画图应用

作者: 落影loyinglin | 来源:发表于2016-03-31 16:20 被阅读4652次

    这是一篇OpenGL ES的实战,紧接 入门教程3
    学了OpenGL ES一段时间,用这个应用来练练手。

    OpenGL ES系列教程在这里
    OpenGL ES系列教程的代码地址 - 你的star和fork是我的源动力,你的意见能让我走得更远。

    效果展示

    实战.gif

    demo来自苹果官方,可以学习苹果的工程师如何应用OpenGL ES。
    这次的内容包括,shaderCoreGraphics手势识别运动轨迹模糊点效果

    shader

    自定义enum,方便OC与shader之间的赋值,配合下面的自动assign功能,非常便捷。

    enum {
        PROGRAM_POINT,
        NUM_PROGRAMS
    };
    
    enum {
        UNIFORM_MVP,
        UNIFORM_POINT_SIZE,
        UNIFORM_VERTEX_COLOR,
        UNIFORM_TEXTURE,
        NUM_UNIFORMS
    };
    
    enum {
        ATTRIB_VERTEX,
        NUM_ATTRIBS
    };
    
    typedef struct {
     char *vert, *frag;
     GLint uniform[NUM_UNIFORMS];
     GLuint id;
    } programInfo_t;
    
    programInfo_t program[NUM_PROGRAMS] = {
        { "point.vsh",   "point.fsh" },     // PROGRAM_POINT
    };
    

    创建program的过程,用glBindAttribLocation()glueGetUniformLocation ()来绑定attribute和uniform变量。

    /* Convenience wrapper that compiles, links, enumerates uniforms and attribs */
    GLint glueCreateProgram(const GLchar *vertSource, const GLchar *fragSource,
                           GLsizei attribNameCt, const GLchar **attribNames, 
                           const GLint *attribLocations,
                           GLsizei uniformNameCt, const GLchar **uniformNames, 
                           GLint *uniformLocations,
                           GLuint *program)
    {
     GLuint vertShader = 0, fragShader = 0, prog = 0, status = 1, i;
     
     prog = glCreateProgram();
    
     status *= glueCompileShader(GL_VERTEX_SHADER, 1, &vertSource, &vertShader);
     status *= glueCompileShader(GL_FRAGMENT_SHADER, 1, &fragSource, &fragShader);
     glAttachShader(prog, vertShader);
     glAttachShader(prog, fragShader);
     
     for (i = 0; i < attribNameCt; i++)
     {
      if(strlen(attribNames[i]))
       glBindAttribLocation(prog, attribLocations[i], attribNames[i]);
     }
     
     status *= glueLinkProgram(prog);
     status *= glueValidateProgram(prog);
    
     if (status)
     { 
            for(i = 0; i < uniformNameCt; i++)
      {
                if(strlen(uniformNames[i]))
           uniformLocations[i] = glueGetUniformLocation(prog, uniformNames[i]);
      }
      *program = prog;
     }
     if (vertShader)
      glDeleteShader(vertShader);
     if (fragShader)
      glDeleteShader(fragShader);
     glError();
      
     return status;
    }
    

    shader的编译之前困扰过我很久,这个demo介绍了一种方法可以获取编译错误信息,非常的nice。

    #define glError() { \
     GLenum err = glGetError(); \
     if (err != GL_NO_ERROR) { \
      printf("glError: %04x caught at %s:%u\n", err, __FILE__, __LINE__); \
     } \
    }
    

    CoreGraphics

    自定义textureInfo_t结构体,在textureFromName()用CoreGraphics把url对应的image data缓存到OpenGLES,并通过textureInfo_t返回信息。

    // Texture
    typedef struct {
        GLuint id;
        GLsizei width, height;
    } textureInfo_t;
    
    // Create a texture from an image
    - (textureInfo_t)textureFromName:(NSString *)name
    {
        CGImageRef  brushImage;
     CGContextRef brushContext;
     GLubyte   *brushData;
     size_t   width, height;
        GLuint          texId;
        textureInfo_t   texture;
        
        // First create a UIImage object from the data in a image file, and then extract the Core Graphics image
        brushImage = [UIImage imageNamed:name].CGImage;
        
        // Get the width and height of the image
        width = CGImageGetWidth(brushImage);
        height = CGImageGetHeight(brushImage);
        
        // Make sure the image exists
        if(brushImage) {
            // Allocate  memory needed for the bitmap context
            brushData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
            // Use  the bitmatp creation function provided by the Core Graphics framework.
            brushContext = CGBitmapContextCreate(brushData, width, height, 8, width * 4, CGImageGetColorSpace(brushImage), kCGImageAlphaPremultipliedLast);
            // After you create the context, you can draw the  image to the context.
            CGContextDrawImage(brushContext, CGRectMake(0.0, 0.0, (CGFloat)width, (CGFloat)height), brushImage);
            // You don't need the context at this point, so you need to release it to avoid memory leaks.
            CGContextRelease(brushContext);
            // Use OpenGL ES to generate a name for the texture.
            glGenTextures(1, &texId);
            // Bind the texture name.
            glBindTexture(GL_TEXTURE_2D, texId);
            // Set the texture parameters to use a minifying filter and a linear filer (weighted average)
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            // Specify a 2D texture image, providing the a pointer to the image data in memory
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)width, (int)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, brushData);
            // Release  the image data; it's no longer needed
            free(brushData);
            
            texture.id = texId;
            texture.width = (int)width;
            texture.height = (int)height;
        }
        
        return texture;
    }
    

    手势识别

    这里的手势只有点击和滑动,通过记录touchesBegan,获取第一个点的位置,之后滑动的过程中touchesMoved获取到这次的位置和上次的位置,可以画出一道手指滑动的轨迹,通过renderLineFromPoint()绘制。

    // Handles the start of a touch
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {   
     CGRect    bounds = [self bounds];
     UITouch*   touch = [[event touchesForView:self] anyObject];
     firstTouch = YES;
     // Convert touch point from UIView referential to OpenGL one (upside-down flip)
     location = [touch locationInView:self];
     location.y = bounds.size.height - location.y;
    }
    
    // Handles the continuation of a touch.
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
    {   
     CGRect    bounds = [self bounds];
     UITouch*   touch = [[event touchesForView:self] anyObject];
      
     // Convert touch point from UIView referential to OpenGL one (upside-down flip)
     if (firstTouch) {
      firstTouch = NO;
      previousLocation = [touch previousLocationInView:self];
      previousLocation.y = bounds.size.height - previousLocation.y;
     } else {
      location = [touch locationInView:self];
         location.y = bounds.size.height - location.y;
      previousLocation = [touch previousLocationInView:self];
      previousLocation.y = bounds.size.height - previousLocation.y;
     }
      
     // Render the stroke
        if (!lyArr) {
            lyArr = [NSMutableArray array];
        }
        [lyArr addObject:[[LYPoint alloc] initWithCGPoint:previousLocation]];
        [lyArr addObject:[[LYPoint alloc] initWithCGPoint:location]];
    
        
     [self renderLineFromPoint:previousLocation toPoint:location];
    }
    
    // Handles the end of a touch event when the touch is a tap.
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
    {
     CGRect    bounds = [self bounds];
     UITouch*  touch = [[event touchesForView:self] anyObject];
     if (firstTouch) {
      firstTouch = NO;
      previousLocation = [touch previousLocationInView:self];
      previousLocation.y = bounds.size.height - previousLocation.y;
      [self renderLineFromPoint:previousLocation toPoint:location];
     }
    }
    
    // Handles the end of a touch event.
    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
    {
     // If appropriate, add code necessary to save the state of the application.
     // This application is not saving state.
        NSLog(@"cancell");
    }
    

    运动轨迹

    通过把起点到终点的轨迹分解成若干个点,分别来绘制每个点,从而达到线的效果。
    count = MAX(ceilf(sqrtf((end.x - start.x) * (end.x - start.x) + (end.y - start.y) * (end.y - start.y)) / kBrushPixelStep), 1);
    这行代码是核心思想,分出count个点。
    然后通过glDrawArrays(GL_POINTS, 0, (int)vertexCount);绘制。

    // Drawings a line onscreen based on where the user touches
    - (void)renderLineFromPoint:(CGPoint)start toPoint:(CGPoint)end
    {
     static GLfloat*  vertexBuffer = NULL;
     static NSUInteger vertexMax = 64;
     NSUInteger   vertexCount = 0,
          count,
          i;
     
     [EAGLContext setCurrentContext:context];
     glBindFramebuffer(GL_FRAMEBUFFER, viewFramebuffer);
     
     // Convert locations from Points to Pixels
     CGFloat scale = self.contentScaleFactor;
     start.x *= scale;
     start.y *= scale;
     end.x *= scale;
     end.y *= scale;
     
     // Allocate vertex array buffer
     if(vertexBuffer == NULL)
      vertexBuffer = malloc(vertexMax * 2 * sizeof(GLfloat));
     
     // Add points to the buffer so there are drawing points every X pixels
     count = MAX(ceilf(sqrtf((end.x - start.x) * (end.x - start.x) + (end.y - start.y) * (end.y - start.y)) / kBrushPixelStep), 1);
     for(i = 0; i < count; ++i) {
      if(vertexCount == vertexMax) {
       vertexMax = 2 * vertexMax;
       vertexBuffer = realloc(vertexBuffer, vertexMax * 2 * sizeof(GLfloat));
      }
      
      vertexBuffer[2 * vertexCount + 0] = start.x + (end.x - start.x) * ((GLfloat)i / (GLfloat)count);
      vertexBuffer[2 * vertexCount + 1] = start.y + (end.y - start.y) * ((GLfloat)i / (GLfloat)count);
      vertexCount += 1;
     }
        
     // Load data to the Vertex Buffer Object
     glBindBuffer(GL_ARRAY_BUFFER, vboId);
     glBufferData(GL_ARRAY_BUFFER, vertexCount*2*sizeof(GLfloat), vertexBuffer, GL_DYNAMIC_DRAW);
     
        glEnableVertexAttribArray(ATTRIB_VERTEX);
        glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, GL_FALSE, 0, 0);
     
     // Draw
        glUseProgram(program[PROGRAM_POINT].id);
     glDrawArrays(GL_POINTS, 0, (int)vertexCount);
     
     // Display the buffer
     glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer);
     [context presentRenderbuffer:GL_RENDERBUFFER];
    }
    

    模糊点的效果

    点模糊的效果通过开启混合模式,并设置混合函数

    // Enable blending and set a blending function appropriate for premultiplied alpha pixel data
        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    

    注意color和texture2D的操作符是*,不是+。

    uniform sampler2D texture;
    varying lowp vec4 color;
    
    void main()
    {
     gl_FragColor = color * texture2D(texture, gl_PointCoord);
    }
    

    最后

    送上一张图

    画图.gif
    附上源码

    思考题

    • 如何改动开头的加油?

    相关文章

      网友评论

      • Theendisthebegi:感觉颜色怪怪的,红色不像红色
      • CCla:你好落影大神,我想问个问题 glGetProgramiv(program, GL_LINK_STATUS, &status),这个方法应该不只是检查链接状态吧,我误写成 glGetProgramiv(program, GL_COMPILE_STATUS, &status) 结果画板完全不能用。能加个QQ 吗?我不会经常打扰你的 924045203
      • 超人伟伟:你好,非常感谢你的讲解,我基于你的例子修改为每个点画一张小图片,但是发现给非常奇怪的问题,就是图片的宽高必须是 2.26 倍数(ParticleEx.png的宽高就是2.26),否则画出来是黑的,很疑惑是什么问题?
      • 0a2868f025e0:落影大神,怎么在画线的过程中加上粒子效果
      • 2c8aea30e451:樓主好,最近我在製作 OpenGLES 畫圖的功能,正正常常的,但當我的畫面愈來愈長時,便有一個難題解決不了
        當我的 print View 超過某長度(例如4000),他便會報 " failed to make complete framebuffer object 8cd6 "
        需要設換至 2000左右的長度才可以使用,是什麼原因?
        2c8aea30e451:@落影loyinglin 那有沒有辦法更改 frameBuffer 的大小?我加了 depth buffer 也看似未什麼作用的 ...
        落影loyinglin:@kinGwL frameBuffer是有大小限制的
      • shineDeveloper:我看了代码,才知道开头的加油是这段代码起的作用,opengl这个框架总是依赖c。我对c不是很熟悉
        // Playback recorded path, which is "Shake Me"
        NSString* path = [[NSBundle mainBundle] pathForResource:@"abc" ofType:@"string"];
        NSString* str = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];

        lyArr = [NSMutableArray array];
        NSArray* jsonArr = [NSJSONSerialization JSONObjectWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:nil];
        for (NSDictionary* dict in jsonArr) {
        LYPoint* point = [LYPoint new];
        point.mX = [dict objectForKey:@"mX"];
        point.mY = [dict objectForKey:@"mY"];
        [lyArr addObject:point];
        }
        落影loyinglin:@细雨999 可以的。你可以264文件 转成mp4 再播放
        shineDeveloper:@落影lying-in iOS中的Core Animation框架也可以做各种动画,画出各种图。和OpenGL 有很多相似的用法。我是这几天了解直播的相关内容,解码264视屏文件之后,用到了opengl的框架,播放视频。我不明白直播就必须要用opengl框架吗?从服务器下载下来的264文件就不能转成其他的视频格式吗?
        落影loyinglin:@细雨999 其实gl语法是通用的,这部分和安卓也是通用的
      • 旅行的光:你好大神,最近看了你的OpenGLES系列,自己也通过书籍了解了一下OpenGLES的开发,算是有了一个初步入门的概念。看了你这篇文章后,我想问一下,如果有一张图片,然后我在上面写了字。如何能够保存写过字之后的图片呢?谢谢,另外能否加一个QQ,401169638。
      • 困惑困惑困惑:开头的加油如何改啊
        落影loyinglin:@困惑困惑困惑 你记录下手指的位置,自己手写一遍,再重复播放就行。
      • 人民形象公司:楼主好,首先谢谢您的文章。我想根据这篇源代码中的GLpaint源代码实现一个涂抹功能,可以指定用一些模糊的马赛克、雨点之类的图片作纹理。现在我遇到了几个问题想请教下:看到文章图片中有上一步下一步的功能,但是gitHub中的代码中没有实现,我自己实现的话目前的思路是用一个数组保存每一次touchMove中产生的店,然后点上一步的时再对这个数组调 [self renderLineFromPoint:point1 toPoint:point2]这个方法重新渲染,那么有两个问题:1.针对每一个点遍历的话会有种“反手指移动轨迹”的感觉 ,怎么才能一下将上一步画的涂抹去掉或者恢复呢?2.颜色问题,假如我将PaintingView作为背景透明的一个遮罩盖在一张图片上,涂抹之后点击上一步,将填充颜色改为透明也不可行,因为那样会将某个区域内的涂抹颜色全部擦掉,就不是真正的“上一步”了。楼主能指导下么?
        人民形象公司:@落影loyinglin 好的,我研究下。多谢
        落影loyinglin:@人民形象公司 做图片的时候是有这个功能的,我觉得有点意思,就删掉了,留给感兴趣的人去尝试。
        问题1,记录touchMove的点是没错的。每一次touchBegin到touchEnd作为一个操作,这样你维持一个操作栈,每次进出栈即可(每次变化后根据栈里的点直接绘制)。
        问题2,同上。

      本文标题:iOS开发-OpenGL ES画图应用

      本文链接:https://www.haomeiwen.com/subject/upjslttx.html