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 }