предисловие
Эффекты перехода наиболее распространены в инструментах редактирования видео. Добавление эффекта «перехода» между двумя видео или изображениями может сделать весь процесс более плавным и естественным. Общие переходы, такие как градиентные переходы, повороты, стирания и т. д. (на рисунке ниже показаны собственные переходы iMovie):
И теперь многие видеоприложения также имеют функцию альбома, вы можете выбирать различные переходы для создания динамического альбома:
Реализация переходов в WebGL сильно отличается от редактора, эти отличия наводят на некоторые мысли:
1. Время переключения материала
доэтоВ статье упоминается переключение двух материалов, но вообще альбом будет больше двух картинок, как сделать весь цикл переключения без восприятия? Вот пример с простой анимацией:
Для простого объяснения предположим, что наш эффект перехода заключается в переключении справа налево (как показано на анимации), время переключения — это конец каждого раунда анимации.u_Sampler0
а такжеu_Sampler1
Для переназначения первое изображение каждого раунда анимации является следующим изображением предыдущего раунда анимации.Это мгновенное назначение заставит всю анимацию чувствовать себя неизменной, чтобы реализовать цикл различных материалов и не будет занимать пространство WebGL. Текстурных пространств много (нужно только два), и этот метод тоже исходит из опыта написания Slider на веб-стороне.
Соответствующий код выглядит следующим образом:
// 更换材质
function changeTexture(gl, imgList, count) {
var texture0 = gl.createTexture();
var texture1 = gl.createTexture();
if (!texture0 && !texture1) {
console.log('Failed to create the texture object');
return false;
}
var u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0');
if (!u_Sampler0) {
console.log('Failed to get the storage location of u_Sampler0');
return false;
}
var u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1');
if (!u_Sampler1) {
console.log('Failed to get the storage location of u_Sampler1');
return false;
}
loadTexture(gl, texture0, u_Sampler0, imgList[count%imgList.length], 0);
loadTexture(gl, texture1, u_Sampler1, imgList[(count+1)%imgList.length], 1);
}
// 加载材质
function loadTexture(gl, texture, u_Sampler, image, index) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
gl.activeTexture(gl['TEXTURE'+index])
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.uniform1i(u_Sampler, index);
return true;
}
Во-вторых, переключение эффекта перехода
Много раз мы будем использовать комбинацию различных эффектов перехода, вот две идеи:
1. Реализовать переключение переходов в шейдере
Передайте переменную, которая записывает количество переходов, определите количество раз в коде шейдера и переключите переходы.
precision mediump float;
varying vec2 uv;
uniform float time; // 变化时间
uniform sampler2D u_Sampler0;
uniform sampler2D u_Sampler1;
uniform float count; // 循环第几次
void main() {
if (count == 1.) {
// 第一次转场
// 播放第一个效果
}
else if (count == 2.) {
// 第二次转场
// 播放第二个效果
}
}
Недостатки этого способа очевидны: во-первых, файлы недостаточно детализированы, а эффектов в одном файле несколько, во-вторых, логика и эффекты сцеплены вместе, поэтому делать какую-либо комбинацию разных переходов неудобно. Например, у меня есть переходы 1, 2 и 3. Если это независимое хранилище файлов, и я могу настроить последовательность 123/132/231/213/312/321/1123/.... для управления продолжительностью воспроизведения каждый переход. Поэтому второй метод более рекомендуется:
2. Каждый переход представляет собой независимый файл, а код переключается
// transition1.glsl
precision mediump float;
varying vec2 uv;
uniform float time;
uniform sampler2D u_Sampler0;
uniform sampler2D u_Sampler1;
void main() {
// ...
}
// transition2.gls
precision mediump float;
varying vec2 uv;
uniform float time;
uniform sampler2D u_Sampler0;
uniform sampler2D u_Sampler1;
void main() {
// ...
}
Затем мы управляем переходом в JavaScript:
// 在 main() 底部加入这段代码
void main() {
function render() {
var img1 = null;
var img2 = null;
// 每次移出一张图来
if (imgList.length > 2) {
img1 = imgList.shift()
img2 = imgList[0]
} else {
return;
}
// 我随便添加了一个逻辑,在图片还剩三张的时候,切换第二个转场。
// 这里忽略了文件获取过程
if (imgList.length == 3) {
setShader(gl, VSHADER_SOURCE, FSHADER_SOURCE2);
} else {
setShader(gl, VSHADER_SOURCE, FSHADER_SOURCE);
}
// 设置材质
setTexture(gl, img1, img2);
// 下面通过 time 和 timeRange 来确定每个轮播的时间(这里用的是时间戳)
// 并通过 getAnimationTime() 来获取从 0~1 的 progress 时间
var todayTime = (function() {
var d = new Date();
d.setHours(0, 0, 0, 0);
return d.getTime();
})()
var duration = 2000;
var startTime = new Date().getTime() - todayTime;
var timeRange = gl.getUniformLocation(gl.program, 'timeRange');
gl.uniform2f(timeRange, startTime, duration);
var time = gl.getUniformLocation(gl.program, 'time');
gl.uniform1f(time, todayTime);
// 因为调用 setShader 重新设置了 program,所有所有跟 gl.program 相关的变量要重新赋值
var xxx = gl.getUniformLocation(gl.program, 'xxx');
gl.uniform2f(xxx, 750., 1334.);
// 内循环,每次把这轮的转场播放完
var requestId = 0;
(function loop(requestId) {
var curTime = new Date().getTime() - todayTime;
if (curTime <= startTime + duration) {
gl.uniform1f(time, curTime)
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
requestId = requestAnimationFrame(loop.bind(this, requestId))
} else {
cancelAnimationFrame(requestId)
render()
}
})(requestId)
}
render()
}
// 更换材质
function setTexture(gl, img1, img2) {
var texture0 = gl.createTexture();
var texture1 = gl.createTexture();
var inputImageTexture = gl.getUniformLocation(gl.program, 'inputImageTexture');
var inputImageTexture2 = gl.getUniformLocation(gl.program, 'inputImageTexture2');
loadTexture(gl, texture0, inputImageTexture, img1, 0);
loadTexture(gl, texture1, inputImageTexture2, img2, 1);
}
// 切换不同的转场(只需要改变 fshader)
function setShader(gl, vshader, fshader) {
if (!initShaders(gl, vshader, fshader)) {
console.log('Failed to intialize shaders.');
return;
}
}
3. Метод перехода материала
Переход обычно сопровождается переключением двух картинок, существует два распространенных способа переключения:
- Линейная интерполяция двух пикселей изображения
mix()
, переключение мягче - Переключение по времени, переключение более тупое
1. Линейная интерполяция
Обычно он подходит для переходов с плавными переходами, и хорошо виден процесс чередования двух изображений:
return mix(texture2D(u_Sampler0, uv), texture2D(u_Sampler1, uv), progress);
2. Переключение по времени
Обычно он подходит для ситуаций, когда переход меняется быстро, и это переключение невозможно различить невооруженным глазом.
if (progress < 0.5) {
gl_FragColor = texture2D(u_Sampler0, uv);
} else {
gl_FragColor = texture2D(u_Sampler1, uv);
}
Например, в первом переходе на следующем рисунке текстура переключается мгновенно по времени (но этого не видно), а последний является градиентным за счет линейной интерполяции:
В-четвертых, моделирование скорости анимации
В принципе, не все переходы будут простыми к линейному равномерному движению, поэтому здесь необходимо моделировать различные кривые скорости. Чтобы восстановить лучший эффект перехода, необходимо выполнить несколько шагов:
1. Получите кривую в реальном времени
Предполагая, что переход разработан вами, вы можете использовать некоторые предустановленные кривые, такие какздесьчто обеспечило:
Мы можем напрямую получить формулу Безье для кривой:
Если предположить, что эффект перехода обеспечивается другими, например, дизайнер использует AE для создания эффекта перехода, то кривую изменения времени, соответствующую соответствующему движению, можно найти в AE:
2. Использование кривой скорости
После получения кривой, конечно же, следующим шагом будет получение ее математической формулы и перенос ее в наши переменные (прогресс/время/uv.x и т.д.).
Прежде всего, необходимо уяснить, что время в реальном мире не становится быстрее или медленнее, а это означает, что время всегда движется с постоянной скоростью. Просто когда мы накладываем формулу на единицу времени, пустьрезультатС изменением скорости (если движение по оси x является нашей независимой переменной, то y может быть зависимой переменной).
#ifdef GL_ES
precision mediump float;
#endif
#define PI 3.14159265359
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
float plot(vec2 st, float pct){
return smoothstep( pct-0.01, pct, st.y) -
smoothstep( pct, pct+0.01, st.y);
}
float box(vec2 _st, vec2 _size, float _smoothEdges){
_size = vec2(0.5)-_size*0.5;
vec2 aa = vec2(_smoothEdges*0.5);
vec2 uv = smoothstep(_size,_size+aa,_st);
uv *= smoothstep(_size,_size+aa,vec2(1.0)-_st);
return uv.x*uv.y;
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution;
vec2 boxst = st + .5;
// 这里用线条绘制出数学公式 y = f(x)
// 自变量是 st.x,因变量是 st.y
float f_x = sin(st.x*PI);
// 这里则计算小正方形每次运动的位置
// 公式跟上面 f(x) 展示的一样,只不过
// 我们的因变量从 st.x 变成了 fract(u_time)
// fract(u_time) 让时间永远从0到1
// 之所以要 *.6 是因为不让运动太快以至于看不清运动速率变化
boxst.y -= sin(fract(u_time*.6)*PI);
boxst.x -= fract(u_time*.6);
// 绘制时间曲线和正方形
float box = box(boxst, vec2(.08,.08), 0.001);
float pct = plot(st, f_x);
vec3 color = pct*vec3(0.0,1.0,0.0)+box;
gl_FragColor = vec4(color,1.0);
}
Позже нам просто нужно заменить формулу здесь наst.x
илиu_time / progress
В качестве независимой переменной можно получить соответствующую кривую движения и анимацию.Далее мы можем попробовать другие кривые анимации:
// 展示部分代码
float f_x = pow(st.x, 2.);
boxst.y -= pow(fract(u_time*.6), 2.);
boxst.x -= fract(u_time*.6);
float f_x = -(pow((st.x-1.), 2.) -1.);
boxst.y -= -(pow((fract(u_time*.6)-1.), 2.) -1.);
boxst.x -= fract(u_time*.6);
// easeInOutQuint
float f_x = st.x<.5 ? 16.*pow(st.x, 5.) : 1.+16.*(--st.x)*pow(st.x, 4.);
boxst.y -= fract(u_time*.6)<.5 ? 16.*pow(fract(u_time*.6), 5.) : 1.+16.*(fract(u_time*.6)-1.)*pow(fract(u_time*.6)-1., 4.);
boxst.x -= fract(u_time*.6);
// easeInElastic
float f_x = ((.04 -.04/st.x) * sin(25.*st.x) + 1.)*.8;
boxst.y -= ((.04 -.04/fract(u_time*.6)) * sin(25.*fract(u_time*.6)) + 1.)*.8;
boxst.x -= fract(u_time*.6);
// easeOutElastic
float f_x = (.04*st.x /(--st.x)*sin(25.*st.x))+.2;
boxst.y -= (.04*fract(u_time*.6)/(fract(u_time*.6)-1.)*sin(25.*fract(u_time*.6)))+.2;
boxst.x -= fract(u_time*.6);
Дополнительные функции смягчения:
EasingFunctions = {
// no easing, no acceleration
linear: function (t) { return t },
// accelerating from zero velocity
easeInQuad: function (t) { return t*t },
// decelerating to zero velocity
easeOutQuad: function (t) { return t*(2-t) },
// acceleration until halfway, then deceleration
easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
// accelerating from zero velocity
easeInCubic: function (t) { return t*t*t },
// decelerating to zero velocity
easeOutCubic: function (t) { return (--t)*t*t+1 },
// acceleration until halfway, then deceleration
easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
// accelerating from zero velocity
easeInQuart: function (t) { return t*t*t*t },
// decelerating to zero velocity
easeOutQuart: function (t) { return 1-(--t)*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
// accelerating from zero velocity
easeInQuint: function (t) { return t*t*t*t*t },
// decelerating to zero velocity
easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t },
// elastic bounce effect at the beginning
easeInElastic: function (t) { return (.04 - .04 / t) * sin(25 * t) + 1 },
// elastic bounce effect at the end
easeOutElastic: function (t) { return .04 * t / (--t) * sin(25 * t) },
// elastic bounce effect at the beginning and end
easeInOutElastic: function (t) { return (t -= .5) < 0 ? (.02 + .01 / t) * sin(50 * t) : (.02 - .01 / t) * sin(50 * t) + 1 },
easeIn: function(t){return function(t){return pow(t, t)}},
easeOut: function(t){return function(t){return 1 - abs(pow(t-1, t))}},
easeInSin: function (t) { return 1 + sin(PI / 2 * t - PI / 2)},
easeOutSin : function (t) {return sin(PI / 2 * t)},
easeInOutSin: function (t) {return (1 + sin(PI * t - PI / 2)) / 2 }
}
3. Создайте пользовательскую кривую скорости
Мы можем рисовать собственные кривые скорости с помощью кривых Безье Как преобразовать кривые Безье, которые мы обычно используем в CSS, в математические формулы? этостатьяЭто дало нам идею, и, преобразовав предоставленный код JavaScript, мы получили следующую функцию шейдера:
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;
}
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);
}
Как следует использовать эту функцию?Во-первых, мы получаем четыре параметра через редактор кривой Безье, такие как эти два инструмента:bezier-easing-editorилиcubic-bezier:
или
Соответствующую кривую можно получить, подставив эти четыре числа и независимые переменные, например, мы сами построили кривую:
тогда поставь.1, .96, .89, .17
Подставляя, мы можем получить кривую движения, которую мы хотим:
Однако, когда мы передаем некоторые специальные значения, такие как0.99,0.14,0,0.27
получится странная кривая:
На самом деле желаемая кривая:
Это связано с тем, что автор не учел многоугольный наклон при реализации преобразования, после его обновления мы получили более надежный код, такой как:GitHub.com/GRE/whitemoth-and-…, и снова конвертирую их в шейдерные функции:
float sampleValues[11];
const float NEWTON_ITERATIONS = 10.;
const float NEWTON_MIN_SLOPE = 0.001;
const float SUBDIVISION_PRECISION = 0.0000001;
const float SUBDIVISION_MAX_ITERATIONS = 10.;
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 newtonRaphsonIterate(float aX, float aGuessT, float mX1, float mX2) {
for (float i = 0.; i < NEWTON_ITERATIONS; ++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;
}
float binarySubdivide(float aX, float aA, float aB, float mX1, float mX2) {
float currentX, currentT;
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else {
aA = currentT;
}
for(float i=0.; i<SUBDIVISION_MAX_ITERATIONS; ++i) {
if (abs(currentX)>SUBDIVISION_PRECISION) {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else {
aA = currentT;
}
} else {
break;
}
}
return currentT;
}
float GetTForX(float aX, float mX1, float mX2, int kSplineTableSize, float kSampleStepSize) {
float intervalStart = 0.0;
const int lastSample = 10;
int currentSample = 1;
for (int i = 1; i != lastSample; ++i) {
if (sampleValues[i] <= aX) {
currentSample = i;
intervalStart += kSampleStepSize;
}
}
--currentSample;
// Interpolate to provide an initial guess for t
float dist = (aX - sampleValues[9]) / (sampleValues[10] - sampleValues[9]);
float guessForT = intervalStart + dist * kSampleStepSize;
float initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= NEWTON_MIN_SLOPE) {
return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
} else if (initialSlope == 0.0) {
return guessForT;
} else {
return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2);
}
}
float KeySpline(float aX, float mX1, float mY1, float mX2, float mY2) {
const int kSplineTableSize = 11;
float kSampleStepSize = 1. / (float(kSplineTableSize) - 1.);
if (!(0. <= mX1 && mX1 <= 1. && 0. <= mX2 && mX2 <= 1.)) {
// bezier x values must be in [0, 1] range
return 0.;
}
if (mX1 == mY1 && mX2 == mY2) return aX; // linear
for (int i = 0; i < kSplineTableSize; ++i) {
sampleValues[i] = calcBezier(float(i)*kSampleStepSize, mX1, mX2);
}
if (aX == 0.) return 0.;
if (aX == 1.) return 1.;
return calcBezier(GetTForX(aX, mX1, mX2, kSplineTableSize, kSampleStepSize), mY1, mY2);
}
Наконец мы получили нужную нам кривую движения:
С помощью мощного инструмента кривой Безье мы можем в основном удовлетворить наши потребности в любой скорости изменения анимации. Это дает нам надежную гарантию достижения элегантных и естественных эффектов перехода.
Давайте почувствуем тонкие сенсорные различия между равномерным движением и неравномерным движением при переходах между двумя движущимися изображениями (первое изображение является однородным, а второе изображение добавляет кривую Безье. GIF повлияет на конечный эффект, но может примерно почувствовать):
Ссылки по теме: