Тема 15. Трехмерный мир


  1. Введение
  2. Расширение Swift 3D Express
  3. Математика
  4. Размещение клипов
  5. Многогранники
  6. Перемещение наблюдателя
  7. Текстуры
  8. Что дальше?

1. Введение

В первую очередь нужно понять, нужна ли в самом деле в вашем фильме трехмерная графика. В большинстве случаев разумный ответ — «не нужна». Если вы можете решить свою задачу без трехмерных эффектов, лучше их не использовать.

Программа Flash изначально была предназначена для двухмерной графики (рисунки на плоскости), и в ней нет специальных средств для создания трехмерных эффектов. Поэтому достаточно сложно сделать качественную трехмерную анимацию — накладывать текстуры (рисунки) на поверхности объектов, создавать детальные природные сцены.

Дело еще и в том, что построение трехмерных реалистичных картинок, таких, как в популярных играх, требует большого объема вычислений, с которыми не справляется Flash-проигрыватель.

Но кое-что все-таки сделать можно. Существует два подхода:

Первый способ приводит, как правило, к серьезному увеличению объема Flash-фильма, поскольку все рисунки должны храниться внутри него. При этом теряется главное преимущество применения Flash-анимаций в Интернете. Кроме того, анимация ограничена заранее заданным набором изображений.

Второй способ требует довольно серьезного программирования и понимания математики трехмерного мира. Кроме того, сложные вычисления сильно нагружают процессор, и поэтому в анимации рекомендуется использовать не более 30 клипов, управляемых из программы.

к началу К началу страницы

2. Расширение Swift 3D Express

В состав программы Adobe Flash CS3 входит модуль (расширение) Swift 3D Express, предназначенный для создания объемных изображений методом «выдавливания» (то есть плоской фигуре добавляется толщина). Этот модуль представляет собой упрощенную версию программы Swift 3D фирмы Electric Rain (сайт www.erain.com). Чтобы посмотреть, как используется модуль Swift 3D Express, мы построим трехмерное изображение оконной рамы, которое можно повернуть влево или вправо с помощью клавиш-стрелок.

  Создайте новый документ и сохраните его в папке PRACTICE\15 под именем okno.fla. Включите инструмент , установите прозрачную границу и бежевый цвет заливки. Отключите режим рисования объектов (нужно, чтобы кнопка в нижней части панели инструментов не была нажата). Нарисуйте прямоугольник, изображающий одну половину рамы.
  Измените цвет заливки на любой другой и нарисуйте два прямоугольника, обозначающих место для стекол. Затем, выделив эти прямоугольники, удалите их.
  Скопируйте половину рамы, перетащив ее при нажатой клавише Alt, постройте отражение для копии (Modify—Transform—Flip Horizontal) и соедините обе половинки так, чтобы заливки слились в одну.
  Выделите раму и выберите пункт меню Commands—Swift 3D Express. В левой части окна щелкните по строчке Sizing (размеры) и установите параметры Width=0.8 (ширина), Height=0.8 (высота) и Depth=2 (глубина).

Таким образом, мы немного уменьшили ширину и высоту фигуры (до 80% исходного размера), и в 2 раза увеличили глубину объема.

  В нижней части окна выберите вкладку Bevels (фаски) и перетащите вариант Beveled (прямая фаска) на раму в правой части экрана.

  В нижней части окна выберите вкладку Materials (материалы), а чуть ниже — вкладку Wood (дерево). Схватите мышкой один из образцов и перетащите его прямо на раму в правой части окна. Затем перетащите тот же образец на внутреннюю поверхность рамы.

  Сделайте активной вкладку Lighting и установите понравившийся вам вариант освещений, перетащив его на изображение рамы.
  Перейдите на вкладку Animation (анимация), далее на вкладку Common Spins (общие вращения) и перетащите на раму вариант Horizontal Right (горизонтальное вращение вправо).

Теперь стал активным блок управления анимацией под полем просмотра:

Кнопки (слева направо) предназначены для:

Справа от блока управления в поле Frames мы видим число кадров анимации (по умолчанию — 19). Это значение можно изменять. Увеличение числа кадров делает анимацию более плавной, но увеличивает объем файла.

  С помощью панели управления просмотрите анимацию в непрерывном режиме, а затем — по кадрам.
  В верхней части окна перейдите на вкладку Render and Export to Flash (построить изображение и передать во Flash). Нажмите на кнопку Vector (векторные рисунки), выберите в окне слева строчку Fill options (настройка заливки) им установите в списке Fill Type (тип заливки) вариант Area Gradient Shading.
  Щелкните на кнопку Render Frames (построить изображение фреймов) и дождитесь, когда этот процесс закончится.

  Щелкните по кнопке Create Flash Movie Clip (создать отдельный клип). Разместите клип на поле так, как вам хочется, и проверьте работу фильма.
  Выделите клип и добавьте к нему код
 onClipEvent ( load ) {
   function rotate ( x ) {
     frame = _currentframe + x;
     if ( frame < 1 ) frame += _totalframes;
     if ( frame > _totalframes ) frame -= _totalframes;
     gotoAndStop ( frame );
   }
   stop();
 }
 onClipEvent ( keyDown ){
   key = Key.getCode();
   if ( key == Key.LEFT ) rotate ( 1 );
   if ( key == Key.RIGHT ) rotate ( -1 );
 }
Проверьте работу клавиш-стрелок.
к началу К началу страницы

3. Математика

Система координат

В пространстве точка описывается тремя числами (координатами), поэтому такое пространство называется трехмерным. Существует много способов определить координаты, но чаще всего используется прямоугольная система координат, показанная на рисунке.

Оси X и Y направлены так, как в обычной декартовой системе на плоскости. Ось Z идет перпендикулярно осям X и Y, она уходит от зрителя так, что точки с положительными значениями z-координаты находятся «за» экраном (плоскостью просмотра), а точки с отрицательными z-координатами — перед экраном.

