FBO 滤镜叠加

一 FBO

一、FBO的核心概念

FBO(Frame Buffer Object,帧缓冲对象)是OpenGL/WebGL等图形API中用于离屏渲染的核心组件,其本质是显存中可动态管理的缓冲区容器,通过关联纹理或渲染缓冲对象(RBO)实现颜色、深度、模板缓冲区的灵活配置。

  • 核心功能​:

    1. 替代默认帧缓冲​:默认帧缓冲由窗口系统管理,仅用于屏幕输出;FBO允许开发者创建自定义缓冲区,将渲染结果输出到纹理或RBO中,实现离屏渲染。

    2. 多目标渲染(MRT)​​:支持同时绑定多个颜色附件(如GL_COLOR_ATTACHMENT0GL_COLOR_ATTACHMENT1),实现单次渲染到多个目标(如颜色、法线、深度等)。

    3. 动态资源管理​:通过挂接纹理或RBO,灵活切换渲染目标,避免显存拷贝,提升性能。

  • 关键组成​:

    • 颜色附件​:存储颜色数据,支持纹理或RBO挂接。

    • 深度/模板附件​:通常使用RBO存储深度和模板信息,格式如GL_DEPTH24_STENCIL8

    • 挂接点​:FBO提供多个逻辑挂接点(如GL_COLOR_ATTACHMENT0),用于绑定实际存储对象。


二、FBO在渲染管线中的位置

在图形渲染管线中,FBO位于光栅化与输出合并阶段之后,作为最终的渲染目标容器。具体流程如下:

  1. 顶点处理​:顶点着色器处理几何数据,生成裁剪空间坐标。

  2. 光栅化​:将几何图元转换为片段(Fragment)。

  3. 片段处理​:片段着色器计算颜色、深度等属性,执行深度/模板测试。

  4. 输出合并​:通过FBO将测试通过的片段数据写入颜色、深度、模板缓冲区​:

    • 默认管线​:写入窗口系统提供的默认帧缓冲(屏幕输出)。

    • FBO管线​:写入自定义的纹理或RBO(离屏渲染)。

  • FBO的绑定与切换​:

    • 使用glBindFramebuffer(GL_FRAMEBUFFER, fbo)绑定FBO,后续渲染操作将输出到其附件。

    • 通过glBindFramebuffer(GL_FRAMEBUFFER, 0)切换回默认帧缓冲,恢复屏幕显示。


三、FBO与渲染管线的交互细节

  1. 数据流向​:

    • 光栅化后的片段数据通过FBO的挂接点(如颜色附件纹理)存储,供后续渲染或后处理使用。

    • 例如,将场景渲染到FBO纹理后,可通过全屏四边形进行模糊处理(后处理阶段)。

  2. 性能优势​:

    • 减少状态切换​:复用FBO附件避免频繁绑定不同纹理。

    • 内存优化​:RBO直接存储深度/模板数据,无需额外内存开销。

  3. 完整性验证​:

    • 使用glCheckFramebufferStatus()检查FBO配置是否合法(如附件尺寸一致、格式匹配)。


​二、FBO 使用流程

1. 创建并绑定FBO

GLuint fbo;
glGenFramebuffers(1, &fbo);          // 生成FBO对象
glBindFramebuffer(GL_FRAMEBUFFER, fbo); // 绑定FBO
  • 作用​:创建独立的渲染目标容器,后续操作将输出到此FBO而非默认帧缓冲。


2. 创建颜色附件(纹理或RBO)​

选项1:纹理附件(推荐用于颜色输出)​
GLuint colorTex;
glGenTextures(1, &colorTex);
glBindTexture(GL_TEXTURE_2D, colorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0); // 附加到颜色附件点
  • 关键参数​:GL_RGBA格式、GL_LINEAR过滤、GL_UNSIGNED_BYTE数据类型。

选项2:RBO附件(适用于颜色缓冲区优化)​
GLuint rboColor;
glGenRenderbuffers(1, &rboColor);
glBindRenderbuffer(GL_RENDERBUFFER, rboColor);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboColor);

3. 创建深度/模板附件(RBO)​

GLuint rboDepth;
glGenRenderbuffers(1, &rboDepth);
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); // 深度+模板
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
  • 作用​:存储深度和模板信息,支持深度测试和模板测试。


4. 检查FBO完整性

if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    std::cerr << "FBO创建失败!状态码:" << glCheckFramebufferStatus(GL_FRAMEBUFFER) << std::endl;
}
  • 必要性​:确保附件配置正确(如尺寸一致、格式兼容)。


5. 渲染到FBO

glBindFramebuffer(GL_FRAMEBUFFER, fbo); // 绑定FBO
glViewport(0, 0, width, height);       // 设置视口匹配FBO尺寸
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除缓冲区

// 绘制场景(使用当前绑定的着色器和VAO)
drawScene();

glBindFramebuffer(GL_FRAMEBUFFER, 0); // 解绑,恢复默认帧缓冲
  • 视口设置​:必须与FBO附件尺寸一致,否则渲染结果可能错乱。


