Android使用OpenGL和MediaCodec录制

news2025/4/7 16:27:35

目录

一,什么是opengl

二,什么是Android OpenGL ES

三, OpenGL 绘制流程

四, OpenGL坐标系

五, OpenGL 着色器

六, GLSL编程语言

七,使用MediaCodec录制在Opengl中渲染架构

八,代码实现

8.1 自定义渲染view继承GLSurfaceView

8.2 自定义渲染器TigerRender

8.3 创建编码器MediaRecorder

8.4 配置egl环境

8.5 配置shader


一,什么是opengl

Open Graphics Library     图形领域的工业标准,是一套跨编程语言、跨平台的、专业的图形编程(软件)接口。它用于二维、三维图像,是一个功能强大,调用方便的底层图形库。     与硬件无关。可以在不同的平台如Windows、Linux、Mac、Android、IOS之间进行移植。因此,支持OpenGL的软件具有很好的移植性,可以获得非常广泛的应用。

OpenGL ES 1.0 和 1.1 :Android 1.0和更高的版本支持这个API规范。 OpenGL ES 2.0 :Android 2.2(API 8)和更高的版本支持这个API规范。 OpenGL ES 3.0 :Android 4.3(API 18)和更高的版本支持这个API规范。 OpenGL ES 3.1 : Android 5.0(API 21)和更高的版本支持这个API规范

还须要由设备制造商提供了实现支持 目前广泛支持的是2.0 <uses-feature android:glEsVersion="0x00020000" android:required="true"/>

二,什么是Android OpenGL ES

针对手机、PDA和游戏主机等嵌入式设备而设计的OpenGL API 子集。

GLSurfaceView

继承至SurfaceView,它内嵌的surface专门负责OpenGL渲染。     管理Surface与EGL     允许自定义渲染器(render)。     让渲染器在独立的线程里运作,和UI线程分离。      支持按需渲染(on-demand)和连续渲染(continuous)。

OpenGL是一个跨平台的操作GPU的API,但OpenGL需要本地视窗系统进行交互,这就需要一个中间控制层, EGL就是连接OpenGL ES和本地窗口系统的接口,引入EGL就是为了屏蔽不同平台上的区别。

三, OpenGL 绘制流程

其实就是一个可编程管线pipline

渲染管线做的事情就是让计算机完成图形功能。 固定管线:程序员只能设置参数。比如 f(x)=axx + bx + c程序员只能设置a,b,c的值,却不能修改这个公式。 可编程管线:程序员掌控一切。

四, OpenGL坐标系

五, OpenGL 着色器

着色器(Shader)是运行在GPU上的小程序。顶点着色器(vertex shader)  如何处理顶点、法线等数据的小程序。片元着色器(fragment shader)     如何处理光、阴影、遮挡、环境等等对物体表面的影响,最终生成一副图像的小程序

六, GLSL编程语言

七,使用MediaCodec录制在Opengl中渲染架构

八,代码实现

8.1 自定义渲染view继承GLSurfaceView

public class TigerView extends GLSurfaceView {

    private TigerRender mTigerRender;
    //默认正常速度
    private Speed mSpeed = Speed.MODE_NORMAL;

    public void setSpeed(Speed speed) {
        mSpeed = speed;
    }

    public enum Speed {
        MODE_EXTRA_SLOW, MODE_SLOW, MODE_NORMAL, MODE_FAST, MODE_EXTRA_FAST
    }

    public TigerView(Context context) {
        super(context);
    }

    public TigerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        /**
         * 设置egl版本
         */
        setEGLContextClientVersion(2);
        /**
         * 设置渲染器
         */
        mTigerRender = new TigerRender(this);
        setRenderer(mTigerRender);

        /**
         * 设置按需渲染,当我们调用requestRender()的时候就会调用GlThread回调一次onDrawFrame()
         */
        setRenderMode(RENDERMODE_WHEN_DIRTY);

    }

    public void startRecord() {
        float speed = 1.f;
        switch (mSpeed) {
            case MODE_EXTRA_SLOW:
                speed = 0.3f;
                break;
            case MODE_SLOW:
                speed = 0.5f;
                break;
            case MODE_NORMAL:
                speed = 1.f;
                break;
            case MODE_FAST:
                speed = 1.5f;
                break;
            case MODE_EXTRA_FAST:
                speed = 3.f;
                break;
        }
        mTigerRender.startRecord(speed);
    }

    public void stopRecord() {
        mTigerRender.stopRecord();
    }
}

