OpenGL渲染YUV數(shù)據(jù)

本文主要介紹使用OpenGL ES來渲染I420(YUV420P) , NV12(YUV420SP)的方法,關于YUV的知識,可以看這里《YUV顏色編碼解析》,同樣會用到一些簡單的OpenGL shader知識,可以看看OpenGL的著色器語言。為了書寫方便,以下所談的OpenGL特指OpenGL ES。

OpenGL ES是OpenGL的精簡版本,主要針對于手機、游戲主機等嵌入式設備,它提供了一套設備圖形硬件的軟件接口,通過直接操作圖形硬件,使我們能夠高效地繪制圖形。OpenGL在iOS架構中屬于媒體層,與quartz(core graphics)類似,是相對底層的技術,可以控制每一幀的圖形繪制。由于圖形渲染是通過圖形硬件(GPU)來完成的,相對于使用CPU,能夠獲得更高的幀率同時不會因為負載過大而造成卡頓。


OpenGL處于繪制接口的底層
OpenGL處于繪制接口的底層

創(chuàng)建GLView

我們需要創(chuàng)建一個用來展示OpenGL繪制內(nèi)容的View,只需要將UIView的根圖層(underlying layer)替換成CAEAGLLayer實例即可。通過覆蓋UIView的類方法+(Class)layerClass,可以實現(xiàn)這一點,CAEAGLLayer默認是透明的,這會影響性能,所以將它設為不透明。

+ (class)layerClass {
    return [CAEAGLLayer class];
}
- (void)setupLayer {
    _eaglLayer = (CAEAGLLayer*) self.layer;
    _eaglLayer.opaque = YES;
}

創(chuàng)建EAGLContext

EAGLContext對象管理OpenGL繪制所需要的所有信息,和Quartz 2D所使用的CGContext類似。

- (void)setupContext {   
//創(chuàng)建一個OpenGLES 2.0接口的context
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
    _context = [[EAGLContext alloc] initWithAPI:api];
    if (!_context) {
        NSLog(@"Failed to initialize OpenGLES 2.0 context");
        exit(1);
    }
 //將其設置為current context
    if (![EAGLContext setCurrentContext:_context]) {
        NSLog(@"Failed to set current OpenGL context");
        exit(1);
    }
}

創(chuàng)建render buffer

render buffer用來存儲將要繪制到屏幕上圖像。OpenGL中的對象都需要創(chuàng)建、綁定,并且都是ID引用的。

- (void)setupRenderBuffer {
    //創(chuàng)建一個render buffer對象,并綁定到GL_RENDERBUFFER目標上
    glGenRenderbuffers(1, &_renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); 
    //為render buffer分配存儲空間
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];    
}

創(chuàng)建frame buffer

一個frame buffer對象包括render buffer, depth buffer, stencil buffer等,擁有OpenGL繪制時需要的信息。

- (void)setupFrameBuffer {    
    GLuint framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    //將之前創(chuàng)建的render buffer附著到frame buffer作為其logical buffer
    //GL_COLOR_ATTACHMENT0指定第一個顏色緩沖區(qū)附著點
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
        GL_RENDERBUFFER, _renderBuffer);
 }

glFramebufferRenderbuffer調(diào)用后,render buffer通過GL_COLOR_ATTACHMENT0引用使用render buffer

渲染

- (void)render {
//設置用來清除屏幕的顏色,類似于quartz中設置CGcontext畫筆的顏色
    glClearColor(0, 0, 0, 1.0);
    //執(zhí)行清除操作,設置render buffer中的像素顏色為上一步指定的顏色
    glClear(GL_COLOR_BUFFER_BIT);
    //渲染render buffer中的圖像到GLView的CAEAGLLayer
    [_context presentRenderbuffer:GL_RENDERBUFFER];
}

shader(著色器)

