我有一个二维纹理数组,需要将它渲染到屏幕上,屏幕上的每个像素来自于数组中不同下标的纹理的对应位置的像素。举个例子,纹理坐标 (0.2,0.3)(0.2, 0.3)(0.2,0.3) 处的像素,它对应的纹理数组的下标是 333,那么就去第333幅纹理里采样 (0.2,0.3)(0.2,0.3)(0.2,0.3)这个纹理坐标的像素。
为了保存屏幕上每个像素(准确来说是子像素)来自纹理数组的哪张纹理,我们将每个像素对应的纹理数组下标也保存到一张纹理里面,暂且叫它索引纹理吧,这样我们只需要对索引纹理进行采样,就能得到每个子像素对应的纹理数组下标了。
虽然 Unity Shader 支持纹理数组(怎么用最后再说),但是我的纹理数组是从相机渲染得到的,我尝试了很多办法,都无法从相机直接渲染到纹理数组,总是只能渲染到纹理数组的第一张纹理上,不知道是真的不行还是我没找到方法。所以我现在只能将纹理数组都渲染到一张大纹理上,相当于把一位数组变成了二维数组。
现在有了网格纹理,也有了索引纹理,只需要在 shader 的片元着色器中完成采样就可以了。思路也很简单,先确定当前像素来自第几幅图,然后将纹理坐标重定位到该子图内,最后用重定位的纹理坐标对网格纹理采样就可以了。首先是纹理坐标重定位,这个过程其实很简单,假设网格纹理有 RRR 行 CCC 列。
- 首先计算纹理坐标在子图内的坐标。假设网格纹理的大小是 W×HW\times HW×H,则每个子图的大小只有 WC×HR\frac{W}{C}\times\frac{H}{R}CW×RH,相当于 宽高分别缩小了 CCC 和 RRR 倍,因此将纹理坐标 uvuvuv 也分别缩小 CCC 和 RRR 倍就是纹理坐标在子图中的位置。
- 然后要计算子图在网格纹理中的偏移量,再加上第一步得到的经过缩放的纹理坐标,就是网格纹理的采样坐标。由于纹理坐标是归一化到 0→10\rightarrow 10→1 的,所以这一步的关键是计算子图在宫格纹理里是第几行,第几列。假设要采样第 iii 行 jjj 列的子图,那么纹理坐标偏移就是 (jC,iR)(\frac{j}{C},\frac{i}{R})(Cj,Ri)。

所以现在的问题是计算子图在第几行第几列。我们将纹理数组的下标存到了索引纹理中,但是 shader 中的图像像素也是归一化的,因此在生成索引纹理时,我们就已经对下标进行了归一化,也就是索引纹理里面存放的其实是 i数组长度\frac{i}{数组长度}数组长度i。所以我们用索引纹理的采样结果乘以数组长度(R×CR\times CR×C)就能得到真正的下标了,接下来就是一维数组下标转二维数组下标的问题了,这是个很简单的问题。
c
float2 redirectUV(float3 uv)
{
int count = R * C;
int index = floor(uv.z * count)
int x = index % C;
int y = index / C;
return float2((x+uv.x)/C, (y+uv.y)/R)
}
float3 rgb = tex2D(_IndexTex, uv)
float2 uv_r = redirectUV(float3(uv, rgb.r));
float2 uv_g = redirectUV(float3(uv, rgb.g));
float2 uv_b = redirectUV(float3(uv, rgb.b));
// sample grid texture
上面我们为了计算子图的横纵坐标,需要先计算子图总数,然后得到子图下标,再转化成行列坐标。那么有没有办法不计算子图总数,也不计算下标,直接得到行列坐标呢?
有的,兄弟!有的!
我们可以换个思路想这个问题,假设有一根长度为 111 的线段,我们先将他分成 RRR 大段,然后再将每一大段分成 CCC 小段,那么其实我们要求解的问题变成了这个归一化的索引下标落在哪一大段,哪一小段。
我们将归一化的索引乘以 RRR,它的整数部分就是行数;而它的小数部分乘以 CCC 就是列数,在 shader 里我们可以使用 modf
函数来拆分浮点数。
c
float2 redirectUV(float3 uv)
{
float2 ov;
ov.x = floor(modf(uv.z * R, ov.y) * C);
return (ov + uv.xy) / float2(C, R);
}
float3 rgb = tex2D(_IndexTex, uv)
float2 uv_r = redirectUV(float3(uv, rgb.r));
float2 uv_g = redirectUV(float3(uv, rgb.g));
float2 uv_b = redirectUV(float3(uv, rgb.b));
// sample grid texture
这样只用一行代码就能把行列坐标同时计算出来了,这个公式可以从前面的方法中推导出来,感兴趣的朋友可以试一试。
unity shader 中使用纹理数组
最后说一说怎么在 unity shader 中使用纹理数组。
首先声明纹理的时候要使用 2DArray
类型。
Properties
{
_MainTex ("Tex", 2DArray) = "" {}
}
然后是在 Pass
里面声明采样器,普通2D纹理通常是:
sampler2D _MainTex;
float4 _MainTex_ST;
而纹理数组使用的是:
UNITY_DECLARE_TEX2DARRAY(_MyArr);
最后是纹理采样,需要使用下面的函数:
UNITY_SAMPLE_TEX2DARRAY(_MyArr, i.uv);
注意这里的 uv
是 float3
,z
分量就是纹理数组的下标,这里不是归一化下标,是真实下标。
在 C# 中创建 RenderTexture
纹理数组需要在纹理真正创建前设置下面的属性:
csharp
renderTexture.dimension = UnityEngine.Rendering.TextureDimension.Tex2DArray;
renderTexture.volumeDepth = arrayLength;