GrabPass

GrabPass是Unity为我们提供的一个很方便的功能,可以直接将当前屏幕内容渲染到一张贴图上,我们可以直接在shader中使用这张贴图而不用自己去实现渲染到贴图这样的一个过程,大大的方便了我们的shader编写。GrabPass的使用非常简单,我们在写vertex fragment shader的时候都需要写一个pass,GrabPass也是一个pass,只不过是Unity为我们实现好的一个pass。我们只需要在我们正常的Pass前面加一个GrabPass{}就可以了。
官方文档上有两种GrabPass的写法,第一种是直接GrabPass{}的写法,这种写法抓屏的图片就直接存到_GrabTexture这个系统预定义的贴图变量中了,我们可以直接访问该贴图,但是这种写法会导致每个使用GrabPass的物体进行一次这种旷日持久的抓屏操作!如果用这种shader的物体多了的话,想想就很可怕。另一种是GrabPass{"TextureName"}的写法,其中TextureName是我们自定义的一个贴图名称,这种写法,Unity每帧只会为第一个使用了该名称的物体进行抓屏操作,之后的就可以复用这张贴图了。所以,我们还是使用第二种方式更好一点。下面附上一份最简单的抓屏代码:
//Grabpass shader
//by: puppet_master
//2017.4.23
Shader "ApcShader/GrabPass"
{
SubShader
{
ZWrite Off
//GrabPass
GrabPass
{
//此处给出一个抓屏贴图的名称,抓屏的贴图就可以通过这张贴图来获取,而且每一帧不管有多个物体使用了该shader,只会有一个进行抓屏操作
//如果此处为空,则默认抓屏到_GrabTexture中,但是据说每个用了这个shader的都会进行一次抓屏!
"_GrabTempTex"
}

Pass
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent+1"
}

CGPROGRAM
sampler2D _GrabTempTex;
float4 _GrabTempTex_ST;
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float4 grabPos : TEXCOORD0;
};

v2f vert(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//计算抓屏的位置,其中主要是将坐标从(-1,1)转化到(0,1)空间并处理DX和GL纹理反向的问题
o.grabPos = ComputeGrabScreenPos(o.pos);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
//根据抓屏位置采样Grab贴图,tex2Dproj等同于tex2D(grabPos.xy / grabPos.w)
fixed4 color = tex2Dproj(_GrabTempTex, i.grabPos);
return 1 - color;
}

#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
我们找个面片,附上这个shader的材质。为了更方便的看一下效果,我们就参照官网的写法,直接将最终输出的颜色反向,也就是1-原颜色作为输出(这个颜色不禁让我想起了宇智波鼬的月读........)

看一下这个shader用到的几个函数,第一个是ComputeGrabScreenPos这个函数,我们从UnityCG.cginc中可以找到这个函数的实现:
inline float4 ComputeGrabScreenPos (float4 pos) {
#if UNITY_UV_STARTS_AT_TOP
float scale = -1.0;
#else
float scale = 1.0;
#endif
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*scale) + o.w;
#ifdef UNITY_SINGLE_PASS_STEREO
o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
o.zw = pos.zw;
return o;
}
我们传递进来的参数是经过mvp变换后的顶点坐标,传入之后这个函数主要做了两件事情,第一个是处理DX和OpenGL纹理坐标差异导致的问题,这个之前的文章有记录过。第二件事主要就是将转化到标准裁剪空间(-1,1)区间的顶点转化到(0,1)区间。按照Unity的写法,本人推测,这个GrabPass获取的屏幕贴图应该是基于视空间的,而在这个信息传递到fragment shader后,用了tex2Dproj函数进行采样,tex2Dproj(i.xy)应该等同于tex2D(i.xy/i.w),也就是说这个采样点坐标进行了一次投影变换。

扭曲效果的实现