6. 使用FBO渲染结果

// 绑定默认帧缓冲(屏幕)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, screenWidth, screenHeight); // 恢复屏幕视口

// 使用着色器将FBO纹理绘制到屏幕
shader.use();
glBindTexture(GL_TEXTURE_2D, colorTex); // 绑定FBO颜色纹理
drawQuad(); // 绘制全屏四边形
  • 后处理​:通过屏幕着色器对FBO纹理应用模糊、HDR等效果。


7. 资源清理

glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &colorTex);
glDeleteRenderbuffers(1, &rboDepth);
  • 避免内存泄漏​:释放不再使用的OpenGL对象。


扩展:多颜色附件(MRT)​

// 创建第二个颜色附件
GLuint colorTex2;
glGenTextures(1, &colorTex2);
glBindTexture(GL_TEXTURE_2D, colorTex2);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, width, height, 0, GL_RED, GL_FLOAT, nullptr);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, colorTex2, 0);

// 指定输出到多个颜色缓冲区
GLenum drawBufs[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glDrawBuffers(2, drawBufs);
  • 应用场景​:同时输出颜色、法线、深度等信息到不同纹理。


常见问题与调试

  1. 黑屏或无输出

    • 检查FBO绑定状态和视口尺寸是否匹配。

    • 确保至少有一个颜色附件被正确绑定(GL_COLOR_ATTACHMENT0)。

  2. 深度测试失效

    • 确认深度附件已附加(GL_DEPTH_ATTACHMENTGL_DEPTH_STENCIL_ATTACHMENT)。

    • 启用深度测试:glEnable(GL_DEPTH_TEST)

  3. 纹理模糊

    • 检查纹理过滤模式(如GL_NEAREST避免插值)。

    • 确保渲染到FBO时视口与纹理尺寸一致。


完整代码示例(简化版)​

// 初始化FBO
GLuint fbo, colorTex, rboDepth;
glGenFramebuffers(1, &fbo);
glGenTextures(1, &colorTex);
glGenRenderbuffers(1, &rboDepth);

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

// 颜色附件(纹理)
glBindTexture(GL_TEXTURE_2D, colorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 800, 600, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);

// 深度附件(RBO)
glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepth);

// 检查完整性
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    // 错误处理
}

// 渲染到FBO
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, 800, 600);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawScene();

// 解绑并使用结果
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, screenWidth, screenHeight);
shader.use();
glBindTexture(GL_TEXTURE_2D, colorTex);
drawQuad();

三 相机多级滤镜

一  相机预览

        

package com.example.cameravideofbo

import android.content.Context
import android.content.pm.PackageManager
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.Surface
import java.io.IOException

