Unity 渲染 YUV

YUV和RGB一样,是另一套用来表达颜色的方案。其详细叙述请参阅 YUV的维基。本篇着重讲解如何使用Unity来渲染YUV形式的数据视频或图片。对此,你需要了解以下知识:

  • YUV的数据格式
  • YUV转换到常规的RGB颜色空间公式
  • unity里将Y/U/V的数据送进GPU渲染

YUV的数据格式

YUV由Y、U、V这三个分量组成,根据YUV的维基的描述

Y′ stands for the luma component (the brightness) and U and V are the chrominance (color) components;

可见Y分量主管亮度的(其实就是灰度图),U和V分量主管色彩的。所以即使没有U和V分量,一样可以看到图像,只是没有颜色罢了(只有黑白色)。如果不理解,想想我们小时候的黑白电视机和彩电,黑白电视机就是只有Y分量的图像,而彩电就是有三个分量的图像。所以YUV格式的视频数据也是当时美国用来适配黑白电视机和彩电的一种方案。

那么根据三个分量在内存里的排列方式,又可以分为好多种格式。但不论是何种格式,首先需要认真了解它的数据格式后,从源数据中分离出Y、U、V这三个分量,再根据对应格式提供的的转RGB的公式,都可以正确渲染出其图像。本文以最常用的 ** I420(也叫 YUV420P) **的YUV数据格式为例。
下图便是 I420 的数据格式:
在这里插入图片描述
由图可见,I420格式的YUV数据的三个分量Y/U/V被分离在了3个连续的内存块中,而每个Y都会对应一个像素,每4个像素共用一组UV。所以一张 s = width x height 的图片,就会有 s 个像素,s 个Y,s / 4 个U和V。这张图片占用的内存 m = width * height * 1.5 byte 。
对I420数据正确的采样应该是如上图中相同颜色的Y和U/V。如 Y1 Y2 Y7 Y8 对应 U1 V1,而不是 Y1 Y2 Y3 Y4 对应 U1 V1。不正确的采样会导致不正确的渲染结果。

YUV2RGB

先给出YUV到RGB的转换公式:
R = Y + 1.4075 *(V-128)
G = Y – 0.3455 *(U –128) – 0.7169 *(V –128)
B = Y + 1.779 *(U – 128)
YUV2RGB的转换公式本身是很简单的,但是牵涉到浮点运算,所以,如果要实现快速算法,算法结构本身没什么好研究的了,主要是采用整型运算或者查表来加快计算速度。对此,可以参考经典算法,yuv与rgb互转,查表法,让你的软件飞起来
以上是在CPU里做的转换,本文将会把这种转换放到GPU里去做,毕竟GPU很擅长并行计算,对于这种粗鲁的运算,GPU是很快的。
在GPU里的公式就需要适当的调整了,因为在Unity里每个像素的取值范围是[0,1],而不是[0,255]。
R = Y + 1.4075 *(V-0.5)
G = Y – 0.3455 *(U –0.5) – 0.7169 *(V –0.5)
B = Y + 1.779 *(U – 0.5)

Unity渲染I420数据

这节用一个实例来讲解,本实例的运行在 **Mac unity 2018.2.12f1(64bit)**上。

1.准备yuv数据

可以到 http://trace.eas.asu.edu/yuv/ 这个网站上下载yuv的文件,该网站的yuv文件都是4:2:0的YUV数据格式,而本文的I420正是这种格式的YUV数据。本例以这个小姐姐作为待渲染的yuv数据
在这里插入图片描述
我也上传了一份到CSDN上yuv测试文件 (吐槽一下:这CSDN上传资源默认就要5个积分,也不能修改。大家如果能访问上面的网站尽量在网站上下载。也可以使用ffmpeg来得到,具体命令百度一下),该文件的图像分辨率是 176 * 144,共 150帧。Y U V三个分量全打包在一起了,这种也叫 packed 格式的,与其对立的叫 plannar 格式,是三个分量分开的。所以该文件的内存大小就是 176 * 144 * 1.5 * 150 = 5.44M。

2.加载yuv文件,并分离Y U V

使用IO读取yuv文件的byte数组,根据I420的数据格式,应该知道:
第n帧图像的byte数组范围是: [176 * 144 * 1.5 * (n - 1),176 * 144 * 1.5 * n)
第n帧图像的Y分量范围:[176 * 144 * 1.5 * n ,176 * 144 * 1.5 * n * 4 / 6)
第n帧图像的U分量范围:[176 * 144 * 1.5 * n * 4 / 6,176 * 144 * 1.5 * n * 5 / 6)
第n帧图像的V分量范围:[176 * 144 * 1.5 * n * 5 / 6,176 * 144 * 1.5 * n )
这样就可以轻而易举的得到任意一帧的yuv数据了。如果想看视频,可以添加帧率的控制逻辑按顺序从第1帧播放到第150帧就可以了。而我就想看图片,所以就只渲染了第1帧的图像。其主要代码如下

