Unity引擎制作玻璃的反射和折射效果

Unity引擎制作玻璃球+玻璃杯

  大家好,我是阿赵。
  之前做海面效果的时候,没做反射和折射的效果,因为我觉得过于复杂的效果没有太大的实际作用。这方面的效果,我就做了现在这个例子来补充一下。
在这里插入图片描述

  在这个demo场景里面,我建了一个面片,然后贴了一张室内场景的贴图。然后建了一个球放在面片前面。

一、 反射

  首先我要给这个球加一个反射的效果。熟悉我的朋友应该都知道,我出于性能的考虑,是不会做真反射的。而我选择的反射方案,一般就是Matcap。
  Matcap之前我已经专门写了2篇文章介绍过,有兴趣的朋友可以往前翻一下。这里简略的说一下就算了。Matcap的原理简单,就是用物体的世界法线方向转到View坐标系,然后把这个当做UV去采样一张Matcap贴图。所以Matcap实际上比采样CubeMap消耗还小,而且效果也比较的逼真。
  Matcap的实现很简单,在ASE里面的连线是这样的:
在这里插入图片描述

  把这个Matcap的颜色输出:
在这里插入图片描述

  我赋予了一张Matcap贴图,然后稍微调了一点强度,看看效果:
在这里插入图片描述
在这里插入图片描述

  可以看出,现在球体上已经有一些反射的效果了。

二、 折射

  折射,有多种做法,比较常见的一种,是用CubeMap做一个假环境,然后通过对CubeMap的采样过程进行扭曲,做到折射的效果。
  但我这次并不打算这样做折射,因为CubeMap折射的是假的环境,是不会变化的,我这里做一个稍微真实折射,通过GrabTexture,捕捉屏幕图像,然后再通过对GrabTexture的采样UV进行扭曲,达到折射的效果。
  在ASE里面的连线大概是这样的:
在这里插入图片描述

  需要注意的有以下这些点:
1、 GrabTexture一定要自定义贴图名称
  GrabTexture的获取有2种方式,一种是不指定贴图名称,另一种是指定贴图名称。
这两种方式从结果看,效果是一模一样的。但如果不指定贴图名称,如果同一帧里面有多个材质球需要采样,就会捕捉多次屏幕,造成巨大的性能消耗。
  如果指定了贴图名称,那么就可以大量的减少采样的次数了。GrabTexture捕捉屏幕画面,本身就是一个比较大的消耗,其实能不用最好是不用的。不过一些自带扭曲的效果,如果要实现,要么用后处理的方式做,要么用GrabTexture做,实际上后处理是把一帧的渲染结果当做RenderTexture拿到,再对这张RenderTexture进行处理,消耗也同样不小的。

2、 获取屏幕坐标的方法
  由于dx和OpenGL的UV坐标方向不一样,所以要获得GrabTexture对应顶点的屏幕坐标,是需要分别处理的,ASE里面的GrabScreenPosition节点,已经做了这样的处理:

	inline float4 ASE_ComputeGrabScreenPos( float4 pos )
	{
		#if UNITY_UV_STARTS_AT_TOP
		float scale = -1.0;
		#else
		float scale = 1.0;
		#endif
		float4 o = pos;
		o.y = pos.w * 0.5f;
		o.y = ( pos.y - o.y ) * _ProjectionParams.x * scale + o.y;
		return o;

}

  如果不想自己判断是不是UNITY_UV_STARTS_AT_TOP,也可以直接用UnityCG.cginc内置的方法ComputeScreenPos来获取这个坐标。
3、 通过Lerp来过渡
  首先来明确一个问题,我们现在不是根据折射率来计算真实的折射,而是通过采样一帧的画面,进行扭曲模拟的折射效果。而我们能使用的扭曲画面的参数,我这里是使用了法线方向。根据法线方向的变化,对GrabTexture的采样UV进行偏移。
  既然是这样,那么我们要获得一个世界空间的法线方向和视角的关系,来确定扭曲的强度。这个东西之前我们用过很多了,就是NDotV了:
在这里插入图片描述

  根据这个的变化,来采样GrabTexture,然后再和正常的GrabTexture屏幕坐标,通过一个强度值,做一个Lerp的变化:
在这里插入图片描述

  这样就能获得一个反射的颜色,如果单独输出这个反射颜色
在这里插入图片描述

