001package net.minecraftforge.client.model.obj;
002
003import java.io.BufferedReader;
004import java.io.IOException;
005import java.io.InputStream;
006import java.io.InputStreamReader;
007import java.net.URL;
008import java.util.ArrayList;
009import java.util.regex.Matcher;
010import java.util.regex.Pattern;
011
012import net.minecraft.client.renderer.Tessellator;
013import net.minecraftforge.client.model.IModelCustom;
014import net.minecraftforge.client.model.ModelFormatException;
015
016import org.lwjgl.opengl.GL11;
017
018import cpw.mods.fml.relauncher.Side;
019import cpw.mods.fml.relauncher.SideOnly;
020
021/**
022 *  Wavefront Object importer
023 *  Based heavily off of the specifications found at http://en.wikipedia.org/wiki/Wavefront_.obj_file
024 */
025@SideOnly(Side.CLIENT)
026public class WavefrontObject implements IModelCustom
027{
028
029    private static Pattern vertexPattern = Pattern.compile("(v( (\\-){0,1}\\d+\\.\\d+){3,4} *\\n)|(v( (\\-){0,1}\\d+\\.\\d+){3,4} *$)");
030    private static Pattern vertexNormalPattern = Pattern.compile("(vn( (\\-){0,1}\\d+\\.\\d+){3,4} *\\n)|(vn( (\\-){0,1}\\d+\\.\\d+){3,4} *$)");
031    private static Pattern textureCoordinatePattern = Pattern.compile("(vt( (\\-){0,1}\\d+\\.\\d+){2,3} *\\n)|(vt( (\\-){0,1}\\d+\\.\\d+){2,3} *$)");
032    private static Pattern face_V_VT_VN_Pattern = Pattern.compile("(f( \\d+/\\d+/\\d+){3,4} *\\n)|(f( \\d+/\\d+/\\d+){3,4} *$)");
033    private static Pattern face_V_VT_Pattern = Pattern.compile("(f( \\d+/\\d+){3,4} *\\n)|(f( \\d+/\\d+){3,4} *$)");
034    private static Pattern face_V_VN_Pattern = Pattern.compile("(f( \\d+//\\d+){3,4} *\\n)|(f( \\d+//\\d+){3,4} *$)");
035    private static Pattern face_V_Pattern = Pattern.compile("(f( \\d+){3,4} *\\n)|(f( \\d+){3,4} *$)");
036    private static Pattern groupObjectPattern = Pattern.compile("([go]( [\\w\\d]+) *\\n)|([go]( [\\w\\d]+) *$)");
037
038    private static Matcher vertexMatcher, vertexNormalMatcher, textureCoordinateMatcher;
039    private static Matcher face_V_VT_VN_Matcher, face_V_VT_Matcher, face_V_VN_Matcher, face_V_Matcher;
040    private static Matcher groupObjectMatcher;
041
042    public ArrayList<Vertex> vertices = new ArrayList<Vertex>();
043    public ArrayList<Vertex> vertexNormals = new ArrayList<Vertex>();
044    public ArrayList<TextureCoordinate> textureCoordinates = new ArrayList<TextureCoordinate>();
045    public ArrayList<GroupObject> groupObjects = new ArrayList<GroupObject>();
046    private GroupObject currentGroupObject;
047    private String fileName;
048
049    public WavefrontObject(String fileName, URL resource) throws ModelFormatException
050    {
051        this.fileName = fileName;
052        loadObjModel(resource);
053    }
054
055    private void loadObjModel(URL fileURL) throws ModelFormatException
056    {
057        BufferedReader reader = null;
058        InputStream inputStream = null;
059
060        String currentLine = null;
061        int lineCount = 0;
062
063        try
064        {
065            inputStream = fileURL.openStream();
066            reader = new BufferedReader(new InputStreamReader(inputStream));
067
068            while ((currentLine = reader.readLine()) != null)
069            {
070                lineCount++;
071                currentLine = currentLine.replaceAll("\\s+", " ").trim();
072
073                if (currentLine.startsWith("#") || currentLine.length() == 0)
074                {
075                    continue;
076                }
077                else if (currentLine.startsWith("v "))
078                {
079                    Vertex vertex = parseVertex(currentLine, lineCount);
080                    if (vertex != null)
081                    {
082                        vertices.add(vertex);
083                    }
084                }
085                else if (currentLine.startsWith("vn "))
086                {
087                    Vertex vertex = parseVertexNormal(currentLine, lineCount);
088                    if (vertex != null)
089                    {
090                        vertexNormals.add(vertex);
091                    }
092                }
093                else if (currentLine.startsWith("vt "))
094                {
095                    TextureCoordinate textureCoordinate = parseTextureCoordinate(currentLine, lineCount);
096                    if (textureCoordinate != null)
097                    {
098                        textureCoordinates.add(textureCoordinate);
099                    }
100                }
101                else if (currentLine.startsWith("f "))
102                {
103
104                    if (currentGroupObject == null)
105                    {
106                        currentGroupObject = new GroupObject("Default");
107                    }
108
109                    Face face = parseFace(currentLine, lineCount);
110
111                    if (face != null)
112                    {
113                        currentGroupObject.faces.add(face);
114                    }
115                }
116                else if (currentLine.startsWith("g ") | currentLine.startsWith("o "))
117                {
118                    GroupObject group = parseGroupObject(currentLine, lineCount);
119
120                    if (group != null)
121                    {
122                        if (currentGroupObject != null)
123                        {
124                            groupObjects.add(currentGroupObject);
125                        }
126                    }
127
128                    currentGroupObject = group;
129                }
130            }
131
132            groupObjects.add(currentGroupObject);
133        }
134        catch (IOException e)
135        {
136            throw new ModelFormatException("IO Exception reading model format", e);
137        }
138        finally
139        {
140            try
141            {
142                reader.close();
143            }
144            catch (IOException e)
145            {
146                // hush
147            }
148
149            try
150            {
151                inputStream.close();
152            }
153            catch (IOException e)
154            {
155                // hush
156            }
157        }
158    }
159
160    public void renderAll()
161    {
162        Tessellator tessellator = Tessellator.instance;
163
164        if (currentGroupObject != null)
165        {
166            tessellator.startDrawing(currentGroupObject.glDrawingMode);
167        }
168        else
169        {
170            tessellator.startDrawing(GL11.GL_TRIANGLES);
171        }
172
173        for (GroupObject groupObject : groupObjects)
174        {
175            groupObject.render(tessellator);
176        }
177
178        tessellator.draw();
179    }
180
181    public void renderOnly(String... groupNames)
182    {
183        for (GroupObject groupObject : groupObjects)
184        {
185            for (String groupName : groupNames)
186            {
187                if (groupName.equalsIgnoreCase(groupObject.name))
188                {
189                    groupObject.render();
190                }
191            }
192        }
193    }
194
195    public void renderPart(String partName)
196    {
197        for (GroupObject groupObject : groupObjects)
198        {
199            if (partName.equalsIgnoreCase(groupObject.name))
200            {
201                groupObject.render();
202            }
203        }
204    }
205
206    public void renderAllExcept(String... excludedGroupNames)
207    {
208        for (GroupObject groupObject : groupObjects)
209        {
210            for (String excludedGroupName : excludedGroupNames)
211            {
212                if (!excludedGroupName.equalsIgnoreCase(groupObject.name))
213                {
214                    groupObject.render();
215                }
216            }
217        }
218    }
219
220    private Vertex parseVertex(String line, int lineCount) throws ModelFormatException
221    {
222        Vertex vertex = null;
223
224        if (isValidVertexLine(line))
225        {
226            line = line.substring(line.indexOf(" ") + 1);
227            String[] tokens = line.split(" ");
228
229            try
230            {
231                if (tokens.length == 2)
232                {
233                    return new Vertex(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]));
234                }
235                else if (tokens.length == 3)
236                {
237                    return new Vertex(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]));
238                }
239            }
240            catch (NumberFormatException e)
241            {
242                throw new ModelFormatException(String.format("Number formatting error at line %d",lineCount), e);
243            }
244        }
245        else
246        {
247            throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Incorrect format");
248        }
249
250        return vertex;
251    }
252
253    private Vertex parseVertexNormal(String line, int lineCount) throws ModelFormatException
254    {
255        Vertex vertexNormal = null;
256
257        if (isValidVertexNormalLine(line))
258        {
259            line = line.substring(line.indexOf(" ") + 1);
260            String[] tokens = line.split(" ");
261
262            try
263            {
264                if (tokens.length == 3)
265                    return new Vertex(Float.parseFloat(tokens[0]), Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]));
266            }
267            catch (NumberFormatException e)
268            {
269                throw new ModelFormatException(String.format("Number formatting error at line %d",lineCount), e);
270            }
271        }
272        else
273        {
274            throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Incorrect format");
275        }
276
277        return vertexNormal;
278    }
279
280    private TextureCoordinate parseTextureCoordinate(String line, int lineCount) throws ModelFormatException
281    {
282        TextureCoordinate textureCoordinate = null;
283
284        if (isValidTextureCoordinateLine(line))
285        {
286            line = line.substring(line.indexOf(" ") + 1);
287            String[] tokens = line.split(" ");
288
289            try
290            {
291                if (tokens.length == 2)
292                    return new TextureCoordinate(Float.parseFloat(tokens[0]), 1 - Float.parseFloat(tokens[1]));
293                else if (tokens.length == 3)
294                    return new TextureCoordinate(Float.parseFloat(tokens[0]), 1 - Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]));
295            }
296            catch (NumberFormatException e)
297            {
298                throw new ModelFormatException(String.format("Number formatting error at line %d",lineCount), e);
299            }
300        }
301        else
302        {
303            throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Incorrect format");
304        }
305
306        return textureCoordinate;
307    }
308
309    private Face parseFace(String line, int lineCount) throws ModelFormatException
310    {
311        Face face = null;
312
313        if (isValidFaceLine(line))
314        {
315            face = new Face();
316
317            String trimmedLine = line.substring(line.indexOf(" ") + 1);
318            String[] tokens = trimmedLine.split(" ");
319            String[] subTokens = null;
320
321            if (tokens.length == 3)
322            {
323                if (currentGroupObject.glDrawingMode == -1)
324                {
325                    currentGroupObject.glDrawingMode = GL11.GL_TRIANGLES;
326                }
327                else if (currentGroupObject.glDrawingMode != GL11.GL_TRIANGLES)
328                {
329                    throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Invalid number of points for face (expected 4, found " + tokens.length + ")");
330                }
331            }
332            else if (tokens.length == 4)
333            {
334                if (currentGroupObject.glDrawingMode == -1)
335                {
336                    currentGroupObject.glDrawingMode = GL11.GL_QUADS;
337                }
338                else if (currentGroupObject.glDrawingMode != GL11.GL_QUADS)
339                {
340                    throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Invalid number of points for face (expected 3, found " + tokens.length + ")");
341                }
342            }
343
344            // f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 ...
345            if (isValidFace_V_VT_VN_Line(line))
346            {
347                face.vertices = new Vertex[tokens.length];
348                face.textureCoordinates = new TextureCoordinate[tokens.length];
349                face.vertexNormals = new Vertex[tokens.length];
350                
351                for (int i = 0; i < tokens.length; ++i)
352                {
353                    subTokens = tokens[i].split("/");
354
355                    face.vertices[i] = vertices.get(Integer.parseInt(subTokens[0]) - 1);
356                    face.textureCoordinates[i] = textureCoordinates.get(Integer.parseInt(subTokens[1]) - 1);
357                    face.vertexNormals[i] = vertexNormals.get(Integer.parseInt(subTokens[2]) - 1);
358                }
359
360                face.faceNormal = face.calculateFaceNormal();
361            }
362            // f v1/vt1 v2/vt2 v3/vt3 ...
363            else if (isValidFace_V_VT_Line(line))
364            {
365                face.vertices = new Vertex[tokens.length];
366                face.textureCoordinates = new TextureCoordinate[tokens.length];
367                
368                for (int i = 0; i < tokens.length; ++i)
369                {
370                    subTokens = tokens[i].split("/");
371
372                    face.vertices[i] = vertices.get(Integer.parseInt(subTokens[0]) - 1);
373                    face.textureCoordinates[i] = textureCoordinates.get(Integer.parseInt(subTokens[1]) - 1);
374                }
375
376                face.faceNormal = face.calculateFaceNormal();
377            }
378            // f v1//vn1 v2//vn2 v3//vn3 ...
379            else if (isValidFace_V_VN_Line(line))
380            {
381                face.vertices = new Vertex[tokens.length];
382                face.vertexNormals = new Vertex[tokens.length];
383                
384                for (int i = 0; i < tokens.length; ++i)
385                {
386                    subTokens = tokens[i].split("//");
387
388                    face.vertices[i] = vertices.get(Integer.parseInt(subTokens[0]) - 1);
389                    face.vertexNormals[i] = vertexNormals.get(Integer.parseInt(subTokens[1]) - 1);
390                }
391
392                face.faceNormal = face.calculateFaceNormal();
393            }
394            // f v1 v2 v3 ...
395            else if (isValidFace_V_Line(line))
396            {
397                face.vertices = new Vertex[tokens.length];
398                
399                for (int i = 0; i < tokens.length; ++i)
400                {
401                    face.vertices[i] = vertices.get(Integer.parseInt(tokens[i]) - 1);
402                }
403
404                face.faceNormal = face.calculateFaceNormal();
405            }
406            else
407            {
408                throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Incorrect format");
409            }
410        }
411        else
412        {
413            throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Incorrect format");
414        }
415
416        return face;
417    }
418
419    private GroupObject parseGroupObject(String line, int lineCount) throws ModelFormatException
420    {
421        GroupObject group = null;
422
423        if (isValidGroupObjectLine(line))
424        {
425            String trimmedLine = line.substring(line.indexOf(" ") + 1);
426
427            if (trimmedLine.length() > 0)
428            {
429                group = new GroupObject(trimmedLine);
430            }
431        }
432        else
433        {
434            throw new ModelFormatException("Error parsing entry ('" + line + "'" + ", line " + lineCount + ") in file '" + fileName + "' - Incorrect format");
435        }
436
437        return group;
438    }
439
440    /***
441     * Verifies that the given line from the model file is a valid vertex
442     * @param line the line being validated
443     * @return true if the line is a valid vertex, false otherwise
444     */
445    private static boolean isValidVertexLine(String line)
446    {
447        if (vertexMatcher != null)
448        {
449            vertexMatcher.reset();
450        }
451
452        vertexMatcher = vertexPattern.matcher(line);
453        return vertexMatcher.matches();
454    }
455
456    /***
457     * Verifies that the given line from the model file is a valid vertex normal
458     * @param line the line being validated
459     * @return true if the line is a valid vertex normal, false otherwise
460     */
461    private static boolean isValidVertexNormalLine(String line)
462    {
463        if (vertexNormalMatcher != null)
464        {
465            vertexNormalMatcher.reset();
466        }
467
468        vertexNormalMatcher = vertexNormalPattern.matcher(line);
469        return vertexNormalMatcher.matches();
470    }
471
472    /***
473     * Verifies that the given line from the model file is a valid texture coordinate
474     * @param line the line being validated
475     * @return true if the line is a valid texture coordinate, false otherwise
476     */
477    private static boolean isValidTextureCoordinateLine(String line)
478    {
479        if (textureCoordinateMatcher != null)
480        {
481            textureCoordinateMatcher.reset();
482        }
483
484        textureCoordinateMatcher = textureCoordinatePattern.matcher(line);
485        return textureCoordinateMatcher.matches();
486    }
487
488    /***
489     * Verifies that the given line from the model file is a valid face that is described by vertices, texture coordinates, and vertex normals
490     * @param line the line being validated
491     * @return true if the line is a valid face that matches the format "f v1/vt1/vn1 ..." (with a minimum of 3 points in the face, and a maximum of 4), false otherwise
492     */
493    private static boolean isValidFace_V_VT_VN_Line(String line)
494    {
495        if (face_V_VT_VN_Matcher != null)
496        {
497            face_V_VT_VN_Matcher.reset();
498        }
499
500        face_V_VT_VN_Matcher = face_V_VT_VN_Pattern.matcher(line);
501        return face_V_VT_VN_Matcher.matches();
502    }
503
504    /***
505     * Verifies that the given line from the model file is a valid face that is described by vertices and texture coordinates
506     * @param line the line being validated
507     * @return true if the line is a valid face that matches the format "f v1/vt1 ..." (with a minimum of 3 points in the face, and a maximum of 4), false otherwise
508     */
509    private static boolean isValidFace_V_VT_Line(String line)
510    {
511        if (face_V_VT_Matcher != null)
512        {
513            face_V_VT_Matcher.reset();
514        }
515
516        face_V_VT_Matcher = face_V_VT_Pattern.matcher(line);
517        return face_V_VT_Matcher.matches();
518    }
519
520    /***
521     * Verifies that the given line from the model file is a valid face that is described by vertices and vertex normals
522     * @param line the line being validated
523     * @return true if the line is a valid face that matches the format "f v1//vn1 ..." (with a minimum of 3 points in the face, and a maximum of 4), false otherwise
524     */
525    private static boolean isValidFace_V_VN_Line(String line)
526    {
527        if (face_V_VN_Matcher != null)
528        {
529            face_V_VN_Matcher.reset();
530        }
531
532        face_V_VN_Matcher = face_V_VN_Pattern.matcher(line);
533        return face_V_VN_Matcher.matches();
534    }
535
536    /***
537     * Verifies that the given line from the model file is a valid face that is described by only vertices
538     * @param line the line being validated
539     * @return true if the line is a valid face that matches the format "f v1 ..." (with a minimum of 3 points in the face, and a maximum of 4), false otherwise
540     */
541    private static boolean isValidFace_V_Line(String line)
542    {
543        if (face_V_Matcher != null)
544        {
545            face_V_Matcher.reset();
546        }
547
548        face_V_Matcher = face_V_Pattern.matcher(line);
549        return face_V_Matcher.matches();
550    }
551
552    /***
553     * Verifies that the given line from the model file is a valid face of any of the possible face formats
554     * @param line the line being validated
555     * @return true if the line is a valid face that matches any of the valid face formats, false otherwise
556     */
557    private static boolean isValidFaceLine(String line)
558    {
559        return isValidFace_V_VT_VN_Line(line) || isValidFace_V_VT_Line(line) || isValidFace_V_VN_Line(line) || isValidFace_V_Line(line);
560    }
561
562    /***
563     * Verifies that the given line from the model file is a valid group (or object)
564     * @param line the line being validated
565     * @return true if the line is a valid group (or object), false otherwise
566     */
567    private static boolean isValidGroupObjectLine(String line)
568    {
569        if (groupObjectMatcher != null)
570        {
571            groupObjectMatcher.reset();
572        }
573
574        groupObjectMatcher = groupObjectPattern.matcher(line);
575        return groupObjectMatcher.matches();
576    }
577
578    @Override
579    public String getType()
580    {
581        return "obj";
582    }
583}