关于静态纹理的遍历与读写

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
发表于 2023-1-5 11:42:06 | 显示全部楼层
前言

——“静态纹理”在本文指的是U3D里的Texture2D,UE里的UTexture2D。
UE蓝图

——经过我猜Palette的关键词+外网翻资料的结果来看,蓝图里好像并没有对静态纹理进行读写操作的相关节点(除非你自己写一个节点,也就是说C++可以办到,一会讲),所以这个先略。
——但对于RT(U3D里的RenderTexture,UE里叫RenderTarget,之后我学到这里会单开一篇文来讲)却有这么一对节点可以读到像素值。


——不过这俩方法看注释应该不适合实时调用,估计是涉及到GPU回传CPU的问题。
UE C++

——折磨了一个月的感受,UE的API感觉分的都比较底层比较碎。有很多原本在U3D里调一个方法就解决的问题,到UE里可能需要你配合着用三四个方法才能实现相同的效果,并且其中很多方法可能单看名字和注释都很难理解是什么作用,这次的纹理读写也同样。不过,更底层的操作也意味着更大的优化空间,这个见仁见智。
——目前来看,有问题建议还是看源码orUE的社区去搜,或者能科学上网的去Google搜。
1、读写

——为方便讲解,我先拆成下面几步来分别分析。
1.1、创建
UTexture2D* TexIN = UTexture2D::CreateTransient(SizeX, SizeY, PF_B8G8R8A8);
——CreateTransient的源码实现中会做一些下面的PlatformData ,Mip,BulkData等相关数据的初始化。
——这步不是必要的,你也可以从外部传一个TexIN进来。
1.2、获得纹理相关数据的结构体
FTexturePlatformData* PlatformData = TexIN->PlatformData;
——FTexturePlatformData结构体中主要是一些纹理长宽,颜色类型,mip级别等数据。
1.3、获得指定的mip级别
FTexture2DMipMap* Mip = &PlatformData->Mips[0];
——Mips[0]拿的就是原始分辨率的图像,Mips[n]则代表原尺寸的0.5^n倍,即mipmap的那套算法,感兴趣可以自行百度。
1.4、获得当前mip的数据集
FByteBulkData* BulkData = &Mip.BulkData;
——FByteBulkData 大概就是存了和当前Mip相关数据的一个东西。
1.5、锁定当前图像并获得头指针
uint8* Ptr0 = (uint8*)BulkData->Lock(LOCK_READ_ONLY);
——这一行实际上干了两件事,一是用Lock方法把TexIN暂时锁起来,防止接下来在读写操作时其他线程对TexIN进行操作产生隐患;二是Lock方法能顺带返回这个图像的void*格式的头指针(可以理解为第一个像素的第一个通道的内存地址),接下来的读写都要用它去做地址偏移。
——到目前为止,上面的操作也可以通过下面一行代码来完成。
uint8* Ptr0 = (uint8*)TexIN->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_ONLY);
——至于指针类型具体要转成什么,取决于你的像素颜色格式。比如1中的PF_B8G8R8A8,他就是每个通道颜色是无符号8位整型,所以用范围是0~255的uint8(相当于C#里的byte)。
——需要注意的是,如果之后要做写入操作,Lock中的关键字要换成LOCK_READ_WRITE
1.6、按像素位置进行地址偏移
——先补充一点底层知识。图像的像素值在内存上的地址是连续的,并且各通道按照像素颜色格式被顺序拆开排在一起。比如1中的PF_B8G8R8A8格式,它在内存中实际上就是下面这样排列。


——每个像素占4个8位大小的内存块,其中每个通道按BGRA的顺序排列,每个像素按顺序头尾相接,构成一个一维数组。
——所以只要我们拿到头指针,后面想拿任何位置的像素值就都可以为所欲为。写过openCV的可能比较熟,比如我想读取坐标为(X,Y)的像素值,可以用类似下面的代码来算。
int32 Idx = 4 * (X + Y * TexIN->GetSizeX());
uint8 R = Ptr0[Idx + 2];
uint8 G = Ptr0[Idx + 1];
uint8 B = Ptr0[Idx + 0];
uint8 A = Ptr0[Idx + 3];
——之所以R是+2是因为像素格式是PF_B8G8R8A8,其他情况按对应情况改变顺序即可,当然你也可以用*Ptr的指针方式去拿。
——还有一点需要说明的是,UE里的纹理坐标原点在左上角,遍历顺序是从上到下,从左到右,和U3D坐标原点在左下角不同。
——如果是写入,除了改Lock(LOCK_READ_WRITE)外,把上面代码的等号两边对换即可。
1.7、解锁Tex
——这行很重要,和5的Lock方法是一对。是的,你需要在完成相关操作后手动解锁。
BulkData->Unlock();
——如果没有这行,你会发现刚跑起来一切正常,但在结束运行时,UE光荣崩溃。
1.8、动态修改MipGenSettings
——这是个坑点,我在找的几篇文章里好像都没看到有说。之前我做测试的时候有时候会崩有时候不会,有时候拿得到像素值有时候拿不到,经排查是纹理导入设置的MipGenSettings导致的。


——如果是从外部拖进UE的图,它默认是上面的FromTextureGroup;但如果是用1中的CreateTransient创建出来的,默认是NoMipmaps。
——经测试,当MipGenSettings是NoMipmaps或LeaveExistingMips时,Lock方法能返回有效的指针,其他都会返回空指针,所以改动方法就是在第2步前改一下MipGenSettings,然后在7之后再改回来,关键代码如下。
TexIN->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
TexIN->UpdateResource();//这行作用类似于U3D的AssetDatabase.Refresh,即更新设置到磁盘中
2、案例

——这里我封装了个读取像素值的方法,如果要写入像素值按之前说的改就行。
void GetPixelColor32(FColor& ColorOUT, UTexture2D* TexIN, int32 x, int32 y, bool bChangeMipSetting2No = true)
{
        if (x < TexIN->GetSizeX() && y < TexIN->GetSizeY())
        {
                //修改mip设置
                TextureMipGenSettings OldMipSetting;
                if (bChangeMipSetting2No)
                {
                        OldMipSetting = TexIN->MipGenSettings;
                        TexIN->MipGenSettings = TextureMipGenSettings::TMGS_NoMipmaps;
                        TexIN->UpdateResource();
                }

                //获得像素值
                uint8* Ptr0 = (uint8*)TexIN->PlatformData->Mips[0].BulkData.Lock(LOCK_READ_ONLY);

                if (Ptr0)
                {
                        uint8* TempPtr = &Ptr0[4 * (x + y * TexIN->GetSizeX())];
                        ColorOUT.B = *TempPtr;
                        ColorOUT.G = *++TempPtr;
                        ColorOUT.R = *++TempPtr;
                        ColorOUT.A = *++TempPtr;
                }
                else
                {
                        GLog->Log("ptr invalid");
                }

                TexIN->PlatformData->Mips[0].BulkData.Unlock();

                //重置mip设置
                if (bChangeMipSetting2No)
                {
                        TexIN->MipGenSettings = OldMipSetting;
                        TexIN->UpdateResource();
                }
        }
        else
        {
                GLog->Log("index out boundary");
        }
}
3、补充

U3D

——U3D的遍历顺序是从左到右,从下到上,原点在左下角。
——U3D常用API的有两套,Texture2D.GetPixel和Texture2D.GetPixels32。
——如果你想要处理的是外部导入的图像,请确保Texture2D.isReadable = true,也就是导入设置中的Read/Write Enabled是勾上的。而且这个只能手动勾,不能用常规的代码控制修改。(当然,如果你硬想要拿到一个没勾RW的Texture2D去遍历也不是没办法,比如拿RT渲一下然后再ReadPixels之类的)
//Pixel系列是通用的。如果你的图像色阶超过255,比如某些RGBfloat类型的属性遮罩或HDR图,那么务必用Pixel系列
Color col = texIN.GetPixel(x, y, mipLevel);//返回值各分量是0~1的float
texIN.SetPixel(x, y, colorIN, mipLevel);

//如果你的图像只是一般的255阶颜色,追求性能极致可以考虑用32系列,速度会比上面快很多,但需要创建一个临时的Color32数组
Color32[] cols= texIN.GetPixels32(mipLevel);//Color32的各分量是0~255的int
//也是生成一个一维数组,获取指定位置的像素值可参考之前的UE
texIN.SetPixels32(cols, mipLevel);

//最后,还需要用Apply将更改更新到GPU,否则是没有效果变化的
texIN.Apply();
回复

举报 使用道具

您需要登录后才可以回帖 登录 | 立即注册
快速回复 返回顶部 返回列表