| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- <!--
- demo.design 3D programming FAQ
- Idea, texts, screenshots:
- Andrew A. Aksyonoff,
- shodan@chat.ru
- Web-design, illustrations:
- Andrey Samoilov,
- asy@sense.simbirsk.su
- -->
- <html>
- <head>
- <title>demo.design 3D programming FAQ. Разное. Сплайны Кочанека-Бартельса.</title>
- <link rel=stylesheet href="../style.css" type="text/css">
- </head>
- <script language="javascript">
- <!--//
- browser = navigator.appName;
- version = parseFloat(navigator.appVersion);
- if (browser == "Netscape" && version >= 3.0) { jsenabled = 1; } else
- if (browser == "Microsoft Internet Explorer" && version >= 3.0) { jsenabled = 1; } else { jsenabled = 0; }
- function swap(img,ref) { if (jsenabled) {document.images[img].src = ref;} }
- function loadtocache(img,ref) { cache[img] = new Image(); cache[img].src = ref; }
- if (jsenabled) {
- cache = new Array();
- loadtocache(0,"../img/xdl.gif");
- loadtocache(1,"../img/xfaq.gif");
- loadtocache(2,"../img/xlinks.gif");
- loadtocache(3,"../img/xauthor.gif");
- loadtocache(4,"../img/xe.gif");
- loadtocache(5,"../img/xprev.gif");
- loadtocache(6,"../img/xnext.gif");}
- //-->
- </script>
- <body bgcolor=white><center>
- <!-- Title -->
- <img src="../img/b.gif" width=500 height=1 alt=""><br>
- <img src="../img/t.gif" width=500 height=1 alt=""><br>
- <img src="../img/b.gif" width=500 height=1 alt=""><br>
- <img src="../img/t.gif" width=500 height=2 alt=""><br>
- <table width=500 cellpadding=0 cellspacing=0 border=0>
- <td><img src="../img/t.gif" width=5 height=1 alt=""><a href="../main.htm" onmouseover="swap('logo','../img/xe.gif');" onmouseout="swap('logo','../img/e.gif');"><img src="../img/e.gif" name=logo width=60 height=50 hspace=10 border=0 alt=" в самое начало "></a></td>
- <td><p class=pagetitle><img src="../img/t.gif" width=265 height=1 alt=""><br>demo.design<br>3D programming FAQ</td>
- <td align=center><p class=navy><a href="../download.htm" onmouseover="swap('dl','../img/xdl.gif');" onmouseout="swap('dl','../img/dl.gif');"><img src="../img/dl.gif" name=dl width=40 height=40 border=0 hspace=5 alt=" download "></a><br>download</td>
- <td align=center><p class=navy><a href="../links.htm" onmouseover="swap('links','../img/xlinks.gif');" onmouseout="swap('links','../img/links.gif');"><img src="../img/links.gif" name=links width=40 height=40 border=0 hspace=5 alt=" коллекция линков "></a><br>links</td>
- <td align=center><p class=navy><a href="../author.htm" onmouseover="swap('author','../img/xauthor.gif');" onmouseout="swap('author','../img/author.gif');"><img src="../img/author.gif" name=author width=40 height=40 border=0 hspace=5 alt=" автора! "></a><br>author</td>
- </table>
- <img src="../img/t.gif" width=500 height=4 alt=""><br><img src="../img/b.gif" width=500 height=1 alt=""><br>
- <!-- Head -->
- <table width=500 cellpadding=0 cellspacing=10 border=0><td><div align=justify>
- <p class=title>
- <img src="../img/b7.gif" width=70 height=70 align=left hspace=0 alt="">
- <img src="../img/t.gif" width=5 height=70 align=left hspace=0 alt="">
- РАЗНОЕ<br>7.6. Сплайны Кочанека-Бартельса
- <!-- Article -->
- <p>Пусть у нас имеется набор моментов времени и заданные значения какой-то
- функции (например, координат положения объекта) в эти моменты. Для того,
- чтобы получить значение функции в какой-то промежуточный момент времени,
- между заданными моментами ее придется интерполировать. Самый простой случай,
- конечно, линейная интерполяция (точнее говоря, кусочно-линейная), когда
- между любыми двумя заданными точками p1, p2 значение интерполируется по
- следующей формуле:
- <p class=expression>f(t) = p1.value + (p2.value - p1.value) * t.<br>
- <p>Здесь t - "локальное" время, t=0 в точке p1 и t=1 в точке p2, pi.value -
- значение интерполируемой величины в точке pi. Перевод "локального" времени t
- в "глобальное" время T и наоборот тривиален:
- <p class=expression>t = (T - p1.T) / (p2.T - p1.T),<br>
- T = p1.T + t * (p2.T - p1.T).<br>
- <p>Результат получится такого вида:
- <p><center><img src="illu/illu76a.gif" width=270 height=180 alt="рисунок (illu/illu76a.gif)"></center>
- <p>Но в результате такой интерполяции получаются ломаные - негладкие функции.
- Визуально это выливается во внезапно дергающиеся, неожиданно меняющие
- направление движения объекты и камеру. Негладкость можно математически
- описать как разрывность производной функции, получающейся в результате
- интерполяции; в точках задания значений у нас производная будет резко
- меняться.
- <p>Интерполяция сплайнами - такой метод, который дает в результате гладкую
- функцию. Достигается это следующим образом: между каждыми двумя точками
- функция интерполируется кубической параболой (то есть, полиномом третьей
- степени, вида P(x) = a*x^3+b*x^2+c*x+d), причем функция подбирается так,
- чтобы на концах отрезка (как раз в точках задания значений) совпадали с
- какими-то заранее заданными числами и сами значения функции, и значения
- производной. Совпадение самих значений функции требуется для непрерывности
- получаемой приближающей функции, а совпадение значений производной - для
- непрерывности производной, то есть для "гладкости" приближающей функции.
- Таким образом, результат интерполяции у нас получается составленным не из
- прямых, как при кусочно-линейной интерполяции, а из "сшитых" кусков разных
- кубических парабол. Причем эта кривая получается еще и гладкой, так как мы
- "сшиваем" не только сами значения функции, но и значения ее производной.
- <p>Пусть на концах отрезка "локального" времени [0,1] заданы значения функции
- p1, p2 и значения производной r1, r2. Приближающая функция - кубическая,
- то есть
- <p class=expression>f(t) = a*t^3 + b*t^2 + c*t + d,<br>
- f'(t) = 3*a*t^2 + 2*b*t + c,<br>
- <p>откуда и получаем:
- <p class=expression>f(0) = p1,<br>
- f(1) = p2,<br>
- f'(0) = r1,<br>
- f'(1) = r2,<br>
- <br>
- d = p1,<br>
- a + b + c + d = p2,<br>
- c = r1,<br>
- 3*a + 2*b + c = r2.<br>
- <p>Решив эту систему уравнений, получаем:
- <p class=expression>a = 2*p1 - 2*p2 + r1 + r2,<br>
- b = -3*p1 + 3*p2 - 2*r1 -r2,<br>
- c = r1,<br>
- d = p1,<br>
- <p>отсюда
- <p class=expression>f(t) = (2*p1 - 2*p2 + r1 + r2) * t^3 +<br>
- (-3*p1 + 3*p2 - 2*r1 - r2) * t^2 + r1 * t + p1,<br>
- <p>или, что равносильно,
- <p class=expression>f(t) = p1 * (2*t^3 - 3*t^2 + 1) + r1 * (t^3 - 2*t^2 + t) +<br>
- p2 * (-2*t^3 + 3*t^2) + r2 * (t^3 - t^2).
- <p>Подставляем сюда значения интерполируемой величины в начале и конце отрезка
- (p1 и p2), желаемые значения производных в начале и конце отрезка (r1 и r2),
- локальное время для отрезка интерполяции t, и получаем интерполированное
- значение функции в любой точке отрезка - кусок кубической параболы, который
- проходит через точки (0,p1), (1,p2) и имеет в точках t=0 и t=1 производные
- r1 и r2 соответственно.
- <p>Осталось выяснить, откуда брать значения производных r1, r2. Они считаются
- через заданные (они же ключевые) значения нашей функции в самой точке-ключе,
- следующей и предыдущей точке, а также параметры сплайна tension, continuity
- и bias в данной точке. Все это (значения функции и набор параметров сплайна
- в каком-то наборе точек) задается извне; то есть, например, читается из
- 3DS-файла.
- <p>Введем следующие обозначения. Пусть cur - текущая точка, next - предыдущая,
- prev - следующая, r - производная в текушей точке cur, которую мы и должны
- как-то посчитать. Пусть
- <p class=expression>g1 = cur.value - prev.value,<br>
- g2 = next.value - cur.value,<br>
- g3 = g2 - g1.<br>
- <p>Тогда по умолчанию (это когда параметры tension, continuity, bias равны 0)
- производная считается как
- <p class=expression>r = g1 + 0.5 * g3.<br>
- <p>Параметры tension, continuity и bias соответственно изменяют вес g3, а также
- значения r, g1, g2; с учетом этих параметров формула для производной выглядит
- следующим образом:
- <p class=expression>g1 = (cur.value - prev.value) * (1 + bias),<br>
- g2 = (next.value - cur.value) * (1 - bias),<br>
- g3 = g2 - g1,<br>
- ra = (1 - tension) * (g1 + 0.5 * g3 * (1 + continuity)),<br>
- rb = (1 - tension) * (g1 + 0.5 * g3 * (1 - continuity)).<br>
- <p>Здесь уже появляются два разных значения производной ra и rb. ra - это то
- значение производной, которое используется, когда точка является началом
- отрезка сплайн-интерполяции; rb - когда концом. То есть, при интерполяции
- между какими-то точками p1, p2 используются значения r1 = p1.ra, r2 = p2.rb.
- При continuity = 0 они (ra и rb) равны, при continuity = 1 ra удваивается,
- а rb становится равным нулю. Таким образом, параметр continuity контролирует
- "изломанность", негладкость сплайна.
- <p>Геометрический смысл всех этих параметров нагляднее всего можно показать в
- случае, когда value - 2D-вектор. Тогда cur, prev, next - какие-то точки на
- плоскости, сплайн - проведенная через них кривая, r - вектор-градиент
- кривой в точке cur, он же является касательной (точнее, направляющим вектором
- прямой, являющейся касательной) в этой точке. Параметр tension непосредственно
- изменяет длину градиента; bias меняет веса g1, g2 в g3; continuity заставляет
- конец градиента "ездить" вдоль g3. Все это соотвествующим образом влияет на
- вид самой кривой. Для лучшего понимания посмотрите на приведенную картинку,
- представьте себе гладкую линию (кривую), проведенную через точки prev, cur,
- next и учтите, что r - это желаемое нами положение касательной прямой к этой
- кривой в точке cur.
- <p><center><img src="illu/illu76b.gif" width=270 height=180 alt="рисунок (illu/illu76b.gif)"></center>
- <p>В начальных и конечных точках задания функции производные ra, rb считаются
- по-другому, так как для них нельзя указать предыдущую или следующую точку.
- Для начальной точки
- <p class=expression>ra = next.value - cur.value,<br>
- rb = (1.5 * (next.value - cur.value) - 0.5 * next.ra) * (1 - tension).<br>
- <p>Для конечной точки
- <p class=expression>ra = (1.5 * (cur.value - prev.value) - 0.5 * prev.rb) * (1 - tension),<br>
- rb = cur.value - prev.value.<br>
- <p>ra для начальной точки и rb для конечной на самом деле не используются, они
- приведены здесь лишь для того, чтобы формулы давали правильный результат в
- случае, если есть всего две точки p0, p1; тогда
- <p class=expression>p0.rb = p1.ra = (p1.value - p0.value) * (1 - tension).<br>
- <p>Осталось упомянуть про параметры ease to и ease from. Они контролируют
- нелинейность движения по полученной кривой в зависимости от времени - или,
- если угодно, нелинейность самого времени. Не особо вникая в детали, корректно
- отработать все это можно с помощью следующего куска кода:
- <pre class=source>
- // ...
- float ease(float t, float a, float b) {
- float k;
- float s = a + b;
- if (s == 0.0) return t;
- if (s > 1.0) {
- a = a / s;
- b = b / s;
- }
- k = 1.0 / (2.0 - a - b);
- if (t < a)
- return ((k / a) * t * t);
- else {
- if (t < 1.0 - b){
- return (k * (2 * t - a));
- } else {
- t = 1.0 - t;
- return (1.0 - (k / b) * t * t);
- }
- }
- }
- // ...
- time = ease(time, beginKey.easeFrom, endKey.easeTo);
- // ...
- </pre>
- <p>Подведем итог. Для того, чтобы вычислить значение какой-то величины (скажем,
- положения объекта) в какой-то момент времени, придется сделать следующее:
- <ul>
- <li>найти те два ключа, между которыми попадает этот момент времени (если
- ключ всего один, значение просто постоянно);
- <li>перевести глобальное время в локальное для отрезка между этими ключами;
- <li>скорректировать локальное время с помощью функции ease();
- <li>посчитать "выходящую" производную rb для начального ключа и "входящую"
- производную ra для конечного ключа;
- <li>по значениям величины в ключах, посчитанным производным и локальному
- времени посчитать интерполированное значение.
- </ul>
- </div>
- </td></table>
- <!-- Bottom Navigation -->
- <img src="../img/b.gif" width=500 height=1 alt=""><br><img src="../img/t.gif" width=500 height=2 alt=""><br>
- <table width=500 cellpadding=0 cellspacing=0 border=0>
- <td><img src="../img/t.gif" width=5 height=1 alt=""><a href="../main.htm" onmouseover="swap('logo2','../img/xe.gif');" onmouseout="swap('logo2','../img/e.gif');"><img src="../img/e.gif" name=logo2 width=60 height=50 hspace=10 border=0 alt=" в самое начало "></a></td>
- <td><p class=pagetitle><img src="../img/t.gif" width=265 height=1 alt=""><br>demo.design<br>3D programming FAQ</td>
- <td align=center><p class=navy><a href="75.htm" onmouseover="swap('prev','../img/xprev.gif');" onmouseout="swap('prev','../img/prev.gif');"><img src="../img/prev.gif" name=prev width=40 height=40 border=0 hspace=5 alt=" предыдущая статья "></a><br>previous</td>
- <td align=center><p class=navy><a href="../content.htm" onmouseover="swap('faq','../img/xfaq.gif');" onmouseout="swap('faq','../img/faq.gif');"><img src="../img/faq.gif" name=faq width=40 height=40 border=0 hspace=5 alt=" содержание "></a><br>content</td>
- <td align=center><p class=navy><a href="77.htm" onmouseover="swap('next','../img/xnext.gif');" onmouseout="swap('next','../img/next.gif');"><img src="../img/next.gif" name=next width=40 height=40 border=0 hspace=5 alt=" следующая статья "></a><br>next</td>
- </table>
- <img src="../img/t.gif" width=500 height=4 alt=""><br>
- <img src="../img/b.gif" width=500 height=1 alt=""><br>
- <img src="../img/t.gif" width=500 height=1 alt=""><br>
- <img src="../img/b.gif" width=500 height=1 alt=""><br>
- </center></body>
- </html>
|