这儿是 RenderDemo 的第八篇:用 OpenGL 完成礼物特效。咱们别离在 iOS 和 Android 平台完成了用 OpenGL 对图像进行礼物特效处理并烘托出来。
到目前咱们已经在咱们的付费知识星球中供给了下面这些音视频 Demo 和烘托 Demo 的工程源码,均可直接下载运行:
- iOS AVDemo(1):音频收集
- iOS AVDemo(2):音频编码
- iOS AVDemo(3):音频封装
- iOS AVDemo(4):音频解封装
- iOS AVDemo(5):音频解码
- iOS AVDemo(6):音频烘托
- iOS AVDemo(7):视频收集
- iOS AVDemo(8):视频编码
- iOS AVDemo(9):视频封装
- iOS AVDemo(10):视频解封装
- iOS AVDemo(11):视频转封装
- iOS AVDemo(12):视频编码
- iOS AVDemo(13):视频烘托
- Android AVDemo(1):音频收集
- Android AVDemo(2):音频编码
- Android AVDemo(3):音频封装
- Android AVDemo(4):音频解封装
- Android AVDemo(5):音频解码
- Android AVDemo(6):音频烘托
- Android AVDemo(7):视频收集
- Android AVDemo(8):视频编码
- Android AVDemo(9):视频封装
- Android AVDemo(10):视频解封装
- Android AVDemo(11):视频转封装
- Android AVDemo(12):视频解码
- Android AVDemo(13):视频烘托
- RenderDemo(1):用 OpenGL 画一个三角形(iOS+Android)
- RenderDemo(2):用 OpenGL 烘托视频(iOS+Android)
- RenderDemo(3):用 OpenGL 完成高斯含糊(iOS+Android)
- RenderDemo(4):用 OpenGL 完成反色(iOS+Android)
- RenderDemo(5):用 OpenGL 完成三分屏(iOS+Android)
- RenderDemo(6):用 OpenGL 完成贴纸(iOS+Android)
- RenderDemo(7):用 OpenGL 完成滤镜(iOS+Android)
这些源码关于学习和理解 iOS/Android 音视频开发十分简单上手,vx 查找『gjzkeyframe』 关注『关键帧Keyframe』。发送消息『知识星球』来获得源码
礼物特效是直播与短视频特效的一把利刃,设计师能够很简单的将各种 AE 作用直接进行应用。相关于 GIF 、WEBP、Lottie 等特效更适用于大礼物作用。
1、礼物特效基础知识
能够经过制作 Alpha 通道别离的视频素材,再在客户端上经过 OpenGL 从头完成 Alpha 通道和 RGB 通道的混合,从而完成在端上播映带通明通道的视频。
1.1、视频出产
出产一个礼物,不管你是 GIF、APNG、WEBP,只要把它们生成序列帧,往 AE 里面一丢。
1)输出正常 RGB 视频,导出视频保存。
RGB视频
2)输出纯通道 Alpha 视频,导出视频保存。
A视频
3)新建一个宽度乘 2 的画布,把 2 个刚导出的视频左右别离放置,最终导出视频保存。
原视频
1.2、Shader 完成
Shader 完成如下:
precisionhighpfloat;
varyingvec2textureCoordinate;
uniformsampler2DinputImageTexture;
voidmain()
{
vec4aColor=texture2D(inputImageTexture,vec2(textureCoordinate.x/2.0,textureCoordinate.y));
vec4vColor=texture2D(inputImageTexture,vec2(0.5+textureCoordinate.x/2.0,textureCoordinate.y));
gl_FragColor=vec4(vColor.x,vColor.y,vColor.z,aColor.x);
}
1.3、视图混合
视频特效包括通明通道,还需要设置 GLView Layer opaque 特点,这样能够与 UIView 进行颜色混合。
_glView.layer.opaque=NO;
1.4、烘托作用
视频的作用如下:
礼物特效
2、iOS Demo
2.1、烘托模块
烘托模块与OpenGL 高斯含糊中讲到的共同,最终是封装出一个烘托视图KFOpenGLView
用于展现最终的烘托结果。这儿就不再细讲,只贴一下首要的类和类具体的功用:
-
KFOpenGLView
:运用 OpenGL 完成的烘托 View,供给了设置画面填充模式的接口和烘托一帧纹路的接口。 -
KFGLFilter
:完成 shader 的加载、编译和着色器程序链接,以及 FBO 的办理。一起作为烘托处理节点,供给给了接口支撑多级烘托。 -
KFGLProgram
:封装了运用 GL 程序的部分 API。 -
KFGLFrameBuffer
:封装了运用 FBO 的 API。 -
KFTextureFrame
:表明一帧纹路目标。 -
KFFrame
:表明一帧,类型能够是数据缓冲或纹路。 -
KFGLTextureAttributes
:对纹路 Texture 特点的封装。 -
KFGLBase
:界说了默许的 VertexShader 和 FragmentShader。 -
KFUIImageConvertTexture
:用于完成图片转纹路。
2.2、礼物特效烘托结果烘托流程
咱们在一个ViewController
中完成了礼物特效。代码如下:
staticNSString*constPlayerItemStatusContext=@"PlayerItemStatusContext";
@interfaceKFVideoRenderViewController()<AVPlayerItemOutputPullDelegate>
@property(nonatomic,strong)KFOpenGLView*glView;
@property(nonatomic,strong)KFPixelBufferConvertTexture*pixelBufferConvertTexture;
@property(nonatomic,strong)EAGLContext*context;
@property(nonatomic,strong)KFGLFilter*filter;
@property(strong,nonatomic)AVPlayer*player;
@property(strong,nonatomic,nonnull)dispatch_queue_tvideoOutputQueue;
@property(strong,nonatomic,nonnull)AVPlayerItemVideoOutput*videoOutput;
@property(strong,nonatomic,nonnull)CADisplayLink*displayLink;
@end
@implementationKFVideoRenderViewController
#pragmamark-Property
-(EAGLContext*)context{
if(!_context){
_context=[[EAGLContextalloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
}
return_context;
}
-(KFPixelBufferConvertTexture*)pixelBufferConvertTexture{
if(!_pixelBufferConvertTexture){
_pixelBufferConvertTexture=[[KFPixelBufferConvertTexturealloc]initWithContext:self.context];
}
return_pixelBufferConvertTexture;
}
-(KFGLFilter*)filter{
if(!_filter){
NSString*path=[[NSBundlemainBundle]pathForResource:@"filter"ofType:@"fs"];
_filter=[[KFGLFilteralloc]initWithCustomFBO:NOvertexShader:KFDefaultVertexShaderfragmentShader:[NSStringstringWithContentsOfURL:[NSURLfileURLWithPath:path]encoding:NSUTF8StringEncodingerror:nil]];
__weaktypeof(self)_self=self;
_filter.preDrawCallBack=^(){
__strongtypeof(_self)sself=_self;
if(sself){
}
};
}
return_filter;
}
-(void)dealloc{
}
-(void)viewDidDisappear:(BOOL)animated{
[superviewDidDisappear:animated];
[self->_displayLinkinvalidate];
[_player.currentItemremoveObserver:selfforKeyPath:@"status"];
}
#pragmamark-Lifecycle
-(void)viewDidLoad{
[superviewDidLoad];
[selfsetupUI];
}
-(void)viewWillLayoutSubviews{
[superviewWillLayoutSubviews];
self.glView.frame=self.view.bounds;
}
#pragmamark-Action
-(void)setupUI{
self.edgesForExtendedLayout=UIRectEdgeAll;
self.extendedLayoutIncludesOpaqueBars=YES;
self.title=@"VideoRender";
self.view.backgroundColor=[UIColorredColor];
//烘托view。
_glView=[[KFOpenGLViewalloc]initWithFrame:self.view.boundscontext:self.context];
_glView.fillMode=KFGLViewContentModeFill;
_glView.layer.opaque=NO;
[self.viewaddSubview:self.glView];
//player
AVPlayerItem*item=[[AVPlayerItemalloc]initWithURL:[NSURLfileURLWithPath:[[NSBundlemainBundle]pathForResource:@"569"ofType:@"mp4"]]];
item.audioTimePitchAlgorithm=AVAudioTimePitchAlgorithmTimeDomain;
_videoOutputQueue=dispatch_queue_create("player.output.queue",DISPATCH_QUEUE_SERIAL);
NSDictionary*attributes=@{(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)};
self.videoOutput=[[AVPlayerItemVideoOutputalloc]initWithPixelBufferAttributes:attributes];
[self.videoOutputsetDelegate:selfqueue:self.videoOutputQueue];
_player=[AVPlayerplayerWithPlayerItem:item];
_player.actionAtItemEnd=AVPlayerActionAtItemEndPause;
[_player.currentItemaddOutput:self.videoOutput];
[_player.currentItemaddObserver:selfforKeyPath:@"status"options:0context:(__bridgevoid*)(PlayerItemStatusContext)];
_displayLink=[CADisplayLinkdisplayLinkWithTarget:selfselector:@selector(displayLinkCallback:)];
[_displayLinkaddToRunLoop:[NSRunLoopmainRunLoop]forMode:NSRunLoopCommonModes];
[_displayLinksetPaused:YES];
}
-(void)observeValueForKeyPath:(NSString*)keyPathofObject:(id)objectchange:(NSDictionary<NSString*,id>*)changecontext:(void*)context{
if(context==(__bridgevoid*)(PlayerItemStatusContext)){
if([keyPathisEqualToString:@"status"]){
AVPlayerItem*item=(AVPlayerItem*)object;
if(item.status==AVPlayerItemStatusReadyToPlay){//准备好播映
[self.playerplay];
[self.displayLinksetPaused:NO];
}elseif(item.status==AVPlayerItemStatusFailed){//失败
NSLog(@"failed");
}
}
}
}
#pragmamark-CADisplayLinkCallback
-(void)displayLinkCallback:(CADisplayLink*)sender{
CMTimeitemTime=[self.videoOutputitemTimeForHostTime:CACurrentMediaTime()];
if([self.videoOutputhasNewPixelBufferForItemTime:itemTime]){
CVPixelBufferRefpixelBuffer=NULL;
pixelBuffer=[self.videoOutputcopyPixelBufferForItemTime:itemTimeitemTimeForDisplay:NULL];
if(pixelBuffer){
EAGLContext*preContext=[EAGLContextcurrentContext];
[EAGLContextsetCurrentContext:self.context];
KFTextureFrame*textureFrame=[self.pixelBufferConvertTexturerenderFrame:pixelBuffertime:itemTime];
textureFrame.textureSize=CGSizeMake(textureFrame.textureSize.width/2,textureFrame.textureSize.height);
KFTextureFrame*filterFrame=[self.filterrender:textureFrame];
[self.glViewdisplayFrame:filterFrame];
[EAGLContextsetCurrentContext:preContext];
CFRelease(pixelBuffer);
}
}
}
@end
经过上面的代码,能够看到咱们是用KFGLFilter
来封装一次 OpenGL 的处理节点,它能够接收一个KFTextureFrame
目标,加载 Shader 对其进行烘托处理,处理完后输出处理后的KFTextureFrame
,然后能够接着交给下一个KFGLFilter
来处理,就像一条烘托链。
3、Android Demo
3.1、烘托模块
烘托模块与OpenGL 高斯含糊中讲到的共同,最终是封装出一个烘托视图KFRenderView
用于展现最终的烘托结果。这儿就不再细讲,只贴一下首要的类和类具体的功用:
-
KFGLContext
:担任创立 OpenGL 环境,担任办理和拼装 EGLDisplay、EGLSurface、EGLContext。 -
KFGLFilter
:完成 shader 的加载、编译和着色器程序链接,以及 FBO 的办理。一起作为烘托处理节点,供给给了接口支撑多级烘托。 -
KFGLProgram
:担任加载和编译着色器,创立着色器程序容器。 -
KFGLBase
:界说了默许的 VertexShader 和 FragmentShader。 -
KFSurfaceView
:KFSurfaceView 继承自 SurfaceView 来完成烘托。 -
KFTextureView
:KFTextureView 继承自 TextureView 来完成烘托。 -
KFFrame
:表明一帧,类型能够是数据缓冲或纹路。 -
KFRenderView
:KFRenderView 是一个容器,能够选择运用 KFSurfaceView 或 KFTextureView 作为实际的烘托视图。
3.2、礼物特效烘托结果烘托流程
咱们在一个MainActivity
中完成了礼物特效。代码如下:
publicclassMainActivityextendsAppCompatActivity{
privateMediaPlayermMediaPlayer;
privateKFSurfaceTexturemSurfaceTexture=null;
privateKFGLFiltermOESConvert2DFilter;///<特效
privateSurfacemSurface=null;
privateKFRenderViewmRenderView;
privateKFGLContextmGLContext;
privateKFGLFiltermGLFilter;
privateStringmFragmentShaderString;
@RequiresApi(api=Build.VERSION_CODES.LOLLIPOP)
@Override
protectedvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(ActivityCompat.checkSelfPermission(this,Manifest.permission.RECORD_AUDIO)!=PackageManager.PERMISSION_GRANTED||ActivityCompat.checkSelfPermission(this,Manifest.permission.CAMERA)!=PackageManager.PERMISSION_GRANTED||
ActivityCompat.checkSelfPermission(this,Manifest.permission.READ_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED||
ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions((Activity)this,
newString[]{Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO,Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
}
mFragmentShaderString=getShaderString(this,"filter.fs");
mGLContext=newKFGLContext(null);
mRenderView=newKFRenderView(this,mGLContext.getContext());
mRenderView.setOpaque(false);
WindowManagerwindowManager=(WindowManager)this.getSystemService(this.WINDOW_SERVICE);
RectoutRect=newRect();
windowManager.getDefaultDisplay().getRectSize(outRect);
FrameLayout.LayoutParamsparams=newFrameLayout.LayoutParams(outRect.width(),outRect.height());
addContentView(mRenderView,params);
mGLContext.bind();
mSurfaceTexture=newKFSurfaceTexture(mSurfaceTextureListener);
mOESConvert2DFilter=newKFGLFilter(false,KFGLBase.defaultVertexShader,KFGLBase.oesFragmentShader);
mGLContext.unbind();
mSurface=newSurface(mSurfaceTexture.getSurfaceTexture());
try{
AssetManagerassetManager=this.getAssets();
AssetFileDescriptorvideoAssetFile=assetManager.openFd("569.mp4");
mMediaPlayer=newMediaPlayer();
mMediaPlayer.setDataSource(videoAssetFile.getFileDescriptor(),
videoAssetFile.getStartOffset(),videoAssetFile.getLength());
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.setLooping(true);
mMediaPlayer.setOnPreparedListener(newMediaPlayer.OnPreparedListener(){
@Override
publicvoidonPrepared(MediaPlayermp){
mMediaPlayer.start();
}
});
mMediaPlayer.prepareAsync();
}catch(IOExceptione){
e.printStackTrace();
}
}
privateKFGLFilterListenermFilterListener=newKFGLFilterListener(){
@RequiresApi(api=Build.VERSION_CODES.LOLLIPOP)
@Override
publicvoidpreOnDraw(){
}
@Override
publicvoidpostOnDraw(){
}
};
privateKFSurfaceTextureListenermSurfaceTextureListener=newKFSurfaceTextureListener(){
@RequiresApi(api=Build.VERSION_CODES.LOLLIPOP)
@Override
//<SurfaceTexture数据回调
publicvoidonFrameAvailable(SurfaceTexturesurfaceTexture){
longtimestamp=System.nanoTime();
mGLContext.bind();
///<改写纹路数据至SurfaceTexture
mSurfaceTexture.getSurfaceTexture().updateTexImage();
KFTextureFrameframe=newKFTextureFrame(mSurfaceTexture.getSurfaceTextureId(),newSize(mMediaPlayer.getVideoWidth(),mMediaPlayer.getVideoHeight()),timestamp,true);
mSurfaceTexture.getSurfaceTexture().getTransformMatrix(frame.textureMatrix);
Matrix.scaleM(frame.positionMatrix,0,1,-1,1);
KFTextureFrameconvertFrame=(KFTextureFrame)mOESConvert2DFilter.render(frame);
if(mGLFilter==null){
mGLFilter=newKFGLFilter(false,KFGLBase.defaultVertexShader,mFragmentShaderString,mFilterListener,null);
}
convertFrame.textureSize=newSize(convertFrame.textureSize.getWidth()/2,convertFrame.textureSize.getHeight());
KFTextureFramefilterFrame=(KFTextureFrame)mGLFilter.render(convertFrame);
mRenderView.render((KFTextureFrame)filterFrame);
mGLContext.unbind();
}
};
privatestaticStringgetShaderString(Contextcontext,StringfileName){
StringBuilderstringBuilder=newStringBuilder();
try{
AssetManagerassetManager=context.getAssets();
BufferedReaderbf=newBufferedReader(newInputStreamReader(
assetManager.open(fileName)));
Stringline;
while((line=bf.readLine())!=null){
stringBuilder.append(line);
}
}catch(IOExceptione){
e.printStackTrace();
}
returnstringBuilder.toString();
}
}
可见,当咱们用KFGLFilter
将 OpenGL 烘托能力封装起来,并能够像添加烘托处理节点相同往现有烘托链中添加新的图像处理功用时,相关改动就变得很方便了。
- 完 –