前言:
安卓使用SurfaceView + SurfaceTexture + Surface进行视频播放或者相机预览,只能看到原色画面,但很多场合需求画面可以实现二次加工,例如调整红绿蓝三原色的比例、添加滤镜、降噪等,此时如果使用GLSurfaceview搭配GL Shader Language进行基于GPU的实时处理,就可以又快又节能地满足这个需求。效果如下:
安卓利用GLSurfaceview + OpenGL片元渲染器实现MediaPlayer播放时画面白平衡、对比度和亮度的调节
承接上文:一种基于FBO实现渲染流水线的思路_轮子初级玩家-CSDN博客假如我希望实现如下特性:1、使用片元shader,把YUV信号作为纹理输入,采样过程中转换为RGB信号。2、把第1步取得的画面通过片元shader,使用3*3的卷积核,实现卷积模糊。那么,就有如下几种方案:第一种:片元shader每次采样3*3个坐标,转换后记录到数组,之后对数组实现卷积处理,最后输出片元颜色。第二种:片元shader每次采样1个坐标,转换后直接输出到片元颜色,此时采样后的输出就会在指定的frameBuffer中。然后第二个片元shader使用framebufhttps://blog.csdn.net/cjzjolly/article/details/123412651?spm=1001.2014.3001.5501
一、前置知识:
0、OES纹理是什么呢:
2、BufferQueue是什么
//todo
3、surface是什么
//todo
4、surfaceTexture是什么
用途:OES纹理无论是用于安卓下的相机预览数据获取,还是播放器播放画面的获取,都是比较容易实现的一种途径。把OES纹理输入到片元shader后,即可在采样过程中实现各种各样的图像处理操作,如降噪、美颜、滤镜等,这是在安卓下进行比较复杂的实时图像处理的常用入口。搭配NDK即可实现图像处理和OpenGL方面的代码使用C++进行编写以实现代码跨端,既能在安卓上使用,也能在别的平台上复用。其实安卓的GLES30、GLES20等库本身也是通过JNI调用native下的GL函数而已,只要GLSurfaceView建立成功,则当前进程的EGLContext就已经建立成功,这个时候无论是调用GLES20等库,还是直接在JNI上调用C下的GL函数,只要还是在同一个进程同一个EGLContext环境下,那这个调用就基本上是等价的。
5、画面是如何从播放器中传递到OES纹理的:
//todo
6、为什么使用JNI:
因为GL的语法,在每个平台都基本一致,因此完全可以用C/C++写一份OpenGL处理逻辑即可跨平台使用。但如果使用GLES20/GLES30的安卓自带的JNI桥接库,则不能直接应用于如PC等的平台。因此GL代码使用JNI进行编写我认为是比较合理的。
二、具体实践:
0、创建GLSurfaceView与渲染器GLSurfaceView.Renderer:
首先创建GLSurfaceview,创建这个实例时,就会主动帮你创建好EGLContext,之后就可以自由地在drawCall线程中灵活使用GL的API进行各种各样的绘图操作了。而要使用GLSurfaceview,则必须给它指定一系列的操作,去指导它如何进行渲染,因此我们需要implements一个GLSurfaceView.Renderer,作为我们的GLSurfaceview实例是渲染器,其中主要要实现以下几个方法:
onSurfaceCreated:当GLSurfaceview的surface创建后,该方法就会被回调。此时可以做一些初始化的操作,例如我就在这里创建了一个OES_TEXTURE,并且把纹理的索引值通过SurfaceTexture进行包装,再传入Surface再进行一次包装,此时这个Surface即可作为MediaPlayer或相机API的写入数据的桥梁,摄像机画面或者播放器的画面就会被OES_TEXTURE所记录,同时OpenGL可以使用这个纹理索引作为纹理传入(需要external特性声明),即可在片元渲染器进行各种画面操作,如降噪、卷积图像处理、滤镜、颜色调节等。另外我还在这里创建了一个用于实验的MediaPlayer实例。
onSurfaceChanged:当GLSurfaceview的大小发生改变时,此方法会被回调,此时可以使用传入的width、height变量更新自己的GL绘图视口大小等操作。
onDrawFrame:EGLContext所在的GL线程,每次需要绘图时就会回调该方法,此时用于所填充的绘图代码就会被执行。由于绘图操作都需要在GL线程中执行,所以这是所有绘图操作的起点。在其他线程执行GL绘图操作是不被允许的,虽然编译不会出现问题,但不会执行任何你想要看到的画面。另外,由于SurfaceTexture从BufferQueue中拿帧需要调用updateTexImage方法,这个调用过程放到每一帧绘制的回调中去调用是更合适的。
具体整个GLSurfaceView的代码如下:
package com.opengldecoder.jnibridge;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.opengl.GLES11Ext;
import android.opengl.GLES30;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.Surface;
import java.nio.ByteBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class NativeGLSurfaceView extends GLSurfaceView {
private Bitmap mTestBmp;
private Renderer mRenderer;
/**图层native指针**/
private long mLayer = Long.MIN_VALUE;
private long mRenderOES = Long.MIN_VALUE;
private long mRenderNoiseReduction = Long.MIN_VALUE;
private long mRenderConvolutionDemo = Long.MIN_VALUE;
private long mRenderLut = Long.MIN_VALUE;
private long mRenderDeBackground = Long.MIN_VALUE;
//Android画面数据输入Surface
private Surface mDataInputSurface = null;
//Android画面数据输入纹理
private int[] mDataInputTexturesPointer = null;
private SurfaceTexture mInputDataSurfaceTexture;
private Player mDemoPlayer;
private float mdx, mdy;
private float mPrevX, mPrevY;
public NativeGLSurfaceView(Context context) {
super(context);
}
public NativeGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
Log.i("cjztest", "NativeGLSurfaceView222");
}
private void init() {
this.setEGLContextClientVersion(3);//使用OpenGL ES 3.0需设置该参数为3
mRenderer = new Renderer();//创建Renderer类的对象
this.setRenderer(mRenderer); //设置渲染器
this.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
public Surface getSurface() {
Log.i("cjztest", "GLRenderer.getSurface:" + mDataInputSurface.toString());
return mDataInputSurface;
}
/**亮度调整**/
public void setRenderBrightness(float brightness) {
if (mRenderOES != Long.MIN_VALUE) {
JniBridge.setBrightness(mRenderOES, brightness);
}
}
/**对比度调整**/
public void setRenderContrast(float contrast) {
if (mRenderOES != Long.MIN_VALUE) {
JniBridge.setContrast(mRenderOES, contrast);
}
}
/**白平衡调整**/
public void setRenderWhiteBalance(float rWeight, float gWeight, float bWeight) {
if (mRenderOES != Long.MIN_VALUE) {
JniBridge.setWhiteBalance(mRenderOES, rWeight, gWeight, bWeight);
}
}
/**降噪渲染器开关**/
public void setRenderNoiseReductionOnOff(boolean sw) {
if (mLayer != Long.MIN_VALUE) {
if (mRenderNoiseReduction != Long.MIN_VALUE) {
if (sw) {
JniBridge.addRenderToLayer(mLayer, mRenderNoiseReduction);
} else {
JniBridge.removeRenderForLayer(mLayer, mRenderNoiseReduction);
}
}
}
}
/**滤镜开关**/
public void setRenderLutOnOff(boolean sw) {
if (mLayer != Long.MIN_VALUE && mRenderLut != Long.MIN_VALUE) {
if (sw) {
JniBridge.addRenderToLayer(mLayer, mRenderLut);
} else {
JniBridge.removeRenderForLayer(mLayer, mRenderLut);
}
}
}
/**背景去除程序开关**/
public void setRenderDeBackgroundOnOff(boolean sw) {
if (mLayer != Long.MIN_VALUE && mRenderDeBackground != Long.MIN_VALUE) {
if (sw) {
JniBridge.addRenderToLayer(mLayer, mRenderDeBackground);
} else {
JniBridge.removeRenderForLayer(mLayer, mRenderDeBackground);
}
}
}
/**长宽缩放**/
public void setScale(float sx, float sy) {
if (mLayer != Long.MIN_VALUE) {
JniBridge.layerScale(mLayer, sx, sy);
}
}
/**移动**/
public void setTrans(float x, float y) {
if (mLayer != Long.MIN_VALUE) {
JniBridge.layerTranslate(mLayer, x, y);
}
}
/**旋转**/
public void setRotate(float angle) {
if (mLayer != Long.MIN_VALUE) {
JniBridge.layerRotate(mLayer, angle);
}
}
/**加载滤镜**/
public void setLut(Bitmap lutBMP) {
if (mLayer != Long.MIN_VALUE && mRenderLut != Long.MIN_VALUE) {
byte b[] = new byte[lutBMP.getByteCount()];
ByteBuffer bb = ByteBuffer.wrap(b);
lutBMP.copyPixelsToBuffer(bb);
JniBridge.renderLutTextureLoad(mRenderLut, b, lutBMP.getWidth(), lutBMP.getHeight(), lutBMP.getWidth());
Log.i("cjztest", "lut pixels size:" + lutBMP.getByteCount());
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevX = event.getX();
mPrevY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
mdx += (float) (event.getX() - mPrevX) / getWidth();
mdy -= (float) (event.getY() - mPrevY) / getHeight();
setTrans(mdx, mdy);
mPrevX = event.getX();
mPrevY = event.getY();
break;
}
return true;
}
private class Renderer implements GLSurfaceView.Renderer {
private int mWidth;
private int mHeight;
private int mVideoWidth;
private int mVideoHeight;
private boolean mIsFirstFrame = true;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.i("cjztest", String.format("NativeGlSurfaceView.onSurfaceCreated"));
mWidth = 0;
mHeight = 0;
mVideoWidth = 0;
mVideoHeight = 0;
mIsFirstFrame = true;
//创建一个OES纹理和相关配套对象
if (mDataInputSurface == null) {
//创建OES纹理
mDataInputTexturesPointer = new int[1];
GLES30.glGenTextures(1, mDataInputTexturesPointer, 0);
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mDataInputTexturesPointer[0]);
//设置放大缩小。设置边缘测量
GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
mInputDataSurfaceTexture = new SurfaceTexture(mDataInputTexturesPointer[0]);
mDataInputSurface = new Surface(mInputDataSurfaceTexture);
}
//创建一个demo播放器
if (mDemoPlayer == null) {
mDemoPlayer = new Player(getContext(), getSurface(), new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
/**设置OES图层内容得大小**/
if ((width != mVideoWidth || height != mVideoHeight) && width > 0 && height > 0) {
Log.i("cjztest", String.format("onSurfaceChanged: w:%d, h:%d", width, height));
mVideoWidth = width;
mVideoHeight = height;
}
}
});
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
if ((width != mWidth || height != mHeight) && width > 0 && height > 0) {
this.mWidth = width;
this.mHeight = height;
Log.i("cjztest", String.format("NativeGlSurfaceView.onSurfaceChanged:width:%d, height:%d", mWidth, mHeight));
JniBridge.nativeGLInit(width, height);
mIsFirstFrame = true;
}
}
@Override
public void onDrawFrame(GL10 gl) {
if (mIsFirstFrame) { //不能异步进行gl操作,所以只能移到第一帧(或glrender的各种回调中,但这里需要等待onVideoSizeChanged准备好)进行图层创建
if (mVideoWidth > 0 && mVideoHeight > 0) {
//清除上次用过的图层
if (mLayer != Long.MIN_VALUE) {
JniBridge.removeLayer(mLayer);
}
//创建一个图层(由于这个使用场景种没有数组数据,只有OES纹理,所以dataPointer为0)
mLayer = JniBridge.addFullContainerLayer(mDataInputTexturesPointer[0], new int[]{mVideoWidth, mVideoHeight}, 0, new int[]{0, 0}, GLES30.GL_RGBA); //依次传入纹理、纹理的宽高、数据地址(如果有)、数据的宽高
//添加一个oes渲染器
mRenderOES = JniBridge.makeRender(JniBridge.RENDER_PROGRAM_KIND.RENDER_OES_TEXTURE.ordinal()); //添加oes纹理
// mRenderConvolutionDemo = JniBridge.addRenderForLayer(mLayer, JniBridge.RENDER_PROGRAM_KIND.RENDER_CONVOLUTION.ordinal()); //添加卷积图像处理demo
mRenderNoiseReduction = JniBridge.makeRender(JniBridge.RENDER_PROGRAM_KIND.NOISE_REDUCTION.ordinal()); //添加降噪渲染器
mRenderLut = JniBridge.makeRender(JniBridge.RENDER_PROGRAM_KIND.RENDER_LUT.ordinal()); //添加Lut渲染器
mRenderDeBackground = JniBridge.makeRender(JniBridge.RENDER_PROGRAM_KIND.DE_BACKGROUND.ordinal()); //创建背景去除渲染程序
JniBridge.addRenderToLayer(mLayer, mRenderOES);
JniBridge.addRenderToLayer(mLayer, mRenderNoiseReduction);
mIsFirstFrame = false;
}
}
mInputDataSurfaceTexture.updateTexImage();
JniBridge.renderLayer(0, mWidth, mHeight);
}
}
}
MediaPlayer实例的创建代码如下:
package com.opengldecoder.jnibridge;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.view.Surface;
import java.io.IOException;
public class Player {
private MediaPlayer mMediaPlayer;
public Player(Context context, Surface surface, MediaPlayer.OnVideoSizeChangedListener sizeChangedListener) {
initMediaPlayer(context, surface, sizeChangedListener);
}
private void initMediaPlayer(Context context, Surface surface, MediaPlayer.OnVideoSizeChangedListener sizeChangedListener) {
mMediaPlayer = new MediaPlayer();
try {
AssetFileDescriptor afd = context.getAssets().openFd("car_race.mp4");
mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
// String path = "http://192.168.1.254:8192";
// mediaPlayer.setDataSource(path);
// mediaPlayer.setDataSource(TextureViewMediaActivity.videoPath);
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setLooping(true);
mMediaPlayer.setOnVideoSizeChangedListener(sizeChangedListener);
mMediaPlayer.setSurface(surface);
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
mediaPlayer.start();
}
});
}
}
1、利用OES_TEXTURE在JNI中进行纹理渲染与处理:
此时无论在JNI中和GLSurfaceview中,其实使用的都是同一个进程空间、同一个EGLContext,因此纹理索引都是共用的,指代的都是同样的东西。因此在onDrawFrame中,我把之前创建的OES传递到底下的JNI方法,在C中可以直接使用:
mLayer = JniBridge.addFullContainerLayer(mDataInputTexturesPointer[0], new int[]{mVideoWidth, mVideoHeight}, 0, new int[]{0, 0}, GLES30.GL_RGBA); //依次传入纹理、纹理的宽高、数据地址(如果有)、数据的宽高
这就是我编写的JNI桥接文件:
package com.opengldecoder.jnibridge;
import android.graphics.Bitmap;
import android.view.Surface;
public class JniBridge {
static {
System.loadLibrary("opengl_decoder");
}
/**渲染器类型枚举器 todo java要调用,则也要抄一份**/
public enum RENDER_PROGRAM_KIND {
RENDER_OES_TEXTURE, //OES纹理渲染
RENDER_YUV, //YUV数据或纹理渲染
RENDER_CONVOLUTION, //添加卷积处理
NOISE_REDUCTION, //添加噪声处理
RENDER_LUT, //添加滤镜处理渲染器
DE_BACKGROUND, //去除背景
}
public static native void nativeGLInit(int viewPortWidth, int viewPortHeight);
// public static native void drawRGBABitmap(Bitmap bmp, int bmpW, int bmpH);
public static native void drawToSurface(Surface surface, int color);
public static native void drawBuffer();
public static native long addFullContainerLayer(int texturePointer, int textureWidthAndHeight[], long dataPointer,
int dataWidthAndHeight[],
int dataPixelFormat);
public static native void removeLayer(long layerPointer);
/**创建渲染器
@param renderProgramKind 渲染器类型,参考RENDER_PROGRAM_KIND**/
public static native long makeRender(int renderProgramKind);
public static native void addRenderToLayer(long layerPointer, long renderPointer);
public static native void removeRenderForLayer(long layerPointer, long renderPointer);
public static native void setRenderAlpha(long renderPointer, float alpha);
/**渲染器亮度调整**/
public static native void setBrightness(long renderPointer, float brightness);
/**渲染器对比度调整**/
public static native void setContrast(long renderPointer, float contrast);
/**白平衡调整**/
public static native void setWhiteBalance(long renderPointer, float redWeight, float greenWeight, float blueWeight);
public static native void renderLayer(int fboPointer, int fboWidth, int fboHeight);
public static native void layerScale(long layerPointer, float scaleX, float scaleY);
public static native void layerTranslate(long layerPointer, float dx, float dy);
public static native void layerRotate(long layerPointer, float angle);
/******************************************特定图层非通用功能设置区*************************************************/
public static native void renderLutTextureLoad(long lutRenderPointer, byte lutPixels[], int w, int h, int unitLen);
}
在这个demo中我已经实验了多种渲染器,但由于输入是一个OES纹理,因此首先使用OES渲染器:
mRenderOES = JniBridge.makeRender(JniBridge.RENDER_PROGRAM_KIND.RENDER_OES_TEXTURE.ordinal()); //添加oes纹理
JniBridge.addRenderToLayer(mLayer, mRenderOES);
可以看到,此时会调用我写的方法makeRender,根据传入的枚举参数创建一个OES纹理渲染器对象实例,并返回C下的对象指针(指针本质就是一个内存地址数)。具体代码如下:
/**创建渲染器
@param layerPointer 图层的内存地址
@@param renderProgramKind 渲染器类型**/
JNIEXPORT jlong JNICALL
Java_com_opengldecoder_jnibridge_JniBridge_makeRender(JNIEnv *env, jobject activity,
int renderProgramKind) {
RenderProgram *resultProgram = nullptr;
switch (renderProgramKind) {
default:
break;
//创建OES纹理渲染器
case RENDER_OES_TEXTURE: {
RenderProgramOESTexture *renderProgramOesTexture = new RenderProgramOESTexture();
renderProgramOesTexture->createRender(-1, -mRatio, 0, 2,
mRatio * 2,
mWidth,
mHeight);
resultProgram = renderProgramOesTexture;
break;
}
case NOISE_REDUCTION: {
RenderProgramNoiseReduction *renderProgramNoiseReduction = new RenderProgramNoiseReduction();
renderProgramNoiseReduction->createRender(-1, -mRatio, 0, 2,
mRatio * 2,
mWidth,
mHeight);
resultProgram = renderProgramNoiseReduction;
break;
}
case RENDER_YUV: {
//todo 暂时未完成
break;
}
//创建卷积渲染器
case RENDER_CONVOLUTION: {
float kernel[] = {
1.0, 1.0, 1.0,
1.0, -7.0, 1.0,
1.0, 1.0, 1.0
};
RenderProgramConvolution *renderProgramConvolution = new RenderProgramConvolution(
kernel);
renderProgramConvolution->createRender(-1, -mRatio, 0, 2,
mRatio * 2,
mWidth,
mHeight);
resultProgram = renderProgramConvolution;
break;
}
//创建滤镜渲染器:
case RENDER_LUT: {
RenderProgramFilter *renderProgramFilter = new RenderProgramFilter();
renderProgramFilter->createRender(-1, -mRatio, 0, 2,
mRatio * 2,
mWidth,
mHeight);
resultProgram = renderProgramFilter;
break;
}
//tddo 去除背景:
case DE_BACKGROUND: {
RenderProgramDebackground *renderProgramDebackground = new RenderProgramDebackground();
renderProgramDebackground->createRender(-1, -mRatio, 0, 2,
mRatio * 2,
mWidth,
mHeight);
resultProgram = renderProgramDebackground;
}
}
return (jlong) resultProgram;
}
我们暂时只关注创建RENDER_OES_TEXTURE类型的渲染器的过程即可,具体代码如下:
//
// Created by jiezhuchen on 2021/6/21.
//
#include
#include
#include
#include
#include "RenderProgramOESTexture.h"
#include "android/log.h"
using namespace OPENGL_VIDEO_RENDERER;
static const char *TAG = "nativeGL";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO, TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)
RenderProgramOESTexture::RenderProgramOESTexture() {
vertShader = GL_SHADER_STRING(
\n
uniform mat4 uMVPMatrix; //旋转平移缩放 总变换矩阵。物体矩阵乘以它即可产生变换
attribute vec3 objectPosition; //物体位置向量,参与运算但不输出给片源
attribute vec4 objectColor; //物理颜色向量
attribute vec2 vTexCoord; //纹理内坐标
varying vec4 fragObjectColor;//输出处理后的颜色值给片元程序
varying vec2 fragVTexCoord;//输出处理后的纹理内坐标给片元程序
void main() {
vec2 temp = vec2(1.0, 1.0);
gl_Position = uMVPMatrix * vec4(objectPosition, 1.0); //设置物体位置
fragVTexCoord = vTexCoord; //默认无任何处理,直接输出物理内采样坐标
fragObjectColor = objectColor; //默认无任何处理,输出颜色值到片源
}
);
fragShader = GL_SHADER_STRING(
$#extension GL_OES_EGL_image_external : require\n
precision highp float;
uniform samplerExternalOES oesTexture;//OES形式的纹理输入
uniform int funChoice;
uniform float frame;//第几帧
uniform float brightness;//亮度
uniform float contrast;//对比度
uniform vec3 rgbWeight; //白平衡
uniform vec2 resolution;//容器的分辨率
uniform vec2 videoResolution;//视频自身的分辨率
varying vec4 fragObjectColor;//接收vertShader处理后的颜色值给片元程序
varying vec2 fragVTexCoord;//接收vertShader处理后的纹理内坐标给片元程序
float fakeRandom(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123 * frame / 1000.0);
}
//添加噪声进行测试
vec3 getNoise(vec2 st) {
float rnd = fakeRandom(st);
return vec3(rnd);
}
void main() {
vec2 xy = vec2(fragVTexCoord.s, 1.0 - fragVTexCoord.t);
vec3 rgbWithBrightness = texture2D(oesTexture, xy).rgb * rgbWeight + brightness; //亮度调节
vec3 rgbWithContrast = rgbWithBrightness + (rgbWithBrightness - 0.5) * contrast / 1.0; //对比度调整 参考https://blog.csdn.net/yuhengyue/article/details/103856476
gl_FragColor = vec4(rgbWithContrast, fragObjectColor.a);
//cjztest 噪声测试
// gl_FragColor = vec4(getNoise(fragVTexCoord) + rgbWithContrast.rgb, 1.0);
}
);
float tempTexCoord[] = //纹理内采样坐标,类似于canvas坐标 //这东西有问题,导致两个framebuffer的画面互相取纹理时互为颠倒
{
1.0, 0.0,
0.0, 0.0,
1.0, 1.0,
0.0, 1.0
};
memcpy(mTexCoor, tempTexCoord, sizeof(tempTexCoord));
float tempColorBuf[] = {
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0
};
memcpy(mColorBuf, tempColorBuf, sizeof(tempColorBuf));
}
RenderProgramOESTexture::~RenderProgramOESTexture() {
destroy();
}
void RenderProgramOESTexture::createRender(float x, float y, float z, float w, float h, int windowW,
int windowH) {
mWindowW = windowW;
mWindowH = windowH;
initObjMatrix(); //使物体矩阵初始化为单位矩阵,否则接下来的矩阵操作因为都是乘以0而无效
float vertxData[] = {
x + w, y, z,
x, y, z,
x + w, y + h, z,
x, y + h, z,
};
memcpy(mVertxData, vertxData, sizeof(vertxData));
mImageProgram = createProgram(vertShader + 1, fragShader + 1);
//获取程序中顶点位置属性引用"指针"
mObjectPositionPointer = glGetAttribLocation(mImageProgram.programHandle, "objectPosition");
//纹理采样坐标
mVTexCoordPointer = glGetAttribLocation(mImageProgram.programHandle, "vTexCoord");
//获取程序中顶点颜色属性引用"指针"
mObjectVertColorArrayPointer = glGetAttribLocation(mImageProgram.programHandle, "objectColor");
//获取程序中总变换矩阵引用"指针"
muMVPMatrixPointer = glGetUniformLocation(mImageProgram.programHandle, "uMVPMatrix");
//渲染方式选择,0为线条,1为纹理
mGLFunChoicePointer = glGetUniformLocation(mImageProgram.programHandle, "funChoice");
//渲染帧计数指针
mFrameCountPointer = glGetUniformLocation(mImageProgram.programHandle, "frame");
//亮度指针
mBrightnessPointer = glGetUniformLocation(mImageProgram.programHandle, "brightness");
//对比度指针
mContrastPointer = glGetUniformLocation(mImageProgram.programHandle, "contrast");
//白平衡指针
mRGBWeightPointer = glGetUniformLocation(mImageProgram.programHandle, "rgbWeight");
//设置分辨率指针,告诉gl脚本现在的分辨率
mResoulutionPointer = glGetUniformLocation(mImageProgram.programHandle, "resolution");
}
void RenderProgramOESTexture::setAlpha(float alpha) {
if (mColorBuf != nullptr) {
for (int i = 3; i < sizeof(mColorBuf) / sizeof(float); i += 4) {
mColorBuf[i] = alpha;
}
}
}
void RenderProgramOESTexture::setBrightness(float brightness) {
mBrightness = brightness;
}
void RenderProgramOESTexture::setContrast(float contrast) {
mContrast = contrast;
}
void RenderProgramOESTexture::setWhiteBalance(float redWeight, float greenWeight, float blueWeight) {
mRedWeight = redWeight;
mGreenWeight = greenWeight;
mBlueWeight = blueWeight;
}
void RenderProgramOESTexture::loadData(char *data, int width, int height, int pixelFormat, int offset) {
//不用实现
}
/**@param texturePointers 传入需要渲染处理的纹理,可以为上一次处理的结果,例如处理完后的FBOTexture **/
void RenderProgramOESTexture::loadTexture(Textures textures[]) {
mInputTexturesArray = textures[0].texturePointers;
mInputTextureWidth = textures[0].width;
mInputTextureHeight = textures[0].height;
}
/**@param outputFBOPointer 绘制到哪个framebuffer,系统默认一般为0 **/
void RenderProgramOESTexture::drawTo(float *cameraMatrix, float *projMatrix, DrawType drawType, int outputFBOPointer, int fboW, int fboH) {
if (mIsDestroyed) {
return;
}
glUseProgram(mImageProgram.programHandle);
glUniform1f(mBrightnessPointer, mBrightness);
glUniform1f(mContrastPointer, mContrast);
float whiteBalanceWeight[3] = {mRedWeight, mGreenWeight, mBlueWeight};
glUniform3fv(mRGBWeightPointer, 1, whiteBalanceWeight);
//设置视窗大小及位置
glBindFramebuffer(GL_FRAMEBUFFER, outputFBOPointer);
glViewport(0, 0, mWindowW, mWindowH);
glUniform1i(mGLFunChoicePointer, 1);
glUniform1f(mFrameCountPointer, mframeCount++);
//传入位置信息
locationTrans(cameraMatrix, projMatrix, muMVPMatrixPointer);
//开始渲染:
if (mVertxData != nullptr && mColorBuf != nullptr) {
//将顶点位置数据送入渲染管线
glVertexAttribPointer(mObjectPositionPointer, 3, GL_FLOAT, false, 0, mVertxData); //三维向量,size为2
//将顶点颜色数据送入渲染管线
glVertexAttribPointer(mObjectVertColorArrayPointer, 4, GL_FLOAT, false, 0, mColorBuf);
//将顶点纹理坐标数据传送进渲染管线
glVertexAttribPointer(mVTexCoordPointer, 2, GL_FLOAT, false, 0, mTexCoor); //二维向量,size为2
glEnableVertexAttribArray(mObjectPositionPointer); //启用顶点属性
glEnableVertexAttribArray(mObjectVertColorArrayPointer); //启用颜色属性
glEnableVertexAttribArray(mVTexCoordPointer); //启用纹理采样定位坐标
float resolution[2];
switch (drawType) {
case OPENGL_VIDEO_RENDERER::RenderProgram::DRAW_DATA:
break;
case OPENGL_VIDEO_RENDERER::RenderProgram::DRAW_TEXTURE:
glActiveTexture(GL_TEXTURE0); //激活0号纹理
// glBindTexture(36197, mInputTexturesArrayPointer); //0号纹理绑定内容
glBindTexture(GL_TEXTURE_2D, mInputTexturesArray); //0号纹理绑定内容,发现使用GL_TEXTURE_2D也可以绑定OES纹理
glUniform1i(glGetUniformLocation(mImageProgram.programHandle, "oesTexture"), 0); //映射到渲染脚本,获取纹理属性的指针
resolution[0] = (float) mInputTextureWidth;
resolution[1] = (float) mInputTextureHeight;
glUniform2fv(mResoulutionPointer, 1, resolution);
break;
}
glDrawArrays(GL_TRIANGLE_STRIP, 0, /*mPointBufferPos / 3*/ 4); //绘制线条,添加的point浮点数/3才是坐标数(因为一个坐标由x,y,z3个float构成,不能直接用)
glDisableVertexAttribArray(mObjectPositionPointer);
glDisableVertexAttribArray(mObjectVertColorArrayPointer);
glDisableVertexAttribArray(mVTexCoordPointer);
}
}
void RenderProgramOESTexture::destroy() {
if (!mIsDestroyed) {
//释放纹理所占用的显存
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, 0, 0, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, nullptr);
//删除不用的shaderprogram
destroyProgram(mImageProgram);
}
mIsDestroyed = true;
}
其中最重点的部分就是片元渲染器的main方法:
void main() {
vec2 xy = vec2(fragVTexCoord.s, 1.0 - fragVTexCoord.t);
vec3 rgbWithBrightness = texture2D(oesTexture, xy).rgb * rgbWeight + brightness; //亮度调节
vec3 rgbWithContrast = rgbWithBrightness + (rgbWithBrightness - 0.5) * contrast / 1.0; //对比度调整 参考https://blog.csdn.net/yuhengyue/article/details/103856476
gl_FragColor = vec4(rgbWithContrast, fragObjectColor.a);
//cjztest 噪声测试
// gl_FragColor = vec4(getNoise(fragVTexCoord) + rgbWithContrast.rgb, 1.0);
}
其目的就是为了把传入的纹理索引,通过glBindTexture绑定纹理到glGetUniformLocation取得编译后的shaderProgram中oesTexture纹理的索引,然后片元渲染器就可以通过texture2D方法(GLES20的2d纹理采样函数)把从播放器中传入的纹理,进行采样,即可得到类型为vec4(rgba)的像素数据。此时如果直接赋予给保留变量gl_FragColor,此时对应的顶点封闭图形即可被贴上视频画面的原色。但我这里额外添加了rgbWeight白平衡矩阵、rgbWithBrightness亮度系数和rgbWithContrast系数,使得播放画面的红绿蓝3通道的颜色比例、亮度、对比度均可调。
代码地址:
learnOpengl: 我的OpenGL联系库 - Gitee.comhttps://gitee.com/cjzcjl/learnOpenGLDemo/tree/main/app/src/main
小结:
在安卓的音频和视频处理上,如果单纯使用surfaceview/textureView + surfaceTexture + surface这套组合去进行视频播放,实际上很难进行复杂的变化和调整。但通过glsurfaceview和glsl,上可以贴3D模型,下可以进行通常播放器都有的白平衡调节等功能,使得画面处理的灵活性大大增加,同时由于OpenGL基于GPU的硬件加速特性,处理性能比常规通过Java或者C直接逐像素地依赖CPU处理要快得多,同时能耗也较少,glsurfaceView+glsl处理视频画面是非常不错的搭配。