Flash 8 & Sandy 3D. Модели, грани, вершины и все, все, все

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

Итак, в прошлый раз в конце статьи я привел код Flash-ролика, который создавал модель стола из одного параллелепипеда и четырех ножек цилиндров. Затем обработчик события "начало очередного кадра" вращал этот стол вокруг некоторой оси. Откровенно говоря, тот пример был просто ужасен тем, что в определенные моменты во время вращения возникали ошибки z-сортировки, приводившие к тому, что мы видели сквозь столешницу ножки стола, или наоборот. В серьезных 3d-движках подобной проблемы как таковой нет. Итак, у нас есть некоторая модель, состоящая из полигонов, каждый из которых, в свою очередь, может быть представлен как набор треугольников. Затем эти треугольники (заданные координатами в 3d-пространстве) должны быть спроецированы на плоскость экрана (2d). При этом следует решить задачу z-сортировки, т.е. решить, какие треугольники должны быть отрисованы, а какие будут невидимы, так как перекрываются другими треугольниками. Очевидно, что чем меньше будет треугольник, тем более точно и — главное — однозначно можно решить эту задачу. Если треугольники достаточно велики, то возможно частичное перекрытие, когда мы видим часть одного треугольника и часть другого. В идеале у нас должен быть настоящий z-buffer, представленный в виде двумерного массива размером с экран. Каждая ячейка этого буфера хранит информацию о том, какой пиксель (из множества пикселей, принадлежащих различным треугольникам различных геометрических объектов) будет ближе всего к воображаемой плоскости экрана — вот этот пиксель и следует рисовать. Так вот, весь сюрприз в том, что в Sandy3d нет z-buffer. Как нет, скажете вы, а как решается вопрос, какой из двух пикселей отрисовывать? Давайте посмотрим на исходный текст одного файла из библиотеки Sandy 3d: \sandy\core\buffer\ZBuffer.as. В самом его начале автор в комментарии пишет, что:

. Этот класс не настоящий z-buffer, т.к. Sandy3d — не настоящий 3d-движок.
. Здесь выполняется только Z-сортировка MovieClips.
. Объекты movieClips отрисовываются в корректном порядке так, чтобы создать эффект глубины.
. Эта техника достаточно быстра, но не очень аккуратна, так что у вас могут быть проблемы с z-сортировкой, когда у двух объектов грани расположены достаточно близко или эти грани велики.

Увы, но низкая производительность flash снова стала на пути хорошего 3d. Нам остается выполнять только искусственное повышение количества полигонов/граней, уменьшая их размер до тех пор, пока эффекты перекрытия не станут достаточно малы, чтобы ими пренебречь. Теоретически каждому объекту-примитиву при его создании можно указать параметр качества. Этот параметр некоторым хитрым образом влияет на то, сколько граней-клипов будет создано для представления объекта. В примере ниже я немного изменил старый код — так, чтобы были видны созданные грани.

Var _numfaces = 1; // 2 // 4 // 10 — здесь я указывал количество граней
// создаем объект стол
var stol:Object3D = new Box( 600, 300, 20, 'tri', _numfaces);
// назначаем ему скин в виде сплошной заливки зеленым цветом
//stol.setSkin (new SimpleColorSkin (0x00FF00, 255)); — старый скин с заливкой сплошным цветом выкинем
// а будет новый скин, в котором ребра-грани будут рисоваться черным цветом
stol.setSkin (new MixedSkin (0x00FF00,255,0x000000, 255, 1));
// аналогично я изменяю код создания ножки-цилиндра
leg.setSkin (new MixedSkin (colo,255,0x000000, 255, 1));

