View Javadoc
1   /*
2    * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.geometry.io;
17  
18  import java.io.File;
19  import java.io.IOException;
20  import java.util.HashSet;
21  import java.util.Set;
22  import java.util.StringTokenizer;
23  
24  /**
25   * MaterialLoader implementation for OBJ files, which is capable of reading its
26   * associated MTL file.
27   */
28  public class MaterialLoaderOBJ extends MaterialLoader {
29  
30      /**
31       * Maximum allowed color value.
32       */
33      public static final short MAX_COLOR_VALUE = 255;
34  
35      /**
36       * Set of materials loaded in MTL file.
37       */
38      private final Set<Material> materials = new HashSet<>();
39  
40      /**
41       * Current material being loaded.
42       */
43      private MaterialOBJ currentMaterial = null;
44  
45      /**
46       * Number of textures that have been found in MTL file.
47       */
48      private int textureCounter;
49  
50      /**
51       * Default Constructor.
52       */
53      public MaterialLoaderOBJ() {
54          super();
55      }
56  
57      /**
58       * Constructor.
59       *
60       * @param f material file to be read.
61       * @throws IOException raised if provided file does not exist or an I/O
62       *                     exception occurs.
63       */
64      public MaterialLoaderOBJ(final File f) throws IOException {
65          super(f);
66      }
67  
68      /**
69       * Constructor.
70       *
71       * @param listener material listener to notify start, end and progress
72       *                 events.
73       */
74      public MaterialLoaderOBJ(final MaterialLoaderListener listener) {
75          super(listener);
76      }
77  
78      /**
79       * Constructor.
80       *
81       * @param f        material file to be read.
82       * @param listener material listener to notify start, end and progress
83       *                 events.
84       * @throws IOException raised if provided file does not exist or an I/O
85       *                     exception occurs.
86       */
87      public MaterialLoaderOBJ(final File f, final MaterialLoaderListener listener) throws IOException {
88          super(f, listener);
89      }
90  
91      /**
92       * Indicates if material loader is ready to be used because a file has
93       * already been provided.
94       *
95       * @return true if material loader is ready, false otherwise.
96       */
97      @Override
98      public boolean isReady() {
99          return hasFile();
100     }
101 
102     /**
103      * Starts the loading process of provided file.
104      * This method returns a set containing all the materials that have been
105      * loaded.
106      *
107      * @return a set containing all the materials that have been loaded.
108      * @throws LockedException   raised if this instance is already locked.
109      * @throws NotReadyException raised if this instance is not yet ready.
110      * @throws IOException       if an I/O error occurs.
111      * @throws LoaderException   if file is corrupted or cannot be interpreted.
112      */
113     @Override
114     public Set<Material> load() throws LockedException, NotReadyException, IOException, LoaderException {
115         if (!isReady()) {
116             throw new NotReadyException();
117         }
118         if (isLocked()) {
119             throw new LockedException();
120         }
121 
122         setLocked(true);
123 
124         materials.clear();
125         textureCounter = 0;
126 
127         if (listener != null) {
128             listener.onLoadStart(this);
129         }
130 
131         String line;
132         do {
133             line = reader.readLine();
134             if (line != null) {
135                 parseLine(line);
136             }
137         } while (line != null);
138 
139         currentMaterial.setId(materials.size());
140         materials.add(currentMaterial);
141 
142         if (listener != null) {
143             listener.onLoadEnd(this);
144         }
145 
146         setLocked(false);
147 
148         return materials;
149     }
150 
151     /**
152      * Parses a line in an MTL file.
153      *
154      * @param line line being parsed.
155      * @throws LoaderException if line cannot be interpreted.
156      */
157     @SuppressWarnings({"DuplicateExpressions", "DuplicatedCode"})
158     private void parseLine(final String line) throws LoaderException {
159         if (line == null || line.isEmpty()) {
160             return;
161         }
162 
163         try {
164             final var tokenizer = new StringTokenizer(line);
165             if (!tokenizer.hasMoreTokens()) {
166                 // empty line or simply
167                 // containing separators (tabs, line feeds, etc)
168                 return;
169             }
170 
171             final var command = tokenizer.nextToken();
172 
173             // search for command position
174             final var commandPos = line.indexOf(command);
175 
176             if ("newmtl".equalsIgnoreCase(command)) {
177                 if (currentMaterial != null) {
178                     currentMaterial.setId(materials.size());
179                     materials.add(currentMaterial);
180                 }
181                 final var name = line.substring(commandPos + command.length()).trim();
182                 currentMaterial = new MaterialOBJ(name);
183 
184             } else if ("ka".equalsIgnoreCase(command)) {
185                 if (currentMaterial == null) {
186                     throw new LoaderException();
187                 }
188                 currentMaterial.setAmbientColor(
189                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE),
190                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE),
191                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE));
192 
193             } else if ("kd".equalsIgnoreCase(command)) {
194                 if (currentMaterial == null) {
195                     throw new LoaderException();
196                 }
197                 currentMaterial.setDiffuseColor(
198                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE),
199                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE),
200                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE));
201 
202             } else if ("Ks".equalsIgnoreCase(command)) {
203                 if (currentMaterial == null) {
204                     throw new LoaderException();
205                 }
206                 currentMaterial.setSpecularColor(
207                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE),
208                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE),
209                         (short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE));
210 
211             } else if ("Ns".equalsIgnoreCase(command) || "Ni".equalsIgnoreCase(command)) {
212                 if (currentMaterial == null) {
213                     throw new LoaderException();
214                 }
215                 currentMaterial.setSpecularCoefficient(Float.parseFloat(tokenizer.nextToken()));
216 
217             } else if ("d".equalsIgnoreCase(command) || "Tr".equalsIgnoreCase(command)) {
218                 if (currentMaterial == null) {
219                     throw new LoaderException();
220                 }
221                 currentMaterial.setTransparency((short) (Float.parseFloat(tokenizer.nextToken()) * MAX_COLOR_VALUE));
222 
223             } else if ("illum".equalsIgnoreCase(command)) {
224                 if (currentMaterial == null) {
225                     throw new LoaderException();
226                 }
227                 currentMaterial.setIllumination(Illumination.forValue(Integer.parseInt(tokenizer.nextToken())));
228 
229             } else if ("map_Kd".equalsIgnoreCase(command)) {
230                 final var textureName = line.substring(commandPos + command.length()).trim();
231                 final var tex = new Texture(textureName, textureCounter);
232                 textureCounter++;
233 
234                 var valid = true;
235                 if (textureValidationEnabled && listener != null) {
236                     valid = listener.onValidateTexture(this, tex);
237                 }
238                 if (!valid) {
239                     throw new InvalidTextureException();
240                 }
241 
242                 currentMaterial.setDiffuseTextureMap(tex);
243 
244             } else if ("map_Ka".equalsIgnoreCase(command)) {
245                 final var textureName = line.substring(commandPos + command.length()).trim();
246                 final var tex = new Texture(textureName, textureCounter);
247                 textureCounter++;
248 
249                 var valid = true;
250                 if (textureValidationEnabled && listener != null) {
251                     valid = listener.onValidateTexture(this, tex);
252                 }
253                 if (!valid) throw new InvalidTextureException();
254 
255                 currentMaterial.setAmbientTextureMap(tex);
256 
257             } else if ("map_Ks".equalsIgnoreCase(command)) {
258                 final var textureName = line.substring(commandPos + command.length()).trim();
259                 final var tex = new Texture(textureName, textureCounter);
260                 textureCounter++;
261 
262                 var valid = true;
263                 if (textureValidationEnabled && listener != null) {
264                     valid = listener.onValidateTexture(this, tex);
265                 }
266                 if (!valid) {
267                     throw new InvalidTextureException();
268                 }
269 
270                 currentMaterial.setSpecularTextureMap(tex);
271 
272             } else if ("map_d".equalsIgnoreCase(command)) {
273                 final var textureName = line.substring(commandPos + command.length()).trim();
274                 final var tex = new Texture(textureName, textureCounter);
275                 textureCounter++;
276 
277                 var valid = true;
278                 if (textureValidationEnabled && listener != null) {
279                     valid = listener.onValidateTexture(this, tex);
280                 }
281                 if (!valid) {
282                     throw new InvalidTextureException();
283                 }
284 
285                 currentMaterial.setAlphaTextureMap(tex);
286 
287             } else if ("map_bump".equalsIgnoreCase(command) || "bump".equalsIgnoreCase(command)) {
288                 final var textureName = line.substring(commandPos + command.length()).trim();
289                 final var tex = new Texture(textureName, textureCounter);
290                 textureCounter++;
291 
292                 var valid = true;
293                 if (textureValidationEnabled && listener != null) {
294                     valid = listener.onValidateTexture(this, tex);
295                 }
296                 if (!valid) {
297                     throw new InvalidTextureException();
298                 }
299 
300                 currentMaterial.setBumpTextureMap(tex);
301             }
302         } catch (final Exception t) {
303             throw new LoaderException(t);
304         }
305     }
306 
307     /**
308      * Indicates if any material has been loaded already.
309      *
310      * @return true if materials have been loaded, false otherwise.
311      */
312     public boolean areMaterialsAvailable() {
313         return !materials.isEmpty();
314     }
315 
316     /**
317      * Returns set of materials that have been read.
318      *
319      * @return set of materials that have been read.
320      */
321     public Set<Material> getMaterials() {
322         return materials;
323     }
324 
325     /**
326      * Gets a material by its name, or null if material is not found.
327      *
328      * @param name name of material to be found.
329      * @return a material having provided name or null if none is found.
330      */
331     public MaterialOBJ getMaterialByName(final String name) {
332         if (name == null) {
333             return null;
334         }
335 
336         for (final var m : this.materials) {
337             if (m instanceof MaterialOBJ m2) {
338 
339                 if (m2.getMaterialName() == null) {
340                     continue;
341                 }
342 
343                 if (m2.getMaterialName().equals(name)) {
344                     return m2;
345                 }
346             }
347         }
348         return null;
349     }
350 
351     /**
352      * Indicates if material having provided name has been loaded or not.
353      *
354      * @param name name to search material by.
355      * @return true if material having provided name exists, false otherwise.
356      */
357     public boolean containsMaterial(final String name) {
358         return getMaterialByName(name) != null;
359     }
360 
361     /**
362      * Returns material by texture name.
363      *
364      * @param name name of texture.
365      * @return texture that has been found or null if none has been found.
366      */
367     public Material getMaterialByTextureMapName(final String name) {
368         if (name == null) {
369             return null;
370         }
371 
372         for (final Material m : this.materials) {
373             if (m.getAlphaTextureMap() != null && m.getAlphaTextureMap().getFileName() != null
374                     && m.getAlphaTextureMap().getFileName().equals(name)) {
375                 return m;
376             }
377 
378             if (m.getAmbientTextureMap() != null && m.getAmbientTextureMap().getFileName() != null
379                     && m.getAmbientTextureMap().getFileName().equals(name)) {
380                 return m;
381             }
382 
383             if (m.getDiffuseTextureMap() != null && m.getDiffuseTextureMap().getFileName() != null
384                     && m.getDiffuseTextureMap().getFileName().equals(name)) {
385                 return m;
386             }
387 
388             if (m.getSpecularTextureMap() != null && m.getSpecularTextureMap().getFileName() != null
389                     && m.getSpecularTextureMap().getFileName().equals(name)) {
390                 return m;
391             }
392         }
393         return null;
394     }
395 }