combining texture 所解决的问题:纹理过小。大纹理会产生存储浪费,使用 tile 的方法又很容易看到重复的模式,所以使用纹理混合方案:将一个 untiled texture 和一个 tiled texture 进行混合,得到比较经济实用的结果。
Combining: Use multiple textures at the same time.
1 A × A':自己叠自己
1.1 粗糙的想法 Rough Idea
定义 A 为原图(Untiled Texture),A' 为 A 图的 Tile 版本(Tiled Texture),n 为 Tiled 的密度;
为什么定义 A' (Tiled A)时需要将其 × 2?
对一张纹理进行时,结果纹理的颜色会变暗,因为混合操作是将颜色进行简单叠加的。
为了使画面变亮,选取方法为 A 图叠 (A' 图 × 2),虽然 A' 图部分看起来有点过曝。
1.2 CODING
Shader "Code/PA2_Texture/A&A'"
{
Properties {
_MainTex("Texture", 2D) = "white"{}
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
i.position = mul(unity_MatrixMVP, v.position);
return i;
}
float4 Frag(Interpolators i): SV_Target {
float4 color = tex2D(_MainTex, i.uv);
color *= tex2D(_MainTex, i.uv * 10) * 2;
return color;
}
ENDCG
}
}
}
2 A × B:叠加灰度图
2.1 粗糙的想法 Rough Idea
A × A' (A' = nA × 2) 会使 A' 过曝。解决这个问题,考虑另外使用一张灰度图 B 与 A 图进行叠加。
为新增的灰度图 B 添加一个 Property 及其控制柄来使用它。分配材质,并为设置其 Tilling;
在结构体 Interpolators 中增加一个 UV(此时结构体中包含主纹理 uv 、细节纹理 uvDetail);
在其后的 fragment shader 中,对主纹理坐标和细节纹理坐标分别进行转换(使用两次 TRANSFORM_TEX
宏),连同转换为屏幕空间后的顶点坐标一起打包到 Interpolators 结构体的实例中返回。
什么是灰度图 B?
灰度图 B 是只包含亮度值的图片,所有像素表现为从白到黑的不同灰度。
因为不包含任何颜色信息,所以经常被用于处理亮度或对比度的操作。对亮度进行控制时,以 0.5 作为基准,高于 0.5 时会使图像变亮,反之使图像变暗。此时可以生成细致的纹理混合效果。
如何得到一张灰度图?
灰度图的生成通常是通过将彩色图像的 RGB 值转换为一个单一的亮度值。这个转换的标准方法是根据 RGB 颜色空间中的加权平均值来计算:
这个公式反映了人眼对不同颜色的敏感度。得到的灰度值决定了图像中像素的亮度。通过这种方式,明亮的区域会变深,而较暗的区域会变浅,形成均匀的灰度图像。
2.2 CODING
Shader "Code/PA2_Texture/A&B"
{
// 增加新的 Property
Properties {
_MainTex("Texture", 2D) = "white"{}
_DetailTex("Detail Texture", 2D) = "gray"{}
// gray:表示如果在材质中没有为 Detail Texture 手动分配纹理, Unity 将使用内置的灰色纹理作为默认值。
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
sampler2D _MainTex, _DetailTex;
float4 _MainTex_ST, _DetailTex_ST;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
float2 uvDetail: TEXCOORD1; // 增加 detail uv
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
i.uvDetail = TRANSFORM_TEX(v.uv, _DetailTex); // 分别对顶点纹理坐标进行转换
i.position = mul(unity_MatrixMVP, v.position);
return i;
}
// 最后,在 fragment shader 里将两个纹理采样并混合
float4 Frag(Interpolators i): SV_Target {
float4 color = tex2D(_MainTex, i.uv);
color *= tex2D(_DetailTex, i.uvDetail) * unity_ColorSpaceDouble;
return color;
}
ENDCG
}
}
}
2.3 渐隐模式:Fading Details
当纹理缩小或放在远处时,过于清晰的纹理会使得 tiling 的模式看起来非常明显。因此需要在纹理中进行 Mipmap 的设置。
细节纹理中设置 Fadeout to Gray,同时可以将 Filter Mode 设置为 Trilinear Mode(默认为 Bilinear)。这样一来,细节纹理将在画面放远时逐渐置灰,且不改变结果颜色。
3 喷射战士:Splatting Map
细节纹理被重复使用在了全图范围。如果面对更加复杂的场景,想要对不同的表面使用不同的纹理,就需要新的、非全局的混合办法。
3.1 天才的想法 Genius Idea
需要一个布尔值来判定每个片元使用纹理 A or 纹理 B,Splat map 就是实现这个布尔判定的纹理图;
添加 Properties 和与之对应的变量。因为一般情况下,对于一个场景内所有的纹理,其 Tilling 和 Offset 设置都相同,而 splat map 不做任何 Tile。因此我们可以只在 splat map 上设置 tile,统一应用于所有纹理,如此可以减少信息传输与计算量;
在结构体 Interpolators 中加入新的 UV(uvSplat),并在顶点着色器中为它绑定顶点;
fragment shader: 用 splat 的 RGB 通道分量来进行取值并输出。
3.2 准备工作
开启 Splat map 的 Bypass sRGB Sampling。
因为这张 map 是用来进行操作的,而非使用其中的颜色或亮度信息,因此 splat map 中的颜色值需要直接用于计算,而不经过 sRGB 到线性空间的转换。否则会产生不准确的混合效果
设置它的 Wrap Mode 为 clamp。即不会进行 tile 效果。
为纹理 A 和纹理 B 设置一些 Tilling,比如 4 × 4。
3.3 CODING
// key:理解 tex2D 函数。即接受一个纹理采样器和一个 UV 坐标,输出该位置的颜色值。
Shader "Code/PA2_Texture/Splat"
{
Properties {
// _MainTex 为 splat map;_Texture1 和 _Texture2 分别为待混合的两个纹理
_MainTex("Splat Map", 2D) = "white"{}
[NoScaleOffset] _Texture1("Texture 1", 2D) = "white" {}
[NoScaleOffset] _Texture2("Texture 2", 2D) = "white" {}
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "UnityCG.cginc"
sampler2D _MainTex;
sampler2D _Texture1, _Texture2;
float4 _MainTex_ST;
struct VertexData {
float4 position: POSITION;
float2 uv: TEXCOORD0;
};
struct Interpolators {
float4 position: SV_POSITION;
float2 uv: TEXCOORD0;
float2 uvSplat: TEXCOORD1;
};
Interpolators Vert(VertexData v) {
Interpolators i;
i.position = mul(unity_MatrixMVP, v.position);
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
i.uvSplat = v.uv;
return i;
}
// 最后在片元着色器中完成最终的采样
float4 Frag(Interpolators i): SV_Target {
float4 splat = tex2D(_MainTex, i.uvSplat);
return tex2D(_Texture1, i.uv) * splat.r + tex2D(_Texture2, i.uv) * (1-splat.r);
}
ENDCG
}
}
}
Properties 中 [NoScaleOffset]
是何用意?
[NoScaleOffset]
是一种 Unity Shader 属性修饰符,用于指定该纹理属性在材质编辑器中不会显示或应用 Tiling
和 Offset
设置。
以及因为不需要纹理 A 和纹理 B 的控制柄,因此在变量声明部分不必添加 _ST 后缀的属性变量。
如何理解 vertax shader 中 i.uv
和 i.uvSplat
的赋值?
i.uv
用于两个将要进行混合的纹理。其使用宏 TRANSFORM_TEX
,借用 _MainTex
(主纹理,Splat map) 的 Tiling 和 Offset 为其设置变化量;
i.uvSplat
用于 Splat map,其直接使用原始 UV 坐标。
如何理解 fragment shader 的逻辑?
首先为 splat 使用 tex2D
函数绑定纹理对象与纹理坐标;
在 return 语句中,用 splat 的 RGB 通道来进行取值。
3.4 RGB 喷射战士
原理同上。只需要增加 Properties 与其对应属性,然后在片元着色器中返回多色通道即可。
// RGB 通道的片元着色器返回值
return
(_Texture1, i.uv) * splat.r +
(_Texture2, i.uv) * splat.g +
(_Texture3, i.uv) * splat.b +
(_Texture4, i.uv) * (1 - splat.r - splat.g - splat.b);
}
APPENDIX
Anki
Reference
GAMES101 - Lecture 9 - Shading3 (Texture Mapping cont.)