Изменяя значение переменной _numfaces, мы можем управлять количеством граней. На рис. 1 я показал три варианта (с 1-й гранью, 2-мя и 4-мя гранями соответственно). Обратите внимание: эффект перекрытия постепенно уменьшается. Вариант, когда у Box 10 граней, я не привожу, т.к. при запуске ролика время создания Box превысило отведенный лимит на протяженность одного кадра, и скрипт был аварийно прерван. Но даже в варианте с 4-мя гранями скорость вращения стола была заметно меньше, чем ранее. Как решать эту проблему? Даже если я помещу код создания объектов сложной сцены в нескольких кадрах, это не решит проблему последующей медленной отрисовки. Те, кто профессионально занимается 3d-графикой, знают о высокополигональном и низкополигональном моделировании. Все искусство в том, чтобы для одной модели в некоторых местах сделать много маленьких полигонов/граней, а в другом месте обойтись всего одним, но огромным полигоном. Так, даже в нашем примере очевидно, что на боковые грани столешницы можно отвести меньшее количество граней. Более того, возможно даже верхнюю часть столешницы разбить на грани неравномерно. Так, в тех местах, где находятся ножки, следует сделать больше граней, а в центре стола — поменьше. Если бы стол не вращался, то нижняя (невидимая) плоскость столешницы могла бы состоять всего из пары треугольных граней или даже одной прямоугольной (напомню, что в Sandy3d при создании объектов можно указать тип граней: треугольная или прямоугольная).

Есть только одна проблема: стандартные объекты-примитивы Sandy слишком примитивны (вот такая тавтология). Очевидно, что разработчики не могли предусмотреть все возможные ситуации моделирования и дали нам два возможных пути. Первый из них — выполнить создание объекта из граней вручную. Мы можем создать столько граней, сколько нам нужно, и там, где это нужно. Правда, это очень трудоемко и чревато множеством ошибок. Ведь нужно задать не только координаты вершин граней, но и нормали к этим граням, а также координаты для текстурирования. Второй подход основан на импорте созданной в 3dsmax или иной "серьезной" программе модельки (вот место, где вы можете проявить свои таланты 3d-моделлера) посредством экспорта в специальный формат внутрь flash-ролика. Первый подход все же нужен для ситуаций, когда созданная модель будет конструироваться на основании некоторых вычисляемых данных. Например, вы хотите создать обучающий ролик по математике, который получает в качестве параметра некоторую функцию z=f(x,y) и рисует ее трехмерное представление. Относительно второго подхода с предварительно подготовленной моделью объекта в 3dsmax и импортом ее в Sandy3d. Есть два формата файлов, которые понимает Sandy — это ASE формат и WRL — оба они текстовые, а не двоичные, и могут быть легко прочитаны из flash. WRL — это тот самый VRML язык описания трехмерных сцен, который когда-то очень давно позиционировался на роль стандарта, но не сложилось. Для примера я создам в 3dsmax ОДИН объект "чайник", а затем его экспортирую в два файла с именами tea.ase и tea.wrl. Важно, что, когда вы эскспортируете в VRML (WRL) формат, в настройках диалогового окна необходимо снять checkbox "indention" так, как это показано на рис. 4. В случае ASE-файла вполне подойдут настройки экспорта по умолчанию. Также важно, чтобы в экспортируемом файле была только ОДНА модель. Теоретически для WRL-файлов может быть несколько моделей, но на практике у меня это ни разу не получилось. Далее. Хотя WRL-файл может содержать сведения и о текстурах, на практике WRLParser в составе Sandy3d этого делать еще не умеет. Важный момент в том, что, когда вы загружаете модель, это может занять достаточно длительное время. Поэтому разработчики парсеров сделали хитрый ход: они загружают модель по частям, используя таймер. Изменяя переменную "AseParser.INTERVAL", вы можете менять скорость загрузки модели и соответственно степень загрузки процессора (они обратно пропорциональны). Раз модель грузится постепенно, значит, вам нужен механизм, позволяющий определить, сколько процентов работы уже сделано, и когда загрузка будет завершена. В примере ниже я добавил к парсеру обработчики событий с помощью метода AseParser.addEventListener. Внутри этих обработчиков просто выводится текстовое сообщение, но вы можете реализовать и более сложное поведение.

