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 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 }