Shader 運動模糊(Motion Blur)

瞭解完高斯模糊以後,接下來看看運動模糊。html

什麼是運動模糊? 根據百科的定義:動態模糊或運動模糊是靜態場景或一系列的圖片像電影或是動畫中快速移動的物體形成明顯的模糊拖動痕跡。函數

爲何會出現運動模糊? 攝影機的工做原理是在很短的時間裏把場景在膠片上曝光。場景中的光線投射在膠片上,引發化學反應,最終產生圖片。這就是曝光。若是在曝光的過程當中,場景發生變化,則就會產生模糊的畫面。工具


問題一:運動模糊是否就是單一方向的高斯模糊?

咱們根據運動模糊的物理成像原理能夠知道,離快門關閉越近的圖像越清晰,殘影是存在透明度變化的,它也受速度影響:post

咱們嘗試一下經過單一方向的高斯模糊來模擬運動模糊,看看效果如何:動畫

// 只展現核心代碼
vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, uv) * 0.1964825501511404;
  color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057;
  color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057;
  return color;
}

void main() {
    gl_FragColor = blur13(texture, st, iResolution, vec2(0., 5.));
}
複製代碼

雖然單一方向的高斯模糊並不徹底符合運動模糊的定義。但單純看效果其實分辨不太出來,咱們再把效果強化:ui

問題二:如何讓畫面天然地動起來?

運動模糊,天然須要運動才能體現出來。首先咱們實現一個簡單的位移:spa

// 只展現核心代碼
vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, uv) * 0.1964825501511404;
  color += texture2D(image, uv + (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv - (off1 / resolution)) * 0.2969069646728344;
  color += texture2D(image, uv + (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv - (off2 / resolution)) * 0.09447039785044732;
  color += texture2D(image, uv + (off3 / resolution)) * 0.010381362401148057;
  color += texture2D(image, uv - (off3 / resolution)) * 0.010381362401148057;
  return color;
}

void main() {
    st += time*3.;         // time: 0~1
    st = fract(st);
    gl_FragColor = blur13(texture, st, iResolution, vec2(0., 20.));
}
複製代碼

OK,有點辣眼睛。首先須要解決的問題是圖像邊界鏈接到地方並無運動模糊:.net

這意味着咱們要實時的取當前的座標來對圖像進行取樣,因此傳入一個新的參數,表示當前運動的距離:code

vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, fract(uv + speed)) * 0.1964825501511404;
  color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057;
  color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057;
  return color;
}

void main() {
    vec2 speed = vec2(0, time*3.);

    gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.), speed);
}
複製代碼

那解決完邊界問題,再分析下運動模糊出現的時機,運動開始和結束確定不會產生模糊,只有中間過程纔會有模糊,因此咱們根據時間來調整模糊:orm

咱們先構造一個從 0~1 的單位時間內,它的值從 0~1~0 的變化曲線,做爲咱們模糊的乘數,經過這個工具,對以前的正態分佈機率密度函數進行一點改造:

vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, fract(uv + speed)) * 0.1964825501511404;
  color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057;
  color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057;
  return color;
}

float normpdf(float x) {
    return exp(-20.*pow(x-.5,2.));
}

void main() {
    vec2 speed = vec2(0, time);
    float blur = normpdf(time);
    
    gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.*blur), speed);
}
複製代碼

有點感受了,接着增長時間的緩動效果,很明顯咱們想要的是 easeInOut 的曲線:

float A(float aA1, float aA2) {
    return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}

float B(float aA1, float aA2) {
    return 3.0 * aA2 - 6.0 * aA1;
}

float C(float aA1) {
    return 3.0 * aA1;
}

float GetSlope(float aT, float aA1, float aA2) {
    return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}

float CalcBezier(float aT, float aA1, float aA2) {
    return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT;
}

float GetTForX(float aX, float mX1, float mX2) {
    float aGuessT = aX;
    for (int i = 0; i < 4; ++i) {
        float currentSlope = GetSlope(aGuessT, mX1, mX2);
        if (currentSlope == 0.0) return aGuessT;
        float currentX = CalcBezier(aGuessT, mX1, mX2) - aX;
        aGuessT -= currentX / currentSlope;
    }
    return aGuessT;
}

