【ShaderToy】基础篇之谈谈点、线的绘制

写在前面

前面一篇的时候,发现还是不够基础。因此打算增加几篇基础篇,从点线面开始,希望可以更好理解。

其实用Pixel Shader的过程很像在纸上绘画的过程。屏幕上的每一个像素对应了纸上的一个方格,如果你愿意,你甚至可以一个个判断像素的位置,从而画出任何你想画的图像,也的确有爱好者这么做过。但往往,我们需要的是一个动态的效果,这个效果往往依赖于数学公式的约束。我们可以说是,用数学去绘画。我们用数学去约束,哪些点应该用什么颜色去绘制。

这篇,我们从基本的点和线开始,看一下如何在Pixel Shader里面随心画出点和线。

在哪里画

在开始之前,有一个最基本的问题我们要计算清楚,就是如何知道当前计算的fragment的像素位置。在之前的开篇中,我们给出了模板。其中v2f结构里,有一个很重要的变量srcPos,它的计算如下:

        v2f vert(appdata_base v) {
        	v2f o;
        	o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
            o.srcPos = ComputeScreenPos(o.pos);
            o.w = o.pos.w;
            return o;
        }  

ComputeScreenPos是在UnityCG.cginc中定义的函数,它就作用如名字一样,计算该顶点转换到屏幕上的位置。但如果我们想要得到正确的屏幕位置,还需要在frag函数中这样:

        fixed4 frag(v2f _iParam) : COLOR0 {
        	vec2 fragCoord = gl_FragCoord;
        	return main(gl_FragCoord);
        }  

其中:

  		// 屏幕的尺寸
  		#define iResolution _ScreenParams
  		// 屏幕中的坐标,以pixel为单位
  		#define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy) 

难懂的是gl_FragCoord的定义。(_iParam.srcPos.xy/_iParam.srcPos.w)将得到在屏幕中归一化后的屏幕位置,即返回分量范围在(0, 1)的屏幕横纵坐标值。屏幕的左下角值为(0, 0),右上角值为(1, 1)。然后再乘以屏幕的长款像素值,就得到了该fragment对应的屏幕像素位置。这是我们后面计算的基础。

根据不同的需求,我们会在shader中对位置有不同的需求:有时我们想要得到如上的像素位置,有时我们想得到相对于屏幕中心的uv坐标等等。以下有五种常见的位置需求:

		vec4 main(vec2 fragCoord) {
			vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
			vec2 pos = fragCoord.xy / iResolution.xy; // pos.x ~ (0, 1), pos.y ~ (0, 1)
			vec2 pos = fragCoord / min(iResolution.x, iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (0, 1.xx), pos.y ~ (0, 1)
			vec2 pos =fragCoord.xy / iResolution.xy * 2. - 1.; // pos.x ~ (-1, 1), pos.y ~ (-1, 1)
			vec2 pos = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.x,iResolution.y);	// If iResolution.x > iResolution.y, pos.x ~ (-1.xx, 1.xx), pos.y ~ (-1, 1)

			return vec4(1);
		}

当然需求不同,一开始计算的pos也会不同。

画第一个点(圆)

在Shader中,一个点其实可以当成一个圆形。那么问题就变成了如何绘制一个圆:给定圆心在屏幕的位置(圆心像素值占屏幕的百分比),圆的半径(像素为单位),以及圆的颜色,如何绘制一个圆。

为此,我们先需要在Properties中声明两个参数:_Parameters和_Color:

Shader "shadertoy/Simple Circle" {
	Properties{
		_Parameters ("Circle Parameters", Vector) = (0.5, 0.5, 10, 0) // Center: (x, y), Radius: z
		_Color ("Circle Color", Color) = (1, 1, 1, 1)
	}

_Parameters的x和y分量表示圆心在屏幕中的uv值(即范围在(0, 1)),z分量是圆的半径,单位是像素。

圆在数学里面的表达式相对简单,即到圆心距离小于半径的点就在圆内。那么事情就变得简单了:只要在圆内的点就是用设置的颜色绘制,否则用背景色绘制(黑色)。

        vec4 circle(vec2 pos, vec2 center, float radius, float4 color) {
        	if (length(pos - center) < radius) {
        		// In the circle
        		return vec4(1, 1, 1, 1) * color;
        	} else {
        		return vec4(0, 0, 0, 1);
        	}
        }

		vec4 main(vec2 fragCoord) {
			vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)
//			vec2 pos = fragCoord.xy / iResolution.xy; // pos.x ~ (0, 1), pos.y ~ (0, 1)
//			vec2 pos = fragCoord / min(iResolution.x, iResolution.y); // If iResolution.x > iResolution.y, pos.x ~ (0, 1.xx), pos.y ~ (0, 1)
//			vec2 pos =fragCoord.xy / iResolution.xy * 2. - 1.; // pos.x ~ (-1, 1), pos.y ~ (-1, 1)
//			vec2 pos = (2.0*fragCoord.xy-iResolution.xy)/min(iResolution.x,iResolution.y);	// If iResolution.x > iResolution.y, pos.x ~ (-1.xx, 1.xx), pos.y ~ (-1, 1)

			return circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _Color);
		}