void LoadYUV()
    {
        string filePath = Application.dataPath + "/Resources/suzie_qcif.yuv";
        using (FileStream fstream = new FileStream(filePath, FileMode.Open))
        {
            try
            {
                byte[] buff = new byte[fstream.Length];
                fstream.Read(buff, 0, buff.Length);

                int firstFrameEndIndex = (int)(videoH * videoW * 1.5f);

                int yIndex = firstFrameEndIndex * 4 / 6;
                int uIndex = firstFrameEndIndex * 5 / 6;
                int vIndex = firstFrameEndIndex;

                bufY = new byte[videoW * videoH];
                bufU = new byte[videoW * videoH >> 2];
                bufV = new byte[videoW * videoH >> 2];
                bufUV = new byte[videoW * videoH >> 1];

                for (int i = 0; i < firstFrameEndIndex; i++)
                {
                    if(i < yIndex)
                    {
                        bufY[i] = buff[i];
                    }
                    else if(i < uIndex)
                    {
                        bufU[i - yIndex] = buff[i];
                    }
                    else
                    {
                        bufV[i - uIndex] = buff[i];
                    }
                }

                for(int i = 0; i < bufUV.Length; i+=2)
                {
                    bufUV[i] = bufU[i >> 1];
                    bufUV[i + 1] = bufV[i >> 1];
                }

                //如果不反转数组,得到的图像就是上下颠倒的
                //建议不在这里反转,因为反转数组还是挺耗性能的,
                //应该到shader中去反转一下uv坐标即可
                //Array.Reverse(bufY);
                //Array.Reverse(bufU);
                //Array.Reverse(bufV);
                //Array.Reverse(bufUV);

            }
            catch (Exception e)
            {
                Debug.LogError(e.ToString());
            }
        }
    }

3.将Y U V分量的数据传到GPU

在Unity中,我们可以先把byte[] 写入一张纹理中,然后在将这张纹理赋值给shader中的某个纹理对象,最后就可以在shader中通过纹理采样函数来得到byte[]里的数据了。
所以最容易想到的就是我们可以用三个Texture2D来分别存放Y U V分量的数据。但我们需要对这三个Texture2D的尺寸和格式有要求:
1.由于每个像素都对应着一个Y分量,所以存放Y数据的纹理尺寸应当是原图像的尺寸,本文的就是 176 * 144。
2.根据I420数据格式的采样规则,存放Y数据的纹理的尺寸是存放U/V数据的纹理尺寸的 2 倍,所以U/V纹理的尺寸都应该是 88 * 72。
3.纹理的格式应该以 刚好能够容纳源数据 为原则,比如本例中最合适的就是 TextureFormat.Alpha8格式,即每个像素就刚好一个字节,到shader中直接取纹理采样后的a通道就是该像素对应的YUV的某个分量值。当然你也可以选择 RGBA32的格式,如果你想在shader中运算方便,就只用某个通道,浪费3个字节,否则就需要在shader中多费写周折才可以得到某个像素对应的分量值。这点本例也会把UV分量都写入到RGBA4444格式的纹理中,在shader中就需要多一道运算才可以得到 U/V分量。

所以我们先得到3个Texture2D

	int videoW = 176;
    int videoH = 144;
		texY = new Texture2D(videoW, videoH, TextureFormat.Alpha8, false);
        //U分量和V分量分别存放在两张贴图中
        texU = new Texture2D(videoW >> 1, videoH >> 1, TextureFormat.Alpha8, false);
        texV = new Texture2D(videoW >> 1, videoH >> 1, TextureFormat.Alpha8, false);

然后分别把三个分量的数据写入三个纹理中

			texY.LoadRawTextureData(bufY);
            texU.LoadRawTextureData(bufU);
            texV.LoadRawTextureData(bufV);

            texY.Apply();
            texU.Apply();
            texV.Apply();

最后把这三张纹理分别赋值给shader中的三个纹理对象

 			target.sharedMaterial.SetTexture("_MainTex", texY);
            target.sharedMaterial.SetTexture("_UTex", texU);
            target.sharedMaterial.SetTexture("_VTex", texV);

