OpenGL ES

第二十一节—光照计算3

2020-10-17  本文已影响0人  L_Ares

本文为L_Ares个人写作,如需转载请表明原文出处。

本节依然实现上一节的效果,实现效果图可以看上一节的。

本节使用GLSL来完成光照的计算。

//
//  JDView.m
//  08GLSL光照计算
//
//  Created by EasonLi on 2020/10/16.
//



#import "JDView.h"
#import <GLKit/GLKit.h>

#import <OpenGLES/ES3/gl.h>
#import <OpenGLES/ES3/glext.h>

#define MY_ORIGIN self.frame.origin
#define MY_SIZE self.frame.size

@interface JDView ()

//图层
@property (nonatomic, strong) CAEAGLLayer *mLayer;

//上下文
@property (nonatomic, strong) EAGLContext *mContext;

//渲染缓冲区
@property (nonatomic, assign) GLuint mRenderBuffer;

//帧缓冲区
@property (nonatomic, assign) GLuint mFrameBuffer;

//深度缓冲区
@property (nonatomic, assign) GLuint mDepthBuffer;

//顶点缓冲区
@property (nonatomic, assign) GLuint mVertexBuffer;

//定义一个Program
@property (nonatomic, assign) GLuint mProgram;

@end



//光源位置
static GLKVector3 lightPos = {1.2f,1.f,2.f};



@implementation JDView



#pragma mark - 重设宿主图层类型为CAEAGLLayer
/**
 如果想把自己的图层子类对象设置成当前View的宿主图层,那么就必须要赶在图层创建
 之前重写layerClass,更改返回的宿主图层的类。
 这样就可以在UIView进行初始化的时候,把layerClass返回的类作为自己的宿主图层类。
 一定要在UIView初始化之前完成这个操作,不然UIView创建完成,宿主图层确定类型是无法更改的。
 */
+ (Class)layerClass
{
    return [CAEAGLLayer class];
}

#pragma mark - 重写layoutSubViews
- (void)layoutSubviews{
    
    //设置图层
    [self setUpLayer];
    
    //设置上下文
    [self setUpContext];
    
    //加载shader并且设置program
    [self shaderAndProgram];
    
    //清空缓冲区
    [self cleanUpBuffers];
    
    //设置渲染缓冲区
    [self setUpRenderBufferAndDepthBuffer];
    
    //设置帧缓冲区
    [self setUpFrameBuffer];
    
    //设置顶点数据和顶点缓冲区
    [self setUpVertexDataAndBuffer];
    
    //设置纹理
    [self setUpTexture:@"test.png"];
    
    //渲染
    [self setupLinker];
    
}

#pragma mark - 设置图层
- (void)setUpLayer
{
    
    //把图层交给CAEAGLLayer对象
    self.mLayer = (CAEAGLLayer *)self.layer;
    
    //设置屏幕缩放比例
    [self setContentScaleFactor:[UIScreen mainScreen].scale];
    
    /**
     设置layer图层绘制的描述属性
     (1). 颜色格式
          kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
     (2). 是否保留上次绘制的内容
          kEAGLDrawablePropertyRetainedBacking : @false
     */
    self.mLayer.drawableProperties = @{kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8,kEAGLDrawablePropertyRetainedBacking : @false};
    
}

#pragma mark - 设置图形的上下文
- (void)setUpContext
{
    
    //初始化上下文
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    
    //上下文是否创建
    if (!context) {
        NSLog(@"上下文初始化失败");
        return;
    }
    
    //设置当前上下文
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"设置当前上下文失败");
        return;
    }
    
    //上下文赋值
    self.mContext = context;
    
}

#pragma mark - 加载shader并创建Program
- (void)shaderAndProgram
{
    
    //获得vsh和fsh的路径
    NSString *vshFile = [[NSBundle mainBundle] pathForResource:@"shaderv" ofType:@"vsh"];
    NSString *fshFile = [[NSBundle mainBundle] pathForResource:@"shaderf" ofType:@"fsh"];
    
    //判断当前属性中的program是否已经存在,已经存在的话,就要删除它
    if (self.mProgram) {
        glDeleteProgram(self.mProgram);
        self.mProgram = 0;
    }
    
    //加载着色器并且获得program
    self.mProgram = [self loadVertexShader:vshFile FragmentShader:fshFile];
    
    //链接Program
    glLinkProgram(self.mProgram);
    
    //获取Program的链接状态
    GLint linkStatus;
    glGetProgramiv(self.mProgram, GL_LINK_STATUS, &linkStatus);
    
    //判断Program是否链接成功
    if (linkStatus == GL_FALSE) {
        
        //失败状态下要存储errorinfo,所以定义一个元素类型为GLChar的数组存储
        GLchar errorInfo[256];
        //获取失败信息,最后一个参数是数组的首地址
        glGetProgramInfoLog(self.mProgram, sizeof(errorInfo), 0, errorInfo);
        NSLog(@"链接失败 : %@",[NSString stringWithUTF8String:errorInfo]);
        return;
        
    }
    NSLog(@"链接成功");
    
    //使用Program
    glUseProgram(self.mProgram);
    
}

