美文网首页openGL
OpenGL渲染原理

OpenGL渲染原理

作者: 874b526fa570 | 来源:发表于2017-11-07 17:10 被阅读100次

    站在巨人的肩膀上,通常装逼都能成功!!!

    OpenGL 渲染原理

    01-自定义图层类型
    02-初始化CAEAGLLayer图层属性
    03-创建EAGLContext
    04-创建渲染缓冲区
    05-创建帧缓冲区
    06-创建着色器
    07-创建着色器程序
    08-创建纹理对象
    09-YUV转RGB绘制纹理
    10-渲染缓冲区到屏幕
    11-清理内存

    12DD7A93A60C6222148CE8B55ACBED57.jpg

    属性定义以及需要的相关代码

    // 定义枚举用于获取定义变量的下标
    typedef NS_ENUM(NSUInteger, IJSOpenGLViewEnum) {
        ATTRIB_POSITION,
        ATTRIB_TEXCOORD,
    };
    
    // #: 把参数包装成c语言的字符串
    #define STRINGIZE(x) #x
    #define STRINGIZE2(x) STRINGIZE(x)
    #define SHADER_STRING(name) @STRINGIZE(name)
    
    // 顶点着色器代码
    NSString *const kVertexShaderString = SHADER_STRING
    (
     // 图片信息传递的变量
     attribute vec4 position; // 4个顶点
     attribute vec2 inputTextureCoordinate;  // 两个定点
    
     varying vec2 textureCoordinate;
     
     void main()
     {
         gl_Position = position;
         textureCoordinate = inputTextureCoordinate;
     }
     );
    
    // 片段着色器代码
    NSString *const kYUVFullRangeConversionForLAFragmentShaderString = SHADER_STRING
    (
     // 定义全局变量
     varying highp vec2 textureCoordinate;
     precision mediump float;
     uniform sampler2D luminanceTexture;
     uniform sampler2D chrominanceTexture;
     uniform mediump mat3 colorConversionMatrix;
     void main()
     {  // 把yuv 变成rgb
         mediump vec3 yuv;
         lowp vec3 rgb;
         
         yuv.x = texture2D(luminanceTexture, textureCoordinate).r;
         yuv.yz = texture2D(chrominanceTexture, textureCoordinate).ra - vec2(0.5, 0.5);
         rgb = colorConversionMatrix * yuv;
         
         gl_FragColor = vec4(rgb, 1);
     }
     );
    
    static const GLfloat kColorConversion601FullRange[9] = {
        1.0,    1.0,         1.0,
        0.0,   -0.343,     1.765,
        1.4,     -0.711,    0.0,
    };
    // 属性相关
    {
        GLuint _frameBuffer;
        GLuint _vertShader;
        GLuint _fragShader;
        GLuint _program;
        GLint  _luminanceTextureAtt;   // 亮度属性
        GLint _chrominanceTextureAtt;  // 色度属性
        GLsizei _bufferWidth;
        GLsizei _bufferHeight;
        CVOpenGLESTextureRef _luminanceTextureRef; // 亮度纹理
        CVOpenGLESTextureRef _chrominanceTextureRef;  //色度纹理
        CVOpenGLESTextureCacheRef _textureCacheRef;  // 纹理缓存
        GLuint _luminanceTexture;
        GLuint _chrominanceTexture;
        GLint _colorConversionMatrixAtt;
        GLfloat *_preferredConversion;
    }
    @property(nonatomic,strong) CAEAGLLayer *openGLLayer;  // 图层
    @property(nonatomic,strong) EAGLContext *context;  // 图层
    @property (nonatomic, assign) GLuint colorRenderBuffer;
    @property (nonatomic, assign) GLuint frameBuffer;
    

    正文:

    继承自UIView自定义一个View

    pragma mark - 1.自定义图层类型

    /*
     修改当前的layer, 支持opengl 渲染
     */
    + (Class)layerClass
    {
        return [CAEAGLLayer class];
    }
    

    pragma mark - 2.初始化图层

    - (void)setupLayer
    {
        CAEAGLLayer *openGLLayer = (CAEAGLLayer *)self.layer;
        self.openGLLayer = openGLLayer;
        openGLLayer.opaque = YES; // 设置不透明,CALayer 默认是透明的,透明性能不好,最好设置为不透明.
        // 设置绘图属性drawableProperties
        // kEAGLDrawablePropertyRetainedBacking: 是否缓存之前的数据
        // kEAGLColorFormatRGBA8 : red、green、blue、alpha共8位
        openGLLayer.drawableProperties = @{
                                           kEAGLDrawablePropertyRetainedBacking :[NSNumber numberWithBool:NO],
                                           kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
                                           };
    }
    

    pragma mark - 3、创建OpenGL上下文,并且设置上下文

    - (void)setupContext
    {
        EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;  // 指定OpenGL 渲染 API 的版本,目前都使用 OpenGL ES 2.0
        self.context = [[EAGLContext alloc] initWithAPI:api];   // 创建EAGLContext上下文
        [EAGLContext setCurrentContext:_context];    // 设置为当前上下文,所有的渲染默认渲染到当前上下文
    }
    

    pragma mark - 4、创建渲染缓存---分配内存

    - (void)setupRenderBuffer
    {
        // OpgnGL 通过一些索引,去获取索引 分配n个未使用的渲染缓存对象,并将它存储到renderbuffers中。注意:返回的 id不会为0,0是OpenGL ES 保留的,我们也不能使用 id 为0的 renderbuffer
        /*
         n: 总数
         renderbuffers:渲染缓存区索引
         */
        glGenRenderbuffers(1, &_colorRenderBuffer);
        // 创建并绑定渲染缓存。当第一次来绑定某个渲染缓存的时候,它会分配这个对象的存储空间并初始化,此后再调用这个函数的时候会将指定的渲染缓存对象绑定为当前的激活状态: 操作索引就可以操作地址 -- 使用 GL_RENDERBUFFER 相当于是指针
        /*
         参数renderbuffer就是使用glGenRenderbuffers生成的id
           当指定id的renderbuffer第一次被设置为当前renderbuffer时,会初始化该 renderbuffer对象,其初始值为
                 width 和 height:像素单位的宽和高,默认值为0;
                 internal format:内部格式,三大 buffer 格式之一 -- color,depth or stencil;
                 Color bit-depth:仅当内部格式为 color 时,设置颜色的 bit-depth,默认值为0;
                 Depth bit-depth:仅当内部格式为 depth时,默认值为0;
                 Stencil bit-depth: 仅当内部格式为 stencil,默认值为0
         */
        glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);
        // 把渲染缓存绑定到渲染图层上CAEAGLLayer,并为它分配一个共享内存。
        // 并且会设置渲染缓存的格式,和宽度  为 color renderbuffer 分配存储空间
        /*
         把渲染缓存(renderbuffer)绑定到渲染图层(CAEAGLLayer)上,并为它分配一个共享内存。
         参数target,为哪个renderbuffer分配存储空间
         参数drawable,绑定在哪个渲染图层,会根据渲染图层里的绘图属性生成共享内存
         */
        [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_openGLLayer];
    }
    

    pragma mark - 5、创建帧缓冲区
    它相当于buffer(color, depth, stencil)的管理者,三大buffer可以附加到一个framebuffer上
    本质是把framebuffer内容渲染到屏幕

    - (void)setupFrameBuffer
    {
        // 分配n个未使用的帧缓存对象,并将它存储到framebuffers中
        glGenFramebuffers(1, &_frameBuffer);
         // 设置为当前 framebuffer 设置一个可读可写的帧缓存。当第一次来绑定某个帧缓存的时候,它会分配这个对象的存储空间并初始化,此后再调用这个函数的时候会将指定的帧缓存对象-----绑定为当前的激活状态
        glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
        // 把颜色渲染缓存 添加到 帧缓存的GL_COLOR_ATTACHMENT0上,就会自动把渲染缓存的内容填充到帧缓存,在由帧缓存渲染到屏幕
      
        //该函数是将相关的 buffer(三大buffer之一)attach到framebuffer上(如果 renderbuffer不为 0,知道前面为什么说glGenRenderbuffers 返回的id 不会为 0 吧)或从 framebuffer上detach(如果 renderbuffer为 0)。参数 attachment 是指定 renderbuffer 被装配到那个装配点上,其值是GL_COLOR_ATTACHMENTi, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT中的一个,分别对应 color,depth和 stencil三大buffer
        /*
         将 _colorRenderBuffer 装配到 GL_COLOR_ATTACHMENT0 这个装配点上
         GL_FRAMEBUFFER: 当前帧缓存
         GL_COLOR_ATTACHMENT0: 缓存区有很多层,我们需要添加到颜色层 添加到那一层
         GL_RENDERBUFFER : 之前的渲染缓存
         _colorRenderBuffer :
         */
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderBuffer);
    }
    

    通常用来处理纹理对象,并且把处理好的纹理对象渲染到帧缓存上,从而显示到屏幕上
    着色器分为顶点着色器,片段着色器
    1,顶点着色器用来确定图形形状,顶点着色器 是一个可编程的处理单元,执行顶点变换、纹理坐标变换、光照、材质等顶点的相关操作,每顶点执行一次。替代了传统渲染管线中顶点变换、光照以及纹理坐标的处理。
    2,片段着色器用来确定图形渲染颜色,是一个处理片元值及其相关联数据的可编程单元,片元着色器可执行纹理的访问、颜色的汇总、雾化等操作,每片元执行一次。
    pragma mark - 06、创建着色器

    - (void)setupShader
    {
        // 创建顶点着色器 -- GL_VERTEX_SHADER
        _vertShader = [self loadShader:GL_VERTEX_SHADER withString:kVertexShaderString];
    
        // 创建片段着色器 -- GL_FRAGMENT_SHADER
        _fragShader = [self loadShader:GL_FRAGMENT_SHADER withString:kYUVFullRangeConversionForLAFragmentShaderString];
    }
    // 加载着色器
    - (GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString
    {
        // 创建着色器
        GLuint shader = glCreateShader(type);
        if (shader == 0)
        {
            NSLog(@"创建失败");
            return 0;
        }
        // 加载着色器源代码
        const char * shaderStringUTF8 = [shaderString UTF8String];
        /*
         shader: 指向着色器对象的句柄
         count: 着色器源代码字符串的数量。着色器可以由多个源字符串组成,但是每个着色器只能有一个main函数
         string: 指向着色器源代码的字符串指针
         length: 指向保存着多个(如果有多个)源代码字符串大小的整型数组指针
         */
        glShaderSource(shader, 1, &shaderStringUTF8, NULL);
        glCompileShader(shader); // 编译着色器
        GLint compiled = 0;     // 检查是否完成
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);  // 获取完成状态
        if (compiled == 0)
        {
            glDeleteShader(shader);   // 没有完成就直接删除着色器
            return 0;
        }
        return shader;
    }
    

    pragma mark - 7、创建着色器程序

    - (void)setupProgram
    {
        _program = glCreateProgram(); //1 创建着色器程序
        glAttachShader(_program, _vertShader); // 2, 绑定着色器  / 绑定顶点着色器
        glAttachShader(_program, _fragShader);    // 3, 绑定片段着色器
        
        // 绑定着色器属性,方便以后获取,以后根据角标获取
        // 一定要在链接程序之前绑定属性,否则拿不到
        /*
         给属性绑定ID,通过ID获取属性,方便以后使用
         参数program 程序
         参数index 属性ID
         参数name 属性名称
         */
        glBindAttribLocation(_program, ATTRIB_POSITION , "position");  // 4个定点
        glBindAttribLocation(_program,ATTRIB_TEXCOORD, "inputTextureCoordinate");  //
        
        glLinkProgram(_program);      //4,链接程序
    
        // 5, 获取全局参数的索引值,注意 一定要在连接完成后才行,否则拿不到
        _luminanceTextureAtt = glGetUniformLocation(_program, "luminanceTexture");
        _chrominanceTextureAtt = glGetUniformLocation(_program, "chrominanceTexture");
        _colorConversionMatrixAtt = glGetUniformLocation(_program, "colorConversionMatrix");
        
        glUseProgram(_program);   //6 启动程序
    }
    

    pragma mark - 8、创建纹理对象,渲染采集图片到屏幕

    采集的是一张一张的图片,可以把图片转换为OpenGL中的纹理, 然后再把纹理画到OpenGL的上下文中
    什么是纹理?一个纹理其实就是一幅图像。
    纹理映射,我们可以把这幅图像的整体或部分贴到我们先前用顶点勾画出的物体上去.
    比如绘制一面砖墙,就可以用一幅真实的砖墙图像或照片作为纹理贴到一个矩形上,这样,一面逼真的砖墙就画好了。如果不用纹理映射的方法,则墙上的每一块砖都必须作为一个独立的多边形来画。另外,纹理映射能够保证在变换多边形时,多边形上的纹理图案也随之变化。
    纹理映射是一个相当复杂的过程,基本步骤如下:

    1)激活纹理单元、2)创建纹理 、3)绑定纹理 、4)设置滤波
    注意:纹理映射只能在RGBA方式下执行

    - (void)setupTexture:(CMSampleBufferRef)sampleBuffer
    {
        // 获取图片信息
        CVImageBufferRef imageBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer);
        // CVPixelBufferRef == CVImageBufferRef
        // 获取图片宽度
        GLsizei bufferWidth = (GLsizei)CVPixelBufferGetWidth(imageBufferRef);
        _bufferWidth = bufferWidth;
        GLsizei bufferHeight = (GLsizei)CVPixelBufferGetHeight(imageBufferRef);
        _bufferHeight = bufferHeight;
        
        // 创建亮度纹理
        // 激活纹理单元0, 不激活,创建纹理会失败
        glActiveTexture(GL_TEXTURE0);
        
        // 创建纹理对象
        CVReturn err;
        /*
         根据图片生成纹理
         allocator, 参数allocator kCFAllocatorDefault,默认分配内存
         参数textureCache 纹理缓存
         参数sourceImage 图片
         参数textureAttributes NULL
         参数target , GL_TEXTURE_2D(创建2维纹理对象)
         参数internalFormat GL_LUMINANCE,亮度格式
         参数width 图片宽
         参数height 图片高
         参数format GL_LUMINANCE 亮度格式
         参数type 图片类型 GL_UNSIGNED_BYTE
         参数planeIndex 0,切面角标,表示第0个切面
         参数textureOut 输出的纹理对象
         */
        err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                           _textureCacheRef, imageBufferRef,
                                                           NULL,
                                                           GL_TEXTURE_2D,
                                                           GL_LUMINANCE,
                                                           bufferWidth,
                                                           bufferHeight,
                                                           GL_LUMINANCE,
                                                           GL_UNSIGNED_BYTE,
                                                           0,
                                                           &_luminanceTextureRef);
        if (err)
        {
            NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
        }
        // 获取纹理对象
        _luminanceTexture = CVOpenGLESTextureGetName(_luminanceTextureRef);
        
        // 绑定纹理
        glBindTexture(GL_TEXTURE_2D, _luminanceTexture);
        
        // 设置纹理滤波 -- 固定写法
        /*
         控制滤波,滤波就是去除没用的信息,保留有用的信息
         一般来说,纹理图像为正方形或长方形。但当它映射到一个多边形或曲面上并变换到屏幕坐标时,纹理的单个纹素很少对应于屏幕图像上的像素。根据所用变换和所用纹理映射,屏幕上单个象素可以对应于一个纹素的一小部分(即放大)或一大批纹素(即缩小)
         */
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        
        // 激活单元1
        glActiveTexture(GL_TEXTURE1);
        
        // 创建色度纹理
        /*
         根据图片生成纹理
         参数allocator kCFAllocatorDefault,默认分配内存
         参数textureCache 纹理缓存
         参数sourceImage 图片
         参数textureAttributes NULL
         参数target , GL_TEXTURE_2D(创建2维纹理对象)
         参数internalFormat GL_LUMINANCE,亮度格式
         参数width 图片宽
         参数height 图片高
         参数format GL_LUMINANCE 亮度格式
         参数type 图片类型 GL_UNSIGNED_BYTE
         参数planeIndex 0,切面角标,表示第0个切面
         参数textureOut 输出的纹理对象
         */
        err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                           _textureCacheRef,
                                                           imageBufferRef,
                                                           NULL,
                                                           GL_TEXTURE_2D,
                                                           GL_LUMINANCE_ALPHA,
                                                           bufferWidth / 2,
                                                           bufferHeight / 2,
                                                           GL_LUMINANCE_ALPHA,
                                                           GL_UNSIGNED_BYTE,
                                                           1,
                                                           &_chrominanceTextureRef);
        if (err)
        {
            NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
        }
        // 获取纹理对象
        _chrominanceTexture = CVOpenGLESTextureGetName(_chrominanceTextureRef);
        
        // 绑定纹理
        glBindTexture(GL_TEXTURE_2D, _chrominanceTexture);
        
        // 设置纹理滤波
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    }
    

    pragma mark 9 :YUV 转 RGB

    - (void)convertYUVToRGBOutput
    {
        // 在创建纹理之前,有激活过纹理单元,就是那个数字.GL_TEXTURE0,GL_TEXTURE1
        // 指定着色器中亮度纹理对应哪一层纹理单元
        // 这样就会把亮度纹理,往着色器上贴
        /*
         指定着色器中亮度纹理对应哪一层纹理单元
         参数location:着色器中纹理坐标
         参数x:指定那一层纹理
         */
        glUniform1i(_luminanceTextureAtt, 0);
        
        // 指定着色器中色度纹理对应哪一层纹理单元
        glUniform1i(_chrominanceTextureAtt, 1);
        
        // YUV转RGB矩阵
        glUniformMatrix3fv(_colorConversionMatrixAtt, 1, GL_FALSE, _preferredConversion);
        
        // 计算顶点数据结构
        CGRect vertexSamplingRect = AVMakeRectWithAspectRatioInsideRect(CGSizeMake(self.bounds.size.width, self.bounds.size.height),self.layer.bounds);
        
        CGSize normalizedSamplingSize = CGSizeMake(0.0, 0.0);
        CGSize cropScaleAmount = CGSizeMake(vertexSamplingRect.size.width/self.layer.bounds.size.width, vertexSamplingRect.size.height/self.layer.bounds.size.height);
        
        if (cropScaleAmount.width > cropScaleAmount.height)
        {
            normalizedSamplingSize.width = 1.0;
            normalizedSamplingSize.height = cropScaleAmount.height/cropScaleAmount.width;
        }
        else
        {
            normalizedSamplingSize.width = 1.0;
            normalizedSamplingSize.height = cropScaleAmount.width/cropScaleAmount.height;
        }
        
        // 确定顶点数据结构
        GLfloat quadVertexData [] ={
            -1 * normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
            normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
            -1 * normalizedSamplingSize.width, normalizedSamplingSize.height,
            normalizedSamplingSize.width, normalizedSamplingSize.height,
        };
        
        // 确定纹理数据结构
        GLfloat quadTextureData[] =  { // 正常坐标
            0, 0,
            1, 0,
            0, 1,
            1, 1
        };
        
        // 激活ATTRIB_POSITION顶点数组开启顶点属性数组,只有开启顶点属性,才能给顶点属性信息赋值
        glEnableVertexAttribArray(ATTRIB_POSITION);
        // 给ATTRIB_POSITION顶点数组赋值
        /*
         设置顶点着色器属性,描述属性的基本信息
         参数indx:属性ID,给哪个属性描述信息
         参数size:顶点属性由几个值组成,这个值必须位1,2,3或4;
         参数type:表示属性的数据类型
         参数normalized:GL_FALSE表示不要将数据类型标准化
         参数stride 表示数组中每个元素的长度;
         参数ptr 表示数组的首地址
         */
        glVertexAttribPointer(ATTRIB_POSITION, 2, GL_FLOAT, 0, 0, quadVertexData);
        
        // 激活ATTRIB_TEXCOORD顶点数组开启顶点属性数组,只有开启顶点属性,才能给顶点属性信息赋值
        glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, 0, 0, quadTextureData);
        // 给ATTRIB_TEXCOORD顶点数组赋值
        glEnableVertexAttribArray(ATTRIB_TEXCOORD);
        
        // 渲染纹理数据,注意一定要和纹理代码放一起
        /*
         作用:使用当前激活的顶点着色器的顶点数据和片段着色器数据来绘制基本图形
         mode:绘制方式 一般使用GL_TRIANGLE_STRIP,三角形绘制法
         first:从数组中哪个顶点开始绘制,一般为0
         count:数组中顶点数量,在定义顶点着色器的时候,就定义过了,比如vec4,表示4个顶点
         */
                // 注意点,如果要绘制着色器上的点和片段,必须和着色器赋值代码放在一个代码块中,否则找不到绘制的信息,就绘制不上去,造成屏幕黑屏。
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    }
    

    pragma mark - 10.渲染帧缓存

    - (void)displayFramebuffer:(CMSampleBufferRef)sampleBuffer
    {
        // 因为是多线程,每一个线程都有一个上下文,只要在一个上下文绘制就好,设置线程的上下文为我们自己的上下文,就能绘制在一起了,否则会黑屏.
        if ([EAGLContext currentContext] != _context)
        {
            [EAGLContext setCurrentContext:_context];
        }
        [self cleanUpTextures]; // 清空之前的纹理,要不然每次都创建新的纹理,耗费资源,造成界面卡顿
        [self setupTexture:sampleBuffer];  // 创建纹理对象
        [self convertYUVToRGBOutput];    // YUV 转 RGB
        glViewport(0, 0, self.bounds.size.width, self.bounds.size.height);   // 设置窗口尺寸 设置OpenGL渲染窗口的尺寸大小,一般跟图层尺寸一样
        [_context presentRenderbuffer:GL_RENDERBUFFER];   // 把上下文的东西渲染到屏幕 是将指定renderbuffer呈现在屏幕上上
    }
    

    pragma mark - 11.清理内存

    - (void)dealloc
    {
        [self destoryRenderAndFrameBuffer];    // 清空缓存
        [self cleanUpTextures];   // 清空纹理
    }
    
    #pragma mark - 销毁渲染和帧缓存
    - (void)destoryRenderAndFrameBuffer
    {
        glDeleteRenderbuffers(1, &_colorRenderBuffer);
        _colorRenderBuffer = 0;
        
        glDeleteBuffers(1, &_frameBuffer);
        _frameBuffer = 0;
    }
    
    // 清空纹理
    - (void)cleanUpTextures
    {
        // 清空亮度引用
        if (_luminanceTextureRef)
        {
            CFRelease(_luminanceTextureRef);
            _luminanceTextureRef = NULL;
        }
        
        // 清空色度引用
        if (_chrominanceTextureRef)
        {
            CFRelease(_chrominanceTextureRef);
            _chrominanceTextureRef = NULL;
        }
        
        // 清空纹理缓存
        CVOpenGLESTextureCacheFlush(_textureCacheRef, 0);
    }
    

    相关文章

      网友评论

        本文标题:OpenGL渲染原理

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