Flash 8 & Sandy 3D. Советы и трюки

В прошлый раз мы рассмотрели приемы, позволяющие работать с 3d-моделями как с множеством составляющих их граней и вершин. Научились выполнять индивидуальную настройку граней, создавая обработчики событий и настраивая отдельные текстурные скины. Также мы рассмотрели основные проблемы, возникающие из-за отсутствия в sandy 3d "настоящего" z-buffer. Сегодня мы завершаем изучение возможностей sandy. Изложение материала будет построено в стиле "вопрос-ответ". Я расскажу о вопросах, которые возникали у меня в ходе работы с sandy, и о тех ответах, которые я нашел в форумах и примерах работ других людей.

Я неоднократно упоминал, что 3d во flash очень ресурсоемко и очень медленно, а как насчет конкретных чисел?


Конкретные числа — это fps (количество сформированных кадров в секунду). Если вы все еще верите, что документ выполняется с некоторой частотой fps (которое изменяется с помощью меню Modify -> Document), то самое время избавиться от иллюзий. Это fps — предельное или желаемое. Так, в каждом кадре выполняется код actionscript и отрисовка клипов (из множества которых, по сути, и состоит весь 3d-мир). Теоретически на выполнение одного кадра отводится время 1/fps сек. Если же код в это время не укладывается, то ничего страшного до тех пор, пока он не превысит предельный лимит, когда скрипт будет аварийно прерван. Все, что вам нужно, — создать обработчик события OnRender. Это событие выбрасывает класс World3D всякий раз, когда хочет начать формирование 3d-сцены. В этом обработчике вы ведете подсчет, сколько был вызван данный метод в течение последней секунды. Для еще более полной статистики я написал функцию, выполняющую подсчет количества граней и вершин в сцене. Для этого функция выполняет рекурсивный спуск внизу по дереву 3d-объектов, и, когда встречает на своем пути объекты типа Leaf (конечная ветвь дерева — конкретная модель), то накапливает количество граней и вершин (метод getFaces возвращет их список, в свою очередь, у каждой грани есть метод getVertex, возвращающий список вершин). Ладно-ладно, для подсчета количества вершин можно было просто умножить число граней на 3 или 4.

var timer:Number = 0;
var fps:Number = 0;
// функция, получающая статистику о сцене
function getStatistics(inode:INode) {
var chlds = inode.getChildList();
var stat = {faces:0, points:0};
if (inode == null) {
return stat;
}
for (var i = 0; i<chlds.length; i++) {
if (chlds[i] instanceof Leaf) {
var faces = chlds[i].getFaces();
stat.faces += faces.length;
for (var j = 0; j < faces.length; j++)
stat.points += faces[j].getVertex().length;
} else {
var tmp = getStatistics(chlds[i]);
stat.faces += tmp.faces;
stat.points += tmp.points; } }
return stat;}
// функция, определающая FPS
function updateFPS() {
if (getTimer()>timer+1000) {
_root.fpsfield.text = fps +' fps';
fps = 0;
timer = getTimer();
} else {fps++;}}
// функция, вызываемая по событию "рендеринг"
function updateStat(e):Void {
updateFPS ();
var stat = getStatistics (World3D.getInstance().getRootGroup());
_root.statfield.text = "Faces: "+ stat.faces + " Points: " + stat.points;
}

function init(Void):Void {
// создаем два текстовых поля
_root.createTextField('fpsfield', 10000, 0, 20, 50, 20);
_root.createTextField('statfield', 20000, 100, 20, 200, 20);
// добавляем обработчик события
World3D.getInstance().addEventListener(World3D.onRenderEVENT, this, updateStat);
timer = getTimer();// устанавливаем начальные значения таймера и счетчика fps
fps = 0;
// и все как обычно — создаем модель 3d-мира

}

Мне не хватает стандартных фигур-примитивов sandy. Как создать еще один тип 3d-объекта?