/* * @param aX: 傳入時間變量 * @param mX1/mY1/mX2/mY2: 貝塞爾曲線四個值 * 說明: 這個函數以上的其餘函數都是本函數使用的輔助函數 */
float KeySpline(float aX, float mX1, float mY1, float mX2, float mY2) {
    if (mX1 == mY1 && mX2 == mY2) return aX; // linear
    return CalcBezier(GetTForX(aX, mX1, mX2), mY1, mY2);
}

vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, fract(uv + speed)) * 0.1964825501511404;
  color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057;
  color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057;
  return color;
}

float normpdf(float x) {
    return exp(-20.*pow(x-.5,2.));
}

void main() {
    float easingTime = KeySpline(time, .65,.01,.26,.99);

    vec2 speed = vec2(0, easingTime);
    float blur = normpdf(easingTime);
    
    gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.*blur), speed);
}
複製代碼

最後給它增長一點點的垂直形變,讓它有拉伸感:

float A(float aA1, float aA2) {
    return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}

float B(float aA1, float aA2) {
    return 3.0 * aA2 - 6.0 * aA1;
}

float C(float aA1) {
    return 3.0 * aA1;
}

float GetSlope(float aT, float aA1, float aA2) {
    return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}

float CalcBezier(float aT, float aA1, float aA2) {
    return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT;
}

float GetTForX(float aX, float mX1, float mX2) {
    float aGuessT = aX;
    for (int i = 0; i < 4; ++i) {
        float currentSlope = GetSlope(aGuessT, mX1, mX2);
        if (currentSlope == 0.0) return aGuessT;
        float currentX = CalcBezier(aGuessT, mX1, mX2) - aX;
        aGuessT -= currentX / currentSlope;
    }
    return aGuessT;
}

/* * @param aX: 傳入時間變量 * @param mX1/mY1/mX2/mY2: 貝塞爾曲線四個值 * 說明: 這個函數以上的其餘函數都是本函數使用的輔助函數 */
float KeySpline(float aX, float mX1, float mY1, float mX2, float mY2) {
    if (mX1 == mY1 && mX2 == mY2) return aX; // linear
    return CalcBezier(GetTForX(aX, mX1, mX2), mY1, mY2);
}

vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction, vec2 speed) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, fract(uv + speed)) * 0.1964825501511404;
  color += texture2D(image, fract(uv + (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv - (off1 / resolution) + speed)) * 0.2969069646728344;
  color += texture2D(image, fract(uv + (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv - (off2 / resolution) + speed)) * 0.09447039785044732;
  color += texture2D(image, fract(uv + (off3 / resolution) + speed)) * 0.010381362401148057;
  color += texture2D(image, fract(uv - (off3 / resolution) + speed)) * 0.010381362401148057;
  return color;
}

vec2 stretchUv(vec2 _st, float t, int direction) {
    vec2 stUse = _st;
    float stretchRatio;

    float currentMaxStretchRatio = 1.0;
    if (t < 0.5)
        currentMaxStretchRatio = .4*pow(t, StretchSpeedPowValue) * pow(2.0, StretchSpeedPowValue) * (MaxStretchRatio - 1.0) + 1.0;
    else
        currentMaxStretchRatio = .4*pow((1.0 - t), StretchSpeedPowValue) * pow(2.0, StretchSpeedPowValue) * (MaxStretchRatio - 1.0) + 1.0;

    // 居左
    if (direction == 1) {
        stretchRatio = (currentMaxStretchRatio - 1.0) * (1.-_st.x) + 1.0;
        stUse.y = (_st.y - 0.5) / stretchRatio + 0.5;
    }
    // 居右
    else if (direction == 2) {
        stretchRatio = (currentMaxStretchRatio - 1.0) * _st.x+ 1.0;
        stUse.y = (_st.y - 0.5) / stretchRatio + 0.5;
    }
    // 居上
    else if (direction == 3) {
        stretchRatio = (currentMaxStretchRatio - 1.0) * _st.y + 1.0;
        stUse.x = (_st.x - 0.5) / stretchRatio + 0.5;
    }
    // 居下
    else if (direction == 4) {
        stretchRatio = (currentMaxStretchRatio - 1.0) * (1.-_st.y) + 1.0;
        stUse.x = (_st.x - 0.5) / stretchRatio + 0.5;
    }
    // 垂直
    else if (direction == 5) {
        stretchRatio = (currentMaxStretchRatio - 1.0) * .3 + 1.0;
        stUse.y = (_st.y - 0.5) / stretchRatio + 0.5;
    }
    // 水平
    else if (direction == 6) {
        stretchRatio = (currentMaxStretchRatio - 1.0) * .5 + 1.0;
        stUse.x = (_st.x - 0.5) / stretchRatio + 0.5;
    }

    return stUse; 
}

float normpdf(float x) {
    return exp(-20.*pow(x-.5,2.));
}

void main() {
    float easingTime = KeySpline(time, .65,.01,.26,.99);

    vec2 speed = vec2(0, easingTime);
    float blur = normpdf(easingTime);
    
    // 形變仍是用勻速的 time 時間變量
    myst = stretchUv(myst, time, 5);
    
    gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 20.*blur), speed);
}
複製代碼


