作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要-URP管线4. 纹理贴图和法线纹理
原文发布时间:2022年6月22日
文章类型:授权转载
笔记当前的Unity版本:2019.3
当前最新的Unity版本:2020.2
URP自学笔记4. 纹理贴图和法线纹理
笔记当前的Unity版本:2019.3
当前最新的Unity版本:2020.2
在该节中,我们主要了解一下如何绘制纹理贴图,包括两部分,普通的纹理绘制和法线纹理绘制。其中相对较为复杂的也就是法线计算这块。不过体量就那么大,搞个10遍8遍的,再笨的人也懂了。
在原来的CG语法当中,当属性中定义了一个纹理类型的变量
Properties{
_MainText("Main Texture",2D) = "white"{}
}
我们想要在CGPROGRAM中引用这个纹理,就需要声明一个采样器。
sampler2D _MainTex;
而在HLSL中,sampler2D这个对象被拆分为两部分,即纹理对象和采样器。你需要同时声明两个变量来保存它们。如下所示。
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
/* 声明主纹理并且为主纹理设置一个采样器,没有什么说法,这是一种固定的格式。
主纹理的声明同属性的声明,注意类型为TEXTURE2D,采样通过SAMPLER来定义,括号中的
名字为采样器的变量名,变量名为sampler_纹理名,由于我们这里的纹理名为MainTex,
所以,采样器为sampler_MainTex*/
之后,你需要通过采样器函数SAMPLE_TEXTURE2D来对他们进行采样。
half4 albedo = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv);
以上就是HLSL中关于采样这部分的变动了。其他的地方保持不变。以下是完整的代码。
Shader "URPNotes/Sampler2D"{
/* 关于HLSL的2D采样器 */
Properties{
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
_MainTex("Main Texture",2D) = "white"{}
}
SubShader{
Tags{
"RenderType"="Opaque"
"RenderPipeline"="UniversalRenderPipeline"
}
pass{
HLSLPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
/* 声明主纹理并且为主纹理设置一个采样器,没有什么说法,这是一种固定的格式。
主纹理的声明同属性的声明,注意类型为TEXTURE2D,采样通过SAMPLER来定义,括号中的
名字为采样器的变量名,变量名随意,但同样遵循一般命名原则。*/
CBUFFER_START(UnityPerMaterial)
half4 _BaseColor;
float4 _MainTex_ST;
//_MainTex_ST,当定义一张纹理的时候,该纹理的uv坐标的缩放与偏移会
//整合成一个float4类型数据填充到_MainTex_ST当中,所以你需要手动声明一下
CBUFFER_END
struct vertexInput{
float4 vertex:POSITION;
float2 uv:TEXCOORD0; //第一张纹理的uv坐标,TEXCOORD0在输入结构体当中的含义是明确的
};
struct vertexOutput{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
vertexOutput Vertex(vertexInput v){
vertexOutput o;
o.pos = TransformObjectToHClip(v.vertex.xyz);
o.uv = TRANSFORM_TEX(v.uv,_MainTex);
/*注意这里写的是_MainTex,因为在TransformTex函数中,它会自动在纹理名后面增加一个_ST*/
return o;
}
half4 Pixel(vertexOutput i):SV_TARGET{
half4 albedo = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv);
//使用uv坐标和采样器在主纹理上进行采样,说实话,有点繁琐
return albedo * _BaseColor;
}
ENDHLSL
}
}
}
法线纹理的绘制需要先计算切转世矩阵,由于这部分比较繁琐,而之前在UnityShader入门精要笔记中写的也不是非常详细,所以我打算在这个部分重新讲一下它的原理。如果已经熟悉了,可以完全跳过。
由于三维空间不太方便表述,我也没有找到合适的软件来描绘,所以我们先看一下在2维空间中,法线和切线是怎样计算的。了解了这部分之后,3维空间的就相对简单了。
首先我们讲一下什么是法线,简单来说呢,法线就是垂直于物体表面的,用于表示方向的矢量。如果我们有一个简单的二次函数。
我们可以简单绘制出它的曲线。
对于其中任何一个点来说,你都可以大差不差的画出一个垂直于该点的线段。我们来尝试一下。
这个是我自己手画的,它是有缺点的。两个非常明显的缺点。
这两点几乎可以合并为一个点,简单来说,就是由于它缺少数学描述手段,所以它不够准确,我们大概知道它是垂直于平面的,但是到底垂直于哪部分,我们并不知道。所以此时我们需要数学手段来帮助我们解决这个问题。
虽然对于一个曲线来说,直接计算法线有点困难,但是切线是很好计算的。上过高中的差不多都应该在数学题中见过。给你一个曲线,让你求在某点的切线。计算出切线后,我们可以通过两条相互垂直的线条其斜率为负倒数的性质来计算得到法线方程,有了法线方程之后,就可以得到法线矢量了。
对于二维的曲线来说,我们只需要一个切线方程可以了。但是对于三维的曲线来说,我们还需要另外一条切线,两条切向量作叉积,才可以得到法线。那么这个所谓的另外一条切线,我们称为副切线。
以上差不多就是基本的理论知识了,我们现在回头看一下实际的场景,因为实际场景中并不是需要计算法线。
我们通过一个一连串的问题,来获得最终的答案。
以上就是大概的流程,之后,我们接下来就可以进行编码来实践了。如果我觉得我说的还是不够细节,完全可以参考《UnityShader入门精要》原书的146-153页获取更多的信息。由于大部分情况下,我们都在世界空间中计算法线,所以我们直接跳过在切线空间中计算法线这一部分。
首先我们需要获取法线与切线输入。通过NORMAL和TANGENT语义来实现。
struct vertexInput{
float4 vertex:POSITION;
float3 normal:NORMAL;
float2 uv_MainTex:TEXCOORD0;
float2 uv_BumpTex:TEXCOORD1;
float4 tangent:TANGENT;
//注意tangent是float4类型,因为其w分量是用于控制切线方向的。
};
接着就是在顶点着色器中计算世界切线,副切线和法线。然后填入三个float4类型的变量中,注意每个float4类型的变量最后一个w分量
为世界坐标。
half3 worldNormal = normalize(TransformObjectToWorldNormal(v.normal));
half3 worldTangent = normalize(TransformObjectToWorldDir(v.tangent.xyz));
half3 worldBinormal = normalize(cross(worldNormal,worldTangent)) * v.tangent.w;
float3 worldPos = TransformObjectToWorld(v.vertex.xyz);
o.TW1 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TW2 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TW3 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
在片元着色器中,主要的工作是将对法线纹理进行采样,后通过切转世矩阵,将切线空间中的法线数据转换到世界空间中来。然后利用新的法线代替原始法线进行光照的计算。
float4 normalTex = SAMPLE_TEXTURE2D(_BumpTex,sampler_BumpTex,i.uv_BumpTex); //对法线纹理采样
float3 bump = UnpackNormal(normalTex); //解包,也就是将法线从-1,1重新映射回0,1
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy,bump.xy)));
//这个z的计算是因为法线仅存储x和y信息,而z可以由x^2 + y^2 + z^2 = 1反推出来。(法线是单位矢量)
bump = mul(TW,bump); //将切线空间中的法线转换到世界空间中
上面就是这个着色器的核心点了,下面是完整的代码。
Shader "URPNotes/BumpMap"{
Properties{
_BaseColor("Base Color",Color) = (1.0,1.0,1.0,1.0)
_MainTex("Main Texture",2D) = "white"{}
_BumpTex("Bump Texture",2D) = "white"{}
_BumpScale("Bump Scale",Float) = 1.0
_Gloss("Gloss",Float) = 1.0
}
SubShader{
Tags{
"RenderType"="Opaque"
"RenderPipeline"="UniversalRenderPipeline"
}
pass{
HLSLPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
TEXTURE2D(_BumpTex);
SAMPLER(sampler_BumpTex);
CBUFFER_START(UnityPerMaterial)
float _BumpScale;
float4 _MainTex_ST;
float4 _BumpTex_ST;
half4 _BaseColor;
float _Gloss;
CBUFFER_END
struct vertexInput{
float4 vertex:POSITION;
float3 normal:NORMAL;
float2 uv_MainTex:TEXCOORD0;
float2 uv_BumpTex:TEXCOORD1;
float4 tangent:TANGENT;
//注意tangent是float4类型,因为其w分量是用于控制切线方向的。
};
struct vertexOutput{
float4 pos:SV_POSITION;
float2 uv_MainTex:TEXCOORD0;
float2 uv_BumpTex:TEXCOORD1;
float4 TW1:TEXCOORD2;
float4 TW2:TEXCOORD3;
float4 TW3:TEXCOORD4;
};
vertexOutput Vertex(vertexInput v){
vertexOutput o;
o.pos = TransformObjectToHClip(v.vertex.xyz);
float3 worldNormal = TransformObjectToWorldNormal(v.normal);
float3 worldTangent = TransformObjectToWorldDir(v.tangent.xyz);
float3 worldBinormal = cross(worldNormal,worldTangent) * v.tangent.w;
float3 worldPos = TransformObjectToWorld(v.vertex.xyz);
//计算世界法线,世界切线和世界副切线
o.uv_MainTex = TRANSFORM_TEX(v.uv_MainTex,_MainTex);
o.uv_BumpTex = TRANSFORM_TEX(v.uv_BumpTex,_BumpTex);
//计算采样坐标
o.TW1 = float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TW2 = float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TW3 = float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);
return o;
}
half4 Pixel(vertexOutput i):SV_TARGET{
/* 提取在顶点着色器中的数据 */
float3x3 TW = float3x3(i.TW1.xyz,i.TW2.xyz,i.TW3.xyz);
float3 worldPos = half3(i.TW1.w,i.TW2.w,i.TW3.w);
/* 先计算最终的世界法线 */
float4 normalTex = SAMPLE_TEXTURE2D(_BumpTex,sampler_BumpTex,i.uv_BumpTex); //对法线纹理采样
float3 bump = UnpackNormal(normalTex); //解包,也就是将法线从-1,1重新映射回0,1
bump.xy *= _BumpScale;
bump.z = sqrt(1.0 - saturate(dot(bump.xy,bump.xy)));
//这个z的计算是因为法线仅存储x和y信息,而z可以由x^2 + y^2 + z^2 = 1反推出来。(法线是单位矢量)
bump = mul(TW,bump); //将切线空间中的法线转换到世界空间中
/*计算纹理颜色*/
Light light = GetMainLight();
half3 albedo = _BaseColor.xyz * SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv_MainTex).xyz * light.color.xyz;
/*计算漫反射光照*/
half3 lightDir = TransformObjectToWorldDir(light.direction);
half3 diffuse = albedo * saturate(dot(lightDir,bump));
/*计算Blinn-Phong高光*/
half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - worldPos);
half3 halfDir = normalize(viewDir + lightDir);
half3 specular = pow(saturate(dot(halfDir,bump)),_Gloss) * albedo;
return half4(diffuse + UNITY_LIGHTMODEL_AMBIENT.xyz * albedo + specular,1);
}
ENDHLSL
}
}
}
最终效果如下
本节主要是回顾了一下2D采样器和URP当中的法线纹理绘制,内容不是很多,稍微努力一下就可以理解了。
另外,本次使用到的所有代码都已经上传到了我的github,在下面的链接中可以找到。如果你喜欢这个系列的文章,欢迎关注该专栏,或者给我的github贡献一颗⭐,感谢阅读。