shader是上是在GPU上執(zhí)行的程序,保存在.glsl文件中或以字符串形式寫在OpenGL代碼里,使用GLSL(OpenGL shading language)語言編寫,shader在運行時編譯,鏈接,最終在GPU上執(zhí)行采樣操作。
OpenGL中有兩種shader:

  1. vertex shader(頂點著色器):vertex shader在每個頂點上都執(zhí)行執(zhí)行一次,通過不同世界的坐標系轉化定位頂點的最終位置。它可以數(shù)據(jù)給fragment shader,如紋理坐標、頂點坐標,變換矩陣等。
  2. fragment shader(片段著色器):fragment shader在每個像素上都會執(zhí)行一次,通過插值確定像素的最終顯示顏色。

創(chuàng)建shader

以下是兩個簡單的shader,用來說明GLSL的語法特點。

vertex shader:

//attribute 關鍵字用來描述傳入shader的變量
attribute vec4 vertexPosition; // 需要從外部獲取的4分量vector
attribute vec4 pixelColor; 
//varying 關鍵字用來描述從vertex shader傳遞給fragment shader的變量
//精度修飾符分為三種:highp, mediump, lowp
varying mediump vec4 finalPixelColor; //mediumP修飾代表中等精度,提高效率。
 
void main(void) { 
    finalPixelColor = pixelColor; // 將pixelColor的值通過finalPixelColor傳遞給fragment shader
    gl_Position = vertexPosition; // gl_Position是vertex shader的內(nèi)建變量,gl_Position中的頂點值最終輸出到渲染管線中
}

fragment shader:

varying mediump vec4 finalPixelColor; 
 
void main(void) { 
    gl_FragColor = finalPixelColor; // gl_FragColor是fragment shader的內(nèi)建變量,gl_FragColor中的像素值最終輸出到渲染管線中
}
}

使用shader

shader在運行時完成編譯、鏈接,是在GPU上執(zhí)行的小程序,以下是shader編譯、鏈接的過程,為了閱讀方便,省略了調(diào)試異常情況的判斷和調(diào)試log輸出。

//編譯shader函數(shù)
- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
    // 讀取shader文件的內(nèi)容為字符串
    NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName 
        ofType:@"glsl"];
    NSError* error;
    NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath 
        encoding:NSUTF8StringEncoding error:&error];
    if (!shaderString) {
        NSLog(@"Error loading shader: %@", error.localizedDescription);
        exit(1);
    }
    // 創(chuàng)建shader對象,返回其引用
    GLuint shaderHandle = glCreateShader(shaderType);    
    // 獲取C字符串,作為源代碼傳給OpenGL
    const char * shaderStringUTF8 = [shaderString UTF8String];    
    int shaderStringLength = [shaderString length];
    glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
    // 運行時編譯shader
    glCompileShader(shaderHandle);
    return shaderHandle;
}
//編譯、鏈接shader
-(void)configuerShader{
    // 創(chuàng)建并編譯shader
    GLuint vertexShader = [self compileShader:@"vertexShader" 
        withType:GL_VERTEX_SHADER];
    GLuint fragmentShader = [self compileShader:@"fragmentShader" 
        withType:GL_FRAGMENT_SHADER];
    //創(chuàng)建一個程序?qū)ο螅祷仄湟?    _programHandle = glCreateProgram();
    //將兩個shader綁定到程序?qū)ο? 不需要時可以使用glDetachShader解綁
    glAttachShader(programHandle, vertexShader);
    glAttachShader(programHandle, fragmentShader);
    //鏈接兩個shader
    glLinkProgram(_programHandle);
    //選擇創(chuàng)建的程序?qū)ο鬄楫斍笆褂玫某绦?,類似setCurrentContext, 不需要時使用glDeleteProgram刪除
    glUseProgram(_programHandle);
  }
-(void)configuerSlot{
   //獲取shader中attribute變量的引用
   _vertexPosition = glGetAttribLocation(programHandle, "vertexPosition");
   _pixelColor = glGetAttribLocation(programHandle, "pixelColor");
   //啟用attribute變量,使其對GPU可見,默認為關閉
   glEnableVertexAttribArray(_vertexPosition);
   glEnableVertexAttribArray(_pixelColor);
 }

-(void)initOpenGl{
  [self configuerShader];
  [self coniigureSlot];
}

使用OpenGL繪制一個簡單的矩形

以上內(nèi)容介紹了OpenGL的基本數(shù)據(jù)結構,現(xiàn)在先來繪制一個簡單的矩形

初始化

現(xiàn)在需要給OpenGL提供attribute變量值與頂點數(shù)據(jù)。頂點數(shù)據(jù)用來提供繪制時的幾何信息。OpenGL中只能繪制三角形,三角形保證了其內(nèi)部像素都在同一個平面。要繪制復雜的幾何圖形,可以用三角形組合的方式實現(xiàn)。
頂點數(shù)據(jù)使用VBO(vertex buffer object)來傳遞給GPU。

初始化VBO

OpenGL需要有兩種VBO來確定幾何圖形,vertex VBO提供頂點本身,index VBO提供三角形所使用的頂點的index序列。這樣保證了顯存中存儲的頂點數(shù)據(jù)是唯一的,不會浪費資源。VBO中存儲著CPU傳給GPU的數(shù)據(jù),存儲在顯存里,在執(zhí)行大量重復的繪制操作時,可以提高效率。

初始化attribute變量

之前創(chuàng)建的shader中,有兩個attribute變量,需要使用glVertexAttribPointer輸入給shader。

//我們需要在一個矩形中繪制圖像,需要兩個三角形模擬,所以需要四個頂點,索引數(shù)組說明了兩個三角形頂點組成。
//默認情況下,OpenGL 的Viewport左下角頂點為(-1,-1),右上角頂點為(1,1)。
const float vertices[] = {
 1, -1, 0,//index 0
 1,  1, 0,//index 1
-1,  1, 0,//index 2
-1, -1, 0 //index 3
}
const GLubyte Indices[] = {
     0, 1, 2,
     2, 3, 0
};

- (void)setupVBOs {
    //頂點VBO
    GLuint vertexBuffer;
    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    //將頂點坐標寫入頂點VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), Vertices, GL_STATIC_DRAW);
    //索引VBO
    GLuint indexBuffer;
    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    //將頂點索引數(shù)據(jù)寫入索引VBO
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
  }
-(void)feedAttributeSlot{
    //由于使用了VBO,所以最后一個參數(shù)傳數(shù)據(jù)在VBO中的偏移量,這點需要注意
    glVertexAttribPointer(_vertexPosition, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, 0);
    //沒有使用VBO,直接傳指針給函數(shù),
    float blueColor[] = {0, 0, 1, 0};
    glVertexAttribPointer(_pixelColor, 4, GL_FLOAT, GL_FALSE, 0, blueColor);
  }

注意此時寫入VBO的只是一些二進制的數(shù)據(jù),需要在讀取數(shù)據(jù)是,給出數(shù)據(jù)類型才能正確讀取。

繪制

- (void)render {
    //繪制黑色背景
    glClearColor(0, 0, 0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    //創(chuàng)建一個OpenGL繪制的窗口
    glViewport(0, 0, self.frame.size.width, self.frame.size.height);
    //使用頂點索引,繪制圖形。調(diào)用函數(shù)后,vertex shader會在每個頂點執(zhí)行一遍,確定頂點信息。fragment shader會在每個像素執(zhí)行一遍,確定像素顏色。
    //在使用VBO的情況下,最后一個參數(shù)傳0
    glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]), 
        GL_UNSIGNED_BYTE, 0);
    //EACAGLContext 渲染OpenGL繪制好的圖像到EACAGLLayer
    [_context presentRenderbuffer:GL_RENDERBUFFER];
} 

由于shader中pixelColor變量類型是varying類型,在處理未知值是,會自動插值,默認為線性插值。如果對shader的GLSL語法不熟悉,可以看這篇文章。

所以,當頂點之間的顏色不同是,fragment shader在處理圖形內(nèi)部的像素后會返回一個根據(jù)頂點插值后的數(shù)值,整個圖形是漸變色的。

這里因為四個頂點都設置為了藍色,所以繪制出來是一個藍色的矩形。

(添加圖)

使用OpenGL繪制YUV數(shù)據(jù)

以上內(nèi)容簡單介紹了如何使用OpenGL繪制,現(xiàn)在重點如何使用OpenGL繪制YUV數(shù)據(jù)。
YUV是一種顏色編碼格式,常用的格式有YUV444,YUV422P,YUV420P,YUV420SP等。
本文主要研究YUV420PI420YUV420SPNV12。

紋理

我們需要將YUV數(shù)據(jù)紋理的方式加載到OpenGL,再將紋理貼到之前創(chuàng)建矩形上,完成繪制。
將每個頂點賦予一個紋理坐標,OpenGL會根據(jù)紋理坐標插值得到圖形內(nèi)部的像素值。OpenGL的紋理坐標系是歸一化的,取值范圍是0 - 1,左下角是原點。


三角形貼上紋理需要的紋理坐標
三角形貼上紋理需要的紋理坐標

紋理目標、紋理對象、紋理單元

  1. 紋理目標是顯卡的軟件接口中定義的句柄,指向要進行當前操作的顯存。
  2. 紋理對象是我們創(chuàng)建的用來存儲紋理的顯存,在實際使用過程中使用的是創(chuàng)建后返回的ID。
  3. 紋理單元是顯卡中所有的可用于在shader中進行紋理采樣的顯存,數(shù)量與顯卡類型相關,至少16個。在激活某個紋理單元后,紋理目標就該紋理單元,默認激活的是GL_TEXTURE0。

可以這么想象,紋理目標是轉輪手槍正對彈膛的單孔,紋理對象就是子彈,紋理單元是手槍的六個彈孔。下面用代碼說明它們之間的關系。

//創(chuàng)建一個紋理對象數(shù)組,數(shù)組里是紋理對象的ID
GLuint texture[3];
//創(chuàng)建紋理對象,第一個參數(shù)是要創(chuàng)建的數(shù)量,第二個參數(shù)是數(shù)組的基址
glGenTextures(3, &texture);
//激活GL_TEXTURE0這個紋理單元,用于之后的紋理采樣
glActiveTexture(GL_TEXTURE0);
//綁定紋理對象texture[0]到紋理目標GL_TEXTURE_2D,接下來對紋理目標的操作都發(fā)生在此對象上
glBindTexture(GL_TEXTURE_2D, texture[0]);
//創(chuàng)建圖像,采樣工作在GL_TEXTURE0中完成,圖像數(shù)據(jù)存儲在GL_TEXTURE_2D綁定的對象,即texture[0]中。
glTexImage(GL_TEXTURE_2D, ...);
//解除綁定,此時再對GL_TEXTURE_2D不會影響到texture[0],texture[0]的內(nèi)存不會回收。
glBindTexture(GL_TEXTURE_2D, 0);
//可以不斷創(chuàng)建新的紋理對象,直到顯存耗凈

修改shader

之前創(chuàng)建的簡單shader現(xiàn)在要修改代碼,實現(xiàn)對YUV數(shù)據(jù)的繪制。如果對GLSL語法與YUV不熟悉,可以看OpenGL的著色語言:GLSLYUV顏色編碼解析。

//vertex shader
ttribute vec4 position;
attribute mediump vec2 textureCoordinate;//要獲取的紋理坐標
varying mediump vec2 coordinate;//傳遞給fragm shader的紋理坐標,會自動插值

void main(void) { 
    gl_Position = vertexPosition; 
    coordinate = textureCoordinate;
}
//fragment shader
precision mediump float;

uniform sampler2D SamplerY;//sample2D的常量,用來獲取I420數(shù)據(jù)的Y平面數(shù)據(jù)
uniform sampler2D SamplerU;//U平面
uniform sampler2D SamplerV;//V平面

