Unity可视化Shader工具ASE介绍——6、通过例子说明ASE节点的连接方式

大家好,我是阿赵。继续介绍Unity可视化Shader编辑插件ASE的用法。上一篇已经介绍了很多ASE常用的节点。这一篇通过几个小例子,来看看这些节点是怎样连接使用的。

这篇的内容可能会比较长,最终是做了一个遮挡X光的效果,不过把这个效果拆分成多个步骤,每个步骤是一个小例子。

一、贴图+颜色

首先来一个最简单的,值显示模型的贴图和颜色。

先准备一个需要展示的模型:

然后新建一个ASE的shader

在实际的项目中,我一直很喜欢用这个Unlit的shader模板。这是因为Surface类型虽然功能非常的多,而且效果很好,但可控性不如单纯的顶点片段着色器这么好。而这个Unlit,其实就是单纯的顶点片段着色器。如果需要光照模型、法线贴图等功能,实际上也是可以在Unlit的基础上,自己实现的。

创建了shader之后,打开节点编辑器,看到了这样一个情况。然后按着键盘t键再点击鼠标,创建一个贴图采样节点

双击节点名称,可以修改变量名。我这里把这张贴图的名字改成了MainTex

注意看属性面板,虽然节点的名称改成了MainTex,但实际上属性名称是_MainTex:

接下来,把这个贴图的RGBA输出,拖动到输出节点的FragColor接收接口

如果想给这个节点一张默认的贴图,可以点击select按钮,然后从弹出窗口中选择

接下来看看效果,先保存一下这个shader

然后在shader被选中的情况下右键创建一个材质球

这样就可以快速的创建一个使用该shader的材质球了。当然你也可以随便先创建一个材质球,然后再把材质球的shader改成刚刚编辑的shader,只是我觉得这样比较快。

可以看到,由于刚才在shader里面指定了贴图,所以新建的材质球就已经有一张默认贴图了。不过其实我不太建议这样做,因为这样会导致资源依赖的问题。我这里这样做,只是为了做一个功能的展示。

把这个材质球赋予给模型,就能看到模型已经被赋予贴图的效果了。

接下来回到节点编辑界面,按着大键盘的数字5,然后鼠标点击,创建一个颜色节点,并且命名为mainColor。

这里需要注意一点,贴图类型的参数,是默认就是对外可编辑的,所以刚才我们可以直接在材质球上面看得到贴图的选项。但其他类型的节点参数,默认都是常量,如果需要把它变成可以编辑的参数,需要修改一下:

把这个参数的类型改成Property就可以。

接下来按着键盘的m键,然后点击鼠标,创建一个乘法的节点:

然后把mainTex和mainColor的RGBA输出分别拖到乘法节点的A和B输入口。

最后把乘法节点的输出口拖到FragColor。

最后保存一下shader,然后回到场景看看,会发现,现在模型已经可以控制颜色了。

这是一个最简单的例子,创建了贴图采样和颜色节点,并且用乘法节点把它们连起来,再输出到最终的颜色。

二、光照模型

在刚才那个例子的基础上,我们再加工一点东西,加上光照的效果。

在节点编辑器的空白地方鼠标右键,然后搜索world normal,创建一个世界空间法线节点,这个节点是获取了模型的法线方向,并转成了世界空间坐标

再创建一个世界空间下的灯光方向节点

按着句号键点鼠标左键,或者搜索dot,创建一个点乘节点,并把世界法线和世界灯光方向的输出连到点乘节点上

最后把点乘的结果,拖动到刚才贴图和颜色的乘法节点上。

可以看到,原来乘法节点只有A和B两个输入,但当我们拖动的时候,出现了C输入。其实所有允许多个输入的节点,都会出现这样的情况,默认只有2个,然后你可以继续的拖动其他结果进去作为输入,节点会自动增加输入接口。

这个时候保存一下节点,看看模型,会发现已经有了最基本的光照效果了。

刚才的世界法线和世界灯光方向点乘,其实就是实现了最简单的Lambert光照模型。

