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 com.irurueta.geometry.Point3D;
19 import com.irurueta.geometry.Triangle3D;
20 import com.irurueta.geometry.Triangulator3D;
21 import com.irurueta.geometry.TriangulatorException;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.HashSet;
28 import java.util.LinkedList;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.TreeMap;
32
33 /**
34 * Loads an OBJ file.
35 * If a LoaderListenerOBJ is provided, this class might also attempt to load the
36 * associated material file if available.
37 */
38 public class LoaderOBJ extends Loader {
39
40 /**
41 * Constant defining the default value of maximum number of vertices to keep
42 * in a chunk. This is 65535, which corresponds to the maximum value allowed
43 * by graphical layer such as OpenGL when working with Vertex Buffer Objects.
44 */
45 public static final int DEFAULT_MAX_VERTICES_IN_CHUNK = 0xffff;
46
47 /**
48 * Minimum allowed value for maximum number of vertices in chunk, which is
49 * one.
50 */
51 public static final int MIN_MAX_VERTICES_IN_CHUNK = 1;
52
53 /**
54 * Constant indicating that duplicated vertices are allowed by default,
55 * which allows faster loading.
56 */
57 public static final boolean DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK = true;
58
59 /**
60 * Maximum number of stream positions to be cached by default.
61 */
62 public static final int DEFAULT_MAX_STREAM_POSITIONS = 1000000;
63
64 /**
65 * Minimum allowed number of stream positions.
66 */
67 public static final int MIN_STREAM_POSITIONS = 1;
68
69 /**
70 * Amount of progress variation (1%) used to notify progress.
71 */
72 public static final float PROGRESS_DELTA = 0.01f;
73
74 /**
75 * Indicates that loading should continue even if triangulation of some
76 * polygons fails.
77 */
78 public static final boolean DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR = true;
79
80 /**
81 * Identifies materials.
82 */
83 private static final String USEMTL = "usemtl ";
84
85 /**
86 * Iterator to load OBJ file data in small chunks.
87 * Usually data is divided in chunks that can be directly loaded by
88 * graphic layers such as OpenGL.
89 */
90 private LoaderIteratorOBJ loaderIterator;
91
92 /**
93 * Maximum number of vertices allowed in a chunk. Once this value is
94 * exceeded when loading a file, a new chunk of data is created.
95 */
96 private int maxVerticesInChunk;
97
98 /**
99 * To allow faster file loading, it might be allowed to repeat points in a
100 * chunk. When representing data graphically, this has no visual.
101 * consequences but chunks will take up more memory. This value represents
102 * a trade-off between loading speed and memory usage.
103 */
104 private boolean allowDuplicateVerticesInChunk;
105
106 /**
107 * Maximum number of file stream positions to be cached.
108 * This class keeps a cache of positions in the file to allow faster file
109 * loading at the expense of larger memory usage.
110 * If the geometry of a file reuses a large number of points, keeping a
111 * large cache will increase the speed of loading a file, otherwise the
112 * impact of this parameter will be low.
113 * The default value will work fine for most cases.
114 */
115 private long maxStreamPositions;
116
117 /**
118 * List containing comments contained in the file.
119 */
120 private final List<String> comments;
121
122 /**
123 * Collection of materials contained in the material's file associated to an
124 * OBJ file.
125 */
126 private Set<Material> materials;
127
128 /**
129 * Determines if file loading should continue even if the triangulation of
130 * a polygon fails. The triangulation of a polygon might fail if the polygon
131 * is degenerate or has invalid numerical values such as NaN of infinity.
132 * If true, loading will continue but the result will lack the polygons that
133 * failed.
134 */
135 private boolean continueIfTriangulationError;
136
137 /**
138 * Constructor.
139 */
140 public LoaderOBJ() {
141 loaderIterator = null;
142 maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
143 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
144 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
145 comments = new LinkedList<>();
146 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
147 }
148
149 /**
150 * Constructor.
151 *
152 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
153 * Once this value is exceeded when loading a file, a new chunk of data is
154 * created.
155 * @throws IllegalArgumentException if maximum number of vertices allowed in
156 * a chunk is lower than 1.
157 */
158 public LoaderOBJ(final int maxVerticesInChunk) {
159 loaderIterator = null;
160 internalSetMaxVerticesInChunk(maxVerticesInChunk);
161 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
162 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
163 comments = new LinkedList<>();
164 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
165 }
166
167 /**
168 * Constructor.
169 *
170 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
171 * Once this value is exceeded when loading a file, a new chunk of data is
172 * created.
173 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
174 * chunk are allowed to provide faster file loading. When representing data
175 * graphically, this has no visual consequences but chunks will take up more
176 * memory.
177 * @throws IllegalArgumentException if maximum number of vertices allowed in
178 * a chunk is lower than 1.
179 */
180 public LoaderOBJ(final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk) {
181 loaderIterator = null;
182 internalSetMaxVerticesInChunk(maxVerticesInChunk);
183 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
184 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
185 comments = new LinkedList<>();
186 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
187 }
188
189 /**
190 * Constructor.
191 *
192 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
193 * Once this value is exceeded when loading a file, a new chunk of data is
194 * created.
195 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
196 * chunk are allowed to provide faster file loading. When representing data
197 * graphically, this has no visual consequences but chunks will take up more
198 * memory.
199 * @param maxStreamPositions Maximum number of file stream positions to be
200 * cached.
201 * @throws IllegalArgumentException if maximum number of vertices allowed in
202 * a chunk is lower than 1.
203 */
204 public LoaderOBJ(final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk,
205 final long maxStreamPositions) {
206 loaderIterator = null;
207 internalSetMaxVerticesInChunk(maxVerticesInChunk);
208 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
209 internalSetMaxStreamPositions(maxStreamPositions);
210 comments = new LinkedList<>();
211 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
212 }
213
214 /**
215 * Constructor.
216 *
217 * @param f file to be loaded.
218 * @throws IOException if an I/O error occurs.
219 */
220 public LoaderOBJ(final File f) throws IOException {
221 super(f);
222 loaderIterator = null;
223 maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
224 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
225 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
226 comments = new LinkedList<>();
227 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
228 }
229
230 /**
231 * Constructor.
232 *
233 * @param f file to be loaded.
234 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
235 * Once this value is exceeded when loading a file, a new chunk of data is
236 * created.
237 * @throws IllegalArgumentException if maximum number of vertices allowed in
238 * a chunk is lower than 1.
239 * @throws IOException if an I/O error occurs.
240 */
241 public LoaderOBJ(final File f, final int maxVerticesInChunk) throws IOException {
242 super(f);
243 loaderIterator = null;
244 internalSetMaxVerticesInChunk(maxVerticesInChunk);
245 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
246 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
247 comments = new LinkedList<>();
248 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
249 }
250
251 /**
252 * Constructor.
253 *
254 * @param f file to be loaded.
255 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
256 * Once this value is exceeded when loading a file, a new chunk of data is
257 * created.
258 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
259 * chunk are allowed to provide faster file loading. When representing data
260 * graphically, this has no visual consequences but chunks will take up more
261 * memory.
262 * @throws IllegalArgumentException if maximum number of vertices allowed in
263 * a chunk is lower than 1.
264 * @throws IOException if an I/O error occurs.
265 */
266 public LoaderOBJ(final File f, final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk)
267 throws IOException {
268 super(f);
269 loaderIterator = null;
270 internalSetMaxVerticesInChunk(maxVerticesInChunk);
271 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
272 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
273 comments = new LinkedList<>();
274 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
275 }
276
277 /**
278 * Constructor.
279 *
280 * @param f file to be loaded.
281 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
282 * Once this value is exceeded when loading a file, a new chunk of data is
283 * created.
284 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
285 * chunk are allowed to provide faster file loading. When representing data
286 * graphically, this has no visual consequences but chunks will take up more
287 * memory.
288 * @param maxStreamPositions Maximum number of file stream positions to be
289 * cached.
290 * @throws IllegalArgumentException if maximum number of vertices allowed in
291 * a chunk is lower than 1.
292 * @throws IOException if an I/O error occurs.
293 */
294 public LoaderOBJ(final File f, final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk,
295 final long maxStreamPositions) throws IOException {
296 super(f);
297 loaderIterator = null;
298 internalSetMaxVerticesInChunk(maxVerticesInChunk);
299 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
300 internalSetMaxStreamPositions(maxStreamPositions);
301 comments = new LinkedList<>();
302 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
303 }
304
305 /**
306 * Constructor.
307 *
308 * @param listener listener to be notified of loading progress and when
309 * loading process starts or finishes.
310 */
311 public LoaderOBJ(final LoaderListener listener) {
312 super(listener);
313 maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
314 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
315 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
316 comments = new LinkedList<>();
317 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
318 }
319
320 /**
321 * Constructor.
322 *
323 * @param listener listener to be notified of loading progress and when
324 * loading process starts or finishes.
325 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
326 * Once this value is exceeded when loading a file, a new chunk of data is
327 * created.
328 * @throws IllegalArgumentException if maximum number of vertices allowed in
329 * a chunk is lower than 1.
330 */
331 public LoaderOBJ(final LoaderListener listener, final int maxVerticesInChunk) {
332 super(listener);
333 loaderIterator = null;
334 internalSetMaxVerticesInChunk(maxVerticesInChunk);
335 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
336 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
337 comments = new LinkedList<>();
338 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
339 }
340
341 /**
342 * Constructor.
343 *
344 * @param listener listener to be notified of loading progress and when
345 * loading process starts or finishes.
346 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
347 * Once this value is exceeded when loading a file, a new chunk of data is
348 * created.
349 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
350 * chunk are allowed to provide faster file loading. When representing data
351 * graphically, this has no visual consequences but chunks will take up more
352 * memory.
353 * @throws IllegalArgumentException if maximum number of vertices allowed in
354 * a chunk is lower than 1.
355 */
356 public LoaderOBJ(final LoaderListener listener, final int maxVerticesInChunk,
357 final boolean allowDuplicateVerticesInChunk) {
358 super(listener);
359 loaderIterator = null;
360 internalSetMaxVerticesInChunk(maxVerticesInChunk);
361 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
362 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
363 comments = new LinkedList<>();
364 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
365 }
366
367 /**
368 * Constructor.
369 *
370 * @param listener listener to be notified of loading progress and when
371 * loading process starts or finishes.
372 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
373 * Once this value is exceeded when loading a file, a new chunk of data is
374 * created.
375 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
376 * chunk are allowed to provide faster file loading. When representing data
377 * graphically, this has no visual consequences but chunks will take up more
378 * memory.
379 * @param maxStreamPositions Maximum number of file stream positions to be
380 * cached.
381 * @throws IllegalArgumentException if maximum number of vertices allowed in
382 * a chunk is lower than 1.
383 */
384 public LoaderOBJ(final LoaderListener listener, final int maxVerticesInChunk,
385 final boolean allowDuplicateVerticesInChunk, final long maxStreamPositions) {
386 super(listener);
387 loaderIterator = null;
388 internalSetMaxVerticesInChunk(maxVerticesInChunk);
389 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
390 internalSetMaxStreamPositions(maxStreamPositions);
391 comments = new LinkedList<>();
392 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
393 }
394
395 /**
396 * Constructor.
397 *
398 * @param f file to be loaded.
399 * @param listener listener to be notified of loading progress and when
400 * loading process starts or finishes.
401 * @throws IOException if an I/O error occurs.
402 */
403 public LoaderOBJ(final File f, final LoaderListener listener) throws IOException {
404 super(f, listener);
405 loaderIterator = null;
406 maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
407 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
408 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
409 comments = new LinkedList<>();
410 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
411 }
412
413 /**
414 * Constructor.
415 *
416 * @param f file to be loaded.
417 * @param listener listener to be notified of loading progress and when
418 * loading process starts or finishes.
419 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
420 * Once this value is exceeded when loading a file, a new chunk of data is
421 * created.
422 * @throws IllegalArgumentException if maximum number of vertices allowed in
423 * a chunk is lower than 1.
424 * @throws IOException if an I/O error occurs.
425 */
426 public LoaderOBJ(final File f, final LoaderListener listener, final int maxVerticesInChunk) throws IOException {
427 super(f, listener);
428 loaderIterator = null;
429 internalSetMaxVerticesInChunk(maxVerticesInChunk);
430 allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
431 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
432 comments = new LinkedList<>();
433 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
434 }
435
436 /**
437 * Constructor.
438 *
439 * @param f file to be loaded.
440 * @param listener listener to be notified of loading progress and when
441 * loading process starts or finishes.
442 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
443 * Once this value is exceeded when loading a file, a new chunk of data is
444 * created.
445 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
446 * chunk are allowed to provide faster file loading. When representing data
447 * graphically, this has no visual consequences but chunks will take up more
448 * memory.
449 * @throws IllegalArgumentException if maximum number of vertices allowed in
450 * a chunk is lower than 1.
451 * @throws IOException if an I/O error occurs.
452 */
453 public LoaderOBJ(final File f, final LoaderListener listener, final int maxVerticesInChunk,
454 final boolean allowDuplicateVerticesInChunk) throws IOException {
455 super(f, listener);
456 loaderIterator = null;
457 internalSetMaxVerticesInChunk(maxVerticesInChunk);
458 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
459 maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
460 comments = new LinkedList<>();
461 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
462 }
463
464 /**
465 * Constructor.
466 *
467 * @param f file to be loaded.
468 * @param listener listener to be notified of loading progress and when
469 * loading process starts or finishes.
470 * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
471 * Once this value is exceeded when loading a file, a new chunk of data is
472 * created.
473 * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
474 * chunk are allowed to provide faster file loading. When representing data
475 * graphically, this has no visual consequences but chunks will take up more
476 * memory.
477 * @param maxStreamPositions Maximum number of file stream positions to be
478 * cached.
479 * @throws IllegalArgumentException if maximum number of vertices allowed in
480 * a chunk is lower than 1.
481 * @throws IOException if an I/O error occurs.
482 */
483 public LoaderOBJ(final File f, final LoaderListener listener, final int maxVerticesInChunk,
484 final boolean allowDuplicateVerticesInChunk, final long maxStreamPositions) throws IOException {
485 super(f, listener);
486 loaderIterator = null;
487 internalSetMaxVerticesInChunk(maxVerticesInChunk);
488 this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
489 internalSetMaxStreamPositions(maxStreamPositions);
490 comments = new LinkedList<>();
491 continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
492 }
493
494 /**
495 * Returns maximum number of vertices allowed in a chunk.
496 * Once this value is exceeded when loading a file, a new chunk of data is
497 * created.
498 *
499 * @return maximum number of vertices allowed in a chunk.
500 */
501 public int getMaxVerticesInChunk() {
502 return maxVerticesInChunk;
503 }
504
505 /**
506 * Sets maximum number of vertices allowed in a chunk.
507 * Once this value is exceeded when loading a file, a new chunk of data is
508 * created.
509 *
510 * @param maxVerticesInChunk maximum allowed number of vertices to be set.
511 * @throws IllegalArgumentException if provided value is lower than 1.
512 * @throws LockedException if this loader is currently loading a file.
513 */
514 public void setMaxVerticesInChunk(final int maxVerticesInChunk) throws LockedException {
515 if (isLocked()) {
516 throw new LockedException();
517 }
518 internalSetMaxVerticesInChunk(maxVerticesInChunk);
519 }
520
521 /**
522 * Returns boolean indicating if repeated vertices in a chunk are allowed to
523 * provide faster file loading. When representing data graphically, this has
524 * no visual consequences but chunks will take up more memory.
525 *
526 * @return true if duplicate vertices are allowed, false otherwise.
527 */
528 public boolean areDuplicateVerticesInChunkAllowed() {
529 return allowDuplicateVerticesInChunk;
530 }
531
532 /**
533 * Sets boolean indicating if repeated vertices in a chunk are allowed to
534 * provide faster file loading. When representing data graphically, this has
535 * no visual consequences but chunks will take up more memory.
536 *
537 * @param allow true if duplicate vertices are allowed, false otherwise.
538 * @throws LockedException if this loader is currently loading a file.
539 */
540 public void setAllowDuplicateVerticesInChunk(final boolean allow) throws LockedException {
541 if (isLocked()) {
542 throw new LockedException();
543 }
544 allowDuplicateVerticesInChunk = allow;
545 }
546
547 /**
548 * Returns maximum number of file stream positions to be cached.
549 * This class keeps a cache of positions in the file to allow faster file
550 * loading at the expense of larger memory usage.
551 * If the geometry of a file reuses a large number of points, keeping a
552 * large cache will increase the speed of loading a file, otherwise the
553 * impact of this parameter will be low.
554 * The default value will work fine for most cases.
555 *
556 * @return maximum number of file stream positions to be cached.
557 */
558 public long getMaxStreamPositions() {
559 return maxStreamPositions;
560 }
561
562 /**
563 * Sets maximum number of file stream positions to be cached.
564 * This class keeps a cache of positions in the file to allow faster file
565 * loading at the expense of larger memory usage.
566 * If the geometry of a file reuses a large number of points, keeping a
567 * large cache will increase the speed of loading a file, otherwise the
568 * impact of this parameter will be low.
569 * The default value will work fine for most cases.
570 *
571 * @param maxStreamPositions maximum number of file stream positions to be
572 * set.
573 * @throws IllegalArgumentException if provided value is lower than 1.
574 * @throws LockedException if this loader is currently loading a file.
575 */
576 public void setMaxStreamPositions(final long maxStreamPositions) throws LockedException {
577 if (isLocked()) {
578 throw new LockedException();
579 }
580 internalSetMaxStreamPositions(maxStreamPositions);
581 }
582
583 /**
584 * Returns boolean indicating if file loading should continue even if the
585 * triangulation of a polygon fails. The triangulation of a polygon might
586 * fail if the polygon is degenerate or has invalid numerical values such as
587 * NaN of infinity.
588 *
589 * @return If true, loading will continue but the result will lack the
590 * polygons that failed.
591 */
592 public boolean isContinueIfTriangulationError() {
593 return continueIfTriangulationError;
594 }
595
596 /**
597 * Sets boolean indicating if file loading should continue even if the
598 * triangulation of a polygon fails. The triangulation of a polygon might
599 * fail if the polygon is degenerate or has invalid numerical values such as
600 * NaN or infinity.
601 *
602 * @param continueIfTriangulationError if ture, loading will continue but
603 * the result will lack the polygons that failed.
604 */
605 public void setContinueIfTriangulationError(final boolean continueIfTriangulationError) {
606 this.continueIfTriangulationError = continueIfTriangulationError;
607 }
608
609 /**
610 * Returns a list of the comments contained in the file.
611 *
612 * @return list of the comments contained in the file.
613 */
614 public List<String> getComments() {
615 return Collections.unmodifiableList(comments);
616 }
617
618 /**
619 * Gets collection of materials contained in the materials file associated to an
620 * OBJ file.
621 *
622 * @return collection of material.
623 */
624 public Set<Material> getMaterials() {
625 return materials;
626 }
627
628 /**
629 * If loader is ready to start loading a file.
630 * This is true once a file has been provided.
631 *
632 * @return true if ready to start loading a file, false otherwise.
633 */
634 @Override
635 public boolean isReady() {
636 return hasFile();
637 }
638
639 /**
640 * Returns mesh format supported by this class, which is MESH_FORMAT_OBJ.
641 *
642 * @return mesh format supported by this class.
643 */
644 @Override
645 public MeshFormat getMeshFormat() {
646 return MeshFormat.MESH_FORMAT_OBJ;
647 }
648
649 /**
650 * Determines if provided file is a valid file that can be read by this
651 * loader.
652 *
653 * @return true if file is valid, false otherwise.
654 * @throws LockedException raised if this instance is already locked.
655 * @throws IOException if an I/O error occurs..
656 */
657 @Override
658 public boolean isValidFile() throws LockedException, IOException {
659 if (!hasFile()) {
660 throw new IOException();
661 }
662 if (isLocked()) {
663 throw new LockedException();
664 }
665 return true;
666 }
667
668 /**
669 * Starts the loading process of provided file.
670 * This method returns a LoaderIterator to start the iterative process to
671 * load a file in small chunks of data.
672 *
673 * @return a loader iterator to read the file in a step-by-step process.
674 * @throws LockedException raised if this instance is already locked.
675 * @throws NotReadyException raised if this instance is not yet ready.
676 * @throws IOException if an I/O error occurs.
677 * @throws LoaderException if file is corrupted or cannot be interpreted.
678 */
679 @Override
680 public LoaderIterator load() throws LockedException, NotReadyException, IOException, LoaderException {
681 if (isLocked()) {
682 throw new LockedException();
683 }
684 if (!isReady()) {
685 throw new NotReadyException();
686 }
687
688 setLocked(true);
689 if (listener != null) {
690 listener.onLoadStart(this);
691 }
692
693 loaderIterator = new LoaderIteratorOBJ(this);
694 loaderIterator.setListener(new LoaderIteratorListenerImpl(this));
695 return loaderIterator;
696 }
697
698 /**
699 * Internal method to set maximum number of vertices allowed in a chunk.
700 * This method is reused both in the constructor and in the setter of
701 * maximum number of vertices allowed in a chunk.
702 *
703 * @param maxVerticesInChunk maximum allowed number of vertices to be set.
704 * @throws IllegalArgumentException if provided value is lower than 1.
705 */
706 private void internalSetMaxVerticesInChunk(final int maxVerticesInChunk) {
707 if (maxVerticesInChunk < MIN_MAX_VERTICES_IN_CHUNK) {
708 throw new IllegalArgumentException();
709 }
710
711 this.maxVerticesInChunk = maxVerticesInChunk;
712 }
713
714 /**
715 * Internal method to set maximum number of file stream positions to be
716 * cached.
717 * This method is reused both in the constructor and in the setter of
718 * maximum number stream positions.
719 *
720 * @param maxStreamPositions maximum number of file stream positions to be
721 * cached.
722 * @throws IllegalArgumentException if provided value is lower than 1.
723 */
724 private void internalSetMaxStreamPositions(final long maxStreamPositions) {
725 if (maxStreamPositions < MIN_STREAM_POSITIONS) {
726 throw new IllegalArgumentException();
727 }
728
729 this.maxStreamPositions = maxStreamPositions;
730 }
731
732 /**
733 * Internal listener to be notified when loading process finishes.
734 * This listener is used to free resources when loading process finishes.
735 */
736 private class LoaderIteratorListenerImpl implements LoaderIteratorListener {
737
738 /**
739 * Reference to Loader loading an OBJ file.
740 */
741 private final LoaderOBJ loader;
742
743 /**
744 * Constructor.
745 *
746 * @param loader reference to Loader.
747 */
748 public LoaderIteratorListenerImpl(final LoaderOBJ loader) {
749 this.loader = loader;
750 }
751
752 /**
753 * Method to be notified when the loading process finishes.
754 *
755 * @param iterator iterator loading the file in chunks.
756 */
757 @Override
758 public void onIteratorFinished(final LoaderIterator iterator) {
759 // because iterator is finished, we should allow subsequent calls to
760 // load method
761 try {
762 // attempt restart stream to initial position
763 reader.seek(0);
764 } catch (final Exception ignore) {
765 // this is the best effort operation, if it fails it is ignored
766 }
767
768 // on subsequent calls
769 if (listener != null) {
770 listener.onLoadEnd(loader);
771 }
772 setLocked(false);
773 }
774 }
775
776 /**
777 * Loader iterator in charge of loading file data in small chunks.
778 * Usually data is divided in chunks small enough that can be directly
779 * loaded by graphical layers such as OpenGL (which has a limit of 65535
780 * indices when using Vertex Buffer Objects, which increase graphical
781 * performance).
782 */
783 private class LoaderIteratorOBJ implements LoaderIterator {
784
785 /**
786 * Reference to loader loading OBJ file.
787 */
788 private final LoaderOBJ loader;
789
790 /**
791 * X coordinate of the latest point that has been read.
792 */
793 private float coordX;
794
795 /**
796 * Y coordinate of the latest point that has been read.
797 */
798 private float coordY;
799
800 /**
801 * Z coordinate of the latest point that has been read.
802 */
803 private float coordZ;
804
805 /**
806 * U texture coordinate of the latest point that has been read.
807 * U coordinate refers to the horizontal axis in the texture image and
808 * usually is a normalized value between 0.0 and 1.0. Larger values can
809 * be used to repeat textures, negative values can be used to reverse
810 * textures.
811 */
812 private float textureU;
813
814 /**
815 * V texture coordinate of the latest point that has been read.
816 * V coordinate refers to the vertical axis in the texture image and
817 * usually is a normalized value between 0.0 and 1.0. Larger values can
818 * be used to repeat textures, negative values can be used to reverse
819 * textures.
820 */
821 private float textureV;
822
823 /**
824 * X coordinate of the latest point normal that has been read.
825 */
826 private float nX;
827
828 /**
829 * Y coordinate of the latest point normal that has been read.
830 */
831 private float nY;
832
833 /**
834 * Z coordinate of the latest point normal that has been read.
835 */
836 private float nZ;
837
838 /**
839 * Vertex index in the file of the latest point that has been read.
840 */
841 private int vertexIndex;
842
843 /**
844 * Texture index in the file of the latest point that has been read.
845 */
846 private int textureIndex;
847
848 /**
849 * Normal index in the file of the latest point that has been read.
850 */
851 private int normalIndex;
852
853 // coordinates for bounding box in a chunk
854
855 /**
856 * X coordinate of the minimum point forming the bounding box in a chunk
857 * of data. This value will be updated while the chunk is being filled.
858 */
859 private float minX;
860
861 /**
862 * Y coordinate of the minimum point forming the bounding box in a chunk
863 * of data. This value will be updated while the chunk is being filled.
864 */
865 private float minY;
866
867 /**
868 * Z coordinate of the minimum point forming the bounding box in a chunk
869 * of data. This value will be updated while the chunk is being filled.
870 */
871 private float minZ;
872
873 /**
874 * X coordinate of the maximum point forming the bounding box in a chunk
875 * of data. This value will be updated while the chunk is being filled.
876 */
877 private float maxX;
878
879 /**
880 * Y coordinate of the maximum point forming the bounding box in a chunk
881 * of data. This value will be updated while the chunk is being filled.
882 */
883 private float maxY;
884
885 /**
886 * Z coordinate of the maximum point forming the bounding box in a chunk
887 * of data. This value will be updated while the chunk is being filled.
888 */
889 private float maxZ;
890
891 /**
892 * Indicates if vertices have been loaded and must be added to current
893 * chunk being loaded.
894 */
895 private boolean verticesAvailable;
896
897 /**
898 * Indicates if texture coordinates have been loaded and must be added
899 * to current chunk being loaded.
900 */
901 private boolean textureAvailable;
902
903 /**
904 * Indicates if normals have been loaded and must be added to current
905 * chunk being loaded.
906 */
907 private boolean normalsAvailable;
908
909 /**
910 * Indicates if indices have been loaded and must be added to current
911 * chunk being loaded.
912 */
913 private boolean indicesAvailable;
914
915 /**
916 * Indicates if materials have been loaded and must be added to current
917 * chunk being loaded.
918 */
919 private boolean materialsAvailable;
920
921 /**
922 * Number of vertices that have been loaded in current chunk.
923 */
924 private long numberOfVertices;
925
926 /**
927 * Number of texture coordinates that have been loaded in current chunk.
928 */
929 private long numberOfTextureCoords;
930
931 /**
932 * Number of normals that have been loaded in current chunk.
933 */
934 private long numberOfNormals;
935
936 /**
937 * Number of faces (i.e. polygons) that have been loaded in current
938 * chunk.
939 */
940 private long numberOfFaces;
941
942 /**
943 * Index of current face (i.e. polygon) that has been loaded.
944 */
945 private long currentFace;
946
947 /**
948 * Position of first vertex in the file. This is stored to reduce
949 * fetching time when parsing the OBJ file.
950 */
951 private long firstVertexStreamPosition;
952
953 /**
954 * Indicates if first vertex position has been found.
955 */
956 private boolean firstVertexStreamPositionAvailable;
957
958 /**
959 * Position of first texture coordinate in the file. This is stored to
960 * reduce fetching time when parsing the OBJ file.
961 */
962 private long firstTextureCoordStreamPosition;
963
964 /**
965 * Indicates if first texture coordinate has been found.
966 */
967 private boolean firstTextureCoordStreamPositionAvailable;
968
969 /**
970 * Position of first normal coordinate in the file. This is stored to
971 * reduce fetching time when parsing the OBJ file.
972 */
973 private long firstNormalStreamPosition;
974
975 /**
976 * Indicates if first normal coordinate has been found.
977 */
978 private boolean firstNormalStreamPositionAvailable;
979
980 /**
981 * Position of first face (i.e. polygon) in the file. This is stored to
982 * reduce fetching time when parsing the OBJ file.
983 */
984 private long firstFaceStreamPosition;
985
986 /**
987 * Indicates if first face has been found.
988 */
989 private boolean firstFaceStreamPositionAvailable;
990
991 /**
992 * Indicates location of first material in the file. This is stored to
993 * reduce fetching time when parsing the OBJ file.
994 */
995 private long firstMaterialStreamPosition;
996
997 /**
998 * Indicates if first material has been found.
999 */
1000 private boolean firstMaterialStreamPositionAvailable;
1001
1002 /**
1003 * Contains position where file is currently being loaded.
1004 */
1005 private long currentStreamPosition;
1006
1007 /**
1008 * Reference to the listener of this loader iterator. This listener will
1009 * be notified when the loading process finishes so that resources can
1010 * be freed.
1011 */
1012 private LoaderIteratorListener listener;
1013
1014 /**
1015 * Array containing vertices coordinates to be added to current chunk
1016 * of data.
1017 */
1018 private float[] coordsInChunkArray;
1019
1020 /**
1021 * Array containing texture coordinates to be added to current chunk of
1022 * data.
1023 */
1024 private float[] textureCoordsInChunkArray;
1025
1026 /**
1027 * Array containing normal coordinates to be added to current chunk of
1028 * data.
1029 */
1030 private float[] normalsInChunkArray;
1031
1032 /**
1033 * Array containing indices to be added to current chunk of data. Notice
1034 * that these indices are not the original indices appearing in the file.
1035 * Instead, they are indices referring to data in current chunk,
1036 * accounting for duplicate points, etc. This way, indices in a chunk
1037 * can be directly used to draw the chunk of data by the graphical layer.
1038 */
1039 private int[] indicesInChunkArray;
1040
1041 /**
1042 * Array containing vertex indices as they appear in the OBJ file.
1043 * These indices are only used to fetch data, they will never appear in
1044 * resulting chunk of data.
1045 */
1046 private long[] originalVertexIndicesInChunkArray;
1047
1048 /**
1049 * Array containing texture indices as they appear in the OBJ file.
1050 * These indices are only used to fetch data, they will never appear in
1051 * resulting chunk of data.
1052 */
1053 private long[] originalTextureIndicesInChunkArray;
1054
1055 /**
1056 * Array containing normal indices as they appear in the OBJ file.
1057 * These indices are only used to fetch data, they will never appear in
1058 * resulting chunk of data.
1059 */
1060 private long[] originalNormalIndicesInChunkArray;
1061
1062 /**
1063 * Map to relate vertex indices in a file respect to chunk indices.
1064 */
1065 private final TreeMap<Long, Integer> vertexIndicesMap;
1066
1067 /**
1068 * Map to relate texture coordinates indices in a file respect to chunk
1069 * indices.
1070 */
1071 private final TreeMap<Long, Integer> textureCoordsIndicesMap;
1072
1073 /**
1074 * Map to relate normals coordinates indices in a file respect to chunk
1075 * indices.
1076 */
1077 private final TreeMap<Long, Integer> normalsIndicesMap;
1078
1079 /**
1080 * Map to cache vertex positions in a file.
1081 */
1082 private final TreeMap<Long, Long> verticesStreamPositionMap;
1083
1084 /**
1085 * Map to cache texture coordinates positions in a file.
1086 */
1087 private final TreeMap<Long, Long> textureCoordsStreamPositionMap;
1088
1089 /**
1090 * Map to cache normals coordinates positions in a file.
1091 */
1092 private final TreeMap<Long, Long> normalsStreamPositionMap;
1093
1094 /**
1095 * Number of vertices stored in chunk.
1096 */
1097 private int verticesInChunk;
1098
1099 /**
1100 * Number of indices stored in chunk.
1101 */
1102 private int indicesInChunk;
1103
1104 /**
1105 * Size of indices stored in chunk.
1106 */
1107 private int indicesInChunkSize;
1108
1109 /**
1110 * Vertex position in file.
1111 */
1112 private long vertexStreamPosition;
1113
1114 /**
1115 * Texture coordinate position in file.
1116 */
1117 private long textureCoordStreamPosition;
1118
1119 /**
1120 * Normal coordinate position in file.
1121 */
1122 private long normalStreamPosition;
1123
1124 /**
1125 * Name of current material of data being loaded.
1126 */
1127 private String currentChunkMaterialName;
1128
1129 /**
1130 * Reference to current material of data being loaded.
1131 */
1132 private MaterialOBJ currentMaterial;
1133
1134 /**
1135 * Reference to material loader in charge of loading the associated MTL
1136 * of file to this OBJ file.
1137 */
1138 private MaterialLoaderOBJ materialLoader;
1139
1140 /**
1141 * Constructor.
1142 *
1143 * @param loader reference to loader loading binary file.
1144 * @throws IOException if an I/O error occurs.
1145 * @throws LoaderException if file data is corrupt or cannot be
1146 * understood.
1147 */
1148 public LoaderIteratorOBJ(final LoaderOBJ loader) throws IOException, LoaderException {
1149 this.loader = loader;
1150 nX = nY = nZ = 1.0f;
1151 vertexIndex = textureIndex = normalIndex = 0;
1152 verticesAvailable = textureAvailable = normalsAvailable = indicesAvailable = materialsAvailable = false;
1153 numberOfVertices = numberOfTextureCoords = numberOfNormals = numberOfFaces = 0;
1154 currentFace = 0;
1155 firstVertexStreamPosition = 0;
1156 firstVertexStreamPositionAvailable = false;
1157 firstTextureCoordStreamPosition = 0;
1158 firstTextureCoordStreamPositionAvailable = false;
1159 firstNormalStreamPosition = 0;
1160 firstNormalStreamPositionAvailable = false;
1161 firstFaceStreamPosition = 0;
1162 firstFaceStreamPositionAvailable = false;
1163 firstMaterialStreamPosition = 0;
1164 firstMaterialStreamPositionAvailable = false;
1165 currentStreamPosition = 0;
1166 listener = null;
1167 coordsInChunkArray = null;
1168 textureCoordsInChunkArray = null;
1169 normalsInChunkArray = null;
1170 indicesInChunkArray = null;
1171
1172 originalVertexIndicesInChunkArray = null;
1173 originalTextureIndicesInChunkArray = null;
1174 originalNormalIndicesInChunkArray = null;
1175
1176 vertexIndicesMap = new TreeMap<>();
1177 textureCoordsIndicesMap = new TreeMap<>();
1178 normalsIndicesMap = new TreeMap<>();
1179
1180 verticesStreamPositionMap = new TreeMap<>();
1181 textureCoordsStreamPositionMap = new TreeMap<>();
1182 normalsStreamPositionMap = new TreeMap<>();
1183
1184 verticesInChunk = indicesInChunk = 0;
1185 indicesInChunkSize = 0;
1186
1187 vertexStreamPosition = 0;
1188 textureCoordStreamPosition = 0;
1189 normalStreamPosition = 0;
1190
1191 minX = minY = minZ = Float.MAX_VALUE;
1192 maxX = maxY = maxZ = -Float.MAX_VALUE;
1193
1194 currentChunkMaterialName = "";
1195
1196 materialLoader = null;
1197
1198 setUp();
1199 }
1200
1201 /**
1202 * Method to set listener of this loader iterator.
1203 * This listener will be notified when the loading process finishes.
1204 *
1205 * @param listener listener of this loader iterator.
1206 */
1207 public void setListener(final LoaderIteratorListener listener) {
1208 this.listener = listener;
1209 }
1210
1211 /**
1212 * Indicates if there is another chunk of data to be loaded.
1213 *
1214 * @return true if there is another chunk of data, false otherwise.
1215 */
1216 @Override
1217 public boolean hasNext() {
1218 return currentFace < numberOfFaces;
1219 }
1220
1221 /**
1222 * Loads and returns next chunk of data, if available.
1223 *
1224 * @return next chunk of data.
1225 * @throws NotAvailableException thrown if no more data is available.
1226 * @throws LoaderException if file data is corrupt or cannot be
1227 * understood.
1228 * @throws IOException if an I/O error occurs.
1229 */
1230 @Override
1231 public DataChunk next() throws NotAvailableException, LoaderException, IOException {
1232 if (reader == null) {
1233 throw new IOException();
1234 }
1235
1236 if (!hasNext()) {
1237 throw new NotAvailableException();
1238 }
1239
1240 initChunkArrays();
1241
1242 // reset chunk bounding box values
1243 minX = minY = minZ = Float.MAX_VALUE;
1244 maxX = maxY = maxZ = -Float.MAX_VALUE;
1245
1246 final var progressStep = Math.max((long) (LoaderOBJ.PROGRESS_DELTA * numberOfFaces), 1);
1247
1248 boolean materialChange = false;
1249
1250 try {
1251 while (currentFace < numberOfFaces) { // && !materialChange
1252
1253 final var faceStreamPos = reader.getPosition();
1254 var str = reader.readLine();
1255 if ((str == null) && (currentFace < (numberOfFaces - 1))) {
1256 // unexpected end of file
1257 throw new LoaderException();
1258 } else if (str == null) {
1259 break;
1260 }
1261
1262 // check if line corresponds to face or material, otherwise,
1263 // ignore
1264 if (str.startsWith(USEMTL)) {
1265
1266 if (currentChunkMaterialName.isEmpty()) {
1267 currentChunkMaterialName = str.substring(USEMTL.length()).trim();
1268 // search current material on material library
1269 currentMaterial = null;
1270 if (materialLoader != null) {
1271 currentMaterial = materialLoader.getMaterialByName(currentChunkMaterialName);
1272 }
1273 } else {
1274 // stop reading this chunk and reset position to
1275 // beginning of line so that usemtl is read again
1276 materialChange = true;
1277 reader.seek(faceStreamPos);
1278 break;
1279 }
1280
1281 } else if (str.startsWith("f ")) {
1282
1283 // line is a face, so we keep data after "f"
1284 str = str.substring("f ".length()).trim();
1285 // retrieve words in data
1286 final var valuesTemp = str.split(" ");
1287 final var valuesSet = new HashSet<String[]>();
1288
1289 // check that each face contains three elements to define a
1290 // triangle only
1291 if (valuesTemp.length == 3) {
1292 valuesSet.add(valuesTemp);
1293
1294 } else if (valuesTemp.length > 3) {
1295 // if instead of a triangle we have a polygon then we
1296 // to divide valuesTemp into a set of values forming
1297 // triangles
1298 final var verticesList = getFaceValues(valuesTemp);
1299 try {
1300 valuesSet.addAll(buildTriangulatedIndices(verticesList));
1301 } catch (final TriangulatorException e) {
1302 // triangulation failed for some reason, but
1303 // file reading continues if configured like that
1304 // (by default it is)
1305 if (!continueIfTriangulationError) {
1306 throw new LoaderException(e);
1307 }
1308 }
1309 } else {
1310 throw new LoaderException();
1311 }
1312
1313
1314 // each word corresponds to a vertex/texture/normal index,
1315 // so we check if such number of indices can be added into
1316 // this chunk
1317 if ((verticesInChunk + valuesSet.size() * 3) > loader.maxVerticesInChunk) { // values.length
1318 // no more vertices can be added to chunk, so we reset
1319 // stream to start on current face
1320 reader.seek(faceStreamPos);
1321 break;
1322 }
1323
1324 // keep current stream position for next face
1325 currentStreamPosition = reader.getPosition();
1326
1327 for (final var values : valuesSet) {
1328
1329 // otherwise values can be added into chunk, so we read
1330 // vertex index, texture index and normal index
1331 for (final var value : values) {
1332 // value can be of the form v/vt/vn, where v stands
1333 // for vertex index, vt for texture index and vn for
1334 // normal index, and where vt and vn are optional
1335 final var indices = value.split("/");
1336 var addExistingVertexCoords = false;
1337 var addExistingTextureCoords = false;
1338 var addExistingNormal = false;
1339 var vertexCoordsChunkIndex = -1;
1340 var textureCoordsChunkIndex = -1;
1341 var normalChunkIndex = -1;
1342
1343 boolean addExisting;
1344 var chunkIndex = 0;
1345
1346 // first check if vertex has to be added as new or
1347 // not
1348 if (indices.length >= 1 && (!indices[0].isEmpty())) {
1349 indicesAvailable = true;
1350 // indices start at 1 in OBJ
1351 vertexIndex = Integer.parseInt(indices[0]) - 1;
1352
1353 // determine if vertex coordinates have to be
1354 // added as new, or they can be reused from an
1355 // existing vertex
1356 addExistingVertexCoords = !loader.allowDuplicateVerticesInChunk
1357 && (vertexCoordsChunkIndex = searchVertexIndexInChunk(vertexIndex)) >= 0;
1358 }
1359 if (indices.length >= 2 && (!indices[1].isEmpty())) {
1360 textureAvailable = true;
1361 // indices start at 1 in OBJ
1362 textureIndex = Integer.parseInt(indices[1]) - 1;
1363
1364 // determine if texture coordinates have to be
1365 // added as new, or they can be reused from an
1366 // existing vertex
1367 addExistingTextureCoords = !loader.allowDuplicateVerticesInChunk
1368 && (textureCoordsChunkIndex = searchTextureCoordIndexInChunk(textureIndex)) >= 0;
1369 }
1370 if (indices.length >= 3 && (!indices[2].isEmpty())) {
1371 normalsAvailable = true;
1372 // indices start at 1 in OBJ
1373 normalIndex = Integer.parseInt(indices[2]) - 1;
1374
1375 // determine if normal coordinates have to be
1376 // added as new, or they can be reused from an
1377 // existing vertex
1378 addExistingNormal = !loader.allowDuplicateVerticesInChunk
1379 && (normalChunkIndex = searchNormalIndexInChunk(normalIndex)) >= 0;
1380 }
1381
1382 // if either vertex coordinates, texture coordinates
1383 // or normal indicate that a new vertex needs to be
1384 // added, then do so, only if all three use an
1385 // existing vertex into chunk use that existing
1386 // vertex. Also, in case that existing vertex is
1387 // added, if chunk indices of existing vertex,
1388 // texture and normal are not the same add as a new
1389 // vertex into chunk
1390
1391 // if some chunk index is found, set add existing to
1392 // true
1393 addExisting = (vertexCoordsChunkIndex >= 0) || (textureCoordsChunkIndex >= 0)
1394 || (normalChunkIndex >= 0);
1395 // ensure that if index is present an existing vertex
1396 // in chunk exists
1397 if (indices.length >= 1 && (!indices[0].isEmpty())) {
1398 addExisting &= addExistingVertexCoords;
1399 }
1400 if (indices.length >= 2 && (!indices[1].isEmpty())) {
1401 addExisting &= addExistingTextureCoords;
1402 }
1403 if (indices.length >= 3 && (!indices[2].isEmpty())) {
1404 addExisting &= addExistingNormal;
1405 }
1406
1407 if (addExisting) {
1408 // if finally an existing vertex is added, set
1409 // chunk index
1410 if (vertexCoordsChunkIndex >= 0) {
1411 chunkIndex = vertexCoordsChunkIndex;
1412 }
1413 if (textureCoordsChunkIndex >= 0) {
1414 chunkIndex = textureCoordsChunkIndex;
1415 }
1416 if (normalChunkIndex >= 0) {
1417 chunkIndex = normalChunkIndex;
1418 }
1419 }
1420
1421 if (indices.length >= 1 && (!indices[0].isEmpty()) && !addExistingVertexCoords) {
1422 // new vertex needs to be added into chunk,
1423 // so we need to read vertex data
1424
1425 // fetch vertex data position
1426 fetchVertex(vertexIndex);
1427 vertexStreamPosition = reader.getPosition();
1428
1429 // read all vertex data
1430 String vertexLine = reader.readLine();
1431 if (!vertexLine.startsWith("v ")) {
1432 throw new LoaderException();
1433 }
1434 vertexLine = vertexLine.substring("v ".length()).trim();
1435 // retrieve words in vertexLine, which contain
1436 // vertex coordinates either as x, y, z or x,
1437 // y, z, w
1438 final var vertexCoordinates = vertexLine.split(" ");
1439 if (vertexCoordinates.length == 4) {
1440 // homogeneous coordinates x, y, z, w
1441
1442 // check that values are valid
1443 if (vertexCoordinates[0].isEmpty()) {
1444 throw new LoaderException();
1445 }
1446 if (vertexCoordinates[1].isEmpty()) {
1447 throw new LoaderException();
1448 }
1449 if (vertexCoordinates[2].isEmpty()) {
1450 throw new LoaderException();
1451 }
1452 if (vertexCoordinates[3].isEmpty()) {
1453 throw new LoaderException();
1454 }
1455
1456 final var w = Float.parseFloat(vertexCoordinates[3]);
1457 coordX = Float.parseFloat(vertexCoordinates[0]) / w;
1458 coordY = Float.parseFloat(vertexCoordinates[1]) / w;
1459 coordZ = Float.parseFloat(vertexCoordinates[2]) / w;
1460
1461 } else if (vertexCoordinates.length >= 3) {
1462 // inhomogeneous coordinates x, y, z
1463
1464 // check that values are valid
1465 if (vertexCoordinates[0].isEmpty()) {
1466 throw new LoaderException();
1467 }
1468 if (vertexCoordinates[1].isEmpty()) {
1469 throw new LoaderException();
1470 }
1471 if (vertexCoordinates[2].isEmpty()) {
1472 throw new LoaderException();
1473 }
1474
1475 coordX = Float.parseFloat(vertexCoordinates[0]);
1476 coordY = Float.parseFloat(vertexCoordinates[1]);
1477 coordZ = Float.parseFloat(vertexCoordinates[2]);
1478
1479 } else {
1480 // unsupported length
1481 throw new LoaderException();
1482 }
1483 }
1484 if (indices.length >= 2 && (!indices[1].isEmpty()) && !addExistingTextureCoords) {
1485 // new texture values need to be added into
1486 // chunk, so we need to read texture
1487 // coordinates data
1488
1489 // fetch texture data position
1490 fetchTexture(textureIndex);
1491 textureCoordStreamPosition = reader.getPosition();
1492
1493 // read all texture data
1494 var textureLine = reader.readLine();
1495 if (!textureLine.startsWith("vt ")) {
1496 throw new LoaderException();
1497 }
1498 textureLine = textureLine.substring("vt ".length()).trim();
1499 // retrieve words in textureLine, which contain
1500 // texture coordinates either as u, w or u, v, w
1501 final var textureCoordinates = textureLine.split(" ");
1502 if (textureCoordinates.length == 3) {
1503 // homogeneous coordinates u, v, w
1504
1505 // check that values are valid
1506 if (textureCoordinates[0].isEmpty()) {
1507 throw new LoaderException();
1508 }
1509 if (textureCoordinates[1].isEmpty()) {
1510 throw new LoaderException();
1511 }
1512 if (textureCoordinates[2].isEmpty()) {
1513 throw new LoaderException();
1514 }
1515
1516 final var w = Float.parseFloat(textureCoordinates[2]);
1517
1518 textureU = Float.parseFloat(textureCoordinates[0]) / w;
1519 textureV = Float.parseFloat(textureCoordinates[1]) / w;
1520 if (Math.abs(w) < Float.MIN_VALUE || Float.isInfinite(textureU)
1521 || Float.isNaN(textureU) || Float.isInfinite(textureV)
1522 || Float.isNaN(textureV)) {
1523 textureU = Float.parseFloat(textureCoordinates[0]);
1524 textureV = Float.parseFloat(textureCoordinates[1]);
1525 }
1526
1527 } else if (textureCoordinates.length >= 2) {
1528 // inhomogeneous coordinates u, v
1529
1530 // check that values are valid
1531 if (textureCoordinates[0].isEmpty()) {
1532 throw new LoaderException();
1533 }
1534 if (textureCoordinates[1].isEmpty()) {
1535 throw new LoaderException();
1536 }
1537
1538 textureU = Float.parseFloat(textureCoordinates[0]);
1539 textureV = Float.parseFloat(textureCoordinates[1]);
1540 } else {
1541 // unsupported length
1542 throw new LoaderException();
1543 }
1544 }
1545 if (indices.length >= 3 && (!indices[2].isEmpty()) && !addExistingNormal) {
1546 // new normal needs to be added into chunk,
1547 // so we need to read vertex data
1548
1549 // fetch normal data position
1550 fetchNormal(normalIndex);
1551 normalStreamPosition = reader.getPosition();
1552
1553 // read all normal data
1554 var normalLine = reader.readLine();
1555 if (!normalLine.startsWith("vn ")) {
1556 throw new LoaderException();
1557 }
1558 normalLine = normalLine.substring("vn ".length()).trim();
1559 // retrieve words in normalLine, which must
1560 // contain normal coordinates as x, y, z
1561 final var normalCoordinates = normalLine.split(" ");
1562 if (normalCoordinates.length == 3) {
1563 // normal coordinates x, y, z
1564
1565 // check that values are valid
1566 if (normalCoordinates[0].isEmpty()) {
1567 throw new LoaderException();
1568 }
1569 if (normalCoordinates[1].isEmpty()) {
1570 throw new LoaderException();
1571 }
1572 if (normalCoordinates[2].isEmpty()) {
1573 throw new LoaderException();
1574 }
1575
1576 nX = Float.parseFloat(normalCoordinates[0]);
1577 nY = Float.parseFloat(normalCoordinates[1]);
1578 nZ = Float.parseFloat(normalCoordinates[2]);
1579 } else {
1580 // unsupported length
1581 throw new LoaderException();
1582 }
1583 }
1584
1585 if (addExisting) {
1586 addExistingVertexToChunk(chunkIndex);
1587 } else {
1588 addNewVertexDataToChunk();
1589 }
1590 }
1591 }
1592 // reset face stream position
1593 reader.seek(currentStreamPosition);
1594 currentFace++;
1595 }
1596
1597 // compute progress
1598 if (loader.listener != null && (currentFace % progressStep) == 0) {
1599 loader.listener.onLoadProgressChange(loader,
1600 (float) (currentFace) / (float) (numberOfFaces));
1601 }
1602 }
1603 } catch (final NumberFormatException e) {
1604 throw new LoaderException(e);
1605 }
1606
1607 // trim arrays to store only needed data
1608 trimArrays();
1609
1610 // Instantiate DataChunk with chunk arrays
1611 final var dataChunk = new DataChunk();
1612
1613 if (verticesAvailable) {
1614 dataChunk.setVerticesCoordinatesData(coordsInChunkArray);
1615 dataChunk.setMinX(minX);
1616 dataChunk.setMinY(minY);
1617 dataChunk.setMinZ(minZ);
1618 dataChunk.setMaxX(maxX);
1619 dataChunk.setMaxY(maxY);
1620 dataChunk.setMaxZ(maxZ);
1621 } else {
1622 // so it can be garbage collected
1623 coordsInChunkArray = null;
1624 }
1625
1626 if (textureAvailable) {
1627 dataChunk.setTextureCoordinatesData(textureCoordsInChunkArray);
1628 } else {
1629 // so it can be garbage collected
1630 textureCoordsInChunkArray = null;
1631 }
1632
1633 if (currentMaterial != null) {
1634 dataChunk.setMaterial(currentMaterial);
1635 }
1636
1637 if (materialChange) {
1638 currentChunkMaterialName = "";
1639 currentMaterial = null;
1640 }
1641
1642 if (indicesAvailable) {
1643 dataChunk.setIndicesData(indicesInChunkArray);
1644 } else {
1645 // so it can be garbage collected
1646 indicesInChunkArray = null;
1647 }
1648
1649 if (normalsAvailable) {
1650 dataChunk.setNormalsData(normalsInChunkArray);
1651 } else {
1652 // so it can be garbage collected
1653 normalsInChunkArray = null;
1654 }
1655
1656 if (!hasNext() && listener != null) {
1657 // notify iterator finished
1658 listener.onIteratorFinished(this);
1659 }
1660
1661 // if no more chunks are available, then close input reader
1662 if (!hasNext()) {
1663 reader.close();
1664 }
1665
1666 return dataChunk;
1667 }
1668
1669 /**
1670 * Fetches vertex data in the file using provided index. Index refers
1671 * to indices contained in OBJ file.
1672 *
1673 * @param index index corresponding to vertex being fetched.
1674 * @throws LoaderException if data is corrupted or cannot be understood.
1675 * @throws IOException if an I/O error occurs.
1676 */
1677 public void fetchVertex(long index) throws LoaderException, IOException {
1678 if (index > numberOfVertices) {
1679 throw new LoaderException();
1680 }
1681
1682 var startStreamPos = firstVertexStreamPosition;
1683 var startIndex = 0L;
1684
1685 if (!verticesStreamPositionMap.isEmpty()) {
1686 // with floorEntry, we will pick element immediately
1687 // before or equal to index if any exists
1688 final var entry = verticesStreamPositionMap.floorEntry(index);
1689 if (entry != null) {
1690 final var origIndex = entry.getKey();
1691 final var pos = entry.getValue();
1692 if ((origIndex <= index) && (pos >= 0)) {
1693 startIndex = origIndex;
1694 startStreamPos = pos;
1695 }
1696 }
1697 }
1698
1699 // if we need to read next vertex, don't do anything, otherwise
1700 // move to next vertex location if reading some vertex located
1701 // further on the stream. For previous vertex indices, start
1702 // from beginning
1703 if (reader.getPosition() != startStreamPos) {
1704 reader.seek(startStreamPos);
1705 }
1706
1707 // read from stream until start of data of desired vertex
1708 var streamPosition = 0L;
1709 for (var i = startIndex; i <= index; i++) {
1710
1711 // when traversing stream of data until reaching desired
1712 // index, we add all vertex, texture and normal positions
1713 // into maps
1714 String str;
1715 var end = false;
1716 do {
1717 streamPosition = reader.getPosition();
1718 str = reader.readLine();
1719 if (str == null) {
1720 end = true;
1721 break;
1722 }
1723
1724 if (str.startsWith("v ")) {
1725 // line contains vertex coordinates, so we store
1726 // stream position into corresponding map and exit
1727 // while loop
1728 addVertexPositionToMap(i, streamPosition);
1729 break;
1730 }
1731 } while (true); // read until end of file when str == null
1732
1733 // unexpected end
1734 if (end) {
1735 throw new LoaderException();
1736 }
1737 }
1738
1739 // seek to last streamPosition which contains the desired data
1740 reader.seek(streamPosition);
1741 }
1742
1743 /**
1744 * Fetches texture data in the file using provided index. Index refers
1745 * to indices contained in OBJ file.
1746 *
1747 * @param index index corresponding to texture being fetched.
1748 * @throws LoaderException if data is corrupted or cannot be understood.
1749 * @throws IOException if an I/O error occurs.
1750 */
1751 public void fetchTexture(final long index) throws LoaderException, IOException {
1752 if (index > numberOfTextureCoords) {
1753 throw new LoaderException();
1754 }
1755
1756 var startStreamPos = firstTextureCoordStreamPosition;
1757 var startIndex = 0L;
1758
1759 if (!textureCoordsStreamPositionMap.isEmpty()) {
1760 // with floorEntry, we will pick element immediately
1761 // before or equal to index if any exists
1762 final var entry = textureCoordsStreamPositionMap.floorEntry(index);
1763 if (entry != null) {
1764 final var origIndex = entry.getKey();
1765 final var pos = entry.getValue();
1766 if ((origIndex <= index) && (pos >= 0)) {
1767 startIndex = origIndex;
1768 startStreamPos = pos;
1769 }
1770 }
1771 }
1772
1773 // if we need to read next texture vertex, don't do anything,
1774 // otherwise move to next texture vertex located further on the
1775 // stream. For previous texture vertex indices, start from
1776 // beginning
1777 if (reader.getPosition() != startStreamPos) {
1778 reader.seek(startStreamPos);
1779 }
1780
1781 // read from stream until start of data of desired texture vertex
1782 var streamPosition = 0L;
1783 for (var i = startIndex; i <= index; i++) {
1784
1785 // when traversing stream of data until reaching desired
1786 // index, we add all vertex, texture and normal positions
1787 // into maps
1788 String str;
1789 var end = false;
1790 do {
1791 streamPosition = reader.getPosition();
1792 str = reader.readLine();
1793 if (str == null) {
1794 end = true;
1795 break;
1796 }
1797
1798 if (str.startsWith("vt ")) {
1799 // line contains texture coordinates, so we store
1800 // stream position into corresponding map and exit
1801 // while loop
1802 addTextureCoordPositionToMap(i, streamPosition);
1803 break;
1804 }
1805 } while (true); // read until end of file when str == null
1806
1807 // unexpected end
1808 if (end) {
1809 throw new LoaderException();
1810 }
1811 }
1812
1813 // seek to last streamPosition which contains the desired data
1814 reader.seek(streamPosition);
1815 }
1816
1817 /**
1818 * Fetches normal data in the file using provided index. Index refers
1819 * to indices contained in OBJ file.
1820 *
1821 * @param index index corresponding to normal being fetched.
1822 * @throws LoaderException if data is corrupted or cannot be understood.
1823 * @throws IOException if an I/O error occurs.
1824 */
1825 public void fetchNormal(final long index) throws LoaderException, IOException {
1826 if (index > numberOfNormals) {
1827 throw new LoaderException();
1828 }
1829
1830 var startStreamPos = firstNormalStreamPosition;
1831 var startIndex = 0L;
1832
1833 if (!normalsStreamPositionMap.isEmpty()) {
1834 // with floorEntry, we will pick element immediately before or
1835 // equal to index if any exists
1836 final var entry = normalsStreamPositionMap.floorEntry(index);
1837 if (entry != null) {
1838 final var origIndex = entry.getKey();
1839 final var pos = entry.getValue();
1840 if ((origIndex <= index) && (pos >= 0)) {
1841 startIndex = origIndex;
1842 startStreamPos = pos;
1843 }
1844 }
1845 }
1846
1847 // if we need to read next normal, don't do anything, otherwise
1848 // move to next normal located further on the stream.
1849 // For previous normals indices, start from beginning
1850 if (reader.getPosition() != startStreamPos) {
1851 reader.seek(startStreamPos);
1852 }
1853
1854 // read from stream until start of data of desired normal
1855 var streamPosition = 0L;
1856 for (var i = startIndex; i <= index; i++) {
1857
1858 // when traversing stream of data until reaching desired
1859 // index, we add all vertex, texture and normal positions
1860 // into maps
1861 String str;
1862 var end = false;
1863 do {
1864 streamPosition = reader.getPosition();
1865 str = reader.readLine();
1866 if (str == null) {
1867 end = true;
1868 break;
1869 }
1870
1871 if (str.startsWith("vn ")) {
1872 // line contains normal, so we store stream position
1873 // into corresponding map and exit while loop
1874 addNormalPositionToMap(i, streamPosition);
1875 break;
1876 }
1877 } while (true); // read until end of file when str == null
1878
1879 // unexpected end
1880 if (end) {
1881 throw new LoaderException();
1882 }
1883 }
1884
1885 // seek to last streamPosition which contains the desired data
1886 reader.seek(streamPosition);
1887 }
1888
1889 /**
1890 * Internal method to decompose an array of vertices forming a polygon
1891 * in a set of arrays of vertices corresponding to triangles after
1892 * triangulation of the polygon. This method is used to triangulate
1893 * polygons with more than 3 vertices contained in the file.
1894 *
1895 * @param vertices list of vertices forming a polygon to be triangulated.
1896 * @return a set containing arrays of indices of vertices (in string
1897 * format) corresponding to the triangles forming the polygon after the
1898 * triangulation.
1899 * @throws TriangulatorException if triangulation fails (because polygon
1900 * is degenerate or contains invalid values such as NaN or infinity).
1901 */
1902 private Set<String[]> buildTriangulatedIndices(final List<VertexOBJ> vertices) throws TriangulatorException {
1903 final var polygonVertices = new ArrayList<Point3D>(vertices.size());
1904 for (final var v : vertices) {
1905 if (v.getVertex() == null) {
1906 throw new TriangulatorException();
1907 }
1908 polygonVertices.add(v.getVertex());
1909 }
1910 final var indices = new ArrayList<int[]>();
1911 final var triangulator = Triangulator3D.create();
1912 final var triangles = triangulator.triangulate(polygonVertices, indices);
1913
1914 final var result = new HashSet<String[]>();
1915 String[] face;
1916 var counter = 0;
1917 int[] triangleIndices;
1918 int index;
1919 VertexOBJ vertex;
1920 StringBuilder builder;
1921 for (final var ignored : triangles) {
1922 triangleIndices = indices.get(counter);
1923 face = new String[Triangle3D.NUM_VERTICES];
1924 for (var i = 0; i < Triangle3D.NUM_VERTICES; i++) {
1925 index = triangleIndices[i];
1926 vertex = vertices.get(index);
1927 builder = new StringBuilder();
1928 if (vertex.isVertexIndexAvailable()) {
1929 builder.append(vertex.getVertexIndex());
1930 }
1931 if (vertex.isTextureIndexAvailable() || vertex.isNormalIndexAvailable()) {
1932 builder.append("/");
1933 if (vertex.isTextureIndexAvailable()) {
1934 builder.append(vertex.getTextureIndex());
1935 }
1936 if (vertex.isNormalIndexAvailable()) {
1937 builder.append("/");
1938 builder.append(vertex.getNormalIndex());
1939 }
1940 }
1941
1942 face[i] = builder.toString();
1943 }
1944 counter++;
1945 result.add(face);
1946 }
1947
1948 return result;
1949 }
1950
1951 /**
1952 * This method reads a line containing face (i.e. polygon) indices of
1953 * vertices and fetches those vertices coordinates and associated data
1954 * such as texture coordinates or normal coordinates.
1955 *
1956 * @param values a string containing vertex indices forming a polygon.
1957 * Note that indices refer to the values contained in OBJ file, not the
1958 * indices in the chunk of data.
1959 * @return a list of vertices forming a face (i.e, polygon).
1960 * @throws IOException if an I/O error occurs.
1961 * @throws LoaderException if loading fails because data is corrupted or
1962 * cannot be interpreted.
1963 */
1964 private List<VertexOBJ> getFaceValues(final String[] values) throws IOException, LoaderException {
1965
1966 VertexOBJ tmpVertex;
1967 Point3D point;
1968 final var vertices = new ArrayList<VertexOBJ>(values.length);
1969
1970 // keep current stream position for next face
1971 final var tempPosition = reader.getPosition();
1972
1973 for (final var value : values) {
1974 tmpVertex = new VertexOBJ();
1975 point = Point3D.create();
1976 tmpVertex.setVertex(point);
1977
1978 final var indices = value.split("/");
1979
1980 if (indices.length >= 1 && (!indices[0].isEmpty())) {
1981 vertexIndex = Integer.parseInt(indices[0]) - 1;
1982 tmpVertex.setVertexIndex(vertexIndex + 1);
1983 fetchVertex(vertexIndex);
1984 vertexStreamPosition = reader.getPosition();
1985
1986 var vertexLine = reader.readLine();
1987 if (!vertexLine.startsWith("v ")) {
1988 throw new LoaderException();
1989 }
1990 vertexLine = vertexLine.substring("v ".length()).trim();
1991 final var vertexCoordinates = vertexLine.split(" ");
1992
1993 if (vertexCoordinates.length == 4) {
1994 // homogeneous coordinates x, y, z, w
1995 // ensure that vertex coordinates are not empty
1996 if (vertexCoordinates[0].isEmpty()) {
1997 throw new LoaderException();
1998 }
1999 if (vertexCoordinates[1].isEmpty()) {
2000 throw new LoaderException();
2001 }
2002 if (vertexCoordinates[2].isEmpty()) {
2003 throw new LoaderException();
2004 }
2005 if (vertexCoordinates[3].isEmpty()) {
2006 throw new LoaderException();
2007 }
2008
2009 try {
2010 point.setHomogeneousCoordinates(
2011 Double.parseDouble(vertexCoordinates[0]),
2012 Double.parseDouble(vertexCoordinates[1]),
2013 Double.parseDouble(vertexCoordinates[2]),
2014 Double.parseDouble(vertexCoordinates[3]));
2015 } catch (final NumberFormatException e) {
2016 // some vertex coordinate value could not be parsed
2017 throw new LoaderException(e);
2018 }
2019 } else if (vertexCoordinates.length >= 3) {
2020 // inhomogeneous coordinates x, y, z
2021 // ensure that vertex coordinate are not empty
2022 if (vertexCoordinates[0].isEmpty()) {
2023 throw new LoaderException();
2024 }
2025 if (vertexCoordinates[1].isEmpty()) {
2026 throw new LoaderException();
2027 }
2028 if (vertexCoordinates[2].isEmpty()) {
2029 throw new LoaderException();
2030 }
2031
2032 try {
2033 point.setInhomogeneousCoordinates(
2034 Double.parseDouble(vertexCoordinates[0]),
2035 Double.parseDouble(vertexCoordinates[1]),
2036 Double.parseDouble(vertexCoordinates[2]));
2037 } catch (final NumberFormatException e) {
2038 // some vertex coordinate value could not be parsed
2039 throw new LoaderException(e);
2040 }
2041 } else {
2042 // unsupported length
2043 throw new LoaderException();
2044 }
2045 }
2046 if (indices.length >= 2 && (!indices[1].isEmpty())) {
2047 tmpVertex.setTextureIndex(Integer.parseInt(indices[1]));
2048 }
2049 if (indices.length >= 3 && (!indices[2].isEmpty())) {
2050 tmpVertex.setNormalIndex(Integer.parseInt(indices[2]));
2051 }
2052
2053 vertices.add(tmpVertex);
2054 }
2055
2056 reader.seek(tempPosition);
2057 return vertices;
2058 }
2059
2060 /**
2061 * Initializes arrays forming current chunk of data.
2062 */
2063 private void initChunkArrays() {
2064 coordsInChunkArray = new float[loader.maxVerticesInChunk * 3];
2065 textureCoordsInChunkArray = new float[loader.maxVerticesInChunk * 2];
2066 normalsInChunkArray = new float[loader.maxVerticesInChunk * 3];
2067 indicesInChunkArray = new int[loader.maxVerticesInChunk];
2068
2069 originalVertexIndicesInChunkArray = new long[loader.maxVerticesInChunk];
2070 originalTextureIndicesInChunkArray = new long[loader.maxVerticesInChunk];
2071 originalNormalIndicesInChunkArray = new long[loader.maxVerticesInChunk];
2072 verticesInChunk = 0;
2073 indicesInChunk = 0;
2074 indicesInChunkSize = loader.maxVerticesInChunk;
2075
2076 vertexIndicesMap.clear();
2077 textureCoordsIndicesMap.clear();
2078 normalsIndicesMap.clear();
2079 }
2080
2081 /**
2082 * Searches vertex index in current chunk of data by using the index
2083 * used in the OBJ file.
2084 * This method searches within the cached indices which relate indices
2085 * in the chunk of data respect to indices in the OBJ file.
2086 *
2087 * @param originalIndex vertex index used in the OBJ file.
2088 * @return vertex index used in current chunk of data or -1 if not found.
2089 */
2090 private int searchVertexIndexInChunk(final long originalIndex) {
2091 // returns chunk index array position where index is found
2092 final var chunkIndex = vertexIndicesMap.get(originalIndex);
2093
2094 if (chunkIndex == null) {
2095 return -1;
2096 }
2097
2098 // returns index of vertex in chunk
2099 return indicesInChunkArray[chunkIndex];
2100 }
2101
2102 /**
2103 * Searches texture index in current chunk of data by using the index
2104 * used in the OBJ file.
2105 * This method searches within the cached indices which relate indices
2106 * in the chunk of data respect to indices in the OBJ file.
2107 *
2108 * @param originalIndex texture index used in the OBJ file.
2109 * @return texture index used in current chunk of data or -1 if not
2110 * found.
2111 */
2112 private int searchTextureCoordIndexInChunk(final long originalIndex) {
2113 return searchVertexIndexInChunk(originalIndex);
2114 }
2115
2116 /**
2117 * Searches normal index in current chunk of data by using the index
2118 * used in the OBJ file.
2119 * This method searches within the cached indices which relate indices
2120 * in the chunk of data respect to indices in the OBJ file.
2121 *
2122 * @param originalIndex normal index used in the OBJ file.
2123 * @return normal index used in current chunk of data or -1 if not found.
2124 */
2125 private int searchNormalIndexInChunk(final long originalIndex) {
2126 return searchVertexIndexInChunk(originalIndex);
2127 }
2128
2129 /**
2130 * Add vertex position to cache of file positions.
2131 *
2132 * @param originalIndex vertex index used in OBJ file.
2133 * @param streamPosition stream position where vertex is located.
2134 */
2135 private void addVertexPositionToMap(final long originalIndex, final long streamPosition) {
2136 if (verticesStreamPositionMap.size() > loader.maxStreamPositions) {
2137 // Map is full. Remove 1st item before adding a new one
2138 final var origIndex = verticesStreamPositionMap.firstKey();
2139 verticesStreamPositionMap.remove(origIndex);
2140 }
2141 // add new item
2142 verticesStreamPositionMap.put(originalIndex, streamPosition);
2143 }
2144
2145 /**
2146 * Add texture coordinate position to cache of file positions.
2147 *
2148 * @param originalIndex texture coordinate index used in OBJ file.
2149 * @param streamPosition stream position where texture coordinate is
2150 * located.
2151 */
2152 private void addTextureCoordPositionToMap(final long originalIndex, final long streamPosition) {
2153 if (textureCoordsStreamPositionMap.size() > loader.maxStreamPositions) {
2154 // Map is full. Remove 1st item before adding a new one
2155 final var origIndex = textureCoordsStreamPositionMap.firstKey();
2156 textureCoordsStreamPositionMap.remove(origIndex);
2157 }
2158 // add new item
2159 textureCoordsStreamPositionMap.put(originalIndex, streamPosition);
2160 }
2161
2162 /**
2163 * Add normal coordinate to cache of file positions.
2164 *
2165 * @param originalIndex normal coordinate index used in OBJ file.
2166 * @param streamPosition stream position where normal coordinate is
2167 * located.
2168 */
2169 private void addNormalPositionToMap(final long originalIndex, final long streamPosition) {
2170 if (normalsStreamPositionMap.size() > loader.maxStreamPositions) {
2171 // Map is full. Remove 1st item before adding a new one
2172 final var origIndex = normalsStreamPositionMap.firstKey();
2173 normalsStreamPositionMap.remove(origIndex);
2174 }
2175 // add new item
2176 normalsStreamPositionMap.put(originalIndex, streamPosition);
2177 }
2178
2179 /**
2180 * Adds data of last vertex being loaded to current chunk of data as a
2181 * new vertex.
2182 */
2183 private void addNewVertexDataToChunk() {
2184 var pos = 3 * verticesInChunk;
2185 var textPos = 2 * verticesInChunk;
2186
2187 coordsInChunkArray[pos] = coordX;
2188 normalsInChunkArray[pos] = nX;
2189 textureCoordsInChunkArray[textPos] = textureU;
2190
2191 pos++;
2192 textPos++;
2193
2194 coordsInChunkArray[pos] = coordY;
2195 normalsInChunkArray[pos] = nY;
2196 textureCoordsInChunkArray[textPos] = textureV;
2197
2198 pos++;
2199
2200 coordsInChunkArray[pos] = coordZ;
2201 normalsInChunkArray[pos] = nZ;
2202
2203 // update bounding box values
2204 if (coordX < minX) {
2205 minX = coordX;
2206 }
2207 if (coordY < minY) {
2208 minY = coordY;
2209 }
2210 if (coordZ < minZ) {
2211 minZ = coordZ;
2212 }
2213
2214 if (coordX > maxX) {
2215 maxX = coordX;
2216 }
2217 if (coordY > maxY) {
2218 maxY = coordY;
2219 }
2220 if (coordZ > maxZ) {
2221 maxZ = coordZ;
2222 }
2223
2224 // if arrays of indices become full, we need to resize them
2225 if (indicesInChunk >= indicesInChunkSize) {
2226 increaseIndicesArraySize();
2227 }
2228 indicesInChunkArray[indicesInChunk] = verticesInChunk;
2229 originalVertexIndicesInChunkArray[indicesInChunk] = vertexIndex;
2230 originalTextureIndicesInChunkArray[indicesInChunk] = textureIndex;
2231 originalNormalIndicesInChunkArray[indicesInChunk] = normalIndex;
2232 // store original indices in maps, so we can search chunk index by
2233 // original indices of vertices, texture or normal
2234 vertexIndicesMap.put((long) vertexIndex, indicesInChunk);
2235 textureCoordsIndicesMap.put((long) textureIndex, indicesInChunk);
2236 normalsIndicesMap.put((long) normalIndex, indicesInChunk);
2237
2238 // store vertex, texture and normal stream positions
2239 addVertexPositionToMap(vertexIndex, vertexStreamPosition);
2240 addTextureCoordPositionToMap(textureIndex, textureCoordStreamPosition);
2241 addNormalPositionToMap(normalIndex, normalStreamPosition);
2242
2243 verticesInChunk++;
2244 indicesInChunk++;
2245 }
2246
2247 /**
2248 * Adds index to current chunk of data referring to a previously
2249 * existing vertex in the chunk.
2250 *
2251 * @param existingIndex index of vertex that already exists in the chunk.
2252 */
2253 private void addExistingVertexToChunk(final int existingIndex) {
2254 // if arrays of indices become full, we need to resize them
2255 if (indicesInChunk >= indicesInChunkSize) {
2256 increaseIndicesArraySize();
2257 }
2258 indicesInChunkArray[indicesInChunk] = existingIndex;
2259 originalVertexIndicesInChunkArray[indicesInChunk] = vertexIndex;
2260 originalTextureIndicesInChunkArray[indicesInChunk] = textureIndex;
2261 originalNormalIndicesInChunkArray[indicesInChunk] = normalIndex;
2262
2263 indicesInChunk++;
2264 }
2265
2266 /**
2267 * Increases size of arrays of data. This method is called when needed.
2268 */
2269 private void increaseIndicesArraySize() {
2270 final var newIndicesInChunkSize = indicesInChunkSize + loader.maxVerticesInChunk;
2271 final var newIndicesInChunkArray = new int[newIndicesInChunkSize];
2272 final var newOriginalVertexIndicesInChunkArray = new long[newIndicesInChunkSize];
2273 final var newOriginalTextureIndicesInChunkArray = new long[newIndicesInChunkSize];
2274 final var newOriginalNormalIndicesInChunkArray = new long[newIndicesInChunkSize];
2275
2276 // copy contents of old array
2277 System.arraycopy(indicesInChunkArray, 0, newIndicesInChunkArray, 0, indicesInChunkSize);
2278 System.arraycopy(originalVertexIndicesInChunkArray, 0, newOriginalVertexIndicesInChunkArray,
2279 0, indicesInChunkSize);
2280 System.arraycopy(originalTextureIndicesInChunkArray, 0, newOriginalTextureIndicesInChunkArray,
2281 0, indicesInChunkSize);
2282 System.arraycopy(originalNormalIndicesInChunkArray, 0, newOriginalNormalIndicesInChunkArray,
2283 0, indicesInChunkSize);
2284
2285 // set new arrays and new size
2286 indicesInChunkArray = newIndicesInChunkArray;
2287 originalVertexIndicesInChunkArray = newOriginalVertexIndicesInChunkArray;
2288 originalTextureIndicesInChunkArray = newOriginalTextureIndicesInChunkArray;
2289 originalNormalIndicesInChunkArray = newOriginalNormalIndicesInChunkArray;
2290 indicesInChunkSize = newIndicesInChunkSize;
2291 }
2292
2293 /**
2294 * Trims arrays of data to reduce size of arrays to fit chunk data. This
2295 * method is loaded just before copying data to chunk being returned.
2296 */
2297 private void trimArrays() {
2298 if (verticesInChunk > 0) {
2299 final var elems = verticesInChunk * 3;
2300 final var textElems = verticesInChunk * 2;
2301
2302 final var newCoordsInChunkArray = new float[elems];
2303 final var newTextureCoordsInChunkArray = new float[elems];
2304 final var newNormalsInChunkArray = new float[elems];
2305
2306 // copy contents of old arrays
2307 System.arraycopy(coordsInChunkArray, 0, newCoordsInChunkArray, 0, elems);
2308 System.arraycopy(textureCoordsInChunkArray, 0, newTextureCoordsInChunkArray, 0,
2309 textElems);
2310 System.arraycopy(normalsInChunkArray, 0, newNormalsInChunkArray, 0, elems);
2311
2312 // set new arrays
2313 coordsInChunkArray = newCoordsInChunkArray;
2314 textureCoordsInChunkArray = newTextureCoordsInChunkArray;
2315 normalsInChunkArray = newNormalsInChunkArray;
2316 } else {
2317 // allow garbage collection
2318 coordsInChunkArray = null;
2319 textureCoordsInChunkArray = null;
2320 normalsInChunkArray = null;
2321 }
2322
2323 if (indicesInChunk > 0) {
2324 final var newIndicesInChunkArray = new int[indicesInChunk];
2325 System.arraycopy(indicesInChunkArray, 0, newIndicesInChunkArray, 0, indicesInChunk);
2326
2327 // set new array
2328 indicesInChunkArray = newIndicesInChunkArray;
2329 } else {
2330 // allow garbage collection
2331 indicesInChunkArray = null;
2332 originalVertexIndicesInChunkArray = null;
2333 originalTextureIndicesInChunkArray = null;
2334 originalNormalIndicesInChunkArray = null;
2335 }
2336 }
2337
2338 /**
2339 * Setups loader iterator. This method is called when constructing
2340 * this iterator.
2341 *
2342 * @throws IOException if an I/O error occurs.
2343 * @throws LoaderException if data is corrupted or cannot be understood.
2344 */
2345 private void setUp() throws IOException, LoaderException {
2346 numberOfVertices = numberOfTextureCoords = numberOfNormals = numberOfFaces = 0;
2347
2348 do {
2349 final var streamPosition = reader.getPosition();
2350 final var str = reader.readLine();
2351 if (str == null) {
2352 break;
2353 }
2354
2355 if (str.startsWith("#")) {
2356 // line is a comment, so we should add it to the list of
2357 // comments
2358 loader.comments.add(str.substring("#".length()).trim());
2359 } else if (str.startsWith("vt ")) {
2360 // line contains texture coordinates, so we keep its stream
2361 // position and indicate that chunks will contain texture
2362 // coordinates
2363 if (!firstTextureCoordStreamPositionAvailable) {
2364 firstTextureCoordStreamPosition = streamPosition;
2365 firstTextureCoordStreamPositionAvailable = true;
2366 textureAvailable = true;
2367 }
2368 numberOfTextureCoords++;
2369 } else if (str.startsWith("vn ")) {
2370 // line contains normal, so we keep its stream position and
2371 // indicate that chunks will contain normals
2372 if (!firstNormalStreamPositionAvailable) {
2373 firstNormalStreamPosition = streamPosition;
2374 firstNormalStreamPositionAvailable = true;
2375 normalsAvailable = true;
2376 }
2377 numberOfNormals++;
2378 } else if (str.startsWith("v ")) {
2379 // line contains vertex coordinates, so we keep its stream
2380 // position and indicate that chunks will contain vertex
2381 // coordinates
2382 if (!firstVertexStreamPositionAvailable) {
2383 firstVertexStreamPosition = streamPosition;
2384 firstVertexStreamPositionAvailable = true;
2385 verticesAvailable = true;
2386 }
2387 numberOfVertices++;
2388 } else if (str.startsWith("f ")) {
2389 // line contains face definition, so we keep its stream
2390 // position and indicate that chunks will contain indices
2391 if (!firstFaceStreamPositionAvailable) {
2392 firstFaceStreamPosition = streamPosition;
2393 firstFaceStreamPositionAvailable = true;
2394 indicesAvailable = true;
2395 }
2396
2397 numberOfFaces++;
2398
2399 } else if (str.startsWith("mtllib ")) {
2400 // a material library is found
2401 final var path = str.substring("mtllib ".length()).trim();
2402 if (loader.listener instanceof LoaderListenerOBJ loaderListener) {
2403 materialLoader = loaderListener.onMaterialLoaderRequested(loader, path);
2404 } else {
2405 materialLoader = new MaterialLoaderOBJ(new File(path));
2406 }
2407
2408 // now load library of materials
2409 try {
2410 if (materialLoader != null) {
2411 loader.materials = materialLoader.load();
2412 // to release file resources
2413 materialLoader.close();
2414 }
2415 } catch (final LoaderException e) {
2416 throw e;
2417 } catch (final Exception e) {
2418 throw new LoaderException(e);
2419 }
2420
2421 } else if (str.startsWith(USEMTL) && !firstMaterialStreamPositionAvailable) {
2422 firstMaterialStreamPositionAvailable = true;
2423 firstMaterialStreamPosition = streamPosition;
2424 materialsAvailable = true;
2425 }
2426
2427 // ignore any other line
2428 } while (true); // read until end of file when str == null
2429
2430 // move to first face tream position
2431 if (!firstFaceStreamPositionAvailable) {
2432 throw new LoaderException();
2433 }
2434
2435 if (materialsAvailable && firstMaterialStreamPosition < firstFaceStreamPosition) {
2436 reader.seek(firstMaterialStreamPosition);
2437 } else {
2438 reader.seek(firstFaceStreamPosition);
2439 }
2440 }
2441 }
2442 }