8.2 自定义渲染器TigerRender

public class TigerRender implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
    private final TigerView mView;
    private CameraHelper mCameraHelper;
    private SurfaceTexture mSurfaceTexture;
    float[] mtx = new float[16];
    private ScreeFilter mScreeFilter;
    private int[] mTextures;
    private CameraFilter mCameraFilter;
    private MediaRecorder mMediaRecorder;

    public TigerRender(TigerView tigerView) {
        mView = tigerView;
    }

    /**
     * 画布创建好了
     *
     * @param gl     the GL interface. Use <code>instanceof</code> to
     *               test if the interface supports GL11 or higher interfaces.
     * @param config the EGLConfig of the created surface. Can be used
     *               to create matching pbuffers.
     */
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK);
        //准备好摄像头绘制的画布
        //通过open gl创建一个纹理id
        mTextures = new int[1];
        GLES20.glGenTextures(mTextures.length, mTextures, 0);
        mSurfaceTexture = new SurfaceTexture(mTextures[0]);
        //设置有一帧新的数据到来的时候,回调监听
        mSurfaceTexture.setOnFrameAvailableListener(this);
        //必须要在GlThread里面创建着色器程序
        mCameraFilter = new CameraFilter(mView.getContext());
        mScreeFilter = new ScreeFilter(mView.getContext());
        EGLContext eglContext = EGL14.eglGetCurrentContext();
        mMediaRecorder = new MediaRecorder(mView.getContext(), "/mnt/sdcard/test.mp4", CameraHelper.HEIGHT, CameraHelper.WIDTH, eglContext);
    }

    /**
     * 画布发生改变
     *
     * @param gl     the GL interface. Use <code>instanceof</code> to
     *               test if the interface supports GL11 or higher interfaces.
     * @param width
     * @param height
     */
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        mCameraHelper.startPreview(mSurfaceTexture);
        mCameraFilter.onReady(width, height);
        mScreeFilter.onReady(width, height);
    }

    /**
     * 画画
     *
     * @param gl the GL interface. Use <code>instanceof</code> to
     *           test if the interface supports GL11 or higher interfaces.
     */
    @Override
    public void onDrawFrame(GL10 gl) {
        //告诉open gl需要把屏幕清理成 什么样子的颜色
        GLES20.glClearColor(0, 0, 0, 0);
        //开始真正的屏幕颜色清理,也就是上一次设置的屏幕颜色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        //把摄像头采集的数据输出出来
        //更新纹理,然后我们才可以使用opengl从SurfaceTexure当中获取数据,进行渲染
        mSurfaceTexture.updateTexImage();

        //mSurfaceTexture比较特殊,在设置坐标的时候,需要一个变换矩阵,使用的是特殊的采样器samplerExternalOES
        //这种采样器,正常的是sample2D

        mSurfaceTexture.getTransformMatrix(mtx);
        mCameraFilter.setMatrix(mtx);
        int id = mCameraFilter.onDrawFrame(mTextures[0]);
        //在这里添加各种效果,相当于责任链
        //开始画画
        mScreeFilter.onDrawFrame(id);

        mMediaRecorder.encodeFrame(id, mSurfaceTexture.getTimestamp());
    }

    /**
     * SurfaceTexture有一个新的有效的图片的时候会被回调,此时可以把这个数据回调给GLSurfaceView的onDrawFrame
     *
     * @param surfaceTexture
     */
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        if (mView != null) {
            //开始渲染,有一帧新的图像,就开始调用GLSurfaceView的onDrawFrame进行绘制
            mView.requestRender();
        }
    }

    public void startRecord(float speed) {
        try {
            mMediaRecorder.start(speed);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void stopRecord() {
        mMediaRecorder.stop();
    }
}

8.3 创建编码器MediaRecorder

/**
 * 视频录制
 */
public class MediaRecorder {
    private final Context mContext;
    private final String mPath;
    private final int mWidth;
    private final int mHeight;
    private final EGLContext mEglContext;
    private MediaCodec mMediaCodec;
    private Surface mInputSurface;
    private MediaMuxer mMediaMuxer;
    private Handler mHandler;
    private EGLBase mEglBase;
    private boolean isStart;
    private int index;
    private float mSpeed;

    /**
     * @param context
     * @param path    视频保存地址
     * @param width   视频宽
     * @param height  视频高
     */
    public MediaRecorder(Context context, String path, int width, int height, EGLContext eglContext) {
        mContext = context.getApplicationContext();
        mPath = path;
        mWidth = width;
        mHeight = height;
        mEglContext = eglContext;
    }

    /**
     * 开始录制视频
     */
    public void start(float speed) throws IOException {
        /**
         * 配置MediaCodec编码器,视频编码的宽,高,帧率,码率
         * 录制成mp4格式,视频编码格式是h264 MIMETYPE_VIDEO_AVC 高级编码
         */
        mSpeed = speed;
        MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);
        //配置码率 1500kbs
        videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);
        //帧率
        videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);
        //关键字间隔
        videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 20);

        //创建视频高级编码器
        mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
        //因为是从Surface中读取的,所以不需要设置这个颜色格式
        videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        //配置编码器,CONFIGURE_FLAG_ENCODE,
        mMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //交给虚拟屏幕,通过opengl 将预览的纹理绘制到这一个虚拟屏幕中,这样子Mediacodc 就会自动编码这一帧图像
        mInputSurface = mMediaCodec.createInputSurface();

        //mp4 播放流程  解复用--》解码 》绘制

        //mp4 编码流程  封装器--》编码

        //MUXER_OUTPUT_MPEG_4 MP4格式封装器,将h.264通过他写出到文件就可以了
        mMediaMuxer = new MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        /**
         * 配置EGL环境,也就是配置我们的虚拟屏幕环境
         */

        HandlerThread handlerThread = new HandlerThread("ViewoCodec");
        handlerThread.start();
        Looper looper = handlerThread.getLooper();

        //子线程和子线程之间的通信
        mHandler = new Handler(looper);

        //EGl的绑定线程,对我们自己创建的EGl环境,都是在这个线程里面进行
        mHandler.post(() -> {
            //创建我们的EGL环境(虚拟设备,EGL上下文 )
            mEglBase = new EGLBase(mContext, mWidth, mHeight, mInputSurface, mEglContext);
            //启动编码器
            mMediaCodec.start();
            isStart = true;
        });

    }


    /**
     * textureId 纹理id
     * 调用一次,就有一个新的图片需要编码
     */
    public void encodeFrame(int textureId, long timesnap) {
        if (!isStart) {
            return;
        }

        //切换到子线程中编码
        mHandler.post(() -> {
            //把图像纹理画到虚拟屏幕里面
            mEglBase.draw(textureId, timesnap);

            //此时我们需要从编码器里面的输出缓冲区获取编码以后的数据就可以了,
            getCodec(false);
        });
    }

    private void getCodec(boolean endOfStream) {
        if (endOfStream) {
            //表示停止录制,此时我们不录制了,需要给mediacoic 通知
            mMediaCodec.signalEndOfInputStream();
        }
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        //希望将已经编码完成的数据都获取到,然后写出到mp4文件中
        while (true) {
            //并不是传给mediacodec一帧数据就表示可以编码出一帧数据,有可能需要好多帧数据才可以同时编码出数据出来
            //输出缓冲区
            //传递-1表示一直等到输出缓冲区有一个编码好的有效的数据以后才会继续向下走,不然就会一直卡在127行,
            //10_000超时时间
            int status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
            if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {
                //如果是停止,就继续循环,
                // 继续循环就表示不会接收到新的等待编码的图像了
                //相当于保证mediacodic中所有待编码的数据都编码完成
                // 标记不是停止,我们退出,下一轮接收到更多的数据才来输出编码以后的数据,我们就让继续走
                // 表示需要更多数据才可以编码出图像  false是继续录制,未来还有机会在调用getCodec
                if (!endOfStream) {
                    //结束录制了
                    break;
                }
                //否则继续
            } else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                //开始编码,就会调用一次
                MediaFormat outputFormat = mMediaCodec.getOutputFormat();
                //配置封装器,增加一路指定格式的媒体流
                index = mMediaMuxer.addTrack(outputFormat);
                //启动封装器
                mMediaMuxer.start();
            } else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                //忽略
            } else {
                //成功取出一个有效的输出
                ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(status);
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    //如果获取的ByteBuffer是配置信息,那么就不需要写出到mp4文件中
                    bufferInfo.size = 0;
                }
                if (bufferInfo.size != 0) {
                    bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
                    //写出到 mp4文件中
                    //根据偏移定位去获取数据,而不是从0开始
                    outputBuffer.position(bufferInfo.offset);
                    //设置可读可写的总长度
                    outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                    mMediaMuxer.writeSampleData(index, outputBuffer, bufferInfo);
                }
                //输出缓冲区使用完毕了, 此时就可以回收了,让mediacodec继续使用
                mMediaCodec.releaseOutputBuffer(status, false);

                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    //结束了,
                    break;
                }
            }
        }
    }

    public void stop() {
        isStart = false;
        mHandler.post(() -> {
            getCodec(true);
            mMediaCodec.stop();
            mMediaCodec.release();
            mMediaCodec = null;
            mMediaMuxer.stop();
            mMediaMuxer.release();
            mMediaMuxer = null;
            mEglBase.release();
            mEglBase = null;
            mInputSurface = null;
            mHandler.getLooper().quitSafely();
            mHandler = null;
        });
    }
}

