Unity 渲染YUV数据 ---- 以Unity渲染Android Camera数据为例子

Posted 长江很多号

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity 渲染YUV数据 ---- 以Unity渲染Android Camera数据为例子相关的知识,希望对你有一定的参考价值。

1 背景

一般Unity都是RGB直接渲染的,但是总有特殊情况下,需要渲染YUV数据。比如,Unity读取android的Camera YUV数据,并渲染。本文就基于这种情况,来展开讨论。

Unity读取Android的byte数组,本身就耗时,如果再把YUV数据转为RGB也在脚本中实现(即CPU运行),那就很卡了。
一种办法,就是这个转换,放在GPU完成,即,在shader实现!

接下来,分2块来贴出源码和实现。

2 YUV数据来源 ---- Android 侧

Android的Camera数据,一般是YUV格式的,最常用的就是NV21。其像素布局如下:

即数据排列是YYYYVUVU…

现在,Android就做一项工作,打开Camera,随便渲染到一个空纹理,然后呢,获得Preview的帧数据。
用一个SimpleCameraPlugin作为Unity调用Android的接口类:
代码如下:

package com.chenxf.unitycamerasdk;

import android.app.Activity;
import android.content.Context;
import android.graphics.Point;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.Size;

/**
 *
 * 直接读取YUV数据,在unity渲染的方案
 */
public class SimpleCameraPlugin implements SurfaceTexture.OnFrameAvailableListener,  Camera.PreviewCallback 
    private static final String TAG = "qymv#CameraPlugin";

    private final static int REQUEST_CODE = 1;
    private final static int MSG_START_PREVIEW = 1;
    private final static int MSG_SWITCH_CAMERA = 2;
    private final static int MSG_RELEASE_PREVIEW = 3;
    private final static int MSG_MANUAL_FOCUS = 4;
    private final static int MSG_ROCK = 5;

    private SurfaceTexture mSurfaceTexture;
    private boolean mIsUpdateFrame;
    private Context mContext;

    private Handler mCameraHanlder;
    private Size mExpectedPreviewSize;
    private Size mPreviewSize;
    private boolean isFocusing;
    private int mWidth;
    private int mHeight;

    private byte[] yBuffer = null;
    private byte[] uvBuffer = null;

    public SimpleCameraPlugin() 
        Log.i(TAG, " create");
        initCameraHandler();
    

    private void initCameraHandler() 
        mCameraHanlder = new Handler(Looper.getMainLooper()) 
            @Override
            public void handleMessage(Message msg) 
                switch (msg.what) 
                    case MSG_START_PREVIEW:
                        startPreview();
                        break;
                    case MSG_RELEASE_PREVIEW:
                        releasePreview();
                        break;
                    case MSG_SWITCH_CAMERA:
                        //switchCamera();
                        break;
                    case MSG_MANUAL_FOCUS:
                        //manualFocus(msg.arg1, msg.arg2);
                        break;
                    case MSG_ROCK:
                        autoFocus();
                        break;
                    default:
                        break;
                
            
        ;
    

    public void releasePreview() 
        CameraUtil.releaseCamera();
