float4 tex2D(sampler2D samp, float2 s)
2D纹理采样,CG内置函数。
内部实现分为以下几步:
1. 用图片的宽高度乘以uv数值,得到像素坐标。widthPixel=samp.x*s.x;heightPixel=samp.y*s.y;
2. 因为取到的数值基本上都是带有小数点的,也就是说不是一个整数,这个时候,需要看图片的过滤设置了。也就是Unity3d的图片设置中的Filter Mode。
3. Filter Mode是Point,不过滤,就会取像素点最靠近的整数,也就是四舍五入,得到像素点的坐标,然后出去图片中,这个坐标的颜色。
4. 双线性过滤,会取目标像素的附近4个像素,然后进行插值计算,得到平均颜色值,作为最终颜色。适合纹理由小放大过程中,出现的“马赛克”。
5. 三线性过滤,在双线性过滤的基础上考虑到了深度LOD,会进行两次双线性过滤,来使不同的LOD等级纹理中,更加平滑的过渡。
TRANSFORM_TEX(tex,name)
这个方法的定义在UnityCG.cginc中,它有两个参数,tex.xy是顶点的uv值,name##_ST则是在这个shader所在的材质球中,纹理图片的缩放和偏移,S指Scale,T指Transform,它是一个float4类型,其值分别为(Tiling.x,Tiling.y,Offset.x,Offset.y)。这个方法运算后,得到的是经过偏移和缩放的uv。它的运算公式是TextureCoordinate = tex.xy * name##_ST.xy + name##_ST.zw。如果偏移为0,缩放为默认1,则可以不用经过这个过程。
inline UnpackNormal(fixed4 packednormal)
这个方法是对法线纹理进行采样。它的定义同样在UnityCG.cginc里。
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
return packednormal.xyz * 2 - 1;
#else
return UnpackNormalDXT5nm(packednormal);
#endif
}
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
这里有两个方法,以UnpackNormal方法来说,它最主要的也就是
packednormal.xyz * 2 - 1;
要解释这个,就必须讲到法线纹理的生成。法线纹理是把模型的法线信息存到图片中去,每条法线的x,y,z对应的存到每个像素的r,g,b中。每条法线里的每个数值都是一个[-1,1]的闭合区间里,像素的每个数值则都是在[0,255]中,(n + vec3(1.0,1.0,1.0)) * (255.0 / 2.0),每个法线向量,经过加上 vec3(1.0,1.0,1.0)。变成[0,2]的闭合区间里,然后除以2,再乘以255,发现向量,就会转换成了[0,255]里的数值。这也是上述那条公式的由来。
至于法线纹理如何生成,有兴趣的可以详细了解一下这个算法,各个软件的生成算法不一样,最终得到的法线纹理也不一样。但是纹理里的数据,肯定是符合规范的法线纹理数据,可以在shader中使用。
另外一个方法UnpackNormalDXT5nm ,则是一个压缩法线纹理后的方法。大家都知道,法线是一个单位向量,也就是它的长度是1,所以只需要知道x,y的数值,是可以计算得到z的数值的,z=1-(x+y)的平方。这样就可以减少贴图的大小,减少GPU的数据传输量。
inline float3 UnityObjectToWorldNormal( in float3 norm )
从模型空间到世界空间转换法线。它的定义同样在UnityCG.cginc里
inline float3 UnityObjectToWorldNormal( in float3 norm )
{
// Multiply by transposed inverse matrix, actually using transpose() generates badly optimized code
return normalize(_World2Object[0].xyz * norm.x + _World2Object[1].xyz * norm.y + _World2Object[2].xyz * norm.z);
}
其实也相当于
normalize(mul((float3x3)_World2Object),norm);
_World2Object在上一篇
Unity3d中Shader的一些关于矩阵变换的基本信息中说过它是当前世界矩阵的逆矩阵。
inline float3 UnityObjectToWorldDir( in float3 dir )
inline float3 UnityObjectToWorldDir( in float3 dir )
{
return normalize(mul((float3x3)_Object2World, dir));
}
光照的计算
拿”Mobile/Bumped Diffuse”这个shader来说,它的代码很短,是一个中间代码,需要点击右方的Show generated code,出现详细的代码,其中的顶点函数
v2f_surf vert_surf (appdata_full v) {
v2f_surf o;
UNITY_INITIALIZE_OUTPUT(v2f_surf,o);
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
//这里是计算顶点的世界坐标
float3 worldPos = mul(_Object2World, v.vertex).xyz;
//得到顶点法线转换到世界空间的法线,得到切线空间的N
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
//得到顶点的切线转换到世界空间的切线,得到切线空间的T
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
//计算方向,后面用到
fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
//通过T和N的向量积,得到垂直这两个向量的向量,但是它的方向有两个,所以乘以上面得到的方向参数,得到最终的向量,得到切线空间的B
fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
//所以下面是得到模型的顶点的切线空间到世界空间的矩阵,分行显示,没看明白的可以看一下矩阵的基本知识
o.tSpace0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.tSpace1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.tSpace2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
#ifndef DYNAMICLIGHTMAP_OFF
o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#endif
#ifndef LIGHTMAP_OFF
o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#endif
// SH/ambient and vertex lights
#ifdef LIGHTMAP_OFF
#if UNITY_SHOULD_SAMPLE_SH
o.sh = 0;
// Approximated illumination from non-important point lights
#ifdef VERTEXLIGHT_ON
o.sh += Shade4PointLights (
unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
unity_4LightAtten0, worldPos, worldNormal);
#endif
o.sh = ShadeSHPerVertex (worldNormal, o.sh);
#endif
#endif // LIGHTMAP_OFF
TRANSFER_SHADOW(o); // pass shadow coordinates to pixel shader
UNITY_TRANSFER_FOG(o,o.pos); // pass fog coordinates to pixel shader
return o;
}
像素片段函数
// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
// prepare and unpack data
Input surfIN;
UNITY_INITIALIZE_OUTPUT(Input,surfIN);
surfIN.uv_MainTex.x = 1.0;
surfIN.uv_MainTex = IN.pack0.xy;
//把顶点函数取得的世界顶点坐标取出来<
float3 worldPos = float3(IN.tSpace0.w, IN.tSpace1.w, IN.tSpace2.w);
#ifndef USING_DIRECTIONAL_LIGHT
//如果用的是直线光,就把顶点的位置转换成向量,成为光照向量
fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
#else
//否者直接用世界光照的方向
fixed3 lightDir = _WorldSpaceLightPos0.xyz;
#endif
#ifdef UNITY_COMPILER_HLSL
SurfaceOutput o = (SurfaceOutput)0;
#else
SurfaceOutput o;
#endif
o.Albedo = 0.0;
o.Emission = 0.0;
o.Specular = 0.0;
o.Alpha = 0.0;
o.Gloss = 0.0;
fixed3 normalWorldVertex = fixed3(0,0,1);
// call surface function
surf (surfIN, o);
// compute lighting & shadowing factor
UNITY_LIGHT_ATTENUATION(atten, IN, worldPos)
fixed4 c = 0;
fixed3 worldN;
/*通过向量计算,得到世界法线的方向,这里Surf方法里o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_MainTex));
得到的是该顶点的切空间的法线方向,实际上,下面这个方法也等于
worldN= normalize(mul( float3x3(IN.tSpace0.xyz, IN.tSpace1.xyz, IN.tSpace2.xyz),o.Normal));
*/
worldN.x = dot(IN.tSpace0.xyz, o.Normal);
worldN.y = dot(IN.tSpace1.xyz, o.Normal);
worldN.z = dot(IN.tSpace2.xyz, o.Normal);
o.Normal = worldN;
// Setup lighting environment
UnityGI gi;
UNITY_INITIALIZE_OUTPUT(UnityGI, gi);
gi.indirect.diffuse = 0;
gi.indirect.specular = 0;
#if !defined(LIGHTMAP_ON)
gi.light.color = _LightColor0.rgb;
gi.light.dir = lightDir;
//进行光照计算,获得夹角
gi.light.ndotl = LambertTerm (o.Normal, gi.light.dir);
#endif
// Call GI (lightmaps/SH/reflections) lighting function
UnityGIInput giInput;
UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
giInput.light = gi.light;
giInput.worldPos = worldPos;
giInput.atten = atten;
#if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON)
giInput.lightmapUV = IN.lmap;
#else
giInput.lightmapUV = 0.0;
#endif
#if UNITY_SHOULD_SAMPLE_SH
giInput.ambient = IN.sh;
#else
giInput.ambient.rgb = 0.0;
#endif
giInput.probeHDR[0] = unity_SpecCube0_HDR;
giInput.probeHDR[1] = unity_SpecCube1_HDR;
#if UNITY_SPECCUBE_BLENDING || UNITY_SPECCUBE_BOX_PROJECTION
giInput.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending
#endif
#if UNITY_SPECCUBE_BOX_PROJECTION
giInput.boxMax[0] = unity_SpecCube0_BoxMax;
giInput.probePosition[0] = unity_SpecCube0_ProbePosition;
giInput.boxMax[1] = unity_SpecCube1_BoxMax;
giInput.boxMin[1] = unity_SpecCube1_BoxMin;
giInput.probePosition[1] = unity_SpecCube1_ProbePosition;
#endif
LightingLambert_GI(o, giInput, gi);
// realtime lighting: call lighting function
c += LightingLambert (o, gi);
UNITY_APPLY_FOG(IN.fogCoord, c); // apply fog
UNITY_OPAQUE_ALPHA(c.a);
return c;
}
以上就是主要的光照和法线贴图的使用。其实还可以把光照转换到切空间中进行计算,得到计算结果后,再转换回世界空间,都是可行的。