《Unity着色器圣经》6.0.2 | DXT压缩

目录索引

译文

法线贴图对于增加模型表面的细节来说非常有用。然而,法线贴图很大,这会给 GPU 造成较大的图形负担,导致移动设备发烫等影响用户体验的问题。出于上述原因,在着色器中压缩法线贴图是非常必要的,DXT 压缩就是压缩法线贴图时最常用的一种压缩手段。

使用 RGBA 通道时,每个像素需要 在帧缓冲中存储 32 位的信息。然而,DXT 压缩技术能够将纹理划分为 4 * 4 的像素块,然后仅使用其中两个通道(AG)进行压缩,从而将法线贴图优化为 1/4 分辨率。

要想理解这个概念,我们首先需要在片元着色器中计算法线贴图的 UV 坐标。为此,我们需要使用 tex2D 函数:

fixed4 frag (v2f i) : SV_Target
{
    fixed4 normal_map = tex2D(_NormalMap, i.uv_normal);
    ...
}

与默认的 col 向量一样,我们也生成了一个名为 normal_map 的四维(RGBA)向量,存储了法线贴图及其 UV 坐标。 RGBA 通道的范围介于 0 到 1 之间,但由于法线贴图的范围介于 -1 到 1 之间,因此必须对其进行修改。为此,我们将创建一个名为“DXTCompression”的新函数,在后续的片元着色器中使用:

float3 DXTCompression (float4 normalMap)
{
    #if defined (UNITY_NO_DXT5nm)
        return normalMap.rgb * 2 - 1;
    #else
        float3 normalCol;
        normalCol = float3 (normalMap.a * 2 - 1, normalMap.g * 2 - 1, 0);
        normalCol.b = sqrt(1 - (pow(normalCol.r, 2) + pow(normalCol.g, 2)));
        
        return normalCol;
    #endif
}

这个函数主要做了四件事:

  1. .函数根据情况返回一个三维向量。
  2. 函数定义了 UNITY_NO_DXT5nm,其功能是为不支持 DXT5nm 压缩的平台编译着色器,在这种情况下法线贴图将以 RGB 编码代替。
  3. 在 #else 条件中,我们只使用两个通道进行 DXT 压缩。如果我们注意观察,就会发现我们用 A 通道(normalMap.a)替换了 R 通道,然后用 G 通道作为第二个通道。
  4. 向量中的第三个通道(normalCol.b)被舍弃了,但随后使用函数为其赋了新的值:

sqrt(1 – (pow(normalCol.r, 2) + pow(normalCol.g, 2)))

为什么 B 通道等于这个公式的结果?答案就在于勾股定理:

在直角三角形中,斜边的平方等于两个直角边的平方和。

图片[1]-《Unity着色器圣经》6.0.2 | DXT压缩-软件开发学习笔记
Fig. 6.0.2a

即:

C² = A² + B²

因此同样地,如果我们想要计算一个三维向量的大小,就必须通过勾股定理来实现:

|| V ||² = A² + B² + C²

在我们变换法线、切线和副切线的坐标空间时,需要先将其“归一化”处理,使向量的大小为 1。那么,我们用于 B(Z)通道的值的平方与其余通道的值的平方和就应该等于 1:

1 = X² + Y² + Z² Or in its variation R² + G² + B² which is the same.

因此,B(Z)通道的平方就应该等于:

1 – (X² + Y²) = Z²

对其开平方,得到最终 B(Z)通道的结果为:

Z=√(1 − (x² + y²)​)

这个等式在 Cg 或 HLSL 中就写为:

normalCol.b = sqrt(1 – (pow(normalCol.r, 2) + pow(normalCol.g, 2)))

那么为什么我们要这么做呢?原先我们的法线贴图有 RGBA(XYZW)四个通道,在 DXT 压缩中,我们只用到了其中两个通道(AG),第三个通道被舍弃了。如此一来我们就不需要使用法线贴图的 B 通道的值,但我们需要利用上述公式计算出 B 通道,否则贴图将无法正常工作。

现在,在片元着色器中将 DXT 压缩应用到法线贴图上:

float3 DXTCompression (float4 normalMap){... }

fixed4 frag (v2f i) : SV_Target
{
    fixed4 normal_map = tex2D(_NormalMap, i.uv_normal);
    fixed3 normal_compressed = DXTCompression(normal_map);
    ...
}

我们刚刚完成的练习和包含在 UnityCg.cginc 中的 UnpackNormal 函数是等价的,这代表我们可以用该方法代替我们刚才编写的 DXTCompression 函数,并获得一样的效果:

fixed4 frag (v2f i) : SV_Target
{
    fixed4 normal_map = tex2D(_NormalMap, i.uv_normal);
    fixed3 normal_compressed = UnpackNormal(normal_map);
    ...
}