Z-координата нужна для того, чтобы

  Создайте новый документ типа ActionScript File и сохраните его в папке PRACTICE\15 под именем 3D.fla. В этот файл добавьте код функции
 function pt3D ( x, y, z ) {
   var p = {x:x, y:y, z:z};
   return p;
 }
Эта простая функция служит для создания объекта-точки с заданными координатами. Этот объект имеет свойства x, y и z. Объекты такого типа мы будем использовать для описания фигур в пространстве.

Проекция на экран

Все точки трехмерного пространства мы видим на плоском экране, в проекции на этот экран. Поэтому для построения изображения нужно ответить на вопрос: «Какие координаты на экране будет иметь точка с пространственными координатами x, y и z».

Для упрощения можно не учитывать перспективу. Это значит, что видимые размеры удаленных предметов не будут изменяться. В этом случае z-координата нужна только для того, чтобы определить порядок рисования объектов (сначала дальние, затем ближние). Все точки, которые имеют одинаковые координаты x и y, проецируются в одну и ту же точку на экране.

Для преобразования пространственных координат точки в экранные координаты нужно учесть два момента:

Чтобы «забыть» обо всем этом, можно сразу задавать координаты объектов в пикселях, а для оси Y использовать координаты с обратным знаком.

Преобразования координат

Существуют три основных типа преобразований координат: Можно показать, что при вращении вокруг оси Z на угол φ изменение координат задается формулами:
X = x·cos(φ) - y·sin(φ)
Y = x·sin(φ) + y·cos(φ)
Z = z
Если забыть про ось Z, фактически это поворот относительно начала координат на плоскости XOY. Как же повернуть точку вокруг другого центра, скажем, точки с координатами (a,b,c)? Оказывается, что очень просто. Для этого нужно сначала перенести начало координат в точку (a,b,c), затем выполнить вращение и, наконец, сделать обратный перенос. Формулы выглядят следующим образом:
X = (x-a)·cos(φ) - (y-b)·sin(φ) + a
Y = (x-a)·sin(φ) + (y-b)·cos(φ) + b
Z = z

Аналогичные формулы можно записать для вращения относительно оси X

X = x
Y = (y-b)·cos(φ) - (z-c)·sin(φ) + b
Z = (y-b)·sin(φ) + (z-c)·cos(φ) + c
и относительно оси Y:
X = (z-c)·sin(φ) + (x-a)·cos(φ) + a
Y = y
Z = (z-c)·cos(φ) - (x-a)·siz(φ) + c
  Добавьте в файл 3D.as код функции, которая выполняет поворот массива точек на заданные углы относительно трех координатных осей:
function rotate3D ( points, angles, center )
 {
   var g2R = Math.PI/180;
   var sx = Math.sin(angles.x*g2R);
   var cx = Math.cos(angles.x*g2R);
   var sy = Math.sin(angles.y*g2R);
   var cy = Math.cos(angles.y*g2R);
   var sz = Math.sin(angles.z*g2R);
   var cz = Math.cos(angles.z*g2R);
   var x,y,z, xy,xz, yx,yz, zx,zy, scale;
   var a = 0, b = 0, c = 0;
   if ( arguments.length > 2 ) {
     a = center.x;
     b = center.y;
     c = center.z;
   }
   for (i=0; i<points.length; i++) {
     x = points[i].x - a;
     y = points[i].y - b;
     z = points[i].z - c;
       // вращение вокруг оси x
     xy = cx*y - sx*z;
     xz = sx*y + cx*z;
       // вращение вокруг оси y
     yz = cy*xz - sy*x;
     yx = sy*xz + cy*x;
       // вращение вокруг оси z
     zx = cz*yx - sz*xy;
     zy = sz*yx + cz*xy;
       // сохранение новых координат
     points[i].x = zx + a;
     points[i].y = zy + b;
     points[i].z = yz + c;
   }
 }
Эта функция принимает три параметра: Функция выполняет три последовательных поворота: сначала поворот относительно оси X, затем — относительно оси Y и напоследок — относительно оси Z.

Параметр center можно не указывать, тогда вращение выполняется относительно начала координат.

В начале функции вводятся вспомогательные локальные переменные. Здесь g2R — коэффициент для перевода угла из градусов в радианы, еще 6 переменных используются для предварительного вычисления синусов и косинусов этих углов (не нужно будет повторять расчеты для каждой точки!).

В переменные a, b и c записываются координаты центра вращения. По умолчанию все они равны нулю. Оператор

if ( arguments.length > 2 ) ...
срабатывает тогда, когда количество аргументов, переданное функции, больше двух.
  К аргументам функции можно обратиться с помощью массива arguments, длина которого равна их количеству. В данном случае arguments[0] — это параметр points, arguments[1] — это angles и т.д.
Основная часть функции — цикл по всем точкам массива points. Сначала выполняется перенос начала координат в точку (a,b,c). Затем последовательно делаем повороты на заданные углы и записываем в тот же массив points измененные координаты. Сразу добавляем к полученным значениям x, y и z соответственно a, b и c — это обратный перенос начала координат в исходное положение.

Перспектива

Все знают, что объекты вдали кажутся меньше по размеру. Аналогичный эффект нужно обеспечить и при проецировании трехмерного мира на плоский экран.

Пусть объект имеет высоту h и координаты (x,y,z). Как следует из подобия треугольников на рисунке, изображение на экране будет иметь высоту

H = h·F / (F + z);
где F — расстояние от глаза до экрана, называемое фокусом. Таким образом, все размеры умножаются на коэффициент
p = F / (F + z);
Кроме того, на этот же коэффициент нужно умножить x-координату и y-координату всех точек, которые находятся на данной глубине (имеют одинаковую z-координату). Таким образом, при учете перспективы меняется не только размер, но и видимое расположение объектов на экране.

Заметим, что при уменьшении фокуса эффект перспективы будет усиливаться, при увеличении — ослабляться.

  Добавьте в конец файла 3D.as функцию, которая строит массив видимых точек с учетом перспективы:
 function perspective (points, focalLength) {
   var pt2D = [];
   var i, x, y, z, scale;
   for (i=0; i<points.length; i++) {
     scale = focalLength/(focalLength + points[i].z);
     x = points[i].x*scale;
     y = points[i].y*scale;
     z = points[i].z;
     pt2D[i] = {x:x, y:y, z:z, scale: scale, id: i };
   }
   return pt2D;
 };
