/** * Metasequoia.as * * @see http://snippets.libspark.org/ * @see http://snippets.libspark.org/trac/wiki/rch850/Metasequoia * * Copyright (c) 2007-2008 rch850 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.libspark.pv3d { import flash.display.BitmapData; import flash.events.*; import flash.net.URLLoader; import flash.net.URLLoaderDataFormat; import flash.net.URLRequest; import flash.utils.ByteArray; import flash.utils.Dictionary; import org.libspark.pv3d.decoders.TGADecoder; import org.papervision3d.core.geom.TriangleMesh3D; import org.papervision3d.core.geom.renderables.Triangle3D; import org.papervision3d.core.geom.renderables.Vertex3D; import org.papervision3d.core.math.Matrix3D; import org.papervision3d.core.math.NumberUV; import org.papervision3d.core.proto.DisplayObjectContainer3D; import org.papervision3d.core.proto.GeometryObject3D; import org.papervision3d.core.proto.MaterialObject3D; import org.papervision3d.events.FileLoadEvent; import org.papervision3d.events.InteractiveScene3DEvent; import org.papervision3d.materials.BitmapFileMaterial; import org.papervision3d.materials.BitmapMaterial; import org.papervision3d.materials.ColorMaterial; import org.papervision3d.materials.utils.MaterialsList; import org.papervision3d.objects.DisplayObject3D; import com.voidelement.images.BMPDecoder; /** * メタセコイアのファイル(.mqo)を読み込むためのクラス。 * * var mqo = new Metasequoia(); * mqo.addEventListener(...); * mqo.load("hoge.mqo"); */ public class Metasequoia extends TriangleMesh3D { /** * コンストラクタ */ function Metasequoia() { this.materials = new MaterialsList(); super(null, new Array(), new Array(), null); } /** * @param file 読み込むファイルの URL。絶対パスで指定してください。 * @param scale 読み込むときの拡大率。1 が原寸大です。 */ public function load(file:String, scale:Number = 1):void { _filename = file; _scale = scale; loadMetasequoia(); } /** * ファイルの文字コード。よほどのことが無い限り shift_jis だと思います。 */ public var charset:String = "shift_jis"; /** * 面の両側にマテリアルを貼るかどうかを指定します。 */ public var doubleSided:Boolean = false; /** * インタラクティビティを設定します。 */ public var interactive:Boolean = false; private var _loader:URLLoader; private var _filename:String; private var _materialsToLoad:int =0; private var _materialNames:Array; private var _scale:Number = 1; private var _prevMesh:DisplayObject3D; private var _prevDepth:int; private function loadMetasequoia():void { _loader = new URLLoader(); _loader.dataFormat = URLLoaderDataFormat.BINARY; _loader.addEventListener(Event.COMPLETE, completeHandler); _loader.addEventListener(IOErrorEvent.IO_ERROR, defaultHandler); _loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, defaultHandler); _loader.addEventListener(ProgressEvent.PROGRESS, defaultHandler); _loader.load(new URLRequest(_filename)); } private function completeHandler(evt:Event):void { var byteArray:ByteArray = ByteArray(_loader.data); buildMetasequoia(byteArray.readMultiByte(byteArray.length, charset)); dispatchEvent(evt.clone()); } private function defaultHandler(evt:Event):void { dispatchEvent(evt.clone()); } private function buildMetasequoia(plainText:String):void { var lines:Array = plainText.split("\r\n"); //trace("num lines = " + lines.length); var l:int = 0; // Material チャンクを読み込む l = parseMaterialChunk(lines, 0); _prevDepth = 0; _prevMesh = this; // Object チャンクを読み込めなくなるまで読み込む while (l != -1) { l = parseObjectChunk(lines, l); } geometry.ready = true; } /** * Material チャンクの開始行を返します。 * 見つからなかった場合には -1 を返します。 */ private function getMaterialChunkLine(lines:Array, startLine:int = 0):int { for (var i:uint = startLine; i < lines.length; ++i) { if (lines[i].indexOf("Material") == 0) { return int(i); } } return -1; } /** * Material チャンクを読み込み、その最後の行番号を返します。 * エラーが起こった場合は -1 を返します。 */ private function parseMaterialChunk(lines:Array, startLine:int):int { var l:int = getMaterialChunkLine(lines, startLine); if (l == -1) { return -1; } // 解析中の行の文字列 var line:String = lines[l]; // マテリアル数を取得 var num:Number = parseInt(line.substr(9)); if (isNaN(num)) { return -1; } ++l; _materialNames = new Array(); // } で閉じているところの行番号 var endLine:int = l + int(num); // mqo ファイルのあるディレクトリのパス var path:String = _filename.slice(0, _filename.lastIndexOf("/") + 1); for (; l < endLine; ++l) { var material:MaterialObject3D; line = lines[l]; // マテリアルの名前を取得 var nameBeginIndex:int = line.indexOf("\""); var nameEndIndex:int = line.indexOf("\"", nameBeginIndex + 1); var name:String = line.substring(nameBeginIndex + 1, nameEndIndex); _materialNames.push(name); // テクスチャファイル名 var tex:String = getParam(line, "tex"); if (tex) { // テクスチャファイル名を取り囲む " " を取り除く tex = tex.substr(1, tex.length - 2); _materialsToLoad++; if (tex.toLowerCase().search(/\.tga$/) != -1) { material = loadTGAMaterial(path + tex); } else if (tex.toLowerCase().search(/\.bmp$/) != -1) { material = loadBMPMaterial(path + tex); } else { // テクスチャの URL を絶対にして読み込む material = new BitmapFileMaterial(path + tex); material.addEventListener(FileLoadEvent.LOAD_COMPLETE, materialLoadCompleteHandler); material.addEventListener(FileLoadEvent.LOAD_ERROR, materialLoadErrorHandler); } // あまり重さが変わらないのでせっかくだからスムージング material.smooth = true; } else { // 形式 - col(1.000 1.000 0.000 1.000) var colorstr:String = getParam(line, "col"); if (colorstr != null) { var color:Array = colorstr.match(/\d+\.\d+/g); var r:int = parseFloat(color[0]) * 255; var g:int = parseFloat(color[1]) * 255; var b:int = parseFloat(color[2]) * 255; var a:Number = parseFloat(color[3]) * 100; //trace("rgb = " + r + "," + g + "," + b); material = new ColorMaterial((r << 16) | (g << 8) | b); } else { material = MaterialObject3D.DEFAULT; } } material.doubleSided = this.doubleSided; material.interactive = this.interactive; material.name = name; materials.addMaterial(material, name); } return endLine; } /** * Creates a BitmapMaterial from TGA file and returns it. */ private function loadTGAMaterial(url:String):BitmapMaterial { var material:BitmapMaterial = new BitmapMaterial(); var loader:URLLoader = new URLLoader(); loader.dataFormat = URLLoaderDataFormat.BINARY; loader.addEventListener(Event.COMPLETE, function(event:Event):void { var tga:TGADecoder = new TGADecoder(loader.data); material.bitmap = tga.bitmap; material.maxU = material.maxV = 1; material.resetMapping(); }); loader.load(new URLRequest(url)); return material; } private function loadBMPMaterial(url:String):BitmapMaterial { var material:BitmapMaterial = new BitmapMaterial(); var loader:URLLoader = new URLLoader(); loader.dataFormat = URLLoaderDataFormat.BINARY; loader.addEventListener(Event.COMPLETE, function(event:Event):void { var bmpLoader:URLLoader = event.target as URLLoader; var decoder:BMPDecoder = new BMPDecoder(); material.bitmap = decoder.decode(bmpLoader.data); material.maxU = material.maxV = 1; material.resetMapping(); }); loader.load(new URLRequest(url)); return material; } private function materialLoadCompleteHandler(evt:FileLoadEvent):void { _materialsToLoad--; if(_materialsToLoad == 0){ //COLLADA のソースにあった謎の一行。不具合の元になるのでコメントアウト //materials = new MaterialsList(); dispatchEvent(new FileLoadEvent(FileLoadEvent.COLLADA_MATERIALS_DONE)); } } private function materialLoadErrorHandler(evt:FileLoadEvent):void { _materialsToLoad--; if(_materialsToLoad == 0){ dispatchEvent(new FileLoadEvent(FileLoadEvent.COLLADA_MATERIALS_DONE)); } } /** * Object チャンクの開始行を返します。 * 見つからなかった場合には -1 を返します。 */ private function getObjectChunkLine(lines:Array, startLine:int = 0):int { for (var i:uint = startLine; i < lines.length; ++i) { if (lines[i].indexOf("Object") == 0) { return int(i); } } return -1; } /** * Object チャンクを読み込み、その最後の行番号を返します。 * エラーが起こった場合は -1 を返します。 */ private function parseObjectChunk(lines:Array, startLine:int):int { var l:int = getObjectChunkLine(lines, startLine); if (l == -1) { return -1; } // 解析中の行の文字列 var line:String = lines[l]; // オブジェクト名を取得 var objectName:String = line.substring(8, line.indexOf("\"", 8)); ++l; var mesh:TriangleMesh3D = new TriangleMesh3D(null, new Array(), new Array(), objectName); var vertices:Array = mesh.geometry.vertices; var faces:Array = mesh.geometry.faces; // vertex チャンクを検索 var vline:int = getChunkLine(lines, "vertex", l); if (vline == -1) { return -1; } // プロパティを読み込む var properties:Dictionary = new Dictionary(); for (; l < vline; ++l) { line = lines[l]; var props:Array = RegExp(/^\s*([\w]+)\s+(.*)$/).exec(line); properties[props[1]] = props[2]; } line = lines[l]; l = vline + 1; // 頂点数を取得 var numVertices:int = parseInt(line.substring(line.indexOf("vertex") + 7)); var vertexEndLine:int = l + numVertices; var firstVertexIndex:int = vertices.length; // vertex チャンクを読み込む for (; l < vertexEndLine; ++l) { line = lines[l]; var coords:Array = line.match(/(-?\d+\.\d+)/g); var x:Number = parseFloat(coords[0]) * _scale; var y:Number = parseFloat(coords[1]) * _scale; var z:Number = -parseFloat(coords[2]) * _scale; vertices.push(new Vertex3D(x, y, z)); } // face チャンクを検索 l = getChunkLine(lines, "face", l); if (l == -1) { return -1; } line = lines[l++]; // 面数を取得 var numFaces:int = parseInt(line.substring(line.indexOf("face") + 5)); var faceEndLine:int = l + numFaces; // face チャンクを読み込む for (; l < faceEndLine; ++l) { if (properties["visible"] == "15") { parseFace(faces, lines[l], vertices, firstVertexIndex, properties); } } // Resolve parent-child relationship. var depth:int; try { depth = parseInt(properties["depth"]); } catch (e:Error) { depth = 0; } var parentMesh:DisplayObjectContainer3D = _prevMesh; if (depth <= 0) { parentMesh = this; depth = 0; } else { while (depth <= _prevDepth) { parentMesh = DisplayObject3D(parentMesh).parent; --_prevDepth; } } parentMesh.addChild(mesh); _prevMesh = mesh; _prevDepth = depth; return l; } private function parseFace(faces:Array, line:String, vertices:Array, vertexOffset:int, properties:Dictionary):void { var vstr:String = getParam(line, "V"); var mstr:String = getParam(line, "M"); var uvstr:String = getParam(line, "UV"); var v:Array = (vstr != null) ? vstr.match(/\d+/g) : []; var uv:Array = (uvstr != null) ? uvstr.match(/-?\d+\.\d+/g) : []; var a:Vertex3D; var b:Vertex3D; var c:Vertex3D; var d:Vertex3D; var material:MaterialObject3D; var uvA:NumberUV; var uvB:NumberUV; var uvC:NumberUV; var uvD:NumberUV; var face:Triangle3D; if (v.length == 3) { c = vertices[parseInt(v[0]) + vertexOffset]; b = vertices[parseInt(v[1]) + vertexOffset]; a = vertices[parseInt(v[2]) + vertexOffset]; if (mstr != null) { material = materials.getMaterialByName(_materialNames[parseInt(mstr)]); } if (uv.length != 0) { uvC = new NumberUV(parseFloat(uv[0]), 1 - parseFloat(uv[1])); uvB = new NumberUV(parseFloat(uv[2]), 1 - parseFloat(uv[3])); uvA = new NumberUV(parseFloat(uv[4]), 1 - parseFloat(uv[5])); face = new Triangle3D(this, [a, b, c], material, [uvA, uvB, uvC]); } else { face = new Triangle3D(this, [a, b, c], material, [new NumberUV(0, 0), new NumberUV(1, 0), new NumberUV(0, 1)]); } faces.push(face); if (properties["mirror"] == "1") { var mirrorAxis:int = parseInt(properties["mirror_axis"]); a = mirrorVertex(a, mirrorAxis); b = mirrorVertex(b, mirrorAxis); c = mirrorVertex(c, mirrorAxis); vertices.push(a); vertices.push(b); vertices.push(c); face = new Triangle3D(this, [c, b, a], material, face.uv.reverse()); faces.push(face); } } else if (v.length == 4) { d = vertices[parseInt(v[0]) + vertexOffset]; c = vertices[parseInt(v[1]) + vertexOffset]; b = vertices[parseInt(v[2]) + vertexOffset]; a = vertices[parseInt(v[3]) + vertexOffset]; if (mstr != null) { material = materials.getMaterialByName(_materialNames[parseInt(mstr)]); } if (uv.length != 0) { uvD = new NumberUV(parseFloat(uv[0]), 1 - parseFloat(uv[1])); uvC = new NumberUV(parseFloat(uv[2]), 1 - parseFloat(uv[3])); uvB = new NumberUV(parseFloat(uv[4]), 1 - parseFloat(uv[5])); uvA = new NumberUV(parseFloat(uv[6]), 1 - parseFloat(uv[7])); } else { uvD = new NumberUV(0, 0); uvC = new NumberUV(1, 0); uvB = new NumberUV(0, 1); uvA = new NumberUV(1, 1); } face = new Triangle3D(this, [a, b, c], material, [uvA, uvB, uvC]); faces.push(face); face = new Triangle3D(this, [c, d, a], material, [uvC, uvD, uvA]); faces.push(face); if (properties["mirror"] == "1") { mirrorAxis = parseInt(properties["mirror_axis"]); a = mirrorVertex(a, mirrorAxis); b = mirrorVertex(b, mirrorAxis); c = mirrorVertex(c, mirrorAxis); d = mirrorVertex(d, mirrorAxis); vertices.push(a); vertices.push(b); vertices.push(c); vertices.push(d); face = new Triangle3D(this, [c, b, a], material, [uvC, uvB, uvA]); faces.push(face); face = new Triangle3D(this, [a, d, c], material, [uvA, uvD, uvC]); faces.push(face); } } } /** * 頂点を軸に沿って反転させたものを返します。 */ private static function mirrorVertex(v:Vertex3D, axis:int):Vertex3D { return new Vertex3D( ((axis & 1) != 0) ? -v.x : v.x, ((axis & 2) != 0) ? -v.y : v.y, ((axis & 4) != 0) ? -v.z : v.z); } /** * Object チャンクの開始行を返します。 */ private static function getChunkLine(lines:Array, chunkName:String, startLine:int = 0):int { for (var i:uint = startLine; i < lines.length; ++i) { if (lines[i].indexOf(chunkName) != -1) { return int(i); } } return -1; } /** * line 内で paramName(...) という形式で指定されているパラメータを返します。 */ private static function getParam(line:String, paramName:String):String { var prefix:String = paramName + "("; var prefixLen:int = prefix.length; var begin:int = line.indexOf(prefix, 0); if (begin == -1) { return null; } var end:int = line.indexOf(")", begin + prefixLen); if (end == -1){ return null; } return line.substring(begin + prefixLen, end); } } }