如何使用OpenGL来绘制一个圆角矩形

iOS系统的流行带来了一阵圆角矩形的热风。许多设计狮与产品汪都对圆角矩形比较感冒,那作为程序猿该如何应付呢?

幸好,当前无论是iOS还是Android系统,系统框架库都带了一些API能让我们比较便利地实现圆角矩形的功能。这里,笔者将利用OpenGL更底层的图形API来实现“磨圆”一个矩形的四个角,使得我们对圆角矩形有一个更理性的认识。😁

这里给大家先科普一下基本概念,所谓的圆角矩形,这圆角是怎么来的。我们要对一个多边形的角处理成圆角时,一般是指定某个 半径 对组成这个角的两条边绘制一个 内切圆,那么相对于那个角的 圆弧 正好就作为圆角的边了,那个角到圆角的边所包含的区域全部被剔除。如下图所示。

image.png

这里我们绘制了一个白色矩形,然后对它的左上角处理成圆角。这里我们指定了某个半径对构成左上角的左侧边与上侧边绘制了一个内切圆。由虚线所构成的小扇形的弧就是处理后的圆角的边了,而它到左上角的白色区域将全部被剔除。
这里就牵涉到了一个概念,也就是我们在做应用开发中经常碰到的一个单词——Corner Radius,也就是所谓的 圆角半径,正是这个内切圆的半径。

了解了这些相关概念之后,我们就可以动手干了!其实用OpenGL来实现圆角矩形的方式有许多种,你可以将一个矩形划分为若干部分——四个角所对应的小扇形、上下左右四条边相应的小矩形和中间的大矩形。如果你所用的OpenGL版本是2.0以上的话还能用着色器进行处理,这里提供一个demo
本文将利用 模板Stencil)来实现圆角矩形。

其原理是这样的。我们先绘制下图所示的四个角对应的要剔除的区域,并且设置好相应的模板值,然后再去绘制整个矩形。该矩形中有设置过模板值的位置的像素将全部被剔除,只保留没有设置过模板值的部分。这样就能很简单地实现一个圆角矩形了。

image2.png

上图中,红色线所标出来的区域就是我们先要绘制的模板区域,这就好比是一个反扇形。下面将给出实现代码。

首先看MyGLView.h头文件:

//
//  MyGLView.h
//  GLRoundedRect
//
//  Created by Zenny Chen on 2019/2/22.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

@import Cocoa;

#ifndef let
#define let     __auto_type
#endif

@interface MyGLView : NSOpenGLView
{
@public
    
    /// 指定圆角半径
    CGFloat eCornerRadius;
}

@end

这里声明了一个名为MyGLView的类。这里各位要注意的是,NSOpenGLLayer对模板测试不予支持,因此我们只能用NSOpenGLView来实现。

下面给出MyGLView.m源文件。

//
//  MyGLView.m
//  GLRoundedRect
//
//  Created by Zenny Chen on 2019/2/22.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

#import "MyGLView.h"
#import <OpenGL/gl.h>

@implementation MyGLView

- (instancetype)initWithFrame:(NSRect)frameRect
{
    // 指定像素格式属性
    NSOpenGLPixelFormatAttribute attrs[] =
    {
        // 使用GPU硬件加速来绘制OpenGL
        NSOpenGLPFAAccelerated,
        
        // 可选地,我们这里使用了双缓冲机制
        NSOpenGLPFADoubleBuffer,
        
        // 由于我们这里就用固定功能流水线,因此直接是用legacy的OpenGL版本即可
        NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersionLegacy,
        
        // 采用32位像素颜色(RGBA8888)
        NSOpenGLPFAColorSize, 32,
        
        // 采用24位深度缓存
        NSOpenGLPFADepthSize, 24,
        
        // 采用8位模板缓存
        NSOpenGLPFAStencilSize, 8,
        
        // 开启多重采样反走样
        NSOpenGLPFAMultisample,
        
        // 指定一个用于MSAA的缓存
        NSOpenGLPFASampleBuffers, 1,
        
        // 指定MSAA使用四个样本
        NSOpenGLPFASamples, 4,
        
        // 属性指定结束
        0
    };
    
    // 创建像素格式
    let pixelFormat = [NSOpenGLPixelFormat.alloc initWithAttributes:attrs];
    self = [super initWithFrame:frameRect pixelFormat:pixelFormat];
    [pixelFormat release];
    
    return self;
}