Сохраните файл.
Функция принимает два параметра: Здесь, в отличие от функции вращения, строится новый массив точек, а не изменяется старый. Элемент нового массива имеет не только поля x, y и z (видимые координаты), но и еще два дополнительных поля: Эти данные будут использоваться в дальнейших примерах.

Таким образом, мы создали файл, содержащий все необходимые нам функции для создания и преобразования координат точек в пространстве.

к началу К началу страницы

4. Размещение клипов

В этом примере мы построим модель из 8 шариков, расположенных в углах куба. Шарики вращаются относительно осей X и Y при нажатии на клавиши-стрелки.

Здесь каждый шарик — это клип (символ типа MovieClip), а в программе только меняются координаты и размер этих клипов (с учетом перспективы).

Определение положения шариков

  Откройте файл PRACTICE\15\ball.fla и добавьте к кадру 1 код
 #include "3d.as"
 focalLength = 300;
 this.createEmptyMovieClip ( "scene", 1 );
 scene._x = 275;
 scene._y = 200;
 vert = [pt3D(50,  50, -50),  pt3D(50, -50, -50),
         pt3D(-50, -50, -50), pt3D(-50, 50, -50),
         pt3D(50, 50, 50),    pt3D(50, -50, 50),
         pt3D(-50, -50, 50),  pt3D(-50, 50, 50) ];
В первой строчке мы подключаем файл 3D.as, который содержит все функции для работы с точками в трехмерном пространстве. Переменная focalLength обозначает величину фокуса для расчета перспективы.

Для рисования создается отдельный пустой клип с именем scene, в котором будут размещаться все клипы-шарики. Он помещается в центр экрана.

Массив vert задает координаты вершин куба (от английского vertex — вершина). Для создания объекта-точки используется функция pt3D, которая находится в файле 3D.as.

Первые 4 точки описывают ближнюю к нам грань (она имеет отрицательную z-координату), а последние 4 точки — дальнюю.

Создание клипов

  Добавьте к кадру 1 код создания клипов-шариков:
 for (i=0; i<vert.length; i++) {
   name = "ball" + i;
   scene.attachMovie ( "ball", name, i );
   if ( i > 3 ) {
     var c = new Color ( scene[name].back );
     c.setRGB ( 0x9900 );
   }
 }
В цикле рассматриваем все вершины. Для каждой из них создается новый клип типа Шарик (из библиотеки). Этот клип имеет кодовое имя ball, которое используется при вызове метода attachMovie. «Родителем» всех клипов будет пустой клип scene, созданный в самом начале.

Имена клипов строятся в переменной name следующим образом: к слову ball добавляется номер точки в массиве vert. Клип с порядковым номером i пока располагается на глубине i, потом мы поместим клипы на разные глубины в соответствии с их z-координатами.

Для наглядности цвет «дальних» шариков (для которых i>3) изменяется на зеленый. Это легко сделать, потому что цвет шарика задается цветом его внутреннего клипа back. Поэтому мы создаем для него новый объект Color и изменяем его цвет с помощью метода setRGB.

Z-сортировка

Для того, чтобы правильно расположить шарики на экране, нужно Первая задача решается с помощью функции perspective, которая хранится в файле 3D.fla.

Для выполнения второй мы должны выполнить так называемую Z-сортировку, то есть расставить в массиве точки в порядке их приближения, от самой дальней к самой ближней, по убыванию z-координаты. Это можно сделать с помощью метода sortOn, который сортирует массивы структур по заданному полю.

  Добавьте в код кадра 1 строчки
 showFigure();
 function showFigure() {
   var i, obj;
   ptScreen = perspective ( vert, focalLength );
   ptScreen.sortOn ( "z", Array.DESCENDING + Array.NUMERIC );
   for (i=0; i < ptScreen.length; i++){
     obj = scene["ball"+ptScreen[i].id];
     obj._x = ptScreen[i].x;
     obj._y = ptScreen[i].y;
     obj._xscale = obj._yscale = 100 * ptScreen[i].scale;
     obj.swapDepths(i+1);
   }
 }
В первой строчке вызывается функция showFigure, которая написана далее — это обеспечивает начальную расстановку шариков.

С помощью функции perspective (из файла 3D.as) строится массив ptScreen, содержащий экранные координаты точек. Кроме того, каждый его элемент содержит два дополнительных поля: scale — (масштаб) и id (номер точки в исходном массиве).

В строчке

ptScreen.sortOn ( "z", Array.DESCENDING + Array.NUMERIC);
вызывается метод sortOn, который сортирует массив ptScreen по полю z. Во втором параметре задаются два дополнительных режима После сортировки выполняется цикл по всем (отсортированным!) точкам массива ptScreen. В переменную obj записывается адрес клипа, который строится с учетом поля id. Далее для этого клипа изменяются координаты и масштаб (умножением на 100 масштаб переводится в проценты).
  В принципе здесь можно было использовать и такой вариант сортировки
 ind = ptScreen.sortOn ( "z", Array.DESCENDING + Array.NUMERIC
                           +  Array.RETURNINDEXEDARRAY);
Параметр Array.RETURNINDEXEDARRAY говорит, что метод возвращает результат — массив индексов после сортировки ind, а сам массив ptScreen остается неизменным. Однако в данном случае этот вариант не очень удобен.

В строчке

obj.swapDepths ( i+1 );
определяются уровни шариков. Метод swapDepths (поменять глубины), вызванный таким образом, переводит объект obj на глубину i+1. При этом объект, который был расположен на глубине i+1, переводится на ту глубину, где был объект obj (они меняются глубинами). Это значит, что точки, расположенные раньше в отсортированном массиве (имеющие большую z-координату) будут иметь меньшую глубину, то есть будут «ниже» следующих за ними точек.
  Проверьте работу фильма, нажав клавиши Ctrl+Enter. Вы должны увидеть начальную расстановку шариков.

