iOS OpenGL ES 3.0入门实践

一、效果图 

入门实践,做的东西比较简单,效果如下:

二、关于顶点坐标和纹理坐标

绘制图片需要设置顶点坐标和纹理坐标并加载像素数据,之所以要指定两组坐标是因为纹理和顶点使用不同的坐标系,就是告诉OpenGL:把图像的某一区域绘制到屏幕的某一区域,3个点能确定一个三角形区域,我们把一张图分成4个三角形,分别是中心点和每条边点两个顶点组成的三角形,如下图,下图能够帮助你理解后面代码 GLImage.m 中的两组顶点为什么是那样子设置的

三、关于坐标轴方向、投影和坐标映射

具体参考我的另一篇文章Android OpenGL ES 2.0入门实践,主要也是参考的Android官方文档,好在iOS都有对应的函数,就没再仔细看iOS官方文档了。 

四、代码

OpenGL在iOS12 就已经不推荐使用了,官方推荐使用Metal,最近在学习OpenGL ES就在Android和iOS上分别实践了一下,iOS端最简单的方式就是使用GLKit,主要使用GLKViewController这个现成的绘制环境,有了这个环境,就可以直接使用OpenGL的接口进行编码了,平台有更简单易用的 GLKBaseEffect,我没有使用,还是沿用了最基本的流程,使用GL着色器,下面是主要代码:

GlUtil.h

#import <Foundation/Foundation.h>
#import <GLKit/GLKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface GLUtil : NSObject

+(GLuint)createProgram:(NSString*)vertexShaderCode :(NSString*)fragmentShaderCode;

+(void)printActiveUniforms:(GLuint)program;

@end

NS_ASSUME_NONNULL_END

GLUtil.m

#import "GLUtil.h"

@implementation GLUtil

+ (NSString *)readShaderSource:(NSString *)bundleFileName {
    NSString* path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:bundleFileName];
    NSString* content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//    NSLog(@"%@", content);
    return content;
}

+ (GLuint)loadShader:(GLenum)type :(const char *)shaderCode {
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &shaderCode, NULL);
    glCompileShader(shader);
    
    GLint commpiled;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &commpiled);
    if(!commpiled) {
        GLint infoLen = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
        if(infoLen > 0) {
            char* infoLog = malloc(infoLen);
            glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
            NSLog(@"%s", infoLog);
            free(infoLog);
            glDeleteShader(shader);
            return 0;
        }
    }
    return shader;
}

+ (GLuint)createProgram:(NSString *)vertexShaderFileName :(NSString *)fragmentShaderFileName {
    GLuint program = glCreateProgram();
    
    const char* vertexShaderCode = [[GLUtil readShaderSource:vertexShaderFileName] UTF8String];
    const char* fragmentShaderCode = [[GLUtil readShaderSource:fragmentShaderFileName] UTF8String];
    
    GLuint vertexShader = [self loadShader:GL_VERTEX_SHADER:vertexShaderCode];
    GLuint fragmentShader = [self loadShader:GL_FRAGMENT_SHADER:fragmentShaderCode];
    
    if (vertexShader == 0 || fragmentShader == 0) {
        return 0;
    }
    
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    glLinkProgram(program);
    GLint linked;
    glGetProgramiv(program, GL_LINK_STATUS, &linked);
    if(!linked) {
        GLint infoLen = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
        if(infoLen > 0) {
            char* infoLog = malloc(infoLen);
            glGetProgramInfoLog(program, infoLen, NULL, infoLog);
            NSLog(@"%s", infoLog);
            free(infoLog);
            glDeleteProgram(program);
            return 0;
        }
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    return program;
}


+ (void)printActiveUniforms:(GLuint)program {
    GLint maxUniformLen;//变量名的最大长度
    GLint numUniforms;//变量个数
    char* uniformName;//变量名
    glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &numUniforms);
    glGetProgramiv(program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxUniformLen);
    uniformName = malloc(maxUniformLen);
    for (GLint index = 0; index < numUniforms; index++) {
        GLint size;
        GLenum type;
        GLint location;
        glGetActiveUniform(program, index, maxUniformLen, NULL, &size, &type, uniformName);
        location = glGetUniformLocation(program, uniformName);
        NSString* typeStr;
        switch (type) {
            case GL_FLOAT:
                typeStr = (@"GL_FLOAT");
                break;
                
            case GL_FLOAT_VEC2:
                typeStr = (@"GL_FLOAT_VEC2");
                break;
                
            case GL_FLOAT_VEC3:
                typeStr = (@"GL_FLOAT_VEC3");
                break;
                
            case GL_FLOAT_VEC4:
                typeStr = (@"GL_FLOAT_VEC4");
                break;
                
            case GL_FLOAT_MAT4:
                typeStr = (@"GL_FLOAT_MAT4");
                break;
                
            case GL_BOOL:
                typeStr = (@"GL_BOOL");
                break;
                
            case GL_SAMPLER_2D:
                typeStr = (@"GL_SAMPLER_2D");
                break;
                
            default:
                typeStr = [NSString stringWithFormat:@"变量类型:0x%X", type];
                break;
        }
        
        NSLog(@"index=%d, name=%s, size=%d, type=%@, location=%d", index, uniformName, size, typeStr, location);
    }
    free(uniformName);
}