class CameraManager(
    private val context: Context,
    private val surfaceTexture: SurfaceTexture
) {
    private val TAG = "CameraManager"

    // 相机相关属性
    private val cameraManager: android.hardware.camera2.CameraManager
    private var cameraDevice: CameraDevice? = null
    private var captureSession: CameraCaptureSession? = null
    private var backgroundThread: HandlerThread? = null
    private var backgroundHandler: Handler? = null
    private var cameraPreviewSurface: Surface? = null

    // 相机ID
    private var cameraId: String = ""

    // 构造函数初始化
    init {
        this.cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as android.hardware.camera2.CameraManager
    }

    fun startCameraPreview() {
        try {
            Log.d(TAG, "Starting camera preview")
            
            // 启动后台线程
            startBackgroundThread()
            
            // 获取相机ID
            cameraId = getCameraId()
            
            // 打开相机
            openCamera(cameraId)
        } catch (e: Exception) {
            Log.e(TAG, "Error starting camera preview: ${e.message}", e)
            throw IOException("Failed to start camera preview", e)
        }
    }

    // 获取相机ID(默认使用后置相机)
    private fun getCameraId(): String {
        try {
            val cameraIds = cameraManager.cameraIdList
            for (id in cameraIds) {
                val characteristics = cameraManager.getCameraCharacteristics(id)
                val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
                if (facing == CameraCharacteristics.LENS_FACING_BACK) {
                    return id
                }
            }
            // 如果没有后置相机,返回第一个相机ID
            if (cameraIds.isNotEmpty()) {
                return cameraIds[0]
            }
            throw IOException("No camera available")
        } catch (e: Exception) {
            Log.e(TAG, "Error getting camera ID: ${e.message}", e)
            throw IOException("Failed to get camera ID", e)
        }
    }

    // 打开相机
    private fun openCamera(cameraId: String) {
        try {
            Log.d(TAG, "Opening camera: $cameraId")
            
            // 检查相机权限
            if (context.checkSelfPermission(android.Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
                Log.e(TAG, "Camera permission not granted")
                throw IOException("Camera permission not granted")
            }
            
            // 配置SurfaceTexture
            surfaceTexture.setDefaultBufferSize(1280, 720) // 设置合适的预览尺寸
            cameraPreviewSurface = Surface(surfaceTexture)
            
            // 打开相机
            cameraManager.openCamera(
                cameraId,
                object : CameraDevice.StateCallback() {
                    override fun onOpened(camera: CameraDevice) {
                        Log.d(TAG, "Camera opened successfully")
                        cameraDevice = camera
                        // 创建相机预览会话
                        createCameraPreviewSession()
                    }
                    
                    override fun onDisconnected(camera: CameraDevice) {
                        Log.e(TAG, "Camera disconnected")
                        camera.close()
                        cameraDevice = null
                    }
                    
                    override fun onError(camera: CameraDevice, error: Int) {
                        Log.e(TAG, "Camera error: $error")
                        camera.close()
                        cameraDevice = null
                    }
                },
                backgroundHandler
            )
        } catch (e: Exception) {
            Log.e(TAG, "Error opening camera: ${e.message}", e)
            throw IOException("Failed to open camera", e)
        }
    }

    // 创建相机预览会话
    private fun createCameraPreviewSession() {
        try {
            val localCamera = cameraDevice ?: run {
                Log.e(TAG, "Camera device is null when creating preview session")
                return
            }
            
            // 创建预览请求构建器
            val captureRequestBuilder = localCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
            
            // 设置SurfaceTexture作为相机输出目标
            captureRequestBuilder.addTarget(cameraPreviewSurface!!)
            
            // 创建预览请求
            val captureRequest = captureRequestBuilder.build()
            
            // 创建相机捕获会话
            val localSurface = cameraPreviewSurface
            if (localSurface != null) {
                localCamera.createCaptureSession(
                    listOf(localSurface),
                    object : CameraCaptureSession.StateCallback() {
                        override fun onConfigured(session: CameraCaptureSession) {
                            if (localCamera != cameraDevice) {
                                return
                            }
                            
                            try {
                                captureSession = session
                                // 设置预览请求
                                session.setRepeatingRequest(captureRequest, null, backgroundHandler)
                                Log.d(TAG, "Camera preview session configured successfully")
                            } catch (e: CameraAccessException) {
                                Log.e(TAG, "Error setting repeating request: ${e.message}", e)
                            }
                        }
                        
                        override fun onConfigureFailed(session: CameraCaptureSession) {
                            Log.e(TAG, "Failed to configure camera capture session")
                        }
                    },
                    backgroundHandler
                )
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error creating camera preview session: ${e.message}", e)
        }
    }

    // 启动后台线程
    private fun startBackgroundThread() {
        backgroundThread = HandlerThread("CameraBackground")
        backgroundThread?.start()
        backgroundThread?.looper?.let {
            backgroundHandler = Handler(it)
        }
    }

    // 停止后台线程
    private fun stopBackgroundThread() {
        backgroundThread?.quitSafely()
        try {
            backgroundThread?.join()
            backgroundThread = null
            backgroundHandler = null
        } catch (e: InterruptedException) {
            Log.e(TAG, "Error joining background thread: ${e.message}", e)
        }
    }

    // 关闭相机
    private fun closeCamera() {
        try {
            // 停止相机预览
            captureSession?.stopRepeating()
            captureSession?.close()
            captureSession = null
            
            // 关闭相机设备
            cameraDevice?.close()
            cameraDevice = null
            
            // 释放Surface
            cameraPreviewSurface?.release()
            cameraPreviewSurface = null
            
            Log.d(TAG, "Camera closed successfully")
        } catch (e: Exception) {
            Log.e(TAG, "Error closing camera: ${e.message}", e)
        }
    }

    // 停止相机预览
    fun stopCameraPreview() {
        try {
            Log.d(TAG, "Stopping camera preview")
            closeCamera()
        } catch (e: Exception) {
            Log.e(TAG, "Error stopping camera preview: ${e.message}", e)
        }
    }

    // 释放资源
    fun release() {
        try {
            Log.d(TAG, "Releasing camera manager resources")
            
            // 停止相机预览
            closeCamera()
            
            // 停止后台线程
            stopBackgroundThread()
            
            Log.d(TAG, "Camera manager resources released successfully")
        } catch (e: Exception) {
            Log.e(TAG, "Error releasing camera manager resources: ${e.message}", e)
        }
    }
}

二 灰白滤镜

private fun createFilterProgram() {
        // 顶点着色器代码
        val vertexShaderCode = """
            attribute vec4 aPosition;
            attribute vec2 aTexCoord;
            varying vec2 vTexCoord;
            void main() {
                gl_Position = aPosition;
                vTexCoord = aTexCoord;
            }
        """

        // 片段着色器代码 - 灰度滤镜
        val fragmentShaderCode = """
            precision mediump float;
            uniform sampler2D uTexture;
            varying vec2 vTexCoord;
            void main() {
                vec4 color = texture2D(uTexture, vTexCoord);
                // 灰度公式: 0.299*R + 0.587*G + 0.114*B
                float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
                gl_FragColor = vec4(gray, gray, gray, color.a);
            }
        """

        filterProgramId = createShaderProgram(vertexShaderCode, fragmentShaderCode, "filter")
    }

三 FBO创建

private fun initFBO(width: Int, height: Int) {
        // 如果尺寸没有变化且资源已初始化,不需要重新初始化
        if (width == fboWidth && height == fboHeight && fboId > 0 && fboTextureId > 0 && tempTextureId > 0) {
            return
        }

        // 释放旧的资源
        releaseFboResources()

        fboWidth = width
        fboHeight = height

        try {
            // 生成FBO
            val fboIds = IntArray(1)
            GLES20.glGenFramebuffers(1, fboIds, 0)
            fboId = fboIds[0]

            // 生成两个纹理:主纹理和临时纹理
            val textureIds = IntArray(2)
            GLES20.glGenTextures(2, textureIds, 0)
            fboTextureId = textureIds[0]
            tempTextureId = textureIds[1]

            // 初始化两个纹理
            initializeTexture(fboTextureId, width, height)
            initializeTexture(tempTextureId, width, height)

            // 绑定FBO并附加主纹理
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId)
            GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                GLES20.GL_TEXTURE_2D, fboTextureId, 0)

            // 检查FBO状态
            val status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER)
            if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
                Log.e(TAG, "FBO initialization failed: $status")
                releaseFboResources()
                return
            }

            // 创建滤镜着色器程序(如果尚未创建)
            if (filterProgramId <= 0) {
                createFilterProgram()
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error initializing FBO: ${e.message}", e)
            releaseFboResources()
        } finally {
            // 确保解绑
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        }
    }

    // 初始化单个纹理
    private fun initializeTexture(textureId: Int, width: Int, height: Int) {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
            GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
    }