Управление клавишами

Остается сделать так, чтобы при нажатии на клавиши-стрелки шарики поворачивались относительно центра куба в нужную сторону.
  Добавьте к кадру 1 код
 Key.addListener ( scene );
 scene.onKeyDown = function () {
   var ax = 0, ay = 0, az = 0;
   switch ( Key.getCode() ) {
     case Key.LEFT:  ay = -5; break;
     case Key.RIGHT: ay = 5; break;
     case Key.UP:    ax = -5; break;
     case Key.DOWN:  ax = 5; break;
   }
   rotate3D ( vert, pt3D(ax, ay, az) );
   showFigure();
 }
В первой строчке клип scene становится слушателем глобального объекта Key, то есть будет получать события от клавиатуры.

Далее задается обработчик события keyDown (нажатие клавиши). Углы поворота относительно осей X, Y и Z записываются в переменные ax, ay и az соответственно. Нужный поворот выбирается в зависимости от кода нажатой клавиши, который определяется с помощью метода Key.getCode(). Затем вызывается функция rotate3D, определенная в файле 3D.as, и функция showFigure, которая изменяет рисунок на экране.

к началу К началу страницы

5. Многогранники

В следующем примере мы сделаем полупрозрачную пирамидку, которая вращается с помощью клавиш-стрелок.

  Создайте новый документ и сохраните его в папке PRACTICE\15 под именем pyramid.fla. Добавьте в кадру 1 код
 #include "3d.as"
 focalLength = 400;
 this.createEmptyMovieClip ( "scene", 1 );
 scene._x = 275;
 scene._y = 200;

Вершины

Теперь создадим массив vert, содержащий координаты всех вершин пирамиды. На рисунке показана пирамида и две ее проекции (вид сверху и вид сбоку), вершины обозначены номерами в кружках (нумерация с нуля!).

В основании пирамиды лежит равносторонний треугольник, все стороны которого равны a, а все углы — 60°. С помощью простых вычислений можно показать, что его высота определяется формулой

h = a·sqrt(2/3)
где sqrt() обозначает квадратный корень. Мы расположим начало координат так, чтобы Заметьте, что мы уже учли, что ось Y направлена вниз, и y-координата вершины отрицательна, тогда как y-координаты углов основания положительны. Таким образом, получаем следующие координаты вершин:
вершина 0: x=a/2,   y=h/3,  z= a·sqrt(3)/6
вершина 1: x=0,     y=h/3,  z=-a·sqrt(3)/3
вершина 2: x=a/2,   y=h/3,  z= a·sqrt(3)/6
вершина 3: x=a/2,   y=-2·h/3,  z=0
Они записываются в массив vert с помощью функции pt3D.
  Добавьте к кадру 1 код
 a = 200;
 sq3 = Math.sqrt(3);
 h = a*Math.sqrt(2/3);
 b = h/3;
 t = 2*h/3;
 vert = [ pt3D(a/2,   b,  a*sq3/6),
          pt3D(0,     b, -a*sq3/3),
          pt3D(-a/2,  b,  a*sq3/6),
          pt3D(0,    -t,        0) ];
Переменная sq3 здесь введена для того, чтобы не вычислять несколько раз квадратный корень из трех.

Грани

В этой задаче нужно не просто менять координаты точек, но и рисовать грани — треугольники, формирующие стороны пирамиды. Чтобы определить грань, мы просто перечислим номера вершин, через которые она проходит.
  Добавьте к кадру 1 код
 faucet = [ [0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3] ];
 color = [ 0xFFFFFF, 0xFF0000, 0xFF00, 0xFF ];
 showFigure();
Массив faucet состоит из четырех массивов (у нас 4 грани!). В каждом из этих внутренних массивов перечислены номера вершины, например, грань 0 проходит через вершины 0, 1 и 2.

В массив color записаны цвета каждой из граней, вы можете выбрать их на свой вкус. Здесь грань 0 — белого цвета, грань 1 — красного, грань 2 — зеленого и грань 3 — синего.

Функция showFigure, которая вызывается в последней строчке нового блока, рисует фигуру. При этом важно сначала нарисовать удаленные грани, а потом уже ближние, то есть нужна Z-сортировка. Мы уже использовали Z-сортировку для точек, теперь нужно применить ее для граней, с первого взгляда эта задача нетривиальна. Однако можно свести решение к уже известному результату — применить Z-сортировку для центральных точек граней, координаты которых находятся как среднее арифметическое координат ее вершин.

  Лобавьте к кадру 1 код функции
 function showFigure() {
   var i, midScreen;
   ptScreen = perspective(vert, focalLength);
   midScreen = midPointsZ();
   midScreen.sortOn ( "z", Array.DESCENDING + Array.NUMERIC );
   scene.clear();
   for (i=0; i<midScreen.length; i++)
     drawFaucet ( midScreen[i].id );
 }
Сначала координаты вершин из массива vert преобразуются в экранные «видимые» координаты с учетом перспективы. Это делает функция perspective, которую мы записали в файл 3D.as.

Далее в строчке

midScreen = midPointsZ();
строится массив, каждый элемент которого содержит два поля: zz-координата центральной точки (остальные ее координаты вообще не нужны), и id — номер грани в исходном массиве (для того, чтобы найти ее после Z-сортировки).

С помощью метода sortOn центральные точки сортируются по убыванию z-координаты (от дальних к ближним) и в цикле строятся все грани. Функцию drawFaucet, которая выполняет рисование грани с заданным номером, мы напишем далее.

  Добавьте код функции
 function midPointsZ() {
   var i, z;
   var mid = new Array();
   for(i=0; i<faucet.length; i++) {
     z = (ptScreen[faucet[i][0]].z +
          ptScreen[faucet[i][1]].z +
          ptScreen[faucet[i][2]].z) / 3;
     mid[i] = {id:i, z:z};
   }
   return mid;
 }
Эта функция предельно проста, она считает для каждой грани z-координату центральной точки как среднее арифметическое z-координат ее вершин и возвращает новый массив.