接下来再加工一下,新增一个乘法节点,然后把dot的结果输入到乘法节点的A参数。这里注意一个情况,B参数我并没有再新建节点输入,而是直接选择乘法节点,在属性栏里面,可以直接指定B的值。这里我输入B的值是0.5。

继续创建一个Add加法节点,然后把乘法节点输入到A参数,再指定B参数为0.5.。

最后把加法的结果输入到原来的乘法C参数

这时候保存shader,看看模型,会看到模型比刚才亮了一些,阴影没那么重。

这是因为刚才的dot后乘以0.5再加0.5,是Half-Lambert光照模型的算法。

三、边缘光

由于前面的步骤介绍得比较详细,浪费了不少篇章,所以接下来会加快一点速度。

新建一个Unlit模板的ASE的shader

新建一个颜色节点,命名为rimColor。然后新建worldNormal和worldSpaceViewDir节点。其中worldNormal刚才用过,是世界空间法线。而worldSpaceViewDir是世界空间的观察方向。

给worldSpaceViewDir连一个Normalize标准化节点,然后再和WorldNormal做dot点乘,再用OneMinus(一减)节点让dot的结果被1减,得到的结果连到FragColor输入

这时候看模型是这样的:

现在模型的边缘光已经有一点点感觉了,接下来做一个非常常用的操作,先对刚才的结果做一个次方(Power)的操作,然后再做一个乘法的操作。这个操作可以记下来,可以用于控制一些效果的范围和强度

通过调节powVal,可以调节边缘光的范围,通过调节mulVal,可以控制边缘光的强度

把颜色乘上去,就可以控制边缘光的颜色了

最后还要做一步,现在的模型并不是透明的,需要把它设置一下混合模式

选择输出节点,找到BlendMode,选择一下Alpha Blend

这时候就看到模型变透明了,效果就做完了

四、遮挡X光效果

这里再新建一个叫做XRay的ASE的Shader,使用的是上一篇文章介绍的2个pass的模板。

可以看到,这里就有2个输出节点了。

重新打开刚才上一步做的RimColor边缘光的shader编辑界面,然后框选除了输出节点以外的其他节点,按Ctrl+C复制:

然后回到XRay的shader节点编辑界面,按Ctrl+V,把节点复制到XRay里面,并且把复制过来的节点的输出连接到XRay第一个输出节点。

打开第二步做的DiffuseTex的带光照和贴图的Shader的编辑界面,然后框选除了输出节点以外的其他节点,按Ctrl+C复制:

又回到了XRay的编辑界面,按Ctrl+V把节点复制过来,并且把输出连到UnlitPass2的输出。

上面的步骤,其实就是把之前做的2个shader的效果复制过来,分别连接到这个新Shader的2个Pass的输出而已。做XRay效果其实就是这么简单,一个pass显示正常颜色,另外一个pass显示透明的边缘光而已。

不过2个pass之间怎样去决定谁应该什么时候渲染呢?这里是需要通过ZTest来控制了。当前面没东西遮挡的时候,也就是ZTest LEqual的时候,显示正常颜色,当前面有东西遮挡,也就是ZTest Greater的时候,显示边缘光。然后由于边缘光需要半透明混合,所以还需要加一个ZWrite Off。

然后问题来了,选择一下这两个输出节点,看到了Pass里面并没有ZTest和ZWrite的选项,怎么办呢?

上一篇文章已经说过了,需要什么可以编辑的参数,去模板加就行,所以打开模板,然后在两个Pass里面都加上ZTest和ZWrite。

再回到节点编辑器,就可以看到Pass里面已经有这两个选项了:

于是修改,注意,模板里面的第一个Pass的名字,在节点属性里面会显示成shader的名字,比如我这个Shader叫做XRay,所以第一个输出节点就叫XRay,但点进去属性里面,还是显示UnlitPass1的,这个不要搞错。第一个Pass是显示边缘光,所以ZWrite要off,ZTest要Greater,然后混合模式用AlphaBlend:

第二个Pass显示的是正常的漫反射颜色,所以ZWrite用默认,也就是On,ZTest选择Less or Equal。最后混合模式选Off。