Прежде всего, вы создаете собственный класс, производный от Object3D и поддерживающий интерфейс Primitive. В составе данного интерфейса вам нужно реализовать функцию generate и вызвать ее из конструктора класса. Затем, вооружившись учебником по геометрии, вы внутри generate определяете множество вершин и граней, из которых, по сути, и состоит ваша фигура, и добавляете их в специальные массивы aPoints, aNormals, _aFaces. Эти массивы вы получили по наследству от класса Object3D. Для примера я создал скрипт, который строит график функции z = f(x,y). Конкретный вид функции определяется параметром, переданным конструктору примитива. Также передаются параметры, задающие координаты области построения функции. Также важно, что при построении графика я выполняю сборку фигуры из граней типа TriFace3D (треугольник). Очень важно при задании вершин, образующих грань, задать их в правильном порядке. Так, направление обхода по или против часовой стрелки определяет, куда будет направлена нормаль. Или, проще говоря, будет ли видна построенная фигура. Для себя я в конструкторе ZFunctionPrim присвоил специальной переменной enableBackFaceCulling значение false, что означает рисовать все, в том числе и обратную сторону грани. Результат работы скрипта показан на рис. 2. Попробуйте самостоятельно реализовать построение графика функции, заданной параметрически. Cначала я привожу код класса нового примитива — он должен находиться в файле с именем ZFunctionPrim.as в той же папке, что и файл ролика.

import sandy.core.data.Vertex;
import sandy.core.data.UVCoord;
import sandy.core.face.Face;
import sandy.core.face.TriFace3D;
import sandy.core.Object3D;
import sandy.primitive.Primitive3D;
class ZFunctionPrim extends Object3D implements Primitive3D {
var foo:Function = null;
// ссылка на пользовательскую функцию
var min_x, max_x;
// минимальные и максимальные координаты прямоугольной области построения функции
var min_y, max_y;
//коэффициент масштабирования функции, а также параметр, управляющий величиной шага; в случае маленького размера шага количество граней растет по квадрату, и вскоре flash "умирает"
var koeff, xy_step;
public function ZFunctionPrim(min_x, max_x, min_y, max_y, koeff, xy_step, foo) {
super();
this.enableBackFaceCulling = false;
this.foo = foo;
this.min_x = min_x;
this.max_x = max_x;
this.min_y = min_y;
this.max_y = max_y;
this.koeff = koeff;
this.xy_step = xy_step;
generate();// вызов функции генерации поверхности
}
public function generate(Void):Void {
// создаем множество вершин и граней, из которых и состоит объект
aPoints = [];
aNormals = [];
_aFaces = [];
var STEP = xy_step;
// начинаем расчет набора значений некоторой функции
var valuesZ = new Array();
for (var x = min_x, xi = 0; x<=max_x; x += STEP, xi++) {
valuesZ[xi] = [];
for (var y = min_y, yi = 0; y<=max_y; y += STEP, yi++) {
var z = Math.round(koeff*foo.call(null, x, y));
var zp = new Vertex(x, y, z);
valuesZ[xi][yi] = zp;
aPoints.push(zp);
}
}
for (var xi = 0; xi<valuesZ.length-1; xi++) {
for (var yi = 0; yi<valuesZ[xi].length-1; yi++) {
var facePlane_1:TriFace3D = new TriFace3D(this, valuesZ[xi][yi], valuesZ[xi+1][yi], valuesZ[xi+1][yi+1]);
// создаем грани и добавляем их с помощью унаследованного метода addFace
addFace(facePlane_1);
var facePlane_2:TriFace3D = new TriFace3D(this, valuesZ[xi+1][yi+1], valuesZ[xi][yi+1], valuesZ[xi][yi]);
addFace(facePlane_2);
}}}}

А теперь пример использования этого примитива.

// пользовательская функция, вычисляющая значение высоты в точке x,y
function calcFoo (x, y){
return Math.sin(x+y);
}
function init( Void ):Void{
var screen:ClipScreen = new ClipScreen( this.createEmptyMovieClip('screen', 1), 550, 400 );
var cam:Camera3D = new Camera3D( 200, screen);
cam.setPosition(200,200, 500);
cam.lookAt (0,0,0);
World3D.getInstance().addCamera( cam );
var bg:Group = new Group();
World3D.getInstance().setRootGroup( bg );
//создаем новый пользовательский примитив с параметрами
var func = new ZFunctionPrim (-500, 500, -700, 300, 70, 50, calcFoo);
func.setSkin (new MixedSkin (0x00FF00,255,0x000000, 255, 1));
// стандартный код, перемещающий график функции и запускающий отрисовку
}

Вот мы загружали в 3d-сцену объекты, созданные в 3dsmax в форматах wrl и ase, и получали "голые" каркасы. А можно ли что-то сделать с материалами?