問題三:如何解決任意方向的運動模糊?

假設我想實現旋轉轉場:

void main() {
    vec2 myst = uv;                                 // 用於座標計算
    float ratio = iResolution.x / iResolution.y;    // 屏幕比例

    float animationTime = getAnimationTime();
    float easingTime = KeySpline(animationTime, .68,.01,.17,.98);

    float r = 0.;
    float rotation = 180./180.*3.14159;

    if (easingTime <= .5) {
        r = rotation * easingTime;
    } else {
        r = -rotation + rotation * easingTime;
    }

    myst.y *= 1./ratio;
    myst = rotateUv(myst, r, vec2(1., 0.), -1.);
    myst.y *= ratio;

    myst = fract(myst);

    if (easingTime <= .5) {
        gl_FragColor = texture2D(inputImageTexture, myst);
    } else {
        gl_FragColor = texture2D(inputImageTexture2, myst);
    }
}
複製代碼

旋轉運動的運動模糊方向就不算單純水平或垂直或傾斜了,假設咱們粗暴的設置一個方向,看看會是怎樣:

// 改造了一下 blur13 函數,去掉了 speed 參數,由於旋轉已經在外部完成了
vec4 blur13(sampler2D image, vec2 uv, vec2 resolution, vec2 direction) {
  vec4 color = vec4(0.0);
  vec2 off1 = vec2(1.411764705882353) * direction;
  vec2 off2 = vec2(3.2941176470588234) * direction;
  vec2 off3 = vec2(5.176470588235294) * direction;
  color += texture2D(image, fract(uv)) * 0.1964825501511404;
  color += texture2D(image, fract(uv + (off1 / resolution))) * 0.2969069646728344;
  color += texture2D(image, fract(uv - (off1 / resolution))) * 0.2969069646728344;
  color += texture2D(image, fract(uv + (off2 / resolution))) * 0.09447039785044732;
  color += texture2D(image, fract(uv - (off2 / resolution))) * 0.09447039785044732;
  color += texture2D(image, fract(uv + (off3 / resolution))) * 0.010381362401148057;
  color += texture2D(image, fract(uv - (off3 / resolution))) * 0.010381362401148057;
  return color;
}

