0.本文示例代码地址
1. 纹理和纹素
什么是纹理?
先直接把纹理理解成一张图片吧。纹理在渲染过程中有什么作用?
作用很多,也很重要。这里先只关注它最简单和最基本的作用:用来表现表面的漫反射颜色。可以这么理解:想象一个地球仪,把贴在地球仪表面的图片撕下来展开,就得到一个纹理,这个纹理在某一个位置的颜色就是地球仪表面对应位置的颜色。什么是纹素(texel)
纹理是一张图片,图片有大小,例如一张4x4的纹理,只有16个纹素,256x256的纹理,包含256x256个纹素,每个纹素具有单一的颜色。
2. uv 坐标
什么是 uv 坐标?
uv 坐标首先是一个二维坐标,而坐标用来确定一个位置,uv 就是用来确定一个纹素在某张纹理上的位置。uv 坐标的范围是多少?
纹理的大小各异,希望能用同一个范围内的坐标来表示所有大小的纹理的 uv 坐标,所以 uv 坐标通常是归一化后的坐标,区间在[0,1]。如何获得 uv 坐标,或者说 uv 坐标存放在哪里?
你需要渲染一个物体时,首先需要有这个物体的模型(顶点数据),以及某一个顶点对应图片的哪一个坐标,所以 uv 坐标是存放在顶点数据中的。在模型中,每一个顶点有一个属性,表示uv坐标。在3d游戏中,3d建模人员在建模软件中完成顶点绑定纹理坐标的操作。纹理采样是什么意思?
给定一个 uv 坐标和一张纹理,获得这张纹理在 uv 坐标处的纹素的颜色值。这个过程就是纹理采样。GPU 中有专门的纹理采样硬件单元。uv 坐标会超过取值范围吗?
通常存储在模型顶点中的 uv 坐标都在 [0,1] 区间内。但是在进行纹理采样时,传入的 uv 却不一定,这是因为我们可以在 着色器内对 uv 进行各种计算。
在进行纹理采样时,如果接受到一个 不在 [0, 1] 区间的纹理坐标时,如何确定该返回哪个纹素,由纹理的“平铺模式”确定。在 Unity 中,纹理的“平铺模式”通常由引擎开发选项进行设置。-
Unity 中的纹理坐标系
OpenGL 中纹理坐标原点在左下角,DirectX 中纹理坐标原点在左上角。Unity中采用和 OpenGL 一直的纹理坐标系。
3. 采样单张纹理
在之前的 Blinn-Phong 中,我们使用如下的方式来计算物体表面的漫反射部分颜色:
// 半兰伯特计算漫反射
fixed3 diffuse = _LightColor0.rgb * _DiffuseColor.rgb * (dot(lightDir, worldNormal) * 0.5 + 0.5);
这样我们只能表示一个纯色物体,当我们需要表示物体表面各个位置不同颜色时,可以将不同位置处的颜色记录到纹理中,然后在渲染的时候读取这个纹理,把纹理中存储的颜色取出来,应用到漫反射计算公式中的 _DiffuseColor 即可。
现在我们先不考虑高光、环境光和自发光的情况,仅仅考虑如何将一个图片“贴”到物体的表面。
先上整体 Shader 代码 04_Texture.shader:
Shader "Shader_Examples/04_Texture"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);// 顶点世界坐标
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 纹理采样
fixed3 diffuse = tex2D(_MainTex, i.uv);
return fixed4(diffuse, 1);
}
ENDCG
}
}
}
下面逐步分析这段代码的各个部分
3.1 纹理属性的声明
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
}
- 2D 是纹理类型属性的声明方式,"white" {} 是纹理的默认值,没有设置时使用内置的白色纹理作为参数。
3.2 顶点着色器输入和片段着色器输入
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};
- app_data 中使用语义TEXCOORD0 将模型第一组纹理坐标指定给 uv 字段,传递给顶点着色器
- v2f 中添加变量 uv,用来接受顶点着色器中的 uv 字段,以便片段着色器中使用
3.3 为属性声明相应的变量
sampler2D _MainTex;
float4 _MainTex_ST;
- _MainTex 是对应纹理属性的变量
- 针对每一个纹理属性,都必须声明一个 float4 类型的变量:纹理名_ST,可以通过这个变量获得纹理的缩放和平移值。也就是在 Unity 材质面板中如下的属性:
现在只需要知道声明的 _ST 变量与图中这两个值有关就行了。
3.4 顶点着色器
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);// 顶点世界坐标
o.uv = v.uv;
return o;
}
- 顶点着色器的基本作用:1. 顶点坐标变换
- 顶点着色器的基本作用:2. 将顶点的 uv 坐标传递给片段着色器
3.5 片段着色器
fixed4 frag (v2f i) : SV_Target
{
// 纹理采样
fixed3 diffuse = tex2D(_MainTex, i.uv);
return fixed4(diffuse, 1);
}
- tex2D 是一个内置操作,作用是根据纹理坐标,取出纹理对应的颜色
使用上面的 04_Texture.shader ,创建材质并指定给物体,然后照一张贴图指定给材质,可以得到如下的效果:
4. 采样贴图后加入环境光和高光
在将贴图“贴”到物体表面后,我们再给物体添加环境光和高光,关于高光和环境光,请看我之前的文章简单光照模型,添加环境光和高光后的 shader 代码如下:
Shader "Shader_Examples/04_BlinnPhong_Texture"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_SpecularColor ("SpecularColor", Color) = (1,1,1,1)
_Gloss ("Gloss", Range(8, 256)) = 20
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _SpecularColor;
float _Gloss;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);// 顶点世界坐标
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);// 法线变换
o.worldPos = mul(unity_ObjectToWorld, o.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 worldNormal = normalize(i.worldNormal);
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
float3 halfDir = normalize(lightDir + viewDir);
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 纹理采样
fixed3 diffuse = tex2D(_MainTex, i.uv).rgb * _LightColor0.rgb * (dot(lightDir, worldNormal) * 0.5 + 0.5);
// 高光
fixed3 specular = _LightColor0.rgb * _SpecularColor.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
return fixed4(diffuse + ambient + specular, 1);
}
ENDCG
}
}
}
渲染出的效果如下图:
4. 纹理的其它应用
4.1 法线纹理(法线贴图)
法线贴图用来表现物体表面的凹凸效果,在贴图中存储物体在 "凹凸" 效果下的法线信息,渲染时将存储的法线信息采样出来,替代物体表面法线来进行光照计算,“还原”凹凸效果。具体法线贴图的应用,可以参考Unity Shader 入门到改行5——法线贴图。
4.2 渐变纹理(渐变贴图)
渐变贴图是一种更灵活控制表面漫反射的方法,提供一张渐变贴图(通常是一维贴图),与传统的漫反射光照计算结合,可以提供各种有趣的渐变效果。
上图来自 UnityShader入门精要 中使用的3种渐变贴图和应用效果,对渐变贴图使用的核心代码是:
// Use the texture to sample the diffuse color
fixed halfLambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
fixed3 diffuseColor = tex2D(_RampTex, fixed2(halfLambert, halfLambert)).rgb * _Color.rgb;
fixed3 diffuse = _LightColor0.rgb * diffuseColor;
渐变贴图通常为 一维 贴图,所以这里采样时用的 u 和 v 坐标一样,从代码可以看出,表面的法线与光线越夹角越小,漫反射颜色越接近渐变贴图的右边颜色,表面法线与光线夹角越大,漫反射颜色越接近渐变贴图的左边颜色。
4.3 遮罩纹理(遮罩贴图)
遮罩纹理是一种可以更加精准控制模型表面性质的方法,例如,我希望物体的某一部分不会产生高光,可以生成一张遮罩纹理,把物体在不产生高光部分的颜色值分量r设置为0并写到遮罩纹理中。渲染时采样遮罩纹理,使用 r 分量与计算出的 specular 相乘得到最终的 高光分量。使用这种方法可以灵活和精准地控制表面渲染效果。