Да, можно, но с ограничениями и очень аккуратно. Для того, чтобы текстуры были нормально наложены на объект, необходимо для каждой грани/треугольника иметь координаты UV. Эти координаты определяют, какая точка графического файла будет использована для отрисовки определенного пикселя. В sandy есть специальный объект UVCoord, хранящий текстурные координаты. Когда модель импортируется, следует не только наполнить базовый класс Object3D вершинами и faces, но и указать UVCoord. Это делает только AseParser. Общую информацию о том, что это за формат, можно узнать по адресу: сайт .

Я не большой эксперт в 3dsmax, поэтому, возможно, что-то делаю не так, за что заранее прошу извинить. Шаг первый — создание самой 3d-модели. Вначале в качестве примера я взял обычный box. Так как в общем случае это мог быть более сложный объект, то я конвертировал его в Editable Mesh. К ней я применяю по очереди модификаторы UVW Mapping и Unwrap UVW. Затем в свитке Parameters -> Edit я попадаю в специальное диалоговое окно, где отображается развертка модели коробки. Используя разные пункты меню Mappping -> Flatten Mapping, Normal Mapping, Unfold mapping, я подбираю такое расположение граней развертки, чтобы потом удобно и понятно редактировать ее в каком либо графическом пакете. Затем делаю копию экрана или использую утилиту Texporter — так, как показано на рис. 4. Выбираю размер текстуры кратным двум (пусть это будет 512px), жму кнопку pick object, после чего появляется окно рендерера с прототипом моей будущей текстуры.

Остается ее только импортировать в photoshop и подправить, сгладив стыки, добавить мелкие детали. Результатом работы будет файл jpg, который следует импортировать в flash, добавив его в библиотеку, и обязательно дать ему идентификатор для вызова из actionscript (я назвал этот идентификатор mybox). Затем следует экспортировать модель из 3dsmax в формат ASE, обязательно установив checkbox для Mapping Coordinates, после чего можно уже и загружать модель с помощью sandy 3d. Результат работы показан на рис. 3.

import flash.display.BitmapData;
function init( Void ):Void{
// как обычно, создаем камеру, экран и связываем и
// создаем пустой объект 3d
var box = new Object3D ();
// выполняем разбор модели прямоугольника
AseParser.parse (box, "krutolet.ASE");
//назначаем загруженную в библиотеку текстуру
box.setSkin (new TextureSkin (BitmapData.loadBitmap("mybox")));
tg.addChild (box);
// и все как обычно …
}

А как насчет видеороликов в качестве текстуры объекта?

Нет проблем: все, что вам нужно, — это добавить на слой объект видеопотока, дав ему имя video_obj, а затем привязать этот объект к объекту типа VideoSkin. Не забудьте только поместить video_obj где-нибудь подальше за границами экрана, чтобы не видеть видео одновременно не только на гранях box, но и в том месте, где вы положили объект video.

box = new Box (100,100,100);
nc = new NetConnection();
nc.connect(null);
ns = new NetStream(nc);
video_obj.attachVideo(ns);
ns.play("nes_in_russia_get_video.flv");
box.setSkin(new VideoSkin(video_obj));
tg.addChild (box);

А как насчет нескольких 3d-миров, камер и источников света?

Класс World3D реализован как singleton (класс, экземпляр которого должен быть единственным). Это означает, что вы не сможете в одном flash- ролике содержать два или более 3d-мира. Источник света для мира тоже только один. Но для одного мира может быть произвольное количество камер, рассматривающих его с различных сторон. Когда вы создаете объект-камеру, то указываете ей в качестве параметра тот клип, на котором будет выполняться отрисовка той части 3d-мира, которую видит эта камера. Не забудьте только изменить координаты созданных камер — иначе они будут видеть одно и то же. В примере ниже я создаю два расположенных друг под другом клипа, каждый из которых будет играть роль экрана, на который будет спроецирован собственный вид камеры. Важно задать координаты "пустых" клипов, но ни в коем случае не изменять их высоту и ширину.

