0%

【Unity Shader】(五)高级纹理

之前我们学习过关于基础纹理的内容,这些纹理包括法线纹理、渐变纹理和遮罩纹理等。这些纹理尽管用处不同,但它们都属于低维( 一维或二维)纹理。这一节我们将学习一些更复杂的纹理,但都是我们曾经在图形学中学到过的。包括使用立方体纹理(Cubemap)实现环境映射,以及强大的渲染纹理(Render Texture),最后学习程序纹理(Procedure Texture)。

1 立方体纹理

在图形学中我们已经学过环境映射,使用的就是展开的立方体纹理,在 Unity 中,立方体纹理可以通过直接而导入获得,也可以通过脚本生成获得。获得环境的立方体纹理之后,就可以使用该纹理对物体进行渲染,使得物体能够反射周围环境。

1.1 反射

想要模拟反射效果很简单,我们只需要通过入射光线的方向和表面法线方向来计算反射方向,再利用反射方向对立方体纹理采样即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
Shader "Unity Shaders Book/Chapter 10/Reflection" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
// 控制反射颜色
_ReflectColor ("Reflection Color", Color) = (1, 1, 1, 1)
// 控制材质的反射程度
_ReflectAmount ("Reflect Amount", Range(0, 1)) = 1
// 环境映射纹理
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {}
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}

Pass {
Tags { "LightMode"="ForwardBase" }

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Color;
fixed4 _ReflectColor;
fixed _ReflectAmount;
samplerCUBE _Cubemap;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
fixed3 worldNormal : TEXCOORD1;
fixed3 worldViewDir : TEXCOORD2;
fixed3 worldRefl : TEXCOORD3;
SHADOW_COORDS(4)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// 计算世界空间下的反射方向
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir);

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));

// 使用反射方向对立方体纹理采样
fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

// 使用lerp函数混合漫反射颜色和环境反射颜色
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;

return fixed4(color, 1.0);
}

ENDCG
}
}
FallBack "Reflective/VertexLit"
}

得到的效果如下:

image-20220406210422338

1.2 折射

我们也可以用环境贴图模拟折射,虽然对于透明物体,折射应该是两次,一次是光线进入物体内部,另一次是光线从物体内部出去,这样才能被我们所看到,但模拟两次折射比较复杂并且会大幅降低 Shader 性能,因此大多数情况下在实时渲染中我们只模拟一次折射,虽然这是不对的,但是渲染效果看起来并不差,在实时渲染中,“只要看起来它是对的,那么它就是对的”,因此我们可以用一次折射来得到想要的效果。

类似于反射,我们只要用光线折射方向对环境纹理采样即可,折射方向的计算我们也在图形学中学过,可以利用折射率和角度的关系得到。和反射一样,我们也可以通过计算折射的函数直接得到折射方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
Shader "Unity Shaders Book/Chapter 10/Refraction" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_RefractColor ("Refraction Color", Color) = (1, 1, 1, 1)
_RefractAmount ("Refraction Amount", Range(0, 1)) = 1
// 介质的透射比,即两种介质的折射率的比值
_RefractRatio ("Refraction Ratio", Range(0.1, 1)) = 0.5
_Cubemap ("Refraction Cubemap", Cube) = "_Skybox" {}
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}

Pass {
Tags { "LightMode"="ForwardBase" }

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Color;
fixed4 _RefractColor;
float _RefractAmount;
fixed _RefractRatio;
samplerCUBE _Cubemap;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
fixed3 worldNormal : TEXCOORD1;
fixed3 worldViewDir : TEXCOORD2;
fixed3 worldRefr : TEXCOORD3;
SHADOW_COORDS(4)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.worldNormal = UnityObjectToWorldNormal(v.normal);

o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);

// 使用refract函数计算世界空间下的折射方向
// 第一个参数是入射光方向,第二个参数是表面法线,都要归一化
// 第三个参数是入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值
o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);

TRANSFER_SHADOW(o);

return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir);

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));

// 使用折射方向对环境纹理采样
fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

// 将漫反射和折射颜色混合
fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;

return fixed4(color, 1.0);
}

ENDCG
}
}
FallBack "Reflective/VertexLit"
}

渲染效果如下:

image-20220406211252780

1.3 菲涅尔效果

在图形学中我们已经学过菲涅尔效应,菲涅尔项的物理计算非常复杂,但在图形学中有一个著名的近似公式,回顾当时学习的对菲涅尔项的近似公式:

image-20220406211852604

其中 $cos\theta$ 是视线和表面法线的夹角余弦,也可以写成点积形式,这一节我们用该公式进行菲涅尔项的渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Shader "Unity Shaders Book/Chapter 10/Fresnel" {
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
// 菲涅尔反射系数,相当于公式中的R0
_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {}
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}

