/***********************************************************************
* mt4j Copyright (c) 2008 - 2009 C.Ruff, Fraunhofer-Gesellschaft All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
***********************************************************************/
package org.mt4j.util.modelImporter.file3ds;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
import mri.v3ds.Face3ds;
import mri.v3ds.FaceMat3ds;
import mri.v3ds.Material3ds;
import mri.v3ds.Mesh3ds;
import mri.v3ds.Scene3ds;
import mri.v3ds.TexCoord3ds;
import mri.v3ds.TextDecode3ds;
import mri.v3ds.Vertex3ds;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.SimpleLayout;
import org.mt4j.MTApplication;
import org.mt4j.components.visibleComponents.GeometryInfo;
import org.mt4j.components.visibleComponents.shapes.mesh.MTTriangleMesh;
import org.mt4j.util.MT4jSettings;
import org.mt4j.util.TriangleNormalGenerator;
import org.mt4j.util.math.Tools3D;
import org.mt4j.util.math.Vertex;
import org.mt4j.util.modelImporter.ModelImporterFactory;
import org.mt4j.util.opengl.GLTexture;
import org.mt4j.util.opengl.GLTextureSettings;
import org.mt4j.util.opengl.GLTexture.EXPANSION_FILTER;
import org.mt4j.util.opengl.GLTexture.SHRINKAGE_FILTER;
import org.mt4j.util.opengl.GLTexture.TEXTURE_TARGET;
import org.mt4j.util.opengl.GLTexture.WRAP_MODE;
import processing.core.PApplet;
import processing.core.PImage;
/**
* A factory for creating Model3dsFile objects.
* @author Christopher Ruff
*/
public class Model3dsFileFactory extends ModelImporterFactory{
private static final Logger logger = Logger.getLogger(Model3dsFileFactory.class.getName());
static{
logger.setLevel(Level.ERROR);
SimpleLayout l = new SimpleLayout();
ConsoleAppender ca = new ConsoleAppender(l);
logger.addAppender(ca);
}
private PApplet pa;
private Map<String, PImage> textureCache ;
/**
* Load model.
*
* @param pa the pa
* @param pathToModel the path to model
* @param creaseAngle the crease angle
* @param flipTextureY flip texture y
* @param flipTextureX flip texture x
*
* @return the MT triangle meshes[]
*
* @throws FileNotFoundException the file not found exception
*/
public MTTriangleMesh[] loadModelImpl(PApplet pa, String pathToModel, float creaseAngle, boolean flipTextureY, boolean flipTextureX) throws FileNotFoundException{
long timeA = System.currentTimeMillis();
this.pa = pa;
ArrayList<MTTriangleMesh> returnMeshList = new ArrayList<MTTriangleMesh>();
TriangleNormalGenerator normalGenerator = new TriangleNormalGenerator();
// normalGenerator.setDebug(debug);
HashMap<Integer, Group> materialIdToGroup = new HashMap<Integer, Group>();
//TODO implement
if (textureCache != null)
textureCache.clear();
textureCache = new WeakHashMap<String, PImage>();
Scene3ds scene = null;
try{
TextDecode3ds decode = new TextDecode3ds();
// int level = Scene3ds.DECODE_USED_PARAMS_AND_CHUNKS; //Scene3ds.DECODE_ALL; DECODE_USED_PARAMS, DECODE_USED_PARAMS_AND_CHUNKS
int level = Scene3ds.DECODE_ALL;
//LOAD all meshes from file into scene object
File file = new File(pathToModel);
if (file.exists()){
scene = new Scene3ds(file, decode, level );
}else{
InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(pathToModel);
if (in == null){
in = pa.getClass().getResourceAsStream(pathToModel);
}
if (in != null){
scene = new Scene3ds(in, decode, level );
}else{
throw new FileNotFoundException("File not found: " + file.getAbsolutePath());
}
}
if (debug)
logger.debug("\n-> Loading model: " + file.getName() + " <-");
if (debug){
//Go through all MATERIALS
logger.debug("\nNum Scene Materials: " + scene.materials() );
for( int m=0; m < scene.materials(); m++ ){
Material3ds mat = scene.material( m );
logger.debug(" Material " + m + ": \" " + mat.name() + "\"");
}
logger.debug("");
}
///////// Go through all MESHES //////////////////////////
for( int i=0; i<scene.meshes(); i++ ) {
Mesh3ds m = scene.mesh( i );
if (debug){
int texMapType = m.texMapType();
logger.debug("Texture coordinates provided: " + m.texCoords());
logger.debug("Texture mapping type: " + texMapType);
logger.debug("Mesh:" + m.name() + " Pivot:" + m.pivot());
}
/*
XYZTrack3ds pos = m.positionTrack();
RotationTrack3ds rot = m.rotationTrack();
XYZTrack3ds sc = m.scaleTrack();
//FIXME .track and key(i) werden nicht zur verf�gung gestellt?!? aber in javadoc
*/
if (debug){
logger.debug("->Processing mesh: " + i + " of " + scene.meshes() + " Name: \"" + m.name() + "\"");
logger.debug(" Num Faces: " + m.faces());
logger.debug(" Num Vertices: " + m.vertices());
logger.debug(" Num TextureCoordinates: " + m.texCoords());
logger.debug("");
}
//Create the arrays needed for the cruncher
Vertex[] vertices = new Vertex[m.vertices()];
int[] indices = new int[m.faces()*3];
int[] texCoordIndices = new int[m.faces()*3];
float[][] textureCoords = new float[m.texCoords()][2];
//Fill Vertices array
for (int j = 0; j < m.vertices(); j++) {
Vertex3ds vert = m.vertex(j);
if (this.flipY){
vertices[j] = new Vertex(vert.X, -vert.Y, vert.Z, -1,-1);
}else{
vertices[j] = new Vertex(vert.X, vert.Y, vert.Z, -1,-1);
}
if (m.texCoords()> j)
textureCoords[j] = new float[]{m.texCoord(j).U, m.texCoord(j).V };
}
//Fill texcoords array
for (int j = 0; j < m.texCoords(); j++) {
TexCoord3ds tex = m.texCoord(j);
float[] texCoord = new float[2];
texCoord[0] = tex.U;
texCoord[1] = tex.V;
textureCoords[j] = texCoord;
}
//TODO so werden gleiche materials in verschiedenen meshes nicht zu einem mesh gemacht!
//also m�sste also ohne clear machen und daf�r vertices + texcoords in einen grossen
//array, daf�r m�ssten indices aber per offset angepasst werden dass die wieder stimmen!
materialIdToGroup.clear();
if (m.faceMats() > 0){
//List face Materials //TODO USE LOGGERS!!
logger.debug(" Num Face-Materials: " + m.faceMats() );
for( int n = 0; n < m.faceMats(); n++ ){
FaceMat3ds fmat = m.faceMat( n );
logger.debug(" FaceMat ID: " + fmat.matIndex() );
logger.debug(" FaceMat indices: " + fmat.faceIndexes());
int[] faceIndicesForMaterial = fmat.faceIndex();
if (faceIndicesForMaterial.length <= 0){
continue;
}
//Check if there already is a group with the same material
Group group = materialIdToGroup.get(fmat.matIndex());
//If not, create a new group and save it in map
if (group == null){
group = new Group(new Integer(fmat.matIndex()).toString());
materialIdToGroup.put(fmat.matIndex(), group);
}
//Go through all pointers to the faces for this material
//and get the corresponding face
for (int j = 0; j < faceIndicesForMaterial.length; j++) {
int k = faceIndicesForMaterial[j];
Face3ds face = m.face(k);
AFace aFace = new AFace();
aFace.p0 = face.P0;
aFace.p1 = face.P1;
aFace.p2 = face.P2;
aFace.t0 = face.P0;
aFace.t1 = face.P1;
aFace.t2 = face.P2;
group.addFace(aFace);
}
}
Iterator<Integer> it = materialIdToGroup.keySet().iterator();
logger.debug("Mesh: " + m.name() + " Anzahl Groups:" + materialIdToGroup.keySet().size());
while (it.hasNext()) {
int currentGroupName = it.next();
Group currentGroup = materialIdToGroup.get(currentGroupName);
logger.debug("Current group: " + currentGroupName);
currentGroup.compileItsOwnLists(vertices, textureCoords);
//Get the new arrays
Vertex[] newVertices = currentGroup.getGroupVertices();
int[] newIndices = currentGroup.getIndexArray();
float[][] newTextureCoords = currentGroup.getGroupTexCoords();
int[] newTexIndices = currentGroup.getTexCoordIndices();
logger.debug("\nGroup: \"" + currentGroup.name + "\" ->Vertices: " + currentGroup.verticesForGroup.size()+ " ->TextureCoords: " + currentGroup.texCoordsForGroup.size() + " ->Indices: " + currentGroup.indexArray.length + " ->Texcoord Indices: " + currentGroup.texCoordIndexArray.length );
logger.debug("");
if (vertices.length > 2){
GeometryInfo geometry = null;
//Load as all vertex normals smoothed if creaseAngle == 180;
if (creaseAngle == 180){
geometry = normalGenerator.generateSmoothNormals(pa, newVertices , newIndices, newTextureCoords, newTexIndices, creaseAngle, flipTextureY, flipTextureX);
}else{
geometry = normalGenerator.generateCreaseAngleNormals(pa, newVertices, newIndices, newTextureCoords, newTexIndices, creaseAngle, flipTextureY, flipTextureX);
}
MTTriangleMesh mesh = new MTTriangleMesh(pa, geometry);
if (mesh != null){
mesh.setName(m.name() + " material: " + new Integer(currentGroupName).toString());
//Assign texture
this.assignMaterial(pathToModel, file, scene, m, currentGroupName, mesh);
if (mesh.getTexture() != null){
mesh.setTextureEnabled(true);
}else{
logger.debug("No texture could be assigned to mesh.");
}
returnMeshList.add(mesh);
}
}
}
}else{
//If there are no materials for this mesh dont split mesh into
//groups by material
//Fill indices array and texcoords array (Here: vertex index = texcoord index)
for( int faceIndex = 0; faceIndex < m.faces(); faceIndex++ ){
Face3ds f = m.face( faceIndex );
indices[faceIndex*3] = f.P0;
indices[faceIndex*3+1] = f.P1;
indices[faceIndex*3+2] = f.P2;
texCoordIndices[faceIndex*3] = f.P0;
texCoordIndices[faceIndex*3+1] = f.P1;
texCoordIndices[faceIndex*3+2] = f.P2;
}//for faces
//Create the Mesh and set a texture
if (vertices.length > 2){
//Create normals for the mesh and duplicate vertices for faces that share the same
//Vertex, but with different texture coordinates or different normals
GeometryInfo geometry = null;
//Generate normals and denormalize vertices with more than 1 texture coordinate
if (creaseAngle == 180){
geometry = normalGenerator.generateSmoothNormals(pa, vertices, indices, textureCoords, texCoordIndices, creaseAngle, flipTextureY, flipTextureX);
}else{
geometry = normalGenerator.generateCreaseAngleNormals(pa, vertices, indices, textureCoords, texCoordIndices, creaseAngle, flipTextureY, flipTextureX);
}
MTTriangleMesh mesh = new MTTriangleMesh(pa, geometry);
mesh.setName(m.name());
// this.assignMaterial(file, scene, m, sceneMaterialID, mesh);
returnMeshList.add(mesh);
}//end if vertices.lentgh > 2
}
}//for meshes
// logger.debug(decode.text());
}catch (Exception e) {
e.printStackTrace();
}
materialIdToGroup.clear();
long timeB = System.currentTimeMillis();
long delta = timeB-timeA;
logger.debug("Loaded model in: " + delta + " ms");
return (MTTriangleMesh[])returnMeshList.toArray(new MTTriangleMesh[returnMeshList.size()]);
}
/**
* Assigns the texture.
* @param pathToModel
* @param modelFile
* @param scene
* @param m
* @param sceneMaterialID
* @param mesh
*/
private void assignMaterial(String pathToModel, File modelFile, Scene3ds scene, Mesh3ds m, int sceneMaterialID, MTTriangleMesh mesh){
if (scene.materials() > 0){
if (m.faceMats() > 0){
//Just take the first material in the mesh, it could have more but we dont support more than 1 material for a mesh
// materialIndexForMesh = m.faceMat(0).matIndex();
Material3ds mat = scene.material(sceneMaterialID);
String materialName = mat.name();
if (debug)
logger.debug("Material name for mesh \"" + mesh.getName() + ":-> \"" + materialName + "\"");
materialName.trim();
materialName.toLowerCase();
//Try to load texture
try {
PImage cachedImage = textureCache.get(materialName);
if (cachedImage != null){
mesh.setTexture(cachedImage);
mesh.setTextureEnabled(true);
if (debug)
logger.debug("->Loaded texture from CACHE : \"" + materialName + "\"");
return;
}
if (modelFile.exists()){ //If model is loaded from local file system
String modelFolder = modelFile.getParent();// pathToModel.substring(), pathToModel.lastIndexOf(File.pathSeparator)
File modelFolderFile = new File (modelFolder);
if (modelFolderFile.exists() && modelFolderFile.isDirectory())
modelFolder = modelFolderFile.getAbsolutePath();
else{
modelFolder = "";
}
String[] suffix = new String[]{"jpg", "JPG", "tga" , "TGA", "bmp", "BMP", "png", "PNG", "tiff", "TIFF"};
for (int j = 0; j < suffix.length; j++) {
String suffixString = suffix[j];
//Try to load and set texture to mesh
String texturePath = modelFolder + MTApplication.separator + materialName + "." + suffixString;
File textureFile = new File(texturePath);
if (textureFile.exists()){
boolean success = textureFile.renameTo(new File(texturePath));
if (!success) {
// File was not successfully renamed
logger.debug("failed to RENAME file: " + textureFile.getAbsolutePath());
}
PImage texture = null;
if (MT4jSettings.getInstance().isOpenGlMode()){ //TODO check if render thread
PImage img = pa.loadImage(texturePath);
if (Tools3D.isPowerOfTwoDimension(img)){
texture = new GLTexture(pa, img, new GLTextureSettings(TEXTURE_TARGET.TEXTURE_2D, SHRINKAGE_FILTER.Trilinear, EXPANSION_FILTER.Bilinear, WRAP_MODE.REPEAT, WRAP_MODE.REPEAT));
}else{
texture = new GLTexture(pa, img, new GLTextureSettings(TEXTURE_TARGET.RECTANGULAR, SHRINKAGE_FILTER.Trilinear, EXPANSION_FILTER.Bilinear, WRAP_MODE.REPEAT, WRAP_MODE.REPEAT));
// ((GLTexture)texture).setFilter(SHRINKAGE_FILTER.BilinearNoMipMaps, EXPANSION_FILTER.Bilinear);
}
}else{
texture = pa.loadImage(texturePath);
}
mesh.setTexture(texture);
mesh.setTextureEnabled(true);
textureCache.put(materialName, texture);
if (debug)
logger.debug("->Loaded material texture: \"" + materialName + "\"");
break;
}
if (j+1==suffix.length){
logger.error("Couldnt load material texture: \"" + materialName + "\"");
}
}
}else{//Probably loading from jar file
PImage texture = null;
String[] suffix = new String[]{"jpg", "JPG", "tga" , "TGA", "bmp", "BMP", "png", "PNG", "tiff", "TIFF"};
for (String suffixString : suffix) {
String modelFolder = pathToModel.substring(0, pathToModel.lastIndexOf(MTApplication.separator));
String texturePath = modelFolder + MTApplication.separator + materialName + "." + suffixString;
if (MT4jSettings.getInstance().isOpenGlMode()){
PImage img = pa.loadImage(texturePath);
if (Tools3D.isPowerOfTwoDimension(img)){
texture = new GLTexture(pa, img, new GLTextureSettings(TEXTURE_TARGET.TEXTURE_2D, SHRINKAGE_FILTER.Trilinear, EXPANSION_FILTER.Bilinear, WRAP_MODE.REPEAT, WRAP_MODE.REPEAT));
}else{
texture = new GLTexture(pa, img, new GLTextureSettings(TEXTURE_TARGET.RECTANGULAR, SHRINKAGE_FILTER.Trilinear, EXPANSION_FILTER.Bilinear, WRAP_MODE.REPEAT, WRAP_MODE.REPEAT));
// ((GLTexture)texture).setFilter(SHRINKAGE_FILTER.BilinearNoMipMaps, EXPANSION_FILTER.Bilinear);
}
}else{
texture = pa.loadImage(texturePath);
}
mesh.setTexture(texture);
mesh.setTextureEnabled(true);
textureCache.put(materialName, texture);
if (debug)
logger.debug("->Loaded material texture: \"" + materialName + "\"");
break;
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}//if (m.faceMats() > 0)
}//if (scene.materials() > 0)
}
private boolean debug = true;
private boolean flipY = true;
public void setDebug(boolean debug) {
this.debug = debug;
if (debug)
logger.setLevel(Level.DEBUG);
else
logger.setLevel(Level.ERROR);
}
public void setFlipY(boolean flipY){
this.flipY = flipY;
}
/**
* A class representing one group in a .3ds file
* @author C.Ruff
*
*/
private class Group{
private String name;
private ArrayList<AFace> faces;
private ArrayList<Vertex> verticesForGroup;
private HashMap<Integer, Integer> oldIndexToNewIndex;
private ArrayList<float[]> texCoordsForGroup;
private HashMap<Integer, Integer> oldTexIndexToNewTexIndex;
private int[] indexArray;
private int[] texCoordIndexArray;
public Group(String name){
this.name = name;
faces = new ArrayList<AFace>();
verticesForGroup = new ArrayList<Vertex>();
oldIndexToNewIndex = new HashMap<Integer, Integer>();
texCoordsForGroup = new ArrayList<float[]>();
oldTexIndexToNewTexIndex = new HashMap<Integer, Integer>();
indexArray = new int[0];
texCoordIndexArray = new int[0];
//name = "default";
}
public void addFace(AFace face){
faces.add(face);
}
//Irgendwann am ende ausf�hren
//=>f�r jede gruppe eigene liste von vertices on indices speichern
//aus hauptlisten rausholen
/**
* Uses the faces attached to this group during the parsing process and the lists with
* all vertex and all texture coordinates of the obj file to create arrays for this group
* with vertices and texture coords that only belong to this single group.
*
* @param allFileVerts
* @param allTexCoords
*/
public void compileItsOwnLists(Vertex[] allFileVerts, float[][] allTexCoords){
indexArray = new int[faces.size()*3];
if (allTexCoords.length > 0){
texCoordIndexArray = new int[faces.size()*3];
}
for (int i = 0; i < faces.size(); i++) {
AFace currentFace = faces.get(i);
Vertex v0 = allFileVerts[currentFace.p0];
Vertex v1 = allFileVerts[currentFace.p1];
Vertex v2 = allFileVerts[currentFace.p2];
if ( allTexCoords.length > currentFace.t0
&& allTexCoords.length > currentFace.t1
&& allTexCoords.length > currentFace.t2
){
float[] texV0 = allTexCoords[currentFace.t0];
float[] texV1 = allTexCoords[currentFace.t1];
float[] texV2 = allTexCoords[currentFace.t2];
//Etwas redundant immer wieder zu machen beim gleichen vertex..whatever
v0.setTexCoordU(texV0[0]);
v0.setTexCoordV(texV0[1]);
v1.setTexCoordU(texV1[0]);
v1.setTexCoordV(texV1[1]);
v2.setTexCoordU(texV2[0]);
v2.setTexCoordV(texV2[1]);
//Check if there is a texture index in the hashtable at the faces texture pointer
//if not, create a new index = the end of thexcoords list, and put the pointer into the hash
//if yes, point the faces texture pointer to the pointer in the hash
//This process maps the texture coords and indices of all the groups in the obj
//file to only this groups texture coord list and texture indices, the indices
//are created from the index in the thex coord list when they are put in
//Only the texture coordinates are added to the list that have not been adressed
//in the texture indices pointers in the faces
//Same texture pointers will point to the same texcoord in the list
Integer oldToNewT0 = oldTexIndexToNewTexIndex.get(currentFace.t0);
if (oldToNewT0 != null){
currentFace.t0 = oldToNewT0;
}else{
int newIndex = texCoordsForGroup.size();
texCoordsForGroup.add(texV0);
oldTexIndexToNewTexIndex.put(currentFace.t0, newIndex);
currentFace.t0 = newIndex;
}
Integer oldToNewT1 = oldTexIndexToNewTexIndex.get(currentFace.t1);
if (oldToNewT1 != null){
currentFace.t1 = oldToNewT1;
}else{
int newIndex = texCoordsForGroup.size();
texCoordsForGroup.add(texV1);
oldTexIndexToNewTexIndex.put(currentFace.t1, newIndex);
currentFace.t1 = newIndex;
}
Integer oldToNewT2 = oldTexIndexToNewTexIndex.get(currentFace.t2);
if (oldToNewT2 != null){
currentFace.t2 = oldToNewT2;
}else{
int newIndex = texCoordsForGroup.size();
texCoordsForGroup.add(texV2);
oldTexIndexToNewTexIndex.put(currentFace.t2, newIndex);
currentFace.t2 = newIndex;
}
}
//Do the same for the vertices.
//Create a new vertex pointer when adding the vertex to the list
Integer oldToNewP0 = oldIndexToNewIndex.get(currentFace.p0);
if (oldToNewP0 != null){
//index of the old vertex list has already been mapped to a new one here -> use the new index in the face
currentFace.p0 = oldToNewP0;
}else{
int newIndex = verticesForGroup.size();
verticesForGroup.add(v0);
//mark that the former index (for exmample 323) is now at new index (f.e. 1)
oldIndexToNewIndex.put(currentFace.p0, newIndex);
currentFace.p0 = newIndex;
}
Integer oldToNewP1 = oldIndexToNewIndex.get(currentFace.p1);
if (oldToNewP1 != null){
currentFace.p1 = oldToNewP1;
}else{
int newIndex = verticesForGroup.size();
verticesForGroup.add(v1);
oldIndexToNewIndex.put(currentFace.p1, newIndex);
currentFace.p1 = newIndex;
}
Integer oldToNewP2 = oldIndexToNewIndex.get(currentFace.p2);
if (oldToNewP2 != null){
currentFace.p2 = oldToNewP2;
}else{
int newIndex = verticesForGroup.size();
verticesForGroup.add(v2);
oldIndexToNewIndex.put(currentFace.p2, newIndex);
currentFace.p2 = newIndex;
}
indexArray[i*3] = currentFace.p0;
indexArray[i*3+1] = currentFace.p1;
indexArray[i*3+2] = currentFace.p2;
if (allTexCoords.length > 0){
texCoordIndexArray[i*3] = currentFace.t0;
texCoordIndexArray[i*3+1] = currentFace.t1;
texCoordIndexArray[i*3+2] = currentFace.t2;
}
}
}
public int[] getIndexArray() {
return indexArray;
}
public String getName() {
return name;
}
public int[] getTexCoordIndices() {
return texCoordIndexArray;
}
public float[][] getGroupTexCoords() {
return texCoordsForGroup.toArray(new float[this.texCoordsForGroup.size()][]);
}
public Vertex[] getGroupVertices() {
return verticesForGroup.toArray(new Vertex[this.verticesForGroup.size()]);
}
}
/**
* A class representing a face in the obj file.
* Has pointers into the vertex and the texture arrays.
*
* @author C.Ruff
*
*/
private class AFace{
int p0;
int p1;
int p2;
int t0;
int t1;
int t2;
public AFace(){
p0 = -1;
p1 = -1;
p2 = -1;
t0 = 0;
t1 = 0;
t2 = 0;
}
}
}