纹理基础
纹理采样
凹凸纹理计算方法
1 纹理坐标
1-1. direct3d API定义纹理坐标,起始点左上角
1-2. 其他API定义的纹理坐标,起始点左下角
Unity的默认网格有适合纹理映射的UV坐标,可以使用TEXCOORD(X)访问
:最后一位数字代表插值寄存器
float2 UV : TEXCOORD0;
float4 normal : TEXCOORD1;
1.1 采样函数:
tex2D(_MainTex, i.uv);
下面是最基本的UV坐标获取,先不转换空间:
//模型数据 struct VertexData { float4 position : POSITION; float2 uv : TEXCOORD0; }; //插值到片元函数中间变量 struct Interpolators{ float4 position : SV_POSITION; float3 uv : TEXCOORD0; }; Interpolators MyVertexProgram (VertexData v){ Interpolators i; i.position = UnityObjectToClipPos(v.position); i.uv = v.uv;//先不转换空间 return i; } float4 MyFragmentProgram (Interpolators i) : SV_TARGET { return float4(i.uv, 1); }
看其定点函数汇编:
vs_4_0 dcl_constantbuffer CB0[4], immediateIndexed dcl_constantbuffer CB1[21], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output_siv o0.xyzw, position dcl_output o1.xy dcl_temps 2 0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw 1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw 2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw 3: add r0.xyzw, r0.xyzw, cb0[3].xyzw 4: mul r1.xyzw, r0.yyyy, cb1[18].xyzw 5: mad r1.xyzw, cb1[17].xyzw, r0.xxxx, r1.xyzw 6: mad r1.xyzw, cb1[19].xyzw, r0.zzzz, r1.xyzw //0-6是变换空间操作 7: mad o0.xyzw, cb1[20].xyzw, r0.wwww, r1.xyzw //输出position 8: mov o1.xy, v1.xyxx //输出uv 9: retView Code
然后把图片拷贝进项目,并在shader中加上 _MainTex("MainTex", 2D) = "white" {} 这一句。
_MainTex是使用惯例命名可以用Material.MainTexture在脚本中访问到,当然也可以其他名字命名。
2D是Texture2D简写。代表着纹理类型
“white" {} :white是默认颜色,括号{}是fixed-function shader遗留下的产物,现阶段纹理赋默认值格式必须这样写!
增加纹理访问变量:sampler2D _MainTex。可以使用该变量访问纹理, 注意变量名与Properties中纹理命名需要对应。然后先在片元函数中试着采样并直接输出看看效果。
float4 MyFragmentProgram (Interpolators i) : SV_TARGET { return tex2D(_MainTex, i.uv); }
看看片元函数的汇编:
ps_4_0 dcl_sampler s0, mode_default //纹理访问变量 dcl_resource_texture2d (float,float,float,float) t0 //纹理数据 dcl_input_ps linear v1.xy //从顶点插值来的uv dcl_output o0.xyzw //输出的结果 0: sample o0.xyzw, v1.xyxx, t0.xyzw, s0 //采样 1: retView Code
1-3. 球体的极点纹理不规则
发生纹理变形是因为插值结果在三角形之间是线性的。而Unity的球体两极附近三角形又少,扭曲更加厉害。
1.2 Tiling and Offset 平铺和偏移:
区间在(0, 1),超过上下限会被自动重复
[NoScaleOffset]属性前缀,隐藏Tilling和Offset
//为了获取纹理的属性,unity语义提供了:纹理名_ST float4 _MainTex_ST; //S : scale; T: translation //_MainTex_ST.xy : 存储纹理缩放值 //_MainTex_ST.zw : 存储纹理偏移值 //对纹理缩放偏移计算 v.uv = a.uv * _MainTex_ST.xy + _MainTex_ST.zw; //unity内置宏计算纹理平移缩放 TRANSFORM_TEX(v.uv, _MainTex);View Code
1-4. 纹理的平铺模式,超过(0,1)之后如何平铺
- Repeat:到达边缘后,重复采样平铺。例如1.01、2.07、3.05,舍弃整数部分只对小数采样,无线重复。Clamp:到达边缘后,只对边缘最后一像素位置采样,被拉伸的感觉。
Mirror:镜像翻转。
Per_axis:单独给x和y轴设定平铺模式。
1-5. 纹理的过滤模式
shader中访问平铺和偏移,格式:_纹理名_ST:声明float4 _MainTex_ST。ST后缀可以指纹理的缩放和平移属性,xy代表平铺,zw代表偏移
i.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
除了上述自写方式,UnityCG.cginc也封装了这样的函数TRANSFORM_TEX(uv, tex)
i.uv = TRANSFORM_TEX(v.uv, _MainTex);
#define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
1.3 Texture Setting
Wrap Mode是对边缘像素处理选项:
- Repeat 边缘像素无限重复平铺
Clamp 边缘像素会与它们自己混合,会被无限拉伸
1-5. 左repeat, 右clamp. 在Tilling非(0,1)区间异常表现
Fileter Mode是对Wrap Mode的一次修正,计算量由低到高如下排列:
- Point:使用最近的纹素采样,如果纹素没有精确映射到像素,呈现质量就是像素风、块状感。
Bilinear:以自身为中心找到临近四个纹素做双线性插值,以得到更好的平滑。256纹素密度与256像素密度,如果放大到512像素密度会模糊,缩小到128像素密度会被锐化(丢弃了一些纹素)。 与之对应的解决办法就是使用MipMap。
Trilinear:一般不用,手机上太费了。
Ansio Level 各向异性
1-6. 各向异性level
1-7. 开启关闭控制
1-8开启、关闭ansio视觉效果
在透视投影下,一个纹理的投射角度差异,前后维度上会出现的视角上的扭曲,开启各向异性可以降低一定程度上影响。
- Disabled:关闭所有纹理Aniso Level,不论如何设置都不采样。
Per Texture: 单独为某个纹理开启,支持该纹理Ansio采样。
Forced On:为所有纹理强制开启, 也可单独关闭某个纹理(值为0)。
MipMap and Fade out MipMap
1-9. 淡出距离越远(与摄像机的距离越远),生成的MipMap就越模糊
1.4 Multiple Texture纹理合并
将两张和合并,一张主要纹理一张细节纹理
v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);//v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);// v.uv.xy * _DetailTex_ST.xy + _DetailTex_ST.zw; return o; } fixed4 frag (v2f i) : SV_Target { //fixed4 col = (tex2D(_MainTex, i.uv*10) * _Tint * 10) * tex2D(_MainTex, i.uv) ; float4 col = tex2D(_MainTex, i.uv.xy ) * _Tint; col *= tex2D(_DetailTex, i.uv.zw * 10)*2; return col; }
combine src1 * src2 |
例如将模型顶点颜色与纹理采样后相乘,或者多个纹理采样后相乘。结果颜色更深 |
combine src1 + src2 |
---。结果色更明亮 |
|
(1,0,0,1) - (1, 1, 0, 1) = (0,-1,0, 0)负数强制取正。为黑色 (1,1,0,1) - (1, 0, 0, 1) = (0,1,0, 0)为绿色 |
combine src1 lerp (src2) src3 |
使用src2的alpha值在src3和src1之间插值。注意,当为1时使用src1,当为0时使用src3。 Color.lerp(c_from , c_to , t); t为0返回from,t为1返回to |
combine src1 * src2 + src3 |
src1与src2的alpha相乘,结果加到src3。 |
SetTexture [TextureName] {Texture Block}
//SetTexture [_MainTex] { combine previous * texture, previous + texture }
- Previous 上一次SetTexture结果.
- Primary 来自光照计算结果或者顶点颜色. //diffuse, ambient and specular 三种颜色的集合
- Texture 指定TextureName的纹理.
constantColor 指定常量的颜色. //在Combie中定义一个常量颜色
2 Detail Texture
纹理可以丰富画面表现力,但是多大尺寸的纹理是合适的?纹理大小是由像素密度决定,更大的密度意味着更多的细节,存储过多的额外数据其实是一种浪费。这里提出平铺纹理的概念,以增加表现力。
2-1. 左图拉远看还行;右图拉近模糊伪影
2.1 多纹理采样
先在Fragment函数采样单张纹理并使用该结果
fixed4 frag (v2f i) : SV_Target { // sample sigle texture fixed4 col = tex2D(_MainTex, i.uv) * _Tint; return col; }
然后再次对该纹理采样,注意uv参数需要扩大10倍,这样做的目的就是是纹理平铺增加密度
fixed4 frag (v2f i) : SV_Target { // sample sigle texture fixed4 col = tex2D(_MainTex, i.uv) * _Tint; col = tex2D(_MainTex, i.uv * 10); return col; }
虽然执行了两次采样,但是我们却只使用了最后一次的采样结果。是不是觉得有点浪费?先看看编译代码
/OpenGL Core uniform sampler2D _MainTex; in vec2 vs_TEXCOORD0; layout(location = 0) out vec4 SV_Target0; vec2 u_xlat0; void main() { u_xlat0.xy = vs_TEXCOORD0.xy * vec2(10.0, 10.0); SV_Target0 = texture(_MainTex, u_xlat0.xy); return; } //DX 11 ps_4_0 dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps linear v0.xy dcl_output o0.xyzw dcl_temps 1 0: mul r0.xy, v0.xyxx, l(10.000000, 10.000000, 0.000000, 0.000000) 1: sample o0.xyzw, r0.xyxx, t0.xyzw, s0 2: ret
注意到有色文字,。编译器自动优化掉了无用的代码。
那如果我们想要这两次采样的结果,就不得不进行合并
fixed4 frag (v2f i) : SV_Target { // sample sigle texture fixed4 col = tex2D(_MainTex, i.uv) * _Tint;
//col = tex2D(_MainTex, i.uv); 这句编译器也会优化掉,把两次相同的采样只取一次,放在临时寄存器内 col *= tex2D(_MainTex, i.uv * 10); return col; }
2-2. 先采样平铺一次,再采样平铺10次
为什么呢?因为uv范围(0,1),放大10倍后,采样的面积加大了,然后再填充。看看汇编
//dx 11 ps_4_0 dcl_constantbuffer CB0[4], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps linear v0.xy dcl_output o0.xyzw dcl_temps 2 0: sample r0.xyzw, v0.xyxx, t0.xyzw, s0 //第一次采样 1: mul r0.xyzw, r0.xyzw, cb0[3].xyzw //采样结果与tint颜色相乘 2: mul r1.xy, v0.xyxx, l(10.000000, 10.000000, 0.000000, 0.000000)//uv放大10倍 3: sample r1.xyzw, r1.xyxx, t0.xyzw, s0 //第二次采样 4: mul o0.xyzw, r0.xyzw, r1.xyzw //相乘 5: ret
2.2 分离细节纹理
上面两张纹理合并之后,显得很黑。这是由于每个纹素的颜色通道值介于0到1。为了使原纹理变亮,就需要值大于1。将细节纹理颜色加倍,再与原始纹理颜色相乘。
fixed4 frag (v2f i) : SV_Target { // sample sigle texture fixed4 col = tex2D(_MainTex, i.uv) * _Tint; col *= tex2D(_MainTex, i.uv * 10) * 2; return col; }
这种直接扩大倍数的做法很粗暴。我们知道任何数乘以1不变,但是对细节纹理色加倍时,对1/2这个色就适用了。颜色区间是0-1,低于1/2的值将是结果变暗,高于1/2的值将变亮。用特殊的灰度细节纹理来处理。
灰度细节纹理?一般都是用灰度细节纹理来增白或加深原始颜色做二次细节调整,不是灰度图跳出的颜色不是那么直观的结果。
2.2 使用两个UV坐标
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float2 detailUV : TEXCOORD1;
};
struct v2f
{
float2 uv : TEXCOORD0;
float2 detailUV : TEXCOORD1;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex, _DetailTex;
fixed4 _MainTex_ST, _DetailTex_ST;
fixed4 _Tint;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.detailUV = TRANSFORM_TEX(v.detailUV, _DetailTex);
return o;
}
再看编译代码
vs_4_0 dcl_constantbuffer CB0[4], immediateIndexed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[21], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xy dcl_output o0.xy dcl_output o0.zw dcl_output_siv o1.xyzw, position dcl_temps 2 0: mad o0.xy, v1.xyxx, cb0[2].xyxx, cb0[2].zwzz//mad 是 v1 * cb0[2].xy + cb0[2].zw 1: mad o0.zw, v2.xxxy, cb0[3].xxxy, cb0[3].zzzw//翻译tex.xy * _MainTex_ST.xy + _MainTex_ST.zw 2: mul r0.xyzw, v0.yyyy, cb1[1].xyzw 3: mad r0.xyzw, cb1[0].xyzw, v0.xxxx, r0.xyzw 4: mad r0.xyzw, cb1[2].xyzw, v0.zzzz, r0.xyzw 5: add r0.xyzw, r0.xyzw, cb1[3].xyzw 6: mul r1.xyzw, r0.yyyy, cb2[18].xyzw 7: mad r1.xyzw, cb2[17].xyzw, r0.xxxx, r1.xyzw 8: mad r1.xyzw, cb2[19].xyzw, r0.zzzz, r1.xyzw 9: mad o1.xyzw, cb2[20].xyzw, r0.wwww, r1.xyzw 10: ret
OpenGL Core
void main() { vs_TEXCOORD0.xy = in_TEXCOORD0.xy * _MainTex_ST.xy + _MainTex_ST.zw; vs_TEXCOORD1.xy = in_TEXCOORD1.xy * _DetailTex_ST.xy + _DetailTex_ST.zw; //. . . }
DX11使用o0.xyzw一个变量存储两个UV的思想。同时也加了注释
// Output signature: // // Name Index Mask Register SysValue Format Used // -------------------- ----- ------ -------- -------- ------- ------ // TEXCOORD 0 xy 0 NONE float xy // TEXCOORD 1 zw 0 NONE float zw // SV_POSITION 0 xyzw 1 POS float xyzw
然后我们正式把Detail纹理加到主纹理上,
col *= tex2D(_MainTex, i.detailUV * 10) * 2;
2-4. 过度清晰
2.4 细节渐变
添加细节纹理是为了增加材质细节纹理表现,当靠近或拉远时,平铺细节变的很明显。因此,我们需要一种随着纹理的显示尺寸减小而淡化细节的方法。 我们可以通过将细节纹理渐变为灰色来实现此目的,因为这不会导致颜色变化。
上面有讲过!我们需要做的就是在细节纹理的导入设置中启用淡出Mip贴图。请注意,这也会自动切换过滤器模式为三线性,以便渐变到灰色是渐进的。
2.5.从远到近细节的变化
2.5 线性颜色空间
Liner Space & Gama Space
经过Gama矫正,它是对光强度的调整。最简单的做法是将值提高一定幂次,valueGama。该转换最初被引入是为了适应CRT显示器非线性特性,还有一个好处是它也大致对应于我们的眼睛对于不同光线的敏感程度。我们注意到暗色之间的差异大于亮色之间的差异。 因此,有意义的是将更多数字位分配给较暗的值而不是较亮的值。 指数运算可以将较低的值扩展到更大的范围,同时压缩较高的值。sRGB是使用最广泛的图像颜色格式。 它使用的公式比简单的幂运算更复杂,但是它存储的平均伽玛值为1 / 2.2的颜色。 在许多情况下,这是一个合理的近似值。 要将数据转换回原始颜色,请乘以2.2的伽玛校正。
2.6 Gama encoding vs decoding
Unity假定纹理和颜色存储为sRGB。 在伽玛空间中渲染时,着色器直接访问原始颜色和纹理数据。 这就是我们到目前所用的。在线性空间中渲染时,这不再成立。 GPU将纹理样本转换为线性空间。 同样,Unity还将材质颜色属性转换为线性空间。 然后,着色器将使用这些线性颜色进行操作。 之后,片段程序的输出将被转换回伽玛空间。使用线性颜色的优点之一是它可以实现更逼真的照明计算。 那是因为光的相互作用在现实生活中是线性的,而不是指数的。
切换到线性空间后,材质变得更暗。 为什么会这样?
2.7 Gama vs. Liner space
因为我们在细节纹理的采样结果加倍后,并不会改变主纹理。然而当转换到线性空间是(1/2)2.2≈0.22 ,加倍后是0.44远小于1,这就是变暗的原因。解决办法就是重新调整细节颜色,我们可以通过乘以(1 / (1/2)2.2)≈4.59代替常数2.注意这只能在线性空间下使用这一转换。幸运的是Unity.cginc定义好了这样一个float4变量unity_ColorSpaceDouble.
#ifdef UNITY_COLORSPACE_GAMMA #define unity_ColorSpaceDouble fixed4(2.0, 2.0, 2.0, 2.0) #else // Linear values #define unity_ColorSpaceDouble fixed4(4.59479380, 4.59479380, 4.59479380, 2.0) #endif
col *= tex2D(_MainTex, i.detailUV * 10) * unity_ColorSpaceDouble;
3 原文一和二和三
原文章很好,给他点赞、加鸡腿!