Про 3-е измерение во flash с помощью sandy 3d - часть 3
Материал из Dom.
[править] 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 секунд, после чего тает и удаляется из сцены. Неплохое введение есть на сайте http://graphics.cs.msu.su/courses/cg/assigns/2006/hw1/
|
|
Subscribe Now! |
|