8.4 配置egl环境

/**
 * EGL配置 和录制opengl的操作
 */
public class EGLBase {
    private final EGLSurface mEglSurface;
    private final ScreeFilter mScreeFilter;
    private EGLDisplay mEglDisplay;
    private EGLConfig mEglConfig;
    private EGLContext mEGLContext;

    /**
     * @param context
     * @param width
     * @param height
     * @param surface MediaCodec创建的surface, 我们需要将这个surface贴到虚拟屏幕里面
     */
    public EGLBase(Context context, int width, int height, Surface surface, EGLContext eglContext) {
        createEGL(eglContext);
        //把surface贴到EGLDisplay 虚拟屏幕里面
        int[] attrib_list = {
                //不需要配置什么属性
                EGL14.EGL_NONE};

        //就是向mEglDisplay这个虚拟屏幕上面画画
        mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);
        //必须要绑定当前线程的显示上下文,不然就绘制不上去,这样子之后操作的opelgl就是在这个虚拟屏幕上操作,读和写都是在同一个surface里面
        if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEGLContext)) {
            throw new RuntimeException("eglMakeCurrent failed");
        }

        //向虚拟屏幕画画
        mScreeFilter = new ScreeFilter(context);
        mScreeFilter.onReady(width, height);
    }

    private void createEGL(EGLContext eglContext) {
        //创建虚拟显示器
        mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("eglGetDisplay failed");
        }
        int[] version = new int[2];
        //初始化虚拟设备
        if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
            throw new RuntimeException("eglInitialize failed");
        }

        int[] attrib_list = new int[]{//rgba 红绿蓝透明度
                EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,//和egl的版本有关系
                EGL14.EGL_NONE//这个很重要,一定要配置为NONE,表示配置结束了
        };
        EGLConfig[] configs = new EGLConfig[1];
        int[] num_config = new int[1];
        boolean eglChooseConfig = EGL14.eglChooseConfig(mEglDisplay, attrib_list, 0, configs, 0, configs.length, num_config, 0);
        if (!eglChooseConfig) {
            //如果配置失败
            throw new IllegalArgumentException("eglChooseConfig failed");
        }
        mEglConfig = configs[0];
        int[] attriblist = {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
        //创建EGL上下文
        //share_context 共享上下,传递绘制线程的(GLThread)EGL上下文,达到共享资源的目的
        mEGLContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, attriblist, 0);
        if (mEGLContext == null || mEGLContext == EGL14.EGL_NO_CONTEXT) {
            mEGLContext = null;
            throw new RuntimeException("createContex error !");
        }
    }

    /**
     * @param textureId 纹理id,代表一张图片
     * @param timesnap  时间戳
     */
    public void draw(int textureId, long timesnap) {
        //必须要绑定当前线程的显示上下文,不然就绘制不上去,这样子之后操作的opelgl就是在这个虚拟屏幕上操作,读和写都是在同一个surface里面
        //画画之前也必须要绑定
        if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEGLContext)) {
            throw new RuntimeException("eglMakeCurrent failed");
        }
        //向虚拟屏幕画画
        mScreeFilter.onDrawFrame(textureId);

        //刷新eglSurface时间戳
        EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, timesnap);

        //交换数据,EGL工作模式,双缓存模式,内部有两个frameBuff,当EGL将一个frame显示到屏幕上以后,
        // 另一个frame就在后台等待opengl进行交换
        //也就是画完一次,交换一次
        EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);

    }


    public void release() {
        EGL14.eglDestroySurface(mEglDisplay, mEglSurface);
        EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
        EGL14.eglDestroyContext(mEglDisplay, mEGLContext);
        EGL14.eglReleaseThread();
        EGL14.eglTerminate(mEglDisplay);
    }
}