Pass {
Tags { "LightMode"="ForwardBase" }

CGPROGRAM

#pragma multi_compile_fwdbase

#pragma vertex vert
#pragma fragment frag

#include "Lighting.cginc"
#include "AutoLight.cginc"

fixed4 _Color;
fixed _FresnelScale;
samplerCUBE _Cubemap;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f {
float4 pos : SV_POSITION;
float3 worldPos : TEXCOORD0;
fixed3 worldNormal : TEXCOORD1;
fixed3 worldViewDir : TEXCOORD2;
fixed3 worldRefl : TEXCOORD3;
SHADOW_COORDS(4)
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
TRANSFER_SHADOW(o);
return o;
}

fixed4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(i.worldViewDir);

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb;
// 计算菲涅尔项
fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);

fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
// 将菲涅尔项和漫反射、反射颜色混合
fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;

return fixed4(color, 1.0);
}

ENDCG
}
}
FallBack "Reflective/VertexLit"
}

当菲涅尔系数为 1 时,得到的就是完整的环境反射效果:

image-20220406212521693

当菲涅尔系数为 0 时,就会得到一个具有边缘光照效果的漫反射物体:

image-20220406212617407

2 渲染纹理

在之前的学习中, 一个摄像机的渲染结果会输出到颜色缓冲中,并显示到我们的屏幕上。现代的 GPU 允许我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理(Render Target Texture, RTT),而不是传统的帧缓冲或后备缓冲(back buffer)。与之相关的是多重渲染目标(Multiple Render Target, MRT),这种技术指的是 GPU 允许我们把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染就是使用多重渲染目标的一个应用。

Unity 为渲染目标纹理定义了一种专门的纹理类型——渲染纹理(Render Texture)。在 Unity 中使用渲染纹理有两种方式:

  • 一种方式是在 Project 目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置成该渲染纹理,这样一来该摄像机的渲染结果就会实时更新到渲染纹理中,而不会显示在屏幕上。 使用这种方法,我们还可以选择渲染纹理的分辨率、滤波模式等纹理属性。
  • 另一种方式是在屏幕后处理时使用 GrabPass 命令或 OnRenderimage 函数来获取当前屏幕图像,Unity 会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中,之后我们可以在自定义的 Pass 中把它们当成普通的纹理来处理,从而实现各种屏幕特效。

2.1 镜子效果

镜子实现的原理很简单,它使用一个渲染纹理作为输入属性,并把该渲染纹理在水平方向上翻转后直接显示到物体上即可。

我们搭建的场景如下:

image-20220406214409944

白色部分是一个镜子,现在我们还没有使用渲染纹理渲染镜子,要想让镜子显示场景信息,我们要在镜子上安一个虚拟摄像机,并新建一个渲染纹理叫做 Mirror Texture,然后将该摄像机的渲染目标改为渲染纹理:

image-20220406214616872

然后编写 Shader,用渲染纹理渲染镜子,是一个非常简单的二维平面渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Shader "Unity Shaders Book/Chapter 10/Mirror" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Geometry"}

Pass {
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

sampler2D _MainTex;

struct a2v {
float4 vertex : POSITION;
float3 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

v2f vert(a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);

o.uv = v.texcoord;
// 水平翻转 x
o.uv.x = 1 - o.uv.x;

return o;
}

fixed4 frag(v2f i) : SV_Target {
return tex2D(_MainTex, i.uv);
}

ENDCG
}
}
FallBack Off
}

然后为该材质选择渲染纹理,得到的效果如下:

image-20220406214900520

2.2 玻璃效果

在 Unity 中,我们还可以在 Unity Shader 中使用一种特殊的 Pass 来完成获取屏幕图像的目的,这就是 GrabPass 。当我们在 Shader 中定义了一个 GrabPass 后, Unity 会把当前屏幕的图像绘制在一张纹理中,以便我们在后续的 Pass 中访问它。我们通常会使用 GrabPass 来实现诸如玻璃等透明材质的模拟,与使用简单的透明混合不同,使用 GrabPass 可以让我们对该物体后面的图像进行更复杂的处理,例如使用法线来模拟折射效果,而不再是简单的和原屏幕颜色进行混合。

需要注意的是,在使用 GrabPass 的时候,我们需要额外小心物体的渲染队列设置。正如之前所说, GrabPass 通常用于渲染透明物体,尽管代码里并不包含混合指令,但我们往往仍然需要把物体的渲染队列设置成透明队列(即”Queue”=”Transparent”)。这样才可以保证当渲染该物体时,所有的不透明物体都已经被绘制在屏幕上,从而获取正确的屏幕图像。