var t_teapot_1; // два объекта-чайника
var t_teapot_2;
var tg = new TransformGroup ();
var frame_num = 0;
function init( Void ):Void{
var screen:ClipScreen = new ClipScreen( this.createEmptyMovieClip('screen', 1), 550, 400 );
var cam:Camera3D = new Camera3D( 400, screen);
cam.setPosition(200,-100, 100);
cam.lookAt (0,0,0);
World3D.getInstance().addCamera( cam );
var bg:Group = new Group();
World3D.getInstance().setRootGroup( bg );

// сначала создается пустой объект Object3D, внутрь которого потом парсер заносит вершины и грани
t_teapot_1 = new Object3D ();
// читаем первый файл с моделью в формате ASE
// сначала добавляем обработчики событий
AseParser.addEventListener (AseParser.onInitEVENT, function(){trace ('Init ASE');});
AseParser.addEventListener (AseParser.onLoadEVENT, function(){trace ('Load ASE');});
AseParser.addEventListener (AseParser.onFailEVENT, function(){trace ('Fail ASE');});
AseParser.addEventListener (AseParser.onProgressEVENT, function(){trace ('Progress ASE');});
// и, наконец, запускаем разбор на выполнение
AseParser.parse( t_teapot_1, 'tea.ASE' );
t_teapot_1.setSkin (new MixedSkin( 0x00FF00, 80, 0, 100, 1 ));
// прочитанному чайнику мы можем назначить скин, также как и любому другому геометрическому объекту
tg.addChild (t_teapot_1);

// теперь читаем второй файл с моделью в формате WRL
t_teapot_2 = new Object3D ();
WrlParser.parse( t_teapot_2, 'tea.WRL' );
t_teapot_2.setSkin (new MixedSkin( 0x0000FF, 255, 0, 100, 1 ));

// теперь второй чайник нужно немного сдвинуть в сторону так, чтобы его было видно
var t2_m:Transform3D = new Transform3D ();
var t2_g = new TransformGroup ();
t2_m.translate (50, 50, 0);
t2_g.setTransform (t2_m);
t2_g.addChild (t_teapot_2);

tg.addChild (t2_g);
bg.addChild(tg);
// запустить отрисовку мира
World3D.getInstance().render();
}
init();// создаем 3d-мир
// и добавляем обработчик события, который при каждом новом кадре будет вращать чайник на некоторый градус, зависящий от номера кадра _root.onEnterFrame = function (){
var t = new Transform3D ();
t.rot (-frame_num, 0, 0);
tg.setTransform (t);
frame_num++;}

В качестве демонстрации примера работы импорта моделей из 3dsmax я привожу рис. 5 — на нем изображен не только чайник, но и пара прямоугольников, вращающихся вокруг своей оси. Программное создание граней наталкивает на мысль, что возможно управлять параметрами грани независимо от других граней объектов. Что может быть таким параметром? Прежде всего, это Skin. А также, что самое интересное, обработка событий. В качестве примера я создам вращающийся куб, на гранях которого будут размещены логотипы поисковых машин. Нажатие на грань приводит к открытию в окне браузера страницы, соответствующей этому поисковику. Также при наведении указателя мыши на грань ее текстура будет меняться. Для начала выполните подготовительные действия. Вам нужно импортировать в библиотеку 8 картинок с изображениями граней куба (ладно-ладно, у меня все равно нет логотипов сайтов, поэтому я просто возьму jpg-картинку с цифрой). Обязательно нужно разрешить использовать импортированное изображение в actionscript.

Дайте картинкам идентификаторы по правилу: img_1, img_2, … Как это сделать, показано на рис. 2. Когда мышь наводят на грань, то ее скин будет заменен на другой. Создайте также 8 картинок, имитирующих "активное" состояние грани — импортируйте и подключите их так же, как предыдущие картинки, но уже с именами: ove_1, ove_2, … Внутреннее устройство скрипта очень простое — самое главное скрывается за тремя массивами: mapSavedSkins, mapOverSkins, в которых хранятся карты отображения идентификаторов граней на объект Skin для грани в неактивном и активном состоянии соответственно. Массив mapSavedURLs по аналогии отображает идентификатор грани на адрес страницы для перехода. Дело в том, что каждая грань автоматически получает уникальный идентификатор, и именно его мы можем использовать внутри функции — обработчика событий наведение_на_грань_мыши, клик_по_грани, мышь_убрана_с_грани для того, чтобы определить, с кем именно что-то случилось. Для того, чтобы получить значение этого идентификатора, используйте вызов метода getId(). Апологеты ООП предпочтут хранить два скина грани и адрес ссылки в виде свойств объектов граней и будут, соответственно, правы, но это все же учебный пример. Каждый геометрический объект, порожденный от класса Object3D, содержит метод getFaces, который возвращает массив граней объекта. Затем я по этому массиву организую цикл, в котором назначаю обработчики событий. Результаты работы скрипта показаны на рис. 3.