//加载着色器,并获取和shader关联好了的program
- (GLuint)loadVertexShader:(NSString *)vshFile FragmentShader:(NSString *)fshFile
{

    //定义两个局部变量,用以存储着色器中的内容
    GLuint vertexShader,fragmentShader;

    //创建一个局部的program
    GLuint program = glCreateProgram();

    //编译顶点着色器和片元着色器,拿到着色器中的内容,并写入到两个临时的着色器变量中
    //顶点着色器
    [self compileShader:&vertexShader type:GL_VERTEX_SHADER file:vshFile];
    [self compileShader:&fragmentShader type:GL_FRAGMENT_SHADER file:fshFile];

    //将着色器都附着到program上面
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);

    //即然已经都附着到了program上了,就不需要再留着两个临时着色器变量了,直接删除就行
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    return program;

}

//编译着色器
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{

    //读取着色器源码的内容
    NSString *shaderContent = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];

    //着色器内容附着到着色器变量上要用到glShaderSource
    //glShaderSource的第二个参数就是char类型的指针变量
    //所以定义一个char类型的变量,存储着色器内容
    const GLchar *cshContent = (GLchar *)[shaderContent UTF8String];

    //创建一个shader,并把创建的shader赋值给传来的着色器的地址上
    *shader = glCreateShader(type);

    //将着色器内容附着到着色器对象上
    //函数的最后一个参数是着色器字符串的长度
    //在不知道的情况下可以直接写NULL,意为遇到NULL终止符就停止
    glShaderSource(*shader, 1, &cshContent, NULL);

    //将着色器源码编译成目标代码
    glCompileShader(*shader);

}

#pragma mark - 清空缓冲区
- (void)cleanUpBuffers
{
    
    //清空VBO
    glDeleteBuffers(1, &_mVertexBuffer);
    self.mVertexBuffer = 0;
    
    //清空渲染缓冲区
    glDeleteBuffers(1, &_mRenderBuffer);
    self.mRenderBuffer = 0;
    
    //清空帧缓冲区
    glDeleteBuffers(1, &_mFrameBuffer);
    self.mFrameBuffer = 0;
    
    //清空深度缓冲区
    glDeleteBuffers(1, &_mDepthBuffer);
    self.mDepthBuffer = 0;
    
}

#pragma mark - 设置渲染缓冲区及其内部的深度缓冲区
- (void)setUpRenderBufferAndDepthBuffer
{
    
    //申请渲染缓冲区空间
    glGenRenderbuffers(1, &_mRenderBuffer);
    //绑定渲染缓冲区
    glBindRenderbuffer(GL_RENDERBUFFER, self.mRenderBuffer);
    //把renderBuffer存储或者说绑定到CAEAGLLayer对象上
    BOOL result = [self.mContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.mLayer];
    //判断是否存储或者说绑定成功
    if (!result) {
        NSLog(@"renderBuffer绑定图层失败");
        return;
    }
    
/***********************************************************************************/
    
    //定义宽和高的临时变量,下面深度缓冲区存储的时候有用
    int width,height;
    /**
     返回renderBuffer对象的参数,我们在这里获取当前绑定的渲染缓冲区的宽和高(单位:像素)
     如果生成信息发生了错误,则不会生成params内容
     
     glGetRenderbufferParameteriv (GLenum target, GLenum pname, GLint* params)
     参数:
     (1).target : 指定目标renderbuffer对象。符号常量必须为GL_RENDERBUFFER。
     (2).pname  : 指定renderbuffer对象参数的符号名称。
     (3).params : 返回请求的参数
     
     //这里注意,glGetRenderbufferParameteriv获取的图像分辨率是组件实际存储的分辨率,可能
     //与glRenderbufferStorage函数的internalformat参数不一致
     */
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);
    
    //申请深度缓冲区空间
    glGenRenderbuffers(1, &_mDepthBuffer);
    //绑定深度缓冲区
    glBindRenderbuffer(GL_RENDERBUFFER, self.mDepthBuffer);
    //存储深度缓冲区的信息
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
    
}

