You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Android Studio中基于二进制图像在OpenGL动态绘制图形?

嘿,我来分享一套在Android Studio里用OpenGL实现动态基于二进制图像绘制图形的可行方案,亲测在项目里用过,效果不错~

核心思路

咱们的需求是每秒更新一张二进制图像,然后把图像里的图形(不管是三角形还是任意形状)用OpenGL画出来。核心逻辑就是:每帧检测是否有新的二进制图像→解析图像生成OpenGL可识别的顶点数据→更新GPU缓冲区→重新绘制

具体步骤拆解

1. 搭建Android OpenGL基础环境

首先得用GLSurfaceView来承载OpenGL绘制,自定义一个Renderer类处理绘制逻辑。这是基础框架,代码大概是这样:

public class CustomGLSurfaceView extends GLSurfaceView {
    public CustomGLSurfaceView(Context context) {
        super(context);
        setEGLContextClientVersion(2); // 用GLES 2.0兼容大部分设备
        setRenderer(new CustomRenderer());
        setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); // 持续渲染,方便动态更新
    }

    private static class CustomRenderer implements GLSurfaceView.Renderer {
        private int programId;
        private int vboId;
        private int vertexCount = 0;
        private float[] currentVertices; // 存储当前要绘制的顶点数据

        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
            // 加载编译着色器
            String vertexShader = "attribute vec2 aPosition;\nvoid main() {\ngl_Position = vec4(aPosition, 0.0, 1.0);\n}";
            String fragmentShader = "precision mediump float;\nvoid main() {\ngl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n}";
            int vertexShaderId = loadShader(GLES20.GL_VERTEX_SHADER, vertexShader);
            int fragmentShaderId = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
            programId = GLES20.glCreateProgram();
            GLES20.glAttachShader(programId, vertexShaderId);
            GLES20.glAttachShader(programId, fragmentShaderId);
            GLES20.glLinkProgram(programId);

            // 初始化VBO
            int[] vbos = new int[1];
            GLES20.glGenBuffers(1, vbos, 0);
            vboId = vbos[0];
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId);
            // 先分配一个初始缓冲区,后面动态更新
            GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, 1024 * 4 * 2, null, GLES20.GL_DYNAMIC_DRAW);
        }

        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            GLES20.glViewport(0, 0, width, height);
        }

        @Override
        public void onDrawFrame(GL10 gl) {
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            if (currentVertices == null || vertexCount == 0) return;

            GLES20.glUseProgram(programId);
            int positionHandle = GLES20.glGetAttribLocation(programId, "aPosition");
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId);
            // 更新VBO数据
            GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, 0, currentVertices.length * 4, FloatBuffer.wrap(currentVertices));
            GLES20.glEnableVertexAttribArray(positionHandle);
            GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, 0);
            // 绘制,这里用三角形带还是四边形,根据你的顶点数据来
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertexCount);
            GLES20.glDisableVertexAttribArray(positionHandle);
        }

        // 提供外部方法更新顶点数据
        public void updateVertices(float[] vertices, int count) {
            currentVertices = vertices;
            vertexCount = count;
        }

        private int loadShader(int type, String shaderCode) {
            int shader = GLES20.glCreateShader(type);
            GLES20.glShaderSource(shader, shaderCode);
            GLES20.glCompileShader(shader);
            return shader;
        }
    }
}

2. 二进制图像解析成顶点数据

二进制图像一般是byte[]或者Bitmap(黑白,0代表背景,1代表图形)。咱们需要把图像里的1区域转换成OpenGL的顶点坐标(注意OpenGL的坐标是[-1,1]的归一化坐标,要把图像的像素坐标转换过去)。

举个例子,假设你的二进制图像是int[][] binaryImage,宽高是imgWidthimgHeight,可以这样生成顶点:

public static float[] convertBinaryImageToVertices(int[][] binaryImage, int imgWidth, int imgHeight) {
    List<Float> vertices = new ArrayList<>();
    float unitX = 2.0f / imgWidth;
    float unitY = 2.0f / imgHeight;

    // 遍历每个像素,生成四边形顶点(每个像素对应一个小矩形,用三角形带绘制)
    for (int y = 0; y < imgHeight - 1; y++) {
        for (int x = 0; x < imgWidth - 1; x++) {
            if (binaryImage[y][x] == 1) {
                // 转换OpenGL坐标:(x,y)对应屏幕的(-1 + x*unitX, 1 - y*unitY)
                float x1 = -1 + x * unitX;
                float y1 = 1 - y * unitY;
                float x2 = x1 + unitX;
                float y2 = y1;
                float x3 = x1;
                float y3 = y1 - unitY;
                float x4 = x2;
                float y4 = y3;

                // 添加三角形带的顶点(两个三角形组成四边形)
                vertices.add(x1); vertices.add(y1);
                vertices.add(x2); vertices.add(y2);
                vertices.add(x3); vertices.add(y3);
                vertices.add(x4); vertices.add(y4);
            }
        }
    }

    float[] result = new float[vertices.size()];
    for (int i = 0; i < vertices.size(); i++) {
        result[i] = vertices.get(i);
    }
    return result;
}

如果是复杂形状,你可以用轮廓提取算法(比如OpenCV的findContours)来生成更精简的顶点,减少绘制压力。

3. 动态更新图像与绘制

因为每秒要更新一张图像,咱们可以用后台线程定时获取新的二进制图像,解析成顶点数据后,切换到GL线程更新Renderer的顶点数据:

// 在Activity里
private CustomGLSurfaceView glSurfaceView;
private Handler handler = new Handler(Looper.getMainLooper());
private Runnable updateTask = new Runnable() {
    @Override
    public void run() {
        // 这里模拟每秒获取新的二进制图像,实际替换成你的图像来源
        int[][] newBinaryImage = generateNewBinaryImage();
        float[] vertices = convertBinaryImageToVertices(newBinaryImage, 256, 256);
        // 切换到GL线程更新数据
        glSurfaceView.queueEvent(() -> {
            CustomRenderer renderer = (CustomRenderer) glSurfaceView.getRenderer();
            renderer.updateVertices(vertices, vertices.length / 2);
        });
        // 每秒执行一次
        handler.postDelayed(this, 1000);
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    glSurfaceView = new CustomGLSurfaceView(this);
    setContentView(glSurfaceView);
    handler.post(updateTask);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacks(updateTask);
}

// 模拟生成新的二进制图像
private int[][] generateNewBinaryImage() {
    int width = 256;
    int height = 256;
    int[][] image = new int[height][width];
    // 这里可以随机生成图形,或者从你的数据源获取
    int centerX = width / 2;
    int centerY = height / 2;
    int radius = (int)(Math.random() * 50 + 30);
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            if (Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2) <= radius * radius) {
                image[y][x] = 1;
            }
        }
    }
    return image;
}

4. 性能优化要点

  • 异步解析:图像解析是耗时操作,一定要放在后台线程,不要在GL线程做,避免卡顿。
  • 顶点精简:不要每个像素都生成顶点,尽量合并相邻区域,或者用轮廓提取减少顶点数量。
  • VBO复用:用glBufferSubData而不是每次重新创建VBO,减少GPU资源开销。
  • 按需渲染:如果有时候没有新图像,可以把RENDERMODE_CONTINUOUSLY改成RENDERMODE_WHEN_DIRTY,有新数据时调用requestRender(),节省性能。
注意事项
  • 要处理OpenGL的线程安全问题:所有和GL相关的操作必须在GL线程执行,用queueEvent()来切换线程。
  • 坐标转换:一定要把图像的像素坐标转换成OpenGL的归一化坐标([-1,1]),不然会出现绘制位置不对的问题。
  • 着色器调试:如果看不到图形,可以检查着色器是否编译链接成功,用glGetProgramiv获取链接状态。

内容的提问来源于stack exchange,提问作者Gabriele Corsi

火山引擎 最新活动