得到的效果如下:

 

我不要锯齿!

上面得到的圆在边缘处有一些小锯齿,这当然是我们无法忍受的拉!Shader中抗锯齿的原理大概是这样:由于原来非A即B的计算会使得A和B的交界处产生锯齿(例如上面圆的边界),因此我们只需要在A和B的边界平缓过渡即可。这往往需要透明度的配合,即使用透明度来混合颜色。

在shader中,一种常见的抗锯齿(平滑)操作是使用smoothstep函数(当然有人诟病这种方法不直观,但我觉得挺好用的。。。whatever~)。smoothstep函数在CG文档里面是这样的:

Interpolates smoothly from 0 to 1 based on x compared to a and b.

1) Returns 0 if x < a < b or x > a > b
1) Returns 1 if x < b < a or x > b > a
3) Returns a value in the range [0,1] for the domain [a,b].

也就是说它的返回值范围总是在(0, 1),也就是透明度的取值范围,这也是为何它在抗锯齿领域如此受欢迎的原因了吧。

这样,我们就可以改写原来的circle函数:

        vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
        	float d = length(pos - center) - radius;
        	float t = smoothstep(0, antialias, d);
        	return vec4(color, 1.0 - t);
        }

antialias就是平滑过渡的边界范围。为了方便调试,我们可以在shader中利用_Parameters的z分量作为抗锯齿因子,当然在实际工程中可以设为定值。接下来就是和背景颜色进行混合,我们使用的是lerp函数(在ShaderToy中对应的是mix函数):

		vec4 main(vec2 fragCoord) {
			vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)

			vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
			vec4 layer2 = circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);

			return mix(layer1, layer2, layer2.a);
		}

完整代码如下:

Shader "shadertoy/Simple Circle" {
	Properties{
		_Parameters ("Circle Parameters", Vector) = (0.5, 0.5, 10, 1) // Center: (x, y), Radius: z
		_CircleColor ("Circle Color", Color) = (1, 1, 1, 1)
		_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
	}

	CGINCLUDE
	 	#include "UnityCG.cginc"
  		#pragma target 3.0      

  		#define vec2 float2
  		#define vec3 float3
  		#define vec4 float4
  		#define mat2 float2x2
  		#define iGlobalTime _Time.y
  		#define mod fmod
  		#define mix lerp
  		#define atan atan2
  		#define fract frac
  		#define texture2D tex2D
  		// 屏幕的尺寸
  		#define iResolution _ScreenParams
  		// 屏幕中的坐标,以pixel为单位
  		#define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy) 

  		#define PI2 6.28318530718
  		#define pi 3.14159265358979
  		#define halfpi (pi * 0.5)
  		#define oneoverpi (1.0 / pi)

  		float4 _Parameters;
  		float4 _CircleColor;
  		float4 _BackgroundColor;

        struct v2f {
            float4 pos : SV_POSITION;
            float4 srcPos : TEXCOORD0;
        };              

       //   precision highp float;
        v2f vert(appdata_base v) {
        	v2f o;
        	o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
            o.srcPos = ComputeScreenPos(o.pos);
            return o;
        }  

        vec4 main(vec2 fragCoord);

        fixed4 frag(v2f _iParam) : COLOR0 {
        	vec2 fragCoord = gl_FragCoord;
        	return main(gl_FragCoord);
        }  

        vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
        	float d = length(pos - center) - radius;
        	float t = smoothstep(0, antialias, d);
        	return vec4(color, 1.0 - t);
        }

		vec4 main(vec2 fragCoord) {
			vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)

			vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
			vec4 layer2 = circle(pos, _Parameters.xy * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);

			return mix(layer1, layer2, layer2.a);
		}

    ENDCG    

    SubShader {
        Pass {
            CGPROGRAM    

            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest     

            ENDCG
        }
    }
    FallBack Off
}

抗锯齿效果如下:

 

 

画两个点