@end

vertexShader.glsl

顶点着色器代码 vertexShader.glsl

#version 300 es
uniform mat4 uMVPMatrix;
uniform mat4 translateMatrix;
uniform bool isTranslate;
layout(location = 0) in vec4 vPosition;
layout(location = 1) in vec2 aTextureCoord;
out vec2 vTexCoord;
void main() {
    if (isTranslate) {
        gl_Position = uMVPMatrix * translateMatrix * vPosition;
    } else {
        gl_Position = uMVPMatrix * vPosition;
    }
    vTexCoord = aTextureCoord;
}

fragmentShader.glsl

片段着色器代码 fragmentShader.glsl

#version 300 es
precision mediump float;
uniform bool isTexture;
uniform vec4 vColor;
uniform sampler2D uTextureUnit;
in vec2 vTexCoord;
out vec4 fragColor;
void main() {
   if(isTexture) {
       fragColor = texture(uTextureUnit,vTexCoord);
   } else {
       fragColor = vColor;
   }
}

绘制2D纹理的类 GLImage

GLImage.h

#import <GLKit/GLKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface GLImage : NSObject

-(void)loadTexture:(NSString*)imageName;

-(void)draw:(int)positionHandle :(int)textureHandle :(GLuint*)vbo;

@end

NS_ASSUME_NONNULL_END

GLImage.m

#import "GLImage.h"
#import <UIKit/UIKit.h>

//原文链接:https://blog.csdn.net/gongxiaoou/article/details/89344561

//图片尺寸:2385 × 3623
static const float POSITION_VERTEX[]  = {
    0.0f, 0.0f,     //顶点坐标V0
    1.0f, 1.52f,   //顶点坐标V1
    -1.0f, 1.52f,   //顶点坐标V2
    -1.0f, -1.52f,  //顶点坐标V3
    1.0f, -1.52f     //顶点坐标V4
};

static const float TEX_VERTEX[]  = {
    0.5f, 0.5f, //纹理坐标V0
    1.0f, 1.0f,     //纹理坐标V1
    0.0f, 1.0f,     //纹理坐标V2
    0.0f, 0.0f,   //纹理坐标V3
    1.0f, 0.0f    //纹理坐标V4
};

static const short VERTEX_INDEX[]  = {
    0, 1, 2,  //V0,V1,V2 三个顶点组成一个三角形
    0, 2, 3,  //V0,V2,V3 三个顶点组成一个三角形
    0, 3, 4,  //V0,V3,V4 三个顶点组成一个三角形
    0, 4, 1   //V0,V4,V1 三个顶点组成一个三角形
};


@interface GLImage()
{
    GLuint textureID;
}

@end

@implementation GLImage


//原文链接:https://blog.csdn.net/qq_30513483/article/details/101538967
- (void)loadTexture:(NSString *)imageName {
    UIImage *image = [UIImage imageNamed:imageName];
    
    // 将 UIImage 转换为 CGImageRef
    CGImageRef cgImageRef = [image CGImage];
    GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
    GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
    CGRect rect = CGRectMake(0, 0, width, height);

    // 绘制图片
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    void *imageData = malloc(width * height * 4);
    CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGContextTranslateCTM(context, 0, height);
    CGContextScaleCTM(context, 1.0f, -1.0f);
    CGColorSpaceRelease(colorSpace);
    CGContextClearRect(context, rect);
    CGContextDrawImage(context, rect, cgImageRef);
     
    // 生成纹理
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);
    // 将图片数据写入纹理缓存
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
        
    // 设置如何把纹素映射成像素
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            
    // 解绑
    glBindTexture(GL_TEXTURE_2D, 0);
    
    // 释放内存
    CGContextRelease(context);
    free(imageData);
}