原文对照

Normal maps are very useful when generating detail in our objects, however, they are very heavy and produce a significant graphic load on the GPU. Likewise, if we are working on mobile devices, their processing will likely generate battery overheating, which could directly affect the user experience. For this reason, it will be essential to compress these textures within our shader.

DXT compression is one of the most commonly used to compress this type of image.

When working with RGBA channels, each pixel needs 32 bits of information to be stored in the frame buffer. However, DXT compression divides the texture into blocks of “four by four” pixels, then compressed using only two of their channels (AG), allowing the normal map to be optimized to ¼ resolution.

To understand this concept, we first have to calculate the normal map and its UV coordinates in the fragment shader stage. For this, we will use the tex2D function.

fixed4 frag (v2f i) : SV_Target
{
    fixed4 normal_map = tex2D(_NormalMap, i.uv_normal);
    ...
}

Like the default col vector, we have generated a four-dimensional vector (RGBA) called normal_map. The normal map and its UV coordinates have been stored in it. Its RGBA channels currently have a range between zero and one [0, 1]. This will have to be modified because the normal map has a range between minus one and one [-1, 1]. To do this, we will create a new function which we will call “DXTCompression” and position it over the fragment shader stage.

float3 DXTCompression (float4 normalMap)
{
    #if defined (UNITY_NO_DXT5nm)
        return normalMap.rgb * 2 - 1;
    #else
        float3 normalCol;
        normalCol = float3 (normalMap.a * 2 - 1, normalMap.g * 2 - 1, 0);
        normalCol.b = sqrt(1 - (pow(normalCol.r, 2) + pow(normalCol.g, 2)));
        
        return normalCol;
    #endif
}

Four main things are happening in the previous function:

  1. The function returns a three-dimensional vector according to a condition.
  2. It has been defined UNITY_NO_DXT5nm, which corresponds to a defined built-in shader and its function is to compile shaders for platforms that do not support DXT5nm compression, which means that the normal map will be encoded in RGB instead.
  3. In the #else condition, we have generated DXT compression using only two channels. If we pay attention, we will notice that we have replaced the “red” channel with the “alpha” channel (normalMap.a), and then we have used the “green” channel for the second channel..
  4. The third channel in the vector (normalCol.b) has been discarded, but then calculated independently using the function:

sqrt(1 – (pow(normalCol.r, 2) + pow(normalCol.g, 2)))

Why are we using this feature? The answer lies in the Pythagoras theorem. As we already know:

In every right triangle, the square of the hypotenuse equals the sum of the squares of the other two sides.

图片[1]-《Unity着色器圣经》6.0.2 | DXT压缩-软件开发学习笔记

Consequently.

C² = A² + B²

A vector in space generates a right triangle, so likewise, if we want to calculate the magnitude of a three-dimensional vector, we will have to do it through the Pythagoras theorem.

|| V ||² = A² + B² + C²

When we transform the space coordinates for the normal, tangent and binormal, we use the “normalize” function which returns a vector with a magnitude of one, likewise, the magnitude of the vector that we use for the coordinate B or Z; which is the same, equals one, then the operation would be as follows:

1 = X² + Y² + Z² Or in its variation R² + G² + B² which is the same.

For the operation we must calculate the coordinate B or Z, then we must make the sum of X and Y, 1.

1 – (X² + Y²) = Z²

Finally, by factorization, the above operation would equal:

Z=√(1 − (x² + y²)​)

Which in Cg or HLSL language would be translated as:

normalCol.b = sqrt(1 – (pow(normalCol.r, 2) + pow(normalCol.g, 2)))

Why are we doing this? Remember that our normal map has up to four channels (RGBA or XYZW) and in DXT compression we are only using two of them (AG). The third channel has been discarded in the normalCol vector; this means that we will not use the B coordinate values that are included in its normal map. Now, we must calculate this coordinate if not our map will not work correctly, that is why we carried out the previous operation, where we calculated a new normalized vector based on the AG coordinates.

Now we must apply the compression to the normal map, so we will go back to the fragment shader stage and pass the texture as an argument to the DXTCompression function as follows:

float3 DXTCompression (float4 normalMap){... }

fixed4 frag (v2f i) : SV_Target
{
    fixed4 normal_map = tex2D(_NormalMap, i.uv_normal);
    fixed3 normal_compressed = DXTCompression(normal_map);
    ...
}

The exercise we have just performed is equivalent to the UnpackNormal function which is included in UnityCg.cginc, this means that we could replace the DXTCompression function with the latter and get the same result.

fixed4 frag (v2f i) : SV_Target
{
    fixed4 normal_map = tex2D(_NormalMap, i.uv_normal);
    fixed3 normal_compressed = UnpackNormal(normal_map);
    ...
}
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容