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 org.apache.commons.codec.binary.Base64;
19
20 import java.io.BufferedWriter;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.io.OutputStreamWriter;
25 import java.nio.charset.Charset;
26 import java.nio.file.Files;
27
28 public class MeshWriterJson extends MeshWriter {
29
30 /**
31 * Indicates if textures must be embedded into resulting file.
32 */
33 public static final boolean DEFAULT_EMBED_TEXTURES = true;
34
35 /**
36 * Indicates if by default a URL indicating where the texture can be located
37 * should be included into resulting file.
38 */
39 public static final boolean DEFAULT_USE_REMOTE_TEXTURE_URL = false;
40
41 /**
42 * Indicates if by default an identifier for the texture should be included
43 * into resulting file so that texture image can be fetched by some other
44 * mean.
45 */
46 public static final boolean DEFAULT_USE_REMOTE_TEXTURE_ID = false;
47
48 /**
49 * Indicates charset to use in resulting JSON file. By default, this will be
50 * UTF-8.
51 */
52 private Charset charset;
53
54 /**
55 * Writer to write resulting JSON into output stream.
56 */
57 private BufferedWriter writer;
58
59 /**
60 * Counter for the number of textures that have been read.
61 */
62 private int textureCounter;
63
64 /**
65 * Indicates if textures will be embedded into resulting JSON stream of
66 * data. When embedding textures their stream of bytes is written using
67 * BASE64 to the output stream.
68 */
69 private boolean embedTexturesEnabled;
70
71 /**
72 * Indicates if a URL indicating where the texture can be located should
73 * be included into resulting file.
74 */
75 private boolean remoteTextureUrlEnabled;
76
77 /**
78 * Indicates if an identifier for the texture should be included into
79 * resulting file so that texture image can be fetched by some other mean.
80 */
81 private boolean remoteTextureIdEnabled;
82
83 /**
84 * Constructor.
85 *
86 * @param loader loader to load a 3D file.
87 * @param stream stream where trans-coded data will be written to.
88 */
89 public MeshWriterJson(final Loader loader, final OutputStream stream) {
90 super(loader, stream);
91 charset = null;
92 embedTexturesEnabled = DEFAULT_EMBED_TEXTURES;
93 remoteTextureUrlEnabled = DEFAULT_USE_REMOTE_TEXTURE_URL;
94 remoteTextureIdEnabled = DEFAULT_USE_REMOTE_TEXTURE_ID;
95 }
96
97 /**
98 * Constructor.
99 *
100 * @param loader loader to load a 3D file.
101 * @param stream stream where trans-coded data will be written to.
102 * @param listener listener to be notified of progress changes or when
103 * transcoding process starts or finishes.
104 */
105 public MeshWriterJson(final Loader loader, final OutputStream stream, final MeshWriterListener listener) {
106 super(loader, stream, listener);
107 charset = null;
108 embedTexturesEnabled = DEFAULT_EMBED_TEXTURES;
109 remoteTextureUrlEnabled = DEFAULT_USE_REMOTE_TEXTURE_URL;
110 remoteTextureIdEnabled = DEFAULT_USE_REMOTE_TEXTURE_ID;
111 }
112
113 /**
114 * Returns charset to use in resulting JSON file. By default, this will be
115 * UTF-8.
116 *
117 * @return charset to use in resulting JSON file.
118 */
119 public Charset getCharset() {
120 return charset;
121 }
122
123 /**
124 * Sets charset to use in resulting JSON file. By default, this will be UTF-8.
125 *
126 * @param charset charset to use in resulting JSON file.
127 * @throws LockedException if this mesh writer is locked processing a file.
128 */
129 public void setCharset(final Charset charset) throws LockedException {
130 if (isLocked()) {
131 throw new LockedException();
132 }
133 this.charset = charset;
134 }
135
136 /**
137 * Indicates if default charset will be used or not.
138 *
139 * @return true if default charset (UTF-8) will be used, false otherwise.
140 */
141 public boolean isDefaultCharsetBeingUsed() {
142 return (charset == null);
143 }
144
145 /**
146 * Indicates if textures are embedded into resulting JSON or not.
147 * When embedding textures their stream of bytes is written using BASE64 to
148 * the output stream.
149 *
150 * @return true if textures are embedded into resulting JSON, false
151 * otherwise.
152 */
153 public boolean isEmbedTexturesEnabled() {
154 return embedTexturesEnabled;
155 }
156
157 /**
158 * Specified whether textures are embedded into resulting JSON or not.
159 * When embedding textures their stream of bytes is written using BASE64 to
160 * the output stream.
161 *
162 * @param embedTexturesEnabled true if textures will be embedded into
163 * resulting JSON, false.
164 * @throws LockedException if this mesh writer is locked processing a file.
165 */
166 public void setEmbedTexturedEnabled(final boolean embedTexturesEnabled) throws LockedException {
167 if (isLocked()) {
168 throw new LockedException();
169 }
170 this.embedTexturesEnabled = embedTexturesEnabled;
171 }
172
173 /**
174 * Indicates if a URL indicating where the texture can be located should
175 * be included into resulting file.
176 *
177 * @return true if a URL indicating where the texture can be located will
178 * be included into resulting file, false otherwise.
179 */
180 public boolean isRemoteTextureUrlEnabled() {
181 return remoteTextureUrlEnabled;
182 }
183
184 /**
185 * Specifies whether a URL indicating where the texture can be located
186 * should be included into resulting file.
187 *
188 * @param remoteTextureUrlEnabled true if a URL indicating where the texture
189 * can be located will be included into resulting file, false otherwise.
190 * @throws LockedException if this mesh writer is locked processing a file.
191 */
192 public void setRemoteTextureUrlEnabled(final boolean remoteTextureUrlEnabled) throws LockedException {
193 if (isLocked()) {
194 throw new LockedException();
195 }
196 this.remoteTextureUrlEnabled = remoteTextureUrlEnabled;
197 }
198
199 /**
200 * Indicates if an identifier for the texture should be included into
201 * resulting file so that texture image can be fetched by some other mean.
202 *
203 * @return true if an identifier for the texture should be included into.
204 * resulting file so that texture image can be fetched by some other mean.
205 */
206 public boolean isRemoteTextureIdEnabled() {
207 return remoteTextureIdEnabled;
208 }
209
210 /**
211 * Specifies whether an identifier for the texture should be included into
212 * resulting file so that texture image can be fetched by some other mean.
213 *
214 * @param remoteTextureIdEnabled true if identifier for the texture should
215 * be included into resulting file.
216 * @throws LockedException if this mesh writer is locked processing file.
217 */
218 public void setRemoteTextureIdEnabled(final boolean remoteTextureIdEnabled) throws LockedException {
219 if (isLocked()) {
220 throw new LockedException();
221 }
222 this.remoteTextureIdEnabled = remoteTextureIdEnabled;
223 }
224
225 /**
226 * Processes input file provided to loader and writes it trans-coded into
227 * output stream.
228 *
229 * @throws LoaderException if 3D file loading fails.
230 * @throws IOException if an I/O error occurs.
231 * @throws NotReadyException if mesh writer is not ready because either a
232 * loader has not been provided or an output stream has not been provided.
233 * @throws LockedException if this mesh writer is locked processing a file.
234 */
235 @SuppressWarnings("DuplicatedCode")
236 @Override
237 public void write() throws LoaderException, IOException, NotReadyException, LockedException {
238 if (!isReady()) {
239 throw new NotReadyException();
240 }
241 if (isLocked()) {
242 throw new LockedException();
243 }
244
245 try {
246 if (charset == null) {
247 // use default charset
248 writer = new BufferedWriter(new OutputStreamWriter(stream));
249 } else {
250 // use provided charset
251 writer = new BufferedWriter(new OutputStreamWriter(stream, charset));
252 }
253
254 locked = true;
255 textureCounter = 0;
256 if (listener != null) {
257 listener.onWriteStart(this);
258 }
259
260 loader.setListener(this.internalListeners);
261
262 writer.write("{\"textures\":[");
263 final var iter = loader.load();
264
265 var minX = Float.MAX_VALUE;
266 var minY = Float.MAX_VALUE;
267 var minZ = Float.MAX_VALUE;
268 var maxX = -Float.MAX_VALUE;
269 var maxY = -Float.MAX_VALUE;
270 var maxZ = -Float.MAX_VALUE;
271
272 // write array opening
273 writer.write("],\"chunks\":[");
274 // indicate that no more textures can follow
275 ignoreTextureValidation = true;
276 while (iter.hasNext()) {
277 final var chunk = iter.next();
278 final var coords = chunk.getVerticesCoordinatesData();
279 final var colors = chunk.getColorData();
280 final var indices = chunk.getIndicesData();
281 final var textureCoords = chunk.getTextureCoordinatesData();
282 final var normals = chunk.getNormalsData();
283
284 final var material = chunk.getMaterial();
285
286 final var coordsAvailable = (coords != null);
287 final var colorsAvailable = (colors != null);
288 final var indicesAvailable = (indices != null);
289 final var textureCoordsAvailable = (textureCoords != null);
290 final var normalsAvailable = (normals != null);
291 var hasPreviousContent = false;
292
293 if (chunk.getMinX() < minX) {
294 minX = chunk.getMinX();
295 }
296 if (chunk.getMinY() < minY) {
297 minY = chunk.getMinY();
298 }
299 if (chunk.getMinZ() < minZ) {
300 minZ = chunk.getMinZ();
301 }
302
303 if (chunk.getMaxX() > maxX) {
304 maxX = chunk.getMaxX();
305 }
306 if (chunk.getMaxY() > maxY) {
307 maxY = chunk.getMaxY();
308 }
309 if (chunk.getMaxZ() > maxZ) {
310 maxZ = chunk.getMaxZ();
311 }
312
313 //write chunk opening
314 writer.write("{");
315
316 // CHUNK CONTENTS
317 if (material != null) {
318 // write material
319 writeMaterial(material);
320 hasPreviousContent = true;
321 }
322 if (indicesAvailable) {
323 // write separator for next piece of data
324 if (hasPreviousContent) {
325 writer.write(",");
326 }
327
328 // write indices opening
329 writer.write("\"indices\":[");
330 for (var i = 0; i < indices.length; i++) {
331 writer.write(Integer.toString(indices[i]));
332 // write separator if more elements in array
333 if (i < (indices.length - 1)) {
334 writer.write(",");
335 }
336 }
337 // write indices closing
338 writer.write("]");
339 hasPreviousContent = true;
340 }
341 if (normalsAvailable) {
342 // write separator for next piece of data
343 if (hasPreviousContent) {
344 writer.write(",");
345 }
346
347 // write normals opening
348 writer.write("\"vertexNormals\":[");
349 for (var i = 0; i < normals.length; i++) {
350 if (Float.isInfinite(normals[i]) || Float.isNaN(normals[i])) {
351 writer.write(Float.toString(Float.MAX_VALUE));
352 } else {
353 writer.write(Float.toString(normals[i]));
354 }
355 // write separator if more elements in array
356 if (i < (normals.length - 1)) {
357 writer.write(",");
358 }
359 }
360 // write normals closing
361 writer.write("]");
362 hasPreviousContent = true;
363 }
364 if (coordsAvailable) {
365 // write separator for next piece of data
366 if (hasPreviousContent) {
367 writer.write(",");
368 }
369
370 // write coords opening
371 writer.write("\"vertexPositions\":[");
372 for (var i = 0; i < coords.length; i++) {
373 if (Float.isInfinite(coords[i]) || Float.isNaN(coords[i])) {
374 writer.write(Float.toString(Float.MAX_VALUE));
375 } else {
376 writer.write(Float.toString(coords[i]));
377 }
378 // write separator if more elements in array
379 if (i < (coords.length - 1)) {
380 writer.write(",");
381 }
382 }
383 // write coords closing
384 writer.write("]");
385 hasPreviousContent = true;
386 }
387 if (textureCoordsAvailable) {
388 // write separator for next piece of data
389 if (hasPreviousContent) {
390 writer.write(",");
391 }
392
393 // write texture coords opening
394 writer.write("\"vertexTextureCoords\":[");
395 for (var i = 0; i < textureCoords.length; i++) {
396 if (Float.isInfinite(textureCoords[i]) || Float.isNaN(textureCoords[i])) {
397 writer.write(Float.toString(Float.MAX_VALUE));
398 } else {
399 writer.write(Float.toString(textureCoords[i]));
400 }
401 // write separator if more elements in array
402 if (i < (textureCoords.length - 1)) {
403 writer.write(",");
404 }
405 }
406 // write texture coords closing
407 writer.write("]");
408 hasPreviousContent = true;
409 }
410 if (coordsAvailable) {
411 // write separator for next piece of data
412 writer.write(",");
413
414 // write min corner
415 writer.write("\"minCorner\":[" + chunk.getMinX() + "," + chunk.getMinY() + ","
416 + chunk.getMinZ() + "],");
417
418 // write max corner
419 writer.write("\"maxCorner\":[" + chunk.getMaxX() + "," + chunk.getMaxY() + ","
420 + chunk.getMaxZ() + "]");
421 }
422 if (colorsAvailable) {
423 // write separator for next piece of data
424 if (hasPreviousContent) {
425 writer.write(",");
426 }
427
428 // write colors opening
429 writer.write("\"vertexColors\":[");
430 for (var i = 0; i < colors.length; i++) {
431 writer.write(Short.toString(colors[i]));
432
433 if (i < (colors.length - 1)) {
434 writer.write(",");
435 }
436 }
437
438 // write colors closing and color components
439 writer.write("],\"colorComponents\": " + chunk.getColorComponents());
440 }
441
442 // write chunk closing
443 writer.write("}");
444
445 // write chunk separator if more chunks are available
446 if (iter.hasNext()) {
447 writer.write(",");
448 }
449
450 writer.flush();
451 }
452 // write array closing
453 writer.write("],");
454 // write bounding box for all chunks
455 // write min corner
456 writer.write("\"minCorner\":[" + minX + "," + minY + "," + minZ + "],");
457
458 // write max corner
459 writer.write("\"maxCorner\":[" + maxX + "," + maxY + "," + maxZ + "]");
460
461 // write object closing
462 writer.write("}");
463 writer.flush();
464
465 if (listener != null) {
466 listener.onWriteEnd(this);
467 }
468 locked = false;
469
470 } catch (final LoaderException | IOException e) {
471 throw e;
472 } catch (final Exception e) {
473 throw new LoaderException(e);
474 }
475
476 }
477
478 /**
479 * Processes texture file. By reading provided texture file that has been
480 * created in a temporal location and embedding it into resulting output
481 * stream.
482 *
483 * @param texture reference to texture that uses texture image
484 * @param textureFile file containing texture image. File will usually be
485 * created in a temporal location.
486 * @throws IOException if an I/O error occurs
487 */
488 @Override
489 protected void processTextureFile(final Texture texture, final File textureFile) throws IOException {
490 if (textureCounter > 0) {
491 writer.write(",");
492 }
493 writer.write("{\"id\":" + texture.getId());
494 writer.write(",\"width\":" + texture.getWidth());
495 writer.write(",\"height\":" + texture.getHeight());
496 if (listener instanceof MeshWriterJsonListener listener2) {
497 if (remoteTextureUrlEnabled) {
498 final String remoteUrl = listener2.onRemoteTextureUrlRequested(this, texture, textureFile);
499 if (remoteUrl != null) {
500 // add url to json
501 writer.write(",\"remoteUrl\":\"" + remoteUrl + "\"");
502 }
503 }
504 if (remoteTextureIdEnabled) {
505 final var remoteId = listener2.onRemoteTextureIdRequested(this, texture, textureFile);
506 if (remoteId != null) {
507 // add id to json
508 writer.write(",\"remoteId\": \"" + remoteId + "\"");
509 }
510 }
511
512 }
513 if (embedTexturesEnabled) {
514 writer.write(",\"data\":\"");
515 // write texture file data as base64
516
517 // flush to sync writer and stream
518 writer.flush();
519
520
521 try (final var textureStream = Files.newInputStream(textureFile.toPath())) {
522 final var imageData = new byte[(int) textureFile.length()];
523
524 if (textureStream.read(imageData) > 0) {
525 // convert image byte array into Base64 string
526 //TODO: could we use a Base64OutputStream to reduce memory usage,
527 // but doing the same replacement for safe json generation?
528 String base64 = Base64.encodeBase64String(imageData);
529 base64 = base64.replace("/", "\\/");
530 writer.write(base64);
531 writer.flush();
532 }
533 }
534 writer.write("\"");
535 }
536 writer.write("}");
537 textureCounter++;
538 }
539
540 /**
541 * Writes material into output JSON file.
542 *
543 * @param material material to be written.
544 * @throws IOException if an I/O error occurs.
545 */
546 private void writeMaterial(final Material material) throws IOException {
547 writer.write("\"material\":{");
548 writer.write("\"id\":" + material.getId());
549 if (material.isAmbientColorAvailable()) {
550 writer.write(",\"ambientColor\":[" + material.getAmbientRedColor() + ","
551 + material.getAmbientGreenColor() + "," + material.getAmbientBlueColor() + "]");
552 }
553 if (material.isDiffuseColorAvailable()) {
554 writer.write(",\"diffuseColor\":[" + material.getDiffuseRedColor() + ","
555 + material.getDiffuseGreenColor() + "," + material.getDiffuseBlueColor() + "]");
556 }
557 if (material.isSpecularColorAvailable()) {
558 writer.write(",\"specularColor\":[" + material.getSpecularRedColor() + ","
559 + material.getSpecularGreenColor() + "," + material.getSpecularBlueColor() + "]");
560 }
561 if (material.isSpecularCoefficientAvailable()) {
562 writer.write(",\"specularCoefficient\":" + material.getSpecularCoefficient());
563 }
564 if (material.isAmbientTextureMapAvailable()) {
565 final var tex = material.getAmbientTextureMap();
566 writer.write(",\"ambientTextureId\":" + tex.getId());
567 }
568 if (material.isDiffuseTextureMapAvailable()) {
569 final var tex = material.getDiffuseTextureMap();
570 writer.write(",\"diffuseTextureId\":" + tex.getId());
571 }
572 if (material.isSpecularTextureMapAvailable()) {
573 final var tex = material.getSpecularTextureMap();
574 writer.write(",\"specularTextureId\":" + tex.getId());
575 }
576 if (material.isAlphaTextureMapAvailable()) {
577 final var tex = material.getAlphaTextureMap();
578 writer.write(",\"alphaTextureId\":" + tex.getId());
579 }
580 if (material.isBumpTextureMapAvailable()) {
581 final var tex = material.getBumpTextureMap();
582 writer.write(",\"bumpTextureId\":" + tex.getId());
583 }
584 if (material.isTransparencyAvailable()) {
585 writer.write(",\"transparency\":" + material.getTransparency());
586 }
587 if (material.isIlluminationAvailable()) {
588 writer.write(",\"illumination\":\"" + material.getIllumination().name() + "\"");
589 }
590 writer.write("}");
591 }
592 }