LoaderBinary.java
/*
* Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.irurueta.geometry.io;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
/**
* Loads a custom binary file implemented for this library.
* The binary format has been created to keep 3D data in a more compact way than
* other formats.
*/
public class LoaderBinary extends Loader {
/**
* Buffer size to load input file.
*/
public static final int BUFFER_SIZE = 1024;
/**
* Version number of the binary format supported by this class.
*/
private static final byte SUPPORTED_VERSION = 2;
/**
* Number of bytes required to determine the bounding box of a chunk or
* the whole 3D object (which are 2 3D points = 2x3 coordinates =
* 6 floats * 4 bytes per float).
*/
private static final int BOUNDING_BYTES_SIZE = 6 * Float.SIZE / 8;
/**
* Iterator to load binary data in small chunks.
* Usually data is divided in chunks that can be directly loaded by
* graphic layers such as OpenGL.
*/
private LoaderIteratorBinary loaderIterator;
/**
* Indicates if file has been checked to have a valid header. Notice that
* file might still be corrupt or incomplete at some point.
*/
private final boolean validityChecked;
/**
* Indicates if after checking validity, the file header has been found to
* be valid or not.
*/
private boolean validFile;
/**
* Constructor.
*/
public LoaderBinary() {
loaderIterator = null;
validityChecked = false;
validFile = false;
}
/**
* Constructor.
*
* @param f file to be loaded.
* @throws IOException if an I/O error occurs.
*/
public LoaderBinary(final File f) throws IOException {
super(f);
loaderIterator = null;
validityChecked = false;
validFile = false;
}
/**
* Constructor.
*
* @param listener listener to be notified of loading progress.
*/
public LoaderBinary(final LoaderListener listener) {
super(listener);
loaderIterator = null;
validityChecked = false;
validFile = false;
}
/**
* Constructor.
*
* @param f file to be loaded.
* @param listener listener to be notified of loading progress.
* @throws IOException if an I/O error occurs.
*/
public LoaderBinary(final File f, final LoaderListener listener) throws IOException {
super(f, listener);
loaderIterator = null;
validityChecked = false;
validFile = false;
}
/**
* If loader is ready to start loading a file.
* This is true once a file has been provided.
*
* @return true if ready to start loading a file, false otherwise.
*/
@Override
public boolean isReady() {
return hasFile();
}
/**
* Returns mesh format supported by this class, which is MESH_FORMAT_BINARY2.
*
* @return mesh format supported by this class.
*/
@Override
public MeshFormat getMeshFormat() {
return MeshFormat.MESH_FORMAT_BINARY2;
}
/**
* Determines if provided file is a valid file that can be read by this
* loader.
*
* @return true if file is valid, false otherwise.
* @throws LockedException raised if this instance is already locked.
* @throws IOException if an I/O error occurs.
*/
@Override
public boolean isValidFile() throws LockedException, IOException {
if (!hasFile()) {
throw new IOException();
}
if (isLocked()) {
throw new LockedException();
}
if (!validityChecked) {
// check that file version is supported
final var version = reader.readByte();
validFile = (version == SUPPORTED_VERSION);
}
return validFile;
}
/**
* Starts the loading process of provided file.
* This method returns a LoaderIterator to start the iterative process to
* load a file in small chunks of data.
*
* @return a loader iterator to read the file in a step-by-step process.
* @throws LockedException raised if this instance is already locked.
* @throws NotReadyException raised if this instance is not yet ready.
* @throws IOException if an I/O error occurs.
* @throws LoaderException if file is corrupted or cannot be interpreted.
*/
@Override
public LoaderIterator load() throws LockedException, NotReadyException, IOException, LoaderException {
if (isLocked()) {
throw new LockedException();
}
if (!isReady()) {
throw new NotReadyException();
}
// check file validity by reading its version
if (!isValidFile()) {
throw new LoaderException();
}
setLocked(true);
if (listener != null) {
listener.onLoadStart(this);
}
// read textures until no more textures are available
while (reader.readBoolean()) {
// texture data follows
final var texId = reader.readInt();
final var texWidth = reader.readInt();
final var texHeight = reader.readInt();
final var texLength = reader.readLong();
final var textureFileStartPos = reader.getPosition();
final var textureFileEndPos = textureFileStartPos + texLength;
// check that at least texLength bytes remain otherwise file is
// incomplete or corrupted
if (textureFileEndPos > file.length()) {
throw new LoaderException();
}
// notify that texture data is available
if (listener instanceof LoaderListenerBinary loaderListener) {
// request file where texture will be stored
final var texFile = loaderListener.onTextureReceived(this, texId, texWidth, texHeight);
if (texFile != null) {
// write texture data at provided file
final var buffer = new byte[BUFFER_SIZE];
var counter = 0;
try (final var outStream = Files.newOutputStream(texFile.toPath())) {
int n;
var len = BUFFER_SIZE;
while (counter < texLength) {
if (counter + len > texLength) {
len = (int) texLength - counter;
}
n = reader.read(buffer, 0, len);
outStream.write(buffer, 0, n);
if (n > 0) {
counter += n;
} else {
break;
}
}
outStream.flush();
}
if (counter != texLength) {
throw new LoaderException();
}
// notify that texture data has been written to provided file
final var valid = loaderListener.onTextureDataAvailable(this, texFile, texId, texWidth,
texHeight);
// texture processing couldn't be correctly done
if (!valid) {
throw new LoaderException();
}
} else {
// skip to end of texture file in case that listener
// didn't provide a file to write texture data
reader.skip(texLength);
}
} else {
// skip to end of texture file in case that there is no listener
reader.skip(texLength);
}
}
loaderIterator = new LoaderIteratorBinary(this);
loaderIterator.setListener(new LoaderIteratorListenerImpl(this));
return loaderIterator;
}
/**
* Internal listener to be notified when loading process finishes.
* This listener is used to free resources when loading process finishes.
*/
private class LoaderIteratorListenerImpl implements LoaderIteratorListener {
/**
* Reference to Loader loading binary file.
*/
private final LoaderBinary loader;
/**
* Constructor.
*
* @param loader reference to Loader.
*/
public LoaderIteratorListenerImpl(final LoaderBinary loader) {
this.loader = loader;
}
/**
* Method to be notified when the loading process finishes.
*
* @param iterator iterator loading the file in chunks.
*/
@Override
public void onIteratorFinished(final LoaderIterator iterator) {
// because iterator is finished, we should allow subsequent calls to
// load method
// on subsequent calls
if (listener != null) {
listener.onLoadEnd(loader);
}
setLocked(false);
}
}
/**
* Loader iterator in charge of loading file data in small chunks.
* Usually data is divided in chunks small enough that can be directly
* loaded by graphical layers such as OpenGL (which has a limit of 65535
* indices when using Vertex Buffer Objects, which increase graphical
* performance).
*/
private class LoaderIteratorBinary implements LoaderIterator {
/**
* Reference to loader loading binary file.
*/
private final LoaderBinary loader;
/**
* Reference to the listener of this loader iterator. This listener will
* be notified when the loading process finishes so that resources can
* be freed.
*/
private LoaderIteratorListener listener;
/**
* Constructor.
*
* @param loader reference to loader loading binary file.
*/
public LoaderIteratorBinary(final LoaderBinary loader) {
this.loader = loader;
listener = null;
}
/**
* Method to set listener of this loader iterator.
* This listener will be notified when the loading process finishes.
*
* @param listener listener of this loader iterator.
*/
public void setListener(final LoaderIteratorListener listener) {
this.listener = listener;
}
/**
* Indicates if there is another chunk of data to be loaded.
*
* @return true if there is another chunk of data, false otherwise.
*/
@Override
public boolean hasNext() {
try {
return !reader.isEndOfStream();
} catch (final IOException e) {
return false;
}
}
/**
* Loads and returns next chunk of data, if available.
*
* @return next chunk of data.
* @throws NotAvailableException thrown if no more data is available.
* @throws LoaderException if file data is corrupt or cannot be
* understood.
* @throws IOException if an I/O error occurs.
*/
@Override
public DataChunk next() throws NotAvailableException, LoaderException, IOException {
if (!hasNext()) {
throw new NotAvailableException();
}
// read chunk size
final var chunkSize = reader.readInt();
// ensure that chunk size is positive, otherwise file is corrupted
if (chunkSize < 0) {
throw new LoaderException();
}
// get position of start of chunk
final var chunkStartPos = reader.getPosition();
// position of end of chunk
final var chunkEndPos = chunkStartPos + chunkSize;
// check that at least chunkSize bytes remain otherwise file is
// incomplete or corrupted
final var fileLength = file.length();
if (chunkEndPos > fileLength) {
throw new LoaderException();
}
final var chunk = new DataChunk();
// ----- MATERIAL ------
if (reader.readBoolean()) {
// material is available
final var materialId = reader.readInt();
final var material = new Material();
chunk.setMaterial(material);
material.setId(materialId);
if (reader.readBoolean()) {
// ambient color is available
byte b;
// red
b = reader.readByte();
material.setAmbientRedColor((short) (b & 0x000000ff));
// green
b = reader.readByte();
material.setAmbientGreenColor((short) (b & 0x000000ff));
// blue
b = reader.readByte();
material.setAmbientBlueColor((short) (b & 0x000000ff));
}
if (reader.readBoolean()) {
// diffuse color is available
// red
var b = reader.readByte();
material.setDiffuseRedColor((short) (b & 0x000000ff));
// green
b = reader.readByte();
material.setDiffuseGreenColor((short) (b & 0x000000ff));
// blue
b = reader.readByte();
material.setDiffuseBlueColor((short) (b & 0x000000ff));
}
if (reader.readBoolean()) {
// specular color is available
// red
var b = reader.readByte();
material.setSpecularRedColor((short) (b & 0x000000ff));
// green
b = reader.readByte();
material.setSpecularGreenColor((short) (b & 0x000000ff));
// blue
b = reader.readByte();
material.setSpecularBlueColor((short) (b & 0x000000ff));
}
if (reader.readBoolean()) {
// specular coefficient is available
material.setSpecularCoefficient(reader.readFloat());
}
if (reader.readBoolean()) {
// ambient texture map is available
final var textureId = reader.readInt();
final var tex = new Texture(textureId);
material.setAmbientTextureMap(tex);
tex.setWidth(reader.readInt());
tex.setHeight(reader.readInt());
}
if (reader.readBoolean()) {
// diffuse texture map is available
final var textureId = reader.readInt();
final var tex = new Texture(textureId);
material.setDiffuseTextureMap(tex);
tex.setWidth(reader.readInt());
tex.setHeight(reader.readInt());
}
if (reader.readBoolean()) {
// specular texture map is available
final var textureId = reader.readInt();
final var tex = new Texture(textureId);
material.setSpecularTextureMap(tex);
tex.setWidth(reader.readInt());
tex.setHeight(reader.readInt());
}
if (reader.readBoolean()) {
// alpha texture map is available
final var textureId = reader.readInt();
final var tex = new Texture(textureId);
material.setAlphaTextureMap(tex);
tex.setWidth(reader.readInt());
tex.setHeight(reader.readInt());
}
if (reader.readBoolean()) {
// bump texture map is available
final var textureId = reader.readInt();
final var tex = new Texture(textureId);
material.setBumpTextureMap(tex);
tex.setWidth(reader.readInt());
tex.setHeight(reader.readInt());
}
if (reader.readBoolean()) {
// transparency is available
final var b = reader.readByte();
material.setTransparency((short) (b & 0x000000ff));
}
if (reader.readBoolean()) {
// illumination is available
final var value = reader.readInt();
material.setIllumination(Illumination.forValue(value));
}
}
// ---- COORDS -------
// read coords size
final var coordsSizeInBytes = reader.readInt();
// ensure that coords size is positive, otherwise file is corrupted
if (coordsSizeInBytes < 0) {
throw new LoaderException();
}
// if size in bytes is not multiple of float size (4 bytes), then
// file is corrupted
if (coordsSizeInBytes % (Float.SIZE / 8) != 0) {
throw new LoaderException();
}
// if coords are available
if (coordsSizeInBytes > 0) {
// ensure that coords fit within chunk, otherwise file is
// corrupted
if (reader.getPosition() + coordsSizeInBytes > chunkEndPos) {
throw new LoaderException();
}
// get number of floats in coords
final var coordsLength = coordsSizeInBytes / (Float.SIZE / 8);
// read coordsSize bytes into array of floats
final var coords = new float[coordsLength];
final var bytes = new byte[coordsSizeInBytes];
reader.read(bytes);
final var bytesBuffer = ByteBuffer.wrap(bytes);
final var floatBuffer = bytesBuffer.asFloatBuffer();
floatBuffer.get(coords);
chunk.setVerticesCoordinatesData(coords);
// compute progress
if (loader.listener != null) {
loader.listener.onLoadProgressChange(loader,
(float) (reader.getPosition()) / (float) (file.length()));
}
}
// ----- COLORS ------
// read colors size
final var colorsSizeInBytes = reader.readInt();
// ensure that colors size is positive, otherwise file is corrupted
if (colorsSizeInBytes < 0) {
throw new LoaderException();
}
// each color data is stored in a byte, so there is no need to check size multiplicity
// if colors are available
if (colorsSizeInBytes > 0) {
// ensure that colors fit within chunk, otherwise file is corrupted
if (reader.getPosition() + colorsSizeInBytes > chunkEndPos) {
throw new LoaderException();
}
// read colorSizeInBytes into array of shorts (conversion
// must be done from unsigned bytes to shorts, as java does not
// support unsigned bytes values
final var colors = new short[colorsSizeInBytes];
final var bytes = new byte[colorsSizeInBytes];
reader.read(bytes);
for (var i = 0; i < colorsSizeInBytes; i++) {
// convert signed bytes into unsigned bytes stored in shorts
colors[i] = (short) (bytes[i] & 0x000000ff);
}
chunk.setColorData(colors);
// read color components
chunk.setColorComponents(reader.readInt());
// compute progress
if (loader.listener != null) {
loader.listener.onLoadProgressChange(loader,
(float) (reader.getPosition()) / (float) (file.length()));
}
}
// ------ INDICES ------
// read indices size
final var indicesSizeInBytes = reader.readInt();
// ensure that indices size is positive, otherwise file is corrupted
if (indicesSizeInBytes < 0) {
throw new LoaderException();
}
// if size in bytes is not multiple of float size (4 bytes), then file is corrupted
if (indicesSizeInBytes % (Short.SIZE / 8) != 0) {
throw new LoaderException();
}
// if indices are available
if (indicesSizeInBytes > 0) {
// ensure that indices fit within chunk, otherwise file is corrupted
if (reader.getPosition() + indicesSizeInBytes > chunkEndPos) {
throw new LoaderException();
}
// get number of shorts in indices
final var indicesLength = indicesSizeInBytes / (Short.SIZE / 8);
// read coordsSize bytes into array of floats
final var indices = new int[indicesLength];
final var bytes = new byte[indicesSizeInBytes];
reader.read(bytes);
int firstByte;
int secondByte;
var counter = 0;
for (var i = 0; i < indicesLength; i++) {
firstByte = bytes[counter] & 0x000000ff;
counter++;
secondByte = bytes[counter] & 0x000000ff;
counter++;
indices[i] = firstByte << 8 | secondByte;
}
chunk.setIndicesData(indices);
// compute progress
if (loader.listener != null) {
loader.listener.onLoadProgressChange(loader,
(float) (reader.getPosition()) / (float) (file.length()));
}
}
// -------- TEXTURE COORDS --------
// read texture coords size
final var texCoordsSizeInBytes = reader.readInt();
// ensure that texture coords size is positive, otherwise file is corrupted
if (texCoordsSizeInBytes < 0) {
throw new LoaderException();
}
// if size in bytes is not multiple of float size (4 bytes), then
// file is corrupted
if (texCoordsSizeInBytes % (Float.SIZE / 8) != 0) {
throw new LoaderException();
}
// if texture coords are available
if (texCoordsSizeInBytes > 0) {
// ensure that texture coords fit within chunk, otherwise file is
// corrupted
if (reader.getPosition() + texCoordsSizeInBytes > chunkEndPos) {
throw new LoaderException();
}
// get number of floats in coords
final var texCoordsLength = texCoordsSizeInBytes / (Float.SIZE / 8);
// read coordsSize bytes into array of floats
final var texCoords = new float[texCoordsLength];
final var bytes = new byte[texCoordsSizeInBytes];
reader.read(bytes);
final var bytesBuffer = ByteBuffer.wrap(bytes);
final var floatBuffer = bytesBuffer.asFloatBuffer();
floatBuffer.get(texCoords);
chunk.setTextureCoordinatesData(texCoords);
// compute progress
if (loader.listener != null) {
loader.listener.onLoadProgressChange(loader,
(float) (reader.getPosition()) / (float) (file.length()));
}
}
// -------- NORMALS --------
// read normals size
final var normalsSizeInBytes = reader.readInt();
// ensure that normals size is positive, otherwise file is
// corrupted
if (normalsSizeInBytes < 0) {
throw new LoaderException();
}
// if size in bytes is not multiple of float size (4 bytes), then
// file is corrupted
if (normalsSizeInBytes % (Float.SIZE / 8) != 0) {
throw new LoaderException();
}
// if texture coords are available
if (normalsSizeInBytes > 0) {
// ensure that normals fit within chunk, otherwise file is
// corrupted
if (reader.getPosition() + normalsSizeInBytes > chunkEndPos) {
throw new LoaderException();
}
// get number of floats in coords
final var normalsLength = normalsSizeInBytes / (Float.SIZE / 8);
// read coordsSize bytes into array of floats
final var normals = new float[normalsLength];
final var bytes = new byte[normalsSizeInBytes];
reader.read(bytes);
final var bytesBuffer = ByteBuffer.wrap(bytes);
final var floatBuffer = bytesBuffer.asFloatBuffer();
floatBuffer.get(normals);
chunk.setNormalsData(normals);
// compute progress
if (loader.listener != null) {
loader.listener.onLoadProgressChange(loader,
(float) (reader.getPosition()) / (float) (file.length()));
}
}
// read bounding box for chunk (min/max x, y, z)
// we need to load 6 floats, so position + 6 * Float.SIZE / 8 bytes must fit within chunk
if (reader.getPosition() + (BOUNDING_BYTES_SIZE) > chunkEndPos) {
throw new LoaderException();
}
final var bytes = new byte[BOUNDING_BYTES_SIZE];
reader.read(bytes);
final var bytesBuffer = ByteBuffer.wrap(bytes);
final var floatBuffer = bytesBuffer.asFloatBuffer();
chunk.setMinX(floatBuffer.get());
chunk.setMinY(floatBuffer.get());
chunk.setMinZ(floatBuffer.get());
chunk.setMaxX(floatBuffer.get());
chunk.setMaxY(floatBuffer.get());
chunk.setMaxZ(floatBuffer.get());
// compute progress
if (loader.listener != null) {
loader.listener.onLoadProgressChange(loader,
(float) (reader.getPosition()) / (float) (file.length()));
}
if (!hasNext() && listener != null) {
// notify iterator finished
listener.onIteratorFinished(this);
}
return chunk;
}
}
}