Blender→Unity間で8bitテクスチャ(PNG)で数値の受け渡しを行う

## はじめに
前回解説した記事では、Unityの色空間設定がLinerでなければ使えないとのことでした。
[Blender→Unity間で32bitテクスチャ(EXR)で数値の受け渡しを行う](https://qiita.com/nekoco/items/47028af8595c0c09f6cd)

しかし、そうなるともっと汎用性が欲しくなります。
というわけでインポート時に色空間が設定できるPNGテクスチャで数値の受け渡しを行いたいと思います。
（基礎的なビット演算の内容となります）

# Blender
Blenderのバージョンは4.1.1 Stableを使っていきます。

### テクスチャ作成
前回までと内容が被っているため、クラス化まで飛ばします。

```python
import bpy
import numpy as np

class TextureClass:
    def __init__(self, texture_name, width, height):
        self.image = bpy.data.images.get(texture_name)
        if not self.image:
            self.image = bpy.data.images.new(texture_name, width=width, height=height, alpha=True)
        elif self.image.size[0] != width or self.image.size[1] != height:
            self.image.scale(width, height)
        
        self.point = np.array(self.image.pixels[:])
        self.point.resize(height, width * 4)
        self.point[:] = 0

        self.point_R = self.point[::, 0::4]
        self.point_G = self.point[::, 1::4]
        self.point_B = self.point[::, 2::4]
        self.point_A = self.point[::, 3::4]
    
    def SetPixel(self, py, px, r, g, b, a):
        self.point_R[py][px] = r
        self.point_G[py][px] = g
        self.point_B[py][px] = b
        self.point_A[py][px] = a

    def Export(self):
        self.image.pixels = self.point.flatten()


image = TextureClass('CustomTexture', 1, 1)
image.SetPixel(0, 0, 0.5, 0, 0, 0.5)
image.Export()
```

### 変数を分割
まず、主な整数型のintや浮動小数型のfloatは32bitです。

そして8bitテクスチャにRGBAがあるので、各色に2進数で分割しながら情報を書きこめば、8bit×4=32bitとなって1ピクセルにデータを納めることが出来ますね！

...

といきたいところですが、1つ確認事項があります。

それはAが0の時です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299484/f29e08ae-5cbc-0388-610c-ae70643728be.png)

目視できる色はありませんが、そのまま値を渡せるのでしょうか？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299484/bdf14673-b8cf-051c-f976-9df4587f0259.png)

実は大丈夫なようです。
安心しました。

それでは気にすることなく、各種関数を作成していきましょう。

```diff_python
import bpy
import struct
import numpy as np

class TextureClass:
    def __init__(self, texture_name, width, height):
        self.image = bpy.data.images.get(texture_name)
        if not self.image:
            self.image = bpy.data.images.new(texture_name, width=width, height=height, alpha=True)
        elif self.image.size[0] != width or self.image.size[1] != height:
            self.image.scale(width, height)
        
        self.point = np.array(self.image.pixels[:])
        self.point.resize(height, width * 4)
        self.point[:] = 0

        self.point_R = self.point[::, 0::4]
        self.point_G = self.point[::, 1::4]
        self.point_B = self.point[::, 2::4]
        self.point_A = self.point[::, 3::4]
    
    def SetPixel(self, py, px, r, g, b, a):
        self.point_R[py][px] = r
        self.point_G[py][px] = g
        self.point_B[py][px] = b
        self.point_A[py][px] = a

    def Export(self):
        self.image.pixels = self.point.flatten()


+ def SplitFloat(f):
+     return [a / 255 for a in struct.unpack('BBBB', struct.pack('f', f))]
+ 
+ def SplitInt(i):
+     return [a / 255 for a in struct.unpack('BBBB', struct.pack('I', i))]
+ 
+ def SplitInt16(i1, i2):
+     return [a / 255 for a in struct.unpack('BBBB', struct.pack('HH', i1, i2))]


image = TextureClass('CustomTexture', 2, 2)
image.SetPixel(0, 0, *SplitFloat(0.12345))
image.SetPixel(0, 1, *SplitInt(123456))
image.SetPixel(1, 0, *SplitInt16(1234, 1234))
image.Export()
```

内容としては、32bitを8bitに分割し、0~255を0~1の範囲にする関数です。

```python
def SplitFloat(f):
    return [a / 255 for a in struct.unpack('BBBB', struct.pack('f', f))]

def SplitInt(i):
    return [a / 255 for a in struct.unpack('BBBB', struct.pack('I', i))]

def SplitInt16(i1, i2):
    return [a / 255 for a in struct.unpack('BBBB', struct.pack('HH', i1, i2))]
```

# Unity
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299484/b6c70b5c-870c-d40f-af89-4abf4fec399d.png)

インポートするテクスチャの設定は以下の通りです。
sRGB：False
Format：RGBA 32bit

#### Shader
RGBA(fixed4)を元の変数型に戻す関数を作成します。
数値表示のシェーダーは、前回と同じくブタジエン氏の物を使用します。

```hlsl
fixed4 frag(v2f i) : SV_Target
{
    int v1, v2;
    CombineInt16(tex2D(_Texture, float2(0, 0.5)), v1, v2);

    fixed4 col = 
          numbercol(float2(i.uv), CombineFloat(tex2D(_Texture, float2(0, 0))), 5, 1, 3, 4)
        + numbercol(float2(i.uv), CombineInt(tex2D(_Texture, float2(0.5, 0))), 5, 2, 7, 1)
        + numbercol(float2(i.uv), v1, 5, 3, 7, 1)
        + numbercol(float2(i.uv), v2, 5, 4, 7, 1)
        ;

    return col;
}
```

関数の内容としては、0~1を0~255に戻して、順番通り並べています。

```hlsl
float CombineFloat(fixed4 color){
    return asfloat(uint(color.r * 255.0) | (uint(color.g * 255.0) << 8) | (uint(color.b * 255.0) << 16) | (uint(color.a * 255.0) << 24));
}

float CombineInt(fixed4 color){
    return asint(uint(color.r * 255.0) | (uint(color.g * 255.0) << 8) | (uint(color.b * 255.0) << 16) | (uint(color.a * 255.0) << 24));
}

void CombineInt16(fixed4 color, out int v1, out int v2){
    v1 = asint(uint(color.r * 255.0) | (uint(color.g * 255.0) << 8));
    v2 = asint(uint(color.b * 255.0) | (uint(color.a * 255.0) << 8));
}
```


#### 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299484/c997bde1-911f-93d7-f3e9-b006aa60f1c3.png)

ちゃんと復元できていますね。

# 懸念点
この方法の欠点としては、テクスチャの読みこみ回数が増えることです。

exr形式のテクスチャを1回参照した時のデータ量は32×4=128なのですが、pngでは8×4=32と読みだせるデータ量が少ないです。

つまりexrと同等のことをしようとすると4倍テクスチャを参照しなければなりません。

自分が作成したVATではexrで6回参照していたため、pngに変更するとなると24回参照することになります。

そのため汎用性を持たせるために、GPU負荷を増やすことになりますね。