#pragma mark - 设置帧缓冲区
- (void)setUpFrameBuffer
{
    
    //申请帧缓冲区
    glGenFramebuffers(1, &_mFrameBuffer);
    //绑定帧缓冲区
    glBindFramebuffer(GL_FRAMEBUFFER, self.mFrameBuffer);
    
    //在每次要做附着点设置之前,最好再确认一下,要设置的缓冲区是否绑定的正确,
    //因为这里多了一个depthBuffer在共用GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, self.mDepthBuffer);
    
    //设置depthBuffer在framebuffer上附着点
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, self.mDepthBuffer);
    
    //同理,来确认一下渲染缓冲区绑定是不是正确
    glBindRenderbuffer(GL_RENDERBUFFER, self.mRenderBuffer);
    //设置renderBuffer在frameBuffer上附着点
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.mRenderBuffer);
    
}

#pragma mark - 设置顶点数据和顶点缓冲区
- (void)setUpVertexDataAndBuffer
{
    
    //顶点数组,一个立方体坐标信息(6个面,每个面2个三角形)
    float vertices[] = {
        //顶点位置              //法线               //纹理坐标
        -0.5f, -0.5f, -0.5f,   0.0f,  0.0f, -1.0f,  0.0f,  0.0f,
        0.5f, -0.5f, -0.5f,    0.0f,  0.0f, -1.0f,  1.0f,  0.0f,
        0.5f,  0.5f, -0.5f,    0.0f,  0.0f, -1.0f,  1.0f,  1.0f,
        0.5f,  0.5f, -0.5f,    0.0f,  0.0f, -1.0f,  1.0f,  1.0f,
        -0.5f,  0.5f, -0.5f,   0.0f,  0.0f, -1.0f,  0.0f,  1.0f,
        -0.5f, -0.5f, -0.5f,   0.0f,  0.0f, -1.0f,  0.0f,  0.0f,
        
        -0.5f, -0.5f,  0.5f,   0.0f,  0.0f,  1.0f,  0.0f,  0.0f,
        0.5f, -0.5f,  0.5f,    0.0f,  0.0f,  1.0f,  1.0f,  0.0f,
        0.5f,  0.5f,  0.5f,    0.0f,  0.0f,  1.0f,  1.0f,  1.0f,
        0.5f,  0.5f,  0.5f,    0.0f,  0.0f,  1.0f,  1.0f,  1.0f,
        -0.5f,  0.5f,  0.5f,   0.0f,  0.0f,  1.0f,  0.0f,  1.0f,
        -0.5f, -0.5f,  0.5f,   0.0f,  0.0f,  1.0f,  0.0f,  0.0f,
        
        -0.5f,  0.5f,  0.5f,   -1.0f,  0.0f,  0.0f,  1.0f,  0.0f,
        -0.5f,  0.5f, -0.5f,   -1.0f,  0.0f,  0.0f,  1.0f,  1.0f,
        -0.5f, -0.5f, -0.5f,   -1.0f,  0.0f,  0.0f,  0.0f,  1.0f,
        -0.5f, -0.5f, -0.5f,   -1.0f,  0.0f,  0.0f,  0.0f,  1.0f,
        -0.5f, -0.5f,  0.5f,   -1.0f,  0.0f,  0.0f,  0.0f,  0.0f,
        -0.5f,  0.5f,  0.5f,   -1.0f,  0.0f,  0.0f,  1.0f,  0.0f,
        
        0.5f,  0.5f,  0.5f,    1.0f,  0.0f,  0.0f,  1.0f,  0.0f,
        0.5f,  0.5f, -0.5f,    1.0f,  0.0f,  0.0f,  1.0f,  1.0f,
        0.5f, -0.5f, -0.5f,    1.0f,  0.0f,  0.0f,  0.0f,  1.0f,
        0.5f, -0.5f, -0.5f,    1.0f,  0.0f,  0.0f,  0.0f,  1.0f,
        0.5f, -0.5f,  0.5f,    1.0f,  0.0f,  0.0f,  0.0f,  0.0f,
        0.5f,  0.5f,  0.5f,    1.0f,  0.0f,  0.0f,  1.0f,  0.0f,
        
        -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,  0.0f,  1.0f,
        0.5f, -0.5f, -0.5f,    0.0f, -1.0f,  0.0f,  1.0f,  1.0f,
        0.5f, -0.5f,  0.5f,    0.0f, -1.0f,  0.0f,  1.0f,  0.0f,
        0.5f, -0.5f,  0.5f,    0.0f, -1.0f,  0.0f,  1.0f,  0.0f,
        -0.5f, -0.5f,  0.5f,   0.0f, -1.0f,  0.0f,  0.0f,  0.0f,
        -0.5f, -0.5f, -0.5f,   0.0f, -1.0f,  0.0f,  0.0f,  1.0f,
        
        -0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f,  0.0f,  1.0f,
        0.5f,  0.5f, -0.5f,    0.0f,  1.0f,  0.0f,  1.0f,  1.0f,
        0.5f,  0.5f,  0.5f,    0.0f,  1.0f,  0.0f,  1.0f,  0.0f,
        0.5f,  0.5f,  0.5f,    0.0f,  1.0f,  0.0f,  1.0f,  0.0f,
        -0.5f,  0.5f,  0.5f,   0.0f,  1.0f,  0.0f,  0.0f,  0.0f,
        -0.5f,  0.5f, -0.5f,   0.0f,  1.0f,  0.0f,  0.0f,  1.0f
    };
    
    //设置VBO(顶点缓冲区)
    glGenBuffers(1, &_mVertexBuffer);
    //绑定VBO
    glBindBuffer(GL_ARRAY_BUFFER, self.mVertexBuffer);
    //将顶点数组从CPU拷贝到GPU中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
    
    //设置属性通道的开关,这里会在vsh中使用layout...in...只有vsh可以用layout...in...
    //顶点通道
    glEnableVertexAttribArray(0);
    //法向量
    glEnableVertexAttribArray(1);
    //纹理坐标
    glEnableVertexAttribArray(2);
    
    //设置数据的读取格式
    //顶点
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)NULL);
    //法向量
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float)));
    //纹理坐标
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(6 * sizeof(float)));
    
}