在本节中,我们将会使用 GrabPass 来模拟一个玻璃效果。我们首先使用一张法线纹理来修改模型的法线信息,然后使用了之前的反射方法,通过一个 Cubemap 来模拟玻璃的反射,而在模拟折射时,则使用了 GrabPass 获取玻璃后面的屏幕图像,并使用切线空间下的法线对屏幕纹理坐标偏移后,再对屏幕图像进行采样来模拟近似的折射效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
Shader "Unity Shaders Book/Chapter 10/Glass Refraction" {
Properties {
_MainTex ("Main Tex", 2D) = "white" {} //玻璃的材质纹理
_BumpMap ("Normal Map", 2D) = "bump" {} //玻璃的法线纹理
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {} //环境纹理
_Distortion ("Distortion", Range(0, 100)) = 10 //用于控制模拟折射时图像的扭曲程度
//用于控制折射程度,值为0时该玻璃只包含反射,值为1时该玻璃只包含折射
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
}
SubShader {
// 一定要在设置为透明队列
Tags { "Queue"="Transparent" "RenderType"="Opaque" }

// 定义一个抓取屏幕的Pass
// 字符串表示抓取得到的屏幕图像将会被存入哪个纹理中,后需要使用这个名字
GrabPass { "_RefractionTex" }
// 渲染玻璃所需的 Pass
Pass {
CGPROGRAM

#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float4 _BumpMap_ST;
samplerCUBE _Cubemap;
float _Distortion;
fixed _RefractAmount;
// 对应上面抓取屏幕存入的纹理名称
sampler2D _RefractionTex;
// 得到该纹理的纹素大小,用于对屏幕图像的采样坐标进行偏移
float4 _RefractionTex_TexelSize;

struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord: TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0;
float4 uv : TEXCOORD1;
float4 TtoW0 : TEXCOORD2;
float4 TtoW1 : TEXCOORD3;
float4 TtoW2 : TEXCOORD4;
};

v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
// 通过顶点在裁剪空间下的坐标计算被抓取的屏幕图像空间的采样坐标
o.scrPos = ComputeGrabScreenPos(o.pos);

o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);

float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;

o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);

return o;
}

fixed4 frag (v2f i) : SV_Target {
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

// 获取切线空间的法线
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));

// 使用法线坐标和_Distortion属性以及纹素大小计算对屏幕图像采样坐标的偏移
// _Distortion属性的值越大,偏移就越大,玻璃背后的物体的扭曲程度也就越大
// 另外,使用切线空间下的法线方向来进行偏移,是因为该空间下的法线可以反映顶点局部空间下的法线方向
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
// 使用偏移和之前计算的屏幕图像空间的采样坐标获得校正后的的屏幕图像空间采样坐标
// 乘以z分量是为了让变形程度随着摄像机距离而发生变化,显得更加真实
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
// 对屏幕图像空间采样坐标做透视除法之后得到真正的屏幕坐标,再对屏幕图像采样得到折射颜色
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;

// 将切线空间法线转换到世界空间
bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
// 计算反射方向
fixed3 reflDir = reflect(-worldViewDir, bump);
// 主纹理采样
fixed4 texColor = tex2D(_MainTex, i.uv.xy);
// 环境纹理采样并混合主纹理颜色得到反射颜色
fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb;
// 最终颜色是反射颜色和折射颜色和线性组合,线性系数由之前定义的折射程度控制
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;

return fixed4(finalColor, 1);
}

ENDCG
}
}

FallBack "Diffuse"
}

渲染效果如下:

image-20220407101029198

3 程序纹理

程序纹理(Procedural Texture)指的是那些由计算机生成的图像,我们通常使用一些特定的算法来创建个性化图案或非常真实的自然元素, 例如木头、石子等。使用程序纹理的好处在于我们可以使用各种参数来控制纹理的外观,而这些属性不仅仅是那些颜色属性,甚至可以是完全不同类型的图案属性,这使得我们可以得到更加丰富的动画和视觉效果。

3.1 实现简单的程序纹理

我们使用一个 C# 脚本生成波点纹理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

