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

news2024/12/25 12:53:51

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

😁😄😏😉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/455115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于TensorRT的yolov5 实例分割部署

yolov5-7.0 github: https://github.com/ultralytics/yolov5/tree/master 1. 代码的使用 1.1 训练yolov5-seg模型 使用的yolov5-7.0的代码,github下载:https://github.com/ultralytics/yolov5/releases/tag/v7.0 训练指令 python segment/train.py --data coco128-seg.y…

案例——数据表的基本操作

目录 案例目的&#xff1a; 创建表&#xff1a; 创建offices&#xff1a; 创建employees表&#xff1a; 修改表&#xff1a; 将 employees 的 mobile 字段移动到 officeCode 字段后&#xff1a; 将 birth 字段名称改为 employee_birth: 修改 sex 字段&#xff0c;数据类…

手写axios源码系列五:CancelToken取消请求类封装

文章目录 一、使用 CancelToken 取消请求1、创建 CancelToken.js 文件2、发送请求时订阅 onCanceled 方法 二、使用 AbortController 取消请求三、使用 json-server 测试"取消请求"功能代码1、全局安装 json-server2、创建 db.json 文件并监听文件3、创建 index.html…

真心不负,合作长久| 凌恩客户亲述“我和凌恩的故事”

我和凌恩的故事 —Biozeron stories— 分享嘉宾介绍 中国热带农业科学院 环境与植物保护研究所 岳政府 助理研究员&#xff0c;2022年加入环植所农业资源环境团队&#xff0c;主要从事抗生素抗性基因的风险评价、病毒介导抗性基因传播的微生物学机制等研究&#xff0c…

K_A35_002 基于STM32等单片机采集TTP223按键模块状态 串口与OLED0.96双显示

K_A35_002 基于STM32等单片机采集TTP223按键模块状态 串口与OLED0.96双显示 所有资源导航一、资源说明二、基本参数参数引脚说明 三、驱动说明模块工作原理:对应程序: 四、部分代码说明1、接线引脚定义1.1、STC89C52RCTTP223按键模块1.2、STM32F103C8T6TTP223按键模块 五、基础…

两个数组的交集

给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 思路&#xff1a; 由于这道题目&#xff0c;输出结果中的每个元素一定是唯一的&#xff0c;也就是说输出的结果的去重的&#xff0c; 同时可…

栈及其应用

栈 在使用一个东西之前&#xff0c;得清楚它是什么&#xff0c;才能知道它的用途以及该如何使用。 栈的简介 栈&#xff08;stack&#xff09;又名堆栈&#xff0c;它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶(top)&#xff0c;相…

Direct3D 12——计算着色器——线程标识的系统值

1.系统会为每个线程组都分配一个ID,这个ID称为线程组ID ( group ID),其系统值的语义为 SV_GroupID。如果Gx x Gy x Gz&#xff1a;为所分派线程组的个数&#xff0c;则组ID的范围为(0, 0, 0)至(Gx-1,Gy-1,Gz-1)。 2.在线程组中&#xff0c;每个线程都被指定了一个组内的唯一 I…

企企通受邀出席嘉兴产业数字化峰会,助力嘉兴“智”造业发展

近日&#xff0c;2023年嘉兴产业数字化峰会在浙江嘉兴圆满举办&#xff0c;企企通作为采购供应链厂商受邀出席&#xff0c;并与众多信息化领域的专家一起分享心得、总结得失、展望未来&#xff0c;为嘉兴市企业的产业数字化发展探索合适的路径与方向。 01、数字蓄能&#xff0c…

【 Linux命令行与Shell脚本编程】第四章 进程管理 ,磁盘统计信息,挂载新磁盘,数据排序,数据归档

Linux命令行与Shell脚本编程 第四章 更多命令 进程管理 磁盘统计信息 挂载新磁盘 数据排序 数据归档 文章目录 Linux命令行与Shell脚本编程四,更多命令4.1,监测程序4.1.1,ps 探查进程4.1.2,top 实时监测进程4.1.3,kill pkill 结束进程1,kill 命令2,pkill 命令 4.2,检测磁盘空间…

Django+模板引擎+Bootstrap +sqlite3 个人博客管理系统