void main() {
    vec2 myst = uv;                                 // 用於座標計算
    float ratio = iResolution.x / iResolution.y;    // 屏幕比例

    float animationTime = getAnimationTime();
    float easingTime = KeySpline(animationTime, .68,.01,.17,.98);

    float blur = normpdf(easingTime);

    float r = 0.;
    float rotation = 180./180.*3.14159;

    if (easingTime <= .5) {
        r = rotation * easingTime;
    } else {
        r = -rotation + rotation * easingTime;
    }

    myst.y *= 1./ratio;
    myst = rotateUv(myst, r, vec2(1., 0.), -1.);
    myst.y *= ratio;

    if (easingTime <= .5) {
        gl_FragColor = blur13(inputImageTexture, myst, iResolution, vec2(0., 50.*blur));
    } else {
        gl_FragColor = blur13(inputImageTexture2, myst, iResolution, vec2(0., 50.*blur));
    }
}
複製代碼

這明顯不是咱們像要的方向。這裏能夠換一個思路,咱們能夠拿下一幀的座標減去上一幀的座標,得出來的值就是咱們的運動方向。

因此能夠這麼作:

void main() {
    vec2 myst = uv;                                 // 用於座標計算
    float ratio = iResolution.x / iResolution.y;    // 屏幕比例

    float animationTime = getAnimationTime();
    float easingTime = KeySpline(animationTime, .68,.01,.17,.98);

    float blur = normpdf(easingTime);

    float r = 0.;
    float rotation = 180./180.*3.14159;

    if (easingTime <= .5) {
        r = rotation * easingTime;
    } else {
        r = -rotation + rotation * easingTime;
    }


    // 當前幀進行旋轉
    vec2 mystCurrent = myst;
    mystCurrent.y *= 1./ratio;
    mystCurrent = rotateUv(mystCurrent, r, vec2(1., 0.), -1.);
    mystCurrent.y *= ratio;

    // 以 fps=60 做爲間隔
    float timeInterval = 0.00016;

    if (easingTime <= .5) {
        r = rotation * (easingTime+timeInterval);
    } else {
        r = -rotation + rotation * (easingTime+timeInterval);
    }

    // 下一幀幀進行旋轉
    vec2 mystNext = myst;
    mystNext.y *= 1./ratio;
    mystNext = rotateUv(mystNext, r, vec2(1., 0.), -1.);
    mystNext.y *= ratio;

    // 獲得單位座標方向
    vec2 speed  = (mystNext - mystCurrent / timeInterval);

    if (easingTime <= .5) {
        gl_FragColor = blur13(inputImageTexture, mystCurrent, iResolution, speed*blur*0.01);
    } else {
        gl_FragColor = blur13(inputImageTexture2, mystCurrent, iResolution, speed*blur*0.01);
    }
}
複製代碼

看來傳統的單方向高斯模糊並不能知足咱們想要的效果,這裏引用另一個函數(含隨機模糊效果):

// 運動模糊
vec4 motionBlur(sampler2D texture, vec2 _st, vec2 speed) {
    vec2 texCoord = _st.xy / vec2(1.0).xy;
    vec3 color = vec3(0.0);
    float total = 0.0;
    float offset = rand(_st);
    for (float t = 0.0; t <= 20.0; t++) {
        float percent = (t + offset) / 20.0;
        float weight = 4.0 * (percent - percent * percent);
        color += getColor(texture, texCoord + speed * percent).rgb * weight;
        total += weight;
    }
    return vec4(color / total, 1.0);
}

