【浅墨Unity3D Shader编程】之十三 单色透明Shader & 标准镜面高光Shader

本系列文章由@浅墨_毛星云 出品,转载请注明出处。  
文章链接:http://blog.csdn.net/poem_qianmo/article/details/50878538
作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442
本文工程使用的Unity3D版本: 5.2.1 

本次更新放出的Shader为透明系列的3个Shader和标准的镜面高光Shader的两个Shader。由易到难,由入门级到应用级,难度梯度合理。

依然是先放出游戏场景的exe和运行截图。

本期用的模型为妙蛙草。

【可运行的本文配套exe游戏场景请点击这里下载】

OK,直奔主题吧。

一、单色透明Shader

在上篇文章中单色透明的基础上进行改造,加入alpha混合,构成了这篇文章的第一个Shader——单色透明Shader。具体代码如下:

//透明单色Shader

Shader "浅墨Shader编程/Volume13/1.SimpleAlphaShader"
{
	//------------------------------------【唯一的子着色器】------------------------------------
	SubShader
	{
		//设置Queue为透明,在所有非透明几何体绘制之后再进行绘制
		Tags{ "Queue" = "Transparent" }

		Pass
		{
			//不写入深度缓冲,为了不遮挡住其他物体
			ZWrite Off

			//选取Alpha混合方式
			Blend  SrcAlpha SrcAlpha
			//Blend SrcAlpha OneMinusSrcAlpha

			//===========开启CG着色器语言编写模块============
			CGPROGRAM

			//编译指令:告知编译器顶点和片段着色函数的名称
			#pragma vertex vert
			#pragma fragment frag

			//--------------------------------【顶点着色函数】-----------------------------
			// 输入:POSITION语义(坐标位置)
			// 输出:SV_POSITION语义(像素位置)
			//---------------------------------------------------------------------------------
			float4 vert(float4 vertexPos : POSITION) : SV_POSITION
			{
				//坐标系变换
				//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
				return mul(UNITY_MATRIX_MVP, vertexPos);
			}

			//--------------------------------【片段着色函数】-----------------------------
			// 输入:无
			// 输出:COLOR语义(颜色值)
			//---------------------------------------------------------------------------------
			float4 frag(void) : COLOR
			{
				//返回单色
				return float4(0.3, 1.0, 0.1, 0.6);
			}

			//===========结束CG着色器语言编写模块===========
			ENDCG
		}
	}
}

将其施用于材质之上的效果如下:

实景效果如下:

二、颜色可以调版单色透明Shader

老规矩,让颜色可调,来一个Properties属性块,替换掉Hard encoding硬编码的颜色。所以,代码如下:

//颜色可以调版单色透明Shader

Shader "浅墨Shader编程/Volume13/2.ColorChangeAlpha"
{
	//------------------------------------【属性值】------------------------------------
	Properties
	{
		//颜色值
		_ColorWithAlpha("ColorWithAlpha", Color) = (0.9, 0.1, 0.1, 0.5)
	}

	//------------------------------------【唯一的子着色器】------------------------------------
	SubShader
	{
		//设置Queue为透明,在所有非透明几何体绘制之后再进行绘制
		Tags{ "Queue" = "Transparent" }

		//--------------------------------唯一的通道-------------------------------
		Pass
		{
			//不写入深度缓冲,为了不遮挡住其他物体
			ZWrite Off

			//选取Alpha混合方式
			Blend  SrcAlpha SrcAlpha
			//Blend SrcAlpha OneMinusSrcAlpha

			//===========开启CG着色器语言编写模块============
			CGPROGRAM

			//编译指令:告知编译器顶点和片段着色函数的名称
			#pragma vertex vert
			#pragma fragment frag

			//变量声明
			uniform float4 _ColorWithAlpha;

			//--------------------------------【顶点着色函数】-----------------------------
			// 输入:POSITION语义(坐标位置)
			// 输出:SV_POSITION语义(像素位置)
			//---------------------------------------------------------------------------------
			float4 vert(float4 vertexPos : POSITION) : SV_POSITION
			{
				//坐标系变换
				//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
				return mul(UNITY_MATRIX_MVP, vertexPos);
			}

			//--------------------------------【片段着色函数】-----------------------------
			// 输入:无
			// 输出:COLOR语义(颜色值)
			//---------------------------------------------------------------------------------
			float4 frag(void) : COLOR
			{
				//返回自定义的RGBA颜色
				return _ColorWithAlpha;
			}

			//===========结束CG着色器语言编写模块===========
			ENDCG
		}
	}
}

将其施用于材质之上的效果如下:

这边的调色板中,,除了RGB三色,还有Alpha值可以进行调节。

实景效果如下:

三、双面双色颜色可以调版透明Shader

我们可以利用Cull语句,分别在两个Pass中Cull Front和Cull Back,以让材质的正面和反面显示出不同的颜色。代码实现如下:

//双面双色颜色可以调版透明Shader

Shader "浅墨Shader编程/Volume13/3.TwoSideColorChangeAlpha"
{
	//------------------------------------【属性值】------------------------------------
	Properties
	{
		//正面颜色值
		_ColorWithAlpha_Front("ColorWithAlpha_Front", Color) = (0.9, 0.1, 0.1, 0.5)
		//背面颜色值
		_ColorWithAlpha_Back("ColorWithAlpha_Back", Color) = (0.1, 0.3, 0.9, 0.5)
	}

	//------------------------------------【唯一的子着色器】------------------------------------
	SubShader
	{
		//设置Queue为透明,在所有非透明几何体绘制之后再进行绘制
		Tags{ "Queue" = "Transparent" }

		//------------------------【通道1:渲染正面】-------------------------
		Pass
		{
			//剔除背面,渲染正面
			Cull Back
			//不写入深度缓冲,为了不遮挡住其他物体
			ZWrite Off 

			//选取Alpha混合方式
			Blend SrcAlpha OneMinusSrcAlpha
			//Blend  SrcAlpha SrcAlpha

			//===========开启CG着色器语言编写模块============
			CGPROGRAM

			//编译指令:告知编译器顶点和片段着色函数的名称
			#pragma vertex vert
			#pragma fragment frag

			//变量声明
			uniform float4 _ColorWithAlpha_Front;

			//--------------------------------【顶点着色函数】-----------------------------
			// 输入:POSITION语义(坐标位置)
			// 输出:SV_POSITION语义(像素位置)
			//---------------------------------------------------------------------------------
			float4 vert(float4 vertexPos : POSITION) : SV_POSITION
			{
				//坐标系变换
				//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
				return mul(UNITY_MATRIX_MVP, vertexPos);
			}

			//--------------------------------【片段着色函数】-----------------------------
			// 输入:无
			// 输出:COLOR语义(颜色值)
			//---------------------------------------------------------------------------------
			float4 frag(void) : COLOR
			{
				//返回自定义的RGBA颜色
				return _ColorWithAlpha_Front;
			}

			//===========结束CG着色器语言编写模块===========
			ENDCG
		}

		//------------------------【通道2:渲染背面】-------------------------
		Pass
		{
			//剔除正面,渲染背面
			Cull Front

			//不写入深度缓冲,为了不遮挡住其他物体
			ZWrite Off

			//选取Alpha混合方式
			Blend SrcAlpha OneMinusSrcAlpha
			//Blend  SrcAlpha SrcAlpha

			//===========开启CG着色器语言编写模块============
			CGPROGRAM

			//编译指令:告知编译器顶点和片段着色函数的名称
			#pragma vertex vert
			#pragma fragment frag

			//变量声明
			uniform float4 _ColorWithAlpha_Back;

			//--------------------------------【顶点着色函数】-----------------------------
			// 输入:POSITION语义(坐标位置)
			// 输出:SV_POSITION语义(像素位置)
			//---------------------------------------------------------------------------------
			float4 vert(float4 vertexPos : POSITION) : SV_POSITION
			{
				//坐标系变换
				//输出的顶点位置(像素位置)为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
				return mul(UNITY_MATRIX_MVP, vertexPos);
			}

			//--------------------------------【片段着色函数】-----------------------------
			// 输入:无
			// 输出:COLOR语义(颜色值)
			//---------------------------------------------------------------------------------
			float4 frag(void) : COLOR
			{
				//返回自定义的RGBA颜色
				return _ColorWithAlpha_Back;
			}

			//===========结束CG着色器语言编写模块===========
			ENDCG
		}
	}
}