目录 1.准备工作1.1 参考博客1.2 项目环境与结构预览1.3 项目功能点与预览效果 1.准备工作 1.1 参考博客 1、【python】怎么导出python虚拟环境库以及导入 2、Django之分页功能 主要参考了这篇博客。 1.2 项目环境与结构预览 创建虚拟环境&#xff0c;基于Django版本1.11.2…

25-Maven

目录 1.什么是Maven&#xff1f; PS&#xff1a;关于 jar 包 2.配置并使用Maven 2.1.Maven依赖管理流程 2.2.Maven国内源配置 ①确认右边的两个勾已经都选中。 ②查看自己电脑上settings.xml文件是否存在&#xff0c;进而进行相关配置 ③配置所有新项目配置文件 PS&am…

剑指offer练习日志02:基于矩阵乘法求斐波那契数列通项

目录 一.矩阵乘法与斐波那契数列 1.利用数列的项构造二阶方阵 2.引入矩阵乘法 二.算法实现 1.MatrixFib对象成员变量 2.MatrixFib对象的构造函数 3. MatrixFib对象的成员算法接口 4.对象测试 一.矩阵乘法与斐波那契数列 1.利用数列的项构造二阶方阵 &#x1f604;现定…

【项目实战-CRM】(二:物理模型设计 搭建CRM项目环境)

文章目录 1.物理模型设计2.搭建开发环境2.1 创建crm项目2.2 创建模块2.3 添加jar包2.4 相关配置2.4.1 MyBatis 配置2.4.2 配置数据连接和事务2.4.3 SpringMVC配置文件2.4.4 Spring总配置文件2.4.5 web.xml2.4.6 设置maven对配置文件的编译选项 2.5 添加静态页面资源2.6 将项目部…

在VMmare上安装Windows 2003

今天和大家介绍一下如何使用VMmare安装一个Windows 2003 Enterprise Edition操作系统 首先小陈先下载了一个VMmare 10虚拟软件&#xff0c;将其安装在自己的电脑上。 一、新建虚拟机 然后打开VMmare软件&#xff0c;在窗口的菜单栏里点击“文件”-->“新建虚拟机” 这边让…

c++验证用户输入合法性的示例代码

c验证用户输入合法性的示例代码 本文介绍c验证用户输入合法性&#xff0c;用于检测限定用户输入值。包括&#xff1a;1、限定用户输入为整数&#xff08;正负整数&#xff09;&#xff1b;2、限定用户输入为正整数&#xff1b;3、限定用户输入为正数&#xff08;可以含有小数&…

AI算力碎片化:矩阵乘法的启示

尽管AI的发展取得了巨大进步&#xff0c;但编译器LLVM之父Chris Lattner认为&#xff0c;AI技术应用并不深入&#xff0c;远远没有发挥出已有机器学习研究的所有潜力。而AI系统和工具的单一化和碎片化正是造成这一问题的根源。 为了让AI发挥其真正的潜力&#xff0c;计算碎片化…

Oracle中实现恢复删除的表或表数据内容

一、需求说明 在我们进行项目开发或运维过程中,由于操作不当,引起的误删Oracle数据库表或指定表的数据内容,导致程序出现故障;而我们又没有对数据库进行备份,此时,如果不能及时恢复数据库内容将会导致严重的事故。我们需要一种能够补救的方法来挽回损失,恢复被误删的表或…

WEB攻防-弱口令暴力破解(包含工具、字典下载地址)

目录 一、弱口令概述 二、Web类-加密&验证码后台猜解 三、服务类-SSH&RDP远程终端猜解 四、应用类-ZIP&Word文件压缩包猜解 一、弱口令概述 弱口令(weak password) 没有严格和准确的定义&#xff0c;通常认为容易被别人&#xff08;他们有可能 对你很了解&#…

025:Mapbox GL加载栅格高程模型raster-dem文件

第025个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中加载image图像文件。栅格 DEM 源。 仅支持 Mapbox Terrain-DEM,您可以将 Terrain-DEM 用于各种视觉和分析应用程序,从样式化地形坡度和山体阴影到为视频游戏生成 3D 地形网格。 直接复制下面的 vue+mapbo…