现在回到模型场景,建一个Cube挡在模型前面,然后调整一下边缘光的颜色和强度,就可以看到我们的最终效果了。

五、总结

这样一个XRay透视效果,我分了挺多步骤来说明,主要是越简单的操作步骤越难说,如果大家都已经很熟悉ASE的操作了,我只是讲节点的连接思路,那会快很多。

最后附上XRay的例子的代码,可以粘贴到Shader里面,应该就能打开看节点了:

bash 复制代码
// Made with Amplify Shader Editor
// Available at the Unity Asset Store - http://u3d.as/y3X 
Shader "XRay"
{
	Properties
	{
		_mainTex("mainTex", 2D) = "white" {}
		_rimColor("rimColor", Color) = (0.8109627,0.7688679,1,0)
		_mainColor("mainColor", Color) = (1,1,1,1)
		_powVal("powVal", Float) = 2
		_mulVal("mulVal", Float) = 1
		[HideInInspector] _texcoord( "", 2D ) = "white" {}

	}
	
	SubShader
	{
		
		
		Tags { "RenderType"="Opaque" }
	LOD 100

		
		
		Pass
		{
			Name "UnlitPass1"
			ZTest Greater
			ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			Tags { "LightMode"="ForwardBase" }
			CGPROGRAM

			

			#ifndef UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX
			//only defining to not throw compilation error over Unity 5.5
			#define UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input)
			#endif
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_instancing
			#include "UnityCG.cginc"
			

			struct appdata
			{
				float4 vertex : POSITION;
				float4 color : COLOR;
				float3 ase_normal : NORMAL;
				UNITY_VERTEX_INPUT_INSTANCE_ID
			};
			
			struct v2f
			{
				float4 vertex : SV_POSITION;
				#ifdef ASE_NEEDS_FRAG_WORLD_POSITION
				float3 worldPos : TEXCOORD0;
				#endif
				float4 ase_texcoord1 : TEXCOORD1;
				UNITY_VERTEX_INPUT_INSTANCE_ID
				UNITY_VERTEX_OUTPUT_STEREO
			};

			uniform float4 _rimColor;
			uniform float _powVal;
			uniform float _mulVal;

			
			v2f vert ( appdata v )
			{
				v2f o;
				UNITY_SETUP_INSTANCE_ID(v);
				UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
				UNITY_TRANSFER_INSTANCE_ID(v, o);

				float3 ase_worldNormal = UnityObjectToWorldNormal(v.ase_normal);
				o.ase_texcoord1.xyz = ase_worldNormal;
				
				
				//setting value to unused interpolator channels and avoid initialization warnings
				o.ase_texcoord1.w = 0;
				float3 vertexValue = float3(0, 0, 0);
				#if ASE_ABSOLUTE_VERTEX_POS
				vertexValue = v.vertex.xyz;
				#endif
				vertexValue = vertexValue;
				#if ASE_ABSOLUTE_VERTEX_POS
				v.vertex.xyz = vertexValue;
				#else
				v.vertex.xyz += vertexValue;
				#endif
				o.vertex = UnityObjectToClipPos(v.vertex);

				#ifdef ASE_NEEDS_FRAG_WORLD_POSITION
				o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
				#endif
				return o;
			}
			
			fixed4 frag (v2f i ) : SV_Target
			{
				UNITY_SETUP_INSTANCE_ID(i);
				UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
				fixed4 finalColor;
				#ifdef ASE_NEEDS_FRAG_WORLD_POSITION
				float3 WorldPosition = i.worldPos;
				#endif
				float3 ase_worldNormal = i.ase_texcoord1.xyz;
				float3 worldSpaceViewDir10 = WorldSpaceViewDir( float4( 0,0,0,1 ) );
				float3 normalizeResult12 = normalize( worldSpaceViewDir10 );
				float dotResult13 = dot( ase_worldNormal , normalizeResult12 );
				
				
				finalColor = ( _rimColor * ( pow( ( 1.0 - dotResult13 ) , _powVal ) * _mulVal ) );
				return finalColor;
			}
			ENDCG
		}
		
		Pass
		{
			Name "UnlitPass2"
			Tags { "LightMode"="ForwardBase" }
			Blend Off
			ZWrite On
			ZTest LEqual
			CGPROGRAM

			

			#ifndef UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX
				//only defining to not throw compilation error over Unity 5.5
				#define UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input)
				#endif
				#pragma vertex vert
				#pragma fragment frag
				#pragma multi_compile_instancing
				#include "UnityCG.cginc"
				#include "Lighting.cginc"
				#include "AutoLight.cginc"
				#define ASE_NEEDS_FRAG_WORLD_POSITION


				struct appdata
				{
					float4 vertex : POSITION;
					float4 color : COLOR;
					float4 ase_texcoord : TEXCOORD0;
					float3 ase_normal : NORMAL;
					UNITY_VERTEX_INPUT_INSTANCE_ID
				};

				struct v2f
				{
					float4 vertex : SV_POSITION;
					#ifdef ASE_NEEDS_FRAG_WORLD_POSITION
					float3 worldPos : TEXCOORD0;
					#endif
					float4 ase_texcoord1 : TEXCOORD1;
					float4 ase_texcoord2 : TEXCOORD2;
					UNITY_VERTEX_INPUT_INSTANCE_ID
					UNITY_VERTEX_OUTPUT_STEREO
				};

				//This is a late directive
				
				uniform sampler2D _mainTex;
				uniform float4 _mainTex_ST;
				uniform float4 _mainColor;


				v2f vert(appdata v )
				{
					v2f o;
					UNITY_SETUP_INSTANCE_ID(v);
					UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
					UNITY_TRANSFER_INSTANCE_ID(v, o);

					float3 ase_worldNormal = UnityObjectToWorldNormal(v.ase_normal);
					o.ase_texcoord2.xyz = ase_worldNormal;
					
					o.ase_texcoord1.xy = v.ase_texcoord.xy;
					
					//setting value to unused interpolator channels and avoid initialization warnings
					o.ase_texcoord1.zw = 0;
					o.ase_texcoord2.w = 0;
					float3 vertexValue = float3(0, 0, 0);
					#if ASE_ABSOLUTE_VERTEX_POS
					vertexValue = v.vertex.xyz;
					#endif
					vertexValue = vertexValue;
					#if ASE_ABSOLUTE_VERTEX_POS
					v.vertex.xyz = vertexValue;
					#else
					v.vertex.xyz += vertexValue;
					#endif
					o.vertex = UnityObjectToClipPos(v.vertex);

					#ifdef ASE_NEEDS_FRAG_WORLD_POSITION
					o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
					#endif
					return o;
				}

				fixed4 frag(v2f i ) : SV_Target
				{
					UNITY_SETUP_INSTANCE_ID(i);
					UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
					fixed4 finalColor;
					#ifdef ASE_NEEDS_FRAG_WORLD_POSITION
					float3 WorldPosition = i.worldPos;
					#endif
					float2 uv_mainTex = i.ase_texcoord1.xy * _mainTex_ST.xy + _mainTex_ST.zw;
					float3 ase_worldNormal = i.ase_texcoord2.xyz;
					float3 worldSpaceLightDir = UnityWorldSpaceLightDir(WorldPosition);
					float dotResult4 = dot( ase_worldNormal , worldSpaceLightDir );
					

					finalColor = ( tex2D( _mainTex, uv_mainTex ) * _mainColor * ( ( dotResult4 * 0.5 ) + 0.5 ) );
					return finalColor;
				}
				ENDCG
			}
			
	}
	CustomEditor "ASEMaterialInspector"
	
	Fallback "Diffuse"
}
/*ASEBEGIN
Version=18500
0;306;1661;681;1937.559;441.4886;1.45936;True;True
Node;AmplifyShaderEditor.WorldSpaceLightDirHlpNode;3;-976.1194,139.55;Inherit;False;False;1;0;FLOAT;0;False;4;FLOAT3;0;FLOAT;1;FLOAT;2;FLOAT;3
Node;AmplifyShaderEditor.WorldNormalVector;2;-986.5195,-64.54977;Inherit;False;False;1;0;FLOAT3;0,0,1;False;4;FLOAT3;0;FLOAT;1;FLOAT;2;FLOAT;3
Node;AmplifyShaderEditor.DotProductOpNode;4;-693.6849,42.34929;Inherit;False;2;0;FLOAT3;0,0,0;False;1;FLOAT3;0,0,0;False;1;FLOAT;0
Node;AmplifyShaderEditor.SimpleMultiplyOpNode;5;-534.3663,61.05127;Inherit;False;2;2;0;FLOAT;0;False;1;FLOAT;0.5;False;1;FLOAT;0
Node;AmplifyShaderEditor.ColorNode;6;-993.7325,-285.175;Inherit;False;Property;_mainColor;mainColor;2;0;Create;True;0;0;False;0;False;1,1,1,1;1,1,1,1;True;0;5;COLOR;0;FLOAT;1;FLOAT;2;FLOAT;3;FLOAT;4
Node;AmplifyShaderEditor.SamplerNode;7;-1024.732,-503.175;Inherit;True;Property;_mainTex;mainTex;0;0;Create;True;0;0;False;0;False;-1;None;138df4511c079324cabae1f7f865c1c1;True;0;False;white;Auto;False;Object;-1;Auto;Texture2D;8;0;SAMPLER2D;;False;1;FLOAT2;0,0;False;2;FLOAT;0;False;3;FLOAT2;0,0;False;4;FLOAT2;0,0;False;5;FLOAT;1;False;6;FLOAT;0;False;7;SAMPLERSTATE;;False;5;COLOR;0;FLOAT;1;FLOAT;2;FLOAT;3;FLOAT;4
Node;AmplifyShaderEditor.SimpleAddOpNode;8;-363.3666,73.05125;Inherit;False;2;2;0;FLOAT;0;False;1;FLOAT;0.5;False;1;FLOAT;0
Node;AmplifyShaderEditor.ColorNode;18;-579.6888,383.1555;Inherit;False;Property;_rimColor;rimColor;1;0;Create;True;0;0;False;0;False;0.8109627,0.7688679,1,0;0.8109627,0.7688679,1,1;True;0;5;COLOR;0;FLOAT;1;FLOAT;2;FLOAT;3;FLOAT;4
Node;AmplifyShaderEditor.NormalizeNode;12;-1045.079,582.2842;Inherit;False;1;0;FLOAT3;0,0,0;False;1;FLOAT3;0
Node;AmplifyShaderEditor.WorldNormalVector;11;-1264.591,398.5844;Inherit;False;False;1;0;FLOAT3;0,0,1;False;4;FLOAT3;0;FLOAT;1;FLOAT;2;FLOAT;3
Node;AmplifyShaderEditor.DotProductOpNode;13;-996.485,444.3506;Inherit;False;2;0;FLOAT3;0,0,0;False;1;FLOAT3;0,0,0;False;1;FLOAT;0
Node;AmplifyShaderEditor.RangedFloatNode;14;-783.7252,675.4164;Inherit;False;Property;_powVal;powVal;3;0;Create;True;0;0;False;0;False;2;2;0;0;0;1;FLOAT;0
Node;AmplifyShaderEditor.OneMinusNode;15;-868.7095,556.3414;Inherit;False;1;0;FLOAT;0;False;1;FLOAT;0
Node;AmplifyShaderEditor.SimpleMultiplyOpNode;19;-486.6632,600.4702;Inherit;False;2;2;0;FLOAT;0;False;1;FLOAT;0;False;1;FLOAT;0
Node;AmplifyShaderEditor.PowerNode;16;-645.6777,609.1974;Inherit;False;False;2;0;FLOAT;0;False;1;FLOAT;1;False;1;FLOAT;0
Node;AmplifyShaderEditor.WorldSpaceViewDirHlpNode;10;-1251.096,622.5241;Inherit;False;1;0;FLOAT4;0,0,0,1;False;4;FLOAT3;0;FLOAT;1;FLOAT;2;FLOAT;3
Node;AmplifyShaderEditor.SimpleMultiplyOpNode;9;-660.4999,-365.2596;Inherit;True;3;3;0;COLOR;0,0,0,0;False;1;COLOR;0,0,0,0;False;2;FLOAT;0;False;1;COLOR;0
Node;AmplifyShaderEditor.SimpleMultiplyOpNode;20;-283.6566,462.576;Inherit;False;2;2;0;COLOR;0,0,0,0;False;1;FLOAT;0;False;1;COLOR;0
Node;AmplifyShaderEditor.RangedFloatNode;17;-621.9011,755.5342;Inherit;False;Property;_mulVal;mulVal;4;0;Create;True;0;0;False;0;False;1;1;0;0;0;1;FLOAT;0
Node;AmplifyShaderEditor.TemplateMultiPassMasterNode;0;0,0;Float;False;True;-1;2;ASEMaterialInspector;100;11;XRay;cb43803649bb8db429b4b5d8ba318e3a;True;UnlitPass1;0;0;UnlitPass1;2;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;True;1;RenderType=Opaque=RenderType;False;0;True;2;5;False;-1;10;False;-1;0;1;False;-1;0;False;-1;False;False;False;False;False;False;False;False;False;False;False;False;False;False;True;2;False;-1;True;2;False;-1;False;True;1;LightMode=ForwardBase;False;0;Diffuse;0;0;Standard;1;Vertex Position,InvertActionOnDeselection;1;0;2;True;True;False;;False;0
Node;AmplifyShaderEditor.TemplateMultiPassMasterNode;1;2.219055,-151.1632;Float;False;False;-1;2;ASEMaterialInspector;100;11;New Amplify Shader;cb43803649bb8db429b4b5d8ba318e3a;True;UnlitPass2;0;1;UnlitPass2;2;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;True;1;RenderType=Opaque=RenderType;False;0;True;0;5;False;-1;10;False;-1;0;5;False;-1;10;False;-1;False;False;False;False;False;False;False;False;False;False;False;False;False;False;True;0;False;-1;True;3;False;-1;False;True;1;LightMode=ForwardBase;False;0;;0;0;Standard;0;False;0
WireConnection;4;0;2;0
WireConnection;4;1;3;0
WireConnection;5;0;4;0
WireConnection;8;0;5;0
WireConnection;12;0;10;0
WireConnection;13;0;11;0
WireConnection;13;1;12;0
WireConnection;15;0;13;0
WireConnection;19;0;16;0
WireConnection;19;1;17;0
WireConnection;16;0;15;0
WireConnection;16;1;14;0
WireConnection;9;0;7;0
WireConnection;9;1;6;0
WireConnection;9;2;8;0
WireConnection;20;0;18;0
WireConnection;20;1;19;0
WireConnection;0;0;20;0
WireConnection;1;0;9;0
ASEEND*/
//CHKSM=6DD70292189FAD2C0FC85A4698D11D5C972718B6
相关推荐
逐·風3 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i5 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
代码盗圣8 小时前
GODOT 4 不用scons编译cpp扩展的方法
游戏引擎·godot
Leoysq14 小时前
【UGUI】实现点击注册按钮跳转游戏场景
游戏·unity·游戏引擎·ugui
PandaQue15 小时前
《潜行者2切尔诺贝利之心》游戏引擎介绍
游戏引擎
_oP_i17 小时前
unity中 骨骼、纹理和材质关系
unity·游戏引擎·材质
Padid1 天前
Unity SRP学习笔记(二)
笔记·学习·unity·游戏引擎·图形渲染·着色器
Tp_jh1 天前
推荐一款非常好用的C/C++在线编译器
linux·c语言·c++·ide·单片机·unity·云原生
dangoxiba2 天前
[Unity Demo]从零开始制作空洞骑士Hollow Knight第十八集补充:制作空洞骑士独有的EventSystem和InputModule
游戏·unity·c#·游戏引擎·playmaker
无敌最俊朗@2 天前
unity3d————屏幕坐标,GUI坐标,世界坐标的基础注意点
开发语言·学习·unity·c#·游戏引擎