Рисование

  Добавьте код функции для построения грани
 function drawFaucet ( id ) {
   var i1 = faucet[id][0];
   var i2 = faucet[id][1];
   var i3 = faucet[id][2];
   with ( scene ) {
     beginFill( color[id],  60);
     moveTo(ptScreen[i1].x, ptScreen[i1].y);
     lineTo(ptScreen[i2].x, ptScreen[i2].y);
     lineTo(ptScreen[i3].x, ptScreen[i3].y);
     lineTo(ptScreen[i1].x, ptScreen[i1].y);
     endFill();
   }
 }
Параметр функции id — это номер нужной грани. Сначала номера точек «вытаскиваются» из массива faucet и записываются в переменные i1, i2 и i3. Затем на клипе scene программным методом рисуется грань (заливка).

Обратите внимание, что обход контура выполняется в том же порядке, в котором перечислены точки в массиве faucet. Для треугольных граней это неважно, но для более сложных (например, четырехугольников) неправильный порядок точек приводит к неправильному изображению.

Стиль заливки определяется строчкой

beginFill( color[id],  60);
Это значит, что из массива color берем цвет нужной грани, и устанавливаем прозрачность 60%.

Клавиатура

Теперь остается добавить обработчик нажатия клавиши, позволяющий вращать фигуру вокруг осейх X и Y. Он точно такой же, как и в предыдущем примере.
  Добавьте к кадру 1 код
 Key.addListener(scene);
 scene.onKeyDown = function() {
   ax = ay = az = 0;
   switch ( Key.getCode() ) {
     case Key.LEFT:  ay = -5; break;
     case Key.RIGHT: ay =  5; break;
     case Key.UP:    ax = -5; break;
     case Key.DOWN:  ax =  5; break;
   }
   rotate3D ( vert, pt3D(ax, ay, az) );
   showFigure();
 }
Запустите ролик и просмотрите результат.

Зачетное задание

  Создайте новый документ и постройте модель куба, управляемого стрелками. Помните, что
  • куб имеет 8 вершин и 6 граней
  • в массиве faucet номера вершин для каждой грани должны перечисляться в порядке их обхода при построении грани.

к началу К началу страницы

6. Перемещение наблюдателя

В предыдущих примерах наблюдатель стоял на месте, а все объекты располагались прямо перед ним, из всех движений мы использовали только вращение вокруг центра объекта. Теперь рассмотрим более сложный случай: Ниже показан пример такой анимации, управляемой клавишами-стрелками. Клавиши «вверх» и «вниз» перемещают наблюдателя вперед и назад, клавиши «влево» и «вправо» поворачивают его в соответствующем направлении. Если удерживать клавишу Shift, клавиши «вверх» и «вниз» изменяют высоту глаз наблюдателя.

Библиотека для работы с фигурами

Для того, чтобы построить такой фильм, мы сначала создадим небольшую библиотеку функций для работы с трехмерной графикой.

Для описания фигуры будем использовать структуру, показанную на рисунке.

Она содержит три поля (свойства):

Как видим, это те же данные, которые использовались для описания пирамиды в предыдущем разделе, только они объединены в одну структуру.

Сначала напишем функцию moveBy, которая перемещает фигуру на вектор (a,b,c). Это значит, что к x-координатам всех вершин нужно добавить a, к y-координатам добавить b и к z-координатам добавить c. При этом начало координат перемещается в точку (a,b,c).

Для такого перемещения нужно сделать цикл по массиву вершин vert и для каждой из них выполнить код:

vert[i].x += a; vert[i].y += b; vert[i].z += c;
Для удобства три координаты вектора перемещения записываются в одну структуру, которая может быть построена с помощью функции pt3D (ее код находится в файле 3D.as).
  Создайте новый документ типа ActionScript File, запишите в него код функции перемещения:
 function moveBy ( fig, way ) {
   var i;
   for (i=0; i<fig.vert.length; i++) {
     with ( fig.vert[i] ) {
       x += way.x; y += way.y; z += way.z;
     }
   }
 }
Сохраните этот файл под именем figures3D.as в папке PRACTICE\15.
У этой функции два параметра — ссылка на фигуру (вернее, на область, где хранятся ее данные), и вектор перемещения way.

Далее создадим функцию, которая строит прямоугольный параллелепипед (блок, «кирпич»). Так можно, например, моделировать дома.

Эта функция должна принимать в параметрах размеры блока, массив из 6 элементов, задающий цвета граней, и начальную точку. В качестве точки привязки выберем левый нижний угол ближней к нам грани. Если начальное положение не задано, эта точка помещается в начало координат.

  Добавьте в файл figures3D.fla код функции
 function brick ( dx, dy, dz, color, place ) {
   var a = 0, b = 0, c = 0;
   if ( arguments.length > 4 ) {
     a = place.x; b = place.y; c = place.z;
   }
   var rect = new Object();
   rect.vert = [ pt3D(0,   0, dz),  pt3D(0, -dy, dz),
                 pt3D(dx, -dy, dz), pt3D(dx,   0, dz),
                 pt3D(0,   0, 0),   pt3D(0, -dy, 0),
                 pt3D(dx, -dy, 0),  pt3D(dx,   0, 0) ];
   rect.faucet = [ [0, 1, 2, 3], [2, 3, 7, 6],
                   [5, 6, 7, 4], [1, 5, 4, 0],
                   [1, 2, 6, 5], [0, 3, 7, 4] ];
   rect.color = color;
   moveBy ( rect, pt3D(a,b,c) );
   return rect;
 }
В функции создается новый объект rect, имеющий поля vert, faucet и color. Параметры dx, dy и dz задают его размеры, массив color — цвета граней, а дополнительный параметр place — это координаты точки, куда нужно переместить точку привязки из начала координат.