#pragma mark - 从图片中加载纹理
- (void)setUpTexture:(NSString *)fileName
{
    
    //纹理需要的是位图,所以只要从路径读取的不是位图,那么就需要将它变成位图
    CGImageRef bitImage = [UIImage imageNamed:fileName].CGImage;
    
    //可以判断一下,是否拿到了这张位图
    if (!bitImage) {
        NSLog(@"解压缩图片失败 : %@",fileName);
        //非正常运行程序导致程序退出。exit(0)是正常运行程序导致退出
        exit(1);
    }
    
    //拿到图片以后读取图片的信息,宽高
    size_t width = CGImageGetWidth(bitImage);
    size_t height = CGImageGetHeight(bitImage);
    
    //获取图片的字节数
    GLubyte *bitByte = (GLubyte *)calloc(width * height * 4, sizeof(GLubyte));
    
    //创建上下文
    CGContextRef bitContextRef = CGBitmapContextCreate(bitByte, width, height, 8, 4 * width, CGImageGetColorSpace(bitImage), kCGImageAlphaPremultipliedLast);
    
    //在上下文上把图片绘制出来
    //先设定好rect
    CGRect rect = CGRectMake(0.f, 0.f, width, height);
    
    /*******************图片翻转********************************/
    //先向Y轴正方向移动一个图形高度的距离,这时候,图形的底边已经变成原来图形位置顶边了
    CGContextTranslateCTM(bitContextRef, 0.f, rect.size.height);
    //这时候把Y轴正方向向下,则图片也随之向下,那么远来是反过来的,这次又反过来一次,负负得正。
    CGContextScaleCTM(bitContextRef, 1.f, -1.f);
    /**********************************************************/
    
    //使用默认方式绘制
    CGContextDrawImage(bitContextRef, rect, bitImage);
    
    //绘制完成后释放上下文
    CGContextRelease(bitContextRef);
    
    //绑定纹理并设置纹理参数
    GLuint texture;
    
    glGenTextures(1, &texture);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture);
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bitByte);
    
    glGenerateMipmap(GL_TEXTURE_2D);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
    free(bitByte);
    glUniform1i(glGetUniformLocation(self.mProgram, "Material.Texture"), 0);
    
}