- (void)draw:(int)positionHandle :(int)textureHandle :(GLuint*)vbo {
    //顶点坐标
    glEnableVertexAttribArray(positionHandle);
    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(POSITION_VERTEX), POSITION_VERTEX, GL_STATIC_DRAW);
    glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 0, 0);
    
    //纹理坐标
    glEnableVertexAttribArray(textureHandle);
    glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(TEX_VERTEX), TEX_VERTEX, GL_STATIC_DRAW);
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 0, 0);
    //激活纹理
    glActiveTexture(GL_TEXTURE0);
    //绑定纹理
    glBindTexture(GL_TEXTURE_2D, textureID);
    //绘制
    glDrawElements(GL_TRIANGLES, sizeof(VERTEX_INDEX)/sizeof(short), GL_UNSIGNED_SHORT, VERTEX_INDEX);
}

@end

GLViewController.h

#import <GLKit/GLKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface GLViewController : GLKViewController {
}

@end

NS_ASSUME_NONNULL_END

GLViewController.m

#import "GLViewController.h"
#import "GLUtil.h"
#import "GLImage.h"


const int VBO_NUM = 2;

static const GLfloat vertices[] = {
    0.0f,  0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f
    
};


@interface GLViewController ()
{
    GLuint program;
    int muMVPMatrixHandle;
    int translateMatrixHandle;
    int isTranslateHandle;
    int isTextureHandle;
    int colorHandle;
    GLKMatrix4 vMatrix;
    GLKMatrix4 projMatrix;
    GLKMatrix4 vPMatrix;
    GLKMatrix4 translateMatrix;
    GLuint vbo[VBO_NUM];
    
    GLImage* glImage;
}

@end

@implementation GLViewController

- (void)initGL{
    program = [GLUtil createProgram:@"vertexShader.glsl" :@"fragmentShader.glsl"];
    glUseProgram(program);
    muMVPMatrixHandle = glGetUniformLocation(program, "uMVPMatrix");
    translateMatrixHandle = glGetUniformLocation(program, "translateMatrix");
    isTranslateHandle = glGetUniformLocation(program, "isTranslate");
    isTextureHandle = glGetUniformLocation(program, "isTexture");
    colorHandle = glGetUniformLocation(program, "vColor");
    
    glGenBuffers(VBO_NUM, vbo);
    [GLUtil printActiveUniforms:program];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    GLKView* glView = (GLKView*)self.view;
    glView.context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];
    [EAGLContext setCurrentContext:glView.context];//这句必须得有,不然画不出来图形
    
    [self initGL];
    
    glImage = [GLImage new];
    [glImage loadTexture:@"dog.jpg"];
    
    // Combine the projection and camera view matrices
    vMatrix = GLKMatrix4MakeLookAt(0.0f, 0.0f, 3.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
}

- (void) onSurfaceChange:(float)width :(float)height {
    NSLog(@"%fx%f", width, height);
    glViewport(0, 0, width, height);
    //投影矩阵
    if (width > height) {
        float ratio = width / height;
        projMatrix = GLKMatrix4MakeFrustum(-ratio, ratio, -1.0f, 1.0f, 3.0f, 7.0f);
    } else {
        float ratio = height / width;
        projMatrix = GLKMatrix4MakeFrustum(-1.0f, 1.0f, -ratio, ratio, 3.0f, 7.0f);
    }
    
    // Combine the projection and camera view matrices
    vPMatrix = GLKMatrix4Multiply(projMatrix, vMatrix);
}


- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    float width = self.view.frame.size.width;
    float height = self.view.frame.size.height;
    NSLog(@"%s %fx%f", __func__, width, height);
    [self onSurfaceChange:width :height];
}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
//    NSLog(@"%s %f", __func__, [[NSDate new] timeIntervalSince1970]);
    // Apply the combined projection and camera view transformations
    glUniformMatrix4fv(muMVPMatrixHandle, 1, false, (float*)&vPMatrix);
    
    glClearColor(0.9f, 0.9f, 0.9f, 1.0f);//背景色
    glClear(GL_COLOR_BUFFER_BIT);
    
    //画图片
    glUniform1i(isTranslateHandle, 0);//禁用矩阵偏移
    glUniform1i(isTextureHandle, 1);//启用纹理
    [glImage draw:0 :1 :vbo];
    
    double degrees = (long)([[NSDate new] timeIntervalSince1970] * 50) % 360;
    double radians = degrees * M_PI / 180.0;
//    NSLog(@"degrees=%f", degrees);
    translateMatrix = GLKMatrix4Translate(GLKMatrix4Identity, cos(radians), sin(radians), 0.0f);
    glUniformMatrix4fv(translateMatrixHandle, 1, GL_FALSE, (float*)&translateMatrix);
    
    glUniform1i(isTranslateHandle, 1);//启用矩阵偏移
    glUniform1i(isTextureHandle, 0);//禁用纹理
    glUniform4f(colorHandle, 0.0f, 0.5f, 1.0f, 1.0f);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

