Chapter13_使用深度和法线纹理

深度纹理

深度纹理实际就是一张渲染纹理,只不过它里边存储的是像素值不是颜色值,而是一个高精度的深度值。

顶点座标转化到NDC(归一化的设备座标)下的座标的z分量就是顶点的深度值。NDC中,z分量范围在[-1,1],为了让z分量可以存到一张纹理中,需要使用公式将z分量映射:

                                                                          d = 0.5 * \large z_{ndc} + 0.5

Unity获取深度纹理方式分为两种方式:

在Unity中,可以让摄像机生成一张深度纹理或是深度+法线纹理。

1.当只生成一张深度纹理时,Unity在使用延迟渲染路径时(渲染路径决定了光照如何应用到Shader中,决定了把光源信息和处理后的光照信息放到那些数据中。),直接从G-buffer中获取深度缓存

2.在使用前向渲染路劲时,渲染深度纹理通过使用着色器替换技术,渲染需要的不透明物体,并使用它投射阴影时使用的Pass(即LightMode 设置为ShadowCaster 的Pass)来得到深度纹理。具体实现是,Unity会使用着色器替换技术渲染那些渲染类型(即SubShader的RenderType标签)为Opaque的物体,判断它们使用的渲染队列是否 <= 2500,如果满足,就把它渲染到深度和法线纹理中。

深度纹理的精度通常是24位活16位。如果选择生成一张深度+法线纹理,Unity会创建一张和屏幕分辨率相同,精度为32位(每个通道8位)的纹理。法线信息存到R和G通道。深度信息存到B和A通道。

  • 使用延迟渲染路径时:Unity只需要合并深度和法线缓存即可。
  • 使用前向渲染路劲时:默认不会创建法线缓存。因此Unity底层使用了一个单独的Pass把整个场景再次渲染一遍。这个Pass被放在一个内置Shader中。

代码具体获取深度纹理步骤:

  1. camera.depehtTextureMode = DepthTextureMode.Depth; //获取深度纹理
  2. Shader中直接访问特定纹理名称(_CameraDepthTexture)即可。

camera.depehtTextureMode = DepthTextureMode.DepthNormals;//获取深度纹理 + 法线纹理

camera.depehtTextureMode |= DepthTextureMode.Depth;

camera.depehtTextureMode = DepthTextureMode.DepthNormals; 以上两句获取一张深度纹理 + 一张深度+法线纹理

采样深度纹理,非线性变线性推导。。。以后补上。。

当摄像机远裁剪平面的距离过大,会导致距离摄像机较近的物体被映射的深度值非常小,生成的深度纹理变"黑"。相反,远裁剪平面过大,会导致距离摄像机较近的物体被映射的深度值偏大。生成的深度纹理变"白"。


实践:

1.运动模糊

速度映射图模拟运动模糊。速度映射图中存储了每个像素的速度,然后使用这个速度决定模糊的方向和大小。

生成速度映射纹理有两种方式:

  1. 把场景中所有物体都渲染到一张纹理中,不过这种方法需要修改场景所有的Shader。
  2. 使用深度纹理在片元着色器中为每个像素计算世界空间下的位置(使用当前帧的视角*投影矩阵的逆矩阵计算)。当得到世界空间下的顶点座标后,然后使用前一帧视角*投影矩阵对其进行变换,得到该顶点位置在前一帧的NDC座标。当前帧顶点的NDC座标 - 前一帧的NDC位置得到该像素的速度。优点是屏幕后统一处理计算,缺点是两次矩阵变换,效率低。

 