// подключаем стандартные библиотеки
import sandy.core.group.*;
import sandy.primitive.*;
import sandy.view.*;
import sandy.core.*;
import sandy.skin.*;
import sandy.core.face.*;
import sandy.events.*;
// для динамической загрузки изображений текстуры необходимо импортировать эту библиотеку
import flash.display.BitmapData;

var tg = new TransformGroup ();
var frame_num = 0;
// созданы два массива, в которых будет храниться отображение идентификаторов каждой грани на соответсвующий скин и адрес страницы для перехода var mapSavedSkins = [];
var mapSavedURLs = [];
// и массив скинов в состоянии мышь наведена на грань
var mapOverSkins = [];

function init( Void ):Void{
var screen:ClipScreen = new ClipScreen( this.createEmptyMovieClip('screen', 1), 550, 400 );
var cam:Camera3D = new Camera3D( 400, screen);
cam.setPosition(200,-100, 100);
cam.lookAt (0,0,0);
World3D.getInstance().addCamera( cam );
var bg:Group = new Group();
World3D.getInstance().setRootGroup( bg );
t_box = new Box (50, 50, 50, 'quad');
faces = t_box.getFaces();
// создаем массив из восьми строк — адресов страниц, куда будет выполняться переход
var urls = ["ya.ru", "rambler.ru", "msn.com", "google.com", "aport.ru", "yahoo.com", "open.by", "tut.by"];
for ( var i = 0; i < faces.length; i++ ){
var face = faces[i];
// объект битмап, в котором находится текстура, взятая из библиотеки
var bmp = BitmapData.loadBitmap("img_"+ (1+i));
mapSavedSkins [face.getId()] = new TextureSkin (bmp);
var bmp2 = BitmapData.loadBitmap("ove_"+ (1+i));
mapOverSkins[face.getId()] = new TextureSkin (bmp2);
face.setSkin (mapSavedSkins [face.getId()]);
// включаем поддержку событий для грани
face.enableEvents(true);
// назначаем обработчики событий для события мышь над гранью
face.addEventListener( ObjectEvent.onRollOverEVENT, this, mouseOverAction );
// события — мышь была убрана с грани
face.addEventListener( ObjectEvent.onRollOutEVENT, this, mouseOutAction );
// нажатие на грань мышью
face.addEventListener( ObjectEvent.onPressEVENT, this, showPageAction );
// для этого последнего события необходимо указать параметр, куда будет выполняться переход
mapSavedURLs [face.getId()] = urls [i];
}
tg.addChild (t_box);
bg.addChild(tg);
// запустить отрисовку мира
World3D.getInstance().render();
}

// обработка события мышь наведена на грань куба
function mouseOverAction( e:ObjectEvent ){
var face:TriFace3D = e.getTarget();
face.setSkin( mapOverSkins [face.getId()] );}
// обработка события мышь ушла с грани куба
function mouseOutAction( e:ObjectEvent ){
var face:TriFace3D = e.getTarget();
face.setSkin( mapSavedSkins [face.getId()] );}
// событие нажатие на грань куба
function showPageAction( e:ObjectEvent ){
var face:TriFace3D = e.getTarget();
getUrl(mapSavedURLs [face.getId()],"_blank"); }

// создаем 3d-мир
init();
// и знакомое уже вам вращение
_root.onEnterFrame = function (){
var t = new Transform3D ();
t.rot (-frame_num, 0, 0);
tg.setTransform (t);
frame_num++;
}

На этом все. В следующий раз мы закроем рассмотрение возможностей Sandy. Нам осталось только разобраться со светом, анимацией и фильтрами, которые можно накладывать на скин, создавая некоторое подобие мультитекстурирования 3d-моделей. Напоследок мы рассмотрим методы создания собственных примитивов.

black zorro, black-zorro@tut.by


Компьютерная газета. Статья была опубликована в номере 21 за 2007 год в рубрике программирование

©1997-2024 Компьютерная газета