我们现在来看看如何画出更多的点。之前的circle函数已经可以画出任何一个大小、圆心的圆了,现在的问题仅仅是如何将这些元素都添加到画布上。一种基本的思想就是图层叠加,这很像我们在Photoshop中做的事情:背景在最后一层,我们只需要增加新的图层,并确保它们按层级顺序一层层向上排列即可。所以,我们可以这样做:

		vec4 main(vec2 fragCoord) {
			vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)

			vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);

			vec2 point1 = vec2(0.3, 0.8);
			vec2 point2 = vec2(0.8, 0.2);

			vec4 layer2 =  circle(pos, point1 * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);
			vec4 layer3 =  circle(pos, point2 * iResolution.xy, _Parameters.z, _CircleColor.rgb, _Parameters.w);

			vec4 fragColor = mix(layer1, layer2, layer2.a);
			fragColor = mix(fragColor, layer3, layer3.a);

			return fragColor;
		}

上面的代码中,我们绘制了两个圆,一个圆心位置在(0.3, 0.8)处,一个在(0.8, 0.2)处。layer1仍旧是背景层,layer2和layer1分别表示两个圆所在图层。我们按照层级顺序依次调用lerp函数(也就是代码中的mix函数)即可以混合这些元素。结果如下:

 

这种思想可以延伸到任意层数的元素叠加~

画一条直线

现在,我们已经知道了直线上的两个点,那么我们来看看如何画出过这两个点的直线。首先,我们可以声明一个空的画直线的函数,并且添加一个新的图层给它,后续再填充这个函数:

        vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {
        	return vec4(0);
        }

        vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
        	float d = length(pos - center) - radius;
        	float t = smoothstep(0, antialias, d);
        	return vec4(color, 1.0 - t);
        }

		vec4 main(vec2 fragCoord) {
			vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)

			vec2 point1 = vec2(0.3, 0.8) * iResolution.xy;
			vec2 point2 = vec2(0.8, 0.2) * iResolution.xy;

			vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
			vec4 layer2 = line(pos, point1, point2, _LineWidth, _LineColor.rgb, _Antialias);
			vec4 layer3 =  circle(pos, point1, _CircleRadius, _CircleColor.rgb, _Antialias);
			vec4 layer4 =  circle(pos, point2, _CircleRadius, _CircleColor.rgb, _Antialias);

			vec4 fragColor = mix(layer1, layer2, layer2.a);
			fragColor = mix(fragColor, layer3, layer3.a);
			fragColor = mix(fragColor, layer4, layer4.a);

			return fragColor;
		}

为了方便,上面新定义了几个参数。后面会给出完整代码,但我相信这里也很好懂。对于图层的顺序我也进行了调增,即第二层为直线,后面是点图层,这是因为我们希望点的颜色可以覆盖直线颜色。保存后返回查看结果,是没有任何变化的,因为此时直线图层返回的颜色的透明度为0。

注意了!!!现在又到了数学魅力展现的时刻了!!!绘制直线的思想其实和圆很类似,我们只需要判断像素位置是否在直线内(因为这里的直线是有宽度的)就可以了:判断像素点到直线的距离是否小于直线宽度的一半。那么,我们只需要回想起当年的点到直线距离公式。你是不是忘记了!!!这里直接给出答案了,公式也很简单:

        vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {
        	float k = (point1.y - point2.y)/(point1.x - point2.x);
    		float b = point1.y - k * point1.x;

    		float d = abs(k * pos.x - pos.y + b) / sqrt(k * k + 1);
    		float t = smoothstep(width/2.0, width/2.0 + antialias, d);
        	return vec4(color, 1.0 - t);
        }

我们先求出了直线公式的参数k和b(y = k * x + b)。这里没有进行高中时期的临界判断,但shader也没有报错呦~然后,我们计算像素点到直线的距离d。在计算透明度因子t的时候,我们考虑了直线的宽度,这意味着,当距离d小于width/2.0的时候将返回0,也就是说该点绝对在直线内;再通过反锯齿因子进行抗锯齿计算。

很简单有木有!!!效果如下:

 

 

完整的代码如下:

Shader "shadertoy/Simple Line" {
	Properties{
		_CircleRadius ("Circle Radius", float) = 5
		_CircleColor ("Circle Color", Color) = (1, 1, 1, 1)
		_LineWidth ("Line Width", float) = 5
		_LineColor ("Line Color", Color) = (1, 1, 1, 1)
		_Antialias ("Antialias Factor", float) = 3
		_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
	}

	CGINCLUDE
	 	#include "UnityCG.cginc"
  		#pragma target 3.0      

  		#define vec2 float2
  		#define vec3 float3
  		#define vec4 float4
  		#define mat2 float2x2
  		#define iGlobalTime _Time.y
  		#define mod fmod
  		#define mix lerp
  		#define atan atan2
  		#define fract frac
  		#define texture2D tex2D
  		// 屏幕的尺寸
  		#define iResolution _ScreenParams
  		// 屏幕中的坐标,以pixel为单位
  		#define gl_FragCoord ((_iParam.srcPos.xy/_iParam.srcPos.w)*_ScreenParams.xy) 

  		#define PI2 6.28318530718
  		#define pi 3.14159265358979
  		#define halfpi (pi * 0.5)
  		#define oneoverpi (1.0 / pi)

  		float _CircleRadius;
  		float4 _CircleColor;
  		float _LineWidth;
  		float4 _LineColor;
  		float _Antialias;
  		float4 _BackgroundColor;

        struct v2f {
            float4 pos : SV_POSITION;
            float4 srcPos : TEXCOORD0;
        };              

       //   precision highp float;
        v2f vert(appdata_base v) {
        	v2f o;
        	o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
            o.srcPos = ComputeScreenPos(o.pos);
            return o;
        }  

        vec4 main(vec2 fragCoord);

        fixed4 frag(v2f _iParam) : COLOR0 {
        	vec2 fragCoord = gl_FragCoord;
        	return main(gl_FragCoord);
        }  

        vec4 line(vec2 pos, vec2 point1, vec2 point2, float width, float3 color, float antialias) {
        	float k = (point1.y - point2.y)/(point1.x - point2.x);
    		float b = point1.y - k * point1.x;

    		float d = abs(k * pos.x - pos.y + b) / sqrt(k * k + 1);
    		float t = smoothstep(width/2.0, width/2.0 + antialias, d);
        	return vec4(color, 1.0 - t);
        }

        vec4 circle(vec2 pos, vec2 center, float radius, float3 color, float antialias) {
        	float d = length(pos - center) - radius;
        	float t = smoothstep(0, antialias, d);
        	return vec4(color, 1.0 - t);
        }

		vec4 main(vec2 fragCoord) {
			vec2 pos = fragCoord; // pos.x ~ (0, iResolution.x), pos.y ~ (0, iResolution.y)

			vec2 point1 = vec2(0.4, 0.1) * iResolution.xy;
			vec2 point2 = vec2(0.7, 0.8) * iResolution.xy;

			vec4 layer1 = vec4(_BackgroundColor.rgb, 1.0);
			vec4 layer2 = line(pos, point1, point2, _LineWidth, _LineColor.rgb, _Antialias);
			vec4 layer3 =  circle(pos, point1, _CircleRadius, _CircleColor.rgb, _Antialias);
			vec4 layer4 =  circle(pos, point2, _CircleRadius, _CircleColor.rgb, _Antialias);

			vec4 fragColor = mix(layer1, layer2, layer2.a);
			fragColor = mix(fragColor, layer3, layer3.a);
			fragColor = mix(fragColor, layer4, layer4.a);

			return fragColor;
		}

    ENDCG    

    SubShader {
        Pass {
            CGPROGRAM    

            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest     

            ENDCG
        }
    }
    FallBack Off
}

写在最后

这篇写的很基础,但有些知识是值得真实项目里借鉴的。有些人说ShaderToy里的只是玩具,没有价值,我个人是不这么觉得啦~游戏里很多细腻的动画效果是无法仅仅靠贴图来完成的,了解些基本或者稍微复杂一点的shader计算还是很有好处滴~

最后,关于ShaderToy的用处,还有一点就是我们可以自己改进成非Pixel Shader的版本,例如利用模型的uv坐标去代替屏幕坐标等等。更多的用处等待自己去发现啦!

时间: 2024-10-11 22:47:54

【ShaderToy】基础篇之谈谈点、线的绘制的相关文章

SQL Server高并发问题系列基础篇(谈谈事务的属性)

前言 SQL Server作为一款优秀的关系型数据库,在支撑起最基本的数据存储功能之外,还承受着各种大量的并发操作和用户访问. 而本系列内容将一层层慢慢分析出在SQL Server的生态环境中,是怎样通过一系列的机制来维持各种访问的秩序,俗话说得好:“有人的地方就有了江湖,有了江湖就有了江湖规矩”,同样在SQL Server的世界中,也有它自己的“江湖规矩”,如果你不按照规矩出牌,那么你将会被踢出局! 同样本系列也会分析出遇到各种问题的解决思路,供院友们参考. 本篇作为第一篇,首先同样先提基础,

ios基础篇(二十)—— UIBezierPath绘制