将其施用于材质之上的效果如下:

可以看到,有两个可供调节的颜色选项,分别表示材质正面和反面的透明颜色,而材质最终表现出来的透明颜色,是这两种颜色的混合。

在实景表现中,从物体外部和内部,可以看到其显示出了不同的颜色。

从物体外部看:

从物体内部看:

四、镜面反射(Specular)Shader

上篇文章中讲到了Diffuse光照(漫反射光照)与实现它的Shader,经常与其相提并论的是Specular光照(镜面反射光照,或称高光)。这里,接着我们来放出镜面反射光照的Shader。其具体的原理翻开任何一本图形学的书都可以找到,这边就不多讲,直接放出详细注释的实现代码:

//镜面反射Shader(specular shader )

Shader "浅墨Shader编程/Volume13/4.Specular"
{
	//------------------------------------【属性值】------------------------------------
	Properties
	{
		//主颜色
		_Color("Main Color", Color) = (1, 1, 1, 1)
		//镜面反射颜色
		_SpecColor("Specular Color", Color) = (1, 1, 1, 1)
		//镜面反射光泽度
		_SpecShininess("Specular Shininess", Range(1.0, 100.0)) = 10.0
	}

	//------------------------------------【唯一的子着色器】------------------------------------
	SubShader
	{
		//渲染类型设置:不透明
		Tags{ "RenderType" = "Opaque" }

		//--------------------------------唯一的通道-------------------------------
		Pass
		{
			//光照模型ForwardBase
			Tags{ "LightMode" = "ForwardBase" }
			//===========开启CG着色器语言编写模块===========
			CGPROGRAM

			//编译指令:告知编译器顶点和片段着色函数的名称
			#pragma vertex vert
			#pragma fragment frag

			//顶点着色器输入结构
			struct appdata
			{
				float4 vertex : POSITION;//顶点位置
				float3 normal : NORMAL;//法线向量坐标
			};

			//顶点着色器输出结构
			struct v2f
			{
				float4 pos : SV_POSITION;//像素位置
				float3 normal : NORMAL;//法线向量坐标
				float4 posWorld : TEXCOORD0;//在世界空间中的坐标位置
			};

			//变量的声明
			float4 _LightColor0;
			float4 _Color;
			float4 _SpecColor;
			float _SpecShininess;

			//--------------------------------【顶点着色函数】-----------------------------
			// 输入:顶点输入结构体
			// 输出:顶点输出结构体
			//---------------------------------------------------------------------------------
			//顶点着色函数
			v2f vert(appdata IN)
			{
				//【1】声明一个输出结构对象
				v2f OUT;

				//【2】填充此输出结构
				//输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
				OUT.pos = mul(UNITY_MATRIX_MVP, IN.vertex);
				//获得顶点在世界空间中的位置坐标
				OUT.posWorld = mul(_Object2World, IN.vertex);
				//获取顶点在世界空间中的法线向量坐标
				OUT.normal = mul(float4(IN.normal, 0.0), _World2Object).xyz;

				//【3】返回此输出结构对象
				return OUT;
			}

			//--------------------------------【片段着色函数】-----------------------------
			// 输入:顶点输出结构体
			// 输出:float4型的像素颜色值
			//---------------------------------------------------------------------------------
			fixed4 frag(v2f IN) : COLOR
			{
				//【1】先准备好需要的参数
				//获取法线的方向
				float3 normalDirection = normalize(IN.normal);
				//获取入射光线的方向
				float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
				//获取视角方向
				float3 viewDirection = normalize(_WorldSpaceCameraPos - IN.posWorld.xyz);

				//【2】计算出漫反射颜色值  Diffuse=LightColor * MainColor * max(0,dot(N,L))
				float3 diffuse = _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection));

				//【3】计算镜面反射颜色值
				float3 specular;
				//若是法线方向和入射光方向大于180度,镜面反射值为0
				if (dot(normalDirection, lightDirection) < 0.0)
				{
					specular = float3(0.0, 0.0, 0.0);
				}
				//否则,根据公式进行计算 Specular =LightColor * SpecColor *pow(max(0,dot(R,V)),Shiness),R=reflect(-L,N)
				else
				{
					float3 reflectDirection = reflect(-lightDirection, normalDirection);
						specular = _LightColor0.rgb * _SpecColor.rgb * pow(max(0.0, dot(reflectDirection, viewDirection)), _SpecShininess);
				}

				//【4】合并漫反射、镜面反射、环境光的颜色值
				float4 diffuseSpecularAmbient = float4(diffuse, 1.0) + float4(specular, 1.0) + UNITY_LIGHTMODEL_AMBIENT;

				//【5】将漫反射-镜面反射-环境光的颜色值返回
				return diffuseSpecularAmbient;
			}

			//===========结束CG着色器语言编写模块===========
			ENDCG
		}
	}
}