球的效果会是这样的:
在这里插入图片描述

  如果叠加上Matcap的反射效果:
在这里插入图片描述

  球体的效果会变成这样:
在这里插入图片描述

三、 边缘

  从模拟的效果看,现在的球体已经有一定的玻璃的特征了,但似乎并没有什么厚度,更像是一个肥皂泡泡。
  为了解决这个问题,我尝试给他加一个边缘光。边缘光的算法其实就是之前做海面效果的菲涅尔了。
在这里插入图片描述

  然后把这个边缘光也叠加上去:
在这里插入图片描述

  调整一下菲涅尔的最大最小值,可以得到这样的效果:
在这里插入图片描述

四、 整体颜色

  现在的玻璃默认是白色的,因为我们叠加的效果都是反射折射和边缘光,还没有给固有色指定。所以我添加一个mainColor作为固有色。也可以通过贴图采样作为固有色,都可以,这里就简单给一个颜色算了。
在这里插入图片描述

  所以调整一下颜色,就可以得到:
在这里插入图片描述

或者:
在这里插入图片描述

五、 应用于其他的模型

  在球体上面实现了这个效果了,接下来把它应用到别的形状的模型,看看效果对不对:
在这里插入图片描述

  这里做了个酒杯,里面有半杯液体。
  由于这个杯子整体来看每个面的法线方向都不一样,可以看出,整体的折射的模拟效果还是比较的正确的。然后由于不同的材质球可以调不同的折射强度,所以液体的折射和被子的折射可以调成不一样,看起来效果会更真实一点。

六、 源码

  由于这个Shader是用ASE编的,所以我也提供一下生成的代码,各位有兴趣可以去ASE里面看看连线的情况:

// Made with Amplify Shader Editor
// Available at the Unity Asset Store - http://u3d.as/y3X 
Shader "ballRefract"
{
	Properties
	{
		_mainColor("mainColor", Color) = (1,1,1,1)
		_matcapTex("matcapTex", 2D) = "white" {}
		_matcapStrength("matcapStrength", Range( 0 , 1)) = 0
		_refractStrength("refractStrength", Range( 0 , 1)) = 0
		_fresnelVal("fresnelVal", Range( 0 , 1)) = 1
		_fresnelMax("fresnelMax", Range( 0 , 1)) = 1
		_fresnelMin("fresnelMin", Range( 0 , 1)) = 0

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

		CGINCLUDE
		#pragma target 3.0
		ENDCG
		Blend Off
		AlphaToMask Off
		Cull Back
		ColorMask RGBA
		ZWrite On
		ZTest LEqual
		Offset 0 , 0
		
		
		GrabPass{ }

		Pass
		{
			Name "Unlit"
			Tags { "LightMode"="ForwardBase" }
			CGPROGRAM

			#if defined(UNITY_STEREO_INSTANCING_ENABLED) || defined(UNITY_STEREO_MULTIVIEW_ENABLED)
			#define ASE_DECLARE_SCREENSPACE_TEXTURE(tex) UNITY_DECLARE_SCREENSPACE_TEXTURE(tex);
			#else
			#define ASE_DECLARE_SCREENSPACE_TEXTURE(tex) UNITY_DECLARE_SCREENSPACE_TEXTURE(tex)
			#endif


			#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 "UnityShaderVariables.cginc"
			#define ASE_NEEDS_FRAG_WORLD_POSITION


			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;
				float4 ase_texcoord2 : TEXCOORD2;
				UNITY_VERTEX_INPUT_INSTANCE_ID
				UNITY_VERTEX_OUTPUT_STEREO
			};

			uniform float4 _mainColor;
			uniform sampler2D _matcapTex;
			uniform float _matcapStrength;
			ASE_DECLARE_SCREENSPACE_TEXTURE( _GrabTexture )
			uniform float _refractStrength;
			uniform float _fresnelMin;
			uniform float _fresnelMax;
			uniform float _fresnelVal;
			inline float4 ASE_ComputeGrabScreenPos( float4 pos )
			{
				#if UNITY_UV_STARTS_AT_TOP
				float scale = -1.0;
				#else
				float scale = 1.0;
				#endif
				float4 o = pos;
				o.y = pos.w * 0.5f;
				o.y = ( pos.y - o.y ) * _ProjectionParams.x * scale + o.y;
				return o;
			}
			

			
			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;
				float4 ase_clipPos = UnityObjectToClipPos(v.vertex);
				float4 screenPos = ComputeScreenPos(ase_clipPos);
				o.ase_texcoord2 = screenPos;
				
				
				//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;
				float4 matcap24 = ( tex2D( _matcapTex, (mul( float4( ase_worldNormal , 0.0 ), UNITY_MATRIX_V ).xyz*0.5 + 0.5).xy ) * _matcapStrength );
				float4 screenPos = i.ase_texcoord2;
				float4 ase_grabScreenPos = ASE_ComputeGrabScreenPos( screenPos );
				float4 ase_grabScreenPosNorm = ase_grabScreenPos / ase_grabScreenPos.w;
				float2 appendResult3 = (float2(ase_grabScreenPosNorm.r , ase_grabScreenPosNorm.g));
				float3 ase_worldViewDir = UnityWorldSpaceViewDir(WorldPosition);
				ase_worldViewDir = normalize(ase_worldViewDir);
				float dotResult8 = dot( ase_worldNormal , ase_worldViewDir );
				float NDotV25 = dotResult8;
				float2 temp_cast_3 = (NDotV25).xx;
				float2 lerpResult5 = lerp( appendResult3 , temp_cast_3 , _refractStrength);
				float4 screenColor1 = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_GrabTexture,lerpResult5);
				float4 refract32 = screenColor1;
				float smoothstepResult9 = smoothstep( _fresnelMin , _fresnelMax , NDotV25);
				float Fresnel29 = ( ( 1.0 - smoothstepResult9 ) * _fresnelVal );
				
				
				finalColor = ( _mainColor * ( matcap24 + refract32 + Fresnel29 ) );
				return finalColor;
			}
			ENDCG
		}
	}
	CustomEditor "ASEMaterialInspector"
	
	
}
/*ASEBEGIN
Version=18500
1920;0;1920;1019;3260.221;1877.513;3.859619;True;True
Node;AmplifyShaderEditor.CommentaryNode;28;-2376.601,15.2;Inherit;False;754.8627;439;Comment;4;7;6;8;25;NDotV;1,1,1,1;0;0
Node;AmplifyShaderEditor.ViewDirInputsCoordNode;7;-2312.601,266.2;Inherit;False;World;False;0;4;FLOAT3;0;FLOAT;1;FLOAT;2;FLOAT;3
Node;AmplifyShaderEditor.WorldNormalVector;6;-2326.601,65.2;Inherit;False;False;1;0;FLOAT3;0,0,1;False;4;FLOAT3;0;FLOAT;1;FLOAT;2;FLOAT;3
Node;AmplifyShaderEditor.CommentaryNode;16;-1784.797,-866.7996;Inherit;False;1347.15;473.2724;Comment;9;24;23;22;20;21;19;38;18;17;matcap;1,1,1,1;0;0
Node;AmplifyShaderEditor.DotProductOpNode;8;-2038.601,175.2;Inherit;False;2;0;FLOAT3;0,0,0;False;1;FLOAT3;0,0,0;False;1;FLOAT;0
Node;AmplifyShaderEditor.WorldNormalVector;18;-1734.797,-816.7996;Inherit;False;False;1;0;FLOAT3;0,0,1;False;4;FLOAT3;0;FLOAT;1;FLOAT;2;FLOAT;3
Node;AmplifyShaderEditor.RegisterLocalVarNode;25;-1845.738,162.044;Inherit;True;NDotV;-1;True;1;0;FLOAT;0;False;1;FLOAT;0
Node;AmplifyShaderEditor.CommentaryNode;34;-3346.037,-815.1047;Inherit;False;1148.658;457.9377;Comment;7;2;3;26;4;5;1;32;refract;1,1,1,1;0;0
Node;AmplifyShaderEditor.CommentaryNode;30;-2542.042,578.4539;Inherit;False;1269.011;491.3077;Comment;8;10;11;27;9;13;12;14;29;Fresnel;1,1,1,1;0;0
Node;AmplifyShaderEditor.ViewMatrixNode;17;-1690.02,-599.9698;Inherit;False;0;1;FLOAT4x4;0
Node;AmplifyShaderEditor.RangedFloatNode;10;-2492.042,755.5684;Inherit;False;Property;_fresnelMin;fresnelMin;6;0;Create;True;0;0;False;0;False;0;0;0;1;0;1;FLOAT;0
Node;AmplifyShaderEditor.RangedFloatNode;11;-2488.042,888.5684;Inherit;False;Property;_fresnelMax;fresnelMax;5;0;Create;True;0;0;False;0;False;1;1;0;1;0;1;FLOAT;0
Node;AmplifyShaderEditor.GetLocalVarNode;27;-2354.074,628.4539;Inherit;False;25;NDotV;1;0;OBJECT;;False;1;FLOAT;0
Node;AmplifyShaderEditor.SimpleMultiplyOpNode;19;-1543.883,-676.3995;Inherit;False;2;2;0;FLOAT3;0,0,0;False;1;FLOAT4x4;0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1;False;1;FLOAT3;0
Node;AmplifyShaderEditor.GrabScreenPosition;2;-3296.037,-765.1047;Inherit;False;0;0;5;FLOAT4;0;FLOAT;1;FLOAT;2;FLOAT;3;FLOAT;4
Node;AmplifyShaderEditor.RangedFloatNode;38;-1598.579,-491.8766;Inherit;False;Constant;_Float0;Float 0;7;0;Create;True;0;0;False;0;False;0.5;0;0;0;0;1;FLOAT;0
Node;AmplifyShaderEditor.SmoothstepOpNode;9;-2123.853,749.8372;Inherit;False;3;0;FLOAT;0;False;1;FLOAT;0;False;2;FLOAT;1;False;1;FLOAT;0
Node;AmplifyShaderEditor.DynamicAppendNode;3;-3002.037,-729.1048;Inherit;False;FLOAT2;4;0;FLOAT;0;False;1;FLOAT;0;False;2;FLOAT;0;False;3;FLOAT;0;False;1;FLOAT2;0
Node;AmplifyShaderEditor.RangedFloatNode;4;-3170.409,-473.167;Inherit;False;Property;_refractStrength;refractStrength;3;0;Create;True;0;0;False;0;False;0;0;0;1;0;1;FLOAT;0
Node;AmplifyShaderEditor.ScaleAndOffsetNode;20;-1404.423,-612.3016;Inherit;False;3;0;FLOAT3;0,0,0;False;1;FLOAT;0.5;False;2;FLOAT;0.5;False;1;FLOAT3;0
Node;AmplifyShaderEditor.GetLocalVarNode;26;-3093.938,-599.0139;Inherit;False;25;NDotV;1;0;OBJECT;;False;1;FLOAT;0
Node;AmplifyShaderEditor.SamplerNode;21;-1140.799,-747.8926;Inherit;True;Property;_matcapTex;matcapTex;1;0;Create;True;0;0;False;0;False;-1;None;461d573efcdd854449e8d60d7851cb85;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.OneMinusNode;12;-1908.118,772.1724;Inherit;False;1;0;FLOAT;0;False;1;FLOAT;0
Node;AmplifyShaderEditor.RangedFloatNode;13;-2110.825,953.7616;Inherit;False;Property;_fresnelVal;fresnelVal;4;0;Create;True;0;0;False;0;False;1;1;0;1;0;1;FLOAT;0
Node;AmplifyShaderEditor.LerpOp;5;-2821.126,-595.4236;Inherit;False;3;0;FLOAT2;0,0;False;1;FLOAT2;0,0;False;2;FLOAT;0;False;1;FLOAT2;0
Node;AmplifyShaderEditor.RangedFloatNode;22;-1188.587,-525.996;Inherit;False;Property;_matcapStrength;matcapStrength;2;0;Create;True;0;0;False;0;False;0;0.484;0;1;0;1;FLOAT;0
Node;AmplifyShaderEditor.SimpleMultiplyOpNode;23;-799.8513,-661.761;Inherit;False;2;2;0;COLOR;0,0,0,0;False;1;FLOAT;0;False;1;COLOR;0
Node;AmplifyShaderEditor.SimpleMultiplyOpNode;14;-1726.323,856.4717;Inherit;False;2;2;0;FLOAT;0;False;1;FLOAT;0;False;1;FLOAT;0
Node;AmplifyShaderEditor.ScreenColorNode;1;-2640.674,-692.4675;Inherit;False;Global;_GrabScreen0;Grab Screen 0;0;0;Create;True;0;0;False;0;False;Object;-1;False;False;1;0;FLOAT2;0,0;False;5;COLOR;0;FLOAT;1;FLOAT;2;FLOAT;3;FLOAT;4
Node;AmplifyShaderEditor.RegisterLocalVarNode;29;-1497.031,822.4349;Inherit;True;Fresnel;-1;True;1;0;FLOAT;0;False;1;FLOAT;0
Node;AmplifyShaderEditor.RegisterLocalVarNode;32;-2421.379,-648.5054;Inherit;False;refract;-1;True;1;0;COLOR;0,0,0,0;False;1;COLOR;0
Node;AmplifyShaderEditor.RegisterLocalVarNode;24;-642.5197,-647.4543;Inherit;False;matcap;-1;True;1;0;COLOR;0,0,0,0;False;1;COLOR;0
Node;AmplifyShaderEditor.GetLocalVarNode;31;-388.149,69.38911;Inherit;False;29;Fresnel;1;0;OBJECT;;False;1;FLOAT;0
Node;AmplifyShaderEditor.GetLocalVarNode;33;-369.1212,4.012573;Inherit;False;32;refract;1;0;OBJECT;;False;1;COLOR;0
Node;AmplifyShaderEditor.GetLocalVarNode;35;-369.3356,-79.31943;Inherit;False;24;matcap;1;0;OBJECT;;False;1;COLOR;0
Node;AmplifyShaderEditor.ColorNode;36;-334.0762,-270.2781;Inherit;False;Property;_mainColor;mainColor;0;0;Create;True;0;0;False;0;False;1,1,1,1;0,0,0,0;True;0;5;COLOR;0;FLOAT;1;FLOAT;2;FLOAT;3;FLOAT;4
Node;AmplifyShaderEditor.SimpleAddOpNode;15;-140.1225,-8.462204;Inherit;False;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;37;-13.75513,-124.8451;Inherit;False;2;2;0;COLOR;0,0,0,0;False;1;COLOR;0,0,0,0;False;1;COLOR;0
Node;AmplifyShaderEditor.TemplateMultiPassMasterNode;0;173.9,-159.1;Float;False;True;-1;2;ASEMaterialInspector;100;1;ballRefract;0770190933193b94aaa3065e307002fa;True;Unlit;0;0;Unlit;2;True;0;1;False;-1;0;False;-1;0;1;False;-1;0;False;-1;True;0;False;-1;0;False;-1;False;False;False;False;False;False;True;0;False;-1;True;0;False;-1;True;True;True;True;True;0;False;-1;False;False;False;True;False;255;False;-1;255;False;-1;255;False;-1;7;False;-1;1;False;-1;1;False;-1;1;False;-1;7;False;-1;1;False;-1;1;False;-1;1;False;-1;True;1;False;-1;True;3;False;-1;True;True;0;False;-1;0;False;-1;True;2;RenderType=Opaque=RenderType;Queue=Transparent=Queue=0;True;2;0;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;False;True;1;LightMode=ForwardBase;False;0;;0;0;Standard;1;Vertex Position,InvertActionOnDeselection;1;0;1;True;False;;False;0
WireConnection;8;0;6;0
WireConnection;8;1;7;0
WireConnection;25;0;8;0
WireConnection;19;0;18;0
WireConnection;19;1;17;0
WireConnection;9;0;27;0
WireConnection;9;1;10;0
WireConnection;9;2;11;0
WireConnection;3;0;2;1
WireConnection;3;1;2;2
WireConnection;20;0;19;0
WireConnection;20;1;38;0
WireConnection;20;2;38;0
WireConnection;21;1;20;0
WireConnection;12;0;9;0
WireConnection;5;0;3;0
WireConnection;5;1;26;0
WireConnection;5;2;4;0
WireConnection;23;0;21;0
WireConnection;23;1;22;0
WireConnection;14;0;12;0
WireConnection;14;1;13;0
WireConnection;1;0;5;0
WireConnection;29;0;14;0
WireConnection;32;0;1;0
WireConnection;24;0;23;0
WireConnection;15;0;35;0
WireConnection;15;1;33;0
WireConnection;15;2;31;0
WireConnection;37;0;36;0
WireConnection;37;1;15;0
WireConnection;0;0;37;0
ASEEND*/
//CHKSM=B4E7B803ED14C858BF513B4EAA46C6658043AB88
  • 14
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值