Массив точек и вершин строится так же, как и ранее, для перемещения используется только что написанная функция moveBy.

  Обратите внимание, что в приведенном варианте функции brick массив color не копируется, а просто в поле rect.color записывается ссылка на него (адрес). Это означает следующее: если мы создаем два блока, используя один и тот же массив color, и потом изменим этот массив, то изменятся цвета обоих блоков. Чтобы этого не произошло, можно создавать массив заново и копировать значения, переданные через параметр:
 rect.color = new Array();
 for ( i=0; i<color.length; i++)
   rect.color[i] = color[i];

В этой программе мы будем работать с несколькими объектами, которые могут перекрывать друг друга. Чтобы учитывать это, нужно собрать все грани вместе, в одном объекте, и применить единую Z-сортировку. Следующая функция добавляет к объекту-фигуре fig новую фигуру newFig того же типа, которая передается как второй параметр. Фактически при этом сливаются их массивы vert, faucet и color, в результате строится один объект, описывающий все грани обоих исходных фигур.

  Для слияния массивов используется метод concat (concatenation — соединение, сцепка). Например, при выполнении кода
 a = [1, 2, 3];
 b = [4, 5];
 c = a.concat ( b );
создается новый массив c с элементами [1,2,3,4,5].
  Добавьте в файл figures3D.fla код функции
 function addFigure ( fig, newFig ) {
   var nPrev = fig.vert.length;
   var i, k, n;
   fig.vert = fig.vert.concat ( newFig.vert );
   for(i=0; i<newFig.faucet.length; i++) {
     n = newFig.faucet[i].length;
     for(k=0; k<n; k++)
       newFig.faucet[i][k] += nPrev;
   }
   fig.faucet = fig.faucet.concat ( newFig.faucet );
   fig.color = fig.color.concat ( newFig.color );
 }
При слиянии массивов важно не забыть преобразовать массив faucet, который описывает грани второй (присоединяемой) фигуры. В нем до слияния указаны номера вершин именно для второй фигуры, а после слияния там должны быть номера тех же вершин, но в общей нумерации.

Например, если первая фигура включала 5 вершин, номера вершин второй фигуры в общем списке будут начинаться с 5. Следовательно, перед слиянием с массиве faucet фигуры newFig нужно увеличить номера всех вершин на 5. Вот как это сделано в функции:

var nPrev = fig.vert.length;
for(i=0; i<newFig.faucet.length; i++) {
  n = newFig.faucet[i].length;
  for(k=0; k<n; k++)
    newFig.faucet[i][k] += nPrev;
}
Следующая функция, showFigures, предназначена для рисования всех граней в нужном порядке.
  Добавьте в файл figures3D.fla код функции
 function showFigures ( fig ) {
   var i, midScreen;
   ptScreen = perspective(fig.vert, focalLength);
   midScreen = midPointsZ ( fig );
   midScreen.sortOn ( "z", Array.DESCENDING + Array.NUMERIC );
   scene.clear();
   for (i=0; i<midScreen.length; i++)
     drawFaucet ( fig, midScreen[i] );
 }
Сначала вызывается функция perspective (из файла 3D.as), которая рассчитывает эффект перспективы для заданного фокуса. Затем с помощью функции midPointZ (ее еще надо написать) для всех граней вычисляется z-координата средней точки, по которой выполняется сортировка. Наконец в цикле для каждой грани вызывается функция рисования drawFaucet (см. далее).
  Добавьте в файл figures3D.fla код функции
 function midPointsZ ( fig ) {
   var i, j, k, nPoints, sumZ;
   var mid = new Array();
   for(i=0; i<fig.faucet.length; i++) {
     sumZ = 0;
     nPoints = fig.faucet[i].length;
     for(j=0; j<nPoints; j++) {
        k = fig.faucet[i][j];
        sumZ += ptScreen[k].z;
     );
   }
   sumZ /= nPoints;
   mid[i] = {id:i, z:sumZ};
   }
   return mid;
 }
В сравнении с предыдущим вариантом (см. пример с пирамидой) функция midPointsZ работает для любого числа вершин (а не только для трех). Единственное, что требуется — вершины в массиве faucet должны быть перечислены в порядке их обхода при построении контура.

Новая версия функции drawFaucet также работает для любого числа вершин, они перебираются в цикле.

  Добавьте в файл figures3D.fla код функции
 function drawFaucet ( fig, midPoint ) {
   if ( midPoint.z < -focalLength ) return;
   var id = midPoint.id;
   with ( scene ) {
     beginFill( fig.color[id],  100);
     var k0 = fig.faucet[id][0];
     moveTo(ptScreen[k0].x, ptScreen[k0].y);
     for (var i=1; i<fig.faucet[id].length; i++) {
       var k = fig.faucet[id][i];
       lineTo(ptScreen[k].x, ptScreen[k].y);
     }
     lineTo(ptScreen[k0].x, ptScreen[k0].y);
     endFill();
   }
 }
Сохраните окончательный вариант библиотеки функций.
В начале этой функции добавлена строчка
if ( midPoint.z < -focalLength ) return;
Мы считаем, что наблюдатель находится в точке с координатами (0,0,-focalLength), поэтому все грани, оказавшиеся позади него (имеющие меньшую z-координату, чем -focalLength) не прорисовываются, поскольку происходит выход из функции по оператору return.

Код Flash-документа

Теперь мы построим сцену с двумя брусками разной высоты и будем изменять положение наблюдателя.
  Создайте новый Flash-документ и сохраните его в папке PRACTICE\15 под именем view.fla. Нарисуйте прямоугольник, занимающий всю сцену, и залейте его градиентом от синего к черному, как на образце.
Поскольку мы будем использовать функции, записанные в файлах 3D.as и figures3D.as, их надо включить в код с помощью директивы #include. Далее создается пустой клип, в котором будем рисовать объекты на сцене. Этот клип помещается в центр окна.
  Добавьте к кадру 1 код
 #include "3d.as"
 #include "figures3d.as"
 focalLength = 800;
 this.createEmptyMovieClip ( "scene", 1 );
 scene._x = 275;
 scene._y = 200;