将其施用于材质之上的效果如下:

其中的Specular Shininess滑条用于调节高光的衰减级数。

OK,我们将颜色调节一下,看看新的效果:

五、带纹理载入的specular shader

当然要支持纹理载入,不然实用性太低。Properties中加入纹理属性,其他的地方进行相应的变化,便可以得到支持纹理载入的specular shader:

//支持纹理载入的specular shader 

Shader "浅墨Shader编程/Volume13/5.Specular with Shader"
{
	//------------------------------------【属性值】------------------------------------
	Properties
	{
		//主纹理
		_MainTex("Texture", 2D) = "white" {}
		//主颜色
		_Color("Main Color", Color) = (1, 1, 1, 1)
		//镜面反射颜色
		_SpecColor("Specular Color", Color) = (1, 1, 1, 1)
		//镜面反射光泽度
		_SpecShininess("Specular Shininess", Range(1.0, 100.0)) = 10.0
	}

	//------------------------------------【唯一的子着色器】------------------------------------
	SubShader
	{
		//渲染类型设置:不透明
		Tags{ "RenderType" = "Opaque" }

		//--------------------------------唯一的通道-------------------------------
		Pass
		{
			//光照模型ForwardBase
			Tags{ "LightMode" = "ForwardBase" }
			//===========开启CG着色器语言编写模块===========
			CGPROGRAM

			//编译指令:告知编译器顶点和片段着色函数的名称
			#pragma vertex vert
			#pragma fragment frag

			//顶点着色器输入结构
			struct appdata
			{
				float4 vertex : POSITION;//顶点位置
				float3 normal : NORMAL;//法线向量坐标
				float2 texcoord : TEXCOORD0;//一级纹理坐标
			};

			//顶点着色器输出结构
			struct v2f
			{
				float4 pos : SV_POSITION;//像素位置
				float3 normal : NORMAL;//法线向量坐标
				float2 texcoord : TEXCOORD0;//一级纹理坐标
				float4 posWorld : TEXCOORD1;//在世界空间中的坐标位置
			};

			//变量的声明
			float4 _LightColor0;
			float4 _Color;
			sampler2D _MainTex;
			float4 _SpecColor;
			float _SpecShininess;

			//--------------------------------【顶点着色函数】-----------------------------
			// 输入:顶点输入结构体
			// 输出:顶点输出结构体
			//---------------------------------------------------------------------------------
			//顶点着色函数
			v2f vert(appdata IN)
			{
				//【1】声明一个输出结构对象
				v2f OUT;

				//【2】填充此输出结构
				//输出的顶点位置为模型视图投影矩阵乘以顶点位置,也就是将三维空间中的坐标投影到了二维窗口
				OUT.pos = mul(UNITY_MATRIX_MVP, IN.vertex);
				//获得顶点在世界空间中的位置坐标
				OUT.posWorld = mul(_Object2World, IN.vertex);
				//获取顶点在世界空间中的法线向量坐标
				OUT.normal = mul(float4(IN.normal, 0.0), _World2Object).xyz;
				//输出的纹理坐标也就是输入的纹理坐标
				OUT.texcoord = IN.texcoord;

				//【3】返回此输出结构对象
				return OUT;
			}

			//--------------------------------【片段着色函数】-----------------------------
			// 输入:顶点输出结构体
			// 输出:float4型的像素颜色值
			//---------------------------------------------------------------------------------
			fixed4 frag(v2f IN) : COLOR
			{
				//【1】先准备好需要的参数
				//获取纹理颜色
				float4 texColor = tex2D(_MainTex, IN.texcoord);
				//获取法线的方向
				float3 normalDirection = normalize(IN.normal);
				//获取入射光线的方向
				float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
				//获取视角方向
				float3 viewDirection = normalize(_WorldSpaceCameraPos - IN.posWorld.xyz);

				//【2】计算出漫反射颜色值  Diffuse=LightColor * MainColor * max(0,dot(N,L))
				float3 diffuse = _LightColor0.rgb * _Color.rgb * max(0.0, dot(normalDirection, lightDirection));

				//【3】计算镜面反射颜色值
				float3 specular;
				//若是法线方向和入射光方向大于180度,镜面反射值为0
				if (dot(normalDirection, lightDirection) < 0.0)
				{
					specular = float3(0.0, 0.0, 0.0);
				}
				//否则,根据公式进行计算 Specular =LightColor * SpecColor *pow(max(0,dot(R,V)),Shiness),R=reflect(-L,N)
				else
				{
					float3 reflectDirection = reflect(-lightDirection, normalDirection);
					specular = _LightColor0.rgb * _SpecColor.rgb * pow(max(0.0, dot(reflectDirection, viewDirection)), _SpecShininess);
				}

				//【4】合并漫反射、镜面反射、环境光的颜色值
				float4 diffuseSpecularAmbient = float4(diffuse, 1.0) + float4(specular, 1.0) + UNITY_LIGHTMODEL_AMBIENT;

				//【5】将漫反射-镜面反射-环境光的颜色值乘以纹理颜色值之后返回
				return diffuseSpecularAmbient * texColor;
			}

			//===========结束CG着色器语言编写模块===========
			ENDCG
		}
	}
}