UIBezierPath类可以创建基于矢量的路径,可以定义简单的形状,如椭圆或者矩形,或者有多个直线和曲线段组成的形状. 一.UIBezierPath使用: 1.创建path: 2.添加路径到path: 3.将path绘制出来: 1 //创建path 2 path = [UIBezierPath bezierPath]; 3 //添加路径 4 [path moveToPoint:(CGPoint){10,50}]; 5 [path addLineToPoint:(CGPoint){100,50}

【ShaderToy】基础篇之再谈抗锯齿(antialiasing,AA)

写在前面 在之前的基础篇中,我们讲到了在绘制点线时如何处理边缘的锯齿,也就是使用smoothstep函数.而模糊参数是一些定值,或者是跟屏幕分辨率相关的数值,例如分辨率宽度的5%等等.但这种方法其实是有一种问题的.这需要我们从绘制的图像说起. ShaderToy中绘制的很多图像可以说是一种Procedure Texture,过程纹理,即是计算机生成的纹理.拿之前画的圆和线来说,这些圆和线的绘制过程,是我们计算每个fragment到"期望图像"的距离,然后根据距离来判断使用哪种颜色.如果

谈谈Python实战数据可视化之pygal模块(基础篇)

前沿 对于需要在尺寸不同的屏幕上显示的图表,请考虑使用Pygal来生成它们,因为它们将自动缩放,以适合观看者的屏幕,这样它们在任何设备上显示时都会很美观.接下来我会谈谈pygal模块生成线.直方图的基本用法,用书本骰子的案例来更深入了解pygal模块的使用,对于pygal其他图形的创建其实方法差不多,实际运用时需要制作哪种图形就去官网查询,官网有很多图形创建的示例代码,pygal画廊官网链接:http://www.pygal.org/如下方图(有图有代码,自己打一遍其实懂得也差不多了): pyg

2000条你应知的WPF小姿势 基础篇&lt;1-7&gt;

在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件工程师,对C#和WPF有着极深的热情.最为出色的是他维护了两个博客:2,000Things You Should Know About C# 和 2,000 Things You Should Know About WPF .听到博客名字就懂这个人有多伟大了吧.他以类似微博式的150字简短语言来每天更新一条WPF和C#重要又容易被遗忘的知识.Follow他的博客也有一段日子了,很希望能够分享给大家. 本系列我不仅会翻译

Linux C 程序设计多线程基础篇

   Linux C 程序设计多线程基础篇 题记:因为 Linux 网络入侵检测系统的设计与实现希望使用多线程,因此希望系统的学习一下 Linux C程序设计多线程的知识 注意事项:因为 pthraed 库不是 Linux 系统默认的库,因此在进行多线程开发的时候,需要加上头文件#include <pthread.h>,编译时要加参数 -lpthread;了:gcc thread.c -o thread -lpthread. 进程和线程: 进程是程序执行,资源分配的基本单位,每个进程都拥有自己

基础篇:4.熟练掌握BroadcastReceiver的接收和使用

1.广播 既然要谈论到广播接收器,那必然先要来谈谈广播,毕竟两者相辅相成.在Android系统中有各种各样的广播如常见的:电池的使用状态.电话的接听.短信的接收等.应用开发者也可以在程序中发送出各种广播.那么广播到底是个什么东西?!---广播是一种广泛运用在应用程序之间传输信息的机制.广播的发送可以通过以下两种方式发送: (1)Context.sendBroadcast---广播无序事件,理论上,所有的接收者同时接收到广播. (2)Context.sendOrderedBroadcast---广

[转]gitHub客户端Desktop的安装使用总结 ---基础篇

gitHub客户端Desktop的安装使用总结 ---基础篇 发表于2015/12/11 11:41:57  8399人阅读 分类: Android之应用实战 这段时间想把我写的东西上传到github上,所以开始收集资料学习,走了很多弯路( msysgit和极慢的FQ网速让我欲仙欲死),最后找到了比较好用的工具gitHub desktop.在此做出详细记录. 一.第一步 注册github账号 1.注册界面 https://github.com/,如下图: 2.下注册流程 3.上面简单的输入一下用

C#笔记——基础篇

C#笔记——基础篇 一.入门知识 VS中的常用快捷键 Ctrl+K+D:快速对齐代码 Ctrl+Z:撤销 Ctrl+S:保存(一定要经常保存!) Ctrl+J:快速弹出智能提示 Shift+End .Shift+Home Ctrl+K+C:注释所选代码 Ctrl+K+U:取消对所选代码的注释 F1:转到帮助文档 折叠冗余代码:#Region 和#EndRegion 新建空的  tab键 新增子节点   回车键 新增兄弟节点 F7快捷截图 F8预览 .net/dotnet:一般指.Net Fram