function init( Void ):Void{
// создаем два объекта клипа, играющих роль экранов, на которые будут проецироваться 3d-модели
var clip_1: MovieClip = _root.createEmptyMovieClip('screen_1', 1);
var clip_2: MovieClip = _root.createEmptyMovieClip('screen_2', 2);
// выполняем перемещение клипов так, чтобы они располагались друг за другом по вертикали
clip_1._x = 0;
clip_1._y = 0;

clip_2._x = 0;
clip_2._y = 200;

// создаем объект экран на основании клипа — обратите внимание на размеры ширина*высота экрана, переданные конструктору
var screen_x:ClipScreen = new ClipScreen( clip_1, 550, 200 );
// создаем первую камеру на основании клипа 1
var cam_x:Camera3D = new Camera3D( 400, screen_x);
cam_x.setPosition(200,-100, 500);
cam_x.lookAt (0,0,0);
World3D.getInstance().addCamera( cam_x );

var screen_y:ClipScreen = new ClipScreen( clip_2, 550, 200 );
var cam_y:Camera3D = new Camera3D( 400, screen_y);
cam_y.setPosition(-200,100, 500);
cam_y.lookAt (0,0,0);
World3D.getInstance().addCamera( cam_y );

// далее, как обычно, создается набор объектов для рендеринга

}
На рис. 1 я показал более сложный пример с четырьмя видами, подобными используемым в 3dsmax (вид сверху, слева, спереди и перспектива). Попробуйте сделать такой пример сами.

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

Да, есть такой баг. Если открыть исходные тексты парсера ASE, то можно заметить, что все его методы, а также поля, в которых накапливается читаемая из внешнего файла информация, являются статическими. А это означает, что как только вы запустили второй разбор ASE, возникает конфликт между данными для первого файла модели и второго. Единственный способ избежать этого — выполнять загрузку второй модели только после того, как будет загружена первая. Вспомните, что ASE-парсер генерирует события, сообщающие о стадиях его жизненного цикла, в том числе и событие Init — когда модель была успешно загружена в память. Такое же поведение свойственно и WRL-парсеру.

t_teapot_1 = new Object3D ();
t_teapot_1.setSkin (new MixedSkin( 0x00FF00, 80, 0, 100, 1 ));
t_teapot_2 = new Object3D ();
t_teapot_2.setSkin (new MixedSkin( 0x0000FF, 255, 0, 100, 1 ));

// читаем первый файл с моделью в формате WRL, и, как только операция разбора будет завершена, запускаем разбор второго файла
var model2Loaded = false;
// эта переменная играет роль флага, чтобы после успешной загрузки второй модели снова не выполнить ее загрузку
AseParser.addEventListener (AseParser.onInitEVENT, function(){
if (model2Loaded)return;
model2Loaded = true;
AseParser.parse( t_teapot_2, 'shuttle_2.ASE' );
});
// выполняем разбор первого файла
AseParser.parse( t_teapot_1, 'shuttle_1.ASE' );

Может быть, есть какие-то приемы, ускоряющие 3d-рендеринг?

Какой-то волшебной функции или команды для ускорения — разумеется, нет. Во-первых, 3d — "медленное" только тогда, когда у вас высокая степень детализации 3d-объектов. Это означает, что не имеет смысла с высокой точностью моделировать все объекты, и — главное — всегда. Если объект находится вдали, то на него можно отвести меньшее количество полигонов, а по мере приближения объекта к камере это число наращивать. Встроенной поддержки подмены моделей в sandy нет, но реализовать ее не так уж сложно. Основная сложность — принятие решения, когда выполнять подмену модели.

А как насчет эффектов — скажем, эффект взрыва/огонь/падающий снег?

Традиционно такие эффекты реализуются через системы частиц. Т.е. большое количество плоских 2d-спрайтов, которые движутся по некоторым законам. Есть нечто, что испускает поток этих спрайтов-частиц по некоторому закону. Также есть нечто оказывающее влияние на движение этих частиц: ветер, гравитация. Важно, что у частицы должен быть ограничен жизненный цикл, иначе через пару минут после того, как пошел снег у вас в ролике, в кадре будет находиться несколько тысяч объектов, и у нас элементарно закончатся ресурсы. Например, в коде ниже есть box — коробка, сверху которой падает снег, частицы снега летят внизу, но сбоку от коробки дует ветер, и частицы немного смещаются в сторону. Каждая частица живет, скажем, 5 секунд, после чего тает и удаляется из сцены. Неплохое введение есть на сайте сайт

black zorro, black-zorro@tut.by


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

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