Эта статья ориентирована на людей знакомых с математикой, и имеющих представление о программировани во Flash ActionScript. Целью же статьи является донесение принципов реализации 3D во Flash. Надеюсь что после прочтения этой стаьи Вы научитесь всем аспектам работы с 3D (и не только) во Flash. А именно:
Проектировать точки, линии, плоскости трехмерного пространства методами ActionScript
Научиться использовать функции рисования ActionScript. Работать с цветом.
Создавать инструменты управления пользователем состояния трехмерной сцены. (вращение трехмерной сцены мышкой.)
Научится управлять временем вычислений Flash (перестать опасаться ограничений времени воплнения кода за один KeyFrame)
Получить навык в построении сплайнов как в 2D, так и в 3D
Понять разницу о 3D "RealTime" и "не RealTime"
Опережая события, отвечу на первый же вопрос который может возникнуть после прочтения статьи: "А зачем все это надо, раз это так сложно?". Отвечаю словами моего старого приятеля: "for fun" :).
Лучше один раз увидеть, чем 100 раз услышать.
Изучать 3D будем на конкретном примере. В качестве примера поставим сами себе следующую задачу - создать интсрумент по созданию и просмотру трехмерного ландшафта. Кому то, быть может, это даже пригодиться :)
Правильно поставленная задача - наполовину решенная задача.
Постановка задачи.
Пусть ландшафт будет организован в виде трехмерного сплайна натянутого на сетку точек, расположение которых жестко задается по осям X и Y, но случайно выбирается по оси Z (в каких-то пределах, конечно). От интсрумента будет требоваться: 1) Строить ландшафт 2) Выводить на экран подсвеченным цветами 3) Вращать ладшафт с помощью мыши 4) Дать возможность по изменению настроек.
Вот в таких рамках мы и начнем работать над этой задачей. Важно отметить, что задачи оптимизации кода ставить не будем, так что последующая работа не притендует на звание оптимального решения!!! Кроме того в данном примере мы не будем использовать ООП, с одной простой целью - дать потом вам возможность заняться оптимизацией кода, если возникнет желание.
Первый шаг всегда трудный
Как организовать сетку? Да очень просто - создаем массив pnt, элементами которого будут другие массивы, таким образом получим двумерный массив, его элемтами будут объекты (Object) содержащую всю необходимую информацию о точках пространства. Ниже приведен код для создания опорных точек сетки.
Этот скрипт создает сетку точек n на n, из которых только grid * grid точкек определяются по высоте значениями в диапозоне от нуля до peak-1, остальные пока заданы нулями.
На рисунке ниже изображено, что же данный код генерит. Зелеными кружками отмечены точки которые будут опорными для нашей сетки. Остальные находятся на плоскости черной сетки и не отмечены ничем на рисунке.
Куда смотреть?
Опорные точки заданы, но как нам проверить - работает ли код?
Займемся визуализацией результата или "рендерингом", проектрованием точек на экран.
Определим сразу для этих целей функцию Render, так как мы не раз ее будем вызывать далее. Эта функция должна будет делать следующее: 1) очищать экран 2) проектировать точки 3) рисовать спроектированные точки
Так как проектирование точек отдельный процесс, то и для него заведем специальную функцию Prj, которая в концепции ООП была бы очевидно методом объекта точки. Но мы обойдемся просто функцией.
Далее, нам потребуется инструмент камеры. Пусть это будет просто массив cam[x, y, z, cos(a), sin(a), cos(u), sin(u), focus]. Элементы массива - параметры камеры. Углы a и u задают направление осмотра ландшафта камерой, точка (x, y, z) - точка куда смотрит камера, focus - ее фокусное расстояние. Вообще говоря x, y и z будут нулевыми, так как нам не требуется перемещать куда-то взгляд, кроме как на рельеф ландшафта. Поэтому эти координаты оставлены просто для общности.
Ниже приводится новый код и SWF-файл.
Появляется новая переменная scale для массштабирования изображения на экране.
Корото поясню шаги функции Prj: Функция берет объект точки p, cдвигает точку так, как если бы камера смотрела в точку (0,0,0) координатной сетки (1), поворачивает точку так, как если бы камера не была повернута (угол a = 0) (2), поворачивает точку так, как если бы камера не была повернута (угол u=0) (3), проектируем на плоскость Y=0 - элементраная проекция точки с учетом точки фокуса лежащей на оси OY на расстоянии focus от начала координат.
Пояснения к функции Render. Пробегаем по всем точкам, проектируем каждую из них, на месте проекции рисуем крестик методами рисования ActionScript. Не нулевые точки помечаем красным цветом, остальные (лежащие в плоскости Z=0) - синим цветом.
step = 3;
grid = 3;
n = grid*step+1;
peak = 50;
pnt = [];
cam = [0, 0, 0, Math.cos(1.2), Math.sin(1.2),
Math.cos(-0.6), Math.sin(-0.6), 500];
scale = 0.5;
for ( var j=0; j < n; j++ ) {
pj = pnt[j] = [];
for ( var i=0; i < n; i++ ) {
pj[i] = {
x:((0.5 + i - n/2) * 400 / n),
y:((0.5 + j - n/2) * 400 / n)
};
if ( !(i%step) && !(j%step) ) {
pj[i].z = 1+random(peak);
} else {
pj[i].z = 0;
}
}
}
function Prj ( p ) {
var t;
// 1
var x = p.x - cam[0];
var y = p.y - cam[1];
var z = p.z - cam[2];
// 2
t = x;
x = y * cam[3] - t * cam[4];
y = t * cam[3] + y * cam[4];
// 3
t = y;
y = t * cam[5] - z * cam[6];
z = z * cam[5] + t * cam[6];
// 4
p.xp = 200 - x * cam[7] * scale / (cam[7] - y);
p.yp = 150 - z * cam[7] * scale / (cam[7] - y);
p.d = y;
}
function Render () {
clear();
var pji, pj, j, i;
for ( j=0; j < n; j++ ) {
pj = pnt[j];
for ( i=0; i < n; i++ ) {
Prj(pji = pj[i]);
if ( pj[i].z != 0 ) {
lineStyle(0, 0xFF0000);
} else {
lineStyle(0, 0x0000FF);
}
moveTo(pji.xp-2, pji.yp-2);
lineTo(pji.xp+2, pji.yp+2);
moveTo(pji.xp-2, pji.yp+2);
lineTo(pji.xp+2, pji.yp-2);
}
}
}
Render();
Добавим жизни!
Теперь самое время добавить интерактивность. Сделать это очень просто - мы привяжем изменение углов поворота камеры к координатам мышки. Пусть при перетаскивании курсора угол a меняется при изменении горизонтального положения, а u - при изменении вертикального положения.
Вот код который нужно добавить к уже существующему
this.onMouseDown = function () {
mx = _xmouse;
my = _ymouse;
a = Math.atan2(cam[4], cam[3]);
u = Math.atan2(cam[6], cam[5]);
this.onMouseMove = function () {
a = (_xmouse-mx)/200 + a;
u = Math.max(-1.5, Math.min(
(my-_ymouse)/300 + u, 0));
cam[3] = Math.cos(a);
cam[4] = Math.sin(a);
cam[5] = Math.cos(u);
cam[6] = Math.sin(u);
mx = _xmouse;
my = _ymouse;
Render();
}
}
this.onMouseUp = function () {
delete this.onMouseMove;
}
А вот что получилось (для наглядности было измененено значение peak на peak=100):
Пора вспоминать ЛинАл, МатАн и Численные методы :)
В данной статье я не собираюсь погружаться в дебри математики, и высчитывать поверхность по частным производным, а воспользуюсь простой формулой для расчета 2D сплайна. Сплайн возьмем Кетмула-Рома, который строится по 4 опорным точкам P1, P2, P3, P4 так, что проходит через точки P2 и P3, и при этом касательная к сплайну в этих точках параллельна отрезкам [P1,P3] и [P2,P4] соответственно. С краями поступим так - P1=P2 или P4=P3, в зависимости от края.
Для начала, возмем код, который генерит опорные точки, и превратим его в функцию CtrlPoint. Для чего это надо, вы узнаете позже. А сейчас создадим еще одну функцию, CtrlPoint2, кторая будет строить промежуточные между опорными, но только те, которые лежат сторого на линиях сетки (черные линии на первом рисунке). Для расчета промежуточных точек, создадим функцию сплайна Spline(object), которая по 4 точкам в переданном объекте расчитывает 4 коэффициента полинома A*t^3 + B*t^2 + C*t + D, который и будет определять куда кладется промежуточная точка.
Вот код этих функций
Теперь расчитаем оставшиеся точки, которые остались не зайдействоваы, для это создадим функцию CtrlPoint3, и немного изменим функцию Render. Ввиду того, что функция CtrlPoint3 очень похожа на функцию CtrlPoint2, то ее код мы опустим так же как и код новой функции Render. Весь же полный код можно найти в прилагаемом исходнике.
Займемся раскраской поверхности, а заодно рисованием "с заливкой"
Первое, что сделаем, это определим цвета для нашего ландшафта. Схема будет не сложной: от синего к зеленому, от зеленого к коричневому. А для легкости последующего использования цветов, создадим 3 массива от 0 до 100 с необходимыми цветами. 3 - это для каждой из 3-х компонент цвета - красного, зеленого, синего. Называться они будут, соответственно, rh, gh и bh.
Подбор цветов - дело вкуса, так что объяснять значения указанных цифр не буду. Скажу только, что i здесь, фактически, - это показатель глубины. 0 сооответствует низине, 100 соответствует вершине.
Теперь перейдем к закраске поверхности, для этого будем модифицировать функцию Render. Введем вспомогательную функцию Draw4, которая будет отрисовывать один квадрат поверхности.
Нужно отметить что как только мы начинаем рисовать закрашенные поверхности, сразу возникает вопрос с глубиной элемента закраски. Нужно закрашивать так что бы дальние участки поверхности не оказались над ближними. Для этого делается сортировка по глубине. Но в нашем случае можно обойтись последовательностью вывода, т.е. надо выводить сначала дальние квадратики, а затем ближние. Для это введем индексы ii и jj, а i и j будут пересчитываться от нуля до n-1 или от n-1 до 0, в зависимости от положения камеры. Такое индексирование поможет выводить дальние квадраты до ближних, тем самым ближние будут перекрывать дальние.
function Draw4 (p1, p2, p3, p4) {
var h = (p1.z + p2.z + p3.z + p4.z) / 4;
h = Math.max(0, Math.min(int(h * 100 / peak), 100));
var c = (rh[h]<<16) | (gh[h]<<8) | bh[h];
var t1 = (p2.xp - p1.xp) * (p3.yp - p2.yp) <
(p3.xp - p2.xp) * (p2.yp - p1.yp);
var t2 = (p4.xp - p3.xp) * (p1.yp - p4.yp) <
(p1.xp - p4.xp) * (p4.yp - p3.yp);
if ( t1 && t2 ) {
beginFill(c);
moveTo(p1.xp, p1.yp);
lineTo(p2.xp, p2.yp);
lineTo(p3.xp, p3.yp);
lineTo(p4.xp, p4.yp);
lineTo(p1.xp, p1.yp);
endFill();
} else if ( t1 ) {
beginFill(c);
moveTo(p1.xp, p1.yp);
lineTo(p2.xp, p2.yp);
lineTo(p3.xp, p3.yp);
lineTo(p1.xp, p1.yp);
endFill();
} else if ( t2 ) {
beginFill(c);
moveTo(p3.xp, p3.yp);
lineTo(p4.xp, p4.yp);
lineTo(p1.xp, p1.yp);
lineTo(p3.xp, p3.yp);
endFill();
}
}
function Render () {
clear();
lineStyle();
var pji, pj, j, i, ii, jj, p00, p10, p01;
Prj(p00 = pnt[0][0]);
Prj(p10 = pnt[n-1][0]);
Prj(p01 = pnt[0][n-1]);
var dj = (p00.d > p10.d) ? 1 : -1;
var di = (p00.d > p01.d) ? 1 : -1;
if ( Math.abs(p00.d - p01.d) > Math.abs(p00.d - p10.d) ) {
for ( ii=0; ii < n; ii++ ) {
i = (di > 0) ? n-1-ii : ii;
for ( jj=0; jj < n; jj++ ) {
j = (dj > 0) ? n-1-jj : jj;
pj = pnt[j];
Prj(pji = pj[i]);
if ( ii && jj ) {
if ( di+dj ) {
Draw4(pji,
pnt[j+dj][i],
pnt[j+dj][i+di],
pj[i+di]);
} else {
Draw4(pji,
pj[i+di],
pnt[j+dj][i+di],
pnt[j+dj][i]);
}
}
}
}
} else {
for ( jj=0; jj < n; jj++ ) {
j = (dj > 0) ? n-1-jj : jj;
pj = pnt[j];
for ( ii=0; ii < n; ii++ ) {
i = (di > 0) ? n-1-ii : ii;
Prj(pji = pj[i]);
if ( ii && jj ) {
if ( di+dj ) {
Draw4(pji,
pnt[j+dj][i],
pnt[j+dj][i+di],
pj[i+di]);
} else {
Draw4(pji,
pj[i+di],
pnt[j+dj][i+di],
pnt[j+dj][i]);
}
}
}
}
}
}
Параметры к изменению: кол-во опорных точек, кол-во промежуточных и максимальная высота холмов. Казалось бы ничего сложного - вставляем текстовые поля и вводим значения, но давайте подумаем, что произойдет если ввести кол-во опорных точек 10 и кол-во промежуточных 10. В таком случае наша сетка станет размерами 100 на 100, или 10000 точек! От такого кол-ва Flash может выполнять код одной функции в течении времени больше допустимого на один KeyFrame (кадр).
Для решения этой проблемы воспользуемся функцией getTimer(), которая нам подскажет, что пора остановится, и перенести выполнение кода на следующий кадр. Вот пример, как мы изменим функцию CtrlPoint. Очень хорошим стилем будет показывать индикатор вычисления или попросту "progressbar". В ниже приведенном коде используется просто прямоугольник, который масштабируется по горизонтали от 0% до 100%, показывая тем самым теущий процесс.
По аналогии изменяем и остальные функци. Можно отметить только Функцию Render, так как она вызывается при передвижении мыши, то она должна работать двояко - если прорисовка успевается за один кадр, то не должно быть больше ни одного лишнего кадра на рендеринг, дабы перемещение выглядело гладким, если же процесс не укладывается в отведенный таймаут, то, конечно, отрисовка разбивается на кадры. Полный код получившейся программки можно скачать отсюда. Вот она в действии:
Ваши замечания можете присылать на указанный ниже электронный адресс.
Буду благодарен тому, кто поможет в исправлении ошибок, а так же поможет с переводом статьи на английский язык. Обращайтесь по ниже указанному email-у. Заранее спасибо.