/*
* model exporter for pmd-file
*
* License : The MIT License
* Copyright(c) 2010 MikuToga Partners
*/
package jp.sourceforge.mikutoga.pmd.model.binio;
import jp.sourceforge.mikutoga.pmd.IllegalPmdDataException;
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.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.MorphType;
import jp.sourceforge.mikutoga.pmd.model.BoneGroup;
import jp.sourceforge.mikutoga.pmd.model.BoneInfo;
import jp.sourceforge.mikutoga.pmd.model.IKChain;
import jp.sourceforge.mikutoga.pmd.model.Material;
import jp.sourceforge.mikutoga.pmd.model.MorphPart;
import jp.sourceforge.mikutoga.pmd.model.MorphVertex;
import jp.sourceforge.mikutoga.pmd.model.PmdModel;
import jp.sourceforge.mikutoga.pmd.model.SerialNumbered;
import jp.sourceforge.mikutoga.pmd.model.ShadeInfo;
import jp.sourceforge.mikutoga.pmd.model.Surface;
import jp.sourceforge.mikutoga.pmd.model.Vertex;
import jp.sourceforge.mikutoga.pmd.parser.PmdLimits;
/**
* PMDファイルのエクスポーター(拡張無し基本フォーマット)。
*
* 英名対応以降の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 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 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 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 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 ik = bone.getIKBone();
if(ik != null) dumpSerialIdAsShort(ik);
else dumpLeShort(NOIKBONE_ID);
}
MkPos3D position = bone.getPosition();
dumpPos3D(position);
return;
}
/**
* IKチェーンリストを出力する。
* @param model モデルデータ
* @throws IOException 出力エラー
*/
private void dumpIKChainList(PmdModel model)
throws IOException{
List 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 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> morphMap = model.getMorphMap();
Set typeSet = morphMap.keySet();
List mergedMorphVertexList = model.mergeMorphVertex();
int totalMorphPart = 0;
for(MorphType type : typeSet){
List 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 partList = morphMap.get(type);
if(partList == null) continue;
for(MorphPart part : partList){
dumpText(part.getMorphName().getPrimaryText(),
PmdLimits.MAXBYTES_MORPHNAME );
List 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> morphMap = model.getMorphMap();
Set typeSet = morphMap.keySet();
int totalMorph = 0;
for(MorphType type : typeSet){
List partList = morphMap.get(type);
if(partList == null) continue;
totalMorph += partList.size();
}
dumpByte(totalMorph);
List typeList = new LinkedList();
for(MorphType type : typeSet){
assert ! type.isBase();
typeList.add(type);
}
Collections.reverse(typeList); // 一応本家と互換性を
for(MorphType type : typeList){
List 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 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;
}
}