前言
項(xiàng)目需要渲染相機(jī)預(yù)覽數(shù)據(jù),相機(jī)返回結(jié)果是yuv420sp數(shù)據(jù),也就是NV21數(shù)據(jù),排布格式是這樣的:

如果需要渲染到 surfaceview,則需要轉(zhuǎn) rgb數(shù)據(jù),再渲染,非常耗時(shí),如果直接使用yuv 數(shù)據(jù)當(dāng)做貼圖,使用自定義 shader來(lái)渲染,效率會(huì)非常高,在驍龍 870 上(紅米 K40)降了 50%(共 800%)的cpu 占用
目前demo 采用非常冷門的 irrlicht 渲染引擎,直接使用 opengl 也是一樣的邏輯
【特別注意】貼圖數(shù)據(jù)填充需要放到渲染線程內(nèi)
Android Image 接口
yuvReader = ImageReader.newInstance(1280, 720, ImageFormat.YUV_420_888, 10);
yuvReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image == null) {
return;
}
ByteBuffer[] imageBuffers = new ByteBuffer[image.getPlanes().length];
for (int i = 0; i < image.getPlanes().length; i++) {
imageBuffers[i] = image.getPlanes()[i].getBuffer();
}
Irrlicht.nativeUpdateImage(imageBuffers, 1280, 720, 1);
image.close();
}
}, imageReadHandler);
如上接口可以直接獲取ByteBuffer[],包含了 yuv 數(shù)據(jù),分為了三個(gè)Plane,第一個(gè) Plane 是 y,第二個(gè) Plane 是 uv,特別注意【第三個(gè) Plane 是 vu,是第二個(gè) Plane 起始地址+1】,其實(shí)只需要用到前兩個(gè) Plane 即可
native 層獲取 yuv 數(shù)據(jù)如下:
jobject bufferObj = env->GetObjectArrayElement(image_buffer, 0);
if (yData == nullptr) {
yData = static_cast<uint8_t *>(malloc(h * w));
}
memcpy(yData, env->GetDirectBufferAddress(bufferObj), h * w);
env->DeleteLocalRef(bufferObj);
bufferObj = env->GetObjectArrayElement(image_buffer, 1);
if (uvData == nullptr) {
uvData = static_cast<uint8_t *>(malloc(h * w / 2));
}
memcpy(uvData, env->GetDirectBufferAddress(bufferObj), h * w / 2);
env->DeleteLocalRef(bufferObj);
如果采用 opencv 轉(zhuǎn)為 rgb 的話
jobject bufferObj = env->GetObjectArrayElement(image_buffer, 0);
cv::Mat y_plane(h, w, CV_8UC1, env->GetDirectBufferAddress(bufferObj));
env->DeleteLocalRef(bufferObj);
bufferObj = env->GetObjectArrayElement(image_buffer, 1);
cv::Mat uv_plane(h / 2, w / 2, CV_8UC2, env->GetDirectBufferAddress(bufferObj));
env->DeleteLocalRef(bufferObj);
cv::cvtColorTwoPlane(y_plane, uv_plane, cameraRGBMat, cv::COLOR_YUV2BGR_NV21);
可以直接將 rgb 數(shù)據(jù)渲染到 2dImage
imageTexture = driver->addTexture(core::dimension2d<u32>(cameraWidth, cameraHeight), "$cameraImage");
auto *frame_buf = cameraRGBMat.data;
auto *tex_buf = (uint8_t *) imageTexture->lock();
for (int j = 0; j < cameraRGBMat.rows * cameraRGBMat.cols; j++) {
*(tex_buf) = *(frame_buf + 2);
*(tex_buf + 1) = *(frame_buf + 1);
*(tex_buf + 2) = *(frame_buf);
*(tex_buf + 3) = 255;
frame_buf += 3;
tex_buf += 4;
}
imageTexture->unlock();
device->getVideoDriver()->enableMaterial2D(true);
auto sd = device->getVideoDriver()->getCurrentRenderTargetSize();
device->getVideoDriver()->draw2DImage(imageTexture,
core::rect<s32>(0, 0, sd.Width,
sd.Height),
core::rect<s32>(0, 0,
imageTexture->getSize().Width,
imageTexture->getSize().Height));
device->getVideoDriver()->enableMaterial2D(false);
創(chuàng)建YUV貼圖,并將yuv數(shù)據(jù)復(fù)制過(guò)去
yuvTexture = driver->addTexture(core::dimension2d<u32>(cameraWidth, cameraHeight),
"yuvTexture", ECF_A8R8G8B8);
這里為了簡(jiǎn)化操作,先將 yuv420 數(shù)據(jù)擴(kuò)充為 yuv444 數(shù)據(jù),并直接復(fù)制到texture的buffer 里,要注意和 shader 里的 rgba 對(duì)應(yīng)關(guān)系,我這里是ECF_A8R8G8B8, buf對(duì)應(yīng)b,buf+3對(duì)應(yīng) a,在 shader 里取值的時(shí)候要對(duì)應(yīng)好
auto buf = static_cast<uint8_t *>(yuvTexture->lock());
auto *y_buf = yData;
int width = yuvTexture->getSize().Width;
int height = yuvTexture->getSize().Height;
for (int h = 0; h < height; ++h) {
auto *uv_buf_row = uvData + int(h / 2) * width; // 每?jī)尚泄蚕硪唤MUV
for (int w = 0; w < width; ++w) {
// Y分量
*buf = *y_buf;
y_buf += 1;
// UV分量
auto *uv_buf_pixel = uv_buf_row + int(w / 2) * 2; // 每?jī)蓚€(gè)像素共享一組UV
*(buf+1) = *uv_buf_pixel; // U分量
*(buf+2) = *(uv_buf_pixel + 1); // V分量
buf += 4;
}
}
yuvTexture->unlock();
頂點(diǎn)和三角形設(shè)計(jì)
定義圖像的四個(gè)頂點(diǎn)
Vertices[0] = S3DVertex(-1.0f, -1.0f, 0.0f, // 位置,對(duì)應(yīng) shader 的inVertexPosition
1.0f, 1.0f, 1.0f, // 法向量,可以忽略
video::SColor(255, 255, 255, 255), // 頂點(diǎn)顏色,可以忽略
0.0f, 1.0f); // 貼圖 uv 坐標(biāo),對(duì)應(yīng) shader 的inTexCoord0
Vertices[1] = S3DVertex(1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 1.0f,
video::SColor(255, 255, 255, 255),
1.0f, 1.0f);
Vertices[2] = S3DVertex(1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f,
video::SColor(255, 255, 255, 255),
1.0f, 0.0f);
Vertices[3] = S3DVertex(-1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f,
video::SColor(255, 255, 255, 255),
0.0f, 0.0f);
定義三角形頂點(diǎn)的順序
只需要定義兩個(gè)三角形即可表達(dá)一個(gè)矩形畫(huà)面
u16 indices[] = {2, 1, 3, 1, 0, 3};
driver->drawIndexedTriangleList(&Vertices[0], 4, &indices[0], 2);
頂點(diǎn)著色器 YUVVertexShader.glsl
由于直接鋪滿整個(gè)屏幕,所以頂點(diǎn)坐標(biāo)可以直接對(duì)應(yīng)到屏幕坐標(biāo),圖像坐標(biāo)和貼圖坐標(biāo)透?jìng)骷纯?br> 注意,irrlicht 默認(rèn)glBindAttribLocation了頂點(diǎn)坐標(biāo)和貼圖坐標(biāo),名字寫死了是inVertexPosition和inTexCoord0,如果是 opengl 的話要注意對(duì)齊
// 指定精度
precision mediump float;
attribute vec3 inVertexPosition; // 頂點(diǎn)坐標(biāo),名字和glBindAttribLocation對(duì)齊
attribute vec2 inTexCoord0; // 貼圖 uv 坐標(biāo),名字和glBindAttribLocation對(duì)齊
// 傳遞給片段著色器的紋理坐標(biāo)
varying vec2 tc; // 名字和片元著色器對(duì)齊
void main()
{
// 設(shè)置頂點(diǎn)位置
gl_Position = vec4(inVertexPosition, 1.0);
// 傳遞紋理坐標(biāo)
tc = inTexCoord0;
}
片元著色器 YUVFragmentShader.glsl
yuv數(shù)據(jù)是放在 argb 數(shù)據(jù)里傳遞的,所以需要對(duì)應(yīng)取出,然后進(jìn)行轉(zhuǎn)換即可
precision mediump float;
varying mediump vec2 tc;
uniform sampler2D yuvTexture;
mat3 yuvToRgb = mat3(
1, 1, 1,
0, -0.39465, 2.03211,
1.13983, -0.58060, 0
);
void main()
{
mediump float y = texture2D(yuvTexture, tc).b; // Y 分量
mediump vec2 uv = texture2D(yuvTexture, tc).gr - 0.5; // UV 分量
// float r = y + 1.13983 * uv.y;
// float g = y - 0.39465 * uv.x - 0.58060 * uv.y;
// float b = y + 2.03211 * uv.x;
// gl_FragColor = vec4(r,g,b, 1.0);
vec3 rgb = yuvToRgb * vec3(y, uv);
gl_FragColor = vec4(rgb, 1.0);
}
加載 shader,進(jìn)行渲染
IVideoDriver *driver = SceneManager->getVideoDriver();
video::IGPUProgrammingServices *gpu = driver->getGPUProgrammingServices();
s32 materialType = gpu->addHighLevelShaderMaterialFromFiles(
mediaPath + "Shaders/YUVVertexShader.glsl",
mediaPath + "Shaders/YUVFragmentShader.glsl", 0);
SMaterial Material;
Material.Wireframe = false;
Material.Lighting = false;
Material.ZBuffer = video::ECFN_DISABLED; // 不做深度測(cè)量,也就是和其他元素沒(méi)有前后遮蓋
Material.MaterialType = (video::E_MATERIAL_TYPE) materialType;
Material.setTexture(0, yuvTexture);
driver->setMaterial(Material);
driver->setTransform(ETS_WORLD, AbsoluteTransformation);
// 繪制三角形
driver->drawIndexedTriangleList(&Vertices[0], 4, &indices[0], 2);
后語(yǔ)
uv 通道數(shù)據(jù)還可以獨(dú)立出來(lái)一個(gè) texture傳入 shader,這里由于 irrlicht 只支持 32 位的 texture,不支持直接創(chuàng)建 8 位和 16 位的 texture, y 和 uv 分開(kāi)的話有點(diǎn)浪費(fèi)空間,干脆擴(kuò)充成 yuv444 再傳入了,也可以把 y 壓縮到和 uv 同寬高再輸入
y 壓縮到和 uv 同寬高
其他邏輯都不用變,只需要改下 Texture 的寬高,以及數(shù)據(jù)填充邏輯
yuvTexture = driver->addTexture(core::dimension2d<u32>(cameraWidth/2, cameraHeight/2),
"yuvTexture", ECF_A8R8G8B8);
auto buf = static_cast<uint8_t *>(yuvTexture->lock());
int width = yuvTexture->getSize().Width;
int height = yuvTexture->getSize().Height;
auto *uv_buf = uvData;
for (int h = 0; h < height; ++h) {
auto *y_buf_row = yData + h * 2 * width * 2; // 壓縮了 y
for (int w = 0; w < width; ++w) {
// Y分量
*buf = *(y_buf_row + w * 2);
// UV分量
*(buf + 1) = *uv_buf; // U分量
*(buf + 2) = *(uv_buf + 1); // V分量
uv_buf += 2;
buf += 4;
}
}