Shader "Chan/Chapter 13/Motion Blur With Depth Texture" {
	Properties
	{
		_MainTex("Base Tex",2D) = "white"{}
		_BlurSize("Blur Size",float) = 1.0
	}
		SubShader
		{
			CGINCLUDE

			#include "UnityCG.cginc"

			sampler2D _MainTex;
			half4 _MainTex_TexelSize;
			//通过_CameraDepthTexture 直接就能访问到当前摄像机的深度纹理
			sampler2D _CameraDepthTexture;
			float4x4 _CurrentViewProjectionInverseMatrix;
			float4x4 _PreviousViewProjectionMatrix;
			half _BlurSize;

			struct v2f
			{
				float4 pos:SV_POSITION;
				half2 uv:TEXCOORD0;
				half2 uv_depth:TEXCOORD1;
			};

			v2f vert(appdata_img v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = v.texcoord;
				o.uv_depth = v.texcoord;

				//多张渲染纹理,可能uv采样座标会出问题 需要根据平台判断
				# if UNITY_UV_STARTS_AT_TOP
				if (_MainTex_TexelSize.y < 0)
					o.uv_depth.y = 1 - o.uv_depth.y;
				#endif
				return o;
			}

			fixed4 frag(v2f i) :SV_Target
			{
				//不同平台要通过uv采样深度纹理得到深度值,可能有差异。使用宏定义 内部处理了。SAMPLE_DEPTH_TEXTURE 
				float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth);
				//归一化设备座标系下的座标[-1,1],纹理通道值[0,1] 所以存储到深度纹理时,进行了映射。此处为反映射
				//归一化设备座标系下的座标座标(x,y值由uv映射而来,z值由深度值映射而来)
				float4 h = float4(i.uv.x * 2 - 1,i.uv.y * 2 - 1,d * 2 - 1,1);
				//归一化设备座标系下的座标 乘以当前摄像机 * 投影矩阵的逆矩阵 得到世界空间座标系的座标
				float4 D = mul(_CurrentViewProjectionInverseMatrix,h);
				//世界空间座标系的座标
				float4 worldPos = D / D.w;

				//当前世界座标乘以 当前摄像机视角 * 投影矩阵 得到上一帧时候在归一化设备座标系下的座标
				float4 currentPos = h;
				float4 previousPos = mul(_PreviousViewProjectionMatrix,worldPos);
				previousPos /= previousPos.w;

				//得到速度
				float2 velocity = (currentPos.xy - previousPos.xy) / 2.0f;

				float2 uv = i.uv;
				float4 c = tex2D(_MainTex,uv);
				uv += velocity * _BlurSize;//控制纹理采样距离
				//采样三地方,取了个平均值
				for (int it = 1; it < 3; it++, uv += velocity * _BlurSize)
				{
					float4 currentColor = tex2D(_MainTex,uv);
					c += currentColor;
				}
				c /= 3;

				return fixed4(c.rgb,1.0);
			}

			ENDCG

		Pass
			{
				//屏幕后处理的标配
				ZTest Always Cull Off 
				ZWrite Off
				CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag
				ENDCG
			}
		}
		Fallback Off
}

配合使用的C#脚本:

 

using UnityEngine;
using System.Collections;

public class MotionBlurWithDepthTexture : PostEffectsBase {

	public Shader motionBlurShader;
	private Material motionBlurMaterial = null;

	public Material material {  
		get {
			motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
			return motionBlurMaterial;
		}  
	}

	private Camera myCamera;
	public Camera camera {
		get {
			if (myCamera == null) {
				myCamera = GetComponent<Camera>();
			}
			return myCamera;
		}
	}

    //定义运动模糊时模糊图像使用的大小
	[Range(0.0f, 1.0f)]
	public float blurSize = 0.5f;
    
    //保存上一帧的 摄像机视角 * 投影矩阵
	private Matrix4x4 previousViewProjectionMatrix;
	
	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;

		previousViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
	}
	
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			material.SetFloat("_BlurSize", blurSize);

			material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
			Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
            //1.当前帧的(视角 * 投影矩阵)的逆矩阵,用于将顶点座标从归一化的设备座标变换到世界空间下
            //2.世界空间下的座标,经过上一帧的 摄像机视角 * 投影矩阵转换,得到上一帧的归一化设备座标
            //3.当前帧的归一化设备空间座标 - 上一帧的归一化设备空间座标 = 当前顶点的速度     
            Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;
			material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
			previousViewProjectionMatrix = currentViewProjectionMatrix;

			Graphics.Blit (src, dest, material);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