提供一下全部的代码:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class I420Player : MonoBehaviour {

    public Renderer target;

    int videoW = 176;
    int videoH = 144;

    byte[] bufY = null;
    byte[] bufU = null;
    byte[] bufV = null;
    byte[] bufUV = null;

    Texture2D texY = null;
    Texture2D texU = null;
    Texture2D texV = null;
    Texture2D texUV = null;

	void Start () {
        texY = new Texture2D(videoW, videoH, TextureFormat.Alpha8, false);
        //U分量和V分量分别存放在两张贴图中
        texU = new Texture2D(videoW >> 1, videoH >> 1, TextureFormat.Alpha8, false);
        texV = new Texture2D(videoW >> 1, videoH >> 1, TextureFormat.Alpha8, false);

        //texUV = new Texture2D(videoW >> 1, videoH >> 1, TextureFormat.RGBA4444,false);
	}

    private void OnGUI()
    {
        if(GUILayout.Button("Load YUV"))
        {
            LoadYUV();

            texY.LoadRawTextureData(bufY);
            texU.LoadRawTextureData(bufU);
            texV.LoadRawTextureData(bufV);

            texY.Apply();
            texU.Apply();
            texV.Apply();
            //texUV.LoadRawTextureData(bufUV);
            //texUV.Apply();
        }

        if(GUILayout.Button("Render YUV"))
        {
            target.sharedMaterial.SetTexture("_MainTex", texY);
            target.sharedMaterial.SetTexture("_UTex", texU);
            target.sharedMaterial.SetTexture("_VTex", texV);
            //target.sharedMaterial.SetTexture("_UVTex", texUV);
        }
    }

    void LoadYUV()
    {
        string filePath = Application.dataPath + "/Resources/suzie_qcif.yuv";
        using (FileStream fstream = new FileStream(filePath, FileMode.Open))
        {
            try
            {
                byte[] buff = new byte[fstream.Length];
                fstream.Read(buff, 0, buff.Length);

                int firstFrameEndIndex = (int)(videoH * videoW * 1.5f);

                int yIndex = firstFrameEndIndex * 4 / 6;
                int uIndex = firstFrameEndIndex * 5 / 6;
                int vIndex = firstFrameEndIndex;

                bufY = new byte[videoW * videoH];
                bufU = new byte[videoW * videoH >> 2];
                bufV = new byte[videoW * videoH >> 2];
                bufUV = new byte[videoW * videoH >> 1];

                for (int i = 0; i < firstFrameEndIndex; i++)
                {
                    if(i < yIndex)
                    {
                        bufY[i] = buff[i];
                    }
                    else if(i < uIndex)
                    {
                        bufU[i - yIndex] = buff[i];
                    }
                    else
                    {
                        bufV[i - uIndex] = buff[i];
                    }
                }

                //如果是把UV分量一起写入到一张RGBA4444的纹理中时,byte[]
                //里的字节顺序应该是  UVUVUVUV....
                //这样在shader中纹理采样的结果 U 分量就存在r、g通道。
                //V 分量就存在b、a通道。

                //for(int i = 0; i < bufUV.Length; i+=2)
                //{
                //    bufUV[i] = bufU[i >> 1];
                //    bufUV[i + 1] = bufV[i >> 1];
                //}

                //如果不反转数组,得到的图像就是上下颠倒的
                //建议不在这里反转,因为反转数组还是挺耗性能的,
                //应该到shader中去反转一下uv坐标即可
                //Array.Reverse(bufY);
                //Array.Reverse(bufU);
                //Array.Reverse(bufV);
                //Array.Reverse(bufUV);

            }
            catch (Exception e)
            {
                Debug.LogError(e.ToString());
            }
        }
    }
}

4.shader

根据上文提供的公式

Shader "Unlit/I420RGB"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
        _UTex ("U", 2D) = "white" {}
        _VTex ("V", 2D) = "white" {}
        //_UVTex ("UV", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

            sampler2D _MainTex;
            sampler2D _UTex;
            sampler2D _VTex;
			sampler2D _UVTex;
			float4 _MainTex_ST;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
                //不在C#侧做数组的反转,应该在这反转一下uv的y分量即可。
                fixed2 uv = fixed2(i.uv.x,1 - i.uv.y);
                fixed4 ycol = tex2D(_MainTex, uv);
                fixed4 ucol = tex2D(_UTex, uv);
				fixed4 vcol = tex2D(_VTex, uv);
                //fixed4 uvcol = tex2D(_UVTex,uv);
                
                //如果是使用 Alpha8 的纹理格式写入各分量的值,各分量的值就可以直接取a通道的值
                float r = ycol.a + 1.4022 * vcol.a - 0.7011;
                float g = ycol.a - 0.3456 * ucol.a - 0.7145 * vcol.a + 0.53005;
                float b = ycol.a + 1.771 * ucol.a - 0.8855;
                
                
                //如果是使用的RGBA4444的纹理格式写入UV分量,就需要多一道计算
                //才可以得到正确的U V分量的值
                //float yVal = ycol.a;
                //float uVal = (uvcol.r * 15 * 16 + uvcol.g * 15) / 255;
                //float vVal = (uvcol.b * 15 * 16 + uvcol.a * 15) / 255;
                
                //float r = yVal + 1.4022 * vVal - 0.7011;
                //float g = yVal - 0.3456 * uVal - 0.7145 * vVal + 0.53005;
                //float b = yVal + 1.771 * uVal - 0.8855;
                
				return fixed4(r,g,b,1);
			}
			ENDCG
		}
	}
}

5.效果

做了那么多,是时候来看看效果了
在这里插入图片描述
本文到此结束了,希望能帮到你!!!

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值