void main() {
    vec2 myst = uv;                                 // 用於座標計算
    float ratio = iResolution.x / iResolution.y;    // 屏幕比例

    float animationTime = getAnimationTime();
    float easingTime = KeySpline(animationTime, .68,.01,.17,.98);

    float blur = normpdf(easingTime);

    float r = 0.;
    float rotation = 180./180.*3.14159;

    if (easingTime <= .5) {
        r = rotation * easingTime;
    } else {
        r = -rotation + rotation * easingTime;
    }

    // 當前幀進行旋轉
    vec2 mystCurrent = myst;
    mystCurrent.y *= 1./ratio;
    mystCurrent = rotateUv(mystCurrent, r, vec2(1., 0.), -1.);
    mystCurrent.y *= ratio;


    // 以 fps=60 做爲間隔
    float timeInterval = 0.0167;
    

    if (easingTime <= .5) {
        r = rotation * (easingTime+timeInterval);
    } else {
        r = -rotation + rotation * (easingTime+timeInterval);
    }


    // 下一幀幀進行旋轉
    vec2 mystNext = myst;
    mystNext.y *= 1./ratio;
    mystNext = rotateUv(mystNext, r, vec2(1., 0.), -1.);
    mystNext.y *= ratio;


    // 獲得單位座標方向
    vec2 speed  = (mystNext - mystCurrent) / timeInterval * blur * 0.5;


    // if (easingTime <= .5) {
    // gl_FragColor = blur13(inputImageTexture, mystCurrent, iResolution, speed*blur*0.01);
    // } else {
    // gl_FragColor = blur13(inputImageTexture2, mystCurrent, iResolution, speed*blur*0.01);
    // }
    if (easingTime <= .5) {
        gl_FragColor = motionBlur(inputImageTexture, mystCurrent, speed);
    } else {
        gl_FragColor = motionBlur(inputImageTexture2, mystCurrent, speed);
    }
}
複製代碼

顯然,對於複雜一些的運動(非位移),單純的高斯模糊並不能帶來很逼真的效果,還須要搭配隨機模糊、變形、扭曲等因素:

接下來咱們再添加一些畫布的縮放,回彈等效果進來:

void main() {
    vec2 myst = uv;                                 // 用於座標計算
    float ratio = iResolution.x / iResolution.y;    // 屏幕比例

    float animationTime = getAnimationTime();
    float animationTime2 = smoothstep(.2, 1., animationTime);
    float easingTime = KeySpline(animationTime2, .4,.71,.26,1.07);
    float easingTime2 = KeySpline(animationTime2, 0.,.47,.99,.57);
    
    float blur = normpdf(easingTime);

    float r = 0.;
    float rotation = 180./180.*3.14159;

    if (easingTime <= .5) {
        r = rotation * easingTime;
    } else {
        r = -rotation + rotation * easingTime;
    }

    if (animationTime < .2) {
        myst -= .5;
        myst *= scaleUv(vec2(0.92-animationTime*.3));
        myst += .5;

        gl_FragColor = texture2D(inputImageTexture, myst);
    }
    else {
        myst = stretchUv(myst, easingTime2, 1);     // 左側拉伸
        myst = stretchUv(myst, easingTime2, 3);     // 頂部拉伸
        myst = stretchUv(myst, easingTime, 5);      // 垂直拉伸


        // 當前幀進行旋轉
        vec2 mystCurrent = myst;
        mystCurrent.y *= 1./ratio;
        mystCurrent = rotateUv(mystCurrent, r, vec2(1., 0.), -1.);
        mystCurrent.y *= ratio;


        // 以 fps=60 做爲間隔,計算出實際幀速率
        float timeInterval = 0.016;
        if (animationTime < 0.5 && animationTime + timeInterval > 0.5)
            timeInterval = 0.5 - animationTime;


        if (easingTime <= .5) {
            r = rotation * (easingTime+timeInterval);
        } else {
            r = -rotation + rotation * (easingTime+timeInterval);
        }

        // 下一幀幀進行旋轉
        vec2 mystNext = myst;
        mystNext.y *= 1./ratio;
        mystNext = rotateUv(mystNext, r, vec2(1., 0.), -1.);
        mystNext.y *= ratio;


        // 獲得單位座標方向
        vec2 speed  = (mystNext - mystCurrent) / timeInterval * blur * 0.5;


        if (easingTime <= .5) {
            mystCurrent -= .5;
            mystCurrent *= scaleUv(vec2(0.92-animationTime*.3));
            mystCurrent += .5;
            gl_FragColor = motionBlur(inputImageTexture, mystCurrent, speed);
        } else {
            mystCurrent -= .5;
            mystCurrent *= scaleUv(vec2(0.92));
            mystCurrent += .5;
            gl_FragColor = motionBlur(inputImageTexture2, mystCurrent, speed);
        }
    }
}
複製代碼



相關連接:

相關文章
相關標籤/搜索