8.5 配置shader

public class AbstractFilter {
    protected FloatBuffer mGLVertexBuffer;
    protected FloatBuffer mGLTextureBuffer;

    //顶点着色
    protected int mVertexShaderId;
    //片段着色
    protected int mFragmentShaderId;


    protected int mGLProgramId;
    /**
     * 顶点着色器
     * attribute vec4 position;
     * 赋值给gl_Position(顶点)
     */
    protected int vPosition;
    /**
     * varying vec2 textureCoordinate;
     */
    protected int vCoord;


    /**
     * uniform mat4 vMatrix;
     */
    protected int vMatrix;

    /**
     * 片元着色器
     * Samlpe2D 扩展 samplerExternalOES
     */
    protected int vTexture;


    protected int mOutputWidth;
    protected int mOutputHeight;

    public AbstractFilter(Context context, int vertexShaderId, int fragmentShaderId) {
        this.mVertexShaderId = vertexShaderId;
        this.mFragmentShaderId = fragmentShaderId;
        // 4个点 x,y = 4*2 float 4字节 所以 4*2*4
        mGLVertexBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mGLVertexBuffer.clear();
        float[] VERTEX = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
        mGLVertexBuffer.put(VERTEX);


        mGLTextureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mGLTextureBuffer.clear();
        float[] TEXTURE = {0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f};
        mGLTextureBuffer.put(TEXTURE);


        initilize(context);
        initCoordinate();
    }