@end

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OpenGL ES 3.0 英文版 第1章——OpenGL ES 3.0简介   第1章简单介绍OpenGL ES,概述了OpenGL ES 3.0图形管线,讨论了OpenGL ES 3.0的设计理念和限制,最后介绍了OpenGL ES 3.0中使用的一些约定和类型。   第2章——你好,三角形:一个OpenGL ES 3.0示例   第2章介绍绘制三角形的一个简单OpenGL ES 3.0示例。我们的目的是说明OpenGL ES 3.0程序的样子,向读者介绍一些API概念,并说明如何构建和运行OpenGL ES 3.0示例程序。   第3章——EGL简介   第3章介绍EGL——为OpenGL ES 3.0创建表面和渲染上下文的API。我们说明与原生窗口系统通信、选择配置和创建EGL渲染上下文及表面的方法,传授足够多的EGL知识,你可以了解到启动OpenGL ES 3.0进行渲染所需的所有知识。   第4章——着色器和程序   着色器对象和程序对象是OpenGL ES 3.0中最基本的对象。第4章介绍创建着色器对象、编译着色器和检查编译错误的方法。这一章还说明如何创建程序对象、将着色器对象连接到程序对象以及链接最终程序对象的方法。我们讨论如何查询程序对象的信息以及加载统一变量(uniform)的方法。此外,你将学习有关源着色器和程序二进制代码之间的差别以及它们的使用方法。   第5章——OpenGL ES着色语言   第5章介绍编写着色器所需的着色语言的基础知识。这些着色语言基础知识包括变量和类型、构造器、结构、数组、统一变量、统一变量块(uniform block)和输入/输出变量。该章还描述着色语言的某些更细微的部分,例如精度限定符和不变性。   第6章——顶点属性、顶点数组和缓冲区对象   从第6章开始(到第11章为止),我们将详细介绍管线,教授设置和编程图形管线各个部分的方法。这一旅程从介绍几何形状输入图形管线的方法开始,包含了对顶点属性、顶点数组和缓冲区对象的讨论。   第7章——图元装配和光栅化   在前一章讨论几何形状输入图形管线的方法之后,第7章将讨论几何形状如何装配成图元,介绍OpenGL ES 3.0中所有可用的图元类型,包括点精灵、直线、三角形、三角形条带和三角扇形。此外,我们还说明了在顶点上进行坐标变换的方法,并简单介绍了OpenGL ES 3.0管线的光栅化阶段。   第8章——顶点着色器   我们所介绍的管线的下一部分是顶点着色器。第8章概述了顶点着色器如何融入管线以及OpenGL ES 着色语言中可用于顶点着色器的特殊变量,介绍了多个顶点着色器的示例,包括逐像素照明和蒙皮(skinning)。我们还给出了用顶点着色器实现OpenGL ES 1.0(和1.1)固定功能管线的示例。   第9章——纹理   第9章开始介绍片段着色器,描述OpenGL ES 3.0中所有可用的纹理功能。该章提供了创建纹理、加载纹理数据以及纹理渲染的细节,描述了纹理包装模式、纹理过滤、纹理格式、压缩纹理、采样器对象、不可变纹理、像素解包缓冲区对象和Mip贴图。该章介绍了OpenGL ES 3.0支持的所有纹理类型:2D纹理、立方图、2D纹理数组和3D纹理。   第10章——片段着色器   第9章的重点是如何在片段着色器中使用纹理,第10章介绍编写片段着色器所需知道的其他知识。该章概述了片段着色器和所有可用的特殊内建变量,还演示了用片段着色器实现OpenGL ES 1.1中所有固定功能技术的方法。多重纹理、雾化、Alpha测试和用户裁剪平面的例子都使用片段着色器实现。   第11章——片段操作   第11章讨论可以适用于整个帧缓冲区或者在OpenGL ES 3.0片段管线中执行片段着色器后适用于单个片段的操作。这些操作包括剪裁测试、模板测试、深度测试、多重采样、混合和抖动。本章介绍OpenGL ES 3.0图形管线的最后阶段。   第12章——帧缓冲区对象   第12章讨论使用帧缓冲区对象渲染屏幕外表面。帧缓冲区对象有多种用法,最常见的是渲染到一个纹理。本章提供API帧缓冲区对象部分的完整概述。理解帧缓冲区对象对于实现许多高级特效(如反射、阴影贴图和后处理)至关重要。   第13章——同步对象和栅栏   第13章概述同步对象和栅栏,它们是在OpenGL ES 3.0主机应用和GPU执行中同步的有效图元。我们讨论同步对象和栅栏的使用方法,并以一个示例作为结束。   第14章——OpenGL ES 3.0高级编程   第14章是核心章节,将本书介绍的许多主题串联在一起。我们已经选择了高级渲染技术的一个样本,并展示了实现这些功能的示例。该章包含使用法线贴图的逐像素照明、环境贴图、粒子系统、图像后处理、程序纹理、阴影贴图、地形渲染
前言<br> 本程序在文件中都有详细的修改说明,方便大家互相研究,如果你喜欢,欢迎使用,但请误乱修改版权,并保留文件中的修改信息!谢谢!<br> 1、使用本站程序有如下服务<br> a、在线官方更新服务(包括南部论坛,动网论坛)<br> 在后台模板中可以看见链接<br> 方便大家即时更新,更新速度与本站同步,提供此服务,除动网外,本站是第一个<br> b、在线(官方论坛)免费技术支持<br> 包括今后的升级,在使用过程中发生的错误等等,只支持论坛技术在线<br> <br> 2、论坛风格皮肤(注:请误修改名称,否则除影响“在线官方更新服务”)<br> a、默认风格(原动网自带)<br> b、忘怀岁月<br> c、阳光无极<br> d、夕阳无限<br> 如想自已增加,请见本站提供的“换风格皮肤插件”使用方法,相当方便,快速!<br> <br> 3、插件<br> 一、前后<br> a、首页多公告显示<br> b、首页显示男,女生<br> c、首页显示建站天数<br> d、首页显示 “男生 ♂ ‖ 女生 ♀ ‖ VIP会员 [Vip] ” e、显示新贴,精华(贴子列表前显示心情符号,已经取消,如果想显示,请见本站相关贴子) f、贴子加强管理 g、个人信息中显示配偶,VIP,以及多信息 h、贴子发言框美化 i、如果要贴间广告插个请留言 j、仿MSN带个人头像短信通知(后面将改为仿QQ好友上站通知等功能) 4、其它插件在后提供下载 a、皮肤风格管理器(商业版) b、音乐系统(非点歌功能),短信服务(使用此插件就有收入,本人在本站收益效果很好,此次并集成在论坛中,与大家分享,详见后述! c、VIP d、版主评定 e、会员查询 f、结婚系统 g、个人形象设计 h、法院 5、后台 a、版面皮肤管理 nbfo_admin_boardskinset.asp b、数据库管理 admin_connsql.asp c、加强后台菜单管理(在帮助栏目可以看到) d、在线官方更新服务 注:在后面将提供使用本站程序的网站的论坛在本站进行宣传,以多种方式,本站有这个技术,:)(即仿动网论坛用户统计功能) 欢迎大家使用,本程序很多很多地方都进行了优化,速度是非常的快,功能非常适用,大家可以下载看看演示,喜欢就用。 在此向支持我的朋友表示谢意,你们的支持就是ES论坛的前进动力 特谢几位测试的朋友,谢谢! 南部将永远为大家服务器,并欢迎和大家交流技术!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值