将其施用于材质之上的效果如下:

将此Shader施于妙蛙草的模型之上,得到的便是如端游《剑灵》一般油腻腻的画风感觉:

本文Shader的全家福:

最后,依然是放出加入特效后的场景截图。

OK,本篇的内容大致如此,下次更新见。

附1: 本博文相关资源下载链接清单

【百度云】博文游戏场景exe下载

【百度云】博文示例场景资源和源码工程下载 ( PS:工程所用Unity版本为5.2.1)

【Github】本文全部Shader源码

附2:Reference

[1] http://docs.unity3d.com/Manual/SL-Reference.html

[2] https://en.wikibooks.org/wiki/Cg_Programming

时间: 2024-10-11 13:57:07

【浅墨Unity3D Shader编程】之十三 单色透明Shader & 标准镜面高光Shader的相关文章

【浅墨Unity3D Shader编程】之二 雪山飞狐篇:Unity的基本Shader框架写法&amp;颜色、光照与材质

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/40955607 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] 本篇文章中,我们学习了Unity Shader的基本写法框架,以及学习了Shader中Properties(属性)的详细写法,光照.材质与颜色的具体写法.写了6个Shader作为本文S

【浅墨Unity3D Shader编程】之七 静谧之秋篇: 表面着色器的写法(二)——自定义光照模式

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://hpw123.net/plus/view.php?aid=183 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] QQ交流群:330595914 更多文章尽在:http://www.hpw123.net 本文主要讲解了Unity中SurfaceShader的自定义光照模式的写法. 上篇文章中我们已经说到,表面着色器将分为两次讲解,上