    protected void initilize(Context context) {
        String vertexSharder = OpenGLUtils.readRawTextFile(context, mVertexShaderId);
        String framentShader = OpenGLUtils.readRawTextFile(context, mFragmentShaderId);
        mGLProgramId = OpenGLUtils.loadProgram(vertexSharder, framentShader);
        // 获得着色器中的 attribute 变量 position 的索引值
        vPosition = GLES20.glGetAttribLocation(mGLProgramId, "vPosition");
        vCoord = GLES20.glGetAttribLocation(mGLProgramId, "vCoord");
        vMatrix = GLES20.glGetUniformLocation(mGLProgramId, "vMatrix");
        // 获得Uniform变量的索引值
        vTexture = GLES20.glGetUniformLocation(mGLProgramId, "vTexture");
    }


    public void onReady(int width, int height) {
        mOutputWidth = width;
        mOutputHeight = height;
    }


    public void release() {
        GLES20.glDeleteProgram(mGLProgramId);
    }


    public int onDrawFrame(int textureId) {
        //设置显示窗口
        GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight);

        //使用着色器
        GLES20.glUseProgram(mGLProgramId);

        //传递坐标
        mGLVertexBuffer.position(0);
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, mGLVertexBuffer);
        GLES20.glEnableVertexAttribArray(vPosition);

        mGLTextureBuffer.position(0);
        GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, mGLTextureBuffer);
        GLES20.glEnableVertexAttribArray(vCoord);


        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        //绑定
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
        GLES20.glUniform1i(vTexture, 0);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        //解绑
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

        return textureId;
    }

    //修改坐标
    protected void initCoordinate() {

    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2329316.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《如何避免虚无》速读笔记

文章目录 书籍信息概览躺派&#xff08;出世&#xff09;卷派&#xff08;入世&#xff09;虚无篇&#xff1a;直面虚无自我篇&#xff1a;认识自我孤独篇&#xff1a;应对孤独幸福篇&#xff1a;追寻幸福超越篇&#xff1a;超越自我 书籍信息 书名&#xff1a;《如何避免虚无…

哈尔滨工业大学:大模型时代的具身智能

大家好&#xff0c;我是樱木。 机器人在工业领域&#xff0c;已经逐渐成熟。具身容易&#xff0c;智能难。 机器人-》智能机器人&#xff0c;需要自主能力&#xff0c;加上通用能力。 智能机器人-》人类&#xff0c;这个阶段就太有想象空间了。而最受关注的-类人机器人。 如何…

理解OSPF 特殊区域NSSA和各类LSA特点

本文基于上文 理解OSPF Stub区域和各类LSA特点 在理解了Stub区域之后&#xff0c;我们再来理解一下NSSA区域&#xff0c;NSSA区域用于需要引入少量外部路由&#xff0c;同时又需要保持Stub区域特性的情况 一、 网络总拓扑图 我们在R1上配置黑洞路由&#xff0c;来模拟NSSA区域…

如何通过优化HMI设计大幅提升产品竞争力?

一、HMI设计的重要性与竞争力提升 HMI&#xff08;人机交互界面&#xff09;设计在现代产品开发中扮演着至关重要的角色。良好的HMI设计不仅能够提升用户体验&#xff0c;还能显著增强产品的竞争力。在功能趋同的市场环境中&#xff0c;用户体验成为产品竞争的关键。HMI设计通…

Linux信号——信号的处理(3)

信号是什么时候被处理&#xff1f; 进程从内核态&#xff0c;切换到用户态的时候&#xff0c;信号会被检测处理。 内核态&#xff1a;操作系统的状态&#xff0c;权限级别高 用户态&#xff1a;你自己的状态 内核态和用户态 进程地址空间第三次 所谓的系统调用本质其实是一堆…

Pod的调度

在默认情况下&#xff0c;一个Pod在哪个Node节点上运行&#xff0c;是由Scheduler组件采用相应的算法计算出来的&#xff0c;这个过程是不受人工控制的。但是在实际使用中&#xff0c;这并不满足的需求&#xff0c;因为很多情况下&#xff0c;我们想控制某些Pod到达某些节点上&…

LabVIEW面向对象编程设计方法

一、概述 面向对象编程&#xff08;OOP&#xff09;在软件开发中占据重要地位&#xff0c;尤其是在大规模软件项目中。它与小型程序开发思路不同&#xff0c;更注重未来功能的升级与扩展。在设计阶段&#xff0c;需思考如何构建既灵活又稳定的系统&#xff0c;这涉及众多设计方…

Dify票据识别遇到的分支判断不准确问题

已测试这篇文章中 https://zhuanlan.zhihu.com/p/5465385787 使用多分支条件判断使用不同的大模型识别图片内容 发现了细节问题。在使用时若不注意&#xff0c;分支会出现走向不准的问题。 需要关注部分 下方红框处。1&#xff0c;2后不能跟点。否则会出问。除此之外&#xff0…

《全栈+双客户端Turnkey方案》架构设计图

今天分享一些全栈双客户端Turnkey方案的架构与结构图。 1&#xff1a;三种分布式部署方案:网关方案&#xff0c;超级服务器单服方案&#xff0c;直连逻辑服方案 2: 单服多线程核心架构: 系统服务逻辑服服务 3: 系统服务的多线程池调度设计 4:LogicServer Update与ECS架构&…

某碰瓷国赛美赛,号称第三赛事的数模竞赛

首先我非常不能理解的就是怎么好意思自称第三赛事的呢&#xff1f;下面我们进行一个简单讨论&#xff0c;当然这里不对国赛和美赛进行讨论。首先我们来明确一点&#xff0c;比赛的含金量由什么来定&#xff1f;这个可能大家的评价指标可能不唯一&#xff0c;我通过DeepSeek选取…

【大模型深度学习】如何估算大模型需要的显存

一、模型参数量 参数量的单位 参数量指的是模型中所有权重和偏置的数量总和。在大模型中&#xff0c;参数量的单位通常以“百万”&#xff08;M&#xff09;或“亿”&#xff08;B&#xff0c;也常说十亿&#xff09;来表示。 百万&#xff08;M&#xff09;&#xff1a;表示…

Mysql 数据库编程技术01

一、数据库基础 1.1 认识数据库 为什么学习数据库 瞬时数据&#xff1a;比如内存中的数据&#xff0c;是不能永久保存的。持久化数据&#xff1a;比如持久化至数据库中或者文档中&#xff0c;能够长久保存。 数据库是“按照数据结构来组织、存储和管理数据的仓库”。是一个长…

Mysql慢查询设置 和 建立索引

1 .mysql慢查询的设置 slow_query_log ON //或 slow_query_log_file /usr/local/mysql/data/slow.log long_query_time 2 修改后重启动mysql 1.1 查看设置后的参数 mysql> show variables like slow_query%; --------------------------------------------------…

【Android】界面布局-相对布局RelativeLayout-例子

题目 完成下面相对布局&#xff0c;要求&#xff1a; 中间的button在整个屏幕的中央&#xff0c;其他的以它为基准排列。Hints&#xff1a;利用layout_toEndof,_toRightof,_toLeftof,_toStartof完成。 结果演示 代码实现 <?xml version"1.0" encoding"u…

Spring Boot 中使用 Redis:从入门到实战

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

7-1 素数求和(线性筛实现)

7-1 素数求和。 分数 10 中等 全屏浏览 切换布局 作者 魏英 单位 浙江科技大学 输入两个正整数m和n&#xff08;1<m<n<500&#xff09;统计并输出m和n之间的素数个数以及这些素数的和。 输入格式: 输入两个正整数m和n&#xff08;1<m<n<500&#xff0…

ZKmall开源商城多云高可用架构方案:AWS/Azure/阿里云全栈实践

随着企业数字化转型的加速&#xff0c;云计算服务已成为IT战略中的核心部分。ZKmall开源商城作为一款高性能的开源商城系统&#xff0c;其在多云环境下的高可用架构方案备受关注。下面将结合AWS、Azure和阿里云三大主流云平台&#xff0c;探讨ZKmall的多云高可用架构全栈实践。…

leetcode二叉树刷题调试不方便的解决办法

1. 二叉树不易构建 在leetcode中刷题时&#xff0c;如果没有会员就需要将代码拷贝到本地的编译器进行调试。但是leetcode中有一类题可谓是毒瘤&#xff0c;那就是二叉树的题。 要调试二叉树有关的题需要根据测试用例给出的前序遍历&#xff0c;自己构建一个二叉树&#xff0c;…

颜色性格测试:探索你的内在性格色彩

颜色性格测试&#xff1a;探索你的内在性格色彩 在我们的日常生活中&#xff0c;颜色无处不在&#xff0c;而我们对颜色的偏好往往能反映出我们内在的性格特质。今天我要分享一个有趣的在线工具 —— 颜色性格测试&#xff0c;它能通过你最喜欢的颜色来分析你的性格倾向。 &…

CMake学习--Window下VSCode 中 CMake C++ 代码调试操作方法

目录 一、背景知识二、使用方法&#xff08;一&#xff09;安装扩展&#xff08;二&#xff09;创建 CMake 项目&#xff08;三&#xff09;编写代码&#xff08;四&#xff09;配置 CMakeLists.txt&#xff08;五&#xff09;生成构建文件&#xff08;六&#xff09;开始调试 …