//为了让脚本能在编辑器模式下运行
[ExecuteInEditMode]
public class ProceduralTextureGeneration : MonoBehaviour {

// 声明一个材质,这个材质将使用该脚本生成的程序纹理
public Material material = null;

// 声明该程序纹理的各种参数
#region Material properties
// 纹理尺寸
[SerializeField, SetProperty("textureWidth")]
private int m_textureWidth = 512;
public int textureWidth {
get {
return m_textureWidth;
}
set {
m_textureWidth = value;
_UpdateMaterial();
}
}

// 背景颜色
[SerializeField, SetProperty("backgroundColor")]
private Color m_backgroundColor = Color.white;
public Color backgroundColor {
get {
return m_backgroundColor;
}
set {
m_backgroundColor = value;
_UpdateMaterial();
}
}

// 波点颜色
[SerializeField, SetProperty("circleColor")]
private Color m_circleColor = Color.yellow;
public Color circleColor {
get {
return m_circleColor;
}
set {
m_circleColor = value;
_UpdateMaterial();
}
}

// 模糊因子,用于模糊圆形波点的边界
[SerializeField, SetProperty("blurFactor")]
private float m_blurFactor = 2.0f;
public float blurFactor {
get {
return m_blurFactor;
}
set {
m_blurFactor = value;
_UpdateMaterial();
}
}
#endregion

// 声明一个Texture2D的纹理变量用于保存生成的纹理
private Texture2D m_generatedTexture = null;

// 初始化
void Start () {
// 判断材质是否为空,如果为空就从该脚本所在的物体上获取材质
if (material == null) {
Renderer renderer = gameObject.GetComponent<Renderer>();
if (renderer == null) {
Debug.LogWarning("Cannot find a renderer.");
return;
}

material = renderer.sharedMaterial;
}
// 调用_UpdateMaterial()函数生成程序纹理
_UpdateMaterial();
}

// 确保材质不为空,然后生成程序纹理并设置为该物体的材质的主纹理
private void _UpdateMaterial() {
if (material != null) {
m_generatedTexture = _GenerateProceduralTexture();
material.SetTexture("_MainTex", m_generatedTexture);
}
}

private Color _MixColor(Color color0, Color color1, float mixFactor) {
Color mixColor = Color.white;
mixColor.r = Mathf.Lerp(color0.r, color1.r, mixFactor);
mixColor.g = Mathf.Lerp(color0.g, color1.g, mixFactor);
mixColor.b = Mathf.Lerp(color0.b, color1.b, mixFactor);
mixColor.a = Mathf.Lerp(color0.a, color1.a, mixFactor);
return mixColor;
}

// 生成程序纹理
private Texture2D _GenerateProceduralTexture() {
Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);

// 定义圆与圆之间的距离
float circleInterval = textureWidth / 4.0f;
// 圆的半径
float radius = textureWidth / 10.0f;
// 边缘模糊系数
float edgeBlur = 1.0f / blurFactor;

for (int w = 0; w < textureWidth; w++) {
for (int h = 0; h < textureWidth; h++) {
// 计算每个像素的颜色,初始化为背景颜色
Color pixel = backgroundColor;

// 画 9 个园
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
// 计算当前圆的圆心
Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j + 1));

// 计算当前像素和圆心的距离
float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;

// 模糊边界
Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));

// 混合颜色
pixel = _MixColor(pixel, color, color.a);
}
}

proceduralTexture.SetPixel(w, h, pixel);
}
}

proceduralTexture.Apply();

return proceduralTexture;
}
}

上面在定义属性时,每个属性都使用了 get/set 的方法,为了在面板上修改属性时仍可以执行 set 函数,还需要使用一个开源插件 SetProperty ,这使得当我们修改了材质属性时,可以执行_UpdateMaterial 函数来使用新的属性重新生成程序纹理。

SetPropertyAttribute.cs 文件很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Copyright (c) 2014 Luminary LLC
// Licensed under The MIT License (See LICENSE for full text)
using UnityEngine;
using System.Collections;

public class SetPropertyAttribute : PropertyAttribute
{
public string Name { get; private set; }
public bool IsDirty { get; set; }

public SetPropertyAttribute(string name)
{
this.Name = name;
}
}

然后我们将该脚本赋给一个立方体,在该立方体的属性面板中会出现调整我们之前定义的程序纹理属性的组件:

image-20220407103059433

我们调整这些参数就可以生成不同的程序纹理,Shader 使用之前的单张纹理 Shader,不同参数得到的纹理效果如下:

image-20220407103158038

image-20220407103455297

3.2 Unity 的程序材质

在 Unity 中,有一类专门使用程序纹理的材质,叫做程序材质 (Procedural Materials) 。这类材质和我们之前使用的那些材质在本原上是一样的,不同的是,它们使用的纹理不是普通的纹理,而是程序纹理。需要注意的是,程序材质和它使用的程序纹理并不是在 Unity 中创建的,而是使用了一个名为 Substance Designer 的软件在 Unity 外部生成的。

Substance Designer 是一个非常出色的纹理生成工具,很多 3A 的游戏项目都使用了由它生成的材质。 我们可以从 Unity 的资源商店或网络中获取到很多免费或付费的 Substance 材质。这些材质都是以 sbsar 为后缀的,我们可以直接把这些材质像其他资源一样拖入 Unity 项目中。然后生成各种各样的程序纹理。

image-20220407104339828

可以看出,程序材质的自由度很高,而且可以和 Shader 配合得到非常出色的视觉效果,它是一种非常强大的材质类型。

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。