在过去十年发布的众多独立游戏中,我最喜欢的游戏之一肯定是
《旅程》 。 得益于其令人惊叹的美学和优美的配乐,
《旅程》已成为几乎所有发展方面的卓越典范。
我是一名游戏开发人员和技术美术师,所以对沙子的渲染方式最着迷。 它不仅美观,而且与基本游戏玩法和整个游戏玩法直接相关。
旅程实际上是用沙子建造的,没有这种惊人的效果,游戏本身根本就不存在。
在这篇分为两篇文章的文章中,我将通过教您如何使用着色器重新创建完全相同的沙渲染来向
Journey遗产致敬。 无论您的游戏中是否需要沙丘,本系列教程都将使您学习如何在自己的游戏中重现特定的美学。 如果要重新创建
Journey中使用的漂亮的沙着色器,则首先需要了解它的构建方式。 尽管看起来非常复杂,但实际上它包含几个相对简单的效果。 为了成为一名成功的技术美术师,必须使用这种编写着色器的方法。 因此,我希望您能与我一起
经历这一
旅程 ,我们不仅探索着色器的创建,而且学习如何将美学与游戏玩法结合起来。
旅途中的沙尘分析
与许多其他尝试重新创建
Journey沙渲染一样,本文基于GDC的一份报告,该报告由游戏公司首席工程师John Edwards撰写,标题为“
旅程中的沙渲染 ”。 在本次演讲中,约翰·爱德华兹(John Edwards)讨论了
旅程岩石沙丘中添加的所有效果层,以实现正确的外观。
该报告非常有用,但是在本教程的上下文中,John Edwards所做的许多限制和决定并不重要。 我们将主要通过视觉参考来尝试重新创建沙色着色器,使人联想到
Journey着色器。
让我们从一个完美光滑的沙丘的简单3D网格开始。 扬沙的信誉取决于两个方面:照明和纹理。 改良的
照明模型提供了一种有趣的方式来反射沙子中的
光线 。 在着色器编码的上下文中,光照模型根据模型的属性和场景的光照条件确定阴影和高光。
但是,所有这些还不足以引起现实主义的幻觉。 问题在于,沙子根本无法用平坦表面建模。 应该考虑沙粒。 这就是为什么有两个单独的效果直接作用
于表面法线的原因 ,可以用来模拟沙丘表面上的小沙粒。
下图显示了我们将在本教程中学习的所有效果。 从技术角度来看,通常在处理照明之前执行计算。 为了便于研究,将以不同顺序描述效果。
漫反射色
最简单的沙着色器效果是其
漫反射色 ,可以大致描述整体外观的
暗淡成分。 漫反射颜色是根据对象的
真实颜色和照明条件计算的。 用白色绘制的球体在任何地方都不会是完美的白色,因为漫反射的颜色取决于入射到其上的光。 使用近似于表面反射光的数学模型来计算漫反射的颜色。 感谢约翰·爱德华兹(John Edwards)和GDC的报告,我们确切地知道了所使用的方程,他称其为
漫反差反射率 。 它基于著名的
Lambert反射模型。
应用方程之前和之后沙正常
原始几何图形完全平滑。 为了弥补这一点,可使用称为
凹凸贴图的技术更改模型的
表面法线 。 它允许您使用纹理来模拟更复杂的几何图形。
边缘照明
每个
旅程级别都使用有限的调色板。 因此,很难理解一个沙丘在哪里结束而另一沙丘在哪里开始。 为了提高可读性,使用了一种小的突出显示技术,该技术仅在沙丘的边缘可见。 它称为
边缘照明 ,并且有很多方法可以实现它。 在本教程中,我选择了一种基于
菲涅耳反射的方法,该方法可以模拟抛光表面
上的反射,即所谓的
入射角 。
海洋的镜面反射
旅程的游戏中最令人愉快的方面之一是“冲浪”沙丘的能力。 这可能就是该游戏公司希望沙子感觉更像液体而不是固体的原因。 为此,使用了强烈的反射,通常可以在水着色器中找到它。 John Edwards将此效果称为
海洋镜面反射 ,在本教程中,我们使用
Blinn-Fong反射实现了该效果。
眩光反射
向沙着色器添加海洋镜面反射分量会使它看起来更流畅。 但是,它仍然不允许传达沙子最重要的视觉方面之一:随机发生的反射。 在真正的沙丘中,发生这种效果的原因是,每粒沙粒都沿其方向反射光,并且这些反射射线中的一种经常进入我们的眼睛。 即使在没有直射阳光的地方也会发生这种
闪光反射 (反射反射)。 它补充了海洋镜面,并增强了信誉感。
沙浪
更改法线使我们可以模拟覆盖沙丘表面的小沙粒的影响。 在现实世界的沙丘上,经常会出现风起的波浪。 它们的形状根据每个沙丘相对于风向的倾斜度和位置而变化。 可能可以通过凹凸纹理创建此类图案,但是在这种情况下,不可能实时更改沙丘的形状。 约翰·爱德华兹(John Edwards)提出的解决方案类似于一种称为
三面阴影的技术:它使用四种不同的纹理,根据每个沙丘的位置和坡度进行混合。
旅途沙着色器解剖
Unity有许多着色器模板,可帮助您入门。 由于我们对可以接收照明和阴影的材质感兴趣,因此我们需要从
曲面着色器 (surface shader)开始。
所有
表面着色器分两个阶段执行。 首先,称为
表面函数 ,
该函数收集需要渲染的表面的属性,例如其
反照率 ,
粗糙度 ,
金属属性 ,
透明度和
法线方向 。 然后将所有这些属性转移到
照明功能 ,该
功能考虑了外部光源的影响并计算阴影和照明。
表面功能
让我们从成为表面函数核心的内容开始,在下面的
surf
代码中进行调用。 我们需要设置的唯一属性是
沙子的
颜色和
表面的
法线 。 3D模型的法线是指示表面位置的向量。 照明功能使用法向矢量来计算光的反射方式。 它们通常是在导入网格时计算的。 但是,可以对其进行修改以模拟更复杂的几何形状。 正是在这里,
沙质法线和
砂 波 法线效果扭曲了沙模,以模拟其粗糙度。
void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; float3 N = float3(0, 0, 1); N = RipplesNormal(N); N = SandNormal (N); o.Normal = N; }
将法线写入
o.Normal
必须在
切线空间中表示它们。 这意味着相对于3D模型的表面选择了矢量。 也就是说,
float3(0, 0, 1)
实际上意味着对普通3D模型实际上没有进行任何更改。
RipplesNormal
和
SandNormal
两个函数
SandNormal
接收法线向量并对其进行修改。 稍后我们将看到如何做到这一点。
照明功能
所有其他效果都在照明功能中实现。 下面的代码显示了如何使用单独的函数(漫反射颜色,边缘照明,海洋镜面反射和闪光反射)计算每个单独的分量。 然后它们全部合并。
#pragma surface surf Journey fullforwardshadows float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi) { float3 diffuseColor = DiffuseColor (); float3 rimColor = RimLighting (); float3 oceanColor = OceanSpecular (); float3 glitterColor = GlitterSpecular (); float3 specularColor = saturate(max(rimColor, oceanColor)); float3 color = diffuseColor + specularColor + glitterColor; return float4(color * s.Albedo, 1); }
组合组件的方法相当随意,可以让我们对其进行更改以研究艺术可能性。
通常,镜面反射会堆叠在漫反射的颜色上。 由于这里我们没有一个镜面反射,而是三个镜面反射(
边缘光 ,
海洋镜面反射和
闪光镜面反射 ),因此我们需要格外小心,以免使沙子
过于闪烁。 由于边缘光和海洋镜面反射是同一效果的一部分,因此我们只能从中选择最大值。 单独添加闪光镜面反射,因为此组件会产生闪烁的沙子。
第2部分。漫反射色
在文章的第二部分,我们将重点介绍游戏中使用的照明模型以及该模型。 如何在Unity中重新创建它。
在上一部分中,我们为逐渐变成我们的Journey沙着色器版本奠定了基础。 如前所述,在
表面着色器中使用
了照明功能来计算照明效果,从而使阴影和高光出现在表面上。 我们发现,旅途有几种影响属于此类。 我们将从此着色器的核心中发现的最基本(最简单)的效果开始:其
漫射照明 (漫反射/漫射照明)。
现在,我们忽略所有其他效果和组件,只着重于
照亮沙子 。
我们在文章的前一部分“
LightingJourney
DiffuseColor
的光照函数只是将沙子的漫反射色的计算委托给一个名为
DiffuseColor
的函数。
float4 LightingJourney (SurfaceOutput s, fixed3 viewDir, UnityGI gi) {
由于每个效果都是独立的并存储在其自己的函数中,因此我们的代码将更加模块化和简洁。
兰伯特反射
在“像在旅途中一样”创建漫射照明之前,很高兴了解“基本”漫射照明功能的外观。 遮罩材料最简单的着色技术称为
朗伯反射率 。 该模型近似于大多数非发光和非金属表面的外观。 它以瑞士百科全书科学家
约翰·海因里希·兰伯特 (
Johann Heinrich Lambert )的名字命名,后者于1760年提出了这一概念。
兰伯特的反射概念基于一个简单的想法:
表面的
亮度取决于入射到其上的光量 。 在几何上,这可以显示在下图中,其中球体由远程光源照明。 尽管球体的红色和绿色区域受到相同的照明量,但是它们的表面积却明显不同。 如果红色区域中的光分布在较大的区域,则意味着红色正方形的每个单元接收的光少于绿色。
从理论上讲,朗伯反射取决于
表面和
入射光之间的相对角度。 从数学的角度来看,我们说这是从
法线到表面和
照明方向的函数。 这些数量使用两个单位长度向量(称为
单位向量 )表示
N 和
L 。 单矢量是在着色器编码的上下文中指定
方向的标准方法。
N和L的值垂直于表面 ñ 是远离表面本身定向的单位向量。
通过类推,我们可以假设照明方向 大号 从光源指向的方向沿光的移动方向移动。 但是,事实并非如此:照明的方向是指向光入射方向的单个矢量。
这可能会造成混淆,特别是如果您不熟悉创建着色器。 但是,由于有了这种表示法,方程式变得更简单了。
兰伯特在Unity中的反思在Unity 5
Standard Shader之前,Lambert反射是用于对照明表面进行着色的标准模型。
您仍然可以在Material Inspector中访问它:在
Legacy着色器中,它称为
Diffuse 。
如果您编写自己的表面着色器,则可以使用Lambert反射作为名为
Lambert
的照明功能:
#pragma surface surf Lambert fullforwardshadows
可以在
CGIncludes\Lighting.cginc
文件中定义的
LightingLambert
函数中找到其实现。
兰伯特反射与气候Lambert反射是一个相当古老的模型,但是它提供了对复杂概念(例如表面着色)的理解。 它也可以用来解释许多其他现象。 例如,同一张图解释了为什么在地球两极比在赤道更冷。
仔细观察后,我们可以看到表面的法线平行于照明方向时,该表面获得的照明量最大。 反之亦然:如果两个单位向量彼此垂直,则没有光。
显然,
N 和
L 根据兰伯特的观点,反射至关重要。 而且,亮度最大,等于
100\% 当角度是
0 和最小(
0\% )当角度趋于
90 circ 。 如果您熟悉
矢量代数 ,则可以理解代表兰伯特反射的量
我 等于
N cdotL 操作员在哪里
cdot 称为
标量产品 。
(1)
$$ display $$ \开始{equation *} I = N \ cdot L \ end {equation *} $$ display $$
标量积是两个向量彼此相对“重合”的量度,并且与
+1 (对于两个相同的向量)
−1 (针对两个相反的向量)。 标量产品是着色的基础,我在《
基于物理的渲染和照明模型》教程中对此进行了详细研究。
实作
和
N 和
L 您可以通过
s.Normal
和
gi.light.dirin
轻松访问曲面着色器照明功能。 为简单起见,我们将在着色器代码中将它们重命名为
N
和
L
float3 DiffuseColor(float3 N, float3 L) { float NdotL = saturate( dot(N, L) ); return NdotL; }
saturate
功能会限制
0 之前
1 。 但是,由于标量积在
−1 之前
+1 ,我们将只需要使用其负值即可。 这就是为什么通常按以下方式实现Lambert反射的原因:
float NdotL = max(0, dot(N, L) );
周围光线的对比反射
尽管兰伯特的反射可以很好地遮盖大多数材料,但它的物理准确性和真实感均不高。 在较早的游戏中,Lambert着色器得到了广泛使用。 使用这种技术的游戏通常
看起来很旧,因为它们可能会无意间重现旧游戏的美感。 如果您不为此而努力,则应避免进行Lambert反射,而应使用更现代的技术。
一个这样的模型就是
Oren-Nayyar反射模型 ,该
模型最初在1994年由Michael Oren和Sri C. Nayyar发表
的Lambert反射模型的概述中概述。 Oren-Nayyar模型是Lambert反射的概括,是专门为粗糙表面设计的。 最初,Journey开发人员希望使用Oren-Nayyar反射作为其沙着色器的基础。 但是,由于计算成本高,这个想法被放弃了。
技术艺术家约翰·爱德华兹(John Edwards)在其2013年的报告中解释说,为旅程沙创建的反射模型是基于一系列的反复试验而开发的,开发人员的意图不是重现沙漠的逼真的渲染效果,而是将生命带入一种具体的,可立即识别的美学中。
据他介绍,生成的阴影模型对应于以下等式:
(2)
$$显示$$ \开始{等式*} I = 4 * \左(\左(N \ odot \左[1,0.3,1 \右] \右)\ cdot L \右)\结束{等式*} $$显示$$
在哪里
odot -
元素 -两个向量的
明智积 。
float3 DiffuseColor(float3 N, float3 L) { Ny *= 0.3; float NdotL = saturate(4 * dot(N, L)); return NdotL; }
反射模型(2)约翰·爱德华兹(John Edwards)称其为
漫反射 ,因此我们将在整个教程中使用该名称。
下面的动画显示了Lambert阴影(左)和“旅程”(Journey)的漫反射(右)的差异。
4和0.3是什么意思?尽管漫射对比度的设计并非物理上准确,但我们仍然可以尝试了解它的作用。
它的核心仍然使用Lambert反射。 第一个明显的区别是,总结果乘以 4 。 这意味着正常接收的所有像素 25 \% 照明会像接收 100 \% 照明。 通过乘以一切 4 根据Lambert的说法,弱阴影会变得更强,而黑暗和明亮之间的过渡区域会更小。 在这种情况下,阴影会变得更加清晰。
y
分量相乘对法线方向的影响 0.3 解释要困难得多。 随着矢量分量的变化,其指向的大致方向也发生变化。 降低y
分量的价值 30 \% 从其原始值开始,漫反射的反射会导致阴影变得更加垂直。
注意:标量乘积仅在两个向量都具有长度时才直接测量它们之间的角度 1个 。 所做的更改减少了正常长度 ñ 不再是单位向量。
从灰色到彩色
上面显示的所有动画都有灰色阴影,因为它们显示了反射模型的值,从
0 之前
1 “我们可以通过使用
NdotL
作为两种颜色之间的插值系数来轻松添加颜色:一种用于完全着色,另一种用于完全照亮的沙子。
float3 _TerrainColor; float3 _ShadowColor; float3 DiffuseColor(float3 N, float3 L) { Ny *= 0.3; float NdotL = saturate(4 * dot(N, L)); float3 color = lerp(_ShadowColor, _TerrainColor, NdotL); return color; }
第3部分。普通砂
在第三部分中,我们将专注于创建将平滑3D模型转换为沙丘的法线贴图。
在本教程的前一部分中,我们实现了Journey沙的漫反射照明。 仅使用此效果时,沙漠沙丘看起来会很平坦且无聊。
旅程最吸引人的效果之一是沙粒感。 查看任何屏幕截图,在我们看来,沙丘不是光滑均匀的,而是由数百万个细微的沙粒产生的。
可以使用一种称为“
凹凸贴图”的技术来实现此效果,该技术可以使光从平面反弹,就好像它更复杂一样。 查看此效果如何更改渲染的外观:
随着增加,可以看到小的差异:
我们处理法线贴图
沙子由无数的沙粒组成,每种沙粒都有自己的形状和成分(请参见下文)。 每个单独的粒子在潜在的随机方向上反射照明。 实现此效果的一种方法是创建一个包含所有这些微观沙粒的3D模型。 但是由于所需的多边形数量惊人,这种方法不可行。
但是,与真实的3D模型相比,还有另一种解决方案通常用于模拟更复杂的几何图形。 3D模型的每个顶点或面都与一个称为其
法线方向的参数相关联。 这是一个单位长度向量,用于计算3D模型表面上的光反射。 也就是说,要模拟沙子,您需要模拟这种看似随机的沙粒分布,并因此模拟它们如何影响表面法线。
这可以以无数种方式完成。 最简单的方法是创建一个更改沙丘模型原始法线方向的纹理。
垂直于表面 N 在一般情况下,它是根据3D模型的几何形状计算的。 但是,您可以使用
法线贴图对其进行修改。 法线贴图是一种纹理,可通过更改法线相对于曲面的局部方向来模拟更复杂的几何形状。 这种技术通常称为
凹凸贴图 。
更改法线是一个相当简单的任务,可以在
曲面着色器的
surf
功能中执行。 此函数有两个参数,其中一个是名为
SurfaceOutput
的
struct
。 它包含渲染3D模型一部分所需的所有属性,从其颜色(
o.Albedo
)到透明度(
o.Alpha
)。 它包含的另一个参数是法线方向(
o.Normal
),可以对其进行重写以更改光在模型上的反射方式。
根据有关
Surface Shaders的Unity文档,所有写入
o.Normal
结构的法线必须在
切线空间中表示:
struct SurfaceOutput { fixed3 Albedo;
因此,我们可以报告必须在相对于网格法线的坐标系中表示单位矢量。 例如,当写入
o.Normal
float3(0, 0, 1)
normal的值将保持不变。
void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; o.Normal = float3(0, 0, 1); }
这是因为向量
float3(0, 0, 1)
实际上是相对于3D模型的几何形状表示的法向向量。
因此,要更改
表面着色器中表面的法线,我们只需要在
o.Normal
中
的表面函数中编写一个新向量:
void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; o.Normal = ...
在文章的其余部分,我们将创建初始近似值,并将在本教程的第六部分中使之复杂化。
沙正常
最具问题的部分是了解沙粒
如何垂直于表面变化。 尽管每个沙粒都可以单独将光散射到任何方向,但总的来说,还是会发生其他情况。 任何物理上准确的方法都应研究砂表面法线向量的分布并对其进行数学建模。 这样的模型确实存在,但是本教程中介绍的解决方案要简单得多,同时非常有效。
在模型的每个点上,从纹理中采样一个
随机单位向量 。 然后,表面法线向此向量倾斜一定量。 通过正确创建随机纹理并选择适当的混合量,我们可以将法线移动到表面,从而营造出颗粒感,而不会丢失沙丘的整体曲率。
可以使用填充有随机颜色的纹理对随机值进行采样。 每个像素的分量R,G和B用作法线向量的分量X,Y和Z。 颜色成分在范围内
\左[0,1\右] ,因此需要将它们转换为间隔
\左[−1,+1\右] 。 然后将所得向量归一化,使其长度等于
1 。
创建随机纹理有很多方法可以生成随机纹理。 为了获得理想的效果,最重要的是可以从纹理中采样的随机向量的一般分布。
在上图中,每个像素都是完全随机的。 纹理中没有占主导地位的方向(颜色),因为每个值与其他所有值具有相同的概率。 这种纹理给我们提供了一种可以在所有方向散射光的沙子。
在GDC演讲中,约翰·爱德华兹(John Edwards)明确指出,《旅途》中沙子的随机纹理是由高斯分布生成的。 这样可以确保主要方向与曲面的法线重合。
是否需要对随机向量进行归一化?我用来采样随机向量的图像是使用完全随机的过程生成的。 不仅每个像素都是单独生成的:一个像素的分量R,G和B也彼此独立。 也就是说,在一般情况下,不能保证从该纹理采样的向量的长度等于 1个 。
当然,您可以生成一个纹理,其中 \左[ 0 , 1 \右] 在 \左[ - 1 , + 1 \右] 而且实际上必须要有一定的长度 1个 。 但是,这里出现两个问题。
, . -, mip-, .
, .
实作
在教程的上半部分,我们介绍了“法线贴图”的概念,它出现在曲面函数的 第一个轮廓中surf
。回顾本文开头显示的图,您可以看到需要两个效果来重新创建Journey sand的渲染。第一次(正常砂),我们考虑在文章的这一部分,而第二(沙浪)检查第六部分。 void surf (Input IN, inout SurfaceOutput o) { o.Albedo = _SandColor; o.Alpha = 1; float3 N = float3(0, 0, 1); N = RipplesNormal(N);
在上一节中,我们介绍了凹凸贴图的概念,该概念向我们展示了部分效果将需要对纹理进行采样(在代码中称为uv_SandTex
)。上面的代码的问题是,对于计算,您需要知道我们要绘制的点的真实位置。实际上,您需要一个UV坐标来对纹理进行采样,从而确定要读取的像素。如果我们使用的3D模型相对平坦并且具有UV转换,则可以使用其UV采样随机纹理。 N = WavesNormal(IN.uv_SandTex.xy, N); N = SandNormal (IN.uv_SandTex.xy, N);
或者,您也可以使用IN.worldPos
渲染点在世界()中的位置。现在,我们终于可以集中精力SandNormal
执行它了。如本部分前面所述,其思想是从随机纹理中采样像素并将其(转换为单位矢量后)用作新法线。 sampler2D_float _SandTex; float3 SandNormal (float2 uv, float3 N) {
如何缩放随机纹理?UV- 3D- , . , .
, Unity . ,
_SandText_ST
. Unity ( )
_SandTex
.
_SandText_ST
: . ,
Tiling Offset :
,
TRANSFORM_TEX
:
sampler2D_float _SandTex; float4 _SandTex_ST; float3 SandNormal (float2 uv, float3 N) {
倾斜法线
上面显示的代码段有效,但效果不佳。原因很简单:如果我们只返回完全随机的法线,但实际上会失去曲率的感觉。实际上,法线方向用于计算光应如何从表面反射,其主要目的是根据模型的曲率对模型进行着色。差异可以在下面的图片中看到。在上面,沙丘的法线是完全随机的,并且不可能理解一个结束而另一个开始的位置。从下面开始,仅使用模型的法线,因此我们得到的表面过于光滑。两种解决方案都不适合我们。我们之间需要一些东西。从纹理采样的随机方向应用于将法线倾斜一定量,如下所示:图中描述的操作称为slerp,代表球面线性插值(球面线性插值)。Slerp的工作原理与lerp完全相同,但有一个例外-它可用于在单位向量之间安全地插值,并且运算的结果将是其他单位向量。不幸的是,slerp的正确实现非常昂贵。为了获得效果,至少基于偶然性,使用它是不合逻辑的。给我看slerp方程,
p0 和
p1 , . 然后
slerp :
(1)
Ω —
p0 和
p1 , :
(2)
重要的是要注意,如果我们使用传统的线性插值,则结果向量将看起来非常不同:两个单独的单位向量之间的Lerp操作并不总是创建其他单位向量。实际上,除非系数为1或0 。
尽管如此,对lerp结果进行归一化实际上会导致单位向量出奇地接近slerp生成的结果: float3 nlerp(float3 n1, float3 n2, float t) { return normalize(lerp(n1, n2, t)); }
这种称为nlerp的技术提供了slerp的近似值。证人的开发商之一凯西•穆拉托里(Casey Muratori)推广了它的使用。如果您有兴趣了解有关此主题的更多信息,建议阅读了解Slerp的文章。然后,Jonathan Blow和Math Magician-Lerp,Slerp和Nlerp 不使用它。多亏了nlerp,我们现在可以有效地将法线向量倾斜到一个随机面,该样本取自: _SandTex
sampler2D_float _SandTex; float _SandStrength; float3 SandNormal (float2 uv, float3 N) {
结果如下所示:接下来是什么
在下一部分中,我们将考虑闪烁的反射,由于这些反射,沙丘将像海洋一样。致谢
Journey视频游戏由Thatgamecompany开发,并由Sony Computer Entertainment发行。它可用于PC(Epic Store)和PS4(PS Store)。邓佳迪创建沙丘背景和照明选项的3D模型。在(现已关闭)FacePunch论坛上找到了旅程角色的3D模型。Unity包
如果要重新创建此效果,则可以从Patreon下载完整的Unity包。它涵盖了您所需的一切,从着色器到3D模型。