//        mCameraSensor.stop();
//        mFocusView.cancelFocus();
        Log.e(TAG, "releasePreview releaseCamera");
    

    public void startPreview() 
        //if (mPreviewSize != null && requestPermission() ) 
        if (mExpectedPreviewSize != null) 
            if (CameraUtil.getCamera() == null) 
                CameraUtil.openCamera();
                Log.e(TAG, "openCamera");
                //CameraUtil.setDisplay(mSurfaceTexture);
            
            Camera.Size previewSize = CameraUtil.startPreview((Activity) mContext, mExpectedPreviewSize.getWidth(), mExpectedPreviewSize.getHeight());
            CameraUtil.setCallback(this);
            if(previewSize != null) 
                mWidth = previewSize.width;
                mHeight = previewSize.height;
                mPreviewSize = new Size(previewSize.width, previewSize.height);
                initSurfaceTexture(previewSize.width, previewSize.height);
                initBuffer(previewSize.width, previewSize.height);
                CameraUtil.setDisplay(mSurfaceTexture);


            
        
    

    private void initBuffer(int width, int height) 
        yBuffer = new byte[width * height];
        uvBuffer = new byte[width * height / 2];
    


    public void autoFocus() 
        if (CameraUtil.isBackCamera() && CameraUtil.getCamera() != null) 
            focus(mWidth / 2, mHeight / 2, true);
        
    

    private void focus(final int x, final int y, final boolean isAutoFocus) 
        Log.i(TAG, "focus, position: " + x + " " + y);
        if (CameraUtil.getCamera() == null || !CameraUtil.isBackCamera()) 
            return;
        
        if (isFocusing && isAutoFocus) 
            return;
        
        if (mWidth == 0 || mHeight == 0)
            return;
        isFocusing = true;
        Point focusPoint = new Point(x, y);
        Size screenSize = new Size(mWidth, mHeight);
        if (!isAutoFocus) 
            //mFocusView.beginFocus(x, y);
        
        CameraUtil.newCameraFocus(focusPoint, screenSize, new Camera.AutoFocusCallback() 
            @Override
            public void onAutoFocus(boolean success, Camera camera) 
                isFocusing = false;
                if (!isAutoFocus) 
                    //mFocusView.endFocus(success);
                
            
        );
    


    /**
     * 初始化
     * 调用该函数需要EGL 线程,否则会出现如下错误
     * libEGL  : call to OpenGL ES API with no current context
     *
     * @param context           android的context,最好传递activity的上下文
     * @param width             纹理宽
     * @param height            纹理高
     */
    public void start(Context context, int width, int height) 
        Log.w(TAG, "Start  context " + context);
        mContext = context;
        mWidth = width;
        mHeight = height;
        callStartPreview(width, height);
    

    private void callStartPreview(int width, int height) 
        mExpectedPreviewSize = new Size(width, height);
        mCameraHanlder.sendEmptyMessage(MSG_START_PREVIEW);
        mCameraHanlder.sendEmptyMessageDelayed(MSG_ROCK, 2000);
    


    public void resume() 

    

    public void pause() 
        mCameraHanlder.sendEmptyMessage(MSG_RELEASE_PREVIEW);
    


    private void initSurfaceTexture(int width, int height) 
        Log.i(TAG, "initSurfaceTexture " + " width " + width + " height " + height);
        //生成CAMERA输出的纹理id
        int videoTextureId = createOESTextureID();
        //根据创建的纹理id生成一个SurfaceTexture, 视频播放输出到surface texture
        mSurfaceTexture = new SurfaceTexture(videoTextureId);
        mSurfaceTexture.setDefaultBufferSize(width, height);
        mSurfaceTexture.setOnFrameAvailableListener(this);
    


    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) 
        Log.i(TAG, "onFrameAvailable");
        //视频播放开始有输出
        mIsUpdateFrame = true;
        if (frameListener != null) 
            frameListener.onFrameAvailable();
        
    

    /**
     * Unity调用,更新视频纹理,然后绘制FBO
     */
    public void updateTexture() 
        Log.i(TAG, "onFrameAvailable");
        mIsUpdateFrame = false;
        mSurfaceTexture.updateTexImage();
    

    public boolean isUpdateFrame() 
        return mIsUpdateFrame;
    

    @Override
    public void onActivityResume() 
        resume();
    

    @Override
    public void onActivityPause() 
        pause();
    


    private FrameListener frameListener;

    public void setFrameListener(FrameListener listener) 
        frameListener = listener;
    

    private synchronized boolean copyFrame(byte[] bytes) 
        Log.i(TAG, "copyFrame start");
        if(yBuffer != null && uvBuffer != null) 
            System.arraycopy(bytes, 0, yBuffer, 0, yBuffer.length);
            int uvIndex = yBuffer.length;
            System.arraycopy(bytes, uvIndex, uvBuffer, 0, uvBuffer.length);
            Log.i(TAG, "copyFrame end");
            return true;
        
        return false;
    

    public synchronized byte[] readYBuffer() 
        Log.i(TAG, "readYBuffer");
        return yBuffer;
    

    public synchronized byte[] readUVBuffer() 
        Log.i(TAG, "readUVBuffer");
        return uvBuffer;
    

    public int getPreviewWidth() 
        Log.i(TAG, "getPreviewWidth " + mWidth);
        return mWidth;
    

    public int getPreviewHeight() 
        Log.i(TAG, "getPreviewWidth " + mHeight);
        return mHeight;
    


    @Override
    public void onPreviewFrame(byte[] bytes, Camera camera) 
        if(copyFrame(bytes)) 
            UnityMsgBridge.notifyGotFrame();
        
    

    public interface FrameListener 
        void onFrameAvailable();
    


    public static int createOESTextureID() 
        int[] texture = new int[1];
        GLES20.glGenTextures(texture.length, texture, 0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
        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);
        return texture[0];
    


依赖很少,有个CameraUtils来打开摄像头:

package com.qiyi.unitycamerasdk;

import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.os.Build;
import android.util.Log;
import android.util.Size;
import android.view.Surface;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Created By Chengjunsen on 2018/8/23
 */
public class CameraUtil 
    private static final String TAG = "qymv#CameraUtil";
    private static Camera mCamera = null;
    private static int mCameraID = Camera.CameraInfo.CAMERA_FACING_FRONT;


    public static void openCamera() 
        mCamera = Camera.open(mCameraID);
        if (mCamera == null) 
            throw new RuntimeException("Unable to open camera");
        
    

    public static Camera getCamera() 
        return mCamera;
    

    public static void releaseCamera() 
        if (mCamera != null) 
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        
    

    public static void setCameraId(int cameraId) 
        mCameraID = cameraId;
    

    public static void switchCameraId() 
        mCameraID = isBackCamera() ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
    

    public static boolean isBackCamera() 
        return mCameraID == Camera.CameraInfo.CAMERA_FACING_BACK;
    

    public static void setDisplay(SurfaceTexture surfaceTexture) 
        try 
            if (mCamera != null) 
                mCamera.setPreviewTexture(surfaceTexture);
            
         catch (IOException e) 
            e.printStackTrace();
        
    

    public static Camera.Size startPreview(Activity activity, int width, int height) 

以上是关于Unity 渲染YUV数据 ---- 以Unity渲染Android Camera数据为例子的主要内容,如果未能解决你的问题,请参考以下文章

Unity 渲染YUV数据 ---- 以Unity渲染Android Camera数据为例子

Unity渲染iOS中的NV12格式数据---(下)

Unity怎么保存文件?保存文件然后继续做?

游戏引擎中三大及时光照渲染方法介绍(以unity3d为例)

Unity 中的渲染顺序

Unity下如何实现RTMP或RTSP流播放和录制