#pragma mark - 渲染
-(void)render{

    glEnable(GL_DEPTH_TEST);
    
    glClearColor(1.0, 0.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //先拿到mainScreen主屏的缩放因子
    CGFloat scale = [UIScreen mainScreen].scale;
    //设置视口
    glViewport(MY_ORIGIN.x * scale, MY_ORIGIN.y * scale, MY_SIZE.width * scale, MY_SIZE.height * scale);
  
    //光照颜色->片元/顶点着色器
    glUniform3f(glGetUniformLocation(self.mProgram, "lightColor"), 1.0f, 1.0f, 1.0f);

    //投影矩阵->顶点着色器
    float aspect = (float)self.bounds.size.width / (float)self.bounds.size.height;
    GLKMatrix4 projectionMatrix =  GLKMatrix4MakePerspective(GLKMathRadiansToDegrees(45.0f), aspect, 0.1f, 800.0f) ;
    glUniformMatrix4fv(glGetUniformLocation(self.mProgram, "projection"), 1, GL_FALSE, (GLfloat*)projectionMatrix.m);
    
    
    //模型视图矩阵-->顶点着色器(这里修改的是观察者或者说视点的位置,你也可以修改物体的位置)
    float radius = 10.0f;
    //这里只是求一个随机值,因为sin和cos的值域范围都是[-1,1],再扩大一下,所以乘10.f
    float camX = sin(CACurrentMediaTime()) * radius;
    float camZ = cos(CACurrentMediaTime()) * radius;
    GLKVector3 viewPo = {camX,camX,camZ};
    //获取世界坐标系去模型矩阵中.
    /*
     LKMatrix4 GLKMatrix4MakeLookAt(float eyeX, float eyeY, float eyeZ,
     float centerX, float centerY, float centerZ,
     float upX, float upY, float upZ)
     等价于 OpenGL 中
     void gluLookAt(GLdouble eyex,GLdouble eyey,GLdouble eyez,GLdouble centerx,GLdouble centery,GLdouble centerz,GLdouble upx,GLdouble upy,GLdouble upz);
     
     目的:根据你的设置返回一个4x4矩阵变换的世界坐标系坐标。
     参数1:眼睛位置的x坐标
     参数2:眼睛位置的y坐标
     参数3:眼睛位置的z坐标
     第一组:就是脑袋的位置
     
     参数4:正在观察的点的X坐标
     参数5:正在观察的点的Y坐标
     参数6:正在观察的点的Z坐标
     第二组:就是眼睛所看物体的位置
     
     参数7:摄像机上向量的x坐标
     参数8:摄像机上向量的y坐标
     参数9:摄像机上向量的z坐标
     第三组:就是头顶朝向的方向(因为你可以头歪着的状态看物体)
     */
    
    GLKMatrix4 view1 = GLKMatrix4MakeLookAt(camX,camX,camZ,0,0,0,0,1,0);
    view1 = GLKMatrix4Scale(view1, 5.0f, 5.0f, 5.0f);
    glUniformMatrix4fv(glGetUniformLocation(self.mProgram, "view"), 1, GL_FALSE, (GLfloat *)view1.m);

    //光源位置
    glUniform3f(glGetUniformLocation(self.mProgram, "lightPo"), lightPos.x, lightPos.y, lightPos.z);
    
    //观察者位置
    glUniform3f(glGetUniformLocation(self.mProgram, "viewPo"), viewPo.x, viewPo.y, viewPo.z);
    
    glDrawArrays(GL_TRIANGLES, 0, 36);
    [self.mContext presentRenderbuffer:GL_RENDERBUFFER];
}


- (void)setupLinker
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CADisplayLink *linker = [CADisplayLink displayLinkWithTarget:self selector:@selector(render)];
        linker.preferredFramesPerSecond = 24;
        [linker addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    });
}


#pragma mark - 销毁
- (void)dealloc
{
    if ([EAGLContext currentContext] == _mContext) {
        [EAGLContext setCurrentContext: nil];
    }
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

@end

最后记得把JDView加入到ViewControllerview上就行了。

实现效果:

效果图.png

shader文件
提取码 : nrmb

shader的代码中有关于聚光灯模式,平行光模式,点光源模式等不同模式的计算方法,另外一定要清楚,这些光照的计算一定是在fragment_shader中实现的,因为它们的根本目的还是要为像素点上色,光照只不过是更现实的显示出像素点的颜色,得到的结果都是要设置给内建变量gl_FragColor或者可以自建一个out vec FragColor,当然还是建议直接使用GLSL的内建变量。

上一篇 下一篇

猜你喜欢

热点阅读