四 渲染

override fun onDrawFrame(gl: GL10?) {
        // 参数验证
        if (surfaceTexture == null || cameraProgramId <= 0 || screenProgramId <= 0 || filterProgramId <= 0) {
            Log.w(TAG, "Skipping frame due to missing resources")
            return
        }

        try {
            // 第一步:将相机预览渲染到FBO
            val localSurfaceTexture = surfaceTexture
            localSurfaceTexture!!.updateTexImage()

            // 绑定FBO
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId)

            // 设置渲染环境
            GLES20.glViewport(0, 0, fboWidth, fboHeight)
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

            // 使用相机预览着色器程序
            GLES20.glUseProgram(cameraProgramId)

            // 设置顶点属性
            setupVertexAttributes(cameraProgramId)

            // 设置MVP矩阵 - 旋转90度修正界面方向
            android.opengl.Matrix.setIdentityM(mvpMatrix, 0)
            android.opengl.Matrix.rotateM(mvpMatrix, 0, 90.0f, 0.0f, 0.0f, 1.0f)
            val uMVPMatrixLocation = GLES20.glGetUniformLocation(cameraProgramId, "uMVPMatrix")
            GLES20.glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mvpMatrix, 0)

            // 绑定相机纹理
            bindTexture(textureId, GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0, cameraProgramId, "uTexture")

            // 绘制四边形
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

            // 清理顶点属性
            disableVertexAttributes(cameraProgramId)

            // 第二步:在FBO中应用滤镜处理
            renderWithFilter()

            // 第三步:将FBO中经过滤镜处理的内容渲染到屏幕
            // 解绑FBO,确保渲染目标是默认帧缓冲区
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)

            // 清除默认帧缓冲区
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

            // 将FBO内容渲染到屏幕
            renderToScreen()

        } catch (e: Exception) {
            Log.e(TAG, "Error in onDrawFrame: ${e.message}", e)
        }
    }

完整代码

package com.example.cameravideofbo

import android.content.Context
import android.graphics.SurfaceTexture
import android.opengl.*
import android.util.Log
import java.io.IOException
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

