--- /dev/null
+/*
+ * model exporter for pmd-file
+ *
+ * License : The MIT License
+ * Copyright(c) 2010 MikuToga Partners
+ */
+
+package jp.sfjp.mikutoga.pmd.binio;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import jp.sfjp.mikutoga.pmd.model.BoneGroup;
+import jp.sfjp.mikutoga.pmd.model.BoneInfo;
+import jp.sfjp.mikutoga.pmd.model.IKChain;
+import jp.sfjp.mikutoga.pmd.model.Material;
+import jp.sfjp.mikutoga.pmd.model.MorphPart;
+import jp.sfjp.mikutoga.pmd.model.MorphVertex;
+import jp.sfjp.mikutoga.pmd.model.PmdModel;
+import jp.sfjp.mikutoga.pmd.model.SerialNumbered;
+import jp.sfjp.mikutoga.pmd.model.ShadeInfo;
+import jp.sfjp.mikutoga.pmd.model.Surface;
+import jp.sfjp.mikutoga.pmd.model.Vertex;
+import jp.sourceforge.mikutoga.binio.BinaryExporter;
+import jp.sourceforge.mikutoga.binio.IllegalTextExportException;
+import jp.sourceforge.mikutoga.math.MkPos2D;
+import jp.sourceforge.mikutoga.math.MkPos3D;
+import jp.sourceforge.mikutoga.math.MkVec3D;
+import jp.sourceforge.mikutoga.pmd.BoneType;
+import jp.sourceforge.mikutoga.pmd.IllegalPmdDataException;
+import jp.sourceforge.mikutoga.pmd.MorphType;
+import jp.sourceforge.mikutoga.pmd.parser.PmdLimits;
+
+/**
+ * PMDファイルのエクスポーター(拡張無し基本フォーマット)。
+ * <p>
+ * 英名対応以降のPMDファイルフォーマットを
+ * 使いたくない場合はこのエクスポーターを用いて出力せよ。
+ */
+public class PmdExporterBase extends BinaryExporter{
+
+ /** 前(親)ボーンが無い場合の便宜的なボーンID。 */
+ public static final int NOPREVBONE_ID = 0xffff;
+ /** 次(子)ボーンが無い場合の便宜的なボーンID。 */
+ public static final int NONEXTBONE_ID = 0x0000;
+ /** 影響元IKボーンが無い場合の便宜的なボーンID。 */
+ public static final int NOIKBONE_ID = 0x0000;
+
+ private static final byte[] MAGIC_BYTES = {
+ (byte)0x50, (byte)0x6d, (byte)0x64, // "Pmd"
+ (byte)0x00, (byte)0x00, (byte)0x80, (byte)0x3f, // 1.0f
+ };
+
+ private static final byte[] NULLFILLER =
+ { (byte)0x00 };
+ private static final byte[] FDFILLER =
+ { (byte)0x00, (byte)0xfd };
+ private static final byte[] LFFILLER =
+ { (byte)0x0a, (byte)0x00, (byte)0xfd };
+
+ /** 改行文字列 CR。 */
+ private static final String CR = "\r"; // 0x0d
+ /** 改行文字列 LF。 */
+ private static final String LF = "\n"; // 0x0a
+ /** 改行文字列 CRLF。 */
+ private static final String CRLF = CR + LF; // 0x0d, 0x0a
+
+ static{
+ assert NOPREVBONE_ID > PmdLimits.MAX_BONE - 1;
+ }
+
+ /**
+ * コンストラクタ。
+ * @param stream 出力ストリーム
+ * @throws NullPointerException 引数がnull
+ */
+ public PmdExporterBase(OutputStream stream)
+ throws NullPointerException{
+ super(stream);
+ return;
+ }
+
+ /**
+ * 改行文字の正規化を行う。
+ * CR(0x0d)およびCRLF(0x0d0a)がLF(0x0a)へと正規化される。
+ * @param text 文字列
+ * @return 正規化の行われた文字列。
+ */
+ protected static String normalizeBreak(String text){
+ String result = text;
+
+ result = result.replace(CRLF, LF);
+ result = result.replace(CR, LF);
+
+ return result;
+ }
+
+ /**
+ * 文字列を指定されたバイト長で出力する。
+ * 文字列の改行記号はLF(0x0a)に正規化される。
+ * エンコード結果がバイト長に満たない場合は
+ * 1つの0x00及びそれに続く複数の0xfdがパディングされる。
+ * @param text 文字列
+ * @param maxByteLength バイト長指定
+ * @throws IOException 出力エラー
+ * @throws IllegalPmdTextException エンコード結果が
+ * 指定バイト長をはみ出した。
+ */
+ protected void dumpText(String text, int maxByteLength)
+ throws IOException, IllegalTextExportException{
+ dumpFixedW31j(text, maxByteLength, FDFILLER);
+ return;
+ }
+
+ /**
+ * モデルデータをPMDファイル形式で出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ * @throws IllegalPmdDataException モデルデータに不備が発見された
+ */
+ public void dumpPmdModel(PmdModel model)
+ throws IOException, IllegalPmdDataException{
+ try{
+ dumpBasic(model);
+ dumpVertexList(model);
+ dumpSurfaceList(model);
+ dumpMaterialList(model);
+ dumpBoneList(model);
+ dumpIKChainList(model);
+ dumpMorphList(model);
+ dumpMorphGroup(model);
+ dumpBoneGroupList(model);
+ }catch(IllegalTextExportException e){
+ throw new IllegalPmdDataException(e);
+ }
+
+ return;
+ }
+
+ /**
+ * モデル基本情報を出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ * @throws IllegalPmdTextException モデル名もしくは説明が長すぎる
+ */
+ private void dumpBasic(PmdModel model)
+ throws IOException, IllegalTextExportException{
+ for(int idx=0; idx < MAGIC_BYTES.length; idx++){
+ dumpByte(MAGIC_BYTES[idx]);
+ }
+
+ String modelName = model.getModelName() .getPrimaryText();
+ String description = model.getDescription().getPrimaryText();
+
+ dumpText(modelName, PmdLimits.MAXBYTES_MODELNAME);
+ dumpText(description, PmdLimits.MAXBYTES_MODELDESC);
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * 頂点リストを出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ */
+ private void dumpVertexList(PmdModel model)
+ throws IOException{
+ List<Vertex> vList = model.getVertexList();
+
+ int vertexNum = vList.size();
+ dumpLeInt(vertexNum);
+
+ for(Vertex vertex : vList){
+ dumpVertex(vertex);
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * 個別の頂点データを出力する。
+ * @param vertex 頂点
+ * @throws IOException 出力エラー
+ */
+ private void dumpVertex(Vertex vertex)
+ throws IOException{
+ MkPos3D position = vertex.getPosition();
+ dumpPos3D(position);
+
+ MkVec3D normal = vertex.getNormal();
+ dumpVec3D(normal);
+
+ MkPos2D uv = vertex.getUVPosition();
+ dumpPos2d(uv);
+
+ BoneInfo boneA = vertex.getBoneA();
+ BoneInfo boneB = vertex.getBoneB();
+ dumpSerialIdAsShort(boneA);
+ dumpSerialIdAsShort(boneB);
+
+ int weight = vertex.getWeightA();
+ dumpByte((byte)weight);
+
+ byte edgeFlag;
+ boolean hasEdge = vertex.getEdgeAppearance();
+ if(hasEdge) edgeFlag = 0x00;
+ else edgeFlag = 0x01;
+ dumpByte(edgeFlag);
+
+ return;
+ }
+
+ /**
+ * 面リストを出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ */
+ private void dumpSurfaceList(PmdModel model)
+ throws IOException{
+ int surfaceNum = 0;
+ List<Material> materialList = model.getMaterialList();
+ for(Material material : materialList){
+ surfaceNum += material.getSurfaceList().size();
+ }
+ dumpLeInt(surfaceNum * 3);
+
+ Vertex[] triangle = new Vertex[3];
+ for(Material material : materialList){
+ for(Surface surface : material){
+ surface.getTriangle(triangle);
+ dumpLeShort(triangle[0].getSerialNumber());
+ dumpLeShort(triangle[1].getSerialNumber());
+ dumpLeShort(triangle[2].getSerialNumber());
+ }
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * マテリアル素材リストを出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ * @throws IllegalPmdTextException シェーディングファイル情報が長すぎる
+ */
+ private void dumpMaterialList(PmdModel model)
+ throws IOException, IllegalTextExportException{
+ List<Material> materialList = model.getMaterialList();
+
+ int materialNum = materialList.size();
+ dumpLeInt(materialNum);
+
+ float[] rgba = new float[4];
+
+ for(Material material : materialList){
+ Color diffuse = material.getDiffuseColor();
+ diffuse.getRGBComponents(rgba);
+ dumpLeFloat(rgba[0]);
+ dumpLeFloat(rgba[1]);
+ dumpLeFloat(rgba[2]);
+ dumpLeFloat(rgba[3]);
+
+ float shininess = material.getShininess();
+ dumpLeFloat(shininess);
+
+ Color specular = material.getSpecularColor();
+ specular.getRGBComponents(rgba);
+ dumpLeFloat(rgba[0]);
+ dumpLeFloat(rgba[1]);
+ dumpLeFloat(rgba[2]);
+
+ Color ambient = material.getAmbientColor();
+ ambient.getRGBComponents(rgba);
+ dumpLeFloat(rgba[0]);
+ dumpLeFloat(rgba[1]);
+ dumpLeFloat(rgba[2]);
+
+ ShadeInfo shade = material.getShadeInfo();
+ int toonIdx = shade.getToonIndex();
+ dumpByte(toonIdx);
+
+ byte edgeFlag;
+ boolean showEdge = material.getEdgeAppearance();
+ if(showEdge) edgeFlag = 0x01;
+ else edgeFlag = 0x00;
+ dumpByte(edgeFlag);
+
+ int surfaceNum = material.getSurfaceList().size();
+ dumpLeInt(surfaceNum * 3);
+
+ dumpShadeFileInfo(shade);
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * シェーディングファイル情報を出力する。
+ * @param shade シェーディング情報
+ * @throws IOException 出力エラー
+ * @throws IllegalTextExportException ファイル名が長すぎる
+ */
+ private void dumpShadeFileInfo(ShadeInfo shade)
+ throws IOException, IllegalTextExportException{
+ String textureFile = shade.getTextureFileName();
+ String spheremapFile = shade.getSpheremapFileName();
+
+ StringBuilder text = new StringBuilder();
+ if(textureFile != null) text.append(textureFile);
+ if(spheremapFile != null && spheremapFile.length() > 0){
+ text.append('*')
+ .append(spheremapFile);
+ }
+
+ byte[] filler;
+ if(text.length() <= 0) filler = NULLFILLER;
+ else filler = FDFILLER;
+
+ dumpFixedW31j(text.toString(),
+ PmdLimits.MAXBYTES_TEXTUREFILENAME,
+ filler );
+
+ return;
+ }
+
+ /**
+ * ボーンリストを出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ * @throws IllegalPmdTextException ボーン名が長すぎる
+ */
+ private void dumpBoneList(PmdModel model)
+ throws IOException, IllegalTextExportException{
+ List<BoneInfo> boneList = model.getBoneList();
+
+ int boneNum = boneList.size();
+ dumpLeShort(boneNum);
+
+ for(BoneInfo bone : boneList){
+ dumpBone(bone);
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * 個別のボーン情報を出力する。
+ * @param bone ボーン情報
+ * @throws IOException 出力エラー
+ * @throws IllegalPmdTextException ボーン名が長すぎる
+ */
+ private void dumpBone(BoneInfo bone)
+ throws IOException, IllegalTextExportException{
+ String boneName = bone.getBoneName().getPrimaryText();
+ dumpText(boneName, PmdLimits.MAXBYTES_BONENAME);
+
+ BoneInfo prev = bone.getPrevBone();
+ if(prev != null) dumpSerialIdAsShort(prev);
+ else dumpLeShort(NOPREVBONE_ID);
+
+ BoneInfo next = bone.getNextBone();
+ if(next != null) dumpSerialIdAsShort(next);
+ else dumpLeShort(NONEXTBONE_ID);
+
+ BoneType type = bone.getBoneType();
+ dumpByte(type.encode());
+
+ if(type == BoneType.LINKEDROT){
+ int ratio = bone.getRotationRatio();
+ dumpLeShort(ratio);
+ }else{
+ BoneInfo srcBone = bone.getSrcBone();
+ if(srcBone != null) dumpSerialIdAsShort(srcBone);
+ else dumpLeShort(NOIKBONE_ID);
+ }
+
+ MkPos3D position = bone.getPosition();
+ dumpPos3D(position);
+
+ return;
+ }
+
+ /**
+ * IKチェーンリストを出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ */
+ private void dumpIKChainList(PmdModel model)
+ throws IOException{
+ List<IKChain> ikChainList = model.getIKChainList();
+
+ int ikNum = ikChainList.size();
+ dumpLeShort(ikNum);
+
+ for(IKChain chain : ikChainList){
+ dumpIKChain(chain);
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * IKチェーンを出力する。
+ * @param chain IKチェーン
+ * @throws IOException 出力エラー
+ */
+ // TODO ボーンリストから自動抽出できる情報ではないのか?
+ private void dumpIKChain(IKChain chain)
+ throws IOException{
+ BoneInfo ikBone = chain.getIkBone();
+ dumpSerialIdAsShort(ikBone);
+
+ List<BoneInfo> boneList = chain.getChainedBoneList();
+
+ BoneInfo bone1st = boneList.get(0);
+ dumpSerialIdAsShort(bone1st);
+
+ int boneNum = boneList.size();
+ dumpByte(boneNum - 1);
+
+ int depth = chain.getIKDepth();
+ float weight = chain.getIKWeight();
+
+ dumpLeShort(depth);
+ dumpLeFloat(weight);
+
+ for(int idx = 1; idx < boneNum; idx++){ // リストの2番目以降全て
+ BoneInfo bone = boneList.get(idx);
+ dumpSerialIdAsShort(bone);
+ }
+
+ return;
+ }
+
+ /**
+ * モーフリストを出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ * @throws IllegalPmdTextException モーフ名が長すぎる
+ */
+ private void dumpMorphList(PmdModel model)
+ throws IOException, IllegalTextExportException{
+ Map<MorphType, List<MorphPart>> morphMap = model.getMorphMap();
+ Set<MorphType> typeSet = morphMap.keySet();
+ List<MorphVertex> mergedMorphVertexList = model.mergeMorphVertex();
+
+ int totalMorphPart = 0;
+ for(MorphType type : typeSet){
+ List<MorphPart> partList = morphMap.get(type);
+ if(partList == null) continue;
+ totalMorphPart += partList.size();
+ }
+
+ if(totalMorphPart <= 0){
+ dumpLeShort(0);
+ return;
+ }else{
+ totalMorphPart++; // baseの分
+ dumpLeShort(totalMorphPart);
+ }
+
+ dumpText("base", PmdLimits.MAXBYTES_MORPHNAME);
+ int totalVertex = mergedMorphVertexList.size();
+ dumpLeInt(totalVertex);
+ dumpByte(MorphType.BASE.encode());
+ for(MorphVertex morphVertex : mergedMorphVertexList){
+ Vertex baseVertex = morphVertex.getBaseVertex();
+ dumpLeInt(baseVertex.getSerialNumber());
+ dumpPos3D(baseVertex.getPosition());
+ }
+
+ for(MorphType type : typeSet){
+ List<MorphPart> partList = morphMap.get(type);
+ if(partList == null) continue;
+ for(MorphPart part : partList){
+ dumpText(part.getMorphName().getPrimaryText(),
+ PmdLimits.MAXBYTES_MORPHNAME );
+ List<MorphVertex> morphVertexList = part.getMorphVertexList();
+ dumpLeInt(morphVertexList.size());
+ dumpByte(part.getMorphType().encode());
+
+ for(MorphVertex morphVertex : morphVertexList){
+ dumpLeInt(morphVertex.getSerialNumber());
+ dumpPos3D(morphVertex.getOffset());
+ }
+ }
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * モーフグループを出力する。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ */
+ private void dumpMorphGroup(PmdModel model)
+ throws IOException{
+ Map<MorphType, List<MorphPart>> morphMap = model.getMorphMap();
+ Set<MorphType> typeSet = morphMap.keySet();
+
+ int totalMorph = 0;
+ for(MorphType type : typeSet){
+ List<MorphPart> partList = morphMap.get(type);
+ if(partList == null) continue;
+ totalMorph += partList.size();
+ }
+ dumpByte(totalMorph);
+
+ List<MorphType> typeList = new LinkedList<MorphType>();
+ for(MorphType type : typeSet){
+ assert ! type.isBase();
+ typeList.add(type);
+ }
+ Collections.reverse(typeList); // 一応本家と互換性を
+
+ for(MorphType type : typeList){
+ List<MorphPart> partList = morphMap.get(type);
+ if(partList == null) continue;
+ for(MorphPart part : partList){
+ dumpSerialIdAsShort(part);
+ }
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * ボーングループリストを出力する。
+ * デフォルトボーングループ内訳は出力されない。
+ * @param model モデルデータ
+ * @throws IOException 出力エラー
+ * @throws IllegalTextExportException ボーングループ名が長すぎる
+ */
+ private void dumpBoneGroupList(PmdModel model)
+ throws IOException, IllegalTextExportException{
+ List<BoneGroup> groupList = model.getBoneGroupList();
+ int groupNum = groupList.size();
+ dumpByte(groupNum - 1);
+
+ int dispBoneNum = 0;
+ for(BoneGroup group : groupList){
+ if(group.isDefaultBoneGroup()) continue;
+ dumpFixedW31j(group.getGroupName().getPrimaryText(),
+ PmdLimits.MAXBYTES_BONEGROUPNAME, LFFILLER );
+ dispBoneNum += group.getBoneList().size();
+ }
+ dumpLeInt(dispBoneNum);
+
+ for(BoneGroup group : groupList){
+ if(group.isDefaultBoneGroup()) continue;
+ for(BoneInfo bone : group){
+ dumpSerialIdAsShort(bone);
+ int groupId = group.getSerialNumber();
+ dumpByte(groupId);
+ }
+ }
+
+ flush();
+
+ return;
+ }
+
+ /**
+ * 各種通し番号をshort値で出力する。
+ * short値に収まらない上位ビットは捨てられる。
+ * @param obj 番号づけられたオブジェクト
+ * @throws IOException 出力エラー
+ */
+ protected void dumpSerialIdAsShort(SerialNumbered obj)
+ throws IOException{
+ int serialId = obj.getSerialNumber();
+ dumpLeShort(serialId);
+ return;
+ }
+
+ /**
+ * 2次元位置情報を出力する。
+ * @param position 2次元位置情報
+ * @throws IOException 出力エラー
+ */
+ protected void dumpPos2d(MkPos2D position) throws IOException{
+ float xPos = (float) position.getXpos();
+ float yPos = (float) position.getYpos();
+
+ dumpLeFloat(xPos);
+ dumpLeFloat(yPos);
+
+ return;
+ }
+
+ /**
+ * 3次元位置情報を出力する。
+ * @param position 3次元位置情報
+ * @throws IOException 出力エラー
+ */
+ protected void dumpPos3D(MkPos3D position) throws IOException{
+ float xPos = (float) position.getXpos();
+ float yPos = (float) position.getYpos();
+ float zPos = (float) position.getZpos();
+
+ dumpLeFloat(xPos);
+ dumpLeFloat(yPos);
+ dumpLeFloat(zPos);
+
+ return;
+ }
+
+ /**
+ * 3次元ベクトル情報を出力する。
+ * @param vector 3次元ベクトル
+ * @throws IOException 出力エラー
+ */
+ protected void dumpVec3D(MkVec3D vector) throws IOException{
+ float xVal = (float) vector.getXVal();
+ float yVal = (float) vector.getYVal();
+ float zVal = (float) vector.getZVal();
+
+ dumpLeFloat(xVal);
+ dumpLeFloat(yVal);
+ dumpLeFloat(zVal);
+
+ return;
+ }
+
+}