作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要笔记7:基础纹理A
原文发布时间:2020年3月6日
文章类型: 授权转载
笔记当前使用的Unity版本:“2019.3.3”
笔记当前Unity最新的版本:“2020.1.0.Alpha 25”
把失败归结于他人的恶意,恰恰证明了你的无能。——拿破仑
该篇笔记主要涉及到《UnityShader入门精要》的第7章基础纹理,在基础纹理中,我们要学会如何使用Shader为一个模型绘制贴图,使用不同的贴图(比如法线贴图)来增加模型的细节,该笔记会分AB两个部分。ok~让我们来试试吧。
纹理最初的目的就是使用一张图片来控制模型的外观。通过纹理映射(Texture Mapping)技术,我们可以把一张图贴在模型的表面,逐纹素(texel)(其实就是逐像素,只不过要和渲染时,屏幕的上的像素做一个术语的区分)的控制模型表面的颜色。
在美术人员建模的时候,通常会在建模软件上利用纹理展开技术把纹理映射坐标(texture-mapping coordinates)存储在每个顶点上,通常这些坐标使用一个二维变量(u,v)来表示,其中u是横向坐标,而v是纵向的坐标。因此纹理映射坐标也被称为UV坐标。
尽管纹理的坐标大多都是不同的,比如256*256,1024*1024等,但是它们都会被映射到0-1的范围内,(因为我们并不知道模型的大小,所以必然要被归一化,否则就会产生,纹理贴到模型上,只贴住了一小部分)
所以其实uv的值是一个二维向量,分量的范围都在0-1。此时对应纹理映射坐标,我们就可以把贴图贴到模型上(不过该部分是由Unity内置的纹理映射技术实现的,我们需要操心的是设定纹理映射坐标)
那么当纹理映射坐标超过0-1该怎么办呢?(这种情况常有)
我们可以到网络上搜一下石砖块纹理,然后随便下载一张没有水印的图。比如我下的这个
在Advanced一栏中我们可以看到,有一个叫做Warp Mode的选项,它就是用来决定,让uv坐标不在0-1范围内的时候,如何对纹理进行采样(采样的意思就是,对应uv坐标,选取纹理上的像素色彩,得到一个颜色值)。
关于它们的作用,暂时按下不表,后面再说。
创建一个新的Shader,删掉所有的内容。然后写入下面的代码。
Shader "UShaderMagicBook/texSimple"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
//增加一个2d纹理类型的输入
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
}
SubShader{
pass{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Lighting.cginc"
#include "UnityCG.cginc"
struct vertexInput{
/*顶点着色器的输入*/
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
//输入第一组纹理坐标,其实就是一个0-1的值
};
struct vertexOutput{
/* 顶点着色器输出 */
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float2 uv : TEXCOORD1;
//uv坐标,在顶点着色器中计算,后使用该坐标去片元着色器中对纹理进行采样
};
sampler2D _MainTex; //别忘记重新声明一下,注意在Properties中的2D类型对应的是sampler2D类型
float4 _BaseColor;
vertexOutput Vertex(vertexInput v){
/* 顶点着色器,除了做基本的转换之外,还需要增加一个uv坐标的计算 */
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.uv = v.texcoord.xy;
//此处的texcoord的范围其实就是0-1的一个值,
//在该着色器中暂时不做计算,直接赋值给uv后传入片元着色器
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
/* 片元着色器,为了简单起见,我们只计算漫反射和环境光 */
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
float3 albedo = tex2D(_MainTex,i.uv).xyz * _BaseColor.xyz;
//使用前面计算得到的uv坐标对主纹理进行采样
//然后和基础颜色进行混合
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
fixed3 diffuse = _LightColor0.xyz * albedo * saturate(dot(worldNormal,worldLightDir));
//计算环境光和漫反射的时候,使用albedo来作为颜色输入
return fixed4(ambient + diffuse,1.0);
}
ENDCG
}
}
}
然后在Unity中创建一个新的材质并赋予刚才所编写的着色器来观察效果,记得在纹理的输入中输入我们刚才设置的图片。
效果还可以。
在将纹理赋给这个材质的时候,我们发现,纹理输入的旁边还有两个数据,分别是tilling和offset。它们是什么意思呢?
与其问它们是什么意思,我们不如来尝试一下改变它们的值。
经过测试,不论我怎么调整Tilling和Offset,模型的贴图没有做任何的改变。
可以看到,如果我们想要利用Tilling和Offset来控制模型表面贴图的变化,还需要定义一个叫做_MainTex_ST的变量。当我们定义一个2D类型的输入时,Unity就会把Tilling和Offset数据填充到_MainTex_ST这个float4类型的变量中去,xy表示Tilling(缩放),zw表示Offset(偏移)。
当然,光储存是没有用的,我们需要对UV进行实际的缩放和偏移的计算。
在我们刚才的代码中,增加几行代码,
首先是声明_MainTex_ST,这样Unity就会把偏移和缩放数据填充到里面
然后是对uv进行计算,计算过程可以概括为,乘一个缩放,加一个偏移。
然后回到Unity中,重新试试看调整Tilling的y值,观察一下变化。
可以看见,模型的表面的贴图发生了一定的变化。不过有一个非常有趣的事情是,即使缩放已经超过了1,模型的采样还是可以做到的。这就和我们之前所说的Warp Mode有关系了。
Warp Mode主要有两种,一种是Repeat重复模式,一种是Clamp裁切模式
当Texture的Warp Mode为Repeat时,如果采样纹理超过1,则会重复采样之前的部分,比如如果y坐标现在是1.1,那么采样的结果和0.1是没有区别的,同理,2.1,3.1,4.1采样结果也和0.1无异,这样的话,如果y轴的缩放为5,那么模型表面就会出现5张主纹理被压缩后拼在一起的样子。
如果Texture的Warp Mode为Clamp,要么超过1的部分,就会被切掉,外面的uv坐标无法采样,而模型表面又必须要有一个值,此刻Unity就会选择把纹理被裁剪后的,边缘的像素值来作为超过uv坐标部分的纹素值。具体可以参看下图
额外说一点,前面所说的计算uv坐标,其实可以通过一个Unity的内置宏来实现
//o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex)
//这两行代码是等价的,不过不论用上面还是下面,你都要定义_MainTex_ST才行。
ok,A篇到这里就结束了,本次使用到的代码只有一个,我已经上传到了我的github,有需要的可以自己下载。
本次所讲的内容比较简单,只说到了纹理采样的基本方式和WarpMode,在B篇中我们再来探讨一下凹凸映射。