class CameraRenderer(
    private val context: Context,
    private val surfaceTextureListener: SurfaceTextureListener? = null
) : GLSurfaceView.Renderer {

    // 监听器接口
    interface SurfaceTextureListener {
        fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture?)
    }

    private val TAG = "CameraRenderer"

    // 常量定义
    private companion object {
        private const val DEFAULT_PREVIEW_WIDTH = 1280
        private const val DEFAULT_PREVIEW_HEIGHT = 720
    }

    // 渲染相关属性
    private val mvpMatrix = FloatArray(16)
    private val textureTransformMatrix = FloatArray(16)

    // 窗口尺寸
    private var viewWidth: Int = 0
    private var viewHeight: Int = 0

    // FBO相关属性
    private var fboId: Int = 0
    private var fboTextureId: Int = 0  // 主FBO纹理
    private var tempTextureId: Int = 0  // 临时纹理,用于滤镜处理
    private var fboWidth: Int = 0
    private var fboHeight: Int = 0

    // 着色器程序
    private var cameraProgramId: Int = 0  // 相机预览着色器
    private var filterProgramId: Int = 0  // 滤镜着色器
    private var screenProgramId: Int = 0  // 屏幕渲染着色器

    // 纹理
    private var textureId: Int = 0
    private var surfaceTexture: SurfaceTexture? = null

    // 相机相关属性
    private var cameraManager: CameraManager? = null
    private var previewWidth: Int = 0
    private var previewHeight: Int = 0

    // 顶点和纹理坐标
    private val vertices = floatArrayOf(
        -1.0f, -1.0f, 0.0f,  // 左下
        1.0f, -1.0f, 0.0f,  // 右下
        -1.0f,  1.0f, 0.0f,  // 左上
        1.0f,  1.0f, 0.0f   // 右上
    )

    private val texCoords = floatArrayOf(
        0.0f, 1.0f,  // 左下
        1.0f, 1.0f,  // 右下
        0.0f, 0.0f,  // 左上
        1.0f, 0.0f   // 右上
    )

    // 顶点和纹理坐标缓冲区
    private val vertexBuffer: java.nio.FloatBuffer
    private val texCoordBuffer: java.nio.FloatBuffer

    init {
        // 初始化顶点缓冲区
        vertexBuffer = java.nio.ByteBuffer.allocateDirect(vertices.size * 4)
            .order(java.nio.ByteOrder.nativeOrder())
            .asFloatBuffer()
        vertexBuffer.put(vertices).position(0)

        // 初始化纹理坐标缓冲区
        texCoordBuffer = java.nio.ByteBuffer.allocateDirect(texCoords.size * 4)
            .order(java.nio.ByteOrder.nativeOrder())
            .asFloatBuffer()
        texCoordBuffer.put(texCoords).position(0)

        // 初始化MVP矩阵为单位矩阵
        android.opengl.Matrix.setIdentityM(mvpMatrix, 0)
    }

    // 初始化FBO和滤镜
    private fun initFBO(width: Int, height: Int) {
        // 如果尺寸没有变化且资源已初始化,不需要重新初始化
        if (width == fboWidth && height == fboHeight && fboId > 0 && fboTextureId > 0 && tempTextureId > 0) {
            return
        }

        // 释放旧的资源
        releaseFboResources()

        fboWidth = width
        fboHeight = height

        try {
            // 生成FBO
            val fboIds = IntArray(1)
            GLES20.glGenFramebuffers(1, fboIds, 0)
            fboId = fboIds[0]

            // 生成两个纹理:主纹理和临时纹理
            val textureIds = IntArray(2)
            GLES20.glGenTextures(2, textureIds, 0)
            fboTextureId = textureIds[0]
            tempTextureId = textureIds[1]

            // 初始化两个纹理
            initializeTexture(fboTextureId, width, height)
            initializeTexture(tempTextureId, width, height)

            // 绑定FBO并附加主纹理
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId)
            GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                GLES20.GL_TEXTURE_2D, fboTextureId, 0)

            // 检查FBO状态
            val status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER)
            if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
                Log.e(TAG, "FBO initialization failed: $status")
                releaseFboResources()
                return
            }

            // 创建滤镜着色器程序(如果尚未创建)
            if (filterProgramId <= 0) {
                createFilterProgram()
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error initializing FBO: ${e.message}", e)
            releaseFboResources()
        } finally {
            // 确保解绑
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        }
    }

    // 初始化单个纹理
    private fun initializeTexture(textureId: Int, width: Int, height: Int) {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId)
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
            GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
    }

    // 释放FBO相关资源
    private fun releaseFboResources() {
        if (tempTextureId > 0) {
            GLES20.glDeleteTextures(1, intArrayOf(tempTextureId), 0)
            tempTextureId = 0
        }
        if (fboTextureId > 0) {
            GLES20.glDeleteTextures(1, intArrayOf(fboTextureId), 0)
            fboTextureId = 0
        }
        if (fboId > 0) {
            GLES20.glDeleteFramebuffers(1, intArrayOf(fboId), 0)
            fboId = 0
        }
    }

    // 创建滤镜着色器程序(灰度滤镜)
    private fun createFilterProgram() {
        // 顶点着色器代码
        val vertexShaderCode = """
            attribute vec4 aPosition;
            attribute vec2 aTexCoord;
            varying vec2 vTexCoord;
            void main() {
                gl_Position = aPosition;
                vTexCoord = aTexCoord;
            }
        """

        // 片段着色器代码 - 灰度滤镜
        val fragmentShaderCode = """
            precision mediump float;
            uniform sampler2D uTexture;
            varying vec2 vTexCoord;
            void main() {
                vec4 color = texture2D(uTexture, vTexCoord);
                // 灰度公式: 0.299*R + 0.587*G + 0.114*B
                float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
                gl_FragColor = vec4(gray, gray, gray, color.a);
            }
        """

        filterProgramId = createShaderProgram(vertexShaderCode, fragmentShaderCode, "filter")
    }

    // 渲染带有滤镜的帧 - 使用临时纹理避免同时读写
    private fun renderWithFilter() {
        if (fboId <= 0 || filterProgramId <= 0 || tempTextureId <= 0 || fboTextureId <= 0) return

        try {
            // 绑定FBO
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId)
            GLES20.glViewport(0, 0, fboWidth, fboHeight)

            // 第一步:将FBO纹理的内容渲染到临时纹理(应用滤镜)
            renderTextureToTexture(fboTextureId, tempTextureId, filterProgramId)

            // 第二步:将临时纹理的内容复制回主FBO纹理
            renderTextureToTexture(tempTextureId, fboTextureId, screenProgramId)

        } catch (e: Exception) {
            Log.e(TAG, "Error rendering with filter: ${e.message}", e)
        } finally {
            // 确保解绑FBO
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
        }
    }

    // 将一个纹理渲染到另一个纹理
    private fun renderTextureToTexture(sourceTextureId: Int, targetTextureId: Int, programId: Int) {
        // 切换FBO的输出纹理
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
            GLES20.GL_TEXTURE_2D, targetTextureId, 0)

        // 使用指定的着色器程序
        GLES20.glUseProgram(programId)

        // 获取属性和统一变量的位置
        val aPositionLocation = GLES20.glGetAttribLocation(programId, "aPosition")
        val aTexCoordLocation = GLES20.glGetAttribLocation(programId, "aTexCoord")
        val uTextureLocation = GLES20.glGetUniformLocation(programId, "uTexture")

        // 设置顶点属性
        vertexBuffer.position(0)
        GLES20.glEnableVertexAttribArray(aPositionLocation)
        GLES20.glVertexAttribPointer(aPositionLocation, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)

        texCoordBuffer.position(0)
        GLES20.glEnableVertexAttribArray(aTexCoordLocation)
        GLES20.glVertexAttribPointer(aTexCoordLocation, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer)

        // 设置MVP矩阵(如果着色器程序使用)
        val uMVPMatrixLocation = GLES20.glGetUniformLocation(programId, "uMVPMatrix")
        if (uMVPMatrixLocation >= 0) {
            android.opengl.Matrix.setIdentityM(mvpMatrix, 0)
            GLES20.glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mvpMatrix, 0)
        }

        // 绑定源纹理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, sourceTextureId)
        GLES20.glUniform1i(uTextureLocation, 0)

        // 绘制四边形
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

        // 清理状态
        GLES20.glDisableVertexAttribArray(aPositionLocation)
        GLES20.glDisableVertexAttribArray(aTexCoordLocation)
    }

    // 将FBO中的内容渲染到屏幕
    private fun renderToScreen() {
        if (screenProgramId <= 0 || fboTextureId <= 0 || viewWidth <= 0 || viewHeight <= 0) return

        try {
            // 设置视口为窗口大小
            GLES20.glViewport(0, 0, viewWidth, viewHeight)

            // 使用屏幕渲染着色器程序
            GLES20.glUseProgram(screenProgramId)

            // 设置顶点和纹理坐标
            setupVertexAttributes(screenProgramId)

            // 设置MVP矩阵
            android.opengl.Matrix.setIdentityM(mvpMatrix, 0)
            val uMVPMatrixLocation = GLES20.glGetUniformLocation(screenProgramId, "uMVPMatrix")
            GLES20.glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mvpMatrix, 0)

            // 绑定FBO纹理
            bindTexture(fboTextureId, GLES20.GL_TEXTURE_2D, 0, screenProgramId, "uTexture")

            // 绘制四边形
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

        } catch (e: Exception) {
            Log.e(TAG, "Error rendering to screen: ${e.message}", e)
        } finally {
            // 清理顶点属性
            disableVertexAttributes(screenProgramId)
        }
    }

    // 设置顶点和纹理坐标属性
    private fun setupVertexAttributes(programId: Int) {
        val aPositionLocation = GLES20.glGetAttribLocation(programId, "aPosition")
        val aTexCoordLocation = GLES20.glGetAttribLocation(programId, "aTexCoord")

        vertexBuffer.position(0)
        GLES20.glEnableVertexAttribArray(aPositionLocation)
        GLES20.glVertexAttribPointer(aPositionLocation, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer)

        texCoordBuffer.position(0)
        GLES20.glEnableVertexAttribArray(aTexCoordLocation)
        GLES20.glVertexAttribPointer(aTexCoordLocation, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer)
    }

    // 禁用顶点属性
    private fun disableVertexAttributes(programId: Int) {
        val aPositionLocation = GLES20.glGetAttribLocation(programId, "aPosition")
        val aTexCoordLocation = GLES20.glGetAttribLocation(programId, "aTexCoord")

        GLES20.glDisableVertexAttribArray(aPositionLocation)
        GLES20.glDisableVertexAttribArray(aTexCoordLocation)
    }

    // 绑定纹理到指定纹理单元
    private fun bindTexture(textureId: Int, textureType: Int, textureUnit: Int, programId: Int, uniformName: String) {
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + textureUnit)
        GLES20.glBindTexture(textureType, textureId)
        GLES20.glUniform1i(GLES20.glGetUniformLocation(programId, uniformName), textureUnit)
    }

    // 启动相机预览
    @Throws(IOException::class)
    fun startCameraPreview() {
        if (surfaceTexture == null) {
            throw IOException("SurfaceTexture not available")
        }

        // 初始化相机管理器
        val localSurfaceTexture = surfaceTexture
        if (localSurfaceTexture != null) {
            cameraManager = CameraManager(context, localSurfaceTexture)
            cameraManager?.startCameraPreview()
        }
    }

    // 停止相机预览
    fun stopCameraPreview() {
        cameraManager?.stopCameraPreview()
    }

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        try {
            Log.d(TAG, "Surface created")

            // 初始化OpenGL环境
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
            GLES20.glDisable(GLES20.GL_DEPTH_TEST)
            GLES20.glEnable(GLES20.GL_TEXTURE_2D)

            // 创建着色器程序
            createCameraProgram()
            createScreenProgram()

            // 生成相机纹理
            generateTexture()

            // 创建SurfaceTexture
            createSurfaceTexture()

            // 验证着色器程序创建成功
            if (cameraProgramId <= 0 || screenProgramId <= 0) {
                Log.e(TAG, "Failed to create shader programs")
            } else {
                Log.d(TAG, "All shader programs created successfully")
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error in onSurfaceCreated: ${e.message}", e)
        }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        try {
            Log.d(TAG, "Surface changed: width=$width, height=$height")

            // 参数验证
            if (width <= 0 || height <= 0) {
                Log.w(TAG, "Invalid surface dimensions: $width x $height")
                return
            }

            // 保存窗口宽高
            viewWidth = width
            viewHeight = height

            // 设置视口
            GLES20.glViewport(0, 0, width, height)

            // 初始化FBO
            initFBO(width, height)
        } catch (e: Exception) {
            Log.e(TAG, "Error in onSurfaceChanged: ${e.message}", e)
        }
    }

    override fun onDrawFrame(gl: GL10?) {
        // 参数验证
        if (surfaceTexture == null || cameraProgramId <= 0 || screenProgramId <= 0 || filterProgramId <= 0) {
            Log.w(TAG, "Skipping frame due to missing resources")
            return
        }

        try {
            // 第一步:将相机预览渲染到FBO
            val localSurfaceTexture = surfaceTexture
            localSurfaceTexture!!.updateTexImage()

            // 绑定FBO
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId)

            // 设置渲染环境
            GLES20.glViewport(0, 0, fboWidth, fboHeight)
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

            // 使用相机预览着色器程序
            GLES20.glUseProgram(cameraProgramId)

            // 设置顶点属性
            setupVertexAttributes(cameraProgramId)

            // 设置MVP矩阵 - 旋转90度修正界面方向
            android.opengl.Matrix.setIdentityM(mvpMatrix, 0)
            android.opengl.Matrix.rotateM(mvpMatrix, 0, 90.0f, 0.0f, 0.0f, 1.0f)
            val uMVPMatrixLocation = GLES20.glGetUniformLocation(cameraProgramId, "uMVPMatrix")
            GLES20.glUniformMatrix4fv(uMVPMatrixLocation, 1, false, mvpMatrix, 0)

            // 绑定相机纹理
            bindTexture(textureId, GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0, cameraProgramId, "uTexture")

            // 绘制四边形
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

            // 清理顶点属性
            disableVertexAttributes(cameraProgramId)

            // 第二步:在FBO中应用滤镜处理
            renderWithFilter()

            // 第三步:将FBO中经过滤镜处理的内容渲染到屏幕
            // 解绑FBO,确保渲染目标是默认帧缓冲区
            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)

            // 清除默认帧缓冲区
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

            // 将FBO内容渲染到屏幕
            renderToScreen()

        } catch (e: Exception) {
            Log.e(TAG, "Error in onDrawFrame: ${e.message}", e)
        }
    }



    // 创建着色器程序
    private fun createShaderProgram(vertexCode: String, fragmentCode: String, programName: String): Int {
        try {
            // 编译着色器
            val vertexShaderId = compileShader(GLES20.GL_VERTEX_SHADER, vertexCode)
            val fragmentShaderId = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode)

            if (vertexShaderId <= 0 || fragmentShaderId <= 0) {
                return 0
            }

            // 链接程序
            val programId = GLES20.glCreateProgram()
            GLES20.glAttachShader(programId, vertexShaderId)
            GLES20.glAttachShader(programId, fragmentShaderId)
            GLES20.glLinkProgram(programId)

            // 检查链接状态
            val linkStatus = IntArray(1)
            GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, 0)
            if (linkStatus[0] != GLES20.GL_TRUE) {
                val error = GLES20.glGetProgramInfoLog(programId)
                Log.e(TAG, "Error linking $programName program: $error")
                GLES20.glDeleteProgram(programId)
                return 0
            }

            return programId
        } catch (e: Exception) {
            Log.e(TAG, "Error creating $programName program: ${e.message}", e)
            return 0
        }
    }

    // 编译单个着色器
    private fun compileShader(type: Int, shaderCode: String): Int {
        val shaderId = GLES20.glCreateShader(type)
        GLES20.glShaderSource(shaderId, shaderCode)
        GLES20.glCompileShader(shaderId)

        // 检查编译状态
        val compileStatus = IntArray(1)
        GLES20.glGetShaderiv(shaderId, GLES20.GL_COMPILE_STATUS, compileStatus, 0)
        if (compileStatus[0] != GLES20.GL_TRUE) {
            val error = GLES20.glGetShaderInfoLog(shaderId)
            val shaderType = if (type == GLES20.GL_VERTEX_SHADER) "vertex" else "fragment"
            Log.e(TAG, "Error compiling $shaderType shader: $error")
            GLES20.glDeleteShader(shaderId)
            return 0
        }

        return shaderId
    }

    // 创建相机预览着色器程序
    private fun createCameraProgram() {
        // 顶点着色器代码
        val vertexShaderCode = """
            attribute vec4 aPosition;
            attribute vec2 aTexCoord;
            uniform mat4 uMVPMatrix;
            varying vec2 vTexCoord;
            void main() {
                gl_Position = uMVPMatrix * aPosition;
                vTexCoord = aTexCoord;
            }
        """

        // 片段着色器代码 - 用于相机预览(OES纹理)
        val fragmentShaderCode = """
            #extension GL_OES_EGL_image_external : require
            precision mediump float;
            uniform samplerExternalOES uTexture;
            varying vec2 vTexCoord;
            void main() {
                gl_FragColor = texture2D(uTexture, vTexCoord);
            }
        """

        cameraProgramId = createShaderProgram(vertexShaderCode, fragmentShaderCode, "camera")
    }

    // 创建屏幕渲染着色器程序
    private fun createScreenProgram() {
        // 顶点着色器代码
        val vertexShaderCode = """
            attribute vec4 aPosition;
            attribute vec2 aTexCoord;
            uniform mat4 uMVPMatrix;
            varying vec2 vTexCoord;
            void main() {
                gl_Position = uMVPMatrix * aPosition;
                vTexCoord = aTexCoord;
            }
        """

        // 片段着色器代码 - 支持普通2D纹理
        val fragmentShaderCode = """
            precision mediump float;
            uniform sampler2D uTexture;
            varying vec2 vTexCoord;
            void main() {
                gl_FragColor = texture2D(uTexture, vTexCoord);
            }
        """

        screenProgramId = createShaderProgram(vertexShaderCode, fragmentShaderCode, "screen")
    }

    private fun generateTexture() {
        try {
            Log.d(TAG, "Generating camera texture")

            // 生成纹理
            val textureIds = IntArray(1)
            GLES20.glGenTextures(1, textureIds, 0)
            textureId = textureIds[0]

            if (textureId <= 0) {
                Log.e(TAG, "Failed to generate camera texture")
                return
            }

            // 设置纹理参数
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId)
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)

            Log.d(TAG, "Camera texture generated successfully: $textureId")
        } catch (e: Exception) {
            Log.e(TAG, "Error generating camera texture: ${e.message}", e)
        } finally {
            // 解绑纹理
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)
        }
    }

    private fun createSurfaceTexture() {
        // 创建SurfaceTexture
        surfaceTexture = SurfaceTexture(textureId)
        surfaceTexture?.setOnFrameAvailableListener {
            // 当有新帧可用时,可以在这里进行处理
        }

        // 通知监听器SurfaceTexture已创建
        surfaceTextureListener?.onSurfaceTextureAvailable(surfaceTexture)
    }

    fun setPreviewSize(width: Int, height: Int) {
        previewWidth = width
        previewHeight = height
    }

    fun release() {
        try {
            Log.d(TAG, "Releasing renderer resources")

            // 停止相机预览并释放CameraManager资源
            cameraManager?.release()
            cameraManager = null

            // 释放FBO相关资源
            releaseFboResources()

            // 释放相机纹理
            if (textureId > 0) {
                GLES20.glDeleteTextures(1, intArrayOf(textureId), 0)
                textureId = 0
            }

            // 释放着色器程序
            if (cameraProgramId > 0) {
                GLES20.glDeleteProgram(cameraProgramId)
                cameraProgramId = 0
            }
            if (screenProgramId > 0) {
                GLES20.glDeleteProgram(screenProgramId)
                screenProgramId = 0
            }
            if (filterProgramId > 0) {
                GLES20.glDeleteProgram(filterProgramId)
                filterProgramId = 0
            }

            // 释放SurfaceTexture
            surfaceTexture?.release()
            surfaceTexture = null

            Log.d(TAG, "Renderer resources released successfully")
        } catch (e: Exception) {
            Log.e(TAG, "Error releasing renderer resources: ${e.message}", e)
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值