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.nio.ByteBuffer;
21  import java.nio.file.Files;
22  
23  /**
24   * Loads a custom binary file implemented for this library.
25   * The binary format has been created to keep 3D data in a more compact way than
26   * other formats.
27   */
28  public class LoaderBinary extends Loader {
29  
30      /**
31       * Buffer size to load input file.
32       */
33      public static final int BUFFER_SIZE = 1024;
34  
35      /**
36       * Version number of the binary format supported by this class.
37       */
38      private static final byte SUPPORTED_VERSION = 2;
39  
40      /**
41       * Number of bytes required to determine the bounding box of a chunk or
42       * the whole 3D object (which are 2 3D points = 2x3 coordinates =
43       * 6 floats * 4 bytes per float).
44       */
45      private static final int BOUNDING_BYTES_SIZE = 6 * Float.SIZE / 8;
46  
47      /**
48       * Iterator to load binary data in small chunks.
49       * Usually data is divided in chunks that can be directly loaded by
50       * graphic layers such as OpenGL.
51       */
52      private LoaderIteratorBinary loaderIterator;
53  
54      /**
55       * Indicates if file has been checked to have a valid header. Notice that
56       * file might still be corrupt or incomplete at some point.
57       */
58      private final boolean validityChecked;
59  
60      /**
61       * Indicates if after checking validity, the file header has been found to
62       * be valid or not.
63       */
64      private boolean validFile;
65  
66      /**
67       * Constructor.
68       */
69      public LoaderBinary() {
70          loaderIterator = null;
71          validityChecked = false;
72          validFile = false;
73      }
74  
75      /**
76       * Constructor.
77       *
78       * @param f file to be loaded.
79       * @throws IOException if an I/O error occurs.
80       */
81      public LoaderBinary(final File f) throws IOException {
82          super(f);
83          loaderIterator = null;
84          validityChecked = false;
85          validFile = false;
86      }
87  
88      /**
89       * Constructor.
90       *
91       * @param listener listener to be notified of loading progress.
92       */
93      public LoaderBinary(final LoaderListener listener) {
94          super(listener);
95          loaderIterator = null;
96          validityChecked = false;
97          validFile = false;
98      }
99  
100     /**
101      * Constructor.
102      *
103      * @param f        file to be loaded.
104      * @param listener listener to be notified of loading progress.
105      * @throws IOException if an I/O error occurs.
106      */
107     public LoaderBinary(final File f, final LoaderListener listener) throws IOException {
108         super(f, listener);
109         loaderIterator = null;
110         validityChecked = false;
111         validFile = false;
112     }
113 
114     /**
115      * If loader is ready to start loading a file.
116      * This is true once a file has been provided.
117      *
118      * @return true if ready to start loading a file, false otherwise.
119      */
120     @Override
121     public boolean isReady() {
122         return hasFile();
123     }
124 
125     /**
126      * Returns mesh format supported by this class, which is MESH_FORMAT_BINARY2.
127      *
128      * @return mesh format supported by this class.
129      */
130     @Override
131     public MeshFormat getMeshFormat() {
132         return MeshFormat.MESH_FORMAT_BINARY2;
133     }
134 
135     /**
136      * Determines if provided file is a valid file that can be read by this
137      * loader.
138      *
139      * @return true if file is valid, false otherwise.
140      * @throws LockedException raised if this instance is already locked.
141      * @throws IOException     if an I/O error occurs.
142      */
143     @Override
144     public boolean isValidFile() throws LockedException, IOException {
145         if (!hasFile()) {
146             throw new IOException();
147         }
148         if (isLocked()) {
149             throw new LockedException();
150         }
151 
152         if (!validityChecked) {
153             // check that file version is supported
154             final var version = reader.readByte();
155             validFile = (version == SUPPORTED_VERSION);
156         }
157 
158         return validFile;
159     }
160 
161     /**
162      * Starts the loading process of provided file.
163      * This method returns a LoaderIterator to start the iterative process to
164      * load a file in small chunks of data.
165      *
166      * @return a loader iterator to read the file in a step-by-step process.
167      * @throws LockedException   raised if this instance is already locked.
168      * @throws NotReadyException raised if this instance is not yet ready.
169      * @throws IOException       if an I/O error occurs.
170      * @throws LoaderException   if file is corrupted or cannot be interpreted.
171      */
172     @Override
173     public LoaderIterator load() throws LockedException, NotReadyException, IOException, LoaderException {
174         if (isLocked()) {
175             throw new LockedException();
176         }
177         if (!isReady()) {
178             throw new NotReadyException();
179         }
180 
181         // check file validity by reading its version
182         if (!isValidFile()) {
183             throw new LoaderException();
184         }
185 
186         setLocked(true);
187         if (listener != null) {
188             listener.onLoadStart(this);
189         }
190 
191         // read textures until no more textures are available
192         while (reader.readBoolean()) {
193             // texture data follows
194             final var texId = reader.readInt();
195             final var texWidth = reader.readInt();
196             final var texHeight = reader.readInt();
197             final var texLength = reader.readLong();
198             final var textureFileStartPos = reader.getPosition();
199             final var textureFileEndPos = textureFileStartPos + texLength;
200 
201             // check that at least texLength bytes remain otherwise file is
202             // incomplete or corrupted
203             if (textureFileEndPos > file.length()) {
204                 throw new LoaderException();
205             }
206 
207             // notify that texture data is available
208             if (listener instanceof LoaderListenerBinary loaderListener) {
209 
210                 // request file where texture will be stored
211                 final var texFile = loaderListener.onTextureReceived(this, texId, texWidth, texHeight);
212 
213                 if (texFile != null) {
214                     // write texture data at provided file
215                     final var buffer = new byte[BUFFER_SIZE];
216 
217                     var counter = 0;
218                     try (final var outStream = Files.newOutputStream(texFile.toPath())) {
219                         int n;
220                         var len = BUFFER_SIZE;
221                         while (counter < texLength) {
222                             if (counter + len > texLength) {
223                                 len = (int) texLength - counter;
224                             }
225                             n = reader.read(buffer, 0, len);
226                             outStream.write(buffer, 0, n);
227                             if (n > 0) {
228                                 counter += n;
229                             } else {
230                                 break;
231                             }
232                         }
233 
234                         outStream.flush();
235                     }
236 
237                     if (counter != texLength) {
238                         throw new LoaderException();
239                     }
240 
241                     // notify that texture data has been written to provided file
242                     final var valid = loaderListener.onTextureDataAvailable(this, texFile, texId, texWidth,
243                             texHeight);
244                     // texture processing couldn't be correctly done
245                     if (!valid) {
246                         throw new LoaderException();
247                     }
248                 } else {
249                     // skip to end of texture file in case that listener
250                     // didn't provide a file to write texture data
251                     reader.skip(texLength);
252                 }
253             } else {
254                 // skip to end of texture file in case that there is no listener
255                 reader.skip(texLength);
256             }
257         }
258 
259         loaderIterator = new LoaderIteratorBinary(this);
260         loaderIterator.setListener(new LoaderIteratorListenerImpl(this));
261         return loaderIterator;
262     }
263 
264     /**
265      * Internal listener to be notified when loading process finishes.
266      * This listener is used to free resources when loading process finishes.
267      */
268     private class LoaderIteratorListenerImpl implements LoaderIteratorListener {
269 
270         /**
271          * Reference to Loader loading binary file.
272          */
273         private final LoaderBinary loader;
274 
275         /**
276          * Constructor.
277          *
278          * @param loader reference to Loader.
279          */
280         public LoaderIteratorListenerImpl(final LoaderBinary loader) {
281             this.loader = loader;
282         }
283 
284         /**
285          * Method to be notified when the loading process finishes.
286          *
287          * @param iterator iterator loading the file in chunks.
288          */
289         @Override
290         public void onIteratorFinished(final LoaderIterator iterator) {
291             // because iterator is finished, we should allow subsequent calls to
292             // load method
293 
294             // on subsequent calls
295             if (listener != null) {
296                 listener.onLoadEnd(loader);
297             }
298             setLocked(false);
299         }
300     }
301 
302     /**
303      * Loader iterator in charge of loading file data in small chunks.
304      * Usually data is divided in chunks small enough that can be directly
305      * loaded by graphical layers such as OpenGL (which has a limit of 65535
306      * indices when using Vertex Buffer Objects, which increase graphical
307      * performance).
308      */
309     private class LoaderIteratorBinary implements LoaderIterator {
310 
311         /**
312          * Reference to loader loading binary file.
313          */
314         private final LoaderBinary loader;
315 
316         /**
317          * Reference to the listener of this loader iterator. This listener will
318          * be notified when the loading process finishes so that resources can
319          * be freed.
320          */
321         private LoaderIteratorListener listener;
322 
323         /**
324          * Constructor.
325          *
326          * @param loader reference to loader loading binary file.
327          */
328         public LoaderIteratorBinary(final LoaderBinary loader) {
329             this.loader = loader;
330             listener = null;
331         }
332 
333         /**
334          * Method to set listener of this loader iterator.
335          * This listener will be notified when the loading process finishes.
336          *
337          * @param listener listener of this loader iterator.
338          */
339         public void setListener(final LoaderIteratorListener listener) {
340             this.listener = listener;
341         }
342 
343         /**
344          * Indicates if there is another chunk of data to be loaded.
345          *
346          * @return true if there is another chunk of data, false otherwise.
347          */
348         @Override
349         public boolean hasNext() {
350             try {
351                 return !reader.isEndOfStream();
352             } catch (final IOException e) {
353                 return false;
354             }
355         }
356 
357         /**
358          * Loads and returns next chunk of data, if available.
359          *
360          * @return next chunk of data.
361          * @throws NotAvailableException thrown if no more data is available.
362          * @throws LoaderException       if file data is corrupt or cannot be
363          *                               understood.
364          * @throws IOException           if an I/O error occurs.
365          */
366         @Override
367         public DataChunk next() throws NotAvailableException, LoaderException, IOException {
368             if (!hasNext()) {
369                 throw new NotAvailableException();
370             }
371 
372             // read chunk size
373             final var chunkSize = reader.readInt();
374 
375             // ensure that chunk size is positive, otherwise file is corrupted
376             if (chunkSize < 0) {
377                 throw new LoaderException();
378             }
379 
380             // get position of start of chunk
381             final var chunkStartPos = reader.getPosition();
382 
383             // position of end of chunk
384             final var chunkEndPos = chunkStartPos + chunkSize;
385 
386             // check that at least chunkSize bytes remain otherwise file is
387             // incomplete or corrupted
388             final var fileLength = file.length();
389             if (chunkEndPos > fileLength) {
390                 throw new LoaderException();
391             }
392 
393             final var chunk = new DataChunk();
394 
395             // ----- MATERIAL ------
396             if (reader.readBoolean()) {
397                 // material is available
398                 final var materialId = reader.readInt();
399 
400                 final var material = new Material();
401                 chunk.setMaterial(material);
402                 material.setId(materialId);
403 
404                 if (reader.readBoolean()) {
405                     // ambient color is available
406                     byte b;
407                     // red
408                     b = reader.readByte();
409                     material.setAmbientRedColor((short) (b & 0x000000ff));
410                     // green
411                     b = reader.readByte();
412                     material.setAmbientGreenColor((short) (b & 0x000000ff));
413                     // blue
414                     b = reader.readByte();
415                     material.setAmbientBlueColor((short) (b & 0x000000ff));
416                 }
417 
418                 if (reader.readBoolean()) {
419                     // diffuse color is available
420                     // red
421                     var b = reader.readByte();
422                     material.setDiffuseRedColor((short) (b & 0x000000ff));
423                     // green
424                     b = reader.readByte();
425                     material.setDiffuseGreenColor((short) (b & 0x000000ff));
426                     // blue
427                     b = reader.readByte();
428                     material.setDiffuseBlueColor((short) (b & 0x000000ff));
429                 }
430 
431                 if (reader.readBoolean()) {
432                     // specular color is available
433                     // red
434                     var b = reader.readByte();
435                     material.setSpecularRedColor((short) (b & 0x000000ff));
436                     // green
437                     b = reader.readByte();
438                     material.setSpecularGreenColor((short) (b & 0x000000ff));
439                     // blue
440                     b = reader.readByte();
441                     material.setSpecularBlueColor((short) (b & 0x000000ff));
442                 }
443 
444                 if (reader.readBoolean()) {
445                     // specular coefficient is available
446                     material.setSpecularCoefficient(reader.readFloat());
447                 }
448 
449                 if (reader.readBoolean()) {
450                     // ambient texture map is available
451                     final var textureId = reader.readInt();
452                     final var tex = new Texture(textureId);
453                     material.setAmbientTextureMap(tex);
454 
455                     tex.setWidth(reader.readInt());
456                     tex.setHeight(reader.readInt());
457                 }
458 
459                 if (reader.readBoolean()) {
460                     // diffuse texture map is available
461                     final var textureId = reader.readInt();
462                     final var tex = new Texture(textureId);
463                     material.setDiffuseTextureMap(tex);
464 
465                     tex.setWidth(reader.readInt());
466                     tex.setHeight(reader.readInt());
467                 }
468 
469                 if (reader.readBoolean()) {
470                     // specular texture map is available
471                     final var textureId = reader.readInt();
472                     final var tex = new Texture(textureId);
473                     material.setSpecularTextureMap(tex);
474 
475                     tex.setWidth(reader.readInt());
476                     tex.setHeight(reader.readInt());
477                 }
478 
479                 if (reader.readBoolean()) {
480                     // alpha texture map is available
481                     final var textureId = reader.readInt();
482                     final var tex = new Texture(textureId);
483                     material.setAlphaTextureMap(tex);
484 
485                     tex.setWidth(reader.readInt());
486                     tex.setHeight(reader.readInt());
487                 }
488 
489                 if (reader.readBoolean()) {
490                     // bump texture map is available
491                     final var textureId = reader.readInt();
492                     final var tex = new Texture(textureId);
493                     material.setBumpTextureMap(tex);
494 
495                     tex.setWidth(reader.readInt());
496                     tex.setHeight(reader.readInt());
497                 }
498 
499                 if (reader.readBoolean()) {
500                     // transparency is available
501                     final var b = reader.readByte();
502                     material.setTransparency((short) (b & 0x000000ff));
503                 }
504 
505                 if (reader.readBoolean()) {
506                     // illumination is available
507                     final var value = reader.readInt();
508                     material.setIllumination(Illumination.forValue(value));
509                 }
510             }
511 
512             // ---- COORDS -------
513 
514             // read coords size
515             final var coordsSizeInBytes = reader.readInt();
516 
517             // ensure that coords size is positive, otherwise file is corrupted
518             if (coordsSizeInBytes < 0) {
519                 throw new LoaderException();
520             }
521             // if size in bytes is not multiple of float size (4 bytes), then
522             // file is corrupted
523             if (coordsSizeInBytes % (Float.SIZE / 8) != 0) {
524                 throw new LoaderException();
525             }
526 
527             // if coords are available
528             if (coordsSizeInBytes > 0) {
529                 // ensure that coords fit within chunk, otherwise file is
530                 // corrupted
531                 if (reader.getPosition() + coordsSizeInBytes > chunkEndPos) {
532                     throw new LoaderException();
533                 }
534 
535                 // get number of floats in coords
536                 final var coordsLength = coordsSizeInBytes / (Float.SIZE / 8);
537 
538                 // read coordsSize bytes into array of floats
539                 final var coords = new float[coordsLength];
540                 final var bytes = new byte[coordsSizeInBytes];
541                 reader.read(bytes);
542                 final var bytesBuffer = ByteBuffer.wrap(bytes);
543                 final var floatBuffer = bytesBuffer.asFloatBuffer();
544                 floatBuffer.get(coords);
545 
546                 chunk.setVerticesCoordinatesData(coords);
547 
548                 // compute progress
549                 if (loader.listener != null) {
550                     loader.listener.onLoadProgressChange(loader,
551                             (float) (reader.getPosition()) / (float) (file.length()));
552                 }
553             }
554 
555             // ----- COLORS ------
556 
557             // read colors size
558             final var colorsSizeInBytes = reader.readInt();
559 
560             // ensure that colors size is positive, otherwise file is corrupted
561             if (colorsSizeInBytes < 0) {
562                 throw new LoaderException();
563             }
564 
565             // each color data is stored in a byte, so there is no need to check size multiplicity
566 
567             // if colors are available
568             if (colorsSizeInBytes > 0) {
569                 // ensure that colors fit within chunk, otherwise file is corrupted
570                 if (reader.getPosition() + colorsSizeInBytes > chunkEndPos) {
571                     throw new LoaderException();
572                 }
573 
574                 // read colorSizeInBytes into array of shorts (conversion
575                 // must be done from unsigned bytes to shorts, as java does not
576                 // support unsigned bytes values
577                 final var colors = new short[colorsSizeInBytes];
578                 final var bytes = new byte[colorsSizeInBytes];
579                 reader.read(bytes);
580                 for (var i = 0; i < colorsSizeInBytes; i++) {
581                     // convert signed bytes into unsigned bytes stored in shorts
582                     colors[i] = (short) (bytes[i] & 0x000000ff);
583                 }
584                 chunk.setColorData(colors);
585 
586                 // read color components
587                 chunk.setColorComponents(reader.readInt());
588 
589                 // compute progress
590                 if (loader.listener != null) {
591                     loader.listener.onLoadProgressChange(loader,
592                             (float) (reader.getPosition()) / (float) (file.length()));
593                 }
594             }
595 
596             // ------ INDICES ------
597 
598             // read indices size
599             final var indicesSizeInBytes = reader.readInt();
600 
601             // ensure that indices size is positive, otherwise file is corrupted
602             if (indicesSizeInBytes < 0) {
603                 throw new LoaderException();
604             }
605             // if size in bytes is not multiple of float size (4 bytes), then file is corrupted
606             if (indicesSizeInBytes % (Short.SIZE / 8) != 0) {
607                 throw new LoaderException();
608             }
609 
610             // if indices are available
611             if (indicesSizeInBytes > 0) {
612                 // ensure that indices fit within chunk, otherwise file is corrupted
613                 if (reader.getPosition() + indicesSizeInBytes > chunkEndPos) {
614                     throw new LoaderException();
615                 }
616 
617                 // get number of shorts in indices
618                 final var indicesLength = indicesSizeInBytes / (Short.SIZE / 8);
619 
620                 // read coordsSize bytes into array of floats
621                 final var indices = new int[indicesLength];
622                 final var bytes = new byte[indicesSizeInBytes];
623                 reader.read(bytes);
624                 int firstByte;
625                 int secondByte;
626                 var counter = 0;
627                 for (var i = 0; i < indicesLength; i++) {
628                     firstByte = bytes[counter] & 0x000000ff;
629                     counter++;
630                     secondByte = bytes[counter] & 0x000000ff;
631                     counter++;
632                     indices[i] = firstByte << 8 | secondByte;
633                 }
634                 chunk.setIndicesData(indices);
635 
636                 // compute progress
637                 if (loader.listener != null) {
638                     loader.listener.onLoadProgressChange(loader,
639                             (float) (reader.getPosition()) / (float) (file.length()));
640                 }
641             }
642 
643             // -------- TEXTURE COORDS --------
644 
645             // read texture coords size
646             final var texCoordsSizeInBytes = reader.readInt();
647 
648             // ensure that texture coords size is positive, otherwise file is corrupted
649             if (texCoordsSizeInBytes < 0) {
650                 throw new LoaderException();
651             }
652             // if size in bytes is not multiple of float size (4 bytes), then
653             // file is corrupted
654             if (texCoordsSizeInBytes % (Float.SIZE / 8) != 0) {
655                 throw new LoaderException();
656             }
657 
658             // if texture coords are available
659             if (texCoordsSizeInBytes > 0) {
660                 // ensure that texture coords fit within chunk, otherwise file is
661                 // corrupted
662                 if (reader.getPosition() + texCoordsSizeInBytes > chunkEndPos) {
663                     throw new LoaderException();
664                 }
665 
666                 // get number of floats in coords
667                 final var texCoordsLength = texCoordsSizeInBytes / (Float.SIZE / 8);
668 
669                 // read coordsSize bytes into array of floats
670                 final var texCoords = new float[texCoordsLength];
671                 final var bytes = new byte[texCoordsSizeInBytes];
672                 reader.read(bytes);
673                 final var bytesBuffer = ByteBuffer.wrap(bytes);
674                 final var floatBuffer = bytesBuffer.asFloatBuffer();
675                 floatBuffer.get(texCoords);
676                 chunk.setTextureCoordinatesData(texCoords);
677 
678                 // compute progress
679                 if (loader.listener != null) {
680                     loader.listener.onLoadProgressChange(loader,
681                             (float) (reader.getPosition()) / (float) (file.length()));
682                 }
683             }
684 
685             // -------- NORMALS --------
686 
687             // read normals size
688             final var normalsSizeInBytes = reader.readInt();
689 
690             // ensure that normals size is positive, otherwise file is
691             // corrupted
692             if (normalsSizeInBytes < 0) {
693                 throw new LoaderException();
694             }
695             // if size in bytes is not multiple of float size (4 bytes), then
696             // file is corrupted
697             if (normalsSizeInBytes % (Float.SIZE / 8) != 0) {
698                 throw new LoaderException();
699             }
700 
701             // if texture coords are available
702             if (normalsSizeInBytes > 0) {
703                 // ensure that normals fit within chunk, otherwise file is
704                 // corrupted
705                 if (reader.getPosition() + normalsSizeInBytes > chunkEndPos) {
706                     throw new LoaderException();
707                 }
708 
709                 // get number of floats in coords
710                 final var normalsLength = normalsSizeInBytes / (Float.SIZE / 8);
711 
712                 // read coordsSize bytes into array of floats
713                 final var normals = new float[normalsLength];
714                 final var bytes = new byte[normalsSizeInBytes];
715                 reader.read(bytes);
716                 final var bytesBuffer = ByteBuffer.wrap(bytes);
717                 final var floatBuffer = bytesBuffer.asFloatBuffer();
718                 floatBuffer.get(normals);
719                 chunk.setNormalsData(normals);
720 
721                 // compute progress
722                 if (loader.listener != null) {
723                     loader.listener.onLoadProgressChange(loader,
724                             (float) (reader.getPosition()) / (float) (file.length()));
725                 }
726             }
727 
728             // read bounding box for chunk (min/max x, y, z)
729 
730             // we need to load 6 floats, so position + 6 * Float.SIZE / 8 bytes must fit within chunk
731             if (reader.getPosition() + (BOUNDING_BYTES_SIZE) > chunkEndPos) {
732                 throw new LoaderException();
733             }
734 
735             final var bytes = new byte[BOUNDING_BYTES_SIZE];
736             reader.read(bytes);
737             final var bytesBuffer = ByteBuffer.wrap(bytes);
738             final var floatBuffer = bytesBuffer.asFloatBuffer();
739             chunk.setMinX(floatBuffer.get());
740             chunk.setMinY(floatBuffer.get());
741             chunk.setMinZ(floatBuffer.get());
742 
743             chunk.setMaxX(floatBuffer.get());
744             chunk.setMaxY(floatBuffer.get());
745             chunk.setMaxZ(floatBuffer.get());
746 
747             // compute progress
748             if (loader.listener != null) {
749                 loader.listener.onLoadProgressChange(loader,
750                         (float) (reader.getPosition()) / (float) (file.length()));
751             }
752 
753             if (!hasNext() && listener != null) {
754                 // notify iterator finished
755                 listener.onIteratorFinished(this);
756             }
757 
758             return chunk;
759         }
760     }
761 }