Unity 渲染 YUV[通俗易懂]

Unity 渲染 YUV[通俗易懂]YUVYUV和RGB一样,是另一套用来表达颜色的方案。其详细叙述请参阅[YUV的维基](https://en.wikipedia.org/wiki/YUV)欢迎使用Markdown编辑器加粗样式你好!这是你第一次使用Markdown编辑器所展示的欢迎页。如果你想学习如何使用Markdown编辑器,可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。新的改变我们对Mar…

大家好,又见面了,我是你们的朋友全栈君。

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.效果

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/160243.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • clion 激活码2022_在线激活「建议收藏」

    (clion 激活码2022)这是一篇idea技术相关文章,由全栈君为大家提供,主要知识点是关于2021JetBrains全家桶永久激活码的内容IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.cn/100143.htmlFZP9ED60OK-eyJsaWN…

  • 跟我一起写 Makefile(二)

    跟我一起写 Makefile(二)三、make是如何工作的在默认的方式下,也就是我们只输入make命令。那么,   1、make会在当前目录下找名字叫“Makefile”或“makefile”的文件。   2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。   3、如果edit文件不存在,或是edit所依赖的后面的.o文件的文

  • 三星s4刷机教程(卡刷)

    三星s4刷机教程(卡刷)···············使用到的工具&#18

  • pcep协议什么意思_SDN学习笔记

    pcep协议什么意思_SDN学习笔记SDN什么是SDNSDN是一种框架和思想,核心诉求是通过软件控制网络,实现业务的自动化部署,为方便软件来控制网络,希望控制面和转发面是分离的。例如,传统的交换机内部,由交换机负责具体的网络流量往哪里转发,在SDN中,有一个控制器进行流量转发的计算,然后将结果发送给交换机,交换机只进行简单的转发,从分布式的控制转发过程称为集中式的控制,使得控制和转发平面相分离。SDN的特点网络开放可编程、数控分离(…

  • perl 正则表达式 匹配字符串 或逻辑

    perl 正则表达式 匹配字符串 或逻辑mark,备忘#!/usr/local/bin/perlusestrict;usewarnings;my@data=qw(nihaowohao);foreach(@data){if($_=~/^(ni|wo)hao$/){print$_}}

  • linux查看nfs端口命令,LinuxNFS端口命令是什么? 爱问知识人

    linux查看nfs端口命令,LinuxNFS端口命令是什么? 爱问知识人在Linux系统中,我们也会常遇到NFS的设置。针对这方面,我们这次主要讲解一下LinuxNFS的端口配置。看看如何设置可以调节好防火墙和端口的设置。#LinuxNFS服务固定端口及防火墙配置#1。在LINUX上正常安装NFS服务2。修改/etc/service,添加以下内容(端口号必须在1024以下,且未被占用)#Localservicesmountd1011/tcp#rpc。mount…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号