准备工作完成,下面步入正题,来看看扭曲效果的实现。首先,要扭曲,就肯定要动,这个shader还是得需要Time系列的变量进行驱动。不过这只是其中一个条件,由于shader是高度并行化的计算,我们没有办法区分每个像素到底需要偏移多少。在屏幕水波纹效果中,我们是通过计算当前像素点到屏幕中心位置的距离作为偏移值的,对于后处理这样做可能比较方便,但是对于普通物体上使用的shader就没有那么简单了。比如,我们同样是让采样坐标按照sin值进行偏移:
fixed4 frag(v2f i) : SV_Target
{
i.grabPos.x += _DistortStrength * sin(_Time.y * 10);
i.grabPos.y += _DistortStrength * sin(_Time.y);
fixed4 color = tex2Dproj(_GrabTempTex, i.grabPos);
return 1 - color;
}
那么所有的顶点就都会按照一致的方向进行偏移:

为了让偏移变得随机,我们就要引入一个能够随机化输出的东东,也就是噪声图。比如我们找到了一张这个样子的噪声图:

然后,只需要用一个连续变化的值去采这个噪声图,就可以得到不连续的随机输出偏移值。下面附上扭曲效果的实现:
//Distort shader
//by: puppet_master
//2017.4.24
Shader "ApcShader/Distort"
{
Properties
{
_DistortStrength("DistortStrength", Range(0,1)) = 0.2
_DistortTimeFactor("DistortTimeFactor", Range(0,1)) = 1
_NoiseTex("NoiseTexture", 2D) = "white" {}
}
SubShader
{
ZWrite Off
Cull Off
//GrabPass
GrabPass
{
//此处给出一个抓屏贴图的名称,抓屏的贴图就可以通过这张贴图来获取,而且每一帧不管有多个物体使用了该shader,只会有一个进行抓屏操作
//如果此处为空,则默认抓屏到_GrabTexture中,但是据说每个用了这个shader的都会进行一次抓屏!
"_GrabTempTex"
}

Pass
{
Tags
{
"RenderType" = "Transparent"
"Queue" = "Transparent + 100"
}

CGPROGRAM
sampler2D _GrabTempTex;
float4 _GrabTempTex_ST;
sampler2D _NoiseTex;
float4 _NoiseTex_ST;
float _DistortStrength;
float _DistortTimeFactor;
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 grabPos : TEXCOORD1;
};

v2f vert(appdata_base v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.grabPos = ComputeGrabScreenPos(o.pos);
o.uv = TRANSFORM_TEX(v.texcoord, _NoiseTex);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
//首先采样噪声图,采样的uv值随着时间连续变换,而输出一个噪声图中的随机值,乘以一个扭曲快慢系数
float4 offset = tex2D(_NoiseTex, i.uv - _Time.xy * _DistortTimeFactor);
//用采样的噪声图输出作为下次采样Grab图的偏移值,此处乘以一个扭曲力度的系数
i.grabPos.xy -= offset.xy * _DistortStrength;
//uv偏移后去采样贴图即可得到扭曲的效果
fixed4 color = tex2Dproj(_GrabTempTex, i.grabPos);
return color;
}

#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
为了更加应景,我搜刮了一下我的资源库,找到了一个火把,2333:

然后在火把附近放一个面片,用上我们的扭曲shader:

最终效果如下图所示:

基于后处理的优化效果

GrabPass非常耗时,在安卓平台也会有问题,虽然对于安卓机的性能,用shader lod直接干掉扭曲效果也是一个不错的选择,不过这个毕竟是下策,首先还是要解决这个问题。正常渲染是往frame buffer中渲染,但是grabpass应该是从当前的frame buffer中将内容再读出来,从显存往内存中拷贝,应该是一个阻塞的过程,我记得之前一帧渲染过3000ms,简直可怕。PS:这种情况在两个(或多个)相机渲染,后面的相机没有Clear并且在后面的相机上挂了后处理的时候也会出现这种情况,猜测原因也是因为在后面的相机进行后处理时需要上一个相机的内容,然而这个东东已经在frame buffer中了,所以后处理如果要在上层相机运用,最好还是慎重考虑一下。关于用后处理卡的问题,这篇文章解释得很好。文章中给了几种解决方案,一种是关抗锯齿,一个是用GL3.0,最后一个是直接改为用渲染到纹理。记得以前还看过一个帖子,不过忘记链接了,这个做法比较极端,就是最终渲染的结果都不走frame buffer,而是都渲染到一个纹理上。然后所有的后处理都在这个纹理上进行,完全绕开了OnRenderImage。额,不小心扯远了,只是希望能给和我遇到一样问题的倒霉蛋一点参考,下面进行正题。

既然GrabPass比较费,那么最简单的,我们可能会想直接用另外一个相机去渲染这个场景到一个RenderTarget上,然后用这个RenderTarget代替我们上面用的GrabTexture。不过这种做法会导致DrawCall翻倍,如果我们的场景中内容较少,比较适合用这种方法。或者我们可以设置另一个相机的层级,使之只渲染某些内容,这样也可以降低一些开销。不过这里就不用这种方式了。之前看到了一篇文章,作者给了这样的一个思路,感觉非常巧妙。简而言之,这个方法作扭曲的部分是用全屏后处理进行的,但是全屏都扭曲了,我们其实只需要扭曲一部分地方,所以我们需要一个Mask图来控制,而这张Mask图我们就可以直接用另一个相机渲染出来,其实就是我们上面用到的特效片,渲染到一个RT上就可以了。相比于用另一个摄像机把场景中的东西都渲染一遍,这种方式只是需要额外渲染一个片外加一次全屏后处理操作,两者各有千秋,视具体情况而定。

我们先写一个全屏扭曲的shader,首先,需要后处理,我们继承这个已经用了无数次的PostEffectBase类,实现后处理的C#部分代码:
/********************************************************************
FileName: DistortEffect.cs
Description: 屏幕扭曲效果
Created: 2017/04/27
by: puppet_master
*********************************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DistortEffect : PostEffectBase {

//扭曲的时间系数
[Range(0.0f, 1.0f)]
public float DistortTimeFactor = 0.15f;
//扭曲的强度
[Range(0.0f, 0.2f)]
public float DistortStrength = 0.01f;
//噪声图
public Texture NoiseTexture = null;

public void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (_Material)
{
_Material.SetTexture("_NoiseTex", NoiseTexture);
_Material.SetFloat("_DistortTimeFactor", DistortTimeFactor);
_Material.SetFloat("_DistortStrength", DistortStrength);
Graphics.Blit(source, destination, _Material);
}
else
{
Graphics.Blit(source, destination);
}
}
}
然后shader部分,扭曲的原理与上面一样,只是处理的对象变了一下,直接处理OnRenderImage传来的MainTex即可:
//全屏幕扭曲Shader
//by:puppet_master
//2017.4.28

Shader "Custom/DistortPostEffect"
{
Properties
{
_MainTex("Base (RGB)", 2D) = "white" {}
_NoiseTex("Base (RGB)", 2D) = "black" {}//默认给黑色,也就是不会偏移
}

CGINCLUDE
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
uniform sampler2D _NoiseTex;
uniform float _DistortTimeFactor;
uniform float _DistortStrength;

fixed4 frag(v2f_img i) : SV_Target
{
//根据时间改变采样噪声图获得随机的输出
float4 noise = tex2D(_NoiseTex, i.uv - _Time.xy * _DistortTimeFactor);
//以随机的输出*控制系数得到偏移值
float2 offset = noise.xy * _DistortStrength;
//像素采样时偏移offset
float2 uv = offset + i.uv;
return tex2D(_MainTex, uv);
}

ENDCG

SubShader
{
Pass
{
ZTest Always
Cull Off
ZWrite Off
Fog{ Mode off }

CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
Fallback off
}
这样,整个屏幕就都扭曲了,动图如下(赶脚好像来到了沙漠一样.....):

这里我把扭曲的强度设置得高一些,感觉也可以直接当一些全屏后处理的样子,比如扭曲,水幕效果:

我们有了全屏的扭曲效果之后,下面我们考虑要怎么把需要扭曲的部分抠出来。那么,第一个想到的就是Mask图,我们可以给一个Mask图,作为权重,白色为需要偏移的权重,黑色为无偏移的权重,这样,我们就可以控制哪个地方需要扭曲。但是,这里,我们的Mask图需要是一个动态的Mask图,因为相机会移动,所以,我们需要实时地生成这张Mask图。在描边效果这篇文章中,我们用过类似的方法。这里,我们故技重施,将需要扭曲的部分,也就是上面我们用的面片渲染到一张RenderTarget上,首先,我们还是创建一个新的摄像机,然后通过在OnPreRender函数中用RenderWithShader,将面片渲染到一张RT上(这个RT可以多降低一些分辨率),渲染的shader就用一个纯白色的shader就可以了。比如下面的这个Shader:
//Mask图生成shader
//by:puppet_master
//2017.5.3

Shader "ApcShader/MaskObjPrepass"
{
//子着色器
SubShader
{
Pass
{
Cull Off
CGPROGRAM
#include "UnityCG.cginc"

struct v2f
{
float4 pos : SV_POSITION;
};

v2f vert(appdata_full v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
//这个Pass直接输出颜色
return fixed4(1,1,1,1);
}

//使用vert函数和frag函数
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
下面附上扭曲效果的C#脚本:
/********************************************************************
FileName: DistortEffect.cs
Description: 屏幕扭曲效果
Created: 2017/04/27
by: puppet_master
*********************************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DistortEffect : PostEffectBase {

//扭曲的时间系数
[Range(0.0f, 1.0f)]
public float DistortTimeFactor = 0.15f;
//扭曲的强度
[Range(0.0f, 0.2f)]
public float DistortStrength = 0.01f;
//噪声图
public Texture NoiseTexture = null;
//渲染Mask图所用的shader
public Shader maskObjShader = null;
//降采样系数
public int downSample = 4;

private Camera mainCam = null;
private Camera additionalCam = null;
private RenderTexture renderTexture = null;

public void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (_Material)
{
_Material.SetTexture("_NoiseTex", NoiseTexture);
_Material.SetFloat("_DistortTimeFactor", DistortTimeFactor);
_Material.SetFloat("_DistortStrength", DistortStrength);
_Material.SetTexture("_MaskTex", renderTexture);
Graphics.Blit(source, destination, _Material);
}
else
{
Graphics.Blit(source, destination);
}
}

void Awake()
{
//创建一个和当前相机一致的相机
InitAdditionalCam();
}

private void InitAdditionalCam()
{
mainCam = GetComponent<Camera>();
if (mainCam == null)
return;

Transform addCamTransform = transform.FindChild("additionalDistortCam");
if (addCamTransform != null)
DestroyImmediate(addCamTransform.gameObject);

GameObject additionalCamObj = new GameObject("additionalDistortCam");
additionalCam = additionalCamObj.AddComponent<Camera>();

SetAdditionalCam();
}

private void SetAdditionalCam()
{
if (additionalCam)
{
additionalCam.transform.parent = mainCam.transform;
additionalCam.transform.localPosition = Vector3.zero;
additionalCam.transform.localRotation = Quaternion.identity;
additionalCam.transform.localScale = Vector3.one;
additionalCam.farClipPlane = mainCam.farClipPlane;
additionalCam.nearClipPlane = mainCam.nearClipPlane;
additionalCam.fieldOfView = mainCam.fieldOfView;
additionalCam.backgroundColor = Color.clear;
additionalCam.clearFlags = CameraClearFlags.Color;
additionalCam.cullingMask = 1 << LayerMask.NameToLayer("Distort");
additionalCam.depth = -999;
//分辨率可以低一些
if (renderTexture == null)
renderTexture = RenderTexture.GetTemporary(Screen.width >> downSample, Screen.height >> downSample, 0);
}
}

void OnEnable()
{
SetAdditionalCam();
additionalCam.enabled = true;
}

void OnDisable()
{
additionalCam.enabled = false;
}

void OnDestroy()
{
if (renderTexture)
{
RenderTexture.ReleaseTemporary(renderTexture);
}
DestroyImmediate(additionalCam.gameObject);
}

//在真正渲染前的回调,此处渲染Mask遮罩图
void OnPreRender()
{
//maskObjShader进行渲染
if (additionalCam.enabled)
{
additionalCam.targetTexture = renderTexture;
additionalCam.RenderWithShader(maskObjShader, "");
}
}
}
还是上面的测试场景,我们将面片改为Distort层级,然后可以直接给这个面片设置一个透明的材质,比如最简单的粒子的shader,让它正常渲染不可见即可:

通过上面的脚本,我们临时将这个Mask图输出到屏幕上(为了性能好一些,降采样比较多,已经有锯齿了,不过在正式使用的时候是看不出来的):

有了Mask图,我们就可以根据Mask图的权重进行修改了,白色的地方是需要扭曲的,黑色的地方不需要扭曲,我们将上面的shader中的offest用这个mask采样图进行修正就能够得到最终的扭曲效果了。后处理版本的shader如下:
//全屏幕扭曲Shader
//by:puppet_master
//2017.5.3

Shader "Custom/DistortPostEffect"
{
Properties
{
_MainTex("Base (RGB)", 2D) = "white" {}
_NoiseTex("Noise", 2D) = "black" {}//默认给黑色,也就是不会偏移
_MaskTex("Mask", 2D) = "black" {}//默认给黑色,权重为0
}

CGINCLUDE
#include "UnityCG.cginc"
uniform sampler2D _MainTex;
uniform sampler2D _NoiseTex;
uniform sampler2D _MaskTex;
uniform float _DistortTimeFactor;
uniform float _DistortStrength;

fixed4 frag(v2f_img i) : SV_Target
{

//根据时间改变采样噪声图获得随机的输出
float4 noise = tex2D(_NoiseTex, i.uv - _Time.xy * _DistortTimeFactor);
//以随机的输出*控制系数得到偏移值
float2 offset = noise.xy * _DistortStrength;
//采样Mask图获得权重信息
fixed4 factor = tex2D(_MaskTex, i.uv);
//像素采样时偏移offset,用Mask权重进行修改
float2 uv = offset * factor.r + i.uv;
return tex2D(_MainTex, uv);
}

ENDCG

SubShader
{
Pass
{
ZTest Always
Cull Off
ZWrite Off
Fog{ Mode off }

CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
Fallback off
}
扭曲效果动态图如下:

通过后处理制作的热空气扭曲效果与GrabPass的效果大致相同,虽然多了全屏后处理操作,但是能够避免安卓机上GrabPass读帧缓存卡死的问题,而且也不需要DrawCall翻倍,对于复杂的场景来说相对效率更高一些。如果场景比较简单,也可以使用另一个相机渲染场景到RT上的方法进行制作。

Unity Shader-热空气扭曲效果的更多相关文章

  1. (转)热空气扭曲效果shader

    转自:http://blog.sina.com.cn/s/blog_89d90b7c0102vaqy.html 热空气扭曲在大自然中形成是比较复杂的,这里只是通过取屏幕纹理和移动UV来模拟热扭曲效果. ...

  2. Unity Shader后处理-搜索灰度效果

    如U3D中Hierarchy面板下的搜索效果: 讲解分析: 1.这种PostEffect效果其实就是指Unity shader的后处理,即游戏中实现屏幕特效的常见方法.顾名思义屏幕后处理就是指在渲染完 ...

  3. unity shader 热扭曲 (屏幕后处理)

      效果: c# using System; using System.Collections; using System.Collections.Generic; using UnityEngine ...

  4. Unity shader UI的3D效果

    原创,转载请标明出处 1.效果 scene视图中的效果: game视图中效果: 2.核心思想:改变UI的顶点坐标 3.好处:可以用正交相机来实现3D效果. 4.Shader 实现 // Unity b ...

  5. Unity shader学习之轮廓效果

    将物体描一层边可以使游戏看起来具有卡通风格,一种简单的实现方法如下: 将物体渲染2次,即使用2个通道. 第一个通道将顶点沿法线(或中心点到顶点的方向)做一个偏移,即将模型扩大一点,并将颜色渲染成轮廓的 ...

  6. 【Unity Shader】(九) ------ 高级纹理之渲染纹理及镜子与玻璃效果的实现

    笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题. [Unity Shader](三) ----- ...

  7. Unity Shader - 消融效果原理与变体

    基本原理与实现 主要使用噪声和透明度测试,从噪声图中读取某个通道的值,然后使用该值进行透明度测试. 主要代码如下: fixed cutout = tex2D(_NoiseTex, i.uvNoiseT ...

  8. Unity3D学习(八):《Unity Shader入门精要》——透明效果

    前言 在实时渲染中要实现透明效果,通常会在渲染模型时控制它的透明通道. Unity中通常使用两种方法来实现透明 :(1)透明度测试(AlphaTest)(2)透明度混合(AlphaBlend).前者往 ...

  9. Unity shader学习之屏幕后期处理效果之高斯模糊

    高斯模糊,见 百度百科. 也使用卷积来实现,每个卷积元素的公式为: 其中б是标准方差,一般取值为1. x和y分别对应当前位置到卷积中心的整数距离. 由于需要对高斯核中的权重进行归一化,即使所有权重相加 ...

随机推荐

  1. 【POJ 2411】 Mondriaan's Dream

    [题目链接] 点击打开链接 [算法] 很明显,我们可以用状态压缩动态规划解决此题 f[n][m]表示n-1行已经放满,第n行状态为m的合法的方案数 状态转移方程很好推 注意这题时限较紧,注意加一些小优 ...

  2. 学习记录:《高性能javascript》【持续更新】

    在看这本书的时候,遇到不懂得地方我一般都会百度一下.这里记录一下我在这本书里捡到的杂碎知识: 1.arrayObject.shift() 2.concat() 3.绑定监听的事件的方法(兼容IE,Fi ...

  3. JavaScript-Tool:jqgrid

    ylbtech-JavaScript-Tool:jqgrid jqGrid 是一个用来显示网格数据的jQuery插件,文档比较全面,附带中文版本. 1.返回顶部   2.返回顶部   3.返回顶部   ...

  4. 机器学习--DIY笔记与感悟--①K-临近算法(2)

    上一篇博客我手动写了KNN算法,并且之后用手写的算法预测了约会的成功率. 而今天,我在大神博客的指导下调用sklearn这个库来预测图片的内容. 一.前期准备 由于我这里使用的是mac版本,而skle ...

  5. string类的常用的几个小东西find,substr

    头文件: #include<iostream> #include<string> 定义: string ss; #include<iostream> #includ ...

  6. Java class不分32位和64位

    1.32位JDK编译的java class在32位系统和64位系统下都可以运行,64位系统兼容32位程序,可以理解.2.无论是Linux还是Windows平台下的JDK编译的java class在Li ...

  7. Swoole和Workerman到底选谁?

    Swoole:面向生产环境的 PHP 异步网络通信引擎 使 PHP 开发人员可以编写高性能的异步并发 TCP.UDP.Unix Socket.HTTP,WebSocket 服务.Swoole 可以广泛 ...

  8. Hadoop Hive概念学习系列之HDFS、Hive、MySQL、Sqoop之间的数据导入导出(强烈建议去看)

    Hive总结(七)Hive四种数据导入方式 (强烈建议去看) Hive几种数据导出方式 https://www.iteblog.com/archives/955 (强烈建议去看) 把MySQL里的数据 ...

  9. Jmeter之Json Path Extractor 接受上一个请求的响应参数

    最近在使用Jmeter进行接口测试,被一个问题困扰了很久,就是第二个请求如何接收上一个请求响应中的参数,刚开始尝试着用网上普遍说的正则表达式,长了了N多次之,都没有达到我想要的效果,被整的够惨,于是, ...

  10. icomoon字体使用

    如何灵活利用免费开源图标字体-IcoMoon篇 by zhangxinxu from http://www.zhangxinxu.com本文地址:http://www.zhangxinxu.com/w ...