Теперь нужно создать две фигуры и объединить их в одну.
  Добавьте к кадру 1 код
 color = [ 0xFFFFFF, 0xFF00, 0xFFFF00, 0xFF, 0xFF9900, 0xFF0000 ];
 figAll = brick ( 100, 100, 100, color, pt3D(100,100,0) );
 addFigure ( figAll,
          brick ( 100, 200, 100, color, pt3D(-200,100,0) ) );
 showFigures ( figAll );
Первый блок имеет размеры 100×100&tmes;100, его точка привязки — (100,100,0). У второго высота равна 200 и точка привязки (-200,100,0).

Информация о вершинах и гранях обоих объектов записывается в структуру figAll, которая выводится на экран с помощью функции showFigures.

  Запустите ролик и посмотрите на начальное положение фигур. Проверьте, верно ли рисуются грани у объекта слева.
Наверняка вы заметили, что верхняя и нижняя грани левого блока отрисованы после боковой (зеленой) грани, и это неверно. Дело в том, что 4 из 6 граней этой фигуры имеют одинаковую z-координату, и при сортировке программа может расположить их в любом порядке.
  Стандартная сортировка по z-координате центра грани работает только тогда, когда наблюдатель находится прямо перед объектом.
Чтобы исправить ситуацию, нужно считать координату для сортировки как-то иначе. Самый простой способ — взять среднее расстояние между вершинами грани и наблюдателем, то есть точкой (0,0,-focalLength). Для точки (x,y,z) это расстояние равно

Здесь через F обозначен фокус, в программе это переменная focalLength.

  Откройте файл figures3D.as и измените алгоритм вычисления z-координаты, по которой выполняется сортировка:
 sumZ += Math.sqrt(
         ptScreen[k].x*ptScreen[k].x +
         ptScreen[k].y*ptScreen[k].y +
         (ptScreen[k].z+focalLength)*(ptScreen[k].z+focalLength)
         );
Проверьте результат.
Остается сделать обработчик нажатия на клавиши, в котором и выполняется изменение сцены. Очевидно, что перемещение наблюдателя относительно объектов можно представить как изменение положения объектов по отношению к наблюдателю.

При движении вперед и назад объекты становятся ближе или дальше, значит, изменяется z-координата. Если двигаемся вверх или вниз (при нажатой клавише Shift), для всех точек изменяется y-координата. А при поворотах все точки сцены вращаются относительно наблюдателя, то есть относительно точки (0,0,-focallength). Все это легко реализовать с помощью функций moveBy (перемещение) и rotate3D (вращение).

  Добавьте к коду кадра 1 обработчик события enterFrame для клипа scene:
 Key.addListener ( scene );
 scene.onEnterFrame = function() {
   var dx=0, dy=0, dz=0;
   var ax=0, ay=0, az=0;
   if ( Key.isDown(Key.LEFT) )  ay = 1; break;
   if ( Key.isDown(Key.RIGHT) ) ay = -1; break;
   if ( Key.isDown(Key.UP) )
     if ( Key.isDown(Key.SHIFT) )
          dy = 10;
     else dz = -10;
   if ( Key.isDown(Key.DOWN) )
     if ( Key.isDown(Key.SHIFT) )
          dy = -10;
     else dz = 10;
   moveBy ( figAll, pt3D(dx, dy, dz) );
   rotate3D ( figAll.vert, pt3D(ax,ay,az), pt3D(0,0,-focalLength) );
   showFigures ( figAll );
 }
Сохраните файл и проверьте работу ролика.
Здесь переменные dx, dy и dz обозначают перемещение по координатным осям, тогда как ax, ay и az — углы поворота относительно этих осей.
к началу К началу страницы

7. Текстуры

Введение

Текстуры — это растровые рисунки, которые накладываются на грань объемной фигуры.

В этом разделе мы будем использовать параллельную проекцию (без учета перспективных искажений, которые существенно осложняют дело).

В этом примере текстуры используются на каждой грани куба, его можно вращать в разные стороны мышкой и клавишами-стрелками.

Щелкнув по кнопке с изображением маленького кубика можно установить его в начальное положение, которое соответствует особой изометрической проекции, которая не искажает пропорции. Длины всех ребер куба здесь одинаковы, угол между ребрами равен 120°.

Для построения изометрии кубик нужно сначала повернуть на 45° относительно оси Y, а затем — на 30° относительно оси X:

rotate3D ( vert, pt3D(0, 45, 0) );
rotate3D ( vert, pt3D(30, 0, 0) );

Вращение грани с текстурой

Чтобы построить параллельную проекцию грани с текстурой, нужно прямоугольный рисунок растянуть на параллелограмм, вершины которого рассчитываются стандартным методом.

К сожалению Flash пока не имеет встроенных функций для работы с текстурами, поэтому придется писать свои. Один из способов наложения текстуры использует вложенный клип. Представьте себе клип clip, внутри которого находится внутренний клип с именем inner — прямоугольник, повернутый на 45° против часовой стрелки. У главного клипа мы будем изменять свойства _yscale (масштаб по оси y) и _rotation (угол поворота), а у вложенного — только масштабы по обеим осям. Важно что точка регистрации (точка (0,0)) у главного и вложенного клипов совпадают с точкой p0 на рисунке.

Попробуйте с помощью кнопок-стрелок изменять форму прямоугольника.

Важно заметить что:

Проверим это на примере. Вы можете установить красную, синюю и зеленую точки там, где вам нравится, и нажать кнопку со стрелкой. Разноцветный прямоугольник будет деформирован так, что красный угол окажется под красной точкой, синий — под синей и зеленый — точно под зеленой.

Математика

Теперь настало время попробовать разобраться в математике такого преобразования. Слева на схеме показан желаемый параллелограмм, а справа — исходный прямоугольник, повернутый на 45°.

Здесь через p0 обозначена точка регистрации клипов, а через pH и pW — угловые точки, определяющие высоту и ширину параллелограмма. Тангенсы углов α1 и α2 могут быть найдены из формул

а сами углы — с помощью функции Math.atan2. Здесь через Δx обозначена разность x-координат точек, а через Δy — разность их y-координат.

Тогда внутренний угол параллелограмма, обозначенный как 2α, вычисляется как разность

