最近看了shader的高级纹理 做个总结 复习! shader迟早是要拿下
这就是学习到的内容一个简单的总结 便于自己理解
立方体纹理:
有6个面组成一个立方体!采样的方法就是从立方体的中心点四散出发,特定距离后看到的情景。这种纹理主要用在环境映射中,类似烘场景地形,天空盒等。
优点:实现简单快速,效果比较好。 ** 缺点**:不支持动态,每次都重新采样生成。仅可以反射环境,不能反射物体本身。尽量对凸面使用立方纹理,凹面会反射自身没法实现。
天空盒实现: 在unity中应用就是正常的天空盒(skyBox)的设置,Lighting中设置skybox ,摄像机的Clear Flags 设置成skybox,位置调好、
环境映射实现: 环境的映射可以模拟出高反光的材质,例如金属质感。Unity中三种方法,
1、特殊纹理创建,立方体展开图,TextureType设置为CubeMap 剩下都是Unity的事情。官方推荐这种,会对纹理数据压缩,支持编译修正,光滑反射和HDR(高动态光照渲染)功能
2、手动创建一个CubeMap 拖6张纹理图进去
3、脚本生成,可以动态生成不同的问题, 只用使用了Camera.RenderToCubeMap (图形的采样依靠Camera) C#代码示例
void OnWizardCreate () {
// create temporary camera for rendering
GameObject go = new GameObject( "CubemapCamera");
go.AddComponent<Camera>();
// place it on the object
go.transform.position = renderFromPosition.position;
// render into cubemap
go.GetComponent<Camera>().RenderToCubemap(cubemap);
// destroy temporary camera
DestroyImmediate( go );
}
反射
核心算法:o.worldRefl = reflect(-o.worldViewDir, o.worldNormal); //CG 中的reflect 函数 物体反射到摄像机中的光线方向,可以由光路可逆的原则来反向求得。也就是说,我们可以计算视角方向关于顶点法线的反射方向来求得入射光线的方向。
在顶点着色器中求出反射方向,在片元中利用这个方向来立方体纹理采样。采样是用的CG的texCUBE函数
这里有一个知识点: 在片元着色器求方向会得到更好的效果更佳细腻,片元会有插值等运算,比顶点要点多。但是这种差别一般人发现不了,所以用顶点就足够了,出于性能考虑。
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" {}
}
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 = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// Compute the reflect dir in world space
//通过光路的可逆性 找到光线来源的方向
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));
// Use the reflect dir in world space to access the cubemap
fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb * _ReflectColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// Mix the diffuse color with the reflected color
fixed3 color = ambient + lerp(diffuse, reflection, _ReflectAmount) * atten;
return fixed4(color, 1.0);
}
FallBack "Reflective/VertexLit"
折射
初中物理的知识 每个材质都有自己的折射率。入射角和出射角 遵循定律 其中, η1 和η2 分别是两个介质的折射率( index of 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 = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, v.vertex).xyz;
o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
// Compute the refract dir in world space
//CG 的refract函数来计算折射方向 参数1 入射光线方向(必须归一),2:表现法线,法线方向归一,3:入射光线所在介质的折射率和折射光线所在介质的折射率之间的比值
o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
TRANSFER_SHADOW(o);
return o;
}
//z这部分实现和反射基本类似
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));
// Use the refract dir in world space to access the cubemap
fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
// Mix the diffuse color with the refract color
fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
return fixed4(color, 1.0);
}
ENDCG
}
}
FallBack "Reflective/VertexLit"
}
菲涅耳反射 Fresnel reflection
在实时渲染中,我们通常会使用一些近似公式来计算。其中一个著名的近似公式就是Schlick 菲涅耳近似等式:
在许多车漆、水面等材质的渲染中,我们会经常使用菲涅耳反射来模拟更加真实的反射效果。
Properties {
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
_Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {}
}
顶点着色器代码
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(_Object2World, 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);
}
渲染纹理
为渲染目标纹理定义一种专门纹理类型
GPU允许我们把整个三维场景渲染到中间缓存中-----渲染目标纹理(RenderTargetTexture ,RTT),
多重渲染纹理(Multiple Render Target, MRT ),GPU允许我们把场景同时渲染到多个渲染目标纹理,不需要为了某个纹理而再单独渲染一次整个场景。延时渲染的使用就是这个应用。
Unity 为渲染目标纹理定义了一种专门的纹理类型一一渲染纹理(Render Texture ) 。在Unity中使用渲染纹理通常有两种方式: 1.是在Project 目录下创建一个渲染纹理,然后把某个摄像机的渲染目标设置成该渲染纹理,这样一来该摄像机的渲染结果就会实时更新到渲染纹理中,而不会显示在屏幕上。使用这种方法,我们还可以选择渲染纹理的分辨率、滤波模式等纹理属性。
2.是在屏幕后处理时使**用GrabPass 命令或OnRenderimage **函数来获取当前屏幕图像,Unity 会把这个屏幕图像放到一张和屏幕分辨率等同的渲染纹理中,下面我们可以在自定义的Pass 中把它们当成普通的纹理来处理,从而实现各种屏幕特效。我们将依次学习这两种方法在Unity中的实现( OnRenderlmage 函数会在第12 章中讲到) 。
实现
1.在Project 视图下创建一个渲染纹理(右键单击Create → Render Texture ) 布置场景 创建一个摄像机Camera,把生成的纹理赋值给TargetTexture,其实就是 RT 平时用来做背包的那个
实现就是 camera 传给RT 纹理,RT给到shader 在镜面物体上使用这个材质。
Properties {
_MainTex ("Main Tex", 2D) = "white" {}
}
v2f vert(a2v v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord;
// Mirror needs to flip x 左右翻转
o.uv.x = 1 - o.uv.x;
return o;
}
fixed4 frag(v2f i) : SV_Target {
//渲染纹理进行采样和输出:
return tex2D(_MainTex, i.uv);
}
2 方法2的实现
玻璃效果
unity中特殊的pass---- GrabPass 使用GrabPass 可以让我们对该物体后面的图像进行更复杂的处理,例如使用法线来模拟折射效果,而不再是简单的和原屏幕颜色进行混合。
我们需要额外小心物体的渲染队列设置。正如之前所说, GrabPass 通常用于渲染透明物体, 尽管代码里并不包含混合指令, 但我们往往仍然需要把物体的渲染队列设置成透明队列(即Queue”=”Transparent”〉。这样才可以保证当渲染该物体时,所有的不透明物体都己经被绘制在屏幕上, 从而获取正确的屏幕图像。
我们首先使用一张法线纹理来修改模型的法线信息,然后使用了10.1 节介绍的反射方法,核心是用偏移来模拟折射
1.通过一个Cubemap 来模拟玻璃的反射,
2.在模拟折射时,则使用了GrabPass 获取玻璃后面的屏幕图像,并使用切线空间下的法线对屏幕纹理坐标偏移后,再对屏幕图像进行采样来模拟近似的折射效果
Properties {
_MainTex ("Main Tex", 2D) = "white" {} //_MainTex 是该玻璃的材质纹理, 默认为白色纹理
_BumpMap ("Normal Map", 2D) = "bump" {} // _BumpMap 是玻璃的法线纹理
_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {} //_Cubemap 是用于模拟反射的环境纹理
_Distortion ("Distortion", Range(0, 100)) = 10 //_Distortion 则用于控制模拟折射时图像的扭曲程度
_RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0 //_RefractAmount 用于控制折射程度 0 只反射,1只折射
}
SubShader {
// We must be transparent, so other objects are drawn before this one.
//把Queue设置成Transparent 可以确保该物体渲染时,其他所有不透明物体都已经被渲染到屏幕上了, 否则就可能无法正确得到“透过玻璃看到的图像”。而设置RenderType 则是为了在使用着色器替换( Shader Replacement )时, 该物体可以在需要时被正确渲染
Tags { "Queue"="Transparent" "RenderType"="Opaque" }
// This pass grabs the screen behind the object into a texture.
// We can access the result in the next pass as _RefractionTex
//该字符串内部的名称决定了抓取得到的屏幕图像将会被存入哪个纹理中。实际上,我们可以省略声明该字符串,但直接声明纹理名称的方法往往可以得到更高的性能,
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; // 可以让我们得到该纹理的纹素大小,例如一个大小为256 × 512 的纹理, 它的纹素大小为(1/256, 1/512) 。我们需要在对屏幕图像的采样坐标进行偏移时使用该变量。
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 = mul(UNITY_MATRIX_MVP, v.vertex);
o.scrPos = ComputeGrabScreenPos(o.pos); //内置的ComputeGrabScreenPos 函数来得到对应被抓取的屏幕图像的采样坐标
o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); //计算了 _MainTex 和 _BumpMap 的采样坐标,井把它们分别存储在一个float4 类型变量的xy 和zw 分量中
o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
float3 worldPos = mul(_Object2World, v.vertex).xyz;
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
//由于我们需要在片元着色器中把法线方向从切线空间(由法线纹理来样得到〉变换到世界空间下,
//以便对Cubemap 进行采样,因此,我们需要在这里计算该顶点对应的从切线空间到世界空间的变换矩阵,
//并把该矩阵的每一行分别存储在TtoW0 、TtoW1 和 TtoW2 的xyz 分量中。这里面使用的数学方法就是,
//得到切线空间下的3 个坐标轴(xyz 轴分别对应了副切线、切线和法线的方向)在世界空间下的表示,
//再把它们依次按列组成一个变换矩阵即可。TtoW0 等值的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 {
//通过TtoW0 等变量的w 分量得到世界坐标,
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
//并用该值得到该片元对应的视角方向
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
// Get the normal in tangent space
//我们对法线纹理进行采样,得到切线空间下的法线方向
fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
// Compute the offset in tangent space
//使用该值和 _Distortion 属性以及 _RefractionTex_TexelSize 来对屏幕图像的采样坐标进行偏移,模拟折射效果
//Distortion 值越大,偏移量越大,玻璃背后的物体看起来变形程度越大
//我们选择使用切线空间下的法线方向来进行偏移,是因为该空间下的法线可以反映顶点局部空间下的法线方向
float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
//们对scrPos 透视除法得到真正的屏幕坐标
i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
//用该坐标对抓取的屏幕图像 _RefractionTex 进行采样得到模拟的折射颜色
fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
// Convert the normal to world space 法线方向从切线空间转换到世界空间下
//(使用变换矩阵的每一行,即TtoW0 、TtoW1 和TtoW2,分别和法线方向点乘,构成新的法线方向〉
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;
//用RefractAmount 属性对反射和折射颜色进行混合,作为最终的输出颜色。
fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount;
return fixed4(finalColor, 1);
}
ENDCG
}
}
FallBack "Diffuse"
上文中使用是GrabPass 声明名称
- 直接使用GrabPass {} , 然后在后续的Pass 中直接使用 _GrabTexture 来访问屏幕图像。但是,当场景中有多个物体都使用了这样的形式来抓取屏幕时, 这种方法的性能消耗比较大,因为对于每一个使用它的物体, Unity 都会为它单独进行一次昂贵的屏幕抓取操作。但这种方法可以让每个物体得到不同的屏幕图像,这取决于它们的渲染队列及渲染它们时当前的屏幕缓冲中的颜色。
- 使用GrabPass {”TextureName” },正如本节中的实现,我们可以在后续的Pass 中使用TextureName 来访问屏幕图像。使用这种方法同样可以抓取屏幕,但Unity 只会在每一帧时为第一个使用名为TextureName 的纹理的物体执行一次抓取屏幕的操作,而这个纹理同样可以在其他Pass 中被访问。这种方法更高效,因为不管场景中有多少物体使用了该命令,每一帧中Unity 都只会执行一次抓取工作,但这也意味着所有物体都会使用同一张屏幕图像。不过,在大多数情况下这已经足够了。
渲染纹理vs. GrabPass
尽管GrabPass 和10.2.1 节中使用的渲染纹理+额外摄像机的方式都可以抓取屏幕图像,但它们之间还是有一些不同的。GrabPass 的好处在于实现简单,我们只需要在Shader 中写几行代码就可以实现抓取屏幕的目的。而要使用渲染纹理的话,我们首先需要创建一个渲染纹理和一个额外的摄像机,再把该摄像机的Render Target 设置为新建的渲染纹理对象,最后把该渲染纹理传递给相应的Shader 。
但从效率上来讲,使用渲染纹理的效率往往要好于GrabPass ,尤其在移动设备上。使用渲染纹理我们可以自定义渲染纹理的大小, 尽管这种方法需要把部分场景再次渲染一遍,但我们可以通过调整摄像机的渲染层来减少二次渲染时的场景大小,或使用其他方法来控制摄像机是否需要开启。而使用GrabPass 获取到的图像分辨率和显示屏幕是一致的,这意味着在一些高分辨率的设备上可能会造成严重的带宽影响。而且在移动设备上, GrabPass 虽然不会重新渲染场景, 但它往往需要CPU 直接读取后备缓冲(back buffer)中的数据,破坏了CPU 和GPU 之间的并行性,这是比较耗时的,甚至在一些移动设备上这是不支持的。
在Unity 5 中, Unity 引入了命令缓冲(Command Buffers ) 来允许我们扩展Unity 的渲染流水线。使用命令缓冲我们也可以得到类似抓屏的效果,它可以在不透明物体渲染后把当前的图像复制到一个临时的渲染目标纹理中,然后在那里进行一些额外的操作,例如模糊等,最后把图像传递给需要使用它的物体进行处理和显示。除此之外, 命令缓冲还允许我们实现很多特殊的效果,读者可以在Unity 官方手册的图像命令缓冲一文( http://docs.unity3d.com/ManuaVGraphicsCommandBuffers.html )中找到更多内容, Unity 还提供了一个示例工程供我们学习。
程序纹理
简单说就 通过代码 进行像素操作 生成纹理图像
c# 核心代码 生成一个程序纹理
private Texture2D _GenerateProceduralTexture() {
Texture2D proceduralTexture = new Texture2D(textureWidth, textureWidth);
// The interval between circles
float circleInterval = textureWidth / 4.0f;
// The radius of circles
float radius = textureWidth / 10.0f;
// The blur factor
float edgeBlur = 1.0f / blurFactor;
for (int w = 0; w < textureWidth; w++) {
for (int h = 0; h < textureWidth; h++) {
// Initalize the pixel with background color
Color pixel = backgroundColor;
// Draw nine circles one by one
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
// Compute the center of current circle
Vector2 circleCenter = new Vector2(circleInterval * (i + 1), circleInterval * (j + 1));
// Compute the distance between the pixel and the center
float dist = Vector2.Distance(new Vector2(w, h), circleCenter) - radius;
// Blur the edge of the circle
Color color = _MixColor(circleColor, new Color(pixel.r, pixel.g, pixel.b, 0.0f), Mathf.SmoothStep(0f, 1.0f, dist * edgeBlur));
// Mix the current color with the previous color
pixel = _MixColor(pixel, color, color.a);
}
}
proceduralTexture.SetPixel(w, h, pixel);
}
}
proceduralTexture.Apply();
return proceduralTexture;
}
10.3.2 Unity 的程序材质
在Unity 中,有一类专门使用程序纹理的材质,叫做程序材质( Procedural Materials )。这类材质和我们之前使用的那些材质在本质上是一样的,不同的是,它们使用的纹理不是普通的纹理,而是程序纹理。需要注意的是,程序材质和它使用的程序纹理并不是在Unity 中创建的,而是使用了一个名为Substance Designer 的软件在Unity 外部生成的。
**Substance Designer **是一个非常出色的纹理生成工具,很多3A 的游戏项目都使用了由它生成的材质。我们可以从Unity 的资源商店或网络中获取到很多免费或付费的Substance 材质。这些材质都是以sbsar 为后缀的,