注解:

   归一化设备座标系(NDC)下的座标(x,y值由uv映射而来,z值由深度值映射而来)

   NDC(归一化的设备座标)下的座标的z分量就是顶点的深度值。NDC中,z分量范围在[-1,1],为了让z分量可以存到一张纹理中,需要使用公式将z分量映射:

                                                                          d = 0.5 * \large z_{ndc} + 0.5

  因此采样深度纹理后,需要进行反映射:\large z_{ndc} = d * 2 - 1


2.全局雾效 

基于屏幕后处理。根据深度纹理来重建每个像素在世界空间中的位置。

2.1重建世界座标

float4 worldPos = _WorldSpaceCameraPos + linearDepth * interpolatedRay;首先获取世界空间下的摄像机位置WorldSpaceCameraPos 。linearDepth * interpolatedRay 该像素相对于摄像机位置的偏移量。

  • 摄像机位置WorldSpaceCameraPos 。
  • linearDepth  = 深度纹理得到的线性深度值。\large z_{ndc} = d * 2 - 1
  • interpolatedRay 顶点着色器输出并插值后得到的射线,它不仅包含了该像素到摄像机的方向,也包含了距离信息。

 

 

scale = |TL|/Near 

 

2.2雾的计算

使用一个雾效系数 f ,混合原始颜色和雾的颜色。

float3 afterFog = f * fogColor + ( 1 - f ) * origColor;

Unity内置雾效,有三种方式计算次数f:

  • Linear(线性):

     \LARGE f = \tfrac{(d_{max} - |z|)}{(d_{max}-d_{min})}

dmax 和 dmin表示受雾影响的最大最小距离。

  • Exponential(指数): 

\LARGE f = e^{-d*|z|}

d 控制雾的浓度。 b 

  • Exponential Squared(指数): 

\LARGE f = e^{-(d-|z|)^{2}}

d 控制雾的浓度。

本文采样类似线性雾效计算方式。

\LARGE f = \tfrac{(H_{end} - y)}{(H_{end}-H_{start})}

Hend 和 Hsart分辨代表受雾影响的起始高度和终止高度。 

 

Shader "Chan/Chapter 13/Fog With Depth Texture" {
	Properties
	{
		_MainTex("Main Tex",2D) = "white"{}
		//雾密度
		_FogDensity("Fog Density",float) = 1.0
		//雾颜色
		_FogColor("Fog Color",Color) = (1,1,1,1)
		//雾开始高度
		_FogStart("Fog Start",float) = 0.0
		//雾结束高度
		_FogEnd("Fog End",float) = 1.0
	}
	SubShader
	{
		CGINCLUDE

		#include "UnityCG.cginc"

			float4x4 _FrustumCornersRay;
			sampler2D _MainTex;
			half4 _MainTex_TexelSize;
			//深度纹理
			sampler2D _CameraDepthTexture;

			half _FogDensity;
			fixed4 _FogColor;
			float _FogStart;
			float _FogEnd;

			struct v2f
			{
				float4 pos:SV_POSITION;
				half2 uv:TEXCOORD0;
				half2 uv_depth:TEXCOORD1;
				//存储
				float4 interpolateRay:TEXCOORD2;
			};

			v2f vert(appdata_img v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = v.texcoord;
				o.uv_depth = v.texcoord;

				#if UNITY_UV_STARTS_AT_TOP
				if (_MainTex_TexelSize.y < 0)
				{
					o.uv_depth.y = 1 - o.uv_depth.y;
				}
				#endif

				//屏幕后全局所用的模型是一个四边形网格,只包含四个顶点
				//判断当前顶点是哪个顶点,此处使用uv纹理座标判断。左下->右下->右上->左上
				int index = 0;
				if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5)
				{
					index = 0;
				}
				else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5)
				{
					index = 1;
				}
				else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5)
				{
					index = 2;
				}
				else
				{
					index = 3;
				}	

				//平台,设置差异 可能导致左下->右下->右上->左上错乱,跟c#脚本对不上,差异化处理
				#if UNITY_UV_STARTS_AT_TOP
				if (_MainTex_TexelSize.y < 0)
				{
					index = 3 - index;
				}
				#endif
				o.interpolateRay = _FrustumCornersRay[index];
				return o;
			}

			fixed4 frag(v2f i) :SV_Target
			{
				//不同平台要通过uv采样深度纹理得到深度值,可能有差异。使用宏定义 内部处理了。SAMPLE_DEPTH_TEXTURE 
				float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
			    //计算得到某个像素点的世界座标
				float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolateRay.xyz;
				//根据公式,雾效密度
				float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
				fogDensity = saturate(fogDensity * _FogDensity);
				fixed4 finalColor = tex2D(_MainTex,i.uv);
				//原始纹理颜色混合雾效颜色 = 最终颜色
				finalColor.rgb = lerp(finalColor.rgb,_FogColor.rgb,fogDensity);
				return finalColor;
			}
		ENDCG

		Pass
		{
			ZTest Always Cull Off
			ZWrite Off

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			ENDCG
		}
	}

	FallBack Off
}

