阅读 744

Shader 运动模糊(Motion Blur)

了解完高斯模糊之后,接下来看看运动模糊。

什么是运动模糊? 根据百科的定义:动态模糊或运动模糊是静态场景或一系列的图片像电影或是动画中快速移动的物体造成明显的模糊拖动痕迹。

为什么会出现运动模糊? 摄影机的工作原理是在很短的时间里把场景在胶片上曝光。场景中的光线投射在胶片上,引起化学反应,最终产生图片。这就是曝光。如果在曝光的过程中,场景发生变化,则就会产生模糊的画面。


问题一:运动模糊是否就是单一方向的高斯模糊?

我们根据运动模糊的物理成像原理可以知道,离快门关闭越近的图像越清晰,残影是存在透明度变化的,它也受速度影响:

我们尝试一下通过单一方向的高斯模糊来模拟运动模糊,看看效果如何:

// 只展示核心代码
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.));
}
复制代码

虽然单一方向的高斯模糊并不完全符合运动模糊的定义。但单纯看效果其实分辨不太出来,我们再把效果强化:

问题二:如何让画面自然地动起来?

运动模糊,自然需要运动才能体现出来。首先我们实现一个简单的位移:

// 只展示核心代码
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,有点辣眼睛。首先需要解决的问题是图像边界连接到地方并没有运动模糊:

这意味着我们要实时的取当前的坐标来对图像进行取样,所以传入一个新的参数,表示当前运动的距离:

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);
}
复制代码

那解决完边界问题,再分析下运动模糊出现的时机,运动开始和结束肯定不会产生模糊,只有中间过程才会有模糊,所以我们根据时间来调整模糊:

我们先构造一个从 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);
        }
    }
}
复制代码



相关链接:

关注下面的标签,发现更多相似文章
评论