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.BufferedOutputStream;
19 import java.io.DataOutputStream;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.nio.file.Files;
24
25 /**
26 * Reads a 3D object and converts it into custom binary format.
27 */
28 public class MeshWriterBinary extends MeshWriter {
29
30 /**
31 * Version of the binary file supported by this class.
32 */
33 public static final byte VERSION = 2;
34
35 /**
36 * Buffer size to write data into output stream.
37 */
38 public static final int BUFFER_SIZE = 1024;
39
40 /**
41 * Stream to write binary data to output.
42 */
43 private DataOutputStream dataStream;
44
45 /**
46 * Constructor.
47 *
48 * @param loader loader to load a 3D file.
49 * @param stream stream where trans-coded data will be written to.
50 */
51 public MeshWriterBinary(final Loader loader, final OutputStream stream) {
52 super(loader, stream);
53 }
54
55 /**
56 * Constructor.
57 *
58 * @param loader loader to load a 3D file.
59 * @param stream stream where trans-coded data will be written to.
60 * @param listener listener to be notified of progress changes or when
61 * transcoding process starts or finishes.
62 */
63 public MeshWriterBinary(final Loader loader, final OutputStream stream, final MeshWriterListener listener) {
64 super(loader, stream, listener);
65 }
66
67 /**
68 * Processes input file provided to loader and writes it trans-coded into
69 * output stream.
70 *
71 * @throws LoaderException if 3D file loading fails.
72 * @throws IOException if an I/O error occurs.
73 * @throws NotReadyException if mesh writer is not ready because either a
74 * loader has not been provided or an output stream has not been provided.
75 * @throws LockedException if this mesh writer is locked processing a file.
76 */
77 @SuppressWarnings("DuplicatedCode")
78 @Override
79 public void write() throws LoaderException, IOException, NotReadyException, LockedException {
80
81 if (!isReady()) {
82 throw new NotReadyException();
83 }
84 if (isLocked()) {
85 throw new LockedException();
86 }
87
88 try {
89 dataStream = new DataOutputStream(new BufferedOutputStream(stream));
90
91 locked = true;
92 if (listener != null) {
93 listener.onWriteStart(this);
94 }
95
96 loader.setListener(this.internalListeners);
97
98 // write version
99 dataStream.writeByte(VERSION);
100
101 final var iter = loader.load();
102
103 var minX = Float.MAX_VALUE;
104 var minY = Float.MAX_VALUE;
105 var minZ = Float.MAX_VALUE;
106 var maxX = -Float.MAX_VALUE;
107 var maxY = -Float.MAX_VALUE;
108 var maxZ = -Float.MAX_VALUE;
109
110 while (iter.hasNext()) {
111 final var chunk = iter.next();
112 if (listener != null) {
113 listener.onChunkAvailable(this, chunk);
114 }
115
116 final var coords = chunk.getVerticesCoordinatesData();
117 final var colors = chunk.getColorData();
118 final var indices = chunk.getIndicesData();
119 final var textureCoords = chunk.getTextureCoordinatesData();
120 final var normals = chunk.getNormalsData();
121 final var colorComponents = chunk.getColorComponents();
122
123 final var coordsAvailable = (coords != null);
124 final var colorsAvailable = (colors != null);
125 final var indicesAvailable = (indices != null);
126 final var textureCoordsAvailable = (textureCoords != null);
127 final var normalsAvailable = (normals != null);
128
129 if (chunk.getMinX() < minX) {
130 minX = chunk.getMinX();
131 }
132 if (chunk.getMinY() < minY) {
133 minY = chunk.getMinY();
134 }
135 if (chunk.getMinZ() < minZ) {
136 minZ = chunk.getMinZ();
137 }
138
139 if (chunk.getMaxX() > maxX) {
140 maxX = chunk.getMaxX();
141 }
142 if (chunk.getMaxY() > maxY) {
143 maxY = chunk.getMaxY();
144 }
145 if (chunk.getMaxZ() > maxZ) {
146 maxZ = chunk.getMaxZ();
147 }
148
149 // compute size of material in bytes
150 final var material = chunk.getMaterial();
151 // boolean indicating availability
152 // of material
153 var materialSizeInBytes = 1;
154
155 if (material != null) {
156 // material id (int)
157 materialSizeInBytes += Integer.SIZE / 8;
158 // ambient color (RGB) -> 3 bytes
159
160 //boolean indicating availability
161 materialSizeInBytes += 1;
162 if (material.isAmbientColorAvailable()) {
163 // RGB components
164 materialSizeInBytes += 3;
165 }
166
167 // diffuse color
168
169 // boolean indicating availability
170 materialSizeInBytes += 1;
171 if (material.isDiffuseColorAvailable()) {
172 // RGB components
173 materialSizeInBytes += 3;
174 }
175
176 // specular color
177
178 // boolean indicating availability
179 materialSizeInBytes += 1;
180 if (material.isSpecularColorAvailable()) {
181 // RGB components
182 materialSizeInBytes += 3;
183 }
184
185 // specular coefficient (float)
186
187 // boolean indicating availability
188 materialSizeInBytes += 1;
189 if (material.isSpecularCoefficientAvailable()) {
190 materialSizeInBytes += Float.SIZE / 8;
191 }
192
193 // ambient texture map
194
195 // boolean indicating availability
196 materialSizeInBytes += 1;
197 if (material.isAmbientTextureMapAvailable()) {
198 // id of texture (int)
199 materialSizeInBytes += Integer.SIZE / 8;
200 // texture width (int)
201 materialSizeInBytes += Integer.SIZE / 8;
202 // texture height (int)
203 materialSizeInBytes += Integer.SIZE / 8;
204 }
205
206 // diffuse texture map
207
208 // boolean indicating availability
209 materialSizeInBytes += 1;
210 if (material.isDiffuseTextureMapAvailable()) {
211 // id of texture (int)
212 materialSizeInBytes += Integer.SIZE / 8;
213 // texture width (int)
214 materialSizeInBytes += Integer.SIZE / 8;
215 // texture height (int)
216 materialSizeInBytes += Integer.SIZE / 8;
217 }
218
219 // specular texture map
220
221 // boolean indicating availability
222 materialSizeInBytes += 1;
223 if (material.isSpecularTextureMapAvailable()) {
224 // id of texture (int)
225 materialSizeInBytes += Integer.SIZE / 8;
226 // texture width (int)
227 materialSizeInBytes += Integer.SIZE / 8;
228 // texture height (int)
229 materialSizeInBytes += Integer.SIZE / 8;
230 }
231
232 // alpha texture map
233
234 // boolean indicating availability
235 materialSizeInBytes += 1;
236 if (material.isAlphaTextureMapAvailable()) {
237 // id of texture (int)
238 materialSizeInBytes += Integer.SIZE / 8;
239 // texture width (int)
240 materialSizeInBytes += Integer.SIZE / 8;
241 // texture height (int)
242 materialSizeInBytes += Integer.SIZE / 8;
243 }
244
245 // bump texture map
246
247 // boolean indicating availability
248 materialSizeInBytes += 1;
249 if (material.isBumpTextureMapAvailable()) {
250 // id of texture (int)
251 materialSizeInBytes += Integer.SIZE / 8;
252 // texture width (int)
253 materialSizeInBytes += Integer.SIZE / 8;
254 // texture height (int)
255 materialSizeInBytes += Integer.SIZE / 8;
256 }
257
258 // transparency
259
260 // availability
261 materialSizeInBytes += 1;
262 if (material.isTransparencyAvailable()) {
263 // one byte 0-255 of
264 // transparency
265 materialSizeInBytes += 1;
266 }
267
268 // enum of illumination(int)
269 // boolean containing availability
270 materialSizeInBytes += 1;
271 if (material.isIlluminationAvailable()) {
272 materialSizeInBytes += Integer.SIZE / 8;
273 }
274 }
275
276 // compute size of chunk data in bytes
277 var coordsSizeInBytes = 0;
278 var colorsSizeInBytes = 0;
279 var indicesSizeInBytes = 0;
280 var textureCoordsSizeInBytes = 0;
281 var normalsSizeInBytes = 0;
282
283 if (coordsAvailable) {
284 coordsSizeInBytes = coords.length * Float.SIZE / 8;
285 }
286 if (colorsAvailable) {
287 colorsSizeInBytes = colors.length;
288 }
289 if (indicesAvailable) {
290 indicesSizeInBytes = indices.length * Short.SIZE / 8;
291 }
292 if (textureCoordsAvailable) {
293 textureCoordsSizeInBytes = textureCoords.length * Float.SIZE / 8;
294 }
295 if (normalsAvailable) {
296 normalsSizeInBytes = normals.length * Float.SIZE / 8;
297 }
298
299 int chunkSize = materialSizeInBytes + coordsSizeInBytes + colorsSizeInBytes + indicesSizeInBytes
300 + textureCoordsSizeInBytes + normalsSizeInBytes
301 + (5 * Integer.SIZE / 8) + // sizes
302 (6 * Float.SIZE / 8); // min/max values
303 if (colorsAvailable) {
304 // bytes for number of color components
305 chunkSize += Integer.SIZE / 8;
306 }
307
308 // indicate that no more textures follow
309 if (!ignoreTextureValidation) {
310 // byte below is written only once after all textures and
311 // before all vertex data
312 dataStream.writeBoolean(false);
313
314 // so that no more textures are
315 // written into stream
316 ignoreTextureValidation = true;
317 }
318
319 // write total chunk size
320 dataStream.writeInt(chunkSize);
321
322 // indicate material availability
323 dataStream.writeBoolean(material != null);
324 if (material != null) {
325 // material id
326 dataStream.writeInt(material.getId());
327
328 // ambient color
329 dataStream.writeBoolean(material.isAmbientColorAvailable());
330 if (material.isAmbientColorAvailable()) {
331 var b = (byte) (material.getAmbientRedColor() & 0x00ff);
332 dataStream.writeByte(b);
333 b = (byte) (material.getAmbientGreenColor() & 0x00ff);
334 dataStream.writeByte(b);
335 b = (byte) (material.getAmbientBlueColor() & 0x00ff);
336 dataStream.writeByte(b);
337 }
338
339 // diffuse color
340 dataStream.writeBoolean(material.isDiffuseColorAvailable());
341 if (material.isDiffuseColorAvailable()) {
342 var b = (byte) (material.getDiffuseRedColor() & 0x00ff);
343 dataStream.writeByte(b);
344 b = (byte) (material.getDiffuseGreenColor() & 0x00ff);
345 dataStream.writeByte(b);
346 b = (byte) (material.getDiffuseBlueColor() & 0x00ff);
347 dataStream.writeByte(b);
348 }
349
350 // specular color
351 dataStream.writeBoolean(material.isSpecularColorAvailable());
352 if (material.isSpecularColorAvailable()) {
353 var b = (byte) (material.getSpecularRedColor() & 0x00ff);
354 dataStream.writeByte(b);
355 b = (byte) (material.getSpecularGreenColor() & 0x00ff);
356 dataStream.writeByte(b);
357 b = (byte) (material.getSpecularBlueColor() & 0x00ff);
358 dataStream.writeByte(b);
359 }
360
361 // specular coefficient (float)
362 dataStream.writeBoolean(material.isSpecularCoefficientAvailable());
363 if (material.isSpecularCoefficientAvailable()) {
364 dataStream.writeFloat(material.getSpecularCoefficient());
365 }
366
367 // ambient texture map
368 dataStream.writeBoolean(material.isAmbientTextureMapAvailable());
369 if (material.isAmbientTextureMapAvailable()) {
370 final var tex = material.getAmbientTextureMap();
371 // texture id
372 dataStream.writeInt(tex.getId());
373 // texture width
374 dataStream.writeInt(tex.getWidth());
375 // texture height
376 dataStream.writeInt(tex.getHeight());
377 }
378
379 // diffuse texture map
380 dataStream.writeBoolean(material.isDiffuseTextureMapAvailable());
381 if (material.isDiffuseTextureMapAvailable()) {
382 final var tex = material.getDiffuseTextureMap();
383 // texture id
384 dataStream.writeInt(tex.getId());
385 // texture width
386 dataStream.writeInt(tex.getWidth());
387 // texture height
388 dataStream.writeInt(tex.getHeight());
389 }
390
391 // specular texture map
392 dataStream.writeBoolean(material.isSpecularTextureMapAvailable());
393 if (material.isSpecularTextureMapAvailable()) {
394 final var tex = material.getSpecularTextureMap();
395 // texture id
396 dataStream.writeInt(tex.getId());
397 // texture width
398 dataStream.writeInt(tex.getWidth());
399 // texture height
400 dataStream.writeInt(tex.getHeight());
401 }
402
403 // alpha texture map
404 dataStream.writeBoolean(material.isAlphaTextureMapAvailable());
405 if (material.isAlphaTextureMapAvailable()) {
406 final var tex = material.getAlphaTextureMap();
407 // texture id
408 dataStream.writeInt(tex.getId());
409 // texture width
410 dataStream.writeInt(tex.getWidth());
411 // texture height
412 dataStream.writeInt(tex.getHeight());
413 }
414
415 // bump texture map
416 dataStream.writeBoolean(material.isBumpTextureMapAvailable());
417 if (material.isBumpTextureMapAvailable()) {
418 final var tex = material.getBumpTextureMap();
419 // texture id
420 dataStream.writeInt(tex.getId());
421 // texture width
422 dataStream.writeInt(tex.getWidth());
423 // texture height
424 dataStream.writeInt(tex.getHeight());
425 }
426
427 dataStream.writeBoolean(material.isTransparencyAvailable());
428 if (material.isTransparencyAvailable()) {
429 final var b = (byte) (material.getTransparency() & 0x00ff);
430 dataStream.writeByte(b);
431 }
432
433 dataStream.writeBoolean(material.isIlluminationAvailable());
434 if (material.isIlluminationAvailable()) {
435 dataStream.writeInt(material.getIllumination().value());
436 }
437 }
438
439 // write coords size
440 dataStream.writeInt(coordsSizeInBytes);
441 // write coords
442 if (coordsAvailable) {
443 for (final var coord : coords) {
444 dataStream.writeFloat(coord);
445 }
446 }
447
448 // write colors size
449 dataStream.writeInt(colorsSizeInBytes);
450 // write colors
451 if (colorsAvailable) {
452 var i = 0;
453 while (i < colors.length) {
454 final var b = (byte) (colors[i] & 0x00ff);
455 dataStream.writeByte(b);
456 i++;
457 }
458 dataStream.writeInt(colorComponents);
459 }
460
461 // write indices size
462 dataStream.writeInt(indicesSizeInBytes);
463 // write indices
464 if (indicesAvailable) {
465 for (final var index : indices) {
466 final var s = (short) (index & 0x0000ffff);
467 dataStream.writeShort(s);
468 }
469 }
470
471 // write texture coords
472 dataStream.writeInt(textureCoordsSizeInBytes);
473 // write texture coords
474 if (textureCoordsAvailable) {
475 for (final var textureCoord : textureCoords) {
476 dataStream.writeFloat(textureCoord);
477 }
478 }
479
480 // write normals
481 dataStream.writeInt(normalsSizeInBytes);
482 // write normals
483 if (normalsAvailable) {
484 for (final var normal : normals) {
485 dataStream.writeFloat(normal);
486 }
487 }
488
489 // write min/max x, y, z
490 dataStream.writeFloat(chunk.getMinX());
491 dataStream.writeFloat(chunk.getMinY());
492 dataStream.writeFloat(chunk.getMinZ());
493
494 dataStream.writeFloat(chunk.getMaxX());
495 dataStream.writeFloat(chunk.getMaxY());
496 dataStream.writeFloat(chunk.getMaxZ());
497
498 dataStream.flush();
499 }
500
501 if (listener != null) {
502 listener.onWriteEnd(this);
503 }
504 locked = false;
505
506 } catch (final LoaderException | IOException e) {
507 throw e;
508 } catch (final Exception e) {
509 throw new LoaderException(e);
510 }
511 }
512
513 /**
514 * Processes texture file. By reading provided texture file that has been
515 * created in a temporal location and embedding it into resulting output
516 * stream.
517 *
518 * @param texture reference to texture that uses texture image.
519 * @param textureFile file containing texture image. File will usually be
520 * created in a temporal location.
521 * @throws IOException if an I/O error occurs.
522 */
523 @Override
524 protected void processTextureFile(final Texture texture, final File textureFile) throws IOException {
525 if (!textureFile.exists()) {
526 return;
527 }
528
529 // write boolean to true indicating that a texture follows
530 dataStream.writeBoolean(true);
531
532 // write int containing texture id
533 final var textureId = texture.getId();
534 dataStream.writeInt(textureId);
535
536 // write int containing image width
537 final var width = texture.getWidth();
538 dataStream.writeInt(width);
539
540 // write int containing image height
541 final var height = texture.getHeight();
542 dataStream.writeInt(height);
543
544 // write length of texture file in bytes
545 final var length = textureFile.length();
546 dataStream.writeLong(length);
547
548 // write file data
549 try (final var textureStream = Files.newInputStream(textureFile.toPath())) {
550 final var buffer = new byte[BUFFER_SIZE];
551 int n;
552 while ((n = textureStream.read(buffer)) > 0) {
553 dataStream.write(buffer, 0, n);
554 }
555 dataStream.flush();
556 }
557 }
558 }