/// 矩形顶点坐标
static const GLfloat sRectVertices[] = {
    // 矩形左上顶点
    -0.7f, 0.5f,
    // 矩形左下顶点
    -0.7f, -0.5f,
    // 矩形右上顶点
    0.7f, 0.5f,
    // 矩形右下顶点
    0.7f, -0.5f
};

/// 圆角扇形顶点坐标
static GLfloat sFanVertices[256];

static int sFanVertexStartIndices[4];

/// 矩形顶点颜色
static const GLfloat sRectColors[] = {
    1.0f, 0.0f, 0.0f, 1.0f,
    0.0f, 1.0f, 0.0f, 1.0f,
    0.0f, 0.0f, 1.0f, 1.0f,
    1.0f, 1.0f, 1.0f, 1.0f
};

/// 生成指定顶点的圆角扇形顶点
/// @param index 指定矩形顶点索引
/// @param radius 映射到当前变换视图中的圆角半径
/// @param vertexCoordIndex 顶点坐标起始索引
/// @return 存放下一个圆角扇形顶点的索引
static int GenerateCornerVertices(int index, GLfloat radius, int vertexCoordIndex)
{
    GLfloat startRadian, endRadian;
    GLfloat cornerOriginX, cornerOriginY;
    const GLfloat deltaRadian = 5.0 * M_PI / 180.0;

    const let fanOriginX = sRectVertices[index * 2 + 0];
    const let fanOriginY = sRectVertices[index * 2 + 1];
    
    sFanVertices[vertexCoordIndex++] = fanOriginX;
    sFanVertices[vertexCoordIndex++] = fanOriginY;

    switch(index)
    {
        case 0:
            // 左上角
            startRadian = M_PI;
            endRadian = M_PI_2;
            
            cornerOriginX = fanOriginX + radius;
            cornerOriginY = fanOriginY - radius;
            break;
            
        case 1:
            // 左下角
            startRadian = 1.5 * M_PI;
            endRadian = M_PI;
            
            cornerOriginX = fanOriginX + radius;
            cornerOriginY = fanOriginY + radius;
            break;
            
        case 2:
            // 右上角
            startRadian = M_PI_2;
            endRadian = 0.0f;
            
            cornerOriginX = fanOriginX - radius;
            cornerOriginY = fanOriginY - radius;
            break;
            
        case 3:
        default:
            // 右下角
            startRadian = 2.0 * M_PI;
            endRadian = 1.5 * M_PI;
            
            cornerOriginX = fanOriginX - radius;
            cornerOriginY = fanOriginY + radius;
            break;
    }

    for(GLfloat radian = startRadian; radian >= endRadian; radian -= deltaRadian)
    {
        sFanVertices[vertexCoordIndex++] = cornerOriginX + radius * cosf(radian);
        sFanVertices[vertexCoordIndex++] = cornerOriginY + radius * sinf(radian);
    }
    
    return vertexCoordIndex;
}

- (void)prepareOpenGL
{
    let version = glGetString(GL_VERSION);
    let vendor = glGetString(GL_VENDOR);
    let renderer = glGetString(GL_RENDERER);
    printf("Current OpenGL version: %s, vendor is: %s\n", version, vendor);
    printf("Current OpenGL renderer: %s\n", renderer);

    // 开启面切除
    glEnable(GL_CULL_FACE);

    // 指定逆时针方向为正面
    glFrontFace(GL_CCW);

    // 切除背面
    glCullFace(GL_BACK);

    // 使用梯度着色模型
    glShadeModel(GL_SMOOTH);
    
    // 设置颜色缓存清除色
    glClearColor(0.4, 0.5, 0.4, 1.0);
    
    // 开启主机端的顶点数组功能
    glEnableClientState(GL_VERTEX_ARRAY);
    
    // 设置模板清除值
    glClearStencil(0);
    
    // 设置视口大小
    let viewPort = self.frame.size;
    glViewport(0, 0, viewPort.width, viewPort.height);
    
    // 确定圆角扇形顶点坐标
    const let radius = eCornerRadius / (viewPort.width / 2.0);

    // 生成四个角的圆角扇形顶点坐标
    sFanVertexStartIndices[0] = 0;
    sFanVertexStartIndices[1] = GenerateCornerVertices(0, radius, 0);
    sFanVertexStartIndices[2] = GenerateCornerVertices(1, radius, sFanVertexStartIndices[1]);
    sFanVertexStartIndices[3] = GenerateCornerVertices(2, radius, sFanVertexStartIndices[2]);
    GenerateCornerVertices(3, radius, sFanVertexStartIndices[3]);

    // 做投影变换
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-1.0, 1.0, -1.0, 1.0, 1.0, 5.0);

    // 做视图模型变换
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslatef(0.0f, 0.0f, -3.0f);
}