uniform sampler2D SamplerNV12_Y;//NV12數(shù)據(jù)的Y平面
uniform sampler2D SamplerNV12_UV;//NV12數(shù)據(jù)的UV平面

varying highp vec2 coordinate;//紋理坐標

uniform int yuvType;//0 代表 I420, 1 代表 NV12

//用來做YUV --> RGB 的變換矩陣
const vec3 delyuv = vec3(-0.0/255.0,-128.0/255.0,-128.0/255.0);
const vec3 matYUVRGB1 = vec3(1.0,0.0,1.402);
const vec3 matYUVRGB2 = vec3(1.0,-0.344,-0.714);
const vec3 matYUVRGB3 = vec3(1.0,1.772,0.0);

void main()
{
    vec3 CurResult;
    highp vec3 yuv;
    
    if (yuvType == 0){
        yuv.x = texture2D(SamplerY, coordinate).r;//因為是YUV的一個平面,所以采樣后的r,g,b,a這四個參數(shù)的數(shù)值是一樣的
        yuv.y = texture2D(SamplerU, coordinate).r;
        yuv.z = texture2D(SamplerV, coordinate).r;
    }
    else{
        yuv.x = texture2D(SamplerY, coordinate).r;
        yuv.y = texture2D(SamplerUV, coordinate).r;//因為NV12是2平面的,對于UV平面,在加載紋理時,會指定格式,讓U值存在r,g,b中,V值存在a中。
        yuv.z = texture2D(SamplerUV, coordinate).a;//這里會在下面解釋
    }
    
    yuv += delyuv;//讀取值得范圍是0-255,讀取時要-128回歸原值
    //用數(shù)量積來模擬矩陣變換,轉換成RGB值
    CurResult.x = dot(yuv,matYUVRGB1);
    CurResult.y = dot(yuv,matYUVRGB2);
    CurResult.z = dot(yuv,matYUVRGB3);
    //輸出像素值給光柵器
    gl_FragColor = vec4(CurResult.rgb, 1);
}

加載YUV數(shù)據(jù)到紋理對象

現(xiàn)在有了可以處理YUV數(shù)據(jù)的shader,我們需要加載YUV數(shù)據(jù),來讓OpenGL完成繪制。

//創(chuàng)建紋理對象,需要3個紋理對象來獲取不同平面的數(shù)據(jù)
-(void)setupTexture{
    _planarTextureHandles = (GLuint *)malloc(3*sizeof(GLuint));
    glGenTextures(3, _planarTextureHandles);
}
-(void)feedTextureWithImageData:(Byte*)imageData imageSize:(CGSize)imageSize type:(NSInteger)type{
    //根據(jù)YUV編碼的特點,獲得不同平面的基址
    Byte * yPlane =  imageData;
    Byte * uPlane =  imageData + imageSize.width*imageSize.height;
    Byte * vPlane =  imageData + imageSize.width*imageSize.height * 5 / 4;
    if (type == 0) {
        [self textureYUV:yPlane widthType:imageSize.width heightType:imageSize.height index:0];
        [self textureYUV:uPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:1];
        [self textureYUV:vPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:2];
    }else{
        [self textureYUV:yPlane widthType:imageSize.width heightType:imageSize.height index:0];
        [self textureNV12:uPlane widthType:imageSize.width/2 heightType:imageSize.height/2 index:1];
    }
}
- (void) textureYUV: (Byte*)imageData widthType: (int) width heightType: (int) height index: (int) index
{
    //將紋理對象綁定到紋理目標
    glBindTexture(GL_TEXTURE_2D, _planarTextureHandles[index]);
    //設置放大和縮小時,紋理的過濾選項為:線性過濾
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    //設置紋理X,Y軸的紋理環(huán)繞選項為:邊緣像素延伸
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    //加載圖像數(shù)據(jù)到紋理,GL_LUMINANCE指明了圖像數(shù)據(jù)的像素格式為只有亮度,雖然第三個和第七個參數(shù)都使用了GL_LUMINANCE,
    //但意義是不一樣的,前者指明了紋理對象的顏色分量成分,后者指明了圖像數(shù)據(jù)的像素格式
    //獲得紋理對象后,其每個像素的r,g,b,a值都為相同,為加載圖像的像素亮度,在這里就是YUV某一平面的分量值
    glTexImage2D( GL_TEXTURE_2D, 0, GL_LUMINANCE, width, height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, imageData );
    //解綁
    glBindTexture(GL_TEXTURE_2D, 0);
}