【浅墨Unity3D Shader编程】之三 光之城堡篇:子着色器、通道与标签的写法 &amp; 纹理混合

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/41175585 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] 本文介绍了Unity中子着色器.通道和标签相关的详细概念与写法,以及纹理的设置方法,基本的纹理混合写法,写了5个Shader作为本文Shader讲解的实战内容,最后创建了一个梦幻的光之

【浅墨Unity3D Shader编程】之五 圣诞夜篇: Unity中Shader的三种形态对比&amp;混合操作合辑

本系列文章由@浅墨_毛星云 出品,转载请注明出处.  文章链接:http://hpw123.net/a/C__/kongzhitaichengxu/2014/1222/164.html 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] QQ交流群:330595914 更多文章尽在:http://www.hpw123.net 本文算是固定功能Shader的最后一篇,下一次更新应该就会开始讲解表面Shader,而

【浅墨Unity3D Shader编程】之六 暗黑城堡篇: 表面着色器(Surface Shader)的写法(一)

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://hpw123.net/plus/view.php?aid=165 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] QQ交流群:330595914 更多文章尽在:http://www.hpw123.net 本文主要讲解了Unity中SurfaceShader的具体写法,以及几个常用的CG函数的用法. 在这里先说明一下,表面着色器将分为

【浅墨Unity3D Shader编程】之一 游戏场景的创建 &amp; 第一个Shader的书写

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/40723789 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] 作为一个系统介绍Unity3D中Shader编写的系列文章的开篇,本文的第一部分系列文章的前言,然后第二部分介绍了这个系列文章中我们会使用的游戏场景创建方式,最后一部分讲解了如何在Un

【浅墨Unity3D Shader编程】之四 热带雨林篇: 剔除、深度测试、Alpha测试以及基本雾效合辑

本系列文章由@浅墨_毛星云 出品,转载请注明出处.   文章链接:http://hpw123.net/a/C__/kongzhitaichengxu/2014/1222/163.html 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] QQ交流群:330595914 更多文章尽在:http://www.hpw123.net 本文介绍了Unity中Shader书写中会用到的剔除.深度测试.Alpha测试以及基

【浅墨Unity3D Shader编程】之十一 深入理解Unity5中的Standard Shader(三)&amp;屏幕像素化特效的实现

本系列文章由@浅墨_毛星云 出品,转载请注明出处.   文章链接:http://blog.csdn.net/poem_qianmo/article/details/50095705 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 本文工程使用的Unity3D版本: 5.2.1  概要:续接上文,本文进一步讲解与分析了上文未讲完的Unity5中Standard Shader正向基础渲染通道源码的片段着色实现部分,以及对屏幕像素化后期特效进行了实现. 同

【浅墨Unity3D Shader编程】之一 夏威夷篇:游戏场景的创建 &amp; 第一个Shader的书写

本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/40723789 作者:毛星云(浅墨)    微博:http://weibo.com/u/1723155442 邮箱: [email protected] 作为一个系统介绍Unity3D中Shader编写的系列文章的开篇,本文的第一部分为系列文章的前言,然后第二部分介绍了这个系列文章中我们会使用的游戏场景创建方式,最后一部分讲解了如何在U