package alternativa.engine3d.loaders { import alternativa.engine3d.*; import alternativa.engine3d.core.Face; import alternativa.engine3d.core.Mesh; import alternativa.engine3d.core.Object3D; import alternativa.engine3d.core.Surface; import alternativa.engine3d.core.Vertex; import alternativa.engine3d.materials.FillMaterial; import alternativa.engine3d.materials.TextureMaterial; import alternativa.engine3d.materials.TextureMaterialPrecision; import alternativa.types.Map; import alternativa.types.Point3D; import alternativa.types.Texture; import flash.display.BlendMode; import flash.events.ErrorEvent; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.IOErrorEvent; import flash.events.SecurityErrorEvent; import flash.geom.Point; import flash.net.URLLoader; import flash.net.URLRequest; import flash.system.LoaderContext; use namespace alternativa3d; /** * Загрузчик моделей из файла в формате OBJ. Так как OBJ не поддерживает иерархию объектов, все загруженные * модели помещаются в один контейнер Object3D. *

* Поддерживаюся следующие команды формата OBJ: *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
КомандаОписаниеДействие
o object_nameОбъявление нового объекта с именем object_nameЕсли для текущего объекта были определены грани, то команда создаёт новый текущий объект с указанным именем, * иначе у текущего объекта просто меняется имя на указанное.
v x y zОбъявление вершины с координатами x y zВершина помещается в общий список вершин сцены для дальнейшего использования
vt u [v]Объявление текстурной вершины с координатами u vВершина помещается в общий список текстурных вершин сцены для дальнейшего использования
f v0[/vt0] v1[/vt1] ... vN[/vtN]Объявление грани, состоящей из указанных вершин и опционально имеющую заданные текстурные координаты для вершин.Грань добавляется к текущему активному объекту. Если есть активный материал, то грань также добавляется в поверхность * текущего объекта, соответствующую текущему материалу.
usemtl material_nameУстановка текущего материала с именем material_nameС момента установки текущего материала все грани, создаваемые в текущем объекте будут помещаться в поверхность, * соотвествующую этому материалу и имеющую идентификатор, совпадающий с его именем.
mtllib file1 file2 ...Объявление файлов, содержащих определения материаловВыполняется загрузка файлов и формирование библиотеки материалов
* *

* Пример использования: *

	 * var loader:LoaderOBJ = new LoaderOBJ();
	 * loader.addEventListener(Event.COMPLETE, onLoadingComplete);
	 * loader.load("foo.obj");
	 * 
	 * function onLoadingComplete(e:Event):void {
	 *   scene.root.addChild(e.target.content);
	 * }
	 * 
*/ public class LoaderOBJ extends EventDispatcher { private static const COMMENT_CHAR:String = "#"; private static const CMD_OBJECT_NAME:String = "o"; private static const CMD_GROUP_NAME:String = "g"; private static const CMD_VERTEX:String = "v"; private static const CMD_TEXTURE_VERTEX:String = "vt"; private static const CMD_FACE:String = "f"; private static const CMD_MATERIAL_LIB:String = "mtllib"; private static const CMD_USE_MATERIAL:String = "usemtl"; private static const REGEXP_TRIM:RegExp = /^\s*(.*?)\s*$/; private static const REGEXP_SPLIT_FILE:RegExp = /\r*\n/; private static const REGEXP_SPLIT_LINE:RegExp = /\s+/; private var basePath:String; private var objLoader:URLLoader; private var mtlLoader:LoaderMTL; private var loaderContext:LoaderContext; private var loadMaterials:Boolean; // Объект, содержащий все определённые в obj файле объекты private var _content:Object3D; // Текущий конструируемый объект private var currentObject:Mesh; // Стартовый индекс вершины в глобальном массиве вершин для текущего объекта private var vIndexStart:int = 0; // Стартовый индекс текстурной вершины в глобальном массиве текстурных вершин для текущего объекта private var vtIndexStart:int = 0; // Глобальный массив вершин, определённых во входном файле private var globalVertices:Array; // Глобальный массив текстурных вершин, определённых во входном файле private var globalTextureVertices:Array; // Имя текущего активного материала. Если значение равно null, то активного материала нет. private var currentMaterialName:String; // Массив граней текущего объекта, которым назначен текущий материал private var materialFaces:Array; // Массив имён файлов, содержащих определения материалов private var materialFileNames:Array; private var currentMaterialFileIndex:int; private var materialLibrary:Map; /** * Сглаживание текстур при увеличении масштаба. * * @see alternativa.engine3d.materials.TextureMaterial */ public var smooth:Boolean = false; /** * Режим наложения цвета для создаваемых текстурных материалов. * * @see alternativa.engine3d.materials.TextureMaterial */ public var blendMode:String = BlendMode.NORMAL; /** * Точность перспективной коррекции для создаваемых текстурных материалов. * * @see alternativa.engine3d.materials.TextureMaterial */ public var precision:Number = TextureMaterialPrecision.MEDIUM; /** * Устанавливаемый уровень мобильности загруженных объектов. */ public var mobility:int = 0; /** * При установленном значении true выполняется преобразование координат геометрических вершин посредством * поворота на 90 градусов относительно оси X. Смысл флага в преобразовании системы координат, в которой вверх направлена * ось Y, в систему координат, использующуюся в Alternativa3D (вверх направлена ось Z). */ public var rotateModel:Boolean; /** * Создаёт новый экземпляр загрузчика. */ public function LoaderOBJ() { } /** * Контейнер, содержащий все загруженные из OBJ-файла модели. */ public function get content():Object3D { return _content; } /** * Прекращение текущей загрузки. */ public function close():void { try { objLoader.close(); } catch (e:Error) { } mtlLoader.close(); } /** * Загрузка сцены из OBJ-файла по указанному адресу. По окончании загрузки посылается сообщение Event.COMPLETE, * после чего контейнер с загруженными объектами становится доступным через свойство content. *

* При возникновении ошибок, связанных с вводом-выводом или с безопасностью, посылаются сообщения IOErrorEvent.IO_ERROR и * SecurityErrorEvent.SECURITY_ERROR соответственно. *

* @param url URL OBJ-файла * @param loadMaterials флаг загрузки материалов. Если указано значение true, будут обработаны все файлы * материалов, указанные в исходном OBJ-файле. * @param context LoaderContext для загрузки файлов текстур * * @see #content */ public function load(url:String, loadMaterials:Boolean = true, context:LoaderContext = null):void { _content = null; this.loadMaterials = loadMaterials; this.loaderContext = context; basePath = url.substring(0, url.lastIndexOf("/") + 1); if (objLoader == null) { objLoader = new URLLoader(); objLoader.addEventListener(Event.COMPLETE, onObjLoadComplete); objLoader.addEventListener(IOErrorEvent.IO_ERROR, onObjLoadError); objLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onObjLoadError); } else { close(); } objLoader.load(new URLRequest(url)); } /** * Обработка окончания загрузки obj файла. * * @param e */ private function onObjLoadComplete(e:Event):void { parse(objLoader.data); } /** * Обработка ошибки при загрузке. * * @param e */ private function onObjLoadError(e:ErrorEvent):void { dispatchEvent(e); } /** * Метод выполняет разбор данных, полученных из obj файла. * * @param s содержимое obj файла * @param materialLibrary библиотека материалов * @return объект, содержащий все трёхмерные объекты, определённые в obj файле */ private function parse(data:String):void { _content = new Object3D(); currentObject = new Mesh(); currentObject.mobility = mobility; _content.addChild(currentObject); globalVertices = new Array(); globalTextureVertices = new Array(); materialFileNames = new Array(); var lines:Array = data.split(REGEXP_SPLIT_FILE); for each (var line:String in lines) { parseLine(line); } moveFacesToSurface(); // Вся геометрия загружена и сформирована. Выполняется загрузка информации о материалах. if (loadMaterials && materialFileNames.length > 0) { loadMaterialsLibrary(); } else { dispatchEvent(new Event(Event.COMPLETE)); } } /** * */ private function parseLine(line:String):void { line = line.replace(REGEXP_TRIM,"$1"); if (line.length == 0 || line.charAt(0) == COMMENT_CHAR) { return; } var parts:Array = line.split(REGEXP_SPLIT_LINE); switch (parts[0]) { // Объявление нового объекта case CMD_OBJECT_NAME: defineObject(parts[1]); break; // Объявление вершины case CMD_VERTEX: globalVertices.push(new Point3D(Number(parts[1]), Number(parts[2]), Number(parts[3]))); break; // Объявление текстурной вершины case CMD_TEXTURE_VERTEX: globalTextureVertices.push(new Point3D(Number(parts[1]), Number(parts[2]), Number(parts[3]))); break; // Объявление грани case CMD_FACE: createFace(parts); break; case CMD_MATERIAL_LIB: storeMaterialFileNames(parts); break; case CMD_USE_MATERIAL: setNewMaterial(parts); break; } } /** * Объявление нового объекта. * * @param objectName имя объекта */ private function defineObject(objectName:String):void { if (currentObject.faces.length == 0) { // Если у текущего объекта нет граней, то он остаётся текущим, но меняется имя currentObject.name = objectName; } else { // Если у текущего объекта есть грани, то обявление нового имени создаёт новый объект moveFacesToSurface(); currentObject = new Mesh(objectName); currentObject.mobility = mobility; _content.addChild(currentObject); } vIndexStart = globalVertices.length; vtIndexStart = globalTextureVertices.length; } /** * Создание грани в текущем объекте. * * @param parts массив, содержащий индексы вершин грани, начиная с элемента с индексом 1 */ private function createFace(parts:Array):void { // Стартовый индекс вершины в объекте для добавляемой грани var startVertexIndex:int = currentObject.vertices.length; // Создание вершин в объекте var faceVertexCount:int = parts.length - 1; var vtIndices:Array = new Array(3); // Массив идентификаторов вершин грани var faceVertices:Array = new Array(faceVertexCount); for (var i:int = 0; i < faceVertexCount; i++) { var indices:Array = parts[i + 1].split("/"); // Создание вершины var vIdx:int = int(indices[0]); // Если индекс положительный, то его значение уменьшается на единицу, т.к. в obj формате индексация начинается с 1. // Если индекс отрицательный, то выполняется смещение на его значение назад от стартового глобального индекса вершин для текущего объекта. var actualIndex:int = vIdx > 0 ? vIdx - 1 : vIndexStart + vIdx; var vertex:Vertex = currentObject.vertices[actualIndex]; // Если вершины нет в объекте, она добавляется if (vertex == null) { var p:Point3D = globalVertices[actualIndex]; if (rotateModel) { // В формате obj направление "вверх" совпадает с осью Y, поэтому выполняется поворот координат на 90 градусов по оси X vertex = currentObject.createVertex(p.x, -p.z, p.y, actualIndex); } else { vertex = currentObject.createVertex(p.x, p.y, p.z, actualIndex); } } faceVertices[i] = vertex; // Запись индекса текстурной вершины if (i < 3) { vtIndices[i] = int(indices[1]); } } // Создание грани var face:Face = currentObject.createFace(faceVertices, currentObject.faces.length); // Установка uv координат if (vtIndices[0] != 0) { p = globalTextureVertices[vtIndices[0] - 1]; face.aUV = new Point(p.x, p.y); p = globalTextureVertices[vtIndices[1] - 1]; face.bUV = new Point(p.x, p.y); p = globalTextureVertices[vtIndices[2] - 1]; face.cUV = new Point(p.x, p.y); } // Если есть активный материал, то грань заносится в массив для последующего формирования поверхности в объекте if (currentMaterialName != null) { materialFaces.push(face); } } /** * Загрузка библиотек материалов. * * @param parts массив, содержащий имена файлов материалов, начиная с элемента с индексом 1 */ private function storeMaterialFileNames(parts:Array):void { for (var i:int = 1; i < parts.length; i++) { materialFileNames.push(parts[i]); } } /** * Установка нового текущего материала. * * @param parts массив, во втором элементе которого содержится имя материала */ private function setNewMaterial(parts:Array):void { // Все сохранённые грани добавляются в соответствующую поверхность текущего объекта moveFacesToSurface(); // Установка нового текущего материала currentMaterialName = parts[1]; } /** * Добавление всех граней с текущим материалом в поверхность с идентификатором, совпадающим с именем материала. */ private function moveFacesToSurface():void { if (currentMaterialName != null && materialFaces.length > 0) { if (currentObject.hasSurface(currentMaterialName)) { // При наличии поверхности с таким идентификатором, грани добавляются в неё var surface:Surface = currentObject.getSurfaceById(currentMaterialName); for each (var face:* in materialFaces) { surface.addFace(face); } } else { // При отсутствии поверхности с таким идентификатором, создатся новая поверхность currentObject.createSurface(materialFaces, currentMaterialName); } } materialFaces = []; } /** * Загрузка материалов. */ private function loadMaterialsLibrary():void { if (mtlLoader == null) { mtlLoader = new LoaderMTL(); mtlLoader.addEventListener(Event.COMPLETE, onMaterialFileLoadComplete); mtlLoader.addEventListener(IOErrorEvent.IO_ERROR, onObjLoadError); mtlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onObjLoadError); } materialLibrary = new Map(); currentMaterialFileIndex = -1; loadNextMaterialFile(); } /** * Обработка успешной загрузки библиотеки материалов. */ private function onMaterialFileLoadComplete(e:Event):void { materialLibrary.concat(mtlLoader.library); // Загрузка следующего файла материалов loadNextMaterialFile(); } /** * */ private function loadNextMaterialFile():void { currentMaterialFileIndex++; if (currentMaterialFileIndex == materialFileNames.length) { setMaterials(); dispatchEvent(new Event(Event.COMPLETE)); } else { mtlLoader.load(basePath + materialFileNames[currentMaterialFileIndex], loaderContext); } } /** * Установка материалов. */ private function setMaterials():void { if (materialLibrary != null) { for (var objectKey:* in _content.children) { var object:Mesh = objectKey; for (var surfaceKey:* in object.surfaces) { var surface:Surface = object.surfaces[surfaceKey]; // Поверхности имеют идентификаторы, соответствующие именам материалов var materialInfo:MaterialInfo = materialLibrary[surfaceKey]; if (materialInfo != null) { if (materialInfo.bitmapData == null) { surface.material = new FillMaterial(materialInfo.color, materialInfo.alpha, blendMode); } else { surface.material = new TextureMaterial(new Texture(materialInfo.bitmapData, materialInfo.textureFileName), materialInfo.alpha, materialInfo.repeat, (materialInfo.bitmapData != LoaderMTL.stubBitmapData) ? smooth : false, blendMode, -1, 0, precision); transformUVs(surface, materialInfo.mapOffset, materialInfo.mapSize); } } } } } } /** * Метод выполняет преобразование UV-координат текстурированных граней. В связи с тем, что в формате MRL предусмотрено * масштабирование и смещение текстурной карты в UV-пространстве, а в движке такой фунциональности нет, необходимо * эмулировать преобразования текстуры преобразованием UV-координат граней. Преобразования выполняются исходя из предположения, * что текстурное пространство сначала масштабируется относительно центра, а затем сдвигается на указанную величину * смещения. * * @param surface поверхность, грани которой обрабатываюся * @param mapOffset смещение текстурной карты. Значение mapOffset.x указывает смещение по U, значение mapOffset.y * указывает смещение по V. * @param mapSize коэффициенты масштабирования текстурной карты. Значение mapSize.x указывает коэффициент масштабирования * по оси U, значение mapSize.y указывает коэффициент масштабирования по оси V. */ private function transformUVs(surface:Surface, mapOffset:Point, mapSize:Point):void { for (var key:* in surface.faces) { var face:Face = key; var uv:Point = face.aUV; if (uv != null) { uv.x = 0.5 + (uv.x - 0.5 - mapOffset.x) * mapSize.x; uv.y = 0.5 + (uv.y - 0.5 - mapOffset.y) * mapSize.y; face.aUV = uv; uv = face.bUV; uv.x = 0.5 + (uv.x - 0.5 - mapOffset.x) * mapSize.x; uv.y = 0.5 + (uv.y - 0.5 - mapOffset.y) * mapSize.y; face.bUV = uv; uv = face.cUV; uv.x = 0.5 + (uv.x - 0.5 - mapOffset.x) * mapSize.x; uv.y = 0.5 + (uv.y - 0.5 - mapOffset.y) * mapSize.y; face.cUV = uv; } } } } }