CADisplayLink定時繪制

現(xiàn)在已經(jīng)能夠?qū)UV數(shù)據(jù)加載到紋理對象了,下一步來改造render方法,將其繪制到屏幕上??梢杂肅ADisplayLink定時調(diào)用render方法,可以根據(jù)屏幕刷新頻率來控制YUV視頻流的幀率。

- (void)render {
    //繪制黑色背景
    glClearColor(0, 0, 0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    //獲取平面的scale
    CGFloat scale = [[UIScreen mainScreen] scale];
    CGFloat width = _frame.size.width*scale;
    CGFloat height = _frame.size.height*scale;
    //創(chuàng)建一個OpenGL繪制的窗口
    glViewport(0, 0,width,height);
    [self drawTexture];
    //EACAGLContext 渲染OpenGL繪制好的圖像到EACAGLLayer
    [_context presentRenderbuffer:GL_RENDERBUFFER];
} 
//fragment shader的sample數(shù)組
GLint sampleHandle[3];
//繪制紋理
- (void) drawTexture{
    //傳紋理坐標給fragment shader
    glVertexAttribPointer([AVGLShareInstance shareInstance].texCoordAttributeLocation, 2, GL_FLOAT, GL_FALSE,
                          sizeof(Vertex), (void*)offsetof(Vertex, TexCoord));
    glEnableVertexAttribArray([AVGLShareInstance shareInstance].texCoordAttributeLocation);
    //傳紋理的像素格式給fragment shader
    GLint yuvType = glGetUniformLocation(_programHandle, "yuvType");
    glUniform1i([AVGLShareInstance shareInstance].drawTypeUniform, yuvType);
    //type: 0是I420, 1是NV12
    int planarCount = 0;
    if (type == 0) {
        planarCount = 3;//I420有3個平面
        sampleHandle[1] = glGetUniformLocation(_programHandle, "samplerY");
        sampleHandle[2] = glGetUniformLocation(_programHandle, "samplerU");
        sampleHandle[3] = glGetUniformLocation(_programHandle, "samplerV");
    }else{
        planarCount = 2;//NV12有兩個平面
        sampleHandle[1] = glGetUniformLocation(_programHandle, "SamplerNV12_Y");
        sampleHandle[2] = glGetUniformLocation(_programHandle, "SamplerNV12_UV");
    }
        for (int i=0; i<planarCount; i++){
            glActiveTexture(GL_TEXTURE0+i);
            glBindTexture(GL_TEXTURE_2D, _planarTextureHandles[i]);
            glUniform1i(sampleHandle[i], i);
        }
    //繪制函數(shù),使用三角形作為圖元構造要繪制的幾何圖形,由于頂點的indexs使用了VBO,所以最后一個參數(shù)傳0
    //調(diào)用這個函數(shù)后,vertex shader先在每個頂點執(zhí)行一次,之后fragment shader在每個像素執(zhí)行一次,繪制后的圖像存儲在render buffer中。
    glDrawElements(GL_TRIANGLES, 6,GL_UNSIGNED_BYTE, 0);
}

可以想象的應用場景

使用OpenGL繪制視頻,是實現(xiàn)簡單AR最簡單的方式,也可以根據(jù)業(yè)務來對視頻播放做進一步的個性定制,比如動態(tài)打碼,貼紙等。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容