注解:

                //不同平台要通过uv采样深度纹理得到深度值,可能有差异。使用宏定义 内部处理了。SAMPLE_DEPTH_TEXTURE 
                float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
                

                //计算得到某个像素点的世界座标 
                //摄像机世界座标 + 像素点相对于摄像机的偏移量(linearDepth * i.interpolateRay.xyz) = 像素的世界座标
               //linearDepth * i.interpolateRay.xyz = dist = depth * scale
                float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolateRay.xyz;

 

配合使用的C#脚本: 

using UnityEngine;
using System.Collections;

public class FogWithDepthTexture : PostEffectsBase {

	public Shader fogShader;
	private Material fogMaterial = null;

	public Material material {  
		get {
			fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial);
			return fogMaterial;
		}  
	}

	private Camera myCamera;
	public Camera camera {
		get {
			if (myCamera == null) {
				myCamera = GetComponent<Camera>();
			}
			return myCamera;
		}
	}

	private Transform myCameraTransform;
	public Transform cameraTransform {
		get {
			if (myCameraTransform == null) {
				myCameraTransform = camera.transform;
			}

			return myCameraTransform;
		}
	}

    //雾效密度上限
	[Range(0.0f, 3.0f)]
	public float fogDensity = 1.0f;

    //雾效颜色
	public Color fogColor = Color.white;

    //雾效起始高度
	public float fogStart = 0.0f;
    //雾效结束高度
	public float fogEnd = 2.0f;

	void OnEnable() {
		camera.depthTextureMode |= DepthTextureMode.Depth;
	}
	
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			Matrix4x4 frustumCorners = Matrix4x4.identity;

			float fov = camera.fieldOfView;
			float near = camera.nearClipPlane;
			float aspect = camera.aspect;

			float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad);
			Vector3 toRight = cameraTransform.right * halfHeight * aspect;
			Vector3 toTop = cameraTransform.up * halfHeight;

			Vector3 topLeft = cameraTransform.forward * near + toTop - toRight;
			float scale = topLeft.magnitude / near;

            // 与采样后的深度值相乘得到四个点相对于摄像机的偏移量
			topLeft.Normalize();
			topLeft *= scale;

			Vector3 topRight = cameraTransform.forward * near + toRight + toTop;
			topRight.Normalize();
			topRight *= scale;

			Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight;
			bottomLeft.Normalize();
			bottomLeft *= scale;

			Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop;
			bottomRight.Normalize();
			bottomRight *= scale;

			frustumCorners.SetRow(0, bottomLeft);
			frustumCorners.SetRow(1, bottomRight);
			frustumCorners.SetRow(2, topRight);
			frustumCorners.SetRow(3, topLeft);

            //将摄像机近裁剪平面的 左下->右下->右上->左上 计算出来后,传入shader中
            material.SetMatrix("_FrustumCornersRay", frustumCorners);

			material.SetFloat("_FogDensity", fogDensity);
			material.SetColor("_FogColor", fogColor);
			material.SetFloat("_FogStart", fogStart);
			material.SetFloat("_FogEnd", fogEnd);

			Graphics.Blit (src, dest, material);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

 

3.边缘检测

使用Sobel卷积算子进行边缘,物体的纹理,阴影可以能被描边。使用Roberts算子来替代。

Roberts算子本质是计算左上角-右下角的差值,乘以右上角-左下角的差值,作为评估边缘的依据。

具体代码操作:

通过uv采样深度+法线纹理的某点的相邻上下左右四点信息,然后比较左上-右下,右上-左下的深度值和 "法线"值,当深度值和法线值小于某个阈值,证明此点为边缘点。

Shader "Chan/Chapter 13/Edge Detection Normals And Depth" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		//边缘和原图混合比例
		_EdgeOnly ("Edge Only", Float) = 1.0
		//边缘颜色
		_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
		//背景色
		_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
		//采样距离
		_SampleDistance ("Sample Distance", Float) = 1.0
		//xy分量 分别对应了法线和深度的检测灵敏度
		_Sensitivity ("Sensitivity", Vector) = (1, 1, 1, 1)
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		fixed _EdgeOnly;
		fixed4 _EdgeColor;
		fixed4 _BackgroundColor;
		float _SampleDistance;
		half4 _Sensitivity;
		
		//深度+法线 纹理
		sampler2D _CameraDepthNormalsTexture;
		
		struct v2f {
			float4 pos : SV_POSITION;
			half2 uv[5]: TEXCOORD0;
		};
		  
		v2f vert(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);
			
			half2 uv = v.texcoord;
			o.uv[0] = uv;
			
			#if UNITY_UV_STARTS_AT_TOP
			if (_MainTex_TexelSize.y < 0)
				uv.y = 1 - uv.y;
			#endif
			
			//采样相邻的四个区域,以便进行卷积计算
			o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
			o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
			o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
			o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;
					 
			return o;
		}
		
		half CheckSame(half4 center, half4 sample) {

			//因为值比较两点的法线差值,因此不需要吧xy分量转化为整整的法线
			half2 centerNormal = center.xy;
			//将存在纹理信息zw分量中的深度值转化为float
			float centerDepth = DecodeFloatRG(center.zw);
			
			half2 sampleNormal = sample.xy;
			float sampleDepth = DecodeFloatRG(sample.zw);
			
			// difference in normals
			// do not bother decoding normals - there's no need here
			half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
			//传入的两个参数的“法线”差值小于0.1,判定法线有差异
			int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
			// difference in depth
			float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
			//传入的两个参数的深度差值小于0.1,判定深度有差异
			int isSameDepth = diffDepth < 0.1 * centerDepth;
			
			// return:
			// 1 - 差异比较小,不判定为边缘点
			// 0 - 差异比较大,判定为边缘点
			return isSameNormal * isSameDepth ? 1.0 : 0.0;
		}
		
		fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target {
			//通过uv 采样四个点
			half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
			half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
			half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
			half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
			
			half edge = 1.0;
			
			//深度和法线值相差都很大的情况下,才认为此点为边缘点
			edge *= CheckSame(sample1, sample2);
			edge *= CheckSame(sample3, sample4);
			
			fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
			fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
			
			return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
		}
		
		ENDCG
		
		Pass { 
			ZTest Always Cull Off 
		    ZWrite Off
			
			CGPROGRAM      
			
			#pragma vertex vert  
			#pragma fragment fragRobertsCrossDepthAndNormal
			
			ENDCG  
		}
	} 
	FallBack Off
}

配合使用的C#脚本:

using UnityEngine;
using System.Collections;

public class EdgeDetectNormalsAndDepth : PostEffectsBase {

	public Shader edgeDetectShader;
	private Material edgeDetectMaterial = null;
	public Material material {  
		get {
			edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
			return edgeDetectMaterial;
		}  
	}

	[Range(0.0f, 1.0f)]
	public float edgesOnly = 0.0f;

	public Color edgeColor = Color.black;

	public Color backgroundColor = Color.white;

    //采样距离
	public float sampleDistance = 1.0f;
    //深度值灵敏度
	public float sensitivityDepth = 1.0f;
    //法线值灵敏度
	public float sensitivityNormals = 1.0f;
	
	void OnEnable() {
        //获取深度+法线纹理
		GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
	}

	[ImageEffectOpaque]
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			material.SetFloat("_EdgeOnly", edgesOnly);
			material.SetColor("_EdgeColor", edgeColor);
			material.SetColor("_BackgroundColor", backgroundColor);
			material.SetFloat("_SampleDistance", sampleDistance);
			material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));

			Graphics.Blit(src, dest, material);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章