- (void)drawRect:(NSRect)dirtyRect {
    
    // Drawing code here.
    
    // 清除颜色缓存与模板缓存
    glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    
    // 开启模板测试
    glEnable(GL_STENCIL_TEST);
    
    // 准备绘制四个被镂空的小扇形
    glDisableClientState(GL_COLOR_ARRAY);
    
    glColor4f(0.0f, 0.0f, 0.0f, 1.0f);
    
    const let fanVertexCount = sFanVertexStartIndices[1] / 2;
    
    // 设置模板功能:参考值与当前模板值相等时测试成功;参考值为1,掩膜为1
    glStencilFunc(GL_EQUAL, 0x1, 0x1);
    
    // 设置模板操作:模板测试失败,则将当前参考值替换掉原来的模板值;深度失败以及测试全都成功,则保留原有的模板值
    glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP);
    
    // 绘制左上角
    glVertexPointer(2, GL_FLOAT, 0, sFanVertices);
    glDrawArrays(GL_TRIANGLE_FAN, 0, fanVertexCount);
    
    // 绘制左下角
    glDrawArrays(GL_TRIANGLE_FAN, sFanVertexStartIndices[1] / 2, fanVertexCount);
    
    // 绘制右上角
    glDrawArrays(GL_TRIANGLE_FAN, sFanVertexStartIndices[2] / 2, fanVertexCount);
    
    // 绘制右下角
    glDrawArrays(GL_TRIANGLE_FAN, sFanVertexStartIndices[3] / 2, fanVertexCount);
    
    // 准备绘制正常显示的大矩形
    glVertexPointer(2, GL_FLOAT, 0, sRectVertices);
    
    glEnableClientState(GL_COLOR_ARRAY);
    glColorPointer(4, GL_FLOAT, 0, sRectColors);
    
    // 设置模板功能:参考值大于当前模板值时测试成功;参考值为1,掩膜为1
    // 这里保留上面设置的j模板操作
    glStencilFunc(GL_GREATER, 0x1, 0x1);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    
    glFlush();
    
    [NSOpenGLContext.currentContext flushBuffer];
}

@end

完成之后,我们来看一下UI部分是如何使用MyGLView这个视图对象的。下面给出ViewController.m。

//
//  ViewController.m
//  GLRoundedRect
//
//  Created by Zenny Chen on 2019/2/22.
//  Copyright © 2019 Zenny Chen. All rights reserved.
//

#import "ViewController.h"
#import "MyGLView.h"

@implementation ViewController
{
@private
    
    MyGLView *mGLView;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Do any additional setup after loading the view.
    self.view.wantsLayer = YES;
    
    const let viewSize = self.view.frame.size;
    const let y = viewSize.height - 20.0 - 25.0;
    CGFloat x = 20.0;
    let button = [NSButton buttonWithTitle:@"Show" target:self action:@selector(showButtonClicked:)];
    button.frame = NSMakeRect(x, y, 70.0, 25.0);
    [self.view addSubview:button];
    
    x += button.frame.size.width + 10.0;
    button = [NSButton buttonWithTitle:@"Close" target:self action:@selector(closeButtonClicked:)];
    button.frame = NSMakeRect(x, y, 70.0, 25.0);
    [self.view addSubview:button];
}

- (void)showButtonClicked:(NSButton*)sender
{
    if(mGLView != nil)
        return;
    
    const let viewSize = self.view.frame.size;
    const let x = (viewSize.width - 512.0) * 0.5;
    
    mGLView = [MyGLView.alloc initWithFrame:NSMakeRect(x, 30.0, 512.0, 512.0)];
    mGLView->eCornerRadius = viewSize.width / 25.0;
    [self.view addSubview:mGLView];
    [mGLView release];
}

- (void)closeButtonClicked:(NSButton*)sender
{
    if(mGLView != nil)
    {
        [mGLView removeFromSuperview];
        mGLView = nil;
    }
}

- (void)setRepresentedObject:(id)representedObject {
    [super setRepresentedObject:representedObject];

    // Update the view, if already loaded.
}

@end

实现代码基本就是如此。各位可以自己尝试一下,若有问题,欢迎回复。

最后再给出一个效果图:

屏幕快照 2019-02-23 下午10.58.48.png

😁😄😏😉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值