之前我们学习过关于基础纹理的内容,这些纹理包括法线纹理、渐变纹理和遮罩纹理等。这些纹理尽管用处不同,但它们都属于低维( 一维或二维)纹理。这一节我们将学习一些更复杂的纹理,但都是我们曾经在图形学中学到过的。包括使用立方体纹理(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); fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten; return fixed4(color, 1.0); } ENDCG } } FallBack "Reflective/VertexLit" }
|
得到的效果如下:
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); 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" }
|
渲染效果如下:
1.3 菲涅尔效果
在图形学中我们已经学过菲涅尔效应,菲涅尔项的物理计算非常复杂,但在图形学中有一个著名的近似公式,回顾当时学习的对菲涅尔项的近似公式:
其中 $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) _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 时,得到的就是完整的环境反射效果:
当菲涅尔系数为 0 时,就会得到一个具有边缘光照效果的漫反射物体:
2 渲染纹理
在之前的学习中, 一个摄像机的渲染结果会输出到颜色缓冲中,并显示到我们的屏幕上。现代的 GPU 允许我们把整个三维场景渲染到一个中间缓冲中,即渲染目标纹理(Render Target Texture, RTT),而不是传统的帧缓冲或后备缓冲(back buffer)。与之相关的是多重渲染目标(Multiple Render Target, MRT),这种技术指的是 GPU 允许我们把场景同时渲染到多个渲染目标纹理中,而不再需要为每个渲染目标纹理单独渲染完整的场景。延迟渲染就是使用多重渲染目标的一个应用。
Unity 为渲染目标纹理定义了一种专门的纹理类型——渲染纹理(Render Texture)。在 Unity 中使用渲染纹理有两种方式:
- 一种方式是在 Project 目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置成该渲染纹理,这样一来该摄像机的渲染结果就会实时更新到渲染纹理中,而不会显示在屏幕上。 使用这种方法,我们还可以选择渲染纹理的分辨率、滤波模式等纹理属性。
- 另一种方式是在屏幕后处理时使用 GrabPass 命令或 OnRenderimage 函数来获取当前屏幕图像,Unity 会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中,之后我们可以在自定义的 Pass 中把它们当成普通的纹理来处理,从而实现各种屏幕特效。
2.1 镜子效果
镜子实现的原理很简单,它使用一个渲染纹理作为输入属性,并把该渲染纹理在水平方向上翻转后直接显示到物体上即可。
我们搭建的场景如下:
白色部分是一个镜子,现在我们还没有使用渲染纹理渲染镜子,要想让镜子显示场景信息,我们要在镜子上安一个虚拟摄像机,并新建一个渲染纹理叫做 Mirror Texture,然后将该摄像机的渲染目标改为渲染纹理:
然后编写 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; o.uv.x = 1 - o.uv.x; return o; } fixed4 frag(v2f i) : SV_Target { return tex2D(_MainTex, i.uv); } ENDCG } } FallBack Off }
|
然后为该材质选择渲染纹理,得到的效果如下:
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 _RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0 } SubShader { Tags { "Queue"="Transparent" "RenderType"="Opaque" } GrabPass { "_RefractionTex" } 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)); float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy; 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" }
|
渲染效果如下:
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
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(); }
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;
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
|
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; } }
|
然后我们将该脚本赋给一个立方体,在该立方体的属性面板中会出现调整我们之前定义的程序纹理属性的组件:
我们调整这些参数就可以生成不同的程序纹理,Shader 使用之前的单张纹理 Shader,不同参数得到的纹理效果如下:
3.2 Unity 的程序材质
在 Unity 中,有一类专门使用程序纹理的材质,叫做程序材质 (Procedural Materials) 。这类材质和我们之前使用的那些材质在本原上是一样的,不同的是,它们使用的纹理不是普通的纹理,而是程序纹理。需要注意的是,程序材质和它使用的程序纹理并不是在 Unity 中创建的,而是使用了一个名为 Substance Designer 的软件在 Unity 外部生成的。
Substance Designer 是一个非常出色的纹理生成工具,很多 3A 的游戏项目都使用了由它生成的材质。 我们可以从 Unity 的资源商店或网络中获取到很多免费或付费的 Substance 材质。这些材质都是以 sbsar 为后缀的,我们可以直接把这些材质像其他资源一样拖入 Unity 项目中。然后生成各种各样的程序纹理。
可以看出,程序材质的自由度很高,而且可以和 Shader 配合得到非常出色的视觉效果,它是一种非常强大的材质类型。