2·α = 180° - α1 - α2
Этот угол нужно получить масштабированием исходного прямоугольника (на рисунке справа) только по оси Y. Обозначим коэффициент масштаба через k, он равен масштабному коэффициенту, который нужно применить для главного клипа.

При y-масштабировании все вертикальные размеры умножаются на k, а горизонтальные — сохраняются, поэтому имеем

Теперь найдем масштабные коэффициенты kx и ky для вложенного клипа. Обозначим через w' длину ребра, соединяющего точки p0 и pW, а через w0 — нужную ширину внутреннего клипа-прямоугольника. Учитывая масштабирование главного клипа, имеем

Аналогичные выражения можно записать и для высоты:

  Создайте новый файл типа ActionScript File, запишите в него код функции сдвига
 function skewClip ( clip, p0, pW, pH, w, h ) {
   function dX ( p1, p2 ) { return p1.x - p2.x; }
   function dY ( p1, p2 ) { return p1.y - p2.y; }
   function dist ( p1, p2 ) {
     var dx = dX(p1,p2), dy = dY(p1,p2);
     return Math.sqrt(dx*dx+dy*dy);
   }
   var a, a1, a2, da, s, scale;
   clip._x = p0.x;
   clip._y = p0.y;
   clip._xscale = 100;
   clip._rotation = 0;
   a1 = Math.atan2(dX(pW,p0), dY(p0,pW));
   a2 = Math.atan2(dX(pH,p0), dY(pH,p0));
   a = (Math.PI - a1 - a2) / 2;
   scale = Math.tan(a);
   clip._yscale = 100*scale;
   da = Math.PI/2 - a - a1;
   clip._rotation -= da*180/Math.PI;
   s = dist(p0,pW);
   q = Math.sin(a)/scale*Math.sqrt(2);
   clip.inner._xscale = 100*s*q/w;
   s = dist(p0,pH);
   clip.inner._yscale = 100*s*q/h;
 }
Сохраните файл в папке PRACTICE\15 под именем skew.as.
Эта функция в точности реализует описанный выше алгоритм. В ней введены внутренние функции dX, dY и dist, которые вычисляют соответственно разность x-координат, разность y-координат и расстояние между двумя точками.

Пример

Сначала мы построим пример, в котором вращается одна грань с наложенной текстурой (наведите мышку на рисунок ниже).

  Откройте файл texture.fla из папки PRACTICE\15 и добавьте к кадру 1 код
 #include "3d.as"
 #include "skew.as"
 this.createEmptyMovieClip ( "scene", 1 );
 scene._x = 275;
 scene._y = 200;
Этот код стандартный и не требует пояснений.
  Добавьте к кадру 1 код
 fig = new Object();
 fig.vert = [ pt3D(-50,50,0), pt3D(-50,-50,0),
              pt3D(50,-50,0), pt3D(50,50,0) ];
 fig.faucet = [ [0, 1, 2, 3] ];
 fig.bitmap = [ 1 ];
Здесь создается объект, имеющий поля vert (вершины), faucet (грани) и bitmap (рисунки). В данном случае 4 вершины, одна грань для которой выбран рисунок с номером 1.

Важно, что в массиве faucet первый номер вершины соответствует точке pH, второй — точке p0, а третий — точке pW (см. рисунок выше). Этот порядок будет использоваться при наложении текстуры.

Все рисунки записаны в библиотеку в виде клипов pic1, pic2, ..., pic6.

  Присвойте клипам-рисункам pic1...pic6 такие же кодовые имена для использования в программе (правая кнопка мыши, пункт Linkage в контекстном меню).
  Добавьте к кадру 1 код
 scene.createEmptyMovieClip ( "faucet0", 1 );
 scene.faucet0.attachMovie ( "pic"+fig.bitmap[0], "inner", 1);
 scene.faucet0.inner._rotation = -45;
Внутри клипа scene создается клип faucet0, представляющий грань. Символ с текстурой присоединяется к нему как внутренний клип inner. Имя клипа в библиотеке строится из строки "pic" и номера рисунка для данной грани, который хранится в массиве bitmap. Внутренний клип сразу поворачивается на 45° против часовой стрелки.
  Добавьте к кадру 1 код
 w0 = 100;
 h0 = 100;
 rotate3D ( fig.vert, pt3D(0,45,45) );
 showFigureBmp(fig);
 function showFigureBmp ( fig ) {
   var v = fig.faucet[0];
   var pH = fig.vert[v[0]];
   var p0 = fig.vert[v[1]];
   var pW = fig.vert[v[2]];
   skewClip ( scene.faucet0, p0, pW, pH, w0, h0 );
 }
Переменные w0 и h0 задают ширину и высоту рисунков. Грань сразу поворачивается на 45° по осям Y и Z и выводится с помощью функции showFigureBmp. Работа этой функции сводится к определению нужных вершин и вызову функции skewClip.
  Добавьте к кадру 1 код обработчика
 scene.onEnterFrame = function () {
   rotate3D ( fig.vert, pt3D(2,0,2) );
   showFigureBmp(fig);
 }
Сохраните файл и проверьте работу клипа
При смене кадра фигура поворачивается на 2° по осям X и Z.

Возникает вопрос: «Как наложить текстуру на фигуру другой формы (треугольник, трапецию и т.п.). Простейший способ — использовать маску, то есть накладывать текстуру на прямоугольник, но ненужную часть отсекать с помощью маски.

Зачетное задание

  Сохраните файл под именем cube_texture.fla и доработайте его так, чтобы на экране получился вращающийся куб, все 6 граней которого залиты текстурами.
При выполнении этой работы нужно учитывать, что видимость граней теперь определяется не порядком рисования, а глубиной каждой грани-клипа. Поэтому после Z-сортировки нужно использовать метод swapDepths, который переводит клип на нужный уровень.
к началу К началу страницы

8. Что дальше?

к началу К началу страницы


Оглавление
 Рисование из программы Назад В начало Вперед ...


© 2007  К. Поляков


Сайт создан в системе uCoz